feat(数据权限): 模板明细表单化并合并关联角色与套用

模板明细改为业务表单与示例预设;关联角色与套用合并至基本信息;修复角色列表 PageSize 上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-03 17:47:31 +08:00
parent 99f7ddf73c
commit 5cd6a5462d
5 changed files with 1171 additions and 131 deletions

View File

@ -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',

View File

@ -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': '选择维度',

View File

@ -0,0 +1,271 @@
<template>
<div class="template-item-form">
<p class="form-tip">{{ dimensionTip }}</p>
<!-- 组织 -->
<template v-if="model.dimensionType === 'org'">
<a-row :gutter="16">
<a-col :span="14">
<a-form-item :label="t('system.dataPerm.templates.form.org')" required>
<a-tree-select
v-model="model.orgOrganizationId"
:data="orgTree"
:placeholder="t('system.dataPerm.templates.form.orgPlaceholder')"
allow-search
/>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item :label="t('system.dataPerm.templates.form.orgScope')" required>
<a-select v-model="model.orgScope" :options="orgScopeOptions" />
</a-form-item>
</a-col>
</a-row>
<a-alert type="normal" class="example-alert">
{{ t('system.dataPerm.templates.form.orgExample') }}
</a-alert>
</template>
<!-- 数据范围 -->
<template v-else-if="model.dimensionType === 'data_scope'">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.form.dataScope')" required>
<a-select v-model="model.dataScope" :options="dataScopeOptions" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.form.module')">
<a-select
v-model="model.dataResourceCode"
:options="resourceOptions"
:placeholder="t('system.dataPerm.templates.form.moduleAll')"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-alert type="normal" class="example-alert">
{{ t('system.dataPerm.templates.form.dataScopeExample') }}
</a-alert>
</template>
<!-- 业务标签 -->
<template v-else-if="model.dimensionType === 'biz'">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item :label="t('system.dataPerm.templates.form.bizType')" required>
<a-select v-model="model.bizType" :options="bizTypeOptions" @change="onBizTypeChange" />
</a-form-item>
</a-col>
<a-col :span="16">
<a-form-item :label="t('system.dataPerm.templates.form.bizTag')" required>
<a-select
v-model="model.bizId"
:options="bizOptions"
:placeholder="t('system.dataPerm.templates.form.bizTagPlaceholder')"
allow-search
:loading="bizLoading"
@popup-visible-change="(v: boolean) => v && loadBizOptions()"
@change="onBizIdChange"
/>
</a-form-item>
</a-col>
</a-row>
<a-alert type="normal" class="example-alert">
{{ t('system.dataPerm.templates.form.bizExample') }}
</a-alert>
</template>
<!-- 密级 -->
<template v-else-if="model.dimensionType === 'secret'">
<a-form-item :label="t('system.dataPerm.templates.form.secretLevels')" required>
<a-checkbox-group v-model="model.secretLevels">
<a-checkbox v-for="opt in secretLevelOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item :label="t('system.dataPerm.templates.form.module')">
<a-select
v-model="model.dataResourceCode"
:options="resourceOptions"
:placeholder="t('system.dataPerm.templates.form.moduleAll')"
allow-clear
/>
</a-form-item>
<a-alert type="normal" class="example-alert">
{{ t('system.dataPerm.templates.form.secretExample') }}
</a-alert>
</template>
<!-- 自定义过滤(简化为常用条件) -->
<template v-else-if="model.dimensionType === 'filter'">
<a-form-item :label="t('system.dataPerm.templates.form.module')" required>
<a-select v-model="model.filterResourceCode" :options="resourceOptions" />
</a-form-item>
<div class="filter-conditions">
<div
v-for="(cond, cIdx) in model.filterConditions"
:key="cIdx"
class="filter-cond-row"
>
<a-select
v-model="cond.field"
:options="filterFieldOptions"
:placeholder="t('system.dataPerm.templates.form.filterField')"
style="width: 140px"
/>
<a-select v-model="cond.op" :options="operatorOptions" style="width: 110px" />
<a-input
v-model="cond.value"
:placeholder="t('system.dataPerm.templates.form.filterValuePlaceholder')"
style="flex: 1"
/>
<a-button
v-if="(model.filterConditions?.length || 0) > 1"
type="text"
status="danger"
@click="removeCondition(cIdx)"
>
{{ t('system.dataPerm.templates.removeItem') }}
</a-button>
</div>
<a-button type="dashed" size="small" @click="addCondition">
{{ t('system.dataPerm.templates.form.addCondition') }}
</a-button>
</div>
<a-alert type="normal" class="example-alert">
{{ t('system.dataPerm.templates.form.filterExample') }}
</a-alert>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { getBizOptions } from '@/api/rbac/data-perm'
import { DEFAULT_BIZ_TYPES, FILTER_OPERATORS } from '../constants'
import type { TemplateItemFormModel } from '../templates/templateItemContent'
const props = defineProps<{
modelValue: TemplateItemFormModel
orgTree: { key: string; title: string; value: string; children?: unknown[] }[]
orgScopeOptions: { value: number; label: string }[]
dataScopeOptions: { value: number; label: string }[]
secretLevelOptions: { value: number; label: string }[]
dataResources: { code: string; name: string; filterableFields?: string[] }[]
bizTypeOptions?: { value: string; label: string }[]
}>()
const { t } = useI18n()
const model = defineModel<TemplateItemFormModel>({ required: true })
const bizOptions = ref<{ value: string; label: string }[]>([])
const bizLoading = ref(false)
const bizTypeOptions = computed(
() => props.bizTypeOptions?.length ? props.bizTypeOptions : DEFAULT_BIZ_TYPES
)
const resourceOptions = computed(() =>
props.dataResources.map((r) => ({ value: r.code, label: r.name }))
)
const operatorOptions = FILTER_OPERATORS.map((op) => ({ value: op, label: op }))
const filterFieldOptions = computed(() => {
const res = props.dataResources.find((r) => r.code === model.value.filterResourceCode)
const fields = res?.filterableFields || ['CreateTime', 'OrderStatus', 'IsPaid']
return fields.map((f) => ({ value: f, label: f }))
})
const dimensionTip = computed(() => {
const key = `system.dataPerm.templates.form.tip.${model.value.dimensionType}`
return t(key)
})
async function loadBizOptions() {
if (!model.value.bizType || bizOptions.value.length) return
bizLoading.value = true
try {
const res = await getBizOptions({ bizType: model.value.bizType, page: 1, pageSize: 100 })
bizOptions.value = (res.items || []).map((i) => ({ value: i.bizId, label: i.bizName }))
} catch {
bizOptions.value = []
} finally {
bizLoading.value = false
}
}
function onBizTypeChange() {
model.value.bizId = ''
model.value.bizName = ''
bizOptions.value = []
}
function onBizIdChange(bizId: string) {
const opt = bizOptions.value.find((o) => o.value === bizId)
model.value.bizName = opt?.label || ''
}
function addCondition() {
if (!model.value.filterConditions) {
model.value.filterConditions = []
}
model.value.filterConditions.push({ field: '', op: 'eq', value: '' })
}
function removeCondition(index: number) {
model.value.filterConditions?.splice(index, 1)
}
watch(
() => model.value.bizType,
() => {
bizOptions.value = []
}
)
watch(
() => model.value.bizId,
(id) => {
if (id && !bizOptions.value.length) loadBizOptions()
},
{ immediate: true }
)
</script>
<style scoped lang="less">
.template-item-form {
:deep(.arco-form-item) {
margin-bottom: 12px;
}
}
.form-tip {
margin: 0 0 12px;
font-size: 13px;
color: var(--color-text-3);
line-height: 1.5;
}
.example-alert {
margin-top: 4px;
font-size: 12px;
}
.filter-conditions {
display: flex;
flex-direction: column;
gap: 8px;
}
.filter-cond-row {
display: flex;
gap: 8px;
align-items: center;
}
</style>

View File

@ -50,111 +50,211 @@
</div>
</template>
<template #actions>
<a-space>
<a-button
v-if="!isCreating && hasPermission('Rbac.DataPerm.Template')"
@click="openApply"
>
{{ t('system.dataPerm.templates.apply') }}
</a-button>
<a-button
v-if="hasPermission('Rbac.DataPerm.Template')"
type="primary"
:loading="formLoading"
@click="handleSave"
>
{{ t('system.dataPerm.templates.save') }}
</a-button>
</a-space>
<a-button
v-if="hasPermission('Rbac.DataPerm.Template')"
type="primary"
:loading="formLoading"
@click="handleSave"
>
{{ t('system.dataPerm.templates.save') }}
</a-button>
</template>
<div v-show="activeTab === 'basic'" class="section-card">
<h3 class="section-card-title">{{ t('system.dataPerm.templates.tab.basic') }}</h3>
<a-form :model="form" layout="vertical" class="template-form">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.field.name')" required>
<a-input v-model="form.name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.field.code')" required>
<a-input v-model="form.code" :disabled="!isCreating && form.isBuiltIn" />
</a-form-item>
</a-col>
</a-row>
<a-form-item :label="t('system.dataPerm.templates.field.description')">
<a-textarea v-model="form.description" :auto-size="{ minRows: 2 }" />
</a-form-item>
<a-form-item :label="t('system.dataPerm.templates.field.roleCode')">
<a-input v-model="form.roleCode" :placeholder="t('system.dataPerm.templates.roleCodePlaceholder')" />
</a-form-item>
</a-form>
<div v-show="activeTab === 'basic'">
<div class="section-card">
<h3 class="section-card-title">{{ t('system.dataPerm.templates.tab.basic') }}</h3>
<a-form :model="form" layout="vertical" class="template-form">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.field.name')" required>
<a-input v-model="form.name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.field.code')" required>
<a-input v-model="form.code" :disabled="!isCreating && form.isBuiltIn" />
</a-form-item>
</a-col>
</a-row>
<a-form-item :label="t('system.dataPerm.templates.field.description')">
<a-textarea v-model="form.description" :auto-size="{ minRows: 2 }" />
</a-form-item>
</a-form>
</div>
<div class="section-card">
<h3 class="section-card-title">{{ t('system.dataPerm.templates.section.bindApply') }}</h3>
<a-alert type="info" class="bind-alert">
{{ t('system.dataPerm.templates.bindApplyHint') }}
</a-alert>
<a-form layout="vertical" class="template-form">
<a-form-item
:label="t('system.dataPerm.templates.field.bindRole')"
:extra="t('system.dataPerm.templates.field.bindRoleExtra')"
>
<a-select
v-model="boundRoleId"
:options="roleOptions"
:placeholder="t('system.dataPerm.templates.field.bindRolePlaceholder')"
allow-clear
allow-search
:loading="rolesLoading"
@change="onBoundRoleChange"
/>
</a-form-item>
</a-form>
<template v-if="canApply">
<a-divider orientation="left">{{ t('system.dataPerm.templates.section.applyNow') }}</a-divider>
<div class="apply-row">
<div class="apply-row-text">
<span class="apply-row-title">{{ t('system.dataPerm.templates.applyToRole') }}</span>
<span class="apply-row-desc">{{ t('system.dataPerm.templates.applyToRoleDesc') }}</span>
</div>
<a-button
:disabled="!boundRoleId"
:loading="roleApplyLoading"
@click="applyToBoundRole"
>
{{ t('system.dataPerm.templates.applyToRoleBtn') }}
</a-button>
</div>
<a-divider />
<div class="apply-user-block">
<p class="apply-block-title">{{ t('system.dataPerm.templates.applyToUsers') }}</p>
<p class="apply-block-desc">{{ t('system.dataPerm.templates.applyToUsersDesc') }}</p>
<a-form :model="applyForm" layout="vertical">
<a-form-item :label="t('system.dataPerm.templates.field.targetUsers')" required>
<a-select
v-model="applyForm.targetUserIds"
:options="userOptions"
multiple
allow-search
:placeholder="t('system.dataPerm.templates.field.targetUsersPlaceholder')"
:loading="usersLoading"
@search="searchUsers"
/>
</a-form-item>
<a-form-item :label="t('system.dataPerm.templates.field.applyMode')">
<a-radio-group v-model="applyForm.mode">
<a-radio value="replace">{{ t('system.dataPerm.templates.mode.replace') }}</a-radio>
<a-radio value="merge">{{ t('system.dataPerm.templates.mode.merge') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-checkbox v-model="applyForm.temporaryEnabled">
{{ t('system.dataPerm.templates.field.temporary') }}
</a-checkbox>
</a-form-item>
<a-row v-if="applyForm.temporaryEnabled" :gutter="16">
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.field.effectiveTime')">
<a-date-picker
v-model="applyForm.effectiveTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss[Z]"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="t('system.dataPerm.templates.field.expireTime')" required>
<a-date-picker
v-model="applyForm.expireTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss[Z]"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" :loading="applyLoading" @click="applyToUsers">
{{ t('system.dataPerm.templates.applyToUsersBtn') }}
</a-button>
</a-form>
</div>
</template>
<a-alert v-else type="warning" class="bind-alert">
{{ t('system.dataPerm.templates.applyNeedSave') }}
</a-alert>
</div>
</div>
<div v-show="activeTab === 'items'">
<div class="sticky-toolbar">
<span class="items-hint">{{ t('system.dataPerm.templates.itemsHint') }}</span>
<a-button type="dashed" @click="addItem">
<template #icon><icon-plus /></template>
{{ t('system.dataPerm.templates.addItem') }}
</a-button>
<span class="items-hint">{{ t('system.dataPerm.templates.itemsHintForm') }}</span>
<a-space wrap>
<a-dropdown @select="applyExamplePreset">
<a-button type="outline">
{{ t('system.dataPerm.templates.addExample') }}
<icon-down />
</a-button>
<template #content>
<a-doption v-for="preset in examplePresets" :key="preset.key" :value="preset.key">
<div class="preset-option">
<span class="preset-label">{{ t(preset.labelKey) }}</span>
<span class="preset-desc">{{ t(preset.descKey) }}</span>
</div>
</a-doption>
</template>
</a-dropdown>
<a-button type="dashed" @click="addItem">
<template #icon><icon-plus /></template>
{{ t('system.dataPerm.templates.addItem') }}
</a-button>
</a-space>
</div>
<div v-if="form.items.length === 0" class="items-empty section-card">
<p>{{ t('system.dataPerm.templates.itemsEmpty') }}</p>
<a-button type="primary" @click="addItem">{{ t('system.dataPerm.templates.addItem') }}</a-button>
<a-space>
<a-button type="primary" @click="addItem">{{ t('system.dataPerm.templates.addItem') }}</a-button>
<a-button @click="applyExamplePreset('salesStaff')">
{{ t('system.dataPerm.templates.example.salesStaff') }}
</a-button>
</a-space>
</div>
<div v-for="(item, idx) in form.items" :key="idx" class="item-card section-card">
<div v-for="(item, idx) in form.items" :key="item._key" class="item-card section-card">
<div class="item-card-head">
<div class="item-card-head-left">
<span class="item-index">#{{ idx + 1 }}</span>
<a-tag :color="dimensionColor(item.dimensionType)" size="small">
{{ dimensionLabel(item.dimensionType) }}
<a-select
v-model="item.form.dimensionType"
:options="dimensionOptions"
style="width: 160px"
:placeholder="t('system.dataPerm.templates.dimensionPlaceholder')"
@change="(v: string) => onDimensionChange(item, v)"
/>
<a-tag :color="dimensionColor(item.form.dimensionType)" size="small">
{{ dimensionLabel(item.form.dimensionType) }}
</a-tag>
</div>
<a-space>
<a-dropdown @select="(v: string) => applyJsonPreset(idx, v)">
<a-button size="mini" type="text">{{ t('system.dataPerm.templates.jsonPreset') }}</a-button>
<template #content>
<a-doption v-for="preset in jsonPresets" :key="preset.key" :value="preset.key">
{{ preset.label }}
</a-doption>
</template>
</a-dropdown>
<a-button type="text" status="danger" size="mini" @click="form.items.splice(idx, 1)">
{{ t('system.dataPerm.templates.removeItem') }}
</a-button>
</a-space>
<a-button type="text" status="danger" size="mini" @click="form.items.splice(idx, 1)">
{{ t('system.dataPerm.templates.removeItem') }}
</a-button>
</div>
<div class="item-card-body">
<a-select
v-model="item.dimensionType"
:options="dimensionOptions"
style="width: 160px"
:placeholder="t('system.dataPerm.templates.dimensionPlaceholder')"
/>
<a-textarea
v-model="item.itemContentJson"
:auto-size="{ minRows: 3, maxRows: 8 }"
:placeholder="t('system.dataPerm.templates.itemJsonPlaceholder')"
class="item-json-input"
<TemplateItemForm
v-model="item.form"
:org-tree="orgTree"
:org-scope-options="orgScopeOptions"
:data-scope-options="dataScopeOptions"
:secret-level-options="secretLevelOptions"
:data-resources="meta.dataResources"
:biz-type-options="bizTypeOptions"
/>
</div>
</div>
</div>
</RbacDetailShell>
</div>
<ApplyTemplateModal
v-model:visible="applyVisible"
target-type="user"
:target-ids="[]"
:default-template-id="applyTemplateId"
allow-pick-targets
@success="loadData"
/>
</div>
</template>
@ -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<DataPermTemplateListItem | null>(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<string>()
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<DataPermMetaDto>({
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()
})
</script>
<style scoped lang="less">
@ -507,6 +844,23 @@ onMounted(loadData)
}
}
.preset-option {
display: flex;
flex-direction: column;
gap: 2px;
max-width: 280px;
}
.preset-label {
font-size: 13px;
color: var(--color-text-1);
}
.preset-desc {
font-size: 11px;
color: var(--color-text-3);
}
.item-card {
margin-bottom: 12px;
padding: 0;
@ -526,6 +880,7 @@ onMounted(loadData)
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.item-index {
@ -535,15 +890,52 @@ onMounted(loadData)
}
.item-card-body {
display: flex;
gap: 12px;
padding: 16px;
align-items: flex-start;
padding: 16px 20px;
}
.item-json-input {
flex: 1;
font-family: 'Consolas', 'Monaco', monospace;
.bind-alert {
margin-bottom: 16px;
}
.apply-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 0;
}
.apply-row-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.apply-row-title {
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
}
.apply-row-desc {
font-size: 12px;
color: var(--color-text-3);
}
.apply-user-block {
padding-top: 4px;
}
.apply-block-title {
margin: 0 0 4px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
}
.apply-block-desc {
margin: 0 0 12px;
font-size: 12px;
color: var(--color-text-3);
}
</style>

View File

@ -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<string, unknown> | 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<string, unknown> {
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<string, unknown>): 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] },
],
},
]
}