feat(admin): 用户菜单新增修改密码弹窗
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import api from '../api';
|
||||
import { useApiMutation } from '../hooks/useApiMutation';
|
||||
import { message } from '../ui/app-message';
|
||||
import { PASSWORD_RULES } from '../utils/password-rules';
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({ open, onClose }) => {
|
||||
const [form] = Form.useForm();
|
||||
const mutation = useApiMutation((values: { newPassword: string }) =>
|
||||
api.post('/auth/change-password', values),
|
||||
);
|
||||
|
||||
// 关闭时重置表单,避免下次打开残留旧值
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
// 同步守卫防止快速双击:confirmLoading 要等 validateFields 返回后才生效
|
||||
const submittingRef = useRef(false);
|
||||
|
||||
const handleOk = async () => {
|
||||
if (submittingRef.current) return;
|
||||
submittingRef.current = true;
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await mutation.mutateAsync({
|
||||
newPassword: values.newPassword,
|
||||
});
|
||||
message.success('密码已修改');
|
||||
form.resetFields();
|
||||
onClose();
|
||||
} catch {
|
||||
// 错误提示由 useApiMutation 统一处理
|
||||
} finally {
|
||||
submittingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={onClose}
|
||||
confirmLoading={mutation.isPending}
|
||||
okText="确认修改"
|
||||
cancelText="取消"
|
||||
forceRender
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="newPassword" label="新密码" rules={PASSWORD_RULES}>
|
||||
<Input.Password autoComplete="new-password" placeholder="至少8位,不含空格" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认新密码"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '请再次输入新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator: (_: unknown, value: string) =>
|
||||
!value || getFieldValue('newPassword') === value
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('两次输入的密码不一致')),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder="请再次输入新密码" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { Layout, Menu, Button, Avatar, Badge, Dropdown, Drawer, Grid, Tooltip } from 'antd';
|
||||
import { BrandLogo } from '../components/BrandLogo';
|
||||
@@ -41,6 +41,7 @@ import NotificationBell from '../components/NotificationBell';
|
||||
import RouteDock from '../components/RouteDock';
|
||||
import RouteKeeper from '../components/RouteKeeper';
|
||||
import BackTop from '../components/BackTop';
|
||||
import ChangePasswordModal from '../components/ChangePasswordModal';
|
||||
import { buildMenu, type AppMenuItem } from '../auth/menu-policy';
|
||||
|
||||
const AiChatDrawer = React.lazy(() => import('../components/AiChat/AiChatDrawer'));
|
||||
@@ -93,6 +94,7 @@ const MainLayout: React.FC = () => {
|
||||
const setDrawerOpen = useAppStore((state) => state.setMobileDrawerOpen);
|
||||
const setAiChatOpen = useAppStore((state) => state.setAiChatOpen);
|
||||
const setAiWorking = useAppStore((state) => state.setAiWorking);
|
||||
const [pwdOpen, setPwdOpen] = useState(false);
|
||||
const setOpenKeys = useAppStore((state) => state.setMenuOpenKeys);
|
||||
const { permissions, hasPermission } = usePermission();
|
||||
|
||||
@@ -385,6 +387,12 @@ const MainLayout: React.FC = () => {
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'change-password',
|
||||
icon: <KeyOutlined />,
|
||||
label: '修改密码',
|
||||
onClick: () => setPwdOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
@@ -433,6 +441,7 @@ const MainLayout: React.FC = () => {
|
||||
/>
|
||||
</React.Suspense>
|
||||
)}
|
||||
<ChangePasswordModal open={pwdOpen} onClose={() => setPwdOpen(false)} />
|
||||
<BackTop />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useDirtyGuard } from '../../hooks/useDirtyGuard';
|
||||
import { validateResponse } from '../../utils/validate';
|
||||
import { rolesSchema, usersSchema } from '../../api/schemas';
|
||||
import { getErrorMessage } from '../../utils/error';
|
||||
import type { Rule } from 'antd/es/form';
|
||||
import { PASSWORD_RULES } from '../../utils/password-rules';
|
||||
|
||||
const USER_FIELDS = {
|
||||
username: 'username',
|
||||
@@ -30,19 +30,6 @@ const USER_FIELDS = {
|
||||
status: 'status',
|
||||
} as const;
|
||||
|
||||
// 与后端 CreateUserDto/ResetPasswordDto 保持一致:bcrypt 截断于 72 字节(UTF-8),
|
||||
// 多字节字符(如中文 3 字节)下按字符数上限会漏检,这里按字节数校验。
|
||||
const PASSWORD_RULES: Rule[] = [
|
||||
{ required: true, min: 8, message: '密码至少8位' },
|
||||
{
|
||||
validator: (_: unknown, value: string) =>
|
||||
!value || new TextEncoder().encode(value).length <= 72
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('密码不能超过72字节(bcrypt 上限)')),
|
||||
},
|
||||
{ pattern: /^\S+$/, message: '密码不能包含空格' },
|
||||
];
|
||||
|
||||
const UsersPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Rule } from 'antd/es/form';
|
||||
|
||||
// 与后端 CreateUserDto/ResetPasswordDto/ChangePasswordDto 保持一致:
|
||||
// bcrypt 截断于 72 字节(UTF-8),多字节字符(如中文 3 字节)下按字符数上限会漏检,
|
||||
// 这里按字节数校验。
|
||||
export const PASSWORD_RULES: Rule[] = [
|
||||
{ required: true, min: 8, message: '密码至少8位' },
|
||||
{
|
||||
validator: (_: unknown, value: string) =>
|
||||
!value || new TextEncoder().encode(value).length <= 72
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('密码不能超过72字节(bcrypt 上限)')),
|
||||
},
|
||||
{ pattern: /^\S+$/, message: '密码不能包含空格' },
|
||||
];
|
||||
Reference in New Issue
Block a user