From 97e8d7144fad6d732a13177ab28f8a30ad3f48cd Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 16 Sep 2026 12:46:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E7=AC=AC9=E8=BD=AE=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=E6=89=B9=E6=AC=A14=E2=80=94=E2=80=94=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=9F=9F=E4=B8=80=E6=89=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 头像上传从未带purpose=user_avatar:之前是照抄一个已经修复的旧后端 bug(问题28)绕开的写法,现在确认接口早就支持这个purpose了,补上—— uploadFile()新增可选purpose参数,不影响其它已有调用点(聊天附件/群 文件继续用topic的chat_file路径) - 桌面卡片行分隔线6处错用了移动端专属的border-list-item token,改成 桌面正确的border-row - 退出登录按钮外边距/设置目录导航图标尺寸(18px→20px)与设计稿对齐 - 地区字段补上maxLength兜底 - 修改密码没有用
包裹,输入框按回车没反应——补上表单+onSubmit - 记录一条此前没进清单的后端缺口:"加我为好友时需要验证"这个隐私开关 设计稿画了,但响应体里根本没有对应字段 Co-Authored-By: Claude Sonnet 5 --- .../features/settings/PrivacySettingsPage.tsx | 11 +++++-- .../features/settings/ProfileSettingsPage.tsx | 32 +++++++++++++------ .../settings/SecuritySettingsPage.tsx | 14 +++++--- web/src/features/settings/SettingsLayout.tsx | 6 ++-- web/src/lib/api/upload.ts | 11 ++++++- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/web/src/features/settings/PrivacySettingsPage.tsx b/web/src/features/settings/PrivacySettingsPage.tsx index c869cf75..f26cded8 100644 --- a/web/src/features/settings/PrivacySettingsPage.tsx +++ b/web/src/features/settings/PrivacySettingsPage.tsx @@ -62,7 +62,12 @@ export function PrivacySettingsPage() { // re-enables every field a tenant policy had actually locked, until an unrelated refetch happens // to restore it. onSuccess: (data) => queryClient.setQueryData(['my-privacy'], (prev: typeof query.data) => (prev ? { ...prev, ...data } : data)), - onError: (err) => setError(err instanceof Error ? err.message : '设置失败'), + // Same reasoning as ProfileSettingsPage's own saveMutation: a version conflict shouldn't leave + // the cached `version` stale, or every retry just resends the same rejected value forever. + onError: (err) => { + setError(err instanceof Error ? err.message : '设置失败'); + queryClient.invalidateQueries({ queryKey: ['my-privacy'] }); + }, }); const patch = (partial: Partial[0], 'version'>>) => { if (!query.data) return; @@ -113,7 +118,7 @@ export function PrivacySettingsPage() {
-
+
资料可见范围
{VISIBILITY_OPTIONS.map((opt) => ( @@ -129,7 +134,7 @@ export function PrivacySettingsPage() { ))}
-
+
最近在线可见范围
{VISIBILITY_OPTIONS.map((opt) => ( diff --git a/web/src/features/settings/ProfileSettingsPage.tsx b/web/src/features/settings/ProfileSettingsPage.tsx index 3852fc7b..304cde5c 100644 --- a/web/src/features/settings/ProfileSettingsPage.tsx +++ b/web/src/features/settings/ProfileSettingsPage.tsx @@ -32,9 +32,14 @@ const InlineField = forwardRef (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 ? setShowQrCode(false)} /> : null}
-
+
昵称
-
+
性别
{GENDER_OPTIONS.map((opt) => ( @@ -199,7 +211,7 @@ export function ProfileSettingsPage() {
-
+
出生日期
-
+
地区 - setDraft({ ...draft, region: e.target.value })} placeholder="未填写" /> + setDraft({ ...draft, region: e.target.value })} placeholder="未填写" maxLength={50} />
diff --git a/web/src/features/settings/SecuritySettingsPage.tsx b/web/src/features/settings/SecuritySettingsPage.tsx index 51cb4608..473525e3 100644 --- a/web/src/features/settings/SecuritySettingsPage.tsx +++ b/web/src/features/settings/SecuritySettingsPage.tsx @@ -95,7 +95,14 @@ export function SecuritySettingsPage() {
{changingPassword ? ( -
+ { + e.preventDefault(); + if (currentPassword.length === 0 || newPassword.length < minPasswordLength || passwordsMismatch || confirmPassword.length === 0 || passwordMutation.isPending) return; + passwordMutation.mutate(); + }} + className="flex flex-col gap-3 px-7 py-4" + > 修改密码
-
+ ) : ( diff --git a/web/src/lib/api/upload.ts b/web/src/lib/api/upload.ts index 4790fdad..6ec60731 100644 --- a/web/src/lib/api/upload.ts +++ b/web/src/lib/api/upload.ts @@ -36,13 +36,22 @@ const UPLOAD_CTRL_TEXT_CN: Record = { 'not found': '内容不存在或已被删除', }; -export async function uploadFile(file: File, topic?: string): Promise { +/** + * `purpose` only matters when `topic` is omitted (a chat/group-file upload always sends + * `chat_file` regardless) -- currently just the avatar upload path, which used to send no + * `purpose` at all. That omission dated back to this same endpoint's `purpose=user_avatar` 400ing + * (docs/IM-后端问题清单.md 问题28) -- confirmed since fixed (a real PNG upload with that purpose + * now returns 200), so there's no reason left to omit it. + */ +export async function uploadFile(file: File, topic?: string, purpose?: string): Promise { const form = new FormData(); form.append('file', file); form.set('id', getTinode().getNextUniqueId()); if (topic) { form.append('purpose', 'chat_file'); form.append('topic', topic); + } else if (purpose) { + form.append('purpose', purpose); } const res = await fetch(`${fileBase()}/u/`, { method: 'POST',