20260729231854

This commit is contained in:
oneao committed 2026-07-29 23:18:54 +08:00
1 parent 1c839b707a
commit 84673b3d3d
39 files changed
+4683 -281

No files matched your search

@@ -21,7 +21,6 @@ import {
syncModuleItemI18nKeys,
validateModuleCode,
} from "@/utils/i18n";
import { Plus } from "@lucide/vue";
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
import ModuleListConfigPanel from "./components/ModuleListConfigPanel.vue";
@@ -30,7 +29,7 @@ import ModuleQueryConfigPanel from "./components/ModuleQueryConfigPanel.vue";
import ModuleAutoCodePanel from "./components/ModuleAutoCodePanel.vue";
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
import { createAutoCodeDraft } from "./autoCode";
import { createAutoCodeDraft } from "./common";
// ---- 状态 ----
const appStore = useAppStore();
@@ -43,6 +42,10 @@ const activeTab = ref("basic");
function buildTreeContextMenu(node) {
const items = [];
if (!node) return { items };
if (node.isFmsRoot) {
items.push({ label: "添加目录", key: "add-directory" });
return { items };
}
if (node.b_module_type === "directory") {
items.push(
{ label: "添加目录", key: "add-directory" },
@@ -241,11 +244,131 @@ async function loadTree() {
try {
const res = await loadDataApi("s_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
treeData.value = buildModuleTree(res.data || []);
unsavedModuleIds.clear();
} finally {
treeLoading.value = false;
}
}
// ---- 拖拽 ----
const dragNodeKey = ref(null);
const unsavedModuleIds = new Set();
function onTreeDragStart(info) {
dragNodeKey.value = info.node.key;
}
function isDescendantOf(ancestorKey, descendantKey) {
const ancestorNode = findNode(ancestorKey);
if (!ancestorNode) return false;
const walk = (nodes) => {
for (const n of nodes || []) {
if (String(n.key || n.b_id) === String(descendantKey)) return true;
if (walk(n.children)) return true;
}
return false;
};
return walk(ancestorNode.children);
}
function allowDrop({ dragNode, dropNode }) {
let dragKey = dragNodeKey.value;
if (dragNode && dragNode.key) {
dragKey = dragNode.key;
}
if (!dragKey || !dropNode || dragNode?.isFmsRoot) return false;
if (String(dropNode.key) === String(dragKey)) return false;
if (isDescendantOf(dragKey, dropNode.key)) return false;
return true;
}
async function onTreeDrop(info) {
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split("-");
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const data = cloneData(treeData.value);
// 从树中移除拖拽节点
let dragObj = null;
function removeFromTree(nodes, key) {
for (let i = 0; i < nodes.length; i++) {
if (String(nodes[i].key) === String(key)) {
dragObj = nodes.splice(i, 1)[0];
return true;
}
if (nodes[i].children && removeFromTree(nodes[i].children, key)) return true;
}
return false;
}
removeFromTree(data, dragKey);
if (!dragObj) {
dragNodeKey.value = null;
return;
}
// 插入到新位置
function insertIntoTree(nodes, dropNodeKey, parentNode) {
for (let i = 0; i < nodes.length; i++) {
if (String(nodes[i].key) === String(dropNodeKey)) {
if (!info.dropToGap) {
// 拖放到节点上 → 成为其子节点
nodes[i].children = nodes[i].children || [];
nodes[i].children.unshift(dragObj);
dragObj.b_parent_id = nodes[i].b_id;
} else if (dropPosition === -1) {
// 拖放到节点上方 → 同级插入
nodes.splice(i, 0, dragObj);
dragObj.b_parent_id = parentNode ? parentNode.b_id : null;
} else {
// 拖放到节点下方 → 同级插入
nodes.splice(i + 1, 0, dragObj);
dragObj.b_parent_id = parentNode ? parentNode.b_id : null;
}
return true;
}
if (nodes[i].children && insertIntoTree(nodes[i].children, dropNodeKey, nodes[i])) return true;
}
return false;
}
if (info.node.isFmsRoot) {
dragObj.b_parent_id = null;
data.unshift(dragObj);
} else {
insertIntoTree(data, dropKey, null);
}
treeData.value = data;
const isUnsaved = unsavedModuleIds.has(String(dragObj.b_id));
// 未保存的模块只更新本地数据,等用户点击"保存"时一并落库
if (isUnsaved) {
if (drafts.value.module && String(drafts.value.module.b_id) === String(dragObj.b_id)) {
drafts.value.module.b_parent_id = dragObj.b_parent_id || null;
}
dragNodeKey.value = null;
return;
}
// 已存在的模块直接持久化到后端
try {
await saveObjectApi([
{
table: "s_module",
key_field: "b_id",
updates: [{ b_id: String(dragObj.b_id), b_parent_id: dragObj.b_parent_id || null }],
},
]);
} catch (e) {
message.error("移动失败:" + (e.message || e));
await loadTree();
} finally {
dragNodeKey.value = null;
}
}
// ---- 模块配置加载 ----
async function loadModuleConfig(moduleId) {
configLoading.value = true;
@@ -321,6 +444,10 @@ async function loadModuleConfig(moduleId) {
// ---- 树节点选择 ----
function onTreeSelect(keys, info) {
if (busy.value) return;
if (info?.node?.isFmsRoot) {
selectedModuleId.value = null;
return;
}
const nodeKey = info?.node?.key ?? keys[0];
if (nodeKey == null) return;
@@ -537,22 +664,19 @@ async function onAddModule(parentId, moduleType = "directory") {
selectedModuleId.value = newId;
loadedModuleId.value = newId;
activeTab.value = "basic";
unsavedModuleIds.add(newId);
updateSavedTreeNode(newModule);
if (parentId && !expandedTreeKeys.value.includes(String(parentId))) {
expandedTreeKeys.value = [...expandedTreeKeys.value, String(parentId)];
}
}
const addModuleMenu = {
items: [{ key: "directory", label: "目录" }],
onClick: ({ key }) => onAddModule(null, key),
};
// ---- 右键菜单 ----
function onTreeMenuClick({ key, node }) {
if (busy.value) return;
if (key.startsWith("add-")) {
onAddModule(node.key, key.replace("add-", ""));
const parentId = node.isFmsRoot ? null : node.key;
onAddModule(parentId, key.replace("add-", ""));
} else if (key === "delete") {
if (hasChildNode(node.key)) {
Modal.warning({
@@ -749,6 +873,7 @@ async function saveAll() {
if (i18nPanelRef.value) {
i18nPanelRef.value.markSaved();
}
unsavedModuleIds.delete(moduleId);
updateSavedTreeNode(moduleData);
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
message.success("保存成功");
@@ -984,43 +1109,43 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
<template>
<div class="mm-page">
<!-- 左侧模块树 -->
<aside class="mm-sidebar">
<div class="mm-sidebar-header">
<span class="mm-sidebar-title">模块列表</span>
<a-dropdown :menu="addModuleMenu" :trigger="['click']" :disabled="busy">
<a-button :disabled="busy"><Plus :size="14" />新增</a-button>
</a-dropdown>
</div>
<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>
<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>
<a-splitter class="mm-splitter" lazy>
<a-splitter-panel :default-size="260" :min="210" :max="440" collapsible>
<!-- 左侧模块树 -->
<aside class="mm-sidebar">
<div class="mm-tree-stage">
<div v-if="treeLoading" class="mm-tree-empty">
<a-spin />
</div>
<FmsTree
v-else
:disabled="configSaving"
:tree-data="treeData"
v-model:expanded-keys="expandedTreeKeys"
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
:context-menu="buildTreeContextMenu"
:context-menu-disabled="busy"
draggable
:allow-drop="allowDrop"
@select="onTreeSelect"
@menu-click="onTreeMenuClick"
@drop="onTreeDrop"
@drag-start="onTreeDragStart"
>
<template #title="node">
<span class="mm-tree-node">
<span>{{ node.title }}</span>
</span>
</template>
</FmsTree>
</div>
</aside>
</a-splitter-panel>
<!-- 右侧详情 -->
<main class="mm-main">
<FmsLoadingOverlay :loading="configLoading">
<a-splitter-panel :min="720">
<!-- 右侧详情 -->
<main class="mm-main">
<FmsLoadingOverlay :loading="configLoading">
<template v-if="selectedModule">
<!-- 页面头部 -->
<div class="mm-header">
@@ -1100,23 +1225,31 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
v-else-if="tab.key === 'list'"
:module-id="String(selectedModule.b_id)"
:list-config="drafts.listConfig"
:edit-config="drafts.editConfig"
:query-config="drafts.queryConfig"
:fields="drafts.fields"
:groups="drafts.groups"
:referenced-group-ids="referencedGroupIds"
:loading="busy"
@update="(v) => onUpdate('listConfig', v)"
@groups-saved="onGroupsSaved"
@sync-edit="(v) => onUpdate('editConfig', v)"
@sync-query="(v) => onUpdate('queryConfig', v)"
/>
<ModuleEditConfigPanel
v-else-if="tab.key === 'edit'"
:module-id="String(selectedModule.b_id)"
:edit-config="drafts.editConfig"
:list-config="drafts.listConfig"
:query-config="drafts.queryConfig"
:fields="drafts.fields"
:groups="drafts.groups"
:referenced-group-ids="referencedGroupIds"
:loading="busy"
@update="(v) => onUpdate('editConfig', v)"
@groups-saved="onGroupsSaved"
@sync-list="(v) => onUpdate('listConfig', v)"
@sync-query="(v) => onUpdate('queryConfig', v)"
/>
<ModuleQueryConfigPanel
v-else-if="tab.key === 'query'"
@@ -1159,50 +1292,43 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
<div v-else class="mm-empty">
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
</div>
</FmsLoadingOverlay>
</main>
</FmsLoadingOverlay>
</main>
</a-splitter-panel>
</a-splitter>
</div>
</template>
<style scoped lang="scss">
.mm-page {
display: flex;
.mm-page,
.mm-splitter {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.mm-page {
background: var(--fms-bg);
}
.mm-sidebar {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
flex-direction: column;
flex: none;
width: 260px;
border-right: 1px solid var(--fms-border);
background: var(--fms-surface);
}
.mm-sidebar-header {
.mm-tree-stage {
display: flex;
align-items: center;
justify-content: space-between;
flex: none;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid var(--fms-border);
}
.mm-sidebar-title {
font-size: 14px;
font-weight: 600;
color: var(--fms-text);
}
.mm-tree {
flex: 1;
min-height: 0;
overflow: auto;
padding: 8px 10px;
flex-direction: column;
padding: 12px;
}
.mm-tree-empty {
@@ -1224,10 +1350,11 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
.mm-main {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
flex-direction: column;
background: var(--fms-surface);
}