20260729231854
This commit is contained in:
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user