feat: 添加权限控制相关功能,重构菜单与页面访问逻辑
1. 新增基于权限的菜单过滤和页面路由控制 2. 重构usePageData钩子支持按需加载 3. 调整个人中心页面布局与密码修改功能 4. 优化表格列展示与顶部用户菜单样式
This commit is contained in:
20
src/App.css
20
src/App.css
@@ -210,6 +210,22 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-menu-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #1d2129;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu-trigger:hover {
|
||||||
|
background: #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -272,6 +288,10 @@
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-grid .section-card:first-child {
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-summary {
|
.profile-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
225
src/App.tsx
225
src/App.tsx
@@ -4,6 +4,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Dropdown,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
Layout,
|
||||||
@@ -67,6 +68,7 @@ type AdminProfile = {
|
|||||||
status: string;
|
status: string;
|
||||||
isSuperAdmin: boolean;
|
isSuperAdmin: boolean;
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
|
permissions: Permission[] | ['*'];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ async function fetchPage<T>(url: string): Promise<PageData<T>> {
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePageData<T>(url: string) {
|
function usePageData<T>(url: string, enabled = true) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<PageData<T>>({
|
const [data, setData] = useState<PageData<T>>({
|
||||||
list: [],
|
list: [],
|
||||||
@@ -159,6 +161,9 @@ function usePageData<T>(url: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setData(await fetchPage<T>(url));
|
setData(await fetchPage<T>(url));
|
||||||
@@ -168,8 +173,10 @@ function usePageData<T>(url: string) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (enabled) {
|
||||||
void load();
|
void load();
|
||||||
}, [url]);
|
}
|
||||||
|
}, [url, enabled]);
|
||||||
|
|
||||||
return { data, loading, reload: load };
|
return { data, loading, reload: load };
|
||||||
}
|
}
|
||||||
@@ -244,6 +251,7 @@ type MenuConfig = {
|
|||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
requiredPermissions?: string[];
|
||||||
children?: MenuConfig[];
|
children?: MenuConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,25 +259,56 @@ const SYSTEM_MENU_KEY = '/system';
|
|||||||
|
|
||||||
const menuItems: MenuConfig[] = [
|
const menuItems: MenuConfig[] = [
|
||||||
{ key: '/dashboard', label: '工作台', icon: <IconDashboard /> },
|
{ key: '/dashboard', label: '工作台', icon: <IconDashboard /> },
|
||||||
{ key: '/dramas', label: '短剧管理', icon: <IconVideoCamera /> },
|
{ key: '/dramas', label: '短剧管理', icon: <IconVideoCamera />, requiredPermissions: ['drama:read'] },
|
||||||
{ key: '/users', label: '用户管理', icon: <IconUser /> },
|
{ key: '/users', label: '用户管理', icon: <IconUser />, requiredPermissions: ['user:read'] },
|
||||||
{
|
{
|
||||||
key: SYSTEM_MENU_KEY,
|
key: SYSTEM_MENU_KEY,
|
||||||
label: '系统管理',
|
label: '系统管理',
|
||||||
icon: <IconSettings />,
|
icon: <IconSettings />,
|
||||||
children: [
|
children: [
|
||||||
{ key: '/permissions', label: '权限管理', icon: <IconSafe /> },
|
{ key: '/permissions', label: '权限管理', icon: <IconSafe />, requiredPermissions: ['permission:read'] },
|
||||||
{ key: '/roles', label: '角色管理', icon: <IconApps /> },
|
{ key: '/roles', label: '角色管理', icon: <IconApps />, requiredPermissions: ['role:read'] },
|
||||||
{ key: '/personnel', label: '人员管理', icon: <IconUser /> },
|
{ key: '/personnel', label: '人员管理', icon: <IconUser />, requiredPermissions: ['admin-user:read'] },
|
||||||
|
{ key: '/profile', label: '个人中心', icon: <IconUser /> },
|
||||||
|
{ key: '/logs', label: '日志管理', icon: <IconFile />, requiredPermissions: ['log:read'] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ key: '/logs', label: '日志管理', icon: <IconFile /> },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function findCurrentMenu(pathname: string) {
|
function getPermissionCodes(profile: AdminProfile | null) {
|
||||||
if (pathname.startsWith('/profile')) {
|
if (!profile) {
|
||||||
return { item: { key: '/profile', label: '个人中心' } };
|
return [];
|
||||||
}
|
}
|
||||||
|
if (profile.isSuperAdmin || profile.permissions[0] === '*') {
|
||||||
|
return ['*'];
|
||||||
|
}
|
||||||
|
return (profile.permissions as Permission[]).map((permission) => permission.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPermissions(profile: AdminProfile | null, requiredPermissions?: string[]) {
|
||||||
|
if (!requiredPermissions?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const codes = getPermissionCodes(profile);
|
||||||
|
if (codes.includes('*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return requiredPermissions.every((permission) => codes.includes(permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMenuItems(items: MenuConfig[], profile: AdminProfile | null): MenuConfig[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
if (item.children) {
|
||||||
|
const children = filterMenuItems(item.children, profile);
|
||||||
|
return children.length ? { ...item, children } : null;
|
||||||
|
}
|
||||||
|
return hasPermissions(profile, item.requiredPermissions) ? item : null;
|
||||||
|
})
|
||||||
|
.filter((item): item is MenuConfig => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCurrentMenu(pathname: string) {
|
||||||
for (const item of menuItems) {
|
for (const item of menuItems) {
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
const child = item.children.find((childItem) => pathname.startsWith(childItem.key));
|
const child = item.children.find((childItem) => pathname.startsWith(childItem.key));
|
||||||
@@ -296,14 +335,39 @@ function renderMenuItem(item: MenuConfig) {
|
|||||||
return <Menu.Item key={item.key}>{item.icon}{item.label}</Menu.Item>;
|
return <Menu.Item key={item.key}>{item.icon}{item.label}</Menu.Item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NoAccessPage() {
|
||||||
|
return (
|
||||||
|
<Card className="section-card" title="暂无权限">
|
||||||
|
<Text type="secondary">当前账号没有访问该功能的权限,请联系系统管理员分配角色。</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PermissionRoute({
|
||||||
|
profile,
|
||||||
|
requiredPermissions,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
profile: AdminProfile | null;
|
||||||
|
requiredPermissions?: string[];
|
||||||
|
children: ReactElement;
|
||||||
|
}) {
|
||||||
|
if (!profile) {
|
||||||
|
return <Card className="section-card" loading />;
|
||||||
|
}
|
||||||
|
if (!hasPermissions(profile, requiredPermissions)) {
|
||||||
|
return <NoAccessPage />;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
function AdminLayout() {
|
function AdminLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [profile, setProfile] = useState<AdminProfile | null>(null);
|
const [profile, setProfile] = useState<AdminProfile | null>(null);
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
||||||
const [changingPassword, setChangingPassword] = useState(false);
|
|
||||||
const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]);
|
const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]);
|
||||||
|
const visibleMenuItems = useMemo(() => filterMenuItems(menuItems, profile), [profile]);
|
||||||
const [openMenuKeys, setOpenMenuKeys] = useState<string[]>(current?.parent ? [current.parent.key] : []);
|
const [openMenuKeys, setOpenMenuKeys] = useState<string[]>(current?.parent ? [current.parent.key] : []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -326,26 +390,6 @@ function AdminLayout() {
|
|||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const changePassword = async (values: { oldPassword: string; newPassword: string; confirmPassword: string }) => {
|
|
||||||
if (values.newPassword !== values.confirmPassword) {
|
|
||||||
Message.error('两次输入的新密码不一致');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setChangingPassword(true);
|
|
||||||
try {
|
|
||||||
await api.patch('/api/admin/v1/auth/password', {
|
|
||||||
oldPassword: values.oldPassword,
|
|
||||||
newPassword: values.newPassword,
|
|
||||||
});
|
|
||||||
Message.success('密码修改成功,请重新登录');
|
|
||||||
setPasswordVisible(false);
|
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
|
||||||
navigate('/login', { replace: true });
|
|
||||||
} finally {
|
|
||||||
setChangingPassword(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="layout">
|
<Layout className="layout">
|
||||||
<Sider className="sidebar" width={220} collapsed={collapsed} collapsible trigger={null}>
|
<Sider className="sidebar" width={220} collapsed={collapsed} collapsible trigger={null}>
|
||||||
@@ -359,7 +403,7 @@ function AdminLayout() {
|
|||||||
onClickMenuItem={(key) => navigate(key)}
|
onClickMenuItem={(key) => navigate(key)}
|
||||||
onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)}
|
onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)}
|
||||||
>
|
>
|
||||||
{menuItems.map((item) => renderMenuItem(item))}
|
{visibleMenuItems.map((item) => renderMenuItem(item))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -371,62 +415,63 @@ function AdminLayout() {
|
|||||||
<span>后端 API: {API_BASE_URL}</span>
|
<span>后端 API: {API_BASE_URL}</span>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Dropdown
|
||||||
|
position="br"
|
||||||
|
trigger="click"
|
||||||
|
droplist={(
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item key="profile" onClick={() => navigate('/profile')}>个人中心</Menu.Item>
|
||||||
|
<Menu.Item key="logout" onClick={logout}>退出登录</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button className="user-menu-trigger" type="button">
|
||||||
<Avatar size={28}>{profile?.username?.slice(0, 1).toUpperCase() || 'A'}</Avatar>
|
<Avatar size={28}>{profile?.username?.slice(0, 1).toUpperCase() || 'A'}</Avatar>
|
||||||
<Text>{profile?.nickname || profile?.username || '管理员'}</Text>
|
<Text>{profile?.nickname || profile?.username || '管理员'}</Text>
|
||||||
<Button onClick={() => navigate('/profile')}>个人中心</Button>
|
</button>
|
||||||
<Button onClick={() => setPasswordVisible(true)}>修改密码</Button>
|
</Dropdown>
|
||||||
<Button onClick={logout}>退出登录</Button>
|
|
||||||
</Space>
|
|
||||||
</Header>
|
</Header>
|
||||||
<Content className="content">
|
<Content className="content">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
|
||||||
<Route path="/dramas" element={<DramasPage />} />
|
<Route path="/dramas" element={<PermissionRoute profile={profile} requiredPermissions={['drama:read']}><DramasPage /></PermissionRoute>} />
|
||||||
<Route path="/users" element={<UsersPage />} />
|
<Route path="/users" element={<PermissionRoute profile={profile} requiredPermissions={['user:read']}><UsersPage /></PermissionRoute>} />
|
||||||
<Route path="/permissions" element={<PermissionsPage />} />
|
<Route path="/permissions" element={<PermissionRoute profile={profile} requiredPermissions={['permission:read']}><PermissionsPage /></PermissionRoute>} />
|
||||||
<Route path="/roles" element={<RolesPage />} />
|
<Route path="/roles" element={<PermissionRoute profile={profile} requiredPermissions={['role:read']}><RolesPage /></PermissionRoute>} />
|
||||||
<Route path="/personnel" element={<PersonnelPage />} />
|
<Route path="/personnel" element={<PermissionRoute profile={profile} requiredPermissions={['admin-user:read']}><PersonnelPage /></PermissionRoute>} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<PermissionRoute profile={profile}><ProfilePage /></PermissionRoute>} />
|
||||||
<Route path="/logs" element={<LogsPage />} />
|
<Route path="/logs" element={<PermissionRoute profile={profile} requiredPermissions={['log:read']}><LogsPage /></PermissionRoute>} />
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Modal title="修改密码" visible={passwordVisible} onCancel={() => setPasswordVisible(false)} footer={null}>
|
|
||||||
<Form layout="vertical" onSubmit={changePassword}>
|
|
||||||
<Form.Item label="旧密码" field="oldPassword" rules={[{ required: true }]}>
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="新密码" field="newPassword" rules={[{ required: true }, { minLength: 6, message: '新密码至少 6 位' }]}>
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="确认新密码" field="confirmPassword" rules={[{ required: true }]}>
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" loading={changingPassword}>确认修改</Button>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardPage() {
|
function DashboardPage({ profile }: { profile: AdminProfile | null }) {
|
||||||
const dramas = usePageData<Drama>('/api/admin/v1/dramas');
|
const canReadDramas = hasPermissions(profile, ['drama:read']);
|
||||||
const users = usePageData<AppUser>('/api/admin/v1/users');
|
const canReadUsers = hasPermissions(profile, ['user:read']);
|
||||||
const logs = usePageData<RequestLog>('/api/admin/v1/logs/requests');
|
const canReadLogs = hasPermissions(profile, ['log:read']);
|
||||||
|
const dramas = usePageData<Drama>('/api/admin/v1/dramas', canReadDramas);
|
||||||
|
const users = usePageData<AppUser>('/api/admin/v1/users', canReadUsers);
|
||||||
|
const logs = usePageData<RequestLog>('/api/admin/v1/logs/requests', canReadLogs);
|
||||||
|
const hasAnyDataModule = canReadDramas || canReadUsers || canReadLogs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="page-grid">
|
<div className="page-grid">
|
||||||
<MetricCard label="短剧管理" value={dramas.data.pagination.total} />
|
{canReadDramas && <MetricCard label="短剧管理" value={dramas.data.pagination.total} />}
|
||||||
<MetricCard label="用户管理" value={users.data.pagination.total} />
|
{canReadUsers && <MetricCard label="用户管理" value={users.data.pagination.total} />}
|
||||||
<MetricCard label="请求日志" value={logs.data.pagination.total} />
|
{canReadLogs && <MetricCard label="请求日志" value={logs.data.pagination.total} />}
|
||||||
<MetricCard label="接口状态" value="运行中" />
|
<MetricCard label="接口状态" value="运行中" />
|
||||||
</div>
|
</div>
|
||||||
|
{canReadLogs ? (
|
||||||
<Card className="section-card" title="最近请求">
|
<Card className="section-card" title="最近请求">
|
||||||
<RequestLogTable data={logs.data.list.slice(0, 8)} loading={logs.loading} />
|
<RequestLogTable data={logs.data.list.slice(0, 8)} loading={logs.loading} />
|
||||||
</Card>
|
</Card>
|
||||||
|
) : null}
|
||||||
|
{!hasAnyDataModule ? <NoAccessPage /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -497,7 +542,7 @@ function UsersPage() {
|
|||||||
|
|
||||||
function PermissionsPage() {
|
function PermissionsPage() {
|
||||||
const { data, loading } = usePageData<Permission>('/api/admin/v1/permissions');
|
const { data, loading } = usePageData<Permission>('/api/admin/v1/permissions');
|
||||||
return <Card className="section-card" title="权限管理"><Table rowKey="id" loading={loading} data={data.list} pagination={{ total: data.pagination.total, pageSize: data.pagination.pageSize }} columns={[{ title: 'ID', dataIndex: 'id', width: 80 }, { title: '编码', dataIndex: 'code' }, { title: '名称', dataIndex: 'name' }, { title: '描述', dataIndex: 'description' }]} /></Card>;
|
return <Card className="section-card" title="权限管理"><Table rowKey="id" loading={loading} data={data.list} pagination={{ total: data.pagination.total, pageSize: data.pagination.pageSize }} columns={[{ title: 'ID', dataIndex: 'id', width: 80 }, { title: '权限名称', dataIndex: 'name' }, { title: '描述', dataIndex: 'description' }]} /></Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PermissionTreeNode = {
|
type PermissionTreeNode = {
|
||||||
@@ -553,8 +598,7 @@ function getPermissionAction(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPermissionTitle(permission: Permission, module?: PermissionModuleConfig) {
|
function getPermissionTitle(permission: Permission, module?: PermissionModuleConfig) {
|
||||||
const action = actionNames[getPermissionAction(permission.code)] || permission.name;
|
return permission.name || (module ? `${actionNames[getPermissionAction(permission.code)]}${module.subject}` : permission.code);
|
||||||
return module ? `${action}${module.subject}` : permission.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPermissionModuleNode(
|
function buildPermissionModuleNode(
|
||||||
@@ -723,7 +767,7 @@ function RolesPage() {
|
|||||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||||
{ title: '编码', dataIndex: 'code' },
|
{ title: '编码', dataIndex: 'code' },
|
||||||
{ title: '名称', dataIndex: 'name' },
|
{ title: '名称', dataIndex: 'name' },
|
||||||
{ title: '权限列表', render: (_, record: Role) => <Space wrap>{record.permissions.map((item) => <Tag key={item.id}>{item.code}</Tag>)}</Space> },
|
{ title: '权限列表', render: (_, record: Role) => <Space wrap>{record.permissions.map((item) => <Tag key={item.id}>{item.name}</Tag>)}</Space> },
|
||||||
{ title: '操作', width: 140, render: (_, record: Role) => <Button size="small" onClick={() => openAssignPermissions(record)}>分配权限</Button> },
|
{ title: '操作', width: 140, render: (_, record: Role) => <Button size="small" onClick={() => openAssignPermissions(record)}>分配权限</Button> },
|
||||||
]} />
|
]} />
|
||||||
<Modal title="新增角色" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null}>
|
<Modal title="新增角色" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null}>
|
||||||
@@ -908,9 +952,12 @@ function PersonnelPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProfilePage() {
|
function ProfilePage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [passwordForm] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [changingPassword, setChangingPassword] = useState(false);
|
||||||
const [profile, setProfile] = useState<AdminProfile | null>(null);
|
const [profile, setProfile] = useState<AdminProfile | null>(null);
|
||||||
|
|
||||||
const loadProfile = async () => {
|
const loadProfile = async () => {
|
||||||
@@ -942,6 +989,26 @@ function ProfilePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changePassword = async (values: { oldPassword: string; newPassword: string; confirmPassword: string }) => {
|
||||||
|
if (values.newPassword !== values.confirmPassword) {
|
||||||
|
Message.error('两次输入的新密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setChangingPassword(true);
|
||||||
|
try {
|
||||||
|
await api.patch('/api/admin/v1/auth/password', {
|
||||||
|
oldPassword: values.oldPassword,
|
||||||
|
newPassword: values.newPassword,
|
||||||
|
});
|
||||||
|
Message.success('密码修改成功,请重新登录');
|
||||||
|
passwordForm.resetFields();
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
} finally {
|
||||||
|
setChangingPassword(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-grid">
|
<div className="profile-grid">
|
||||||
<Card className="section-card" title="个人中心" loading={loading}>
|
<Card className="section-card" title="个人中心" loading={loading}>
|
||||||
@@ -970,6 +1037,20 @@ function ProfilePage() {
|
|||||||
<Button type="primary" htmlType="submit" loading={saving}>保存修改</Button>
|
<Button type="primary" htmlType="submit" loading={saving}>保存修改</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card className="section-card" title="修改密码">
|
||||||
|
<Form form={passwordForm} layout="vertical" onSubmit={changePassword}>
|
||||||
|
<Form.Item label="旧密码" field="oldPassword" rules={[{ required: true }]}>
|
||||||
|
<Input.Password placeholder="请输入旧密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="新密码" field="newPassword" rules={[{ required: true }, { minLength: 6, message: '新密码至少 6 位' }]}>
|
||||||
|
<Input.Password placeholder="请输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="确认新密码" field="confirmPassword" rules={[{ required: true }]}>
|
||||||
|
<Input.Password placeholder="请再次输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={changingPassword}>确认修改</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user