first commit
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
import XinTable from "@/components/XinTable";
|
||||
import type {XinTableColumn} from "@/components/XinTable/typings.ts";
|
||||
import {Button, Card, type CardProps, Col, message, Row, Switch, Table, type TableProps, Tag, Tooltip, Tree, type TreeProps, Typography} from "antd";
|
||||
import {type RuleFieldsList, rulesList, setRule, statusRole, users as usersApi} from "@/api/system/sys_role";
|
||||
import type {ISysRole} from "@/domain/iSysRole.ts";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import type ISysUser from "@/domain/iSysUser.ts";
|
||||
import {KeyOutlined, SaveOutlined, SmileOutlined, TeamOutlined} from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isArray } from "lodash";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Role = () => {
|
||||
const {t} = useTranslation();
|
||||
// 状态管理
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<number | undefined>();
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
const [roleUsers, setRoleUsers] = useState<ISysUser[]>([]);
|
||||
const [roleUsersTotal, setRoleUsersTotal] = useState<number>(0);
|
||||
const [isLoadingUsers, setIsLoadingUsers] = useState<boolean>(false);
|
||||
const [ruleFields, setRuleFields] = useState<RuleFieldsList[]>([]);
|
||||
const [checkedRuleKeys, setCheckedRuleKeys] = useState<React.Key[]>([]);
|
||||
const [isSavingRules, setIsSavingRules] = useState<boolean>(false);
|
||||
// 树展开状态
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||
// 树引用
|
||||
const treeRef = useRef<any>(null);
|
||||
// Tab 配置
|
||||
const tabList: CardProps['tabList'] = [
|
||||
{
|
||||
key: "users",
|
||||
icon: <TeamOutlined />,
|
||||
label: t('system.role.tab.users'),
|
||||
},
|
||||
{
|
||||
key: "rules",
|
||||
icon: <KeyOutlined />,
|
||||
label: t('system.role.tab.rules')
|
||||
}
|
||||
];
|
||||
// 角色表格列配置
|
||||
const roleColumns: XinTableColumn<ISysRole>[] = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: "id",
|
||||
valueType: "text",
|
||||
align: 'center',
|
||||
width: 80,
|
||||
hideInForm: true
|
||||
},
|
||||
{
|
||||
title: t('system.role.table.roleName'),
|
||||
dataIndex: "name",
|
||||
valueType: "text",
|
||||
align: "center",
|
||||
required: true,
|
||||
colProps: {span: 8},
|
||||
rules: [{ required: true, message: t('system.role.table.roleName.required') }],
|
||||
render: (value: any, record: ISysRole) => (
|
||||
<>
|
||||
<Tooltip title={record.description}>
|
||||
<Tag variant="filled" color="blue">{value}</Tag>
|
||||
</Tooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('system.role.table.sort'),
|
||||
dataIndex: "sort",
|
||||
valueType: "digit",
|
||||
hideInSearch: true,
|
||||
align: "center",
|
||||
required: true,
|
||||
colProps: {span: 8},
|
||||
rules: [{ required: true, message: t('system.role.table.sort.required') }],
|
||||
render: (value: number) => <Tag variant="filled" color="purple">{value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: t('system.role.table.userCount'),
|
||||
dataIndex: "countUser",
|
||||
valueType: "text",
|
||||
hideInForm: true,
|
||||
align: "center",
|
||||
render: (value: number) => <a><u>{value}{t('system.role.userTable.person')}</u></a>,
|
||||
},
|
||||
{
|
||||
title: t('system.role.table.status'),
|
||||
dataIndex: "status",
|
||||
valueType: "switch",
|
||||
align: "center",
|
||||
colProps: {span: 8},
|
||||
filters: [
|
||||
{ text: t('system.role.table.status.disable'), value: 0 },
|
||||
{ text: t('system.role.table.status.enable'), value: 1 },
|
||||
],
|
||||
hideInSearch: true,
|
||||
required: true,
|
||||
rules: [{ required: true, message: t('system.role.table.status.required') }],
|
||||
render: (_, record: ISysRole) => (
|
||||
<Switch
|
||||
disabled={record.id === 1}
|
||||
checked={record.status === 1}
|
||||
checkedChildren={t('system.role.table.status.enable')}
|
||||
unCheckedChildren={t('system.role.table.status.disable')}
|
||||
onChange={async (_, event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await statusRole(record.id!);
|
||||
message.success(t('system.role.message.statusUpdateSuccess'));
|
||||
} catch (error) {
|
||||
message.error(t('system.role.message.statusUpdateFailed'));
|
||||
console.error('Failed to update role status:', error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
valueType: 'textarea',
|
||||
title: t('system.role.table.description'),
|
||||
dataIndex: 'description',
|
||||
colProps: {span: 24},
|
||||
hideInTable: true,
|
||||
},
|
||||
{
|
||||
title: t('system.role.table.createdAt'),
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
dataIndex: 'created_at',
|
||||
align: 'center',
|
||||
render: (value: string) => value ? dayjs(value).fromNow() : '-',
|
||||
},
|
||||
{
|
||||
title: t('system.role.table.updatedAt'),
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
dataIndex: 'updated_at',
|
||||
align: 'center',
|
||||
render: (value: string) => value ? dayjs(value).fromNow() : '-',
|
||||
},
|
||||
];
|
||||
// 用户列表表格列配置
|
||||
const userColumns: TableProps<ISysUser>['columns'] = [
|
||||
{
|
||||
title: t('system.role.userTable.userId'),
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: t('system.role.userTable.username'),
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t('system.role.userTable.nickname'),
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t('system.role.userTable.email'),
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
align: 'center',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: t('system.role.userTable.mobile'),
|
||||
dataIndex: 'mobile',
|
||||
key: 'mobile',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: t('system.role.userTable.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
render: (value: number) => {
|
||||
const status = value === 0
|
||||
? { color: 'error', text: t('system.role.userTable.status.banned') }
|
||||
: { color: 'success', text: t('system.role.userTable.status.normal') };
|
||||
return <Tag color={status.color}>{status.text}</Tag>;
|
||||
},
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
// 获取角色用户列表
|
||||
const fetchRoleUsers = async (page: number = 1, pageSize: number = 10) => {
|
||||
if (!selectedRoleId) return;
|
||||
setIsLoadingUsers(true);
|
||||
try {
|
||||
const response = await usersApi(selectedRoleId, { page, pageSize });
|
||||
const { data, total } = response.data.data!;
|
||||
setRoleUsers(data);
|
||||
setRoleUsersTotal(total);
|
||||
} finally {
|
||||
setIsLoadingUsers(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
rulesList().then(response => {
|
||||
setRuleFields(response.data.data!);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 当selectedRoleId变化时,重新获取用户列表
|
||||
useEffect(() => { fetchRoleUsers(1, 10) }, [selectedRoleId]);
|
||||
|
||||
// 事件处理函数
|
||||
const handleRoleSelect = (record: ISysRole) => {
|
||||
setSelectedRoleId(record.id!);
|
||||
// 回显当前角色的权限
|
||||
if (record.ruleIds && record.ruleIds.length > 0) {
|
||||
setCheckedRuleKeys(record.ruleIds);
|
||||
} else {
|
||||
setCheckedRuleKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染权限树节点
|
||||
const renderRuleTreeNode = (node: RuleFieldsList) => {
|
||||
if (node.local) {
|
||||
return (
|
||||
<>
|
||||
{t(node.local)} - <span style={{ color: '#00000040' }}>{node.title}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <>{node.title}</>;
|
||||
};
|
||||
|
||||
// 处理权限树勾选变化
|
||||
const handleRuleCheck: TreeProps['onCheck'] = (checkedKeys) => {
|
||||
if(isArray(checkedKeys)) {
|
||||
setCheckedRuleKeys(checkedKeys);
|
||||
}else {
|
||||
setCheckedRuleKeys(checkedKeys.checked);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理树展开状态变化
|
||||
const handleTreeExpand = (keys: React.Key[]) => {
|
||||
setExpandedKeys(keys);
|
||||
};
|
||||
|
||||
// 获取所有节点的key
|
||||
const getAllNodeKeys = (nodes: RuleFieldsList[]): React.Key[] => {
|
||||
let keys: React.Key[] = [];
|
||||
nodes.forEach(node => {
|
||||
keys.push(node.key);
|
||||
if (node.children && node.children.length > 0) {
|
||||
keys = [...keys, ...getAllNodeKeys(node.children)];
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
// 展开全部节点
|
||||
const expandAll = () => {
|
||||
const allKeys = getAllNodeKeys(ruleFields);
|
||||
setExpandedKeys(allKeys);
|
||||
};
|
||||
|
||||
// 折叠全部节点
|
||||
const collapseAll = () => {
|
||||
setExpandedKeys([]);
|
||||
};
|
||||
|
||||
// 选择全部节点
|
||||
const selectAll = () => {
|
||||
const allKeys = getAllNodeKeys(ruleFields);
|
||||
setCheckedRuleKeys(allKeys);
|
||||
};
|
||||
|
||||
// 取消选择全部节点
|
||||
const deselectAll = () => {
|
||||
setCheckedRuleKeys([]);
|
||||
};
|
||||
|
||||
// 反选节点
|
||||
const invertSelection = () => {
|
||||
const allKeys = getAllNodeKeys(ruleFields);
|
||||
const currentCheckedSet = new Set(checkedRuleKeys);
|
||||
const invertedKeys = allKeys.filter(key => !currentCheckedSet.has(key));
|
||||
setCheckedRuleKeys(invertedKeys);
|
||||
};
|
||||
|
||||
// 保存权限设置
|
||||
const handleSaveRules = async () => {
|
||||
if (!selectedRoleId) {
|
||||
message.warning(t('system.role.message.selectRoleFirst'));
|
||||
return;
|
||||
}
|
||||
setIsSavingRules(true);
|
||||
try {
|
||||
const ruleIds = checkedRuleKeys.map(key => Number(key));
|
||||
await setRule(selectedRoleId, ruleIds);
|
||||
message.success(t('system.role.message.rulesSaveSuccess'));
|
||||
} finally {
|
||||
setIsSavingRules(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'mb-5'}>
|
||||
<Title level={3}>{t('system.role.page.title')}</Title>
|
||||
<Text type="secondary">{t('system.role.page.description')}</Text>
|
||||
</div>
|
||||
<Row gutter={[20, 20]}>
|
||||
{/* 角色列表 */}
|
||||
<Col xxl={14} lg={12} xs={24}>
|
||||
<XinTable<ISysRole>
|
||||
api="/system/role"
|
||||
accessName="system.role"
|
||||
columns={roleColumns}
|
||||
rowKey="id"
|
||||
searchShow={false}
|
||||
searchProps={false}
|
||||
scroll={{x: 800}}
|
||||
editShow={(row) => row.id !== 1}
|
||||
deleteShow={(row) => row.id !== 1}
|
||||
formProps={{
|
||||
grid: true,
|
||||
rowProps: { gutter: [20, 0] },
|
||||
layout: 'vertical'
|
||||
}}
|
||||
modalProps={{
|
||||
width: 800
|
||||
}}
|
||||
operateProps={{
|
||||
fixed: 'right'
|
||||
}}
|
||||
rowSelection={{
|
||||
type: 'radio',
|
||||
selectedRowKeys: selectedRoleId ? [selectedRoleId] : [],
|
||||
onChange: (_, rows) => handleRoleSelect(rows[0])
|
||||
}}
|
||||
onRow ={(record) => ({
|
||||
onClick: () => handleRoleSelect(record)
|
||||
})}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/* 用户列表和权限管理 */}
|
||||
<Col xxl={10} lg={12} xs={24}>
|
||||
<Card
|
||||
tabList={tabList}
|
||||
onTabChange={setActiveTab}
|
||||
activeTabKey={activeTab}
|
||||
styles={{ body: { minHeight: '70vh' } }}
|
||||
>
|
||||
{ selectedRoleId ? (
|
||||
activeTab === 'users' ? (
|
||||
<Table<ISysUser>
|
||||
loading={isLoadingUsers}
|
||||
dataSource={roleUsers}
|
||||
bordered={true}
|
||||
columns={userColumns}
|
||||
size="small"
|
||||
pagination={{
|
||||
total: roleUsersTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: fetchRoleUsers
|
||||
}}
|
||||
scroll={{x: 600}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ height: '600px', overflow: 'auto', marginBottom: 12 }}>
|
||||
<Tree
|
||||
ref={treeRef}
|
||||
checkable
|
||||
treeData={ruleFields}
|
||||
checkedKeys={checkedRuleKeys}
|
||||
onCheck={handleRuleCheck}
|
||||
titleRender={renderRuleTreeNode}
|
||||
checkStrictly={true}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={handleTreeExpand}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ color: '#666', fontSize: 12 }}>
|
||||
{t('system.role.permission.selectedCount', { count: checkedRuleKeys.length })}
|
||||
</span>
|
||||
<Button size="small" onClick={expandAll}>{t('system.role.button.expandAll')}</Button>
|
||||
<Button size="small" onClick={collapseAll}>{t('system.role.button.collapseAll')}</Button>
|
||||
<Button size="small" onClick={selectAll}>{t('system.role.button.selectAll')}</Button>
|
||||
<Button size="small" onClick={deselectAll}>{t('system.role.button.clearAll')}</Button>
|
||||
<Button size="small" onClick={invertSelection}>{t('system.role.button.invertSelection')}</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={isSavingRules}
|
||||
onClick={handleSaveRules}
|
||||
size="small"
|
||||
disabled={selectedRoleId === 1}
|
||||
>
|
||||
{t('system.role.button.saveRules')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: '#00000040' }}>
|
||||
<SmileOutlined style={{ fontSize: 40, marginBottom: 12 }} />
|
||||
<p>{t('system.role.placeholder.selectRole')}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Role;
|
||||
Reference in New Issue
Block a user