810 lines
22 KiB
Vue
810 lines
22 KiB
Vue
<script setup>
|
|
import { computed, h, ref, watch } from "vue";
|
|
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";
|
|
import { createTempId } from "@/utils/tempId";
|
|
import { Plus } from "@lucide/vue";
|
|
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
|
|
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
|
|
import ModuleListConfigPanel from "./components/ModuleListConfigPanel.vue";
|
|
import ModuleEditConfigPanel from "./components/ModuleEditConfigPanel.vue";
|
|
import ModuleQueryConfigPanel from "./components/ModuleQueryConfigPanel.vue";
|
|
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
|
|
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
|
|
|
// ---- 状态 ----
|
|
const treeData = ref([]);
|
|
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({
|
|
module: null,
|
|
fields: [],
|
|
groups: [],
|
|
listConfig: [],
|
|
editConfig: [],
|
|
queryConfig: [],
|
|
powers: [],
|
|
});
|
|
|
|
// 编辑草稿
|
|
const drafts = ref({
|
|
module: null,
|
|
fields: [],
|
|
groups: [],
|
|
listConfig: [],
|
|
editConfig: [],
|
|
queryConfig: [],
|
|
powers: [],
|
|
});
|
|
|
|
const configLoading = ref(false);
|
|
const configSaving = ref(false);
|
|
const i18nPanelRef = ref(null);
|
|
|
|
// 当前选中模块对象(从 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;
|
|
if (!type) return [];
|
|
const tabs = [{ key: "basic", tab: "基本信息", icon: "Info" }];
|
|
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" },
|
|
);
|
|
}
|
|
if (type === "page") {
|
|
tabs.push({ key: "powers", tab: "权限配置", icon: "Lock" });
|
|
}
|
|
if (type !== "directory") {
|
|
tabs.push({ key: "i18n", tab: "多语言", icon: "Translation" });
|
|
}
|
|
return tabs;
|
|
});
|
|
|
|
// ---- 模块树 ----
|
|
function buildTree(modules) {
|
|
const map = new Map();
|
|
const roots = [];
|
|
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);
|
|
});
|
|
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;
|
|
}
|
|
|
|
async function loadTree() {
|
|
treeLoading.value = true;
|
|
try {
|
|
const res = await loadDataApi("b_module", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
|
treeData.value = buildTree(res.data || []);
|
|
} finally {
|
|
treeLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 模块配置加载 ----
|
|
async function loadModuleConfig(moduleId) {
|
|
configLoading.value = true;
|
|
try {
|
|
const id = String(moduleId);
|
|
const [moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, powersRes] =
|
|
await Promise.all([
|
|
loadDataApi("b_module", `b_id = ${id}`),
|
|
loadDataApi("b_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
|
loadDataApi("b_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
loadDataApi("b_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
|
]);
|
|
|
|
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 || [],
|
|
};
|
|
originals.value = original;
|
|
drafts.value = cloneData(original);
|
|
activeTab.value = "basic";
|
|
} finally {
|
|
configLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 树节点选择 ----
|
|
function onTreeSelect(keys) {
|
|
if (keys.length > 0) {
|
|
selectedModuleId.value = String(keys[0]);
|
|
}
|
|
}
|
|
|
|
watch(selectedModuleId, (id) => {
|
|
if (!id) return;
|
|
if (skipLoad.value) {
|
|
skipLoad.value = false;
|
|
return;
|
|
}
|
|
loadModuleConfig(id);
|
|
});
|
|
|
|
// ---- 子面板数据变更 ----
|
|
function onUpdate(section, newData) {
|
|
drafts.value[section] = newData;
|
|
}
|
|
|
|
function onSetField(field, value) {
|
|
if (drafts.value.module) {
|
|
drafts.value.module[field] = value;
|
|
}
|
|
}
|
|
|
|
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
|
|
function onGroupsSaved(newGroups) {
|
|
originals.value.groups = cloneData(newGroups);
|
|
drafts.value.groups = cloneData(newGroups);
|
|
}
|
|
|
|
// ---- 新增模块 ----
|
|
async function onAddModule(parentId, moduleType = "directory") {
|
|
const res = await nextIdApi();
|
|
const newId = String(res.data);
|
|
const newModule = {
|
|
b_id: newId,
|
|
b_code: "",
|
|
b_parent_id: parentId ? String(parentId) : null,
|
|
b_name: "",
|
|
b_i18n: null,
|
|
b_module_type: moduleType,
|
|
b_route: null,
|
|
b_viewtable: null,
|
|
b_savetable: null,
|
|
b_keyfield: null,
|
|
b_orderfield: null,
|
|
b_canmenu: 0,
|
|
b_canuse: 1,
|
|
b_xh: 0,
|
|
};
|
|
|
|
// 更新 originals 和 drafts
|
|
originals.value = {
|
|
module: cloneData(newModule),
|
|
fields: [],
|
|
groups: [],
|
|
listConfig: [],
|
|
editConfig: [],
|
|
queryConfig: [],
|
|
powers: [],
|
|
};
|
|
drafts.value = cloneData(originals.value);
|
|
skipLoad.value = true;
|
|
selectedModuleId.value = newId;
|
|
activeTab.value = "basic";
|
|
|
|
// 刷新树
|
|
await loadTree();
|
|
}
|
|
|
|
const addModuleMenu = {
|
|
items: [{ key: "directory", label: "目录" }],
|
|
onClick: ({ key }) => onAddModule(null, key),
|
|
};
|
|
|
|
// ---- 右键菜单 ----
|
|
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") {
|
|
if (hasChildNode(node.key)) {
|
|
Modal.warning({
|
|
title: "无法删除",
|
|
content: "该节点下还有子节点,请先删除子节点后再删除当前节点。",
|
|
});
|
|
return;
|
|
}
|
|
Modal.confirm({
|
|
title: `确定删除「${node.title}」?`,
|
|
content: "此操作不可恢复",
|
|
okText: "删除",
|
|
cancelText: "取消",
|
|
okButtonProps: { danger: true },
|
|
onOk: () => onDeleteNodeById(node.key),
|
|
});
|
|
}
|
|
}
|
|
|
|
function findNode(key) {
|
|
const walk = (nodes) => {
|
|
for (const n of nodes || []) {
|
|
if (String(n.b_id) === String(key)) return n;
|
|
const child = walk(n.children);
|
|
if (child) return child;
|
|
}
|
|
return null;
|
|
};
|
|
return walk(treeData.value);
|
|
}
|
|
|
|
function hasChildNode(moduleId) {
|
|
const node = findNode(moduleId);
|
|
return !!node?.children?.length;
|
|
}
|
|
|
|
async function onDeleteNodeById(moduleId) {
|
|
if (hasChildNode(moduleId)) {
|
|
Modal.warning({
|
|
title: "无法删除",
|
|
content: "该节点下还有子节点,请先删除子节点后再删除当前节点。",
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
await saveObjectApi([{ table: "b_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
|
|
message.success("模块已删除");
|
|
if (selectedModuleId.value === moduleId) {
|
|
selectedModuleId.value = null;
|
|
drafts.value.module = null;
|
|
}
|
|
await loadTree();
|
|
} catch (e) {
|
|
message.error("删除失败:" + (e.message || e));
|
|
}
|
|
}
|
|
|
|
// ---- 统一保存 ----
|
|
async function saveAll() {
|
|
if (!selectedModule.value) return;
|
|
configSaving.value = true;
|
|
|
|
try {
|
|
const saveReqs = [];
|
|
|
|
// b_module 主表
|
|
const moduleData = drafts.value.module;
|
|
const moduleOriginal = originals.value.module;
|
|
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
|
const isNew = !moduleOriginal?.b_code;
|
|
saveReqs.push({
|
|
table: "b_module",
|
|
key_field: "b_id",
|
|
[isNew ? "inserts" : "updates"]: [moduleData],
|
|
});
|
|
}
|
|
|
|
// 子表:fields, listConfig, editConfig, queryConfig, powers
|
|
const subTables = [
|
|
["b_module_field", "fields"],
|
|
["b_module_field_list", "listConfig"],
|
|
["b_module_field_edit", "editConfig"],
|
|
["b_module_field_query", "queryConfig"],
|
|
["b_module_power", "powers"],
|
|
];
|
|
for (const [table, section] of subTables) {
|
|
const current = drafts.value[section] || [];
|
|
const previous = originals.value[section] || [];
|
|
const changes = diffRows(current, previous);
|
|
const req = tableChange(table, changes);
|
|
if (hasChanges(req)) {
|
|
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
|
saveReqs.push(req);
|
|
}
|
|
}
|
|
|
|
// 多语言:由各子面板收集变更,统一在此保存
|
|
if (i18nPanelRef.value) {
|
|
const i18nReq = await i18nPanelRef.value.buildSaveRequest();
|
|
if (i18nReq) saveReqs.push(i18nReq);
|
|
}
|
|
|
|
if (saveReqs.length === 0) {
|
|
message.info("没有需要保存的变更");
|
|
return;
|
|
}
|
|
|
|
await saveObjectApi(saveReqs);
|
|
|
|
// 更新 originals
|
|
originals.value = cloneData(drafts.value);
|
|
i18nPanelRef.value?.markSaved();
|
|
message.success("保存成功");
|
|
await loadTree();
|
|
} catch (e) {
|
|
message.error("保存失败:" + (e.message || e));
|
|
} finally {
|
|
configSaving.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 字段同步 ----
|
|
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";
|
|
}
|
|
|
|
async function syncFields() {
|
|
const module = selectedModule.value;
|
|
if (!module?.b_viewtable) {
|
|
message.warning("当前模块未配置查询视图");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
configSaving.value = true;
|
|
const res = await describeApi(module.b_viewtable);
|
|
const columns = res.data || [];
|
|
const existingFields = originals.value.fields || [];
|
|
|
|
// 构建视图列名集合
|
|
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
|
|
|
|
// 已有字段映射(按字段名)
|
|
const existingMap = new Map(existingFields.map((f) => [f.b_field?.toLowerCase(), f]));
|
|
|
|
const mergedFields = [];
|
|
|
|
// 遍历视图列
|
|
for (const col of columns) {
|
|
const existing = existingMap.get(col.name.toLowerCase());
|
|
if (existing) {
|
|
// 已有字段:保留原配置
|
|
mergedFields.push({ ...existing, b_canuse: 1 });
|
|
} else {
|
|
// 新字段:创建默认记录
|
|
mergedFields.push({
|
|
b_id: createTempId(),
|
|
b_module_id: String(module.b_id),
|
|
b_field: col.name,
|
|
b_name: col.name,
|
|
b_i18n: null,
|
|
b_type: typeFromSqlType(col.type),
|
|
b_default_value: null,
|
|
b_options: null,
|
|
b_canuse: 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 视图中不存在的已有字段标记为不可用
|
|
for (const existing of existingFields) {
|
|
if (!viewColumnSet.has(existing.b_field?.toLowerCase())) {
|
|
mergedFields.push({ ...existing, b_canuse: 0 });
|
|
}
|
|
}
|
|
|
|
// 保存到 b_module_field
|
|
const changes = diffRows(mergedFields, existingFields);
|
|
const req = tableChange("b_module_field", changes);
|
|
if (hasChanges(req)) {
|
|
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
|
await saveObjectApi([req]);
|
|
message.success(
|
|
`字段同步完成:${columns.length} 列,新增 ${req.inserts?.length || 0},停用 ${req.deletes?.length || 0}`,
|
|
);
|
|
} else {
|
|
message.info("字段已是最新,无需同步");
|
|
}
|
|
|
|
// 重新加载
|
|
const fieldsRes = await loadDataApi(
|
|
"b_module_field",
|
|
`b_module_id = ${module.b_id}`,
|
|
"b_id ASC",
|
|
);
|
|
originals.value.fields = fieldsRes.data || [];
|
|
drafts.value.fields = cloneData(originals.value.fields);
|
|
} catch (e) {
|
|
message.error("字段同步失败:" + (e.message || e));
|
|
} finally {
|
|
configSaving.value = false;
|
|
}
|
|
}
|
|
|
|
// ---- 初始化 ----
|
|
loadTree();
|
|
</script>
|
|
|
|
<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']">
|
|
<a-button><Plus :size="14" />新增</a-button>
|
|
</a-dropdown>
|
|
</div>
|
|
<div v-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"
|
|
>
|
|
<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>
|
|
|
|
<!-- 右侧详情 -->
|
|
<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"
|
|
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
|
/>
|
|
<a-divider type="vertical" />
|
|
<span>显示菜单</span>
|
|
<a-switch
|
|
:checked="drafts.module?.b_canmenu === 1"
|
|
:disabled="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>
|
|
|
|
<!-- 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>
|
|
<ModuleBasicPanel
|
|
v-if="tab.key === 'basic'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:data="drafts.module"
|
|
:parent-name="parentModuleName"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('module', v)"
|
|
/>
|
|
<ModuleFieldsPanel
|
|
v-else-if="tab.key === 'fields'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:fields="drafts.fields"
|
|
:view-table="selectedModule.b_viewtable"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('fields', v)"
|
|
@sync="syncFields"
|
|
/>
|
|
<ModuleListConfigPanel
|
|
v-else-if="tab.key === 'list'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:list-config="drafts.listConfig"
|
|
:fields="drafts.fields"
|
|
:groups="drafts.groups"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('listConfig', v)"
|
|
@update-groups="onGroupsSaved"
|
|
/>
|
|
<ModuleEditConfigPanel
|
|
v-else-if="tab.key === 'edit'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:edit-config="drafts.editConfig"
|
|
:fields="drafts.fields"
|
|
:groups="drafts.groups"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('editConfig', v)"
|
|
@update-groups="onGroupsSaved"
|
|
/>
|
|
<ModuleQueryConfigPanel
|
|
v-else-if="tab.key === 'query'"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:query-config="drafts.queryConfig"
|
|
:fields="drafts.fields"
|
|
:loading="configSaving"
|
|
@update="(v) => onUpdate('queryConfig', v)"
|
|
/>
|
|
<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)"
|
|
/>
|
|
<ModuleI18nPanel
|
|
v-else-if="tab.key === 'i18n'"
|
|
ref="i18nPanelRef"
|
|
:module-id="String(selectedModule.b_id)"
|
|
:loading="configSaving"
|
|
/>
|
|
</template>
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 未选中模块 -->
|
|
<div v-else class="mm-empty">
|
|
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.mm-page {
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
background: var(--fms-bg);
|
|
}
|
|
|
|
.mm-sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: none;
|
|
width: 260px;
|
|
border-right: 1px solid var(--fms-border);
|
|
background: var(--fms-surface);
|
|
}
|
|
|
|
.mm-sidebar-header {
|
|
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;
|
|
}
|
|
|
|
.mm-tree-empty {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 0;
|
|
}
|
|
|
|
.mm-tree-node {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
gap: 4px;
|
|
user-select: none;
|
|
}
|
|
|
|
.mm-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
background: var(--fms-surface);
|
|
}
|
|
|
|
.mm-header {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: none;
|
|
gap: 12px;
|
|
padding: 12px 20px;
|
|
background: var(--fms-surface);
|
|
border-bottom: 1px solid var(--fms-border);
|
|
}
|
|
|
|
.mm-header-center {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.mm-header-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--fms-text);
|
|
}
|
|
|
|
.mm-header-code {
|
|
font-size: 13px;
|
|
color: var(--fms-text-secondary);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.mm-header-type {
|
|
flex: none;
|
|
}
|
|
|
|
.mm-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mm-tabs {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
:deep(.ant-tabs-nav) {
|
|
margin-bottom: 0;
|
|
padding: 0 20px;
|
|
background: var(--fms-surface);
|
|
}
|
|
|
|
:deep(.ant-tabs-content-holder) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.ant-tabs-content) {
|
|
flex: 1;
|
|
min-height: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.ant-tabs-tabpane) {
|
|
height: 100%;
|
|
}
|
|
|
|
:deep(.ant-tabs-body),
|
|
:deep(.ant-tabs-body-top) {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.mm-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 200px;
|
|
}
|
|
|
|
.mm-empty {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
}
|
|
</style>
|