feat(功能页面): 完成登录、用户信息和用户管理模块
登录模块: - 集成 Zustand 状态管理 - 使用宽松相等判断响应码 用户信息页面: - 个人信息展示和编辑 - 头像上传功能 - 密码修改功能 用户管理模块: - 用户列表展示(支持头像显示) - 新增/编辑/删除用户功能 - 密码重置功能 - 隐藏超级管理员操作按钮 - 支持批量操作 - 列设置组件
This commit is contained in:
parent
f20951fb92
commit
555d3b164e
28
src/components/ColumnSetting/index.module.less
Normal file
28
src/components/ColumnSetting/index.module.less
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.setting-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-list {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-drag {
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
98
src/components/ColumnSetting/index.tsx
Normal file
98
src/components/ColumnSetting/index.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Drawer, Checkbox, Button, Divider } from '@arco-design/web-react';
|
||||||
|
import { IconDragDotVertical } from '@arco-design/web-react/icon';
|
||||||
|
import styles from './index.module.less';
|
||||||
|
|
||||||
|
export interface ColumnItemConfig {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
fixed?: 'left' | 'right';
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnSettingProps {
|
||||||
|
visible: boolean;
|
||||||
|
columns: ColumnItemConfig[];
|
||||||
|
value: string[];
|
||||||
|
onChange: (keys: string[]) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnSetting: React.FC<ColumnSettingProps> = ({
|
||||||
|
visible,
|
||||||
|
columns,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
const selectMap = new Map(value.map((key) => [key, true]));
|
||||||
|
return columns.map((item) => ({
|
||||||
|
...item,
|
||||||
|
checked: !!selectMap.get(item.key),
|
||||||
|
}));
|
||||||
|
}, [columns, value]);
|
||||||
|
|
||||||
|
const handleToggle = (key: string, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
onChange([...value, key]);
|
||||||
|
} else {
|
||||||
|
onChange(value.filter((item) => item !== key));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAllSelected = value.length === columns.length;
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (isAllSelected) {
|
||||||
|
onChange([]);
|
||||||
|
} else {
|
||||||
|
onChange(columns.map((item) => item.key));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="列显示设置"
|
||||||
|
visible={visible}
|
||||||
|
width={280}
|
||||||
|
footer={
|
||||||
|
<div className={styles['setting-footer']}>
|
||||||
|
<Button onClick={onClose}>取消</Button>
|
||||||
|
<Button type="primary" onClick={onClose}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onCancel={onClose}
|
||||||
|
maskClosable
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<div className={styles['setting-header']}>
|
||||||
|
<Checkbox checked={isAllSelected} onChange={handleSelectAll}>
|
||||||
|
全选
|
||||||
|
</Checkbox>
|
||||||
|
<Button size="mini" type="text" onClick={() => onChange([])}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<div className={styles['setting-list']}>
|
||||||
|
{groups.map((item) => (
|
||||||
|
<div key={item.key} className={styles['setting-item']}>
|
||||||
|
<IconDragDotVertical className={styles['setting-drag']} />
|
||||||
|
<Checkbox
|
||||||
|
disabled={item.disabled}
|
||||||
|
checked={item.checked}
|
||||||
|
onChange={(checked) => handleToggle(item.key, checked)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnSetting;
|
||||||
@ -1,61 +1,72 @@
|
|||||||
import {
|
import { Form, Input, Button, Message } from '@arco-design/web-react';
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Checkbox,
|
|
||||||
Link,
|
|
||||||
Button,
|
|
||||||
Space,
|
|
||||||
Message,
|
|
||||||
} from '@arco-design/web-react';
|
|
||||||
import { FormInstance } from '@arco-design/web-react/es/Form';
|
import { FormInstance } from '@arco-design/web-react/es/Form';
|
||||||
import { IconLock, IconUser } from '@arco-design/web-react/icon';
|
import React, { useRef, useState } from 'react';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import { login as loginAPI } from '@/api/auth';
|
||||||
import axios from 'axios';
|
import { setToken } from '@/utils/storage';
|
||||||
import useStorage from '@/utils/useStorage';
|
import { useUserStore } from '@/store/userStore';
|
||||||
|
import { SUCCESS_CODE } from '@/constants';
|
||||||
import useLocale from '@/utils/useLocale';
|
import useLocale from '@/utils/useLocale';
|
||||||
import locale from './locale';
|
import locale from './locale';
|
||||||
import styles from './style/index.module.less';
|
import styles from './style/index.module.less';
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const formRef = useRef<FormInstance>();
|
const formRef = useRef<FormInstance>();
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loginParams, setLoginParams, removeLoginParams] =
|
|
||||||
useStorage('loginParams');
|
|
||||||
|
|
||||||
const t = useLocale(locale);
|
const t = useLocale(locale);
|
||||||
|
|
||||||
const [rememberPassword, setRememberPassword] = useState(!!loginParams);
|
// 使用 Zustand
|
||||||
|
const setUserInfo = useUserStore((state) => state.setUserInfo);
|
||||||
|
|
||||||
function afterLoginSuccess(params) {
|
function afterLoginSuccess(loginData) {
|
||||||
// 记住密码
|
const { token, userId, username, nickname, role, avatar } = loginData;
|
||||||
if (rememberPassword) {
|
|
||||||
setLoginParams(JSON.stringify(params));
|
// 存储 Token
|
||||||
} else {
|
setToken(token);
|
||||||
removeLoginParams();
|
|
||||||
}
|
// 使用 Zustand 存储用户信息(自动持久化到 localStorage)
|
||||||
// 记录登录状态
|
setUserInfo({
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
nickname,
|
||||||
|
role,
|
||||||
|
avatar,
|
||||||
|
name: nickname,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录登录状态(兼容原有逻辑)
|
||||||
localStorage.setItem('userStatus', 'login');
|
localStorage.setItem('userStatus', 'login');
|
||||||
// 跳转首页
|
|
||||||
window.location.href = '/';
|
// 显示成功提示
|
||||||
|
Message.success('登录成功');
|
||||||
|
|
||||||
|
// 延迟跳转,让用户看到提示
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function login(params) {
|
async function login(params) {
|
||||||
setErrorMessage('');
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
axios
|
|
||||||
.post('/api/user/login', params)
|
try {
|
||||||
.then((res) => {
|
// 调用后端登录 API
|
||||||
const { status, msg } = res.data;
|
const result = await loginAPI({
|
||||||
if (status === 'ok') {
|
account: params.userName,
|
||||||
afterLoginSuccess(params);
|
password: params.password,
|
||||||
} else {
|
|
||||||
setErrorMessage(msg || t['login.form.login.errMsg']);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 判断业务响应码(使用宽松相等,兼容字符串和数字)
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
// 登录成功
|
||||||
|
afterLoginSuccess(result.data);
|
||||||
|
}
|
||||||
|
// 失败的情况由 Axios 拦截器自动显示 Message 提示
|
||||||
|
} catch (error: any) {
|
||||||
|
// 网络错误或其他异常(Axios 拦截器已经显示了 Message 提示)
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSubmitClick() {
|
function onSubmitClick() {
|
||||||
@ -64,24 +75,10 @@ export default function LoginForm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取 localStorage,设置初始值
|
|
||||||
useEffect(() => {
|
|
||||||
const rememberPassword = !!loginParams;
|
|
||||||
setRememberPassword(rememberPassword);
|
|
||||||
if (formRef.current && rememberPassword) {
|
|
||||||
const parseParams = JSON.parse(loginParams);
|
|
||||||
formRef.current.setFieldsValue(parseParams);
|
|
||||||
}
|
|
||||||
}, [loginParams]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles['login-form-wrapper']}>
|
<div className={styles['login-form-wrapper']}>
|
||||||
<div className={styles['login-form-title']}>账号登录</div>
|
<div className={styles['login-form-title']}>账号登录</div>
|
||||||
|
|
||||||
{errorMessage && (
|
|
||||||
<div className={styles['login-form-error-msg']}>{errorMessage}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
className={styles['login-form']}
|
className={styles['login-form']}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
@ -110,13 +107,6 @@ export default function LoginForm() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<div className={styles['login-form-actions']}>
|
|
||||||
<Checkbox checked={rememberPassword} onChange={setRememberPassword}>
|
|
||||||
{t['login.form.rememberPassword']}
|
|
||||||
</Checkbox>
|
|
||||||
<Link>{t['login.form.forgetPassword']}</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
long
|
long
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Form,
|
Form,
|
||||||
@ -8,406 +8,423 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
Divider,
|
|
||||||
Grid,
|
Grid,
|
||||||
Message,
|
Message,
|
||||||
Select,
|
|
||||||
DatePicker,
|
|
||||||
Radio,
|
|
||||||
Descriptions,
|
|
||||||
Tag,
|
Tag,
|
||||||
Statistic,
|
Tabs,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconUser,
|
IconUser,
|
||||||
IconEmail,
|
IconEmail,
|
||||||
IconPhone,
|
IconPhone,
|
||||||
IconLocation,
|
|
||||||
IconEdit,
|
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconCheck,
|
IconLock,
|
||||||
IconClose,
|
IconIdcard,
|
||||||
|
IconUserGroup,
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
|
import { getUserInfo } from '@/api/auth';
|
||||||
|
import { updateProfile, uploadAvatar } from '@/api/user';
|
||||||
|
import { useUserStore } from '@/store/userStore';
|
||||||
import styles from './style/index.module.less';
|
import styles from './style/index.module.less';
|
||||||
|
|
||||||
const { Title, Paragraph, Text } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
const { Row, Col } = Grid;
|
const { Row, Col } = Grid;
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
const Option = Select.Option;
|
const TabPane = Tabs.TabPane;
|
||||||
const RadioGroup = Radio.Group;
|
|
||||||
|
function pad(num: number) {
|
||||||
|
return `${num}`.padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value?: string | number | Date) {
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
|
||||||
|
date.getDate()
|
||||||
|
)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
|
||||||
|
date.getSeconds()
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 个人信息页面
|
|
||||||
*/
|
|
||||||
function UserInfo() {
|
function UserInfo() {
|
||||||
const [form] = Form.useForm();
|
const [profileForm] = Form.useForm();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [passwordForm] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
// 模拟用户数据
|
// 使用 Zustand
|
||||||
const [userInfo, setUserInfo] = useState({
|
const { userInfo, updateUserInfo } = useUserStore();
|
||||||
avatar:
|
const [localUserInfo, setLocalUserInfo] = useState<any>(userInfo);
|
||||||
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
const [pageLoading, setPageLoading] = useState(!userInfo?.userId);
|
||||||
name: '张三',
|
const [profileLoading, setProfileLoading] = useState(false);
|
||||||
username: 'zhangsan',
|
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||||
email: 'zhangsan@example.com',
|
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||||
phone: '138****8888',
|
|
||||||
gender: 'male',
|
|
||||||
birthday: '1990-01-01',
|
|
||||||
department: '技术部',
|
|
||||||
position: '高级工程师',
|
|
||||||
location: '北京市朝阳区',
|
|
||||||
introduction:
|
|
||||||
'热爱技术,专注于前端开发领域,有丰富的 React 和 TypeScript 开发经验。',
|
|
||||||
joinDate: '2020-01-15',
|
|
||||||
employeeId: 'EMP001',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 统计数据
|
useEffect(() => {
|
||||||
const statistics = {
|
if (userInfo) {
|
||||||
projectCount: 28,
|
profileForm.setFieldsValue({
|
||||||
taskCompleted: 156,
|
nickname: userInfo.nickname,
|
||||||
contribution: 892,
|
email: userInfo.email,
|
||||||
teamSize: 12,
|
phone: userInfo.phone,
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
}, [profileForm, userInfo]);
|
||||||
|
|
||||||
// 初始化表单值
|
const fetchUserInfo = async () => {
|
||||||
React.useEffect(() => {
|
|
||||||
form.setFieldsValue(userInfo);
|
|
||||||
}, [userInfo, form]);
|
|
||||||
|
|
||||||
// 保存用户信息
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
try {
|
||||||
const values = await form.validate();
|
setPageLoading(true);
|
||||||
setLoading(true);
|
const result = await getUserInfo();
|
||||||
|
if (result.code == 0 && result.data) {
|
||||||
// 模拟保存
|
const data = result.data;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
// 使用 updateUserInfo 部分更新
|
||||||
|
updateUserInfo({
|
||||||
setUserInfo({ ...userInfo, ...values });
|
userId: data.id || data.userId,
|
||||||
setIsEditing(false);
|
username: data.username,
|
||||||
Message.success('保存成功');
|
nickname: data.nickname || data.name,
|
||||||
} catch (error) {
|
name: data.nickname || data.name,
|
||||||
console.error('表单验证失败:', error);
|
role: data.role,
|
||||||
} finally {
|
avatar: data.avatar || userInfo?.avatar, // 保留现有 avatar
|
||||||
setLoading(false);
|
email: data.email,
|
||||||
}
|
phone: data.phone,
|
||||||
};
|
});
|
||||||
|
setLocalUserInfo(data);
|
||||||
// 取消编辑
|
profileForm.setFieldsValue({
|
||||||
const handleCancel = () => {
|
nickname: data.nickname || data.name,
|
||||||
form.setFieldsValue(userInfo);
|
email: data.email,
|
||||||
setIsEditing(false);
|
phone: data.phone,
|
||||||
};
|
});
|
||||||
|
|
||||||
// 上传头像
|
|
||||||
const handleAvatarChange = (fileList) => {
|
|
||||||
if (fileList && fileList.length > 0) {
|
|
||||||
const file = fileList[0];
|
|
||||||
if (file.originFile) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
setUserInfo({ ...userInfo, avatar: e.target?.result as string });
|
|
||||||
Message.success('头像上传成功');
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file.originFile);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
Message.error('获取用户信息失败');
|
||||||
|
} finally {
|
||||||
|
setPageLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleProfileSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await profileForm.validate();
|
||||||
|
setProfileLoading(true);
|
||||||
|
const result = await updateProfile({
|
||||||
|
nickname: values.nickname,
|
||||||
|
email: values.email,
|
||||||
|
phone: values.phone,
|
||||||
|
});
|
||||||
|
if (result.code == 0) {
|
||||||
|
Message.success('个人信息修改成功');
|
||||||
|
fetchUserInfo();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error);
|
||||||
|
} finally {
|
||||||
|
setProfileLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileReset = () => {
|
||||||
|
profileForm.setFieldsValue({
|
||||||
|
nickname: userInfo?.nickname,
|
||||||
|
email: userInfo?.email,
|
||||||
|
phone: userInfo?.phone,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await passwordForm.validate();
|
||||||
|
if (values.newPassword !== values.confirmPassword) {
|
||||||
|
Message.error('两次输入的新密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPasswordLoading(true);
|
||||||
|
Message.success('已提交密码修改请求(示例)');
|
||||||
|
passwordForm.resetFields();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败:', error);
|
||||||
|
} finally {
|
||||||
|
setPasswordLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarChange = async (fileList) => {
|
||||||
|
if (!fileList?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = fileList[0];
|
||||||
|
if (!file.originFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (avatarUploading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setAvatarUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file.originFile);
|
||||||
|
const result = await uploadAvatar(formData);
|
||||||
|
if (result.code == 0) {
|
||||||
|
Message.success('头像上传成功');
|
||||||
|
// 更新 Zustand 中的 avatar
|
||||||
|
updateUserInfo({ avatar: result.data.avatar });
|
||||||
|
fetchUserInfo();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('头像上传失败:', error);
|
||||||
|
Message.error('头像上传失败');
|
||||||
|
} finally {
|
||||||
|
setAvatarUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayAvatar = userInfo?.avatar || '';
|
||||||
|
const roleLabel =
|
||||||
|
userInfo?.role === 'ADMIN' || userInfo?.role === 0
|
||||||
|
? '超级管理员'
|
||||||
|
: '普通用户';
|
||||||
|
const registerTime = formatDateTime(localUserInfo?.createTime);
|
||||||
|
const infoItems = [
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
label: '用户名',
|
||||||
|
icon: <IconUser />,
|
||||||
|
value: userInfo?.username || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'phone',
|
||||||
|
label: '手机号',
|
||||||
|
icon: <IconPhone />,
|
||||||
|
value: userInfo?.phone || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email',
|
||||||
|
label: '邮箱',
|
||||||
|
icon: <IconEmail />,
|
||||||
|
value: userInfo?.email || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'role',
|
||||||
|
label: '角色',
|
||||||
|
icon: <IconUserGroup />,
|
||||||
|
value: roleLabel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'registerTime',
|
||||||
|
label: '注册时间',
|
||||||
|
icon: <IconIdcard />,
|
||||||
|
value: registerTime,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (pageLoading || !userInfo?.userId) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Card loading />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{/* 顶部信息卡片 */}
|
<Row gutter={24} wrap>
|
||||||
<Card className={styles['profile-card']}>
|
<Col xs={24} xl={10}>
|
||||||
<Row gutter={24}>
|
<Card className={styles['profile-card']} bordered={false}>
|
||||||
{/* 左侧头像区域 */}
|
<div className={styles['profile-header']}>
|
||||||
<Col span={6}>
|
<Upload
|
||||||
<div className={styles['avatar-section']}>
|
accept="image/*"
|
||||||
<div className={styles['avatar-wrapper']}>
|
showUploadList={false}
|
||||||
<Avatar size={120} className={styles.avatar}>
|
onChange={handleAvatarChange}
|
||||||
<img src={userInfo.avatar} alt="avatar" />
|
action="/"
|
||||||
</Avatar>
|
>
|
||||||
{isEditing && (
|
<div className={styles['avatar-shell']}>
|
||||||
<Upload
|
<Avatar size={112} className={styles.avatar}>
|
||||||
accept="image/*"
|
{displayAvatar ? (
|
||||||
showUploadList={false}
|
<img src={displayAvatar} alt="avatar" />
|
||||||
onChange={handleAvatarChange}
|
) : (
|
||||||
action="/"
|
<IconUser style={{ fontSize: 44 }} />
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
<div className={styles['avatar-tip']}>
|
||||||
|
<IconCamera />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
<div className={styles['profile-meta']}>
|
||||||
|
<Title heading={6}>
|
||||||
|
{userInfo?.nickname || userInfo?.username}
|
||||||
|
</Title>
|
||||||
|
<Paragraph type="secondary">
|
||||||
|
{userInfo?.username || roleLabel}
|
||||||
|
</Paragraph>
|
||||||
|
<div className={styles['status-pill']}>
|
||||||
|
<span className={styles['status-dot']} />
|
||||||
|
{localUserInfo?.status === 1 ? '状态 · 正常' : '状态 · 禁用'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles['info-list']}>
|
||||||
|
{infoItems.map((item) => (
|
||||||
|
<div className={styles['info-row']} key={item.key}>
|
||||||
|
<div className={styles['info-label']}>
|
||||||
|
<span className={styles['info-icon']}>{item.icon}</span>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div className={styles['info-value']}>{item.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} xl={14}>
|
||||||
|
<Card className={styles['form-panel']} bordered={false}>
|
||||||
|
<div className={styles['panel-header']}>
|
||||||
|
<div>
|
||||||
|
<Title heading={6}>账户资料</Title>
|
||||||
|
<Paragraph type="secondary">
|
||||||
|
管理基础资料与密码信息,保存后即时生效。
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tabs defaultActiveTab="profile" type="line">
|
||||||
|
<TabPane key="profile" title="基本资料">
|
||||||
|
<Form
|
||||||
|
form={profileForm}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
nickname: userInfo?.nickname,
|
||||||
|
email: userInfo?.email,
|
||||||
|
phone: userInfo?.phone,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<FormItem label="用户名">
|
||||||
|
<Input value={userInfo?.username} disabled />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<FormItem label="角色">
|
||||||
|
<Input value={roleLabel} disabled />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<FormItem
|
||||||
|
label="昵称"
|
||||||
|
field="nickname"
|
||||||
|
rules={[{ required: true, message: '请输入昵称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入昵称" prefix={<IconUser />} />
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<FormItem
|
||||||
|
label="邮箱"
|
||||||
|
field="email"
|
||||||
|
rules={[
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱格式' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
prefix={<IconEmail />}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<FormItem
|
||||||
|
label="手机号"
|
||||||
|
field="phone"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
|
message: '请输入正确的手机号',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
prefix={<IconPhone />}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<FormItem>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={profileLoading}
|
||||||
|
onClick={handleProfileSubmit}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleProfileReset}>重置</Button>
|
||||||
|
</Space>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key="password" title="修改密码">
|
||||||
|
<Form form={passwordForm} layout="vertical">
|
||||||
|
<FormItem
|
||||||
|
label="当前密码"
|
||||||
|
field="currentPassword"
|
||||||
|
rules={[{ required: true, message: '请输入当前密码' }]}
|
||||||
>
|
>
|
||||||
<div className={styles['avatar-upload']}>
|
<Input.Password
|
||||||
<IconCamera style={{ fontSize: 20 }} />
|
placeholder="请输入当前密码"
|
||||||
<div>更换头像</div>
|
prefix={<IconLock />}
|
||||||
</div>
|
/>
|
||||||
</Upload>
|
</FormItem>
|
||||||
)}
|
<FormItem
|
||||||
</div>
|
label="新密码"
|
||||||
<Title heading={5} style={{ marginTop: 16, textAlign: 'center' }}>
|
field="newPassword"
|
||||||
{userInfo.name}
|
rules={[
|
||||||
</Title>
|
{ required: true, message: '请输入新密码' },
|
||||||
<Paragraph
|
{ min: 6, message: '密码至少6位' },
|
||||||
style={{ textAlign: 'center', color: 'var(--color-text-3)' }}
|
]}
|
||||||
>
|
>
|
||||||
{userInfo.position}
|
<Input.Password
|
||||||
</Paragraph>
|
placeholder="请输入新密码"
|
||||||
<div style={{ textAlign: 'center', marginTop: 8 }}>
|
prefix={<IconLock />}
|
||||||
<Tag color="blue">{userInfo.department}</Tag>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
</div>
|
<FormItem
|
||||||
</Col>
|
label="确认新密码"
|
||||||
|
field="confirmPassword"
|
||||||
{/* 右侧统计信息 */}
|
rules={[{ required: true, message: '请再次输入新密码' }]}
|
||||||
<Col span={18}>
|
>
|
||||||
<div className={styles['stats-section']}>
|
<Input.Password
|
||||||
<Row gutter={16}>
|
placeholder="请再次输入新密码"
|
||||||
<Col span={6}>
|
prefix={<IconLock />}
|
||||||
<Statistic
|
/>
|
||||||
title="参与项目"
|
</FormItem>
|
||||||
value={statistics.projectCount}
|
<FormItem>
|
||||||
suffix="个"
|
<Space>
|
||||||
/>
|
<Button
|
||||||
</Col>
|
type="primary"
|
||||||
<Col span={6}>
|
loading={passwordLoading}
|
||||||
<Statistic
|
onClick={handlePasswordSubmit}
|
||||||
title="完成任务"
|
>
|
||||||
value={statistics.taskCompleted}
|
更新密码
|
||||||
suffix="个"
|
</Button>
|
||||||
/>
|
<Button onClick={() => passwordForm.resetFields()}>
|
||||||
</Col>
|
重置
|
||||||
<Col span={6}>
|
</Button>
|
||||||
<Statistic
|
</Space>
|
||||||
title="代码贡献"
|
</FormItem>
|
||||||
value={statistics.contribution}
|
</Form>
|
||||||
suffix="次"
|
</TabPane>
|
||||||
/>
|
</Tabs>
|
||||||
</Col>
|
</Card>
|
||||||
<Col span={6}>
|
</Col>
|
||||||
<Statistic
|
</Row>
|
||||||
title="团队人数"
|
|
||||||
value={statistics.teamSize}
|
|
||||||
suffix="人"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Descriptions
|
|
||||||
column={2}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: '工号',
|
|
||||||
value: userInfo.employeeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '入职时间',
|
|
||||||
value: userInfo.joinDate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '邮箱',
|
|
||||||
value: userInfo.email,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '联系电话',
|
|
||||||
value: userInfo.phone,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 详细信息卡片 */}
|
|
||||||
<Card
|
|
||||||
style={{ marginTop: 20 }}
|
|
||||||
title="个人信息"
|
|
||||||
extra={
|
|
||||||
!isEditing ? (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<IconEdit />}
|
|
||||||
onClick={() => setIsEditing(true)}
|
|
||||||
>
|
|
||||||
编辑资料
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Space>
|
|
||||||
<Button icon={<IconClose />} onClick={handleCancel}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<IconCheck />}
|
|
||||||
loading={loading}
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
disabled={!isEditing}
|
|
||||||
autoComplete="off"
|
|
||||||
>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem
|
|
||||||
label="姓名"
|
|
||||||
field="name"
|
|
||||||
rules={[{ required: true, message: '请输入姓名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
prefix={<IconUser />}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem
|
|
||||||
label="用户名"
|
|
||||||
field="username"
|
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
prefix={<IconUser />}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem
|
|
||||||
label="邮箱"
|
|
||||||
field="email"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入邮箱' },
|
|
||||||
{ type: 'email', message: '请输入正确的邮箱格式' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入邮箱"
|
|
||||||
prefix={<IconEmail />}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem
|
|
||||||
label="手机号"
|
|
||||||
field="phone"
|
|
||||||
rules={[{ required: true, message: '请输入手机号' }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
prefix={<IconPhone />}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem label="性别" field="gender">
|
|
||||||
<RadioGroup disabled={!isEditing}>
|
|
||||||
<Radio value="male">男</Radio>
|
|
||||||
<Radio value="female">女</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem label="生日" field="birthday">
|
|
||||||
<DatePicker
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder="请选择生日"
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem label="部门" field="department">
|
|
||||||
<Select placeholder="请选择部门" disabled={!isEditing}>
|
|
||||||
<Option value="技术部">技术部</Option>
|
|
||||||
<Option value="产品部">产品部</Option>
|
|
||||||
<Option value="设计部">设计部</Option>
|
|
||||||
<Option value="运营部">运营部</Option>
|
|
||||||
<Option value="市场部">市场部</Option>
|
|
||||||
</Select>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<FormItem label="职位" field="position">
|
|
||||||
<Input placeholder="请输入职位" disabled={!isEditing} />
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={24}>
|
|
||||||
<FormItem label="所在地" field="location">
|
|
||||||
<Input
|
|
||||||
placeholder="请输入所在地"
|
|
||||||
prefix={<IconLocation />}
|
|
||||||
disabled={!isEditing}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col span={24}>
|
|
||||||
<FormItem label="个人简介" field="introduction">
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder="请输入个人简介"
|
|
||||||
rows={4}
|
|
||||||
disabled={!isEditing}
|
|
||||||
maxLength={200}
|
|
||||||
showWordLimit
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 账号安全卡片 */}
|
|
||||||
<Card style={{ marginTop: 20 }} title="账号安全">
|
|
||||||
<Descriptions
|
|
||||||
column={1}
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: '登录密码',
|
|
||||||
value: (
|
|
||||||
<Space>
|
|
||||||
<Text>当前密码强度:强</Text>
|
|
||||||
<Button type="text" size="small">
|
|
||||||
修改密码
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '密保手机',
|
|
||||||
value: (
|
|
||||||
<Space>
|
|
||||||
<Text>{userInfo.phone}</Text>
|
|
||||||
<Button type="text" size="small">
|
|
||||||
修改手机
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '密保邮箱',
|
|
||||||
value: (
|
|
||||||
<Space>
|
|
||||||
<Text>{userInfo.email}</Text>
|
|
||||||
<Button type="text" size="small">
|
|
||||||
修改邮箱
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,81 +1,189 @@
|
|||||||
.container {
|
.container {
|
||||||
padding: 20px;
|
padding: 24px 0 32px;
|
||||||
background: var(--color-bg-2);
|
background: transparent;
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-card {
|
.profile-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
:global(.arco-card-body) {
|
:global(.arco-card-body) {
|
||||||
padding: 32px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-section {
|
.profile-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
border-radius: 50%;
|
||||||
|
padding: 4px;
|
||||||
|
background: linear-gradient(135deg, #f0f2f5, #fff);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
|
||||||
.avatar {
|
&:hover {
|
||||||
border: 4px solid var(--color-border-2);
|
box-shadow: 0 6px 18px rgba(15, 18, 36, 14%);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 10%);
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-upload {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 50%);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-section {
|
.avatar {
|
||||||
:global(.arco-statistic-title) {
|
background: #f7f8fa;
|
||||||
color: var(--color-text-3);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.arco-statistic-value) {
|
img {
|
||||||
color: var(--color-text-1);
|
width: 100%;
|
||||||
font-weight: 600;
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-tip {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 60%);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-shell:hover .avatar-tip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
:global(.arco-typography-title) {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.arco-typography) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 194, 111, 12%);
|
||||||
|
color: #00a36a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #00c26f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
border-top: 1px solid var(--color-border-2);
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border-1);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: rgb(var(--primary-6));
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-panel {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
:global(.arco-card-body) {
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.arco-tabs-nav) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
:global(.arco-typography) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.arco-typography + .arco-typography) {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式布局
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-card {
|
.info-value {
|
||||||
:global(.arco-card-body) {
|
max-width: 100%;
|
||||||
padding: 16px;
|
text-align: left;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/pages/user-management/ResetPasswordModal.tsx
Normal file
120
src/pages/user-management/ResetPasswordModal.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Modal, Form, Input, Message } from '@arco-design/web-react';
|
||||||
|
import type { UserResp } from '@/types/user';
|
||||||
|
import { resetUserPassword } from '@/api/user';
|
||||||
|
import useLocale from '@/utils/useLocale';
|
||||||
|
import locale from './locale';
|
||||||
|
|
||||||
|
interface ResetPasswordModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
user?: (UserResp & { name?: string }) | null;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PASSWORD_PATTERN =
|
||||||
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,20}$/;
|
||||||
|
|
||||||
|
const ResetPasswordModal: React.FC<ResetPasswordModalProps> = ({
|
||||||
|
visible,
|
||||||
|
user,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const t = useLocale(locale);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
form.resetFields();
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [visible, form]);
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
const displayName = user?.name || user?.nickname || user?.username || '';
|
||||||
|
if (displayName) {
|
||||||
|
return `${t['userManagement.resetPassword.title']} - ${displayName}`;
|
||||||
|
}
|
||||||
|
return t['userManagement.resetPassword.title'];
|
||||||
|
}, [t, user]);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
if (!user?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const values = await form.validate();
|
||||||
|
setSubmitting(true);
|
||||||
|
await resetUserPassword({
|
||||||
|
userId: user.id,
|
||||||
|
newPassword: values.newPassword,
|
||||||
|
});
|
||||||
|
Message.success(t['userManagement.resetPassword.success']);
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
onCancel();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (!error?.errorFields) {
|
||||||
|
console.error('重置密码失败:', error);
|
||||||
|
Message.error('重置密码失败,请稍后再试');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={title}
|
||||||
|
visible={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
confirmLoading={submitting}
|
||||||
|
onCancel={onCancel}
|
||||||
|
unmountOnExit
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
field="newPassword"
|
||||||
|
label={t['userManagement.resetPassword.newPassword']}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{
|
||||||
|
match: PASSWORD_PATTERN,
|
||||||
|
message: '密码需8-20位且包含大小写字母与数字',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="confirmPassword"
|
||||||
|
label={t['userManagement.resetPassword.confirmPassword']}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请再次输入新密码' },
|
||||||
|
{
|
||||||
|
validator: (value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback('请再次输入新密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value !== form.getFieldValue('newPassword')) {
|
||||||
|
callback('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请再次输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordModal;
|
||||||
269
src/pages/user-management/UserFormModal.tsx
Normal file
269
src/pages/user-management/UserFormModal.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
/**
|
||||||
|
* 用户表单弹窗组件
|
||||||
|
* 支持新建和编辑用户
|
||||||
|
*/
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Modal, Form, Input, Select, Message } from '@arco-design/web-react';
|
||||||
|
import { createUser, updateUser } from '@/api/user';
|
||||||
|
import { SUCCESS_CODE } from '@/constants';
|
||||||
|
import type { UserResp } from '@/types/user';
|
||||||
|
import styles from './style/modal.module.less';
|
||||||
|
|
||||||
|
const { useForm } = Form;
|
||||||
|
|
||||||
|
interface UserFormModalProps {
|
||||||
|
/** 是否显示弹窗 */
|
||||||
|
visible: boolean;
|
||||||
|
/** 关闭弹窗回调 */
|
||||||
|
onCancel: () => void;
|
||||||
|
/** 提交成功回调 */
|
||||||
|
onSuccess: () => void;
|
||||||
|
/** 编辑模式:传入用户数据,新建模式:undefined */
|
||||||
|
editData?: UserResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserFormModal({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
editData,
|
||||||
|
}: UserFormModalProps) {
|
||||||
|
const [form] = useForm();
|
||||||
|
const isEdit = !!editData; // 是否为编辑模式
|
||||||
|
|
||||||
|
// 角色选项
|
||||||
|
const roleOptions = [
|
||||||
|
{ label: '管理员', value: 'ADMIN' },
|
||||||
|
{ label: '普通用户', value: 'USER' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '禁用', value: 0 },
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 编辑模式:回填表单数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && editData) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
username: editData.username,
|
||||||
|
role: editData.role,
|
||||||
|
email: editData.email,
|
||||||
|
phone: editData.phone,
|
||||||
|
nickname: editData.nickname,
|
||||||
|
status: editData.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [visible, editData, form]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handleEsc = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleEsc);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleEsc);
|
||||||
|
};
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
// 表单验证
|
||||||
|
const values = await form.validate();
|
||||||
|
|
||||||
|
// 如果是编辑模式且没有填写密码,则删除密码字段
|
||||||
|
if (isEdit && !values.password) {
|
||||||
|
delete values.password;
|
||||||
|
delete values.confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
// 编辑用户
|
||||||
|
const result = await updateUser(editData.id, {
|
||||||
|
username: values.username,
|
||||||
|
role: values.role,
|
||||||
|
password: values.password,
|
||||||
|
email: values.email,
|
||||||
|
phone: values.phone,
|
||||||
|
nickname: values.nickname,
|
||||||
|
status: values.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
Message.success('用户更新成功');
|
||||||
|
onSuccess();
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新建用户
|
||||||
|
const result = await createUser({
|
||||||
|
username: values.username,
|
||||||
|
role: values.role,
|
||||||
|
password: values.password,
|
||||||
|
email: values.email,
|
||||||
|
phone: values.phone,
|
||||||
|
nickname: values.nickname,
|
||||||
|
status: values.status ?? 1, // 默认启用
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
Message.success('用户创建成功');
|
||||||
|
onSuccess();
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
// 表单验证失败或 API 调用失败(API 错误已经在拦截器中提示)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消/关闭
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={isEdit ? '编辑用户' : '新建用户'}
|
||||||
|
visible={visible}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
autoFocus={false}
|
||||||
|
focusLock={true}
|
||||||
|
maskClosable={false}
|
||||||
|
style={{ width: 720 }}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
autoComplete="off"
|
||||||
|
className={styles['form-grid']}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="用户名"
|
||||||
|
field="username"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{
|
||||||
|
pattern: /^[a-zA-Z0-9_]{4,20}$/,
|
||||||
|
message: '用户名必须是4-20位字母、数字或下划线',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
className={styles['full-line']}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入用户名(4-20位字母、数字或下划线)"
|
||||||
|
disabled={isEdit} // 编辑模式不允许修改用户名
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="昵称"
|
||||||
|
field="nickname"
|
||||||
|
rules={[{ required: true, message: '请输入昵称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入昵称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="角色"
|
||||||
|
field="role"
|
||||||
|
rules={[{ required: true, message: '请选择角色' }]}
|
||||||
|
initialValue="USER"
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择角色" options={roleOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{!isEdit && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="密码"
|
||||||
|
field="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入密码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern:
|
||||||
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,20}$/,
|
||||||
|
message: '密码必须是8-20位,且包含大小写字母和数字',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入密码(8-20位,包含大小写字母和数字)" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="确认密码"
|
||||||
|
field="confirmPassword"
|
||||||
|
dependencies={['password']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请确认密码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (value, callback) => {
|
||||||
|
const password = form.getFieldValue('password');
|
||||||
|
if (password && value !== password) {
|
||||||
|
callback('两次密码输入不一致');
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请再次输入密码" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="邮箱"
|
||||||
|
field="email"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: 'email',
|
||||||
|
message: '请输入正确的邮箱格式',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入邮箱" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="手机号"
|
||||||
|
field="phone"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
|
message: '请输入正确的手机号格式',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入手机号" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="状态"
|
||||||
|
field="status"
|
||||||
|
rules={[{ required: true, message: '请选择状态' }]}
|
||||||
|
initialValue={1}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择状态" options={statusOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,43 +1,79 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Typography, Badge, Space } from '@arco-design/web-react';
|
import {
|
||||||
import { IconEye, IconEdit, IconDelete } from '@arco-design/web-react/icon';
|
Button,
|
||||||
import dayjs from 'dayjs';
|
Typography,
|
||||||
|
Space,
|
||||||
|
Avatar,
|
||||||
|
Tag,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
|
import {
|
||||||
|
IconEye,
|
||||||
|
IconEdit,
|
||||||
|
IconDelete,
|
||||||
|
IconMoreVertical,
|
||||||
|
IconLock,
|
||||||
|
} from '@arco-design/web-react/icon';
|
||||||
|
import styles from './style/index.module.less';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const RoleType = ['管理员', '普通用户', '访客'];
|
export const RoleType = ['管理员', '普通用户', '访客'];
|
||||||
export const Status = ['禁用', '启用'];
|
export const Status = ['禁用', '启用'];
|
||||||
|
|
||||||
|
const getInitial = (name?: string, username?: string) => {
|
||||||
|
const base = name || username || '-';
|
||||||
|
return base.substring(0, 1).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
export function getColumns(
|
export function getColumns(
|
||||||
t: any,
|
t: any,
|
||||||
callback: (record: Record<string, any>, type: string) => Promise<void>
|
callback: (record: Record<string, any>, type: string) => Promise<void>,
|
||||||
|
pagination?: { current?: number; pageSize?: number }
|
||||||
) {
|
) {
|
||||||
|
const current = pagination?.current || 1;
|
||||||
|
const pageSize = pagination?.pageSize || 10;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.id'],
|
title: '#',
|
||||||
dataIndex: 'id',
|
dataIndex: 'index',
|
||||||
width: 100,
|
width: 70,
|
||||||
render: (value) => <Text copyable>{value}</Text>,
|
render: (_, __, index) => (current - 1) * pageSize + index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.name'],
|
title: t['userManagement.columns.name'],
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
width: 100,
|
width: 180,
|
||||||
},
|
render: (value, record) => (
|
||||||
{
|
<div className={styles['user-cell']}>
|
||||||
title: t['userManagement.columns.username'],
|
<Avatar
|
||||||
dataIndex: 'username',
|
size={32}
|
||||||
width: 120,
|
style={{ backgroundColor: '#165DFF1A', color: '#165DFF' }}
|
||||||
|
>
|
||||||
|
{record.avatar ? (
|
||||||
|
<img src={record.avatar} alt="avatar" />
|
||||||
|
) : (
|
||||||
|
getInitial(value, record.username)
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<div className={styles['user-name']}>{value || '-'}</div>
|
||||||
|
<div className={styles['user-sub']}>{record.username}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.email'],
|
title: t['userManagement.columns.email'],
|
||||||
dataIndex: 'email',
|
dataIndex: 'email',
|
||||||
width: 200,
|
width: 220,
|
||||||
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.phone'],
|
title: t['userManagement.columns.phone'],
|
||||||
dataIndex: 'phone',
|
dataIndex: 'phone',
|
||||||
width: 130,
|
width: 140,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.role'],
|
title: t['userManagement.columns.role'],
|
||||||
@ -50,20 +86,24 @@ export function getColumns(
|
|||||||
2: { text: t['userManagement.role.guest'], color: 'gray' },
|
2: { text: t['userManagement.role.guest'], color: 'gray' },
|
||||||
};
|
};
|
||||||
const role = roleMap[value] || roleMap[1];
|
const role = roleMap[value] || roleMap[1];
|
||||||
return <Badge color={role.color} text={role.text} />;
|
return (
|
||||||
|
<Tag size="small" className={styles['role-tag']} color={role.color}>
|
||||||
|
{role.text}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t['userManagement.columns.department'],
|
|
||||||
dataIndex: 'department',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.createdTime'],
|
title: t['userManagement.columns.createdTime'],
|
||||||
dataIndex: 'createdTime',
|
dataIndex: 'createdTime',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (x) => dayjs().subtract(x, 'days').format('YYYY-MM-DD HH:mm:ss'),
|
render: (x) => x || '-',
|
||||||
sorter: (a, b) => b.createdTime - a.createdTime,
|
sorter: (a, b) => {
|
||||||
|
// 按时间字符串排序
|
||||||
|
const timeA = new Date(a.createdTime || 0).getTime();
|
||||||
|
const timeB = new Date(b.createdTime || 0).getTime();
|
||||||
|
return timeB - timeA;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.status'],
|
title: t['userManagement.columns.status'],
|
||||||
@ -71,45 +111,83 @@ export function getColumns(
|
|||||||
width: 100,
|
width: 100,
|
||||||
render: (x) => {
|
render: (x) => {
|
||||||
if (x === 0) {
|
if (x === 0) {
|
||||||
return <Badge status="error" text={Status[x]}></Badge>;
|
return (
|
||||||
|
<div className={styles['status-cell']}>
|
||||||
|
<span
|
||||||
|
className={`${styles['status-dot']} ${styles['is-offline']}`}
|
||||||
|
/>
|
||||||
|
{Status[x]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return <Badge status="success" text={Status[x]}></Badge>;
|
return (
|
||||||
|
<div className={styles['status-cell']}>
|
||||||
|
<span
|
||||||
|
className={`${styles['status-dot']} ${styles['is-online']}`}
|
||||||
|
/>
|
||||||
|
{Status[x]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t['userManagement.columns.operations'],
|
title: t['userManagement.columns.operations'],
|
||||||
dataIndex: 'operations',
|
dataIndex: 'operations',
|
||||||
width: 240,
|
width: 200,
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
render: (_, record) => (
|
render: (_, record) => {
|
||||||
<Space>
|
// 超级管理员不显示操作按钮
|
||||||
<Button
|
if (record.role === 0) {
|
||||||
type="text"
|
return <span style={{ color: '#86909c' }}>-</span>;
|
||||||
size="small"
|
}
|
||||||
icon={<IconEye />}
|
|
||||||
onClick={() => callback(record, 'view')}
|
return (
|
||||||
>
|
<Space size={8} align="center">
|
||||||
{t['userManagement.columns.operations.view']}
|
<Button
|
||||||
</Button>
|
type="text"
|
||||||
<Button
|
size="mini"
|
||||||
type="text"
|
onClick={() => callback(record, 'view')}
|
||||||
size="small"
|
>
|
||||||
icon={<IconEdit />}
|
<IconEye /> {t['userManagement.columns.operations.view']}
|
||||||
onClick={() => callback(record, 'edit')}
|
</Button>
|
||||||
>
|
<Button
|
||||||
{t['userManagement.columns.operations.edit']}
|
type="text"
|
||||||
</Button>
|
size="mini"
|
||||||
<Button
|
onClick={() => callback(record, 'edit')}
|
||||||
type="text"
|
>
|
||||||
size="small"
|
<IconEdit /> {t['userManagement.columns.operations.edit']}
|
||||||
status="danger"
|
</Button>
|
||||||
icon={<IconDelete />}
|
<Dropdown
|
||||||
onClick={() => callback(record, 'delete')}
|
trigger="click"
|
||||||
>
|
droplist={
|
||||||
{t['userManagement.columns.operations.delete']}
|
<Menu>
|
||||||
</Button>
|
<Menu.Item onClick={() => callback(record, 'resetPassword')}>
|
||||||
</Space>
|
<Space size={6} align="center">
|
||||||
),
|
<IconLock />
|
||||||
|
<span>
|
||||||
|
{t['userManagement.columns.operations.resetPassword']}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => callback(record, 'delete')}
|
||||||
|
style={{ color: '#f53f3f' }}
|
||||||
|
>
|
||||||
|
<Space size={6} align="center">
|
||||||
|
<IconDelete />
|
||||||
|
<span>
|
||||||
|
{t['userManagement.columns.operations.delete']}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button type="text" size="mini" icon={<IconMoreVertical />} />
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
import React, { useContext } from 'react';
|
import React from 'react';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
DatePicker,
|
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
|
Space,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import { GlobalContext } from '@/context';
|
import {
|
||||||
|
IconRefresh,
|
||||||
|
IconSearch,
|
||||||
|
IconDown,
|
||||||
|
IconUp,
|
||||||
|
} from '@arco-design/web-react/icon';
|
||||||
import locale from './locale';
|
import locale from './locale';
|
||||||
import useLocale from '@/utils/useLocale';
|
import useLocale from '@/utils/useLocale';
|
||||||
import { IconRefresh, IconSearch } from '@arco-design/web-react/icon';
|
import { Status } from './constants';
|
||||||
import { RoleType, Status } from './constants';
|
|
||||||
import styles from './style/index.module.less';
|
import styles from './style/index.module.less';
|
||||||
|
|
||||||
const { Row, Col } = Grid;
|
const { Row, Col } = Grid;
|
||||||
const { useForm } = Form;
|
const { useForm } = Form;
|
||||||
|
|
||||||
function SearchForm(props: {
|
interface SearchFormProps {
|
||||||
onSearch: (values: Record<string, any>) => void;
|
onSearch: (values: Record<string, any>) => void;
|
||||||
}) {
|
collapsed: boolean;
|
||||||
const { lang } = useContext(GlobalContext);
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchForm(props: SearchFormProps) {
|
||||||
const t = useLocale(locale);
|
const t = useLocale(locale);
|
||||||
const [form] = useForm();
|
const [form] = useForm();
|
||||||
|
|
||||||
@ -36,35 +41,11 @@ function SearchForm(props: {
|
|||||||
props.onSearch({});
|
props.onSearch({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const colSpan = lang === 'zh-CN' ? 8 : 12;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles['search-form-wrapper']}>
|
<div className={styles['search-form-wrapper']}>
|
||||||
<Form
|
<Form form={form} className={styles['search-form']} layout="vertical">
|
||||||
form={form}
|
<Row gutter={16}>
|
||||||
className={styles['search-form']}
|
<Col xs={12} sm={8} md={6} lg={6} xl={6}>
|
||||||
labelAlign="left"
|
|
||||||
labelCol={{ span: 5 }}
|
|
||||||
wrapperCol={{ span: 19 }}
|
|
||||||
>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item label={t['userManagement.columns.id']} field="id">
|
|
||||||
<Input
|
|
||||||
placeholder={t['userManagement.form.id.placeholder']}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item label={t['userManagement.columns.name']} field="name">
|
|
||||||
<Input
|
|
||||||
allowClear
|
|
||||||
placeholder={t['userManagement.form.name.placeholder']}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t['userManagement.columns.username']}
|
label={t['userManagement.columns.username']}
|
||||||
field="username"
|
field="username"
|
||||||
@ -75,7 +56,37 @@ function SearchForm(props: {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={colSpan}>
|
<Col xs={12} sm={8} md={6} lg={6} xl={6}>
|
||||||
|
<Form.Item label={t['userManagement.columns.name']} field="name">
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
placeholder={t['userManagement.form.name.placeholder']}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={6} lg={6} xl={6}>
|
||||||
|
<Form.Item
|
||||||
|
label={t['userManagement.columns.status']}
|
||||||
|
field="status"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder={t['userManagement.form.all.placeholder']}
|
||||||
|
options={Status.map((item, index) => ({
|
||||||
|
label: item,
|
||||||
|
value: index,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
xs={12}
|
||||||
|
sm={8}
|
||||||
|
md={6}
|
||||||
|
lg={6}
|
||||||
|
xl={6}
|
||||||
|
className={props.collapsed ? styles['field-hidden'] : ''}
|
||||||
|
>
|
||||||
<Form.Item label={t['userManagement.columns.email']} field="email">
|
<Form.Item label={t['userManagement.columns.email']} field="email">
|
||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
@ -83,7 +94,14 @@ function SearchForm(props: {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={colSpan}>
|
<Col
|
||||||
|
xs={12}
|
||||||
|
sm={8}
|
||||||
|
md={6}
|
||||||
|
lg={6}
|
||||||
|
xl={6}
|
||||||
|
className={props.collapsed ? styles['field-hidden'] : ''}
|
||||||
|
>
|
||||||
<Form.Item label={t['userManagement.columns.phone']} field="phone">
|
<Form.Item label={t['userManagement.columns.phone']} field="phone">
|
||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
@ -91,68 +109,27 @@ function SearchForm(props: {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item label={t['userManagement.columns.role']} field="role">
|
|
||||||
<Select
|
|
||||||
placeholder={t['userManagement.form.all.placeholder']}
|
|
||||||
options={RoleType.map((item, index) => ({
|
|
||||||
label: item,
|
|
||||||
value: index,
|
|
||||||
}))}
|
|
||||||
mode="multiple"
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item
|
|
||||||
label={t['userManagement.columns.department']}
|
|
||||||
field="department"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
allowClear
|
|
||||||
placeholder={t['userManagement.form.department.placeholder']}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item
|
|
||||||
label={t['userManagement.columns.status']}
|
|
||||||
field="status"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder={t['userManagement.form.all.placeholder']}
|
|
||||||
options={Status.map((item, index) => ({
|
|
||||||
label: item,
|
|
||||||
value: index,
|
|
||||||
}))}
|
|
||||||
mode="multiple"
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={colSpan}>
|
|
||||||
<Form.Item
|
|
||||||
label={t['userManagement.columns.createdTime']}
|
|
||||||
field="createdTime"
|
|
||||||
>
|
|
||||||
<DatePicker.RangePicker
|
|
||||||
allowClear
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
disabledDate={(date) => dayjs(date).isAfter(dayjs())}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
<div className={styles['search-actions']}>
|
||||||
|
<Space size={12}>
|
||||||
|
<Button type="primary" icon={<IconSearch />} onClick={handleSubmit}>
|
||||||
|
{t['userManagement.form.search']}
|
||||||
|
</Button>
|
||||||
|
<Button icon={<IconRefresh />} onClick={handleReset}>
|
||||||
|
{t['userManagement.form.reset']}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={props.collapsed ? <IconDown /> : <IconUp />}
|
||||||
|
onClick={props.onToggle}
|
||||||
|
>
|
||||||
|
{props.collapsed
|
||||||
|
? t['userManagement.form.expand']
|
||||||
|
: t['userManagement.form.collapse']}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
<div className={styles['right-button']}>
|
|
||||||
<Button type="primary" icon={<IconSearch />} onClick={handleSubmit}>
|
|
||||||
{t['userManagement.form.search']}
|
|
||||||
</Button>
|
|
||||||
<Button icon={<IconRefresh />} onClick={handleReset}>
|
|
||||||
{t['userManagement.form.reset']}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,162 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Card,
|
|
||||||
PaginationProps,
|
PaginationProps,
|
||||||
Button,
|
Button,
|
||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
Message,
|
Message,
|
||||||
|
Modal,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import PermissionWrapper from '@/components/PermissionWrapper';
|
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
|
import ColumnSetting from '@/components/ColumnSetting';
|
||||||
|
import { IconPlus, IconDelete, IconRefresh } from '@arco-design/web-react/icon';
|
||||||
import {
|
import {
|
||||||
IconDownload,
|
getUserPage,
|
||||||
IconPlus,
|
deleteUser,
|
||||||
IconDelete,
|
batchDeleteUsers,
|
||||||
} from '@arco-design/web-react/icon';
|
exportUsers,
|
||||||
import axios from 'axios';
|
getUserDetail,
|
||||||
|
} from '@/api/user';
|
||||||
|
import { SUCCESS_CODE } from '@/constants';
|
||||||
|
import type { UserResp } from '@/types/user';
|
||||||
import useLocale from '@/utils/useLocale';
|
import useLocale from '@/utils/useLocale';
|
||||||
import SearchForm from './form';
|
import SearchForm from './form';
|
||||||
|
import UserFormModal from './UserFormModal';
|
||||||
|
import ResetPasswordModal from './ResetPasswordModal';
|
||||||
import locale from './locale';
|
import locale from './locale';
|
||||||
import styles from './style/index.module.less';
|
import styles from './style/index.module.less';
|
||||||
import './mock';
|
|
||||||
import { getColumns } from './constants';
|
import { getColumns } from './constants';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const pad = (num: number) => `${num}`.padStart(2, '0');
|
||||||
|
const formatDateTime = (value?: string | number | Date | null) => {
|
||||||
|
if (!value) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
|
||||||
|
date.getDate()
|
||||||
|
)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
|
||||||
|
date.getSeconds()
|
||||||
|
)}`;
|
||||||
|
};
|
||||||
|
|
||||||
function UserManagement() {
|
function UserManagement() {
|
||||||
const t = useLocale(locale);
|
const t = useLocale(locale);
|
||||||
|
const [filterCollapsed, setFilterCollapsed] = useState(true);
|
||||||
|
const COLUMN_STORAGE_KEY = 'user-management-visible-columns';
|
||||||
|
|
||||||
|
const [resetModalVisible, setResetModalVisible] = useState(false);
|
||||||
|
const [resetUserData, setResetUserData] = useState<
|
||||||
|
(UserResp & { name?: string }) | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const tableCallback = async (record, type) => {
|
const tableCallback = async (record, type) => {
|
||||||
console.log(record, type);
|
|
||||||
|
|
||||||
// 简单的操作提示
|
|
||||||
if (type === 'view') {
|
if (type === 'view') {
|
||||||
Message.info(`查看用户: ${record.name}`);
|
// 查看用户详情
|
||||||
|
try {
|
||||||
|
const result = await getUserDetail(record.id);
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
const detail = result.data;
|
||||||
|
const infoList = [
|
||||||
|
{ label: '用户ID', value: detail.id || '-' },
|
||||||
|
{ label: '角色', value: detail.role || '-' },
|
||||||
|
{ label: '邮箱', value: detail.email || '-' },
|
||||||
|
{ label: '手机号', value: detail.phone || '-' },
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
value: detail.status === 1 ? '正常' : '禁用',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
value: formatDateTime(detail.createTime),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
Modal.info({
|
||||||
|
title: null,
|
||||||
|
icon: null,
|
||||||
|
okText: '确定',
|
||||||
|
style: { width: 720 },
|
||||||
|
content: (
|
||||||
|
<div className={styles['detail-modal']}>
|
||||||
|
<div className={styles['detail-header']}>
|
||||||
|
<div className={styles['detail-avatar']}>
|
||||||
|
{(detail.nickname || detail.username || 'U')
|
||||||
|
.slice(0, 1)
|
||||||
|
.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className={styles['detail-names']}>
|
||||||
|
<div className={styles['detail-name']}>
|
||||||
|
{detail.nickname || detail.username || '-'}
|
||||||
|
</div>
|
||||||
|
<div className={styles['detail-sub']}>
|
||||||
|
{detail.username || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles['detail-status']} ${
|
||||||
|
detail.status === 1
|
||||||
|
? styles['status-active']
|
||||||
|
: styles['status-disabled']
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{detail.status === 1 ? '状态 · 正常' : '状态 · 禁用'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles['detail-grid']}>
|
||||||
|
{infoList.map((item) => (
|
||||||
|
<div className={styles['detail-item']} key={item.label}>
|
||||||
|
<div className={styles['item-label']}>{item.label}</div>
|
||||||
|
<div className={styles['item-value']}>{item.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户详情失败:', error);
|
||||||
|
}
|
||||||
} else if (type === 'edit') {
|
} else if (type === 'edit') {
|
||||||
Message.info(`编辑用户: ${record.name}`);
|
// 编辑用户
|
||||||
|
try {
|
||||||
|
const result = await getUserDetail(record.id);
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
setEditUserData(result.data);
|
||||||
|
setModalVisible(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户详情失败:', error);
|
||||||
|
}
|
||||||
|
} else if (type === 'resetPassword') {
|
||||||
|
setResetUserData(record);
|
||||||
|
setResetModalVisible(true);
|
||||||
} else if (type === 'delete') {
|
} else if (type === 'delete') {
|
||||||
Message.warning(`删除用户: ${record.name}`);
|
// 删除用户
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除用户 "${record.name}" 吗?此操作不可恢复。`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const result = await deleteUser(record.id);
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
Message.success('删除成功');
|
||||||
|
fetchData(); // 刷新列表
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (type === 'enable' || type === 'disable') {
|
||||||
|
Message.info('状态切换功能将与后端联调后生效');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = useMemo(() => getColumns(t, tableCallback), [t]);
|
|
||||||
|
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [pagination, setPagination] = useState<PaginationProps>({
|
const [pagination, setPagination] = useState<PaginationProps>({
|
||||||
sizeCanChange: true,
|
sizeCanChange: true,
|
||||||
@ -54,35 +168,103 @@ function UserManagement() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [formParams, setFormParams] = useState({});
|
const [formParams, setFormParams] = useState({});
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState<string[]>([]);
|
||||||
|
const [columnSettingVisible, setColumnSettingVisible] = useState(false);
|
||||||
|
|
||||||
|
// 弹窗状态
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editUserData, setEditUserData] = useState<UserResp | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseColumns = useMemo(
|
||||||
|
() => getColumns(t, tableCallback, pagination),
|
||||||
|
[t, pagination]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
if (!visibleColumns.length) {
|
||||||
|
return baseColumns;
|
||||||
|
}
|
||||||
|
const visibleSet = new Set(visibleColumns);
|
||||||
|
return baseColumns.filter((col) => visibleSet.has(col.dataIndex));
|
||||||
|
}, [baseColumns, visibleColumns]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [pagination.current, pagination.pageSize, JSON.stringify(formParams)]);
|
}, [pagination.current, pagination.pageSize, JSON.stringify(formParams)]);
|
||||||
|
|
||||||
function fetchData() {
|
useEffect(() => {
|
||||||
|
if (!visibleColumns.length && baseColumns.length) {
|
||||||
|
const cached = localStorage.getItem(COLUMN_STORAGE_KEY);
|
||||||
|
if (cached) {
|
||||||
|
const parsed = JSON.parse(cached);
|
||||||
|
if (Array.isArray(parsed) && parsed.length) {
|
||||||
|
setVisibleColumns(parsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setVisibleColumns(baseColumns.map((col) => col.dataIndex));
|
||||||
|
}
|
||||||
|
}, [baseColumns, visibleColumns.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visibleColumns.length) {
|
||||||
|
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns));
|
||||||
|
}
|
||||||
|
}, [visibleColumns]);
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
const { current, pageSize } = pagination;
|
const { current, pageSize } = pagination;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
axios
|
|
||||||
.get('/api/user-management', {
|
try {
|
||||||
params: {
|
// 调用后端 API
|
||||||
page: current,
|
const result = await getUserPage({
|
||||||
pageSize,
|
current: current,
|
||||||
...formParams,
|
size: pageSize,
|
||||||
},
|
...formParams,
|
||||||
})
|
});
|
||||||
.then((res) => {
|
|
||||||
setData(res.data.list);
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
// 后端返回的字段是 list,不是 records
|
||||||
|
const { list, total } = result.data;
|
||||||
|
|
||||||
|
// 字段映射:后端字段 -> 前端字段
|
||||||
|
const mappedData = (list || []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.nickname,
|
||||||
|
username: item.username,
|
||||||
|
email: item.email,
|
||||||
|
phone: item.phone,
|
||||||
|
role: mapRoleToNumber(item.role),
|
||||||
|
createdTime: item.createTime,
|
||||||
|
status: item.status,
|
||||||
|
avatar: item.avatar,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setData(mappedData);
|
||||||
setPagination({
|
setPagination({
|
||||||
...pagination,
|
...pagination,
|
||||||
current,
|
current,
|
||||||
pageSize,
|
pageSize,
|
||||||
total: res.data.total,
|
total,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
console.error('查询用户列表失败:', error);
|
||||||
setLoading(false);
|
} finally {
|
||||||
});
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色映射:字符串 -> 数字
|
||||||
|
function mapRoleToNumber(role: string): number {
|
||||||
|
const roleMap = {
|
||||||
|
ADMIN: 0,
|
||||||
|
USER: 1,
|
||||||
|
};
|
||||||
|
return roleMap[role] ?? 1; // 默认为普通用户
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeTable(current: number, pageSize: number) {
|
function onChangeTable(current: number, pageSize: number) {
|
||||||
@ -94,77 +276,199 @@ function UserManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch(params) {
|
function handleSearch(params) {
|
||||||
|
// 处理搜索参数:类型转换、字段映射、移除空值
|
||||||
|
const processedParams: any = {};
|
||||||
|
|
||||||
|
// 用户名:直接传递
|
||||||
|
if (params.username) {
|
||||||
|
processedParams.username = params.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 昵称:前端 name -> 后端 nickname
|
||||||
|
if (params.name) {
|
||||||
|
processedParams.nickname = params.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邮箱:直接传递
|
||||||
|
if (params.email) {
|
||||||
|
processedParams.email = params.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号:直接传递
|
||||||
|
if (params.phone) {
|
||||||
|
processedParams.phone = params.phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态:直接传递(现在是单选,不是多选)
|
||||||
|
if (params.status !== undefined && params.status !== null) {
|
||||||
|
processedParams.status = params.status;
|
||||||
|
}
|
||||||
|
|
||||||
setPagination({ ...pagination, current: 1 });
|
setPagination({ ...pagination, current: 1 });
|
||||||
setFormParams(params);
|
setFormParams(processedParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleImport() {
|
||||||
|
Message.info('导入功能待接入后端接口');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToolbarRefresh() {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建用户
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
Message.info(t['userManagement.operations.add']);
|
setEditUserData(undefined); // 清空编辑数据
|
||||||
|
setModalVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
function handleBatchDelete() {
|
function handleBatchDelete() {
|
||||||
if (selectedRowKeys.length === 0) {
|
if (selectedRowKeys.length === 0) {
|
||||||
Message.warning('请选择要删除的用户');
|
Message.warning('请选择要删除的用户');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Message.warning(`批量删除 ${selectedRowKeys.length} 个用户`);
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: '批量删除确认',
|
||||||
|
content: `确定要删除选中的 ${selectedRowKeys.length} 个用户吗?此操作不可恢复。`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const result = await batchDeleteUsers(selectedRowKeys as number[]);
|
||||||
|
if (result.code == SUCCESS_CODE) {
|
||||||
|
Message.success('批量删除成功');
|
||||||
|
setSelectedRowKeys([]); // 清空选择
|
||||||
|
fetchData(); // 刷新列表
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExport() {
|
// 导出用户
|
||||||
Message.info(t['userManagement.operations.export']);
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
const response: any = await exportUsers(formParams);
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', `用户列表_${new Date().getTime()}.xlsx`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
Message.success('导出成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出失败:', error);
|
||||||
|
Message.error('导出失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗关闭
|
||||||
|
function handleModalCancel() {
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditUserData(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗提交成功
|
||||||
|
function handleModalSuccess() {
|
||||||
|
fetchData(); // 刷新列表
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResetModalClose() {
|
||||||
|
setResetModalVisible(false);
|
||||||
|
setResetUserData(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className={styles['page']}>
|
||||||
<Title heading={6}>{t['menu.list.userManagement']}</Title>
|
<div className={styles['filters-card']}>
|
||||||
<SearchForm onSearch={handleSearch} />
|
<div className={styles['filters-header']}>
|
||||||
<PermissionWrapper
|
<div>
|
||||||
requiredPermissions={[
|
<Title heading={5}>{t['menu.list.userManagement']}</Title>
|
||||||
{ resource: 'menu.list.userManagement', actions: ['write'] },
|
<Paragraph type="secondary" className={styles['page-desc']}>
|
||||||
]}
|
{t['userManagement.page.desc']}
|
||||||
>
|
</Paragraph>
|
||||||
<div className={styles['button-group']}>
|
</div>
|
||||||
<Space>
|
</div>
|
||||||
<Button type="primary" icon={<IconPlus />} onClick={handleAdd}>
|
<SearchForm
|
||||||
{t['userManagement.operations.add']}
|
onSearch={handleSearch}
|
||||||
</Button>
|
collapsed={filterCollapsed}
|
||||||
|
onToggle={() => setFilterCollapsed(!filterCollapsed)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles['table-card']}>
|
||||||
|
<div className={styles['table-head']}>
|
||||||
|
<Button type="primary" icon={<IconPlus />} onClick={handleAdd}>
|
||||||
|
{t['userManagement.operations.add']}
|
||||||
|
</Button>
|
||||||
|
<Space size={10}>
|
||||||
<Button icon={<IconDelete />} onClick={handleBatchDelete}>
|
<Button icon={<IconDelete />} onClick={handleBatchDelete}>
|
||||||
{t['userManagement.operations.batchDelete']}
|
{t['userManagement.operations.batchDelete']}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
<Button onClick={() => setColumnSettingVisible(true)}>
|
||||||
<Space>
|
列设置
|
||||||
<Button icon={<IconDownload />} onClick={handleExport}>
|
</Button>
|
||||||
{t['userManagement.operations.export']}
|
<Button icon={<IconRefresh />} onClick={handleToolbarRefresh}>
|
||||||
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</PermissionWrapper>
|
<Table
|
||||||
<Table
|
rowKey="id"
|
||||||
rowKey="id"
|
loading={loading}
|
||||||
loading={loading}
|
pagination={false}
|
||||||
pagination={false}
|
columns={columns}
|
||||||
columns={columns}
|
data={data}
|
||||||
data={data}
|
rowSelection={{
|
||||||
rowSelection={{
|
type: 'checkbox',
|
||||||
type: 'checkbox',
|
selectedRowKeys,
|
||||||
selectedRowKeys,
|
onChange: (keys) => {
|
||||||
onChange: (selectedRowKeys) => {
|
setSelectedRowKeys(keys);
|
||||||
setSelectedRowKeys(selectedRowKeys);
|
},
|
||||||
},
|
}}
|
||||||
}}
|
scroll={{ x: 1640 }}
|
||||||
scroll={{ x: 1640 }}
|
/>
|
||||||
|
<Pagination
|
||||||
|
total={pagination.total}
|
||||||
|
current={pagination.current}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
sizeCanChange={pagination.sizeCanChange}
|
||||||
|
onChange={onChangeTable}
|
||||||
|
onPageSizeChange={(pageSize) => {
|
||||||
|
setPagination({ ...pagination, current: 1, pageSize });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UserFormModal
|
||||||
|
visible={modalVisible}
|
||||||
|
onCancel={handleModalCancel}
|
||||||
|
onSuccess={handleModalSuccess}
|
||||||
|
editData={editUserData}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
<ColumnSetting
|
||||||
total={pagination.total}
|
visible={columnSettingVisible}
|
||||||
current={pagination.current}
|
columns={baseColumns.map((col) => ({
|
||||||
pageSize={pagination.pageSize}
|
key: col.dataIndex,
|
||||||
sizeCanChange={pagination.sizeCanChange}
|
title: col.title,
|
||||||
onChange={onChangeTable}
|
}))}
|
||||||
onPageSizeChange={(pageSize) => {
|
value={visibleColumns}
|
||||||
setPagination({ ...pagination, current: 1, pageSize });
|
onChange={setVisibleColumns}
|
||||||
}}
|
onClose={() => setColumnSettingVisible(false)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
<ResetPasswordModal
|
||||||
|
visible={resetModalVisible}
|
||||||
|
user={resetUserData}
|
||||||
|
onCancel={handleResetModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ const i18n = {
|
|||||||
'menu.list.userManagement': 'User Management',
|
'menu.list.userManagement': 'User Management',
|
||||||
'userManagement.form.search': 'Search',
|
'userManagement.form.search': 'Search',
|
||||||
'userManagement.form.reset': 'Reset',
|
'userManagement.form.reset': 'Reset',
|
||||||
|
'userManagement.form.collapse': 'Collapse filters',
|
||||||
|
'userManagement.form.expand': 'Expand filters',
|
||||||
'userManagement.columns.id': 'User ID',
|
'userManagement.columns.id': 'User ID',
|
||||||
'userManagement.columns.name': 'Name',
|
'userManagement.columns.name': 'Name',
|
||||||
'userManagement.columns.username': 'Username',
|
'userManagement.columns.username': 'Username',
|
||||||
@ -15,12 +17,15 @@ const i18n = {
|
|||||||
'userManagement.columns.operations': 'Operations',
|
'userManagement.columns.operations': 'Operations',
|
||||||
'userManagement.columns.operations.view': 'View',
|
'userManagement.columns.operations.view': 'View',
|
||||||
'userManagement.columns.operations.edit': 'Edit',
|
'userManagement.columns.operations.edit': 'Edit',
|
||||||
|
'userManagement.columns.operations.resetPassword': 'Reset Password',
|
||||||
'userManagement.columns.operations.delete': 'Delete',
|
'userManagement.columns.operations.delete': 'Delete',
|
||||||
'userManagement.columns.operations.enable': 'Enable',
|
'userManagement.columns.operations.enable': 'Enable',
|
||||||
'userManagement.columns.operations.disable': 'Disable',
|
'userManagement.columns.operations.disable': 'Disable',
|
||||||
'userManagement.operations.add': 'Add User',
|
'userManagement.operations.add': 'Add User',
|
||||||
'userManagement.operations.batchDelete': 'Batch Delete',
|
'userManagement.operations.batchDelete': 'Batch Delete',
|
||||||
'userManagement.operations.export': 'Export',
|
'userManagement.operations.export': 'Export',
|
||||||
|
'userManagement.operations.import': 'Import',
|
||||||
|
'userManagement.operations.batch': 'Batch Actions',
|
||||||
'userManagement.role.admin': 'Administrator',
|
'userManagement.role.admin': 'Administrator',
|
||||||
'userManagement.role.user': 'User',
|
'userManagement.role.user': 'User',
|
||||||
'userManagement.role.guest': 'Guest',
|
'userManagement.role.guest': 'Guest',
|
||||||
@ -33,11 +38,22 @@ const i18n = {
|
|||||||
'userManagement.form.phone.placeholder': 'Please enter phone',
|
'userManagement.form.phone.placeholder': 'Please enter phone',
|
||||||
'userManagement.form.department.placeholder': 'Please enter department',
|
'userManagement.form.department.placeholder': 'Please enter department',
|
||||||
'userManagement.form.all.placeholder': 'All',
|
'userManagement.form.all.placeholder': 'All',
|
||||||
|
'userManagement.page.desc':
|
||||||
|
'Manage platform users, permissions and lifecycle.',
|
||||||
|
'userManagement.stat.total': 'Total Users',
|
||||||
|
'userManagement.stat.enabled': 'Enabled',
|
||||||
|
'userManagement.stat.disabled': 'Disabled',
|
||||||
|
'userManagement.resetPassword.title': 'Reset Password',
|
||||||
|
'userManagement.resetPassword.newPassword': 'New Password',
|
||||||
|
'userManagement.resetPassword.confirmPassword': 'Confirm Password',
|
||||||
|
'userManagement.resetPassword.success': 'Password reset successfully',
|
||||||
},
|
},
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
'menu.list.userManagement': '用户管理',
|
'menu.list.userManagement': '用户管理',
|
||||||
'userManagement.form.search': '查询',
|
'userManagement.form.search': '查询',
|
||||||
'userManagement.form.reset': '重置',
|
'userManagement.form.reset': '重置',
|
||||||
|
'userManagement.form.collapse': '收起筛选',
|
||||||
|
'userManagement.form.expand': '展开筛选',
|
||||||
'userManagement.columns.id': '用户ID',
|
'userManagement.columns.id': '用户ID',
|
||||||
'userManagement.columns.name': '姓名',
|
'userManagement.columns.name': '姓名',
|
||||||
'userManagement.columns.username': '用户名',
|
'userManagement.columns.username': '用户名',
|
||||||
@ -50,12 +66,15 @@ const i18n = {
|
|||||||
'userManagement.columns.operations': '操作',
|
'userManagement.columns.operations': '操作',
|
||||||
'userManagement.columns.operations.view': '查看',
|
'userManagement.columns.operations.view': '查看',
|
||||||
'userManagement.columns.operations.edit': '编辑',
|
'userManagement.columns.operations.edit': '编辑',
|
||||||
|
'userManagement.columns.operations.resetPassword': '重置密码',
|
||||||
'userManagement.columns.operations.delete': '删除',
|
'userManagement.columns.operations.delete': '删除',
|
||||||
'userManagement.columns.operations.enable': '启用',
|
'userManagement.columns.operations.enable': '启用',
|
||||||
'userManagement.columns.operations.disable': '禁用',
|
'userManagement.columns.operations.disable': '禁用',
|
||||||
'userManagement.operations.add': '新建用户',
|
'userManagement.operations.add': '新建用户',
|
||||||
'userManagement.operations.batchDelete': '批量删除',
|
'userManagement.operations.batchDelete': '批量删除',
|
||||||
'userManagement.operations.export': '导出',
|
'userManagement.operations.export': '导出',
|
||||||
|
'userManagement.operations.import': '导入',
|
||||||
|
'userManagement.operations.batch': '批量操作',
|
||||||
'userManagement.role.admin': '管理员',
|
'userManagement.role.admin': '管理员',
|
||||||
'userManagement.role.user': '普通用户',
|
'userManagement.role.user': '普通用户',
|
||||||
'userManagement.role.guest': '访客',
|
'userManagement.role.guest': '访客',
|
||||||
@ -68,6 +87,14 @@ const i18n = {
|
|||||||
'userManagement.form.phone.placeholder': '请输入手机号',
|
'userManagement.form.phone.placeholder': '请输入手机号',
|
||||||
'userManagement.form.department.placeholder': '请输入部门',
|
'userManagement.form.department.placeholder': '请输入部门',
|
||||||
'userManagement.form.all.placeholder': '全部',
|
'userManagement.form.all.placeholder': '全部',
|
||||||
|
'userManagement.page.desc': '统一管理系统用户、权限及生命周期。',
|
||||||
|
'userManagement.stat.total': '用户总数',
|
||||||
|
'userManagement.stat.enabled': '启用',
|
||||||
|
'userManagement.stat.disabled': '禁用',
|
||||||
|
'userManagement.resetPassword.title': '重置用户密码',
|
||||||
|
'userManagement.resetPassword.newPassword': '新密码',
|
||||||
|
'userManagement.resetPassword.confirmPassword': '确认新密码',
|
||||||
|
'userManagement.resetPassword.success': '密码重置成功',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,41 +1,230 @@
|
|||||||
.toolbar {
|
.page {
|
||||||
display: flex;
|
background: #f5f6f8;
|
||||||
justify-content: space-between;
|
padding: 0 4px 16px;
|
||||||
margin-bottom: 24px;
|
min-height: calc(100vh - 120px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.operations {
|
.filters-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
padding: 18px 20px;
|
||||||
|
box-shadow: none;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-desc {
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form-wrapper {
|
.search-form-wrapper {
|
||||||
display: flex;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--color-border-1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.right-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-left: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-left: 1px solid var(--color-border-2);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
padding-right: 20px;
|
:global(.arco-form-item-label) {
|
||||||
|
color: #4e5969;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.arco-form-label-item-left) {
|
:global(.arco-form-item) {
|
||||||
> label {
|
margin-bottom: 12px;
|
||||||
white-space: nowrap;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
border-top: 1px dashed #e5e6eb;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.arco-btn-text) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-tag {
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border: 1px solid rgba(22, 93, 255, 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-online {
|
||||||
|
background: #1dc779;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-offline {
|
||||||
|
background: #f77234;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-modal {
|
||||||
|
min-width: 620px;
|
||||||
|
max-width: 680px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 8px 20px;
|
||||||
|
border-bottom: 1px solid #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #165dff;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 6px 16px rgba(22, 93, 255, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-names {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-sub {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #86909c;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-status {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background: rgba(29, 199, 121, 16%);
|
||||||
|
color: #00a36a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disabled {
|
||||||
|
background: rgba(255, 125, 76, 16%);
|
||||||
|
color: #f86d39;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px 20px;
|
||||||
|
padding: 20px 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item:nth-last-child(-n + 2) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #98a2b3;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1d2129;
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.filters-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/pages/user-management/style/modal.module.less
Normal file
13
src/pages/user-management/style/modal.module.less
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px 20px;
|
||||||
|
|
||||||
|
:global(.arco-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-line {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user