feat(web): 开发账户修改、密码修改功能
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react';
|
||||
import { App as AntdApp, Button, Form, Input } from 'antd';
|
||||
|
||||
interface PasswordFormValues {
|
||||
old_password: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码页:接口契约尚未提供(缺口已提报后端 AI,需「原密码 + 新密码」的修改密码接口)。
|
||||
* 提交时不做任何本地伪成功,明确提示暂不可用;契约交付后接入真实接口。
|
||||
*/
|
||||
const PasswordSettingsPage = () => {
|
||||
const { message } = AntdApp.useApp();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleFinish = async (_values: PasswordFormValues) => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
message.warning('修改密码接口尚未提供(契约缺口已提报后端 AI),暂无法保存');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<div className="settings-card">
|
||||
<div className="settings-card-title">修改密码</div>
|
||||
<div className="settings-card-body">
|
||||
<Form<PasswordFormValues>
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
onFinish={handleFinish}
|
||||
className="profile-form"
|
||||
>
|
||||
<Form.Item
|
||||
name="old_password"
|
||||
label="原密码"
|
||||
rules={[{ required: true, message: '请输入原密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="请输入原密码"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="新密码"
|
||||
extra="至少 12 个字符;长度与复杂度以服务端安全校验为准"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 12, message: '新密码至少需要 12 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="请输入新密码"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password_confirmation"
|
||||
label="确认新密码"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请再次输入新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的新密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="请再次输入新密码"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="settings-actions">
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
保存修改
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordSettingsPage;
|
||||
@@ -0,0 +1,141 @@
|
||||
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 { authApi } from '@/services/auth';
|
||||
import { useSession } from '@/services/session';
|
||||
import { 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)。
|
||||
*/
|
||||
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('资料已更新(当前为本地临时生效,持久化待接口交付)');
|
||||
} catch (error) {
|
||||
message.error(apiErrorMessage(error));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.setFieldValue('display_name', session.user.display_name ?? '');
|
||||
setPreviewUrl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<div className="settings-card">
|
||||
<div className="settings-card-title">修改个人信息</div>
|
||||
<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 像素
|
||||
</div>
|
||||
|
||||
<Form<ProfileFormValues>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
initialValues={{ display_name: session.user.display_name ?? '' }}
|
||||
onFinish={handleSave}
|
||||
className="profile-form"
|
||||
>
|
||||
<Form.Item
|
||||
name="display_name"
|
||||
label="昵称"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入昵称' }]}
|
||||
>
|
||||
<Input
|
||||
maxLength={80}
|
||||
showCount
|
||||
placeholder="请输入昵称"
|
||||
autoComplete="nickname"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="settings-actions">
|
||||
<Button type="primary" htmlType="submit" loading={saving}>
|
||||
保存修改
|
||||
</Button>
|
||||
<Button onClick={handleReset}>重 置</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSettingsPage;
|
||||
Reference in New Issue
Block a user