diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts
index 85d3cff..8f8f895 100644
--- a/src/locale/en-US.ts
+++ b/src/locale/en-US.ts
@@ -176,8 +176,73 @@ export default {
'system.dataPerm.templates.field.code': 'Code',
'system.dataPerm.templates.field.description': 'Description',
'system.dataPerm.templates.field.roleCode': 'Bound role code',
+ 'system.dataPerm.templates.field.bindRole': 'Linked role',
+ 'system.dataPerm.templates.field.bindRolePlaceholder': 'Select an RBAC role',
+ 'system.dataPerm.templates.field.bindRoleExtra': 'Saved as roleCode — identifies which job role this template belongs to',
+ 'system.dataPerm.templates.section.bindApply': 'Role link & apply',
+ 'system.dataPerm.templates.section.applyNow': 'Apply now',
+ 'system.dataPerm.templates.bindApplyHint': 'Linking a role is metadata only. Use "Apply now" below to write template items into role or user permissions. Linking does not apply by itself.',
+ 'system.dataPerm.templates.applyToRole': 'Sync to linked role',
+ 'system.dataPerm.templates.applyToRoleDesc': 'Write template items into the role\'s data permissions (replaces role direct config)',
+ 'system.dataPerm.templates.applyToRoleBtn': 'Write to role',
+ 'system.dataPerm.templates.applyToUsers': 'Apply to users',
+ 'system.dataPerm.templates.applyToUsersDesc': 'Batch write direct user data permissions; replace or merge',
+ 'system.dataPerm.templates.applyToUsersBtn': 'Apply to users',
+ 'system.dataPerm.templates.field.targetUsers': 'Target users',
+ 'system.dataPerm.templates.field.targetUsersPlaceholder': 'Search and select internal users',
+ 'system.dataPerm.templates.field.applyMode': 'Apply mode',
+ 'system.dataPerm.templates.field.temporary': 'Temporary grant (users only)',
+ 'system.dataPerm.templates.field.effectiveTime': 'Effective from',
+ 'system.dataPerm.templates.field.expireTime': 'Expires at',
+ 'system.dataPerm.templates.mode.replace': 'Replace direct config',
+ 'system.dataPerm.templates.mode.merge': 'Merge with existing',
+ 'system.dataPerm.templates.applyNeedSave': 'Save the template before applying',
+ 'system.dataPerm.templates.bindRoleFirst': 'Select a linked role first',
+ 'system.dataPerm.templates.selectUsersFirst': 'Select target users',
+ 'system.dataPerm.templates.expireTimeRequired': 'Expire time is required for temporary grants',
+ 'system.dataPerm.templates.applySuccess': 'Applied to {count} target(s)',
+ 'system.dataPerm.templates.applyPartial': '{success} succeeded, {fail} failed',
'system.dataPerm.templates.roleCodePlaceholder': 'Optional, auto-apply when role is assigned',
- 'system.dataPerm.templates.itemsTitle': 'Template items',
+ 'system.dataPerm.templates.itemsTitle': '模板明细',
+ 'system.dataPerm.templates.itemsHintForm': 'Select a permission type and fill the form — no JSON required',
+ 'system.dataPerm.templates.addExample': 'Add example preset',
+ 'system.dataPerm.templates.exampleAdded': 'Added example "{name}"',
+ 'system.dataPerm.templates.itemInvalid': 'Item #{index} is incomplete: {field}',
+ 'system.dataPerm.templates.example.salesStaff': 'Sales staff',
+ 'system.dataPerm.templates.example.salesStaffDesc': 'Personal data scope + public/internal secret levels',
+ 'system.dataPerm.templates.example.salesSupervisor': 'Sales supervisor',
+ 'system.dataPerm.templates.example.salesSupervisorDesc': 'Department scope + secret levels 1–3',
+ 'system.dataPerm.templates.example.branchManager': 'Branch manager',
+ 'system.dataPerm.templates.example.branchManagerDesc': 'Org-tree scope + secret levels 1–3',
+ 'system.dataPerm.templates.form.org': 'Organization',
+ 'system.dataPerm.templates.form.orgPlaceholder': 'Select organization',
+ 'system.dataPerm.templates.form.orgScope': 'Org scope',
+ 'system.dataPerm.templates.form.dataScope': 'Data visibility',
+ 'system.dataPerm.templates.form.module': 'Module',
+ 'system.dataPerm.templates.form.moduleAll': 'All modules (leave empty)',
+ 'system.dataPerm.templates.form.bizType': 'Tag type',
+ 'system.dataPerm.templates.form.bizTag': 'Tag value',
+ 'system.dataPerm.templates.form.bizTagPlaceholder': 'Select tag',
+ 'system.dataPerm.templates.form.secretLevels': 'Allowed secret levels',
+ 'system.dataPerm.templates.form.filterField': 'Field',
+ 'system.dataPerm.templates.form.filterValuePlaceholder': 'e.g. Paid or 1,2 (comma-separated)',
+ 'system.dataPerm.templates.form.addCondition': 'Add condition',
+ 'system.dataPerm.templates.form.orgExample': 'Example: grant East China Sales Dept including sub-orgs for regional leads.',
+ 'system.dataPerm.templates.form.dataScopeExample': 'Example: Personal = own records only; Department = all records in the dept.',
+ 'system.dataPerm.templates.form.bizExample': 'Example: restrict to Product Line A orders or customers.',
+ 'system.dataPerm.templates.form.secretExample': 'Example: Public + Internal means confidential data is hidden.',
+ 'system.dataPerm.templates.form.filterExample': 'Example: Order module + OrderStatus equals Paid.',
+ 'system.dataPerm.templates.form.tip.org': 'Which organizations\' data the user can access',
+ 'system.dataPerm.templates.form.tip.data_scope': 'How broad the visible business data is (personal → global)',
+ 'system.dataPerm.templates.form.tip.biz': 'Narrow data by product line, warehouse, etc.',
+ 'system.dataPerm.templates.form.tip.secret': 'Filter by secret level; unchecked levels are hidden',
+ 'system.dataPerm.templates.form.tip.filter': 'Simple filters on a module (multiple conditions use AND)',
+ 'system.dataPerm.templates.form.invalid.orgOrganization': 'Please select an organization',
+ 'system.dataPerm.templates.form.invalid.dataScope': 'Please select data scope',
+ 'system.dataPerm.templates.form.invalid.biz': 'Please select tag type and value',
+ 'system.dataPerm.templates.form.invalid.secretLevels': 'Select at least one secret level',
+ 'system.dataPerm.templates.form.invalid.filterResource': 'Please select a module',
+ 'system.dataPerm.templates.form.invalid.filterCondition': 'Add at least one filter condition',
'system.dataPerm.templates.itemJsonPlaceholder': 'JSON, e.g. {"dataScope":1}',
'system.dataPerm.templates.addItem': 'Add item row',
'system.dataPerm.templates.removeItem': 'Remove',
diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts
index a1eb36f..e7a9c6a 100644
--- a/src/locale/zh-CN.ts
+++ b/src/locale/zh-CN.ts
@@ -1092,6 +1092,32 @@ export default {
'system.dataPerm.templates.field.code': '编码',
'system.dataPerm.templates.field.description': '说明',
'system.dataPerm.templates.field.roleCode': '绑定角色 Code',
+ 'system.dataPerm.templates.field.bindRole': '关联角色',
+ 'system.dataPerm.templates.field.bindRolePlaceholder': '选择要关联的 RBAC 角色',
+ 'system.dataPerm.templates.field.bindRoleExtra': '保存后写入 roleCode,用于标识「哪个岗位对应这份模板」',
+ 'system.dataPerm.templates.section.bindApply': '关联角色与套用',
+ 'system.dataPerm.templates.section.applyNow': '立即生效',
+ 'system.dataPerm.templates.bindApplyHint': '「关联角色」是模板元数据,标明岗位归属;下方「立即生效」才会把明细写入角色或用户的权限配置。两者不重复:关联不等于已套用。',
+ 'system.dataPerm.templates.applyToRole': '同步到关联角色',
+ 'system.dataPerm.templates.applyToRoleDesc': '将本模板明细写入该角色的数据权限(覆盖角色直配)',
+ 'system.dataPerm.templates.applyToRoleBtn': '写入角色权限',
+ 'system.dataPerm.templates.applyToUsers': '套用到指定用户',
+ 'system.dataPerm.templates.applyToUsersDesc': '批量写入用户的直配数据权限,可选择覆盖或合并',
+ 'system.dataPerm.templates.applyToUsersBtn': '套用到用户',
+ 'system.dataPerm.templates.field.targetUsers': '目标用户',
+ 'system.dataPerm.templates.field.targetUsersPlaceholder': '搜索并选择内部用户',
+ 'system.dataPerm.templates.field.applyMode': '套用模式',
+ 'system.dataPerm.templates.field.temporary': '临时授权(仅对用户生效)',
+ 'system.dataPerm.templates.field.effectiveTime': '生效时间',
+ 'system.dataPerm.templates.field.expireTime': '过期时间',
+ 'system.dataPerm.templates.mode.replace': '覆盖直配',
+ 'system.dataPerm.templates.mode.merge': '与现有合并',
+ 'system.dataPerm.templates.applyNeedSave': '请先保存模板,再进行套用操作',
+ 'system.dataPerm.templates.bindRoleFirst': '请先选择关联角色',
+ 'system.dataPerm.templates.selectUsersFirst': '请选择目标用户',
+ 'system.dataPerm.templates.expireTimeRequired': '临时授权需填写过期时间',
+ 'system.dataPerm.templates.applySuccess': '已成功套用 {count} 个目标',
+ 'system.dataPerm.templates.applyPartial': '成功 {success} 个,失败 {fail} 个',
'system.dataPerm.templates.roleCodePlaceholder': '可选,分配角色时自动套用',
'system.dataPerm.templates.itemsTitle': '模板明细',
'system.dataPerm.templates.itemJsonPlaceholder': 'JSON,如 {"dataScope":1}',
@@ -1107,6 +1133,45 @@ export default {
'system.dataPerm.templates.tab.basic': '基本信息',
'system.dataPerm.templates.tab.items': '模板明细',
'system.dataPerm.templates.itemsHint': '每条明细对应一个维度的权限 JSON 配置',
+ 'system.dataPerm.templates.itemsHintForm': '选择权限类型后填写表单即可,无需手写 JSON',
+ 'system.dataPerm.templates.addExample': '添加示例配置',
+ 'system.dataPerm.templates.exampleAdded': '已添加示例「{name}」',
+ 'system.dataPerm.templates.itemInvalid': '第 {index} 条明细未填写完整:{field}',
+ 'system.dataPerm.templates.example.salesStaff': '普通销售',
+ 'system.dataPerm.templates.example.salesStaffDesc': '个人级数据 + 公开/内部密级',
+ 'system.dataPerm.templates.example.salesSupervisor': '销售主管',
+ 'system.dataPerm.templates.example.salesSupervisorDesc': '部门级数据 + 密级1-3',
+ 'system.dataPerm.templates.example.branchManager': '分公司经理',
+ 'system.dataPerm.templates.example.branchManagerDesc': '组织树级数据 + 密级1-3',
+ 'system.dataPerm.templates.form.org': '归属组织',
+ 'system.dataPerm.templates.form.orgPlaceholder': '选择要授权的组织',
+ 'system.dataPerm.templates.form.orgScope': '组织范围',
+ 'system.dataPerm.templates.form.dataScope': '可见数据范围',
+ 'system.dataPerm.templates.form.module': '适用模块',
+ 'system.dataPerm.templates.form.moduleAll': '全部模块(留空)',
+ 'system.dataPerm.templates.form.bizType': '标签类型',
+ 'system.dataPerm.templates.form.bizTag': '标签值',
+ 'system.dataPerm.templates.form.bizTagPlaceholder': '选择具体标签',
+ 'system.dataPerm.templates.form.secretLevels': '可访问密级',
+ 'system.dataPerm.templates.form.filterField': '字段',
+ 'system.dataPerm.templates.form.filterValuePlaceholder': '如:Paid 或 1,2(多值用逗号)',
+ 'system.dataPerm.templates.form.addCondition': '添加条件',
+ 'system.dataPerm.templates.form.orgExample': '示例:授权「华东销售部」且包含下级组织,适用于区域负责人。',
+ 'system.dataPerm.templates.form.dataScopeExample': '示例:选「个人级」表示只能看自己创建的数据;选「部门级」可看本部门所有人数据。',
+ 'system.dataPerm.templates.form.bizExample': '示例:限制只能访问「产品线 A」相关订单或客户。',
+ 'system.dataPerm.templates.form.secretExample': '示例:勾选「公开+内部」表示不能查看机密及以上级别的数据。',
+ 'system.dataPerm.templates.form.filterExample': '示例:订单模块 + 字段 OrderStatus 等于 Paid,表示只看已支付订单。',
+ 'system.dataPerm.templates.form.tip.org': '指定用户可访问哪些组织的数据',
+ 'system.dataPerm.templates.form.tip.data_scope': '控制能看到多大范围的业务数据(个人/小组/部门/组织/全局)',
+ 'system.dataPerm.templates.form.tip.biz': '按产品线、仓库等业务标签进一步收窄数据',
+ 'system.dataPerm.templates.form.tip.secret': '按数据密级过滤,未勾选的密级不可见',
+ 'system.dataPerm.templates.form.tip.filter': '对指定模块追加简单过滤条件(多条条件之间为「且」)',
+ 'system.dataPerm.templates.form.invalid.orgOrganization': '请选择组织',
+ 'system.dataPerm.templates.form.invalid.dataScope': '请选择数据范围',
+ 'system.dataPerm.templates.form.invalid.biz': '请选择业务标签类型和具体标签',
+ 'system.dataPerm.templates.form.invalid.secretLevels': '请至少勾选一个密级',
+ 'system.dataPerm.templates.form.invalid.filterResource': '请选择适用模块',
+ 'system.dataPerm.templates.form.invalid.filterCondition': '请至少填写一条过滤条件',
'system.dataPerm.templates.itemsEmpty': '暂无明细,点击下方按钮添加',
'system.dataPerm.templates.jsonPreset': 'JSON 预设',
'system.dataPerm.templates.dimensionPlaceholder': '选择维度',
diff --git a/src/views/system/dataPerm/components/TemplateItemForm.vue b/src/views/system/dataPerm/components/TemplateItemForm.vue
new file mode 100644
index 0000000..27b31e2
--- /dev/null
+++ b/src/views/system/dataPerm/components/TemplateItemForm.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
diff --git a/src/views/system/dataPerm/templates/index.vue b/src/views/system/dataPerm/templates/index.vue
index 47b510e..d7b7e52 100644
--- a/src/views/system/dataPerm/templates/index.vue
+++ b/src/views/system/dataPerm/templates/index.vue
@@ -50,111 +50,211 @@
-
-
- {{ t('system.dataPerm.templates.apply') }}
-
-
- {{ t('system.dataPerm.templates.save') }}
-
-
+
+ {{ t('system.dataPerm.templates.save') }}
+
-
-
{{ t('system.dataPerm.templates.tab.basic') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
{{ t('system.dataPerm.templates.tab.basic') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('system.dataPerm.templates.section.bindApply') }}
+
+ {{ t('system.dataPerm.templates.bindApplyHint') }}
+
+
+
+
+
+
+
+
+
+ {{ t('system.dataPerm.templates.section.applyNow') }}
+
+
+
+ {{ t('system.dataPerm.templates.applyToRole') }}
+ {{ t('system.dataPerm.templates.applyToRoleDesc') }}
+
+
+ {{ t('system.dataPerm.templates.applyToRoleBtn') }}
+
+
+
+
+
+
+
{{ t('system.dataPerm.templates.applyToUsers') }}
+
{{ t('system.dataPerm.templates.applyToUsersDesc') }}
+
+
+
+
+
+
+ {{ t('system.dataPerm.templates.mode.replace') }}
+ {{ t('system.dataPerm.templates.mode.merge') }}
+
+
+
+
+ {{ t('system.dataPerm.templates.field.temporary') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('system.dataPerm.templates.applyToUsersBtn') }}
+
+
+
+
+
+ {{ t('system.dataPerm.templates.applyNeedSave') }}
+
+
{{ t('system.dataPerm.templates.itemsEmpty') }}
-
{{ t('system.dataPerm.templates.addItem') }}
+
+ {{ t('system.dataPerm.templates.addItem') }}
+
+ {{ t('system.dataPerm.templates.example.salesStaff') }}
+
+
-
+
#{{ idx + 1 }}
-
- {{ dimensionLabel(item.dimensionType) }}
+ onDimensionChange(item, v)"
+ />
+
+ {{ dimensionLabel(item.form.dimensionType) }}
-
- applyJsonPreset(idx, v)">
- {{ t('system.dataPerm.templates.jsonPreset') }}
-
-
- {{ preset.label }}
-
-
-
-
- {{ t('system.dataPerm.templates.removeItem') }}
-
-
+
+ {{ t('system.dataPerm.templates.removeItem') }}
+
-
-
@@ -162,7 +262,7 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Message, Modal } from '@arco-design/web-vue'
-import { IconFile, IconPlus } from '@arco-design/web-vue/es/icon'
+import { IconFile, IconPlus, IconDown } from '@arco-design/web-vue/es/icon'
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue'
import RbacDetailShell from '@/components/RbacSplit/RbacDetailShell.vue'
import type { RbacDetailTag } from '@/components/RbacSplit/RbacDetailShell.vue'
@@ -170,17 +270,42 @@ import { hasPermission } from '@/utils/rbac-permission'
import { handleApiError } from '@/utils/rbac-error-handler'
import { buildFlatLeftMenuTree, findLeftMenuItemByKey } from '@/utils/rbacLeftMenuTree'
import {
+ applyTemplate,
createTemplate,
deleteTemplate,
+ getDataPermMeta,
getTemplateDetail,
getTemplateList,
updateTemplate,
+ type DataPermMetaDto,
type DataPermTemplateListItem,
} from '@/api/rbac/data-perm'
-import ApplyTemplateModal from '../components/ApplyTemplateModal.vue'
+import { OrganizationService, RoleService, getInternalUserList } from '@/api/rbac'
+import type { OrganizationTreeDto } from '@/api/rbac'
+import {
+ DEFAULT_DATA_SCOPES,
+ DEFAULT_ORG_SCOPES,
+ DEFAULT_SECRET_LEVELS,
+ DEFAULT_BIZ_TYPES,
+} from '../constants'
+import TemplateItemForm from '../components/TemplateItemForm.vue'
+import {
+ buildExamplePresets,
+ createDefaultItemForm,
+ formToItemContent,
+ parseContentToForm,
+ validateItemForm,
+ type TemplateDimensionType,
+ type TemplateItemFormModel,
+} from './templateItemContent'
const { t } = useI18n()
+interface TemplateItemRow {
+ _key: string
+ form: TemplateItemFormModel
+}
+
const loading = ref(false)
const detailLoading = ref(false)
const formLoading = ref(false)
@@ -190,11 +315,36 @@ const selectedTemplate = ref(null)
const isCreating = ref(false)
const expandedKeys = ref(['category-templates'])
const activeTab = ref('basic')
+const orgTree = ref<{ key: string; title: string; value: string; children?: unknown[] }[]>([])
const pagination = reactive({ current: 1, pageSize: 100, total: 0 })
-const applyVisible = ref(false)
-const applyTemplateId = ref('')
+const rolesLoading = ref(false)
+const roleOptions = ref<{ value: string; label: string; code: string }[]>([])
+const boundRoleId = ref()
+const applyLoading = ref(false)
+const roleApplyLoading = ref(false)
+const usersLoading = ref(false)
+const userOptions = ref<{ value: string; label: string }[]>([])
+
+const applyForm = reactive({
+ targetUserIds: [] as string[],
+ mode: 'replace' as 'replace' | 'merge',
+ temporaryEnabled: false,
+ effectiveTime: '',
+ expireTime: '',
+})
+
+const canApply = computed(() => !isCreating.value && !!selectedTemplate.value?.id)
+
+const meta = reactive({
+ orgScopes: DEFAULT_ORG_SCOPES,
+ dataScopes: DEFAULT_DATA_SCOPES,
+ secretLevels: DEFAULT_SECRET_LEVELS,
+ bizTypes: DEFAULT_BIZ_TYPES,
+ dataResources: [{ code: 'Order', name: '订单', filterableFields: ['CreateTime', 'OrderStatus', 'IsPaid'] }],
+ filterOperators: [],
+})
const dimensionOptions = computed(() => [
{ value: 'org', label: t('system.dataPerm.panel.tab.org') },
@@ -204,12 +354,20 @@ const dimensionOptions = computed(() => [
{ value: 'filter', label: t('system.dataPerm.panel.tab.filter') },
])
-const jsonPresets = [
- { key: 'dataScopeSelf', label: 'dataScope: 仅本人', json: '{"dataScope":1}' },
- { key: 'dataScopeDept', label: 'dataScope: 本部门', json: '{"dataScope":2}' },
- { key: 'dataScopeAll', label: 'dataScope: 全部', json: '{"dataScope":99}' },
- { key: 'orgScope', label: '组织范围', json: '{"orgScope":1,"organizationIds":[]}' },
-]
+const examplePresets = computed(() => buildExamplePresets(t))
+
+const orgScopeOptions = computed(() =>
+ meta.orgScopes.map((o) => ({ value: Number(o.value), label: String(o.label) }))
+)
+const dataScopeOptions = computed(() =>
+ meta.dataScopes.map((o) => ({ value: Number(o.value), label: String(o.label) }))
+)
+const secretLevelOptions = computed(() =>
+ meta.secretLevels.map((o) => ({ value: Number(o.value), label: String(o.label) }))
+)
+const bizTypeOptions = computed(() =>
+ meta.bizTypes.map((o) => ({ value: String(o.value), label: String(o.label) }))
+)
const form = reactive({
name: '',
@@ -217,7 +375,7 @@ const form = reactive({
description: '',
roleCode: '',
isBuiltIn: false,
- items: [] as { dimensionType: string; itemContentJson: string; sortOrder: number }[],
+ items: [] as TemplateItemRow[],
})
const isEditing = computed(() => isCreating.value || !!selectedTemplate.value)
@@ -245,7 +403,7 @@ const templateStats = computed(() => [
{ label: t('system.dataPerm.templates.stat.items'), value: form.items.length },
{
label: t('system.dataPerm.templates.stat.dimensions'),
- value: new Set(form.items.map((i) => i.dimensionType)).size,
+ value: new Set(form.items.map((i) => i.form.dimensionType)).size,
},
])
@@ -276,6 +434,13 @@ const selectedKeys = computed(() => {
return selectedTemplate.value?.id ? [`item-${selectedTemplate.value.id}`] : []
})
+function newItemRow(formModel?: TemplateItemFormModel): TemplateItemRow {
+ return {
+ _key: `item-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
+ form: formModel || createDefaultItemForm('data_scope', 1),
+ }
+}
+
function dimensionLabel(type: string) {
return dimensionOptions.value.find((o) => o.value === type)?.label || type
}
@@ -291,6 +456,30 @@ function dimensionColor(type: string) {
return map[type] || 'gray'
}
+function convertOrgTree(data: OrganizationTreeDto[]): typeof orgTree.value {
+ return data.map((item) => ({
+ key: item.id!,
+ title: item.name!,
+ value: item.id!,
+ children: item.children?.length ? convertOrgTree(item.children) : undefined,
+ }))
+}
+
+async function loadMeta() {
+ try {
+ const m = await getDataPermMeta()
+ Object.assign(meta, m)
+ } catch {
+ /* 使用 constants 兜底 */
+ }
+ try {
+ const tree = await OrganizationService.getRbacOrganizationGetTree()
+ orgTree.value = convertOrgTree(tree || [])
+ } catch {
+ orgTree.value = []
+ }
+}
+
function resetForm() {
Object.assign(form, {
name: '',
@@ -298,23 +487,162 @@ function resetForm() {
description: '',
roleCode: '',
isBuiltIn: false,
- items: [{ dimensionType: 'data_scope', itemContentJson: '{"dataScope":1}', sortOrder: 1 }],
+ items: [newItemRow(createDefaultItemForm('data_scope', 1))],
})
+ boundRoleId.value = undefined
+ Object.assign(applyForm, {
+ targetUserIds: [],
+ mode: 'replace',
+ temporaryEnabled: false,
+ effectiveTime: '',
+ expireTime: '',
+ })
+}
+
+async function loadRoles() {
+ rolesLoading.value = true
+ try {
+ const res = await RoleService.getRbacRoleGetList(undefined, 1, 100)
+ roleOptions.value = (res?.items || [])
+ .filter((r) => {
+ const code = r.code || ''
+ return code !== 'Customer' && !code.startsWith('CustomerSub_')
+ })
+ .map((r) => ({
+ value: r.id!,
+ label: `${r.name}(${r.code})`,
+ code: r.code || '',
+ }))
+ syncBoundRoleFromCode()
+ } catch {
+ roleOptions.value = []
+ } finally {
+ rolesLoading.value = false
+ }
+}
+
+function syncBoundRoleFromCode() {
+ if (!form.roleCode) {
+ boundRoleId.value = undefined
+ return
+ }
+ boundRoleId.value = roleOptions.value.find((r) => r.code === form.roleCode)?.value
+}
+
+function onBoundRoleChange(roleId: string | undefined) {
+ const role = roleOptions.value.find((r) => r.value === roleId)
+ form.roleCode = role?.code || ''
+}
+
+async function searchUsers(keyword?: string) {
+ usersLoading.value = true
+ try {
+ const res = await getInternalUserList({ keyword, page: 1, pageSize: 50 })
+ userOptions.value = (res?.items || []).map((u) => ({
+ value: u.id!,
+ label: `${u.name || u.account}(${u.account})`,
+ }))
+ } finally {
+ usersLoading.value = false
+ }
+}
+
+function getTemplateIdForApply(): string | null {
+ if (isCreating.value || !selectedTemplate.value?.id) {
+ Message.warning(t('system.dataPerm.templates.applyNeedSave'))
+ return null
+ }
+ return selectedTemplate.value.id
+}
+
+async function runApply(
+ targetType: 'user' | 'role',
+ targetIds: string[],
+ mode?: 'replace' | 'merge'
+) {
+ const templateId = getTemplateIdForApply()
+ if (!templateId) return null
+
+ const res = await applyTemplate({
+ templateId,
+ targetType,
+ targetIds,
+ mode: mode ?? applyForm.mode,
+ temporary: applyForm.temporaryEnabled
+ ? {
+ enabled: true,
+ effectiveTime: applyForm.effectiveTime || undefined,
+ expireTime: applyForm.expireTime || undefined,
+ }
+ : undefined,
+ })
+
+ if (res.failures?.length) {
+ Message.warning(
+ t('system.dataPerm.templates.applyPartial', {
+ success: res.successCount,
+ fail: res.failures.length,
+ })
+ )
+ } else {
+ Message.success(
+ t('system.dataPerm.templates.applySuccess', { count: res.successCount })
+ )
+ }
+ return res
+}
+
+async function applyToBoundRole() {
+ if (!boundRoleId.value) {
+ Message.warning(t('system.dataPerm.templates.bindRoleFirst'))
+ return
+ }
+ roleApplyLoading.value = true
+ try {
+ await runApply('role', [boundRoleId.value], 'replace')
+ } catch (e) {
+ handleApiError(e)
+ } finally {
+ roleApplyLoading.value = false
+ }
+}
+
+async function applyToUsers() {
+ if (!applyForm.targetUserIds.length) {
+ Message.warning(t('system.dataPerm.templates.selectUsersFirst'))
+ return
+ }
+ if (applyForm.temporaryEnabled && !applyForm.expireTime) {
+ Message.warning(t('system.dataPerm.templates.expireTimeRequired'))
+ return
+ }
+ applyLoading.value = true
+ try {
+ await runApply('user', applyForm.targetUserIds)
+ } catch (e) {
+ handleApiError(e)
+ } finally {
+ applyLoading.value = false
+ }
}
function addItem() {
- form.items.push({
- dimensionType: 'data_scope',
- itemContentJson: '{"dataScope":1}',
- sortOrder: form.items.length + 1,
- })
+ form.items.push(newItemRow(createDefaultItemForm('data_scope', form.items.length + 1)))
}
-function applyJsonPreset(idx: number, presetKey: string) {
- const preset = jsonPresets.find((p) => p.key === presetKey)
- if (preset && form.items[idx]) {
- form.items[idx].itemContentJson = preset.json
- }
+function onDimensionChange(row: TemplateItemRow, dimensionType: string) {
+ const sortOrder = row.form.sortOrder
+ row.form = createDefaultItemForm(dimensionType as TemplateDimensionType, sortOrder)
+}
+
+function applyExamplePreset(key: string) {
+ const preset = examplePresets.value.find((p) => p.key === key)
+ if (!preset) return
+ const start = form.items.length
+ preset.items.forEach((itemForm, i) => {
+ form.items.push(newItemRow({ ...itemForm, sortOrder: start + i + 1 }))
+ })
+ Message.success(t('system.dataPerm.templates.exampleAdded', { name: t(preset.labelKey) }))
}
function selectTemplateFromKey(key: string) {
@@ -369,12 +697,11 @@ async function loadDetail(id: string) {
description: detail.description || '',
roleCode: detail.roleCode || '',
isBuiltIn: detail.isBuiltIn,
- items: (detail.items || []).map((it, i) => ({
- dimensionType: it.dimensionType,
- itemContentJson: JSON.stringify(it.itemContent, null, 2),
- sortOrder: it.sortOrder ?? i + 1,
- })),
+ items: (detail.items || []).map((it, i) =>
+ newItemRow(parseContentToForm(it.dimensionType, it.itemContent, it.sortOrder ?? i + 1))
+ ),
})
+ syncBoundRoleFromCode()
} catch (e) {
handleApiError(e)
} finally {
@@ -394,22 +721,31 @@ function openCreate() {
resetForm()
}
-function openApply() {
- if (!selectedTemplate.value?.id) return
- applyTemplateId.value = selectedTemplate.value.id
- applyVisible.value = true
-}
-
async function handleSave() {
if (!form.name?.trim() || !form.code?.trim()) {
Message.warning(t('system.dataPerm.templates.nameCodeRequired'))
return
}
+
+ for (let i = 0; i < form.items.length; i++) {
+ const errKey = validateItemForm(form.items[i].form)
+ if (errKey) {
+ Message.warning(
+ t('system.dataPerm.templates.itemInvalid', {
+ index: i + 1,
+ field: t(`system.dataPerm.templates.form.invalid.${errKey}`),
+ })
+ )
+ activeTab.value = 'items'
+ return
+ }
+ }
+
formLoading.value = true
try {
- const items = form.items.map((it, i) => ({
- dimensionType: it.dimensionType,
- itemContent: JSON.parse(it.itemContentJson || '{}'),
+ const items = form.items.map((row, i) => ({
+ dimensionType: row.form.dimensionType,
+ itemContent: formToItemContent({ ...row.form, sortOrder: i + 1 }),
sortOrder: i + 1,
}))
const body = {
@@ -432,11 +768,7 @@ async function handleSave() {
Message.success(t('system.dataPerm.templates.updateSuccess'))
await loadData()
}
- } catch (e: any) {
- if (e instanceof SyntaxError) {
- Message.error(t('system.dataPerm.templates.invalidJson'))
- return
- }
+ } catch (e) {
handleApiError(e)
} finally {
formLoading.value = false
@@ -476,7 +808,12 @@ watch(
}
)
-onMounted(loadData)
+onMounted(async () => {
+ await loadMeta()
+ await loadRoles()
+ await loadData()
+ searchUsers()
+})
diff --git a/src/views/system/dataPerm/templates/templateItemContent.ts b/src/views/system/dataPerm/templates/templateItemContent.ts
new file mode 100644
index 0000000..aad09c9
--- /dev/null
+++ b/src/views/system/dataPerm/templates/templateItemContent.ts
@@ -0,0 +1,247 @@
+/** 模板明细:表单模型 ↔ 后端 ItemContent 转换 */
+
+export type TemplateDimensionType = 'org' | 'data_scope' | 'biz' | 'secret' | 'filter'
+
+export interface FilterConditionForm {
+ field: string
+ op: string
+ value: string
+}
+
+export interface TemplateItemFormModel {
+ dimensionType: TemplateDimensionType
+ sortOrder: number
+ /** 组织 */
+ orgOrganizationId?: string
+ orgScope?: number
+ /** 数据范围 */
+ dataScope?: number
+ dataResourceCode?: string | null
+ /** 业务标签 */
+ bizType?: string
+ bizId?: string
+ bizName?: string
+ /** 密级(可多选) */
+ secretLevels?: number[]
+ /** 自定义过滤 */
+ filterResourceCode?: string
+ filterConditions?: FilterConditionForm[]
+}
+
+export function createDefaultItemForm(
+ dimensionType: TemplateDimensionType = 'data_scope',
+ sortOrder = 1
+): TemplateItemFormModel {
+ const base: TemplateItemFormModel = { dimensionType, sortOrder }
+ switch (dimensionType) {
+ case 'org':
+ return { ...base, orgOrganizationId: '', orgScope: 2 }
+ case 'data_scope':
+ return { ...base, dataScope: 1, dataResourceCode: null }
+ case 'biz':
+ return { ...base, bizType: 'product_line', bizId: '', bizName: '' }
+ case 'secret':
+ return { ...base, secretLevels: [1, 2], dataResourceCode: null }
+ case 'filter':
+ return {
+ ...base,
+ filterResourceCode: 'Order',
+ filterConditions: [{ field: 'OrderStatus', op: 'eq', value: '' }],
+ }
+ default:
+ return base
+ }
+}
+
+export function parseContentToForm(
+ dimensionType: string,
+ content: Record | null | undefined,
+ sortOrder: number
+): TemplateItemFormModel {
+ const dim = (dimensionType || 'data_scope') as TemplateDimensionType
+ const defaults = createDefaultItemForm(dim, sortOrder)
+ if (!content || typeof content !== 'object') return defaults
+
+ switch (dim) {
+ case 'org':
+ return {
+ ...defaults,
+ orgOrganizationId: str(content.organizationId),
+ orgScope: num(content.orgScope, defaults.orgScope!),
+ }
+ case 'data_scope':
+ return {
+ ...defaults,
+ dataScope: num(content.dataScope, defaults.dataScope!),
+ dataResourceCode: optStr(content.dataResourceCode),
+ }
+ case 'biz':
+ return {
+ ...defaults,
+ bizType: str(content.bizType) || defaults.bizType,
+ bizId: str(content.bizId),
+ bizName: optStr(content.bizName) || '',
+ }
+ case 'secret': {
+ const levels = parseSecretLevels(content)
+ return {
+ ...defaults,
+ secretLevels: levels.length ? levels : defaults.secretLevels,
+ dataResourceCode: optStr(content.dataResourceCode),
+ }
+ }
+ case 'filter': {
+ const fc = content.filterContent as { conditions?: FilterConditionForm[] } | undefined
+ const conditions = (fc?.conditions || []).map((c) => ({
+ field: str(c.field),
+ op: str(c.op) || 'eq',
+ value: c.value != null ? String(c.value) : '',
+ }))
+ return {
+ ...defaults,
+ filterResourceCode: str(content.dataResourceCode) || defaults.filterResourceCode,
+ filterConditions: conditions.length ? conditions : defaults.filterConditions,
+ }
+ }
+ default:
+ return defaults
+ }
+}
+
+export function formToItemContent(model: TemplateItemFormModel): Record {
+ switch (model.dimensionType) {
+ case 'org':
+ return {
+ organizationId: model.orgOrganizationId || '',
+ orgScope: model.orgScope ?? 2,
+ }
+ case 'data_scope':
+ return {
+ dataScope: model.dataScope ?? 1,
+ dataResourceCode: model.dataResourceCode || null,
+ }
+ case 'biz':
+ return {
+ bizType: model.bizType || '',
+ bizId: model.bizId || '',
+ bizName: model.bizName || undefined,
+ }
+ case 'secret':
+ return {
+ secretLevels: model.secretLevels?.length ? model.secretLevels : [1],
+ dataResourceCode: model.dataResourceCode || null,
+ }
+ case 'filter':
+ return {
+ dataResourceCode: model.filterResourceCode || 'Order',
+ filterContent: {
+ logic: 'and',
+ conditions: (model.filterConditions || [])
+ .filter((c) => c.field)
+ .map((c) => ({
+ field: c.field,
+ op: c.op || 'eq',
+ value: parseFilterValue(c.op, c.value),
+ })),
+ },
+ }
+ default:
+ return {}
+ }
+}
+
+export function validateItemForm(model: TemplateItemFormModel): string | null {
+ switch (model.dimensionType) {
+ case 'org':
+ if (!model.orgOrganizationId) return 'orgOrganization'
+ return null
+ case 'data_scope':
+ if (!model.dataScope) return 'dataScope'
+ return null
+ case 'biz':
+ if (!model.bizType || !model.bizId) return 'biz'
+ return null
+ case 'secret':
+ if (!model.secretLevels?.length) return 'secretLevels'
+ return null
+ case 'filter':
+ if (!model.filterResourceCode) return 'filterResource'
+ if (!model.filterConditions?.some((c) => c.field)) return 'filterCondition'
+ return null
+ default:
+ return null
+ }
+}
+
+function parseSecretLevels(content: Record): number[] {
+ if (Array.isArray(content.secretLevels)) {
+ return content.secretLevels.map((x) => Number(x)).filter((x) => !Number.isNaN(x))
+ }
+ if (content.secretLevel != null) {
+ const n = Number(content.secretLevel)
+ return Number.isNaN(n) ? [] : [n]
+ }
+ return []
+}
+
+function parseFilterValue(op: string, raw: string): unknown {
+ if (!raw) return raw
+ if (op === 'in' || op === 'notIn' || op === 'between') {
+ return raw.split(',').map((s) => s.trim())
+ }
+ return raw
+}
+
+function str(v: unknown): string {
+ return v != null ? String(v).trim() : ''
+}
+
+function optStr(v: unknown): string | null {
+ const s = str(v)
+ return s || null
+}
+
+function num(v: unknown, fallback: number): number {
+ const n = Number(v)
+ return Number.isNaN(n) ? fallback : n
+}
+
+/** 一键添加的示例模板 */
+export interface TemplateExamplePreset {
+ key: string
+ labelKey: string
+ descKey: string
+ items: TemplateItemFormModel[]
+}
+
+export function buildExamplePresets(_t?: (key: string) => string): TemplateExamplePreset[] {
+ return [
+ {
+ key: 'salesStaff',
+ labelKey: 'system.dataPerm.templates.example.salesStaff',
+ descKey: 'system.dataPerm.templates.example.salesStaffDesc',
+ items: [
+ createDefaultItemForm('data_scope', 1),
+ { ...createDefaultItemForm('secret', 2), secretLevels: [1, 2] },
+ ],
+ },
+ {
+ key: 'salesSupervisor',
+ labelKey: 'system.dataPerm.templates.example.salesSupervisor',
+ descKey: 'system.dataPerm.templates.example.salesSupervisorDesc',
+ items: [
+ { ...createDefaultItemForm('data_scope', 1), dataScope: 3 },
+ { ...createDefaultItemForm('secret', 2), secretLevels: [1, 2, 3] },
+ ],
+ },
+ {
+ key: 'branchManager',
+ labelKey: 'system.dataPerm.templates.example.branchManager',
+ descKey: 'system.dataPerm.templates.example.branchManagerDesc',
+ items: [
+ { ...createDefaultItemForm('data_scope', 1), dataScope: 4 },
+ { ...createDefaultItemForm('secret', 2), secretLevels: [1, 2, 3] },
+ ],
+ },
+ ]
+}