feat(数据权限): 模板明细表单化并合并关联角色与套用
模板明细改为业务表单与示例预设;关联角色与套用合并至基本信息;修复角色列表 PageSize 上限。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
99f7ddf73c
commit
5cd6a5462d
@ -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',
|
||||
|
||||
@ -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': '选择维度',
|
||||
|
||||
271
src/views/system/dataPerm/components/TemplateItemForm.vue
Normal file
271
src/views/system/dataPerm/components/TemplateItemForm.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
247
src/views/system/dataPerm/templates/templateItemContent.ts
Normal file
247
src/views/system/dataPerm/templates/templateItemContent.ts
Normal 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] },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user