fix(web): 第9轮审查批次4——设置域一批问题

- 头像上传从未带purpose=user_avatar:之前是照抄一个已经修复的旧后端
  bug(问题28)绕开的写法,现在确认接口早就支持这个purpose了,补上——
  uploadFile()新增可选purpose参数,不影响其它已有调用点(聊天附件/群
  文件继续用topic的chat_file路径)
- 桌面卡片行分隔线6处错用了移动端专属的border-list-item token,改成
  桌面正确的border-row
- 退出登录按钮外边距/设置目录导航图标尺寸(18px→20px)与设计稿对齐
- 地区字段补上maxLength兜底
- 修改密码没有用<form>包裹,输入框按回车没反应——补上表单+onSubmit
- 记录一条此前没进清单的后端缺口:"加我为好友时需要验证"这个隐私开关
  设计稿画了,但响应体里根本没有对应字段

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-16 12:46:48 +08:00
co-authored by Claude Sonnet 5
parent 18d21a03bb
commit 97e8d7144f
5 changed files with 53 additions and 21 deletions
@@ -32,9 +32,14 @@ const InlineField = forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
/**
* README §8.2 / screens/07-profile-settings.dc.html#1a. Avatar upload reuses upload.ts's native
* `/v0/file/u/` channel, NOT `POST /v1/im/public/uploads?purpose=user_avatar` -- confirmed
* live-broken (docs/IM-后端问题清单.md 问题28: that endpoint 400s on `purpose=user_avatar`
* specifically), and the old app's own real avatar-upload code never goes through it either.
* `/v0/file/u/` channel (the one every other upload in this app already goes through), NOT
* `POST /v1/im/public/uploads` -- that's a different, pre-login-only channel for enterprise
* registration/tenant feedback (`lib/api/tenant.ts`'s `uploadPublicFile`, whose own `purpose` type
* is explicitly `'enterprise_license' | 'tenant_entry_feedback'`), never meant for an authenticated
* user's own avatar. Passes `purpose: 'user_avatar'` explicitly per docs/IM-前后端对接文档.md:2293
* -- this used to be omitted entirely (traced to `purpose=user_avatar` 400ing on THIS channel too,
* docs/IM-后端问题清单.md 问题28, since confirmed fixed: a real PNG upload with that purpose now
* returns 200).
*/
export function ProfileSettingsPage() {
const queryClient = useQueryClient();
@@ -79,12 +84,19 @@ export function ProfileSettingsPage() {
queryClient.setQueryData(['my-profile'], (prev: typeof query.data) => (prev ? { ...prev, ...data } : data));
queryClient.invalidateQueries({ queryKey: ['me'] });
},
onError: (err) => setError(err instanceof Error ? err.message : '保存失败'),
// IM-前后端对接文档.md 10.3 (版本冲突先拉最新资料再由用户确认): a stale `version` (e.g. edited
// on another tab/device) 400s here, but the cached `query.data.version` used to just sit there
// unchanged -- every retry would resend the exact same stale version and 400 again forever, the
// toast's own "请刷新后重试" copy notwithstanding, since nothing actually refetched.
onError: (err) => {
setError(err instanceof Error ? err.message : '保存失败');
queryClient.invalidateQueries({ queryKey: ['my-profile'] });
},
});
const avatarMutation = useMutation({
mutationFn: async (file: File) => {
const uploaded = await uploadFile(file);
const uploaded = await uploadFile(file, undefined, 'user_avatar');
if (!query.data) throw new Error('尚未加载');
return updateMyProfile({ avatar_file_id: uploaded.file_id, version: query.data.version });
},
@@ -172,7 +184,7 @@ export function ProfileSettingsPage() {
{showQrCode ? <QRCodeModal onClose={() => setShowQrCode(false)} /> : null}
<div className="rounded-xl bg-white text-sm">
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-list-item px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-row px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<span className="text-text-secondary">昵称</span>
<InlineField
value={draft.nickname}
@@ -182,7 +194,7 @@ export function ProfileSettingsPage() {
<Icon name="edit" className="text-[20px] text-icon-arrow" />
</div>
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-list-item px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-row px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<span className="text-text-secondary">性别</span>
<div className="flex overflow-hidden rounded-md border border-[#d5d9df] text-xs" style={{ width: 'fit-content' }}>
{GENDER_OPTIONS.map((opt) => (
@@ -199,7 +211,7 @@ export function ProfileSettingsPage() {
<span />
</div>
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-list-item px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-row px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<span className="text-text-secondary">出生日期</span>
<InlineField
ref={birthDateRef}
@@ -213,9 +225,9 @@ export function ProfileSettingsPage() {
</button>
</div>
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-list-item px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<div className="grid grid-cols-[80px_1fr_auto] items-center gap-x-2 border-b border-border-row px-7 py-4 md:grid-cols-[120px_1fr_auto]">
<span className="text-text-secondary">地区</span>
<InlineField value={draft.region} onChange={(e) => setDraft({ ...draft, region: e.target.value })} placeholder="未填写" />
<InlineField value={draft.region} onChange={(e) => setDraft({ ...draft, region: e.target.value })} placeholder="未填写" maxLength={50} />
<Icon name="chevron_right" className="text-[20px] text-icon-arrow" />
</div>