From e17bbe9db378091936958db3b5e2795a0f305033 Mon Sep 17 00:00:00 2001 From: oneao Date: Mon, 3 Aug 2026 22:54:03 +0800 Subject: [PATCH] 20260803225403 --- code/fms/CLAUDE.md | 13 +- code/fms/CODEBUDDY.md | 9 +- .../033_create_module_structure.sql | 896 ++++++++++ .../migrations/034_module_runtime_views.sql | 454 +++++ code/fms/fms-api/config/migrations/README.md | 15 + .../migrations/verify_module_architecture.sql | 163 ++ code/fms/fms-vue/pnpm-lock.yaml | 3 + .../fms-module-list/FmsModuleListPage.vue | 192 ++- code/fms/fms-vue/src/data/moduleDemo.js | 668 ------- .../src/layouts/components/AppSidebar.vue | 189 +- code/fms/fms-vue/src/router/index.js | 4 +- code/fms/fms-vue/src/router/pageComponents.js | 20 + code/fms/fms-vue/src/router/routes.js | 18 +- code/fms/fms-vue/src/stores/permissions.js | 189 +- .../fms-vue/src/views/base/contact/index.vue | 2 +- .../src/views/base/othercompany/index.vue | 4 +- .../src/views/base/otherdata/index.vue | 227 ++- .../components/FieldGroupManager.vue | 14 +- .../components/FunctionStructurePanel.vue | 258 +++ .../components/ModuleBasicPanel.vue | 211 --- .../components/ModuleEditConfigPanel.vue | 4 +- .../components/ModuleFieldsPanel.vue | 16 +- .../components/ModuleListConfigPanel.vue | 4 +- .../components/ModulePowersPanel.vue | 17 +- .../components/ModuleQueryConfigPanel.vue | 2 + .../components/ModuleResourcePanel.vue | 661 +++++++ .../components/PageCompositionPanel.vue | 294 ++++ .../composables/useFunctionStructure.js | 167 ++ .../composables/useModuleResources.js | 665 +++++++ .../composables/usePageComposition.js | 222 +++ .../src/views/module-management/index.vue | 1535 +---------------- .../user/components/PermissionGrantEditor.vue | 219 +++ .../user/components/RoleManagerModal.vue | 257 +++ .../usePermissionAdministration.js | 309 ++++ .../fms-vue/src/views/system/user/index.vue | 706 ++------ code/fms/数据库迁移计划.md | 2 + code/fms/文件组件设计方案.md | 2 +- code/fms/新数据库结构.md | 513 ++---- code/fms/模块管理实施计划.md | 270 +-- code/新建文件夹/cw/ys/detailbill.vue | 12 +- 40 files changed, 5516 insertions(+), 3910 deletions(-) create mode 100644 code/fms/fms-api/config/migrations/033_create_module_structure.sql create mode 100644 code/fms/fms-api/config/migrations/034_module_runtime_views.sql create mode 100644 code/fms/fms-api/config/migrations/README.md create mode 100644 code/fms/fms-api/config/migrations/verify_module_architecture.sql delete mode 100644 code/fms/fms-vue/src/data/moduleDemo.js create mode 100644 code/fms/fms-vue/src/router/pageComponents.js create mode 100644 code/fms/fms-vue/src/views/module-management/components/FunctionStructurePanel.vue delete mode 100644 code/fms/fms-vue/src/views/module-management/components/ModuleBasicPanel.vue create mode 100644 code/fms/fms-vue/src/views/module-management/components/ModuleResourcePanel.vue create mode 100644 code/fms/fms-vue/src/views/module-management/components/PageCompositionPanel.vue create mode 100644 code/fms/fms-vue/src/views/module-management/composables/useFunctionStructure.js create mode 100644 code/fms/fms-vue/src/views/module-management/composables/useModuleResources.js create mode 100644 code/fms/fms-vue/src/views/module-management/composables/usePageComposition.js create mode 100644 code/fms/fms-vue/src/views/system/user/components/PermissionGrantEditor.vue create mode 100644 code/fms/fms-vue/src/views/system/user/components/RoleManagerModal.vue create mode 100644 code/fms/fms-vue/src/views/system/user/composables/usePermissionAdministration.js diff --git a/code/fms/CLAUDE.md b/code/fms/CLAUDE.md index 13202bc0..c07a0522 100644 --- a/code/fms/CLAUDE.md +++ b/code/fms/CLAUDE.md @@ -82,9 +82,10 @@ Vue 3 ` @@ -167,19 +155,6 @@ onMounted(() => permissionStore.load().catch(() => {})); margin: 0; border-radius: 0; line-height: 36px; - transition: padding-inline-start 0.2s ease; - } - :deep(.ant-menu-inline-collapsed) { - .ant-menu-title-content { - display: none !important; - } - .ant-menu-item, - .ant-menu-submenu-title { - padding-inline-start: 20px !important; - } - .ant-menu-item-icon { - vertical-align: middle; - } } } .brand { diff --git a/code/fms/fms-vue/src/router/index.js b/code/fms/fms-vue/src/router/index.js index cb113840..13ccd251 100644 --- a/code/fms/fms-vue/src/router/index.js +++ b/code/fms/fms-vue/src/router/index.js @@ -34,11 +34,11 @@ router.beforeEach(async (to) => { return getRedirectPath(to.query.redirect); } - if (to.meta.moduleId) { + if (to.meta.requiresAuth && to.path !== "/dashboard" && to.path !== "/") { const permissionStore = usePermissionStore(); try { await permissionStore.load(); - if (!permissionStore.canAccess(to.meta.moduleId)) { + if (!permissionStore.canAccessRoute(to.path)) { message.warning("没有该模块的访问权限"); return false; } diff --git a/code/fms/fms-vue/src/router/pageComponents.js b/code/fms/fms-vue/src/router/pageComponents.js new file mode 100644 index 00000000..cfe2bfe2 --- /dev/null +++ b/code/fms/fms-vue/src/router/pageComponents.js @@ -0,0 +1,20 @@ +const pageComponents = Object.freeze({ + dashboard: () => import("@/views/dashboard/index.vue"), + base_othercompany: () => import("@/views/base/othercompany/index.vue"), + base_contact: () => import("@/views/base/contact/index.vue"), + base_otherdata: () => import("@/views/base/otherdata/index.vue"), + module_management: () => import("@/views/module-management/index.vue"), + i18n_type: () => import("@/views/system/i18n-type/index.vue"), + login_log: () => import("@/views/system/login-log/index.vue"), + user_management: () => import("@/views/system/user/index.vue"), +}); + +export function getPageComponent(componentKey) { + const component = pageComponents[componentKey]; + if (!component) throw new Error(`未注册的页面组件:${componentKey}`); + return component; +} + +export function hasPageComponent(componentKey) { + return Object.hasOwn(pageComponents, componentKey); +} diff --git a/code/fms/fms-vue/src/router/routes.js b/code/fms/fms-vue/src/router/routes.js index 08f6ddd8..16463c0f 100644 --- a/code/fms/fms-vue/src/router/routes.js +++ b/code/fms/fms-vue/src/router/routes.js @@ -1,3 +1,5 @@ +import { getPageComponent } from "./pageComponents"; + const routes = [ { path: "/", @@ -11,13 +13,13 @@ const routes = [ { path: "dashboard", name: "dashboard", - component: () => import("@/views/dashboard/index.vue"), + component: getPageComponent("dashboard"), meta: { title: "工作台", menuKey: "/dashboard" }, }, { path: "base/othercompany", name: "base-othercompany", - component: () => import("@/views/base/othercompany/index.vue"), + component: getPageComponent("base_othercompany"), meta: { title: "合作伙伴", section: "基础数据", @@ -28,7 +30,7 @@ const routes = [ { path: "base/contact", name: "base-contact", - component: () => import("@/views/base/contact/index.vue"), + component: getPageComponent("base_contact"), meta: { title: "联系人", section: "基础数据", @@ -39,7 +41,7 @@ const routes = [ { path: "base/otherdata", name: "base-otherdata", - component: () => import("@/views/base/otherdata/index.vue"), + component: getPageComponent("base_otherdata"), meta: { title: "其他数据", section: "基础数据", @@ -50,7 +52,7 @@ const routes = [ { path: "system/module-management", name: "system-module-management", - component: () => import("@/views/module-management/index.vue"), + component: getPageComponent("module_management"), meta: { title: "模块管理", section: "系统管理", @@ -61,7 +63,7 @@ const routes = [ { path: "system/i18n-type", name: "system-i18n-type", - component: () => import("@/views/system/i18n-type/index.vue"), + component: getPageComponent("i18n_type"), meta: { title: "多语言类型", section: "系统管理", @@ -72,7 +74,7 @@ const routes = [ { path: "system/login-log", name: "system-login-log", - component: () => import("@/views/system/login-log/index.vue"), + component: getPageComponent("login_log"), meta: { title: "登录日志", section: "系统管理", @@ -83,7 +85,7 @@ const routes = [ { path: "system/user", name: "system-user", - component: () => import("@/views/system/user/index.vue"), + component: getPageComponent("user_management"), meta: { title: "用户管理", section: "系统管理", diff --git a/code/fms/fms-vue/src/stores/permissions.js b/code/fms/fms-vue/src/stores/permissions.js index 59f13a57..645d7316 100644 --- a/code/fms/fms-vue/src/stores/permissions.js +++ b/code/fms/fms-vue/src/stores/permissions.js @@ -1,80 +1,75 @@ import { computed, ref, watch } from "vue"; import { defineStore } from "pinia"; import { loadDataApi } from "@/services/api"; +import { hasPageComponent } from "@/router/pageComponents"; import { useAuthStore } from "./auth"; export const usePermissionStore = defineStore("permissions", () => { const authStore = useAuthStore(); const loadedPrincipalKey = ref(""); - const modules = ref([]); - const grantedPageIds = ref([]); - const moduleCodes = ref([]); - const powerCodes = ref([]); + const functionNodes = ref([]); + const pageNodes = ref([]); + const actions = ref([]); + const grantedFunctionNodeIds = ref([]); + const grantedPageNodeIds = ref([]); + const grantedActionIds = ref([]); let loadingPromise = null; let loadingPrincipalKey = ""; let loadVersion = 0; - const moduleByCode = computed( - () => new Map(modules.value.map((module) => [String(module.b_code || ""), module])), + const functionNodeById = computed( + () => new Map(functionNodes.value.map((node) => [String(node.b_id), node])), ); - const grantedPageSet = computed(() => new Set(grantedPageIds.value)); - const powerSet = computed(() => new Set(powerCodes.value)); - const isG3soft = computed(() => { - return ( - String(authStore.user?.account || "") - .trim() - .toLowerCase() === "g3soft" - ); - }); + const grantedFunctionSet = computed(() => new Set(grantedFunctionNodeIds.value)); + const grantedPageSet = computed(() => new Set(grantedPageNodeIds.value)); + const grantedActionSet = computed(() => new Set(grantedActionIds.value)); async function load() { const userId = String(authStore.user?.id || ""); - const principalKey = `${String(authStore.orgId || "")}:${userId}`; - if (isG3soft.value) { - const elevatedKey = `g3soft:${principalKey}`; - if (loadedPrincipalKey.value === elevatedKey) return; - loadVersion++; - loadingPromise = null; - loadingPrincipalKey = ""; - loadedPrincipalKey.value = elevatedKey; - modules.value = []; - grantedPageIds.value = []; - moduleCodes.value = []; - powerCodes.value = []; - return; - } if (!/^\d+$/.test(userId)) throw new Error("登录用户 ID 无效,请重新登录"); + const principalKey = `${String(authStore.orgId || "")}:${userId}`; if (loadedPrincipalKey.value === principalKey) return; if (loadingPromise && loadingPrincipalKey === principalKey) return loadingPromise; const requestVersion = ++loadVersion; loadingPrincipalKey = principalKey; const request = Promise.all([ - loadDataApi("s_module", "b_canuse = 1", "b_id ASC"), - loadDataApi("b_user_module", `b_user_id = ${userId}`, "b_id ASC"), - loadDataApi("b_user_power", `b_user_id = ${userId}`, "b_id ASC"), - loadDataApi("s_module_power", "b_canuse = 1", "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", "b_xh ASC, b_id ASC"), + loadDataApi("v_s_module_action", "b_canuse = 1", "b_xh ASC, b_id ASC"), + loadDataApi( + "v_s_user_function_permission", + `b_user_id = ${userId}`, + "b_function_node_id ASC", + ), + loadDataApi("v_s_user_page_permission", `b_user_id = ${userId}`, "b_page_node_id ASC"), + loadDataApi("v_s_user_action_permission", `b_user_id = ${userId}`, "b_action_id ASC"), ]) - .then(([moduleResponse, userModules, userPowers, powers]) => { - if (requestVersion !== loadVersion) return; - const activeModules = moduleResponse.data || []; - const moduleById = new Map(activeModules.map((module) => [String(module.b_id), module])); - const powerById = new Map(powers.data.map((power) => [String(power.b_id), power])); - const pageIds = userModules.data - .map((item) => String(item.b_module_id || "")) - .filter(Boolean); - modules.value = activeModules; - grantedPageIds.value = pageIds; - moduleCodes.value = userModules.data - .map((item) => moduleById.get(String(item.b_module_id))?.b_code) - .filter(Boolean); - powerCodes.value = userPowers.data - .map((item) => powerById.get(String(item.b_power_id))) - .filter(Boolean) - .map((power) => `${String(power.b_module_id)}.${power.b_code}`) - .filter(Boolean); - loadedPrincipalKey.value = principalKey; - }) + .then( + ([ + functionResponse, + pageResponse, + actionResponse, + functionGrants, + pageGrants, + actionGrants, + ]) => { + if (requestVersion !== loadVersion) return; + functionNodes.value = functionResponse.data || []; + pageNodes.value = pageResponse.data || []; + actions.value = actionResponse.data || []; + grantedFunctionNodeIds.value = (functionGrants.data || []).map((item) => + String(item.b_function_node_id), + ); + grantedPageNodeIds.value = (pageGrants.data || []).map((item) => + String(item.b_page_node_id), + ); + grantedActionIds.value = (actionGrants.data || []).map((item) => + String(item.b_action_id), + ); + loadedPrincipalKey.value = principalKey; + }, + ) .finally(() => { if (requestVersion !== loadVersion) return; loadingPromise = null; @@ -84,26 +79,50 @@ export const usePermissionStore = defineStore("permissions", () => { return request; } - function resolvePageId(code) { - const module = moduleByCode.value.get(String(code || "")); - if (!module) return ""; - if (module.b_module_type === "page") return String(module.b_id || ""); - if (module.b_module_type === "data") return String(module.b_parent_id || ""); - return ""; + function hasGrantedDescendant(nodeId) { + const ancestorIds = new Set(); + for (const grantedId of grantedFunctionSet.value) { + let current = functionNodeById.value.get(grantedId); + while (current?.b_parent_id != null) { + const parentId = String(current.b_parent_id); + ancestorIds.add(parentId); + current = functionNodeById.value.get(parentId); + } + } + return ancestorIds.has(String(nodeId)); } - function canAccess(moduleId) { - if (isG3soft.value || !moduleId) return true; - const pageId = resolvePageId(moduleId); - return !!pageId && grantedPageSet.value.has(pageId); + function canAccessFunctionNode(nodeId) { + const id = String(nodeId || ""); + if (!id) return false; + const node = functionNodeById.value.get(id); + if (!node) return false; + if (node.b_node_type === "directory") return hasGrantedDescendant(id); + if (node.b_page_mode === "custom" && !hasPageComponent(node.b_component_key)) return false; + return grantedFunctionSet.value.has(id); } - function canPower(scopeCode, powerCode) { - if (isG3soft.value) return true; - const pageId = resolvePageId(scopeCode); - return ( - !!pageId && grantedPageSet.value.has(pageId) && powerSet.value.has(`${pageId}.${powerCode}`) + function canAccessRoute(path) { + if (path === "/dashboard") return true; + const node = functionNodes.value.find( + (item) => item.b_node_type === "entry" && String(item.b_route || "") === String(path || ""), ); + return node ? canAccessFunctionNode(node.b_id) : false; + } + + function canViewPageNode(nodeId, permissionRequired = true) { + if (!permissionRequired) return true; + return grantedPageSet.value.has(String(nodeId || "")); + } + + function canAction(moduleIdOrCode, actionCode) { + const action = actions.value.find( + (item) => + (String(item.b_module_id) === String(moduleIdOrCode) || + String(item.b_module_code) === String(moduleIdOrCode)) && + String(item.b_code) === String(actionCode), + ); + return action ? grantedActionSet.value.has(String(action.b_id)) : false; } function clear() { @@ -111,26 +130,30 @@ export const usePermissionStore = defineStore("permissions", () => { loadingPromise = null; loadingPrincipalKey = ""; loadedPrincipalKey.value = ""; - modules.value = []; - grantedPageIds.value = []; - moduleCodes.value = []; - powerCodes.value = []; + functionNodes.value = []; + pageNodes.value = []; + actions.value = []; + grantedFunctionNodeIds.value = []; + grantedPageNodeIds.value = []; + grantedActionIds.value = []; } - watch( - () => [authStore.orgId, authStore.user?.id, authStore.user?.account, authStore.token], - clear, - ); + watch(() => [authStore.orgId, authStore.user?.id, authStore.token], clear); return { load, - canAccess, - canPower, clear, - modules, - grantedPageIds, - moduleCodes, - powerCodes, - isG3soft, + canAccessFunctionNode, + canAccessRoute, + canViewPageNode, + canAction, + canAccess: canAccessRoute, + canPower: canAction, + functionNodes, + pageNodes, + actions, + grantedFunctionNodeIds, + grantedPageNodeIds, + grantedActionIds, }; }); diff --git a/code/fms/fms-vue/src/views/base/contact/index.vue b/code/fms/fms-vue/src/views/base/contact/index.vue index 7118c7a8..635cea09 100644 --- a/code/fms/fms-vue/src/views/base/contact/index.vue +++ b/code/fms/fms-vue/src/views/base/contact/index.vue @@ -4,7 +4,7 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu diff --git a/code/fms/fms-vue/src/views/base/othercompany/index.vue b/code/fms/fms-vue/src/views/base/othercompany/index.vue index e9bf5c0c..5cb17977 100644 --- a/code/fms/fms-vue/src/views/base/othercompany/index.vue +++ b/code/fms/fms-vue/src/views/base/othercompany/index.vue @@ -396,7 +396,7 @@ onMounted(() => {
@@ -419,7 +419,6 @@ onMounted(() => { - @@ -479,5 +478,4 @@ onMounted(() => { border: 0; border-radius: 0; } - diff --git a/code/fms/fms-vue/src/views/base/otherdata/index.vue b/code/fms/fms-vue/src/views/base/otherdata/index.vue index e6f7eb61..c9be4091 100644 --- a/code/fms/fms-vue/src/views/base/otherdata/index.vue +++ b/code/fms/fms-vue/src/views/base/otherdata/index.vue @@ -6,17 +6,22 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu import FmsTree from "@/components/FmsTree.vue"; import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from "@/services/api"; import { useAppStore } from "@/stores/app"; +import { usePermissionStore } from "@/stores/permissions"; import { buildTree, sortTree } from "@/utils/tree"; const ALL_CATEGORIES_KEY = "__fms_tree_root__"; const appStore = useAppStore(); +const permissionStore = usePermissionStore(); const categories = ref([]); const dataModules = ref([]); +const profiles = ref([]); +const otherDataPageId = ref(null); const categoryLoading = ref(false); const moduleLoading = ref(false); const selectedCategoryKey = ref(""); -const selectedModuleCode = ref(""); +const selectedModuleId = ref(""); +const selectedProfileId = ref(""); const expandedCategoryKeys = ref([ALL_CATEGORIES_KEY]); const modalVisible = ref(false); const modalParentId = ref(null); @@ -25,13 +30,20 @@ const editingCategoryId = ref(null); const modalType = ref("category"); const saving = ref(false); const deleting = ref(false); -const form = reactive({ b_name: "", b_module_code: "" }); +const form = reactive({ b_name: "", b_module_id: "" }); const categoryById = computed( () => new Map(categories.value.map((item) => [String(item.b_id), item])), ); -const tableKey = computed(() => selectedModuleCode.value); -const shouldLoadData = computed(() => Boolean(selectedModuleCode.value)); +const tableKey = computed(() => `${selectedModuleId.value}:${selectedProfileId.value}`); +const shouldLoadData = computed(() => Boolean(selectedModuleId.value && selectedProfileId.value)); +const canCreateRow = computed(() => permissionStore.canAction(selectedModuleId.value, "create")); +const canUpdateRow = computed(() => permissionStore.canAction(selectedModuleId.value, "update")); +const canDeleteRow = computed(() => permissionStore.canAction(selectedModuleId.value, "delete")); + +function canManageTree(actionCode) { + return permissionStore.canAction("base_otherdata", actionCode); +} // 编辑表格:当前模块列表实例、选中行、保存/删除状态 const moduleListRef = ref(null); @@ -50,7 +62,7 @@ const selectedCategoryKeys = computed(() => ); // 切换数据模块时清空编辑状态,避免残留脏标记与选中项。 -watch(selectedModuleCode, () => { +watch([selectedModuleId, selectedProfileId], () => { selectedRowKeys.value = []; dirtyCount.value = 0; }); @@ -63,7 +75,8 @@ const moduleOptions = computed(() => { for (const item of categories.value) { if (String(item.b_id) === String(editingCategoryId.value || "")) continue; if (getNodeType(item) !== "module") continue; - if (item.b_module_code) usedCodes.add(String(item.b_module_code)); + const module = moduleById.value.get(String(item.b_module_id)); + if (module?.b_code) usedCodes.add(String(module.b_code)); } const options = []; @@ -71,16 +84,23 @@ const moduleOptions = computed(() => { if (!item.b_code || usedCodes.has(String(item.b_code))) continue; options.push({ label: `${appStore.t(item.b_i18n, item.b_name || item.b_code)}(${item.b_code})`, - value: item.b_code, + value: String(item.b_id), }); } return options; }); const isNodeModal = computed(() => modalType.value === "node"); -const moduleByCode = computed(() => { +const moduleById = computed(() => { const map = new Map(); for (const item of dataModules.value) { - if (item.b_code) map.set(String(item.b_code), item); + if (item.b_id != null) map.set(String(item.b_id), item); + } + return map; +}); +const profileByModuleId = computed(() => { + const map = new Map(); + for (const item of profiles.value) { + if (item.b_module_id != null) map.set(String(item.b_module_id), item); } return map; }); @@ -95,13 +115,14 @@ const categoryTree = computed(() => { function buildCategoryTreeNode(item) { let title = ""; - const moduleCode = item.b_module_code || ""; + const moduleId = item.b_module_id == null ? "" : String(item.b_module_id); + const module = moduleById.value.get(moduleId); + const moduleCode = module?.b_code || ""; const nodeType = getNodeType(item); if (nodeType === "module") { - const module = moduleByCode.value.get(String(moduleCode)); title = appStore.t(module?.b_i18n, module?.b_name || item.b_name || moduleCode); } else { - title = item.b_name; + title = appStore.t(item.b_i18n, item.b_name); } if (Number(item.b_canuse) !== 1) { title = `${title}(${appStore.t("common.disabled", "停用")})`; @@ -113,6 +134,7 @@ function buildCategoryTreeNode(item) { b_parent_id: item.b_parent_id == null ? null : String(item.b_parent_id), b_node_type: nodeType, b_module_code: moduleCode, + b_module_id: moduleId || null, b_xh: item.b_xh, children: [], }; @@ -120,9 +142,9 @@ function buildCategoryTreeNode(item) { function getNodeType(item) { if (!item) return "category"; + if (item.b_node_type === "module_ref" || item.b_node_type === "widget") return "module"; if (item.b_node_type === "module") return "module"; - if (item.b_node_type === "category") return "category"; - if (item.b_module_code) return "module"; + if (item.b_module_id != null) return "module"; return "category"; } @@ -130,35 +152,52 @@ function updateSelectedModule(key) { selectedCategoryKey.value = key; if (!key) { - selectedModuleCode.value = ""; + selectedModuleId.value = ""; + selectedProfileId.value = ""; return; } if (key === ALL_CATEGORIES_KEY) { - selectedModuleCode.value = ""; + selectedModuleId.value = ""; + selectedProfileId.value = ""; return; } const category = categoryById.value.get(key); if (!category || getNodeType(category) !== "module") { - selectedModuleCode.value = ""; + selectedModuleId.value = ""; + selectedProfileId.value = ""; return; } - const moduleCode = category.b_module_code || ""; - if (Number(category.b_canuse) !== 1 || !moduleByCode.value.has(String(moduleCode))) { - selectedModuleCode.value = ""; + const module = moduleById.value.get(String(category.b_module_id || "")); + const profileId = String(category.b_profile_id || ""); + if (Number(category.b_canuse) !== 1 || !module?.b_id || !profileId) { + selectedModuleId.value = ""; + selectedProfileId.value = ""; return; } - selectedModuleCode.value = moduleCode; + selectedModuleId.value = String(module.b_id); + selectedProfileId.value = profileId; } async function loadCategories() { categoryLoading.value = true; try { - const response = await loadDataApi("b_other_bmfl", "", "b_parent_id ASC, b_xh ASC, b_id ASC"); - categories.value = response.data || []; + if (!otherDataPageId.value) await loadOtherDataPage(); + if (!otherDataPageId.value) { + categories.value = []; + return; + } + const response = await loadDataApi( + "s_page_node", + `b_page_module_id = ${otherDataPageId.value}`, + "b_parent_id ASC, b_xh ASC, b_id ASC", + ); + categories.value = (response.data || []).filter((item) => + permissionStore.canViewPageNode(item.b_id, Number(item.b_permission_required) === 1), + ); const existingKeys = new Set(categories.value.map((item) => String(item.b_id))); if ( @@ -187,23 +226,29 @@ async function loadCategories() { async function loadDataModules() { moduleLoading.value = true; try { - const pageRes = await loadDataApi( - "s_module", - "b_code = N'base_otherdata' AND b_module_type = 'page' AND b_canuse = 1", - "b_id ASC", - ); - const pageId = pageRes.data?.[0]?.b_id; - if (!pageId) { + if (!otherDataPageId.value) await loadOtherDataPage(); + if (!otherDataPageId.value) { dataModules.value = []; + profiles.value = []; return; } - const moduleRes = await loadDataApi( - "s_module", - `b_parent_id = ${pageId} AND b_module_type = 'data' AND b_canuse = 1`, - "b_xh ASC, b_id ASC", + const runtimeRes = await loadDataApi( + "v_s_module_runtime", + "b_module_type = 'data' AND b_default = 1 AND b_canuse = 1 AND b_profile_canuse = 1", + "b_module_code ASC, b_module_id ASC", ); - dataModules.value = moduleRes.data || []; + dataModules.value = (runtimeRes.data || []).map((item) => ({ + ...item, + b_id: String(item.b_module_id), + b_code: item.b_module_code, + b_name: item.b_module_name, + b_i18n: item.b_module_i18n, + })); + profiles.value = (runtimeRes.data || []).map((item) => ({ + b_id: String(item.b_profile_id), + b_module_id: String(item.b_module_id), + })); updateSelectedModule(selectedCategoryKey.value); } catch (error) { message.error("数据节点加载失败:" + (error?.message || error)); @@ -212,6 +257,15 @@ async function loadDataModules() { } } +async function loadOtherDataPage() { + const response = await loadDataApi( + "s_module", + "b_code = N'base_otherdata' AND b_module_type = 'page' AND b_canuse = 1", + "b_id ASC", + ); + otherDataPageId.value = response.data?.[0]?.b_id || null; +} + function onCategorySelect(keys, info) { if (info?.node?.isFmsRoot) { updateSelectedModule(ALL_CATEGORIES_KEY); @@ -227,7 +281,7 @@ function onCategorySelect(keys, info) { function buildCategoryContextMenu(node) { const items = []; const isCategory = !node?.isFmsRoot && node?.b_node_type === "category"; - if (node?.isFmsRoot || isCategory) { + if ((node?.isFmsRoot || isCategory) && canManageTree("create")) { items.push( { key: "add-category", @@ -248,20 +302,22 @@ function buildCategoryContextMenu(node) { icon: h(RefreshCw, { size: 14 }), }); } else { - items.push( - { + if (canManageTree("update")) { + items.push({ key: "edit", label: node?.b_node_type === "module" ? "修改节点" : "修改分类", icon: h(Pencil, { size: 14 }), - }, - { type: "divider" }, - { + }); + } + if (canManageTree("delete")) { + if (items.length) items.push({ type: "divider" }); + items.push({ key: "delete", label: "删除", icon: h(Trash2, { size: 14 }), danger: true, - }, - ); + }); + } } return { items, @@ -294,7 +350,7 @@ function openAddModal(node, type) { modalParentId.value = node?.isFmsRoot ? null : String(node?.b_id || node?.key || ""); modalParentName.value = node?.isFmsRoot ? "" : node?.title || ""; form.b_name = ""; - form.b_module_code = ""; + form.b_module_id = ""; modalVisible.value = true; } @@ -313,7 +369,7 @@ function openEditModal(node) { modalParentId.value = parentId; modalParentName.value = parentId ? categoryById.value.get(parentId)?.b_name || "" : ""; form.b_name = category.b_name || ""; - form.b_module_code = category.b_module_code || ""; + form.b_module_id = category.b_module_id == null ? "" : String(category.b_module_id); modalVisible.value = true; } @@ -329,29 +385,35 @@ function nextSort(parentId) { return max + 10; } -function buildNewCategory(id, name, moduleCode, nodeType) { +function buildNewCategory(id, name, moduleId, nodeType) { const parentId = modalParentId.value; return { b_id: id, + b_code: `node_${id}`, + b_page_module_id: otherDataPageId.value, b_parent_id: parentId || null, b_name: name, - b_node_type: nodeType, + b_node_type: nodeType === "module" ? "module_ref" : "group", + b_module_id: nodeType === "module" ? moduleId : null, + b_module_type: nodeType === "module" ? "data" : null, + b_profile_id: + nodeType === "module" ? profileByModuleId.value.get(String(moduleId))?.b_id || null : null, + b_i18n: `meta.node.${id}`, b_xh: nextSort(parentId), b_canuse: 1, - b_module_code: moduleCode, }; } -function findDataModule(code) { +function findDataModule(id) { for (const item of dataModules.value) { - if (item.b_code === code) return item; + if (String(item.b_id) === String(id)) return item; } return null; } -function onModuleChange(code) { - form.b_module_code = code || ""; - const module = findDataModule(form.b_module_code); +function onModuleChange(id) { + form.b_module_id = id || ""; + const module = findDataModule(form.b_module_id); if (module) form.b_name = module.b_name || module.b_code || ""; } @@ -362,10 +424,10 @@ async function saveCategory() { return; } - let moduleCode = String(form.b_module_code || "").trim() || null; + let moduleId = String(form.b_module_id || "").trim() || null; let nodeType = "category"; if (isNodeModal.value) { - const module = findDataModule(moduleCode); + const module = findDataModule(moduleId); if (!module) { message.warning("请选择数据节点"); return; @@ -373,21 +435,28 @@ async function saveCategory() { name = module.b_name || module.b_code || name; nodeType = "module"; } else { - moduleCode = null; + moduleId = null; } saving.value = true; try { if (editingCategoryId.value) { const saves = [ { - table: "b_other_bmfl", + table: "s_page_node", key_field: "b_id", updates: [ { b_id: editingCategoryId.value, + b_page_module_id: otherDataPageId.value, + b_parent_id: modalParentId.value || null, b_name: name, - b_node_type: nodeType, - b_module_code: moduleCode, + b_node_type: nodeType === "module" ? "module_ref" : "group", + b_module_id: nodeType === "module" ? moduleId : null, + b_module_type: nodeType === "module" ? "data" : null, + b_profile_id: + nodeType === "module" + ? profileByModuleId.value.get(String(moduleId))?.b_id || null + : null, }, ], }, @@ -399,9 +468,9 @@ async function saveCategory() { const newId = String(idRes.data); const saves = [ { - table: "b_other_bmfl", + table: "s_page_node", key_field: "b_id", - inserts: [buildNewCategory(newId, name, moduleCode, nodeType)], + inserts: [buildNewCategory(newId, name, moduleId, nodeType)], }, ]; await saveObjectApi(saves); @@ -426,9 +495,9 @@ function hasChildCategory(categoryId) { async function hasPersistedChildCategory(categoryId) { const response = await pageDataApi({ - viewName: "b_other_bmfl", + viewName: "s_page_node", orderBy: "b_id ASC", - searchCondition: `[b_parent_id] = ${categoryId}`, + searchCondition: `[b_page_module_id] = ${otherDataPageId.value} AND [b_parent_id] = ${categoryId}`, pageNo: 1, pageSize: 1, }); @@ -465,9 +534,17 @@ function confirmDeleteCategory(node) { message.warning("该分类下还有子分类,不能删除"); return; } + const grants = await Promise.all([ + loadDataApi("b_role_page_node", `b_page_node_id = ${categoryId}`), + loadDataApi("b_user_page_node", `b_page_node_id = ${categoryId}`), + ]); + if (grants.some((response) => (response.data || []).length)) { + message.warning("该节点仍被角色或用户授权引用"); + return; + } await saveObjectApi([ { - table: "b_other_bmfl", + table: "s_page_node", key_field: "b_id", deletes: [{ b_id: categoryId }], }, @@ -563,7 +640,8 @@ onMounted(() => { v-if="shouldLoadData" ref="moduleListRef" :key="tableKey" - :data-code="selectedModuleCode" + :module-id="selectedModuleId" + :profile-id="selectedProfileId" :editable="true" :row-draggable="true" :row-selection="rowSelection" @@ -571,20 +649,21 @@ onMounted(() => { > @@ -695,5 +773,4 @@ onMounted(() => { align-items: center; justify-content: center; } - diff --git a/code/fms/fms-vue/src/views/module-management/components/FieldGroupManager.vue b/code/fms/fms-vue/src/views/module-management/components/FieldGroupManager.vue index a894b232..a656e041 100644 --- a/code/fms/fms-vue/src/views/module-management/components/FieldGroupManager.vue +++ b/code/fms/fms-vue/src/views/module-management/components/FieldGroupManager.vue @@ -17,6 +17,7 @@ import { buildTree } from "@/utils/tree"; const props = defineProps({ visible: { type: Boolean, default: false }, moduleId: { type: String, required: true }, + profileId: { type: String, required: true }, groups: { type: Array, default: () => [] }, referencedGroupIds: { type: Array, default: () => [] }, }); @@ -38,7 +39,7 @@ const treeData = computed(() => buildTree(localGroups.value, { mapNode: (item) => ({ key: String(item.b_id), - title: item.b_title || `分组-${item.b_id}`, + title: item.b_name || `分组-${item.b_id}`, children: [], }), }), @@ -63,8 +64,10 @@ function addGroup(parentId) { const newGroup = { b_id: createTempId(), b_module_id: props.moduleId, + b_profile_id: props.profileId, b_parent_id: parentId || null, - b_title: "", + b_scene: "shared", + b_name: "", b_i18n: null, b_xh: (localGroups.value.length + 1) * 10, b_canuse: 1, @@ -123,7 +126,7 @@ function onMenuClick({ key, node }) { async function saveAllGroups() { let hasEmptyTitle = false; for (const group of localGroups.value) { - if (!String(group.b_title || "").trim()) { + if (!String(group.b_name || "").trim()) { hasEmptyTitle = true; break; } @@ -134,7 +137,7 @@ async function saveAllGroups() { } const original = cloneData(props.groups || []); const changes = diffRows(localGroups.value, original); - const req = tableChange("s_module_field_group", changes); + const req = tableChange("s_module_layout_group", changes); if (!hasChanges(req)) { message.info("没有需要保存的变更"); return; @@ -146,6 +149,7 @@ async function saveAllGroups() { req.deletes || [], ]); remapForeignKeys(localGroups.value, ["b_parent_id"], idMapping); + for (const group of localGroups.value) group.b_i18n = `meta.group.${group.b_id}`; await saveObjectApi([req]); emit("saved", cloneData(localGroups.value), idMapping); message.success("分组已保存"); @@ -184,7 +188,7 @@ function onClose() {