-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onUpdate('module', v)"
+ @i18n-saved="onI18nSaved"
+ />
+
+ onUpdate('listConfig', v)"
+ @groups-saved="onGroupsSaved"
+ @sync-edit="(v) => onUpdate('editConfig', v)"
+ @sync-query="(v) => onUpdate('queryConfig', v)"
+ />
+ onUpdate('editConfig', v)"
+ @groups-saved="onGroupsSaved"
+ @sync-list="(v) => onUpdate('listConfig', v)"
+ @sync-query="(v) => onUpdate('queryConfig', v)"
+ />
+ onUpdate('queryConfig', v)"
+ />
+
+ onUpdate('powers', v)"
+ @i18n-saved="onI18nSaved"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue b/code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue
deleted file mode 100644
index fe936cd5..00000000
--- a/code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ group.name }}
-
-
-
-
-
-
-
-
-
- {{ group.name }}
-
-
-
-
-
-
-
-
diff --git a/code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue b/code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue
deleted file mode 100644
index c6c06f96..00000000
--- a/code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue
+++ /dev/null
@@ -1,257 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js b/code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js
deleted file mode 100644
index 871a006d..00000000
--- a/code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js
+++ /dev/null
@@ -1,309 +0,0 @@
-import { ref } from "vue";
-import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
-
-function idSet(rows, field) {
- return new Set(rows.map((item) => String(item[field])));
-}
-
-function grantDescriptor(
- table,
- ownerField,
- targetField,
- ownerId,
- selectedIds,
- existingRows,
- extra,
-) {
- const selected = new Set(selectedIds.map(String));
- const existing = idSet(existingRows, targetField);
- return {
- table,
- ownerField,
- targetField,
- ownerId: String(ownerId),
- extra,
- inserts: [...selected].filter((id) => !existing.has(id)),
- deletes: existingRows
- .filter((row) => !selected.has(String(row[targetField])))
- .map((row) => ({ b_id: row.b_id })),
- };
-}
-
-async function saveGrantDescriptors(descriptors, leadingRequests = []) {
- const insertCount = descriptors.reduce((total, item) => total + item.inserts.length, 0);
- let ids = [];
- if (insertCount) {
- const response = await nextIdApi(insertCount);
- ids = Array.isArray(response.data) ? response.data.map(String) : [String(response.data)];
- }
- let idIndex = 0;
- const requests = [...leadingRequests];
- for (const descriptor of descriptors) {
- const inserts = descriptor.inserts.map((targetId) => ({
- b_id: ids[idIndex++],
- [descriptor.ownerField]: descriptor.ownerId,
- [descriptor.targetField]: targetId,
- ...(descriptor.extra || {}),
- }));
- if (inserts.length || descriptor.deletes.length) {
- requests.push({
- table: descriptor.table,
- key_field: "b_id",
- inserts,
- deletes: descriptor.deletes,
- });
- }
- }
- if (requests.length) await saveObjectApi(requests);
-}
-
-export function usePermissionAdministration(operatorId) {
- const roles = ref([]);
- const functionNodes = ref([]);
- const pageNodes = ref([]);
- const actions = ref([]);
- const selectedRoleId = ref("");
- const roleFunctionIds = ref([]);
- const rolePageIds = ref([]);
- const roleActionIds = ref([]);
- const selectedUserId = ref("");
- const userRoleIds = ref([]);
- const userFunctionIds = ref([]);
- const userPageIds = ref([]);
- const userActionIds = ref([]);
- const roleRows = ref({ functions: [], pages: [], actions: [] });
- const userRows = ref({ roles: [], functions: [], pages: [], actions: [] });
- const loading = ref(false);
- const saving = ref(false);
-
- async function loadCatalogs() {
- loading.value = true;
- try {
- const responses = await Promise.all([
- loadDataApi("b_role", "", "b_xh ASC, b_id ASC"),
- loadDataApi("v_s_function_node", "b_canuse = 1", "b_xh ASC, b_id ASC"),
- loadDataApi(
- "v_s_page_node",
- "b_canuse = 1 AND b_permission_required = 1",
- "b_page_module_id ASC, b_xh ASC, b_id ASC",
- ),
- loadDataApi("v_s_module_action", "b_canuse = 1", "b_module_id ASC, b_xh ASC, b_id ASC"),
- ]);
- roles.value = responses[0].data || [];
- functionNodes.value = responses[1].data || [];
- pageNodes.value = responses[2].data || [];
- actions.value = responses[3].data || [];
- } finally {
- loading.value = false;
- }
- }
-
- async function loadRole(roleId) {
- selectedRoleId.value = String(roleId || "");
- if (!selectedRoleId.value) return;
- const responses = await Promise.all([
- loadDataApi("b_role_function_node", `b_role_id = ${selectedRoleId.value}`),
- loadDataApi("b_role_page_node", `b_role_id = ${selectedRoleId.value}`),
- loadDataApi("b_role_action", `b_role_id = ${selectedRoleId.value}`),
- ]);
- roleRows.value = {
- functions: responses[0].data || [],
- pages: responses[1].data || [],
- actions: responses[2].data || [],
- };
- roleFunctionIds.value = [...idSet(roleRows.value.functions, "b_function_node_id")];
- rolePageIds.value = [...idSet(roleRows.value.pages, "b_page_node_id")];
- roleActionIds.value = [...idSet(roleRows.value.actions, "b_action_id")];
- }
-
- async function createRole(values) {
- const response = await nextIdApi();
- const id = String(response.data);
- await saveObjectApi([
- {
- table: "b_role",
- key_field: "b_id",
- inserts: [
- {
- b_id: id,
- b_code: String(values.b_code || "").trim(),
- b_name: String(values.b_name || "").trim(),
- b_i18n: `meta.role.${id}`,
- b_is_super: values.b_is_super ? 1 : 0,
- b_canuse: 1,
- b_xh: Number(values.b_xh) || 0,
- },
- ],
- },
- ]);
- await loadCatalogs();
- await loadRole(id);
- }
-
- async function saveRole(values) {
- const roleId = selectedRoleId.value;
- if (!roleId) return;
- saving.value = true;
- try {
- await saveGrantDescriptors(
- [
- grantDescriptor(
- "b_role_function_node",
- "b_role_id",
- "b_function_node_id",
- roleId,
- roleFunctionIds.value,
- roleRows.value.functions,
- ),
- grantDescriptor(
- "b_role_page_node",
- "b_role_id",
- "b_page_node_id",
- roleId,
- rolePageIds.value,
- roleRows.value.pages,
- ),
- grantDescriptor(
- "b_role_action",
- "b_role_id",
- "b_action_id",
- roleId,
- roleActionIds.value,
- roleRows.value.actions,
- ),
- ],
- [
- {
- table: "b_role",
- key_field: "b_id",
- updates: [
- {
- b_id: roleId,
- b_name: String(values.b_name || "").trim(),
- b_canuse: values.b_canuse ? 1 : 0,
- b_xh: Number(values.b_xh) || 0,
- },
- ],
- },
- ],
- );
- await loadCatalogs();
- await loadRole(roleId);
- } finally {
- saving.value = false;
- }
- }
-
- async function removeRole(roleId) {
- const role = roles.value.find((item) => String(item.b_id) === String(roleId));
- if (Number(role?.b_is_super) === 1) throw new Error("超级管理员角色不能删除");
- const userRoles = await loadDataApi("b_user_role", `b_role_id = ${roleId}`);
- if ((userRoles.data || []).length) throw new Error("该角色仍分配给用户");
- await loadRole(roleId);
- const dataScopes = await loadDataApi("b_role_data_scope", `b_role_id = ${roleId}`);
- await saveObjectApi([
- { table: "b_role_function_node", key_field: "b_id", deletes: roleRows.value.functions },
- { table: "b_role_page_node", key_field: "b_id", deletes: roleRows.value.pages },
- { table: "b_role_action", key_field: "b_id", deletes: roleRows.value.actions },
- { table: "b_role_data_scope", key_field: "b_id", deletes: dataScopes.data || [] },
- { table: "b_role", key_field: "b_id", deletes: [{ b_id: String(roleId) }] },
- ]);
- selectedRoleId.value = "";
- await loadCatalogs();
- }
-
- async function loadUser(userId) {
- selectedUserId.value = String(userId || "");
- const responses = await Promise.all([
- loadDataApi("b_user_role", `b_user_id = ${selectedUserId.value}`),
- loadDataApi("b_user_function_node", `b_user_id = ${selectedUserId.value}`),
- loadDataApi("b_user_page_node", `b_user_id = ${selectedUserId.value}`),
- loadDataApi("b_user_action", `b_user_id = ${selectedUserId.value}`),
- ]);
- userRows.value = {
- roles: responses[0].data || [],
- functions: responses[1].data || [],
- pages: responses[2].data || [],
- actions: responses[3].data || [],
- };
- userRoleIds.value = [...idSet(userRows.value.roles, "b_role_id")];
- userFunctionIds.value = [...idSet(userRows.value.functions, "b_function_node_id")];
- userPageIds.value = [...idSet(userRows.value.pages, "b_page_node_id")];
- userActionIds.value = [...idSet(userRows.value.actions, "b_action_id")];
- }
-
- async function saveUserPermissions() {
- const userId = selectedUserId.value;
- if (!userId) return;
- const extra = { b_inputuser_id: String(operatorId.value || operatorId) };
- saving.value = true;
- try {
- await saveGrantDescriptors([
- grantDescriptor(
- "b_user_role",
- "b_user_id",
- "b_role_id",
- userId,
- userRoleIds.value,
- userRows.value.roles,
- extra,
- ),
- grantDescriptor(
- "b_user_function_node",
- "b_user_id",
- "b_function_node_id",
- userId,
- userFunctionIds.value,
- userRows.value.functions,
- extra,
- ),
- grantDescriptor(
- "b_user_page_node",
- "b_user_id",
- "b_page_node_id",
- userId,
- userPageIds.value,
- userRows.value.pages,
- extra,
- ),
- grantDescriptor(
- "b_user_action",
- "b_user_id",
- "b_action_id",
- userId,
- userActionIds.value,
- userRows.value.actions,
- extra,
- ),
- ]);
- await loadUser(userId);
- } finally {
- saving.value = false;
- }
- }
-
- return {
- roles,
- functionNodes,
- pageNodes,
- actions,
- selectedRoleId,
- roleFunctionIds,
- rolePageIds,
- roleActionIds,
- selectedUserId,
- userRoleIds,
- userFunctionIds,
- userPageIds,
- userActionIds,
- loading,
- saving,
- loadCatalogs,
- loadRole,
- createRole,
- saveRole,
- removeRole,
- loadUser,
- saveUserPermissions,
- };
-}
diff --git a/code/fms/fms-vue/src/views/system/user/index.vue b/code/fms/fms-vue/src/views/system/user/index.vue
index 65371ffc..bcc3e7b6 100644
--- a/code/fms/fms-vue/src/views/system/user/index.vue
+++ b/code/fms/fms-vue/src/views/system/user/index.vue
@@ -1,124 +1,428 @@
-
+ { if (record?.action?.key === 'edit') handleEdit(record.record) }"
+ >
-
- 角色维护
+
+
+ 新增用户
-
-
+
-
-
- 所属角色
-
-
-
- {{ role.b_name }}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
标志位
+
+
+ 启用
+
+
+ 销售员
+
+
+ 单据主管
+
+
+ 可处理事务
+
+
+ 允许异地登录
+
+
+ 可见客户
+
+
+ 管理员
+
+
+ 操作权限
+
+
+ 审批权限
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.b_name }}
+ {{ group.children.length }}
+
+
+
+
+
+
+
+
+
+ {{ selectedModule.b_name }}
+ {{ selectedModule.b_code }}
+
+
+
+
+
+
+
+ 勾选左侧「{{ selectedModule.b_name }}」以授权访问,再配置具体操作权限
+
+
+
+
+ 请选择左侧菜单
+
+
+
+
+
@@ -138,45 +442,207 @@ const rowActions = [
border-radius: 0;
}
-.user-permission-layout {
- display: grid;
- height: 100%;
- min-height: 0;
- grid-template-rows: auto minmax(0, 1fr);
- gap: 14px;
+// ── Modal internal styles ──
+.demo-basic-info {
+ padding-top: 4px;
}
-.role-section,
-.additive-section {
- min-height: 0;
-}
-
-.role-section > strong,
-.additive-section > strong {
+.demo-field-label {
display: block;
- margin-bottom: 8px;
+ margin-bottom: 5px;
+ font-size: 13px;
+ color: #333;
}
-.role-options {
- max-height: 92px;
- overflow: auto;
- padding: 10px 12px;
- border: 1px solid var(--fms-border);
+.demo-required {
+ color: #ff4d4f;
}
-.role-options :deep(.ant-checkbox-group) {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 8px 12px;
+.demo-flags-card {
+ margin-top: 20px;
+ padding-top: 16px;
+ border-top: 1px solid #f0f0f0;
}
-.additive-section {
+.demo-card-title {
+ margin-bottom: 10px;
+ font-size: 13px;
+ font-weight: 600;
+ color: #666;
+}
+
+// ── Menu permission: left tree + right list ──
+.perm-layout {
+ display: flex;
+ height: 440px;
+ border: 1px solid #f0f0f0;
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+// 左侧:菜单树
+.perm-tree {
+ width: 240px;
+ min-width: 240px;
+ padding: 6px 0;
+ border-right: 1px solid #f0f0f0;
+ overflow-y: auto;
+ background: #fafbfc;
+}
+
+.tree-group-hd {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ cursor: pointer;
+ user-select: none;
+ transition: background 0.1s;
+
+ &:hover {
+ background: #f0f1f3;
+ }
+}
+
+.tree-group-ic {
+ display: flex;
+ align-items: center;
+ color: #999;
+}
+
+.tree-group-label {
+ flex: 1;
+ font-size: 13px;
+ font-weight: 600;
+ color: #1a1a1a;
+}
+
+.tree-group-count {
+ font-size: 11px;
+ color: #bbb;
+}
+
+.tree-group-body {
display: flex;
flex-direction: column;
}
-.user-grants {
+.tree-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px 8px 30px;
+ cursor: pointer;
+ transition: background 0.1s;
+
+ &:hover {
+ background: #ebedf0;
+ }
+
+ &.is-active {
+ background: #e6f0ff;
+
+ &:hover {
+ background: #dcebff;
+ }
+ }
+}
+
+.tree-item-name {
+ font-size: 13px;
+ color: #333;
+ user-select: none;
+}
+
+// 右侧:操作权限列表
+.perm-detail {
flex: 1;
- min-height: 0;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+}
+
+.perm-detail-hd {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ padding: 14px 20px;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.perm-detail-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #1a1a1a;
+}
+
+.perm-detail-sub {
+ font-size: 12px;
+ color: #bbb;
+}
+
+.perm-detail-body {
+ flex: 1;
+ padding: 12px 20px;
+ overflow-y: auto;
+}
+
+.perm-detail-all {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ margin-bottom: 6px;
+ border-radius: 6px;
+ background: #f6f7f9;
+ font-size: 13px;
+ color: #333;
+ cursor: pointer;
+ user-select: none;
+}
+
+.perm-pow-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.perm-pow-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 11px 12px;
+ border-bottom: 1px solid #f5f5f5;
+ font-size: 13px;
+ color: #333;
+ cursor: pointer;
+ user-select: none;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ &:hover {
+ background: #fafbfc;
+ }
+}
+
+.perm-detail-tip {
+ margin-top: 8px;
+ padding: 36px 16px;
+ text-align: center;
+ font-size: 13px;
+ color: #999;
+ background: #fafbfc;
+ border-radius: 6px;
+}
+
+.perm-detail-empty {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ color: #bbb;
}
diff --git a/code/fms/module-management-demo.html b/code/fms/module-management-demo.html
new file mode 100644
index 00000000..f1774472
--- /dev/null
+++ b/code/fms/module-management-demo.html
@@ -0,0 +1,375 @@
+
+
+
+
+
+ 模块管理原型
+
+
+
+
+
+
+
F
+
模块管理统一维护目录、页面和数据模块
+
+
+
+
+
+
+
+
+
+
基础资料 / 往来单位 / 数据模块
+
+
+
客户联系人
+
数据模块contact_person已启用
+
+
+
+
+
+
+
+
+
+
+
+
✓当前使用方式:默认。显示名称支持多语言,未翻译时自动回退到中文名称。
+
+
+ 字段概览
+
+ | 字段 | 名称 | 类型 | 列表 | 表单 | 状态 |
+
+ | b_name | 姓名 | varchar | 显示 | 编辑 | 启用 |
+ | b_mobile | 手机 | varchar | 显示 | 编辑 | 启用 |
+ | b_remark | 备注 | nvarchar | 隐藏 | 编辑 | 启用 |
+
+
+
+
权限范围:当前账号可以编辑模块配置、同步字段和管理操作权限。
+
+
+
+
+
+
+
+
diff --git a/code/fms/前端元数据运行时重构计划.md b/code/fms/前端元数据运行时重构计划.md
deleted file mode 100644
index 219d19b8..00000000
--- a/code/fms/前端元数据运行时重构计划.md
+++ /dev/null
@@ -1,262 +0,0 @@
-# FMS 前端元数据运行时重构计划
-
-## 1. 目标
-
-本次重构以内部 ERP 快速开发为目标,将模块配置、运行时解析、列表编辑和页面组合能力集中到前端公共层。业务开发优先通过配置和组合完成,只有特殊业务流程编写页面代码。
-
-预期结果:
-
-```text
-普通数据页面 配置 Module + Profile 后直接使用公共页面
-组合业务页面 配置 PageNode,并通过公共运行时加载多个模块
-特殊业务页面 复用公共运行时,只补充业务事件和业务流程
-模块配置维护 统一在模块管理中完成,不修改后端
-```
-
-## 2. 硬约束
-
-- 不兼容旧系统、旧模块模型和旧业务数据。
-- 日常开发和运行期间不修改、不重启后端。
-- 不新增业务 Controller、Service 或专用接口。
-- 不修改 `/data/loaddata`、`/data/page`、`/data/saveobjt`、`/data/nextid`、`/data/describe` 的协议和处理逻辑。
-- 所有多表保存继续通过一次 `/data/saveobjt` 在同一事务中提交。
-- SQL Server `bigint` 在前端始终按字符串处理。
-- 数据库约束负责主外键、唯一性、类型和树结构的最终完整性。
-- 前端负责运行时解析、配置编排、交互校验、保存请求生成和错误定位。
-- 后端重启不作为前端功能开发、配置调整或页面接入的前置步骤。
-
-## 3. 保留的核心模型
-
-| 模型 | 稳定职责 |
-| --- | --- |
-| `Module` | 可复用资源及公共属性 |
-| `FunctionNode` | 菜单目录和页面入口 |
-| `PageNode` | 页面内部结构和模块引用 |
-| `Profile` | 同一数据模块的不同使用方式 |
-| `Field` | 模块级字段定义 |
-| `Capability` | Profile 级列表、表单、查询等配置及 Module 级操作 |
-
-字段继续属于 Module;列表、表单、查询、导入和导出继续属于 Profile。不得把这些配置重新合并为一张大表。
-
-当前阶段正式支持 `page` 和 `data` 模块。`report`、`dashboard`、`workflow` 只保留数据库契约,在对应运行能力完成前不作为常规配置入口。
-
-## 4. 目标前端结构
-
-```text
-src/
- module-runtime/
- constants.js 元数据表名、视图名、节点类型和能力编码
- runtimeResolver.js Module/Profile/PageNode 统一解析
- configLoader.js 字段及 Profile 配置统一加载
- conditionBuilder.js 固定条件、查询条件和页面参数组合
- saveRequestBuilder.js inserts/updates/deletes 请求生成与排序
- fieldRegistry.js 字段控件和渲染器注册
- actionRegistry.js 通用操作和页面操作注册
- useModuleRuntime.js 面向页面的统一组合入口
- useModuleDataSource.js 分页、刷新、选择和查询状态
- useModuleEditor.js 新增、编辑、删除、临时 ID 和批量保存
-```
-
-目录名可在实施时结合现有结构调整,但职责边界不得重新散落到业务页面。
-
-## 5. 公共运行时契约
-
-### 5.1 模块定位
-
-公共运行时支持以下三种明确定位方式:
-
-1. `moduleId`:使用该模块的默认 Profile
-2. `profileId`:精确定位 Profile 并反查所属 Module
-3. `pageNodeId`
-
-同时提供 `moduleId + profileId` 时只用于校验两者归属关系。新页面不再使用 `moduleCode + profileCode` 定位,也不再通过页面路由后取“第一个 `module_ref`”作为隐式模块。一个页面存在多个模块引用时,必须通过 `pageNodeId` 明确定位。
-
-### 5.2 运行时结果
-
-`useModuleRuntime` 对调用方提供统一对象:
-
-```text
-module 模块及数据源
-profile 当前使用方式
-fields 启用字段
-listConfig 列表配置
-formConfig 表单配置
-queryConfig 查询配置
-actions 操作目录及有效权限
-groups 布局分组
-runtimeKey 当前配置唯一键
-loading/error 加载状态
-reload() 重新加载配置
-```
-
-业务页面不再自行查询 `s_module_field*`、`s_module_profile` 或 `v_s_module_runtime`。
-
-### 5.3 扩展点
-
-通用页面通过明确扩展点承载差异:
-
-- `fixedCondition`
-- `beforeLoad`
-- `afterLoad`
-- `beforeSave`
-- `afterSave`
-- `rowActions`
-- `actionHandlers`
-- 表格和表单插槽
-
-扩展点只处理业务差异,不重新实现分页、字段解析、脏数据和批量保存。
-
-## 6. 实施阶段
-
-### 阶段 0:基线和旧设计清理
-
-状态:`completed`
-
-实际结果:旧设计文档已标记为历史方案;公共常量已建立;`otherdata` 只使用正式的 `group/module_ref` 节点类型。
-
-- 以 `新数据库结构.md` 为唯一模块模型规范。
-- 将旧的 `s_module_group`、`s_option_source`、旧节点类型和编码型 i18n 设计标记为历史方案。
-- 建立前端元数据常量,统一 `directory/entry`、`group/tab/section/module_ref` 等枚举。
-- 修正 `otherdata` 中 `category/group`、`module/module_ref` 的混合判断。
-- 记录当前普通列表、可编辑列表、其他资料、用户管理、登录日志和多语言页面的行为基线。
-
-验收:代码和有效文档中不再存在会参与运行的旧模型定义。
-
-### 阶段 1:抽取运行时解析和配置加载
-
-状态:`completed`
-
-实际结果:已建立 ID 优先的公共解析、配置加载、并发版本控制和会话缓存;多模块页面必须使用 `pageNodeId`。
-
-- 从 `FmsModuleListPage.vue` 抽取模块定位、Profile 定位和配置加载逻辑。
-- 建立 `useModuleRuntime`,统一 ID 字符串转换、加载并发控制和错误状态。
-- 建立显式页面节点解析规则,删除“第一个数据节点”约定。
-- 保持现有 API 请求数量和运行行为,阶段内不调整页面视觉结构。
-- 为同一运行时键增加会话级缓存;模块管理保存后可以定向失效缓存。
-
-验收:普通页面可通过 `moduleId`、`profileId`、`pageNodeId` 或唯一页面引用定位;同一页面可通过不同 `pageNodeId` 明确加载多个数据模块。
-
-### 阶段 2:抽取数据源和编辑器
-
-状态:`completed`
-
-实际结果:分页查询、查询状态、临时 ID、脏行、批量保存、批量删除和拖拽排序已从页面组件抽离;一次保存仍只调用一次 `saveobjt`。
-
-- 从 `FmsModuleListPage.vue` 抽取分页、查询、排序和刷新状态到 `useModuleDataSource`。
-- 抽取新增、编辑、删除、临时 ID、自动编码和保存请求生成到 `useModuleEditor`。
-- 建立统一 `saveRequestBuilder`,负责表级变更合并及删除、更新、插入顺序。
-- 一次用户保存只发起一次 `/data/saveobjt`。
-- 保留 `FmsModuleTable`、`FmsQueryToolbar` 和现有页面插槽契约。
-
-验收:`FmsModuleListPage.vue` 主要负责界面组合,不再同时承担元数据加载、数据访问和保存算法。
-
-### 阶段 3:控件和操作注册中心
-
-状态:`completed`
-
-实际结果:字段注册中心已覆盖输入、长文本、数字、金额、日期、日期时间、布尔和选择控件;数字编辑使用字符串模式保护 bigint;操作注册中心已提供通用操作和页面 handler 扩展。
-
-- 建立字段控件注册中心,收敛输入、数字、日期、布尔、选择和自定义渲染器映射。
-- 建立操作注册中心,统一 `create/update/delete/export` 等通用行为。
-- 特殊业务操作由页面注册 handler,不修改公共运行时主体。
-- 注册项使用稳定 key;未知 key 明确报错并指出模块、Profile 和字段。
-- 表格行内重型控件继续按需挂载,任意时刻只保留当前编辑实例。
-
-验收:新增一种字段控件或通用操作只修改注册中心及对应实现,不修改所有业务页面。
-
-### 阶段 4:模块管理提效
-
-状态:`completed`
-
-实际结果:创建数据模块自动创建默认 Profile;同步字段自动补齐列表和表单配置;Profile 可完整复制;模块管理已增加实时完整性检查和列表预览;常规新建入口只开放 `page/data`。
-
-- 创建数据模块时自动创建 `default` Profile。
-- 同步字段后,新字段自动生成默认列表和表单配置;失效字段只停用。
-- 支持复制 Profile,并完整复制分组、列表、表单和查询配置。
-- 模块管理增加配置预览和完整性检查结果,但不展示底层保存顺序。
-- 常规入口只开放 `page` 和 `data`;预留模块类型放入明确的高级区域或隐藏。
-- 保存后定向刷新运行时缓存、菜单和权限数据,不刷新整个应用。
-
-验收:技术人员可以完成“创建模块、同步字段、调整配置、加入页面、立即使用”的完整流程。
-
-### 阶段 5:页面编排和业务页面接入
-
-状态:`completed`
-
-实际结果:`contact`、`othercompany`、`user`、`login-log`、`i18n-type` 已接入公共运行时;`otherdata` 通过 `pageNodeId` 加载所选模块引用,不再按模块编码定位运行时。
-
-- 页面组件通过 `pageNodeId` 或明确的 `moduleId/profileId` 加载数据模块。
-- `otherdata` 使用统一 PageNode 枚举和运行时解析,不再维护独立模块解析逻辑。
-- `contact`、`othercompany`、`user`、`login-log`、`i18n-type` 按风险从低到高接入公共运行时。
-- 普通页面只保留模块声明和必要插槽;特殊页面只保留业务流程。
-- 不为了接入公共运行时修改后端或重启后端。
-
-验收:新增普通数据页面只需要配置 Module/Profile 和一个轻量页面入口。
-
-### 阶段 6:清理与稳定性验证
-
-状态:`in_progress`
-
-已完成:公共运行时代码已通过 Node 语法检查、定向格式检查和 Vite 生产构建;异步请求均使用运行时键或版本号丢弃过期结果;bigint ID 和数字编辑按字符串处理。
-
-待完成:连接实际运行数据后回归普通列表、可编辑列表、`otherdata`、多模块页面和机构切换,并确认每个路由页面只有一个启用的数据模块引用。
-
-- 删除已无调用方的重复加载、条件组合和保存函数。
-- 删除旧枚举适配、旧模块解析路径和演示数据。
-- 检查所有 bigint 比较、Map key、选择值和请求参数均按字符串处理。
-- 检查模块切换、路由切换和机构切换时不会应用过期异步结果。
-- 检查大列表滚动、编辑器按需挂载和批量保存请求数量。
-- 执行 `pnpm fmt:check`;除非用户明确要求,不执行构建、不启动或重启后端。
-
-验收:公共运行时成为唯一模块解析入口,业务页面没有重复实现模块配置加载。
-
-## 7. 优先改造文件
-
-| 优先级 | 文件/目录 | 处理 |
-| --- | --- | --- |
-| P0 | `src/components/fms-module-list/FmsModuleListPage.vue` | 拆分运行时、数据源和编辑逻辑 |
-| P0 | `src/views/base/otherdata/index.vue` | 统一 PageNode 枚举和模块定位 |
-| P0 | `src/views/module-management/composables/useModuleResources.js` | 复用请求构造、缓存失效和字段同步能力 |
-| P1 | `src/views/module-management/composables/usePageComposition.js` | 使用统一节点规则和运行时定位 |
-| P1 | `src/views/module-management/composables/useFunctionStructure.js` | 使用统一节点常量和树校验 |
-| P1 | `src/stores/permissions.js` | 与运行时共享模块、节点和操作索引 |
-| P2 | 使用 `FmsModuleListPage` 的业务页面 | 逐页删除重复配置加载和保存逻辑 |
-
-## 8. 性能和运行连续性
-
-- 元数据按 `orgId + moduleId + profileId` 缓存,避免同一页面重复请求。
-- 模块配置保存后只失效相关缓存,不清空所有模块缓存。
-- 页面或机构切换使用版本号丢弃过期请求结果。
-- 列表业务数据始终分页;不因元数据重构增加全表读取。
-- 多个元数据请求可并行加载,但不得在行循环中发起请求。
-- 批量编辑只生成一次保存请求,不逐行调用接口。
-- 前端变更通过 Vite 热更新生效;实施期间不重启后端。
-
-## 9. 非目标
-
-- 不建立传统业务 Controller/Service/DAO 分层。
-- 不为每个模块生成后端代码。
-- 不实现旧模块表兼容、双写或旧数据转换。
-- 不在本次重构实现报表引擎、仪表盘设计器或工作流引擎。
-- 不重写 `fms-table`、文件组件或已有手写业务流程。
-- 不一次性改造所有业务页面;公共能力稳定后逐页接入。
-
-## 10. 执行纪律
-
-- 每个阶段单独完成、验证和交付,不跨阶段进行大范围改写。
-- 开始一个阶段前记录受影响页面和当前行为。
-- 公共组件改动后优先回归普通列表、可编辑列表和 `otherdata`。
-- 发现通用接口无法满足需求时停止该项,说明缺口后再决定是否允许修改后端。
-- 每阶段完成后更新本文件状态和实际结果,未通过验收不得进入下一阶段。
-
-## 11. 完成标准
-
-满足以下条件后,本计划完成:
-
-- 技术人员可以不修改后端完成数据模块创建、字段同步、Profile 配置和页面挂载。
-- 开发人员可以通过公共组件或 composable 接入模块,不重复编写元数据查询和保存流程。
-- 普通模块新增和配置调整不需要后端重启。
-- 同一数据模块可被多个页面和多个 Profile 复用。
-- 页面包含多个模块时定位明确,不依赖隐式顺序。
-- 前端只保留一套有效模块模型和节点枚举。
-- 批量保存、权限刷新、缓存失效和异步切换行为可预测。
diff --git a/code/fms/数据库迁移计划.md b/code/fms/数据库迁移计划.md
index a75abe16..3b0c7274 100644
--- a/code/fms/数据库迁移计划.md
+++ b/code/fms/数据库迁移计划.md
@@ -1,7 +1,5 @@
# FMS 业务数据库结构迁移计划
-> 本文件记录业务表迁移历史。当前模块元数据架构和 `033`/`034` 切换顺序以 `新数据库结构.md` 为准;文中旧授权表名只描述切换前状态。
-
## 目标与现状
- 源库:`G3HY2025`,只读取系统目录和视图定义。
diff --git a/code/fms/文件组件设计方案.md b/code/fms/文件组件设计方案.md
index 227d66f0..5eff34ec 100644
--- a/code/fms/文件组件设计方案.md
+++ b/code/fms/文件组件设计方案.md
@@ -124,7 +124,7 @@ FileUpload.vue a-upload 包装(调用 fileService.upload)
- 三栏 flex 布局:左 `FileCategoryTree`(宽 ~220px,受 `showTree` 控制),中 `FileList`(flex:1),右预览区(宽 ~40% 或固定 ~480px,仅当选中文件时显示)。
- 选中文件(来自 `FileList` 的 `select` 事件)→ 右栏渲染 `FileViewer` 并传入预览 URL(`fileService.getContentUrl(subid)`)。
- 负责 `moduleCode` → `moduleId`(b_id) 解析:挂载时调 `loadModuleConfiguration` / 通用 `loaddata` 查 `s_module` 得到 `b_id`,下传 `FileList` 备用(上传时带回)。
-- 工具栏按钮(上传/下载/删除)通过 `permissions.canAction(moduleIdOrCode, actionCode)` 读取有效操作授权并控制显隐。
+- 工具栏按钮(上传/下载/删除)按 `moduleCode` 查 `b_user_power` 控制显隐/禁用(前端权限,沿用现有 `permissions` store 模式)。
**Props**
| 名称 | 类型 | 必填 | 默认 | 说明 |
diff --git a/code/fms/新数据库结构.md b/code/fms/新数据库结构.md
index 7a9651b3..804d02c8 100644
--- a/code/fms/新数据库结构.md
+++ b/code/fms/新数据库结构.md
@@ -1,203 +1,344 @@
-# FMS 模块架构
+# FMS 新数据库结构
-## 目标
+## 设计原则
-模块架构只解决五类稳定职责:
+- `s_module` 是核心元数据对象,统一表示菜单目录、页面和数据模块。
+- 字段来源于数据模块配置的查询视图。系统不创建业务字段,只同步视图字段,并维护其展示、编辑、查询和权限配置。
+- 页面和业务逻辑不完全元数据化。海运等复杂业务页面由 Vue 页面显式组合多个数据模块,元数据只提供 Table、Form 和 Query 配置。
+- 字段基础定义、列表、编辑、查询四个职责分离;一个字段可以只参与其中任一场景。
+- 编辑规则与列表布局解耦:`s_module_field_edit` 是通用编辑配置,FmsForm 和 FmsTable 可按自身规则消费;`s_module_field_list` 只描述列表展示,不保存编辑状态。
+- 模块权限控制访问,操作权限控制按钮动作;操作权限必须依赖模块访问权限。
+- 模块元数据表沿用旧系统的 `s_module*` 命名;基础资料和用户授权表使用 `b_`,字段名继续使用 `b_`。全部 `b_id` 使用 `bigint` 雪花 ID,返回前端时序列化为字符串。
+- 布尔字段统一使用 `int`,`1` 表示是、`0` 表示否;`b_xh` 数值越小越靠前。
+- 当前按机构使用独立数据库,表中不保存机构编号。表名、视图名和字段名必须由后端白名单校验,不能直接拼接 SQL。
+
+## 整体关系
```text
-Module 可复用模块资源
-FunctionNode 菜单目录和页面入口
-PageNode 页面内部编排
-Profile 数据模块的使用方式
-Capability 字段、列表、表单、查询、操作等能力
+s_module
+ |-- parent_id --> s_module
+ |-- 1:N --------> s_module_field
+ |-- 0:1 --------> s_module_auto_code --> s_module_field
+ |-- 1:N --------> s_module_field_group -- parent_id --> s_module_field_group
+ |-- 1:N --------> s_module_power ---- 1:N ----> b_user_power
+ `-- 1:N --------> b_user_module
+
+s_module_field
+ |-- 0:N --------> s_module_field_list -- group_id --> s_module_field_group
+ |-- 0:1 --------> s_module_field_edit -- group_id --> s_module_field_group
+ `-- 0:1 --------> s_module_field_query
+
+s_module / s_module_field / s_module_field_list / s_module_field_group / s_module_power
+ `-- b_i18n --> b_i18n.b_key
```
-业务表、业务数据和手写 Vue 页面不进入元数据模型。通用读写继续使用 `/data/loaddata`、`/data/page`、`/data/saveobjt`,不增加模块专用接口。
-
-## 设计规则
-
-- 所有主键使用不可变 `bigint` 雪花 ID,前端按字符串处理。
-- 技术编码创建后不可修改;显示名称可以修改。
-- `s_module` 只保存资源公共属性,类型专属字段进入子类型表。
-- 数据模块必须自动创建一个 `default` Profile。
-- 字段和操作属于 Module;列表、表单、查询、导入、导出属于 Profile。
-- 树结构使用明确父子外键和循环检测,不把树路径保存为 JSON。
-- 关系和授权使用稳定 ID,不依赖名称或技术编码。
-- 只有控件扩展参数、渲染器参数等不可预知属性使用 JSON。
-- 不保留旧模块元数据兼容表、双写或页面专用模型。
-
-## 资源模型
+## 模块
### `dbo.s_module`
+统一保存目录、页面和数据模块。
+
| 字段 | 类型 | 说明 |
| --- | --- | --- |
-| `b_id` | `bigint` | 不可变雪花 ID |
-| `b_code` | `varchar(50)` | 全局唯一、不可变技术编码 |
-| `b_name` | `nvarchar(100)` | 默认名称 |
-| `b_i18n` | `varchar(150)` | `meta.module.` |
-| `b_module_type` | `varchar(20)` | `page/data/report/dashboard/workflow` |
+| `b_id` | `bigint` | 不可变雪花 ID 主键 |
+| `b_code` | `varchar(50)` | 可修改的唯一模块编码,例如 `m_sea`、`d_container` |
+| `b_parent_id` | `bigint` | 上级模块 ID;根节点为空 |
+| `b_name` | `nvarchar(100)` | 默认中文名称 |
+| `b_i18n` | `varchar(150)` | 多语言键 |
+| `b_module_type` | `varchar(20)` | `directory`、`page` 或 `data` |
+| `b_route` | `varchar(200)` | 页面路由,例如 `/sea/list` |
+| `b_viewtable` | `varchar(128)` | 查询来源表或视图;数据模块字段从此处同步 |
+| `b_savetable` | `varchar(128)` | 保存目标表 |
+| `b_keyfield` | `varchar(50)` | 主键字段,例如 `b_id` |
+| `b_orderfield` | `varchar(500)` | 默认排序,例如 `b_xh ASC, b_id DESC` |
+| `b_canmenu` | `int` | 是否显示在菜单,默认 `0` |
+| `b_canuse` | `int` | 是否启用,默认 `1` |
+| `b_xh` | `int` | 同级显示顺序,默认 `0` |
+
+约束:
+
+- `b_parent_id` 外键引用 `s_module.b_id`;保存服务校验父节点不形成循环。
+- `b_code` 唯一;`b_route` 非空时唯一。
+- `directory` 不能配置 `b_route`、`b_viewtable` 或 `b_savetable`。
+- `page` 必须配置 `b_route`;其业务数据由页面组合的数据模块提供。
+- `data` 不能显示菜单且不能配置 `b_route`;必须配置 `b_viewtable`,保存场景再按需配置 `b_savetable` 与 `b_keyfield`。
+- 建议建立 `(b_parent_id, b_xh, b_id)` 索引。
+
+## 字段来源与同步
+
+### `dbo.s_module_field`
+
+保存字段的基础定义。字段来自数据模块的 `b_viewtable`,并以 `(b_module_id, b_field)` 作为稳定标识;列表、编辑和查询场景属性均由独立配置表维护。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属数据模块 ID |
+| `b_field` | `varchar(50)` | 视图字段名 |
+| `b_name` | `nvarchar(100)` | 默认字段名称,由管理员维护 |
+| `b_i18n` | `varchar(150)` | 多语言键 |
+| `b_type` | `varchar(30)` | 字段控件类型:`input`、`textarea`、`number`、`money`、`date`、`datetime`、`checkbox` 或 `select` |
+| `b_default_value` | `nvarchar(500)` | 新增记录时的默认值 |
+| `b_options` | `nvarchar(max)` | 类型专属 JSON 配置,例如选择器来源、日期格式或金额精度 |
| `b_canuse` | `int` | 是否启用 |
-每个 Module 必须且只能有一个对应子类型记录:
+约束:
-| 子类型表 | 主要字段 |
-| --- | --- |
-| `s_module_page` | `b_route`、`b_page_mode`、`b_component_key` |
-| `s_module_data` | `b_viewtable`、`b_savetable`、`b_keyfield`、`b_orderfield` |
-| `s_module_report` | 来源数据模块/Profile、`b_renderer_key`、`b_options` |
-| `s_module_dashboard` | `b_component_key`、`b_options` |
-| `s_module_workflow` | `b_engine_key`、`b_options` |
+- `b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_field)` 唯一。
+- 建议建立 `(b_module_id, b_canuse, b_id)` 索引。
+- 字段没有 list、edit 或 query 配置记录时,不参与对应场景。
+- `b_type` 是展示和编辑的唯一类型来源:Table 按它渲染默认显示器,Form 或可编辑 Table 按它选择控件。
+- `select` 是否允许自由输入、选项来源、值字段和显示字段均由 `b_options` 控制。
-报表、仪表盘和工作流只定义运行时契约,本阶段不提供报表引擎、设计器或流程引擎。
+### `dbo.s_module_auto_code`
-### `dbo.s_module_profile`
+每个数据模块最多绑定一条自动业务编号规则。`b_id` 仍是数据库技术主键,自动编码写入模块绑定的业务字段;业务字段自身应在实际保存表上建立唯一索引。
-Profile 只属于数据模块。同一模块内 `b_code` 唯一,并且只能有一个 `b_default = 1`。
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属数据模块 ID,唯一 |
+| `b_field_id` | `bigint` | 写入业务编号的字段定义 ID |
+| `b_prefix` | `nvarchar(100)` | 固定前缀,可为空 |
+| `b_date_format` | `varchar(30)` | 日期格式,可为空 |
+| `b_separator` | `nvarchar(10)` | 编码各部分之间的分隔符,可为空 |
+| `b_sequence_width` | `int` | 流水号补零位数 |
+| `b_reset_type` | `varchar(10)` | `none`、`year`、`month` 或 `day` |
+| `b_start_value` | `bigint` | 流水号起始值 |
+| `b_current_period` | `varchar(8)` | 当前计数周期 |
+| `b_current_value` | `bigint` | 当前已分配流水值 |
+| `b_canuse` | `int` | 是否启用 |
-| 字段 | 说明 |
-| --- | --- |
-| `b_id` | 不可变雪花 ID |
-| `b_module_id` | 所属数据模块 |
-| `b_code` | 模块内唯一、不可变技术编码,如 `default`、`selector` |
-| `b_name` / `b_i18n` | 默认名称及 `meta.profile.` |
-| `b_default` / `b_canuse` / `b_xh` | 默认、启用和排序 |
+取号时锁定规则行并递增 `b_current_value`。编号生成后不允许业务用户修改;取号与业务保存分开时允许断号,但目标业务字段必须建立唯一索引。
-运行时列表通过 `module_id + profile_id` 定位;手写页面可以通过模块/Profile 技术编码解析稳定 ID。
+### 视图同步规则
-## 功能结构
+字段同步服务仅面向 `b_module_type = 'data'` 且配置了 `b_viewtable` 的模块执行:
-### `dbo.s_function_node`
+1. 后端校验视图名并读取列名、SQL 类型和可空性等结构信息。
+2. 按列名查找同模块的 `s_module_field`;新列创建字段基础记录。
+3. 新字段按 SQL 类型初始化 `b_type`,例如字符串映射为 `input`、长文本映射为 `textarea`、数值映射为 `number`、日期映射为 `date` 或 `datetime`、位值映射为 `checkbox`。已有字段不覆盖管理员维护的 `b_type` 与 `b_options`;`select`、`money` 等业务类型由管理员按语义调整。
+4. 本次视图中不存在的已有字段保留其记录和全部场景配置,但将 `b_canuse` 置为 `0`。
+5. 不覆盖人工维护的 `b_name`、`b_i18n`、`b_default_value` 与 `b_options`,不物理删除字段或场景配置。
-节点类型:
+## 列表配置
-- `directory`:只负责分组,不引用模块。
-- `entry`:引用一个 `page` Module。
+### `dbo.s_module_field_list`
-规则:
+控制列表字段列。多级表头由 `s_module_field_group` 的 `list` 分组树表达;本表不保存编辑状态。
-- 技术编码全局唯一且不可修改。
-- 子节点只能放在 `directory` 下。
-- 入口节点不能包含子节点。
-- 数据库触发器和维护界面同时阻止循环移动。
-- 删除前必须清除角色和用户授权。
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属模块 ID |
+| `b_field_id` | `bigint` | 关联 `s_module_field.b_id` |
+| `b_group_id` | `bigint` | 所属分组 ID;为空时为顶层列 |
+| `b_i18n` | `varchar(150)` | 字段标题多语言键覆盖值 |
+| `b_visible` | `int` | 是否显示,默认 `1` |
+| `b_xh` | `int` | 同一分组内的显示顺序,默认 `0` |
+| `b_width` | `int` | 字段列宽 |
-菜单运行时读取 `v_s_function_node`,路由组件仍由前端白名单显式注册。
+约束:
-## 页面编排
+- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`,`b_group_id` 引用同模块的 `s_module_field_group`。
+- `b_field_id` 必填;同一字段在同一模块的列表中最多出现一次,使用 `(b_module_id, b_field_id)` 唯一约束。
+- 字段列的显示器由关联字段的 `b_type` 与 `b_options` 决定,例如 `money`、`date`、`datetime` 均由前端按统一规则渲染。建议建立 `(b_module_id, b_group_id, b_xh, b_id)` 索引。
+- 不保存 `level`、`colspan`、`rowspan`;前端加载 Table 分组树和字段列后,按分组的 `b_parent_id`、`b_xh` 组装嵌套表头。隐藏分组的后代不显示;没有可见字段后代的分组不渲染。
-### `dbo.s_page_node`
-
-节点类型:`group`、`tab`、`section`、`module_ref`。
-
-- 所有节点属于一个 `page` Module。
-- 容器节点不引用模块。
-- `module_ref` 可以引用 `data/report/dashboard/workflow` Module。
-- 引用数据模块时必须同时引用属于该模块的 Profile。
-- `module_ref` 不能包含子节点。
-- `b_permission_required = 1` 时需要页面节点授权。
-- `b_fixed_condition` 与 `b_param_mapping` 只保存运行时组合参数。
-
-页面运行时读取 `v_s_page_node`。`otherdata` 的分类和模块树迁入该表,不再保留 `b_other_bmfl`。
-
-## 能力模型
-
-### Module 级能力
-
-| 表 | 职责 |
-| --- | --- |
-| `s_module_field` | 数据字段基础定义,`(module_id, field)` 唯一 |
-| `s_module_action` | 稳定操作定义,`(module_id, code)` 唯一 |
-| `s_module_relation` | 主子模块字段关系 |
-| `s_module_auto_code` | 模块业务编号规则 |
-| `s_module_file` | 模块文件能力契约 |
-| `s_module_workflow_binding` | 模块事件/操作与工作流资源绑定 |
-
-### Profile 级能力
-
-| 表 | 职责 |
-| --- | --- |
-| `s_module_layout_group` | Profile 内列表/表单布局分组 |
-| `s_module_field_list` | 列表列、分组、可见性、顺序、宽度 |
-| `s_module_field_form` | 表单必填、只读、禁用、可见性和顺序 |
-| `s_module_field_query` | 查询控件、操作符、默认值和顺序 |
-| `s_module_import` | 导入方式与扩展参数 |
-| `s_module_export` | 导出方式与扩展参数 |
-
-字段同步按字段技术名合并:保留已有人工配置,新字段补默认列表/表单配置,数据源中失效的字段只停用、不删除。
-
-## 权限
-
-角色是主授权,用户授权只做加法:
+示例:
```text
-b_role
- -> b_role_function_node
- -> b_role_page_node
- -> b_role_action
- -> b_role_data_scope
-
-b_user
- -> b_user_role
- -> b_user_function_node
- -> b_user_page_node
- -> b_user_action
+费用(`s_module_field_group`)
+|-- 人工费(`s_module_field_list.b_group_id` 指向费用)
+`-- 海运费(`s_module_field_list.b_group_id` 指向费用)
```
-有效权限视图:
+## 编辑配置
-- `v_s_user_function_permission`
-- `v_s_user_page_permission`
-- `v_s_user_action_permission`
+### `dbo.s_module_field_edit`
-超级角色自动拥有全部启用权限。授权关系始终保存节点或操作 ID,不保存名称。当前由前端执行菜单、路由、页面节点和操作控制;后端数据范围和写操作拦截为后续扩展点。
+控制字段的通用编辑属性、显示状态与编辑分组。控件类型和类型专属参数统一由 `s_module_field.b_type`、`b_options` 决定;FmsForm 与 FmsTable 自行决定何时使用本配置。
-`b_role_data_scope` 预留 `all/self/department/custom` 数据范围契约及扩展参数。本阶段不在查询接口中执行该规则。
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属模块 ID |
+| `b_field_id` | `bigint` | 关联 `s_module_field.b_id` |
+| `b_required` | `int` | 是否必填,默认 `0` |
+| `b_readonly` | `int` | 是否只读,默认 `0` |
+| `b_disabled` | `int` | 是否禁用,默认 `0` |
+| `b_visible` | `int` | 是否显示,默认 `1` |
+| `b_group_id` | `bigint` | 所属编辑分组 ID;不分组时为空 |
+| `b_xh` | `int` | 编辑场景中的显示顺序,默认 `0` |
-## 多语言
+约束:
-固定前端文案使用语义键,例如 `common.save`。动态元数据使用 ID 键:
+- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验字段属于同一模块。
+- `b_group_id` 必须关联同模块的 `s_module_field_group`;分组为空时字段进入默认编辑区域。
+- 已分组字段按分组的 `b_xh` 排列,组内按 `b_xh` 排列;默认区域字段按 `b_xh` 排列。
+
+## 布局分组
+
+### `dbo.s_module_field_group`
+
+统一保存列表多级表头和编辑分组。分组只负责布局,不定义编辑能力;列表或编辑用途由引用它的配置表决定。
+
+被 `s_module_field_list` 引用时,分组组成同一份 FmsTable 的多级表头;被 `s_module_field_edit` 引用时,分组组成同一份编辑界面中的业务区块,例如提单、订舱、报关、备注,不代表拆分为多个 Form。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属模块 ID |
+| `b_parent_id` | `bigint` | 上级分组 ID;顶层为空 |
+| `b_title` | `nvarchar(100)` | 默认分组标题 |
+| `b_i18n` | `varchar(150)` | 分组标题多语言键 |
+| `b_xh` | `int` | 分组显示顺序,默认 `0` |
+| `b_canuse` | `int` | 是否启用,默认 `1` |
+
+约束:
+
+- `b_module_id` 外键引用 `s_module.b_id`;`b_id` 是分组的稳定关联键。
+- `b_parent_id` 必须引用同模块的分组且不能形成循环。
+- 多级分组可用于 FmsTable 表头;FmsForm 与 FmsTable 如何消费编辑分组由各自实现决定。
+- 建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
+- 无字段引用的分组不渲染;分组停用时,其关联字段或列不显示。
+
+视图型选择器的 `b_options` 示例:
+
+```json
+{
+ "source": "view",
+ "view": "v_partner",
+ "valueField": "b_id",
+ "labelField": "b_name",
+ "allowInput": false
+}
+```
+
+`select` 使用 `allowInput: false` 时保存 `valueField` 的值;设为 `true` 时可保存选择值或用户输入文本。视图名、值字段和标签字段均须由后端校验。
+
+## 查询配置
+
+### `dbo.s_module_field_query`
+
+独立控制主列表查询条件,不依赖字段是否配置为可见列表列。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属模块 ID |
+| `b_field_id` | `bigint` | 关联 `s_module_field.b_id` |
+| `b_component` | `varchar(30)` | 查询组件 |
+| `b_operator` | `varchar(20)` | `=`、`like`、`between` 或 `in` |
+| `b_default_value` | `nvarchar(500)` | 默认查询值 |
+| `b_xh` | `int` | 查询条件显示顺序,默认 `0` |
+| `b_canuse` | `int` | 是否启用,默认 `1` |
+
+约束:
+
+- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验模块一致性。
+- 查询组件与操作符由后端白名单校验;查询值必须参数化,不能由配置直接拼接 SQL。
+- 建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
+
+## 权限与多语言
+
+### `dbo.s_module_power`
+
+定义模块内可授权的按钮操作。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_module_id` | `bigint` | 所属模块 ID |
+| `b_code` | `varchar(50)` | 操作编码,例如 `create`、`update`、`delete`、`audit`、`export` |
+| `b_name` | `nvarchar(100)` | 操作名称 |
+| `b_i18n` | `varchar(150)` | 多语言键 |
+| `b_xh` | `int` | 显示顺序 |
+| `b_canuse` | `int` | 是否启用,默认 `1` |
+
+约束:`b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_code)` 唯一。
+
+### `dbo.b_user_module`
+
+控制用户能否查看、进入和调用模块。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_user_id` | `bigint` | 用户雪花 ID |
+| `b_module_id` | `bigint` | 模块 ID |
+| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
+| `b_inputdatetime` | `datetime` | 授权时间 |
+
+约束:`b_module_id` 外键引用 `s_module.b_id`,`(b_user_id, b_module_id)` 唯一。目录不单独授权;用户拥有下级模块时自动显示上级目录。
+
+### `dbo.b_user_power`
+
+控制用户可执行的按钮操作。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_user_id` | `bigint` | 用户雪花 ID |
+| `b_module_id` | `bigint` | 所属模块 ID |
+| `b_power_id` | `bigint` | `s_module_power.b_id` |
+| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
+| `b_inputdatetime` | `datetime` | 授权时间 |
+
+逻辑关联:`b_module_id` 对应 `s_module.b_id`,`b_power_id` 对应 `s_module_power.b_id`;`(b_user_id, b_power_id)` 唯一。建议建立 `(b_module_id, b_user_id)` 索引。
+
+权限规则:
+
+1. 用户必须拥有 `b_user_module` 记录,才能进入或调用该模块。
+2. 用户必须同时拥有模块权限和对应 `b_user_power` 记录,才能执行按钮动作。
+3. 前端据此控制路由进入和按钮可用性;后端接口后续实施同等权限校验。
+
+### `dbo.b_i18n`
+
+统一保存菜单、模块、字段、权限、按钮、提示和校验信息等界面文案。
+
+| 字段 | 类型 | 说明 |
+| --- | --- | --- |
+| `b_id` | `bigint` | 雪花 ID 主键 |
+| `b_key` | `varchar(150)` | 多语言键 |
+| `b_locale` | `varchar(20)` | 语言编码,例如 `zh-CN` |
+| `b_value` | `nvarchar(500)` | 翻译文本 |
+
+约束:`(b_key, b_locale)` 唯一。
+
+## 页面组合示例
+
+海运业务页面由 Vue 显式组合数据模块,不通过字段配置创建动态页面区域:
```text
-meta.module.
-meta.node.
-meta.profile.
-meta.group.
-meta.field.
-meta.action.
-meta.role.
+海运页面 m_sea(page:路由、菜单、权限、页面编排)
+|-- 海运主单 d_sea(data)
+|-- 费用 d_charge(data)
+|-- 订舱 d_booking(data,独立表和主键时)
+`-- 箱明细 d_container(data)
```
-动态实体保存 `b_name` 与 `b_i18n`,Vue 统一调用:
+若订舱字段实际保存于海运主单表,不创建 `d_booking`,而在 `d_sea` 的编辑配置中用 `b_group_id` 和 `b_xh` 定义编辑分组与顺序,分组标题来自 `s_module_field_group`。主子表关联、加载时机、保存顺序和页面布局均由业务 Vue 页面明确实现。
-```js
-appStore.t(record.b_i18n, record.b_name);
-```
+## 现有字段迁移
-翻译缺失时回退 `b_name`,不会阻塞配置使用。
+当前 `s_module_field` 混合保存基础、列表、编辑和查询属性。迁移应分阶段实施:
-## 运行时视图
+| 当前字段 | 目标位置 |
+| --- | --- |
+| `b_type` | `s_module_field.b_type` |
+| `b_canlist` | `s_module_field_list.b_visible` |
+| `b_width` | `s_module_field_list.b_width` |
+| `b_xh` | 初始化 `s_module_field_list.b_xh`、`s_module_field_edit.b_xh` 与 `s_module_field_query.b_xh` |
+| `b_canform` | 为原 Form 字段创建 `s_module_field_edit` 记录 |
+| `b_must` | `s_module_field_edit.b_required` |
+| `b_readonly` | `s_module_field_edit.b_readonly` |
+| `b_canquery` | 创建 `s_module_field_query` 记录 |
+| `b_value` | 迁移为 `s_module_field.b_default_value` |
-`034_module_runtime_views.sql` 建立:
+迁移步骤:
-- `v_s_function_node`
-- `v_s_page_node`
-- `v_s_module_profile`
-- `v_s_module_runtime`
-- `v_s_module_field_list`
-- `v_s_module_field_form`
-- `v_s_module_field_query`
-- `v_s_module_action`
-- 三个用户有效权限视图
-
-视图只组合读取契约,不承载保存逻辑。所有多表配置仍通过一次 `/data/saveobjt` 在同一事务内提交。
-
-## 迁移与校验
-
-按顺序人工执行:
-
-1. `033_create_module_structure.sql`:一次性迁移有效旧元数据并删除旧模型。
-2. `034_module_runtime_views.sql`:建立运行时视图和领域约束触发器。
-3. `verify_module_architecture.sql`:只读检查子类型、Profile、父子规则、循环、旧表和多语言键。
-
-执行前必须备份数据库。迁移脚本不由应用启动自动执行。
+1. 新建 list、edit、group、query 表和必要索引,新增 `s_module.b_module_type` 与 `s_module_field.b_type` 的兼容列。
+2. 为原有字段创建列表配置;根据原 `b_canform`、`b_canquery` 初始化编辑和查询配置。
+3. 前端和后端改为读取新场景表,查询不再读取 list 配置;模块类型切换为读取 `b_module_type`。
+4. 完成视图同步服务并验证新结构的读写稳定后,删除旧混合列和兼容字段。
diff --git a/code/fms/旧数据库结构.md b/code/fms/旧数据库结构.md
index 57ab1bab..d90940d5 100644
--- a/code/fms/旧数据库结构.md
+++ b/code/fms/旧数据库结构.md
@@ -83,8 +83,6 @@
| [`dbo.b_money`](#db-dbo-b_money) | 13 | - |
| [`dbo.b_oceanline`](#db-dbo-b_oceanline) | 8 | - |
| [`dbo.b_operator`](#db-dbo-b_operator) | 3 | - |
-| [`dbo.b_other`](#db-dbo-b_other) | 10 | - |
-| [`dbo.b_other_bmfl`](#db-dbo-b_other_bmfl) | 10 | - |
| [`dbo.b_othercompany`](#db-dbo-b_othercompany) | 49 | - |
| [`dbo.b_othercompany_accountbank`](#db-dbo-b_othercompany_accountbank) | 9 | - |
| [`dbo.b_othercompany_bmfl`](#db-dbo-b_othercompany_bmfl) | 21 | - |
@@ -557,38 +555,6 @@
| 2 | `b_name` | `varchar(50)` | 是 | - | - | - | - |
| 3 | `b_bz` | `varchar(200)` | 是 | - | - | - | - |
-
-### `dbo.b_other`
-
-| 序号 | 字段 | 类型 | 可空 | 默认值 | 主键 | 属性 | 说明 |
-| ---: | --- | --- | :---: | --- | :---: | --- | --- |
-| 1 | `b_cateid` | `varchar(50)` | 是 | - | - | - | - |
-| 2 | `b_id` | `varchar(50)` | 是 | - | - | - | - |
-| 3 | `b_name_c` | `varchar(50)` | 是 | - | - | - | - |
-| 4 | `b_1` | `numeric(18,0)` | 是 | - | - | - | - |
-| 5 | `b_2` | `varchar(50)` | 是 | - | - | - | - |
-| 6 | `b_desc` | `varchar(255)` | 是 | - | - | - | - |
-| 7 | `b_inputuser_id` | `varchar(50)` | 是 | - | - | - | - |
-| 8 | `b_inputdatetime` | `datetime` | 是 | - | - | - | - |
-| 9 | `b_updateuser_id` | `varchar(50)` | 是 | - | - | - | - |
-| 10 | `b_updatedatetime` | `datetime` | 是 | - | - | - | - |
-
-
-### `dbo.b_other_bmfl`
-
-| 序号 | 字段 | 类型 | 可空 | 默认值 | 主键 | 属性 | 说明 |
-| ---: | --- | --- | :---: | --- | :---: | --- | --- |
-| 1 | `b_id` | `varchar(50)` | 否 | - | 1 | - | - |
-| 2 | `mc_b` | `varchar(50)` | 是 | - | - | - | - |
-| 3 | `b_tablename` | `varchar(50)` | 是 | - | - | - | - |
-| 4 | `b_canuse` | `varchar(1)` | 是 | - | - | - | - |
-| 5 | `b_xh` | `varchar(50)` | 是 | - | - | - | - |
-| 6 | `b_groupby` | `varchar(50)` | 是 | - | - | - | - |
-| 7 | `b_inputuser_id` | `varchar(50)` | 是 | - | - | - | - |
-| 8 | `b_inputdatetime` | `datetime` | 是 | - | - | - | - |
-| 9 | `b_updateuser_id` | `varchar(50)` | 是 | - | - | - | - |
-| 10 | `b_updatedatetime` | `datetime` | 是 | - | - | - | - |
-
### `dbo.b_othercompany`
diff --git a/code/fms/模块管理实施计划.md b/code/fms/模块管理实施计划.md
index 16b7e36b..606cca7b 100644
--- a/code/fms/模块管理实施计划.md
+++ b/code/fms/模块管理实施计划.md
@@ -1,42 +1,248 @@
-# 模块架构实施说明
+# 模块管理实施计划
-本文件记录当前模块架构的实现边界。表结构以 `新数据库结构.md` 为准。
+## 目标
-## 已实现范围
+使用 `fms-api/config/dbconfigs/g3hd.properties` 指向的新数据库建立模块管理表结构,并将 `ModuleManageDemo.vue` 改造成真实页面。
-- Module 资源基表与 page/data/report/dashboard/workflow 子类型表。
-- FunctionNode 菜单和页面入口树。
-- PageNode 页面分组、Tab、区域和模块引用树。
-- 数据模块 Profile 复用,自动创建 `default`,支持复制使用方式。
-- Module 级字段、操作、关系、自动编码、文件和工作流绑定契约。
-- Profile 级列表、表单、查询、导入和导出契约。
-- 角色主授权、用户角色和用户附加授权。
-- 前端菜单、路由、页面节点和模块操作权限控制。
-- ID 型动态多语言键和名称回退。
-- 运行时组合视图、树约束触发器和只读校验脚本。
-- 模块管理按功能结构、页面编排、模块资源拆分。
+本阶段只实现《新数据库结构.md》中的六张表,不实现 `select` 的选项来源和模块关联。
-## 保持不变
+## 实施原则
-- `/data/loaddata`、`/data/page`、`/data/saveobjt` 等通用接口及结构。
-- 通用表格、文件组件和业务表数据。
-- 手写 Vue 页面中的业务事件与业务流程。
-- 前端显式页面组件白名单。
+- 不新增模块管理专用后端接口。
+- 查询复用 `/data/loaddata` 和 `/data/page`。
+- 保存复用 `/data/saveobjt`,一次请求完成多表事务。
+- 雪花 ID 复用 `/data/nextid`。
+- 不使用 `/data/loaddatabysql` 拼接模块管理 SQL。
+- 只操作 `g3hd.properties` 指向的 `fms` 数据库,不修改旧数据库。
+- 真实页面沿用 Demo 的左树右工作区和 Antdv Next Tree、Form、Table 设计。
-## 明确不实现
+## 第一阶段:建立数据库
-- 旧模块元数据兼容层和双写。
-- 报表引擎、仪表盘设计器和工作流引擎。
-- 后端数据范围执行和写操作权限拦截;只保留 `b_role_data_scope` 契约。
-- 业务表或业务数据重构。
+### 建表脚本
-## 切换步骤
+新增:
-1. 备份 `FMS` 数据库。
-2. 执行 `033_create_module_structure.sql`。
-3. 执行 `034_module_runtime_views.sql`。
-4. 执行 `verify_module_architecture.sql`。
-5. 部署同一次提交中的前端代码。
-6. 回归普通列表、可编辑列表、`otherdata`、用户授权和模块管理。
+```text
+fms-api/config/migrations/001_module_management.sql
+fms-api/config/migrations/002_module_order_condition.sql
+fms-api/config/migrations/003_module_surrogate_key.sql
+```
-迁移和服务启动均由执行人员明确操作,应用不会自动执行数据库迁移。
+按以下顺序建立表:
+
+1. `s_module`
+2. `s_module_field`
+3. `s_module_power`
+4. `b_user_module`
+5. `b_user_power`
+6. `b_i18n`
+
+脚本包含:
+
+- 主键、外键和唯一约束。
+- 模块树、字段列表和个人权限所需索引。
+- `XACT_ABORT ON` 和显式事务。
+- `IF NOT EXISTS`,保证脚本可以重复执行。
+- 不删除、不修改任何已有业务表。
+
+### 初始化数据
+
+初始化:
+
+| 模块 | 类型 | 路由 |
+| --- | --- | --- |
+| `m_system` | `directory` | - |
+| `m_module` | `page` | `/system/modules` |
+
+为 `m_module` 初始化按钮:
+
+```text
+create 新增模块
+update 修改模块
+delete 删除模块
+grant 用户授权
+```
+
+根据明确的管理员账号从 `b_user` 取得用户雪花 ID,写入该用户的 `b_user_module` 和四项 `b_user_power`。找不到管理员时终止初始化,不能默认给所有用户授权。
+
+### 执行和验证
+
+- 从 `g3hd.properties` 读取连接信息,密码不打印到日志。
+- 使用 PowerShell `.NET SqlClient` 执行脚本(本机旧版 `sqlcmd` 无法完成 TLS 握手)。
+- 执行前确认目标为 `118.89.70.199:1433 / fms`。
+- 连续执行两次,第二次不得重复建表或重复初始化。
+- 查询 SQL Server 系统表核对字段、约束、外键和索引。
+
+## 第二阶段:登录用户信息
+
+当前 JWT 使用登录账号,新权限表使用 `b_user.b_id`,需要调整:
+
+- 登录时同时保留用户雪花 ID、账号和姓名。
+- JWT 继续使用账号作为会话标识。
+- 登录响应返回 `id`、`account` 和 `name`。
+
+本阶段不修改通用查询和保存接口的请求格式,也不在后端执行模块权限校验。
+
+## 第三阶段:使用通用接口完成数据闭环
+
+### 数据加载
+
+页面通过现有接口加载:
+
+| 数据 | 接口 | 数据源 |
+| --- | --- | --- |
+| 模块树 | `/data/loaddata` | `s_module` |
+| 模块字段 | `/data/loaddata` | `s_module_field` |
+| 操作权限定义 | `/data/loaddata` | `s_module_power` |
+| 多语言 | `/data/loaddata` | `b_i18n` |
+| 删除依赖检查 | `/data/loaddata` | `b_user_module`、`b_user_power` |
+
+配置数据量较小,模块、字段和操作权限可以一次加载后在前端按 `b_module_id` 分组,减少来回请求。
+
+### 雪花 ID
+
+新增以下数据前调用 `/data/nextid`:
+
+- `s_module_field.b_id`
+- `s_module_power.b_id`
+- `b_i18n.b_id`
+- `s_module.b_id`
+
+`s_module.b_id` 使用不可变雪花 ID;可修改的小写模块编码保存在 `s_module.b_code`。
+
+### 模块保存
+
+前端维护服务器快照和编辑副本,计算 inserts、updates、deletes,然后一次调用 `/data/saveobjt` 保存:
+
+```text
+s_module
+s_module_field
+s_module_power
+b_i18n
+```
+
+通用保存请求示意:
+
+```json
+[
+ {
+ "table": "s_module",
+ "key_field": "b_id",
+ "updates": []
+ },
+ {
+ "table": "s_module_field",
+ "key_field": "b_id",
+ "inserts": [],
+ "updates": [],
+ "deletes": []
+ }
+]
+```
+
+前端保存前校验:
+
+- 模块代码使用小写字母、数字和下划线,且在模块间唯一。
+- 父节点存在且不会形成循环。
+- 目录、页面、数据模块字段组合合法。
+- 同一模块字段名和按钮编码不重复。
+- 表名、视图名和字段名只允许合法数据库标识符。
+
+数据库外键和唯一约束负责最后兜底,任一表失败时由现有 `DataSaveService` 回滚整个请求。
+
+### 模块删除
+
+- `m_system` 和 `m_module` 在前端禁止删除。
+- 存在下级模块时禁止删除。
+- 普通模块删除时,一次 `/data/saveobjt` 按依赖顺序删除:
+
+```text
+b_user_power
+b_user_module
+s_module_power
+s_module_field
+b_i18n
+s_module
+```
+
+## 第四阶段:将 Demo 改为真实页面
+
+### 页面结构
+
+路由由 `/demo/modules` 改为 `/system/modules`,删除“前端原型”标识和 mock 数据。
+
+建议拆分:
+
+```text
+views/module-management/ModuleManagementView.vue
+views/module-management/components/ModuleTreePanel.vue
+services/moduleManagement.js
+```
+
+`services/moduleManagement.js` 只组合现有 `loadDataApi`、`saveObjectApi` 和 `nextIdApi`,不增加新的后端地址。
+
+### 页面功能
+
+- 左侧模块树支持搜索、新增目录和新增模块。
+- 左侧模块树只保留单个新增菜单,不直接显示目录、页面、数据三个按钮。
+- 模块管理界面只区分目录和普通模块:目录使用独立表单,新增目录使用 Dialog。
+- 页面模块和数据模块共用普通模块编辑器;配置页面路由时保存为 `page`,否则保存为 `data`,不直接编辑模块类型。
+- 基本信息、字段、操作权限和多语言全部读取真实数据库。
+- 字段、操作权限和多语言内容区独立滚动,页面外层不随数据量滚动。
+- 字段和操作权限采用点击行进入编辑、离开行退出编辑的单行编辑方式。
+- 字段名称支持在字段行内快速维护多语言文本。
+- 增加保存、取消和删除操作,当前阶段不显示未保存提示。
+- 保存失败时保留编辑内容并显示后端错误。
+- 字段编码、操作权限编码和模块代码使用明确的表单校验。
+- 删除字段、操作权限和模块使用确认框。
+- 修复 Demo 菜单预览使用不存在 `id` 作为 Vue key 的问题。
+- 保存成功后重新加载模块树和当前模块。
+
+### 菜单
+
+`AppSidebar` 通过现有通用查询接口读取当前用户的 `b_user_module` 和可用的 `s_module`,在前端构建菜单树。
+
+- 只显示 `b_canmenu = 1`、`b_canuse = 1` 且用户有权限的页面。
+- 上级目录根据可访问下级页面自动显示。
+- 路由组件仍由前端白名单注册,数据库路由不能动态加载任意组件文件。
+- 通用接口加载失败时保留现有静态菜单作为降级。
+
+### 前端权限
+
+- 数据型路由通过 `meta.dataCode` 声明核心数据编码,权限仓库根据数据节点的 `b_parent_id` 解析页面权限;尚未配置数据节点的系统页面暂时兼容 `meta.moduleCode`。
+- 用户没有 `b_user_module` 记录时禁止进入对应页面。
+- 页面操作根据 `b_user_power` 中的 `create`、`update`、`delete` 权限禁用。
+- 本阶段不改通用后端接口,也不做后端权限拦截;后端校验后续单独实施。
+
+## 第五阶段:测试与验收
+
+### 后端
+
+- JWT 能正确取得用户雪花 ID。
+- 多表保存失败时整体回滚。
+- 非法表名和字段名被现有通用接口拒绝,权限引用由数据库外键约束兜底。
+- `bigint` 仍以字符串返回前端。
+
+### 前端
+
+- 模块、字段、操作权限和多语言可以新增、修改、删除并刷新恢复。
+- 模块树搜索、切换和取消操作正常。
+- 加载失败、保存失败和空状态显示正确。
+
+### G3HD 验收
+
+1. 使用管理员账号登录 G3HD。
+2. 进入 `/system/modules`。
+3. 新建临时模块,配置两个字段和新增、修改、删除操作权限。
+4. 保存并刷新,确认所有配置重新加载。
+5. 删除临时模块,确认相关明细和已有个人权限全部清理。
+
+## 完成标准
+
+- 六张表已建立在 `g3hd.properties` 指向的新数据库。
+- 页面不再依赖 `moduleDemo.js`。
+- 没有新增模块管理专用后端接口。
+- 所有查询、保存、取号均复用现有通用接口。
+- 模块、字段、操作权限和多语言可以真实保存并重新加载。
+- 模块管理页面不提供用户授权功能。
+- 前端权限控制正常。