diff --git a/src/api/rbac/data-perm.ts b/src/api/rbac/data-perm.ts new file mode 100644 index 0000000..935cfa0 --- /dev/null +++ b/src/api/rbac/data-perm.ts @@ -0,0 +1,303 @@ +/** + * 数据权限 API(契约见 docs/数据权限设计.md §十四) + * 后端未就绪时接口会 404,页面需 catch 并提示。 + */ +import { axiosInstance } from '@/api/rbac' +import { resolveAdminApiPath } from '@/utils/adminApi' + +function rbacDataPermUrl(path: string) { + return resolveAdminApiPath(`/rbac/data-perm${path.startsWith('/') ? path : `/${path}`}`) +} + +export interface EnumOption { + value: number | string + label: string +} + +export interface DataResourceMeta { + code: string + name: string + mainTable?: string + filterableFields?: string[] +} + +export interface DataPermMetaDto { + orgScopes: EnumOption[] + dataScopes: EnumOption[] + bizTypes: EnumOption[] + secretLevels: EnumOption[] + dataResources: DataResourceMeta[] + filterOperators: string[] +} + +export interface OrgPermItem { + id?: string + organizationId: string + organizationName?: string + orgScope: number + effectiveTime?: string | null + expireTime?: string | null + isTemporary?: boolean + isExpired?: boolean +} + +export interface DataScopeItem { + id?: string + dataScope: number + dataResourceCode?: string | null + expireTime?: string | null + isExpired?: boolean +} + +export interface BizPermItem { + id?: string + bizType: string + bizId: string + bizName?: string + expireTime?: string | null + isExpired?: boolean +} + +export interface SecretPermItem { + id?: string + secretLevel: number + dataResourceCode?: string | null + expireTime?: string | null + isExpired?: boolean +} + +export interface FilterCondition { + field: string + op: string + value?: unknown +} + +export interface FilterContent { + logic: 'and' | 'or' + conditions: FilterCondition[] +} + +export interface FilterRuleItem { + id?: string + dataResourceCode: string + filterContent: FilterContent + expireTime?: string | null + isExpired?: boolean +} + +export interface UserDataPermDto { + userId: string + userName?: string + organizationId?: string + organizationName?: string + orgPerms: OrgPermItem[] + dataScopes: DataScopeItem[] + bizPerms: BizPermItem[] + secretPerms: SecretPermItem[] + filterRules: FilterRuleItem[] +} + +export interface SaveUserDataPermInput { + orgPerms: Omit[] + dataScopes: Omit[] + bizPerms: Omit[] + secretPerms: Omit[] + filterRules: Omit[] +} + +export interface EffectiveDataScopeDto { + userId: string + isAdmin: boolean + dataResourceCode?: string + organizationIds?: string[] + organizationNames?: string[] + dataScope?: number + bizFilters?: { bizType: string; bizIds: string[] }[] + secretLevels?: number[] + summaryText?: string +} + +export interface RoleDataPermDto { + roleId: string + roleName?: string + orgPerms: OrgPermItem[] + dataScopes: DataScopeItem[] + bizPerms: BizPermItem[] + secretPerms: SecretPermItem[] + filterRules: FilterRuleItem[] +} + +export interface DataPermTemplateListItem { + id: string + name: string + code: string + description?: string + roleCode?: string | null + isBuiltIn?: boolean + itemCount?: number +} + +export interface DataPermTemplateItem { + id?: string + dimensionType: string + itemContent: Record + sortOrder?: number +} + +export interface DataPermTemplateDetailDto { + id: string + name: string + code: string + description?: string + roleCode?: string | null + isBuiltIn?: boolean + items: DataPermTemplateItem[] +} + +export interface BizOptionItem { + bizId: string + bizName: string + extra?: unknown +} + +export interface FieldMaskItem { + fieldName: string + secretLevel: number + maskType: string + maskPattern?: string | null +} + +export interface ChangeLogItem { + id: number + targetType: string + targetId: string + action: string + operatorUserId?: string + operatorMapId?: number + createTime: string +} + +export interface AccessLogItem { + id: number + userId: string + dataResourceCode: string + apiPath: string + rowCount?: number + createTime: string +} + +export interface PagedResult { + total: number + items: T[] +} + +export async function getDataPermMeta() { + const res = await axiosInstance.get(rbacDataPermUrl('/meta')) + return res.data +} + +export async function getBizOptions(params: { + bizType: string + keyword?: string + page?: number + pageSize?: number +}) { + const res = await axiosInstance.get>(rbacDataPermUrl('/biz-options'), { params }) + return res.data +} + +export async function getUserDataPerm(userId: string) { + const res = await axiosInstance.get(rbacDataPermUrl('/get-by-user'), { params: { userId } }) + return res.data +} + +export async function getEffectiveDataScope(userId: string, dataResourceCode = 'Order') { + const res = await axiosInstance.get(rbacDataPermUrl('/get-effective-by-user'), { + params: { userId, dataResourceCode }, + }) + return res.data +} + +export async function saveUserDataPerm(userId: string, body: SaveUserDataPermInput) { + const res = await axiosInstance.put(rbacDataPermUrl('/save-user'), body, { params: { userId } }) + return res.data +} + +export async function getRoleDataPerm(roleId: string) { + const res = await axiosInstance.get(rbacDataPermUrl('/get-by-role'), { params: { roleId } }) + return res.data +} + +export async function saveRoleDataPerm(roleId: string, body: SaveUserDataPermInput) { + const res = await axiosInstance.put(rbacDataPermUrl('/save-role'), body, { params: { roleId } }) + return res.data +} + +export async function getTemplateList(params: { keyword?: string; page?: number; pageSize?: number }) { + const res = await axiosInstance.get>(rbacDataPermUrl('/template/list'), { + params, + }) + return res.data +} + +export async function getTemplateDetail(id: string) { + const res = await axiosInstance.get(rbacDataPermUrl('/template/get'), { params: { id } }) + return res.data +} + +export async function createTemplate(body: { + name: string + code: string + description?: string + roleCode?: string + items: DataPermTemplateItem[] +}) { + const res = await axiosInstance.post(rbacDataPermUrl('/template/create'), body) + return res.data +} + +export async function updateTemplate(id: string, body: Parameters[0]) { + const res = await axiosInstance.put(rbacDataPermUrl('/template/update'), body, { + params: { id }, + }) + return res.data +} + +export async function deleteTemplate(id: string) { + await axiosInstance.delete(rbacDataPermUrl('/template/delete'), { params: { id } }) +} + +export async function applyTemplate(body: { + templateId: string + targetType: 'user' | 'role' + targetIds: string[] + mode: 'replace' | 'merge' + temporary?: { enabled: boolean; effectiveTime?: string; expireTime?: string } +}) { + const res = await axiosInstance.post<{ successCount: number; failures: { targetId: string; reason: string }[] }>( + rbacDataPermUrl('/template/apply'), + body + ) + return res.data +} + +export async function getFieldMaskList(dataResourceCode: string) { + const res = await axiosInstance.get<{ items: FieldMaskItem[] }>(rbacDataPermUrl('/field-mask/list'), { + params: { dataResourceCode }, + }) + return res.data +} + +export async function saveFieldMask(dataResourceCode: string, items: FieldMaskItem[]) { + await axiosInstance.put(rbacDataPermUrl('/field-mask/save'), { dataResourceCode, items }) +} + +export async function getChangeLogList(params: Record) { + const res = await axiosInstance.get>(rbacDataPermUrl('/change-log/list'), { params }) + return res.data +} + +export async function getAccessLogList(params: Record) { + const res = await axiosInstance.get>(rbacDataPermUrl('/access-log/list'), { params }) + return res.data +} diff --git a/src/components.d.ts b/src/components.d.ts index 955070d..77e9b31 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -38,6 +38,7 @@ declare module 'vue' { Package: typeof import('./components/CustomSelectFilter/Package/index.vue')['default'] PaginationPro: typeof import('./components/PaginationPro/index.vue')['default'] QuantityInput: typeof import('./components/QuantityInput/index.vue')['default'] + RbacDetailShell: typeof import('./components/RbacSplit/RbacDetailShell.vue')['default'] RightTopMessage: typeof import('./components/top-nav/RightTopMessage.vue')['default'] RightTopUser: typeof import('./components/top-nav/RightTopUser.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/src/components/RbacSplit/RbacDetailShell.vue b/src/components/RbacSplit/RbacDetailShell.vue new file mode 100644 index 0000000..7ca0cac --- /dev/null +++ b/src/components/RbacSplit/RbacDetailShell.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/src/components/RbacSplit/rbac-split-layout.less b/src/components/RbacSplit/rbac-split-layout.less new file mode 100644 index 0000000..e27e7ca --- /dev/null +++ b/src/components/RbacSplit/rbac-split-layout.less @@ -0,0 +1,85 @@ +.rbac-split-layout { + display: flex; + height: calc(100vh - 86px); + max-height: calc(100vh - 86px); + padding: 16px; + gap: 16px; + background: #f0f3f4; + overflow: hidden; + box-sizing: border-box; + width: 100%; +} + +.rbac-split-layout .left-sidebar { + width: 280px; + flex-shrink: 0; + height: 100%; +} + +.rbac-split-layout .right-panel { + flex: 1; + min-width: 0; + height: 100%; + border-radius: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.rbac-split-layout .section-card { + background: var(--color-fill-1); + border: 1px solid var(--color-border-2); + border-radius: 8px; + padding: 16px 20px; + margin-bottom: 16px; +} + +.rbac-split-layout .section-card-title { + font-size: 14px; + font-weight: 600; + color: var(--color-text-1); + margin: 0 0 12px; +} + +.rbac-split-layout .sticky-toolbar { + position: sticky; + top: 0; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + margin: -4px 0 16px; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(8px); + border: 1px solid var(--color-border-2); + border-radius: 8px; + flex-wrap: wrap; +} + +.rbac-split-layout .avatar-circle { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, var(--app-primary-color, #2d8c8c) 0%, #3aa8a8 100%); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 600; +} + +.rbac-split-layout .icon-badge-circle { + width: 48px; + height: 48px; + border-radius: 12px; + background: rgba(var(--primary-6), 0.1); + color: var(--app-primary-color, #2d8c8c); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 0b7194a..85d3cff 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -144,6 +144,60 @@ export default { 'menu.system.warehouseAddress': 'Warehouse Address', 'menu.system.batchEmail': 'Batch Email', 'menu.system.xhsAiAssistant': 'AI Assistant', + 'menu.system.dataPerm': 'Data Permission', + 'menu.system.dataPermUsers': 'User Data Permission', + 'menu.system.dataPermTemplates': 'Permission Templates', + 'menu.system.dataPermAudit': 'Permission Audit', + 'system.dataPerm.users.title': 'User Data Permission', + 'system.dataPerm.users.desc': 'Configure direct five-dimension permissions for internal users', + 'system.dataPerm.users.loading': 'Loading users...', + 'system.dataPerm.users.emptySelect': 'Select a user on the left', + 'system.dataPerm.users.emptyDesc': 'Configure org, data scope, biz tags, secret level and custom filters for internal users', + 'system.dataPerm.panel.primaryOrg': 'Primary organization', + 'system.dataPerm.panel.configType': 'Config type', + 'system.dataPerm.panel.configTypeDirect': 'Direct user config', + 'system.dataPerm.panel.previewModule': 'Preview module', + 'system.dataPerm.panel.cannotEditSelf': 'You cannot edit your own data permissions', + 'system.dataPerm.panel.stat.total': 'Direct items', + 'system.dataPerm.panel.tab.org': 'Organization', + 'system.dataPerm.panel.tab.scope': 'Data scope', + 'system.dataPerm.panel.tab.biz': 'Business tags', + 'system.dataPerm.panel.tab.secret': 'Secret level', + 'system.dataPerm.panel.tab.filter': 'Custom filter', + 'system.dataPerm.templates.add': 'New template', + 'system.dataPerm.templates.search': 'Search name or code', + 'system.dataPerm.templates.title': 'Permission templates', + 'system.dataPerm.templates.loading': 'Loading templates...', + 'system.dataPerm.templates.emptySelect': 'Select a template on the left, or click Add', + 'system.dataPerm.templates.emptyDesc': 'Templates define five-dimension permission rules and can be applied to users or roles', + 'system.dataPerm.templates.save': 'Save', + 'system.dataPerm.templates.apply': 'Apply template', + 'system.dataPerm.templates.field.name': 'Name', + 'system.dataPerm.templates.field.code': 'Code', + 'system.dataPerm.templates.field.description': 'Description', + 'system.dataPerm.templates.field.roleCode': 'Bound role code', + 'system.dataPerm.templates.roleCodePlaceholder': 'Optional, auto-apply when role is assigned', + 'system.dataPerm.templates.itemsTitle': 'Template items', + 'system.dataPerm.templates.itemJsonPlaceholder': 'JSON, e.g. {"dataScope":1}', + 'system.dataPerm.templates.addItem': 'Add item row', + 'system.dataPerm.templates.removeItem': 'Remove', + 'system.dataPerm.templates.nameCodeRequired': 'Name and code are required', + 'system.dataPerm.templates.invalidJson': 'Invalid JSON in template items', + 'system.dataPerm.templates.createSuccess': 'Created', + 'system.dataPerm.templates.updateSuccess': 'Updated', + 'system.dataPerm.templates.deleteSuccess': 'Deleted', + 'system.dataPerm.templates.builtinDeleteDenied': 'Built-in templates cannot be deleted', + 'system.dataPerm.templates.deleteConfirm': 'Delete template "{name}"?', + 'system.dataPerm.templates.tab.basic': 'Basic info', + 'system.dataPerm.templates.tab.items': 'Template items', + 'system.dataPerm.templates.itemsHint': 'Each row is a JSON config for one permission dimension', + 'system.dataPerm.templates.itemsEmpty': 'No items yet. Click below to add one.', + 'system.dataPerm.templates.jsonPreset': 'JSON preset', + 'system.dataPerm.templates.dimensionPlaceholder': 'Select dimension', + 'system.dataPerm.templates.tag.creating': 'Creating', + 'system.dataPerm.templates.tag.builtin': 'Built-in', + 'system.dataPerm.templates.stat.items': 'Items', + 'system.dataPerm.templates.stat.dimensions': 'Dimensions', 'menu.xhs': 'Xiaohongshu', 'menu.xhs.startService': 'Start Service', @@ -775,6 +829,13 @@ export default { 'system.role.actions.add': 'Add', 'system.role.actions.edit': 'Edit', 'system.role.actions.delete': 'Delete', + 'system.role.actions.save': 'Save', + 'system.role.emptySelect': 'Select a role on the left, or click Add', + 'system.role.emptyDesc': 'Edit basic info and assign permissions after selecting a role', + 'system.role.pageTitle': 'Role management', + 'system.role.tab.info': 'Basic info', + 'system.role.loading': 'Loading roles...', + 'system.role.permission.save': 'Save permissions', 'system.role.status.enabled': 'Enabled', 'system.role.status.disabled': 'Disabled', 'system.role.category.business': 'Business role', @@ -920,6 +981,13 @@ export default { 'system.role.permission.uncheckAll': 'Uncheck All', 'system.role.permission.loading': 'Loading permission data...', 'system.role.permission.assignSuccess': 'Permission assigned successfully', + 'system.role.permission.search': 'Search permission name or code', + 'system.role.permission.selected': 'Selected', + 'system.role.permission.total': 'Total', + 'system.role.permission.selectedCount': '{selected} / {total} selected', + 'system.role.permission.noMatch': 'No matching permissions', + 'system.role.tag.creating': 'Creating', + 'system.role.tag.boundWarehouse': 'Warehouse bound', 'system.role.message.deleteSuccess': 'Delete successful', 'system.role.message.updateSuccess': 'Update successful', 'system.role.message.createSuccess': 'Create successful', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 623a5cf..a1eb36f 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -145,6 +145,10 @@ export default { 'menu.system.warehouseAddress': '仓库地址', 'menu.system.batchEmail': '批量发送邮件', 'menu.system.xhsAiAssistant': 'AI 助手', + 'menu.system.dataPerm': '数据权限', + 'menu.system.dataPermUsers': '用户数据权限', + 'menu.system.dataPermTemplates': '权限模板', + 'menu.system.dataPermAudit': '权限审计', 'menu.xhs': '小红书', 'menu.xhs.startService': '启动服务', @@ -776,6 +780,13 @@ export default { 'system.role.actions.add': '新增角色', 'system.role.actions.edit': '修改', 'system.role.actions.delete': '删除', + 'system.role.actions.save': '保存', + 'system.role.emptySelect': '请从左侧选择角色,或点击新增', + 'system.role.emptyDesc': '选择角色后可编辑基本信息与分配功能权限', + 'system.role.pageTitle': '角色管理', + 'system.role.tab.info': '基本信息', + 'system.role.loading': '正在加载角色列表...', + 'system.role.permission.save': '保存权限', 'system.role.status.enabled': '启用', 'system.role.status.disabled': '禁用', 'system.role.category.business': '业务角色', @@ -921,6 +932,13 @@ export default { 'system.role.permission.uncheckAll': '取消全选', 'system.role.permission.loading': '加载权限数据中...', 'system.role.permission.assignSuccess': '权限分配成功', + 'system.role.permission.search': '搜索权限名称或编码', + 'system.role.permission.selected': '已选', + 'system.role.permission.total': '总计', + 'system.role.permission.selectedCount': '已选 {selected} / 共 {total}', + 'system.role.permission.noMatch': '未找到匹配的权限', + 'system.role.tag.creating': '新建中', + 'system.role.tag.boundWarehouse': '已绑仓库', 'system.role.message.deleteSuccess': '删除成功', 'system.role.message.updateSuccess': '更新成功', 'system.role.message.createSuccess': '创建成功', @@ -1045,6 +1063,57 @@ export default { 'system.dict.actions.delete': '删除', 'system.dict.actions.batchImport': '批量导入', 'system.dict.actions.batchExport': '批量导出', + + 'system.dataPerm.users.title': '用户数据权限', + 'system.dataPerm.users.desc': '配置内部用户直配五维权限;运行时 = 角色继承 ∪ 用户直配', + 'system.dataPerm.users.loading': '正在加载用户列表...', + 'system.dataPerm.users.emptySelect': '请从左侧选择用户', + 'system.dataPerm.users.emptyDesc': '选择内部用户后,可配置组织、数据范围、业务标签、密级与自定义过滤五维权限', + 'system.dataPerm.panel.primaryOrg': '主归属组织', + 'system.dataPerm.panel.configType': '配置类型', + 'system.dataPerm.panel.configTypeDirect': '用户直配', + 'system.dataPerm.panel.previewModule': '预览模块', + 'system.dataPerm.panel.cannotEditSelf': '不能修改自己的数据权限', + 'system.dataPerm.panel.stat.total': '直配项', + 'system.dataPerm.panel.tab.org': '组织', + 'system.dataPerm.panel.tab.scope': '数据范围', + 'system.dataPerm.panel.tab.biz': '业务标签', + 'system.dataPerm.panel.tab.secret': '密级', + 'system.dataPerm.panel.tab.filter': '自定义过滤', + 'system.dataPerm.templates.add': '新建模板', + 'system.dataPerm.templates.search': '搜索模板名称/编码', + 'system.dataPerm.templates.title': '权限模板', + 'system.dataPerm.templates.loading': '正在加载模板列表...', + 'system.dataPerm.templates.emptySelect': '请从左侧选择模板,或点击新建', + 'system.dataPerm.templates.emptyDesc': '权限模板可批量定义五维权限规则,并一键套用到用户或角色', + 'system.dataPerm.templates.save': '保存', + 'system.dataPerm.templates.apply': '套用模板', + 'system.dataPerm.templates.field.name': '名称', + 'system.dataPerm.templates.field.code': '编码', + 'system.dataPerm.templates.field.description': '说明', + 'system.dataPerm.templates.field.roleCode': '绑定角色 Code', + 'system.dataPerm.templates.roleCodePlaceholder': '可选,分配角色时自动套用', + 'system.dataPerm.templates.itemsTitle': '模板明细', + 'system.dataPerm.templates.itemJsonPlaceholder': 'JSON,如 {"dataScope":1}', + 'system.dataPerm.templates.addItem': '添加明细行', + 'system.dataPerm.templates.removeItem': '删', + 'system.dataPerm.templates.nameCodeRequired': '请填写名称和编码', + 'system.dataPerm.templates.invalidJson': '明细 JSON 格式不正确', + 'system.dataPerm.templates.createSuccess': '创建成功', + 'system.dataPerm.templates.updateSuccess': '更新成功', + 'system.dataPerm.templates.deleteSuccess': '删除成功', + 'system.dataPerm.templates.builtinDeleteDenied': '内置模板不可删除', + 'system.dataPerm.templates.deleteConfirm': '确定删除模板「{name}」?', + 'system.dataPerm.templates.tab.basic': '基本信息', + 'system.dataPerm.templates.tab.items': '模板明细', + 'system.dataPerm.templates.itemsHint': '每条明细对应一个维度的权限 JSON 配置', + 'system.dataPerm.templates.itemsEmpty': '暂无明细,点击下方按钮添加', + 'system.dataPerm.templates.jsonPreset': 'JSON 预设', + 'system.dataPerm.templates.dimensionPlaceholder': '选择维度', + 'system.dataPerm.templates.tag.creating': '新建中', + 'system.dataPerm.templates.tag.builtin': '内置', + 'system.dataPerm.templates.stat.items': '明细数', + 'system.dataPerm.templates.stat.dimensions': '维度数', 'system.dict.import.modalTitle': '批量导入字典', 'system.dict.import.tip': '请先下载模板或 OMS 预置字典,按列填写后上传 Excel(.xlsx/.xls)。同一字典类型下字典编码唯一;已存在的「类型+编码」将自动跳过。', diff --git a/src/router/routes/newModules/system-management.ts b/src/router/routes/newModules/system-management.ts index 8206842..f0a28d8 100644 --- a/src/router/routes/newModules/system-management.ts +++ b/src/router/routes/newModules/system-management.ts @@ -109,6 +109,36 @@ const SYSTEM_MANAGEMENT: AppRouteRecordRaw = { permission: 'Xhs.Ai.Use', }, }, + { + path: 'dataPerm/users', + name: 'SystemDataPermUsers', + component: () => import('@/views/system/dataPerm/users/index.vue'), + meta: { + locale: 'menu.system.dataPermUsers', + requiresAuth: true, + permission: 'Rbac.DataPerm.Query', + }, + }, + { + path: 'dataPerm/templates', + name: 'SystemDataPermTemplates', + component: () => import('@/views/system/dataPerm/templates/index.vue'), + meta: { + locale: 'menu.system.dataPermTemplates', + requiresAuth: true, + permission: 'Rbac.DataPerm.Template', + }, + }, + { + path: 'dataPerm/audit', + name: 'SystemDataPermAudit', + component: () => import('@/views/system/dataPerm/audit/index.vue'), + meta: { + locale: 'menu.system.dataPermAudit', + requiresAuth: true, + permission: 'Rbac.DataPerm.Audit', + }, + }, ], }; diff --git a/src/utils/rbacLeftMenuTree.ts b/src/utils/rbacLeftMenuTree.ts new file mode 100644 index 0000000..c9b85d8 --- /dev/null +++ b/src/utils/rbacLeftMenuTree.ts @@ -0,0 +1,40 @@ +import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types' + +export function buildFlatLeftMenuTree( + categoryKey: string, + categoryTitle: string, + items: Array<{ id?: string; name?: string | null; account?: string | null; code?: string | null }>, + options?: { titleKey?: 'name' | 'account'; codeKey?: 'code' | 'account' } +): CustomLeftMenuTreeNode[] { + const titleKey = options?.titleKey ?? 'name' + const codeKey = options?.codeKey ?? 'code' + const leaves = items + .filter((item) => item.id) + .map((item) => ({ + key: `item-${item.id}`, + title: String(item[titleKey] || item.account || item.name || item.id), + isLeaf: true, + id: item.id, + name: item.name ?? undefined, + code: String(item[codeKey as keyof typeof item] || item.code || item.account || ''), + })) + + if (leaves.length === 0) return [] + + return [ + { + key: categoryKey, + title: categoryTitle, + children: leaves, + }, + ] +} + +export function findLeftMenuItemByKey( + key: string, + items: T[] +): T | undefined { + if (!key.startsWith('item-')) return undefined + const id = key.slice(5) + return items.find((item) => item.id === id) +} diff --git a/src/views/system/dataPerm/audit/index.vue b/src/views/system/dataPerm/audit/index.vue new file mode 100644 index 0000000..7be1ea8 --- /dev/null +++ b/src/views/system/dataPerm/audit/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/src/views/system/dataPerm/components/BizPermPanel.vue b/src/views/system/dataPerm/components/BizPermPanel.vue new file mode 100644 index 0000000..7b90263 --- /dev/null +++ b/src/views/system/dataPerm/components/BizPermPanel.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/src/views/system/dataPerm/components/DataPermDrawer.vue b/src/views/system/dataPerm/components/DataPermDrawer.vue new file mode 100644 index 0000000..5628825 --- /dev/null +++ b/src/views/system/dataPerm/components/DataPermDrawer.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/views/system/dataPerm/components/DataPermPanel.vue b/src/views/system/dataPerm/components/DataPermPanel.vue new file mode 100644 index 0000000..11018c1 --- /dev/null +++ b/src/views/system/dataPerm/components/DataPermPanel.vue @@ -0,0 +1,443 @@ + + + + + diff --git a/src/views/system/dataPerm/components/DataScopePanel.vue b/src/views/system/dataPerm/components/DataScopePanel.vue new file mode 100644 index 0000000..7939251 --- /dev/null +++ b/src/views/system/dataPerm/components/DataScopePanel.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/views/system/dataPerm/components/EffectiveScopeBanner.vue b/src/views/system/dataPerm/components/EffectiveScopeBanner.vue new file mode 100644 index 0000000..e881815 --- /dev/null +++ b/src/views/system/dataPerm/components/EffectiveScopeBanner.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/views/system/dataPerm/components/FilterRulePanel.vue b/src/views/system/dataPerm/components/FilterRulePanel.vue new file mode 100644 index 0000000..5088dfe --- /dev/null +++ b/src/views/system/dataPerm/components/FilterRulePanel.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/views/system/dataPerm/components/OrgPermPanel.vue b/src/views/system/dataPerm/components/OrgPermPanel.vue new file mode 100644 index 0000000..05b1599 --- /dev/null +++ b/src/views/system/dataPerm/components/OrgPermPanel.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/views/system/dataPerm/components/SecretPermPanel.vue b/src/views/system/dataPerm/components/SecretPermPanel.vue new file mode 100644 index 0000000..6ceb41f --- /dev/null +++ b/src/views/system/dataPerm/components/SecretPermPanel.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/views/system/dataPerm/constants.ts b/src/views/system/dataPerm/constants.ts new file mode 100644 index 0000000..2b0b047 --- /dev/null +++ b/src/views/system/dataPerm/constants.ts @@ -0,0 +1,44 @@ +/** 前端兜底枚举(meta 接口失败时使用) */ +export const DEFAULT_ORG_SCOPES = [ + { value: 1, label: '仅本级' }, + { value: 2, label: '本级+下级' }, +] + +export const DEFAULT_DATA_SCOPES = [ + { value: 1, label: '个人级' }, + { value: 2, label: '小组级' }, + { value: 3, label: '部门级' }, + { value: 4, label: '组织树级' }, + { value: 5, label: '全局级' }, +] + +export const DEFAULT_SECRET_LEVELS = [ + { value: 1, label: '公开' }, + { value: 2, label: '内部' }, + { value: 3, label: '机密' }, + { value: 4, label: '绝密' }, +] + +export const DEFAULT_BIZ_TYPES = [ + { value: 'product_line', label: '产品线' }, + { value: 'kpi_area', label: 'KPI分区' }, + { value: 'customer_group', label: '客户分层' }, + { value: 'warehouse', label: '仓库' }, + { value: 'region', label: '区域' }, +] + +export const FILTER_OPERATORS = ['eq', 'ne', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte', 'between', 'like'] + +export function emptySaveInput() { + return { + orgPerms: [] as any[], + dataScopes: [] as any[], + bizPerms: [] as any[], + secretPerms: [] as any[], + filterRules: [] as any[], + } +} + +export function emptyFilterContent() { + return { logic: 'and' as const, conditions: [] as { field: string; op: string; value?: unknown }[] } +} diff --git a/src/views/system/dataPerm/templates/index.vue b/src/views/system/dataPerm/templates/index.vue new file mode 100644 index 0000000..47b510e --- /dev/null +++ b/src/views/system/dataPerm/templates/index.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/src/views/system/dataPerm/users/index.vue b/src/views/system/dataPerm/users/index.vue index 794f9e9..7f1bc3d 100644 --- a/src/views/system/dataPerm/users/index.vue +++ b/src/views/system/dataPerm/users/index.vue @@ -1,86 +1,85 @@ - diff --git a/src/views/system/permission/role/index.vue b/src/views/system/permission/role/index.vue index 9a5d50b..49e85f0 100644 --- a/src/views/system/permission/role/index.vue +++ b/src/views/system/permission/role/index.vue @@ -1,226 +1,180 @@ - -