diff --git a/code/fms/FMS新系统核心表结构设计.md b/code/fms/FMS新系统核心表结构设计.md index e5c20677..beb96e6a 100644 --- a/code/fms/FMS新系统核心表结构设计.md +++ b/code/fms/FMS新系统核心表结构设计.md @@ -263,31 +263,31 @@ create index ix_s_menu_route ## 8. 权限 -> 权限直接授予用户,不引入角色继承。`s_power` 是权限点定义表,`s_user_power` 是用户授权表;数据范围使用独立的 `s_user_data_scope` 表。权限点按资源类型扩展,新增报表、页面等资源时复用同一模型。 +> 权限直接授予用户,不引入角色继承。`s_power.b_id` 是内部主键,`s_power.b_code` 是稳定且全局唯一的业务键;字段授权使用独立的字段访问策略表。 ```sql create table dbo.s_power ( - b_id varchar(50) not null primary key, -- 权限编码本身 + b_id bigint not null, + b_code varchar(300) not null, b_name nvarchar(200) not null, b_i18n varchar(150) null, - b_power_type varchar(20) not null, - b_resource varchar(128) null, - b_module_id varchar(50) null, - b_field varchar(50) null, - b_operation varchar(30) not null, + b_power_type varchar(20) not null, -- menu/page/report/module/action + b_resource_id varchar(128) not null, + b_owner_type varchar(20) null, -- action owner type + b_owner_id varchar(128) null, -- action owner id + b_capability varchar(30) not null, -- access/execute/read/create/update/delete/export b_canuse tinyint not null default 1, - b_xh int not null default 0 + b_xh int not null default 0, + primary key (b_id), + unique (b_code) ); -create index ix_s_power_module - on dbo.s_power (b_module_id, b_field, b_canuse); - create index ix_s_power_resource - on dbo.s_power (b_power_type, b_resource, b_canuse); + on dbo.s_power (b_power_type, b_owner_type, b_owner_id, b_resource_id, b_canuse); create table dbo.s_user_power ( b_user_id varchar(50) not null, - b_power_id varchar(50) not null, + b_power_id bigint not null, b_canuse tinyint not null default 1, b_updatedatetime datetime2 null, primary key (b_user_id, b_power_id) @@ -296,20 +296,41 @@ create table dbo.s_user_power ( create index ix_s_user_power_user on dbo.s_user_power (b_user_id, b_canuse, b_power_id); +create table dbo.s_user_field_permission ( + b_user_id varchar(50) not null, + b_module_id varchar(50) not null, + b_field varchar(128) not null, + b_access_mode varchar(10) not null, -- hidden / view / edit + b_allow_query tinyint not null default 0, + b_allow_export tinyint not null default 0, + b_canuse tinyint not null default 1, + b_updatedatetime datetime2 null, + primary key (b_user_id, b_module_id, b_field) +); + +create index ix_s_user_field_permission_module + on dbo.s_user_field_permission (b_user_id, b_module_id, b_canuse, b_field); + create table dbo.s_user_data_scope ( b_user_id varchar(50) not null, b_module_id varchar(50) not null, - b_operation varchar(30) not null, -- read / update / delete / export + b_operation varchar(30) not null, -- * / read / create / update / delete / export;* 表示模块默认范围 + b_scope_level varchar(10) not null default 'default', -- default / override b_scope_no int not null default 1, b_scope_type varchar(30) not null, -- own / department / department_tree / all / custom b_scope_field varchar(50) null, -- 业务数据中的归属用户/部门字段 b_scope_value varchar(100) null, -- custom 时每个用户/部门值一行 b_updatedatetime datetime2 null, - primary key (b_user_id, b_module_id, b_operation, b_scope_no) + primary key (b_user_id, b_module_id, b_operation, b_scope_level, b_scope_no), + constraint ck_s_user_data_scope_level check (b_scope_level in ('default','override')), + constraint ck_s_user_data_scope_operation check ( + (b_scope_level = 'default' and b_operation = '*') + or (b_scope_level = 'override' and b_operation in ('read','create','update','delete','export')) + ) ); create index ix_s_user_data_scope_module - on dbo.s_user_data_scope (b_user_id, b_module_id, b_operation, b_scope_type); + on dbo.s_user_data_scope (b_user_id, b_module_id, b_operation, b_scope_level, b_scope_type); ``` 权限点约定: @@ -317,17 +338,17 @@ create index ix_s_user_data_scope_module - `menu/access`:菜单或页面入口访问权限; - `module/read|create|update|delete|export`:模块级数据操作权限; - `action/execute`:菜单或页面中的按钮、动作权限; -- `field/view|edit|query|export`:字段查看、修改、查询、导出能力; +- `s_user_field_permission`:字段 `hidden/view/edit` 访问级别,以及独立的查询/导出能力; - `page/access`、`report/access`:独立页面或报表访问权限,报表列表中的每个报表可作为一个资源; -- `b_power_type` 和 `b_operation` 是可扩展编码,新增资源类型不新增权限表,但需要补充权限定义和前端/后端校验规则。 -- 当前数据库迁移中的权限类型/形状约束若仍只允许 `menu/module/action/field` 或 `field/view|edit`,新增 `page`、`report`、`field/query` 等编码时必须同步扩展约束;不要只修改前端枚举。 +- `b_power_type` 和 `b_capability` 是可扩展编码,新增资源类型不新增权限表,但需要补充权限定义和前端/后端校验规则。 +- 动作业务键必须包含所属资源命名空间,例如 `action.module.cw_fee.audit.execute`,不得使用裸 `action.audit`。 权限计算约定: - 默认拒绝:用户没有有效授权记录时不具备该权限; - 用户存在有效 `s_user_power` 记录时获得该权限;停用的权限点或授权记录不生效; -- 字段 `edit` 只代表用户有修改能力,最终是否可编辑仍受 `s_field_edit.b_readonly`、`b_disabled` 和业务校验约束; -- 字段不可见时,其编辑、查询、导出能力一并无效;字段可见但无 `edit` 权限时,在可编辑列表中显示为只读; +- 字段没有有效策略记录时为 `hidden`;`view` 表示可见只读,`edit` 表示可见可编辑;`s_field_view` / `s_field_edit` 等元数据只能收紧权限; +- `hidden` 字段不可查询、不可导出;`b_allow_query` / `b_allow_export` 不能突破字段访问级别; - 模块数据操作权限与数据范围同时生效:先判断操作权限,再按 `s_user_data_scope` 限制数据行; - 菜单/页面/报表访问权限只控制入口和访问,不能替代模块、字段或数据范围权限。 diff --git a/code/fms/fms-vue/src/views/dashboard/index.vue b/code/fms/fms-vue/src/views/dashboard/index.vue index 20c19736..9f8f6f50 100644 --- a/code/fms/fms-vue/src/views/dashboard/index.vue +++ b/code/fms/fms-vue/src/views/dashboard/index.vue @@ -5,6 +5,7 @@ import { Columns3, Download, Ellipsis, + GripVertical, Maximize2, Plus, Printer, @@ -252,6 +253,7 @@ const advancedNodes = reactive([]) const advancedGroups = reactive([]) const advancedSnapshot = ref(null) const draggingFieldKey = ref('') +const dragOverFieldKey = ref('') const allAdvancedNodes = computed(() => advancedNodes.concat(advancedGroups.flatMap((group) => group.nodes))) function hasNodeValue(value) { return Array.isArray(value) ? value.some((item) => item !== '' && item !== null && item !== undefined) : value !== '' && value !== null && value !== undefined @@ -280,6 +282,13 @@ const extraQuickConditionCount = computed(() => extraQuickSearchFields.value.fil if (field.type === 'numrange') return searchForm[`${field.key}Min`] || searchForm[`${field.key}Max`] return searchForm[field.key] !== '' && searchForm[field.key] !== null && searchForm[field.key] !== undefined }).length) +// 常用区已填条件总数:高级模式的说明条要用 —— 告诉用户这些值还在,只是不参与 +const quickConditionCount = computed(() => quickSearchFields.value.filter((field) => { + if (field.type === 'numrange') return searchForm[`${field.key}Min`] || searchForm[`${field.key}Max`] + return searchForm[field.key] !== '' && searchForm[field.key] !== null && searchForm[field.key] !== undefined +}).length) +// 高级模式下常用区强制收起:保留上下文(知道有哪几个常用条件),但不占版面 +const showAllQuickFields = computed(() => quickSearchExpanded.value && activeQueryMode.value === 'quick') const advancedSearchFields = computed(() => searchFields.value.filter((field) => field.queryable !== false && queryPrefs[field.key]?.area !== 'hidden'), ) @@ -434,12 +443,27 @@ function resetQuickQuery() { Object.assign(searchForm, makeEmptyForm()) if (activeQueryMode.value === 'quick') queryApplied.value = false } -function startFieldDrag(field) { +function startFieldDrag(field, event) { draggingFieldKey.value = field.key + // Firefox 下不写 dataTransfer 不会触发 drop,同时声明「移动」语义拿到正确的光标 + if (event?.dataTransfer) { + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/plain', field.key) + } +} +function endFieldDrag() { + // 必须清理:否则在空白处松手后残留的 key 会让下一次 drop 误排序 + draggingFieldKey.value = '' + dragOverFieldKey.value = '' +} +function rowDragClass(field) { + if (draggingFieldKey.value === field.key) return 'is-dragging' + if (dragOverFieldKey.value === field.key && draggingFieldKey.value !== field.key) return 'is-drop-target' + return '' } function dropField(field, area) { const sourceKey = draggingFieldKey.value - draggingFieldKey.value = '' + endFieldDrag() if (!sourceKey || sourceKey === field.key || queryPrefs[sourceKey]?.area !== area) return const rows = searchFields.value.filter((item) => item.queryable !== false && queryPrefs[item.key]?.area === area) .toSorted((a, b) => queryPrefs[a.key].order - queryPrefs[b.key].order) @@ -605,11 +629,22 @@ function onMoreAction({ key }) {