调整数据授权管理的前端优化

main
kangwenjing 2026-07-01 17:48:21 +08:00
parent aa632ec6ec
commit 09098a180c
2 changed files with 108 additions and 5 deletions

View File

@ -1,5 +1,6 @@
.user-data-scope-page { .user-data-scope-page {
min-height: 100%; min-height: 100%;
overflow: hidden;
} }
.user-data-scope-overview { .user-data-scope-overview {
@ -38,7 +39,45 @@
} }
.user-data-scope-table-card { .user-data-scope-table-card {
flex: 1;
min-height: 0;
overflow: hidden; overflow: hidden;
.ant-card-body {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
.user-data-scope-table {
min-height: 0;
display: flex;
flex: 1;
flex-direction: column;
.ant-spin-nested-loading,
.ant-spin-container {
min-height: 0;
display: flex;
flex: 1;
flex-direction: column;
}
.ant-table {
min-height: 0;
flex: 1;
}
.ant-table-container {
min-height: 0;
}
.ant-table-pagination {
flex-shrink: 0;
margin: 12px 16px !important;
}
} }
@media (max-width: 991px) { @media (max-width: 991px) {

View File

@ -4,10 +4,10 @@ import dayjs from "dayjs";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from "@ant-design/icons"; import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from "@ant-design/icons";
import PageHeader from "@/components/shared/PageHeader"; import PageHeader from "@/components/shared/PageHeader";
import { deleteUserDataScopeGrant, listTenants, listUserDataScopeGrants, listUserDataScopeUsers, saveUserDataScopeAssignment } from "@/api"; import { deleteUserDataScopeGrant, fetchUsersByRoleId, listRoles, listTenants, listUserDataScopeGrants, listUserDataScopeUsers, saveUserDataScopeAssignment } from "@/api";
import { usePermission } from "@/hooks/usePermission"; import { usePermission } from "@/hooks/usePermission";
import { getStandardPagination } from "@/utils/pagination"; import { getStandardPagination } from "@/utils/pagination";
import type { SysTenant, UserDataScopeGrant, UserDataScopeUser } from "@/types"; import type { SysRole, SysTenant, UserDataScopeGrant, UserDataScopeUser } from "@/types";
import "./index.less"; import "./index.less";
const { Text } = Typography; const { Text } = Typography;
@ -141,7 +141,9 @@ export default function UserDataScopePage() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [roleUserLoading, setRoleUserLoading] = useState(false);
const [users, setUsers] = useState<UserDataScopeUser[]>([]); const [users, setUsers] = useState<UserDataScopeUser[]>([]);
const [roles, setRoles] = useState<SysRole[]>([]);
const [grants, setGrants] = useState<UserDataScopeGrant[]>([]); const [grants, setGrants] = useState<UserDataScopeGrant[]>([]);
const [tenants, setTenants] = useState<SysTenant[]>([]); const [tenants, setTenants] = useState<SysTenant[]>([]);
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
@ -170,6 +172,15 @@ export default function UserDataScopePage() {
setUsers(list || []); setUsers(list || []);
}, [selectedTenantId]); }, [selectedTenantId]);
const loadRoles = useCallback(async () => {
if (!selectedTenantId) {
setRoles([]);
return;
}
const list = await listRoles(selectedTenantId);
setRoles(list || []);
}, [selectedTenantId]);
const loadGrants = useCallback(async () => { const loadGrants = useCallback(async () => {
if (!selectedTenantId) { if (!selectedTenantId) {
setGrants([]); setGrants([]);
@ -182,11 +193,11 @@ export default function UserDataScopePage() {
const loadPageData = useCallback(async () => { const loadPageData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
await Promise.all([loadTenants(), loadUsers(), loadGrants()]); await Promise.all([loadTenants(), loadUsers(), loadRoles(), loadGrants()]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [loadTenants, loadUsers, loadGrants]); }, [loadTenants, loadUsers, loadRoles, loadGrants]);
useEffect(() => { useEffect(() => {
loadPageData(); loadPageData();
@ -244,12 +255,28 @@ export default function UserDataScopePage() {
[selectedDrawerViewerId, userOptions] [selectedDrawerViewerId, userOptions]
); );
const roleOptions = useMemo(() => roles.map((role) => {
const label = role.roleName || role.roleCode || String(role.roleId);
return {
label: (
<Space direction="vertical" size={0}>
<span>{label}</span>
{role.roleCode && role.roleCode !== label && <Text type="secondary" style={{ fontSize: 12 }}>{role.roleCode}</Text>}
</Space>
),
displayLabel: label,
value: role.roleId,
searchText: `${role.roleName || ""} ${role.roleCode || ""}`
};
}), [roles]);
const openCreate = () => { const openCreate = () => {
setEditingGrantGroup(null); setEditingGrantGroup(null);
form.resetFields(); form.resetFields();
form.setFieldsValue({ form.setFieldsValue({
resourceTypes: ["OPPORTUNITY"], resourceTypes: ["OPPORTUNITY"],
enabled: true, enabled: true,
roleIds: [],
ownerUserIds: [] ownerUserIds: []
}); });
setDrawerOpen(true); setDrawerOpen(true);
@ -261,6 +288,7 @@ export default function UserDataScopePage() {
form.setFieldsValue({ form.setFieldsValue({
viewerUserId: record.viewerUserId, viewerUserId: record.viewerUserId,
resourceTypes: record.resourceTypes, resourceTypes: record.resourceTypes,
roleIds: [],
ownerUserIds: record.ownerUsers.map((owner) => owner.userId), ownerUserIds: record.ownerUsers.map((owner) => owner.userId),
enabled: record.enabled, enabled: record.enabled,
expireAt: record.expireAt ? dayjs(record.expireAt) : undefined, expireAt: record.expireAt ? dayjs(record.expireAt) : undefined,
@ -274,6 +302,27 @@ export default function UserDataScopePage() {
setEditingGrantGroup(null); setEditingGrantGroup(null);
}; };
const handleRoleSelectChange = async (roleIds: number[]) => {
form.setFieldValue("roleIds", roleIds);
if (!roleIds.length) {
return;
}
const selectedViewerUserId = form.getFieldValue("viewerUserId");
const selectedOwnerIds = new Set<number>(form.getFieldValue("ownerUserIds") || []);
setRoleUserLoading(true);
try {
const roleUsers = await Promise.all(roleIds.map((roleId) => fetchUsersByRoleId(roleId)));
roleUsers.flat().forEach((user) => {
if (user?.userId && user.userId !== selectedViewerUserId) {
selectedOwnerIds.add(user.userId);
}
});
form.setFieldValue("ownerUserIds", Array.from(selectedOwnerIds));
} finally {
setRoleUserLoading(false);
}
};
const handleSave = async () => { const handleSave = async () => {
if (!selectedTenantId) { if (!selectedTenantId) {
message.warning("请先选择租户"); message.warning("请先选择租户");
@ -469,12 +518,13 @@ export default function UserDataScopePage() {
<Card className="app-page__content-card user-data-scope-table-card" styles={{ body: { padding: 0 } }}> <Card className="app-page__content-card user-data-scope-table-card" styles={{ body: { padding: 0 } }}>
<Table <Table
className="user-data-scope-table"
rowKey="id" rowKey="id"
loading={loading} loading={loading}
columns={columns} columns={columns}
dataSource={pagedGrantGroups} dataSource={pagedGrantGroups}
size="middle" size="middle"
scroll={{ y: "calc(100vh - 390px)", x: 980 }} scroll={{ y: "max(260px, calc(100vh - 470px))", x: 980 }}
pagination={getStandardPagination( pagination={getStandardPagination(
filteredGrantGroups.length, filteredGrantGroups.length,
query.current, query.current,
@ -506,6 +556,20 @@ export default function UserDataScopePage() {
<Form.Item label="资源范围" name="resourceTypes" rules={[{ required: true, message: "请选择资源范围" }]}> <Form.Item label="资源范围" name="resourceTypes" rules={[{ required: true, message: "请选择资源范围" }]}>
<Select mode="multiple" allowClear options={resourceOptions} maxTagCount="responsive" /> <Select mode="multiple" allowClear options={resourceOptions} maxTagCount="responsive" />
</Form.Item> </Form.Item>
<Form.Item label="按角色添加" name="roleIds">
<Select
mode="multiple"
showSearch
allowClear
placeholder="选择角色后自动加入该角色下人员"
options={roleOptions}
loading={roleUserLoading}
optionFilterProp="searchText"
optionLabelProp="displayLabel"
maxTagCount="responsive"
onChange={handleRoleSelectChange}
/>
</Form.Item>
<Form.Item label="可见人员" name="ownerUserIds" rules={[{ required: true, message: "请选择可见人员" }]}> <Form.Item label="可见人员" name="ownerUserIds" rules={[{ required: true, message: "请选择可见人员" }]}>
<Select <Select
mode="multiple" mode="multiple"