feat: Add Personal Center module and UI improvements
- Add ProfilePage with avatar upload, password change, and module status display - Update logo and favicon for login page and browser tab - Redirect Data Cleaning module default route to Tool C - Hide Settings button from top navigation for MVP - Add avatar display in top navigation bar with refresh sync - Add Prompt knowledge base integration development plan docs
This commit is contained in:
@@ -21,6 +21,8 @@ import { MODULES } from './framework/modules/moduleRegistry'
|
||||
import UserListPage from './modules/admin/pages/UserListPage'
|
||||
import UserFormPage from './modules/admin/pages/UserFormPage'
|
||||
import UserDetailPage from './modules/admin/pages/UserDetailPage'
|
||||
// 个人中心页面
|
||||
import ProfilePage from './pages/user/ProfilePage'
|
||||
|
||||
/**
|
||||
* 应用根组件
|
||||
@@ -86,6 +88,10 @@ function App() {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 个人中心路由 - 2026-01-28 新增 */}
|
||||
<Route path="/user/profile" element={<ProfilePage />} />
|
||||
<Route path="/user/settings" element={<ProfilePage />} />
|
||||
</Route>
|
||||
|
||||
{/* 运营管理端 /admin/* */}
|
||||
|
||||
@@ -188,6 +188,19 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
return user.modules?.includes(moduleCode) || false;
|
||||
}, [user]);
|
||||
|
||||
/**
|
||||
* 刷新用户信息(头像更新等场景使用)
|
||||
*/
|
||||
const refreshUser = useCallback(async () => {
|
||||
try {
|
||||
const freshUser = await authApi.getCurrentUser();
|
||||
setUser(freshUser);
|
||||
authApi.saveUser(freshUser);
|
||||
} catch (err) {
|
||||
console.error('刷新用户信息失败:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
@@ -201,7 +214,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
refreshToken,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasModule, // 新增
|
||||
hasModule,
|
||||
refreshUser, // 2026-01-28: 头像更新等场景使用
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -198,6 +198,29 @@ export async function changePassword(request: ChangePasswordRequest): Promise<vo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新头像
|
||||
*/
|
||||
export async function updateAvatar(avatarUrl: string): Promise<{ avatarUrl: string }> {
|
||||
const response = await authFetch<{ avatarUrl: string }>(`${API_BASE}/me/avatar`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ avatarUrl }),
|
||||
});
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw new Error(response.message || '更新头像失败');
|
||||
}
|
||||
|
||||
// 更新本地存储的用户信息
|
||||
const user = getSavedUser();
|
||||
if (user) {
|
||||
user.avatarUrl = avatarUrl;
|
||||
saveUser(user);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,13 @@ export interface AuthUser {
|
||||
isDefaultPassword: boolean;
|
||||
permissions: string[];
|
||||
modules: string[]; // 用户可访问的模块代码列表(如 ['AIA', 'PKB', 'RVW'])
|
||||
// 2026-01-28: 个人中心扩展字段
|
||||
avatarUrl?: string | null;
|
||||
status?: string;
|
||||
kbQuota?: number;
|
||||
kbUsed?: number;
|
||||
isTrial?: boolean;
|
||||
trialEndsAt?: string | null; // ISO date string
|
||||
}
|
||||
|
||||
/** Token信息 */
|
||||
@@ -100,6 +107,8 @@ export interface AuthContextType extends AuthState {
|
||||
hasRole: (...roles: UserRole[]) => boolean;
|
||||
/** 检查模块权限 */
|
||||
hasModule: (moduleCode: string) => boolean;
|
||||
/** 刷新用户信息(头像更新等场景使用) */
|
||||
refreshUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Dropdown, Avatar } from 'antd'
|
||||
import {
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
// SettingOutlined, // MVP阶段暂时隐藏设置按钮
|
||||
ControlOutlined,
|
||||
BankOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { MODULES } from '../modules/moduleRegistry'
|
||||
import { useAuth } from '../auth'
|
||||
import type { ModuleDefinition } from '../modules/types'
|
||||
|
||||
/**
|
||||
* 顶部导航栏组件
|
||||
@@ -53,11 +51,12 @@ const TopNavigation = () => {
|
||||
icon: <UserOutlined />,
|
||||
label: '个人中心',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: <SettingOutlined />,
|
||||
label: '设置',
|
||||
},
|
||||
// MVP阶段暂时隐藏设置按钮
|
||||
// {
|
||||
// key: 'settings',
|
||||
// icon: <SettingOutlined />,
|
||||
// label: '设置',
|
||||
// },
|
||||
// 切换入口 - 根据权限显示
|
||||
...(canAccessOrg || canAccessAdmin ? [{ type: 'divider' as const }] : []),
|
||||
...(canAccessOrg ? [{
|
||||
@@ -103,9 +102,9 @@ const TopNavigation = () => {
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
<img
|
||||
src="/logo.jpg"
|
||||
src="/logo-new.png"
|
||||
alt="AI临床研究平台"
|
||||
className="h-[52px] w-auto"
|
||||
className="h-[48px] w-auto"
|
||||
/>
|
||||
<span className="text-xl font-bold text-blue-600">AI临床研究平台</span>
|
||||
</div>
|
||||
@@ -133,14 +132,15 @@ const TopNavigation = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 用户菜单 - 显示真实用户信息 */}
|
||||
{/* 用户菜单 - 显示真实用户信息和头像 */}
|
||||
<Dropdown
|
||||
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div className="flex items-center gap-2 cursor-pointer px-3 py-2 rounded-md hover:bg-gray-50">
|
||||
<Avatar
|
||||
icon={<UserOutlined />}
|
||||
src={user?.avatarUrl}
|
||||
icon={!user?.avatarUrl && <UserOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
* 数据清洗整理模块
|
||||
*
|
||||
* 路由结构:
|
||||
* - / → Portal工作台(主页)
|
||||
* - / → 直接跳转到 Tool C(科研数据编辑器)
|
||||
* - /tool-a → Tool A - 超级合并器(暂未开发)
|
||||
* - /tool-b → Tool B - 病历结构化机器人(✅ 已完成)
|
||||
* - /tool-c → Tool C - 科研数据编辑器(🚀 Day 4-5开发中)
|
||||
* - /tool-c → Tool C - 科研数据编辑器(🚀 主力工具)
|
||||
* - /portal → Portal工作台(保留,暂不展示)
|
||||
*
|
||||
* 2026-01-28 更新:默认直接进入 Tool C,不再显示 Portal 页面
|
||||
*/
|
||||
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Spin } from 'antd';
|
||||
import Placeholder from '@/shared/components/Placeholder';
|
||||
|
||||
@@ -29,8 +32,11 @@ const DCModule = () => {
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
{/* Portal主页 */}
|
||||
<Route index element={<Portal />} />
|
||||
{/* 默认直接跳转到 Tool C */}
|
||||
<Route index element={<Navigate to="tool-c" replace />} />
|
||||
|
||||
{/* Portal工作台(保留,可通过 /data-cleaning/portal 访问) */}
|
||||
<Route path="portal" element={<Portal />} />
|
||||
|
||||
{/* Tool A - 超级合并器(暂未开发) */}
|
||||
<Route
|
||||
|
||||
@@ -33,6 +33,7 @@ interface TenantConfig {
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: TenantConfig = {
|
||||
name: 'AI临床研究平台',
|
||||
logo: '/logo-new.png', // 默认使用新 Logo
|
||||
primaryColor: '#1890ff',
|
||||
systemName: 'AI临床研究平台',
|
||||
isReviewOnly: false,
|
||||
@@ -261,7 +262,7 @@ export default function LoginPage() {
|
||||
<img
|
||||
src={tenantConfig.logo}
|
||||
alt={tenantConfig.name}
|
||||
style={{ height: 48, marginBottom: 16 }}
|
||||
style={{ height: 108, marginBottom: 16, borderRadius: 8 }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
|
||||
411
frontend-v2/src/pages/user/ProfilePage.tsx
Normal file
411
frontend-v2/src/pages/user/ProfilePage.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* 个人中心页面
|
||||
*
|
||||
* 功能:
|
||||
* - 查看/修改头像(可选)
|
||||
* - 查看姓名、手机号
|
||||
* - 修改密码
|
||||
* - 查看账户状态(有效期)
|
||||
* - 查看已开通模块(展示所有模块,标记已开通)
|
||||
*
|
||||
* @version 2026-01-28
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Avatar,
|
||||
Button,
|
||||
Upload,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Spin,
|
||||
Tag,
|
||||
Divider,
|
||||
Typography,
|
||||
Row,
|
||||
Col
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
CameraOutlined,
|
||||
LockOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
PhoneOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
AppstoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { useAuth } from '@/framework/auth';
|
||||
import { getCurrentUser, changePassword } from '@/framework/auth/api';
|
||||
import type { AuthUser, ChangePasswordRequest } from '@/framework/auth/types';
|
||||
import { MODULES } from '@/framework/modules/moduleRegistry';
|
||||
import type { ModuleDefinition } from '@/framework/modules/types';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { refreshUser } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userInfo, setUserInfo] = useState<AuthUser | null>(null);
|
||||
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
|
||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||
const [uploadLoading, setUploadLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 加载用户详细信息
|
||||
useEffect(() => {
|
||||
loadUserInfo();
|
||||
}, []);
|
||||
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const user = await getCurrentUser();
|
||||
setUserInfo(user);
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败', error);
|
||||
message.error('加载用户信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理头像上传
|
||||
const handleAvatarUpload: UploadProps['customRequest'] = async (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
|
||||
try {
|
||||
setUploadLoading(true);
|
||||
|
||||
// 创建 FormData
|
||||
const formData = new FormData();
|
||||
formData.append('file', file as Blob);
|
||||
|
||||
// 调用后端头像上传接口(上传到 staticStorage)
|
||||
const response = await fetch('/api/v1/auth/me/avatar/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || '上传失败');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '上传失败');
|
||||
}
|
||||
|
||||
// 刷新用户信息(本地页面 + 全局 AuthContext)
|
||||
await loadUserInfo();
|
||||
await refreshUser(); // 同步更新右上角头像
|
||||
|
||||
message.success('头像更新成功');
|
||||
onSuccess?.(result);
|
||||
} catch (error) {
|
||||
console.error('头像上传失败', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '头像上传失败';
|
||||
message.error(errorMessage);
|
||||
onError?.(error as Error);
|
||||
} finally {
|
||||
setUploadLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理修改密码
|
||||
const handleChangePassword = async (values: ChangePasswordRequest) => {
|
||||
try {
|
||||
setPasswordLoading(true);
|
||||
await changePassword(values);
|
||||
message.success('密码修改成功');
|
||||
setPasswordModalOpen(false);
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '修改密码失败';
|
||||
message.error(errorMessage);
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化手机号(隐藏中间4位)
|
||||
const formatPhone = (phone: string) => {
|
||||
if (!phone || phone.length !== 11) return phone;
|
||||
return `${phone.slice(0, 3)}****${phone.slice(7)}`;
|
||||
};
|
||||
|
||||
// 计算账户有效期显示
|
||||
const getAccountStatus = () => {
|
||||
if (!userInfo) return { text: '未知', color: 'default' };
|
||||
|
||||
if (userInfo.status !== 'active') {
|
||||
return { text: '已禁用', color: 'error' };
|
||||
}
|
||||
|
||||
if (userInfo.isTrial && userInfo.trialEndsAt) {
|
||||
const endDate = new Date(userInfo.trialEndsAt);
|
||||
const now = new Date();
|
||||
const daysLeft = Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysLeft <= 0) {
|
||||
return { text: '试用已过期', color: 'error' };
|
||||
} else if (daysLeft <= 7) {
|
||||
return { text: `试用中(剩余 ${daysLeft} 天)`, color: 'warning' };
|
||||
} else {
|
||||
return { text: `试用中(剩余 ${daysLeft} 天)`, color: 'processing' };
|
||||
}
|
||||
}
|
||||
|
||||
return { text: '正式用户', color: 'success' };
|
||||
};
|
||||
|
||||
// 获取有效期显示文本
|
||||
const getExpiryText = () => {
|
||||
if (!userInfo) return '-';
|
||||
|
||||
if (userInfo.isTrial && userInfo.trialEndsAt) {
|
||||
return new Date(userInfo.trialEndsAt).toLocaleDateString('zh-CN');
|
||||
}
|
||||
|
||||
return '永久有效';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center h-[calc(100vh-64px)]">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const accountStatus = getAccountStatus();
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-8 bg-gray-50 min-h-[calc(100vh-64px)] overflow-y-auto">
|
||||
<div className="max-w-4xl mx-auto pb-8">
|
||||
<Title level={2} className="mb-6">个人中心</Title>
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-start gap-6">
|
||||
{/* 头像区域 */}
|
||||
<div className="flex flex-col items-center">
|
||||
<Upload
|
||||
name="avatar"
|
||||
showUploadList={false}
|
||||
accept="image/*"
|
||||
customRequest={handleAvatarUpload}
|
||||
disabled={uploadLoading}
|
||||
>
|
||||
<div className="relative cursor-pointer group">
|
||||
<Avatar
|
||||
size={100}
|
||||
src={userInfo?.avatarUrl}
|
||||
icon={<UserOutlined />}
|
||||
className="border-2 border-gray-200"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-40 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{uploadLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<CameraOutlined className="text-white text-2xl" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Upload>
|
||||
<Text type="secondary" className="mt-2 text-xs">点击更换头像</Text>
|
||||
</div>
|
||||
|
||||
{/* 用户信息 */}
|
||||
<div className="flex-1">
|
||||
<div className="mb-4">
|
||||
<Title level={4} className="mb-1">{userInfo?.name}</Title>
|
||||
<Tag color={accountStatus.color as any}>{accountStatus.text}</Tag>
|
||||
</div>
|
||||
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<PhoneOutlined />
|
||||
<Text>手机号:{formatPhone(userInfo?.phone || '')}</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<SafetyCertificateOutlined />
|
||||
<Text>有效期至:{getExpiryText()}</Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 账户安全卡片 */}
|
||||
<Card title="账户安全" className="mb-6">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<LockOutlined className="text-xl text-gray-400" />
|
||||
<div>
|
||||
<Text strong>登录密码</Text>
|
||||
<br />
|
||||
<Text type="secondary" className="text-sm">
|
||||
{userInfo?.isDefaultPassword
|
||||
? '您正在使用默认密码,建议尽快修改'
|
||||
: '定期修改密码可以提高账户安全性'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type={userInfo?.isDefaultPassword ? 'primary' : 'default'}
|
||||
onClick={() => setPasswordModalOpen(true)}
|
||||
>
|
||||
{userInfo?.isDefaultPassword ? '立即修改' : '修改密码'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 已开通模块卡片 */}
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<AppstoreOutlined />
|
||||
<span>功能模块</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Text type="secondary" className="mb-4 block">
|
||||
以下是平台所有功能模块,绿色标记表示您已开通的模块
|
||||
</Text>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{MODULES.map((module: ModuleDefinition) => {
|
||||
const isEnabled = userInfo?.modules?.includes(module.moduleCode || '');
|
||||
|
||||
return (
|
||||
<Col xs={24} sm={12} md={8} key={module.id}>
|
||||
<div
|
||||
className={`
|
||||
p-4 rounded-lg border-2 transition-all
|
||||
${isEnabled
|
||||
? 'border-green-200 bg-green-50'
|
||||
: 'border-gray-200 bg-gray-50 opacity-60'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`text-2xl ${isEnabled ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
{module.icon && <module.icon />}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text strong className={isEnabled ? 'text-green-700' : 'text-gray-500'}>
|
||||
{module.name}
|
||||
</Text>
|
||||
{isEnabled ? (
|
||||
<CheckCircleOutlined className="text-green-500" />
|
||||
) : (
|
||||
<CloseCircleOutlined className="text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<Text type="secondary" className="text-xs">
|
||||
{module.description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{/* 开通更多模块提示 */}
|
||||
<Divider />
|
||||
<div className="text-center">
|
||||
<Text type="secondary">
|
||||
如需开通更多模块,请联系管理员或客服
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 修改密码弹窗 */}
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={passwordModalOpen}
|
||||
onCancel={() => {
|
||||
setPasswordModalOpen(false);
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleChangePassword}
|
||||
className="mt-4"
|
||||
>
|
||||
{!userInfo?.isDefaultPassword && (
|
||||
<Form.Item
|
||||
name="oldPassword"
|
||||
label="原密码"
|
||||
rules={[{ required: true, message: '请输入原密码' }]}
|
||||
>
|
||||
<Input.Password placeholder="请输入原密码" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码长度至少6位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入新密码(至少6位)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认新密码"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请再次输入新密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0 flex justify-end">
|
||||
<Button onClick={() => setPasswordModalOpen(false)} className="mr-2">
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={passwordLoading}>
|
||||
确认修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
Reference in New Issue
Block a user