feat: 添加角色数据权限管理功能
- 在 `zh-CN.json` 中新增与Bot凭证相关的国际化字符串 - 在 `index.tsx` 中添加数据权限管理标签页,支持自定义部门选择 - 更新API接口,新增获取和保存角色数据权限的方法 - 重构角色选择逻辑,加载角色时同时获取权限和数据权限信息 - 优化用户绑定和权限保存操作的提示信息dev_na
parent
653a9f7ef4
commit
92e6b9fd4d
|
|
@ -1,6 +1,6 @@
|
||||||
import http from "./http";
|
import http from "./http";
|
||||||
import {
|
import {
|
||||||
DeviceInfo, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
|
BotCredential, DeviceInfo, RoleDataScope, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
|
||||||
PermissionNode
|
PermissionNode
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
|
|
@ -29,7 +29,6 @@ export async function listUsers(params?: { tenantId?: number; orgId?: number })
|
||||||
return resp.data.data as SysUser[];
|
return resp.data.data as SysUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function createUser(payload: Partial<SysUser>) {
|
export async function createUser(payload: Partial<SysUser>) {
|
||||||
const resp = await http.post("/sys/api/users", payload);
|
const resp = await http.post("/sys/api/users", payload);
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
|
|
@ -110,6 +109,16 @@ export async function updateMyPassword(payload: any) {
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMyBotCredential() {
|
||||||
|
const resp = await http.get("/sys/api/users/bot-credential");
|
||||||
|
return resp.data.data as BotCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMyBotCredential() {
|
||||||
|
const resp = await http.post("/sys/api/users/bot-credential/generate");
|
||||||
|
return resp.data.data as BotCredential;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createPermission(payload: Partial<SysPermission>) {
|
export async function createPermission(payload: Partial<SysPermission>) {
|
||||||
const resp = await http.post("/sys/api/permissions", payload);
|
const resp = await http.post("/sys/api/permissions", payload);
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
|
|
@ -165,6 +174,16 @@ export async function saveRolePermissions(roleId: number, permIds: number[]) {
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRoleDataScope(roleId: number) {
|
||||||
|
const resp = await http.get(`/sys/api/roles/${roleId}/data-scope`);
|
||||||
|
return resp.data.data as RoleDataScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveRoleDataScope(roleId: number, payload: RoleDataScope) {
|
||||||
|
const resp = await http.post(`/sys/api/roles/${roleId}/data-scope`, payload);
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchUsersByRoleId(roleId: number) {
|
export async function fetchUsersByRoleId(roleId: number) {
|
||||||
const resp = await http.get(`/sys/api/roles/${roleId}/users`);
|
const resp = await http.get(`/sys/api/roles/${roleId}/users`);
|
||||||
return resp.data.data as SysUser[];
|
return resp.data.data as SysUser[];
|
||||||
|
|
@ -194,5 +213,3 @@ export * from "./dict";
|
||||||
export * from "./tenant";
|
export * from "./tenant";
|
||||||
export * from "./org";
|
export * from "./org";
|
||||||
export * from "./platform";
|
export * from "./platform";
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,18 @@
|
||||||
"saveChanges": "Save Changes",
|
"saveChanges": "Save Changes",
|
||||||
"updatePassword": "Update Password",
|
"updatePassword": "Update Password",
|
||||||
"passwordsDoNotMatch": "Passwords do not match.",
|
"passwordsDoNotMatch": "Passwords do not match.",
|
||||||
"standardUser": "Standard User"
|
"standardUser": "Standard User",
|
||||||
|
"botCredentialTab": "Bot Credential",
|
||||||
|
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
|
||||||
|
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
|
||||||
|
"botBindStatus": "Binding Status",
|
||||||
|
"botBound": "Bound",
|
||||||
|
"botUnbound": "Not Generated",
|
||||||
|
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
|
||||||
|
"botLastAccessTime": "Last Access Time",
|
||||||
|
"botLastAccessIp": "Last Access IP",
|
||||||
|
"generateBotCredential": "Generate Credential",
|
||||||
|
"regenerateBotCredential": "Regenerate Credential"
|
||||||
},
|
},
|
||||||
"rolesExt": {
|
"rolesExt": {
|
||||||
"roleList": "Role List",
|
"roleList": "Role List",
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,18 @@
|
||||||
"saveChanges": "保存修改",
|
"saveChanges": "保存修改",
|
||||||
"updatePassword": "更新密码",
|
"updatePassword": "更新密码",
|
||||||
"passwordsDoNotMatch": "两次输入的密码不一致。",
|
"passwordsDoNotMatch": "两次输入的密码不一致。",
|
||||||
"standardUser": "普通用户"
|
"standardUser": "普通用户",
|
||||||
|
"botCredentialTab": "Bot 凭证",
|
||||||
|
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
|
||||||
|
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
|
||||||
|
"botBindStatus": "绑定状态",
|
||||||
|
"botBound": "已绑定",
|
||||||
|
"botUnbound": "未生成",
|
||||||
|
"botSecretHidden": "已隐藏。如需查看新的 Secret,请重新生成。",
|
||||||
|
"botLastAccessTime": "最近访问时间",
|
||||||
|
"botLastAccessIp": "最近访问 IP",
|
||||||
|
"generateBotCredential": "生成凭证",
|
||||||
|
"regenerateBotCredential": "重置凭证"
|
||||||
},
|
},
|
||||||
"rolesExt": {
|
"rolesExt": {
|
||||||
"roleList": "角色列表",
|
"roleList": "角色列表",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Avatar, Badge, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd";
|
import { Avatar, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, Radio, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd";
|
||||||
import type { DataNode } from "antd/es/tree";
|
import type { DataNode } from "antd/es/tree";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
import {
|
||||||
ApartmentOutlined,
|
ApartmentOutlined,
|
||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
|
|
@ -22,11 +21,14 @@ import {
|
||||||
createRole,
|
createRole,
|
||||||
deleteRole,
|
deleteRole,
|
||||||
fetchUsersByRoleId,
|
fetchUsersByRoleId,
|
||||||
|
getRoleDataScope,
|
||||||
|
listOrgs,
|
||||||
listPermissions,
|
listPermissions,
|
||||||
listRolePermissions,
|
listRolePermissions,
|
||||||
listTenants,
|
listTenants,
|
||||||
listUsers,
|
listUsers,
|
||||||
pageRoles,
|
pageRoles,
|
||||||
|
saveRoleDataScope,
|
||||||
saveRolePermissions,
|
saveRolePermissions,
|
||||||
unbindUserFromRole,
|
unbindUserFromRole,
|
||||||
updateRole
|
updateRole
|
||||||
|
|
@ -34,14 +36,25 @@ import {
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
import type { SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] };
|
type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] };
|
||||||
|
type OrgTreeNode = SysOrg & { key: number; children?: OrgTreeNode[] };
|
||||||
|
type RoleTabKey = "permissions" | "dataScope" | "users";
|
||||||
|
|
||||||
const DEFAULT_STATUS = 1;
|
const DEFAULT_STATUS = 1;
|
||||||
const DEFAULT_ROLE_PAGE_SIZE = 10;
|
const DEFAULT_ROLE_PAGE_SIZE = 10;
|
||||||
|
const BUTTON_SHORT_LABEL = "按钮";
|
||||||
|
const DATA_SCOPE_OPTIONS = [
|
||||||
|
{ label: "全部", value: "ALL" },
|
||||||
|
{ label: "个人", value: "SELF" },
|
||||||
|
{ label: "本部门", value: "DEPT" },
|
||||||
|
{ label: "本部门及下级部门", value: "DEPT_AND_CHILD" },
|
||||||
|
{ label: "自定义部门", value: "CUSTOM" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
function normalizeNumber(value: unknown): number | undefined {
|
function normalizeNumber(value: unknown): number | undefined {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
|
|
@ -56,6 +69,7 @@ function normalizeNumber(value: unknown): number | undefined {
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
|
function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
|
||||||
const active = (list || []).filter((permission) => permission.status !== 0);
|
const active = (list || []).filter((permission) => permission.status !== 0);
|
||||||
const map = new Map<number, PermissionNode>();
|
const map = new Map<number, PermissionNode>();
|
||||||
|
|
@ -85,7 +99,7 @@ function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTreeData(nodes: PermissionNode[], t: (key: string, options?: Record<string, unknown>) => string, buttonShortLabel: string): DataNode[] {
|
function toPermissionTreeData(nodes: PermissionNode[], buttonShortLabel: string): DataNode[] {
|
||||||
return nodes.map((node) => ({
|
return nodes.map((node) => ({
|
||||||
key: node.permId,
|
key: node.permId,
|
||||||
title: (
|
title: (
|
||||||
|
|
@ -94,14 +108,63 @@ function toTreeData(nodes: PermissionNode[], t: (key: string, options?: Record<s
|
||||||
{node.permType === "button" ? <Tag color="cyan" style={{ fontSize: 10 }}>{buttonShortLabel}</Tag> : null}
|
{node.permType === "button" ? <Tag color="cyan" style={{ fontSize: 10 }}>{buttonShortLabel}</Tag> : null}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
children: node.children?.length ? toTreeData(node.children, t, buttonShortLabel) : undefined
|
children: node.children?.length ? toPermissionTreeData(node.children, buttonShortLabel) : undefined
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOrgTree(list: SysOrg[]): OrgTreeNode[] {
|
||||||
|
const map = new Map<number, OrgTreeNode>();
|
||||||
|
const roots: OrgTreeNode[] = [];
|
||||||
|
|
||||||
|
list.forEach((item) => {
|
||||||
|
map.set(item.id, { ...item, key: item.id, children: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((node) => {
|
||||||
|
if (node.parentId && map.has(node.parentId)) {
|
||||||
|
map.get(node.parentId)!.children!.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortNodes = (nodes: OrgTreeNode[]) => {
|
||||||
|
nodes.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0));
|
||||||
|
nodes.forEach((node) => node.children && sortNodes(node.children));
|
||||||
|
};
|
||||||
|
|
||||||
|
sortNodes(roots);
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toOrgTreeData(nodes: OrgTreeNode[]): DataNode[] {
|
||||||
|
return nodes.map((node) => ({
|
||||||
|
key: node.id,
|
||||||
|
title: node.orgName,
|
||||||
|
children: node.children?.length ? toOrgTreeData(node.children) : undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataScopeDescription(scopeType: string) {
|
||||||
|
switch (scopeType) {
|
||||||
|
case "ALL":
|
||||||
|
return "当前角色可访问当前租户全部数据。";
|
||||||
|
case "SELF":
|
||||||
|
return "当前角色仅可访问本人数据。";
|
||||||
|
case "DEPT":
|
||||||
|
return "当前角色可访问本人所在部门的数据。";
|
||||||
|
case "DEPT_AND_CHILD":
|
||||||
|
return "当前角色可访问本人所在部门及所有下级部门的数据。";
|
||||||
|
case "CUSTOM":
|
||||||
|
return "当前角色可访问选中部门的数据。";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`;
|
const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`;
|
||||||
|
|
||||||
export default function Roles() {
|
export default function Roles() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { can } = usePermission();
|
const { can } = usePermission();
|
||||||
const { items: statusDict } = useDict("sys_common_status");
|
const { items: statusDict } = useDict("sys_common_status");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -119,21 +182,14 @@ export default function Roles() {
|
||||||
const [userSearchText, setUserSearchText] = useState("");
|
const [userSearchText, setUserSearchText] = useState("");
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [rolePage, setRolePage] = useState({ current: 1, size: DEFAULT_ROLE_PAGE_SIZE, total: 0 });
|
const [rolePage, setRolePage] = useState({ current: 1, size: DEFAULT_ROLE_PAGE_SIZE, total: 0 });
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
setSearchText((value) => value.trim());
|
|
||||||
setRolePage((prev) => ({ ...prev, current: 1 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetSearch = () => {
|
|
||||||
setSearchText("");
|
|
||||||
setFilterTenantId(undefined);
|
|
||||||
setRolePage((prev) => ({ ...prev, current: 1 }));
|
|
||||||
};
|
|
||||||
const [filterTenantId, setFilterTenantId] = useState<number | undefined>(undefined);
|
const [filterTenantId, setFilterTenantId] = useState<number | undefined>(undefined);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<SysRole | null>(null);
|
const [editing, setEditing] = useState<SysRole | null>(null);
|
||||||
const [tenants, setTenants] = useState<SysTenant[]>([]);
|
const [tenants, setTenants] = useState<SysTenant[]>([]);
|
||||||
|
const [activeTab, setActiveTab] = useState<RoleTabKey>("permissions");
|
||||||
|
const [dataScopeType, setDataScopeType] = useState("SELF");
|
||||||
|
const [scopeOrgIds, setScopeOrgIds] = useState<number[]>([]);
|
||||||
|
const [scopeOrgTree, setScopeOrgTree] = useState<DataNode[]>([]);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const isPlatformMode = useMemo(() => {
|
const isPlatformMode = useMemo(() => {
|
||||||
|
|
@ -144,12 +200,15 @@ export default function Roles() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const activeTenantId = useMemo(() => normalizeNumber(localStorage.getItem("activeTenantId")) ?? 0, []);
|
const activeTenantId = useMemo(() => normalizeNumber(localStorage.getItem("activeTenantId")) ?? 0, []);
|
||||||
const buttonShortLabel = useMemo(() => {
|
const permissionTreeData = useMemo(() => toPermissionTreeData(buildPermissionTree(permissions), BUTTON_SHORT_LABEL), [permissions]);
|
||||||
const label = t("buttonShort");
|
const filteredModalUsers = useMemo(() => {
|
||||||
return !label || label === "buttonShort" ? "BTN" : label;
|
const existingIds = new Set(roleUsers.map((user) => user.userId));
|
||||||
}, [t]);
|
return allUsers.filter(
|
||||||
const permissionTreeData = useMemo(() => toTreeData(buildPermissionTree(permissions), t, buttonShortLabel), [buttonShortLabel, permissions, t]);
|
(user) =>
|
||||||
|
!existingIds.has(user.userId) &&
|
||||||
|
(user.username.toLowerCase().includes(userSearchText.toLowerCase()) || user.displayName.toLowerCase().includes(userSearchText.toLowerCase()))
|
||||||
|
);
|
||||||
|
}, [allUsers, roleUsers, userSearchText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPlatformMode) return;
|
if (!isPlatformMode) return;
|
||||||
|
|
@ -160,23 +219,31 @@ export default function Roles() {
|
||||||
try {
|
try {
|
||||||
const list = await listPermissions();
|
const list = await listPermissions();
|
||||||
setPermissions(list || []);
|
setPermissions(list || []);
|
||||||
|
return list || [];
|
||||||
} catch {
|
} catch {
|
||||||
setPermissions([]);
|
setPermissions([]);
|
||||||
|
return [] as SysPermission[];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectRole = async (role: SysRole) => {
|
const selectRole = async (role: SysRole, permissionList: SysPermission[] = permissions) => {
|
||||||
setSelectedRole(role);
|
setSelectedRole(role);
|
||||||
|
setLoadingUsers(true);
|
||||||
try {
|
try {
|
||||||
const ids = await listRolePermissions(role.roleId);
|
const [ids, users, dataScope, orgs] = await Promise.all([
|
||||||
|
listRolePermissions(role.roleId),
|
||||||
|
fetchUsersByRoleId(role.roleId),
|
||||||
|
getRoleDataScope(role.roleId),
|
||||||
|
listOrgs(role.tenantId)
|
||||||
|
]);
|
||||||
const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id));
|
const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id));
|
||||||
const leafIds = normalized.filter((id) => !permissions.some((permission) => permission.parentId === id));
|
const leafIds = normalized.filter((id) => !permissionList.some((permission) => permission.parentId === id));
|
||||||
setSelectedPermIds(leafIds);
|
setSelectedPermIds(leafIds);
|
||||||
setHalfCheckedIds([]);
|
setHalfCheckedIds([]);
|
||||||
|
|
||||||
setLoadingUsers(true);
|
|
||||||
const users = await fetchUsersByRoleId(role.roleId);
|
|
||||||
setRoleUsers(users || []);
|
setRoleUsers(users || []);
|
||||||
|
setDataScopeType(dataScope?.scopeType || role.dataScopeType || "SELF");
|
||||||
|
setScopeOrgIds((dataScope?.orgIds || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)));
|
||||||
|
setScopeOrgTree(toOrgTreeData(buildOrgTree(orgs || [])));
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingUsers(false);
|
setLoadingUsers(false);
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +252,7 @@ export default function Roles() {
|
||||||
const loadRoles = async (page = rolePage.current, size = rolePage.size) => {
|
const loadRoles = async (page = rolePage.current, size = rolePage.size) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const permissionList = await loadPermissions();
|
||||||
const response = await pageRoles({
|
const response = await pageRoles({
|
||||||
current: page,
|
current: page,
|
||||||
size,
|
size,
|
||||||
|
|
@ -199,24 +267,26 @@ export default function Roles() {
|
||||||
setRoleUsers([]);
|
setRoleUsers([]);
|
||||||
setSelectedPermIds([]);
|
setSelectedPermIds([]);
|
||||||
setHalfCheckedIds([]);
|
setHalfCheckedIds([]);
|
||||||
|
setDataScopeType("SELF");
|
||||||
|
setScopeOrgIds([]);
|
||||||
|
setScopeOrgTree([]);
|
||||||
} else if (!selectedRole) {
|
} else if (!selectedRole) {
|
||||||
await selectRole(roles[0]);
|
await selectRole(roles[0], permissionList);
|
||||||
} else {
|
} else {
|
||||||
const updated = roles.find((role) => role.roleId === selectedRole.roleId);
|
const updated = roles.find((role) => role.roleId === selectedRole.roleId);
|
||||||
if (updated) {
|
if (updated) {
|
||||||
setSelectedRole(updated);
|
await selectRole(updated, permissionList);
|
||||||
} else {
|
} else {
|
||||||
await selectRole(roles[0]);
|
await selectRole(roles[0], permissionList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await loadPermissions();
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRoles(rolePage.current, rolePage.size);
|
void loadRoles(rolePage.current, rolePage.size);
|
||||||
}, [filterTenantId, rolePage.current, rolePage.size, searchText]);
|
}, [filterTenantId, rolePage.current, rolePage.size, searchText]);
|
||||||
|
|
||||||
const loadAllUsers = async () => {
|
const loadAllUsers = async () => {
|
||||||
|
|
@ -229,7 +299,7 @@ export default function Roles() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const openUserModal = () => {
|
const openUserModal = () => {
|
||||||
loadAllUsers();
|
void loadAllUsers();
|
||||||
setSelectedUserKeys([]);
|
setSelectedUserKeys([]);
|
||||||
setUserModalOpen(true);
|
setUserModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
@ -237,33 +307,22 @@ export default function Roles() {
|
||||||
const handleAddUsers = async () => {
|
const handleAddUsers = async () => {
|
||||||
if (!selectedRole || selectedUserKeys.length === 0) return;
|
if (!selectedRole || selectedUserKeys.length === 0) return;
|
||||||
await bindUsersToRole(selectedRole.roleId, selectedUserKeys);
|
await bindUsersToRole(selectedRole.roleId, selectedUserKeys);
|
||||||
message.success(t("common.success"));
|
message.success("操作成功");
|
||||||
setUserModalOpen(false);
|
setUserModalOpen(false);
|
||||||
selectRole(selectedRole);
|
await selectRole(selectedRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnbindUser = async (userId: number) => {
|
const handleUnbindUser = async (userId: number) => {
|
||||||
if (!selectedRole) return;
|
if (!selectedRole) return;
|
||||||
if (selectedRole.roleCode === "TENANT_ADMIN" && roleUsers.length <= 1) {
|
if (selectedRole.roleCode === "TENANT_ADMIN" && roleUsers.length <= 1) {
|
||||||
message.warning(t("rolesExt.tenantAdminWarning"));
|
message.warning("租户管理员角色至少需要保留一个绑定用户");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await unbindUserFromRole(selectedRole.roleId, userId);
|
await unbindUserFromRole(selectedRole.roleId, userId);
|
||||||
message.success(t("common.success"));
|
message.success("操作成功");
|
||||||
selectRole(selectedRole);
|
await selectRole(selectedRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredModalUsers = useMemo(() => {
|
|
||||||
const existingIds = new Set(roleUsers.map((user) => user.userId));
|
|
||||||
return allUsers.filter(
|
|
||||||
(user) =>
|
|
||||||
!existingIds.has(user.userId) &&
|
|
||||||
(user.username.toLowerCase().includes(userSearchText.toLowerCase()) || user.displayName.toLowerCase().includes(userSearchText.toLowerCase()))
|
|
||||||
);
|
|
||||||
}, [allUsers, roleUsers, userSearchText]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
|
@ -281,9 +340,9 @@ export default function Roles() {
|
||||||
const handleRemove = async (event: React.MouseEvent, id: number) => {
|
const handleRemove = async (event: React.MouseEvent, id: number) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
await deleteRole(id);
|
await deleteRole(id);
|
||||||
message.success(t("common.success"));
|
message.success("操作成功");
|
||||||
if (selectedRole?.roleId === id) setSelectedRole(null);
|
if (selectedRole?.roleId === id) setSelectedRole(null);
|
||||||
loadRoles(rolePage.current, rolePage.size);
|
await loadRoles(rolePage.current, rolePage.size);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitBasic = async () => {
|
const submitBasic = async () => {
|
||||||
|
|
@ -295,16 +354,17 @@ export default function Roles() {
|
||||||
roleName: values.roleName,
|
roleName: values.roleName,
|
||||||
remark: values.remark,
|
remark: values.remark,
|
||||||
status: values.status ?? DEFAULT_STATUS,
|
status: values.status ?? DEFAULT_STATUS,
|
||||||
tenantId: values.tenantId
|
tenantId: values.tenantId,
|
||||||
|
dataScopeType: editing?.dataScopeType || "SELF"
|
||||||
};
|
};
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await updateRole(editing.roleId, payload);
|
await updateRole(editing.roleId, payload);
|
||||||
} else {
|
} else {
|
||||||
await createRole(payload);
|
await createRole(payload);
|
||||||
}
|
}
|
||||||
message.success(t("common.success"));
|
message.success("操作成功");
|
||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
loadRoles(rolePage.current, rolePage.size);
|
await loadRoles(rolePage.current, rolePage.size);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -319,33 +379,63 @@ export default function Roles() {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await saveRolePermissions(selectedRole.roleId, Array.from(new Set([...selectedPermIds, ...halfCheckedIds])));
|
await saveRolePermissions(selectedRole.roleId, Array.from(new Set([...selectedPermIds, ...halfCheckedIds])));
|
||||||
message.success(t("common.success"));
|
message.success("操作成功");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveDataScope = async () => {
|
||||||
|
if (!selectedRole) return;
|
||||||
|
if (dataScopeType === "CUSTOM" && scopeOrgIds.length === 0) {
|
||||||
|
message.warning("请选择至少一个部门");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const payload: RoleDataScope = {
|
||||||
|
roleId: selectedRole.roleId,
|
||||||
|
scopeType: dataScopeType,
|
||||||
|
orgIds: dataScopeType === "CUSTOM" ? scopeOrgIds : []
|
||||||
|
};
|
||||||
|
await saveRoleDataScope(selectedRole.roleId, payload);
|
||||||
|
message.success("操作成功");
|
||||||
|
setSelectedRole((prev) => (prev ? { ...prev, dataScopeType } : prev));
|
||||||
|
setData((prev) => prev.map((item) => item.roleId === selectedRole.roleId ? { ...item, dataScopeType } : item));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrimarySave = () => {
|
||||||
|
if (activeTab === "permissions") {
|
||||||
|
void savePermissions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeTab === "dataScope") {
|
||||||
|
void saveDataScope();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveDisabled = !selectedRole || activeTab === "users" || (activeTab === "permissions" && !can("sys:role:permission:save")) || (activeTab === "dataScope" && !can("sys:role:update"));
|
||||||
|
const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page roles-page-v2">
|
<div className="app-page roles-page-v2">
|
||||||
<PageHeader
|
<PageHeader title="角色管理" subtitle="维护角色基础信息、功能权限、数据权限与成员绑定" />
|
||||||
title={t("roles.title")}
|
|
||||||
subtitle={t("roles.subtitle")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="app-page__page-actions">
|
<div className="app-page__page-actions">
|
||||||
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</Button>}
|
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{"新增角色"}</Button>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="roles-layout">
|
<div className="roles-layout">
|
||||||
<Row gutter={24} className="roles-layout__row">
|
<Row gutter={24} className="roles-layout__row">
|
||||||
<Col span={7} className="roles-layout__side">
|
<Col span={7} className="roles-layout__side">
|
||||||
<Card title={<Space><ApartmentOutlined /><span>{t("rolesExt.roleList")}</span>
|
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
||||||
{/*<Badge count={rolePage.total} overflowCount={999} className="roles-count-badge" />*/}
|
|
||||||
</Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
|
||||||
<div className="role-search-panel">
|
<div className="role-search-panel">
|
||||||
{isPlatformMode && (
|
{isPlatformMode && (
|
||||||
<Select
|
<Select
|
||||||
placeholder={t("rolesExt.filterTenant")}
|
placeholder="按租户筛选"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
allowClear
|
allowClear
|
||||||
suffixIcon={<FilterOutlined />}
|
suffixIcon={<FilterOutlined />}
|
||||||
|
|
@ -355,8 +445,8 @@ export default function Roles() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="role-search-bar">
|
<div className="role-search-bar">
|
||||||
<Input placeholder={t("roles.searchPlaceholder")} prefix={<SearchOutlined style={{ color: "#94a3b8" }} />} value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
|
<Input placeholder="输入角色名称或编码搜索" prefix={<SearchOutlined style={{ color: "#94a3b8" }} />} value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
|
||||||
<Button type="default" onClick={handleResetSearch}>{t("common.reset")}</Button>
|
<Button type="default" onClick={() => { setSearchText(""); setFilterTenantId(undefined); setRolePage((prev) => ({ ...prev, current: 1 })); }}>{"重置"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -365,17 +455,17 @@ export default function Roles() {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
locale={{ emptyText: <Empty description={t("rolesExt.noRolesFound")} image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
locale={{ emptyText: <Empty description="暂无角色数据" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<div key={item.roleId} className={`role-item-card-v3 ${selectedRole?.roleId === item.roleId ? "active" : ""}`} onClick={() => selectRole(item)}>
|
<div key={item.roleId} className={`role-item-card-v3 ${selectedRole?.roleId === item.roleId ? "active" : ""}`} onClick={() => void selectRole(item)}>
|
||||||
<div className="role-item-symbol" aria-hidden="true">
|
<div className="role-item-symbol" aria-hidden="true">
|
||||||
<SafetyCertificateOutlined />
|
<SafetyCertificateOutlined />
|
||||||
</div>
|
</div>
|
||||||
<div className="role-item-main">
|
<div className="role-item-main">
|
||||||
<div className="role-item-name-row">
|
<div className="role-item-name-row">
|
||||||
<Text strong className="role-name">{item.roleName}</Text>
|
<Text strong className="role-name">{item.roleName}</Text>
|
||||||
{isPlatformMode && <Tag color="blue" style={{ fontSize: 10, scale: "0.8", margin: "0 0 0 4px", borderRadius: "10px" }}>{item.tenantId === 0 ? t("rolesExt.systemTenant") : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `${t("rolesExt.tenantLabel")}:${item.tenantId}`}</Tag>}
|
{isPlatformMode && <Tag color="blue" style={{ fontSize: 10, scale: "0.8", margin: "0 0 0 4px", borderRadius: "10px" }}>{item.tenantId === 0 ? "平台租户" : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `租户:${item.tenantId}`}</Tag>}
|
||||||
{item.status === 0 && <Tag color="error" style={{ fontSize: 10, scale: "0.8", margin: 0 }}>{t("rolesExt.disabled")}</Tag>}
|
{item.status === 0 && <Tag color="error" style={{ fontSize: 10, scale: "0.8", margin: 0 }}>{"停用"}</Tag>}
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" className="role-code">{item.roleCode}</Text>
|
<Text type="secondary" className="role-code">{item.roleCode}</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -386,11 +476,11 @@ export default function Roles() {
|
||||||
) : null}
|
) : null}
|
||||||
<div className="role-item-actions">
|
<div className="role-item-actions">
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<Tooltip title={t("common.edit")}>
|
<Tooltip title="编辑">
|
||||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} />
|
<Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{item.roleCode !== "ADMIN" && (
|
{item.roleCode !== "ADMIN" && (
|
||||||
<Popconfirm title={t("rolesExt.deleteRole")} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={(event) => handleRemove(event!, item.roleId)}>
|
<Popconfirm title="确定删除该角色吗?" okText="确定" cancelText="取消" onConfirm={(event) => void handleRemove(event!, item.roleId)}>
|
||||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} />
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} />
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
|
|
@ -420,10 +510,10 @@ export default function Roles() {
|
||||||
className="app-page__panel-card roles-detail-card"
|
className="app-page__panel-card roles-detail-card"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
|
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
|
||||||
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={savePermissions} disabled={!can("sys:role:permission:save") || (selectedRole.roleCode === "TENANT_ADMIN" && !isPlatformMode)} style={{ borderRadius: "6px" }}>{t("common.save")}</Button>}
|
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>}
|
||||||
>
|
>
|
||||||
<Tabs defaultActiveKey="permissions" className="role-detail-tabs">
|
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as RoleTabKey)} className="role-detail-tabs">
|
||||||
<Tabs.TabPane tab={<Space><KeyOutlined />{t("roles.funcPerms")}</Space>} key="permissions">
|
<Tabs.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions">
|
||||||
<div className="role-detail-pane">
|
<div className="role-detail-pane">
|
||||||
<div className="permission-tree-wrapper">
|
<div className="permission-tree-wrapper">
|
||||||
<Tree
|
<Tree
|
||||||
|
|
@ -443,11 +533,40 @@ export default function Roles() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={<Space><TeamOutlined />{t("rolesExt.membersTab")} ({roleUsers.length})</Space>} key="users">
|
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
|
||||||
|
<div className="role-detail-pane">
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Radio.Group value={dataScopeType} onChange={(event) => setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid">
|
||||||
|
{DATA_SCOPE_OPTIONS.map((item) => (
|
||||||
|
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
|
||||||
|
{dataScopeType === "CUSTOM" ? (
|
||||||
|
<div className="permission-tree-wrapper">
|
||||||
|
<Tree
|
||||||
|
checkable
|
||||||
|
selectable={false}
|
||||||
|
treeData={scopeOrgTree}
|
||||||
|
checkedKeys={scopeOrgIds}
|
||||||
|
onCheck={(keys) => {
|
||||||
|
const checked = Array.isArray(keys) ? keys : keys.checked;
|
||||||
|
setScopeOrgIds(checked.map((key) => Number(key)));
|
||||||
|
}}
|
||||||
|
defaultExpandAll
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab={<Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>} key="users">
|
||||||
<div className="role-detail-pane">
|
<div className="role-detail-pane">
|
||||||
<div className="role-members-toolbar">
|
<div className="role-members-toolbar">
|
||||||
<Title level={5} style={{ margin: 0 }}>{t("rolesExt.assignedUsers")}</Title>
|
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
|
||||||
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{t("rolesExt.bindUser")}</Button>
|
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{"绑定用户"}</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
rowKey="userId"
|
rowKey="userId"
|
||||||
|
|
@ -457,8 +576,8 @@ export default function Roles() {
|
||||||
pagination={{ pageSize: 10, size: "small" }}
|
pagination={{ pageSize: 10, size: "small" }}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: t("users.userInfo"),
|
title: "用户信息",
|
||||||
render: (_: any, user: SysUser) => (
|
render: (_: unknown, user: SysUser) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
|
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -468,14 +587,14 @@ export default function Roles() {
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ title: t("rolesExt.phone"), dataIndex: "phone", className: "tabular-nums" },
|
{ title: "手机号", dataIndex: "phone", className: "tabular-nums" },
|
||||||
{ title: t("common.status"), dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? t("logsExt.success") : t("rolesExt.disabled")}</Tag> },
|
{ title: "状态", dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? "启用" : "停用"}</Tag> },
|
||||||
{
|
{
|
||||||
title: t("common.action"),
|
title: "操作",
|
||||||
key: "action",
|
key: "action",
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (_: any, user: SysUser) => (
|
render: (_: unknown, user: SysUser) => (
|
||||||
<Popconfirm title={t("rolesExt.removeBinding")} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={() => handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
|
<Popconfirm title="确定解除该用户绑定吗?" okText="确定" cancelText="取消" onConfirm={() => void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
|
||||||
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} />
|
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} />
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)
|
)
|
||||||
|
|
@ -487,51 +606,38 @@ export default function Roles() {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="app-page__empty-state"><Empty description={t("roles.selectRole")} /></div>
|
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal title={t("rolesExt.bindUsersToRole")} open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={handleAddUsers} okText={t("common.confirm")} cancelText={t("common.cancel")} width={650} destroyOnClose>
|
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Input placeholder={t("rolesExt.searchUser")} prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
|
<Input placeholder="搜索用户名或显示名称" prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
|
||||||
</div>
|
</div>
|
||||||
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ pageSize: 6 }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: t("rolesExt.displayName"), dataIndex: "displayName" }, { title: t("users.username"), dataIndex: "username" }, { title: t("rolesExt.phone"), dataIndex: "phone" }]} />
|
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ pageSize: 6 }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Drawer title={editing ? t("roles.drawerTitleEdit") : t("roles.drawerTitleCreate")} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submitBasic}>{t("common.save")}</Button></div>}>
|
<Drawer title={editing ? "编辑角色" : "新增角色"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{"取消"}</Button><Button type="primary" loading={saving} onClick={() => void submitBasic()}>{"保存"}</Button></div>}>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item label={t("rolesExt.tenantLabel")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
|
<Form.Item label="租户" name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
|
||||||
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} />
|
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("roles.roleName")} name="roleName" rules={[{ required: true }]}>
|
<Form.Item label="角色名称" name="roleName" rules={[{ required: true }]}>
|
||||||
<Input placeholder={t("rolesExt.enterRoleName")} />
|
<Input placeholder="请输入角色名称" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("roles.roleCode")} name="roleCode" rules={[{ required: true }]}>
|
<Form.Item label="角色编码" name="roleCode" rules={[{ required: true }]}>
|
||||||
<Input placeholder={t("rolesExt.roleCodePlaceholder")} disabled={!!editing} />
|
<Input placeholder="请输入角色编码" disabled={!!editing} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("common.status")} name="status" initialValue={1}>
|
<Form.Item label="状态" name="status" initialValue={1}>
|
||||||
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
|
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("common.remark")} name="remark">
|
<Form.Item label="备注" name="remark">
|
||||||
<Input.TextArea rows={4} placeholder={t("rolesExt.roleScopePlaceholder")} />
|
<Input.TextArea rows={4} placeholder="请输入角色说明或适用范围" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import { Avatar, Button, Card, Col, Form, Input, Row, Tabs, Tag, Typography, message } from "antd";
|
import { Alert, Avatar, Button, Card, Col, Descriptions, Form, Input, Row, Space, Tabs, Tag, Typography, message } from "antd";
|
||||||
import { LockOutlined, SaveOutlined, SolutionOutlined, UserOutlined } from "@ant-design/icons";
|
import { KeyOutlined, LockOutlined, ReloadOutlined, SaveOutlined, SolutionOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getCurrentUser, updateMyPassword, updateMyProfile } from "@/api";
|
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
import type { UserProfile } from "@/types";
|
import type { BotCredential, UserProfile } from "@/types";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Paragraph, Title, Text } = Typography;
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [credentialLoading, setCredentialLoading] = useState(false);
|
||||||
|
const [credentialSaving, setCredentialSaving] = useState(false);
|
||||||
const [user, setUser] = useState<UserProfile | null>(null);
|
const [user, setUser] = useState<UserProfile | null>(null);
|
||||||
|
const [credential, setCredential] = useState<BotCredential | null>(null);
|
||||||
const [profileForm] = Form.useForm();
|
const [profileForm] = Form.useForm();
|
||||||
const [pwdForm] = Form.useForm();
|
const [pwdForm] = Form.useForm();
|
||||||
|
|
||||||
|
|
@ -27,8 +30,19 @@ export default function Profile() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadCredential = async () => {
|
||||||
|
setCredentialLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getMyBotCredential();
|
||||||
|
setCredential(data);
|
||||||
|
} finally {
|
||||||
|
setCredentialLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUser();
|
loadUser();
|
||||||
|
loadCredential();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUpdateProfile = async () => {
|
const handleUpdateProfile = async () => {
|
||||||
|
|
@ -55,6 +69,19 @@ export default function Profile() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGenerateCredential = async () => {
|
||||||
|
try {
|
||||||
|
setCredentialSaving(true);
|
||||||
|
const data = await generateMyBotCredential();
|
||||||
|
setCredential(data);
|
||||||
|
message.success(t("common.success"));
|
||||||
|
} finally {
|
||||||
|
setCredentialSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderValue = (value?: string) => value || "-";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page app-page--contained" style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}>
|
<div className="app-page app-page--contained" style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}>
|
||||||
<PageHeader title={t("profile.title")} subtitle={t("profile.subtitle")} />
|
<PageHeader title={t("profile.title")} subtitle={t("profile.subtitle")} />
|
||||||
|
|
@ -134,6 +161,77 @@ export default function Profile() {
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "bot-credential",
|
||||||
|
label: <span><KeyOutlined /> {t("profile.botCredentialTab")}</span>,
|
||||||
|
children: (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message={t("profile.botCredentialHint")}
|
||||||
|
description={t("profile.botCredentialHintDesc")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Descriptions
|
||||||
|
bordered
|
||||||
|
size="middle"
|
||||||
|
column={1} items={[
|
||||||
|
{
|
||||||
|
key: "bind-status",
|
||||||
|
label: t("profile.botBindStatus"),
|
||||||
|
children: credential?.bound
|
||||||
|
? <Tag color="success">{t("profile.botBound")}</Tag>
|
||||||
|
: <Tag>{t("profile.botUnbound")}</Tag>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "bot-id",
|
||||||
|
label: "X-Bot-Id",
|
||||||
|
children: credential?.botId ? (
|
||||||
|
<Paragraph copyable={{ text: credential.botId }} style={{ marginBottom: 0 }}>
|
||||||
|
{credential.botId}
|
||||||
|
</Paragraph>
|
||||||
|
) : "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "bot-secret",
|
||||||
|
label: "X-Bot-Secret",
|
||||||
|
children: credential?.botSecret ? (
|
||||||
|
<Paragraph copyable={{ text: credential.botSecret }} style={{ marginBottom: 0 }}>
|
||||||
|
{credential.botSecret}
|
||||||
|
</Paragraph>
|
||||||
|
) : t("profile.botSecretHidden")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "last-access-time",
|
||||||
|
label: t("profile.botLastAccessTime"),
|
||||||
|
children: renderValue(credential?.lastAccessTime)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "last-access-ip",
|
||||||
|
label: t("profile.botLastAccessIp"),
|
||||||
|
children: renderValue(credential?.lastAccessIp)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="app-page__page-actions" style={{ margin: "8px 0 0" }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={credential?.bound ? <ReloadOutlined /> : <KeyOutlined />}
|
||||||
|
loading={credentialSaving}
|
||||||
|
onClick={handleGenerateCredential}
|
||||||
|
>
|
||||||
|
{credential?.bound
|
||||||
|
? t("profile.regenerateBotCredential")
|
||||||
|
: t("profile.generateBotCredential")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
@ -143,3 +241,5 @@ export default function Profile() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface SysUser extends BaseEntity {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
password?: string;
|
||||||
passwordHash?: string;
|
passwordHash?: string;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
orgId?: number;
|
orgId?: number;
|
||||||
|
|
@ -33,12 +34,28 @@ export interface UserProfile {
|
||||||
pwdResetRequired?: number;
|
pwdResetRequired?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BotCredential {
|
||||||
|
bound: boolean;
|
||||||
|
botId?: string;
|
||||||
|
botSecret?: string;
|
||||||
|
status?: string;
|
||||||
|
expireTime?: string;
|
||||||
|
lastAccessTime?: string;
|
||||||
|
lastAccessIp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SysRole extends BaseEntity {
|
export interface SysRole extends BaseEntity {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
roleCode: string;
|
roleCode: string;
|
||||||
roleName: string;
|
roleName: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
dataScopeType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleDataScope {
|
||||||
|
roleId?: number;
|
||||||
|
scopeType: string;
|
||||||
|
orgIds: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SysPermission extends BaseEntity {
|
export interface SysPermission extends BaseEntity {
|
||||||
|
|
@ -167,4 +184,3 @@ export interface MenuRoute {
|
||||||
element: ReactNode;
|
element: ReactNode;
|
||||||
perm?: string;
|
perm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue