20260727173703
This commit is contained in:
1 parent
a8478d60ed
commit
9dbaef7787
114 files changed
+959
-2588
No files matched your search
@@ -1,16 +1,19 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
||||
import {
|
||||
cloneData,
|
||||
diffRows,
|
||||
allocateTemporaryIds,
|
||||
remapForeignKeys,
|
||||
tableChange,
|
||||
hasChanges,
|
||||
} from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
|
||||
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
|
||||
@@ -23,11 +26,11 @@ import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
||||
// ---- 状态 ----
|
||||
const treeData = ref([]);
|
||||
const treeLoading = ref(false);
|
||||
const expandedTreeKeys = ref([]);
|
||||
const selectedModuleId = ref(null);
|
||||
const skipLoad = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
const contextNode = ref(null);
|
||||
const contextMenuOpen = ref(false);
|
||||
|
||||
const contextMenuItems = computed(() => {
|
||||
const node = contextNode.value;
|
||||
@@ -71,17 +74,47 @@ const drafts = ref({
|
||||
|
||||
const configLoading = ref(false);
|
||||
const configSaving = ref(false);
|
||||
const configSyncing = ref(false);
|
||||
const i18nPanelRef = ref(null);
|
||||
const loadedModuleId = ref(null);
|
||||
let configRequestId = 0;
|
||||
let operationId = 0;
|
||||
|
||||
// ref 写在 v-for 内部会被 Vue 收集为数组,这里统一取单个组件实例
|
||||
function getI18nPanel() {
|
||||
const r = i18nPanelRef.value;
|
||||
return Array.isArray(r) ? r[0] : r;
|
||||
// 使用函数 ref,避免组件位于 TabPane 的 v-for 内时被 Vue 收集成实例数组。
|
||||
function setI18nPanelRef(instance) {
|
||||
i18nPanelRef.value = instance || null;
|
||||
}
|
||||
|
||||
// 当前选中模块对象(从 drafts.module 获取)
|
||||
const selectedModule = computed(() => drafts.value.module);
|
||||
const busy = computed(() => configLoading.value || configSaving.value || configSyncing.value);
|
||||
const syncDisabled = computed(
|
||||
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
|
||||
);
|
||||
const isCurrentModule = computed(
|
||||
() =>
|
||||
loadedModuleId.value != null && String(loadedModuleId.value) === String(selectedModuleId.value),
|
||||
);
|
||||
const i18nKeys = computed(() => {
|
||||
const keys = new Set();
|
||||
const add = (key) => {
|
||||
if (key) keys.add(String(key));
|
||||
};
|
||||
add(drafts.value.module?.b_i18n);
|
||||
for (const section of ["fields", "groups", "listConfig", "powers"]) {
|
||||
for (const row of drafts.value[section] || []) add(row.b_i18n);
|
||||
}
|
||||
return [...keys];
|
||||
});
|
||||
const referencedGroupIds = computed(() => {
|
||||
const ids = new Set();
|
||||
for (const section of ["listConfig", "editConfig"]) {
|
||||
for (const row of drafts.value[section] || []) {
|
||||
if (row.b_group_id != null) ids.add(String(row.b_group_id));
|
||||
}
|
||||
}
|
||||
return [...ids];
|
||||
});
|
||||
|
||||
// 上级模块名称(从模块树解析 b_parent_id 对应的名称,用于基本信息展示)
|
||||
const parentModuleName = computed(() => {
|
||||
@@ -102,20 +135,20 @@ const parentModuleName = computed(() => {
|
||||
const visibleTabs = computed(() => {
|
||||
const type = selectedModule.value?.b_module_type;
|
||||
if (!type) return [];
|
||||
const tabs = [{ key: "basic", tab: "基本信息", icon: "Info" }];
|
||||
const tabs = [{ key: "basic", tab: "基本信息" }];
|
||||
if (type === "data") {
|
||||
tabs.push(
|
||||
{ key: "fields", tab: "字段定义", icon: "Table" },
|
||||
{ key: "list", tab: "列表配置", icon: "List" },
|
||||
{ key: "edit", tab: "编辑配置", icon: "Edit" },
|
||||
{ key: "query", tab: "查询配置", icon: "Search" },
|
||||
{ key: "fields", tab: "字段定义" },
|
||||
{ key: "list", tab: "列表配置" },
|
||||
{ key: "edit", tab: "编辑配置" },
|
||||
{ key: "query", tab: "查询配置" },
|
||||
);
|
||||
}
|
||||
if (type === "page") {
|
||||
tabs.push({ key: "powers", tab: "权限配置", icon: "Lock" });
|
||||
tabs.push({ key: "powers", tab: "权限配置" });
|
||||
}
|
||||
if (type !== "directory") {
|
||||
tabs.push({ key: "i18n", tab: "多语言", icon: "Translation" });
|
||||
tabs.push({ key: "i18n", tab: "多语言" });
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
@@ -142,6 +175,38 @@ function buildTree(modules) {
|
||||
return roots;
|
||||
}
|
||||
|
||||
function sortTree(nodes) {
|
||||
nodes.sort(
|
||||
(a, b) =>
|
||||
Number(a.b_xh || 0) - Number(b.b_xh || 0) || String(a.b_id).localeCompare(String(b.b_id)),
|
||||
);
|
||||
for (const node of nodes) {
|
||||
if (node.children?.length) sortTree(node.children);
|
||||
}
|
||||
}
|
||||
|
||||
// 普通保存后直接更新现有节点,避免重新请求并重建整棵树导致展开状态丢失。
|
||||
function updateSavedTreeNode(moduleData) {
|
||||
let node = findNode(moduleData.b_id);
|
||||
if (!node) {
|
||||
node = { ...cloneData(moduleData), children: [] };
|
||||
const parent = moduleData.b_parent_id ? findNode(moduleData.b_parent_id) : null;
|
||||
if (parent) {
|
||||
if (!parent.children) parent.children = [];
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
treeData.value.push(node);
|
||||
}
|
||||
}
|
||||
const children = node.children;
|
||||
Object.assign(node, cloneData(moduleData), {
|
||||
key: String(moduleData.b_id),
|
||||
title: moduleData.b_name || moduleData.b_code,
|
||||
});
|
||||
if (children) node.children = children;
|
||||
sortTree(treeData.value);
|
||||
}
|
||||
|
||||
async function loadTree() {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
@@ -157,32 +222,56 @@ async function loadModuleConfig(moduleId, requestId) {
|
||||
configLoading.value = true;
|
||||
try {
|
||||
const id = String(moduleId);
|
||||
const [moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, powersRes] =
|
||||
await Promise.all([
|
||||
loadDataApi("s_module", `b_id = ${id}`),
|
||||
loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
||||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
const moduleType = findNode(id)?.b_module_type;
|
||||
const requests = [["module", loadDataApi("s_module", `b_id = ${id}`)]];
|
||||
if (moduleType === "data") {
|
||||
requests.push(
|
||||
["fields", loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC")],
|
||||
[
|
||||
"groups",
|
||||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"listConfig",
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"editConfig",
|
||||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"queryConfig",
|
||||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
);
|
||||
} else if (moduleType === "page") {
|
||||
requests.push([
|
||||
"powers",
|
||||
loadDataApi("s_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
}
|
||||
const responses = Object.fromEntries(
|
||||
await Promise.all(requests.map(async ([key, request]) => [key, await request])),
|
||||
);
|
||||
|
||||
const original = {
|
||||
module: moduleRes.data?.[0] || null,
|
||||
fields: fieldsRes.data || [],
|
||||
groups: groupsRes.data || [],
|
||||
listConfig: listRes.data || [],
|
||||
editConfig: editRes.data || [],
|
||||
queryConfig: queryRes.data || [],
|
||||
powers: powersRes.data || [],
|
||||
module: responses.module.data?.[0] || null,
|
||||
fields: responses.fields?.data || [],
|
||||
groups: responses.groups?.data || [],
|
||||
listConfig: responses.listConfig?.data || [],
|
||||
editConfig: responses.editConfig?.data || [],
|
||||
queryConfig: responses.queryConfig?.data || [],
|
||||
powers: responses.powers?.data || [],
|
||||
};
|
||||
if (requestId !== configRequestId) return;
|
||||
originals.value = original;
|
||||
drafts.value = cloneData(original);
|
||||
loadedModuleId.value = id;
|
||||
activeTab.value = "basic";
|
||||
} catch (e) {
|
||||
if (requestId === configRequestId) {
|
||||
loadedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
message.error("模块配置加载失败:" + (e.message || e));
|
||||
}
|
||||
} finally {
|
||||
@@ -192,6 +281,7 @@ async function loadModuleConfig(moduleId, requestId) {
|
||||
|
||||
// ---- 树节点选择 ----
|
||||
function onTreeSelect(keys) {
|
||||
if (busy.value) return;
|
||||
if (keys.length > 0) {
|
||||
selectedModuleId.value = String(keys[0]);
|
||||
}
|
||||
@@ -199,8 +289,11 @@ function onTreeSelect(keys) {
|
||||
|
||||
watch(selectedModuleId, (id) => {
|
||||
const requestId = ++configRequestId;
|
||||
operationId++;
|
||||
if (!id) {
|
||||
configLoading.value = false;
|
||||
loadedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
return;
|
||||
}
|
||||
if (skipLoad.value) {
|
||||
@@ -208,6 +301,7 @@ watch(selectedModuleId, (id) => {
|
||||
configLoading.value = false;
|
||||
return;
|
||||
}
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(id, requestId);
|
||||
});
|
||||
|
||||
@@ -223,11 +317,19 @@ function onSetField(field, value) {
|
||||
}
|
||||
|
||||
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
|
||||
function onGroupsSaved(newGroups) {
|
||||
function onGroupsSaved(newGroups, idMapping) {
|
||||
if (idMapping?.size) {
|
||||
remapForeignKeys(drafts.value.listConfig || [], ["b_group_id"], idMapping);
|
||||
remapForeignKeys(drafts.value.editConfig || [], ["b_group_id"], idMapping);
|
||||
}
|
||||
originals.value.groups = cloneData(newGroups);
|
||||
drafts.value.groups = cloneData(newGroups);
|
||||
}
|
||||
|
||||
function onI18nSaved(key) {
|
||||
void i18nPanelRef.value?.refreshKey?.(key);
|
||||
}
|
||||
|
||||
// ---- 新增模块 ----
|
||||
async function onAddModule(parentId, moduleType = "directory") {
|
||||
const res = await nextIdApi();
|
||||
@@ -262,10 +364,12 @@ async function onAddModule(parentId, moduleType = "directory") {
|
||||
drafts.value = cloneData(originals.value);
|
||||
skipLoad.value = true;
|
||||
selectedModuleId.value = newId;
|
||||
loadedModuleId.value = newId;
|
||||
activeTab.value = "basic";
|
||||
|
||||
// 刷新树
|
||||
await loadTree();
|
||||
updateSavedTreeNode(newModule);
|
||||
if (parentId && !expandedTreeKeys.value.includes(String(parentId))) {
|
||||
expandedTreeKeys.value = [...expandedTreeKeys.value, String(parentId)];
|
||||
}
|
||||
}
|
||||
|
||||
const addModuleMenu = {
|
||||
@@ -279,7 +383,7 @@ function onTreeRightClick({ node }) {
|
||||
}
|
||||
|
||||
function onContextMenuClick({ key }) {
|
||||
contextMenuOpen.value = false;
|
||||
if (busy.value) return;
|
||||
const node = contextNode.value;
|
||||
contextNode.value = null;
|
||||
if (!node) return;
|
||||
@@ -345,7 +449,9 @@ async function onDeleteNodeById(moduleId) {
|
||||
|
||||
// ---- 统一保存 ----
|
||||
async function saveAll() {
|
||||
if (!selectedModule.value) return;
|
||||
if (!selectedModule.value || !isCurrentModule.value || busy.value) return;
|
||||
const moduleId = String(selectedModule.value.b_id);
|
||||
const currentOperationId = ++operationId;
|
||||
configSaving.value = true;
|
||||
|
||||
try {
|
||||
@@ -354,8 +460,26 @@ async function saveAll() {
|
||||
// s_module 主表
|
||||
const moduleData = drafts.value.module;
|
||||
const moduleOriginal = originals.value.module;
|
||||
if (!String(moduleData?.b_code || "").trim()) {
|
||||
message.warning(`请填写${moduleData?.b_module_type === "data" ? "模块编码" : "菜单编码"}`);
|
||||
return;
|
||||
}
|
||||
if (!String(moduleData?.b_name || "").trim()) {
|
||||
message.warning("请填写模块名称");
|
||||
return;
|
||||
}
|
||||
if (moduleData.b_module_type === "page" && !String(moduleData.b_route || "").trim()) {
|
||||
message.warning("请填写页面路由");
|
||||
return;
|
||||
}
|
||||
if (moduleData.b_module_type === "data" && !String(moduleData.b_viewtable || "").trim()) {
|
||||
message.warning("请填写查询视图");
|
||||
return;
|
||||
}
|
||||
syncModuleItemI18nKeys(drafts.value.fields, "field", moduleData.b_code, "b_field");
|
||||
syncModuleItemI18nKeys(drafts.value.powers, "power", moduleData.b_code, "b_code");
|
||||
const isNew = !moduleOriginal?.b_code;
|
||||
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
||||
const isNew = !moduleOriginal?.b_code;
|
||||
saveReqs.push({
|
||||
table: "s_module",
|
||||
key_field: "b_id",
|
||||
@@ -383,8 +507,10 @@ async function saveAll() {
|
||||
}
|
||||
|
||||
// 多语言:由各子面板收集变更,统一在此保存
|
||||
const i18nPanel = getI18nPanel();
|
||||
if (i18nPanel) {
|
||||
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
|
||||
return;
|
||||
const i18nPanel = i18nPanelRef.value;
|
||||
if (typeof i18nPanel?.buildSaveRequest === "function") {
|
||||
const i18nReq = await i18nPanel.buildSaveRequest();
|
||||
if (i18nReq) saveReqs.push(i18nReq);
|
||||
}
|
||||
@@ -398,9 +524,9 @@ async function saveAll() {
|
||||
|
||||
// 更新 originals
|
||||
originals.value = cloneData(drafts.value);
|
||||
getI18nPanel()?.markSaved();
|
||||
i18nPanelRef.value?.markSaved?.();
|
||||
updateSavedTreeNode(moduleData);
|
||||
message.success("保存成功");
|
||||
await loadTree();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
} finally {
|
||||
@@ -529,19 +655,20 @@ function buildMissingDefaultConfigs(module, fields) {
|
||||
return generated;
|
||||
}
|
||||
|
||||
async function syncFields(tableName) {
|
||||
async function syncFields() {
|
||||
const module = selectedModule.value;
|
||||
const view = tableName || module?.b_viewtable;
|
||||
if (!view) {
|
||||
message.warning("请先配置查询视图或输入表名");
|
||||
return;
|
||||
}
|
||||
const view = String(module?.b_viewtable || "").trim();
|
||||
if (!view || busy.value) return;
|
||||
const moduleId = String(module.b_id);
|
||||
const currentOperationId = ++operationId;
|
||||
|
||||
try {
|
||||
configSaving.value = true;
|
||||
configSyncing.value = true;
|
||||
const res = await describeApi(view);
|
||||
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
|
||||
return;
|
||||
const columns = res.data || [];
|
||||
const existingFields = originals.value.fields || [];
|
||||
const existingFields = drafts.value.fields || [];
|
||||
|
||||
// 构建视图列名集合
|
||||
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
|
||||
@@ -580,9 +707,9 @@ async function syncFields(tableName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 字段多语言键只补空值,保留已有人工配置。
|
||||
// 多语言键始终按 field.<模块代码>.<字段编码> 自动生成。
|
||||
mergedFields.forEach((field) => {
|
||||
if (!field.b_i18n) field.b_i18n = `field.${field.b_field}`;
|
||||
field.b_i18n = buildModuleItemI18nKey("field", module.b_code, field.b_field);
|
||||
});
|
||||
|
||||
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
|
||||
@@ -594,6 +721,29 @@ async function syncFields(tableName) {
|
||||
fieldReq.deletes || [],
|
||||
]);
|
||||
|
||||
// 为"有 b_i18n 键"的字段生成多语言默认配置(b_i18n 草稿行),注入 i18n 面板,
|
||||
// 由顶部"保存"统一落库;已存在的键由面板去重,不覆盖用户配置。
|
||||
const candidateI18n = mergedFields
|
||||
.filter((f) => f.b_canuse === 1 && f.b_i18n)
|
||||
.map((f) => ({ key: f.b_i18n, value: f.b_name || f.b_field }));
|
||||
let primaryLocale = "zh-CN";
|
||||
try {
|
||||
const langRes = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
const langs = langRes.data || [];
|
||||
if (langs.length) {
|
||||
// 默认字段名翻译写到"简体中文"(zh-CN),找不到再退回排序第一的语言
|
||||
const zh = langs.find((l) => l.b_code === "zh-CN");
|
||||
primaryLocale = (zh || langs[0]).b_code;
|
||||
}
|
||||
} catch (e) {
|
||||
// 取不到语言类型时回退默认值
|
||||
}
|
||||
const i18nEntries = candidateI18n.map((e) => ({
|
||||
key: e.key,
|
||||
locale: primaryLocale,
|
||||
value: e.value,
|
||||
}));
|
||||
|
||||
const generated = buildMissingDefaultConfigs(module, mergedFields);
|
||||
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
|
||||
const generatedCount = generatedSections.reduce(
|
||||
@@ -601,21 +751,15 @@ async function syncFields(tableName) {
|
||||
0,
|
||||
);
|
||||
|
||||
if (!hasChanges(fieldReq) && !generatedCount) {
|
||||
message.info("字段和默认配置已是最新,无需同步");
|
||||
if (!hasChanges(fieldReq) && !generatedCount && !i18nEntries.length) {
|
||||
message.success("同步成功");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasChanges(fieldReq)) {
|
||||
// 字段先保存以取得真实 ID,自动生成的配置留在草稿中由顶部“保存”统一提交。
|
||||
await saveObjectApi([fieldReq]);
|
||||
const fieldsRes = await loadDataApi(
|
||||
"s_module_field",
|
||||
`b_module_id = ${module.b_id}`,
|
||||
"b_id ASC",
|
||||
);
|
||||
originals.value.fields = fieldsRes.data || [];
|
||||
drafts.value.fields = cloneData(originals.value.fields);
|
||||
// 多语言默认配置注入 i18n 面板草稿;字段已预分配真实 ID(见上方 allocateTemporaryIds),
|
||||
// 列表/编辑/查询配置稍后追加到草稿。全部由顶部"保存"统一提交,期间不写库。
|
||||
if (i18nEntries.length) {
|
||||
i18nPanelRef.value?.mergeDefaults?.(i18nEntries);
|
||||
}
|
||||
|
||||
for (const section of generatedSections) {
|
||||
@@ -623,16 +767,13 @@ async function syncFields(tableName) {
|
||||
drafts.value[section] = [...(drafts.value[section] || []), ...cloneData(generated[section])];
|
||||
}
|
||||
|
||||
const disabledCount = existingFields.filter(
|
||||
(field) => field.b_canuse === 1 && !viewColumnSet.has(field.b_field?.toLowerCase()),
|
||||
).length;
|
||||
message.success(
|
||||
`字段同步完成:${columns.length} 列,新增 ${fieldReq.inserts?.length || 0},停用 ${disabledCount};自动生成列表 ${generated.listConfig.length}、编辑 ${generated.editConfig.length}、查询 ${generated.queryConfig.length} 项配置${generatedCount ? ",请点击保存" : ""}`,
|
||||
);
|
||||
drafts.value.fields = cloneData(mergedFields);
|
||||
|
||||
message.success("同步成功");
|
||||
} catch (e) {
|
||||
message.error("字段同步失败:" + (e.message || e));
|
||||
} finally {
|
||||
configSaving.value = false;
|
||||
configSyncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,20 +787,23 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
<aside class="mm-sidebar">
|
||||
<div class="mm-sidebar-header">
|
||||
<span class="mm-sidebar-title">模块列表</span>
|
||||
<a-dropdown :menu="addModuleMenu" :trigger="['click']">
|
||||
<a-button><Plus :size="14" />新增</a-button>
|
||||
<a-dropdown :menu="addModuleMenu" :trigger="['click']" :disabled="busy">
|
||||
<a-button :disabled="busy"><Plus :size="14" />新增</a-button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div v-if="treeData.length === 0" class="mm-tree-empty">
|
||||
<div v-if="treeLoading" class="mm-tree-empty">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else-if="treeData.length === 0" class="mm-tree-empty">
|
||||
<a-empty description="暂无模块" />
|
||||
</div>
|
||||
<a-dropdown v-else :trigger="['contextmenu']" :menu="contextMenuItems">
|
||||
<div
|
||||
style="flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column"
|
||||
>
|
||||
<div class="mm-tree-container">
|
||||
<FmsTree
|
||||
class="mm-tree"
|
||||
:disabled="configSaving"
|
||||
:tree-data="treeData"
|
||||
v-model:expanded-keys="expandedTreeKeys"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
@select="onTreeSelect"
|
||||
@@ -677,66 +821,84 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
|
||||
<!-- 右侧详情 -->
|
||||
<main class="mm-main">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button type="primary" :loading="configSaving" @click="saveAll">保存</a-button>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<span>启用</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="configSaving"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="configSaving || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
<FmsLoadingOverlay :loading="configLoading">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading || configSyncing"
|
||||
@click="saveAll"
|
||||
>保存</a-button
|
||||
>
|
||||
<a-button
|
||||
v-if="selectedModule.b_module_type === 'data'"
|
||||
:loading="configSyncing"
|
||||
:disabled="syncDisabled"
|
||||
@click="syncFields"
|
||||
>
|
||||
同步字段
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<span>启用</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="busy"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-switch
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="busy || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<span class="mm-header-title">{{
|
||||
selectedModule.b_name || selectedModule.b_code
|
||||
}}</span>
|
||||
<span class="mm-header-code">{{ selectedModule.b_code || "(未设置编码)" }}</span>
|
||||
<span class="mm-header-type">
|
||||
<a-tag>{{
|
||||
selectedModule.b_module_type === "directory"
|
||||
? "目录"
|
||||
: selectedModule.b_module_type === "page"
|
||||
? "页面"
|
||||
: "数据"
|
||||
}}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
<span class="mm-header-title">{{ selectedModule.b_name || selectedModule.b_code }}</span>
|
||||
<span class="mm-header-code">{{ selectedModule.b_code || "(未设置编码)" }}</span>
|
||||
<span class="mm-header-type">
|
||||
<a-tag>{{
|
||||
selectedModule.b_module_type === "directory"
|
||||
? "目录"
|
||||
: selectedModule.b_module_type === "page"
|
||||
? "页面"
|
||||
: "数据"
|
||||
}}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tab 面板 -->
|
||||
<div class="mm-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
||||
<a-tab-pane v-for="tab in visibleTabs" :key="tab.key" :tab="tab.tab">
|
||||
<template v-if="configLoading">
|
||||
<div class="mm-loading"><a-spin /></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Tab 面板 -->
|
||||
<div class="mm-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
||||
<a-tab-pane
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:tab="tab.tab"
|
||||
:force-render="tab.key === 'i18n'"
|
||||
>
|
||||
<ModuleBasicPanel
|
||||
v-if="tab.key === 'basic'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:data="drafts.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('module', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleFieldsPanel
|
||||
v-else-if="tab.key === 'fields'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:fields="drafts.fields"
|
||||
:view-table="selectedModule.b_viewtable"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('fields', v)"
|
||||
@sync="syncFields"
|
||||
/>
|
||||
<ModuleListConfigPanel
|
||||
v-else-if="tab.key === 'list'"
|
||||
@@ -744,9 +906,10 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:list-config="drafts.listConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:loading="configSaving"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('listConfig', v)"
|
||||
@update-groups="onGroupsSaved"
|
||||
@groups-saved="onGroupsSaved"
|
||||
/>
|
||||
<ModuleEditConfigPanel
|
||||
v-else-if="tab.key === 'edit'"
|
||||
@@ -754,16 +917,17 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:edit-config="drafts.editConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:loading="configSaving"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('editConfig', v)"
|
||||
@update-groups="onGroupsSaved"
|
||||
@groups-saved="onGroupsSaved"
|
||||
/>
|
||||
<ModuleQueryConfigPanel
|
||||
v-else-if="tab.key === 'query'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModulePowersPanel
|
||||
@@ -771,25 +935,27 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:powers="drafts.powers"
|
||||
:loading="configSaving"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('powers', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleI18nPanel
|
||||
v-else-if="tab.key === 'i18n'"
|
||||
ref="i18nPanelRef"
|
||||
:ref="setI18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:loading="configSaving"
|
||||
:i18n-keys="i18nKeys"
|
||||
:loading="busy"
|
||||
/>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 未选中模块 -->
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
<!-- 未选中模块 -->
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
</FmsLoadingOverlay>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -835,6 +1001,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.mm-tree-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mm-tree-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
Reference in new issue
Block a user