feat(web-auth): 前端修改资料、修改密码优化

This commit is contained in:
R524809
2026-09-03 17:12:36 +08:00
parent 2d435143ab
commit cc61967e3b
6 changed files with 104 additions and 112 deletions
@@ -1,5 +1,10 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { App as AntdApp, Button, Form, Input } from 'antd';
import axios from 'axios';
import { authApi } from '@/services/auth';
import { authToken } from '@/services/authToken';
import { apiErrorDetails, apiErrorMessage } from '@/services/api';
interface PasswordFormValues {
old_password: string;
@@ -8,18 +13,44 @@ interface PasswordFormValues {
}
/**
* 修改密码页:接口契约尚未提供(缺口已提报后端 AI,需「原密码 + 新密码」的修改密码接口)。
* 提交时不做任何本地伪成功,明确提示暂不可用;契约交付后接入真实接口。
* 修改密码页,契约见 docs/api/auth.md(POST /api/auth/change-password)。
* 成功后所有旧 session 失效:立即用响应中的新 access token 替换内存值
* (本设备经新的 refresh cookie 保持登录),清理表单状态后回到首页。
*/
const PasswordSettingsPage = () => {
const { message } = AntdApp.useApp();
const navigate = useNavigate();
const [form] = Form.useForm<PasswordFormValues>();
const [submitting, setSubmitting] = useState(false);
const handleFinish = async (_values: PasswordFormValues) => {
const handleFinish = async (values: PasswordFormValues) => {
setSubmitting(true);
try {
await new Promise((resolve) => setTimeout(resolve, 300));
message.warning('修改密码接口尚未提供(契约缺口已提报后端 AI),暂无法保存');
const result = await authApi.changePassword({
old_password: values.old_password,
password: values.password,
password_confirmation: values.password_confirmation,
});
authToken.set(result.access_token);
form.resetFields();
message.success('密码已修改');
navigate('/online-products', { replace: true });
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
message.error('原密码不正确');
} else {
const details = apiErrorDetails(error);
if (details.length > 0) {
form.setFields(
details.map((item) => ({
name: item.field as keyof PasswordFormValues,
errors: [item.message],
}))
);
} else {
message.error(apiErrorMessage(error));
}
}
} finally {
setSubmitting(false);
}
@@ -31,6 +62,7 @@ const PasswordSettingsPage = () => {
<div className="settings-card-title">修改密码</div>
<div className="settings-card-body">
<Form<PasswordFormValues>
form={form}
layout="vertical"
requiredMark={false}
onFinish={handleFinish}
@@ -1,63 +1,58 @@
import { useMemo, useState } from 'react';
import { App as AntdApp, Avatar, Button, Form, Input, Upload } from 'antd';
import type { UploadFile } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { App as AntdApp, Avatar, Button, Form, Input } from 'antd';
import axios from 'axios';
import { authApi } from '@/services/auth';
import { useSession } from '@/services/session';
import { apiErrorMessage } from '@/services/api';
import { apiErrorDetails, apiErrorMessage } from '@/services/api';
interface ProfileFormValues {
display_name: string;
}
const ACCEPTED_TYPES = ['image/jpeg', 'image/png'];
const MAX_AVATAR_BYTES = 2 * 1024 * 1024;
/**
* 修改资料页(docs/api/auth.md 尚无对应接口):
* 昵称保存走【临时 mock】仅本地生效;头像选择后仅本地预览,
* 持久化依赖用户头像字段与素材上传接口(契约缺口已提报后端 AI)。
* 修改资料页,契约见 docs/api/auth.md(PATCH /api/auth/me,If-Match + Idempotency-Key)。
* 头像持久化依赖素材预签名直传接口(尚未交付):当前仅展示默认头像的禁用态,不提供上传。
*/
const ProfileSettingsPage = () => {
const { message } = AntdApp.useApp();
const { session, applyProfileUpdate } = useSession();
const [form] = Form.useForm<ProfileFormValues>();
const [saving, setSaving] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const displayName = session?.user.display_name ?? session?.user.email ?? '';
const initial = useMemo(() => displayName.charAt(0).toUpperCase(), [displayName]);
if (!session) return null;
const handleBeforeUpload = (file: UploadFile): boolean => {
const blobType = file.type ?? '';
if (!ACCEPTED_TYPES.includes(blobType)) {
message.error('仅支持 JPG、PNG 格式图片');
return false;
}
if ((file.size ?? 0) > MAX_AVATAR_BYTES) {
message.error('图片大小不能超过 2MB');
return false;
}
if (file.originFileObj) {
setPreviewUrl(URL.createObjectURL(file.originFileObj));
}
// 阻止 antd 自动上传:当前无头像上传接口,仅本地预览
return false;
};
const handleSave = async (values: ProfileFormValues) => {
setSaving(true);
try {
// 临时 mock:仅本地生效,接口交付后替换为真实调用
const result = await authApi.updateProfile({
display_name: values.display_name.trim(),
});
applyProfileUpdate({ ...session.user, ...result.user });
message.success('资料已更新(当前为本地临时生效,持久化待接口交付)');
const user = await authApi.updateProfile(
{ display_name: values.display_name.trim() },
// 幂等键每次提交重新生成;以提交时的 revision 做乐观锁
{ revision: session.user.revision, idempotencyKey: crypto.randomUUID() }
);
applyProfileUpdate(user);
message.success('资料已更新');
} catch (error) {
message.error(apiErrorMessage(error));
// 乐观锁版本过期:服务端返回 409 与当前 revision,提示刷新后重试
if (axios.isAxiosError(error) && error.response?.status === 409) {
message.error('资料已被其他操作更新,请刷新页面获取最新资料后重试');
} else {
const details = apiErrorDetails(error);
if (details.length > 0) {
form.setFields(
details
.filter((item) => item.field !== 'If-Match')
.map((item) => ({
name: item.field as keyof ProfileFormValues,
errors: [item.message],
}))
);
} else {
message.error(apiErrorMessage(error));
}
}
} finally {
setSaving(false);
}
@@ -65,7 +60,6 @@ const ProfileSettingsPage = () => {
const handleReset = () => {
form.setFieldValue('display_name', session.user.display_name ?? '');
setPreviewUrl(null);
};
return (
@@ -75,34 +69,11 @@ const ProfileSettingsPage = () => {
<div className="settings-card-body">
<div className="settings-field-label">头像</div>
<div className="avatar-upload-area">
<Upload
showUploadList={false}
accept=".jpg,.jpeg,.png"
beforeUpload={handleBeforeUpload}
>
{previewUrl ? (
<Avatar size={160} src={previewUrl} className="avatar-preview" />
) : (
<div className="avatar-upload-placeholder">
<Avatar
size={160}
style={{ backgroundColor: 'var(--sider-bg)' }}
>
{initial}
</Avatar>
<div className="avatar-upload-overlay">
<PlusOutlined />
<div>上传头像</div>
</div>
</div>
)}
</Upload>
</div>
<div className="avatar-upload-hint">
支持 JPG、PNG 格式,文件大小不超过 2MB
<br />
建议尺寸:200x200 像素
<Avatar size={160} style={{ backgroundColor: 'var(--sider-bg)' }}>
{initial}
</Avatar>
</div>
<div className="avatar-upload-hint">头像上传即将支持,敬请期待</div>
<Form<ProfileFormValues>
form={form}