1443 lines
45 KiB
Vue
1443 lines
45 KiB
Vue
<script setup>
|
||
import { computed, h, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||
import { message, Modal } from "antdv-next";
|
||
import { Plus, Trash2 } from "@lucide/vue";
|
||
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
|
||
import FmsTree from "@/components/FmsTree.vue";
|
||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
||
import { useAppStore } from "@/stores/app";
|
||
import { usePermissionStore } from "@/stores/permissions";
|
||
import {
|
||
cloneData,
|
||
diffRows,
|
||
allocateTemporaryIds,
|
||
remapForeignKeys,
|
||
tableChange,
|
||
hasChanges,
|
||
} from "@/utils/dataChanges";
|
||
import { createTempId } from "@/utils/tempId";
|
||
import { buildTree } from "@/utils/tree";
|
||
import {
|
||
buildModuleI18nKey,
|
||
buildModuleItemI18nKey,
|
||
syncModuleItemI18nKeys,
|
||
validateModuleCode,
|
||
} from "@/utils/i18n";
|
||
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 ModuleAutoCodePanel from "./components/ModuleAutoCodePanel.vue";
|
||
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
|
||
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
||
import { createAutoCodeDraft } from "./common";
|
||
|
||
// ---- 状态 ----
|
||
const appStore = useAppStore();
|
||
const permissionStore = usePermissionStore();
|
||
const treeData = ref([]);
|
||
const treeLoading = ref(false);
|
||
const expandedTreeKeys = ref([]);
|
||
const selectedModuleId = ref(null);
|
||
const skipLoad = ref(false);
|
||
const activeTab = ref("basic");
|
||
const treeSearch = ref("");
|
||
|
||
function filterTree(nodes, keyword) {
|
||
if (!keyword) return nodes;
|
||
const kw = keyword.toLowerCase();
|
||
const result = [];
|
||
for (const node of nodes) {
|
||
const title = (node.b_name || node.b_code || node.title || "").toLowerCase();
|
||
const children = filterTree(node.children || [], kw);
|
||
if (title.includes(kw) || children.length) {
|
||
result.push({ ...node, children });
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
const filteredTreeData = computed(() => {
|
||
return filterTree(treeData.value, treeSearch.value);
|
||
});
|
||
function buildTreeContextMenu(node) {
|
||
const items = [];
|
||
const iconSize = 14;
|
||
if (!node) return { items };
|
||
if (node.isFmsRoot) {
|
||
if (canManage("create"))
|
||
items.push({
|
||
label: "添加目录",
|
||
key: "add-directory",
|
||
icon: h(Plus, { size: iconSize }),
|
||
});
|
||
return { items };
|
||
}
|
||
if (node.b_module_type === "directory") {
|
||
if (canManage("create")) {
|
||
items.push(
|
||
{ label: "添加目录", key: "add-directory", icon: h(Plus, { size: iconSize }) },
|
||
{ label: "添加页面", key: "add-page", icon: h(Plus, { size: iconSize }) },
|
||
);
|
||
}
|
||
} else if (["page", "data_category"].includes(node.b_module_type)) {
|
||
if (canManage("create")) {
|
||
items.push(
|
||
{ label: "添加数据分类", key: "add-data_category", icon: h(Plus, { size: iconSize }) },
|
||
{ label: "添加数据", key: "add-data", icon: h(Plus, { size: iconSize }) },
|
||
);
|
||
}
|
||
}
|
||
if (canManage("delete")) {
|
||
if (items.length) items.push({ type: "divider" });
|
||
items.push({ label: "删除", key: "delete", danger: true, icon: h(Trash2, { size: iconSize }) });
|
||
}
|
||
return { items };
|
||
}
|
||
|
||
function canManage(actionCode) {
|
||
return permissionStore.canPower("m_metadata", actionCode);
|
||
}
|
||
|
||
// 原始数据(加载时保存,用于 diff)
|
||
const originals = ref({
|
||
module: null,
|
||
fields: [],
|
||
groups: [],
|
||
listConfig: [],
|
||
editConfig: [],
|
||
queryConfig: [],
|
||
autoCode: [],
|
||
powers: [],
|
||
});
|
||
|
||
// 编辑草稿
|
||
const drafts = ref({
|
||
module: null,
|
||
fields: [],
|
||
groups: [],
|
||
listConfig: [],
|
||
editConfig: [],
|
||
queryConfig: [],
|
||
autoCode: [],
|
||
powers: [],
|
||
});
|
||
|
||
const configLoading = ref(false);
|
||
const configSaving = ref(false);
|
||
const configSyncing = ref(false);
|
||
const i18nPanelRef = ref(null);
|
||
const loadedModuleId = ref(null);
|
||
|
||
// 使用函数 ref,避免组件位于 TabPane 的 v-for 内时被 Vue 收集成实例数组。
|
||
function setI18nPanelRef(instance) {
|
||
i18nPanelRef.value = instance || null;
|
||
}
|
||
|
||
// 当前选中模块对象(从 drafts.module 获取)
|
||
const selectedModule = computed(() => drafts.value.module);
|
||
const autoCodeEnabled = computed(() => Number(drafts.value.autoCode?.[0]?.b_canuse) === 1);
|
||
const busy = computed(() => configLoading.value || configSaving.value || configSyncing.value);
|
||
const syncDisabled = computed(
|
||
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
|
||
);
|
||
|
||
/* ---- 左侧面板拖拽调整宽度 — 抄 AppSidebar 的 sidebar-resize-handle ---- */
|
||
const MIN_SIDEBAR = 210;
|
||
const MAX_SIDEBAR = 440;
|
||
const leftPanelWidth = ref(260);
|
||
const isResizing = ref(false);
|
||
let resizeStartX = 0;
|
||
let resizeStartWidth = 0;
|
||
|
||
function onResizeStart(e) {
|
||
e.preventDefault();
|
||
isResizing.value = true;
|
||
resizeStartX = e.clientX;
|
||
resizeStartWidth = leftPanelWidth.value;
|
||
document.body.style.cursor = "col-resize";
|
||
document.body.style.userSelect = "none";
|
||
}
|
||
|
||
function onResizeMove(e) {
|
||
if (!isResizing.value) return;
|
||
const delta = e.clientX - resizeStartX;
|
||
leftPanelWidth.value = Math.min(MAX_SIDEBAR, Math.max(MIN_SIDEBAR, resizeStartWidth + delta));
|
||
}
|
||
|
||
function onResizeEnd() {
|
||
if (!isResizing.value) return;
|
||
isResizing.value = false;
|
||
document.body.style.cursor = "";
|
||
document.body.style.userSelect = "";
|
||
}
|
||
|
||
onMounted(() => {
|
||
document.addEventListener("mousemove", onResizeMove);
|
||
document.addEventListener("mouseup", onResizeEnd);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
document.removeEventListener("mousemove", onResizeMove);
|
||
document.removeEventListener("mouseup", onResizeEnd);
|
||
});
|
||
// 构建受管多语言项。managedSource 提供模块/字段/权限的受管键来源,
|
||
// plainSource 提供分组/列表标题等非受管键来源(手动填写,需即时反馈)。
|
||
function buildI18nItems(managedSource, plainSource) {
|
||
const items = new Map();
|
||
const add = (item) => {
|
||
if (!item.key) return;
|
||
const key = String(item.key);
|
||
const existing = items.get(key);
|
||
items.set(key, existing?.managed ? existing : { ...item, key });
|
||
};
|
||
|
||
const module = managedSource.module;
|
||
if (module?.b_code) {
|
||
const moduleKey = buildModuleI18nKey(module.b_module_type, module.b_code);
|
||
add({
|
||
id: `module:${module.b_id}`,
|
||
key: moduleKey,
|
||
sourceKey: module.b_i18n,
|
||
defaultValue: module.b_name || module.b_code,
|
||
managed: true,
|
||
});
|
||
}
|
||
for (const field of managedSource.fields || []) {
|
||
add({
|
||
id: `field:${field.b_id}`,
|
||
key: buildModuleItemI18nKey("field", module?.b_code, field.b_field),
|
||
sourceKey: field.b_i18n,
|
||
defaultValue: field.b_name || field.b_field,
|
||
managed: true,
|
||
});
|
||
}
|
||
for (const power of managedSource.powers || []) {
|
||
add({
|
||
id: `power:${power.b_id}`,
|
||
key: buildModuleItemI18nKey("power", module?.b_code, power.b_code),
|
||
sourceKey: power.b_i18n,
|
||
defaultValue: power.b_name || power.b_code,
|
||
managed: true,
|
||
});
|
||
}
|
||
|
||
// 分组和列表标题仍显示在本模块的多语言面板中,但不是系统生成的受管键。
|
||
for (const section of ["groups", "listConfig"]) {
|
||
for (const row of plainSource[section] || []) {
|
||
add({ key: row.b_i18n, managed: false });
|
||
}
|
||
}
|
||
return [...items.values()];
|
||
}
|
||
|
||
// 实时展示:受管键基于已保存快照(originals),编辑过程中不随草稿实时生成;
|
||
// 非受管键(分组/列表标题)基于草稿,保留手动编辑的即时反馈。
|
||
const i18nItems = computed(() => buildI18nItems(originals.value, drafts.value));
|
||
// 保存时:受管键基于最终草稿态(drafts),此时才按模块编码生成并落库。
|
||
const finalI18nItems = computed(() => buildI18nItems(drafts.value, drafts.value));
|
||
const referencedGroupIds = computed(() => {
|
||
const ids = new Set();
|
||
for (const section of ["listConfig", "editConfig"]) {
|
||
for (const row of drafts.value[section] || []) {
|
||
if (row.b_group_id != null) ids.add(String(row.b_group_id));
|
||
}
|
||
}
|
||
return [...ids];
|
||
});
|
||
|
||
// 上级模块名称(从模块树解析 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: "基本信息" }];
|
||
if (type === "data") {
|
||
tabs.push(
|
||
{ key: "fields", tab: "字段定义" },
|
||
{ key: "list", tab: "列表配置" },
|
||
{ key: "edit", tab: "编辑配置" },
|
||
{ key: "query", tab: "查询配置" },
|
||
{ key: "auto-code", tab: "自动编码" },
|
||
);
|
||
}
|
||
if (type === "page") {
|
||
tabs.push({ key: "powers", tab: "权限配置" });
|
||
}
|
||
if (type !== "directory") {
|
||
tabs.push({ key: "i18n", tab: "多语言" });
|
||
}
|
||
return tabs;
|
||
});
|
||
|
||
// ---- 模块树 ----
|
||
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 != null ? String(m.b_parent_id) : null;
|
||
});
|
||
const tree = buildTree(modules);
|
||
return tree;
|
||
}
|
||
|
||
// 普通保存后直接更新现有节点,避免重新请求并重建整棵树导致展开状态丢失。
|
||
function updateSavedTreeNode(moduleData) {
|
||
let node = findNode(moduleData.b_id);
|
||
if (!node) {
|
||
node = { ...cloneData(moduleData), children: [] };
|
||
const parent = moduleData.b_parent_id ? findNode(moduleData.b_parent_id) : null;
|
||
if (parent) {
|
||
if (!parent.children) parent.children = [];
|
||
parent.children.push(node);
|
||
} else {
|
||
treeData.value.push(node);
|
||
}
|
||
}
|
||
const children = node.children;
|
||
Object.assign(node, cloneData(moduleData), {
|
||
key: String(moduleData.b_id),
|
||
title: moduleData.b_name || moduleData.b_code,
|
||
});
|
||
if (children) node.children = children;
|
||
}
|
||
|
||
async function loadTree() {
|
||
treeLoading.value = true;
|
||
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 unsavedModuleIds = new Set();
|
||
|
||
// ---- 模块配置加载 ----
|
||
function waitForNextPaint() {
|
||
if (typeof requestAnimationFrame !== "function") return Promise.resolve();
|
||
return new Promise((resolve) => {
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(resolve);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadModuleConfig(moduleId, options = {}) {
|
||
const id = String(moduleId);
|
||
const applyReady = options.applyReady;
|
||
if (options.waitForPaint) await waitForNextPaint();
|
||
if (String(selectedModuleId.value) !== id) return;
|
||
|
||
configLoading.value = true;
|
||
try {
|
||
const moduleType = findNode(id)?.b_module_type;
|
||
let moduleResponse;
|
||
let fieldsResponse;
|
||
let groupsResponse;
|
||
let listResponse;
|
||
let editResponse;
|
||
let queryResponse;
|
||
let autoCodeResponse;
|
||
let powersResponse;
|
||
|
||
if (moduleType === "data") {
|
||
const responses = await Promise.all([
|
||
loadDataApi("s_module", `b_id = ${id}`),
|
||
loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||
loadDataApi("s_module_auto_code", `b_module_id = ${id}`, "b_id ASC"),
|
||
]);
|
||
moduleResponse = responses[0];
|
||
fieldsResponse = responses[1];
|
||
groupsResponse = responses[2];
|
||
listResponse = responses[3];
|
||
editResponse = responses[4];
|
||
queryResponse = responses[5];
|
||
autoCodeResponse = responses[6];
|
||
} else if (moduleType === "page") {
|
||
const responses = await Promise.all([
|
||
loadDataApi("s_module", `b_id = ${id}`),
|
||
loadDataApi("s_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||
]);
|
||
moduleResponse = responses[0];
|
||
powersResponse = responses[1];
|
||
} else {
|
||
moduleResponse = await loadDataApi("s_module", `b_id = ${id}`);
|
||
}
|
||
|
||
if (String(selectedModuleId.value) !== id) return;
|
||
if (applyReady && !(await applyReady)) return;
|
||
if (String(selectedModuleId.value) !== id) return;
|
||
|
||
const original = {
|
||
module: moduleResponse.data?.[0] || null,
|
||
fields: fieldsResponse ? fieldsResponse.data || [] : [],
|
||
groups: groupsResponse ? groupsResponse.data || [] : [],
|
||
listConfig: listResponse ? listResponse.data || [] : [],
|
||
editConfig: editResponse ? editResponse.data || [] : [],
|
||
queryConfig: queryResponse ? queryResponse.data || [] : [],
|
||
autoCode: autoCodeResponse ? autoCodeResponse.data || [] : [],
|
||
powers: powersResponse ? powersResponse.data || [] : [],
|
||
};
|
||
originals.value = original;
|
||
drafts.value = cloneData(original);
|
||
loadedModuleId.value = id;
|
||
activeTab.value = "basic";
|
||
} catch (e) {
|
||
if (String(selectedModuleId.value) === String(moduleId)) {
|
||
loadedModuleId.value = null;
|
||
drafts.value.module = null;
|
||
message.error("模块配置加载失败:" + (e.message || e));
|
||
}
|
||
} finally {
|
||
if (String(selectedModuleId.value) === String(moduleId)) {
|
||
configLoading.value = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 树节点选择 ----
|
||
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;
|
||
|
||
selectedModuleId.value = String(nodeKey);
|
||
}
|
||
|
||
watch(selectedModuleId, (id) => {
|
||
if (!id) {
|
||
configLoading.value = false;
|
||
loadedModuleId.value = null;
|
||
drafts.value.module = null;
|
||
return;
|
||
}
|
||
if (skipLoad.value) {
|
||
skipLoad.value = false;
|
||
configLoading.value = false;
|
||
return;
|
||
}
|
||
loadedModuleId.value = null;
|
||
void loadModuleConfig(id);
|
||
});
|
||
|
||
// ---- 子面板数据变更 ----
|
||
function onUpdate(section, newData) {
|
||
drafts.value[section] = newData;
|
||
}
|
||
|
||
function isFieldEnabled(field) {
|
||
if (!field) return false;
|
||
return Number(field.b_canuse) === 1;
|
||
}
|
||
|
||
function applyDisabledFieldDefaults(previousFields, nextFields) {
|
||
const previousEnabledStates = new Map();
|
||
for (const field of previousFields || []) {
|
||
if (field.b_id != null) {
|
||
previousEnabledStates.set(String(field.b_id), isFieldEnabled(field));
|
||
}
|
||
}
|
||
|
||
const disabledFieldIds = new Set();
|
||
for (const field of nextFields || []) {
|
||
if (field.b_id == null) continue;
|
||
const fieldId = String(field.b_id);
|
||
if (previousEnabledStates.get(fieldId) && !isFieldEnabled(field)) {
|
||
disabledFieldIds.add(fieldId);
|
||
}
|
||
}
|
||
if (disabledFieldIds.size === 0) return;
|
||
|
||
for (const row of drafts.value.listConfig || []) {
|
||
if (disabledFieldIds.has(String(row.b_field_id))) row.b_visible = 0;
|
||
}
|
||
for (const row of drafts.value.editConfig || []) {
|
||
if (disabledFieldIds.has(String(row.b_field_id))) row.b_visible = 0;
|
||
}
|
||
for (const row of drafts.value.queryConfig || []) {
|
||
if (disabledFieldIds.has(String(row.b_field_id))) row.b_canuse = 0;
|
||
}
|
||
}
|
||
|
||
function onFieldsUpdate(newFields) {
|
||
applyDisabledFieldDefaults(drafts.value.fields, newFields);
|
||
drafts.value.fields = newFields;
|
||
}
|
||
|
||
function onAutoCodeUpdate(rows) {
|
||
const previousRule = drafts.value.autoCode?.[0];
|
||
const previousFieldId =
|
||
previousRule && Number(previousRule.b_canuse) === 1 ? previousRule.b_field_id : null;
|
||
drafts.value.autoCode = rows;
|
||
const rule = rows && rows.length ? rows[0] : null;
|
||
const nextFieldId = rule && Number(rule.b_canuse) === 1 ? rule.b_field_id : null;
|
||
if (previousFieldId != null && String(previousFieldId) !== String(nextFieldId)) {
|
||
const original = (originals.value.editConfig || []).find(
|
||
(config) => String(config.b_field_id) === String(previousFieldId),
|
||
);
|
||
for (const config of drafts.value.editConfig || []) {
|
||
if (String(config.b_field_id) === String(previousFieldId)) {
|
||
config.b_readonly = Number(original?.b_readonly) === 1 ? 1 : 0;
|
||
}
|
||
}
|
||
}
|
||
if (nextFieldId == null) return;
|
||
for (const config of drafts.value.editConfig || []) {
|
||
if (String(config.b_field_id) === String(nextFieldId)) {
|
||
config.b_readonly = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
function setAutoCodeEnabled(enabled) {
|
||
const current = drafts.value.autoCode?.[0];
|
||
const rule = current
|
||
? { ...current, b_canuse: enabled ? 1 : 0 }
|
||
: createAutoCodeDraft(selectedModule.value?.b_id, drafts.value.fields, enabled);
|
||
onAutoCodeUpdate([rule]);
|
||
}
|
||
|
||
function validateAutoCode() {
|
||
const rows = drafts.value.autoCode || [];
|
||
const rule = rows.length ? rows[0] : null;
|
||
if (!rule || Number(rule.b_canuse) !== 1) return "";
|
||
if (!String(selectedModule.value?.b_savetable || "").trim()) {
|
||
return "启用自动编码前请先配置保存表";
|
||
}
|
||
const field = (drafts.value.fields || []).find(
|
||
(item) => String(item.b_id) === String(rule.b_field_id),
|
||
);
|
||
if (!field || Number(field.b_canuse) !== 1) return "请选择已启用的业务编号字段";
|
||
if (field.b_type !== "input" && field.b_type !== "textarea") {
|
||
return "业务编号字段必须使用文本控件类型";
|
||
}
|
||
if (!Number.isInteger(Number(rule.b_start_value)) || Number(rule.b_start_value) < 0) {
|
||
return "起始值必须是大于或等于 0 的整数";
|
||
}
|
||
if (!["none", "year", "month", "day"].includes(rule.b_reset_type)) {
|
||
return "请选择正确的重置周期";
|
||
}
|
||
const width = Number(rule.b_sequence_width);
|
||
if (!Number.isInteger(width) || width < 1 || width > 18) {
|
||
return "流水号位数必须是 1 到 18 之间的整数";
|
||
}
|
||
if (String(rule.b_prefix || "").length > 100) return "固定前缀长度不能超过 100 个字符";
|
||
if (String(rule.b_separator || "").length > 10) return "分隔符长度不能超过 10 个字符";
|
||
const dateFormat = String(rule.b_date_format || "").trim();
|
||
if (dateFormat.length > 30) return "日期格式长度不能超过 30 个字符";
|
||
const dateLiterals = dateFormat
|
||
.replaceAll("yyyy", "")
|
||
.replaceAll("yy", "")
|
||
.replaceAll("MM", "")
|
||
.replaceAll("dd", "");
|
||
if (/[A-Za-z]/.test(dateLiterals)) return "日期格式只支持 yyyy、yy、MM 和 dd";
|
||
const hasYear = dateFormat.includes("yyyy") || dateFormat.includes("yy");
|
||
const hasMonth = dateFormat.includes("MM");
|
||
const hasDay = dateFormat.includes("dd");
|
||
if (rule.b_reset_type === "year" && !hasYear) return "按年重置时,日期格式必须包含年份";
|
||
if (rule.b_reset_type === "month" && (!hasYear || !hasMonth)) {
|
||
return "按月重置时,日期格式必须包含年份和月份";
|
||
}
|
||
if (rule.b_reset_type === "day" && (!hasYear || !hasMonth || !hasDay)) {
|
||
return "按日重置时,日期格式必须包含年月日";
|
||
}
|
||
for (const config of drafts.value.editConfig || []) {
|
||
if (String(config.b_field_id) === String(rule.b_field_id)) config.b_readonly = 1;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function onSetField(field, value) {
|
||
if (drafts.value.module) {
|
||
drafts.value.module[field] = value;
|
||
}
|
||
}
|
||
|
||
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
|
||
function onGroupsSaved(newGroups, idMapping) {
|
||
if (idMapping && idMapping.size) {
|
||
remapForeignKeys(drafts.value.listConfig || [], ["b_group_id"], idMapping);
|
||
remapForeignKeys(drafts.value.editConfig || [], ["b_group_id"], idMapping);
|
||
}
|
||
originals.value.groups = cloneData(newGroups);
|
||
drafts.value.groups = cloneData(newGroups);
|
||
}
|
||
|
||
function onI18nSaved(key) {
|
||
if (i18nPanelRef.value) {
|
||
void i18nPanelRef.value.refreshKey(key);
|
||
}
|
||
}
|
||
|
||
// ---- 新增模块 ----
|
||
async function onAddModule(parentId, moduleType = "directory") {
|
||
if (!canManage("create")) return;
|
||
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: [],
|
||
autoCode: [],
|
||
powers: [],
|
||
};
|
||
drafts.value = cloneData(originals.value);
|
||
skipLoad.value = true;
|
||
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)];
|
||
}
|
||
}
|
||
|
||
// ---- 右键菜单 ----
|
||
function onTreeMenuClick({ key, node }) {
|
||
if (busy.value) return;
|
||
if (key.startsWith("add-")) {
|
||
const parentId = node.isFmsRoot ? null : node.key;
|
||
onAddModule(parentId, 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 Boolean(node && node.children && node.children.length);
|
||
}
|
||
|
||
async function onDeleteNodeById(moduleId) {
|
||
if (!canManage("delete")) return;
|
||
if (hasChildNode(moduleId)) {
|
||
Modal.warning({
|
||
title: "无法删除",
|
||
content: "该节点下还有子节点,请先删除子节点后再删除当前节点。",
|
||
});
|
||
return;
|
||
}
|
||
try {
|
||
const id = String(moduleId);
|
||
const deleteByModuleId = (table) => ({
|
||
table,
|
||
key_field: "b_module_id",
|
||
deletes: [{ b_module_id: id }],
|
||
});
|
||
|
||
await saveObjectApi([
|
||
deleteByModuleId("b_user_module"),
|
||
deleteByModuleId("b_user_power"),
|
||
deleteByModuleId("s_module_field_list"),
|
||
deleteByModuleId("s_module_field_edit"),
|
||
deleteByModuleId("s_module_field_query"),
|
||
deleteByModuleId("s_module_auto_code"),
|
||
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("模块已删除");
|
||
if (selectedModuleId.value === moduleId) {
|
||
selectedModuleId.value = null;
|
||
drafts.value.module = null;
|
||
}
|
||
await loadTree();
|
||
} catch (e) {
|
||
message.error("删除失败:" + (e.message || e));
|
||
}
|
||
}
|
||
|
||
// ---- 统一保存 ----
|
||
async function saveAll() {
|
||
if (!canManage("update") || !selectedModule.value || busy.value) return;
|
||
if (loadedModuleId.value == null) return;
|
||
if (String(loadedModuleId.value) !== String(selectedModuleId.value)) return;
|
||
const moduleId = String(selectedModule.value.b_id);
|
||
configSaving.value = true;
|
||
|
||
try {
|
||
const saveReqs = [];
|
||
|
||
const autoCodeError = validateAutoCode();
|
||
if (autoCodeError) {
|
||
message.warning(autoCodeError);
|
||
return;
|
||
}
|
||
|
||
// s_module 主表
|
||
const moduleData = drafts.value.module;
|
||
const moduleOriginal = originals.value.module;
|
||
const moduleCode = String(moduleData.b_code || "").trim();
|
||
if (!moduleCode) {
|
||
if (moduleData.b_module_type === "data") {
|
||
message.warning("请填写模块编码");
|
||
} else if (moduleData.b_module_type === "data_category") {
|
||
message.warning("请填写分类编码");
|
||
} else {
|
||
message.warning("请填写菜单编码");
|
||
}
|
||
return;
|
||
}
|
||
const codeError = validateModuleCode(moduleCode);
|
||
if (codeError) {
|
||
message.error(`编码格式不正确:${codeError}`);
|
||
return;
|
||
}
|
||
if (!String(moduleData.b_name || "").trim()) {
|
||
message.warning("请填写模块名称");
|
||
return;
|
||
}
|
||
if (moduleData.b_module_type === "page" && !String(moduleData.b_route || "").trim()) {
|
||
message.warning("请填写页面路由");
|
||
return;
|
||
}
|
||
if (moduleData.b_module_type === "data" && !String(moduleData.b_viewtable || "").trim()) {
|
||
message.warning("请填写查询视图");
|
||
return;
|
||
}
|
||
syncModuleItemI18nKeys(drafts.value.fields, "field", moduleData.b_code, "b_field");
|
||
syncModuleItemI18nKeys(drafts.value.powers, "power", moduleData.b_code, "b_code");
|
||
moduleData.b_i18n = buildModuleI18nKey(moduleData.b_module_type, moduleData.b_code);
|
||
const isNew = unsavedModuleIds.has(moduleId);
|
||
if (moduleData && (isNew || JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal))) {
|
||
const moduleRequest = {
|
||
table: "s_module",
|
||
key_field: "b_id",
|
||
};
|
||
if (isNew) {
|
||
moduleRequest.inserts = [moduleData];
|
||
} else {
|
||
moduleRequest.updates = [moduleData];
|
||
}
|
||
saveReqs.push(moduleRequest);
|
||
}
|
||
|
||
// 子表:fields, listConfig, editConfig, queryConfig, autoCode, powers
|
||
const subTables = [
|
||
["s_module_field", "fields"],
|
||
["s_module_field_list", "listConfig"],
|
||
["s_module_field_edit", "editConfig"],
|
||
["s_module_field_query", "queryConfig"],
|
||
["s_module_auto_code", "autoCode"],
|
||
["s_module_power", "powers"],
|
||
];
|
||
for (const [table, section] of subTables) {
|
||
const current = drafts.value[section] || [];
|
||
const previous = originals.value[section] || [];
|
||
const changes = diffRows(current, previous);
|
||
if (table === "s_module_auto_code") {
|
||
changes.updates = changes.updates.map((row) => {
|
||
const { b_current_period: _period, b_current_value: _value, ...config } = row;
|
||
return config;
|
||
});
|
||
}
|
||
const req = tableChange(table, changes);
|
||
if (hasChanges(req)) {
|
||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||
if (table === "s_module_power" && changes.deletes.length) {
|
||
saveReqs.push({
|
||
table: "b_user_power",
|
||
key_field: "b_power_id",
|
||
deletes: changes.deletes.map(({ b_id }) => ({ b_power_id: String(b_id) })),
|
||
});
|
||
}
|
||
saveReqs.push(req);
|
||
}
|
||
}
|
||
|
||
// 多语言:由各子面板收集变更,统一在此保存
|
||
if (!selectedModule.value || String(selectedModule.value.b_id) !== moduleId) return;
|
||
const i18nPanel = i18nPanelRef.value;
|
||
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === "function") {
|
||
// 保存时才按最终草稿态生成受管键(模块编码/字段/权限),避免编辑过程中实时生成。
|
||
i18nPanel.reconcileManagedRows(finalI18nItems.value);
|
||
}
|
||
if (i18nPanel && typeof i18nPanel.buildSaveRequest === "function") {
|
||
const i18nReq = await i18nPanel.buildSaveRequest();
|
||
if (i18nReq) saveReqs.push(i18nReq);
|
||
}
|
||
|
||
if (saveReqs.length === 0) {
|
||
message.info("没有需要保存的变更");
|
||
return;
|
||
}
|
||
|
||
await saveObjectApi(saveReqs);
|
||
|
||
// 更新 originals
|
||
originals.value = cloneData(drafts.value);
|
||
if (i18nPanelRef.value) {
|
||
i18nPanelRef.value.markSaved();
|
||
}
|
||
unsavedModuleIds.delete(moduleId);
|
||
updateSavedTreeNode(moduleData);
|
||
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
|
||
// 刷新权限数据,让菜单立即反映模块结构变更
|
||
permissionStore.load().catch(() => {});
|
||
message.success("保存成功");
|
||
} catch (e) {
|
||
message.error("保存失败:" + (e.message || e));
|
||
} finally {
|
||
configSaving.value = false;
|
||
}
|
||
}
|
||
|
||
// ---- 字段同步与默认配置 ----
|
||
function configuredFieldIds(section) {
|
||
const ids = new Set();
|
||
const originalRows = originals.value[section] || [];
|
||
const draftRows = drafts.value[section] || [];
|
||
for (const row of originalRows) {
|
||
if (row.b_field_id != null) ids.add(String(row.b_field_id));
|
||
}
|
||
for (const row of draftRows) {
|
||
if (row.b_field_id != null) ids.add(String(row.b_field_id));
|
||
}
|
||
return ids;
|
||
}
|
||
|
||
function buildMissingDefaultConfigs(module, fields) {
|
||
const moduleId = String(module.b_id);
|
||
const listFieldIds = configuredFieldIds("listConfig");
|
||
const editFieldIds = configuredFieldIds("editConfig");
|
||
const queryFieldIds = configuredFieldIds("queryConfig");
|
||
const generated = { listConfig: [], editConfig: [], queryConfig: [] };
|
||
|
||
let orderIndex = 0;
|
||
for (const field of fields) {
|
||
if (field.b_canuse !== 1) continue;
|
||
orderIndex++;
|
||
const fieldId = String(field.b_id);
|
||
const order = orderIndex * 10;
|
||
|
||
if (!listFieldIds.has(fieldId)) {
|
||
generated.listConfig.push({
|
||
b_id: createTempId(),
|
||
b_module_id: moduleId,
|
||
b_field_id: fieldId,
|
||
b_group_id: null,
|
||
b_title: null,
|
||
b_i18n: field.b_i18n || null,
|
||
b_visible: 1,
|
||
b_xh: order,
|
||
b_width: 100,
|
||
b_sortable: 0,
|
||
});
|
||
}
|
||
|
||
if (!editFieldIds.has(fieldId)) {
|
||
generated.editConfig.push({
|
||
b_id: createTempId(),
|
||
b_module_id: moduleId,
|
||
b_field_id: fieldId,
|
||
b_required: 0,
|
||
b_readonly: 0,
|
||
b_disabled: 0,
|
||
b_visible: 1,
|
||
b_group_id: null,
|
||
b_xh: order,
|
||
});
|
||
}
|
||
|
||
if (!queryFieldIds.has(fieldId)) {
|
||
generated.queryConfig.push({
|
||
b_id: createTempId(),
|
||
b_module_id: moduleId,
|
||
b_field_id: fieldId,
|
||
b_component: "input",
|
||
b_operator: "=",
|
||
b_default_value: null,
|
||
b_xh: order,
|
||
b_canuse: 1,
|
||
});
|
||
}
|
||
}
|
||
|
||
return generated;
|
||
}
|
||
|
||
async function syncFields() {
|
||
const module = selectedModule.value;
|
||
if (!module) return;
|
||
const view = String(module.b_viewtable || "").trim();
|
||
if (!view || busy.value) return;
|
||
const moduleId = String(module.b_id);
|
||
|
||
try {
|
||
configSyncing.value = true;
|
||
const res = await describeApi(view);
|
||
if (!selectedModule.value || String(selectedModule.value.b_id) !== moduleId) return;
|
||
const columns = res.data || [];
|
||
const existingFields = drafts.value.fields || [];
|
||
|
||
// 构建视图列名集合
|
||
const viewColumnSet = new Set();
|
||
for (const column of columns) {
|
||
viewColumnSet.add(column.name.toLowerCase());
|
||
}
|
||
|
||
// 已有字段映射(按字段名)
|
||
const existingMap = new Map();
|
||
for (const field of existingFields) {
|
||
if (!field.b_field) continue;
|
||
existingMap.set(field.b_field.toLowerCase(), field);
|
||
}
|
||
|
||
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: "input",
|
||
b_default_value: null,
|
||
b_options: null,
|
||
b_canuse: 1,
|
||
});
|
||
}
|
||
}
|
||
|
||
// 视图中不存在的已有字段:从字段定义中移除,并清理其在列表/编辑/查询配置中的残留行。
|
||
const removedFieldIds = new Set();
|
||
for (const existing of existingFields) {
|
||
const fieldName = existing.b_field ? existing.b_field.toLowerCase() : "";
|
||
if (!viewColumnSet.has(fieldName)) {
|
||
removedFieldIds.add(String(existing.b_id));
|
||
}
|
||
}
|
||
if (removedFieldIds.size > 0) {
|
||
for (const section of ["listConfig", "editConfig", "queryConfig"]) {
|
||
drafts.value[section] = (drafts.value[section] || []).filter(
|
||
(row) => !removedFieldIds.has(String(row.b_field_id)),
|
||
);
|
||
}
|
||
// 多语言面板中移除旧字段对应的受管翻译行(_managedId 格式为 field:<b_id>)
|
||
if (i18nPanelRef.value) {
|
||
const managedIds = new Set(Array.from(removedFieldIds).map((id) => `field:${id}`));
|
||
i18nPanelRef.value.removeByManagedIds(managedIds);
|
||
}
|
||
}
|
||
|
||
// 多语言键始终按 field.<模块代码>.<字段编码> 自动生成。
|
||
for (const field of mergedFields) {
|
||
field.b_i18n = buildModuleItemI18nKey("field", module.b_code, field.b_field);
|
||
}
|
||
|
||
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
|
||
const changes = diffRows(mergedFields, existingFields);
|
||
const fieldReq = tableChange("s_module_field", changes);
|
||
await allocateTemporaryIds([
|
||
fieldReq.inserts || [],
|
||
fieldReq.updates || [],
|
||
fieldReq.deletes || [],
|
||
]);
|
||
|
||
// 为"有 b_i18n 键"的字段生成多语言默认配置(b_i18n 草稿行),注入 i18n 面板,
|
||
// 由顶部"保存"统一落库;已存在的键由面板去重,不覆盖用户配置。
|
||
const candidateI18n = [];
|
||
for (const field of mergedFields) {
|
||
if (field.b_canuse !== 1 || !field.b_i18n) continue;
|
||
candidateI18n.push({
|
||
key: field.b_i18n,
|
||
value: field.b_name || field.b_field,
|
||
managedId: `field:${field.b_id}`,
|
||
});
|
||
}
|
||
let primaryLocale = "";
|
||
try {
|
||
const langRes = await loadDataApi(
|
||
"b_i18n_type",
|
||
"b_canuse = 1",
|
||
"b_default DESC, b_xh ASC, b_id ASC",
|
||
);
|
||
const langs = langRes.data || [];
|
||
if (langs.length) {
|
||
const defaultLanguage = langs.find((item) => Number(item.b_default) === 1);
|
||
primaryLocale = String(defaultLanguage?.b_code || langs[0].b_code || "");
|
||
}
|
||
} catch {
|
||
primaryLocale = "";
|
||
}
|
||
const i18nEntries = [];
|
||
if (primaryLocale) {
|
||
for (const item of candidateI18n) {
|
||
i18nEntries.push({
|
||
key: item.key,
|
||
locale: primaryLocale,
|
||
value: item.value,
|
||
managedId: item.managedId,
|
||
});
|
||
}
|
||
}
|
||
|
||
applyDisabledFieldDefaults(existingFields, mergedFields);
|
||
const generated = buildMissingDefaultConfigs(module, mergedFields);
|
||
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
|
||
let generatedCount = 0;
|
||
for (const section of generatedSections) {
|
||
generatedCount += generated[section].length;
|
||
}
|
||
|
||
if (!hasChanges(fieldReq) && !generatedCount && !i18nEntries.length) {
|
||
message.success("同步成功");
|
||
return;
|
||
}
|
||
|
||
// 多语言默认配置注入 i18n 面板草稿;字段已预分配真实 ID(见上方 allocateTemporaryIds),
|
||
// 列表/编辑/查询配置稍后追加到草稿。全部由顶部"保存"统一提交,期间不写库。
|
||
if (i18nEntries.length && i18nPanelRef.value) {
|
||
i18nPanelRef.value.mergeDefaults(i18nEntries);
|
||
}
|
||
|
||
for (const section of generatedSections) {
|
||
if (!generated[section].length) continue;
|
||
drafts.value[section] = [...(drafts.value[section] || []), ...cloneData(generated[section])];
|
||
}
|
||
|
||
drafts.value.fields = cloneData(mergedFields);
|
||
|
||
message.success("同步成功");
|
||
} catch (e) {
|
||
message.error("字段同步失败:" + (e.message || e));
|
||
} finally {
|
||
configSyncing.value = false;
|
||
}
|
||
}
|
||
|
||
// ---- 初始化 ----
|
||
void permissionStore
|
||
.load()
|
||
.then(loadTree)
|
||
.catch((e) => message.error("模块树加载失败:" + (e.message || e)));
|
||
</script>
|
||
|
||
<template>
|
||
<div class="mm-page">
|
||
<!-- 左侧模块树 -->
|
||
<aside class="mm-sidebar" :style="{ width: leftPanelWidth + 'px' }">
|
||
<div class="mm-tree-search">
|
||
<a-input-search
|
||
v-model:value="treeSearch"
|
||
allow-clear
|
||
/>
|
||
</div>
|
||
<div class="mm-tree-stage">
|
||
<div v-if="treeLoading" class="mm-tree-empty">
|
||
<a-spin />
|
||
</div>
|
||
<div v-else class="mm-tree-scroll">
|
||
<FmsTree
|
||
:disabled="configSaving"
|
||
:tree-data="filteredTreeData"
|
||
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>{{
|
||
appStore.t(node.b_i18n, node.b_name || node.b_code || node.title)
|
||
}}</span>
|
||
</span>
|
||
</template>
|
||
</FmsTree>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<div class="mm-resize-handle" @mousedown="onResizeStart" />
|
||
<!-- 右侧详情 -->
|
||
<main class="mm-main">
|
||
<FmsLoadingOverlay :loading="configLoading">
|
||
<template v-if="selectedModule">
|
||
<!-- 顶部栏:tabs + 开关 + 操作 -->
|
||
<div class="mm-tab-bar">
|
||
<nav class="mm-tab-nav">
|
||
<button
|
||
v-for="tab in visibleTabs"
|
||
:key="tab.key"
|
||
type="button"
|
||
class="mm-tab-nav__item"
|
||
:class="{ active: activeTab === tab.key }"
|
||
@click="activeTab = tab.key"
|
||
>
|
||
{{ tab.tab }}
|
||
</button>
|
||
</nav>
|
||
<div class="mm-tab-bar__right">
|
||
<label class="mm-switch"><span>启用</span><a-checkbox
|
||
:checked="drafts.module?.b_canuse === 1"
|
||
:disabled="busy"
|
||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||
/></label>
|
||
<template v-if="drafts.module?.b_module_type !== 'data'">
|
||
<label class="mm-switch"><span>显示菜单</span><a-checkbox
|
||
:checked="drafts.module?.b_canmenu === 1"
|
||
:disabled="busy || ['data', 'data_category'].includes(drafts.module?.b_module_type)"
|
||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||
/></label>
|
||
</template>
|
||
<a-button
|
||
v-if="canManage('update')"
|
||
type="primary"
|
||
:loading="configSaving"
|
||
:disabled="configLoading || configSyncing"
|
||
@click="saveAll"
|
||
>
|
||
保存
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 面板内容 -->
|
||
<div class="mm-body">
|
||
<ModuleBasicPanel
|
||
v-if="activeTab === 'basic'"
|
||
:module-id="String(selectedModule.b_id)"
|
||
:data="drafts.module"
|
||
:parent-name="parentModuleName"
|
||
:loading="busy"
|
||
@update="(v) => onUpdate('module', v)"
|
||
@i18n-saved="onI18nSaved"
|
||
/>
|
||
<ModuleFieldsPanel
|
||
v-else-if="activeTab === 'fields'"
|
||
:module-id="String(selectedModule.b_id)"
|
||
:module-code="selectedModule?.b_code || ''"
|
||
:fields="drafts.fields"
|
||
:loading="busy"
|
||
@update="onFieldsUpdate"
|
||
@i18n-saved="onI18nSaved"
|
||
@sync-fields="syncFields"
|
||
/>
|
||
<ModuleListConfigPanel
|
||
v-else-if="activeTab === '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="activeTab === '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="activeTab === 'query'"
|
||
:module-id="String(selectedModule.b_id)"
|
||
:query-config="drafts.queryConfig"
|
||
:fields="drafts.fields"
|
||
:loading="busy"
|
||
@update="(v) => onUpdate('queryConfig', v)"
|
||
/>
|
||
<ModuleAutoCodePanel
|
||
v-else-if="activeTab === 'auto-code'"
|
||
:module-id="String(selectedModule.b_id)"
|
||
:auto-code="drafts.autoCode"
|
||
:fields="drafts.fields"
|
||
:loading="busy"
|
||
@update="onAutoCodeUpdate"
|
||
/>
|
||
<ModulePowersPanel
|
||
v-else-if="activeTab === 'powers'"
|
||
:module-id="String(selectedModule.b_id)"
|
||
:module-code="selectedModule?.b_code || ''"
|
||
:powers="drafts.powers"
|
||
:loading="busy"
|
||
@update="(v) => onUpdate('powers', v)"
|
||
@i18n-saved="onI18nSaved"
|
||
/>
|
||
<ModuleI18nPanel
|
||
v-else-if="activeTab === 'i18n'"
|
||
:ref="setI18nPanelRef"
|
||
:module-id="String(selectedModule.b_id)"
|
||
:i18n-items="i18nItems"
|
||
:loading="busy"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 未选中模块 -->
|
||
<div v-else class="mm-empty">
|
||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||
</div>
|
||
</FmsLoadingOverlay>
|
||
</main>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.mm-page {
|
||
display: flex;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
background: #ffffff;
|
||
}
|
||
|
||
/* 拖拽手柄 — 抄 AppSidebar 的 sidebar-resize-handle */
|
||
.mm-resize-handle {
|
||
flex: none;
|
||
width: 4px;
|
||
cursor: col-resize;
|
||
background: transparent;
|
||
transition: background-color 0.15s ease;
|
||
|
||
&:hover {
|
||
background: rgb(0 0 0 / 6%);
|
||
}
|
||
}
|
||
|
||
.mm-sidebar {
|
||
display: flex;
|
||
flex: none;
|
||
height: 100%;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
flex-direction: column;
|
||
background: var(--fms-surface);
|
||
}
|
||
|
||
.mm-tree-search {
|
||
flex: none;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.mm-tree-stage {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
flex-direction: column;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.mm-tree-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
|
||
.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: 1;
|
||
height: 100%;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: var(--fms-surface);
|
||
}
|
||
|
||
/* ---- 顶部栏:tabs + 开关 + 操作 ---- */
|
||
.mm-tab-bar {
|
||
display: flex;
|
||
flex: none;
|
||
align-items: center;
|
||
height: 44px;
|
||
padding: 0 20px;
|
||
|
||
&__right {
|
||
display: flex;
|
||
flex: none;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.mm-switch {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
color: #71717a;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* ---- 自定义 tab 导航 ---- */
|
||
.mm-tab-nav {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
align-items: center;
|
||
gap: 2px;
|
||
|
||
&__item {
|
||
display: inline-flex;
|
||
height: 32px;
|
||
align-items: center;
|
||
padding: 0 12px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: #71717a;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition:
|
||
background-color 0.15s ease,
|
||
color 0.15s ease;
|
||
|
||
&:hover {
|
||
background: #f4f4f5;
|
||
color: #3f3f46;
|
||
}
|
||
|
||
&.active {
|
||
background: #f4f4f5;
|
||
color: #0a0a0a;
|
||
font-weight: 500;
|
||
|
||
&:hover {
|
||
background: #ebebeb;
|
||
color: #0a0a0a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.mm-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
padding: 16px 20px 0;
|
||
}
|
||
|
||
.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>
|