20260728173026

This commit is contained in:
oneao committed 2026-07-28 17:30:27 +08:00
1 parent 63588bd217
commit 5ec7aadef9
25 files changed
+627 -462

No files matched your search

@@ -13,6 +13,7 @@ import {
hasChanges,
} from "@/utils/dataChanges";
import { createTempId } from "@/utils/tempId";
import { buildTree, sortTree } from "@/utils/tree";
import {
buildModuleI18nKey,
buildModuleItemI18nKey,
@@ -35,10 +36,7 @@ const expandedTreeKeys = ref([]);
const selectedModuleId = ref(null);
const skipLoad = ref(false);
const activeTab = ref("basic");
const contextNode = ref(null);
const contextMenuItems = computed(() => {
const node = contextNode.value;
function buildTreeContextMenu(node) {
const items = [];
if (!node) return { items };
if (node.b_module_type === "directory") {
@@ -52,8 +50,8 @@ const contextMenuItems = computed(() => {
}
if (items.length) items.push({ type: "divider" });
items.push({ label: "删除", key: "delete", danger: true });
return { items, onClick: onContextMenuClick };
});
return { items };
}
// 原始数据(加载时保存,用于 diff)
const originals = ref({
@@ -203,35 +201,15 @@ const visibleTabs = computed(() => {
});
// ---- 模块树 ----
function buildTree(modules) {
const map = new Map();
const roots = [];
function buildModuleTree(modules) {
modules.forEach((m) => {
m.key = String(m.b_id);
m.title = m.b_name || m.b_code;
m.b_parent_id = m.b_parent_id ? String(m.b_parent_id) : null;
map.set(m.key, m);
m.b_parent_id = m.b_parent_id != null ? String(m.b_parent_id) : null;
});
modules.forEach((m) => {
if (m.b_parent_id && map.has(m.b_parent_id)) {
const parent = map.get(m.b_parent_id);
if (!parent.children) parent.children = [];
parent.children.push(m);
} else {
roots.push(m);
}
});
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);
}
const tree = buildTree(modules);
sortTree(tree);
return tree;
}
// 普通保存后直接更新现有节点,避免重新请求并重建整棵树导致展开状态丢失。
@@ -260,7 +238,7 @@ async function loadTree() {
treeLoading.value = true;
try {
const res = await loadDataApi("s_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
treeData.value = buildTree(res.data || []);
treeData.value = buildModuleTree(res.data || []);
} finally {
treeLoading.value = false;
}
@@ -427,16 +405,8 @@ const addModuleMenu = {
};
// ---- 右键菜单 ----
function onTreeRightClick({ node }) {
contextNode.value = node;
}
function onContextMenuClick({ key }) {
function onTreeMenuClick({ key, node }) {
if (busy.value) return;
const node = contextNode.value;
contextNode.value = null;
if (!node) return;
if (key.startsWith("add-")) {
onAddModule(node.key, key.replace("add-", ""));
} else if (key === "delete") {
@@ -500,6 +470,7 @@ async function onDeleteNodeById(moduleId) {
deleteByModuleId("s_module_power"),
deleteByModuleId("s_module_field"),
deleteByModuleId("s_module_field_group"),
deleteByModuleId("b_i18n"),
{ table: "s_module", key_field: "b_id", deletes: [{ b_id: id }] },
]);
message.success("模块已删除");
@@ -619,59 +590,6 @@ async function saveAll() {
}
// ---- 字段同步与默认配置 ----
function typeFromSqlType(sqlType) {
if (!sqlType) return "input";
const t = sqlType.toLowerCase();
if (
t.includes("char") ||
t.includes("text") ||
t.includes("nchar") ||
t.includes("ntext") ||
t.includes("xml")
) {
if (t.includes("text") || t.includes("ntext") || t.includes("xml")) return "textarea";
return "input";
}
if (t.includes("int") || t.includes("float") || t.includes("double") || t.includes("real"))
return "number";
if (t.includes("decimal") || t.includes("numeric") || t.includes("money")) {
if (t.includes("money")) return "money";
return "number";
}
if (t.includes("date") || t.includes("time")) {
if (t.includes("date") && (t.includes("time") || t.includes("smalldatetime")))
return "datetime";
return "date";
}
if (t.includes("bit")) return "switch";
return "input";
}
function guessFieldWidth(fieldName) {
const n = (fieldName || "").toLowerCase();
if (n === "b_id" || n.endsWith("_id")) return 100;
if (n.includes("time") || n.includes("date") || n.includes("_at")) return 160;
if (n.includes("name")) return 150;
if (n.includes("code") || n.includes("type")) return 120;
if (n.includes("ip")) return 140;
if (n.includes("status") || n.includes("state") || n.includes("result")) return 90;
return 120;
}
function guessQueryComponent(b_type) {
const t = (b_type || "").toLowerCase();
if (t === "date" || t === "datetime") return "date-picker";
if (t === "select" || t === "switch") return "select";
if (t === "number" || t === "money") return "number-range";
return "input";
}
function guessQueryOperator(b_type) {
const t = (b_type || "").toLowerCase();
if (t === "datetime" || t === "number" || t === "money") return "between";
return "=";
}
function configuredFieldIds(section) {
return new Set(
[...(originals.value[section] || []), ...(drafts.value[section] || [])]
@@ -704,7 +622,7 @@ function buildMissingDefaultConfigs(module, fields) {
b_i18n: field.b_i18n || null,
b_visible: 1,
b_xh: order,
b_width: guessFieldWidth(field.b_field),
b_width: 100,
b_sortable: 0,
});
}
@@ -727,8 +645,8 @@ function buildMissingDefaultConfigs(module, fields) {
b_id: createTempId(),
b_module_id: moduleId,
b_field_id: fieldId,
b_component: guessQueryComponent(field.b_type),
b_operator: guessQueryOperator(field.b_type),
b_component: "input",
b_operator: "=",
b_default_value: null,
b_xh: order,
b_canuse: 1,
@@ -776,7 +694,7 @@ async function syncFields() {
b_field: col.name,
b_name: col.name,
b_i18n: null,
b_type: typeFromSqlType(col.type),
b_type: "input",
b_default_value: null,
b_options: null,
b_canuse: 1,
@@ -809,7 +727,11 @@ async function syncFields() {
// 由顶部"保存"统一落库;已存在的键由面板去重,不覆盖用户配置。
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 }));
.map((f) => ({
key: f.b_i18n,
value: f.b_name || f.b_field,
managedId: `field:${f.b_id}`,
}));
let primaryLocale = "zh-CN";
try {
const langRes = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
@@ -826,6 +748,7 @@ async function syncFields() {
key: e.key,
locale: primaryLocale,
value: e.value,
managedId: e.managedId,
}));
const generated = buildMissingDefaultConfigs(module, mergedFields);
@@ -881,26 +804,24 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
<div v-else-if="treeData.length === 0" class="mm-tree-empty">
<a-empty description="暂无模块" />
</div>
<a-dropdown v-else :trigger="['contextmenu']" :menu="contextMenuItems">
<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"
@right-click="onTreeRightClick"
>
<template #title="node">
<span class="mm-tree-node">
<span>{{ node.title }}</span>
</span>
</template>
</FmsTree>
</div>
</a-dropdown>
<FmsTree
v-else
class="mm-tree"
:disabled="configSaving"
:tree-data="treeData"
v-model:expanded-keys="expandedTreeKeys"
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
:context-menu="buildTreeContextMenu"
:context-menu-disabled="busy"
@select="onTreeSelect"
@menu-click="onTreeMenuClick"
>
<template #title="node">
<span class="mm-tree-node">
<span>{{ node.title }}</span>
</span>
</template>
</FmsTree>
</aside>
<!-- 右侧详情 -->
@@ -1072,14 +993,6 @@ 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;