20260723225202

This commit is contained in:
oneao committed 2026-07-23 22:52:02 +08:00
1 parent 5f73b54802
commit 4d118a0037
22 files changed
+1403 -259

No files matched your search

@@ -1,6 +1,6 @@
<script setup>
import { computed, h, ref, watch } from "vue";
import { message } from "antdv-next";
import { message, Modal } from "antdv-next";
import FmsTree from "@/components/FmsTree.vue";
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
@@ -20,6 +20,26 @@ const treeLoading = ref(false);
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;
const items = [];
if (!node) return { items };
if (node.b_module_type === "directory") {
items.push(
{ label: "添加目录", key: "add-directory" },
{ label: "添加页面", key: "add-page" },
{ label: "添加数据", key: "add-data" },
);
} else if (node.b_module_type === "page") {
items.push({ label: "添加数据", key: "add-data" });
}
if (items.length) items.push({ type: "divider" });
items.push({ label: "删除", key: "delete", danger: true });
return { items, onClick: onContextMenuClick };
});
// 原始数据(加载时保存,用于 diff)
const originals = ref({
@@ -49,6 +69,21 @@ const configSaving = ref(false);
// 当前选中模块对象(从 drafts.module 获取)
const selectedModule = computed(() => drafts.value.module);
// 上级模块名称(从模块树解析 b_parent_id 对应的名称,用于基本信息展示)
const parentModuleName = computed(() => {
const pid = selectedModule.value?.b_parent_id;
if (!pid) return "";
const find = (nodes) => {
for (const n of nodes || []) {
if (String(n.b_id) === String(pid)) return n.b_name || n.b_code;
const child = find(n.children);
if (child) return child;
}
return "";
};
return find(treeData.value);
});
// 可见 Tab
const visibleTabs = computed(() => {
const type = selectedModule.value?.b_module_type;
@@ -62,10 +97,12 @@ const visibleTabs = computed(() => {
{ key: "query", tab: "查询配置", icon: "Search" },
);
}
tabs.push(
{ key: "powers", tab: "权限配置", icon: "Lock" },
{ key: "i18n", tab: "多语言", icon: "Translation" },
);
if (type === "page") {
tabs.push({ key: "powers", tab: "权限配置", icon: "Lock" });
}
if (type !== "directory") {
tabs.push({ key: "i18n", tab: "多语言", icon: "Translation" });
}
return tabs;
});
@@ -210,21 +247,43 @@ async function onAddModule(parentId, moduleType = "directory") {
const addModuleMenu = {
items: [
{ key: "directory", label: "目录" },
{ key: "page", label: "页面" },
{ key: "data", label: "数据" },
],
onClick: ({ key }) => onAddModule(null, key),
};
// ---- 删除模块 ----
async function onDeleteModule() {
const moduleId = selectedModuleId.value;
if (!moduleId) return;
// ---- 右键菜单 ----
function onTreeRightClick({ node }) {
contextNode.value = node;
}
function onContextMenuClick({ key }) {
contextMenuOpen.value = false;
const node = contextNode.value;
contextNode.value = null;
if (!node) return;
if (key.startsWith("add-")) {
onAddModule(node.key, key.replace("add-", ""));
} else if (key === "delete") {
Modal.confirm({
title: `确定删除「${node.title}」?`,
content: "此操作不可恢复",
okText: "删除",
cancelText: "取消",
okButtonProps: { danger: true },
onOk: () => onDeleteNodeById(node.key),
});
}
}
async function onDeleteNodeById(moduleId) {
try {
await saveObjectApi([{ table: "b_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
message.success("模块已删除");
selectedModuleId.value = null;
drafts.value.module = null;
if (selectedModuleId.value === moduleId) {
selectedModuleId.value = null;
drafts.value.module = null;
}
await loadTree();
} catch (e) {
message.error("删除失败:" + (e.message || e));
@@ -412,23 +471,28 @@ loadTree();
<div v-if="treeData.length === 0" class="mm-tree-empty">
<a-empty description="暂无模块" />
</div>
<FmsTree
<a-dropdown
v-else
class="mm-tree"
:tree-data="treeData"
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
:field-names="{ title: 'title', key: 'key', children: 'children' }"
@select="onTreeSelect"
:trigger="['contextmenu']"
:menu="contextMenuItems"
>
<template #title="{ key, title }">
<span class="mm-tree-node">
<span>{{ title }}</span>
<span class="mm-tree-node-actions">
<a-button type="text" @click.stop="onAddModule(key)">+</a-button>
</span>
</span>
</template>
</FmsTree>
<div style="flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;">
<FmsTree
class="mm-tree"
:tree-data="treeData"
: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>
</aside>
<!-- 右侧详情 -->
@@ -438,9 +502,9 @@ loadTree();
<div class="mm-header">
<a-space>
<a-button type="primary" :loading="configSaving" @click="saveAll">保存</a-button>
<a-popconfirm title="确定删除该模块?" @confirm="onDeleteModule">
<a-button danger>删除模块</a-button>
</a-popconfirm>
<a-popconfirm title="确定删除该模块?" @confirm="onDeleteNodeById(selectedModuleId)">
<a-button danger>删除模块</a-button>
</a-popconfirm>
</a-space>
<div class="mm-header-center">
<a-space size="middle">
@@ -481,6 +545,7 @@ loadTree();
v-if="tab.key === 'basic'"
:module-id="String(selectedModule.b_id)"
:data="drafts.module"
:parent-name="parentModuleName"
:loading="configSaving"
@update="(v) => onUpdate('module', v)"
/>
@@ -524,6 +589,7 @@ loadTree();
<ModulePowersPanel
v-else-if="tab.key === 'powers'"
:module-id="String(selectedModule.b_id)"
:module-code="selectedModule?.b_code || ''"
:powers="drafts.powers"
:loading="configSaving"
@update="(v) => onUpdate('powers', v)"
@@ -585,7 +651,7 @@ loadTree();
flex: 1;
min-height: 0;
overflow: auto;
padding: 8px 0;
padding: 8px 10px;
}
.mm-tree-empty {
@@ -602,15 +668,7 @@ loadTree();
justify-content: space-between;
width: 100%;
gap: 4px;
}
.mm-tree-node-actions {
display: none;
flex: none;
}
.mm-tree-node:hover .mm-tree-node-actions {
display: flex;
user-select: none;
}
.mm-main {
@@ -687,6 +745,11 @@ loadTree();
:deep(.ant-tabs-tabpane) {
height: 100%;
}
:deep(.ant-tabs-body),
:deep(.ant-tabs-body-top) {
height: 100%;
}
}
.mm-loading {