20260730231110

This commit is contained in:
oneao committed 2026-07-30 23:11:10 +08:00
1 parent 4929b78434
commit 318fa0d9e1
23 files changed
+4026 -506

No files matched your search

@@ -1,5 +1,5 @@
<script setup>
import { computed, ref, watch } from "vue";
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { message, Modal } from "antdv-next";
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
import FmsTree from "@/components/FmsTree.vue";
@@ -40,6 +40,7 @@ const selectedModuleId = ref(null);
const skipLoad = ref(false);
const expandTimer = ref(null);
const activeTab = ref("basic");
let expandDelayResolve = null;
function buildTreeContextMenu(node) {
const items = [];
if (!node) return { items };
@@ -329,7 +330,8 @@ async function onTreeDrop(info) {
}
return true;
}
if (nodes[i].children && insertIntoTree(nodes[i].children, dropNodeKey, nodes[i])) return true;
if (nodes[i].children && insertIntoTree(nodes[i].children, dropNodeKey, nodes[i]))
return true;
}
return false;
}
@@ -371,10 +373,23 @@ async function onTreeDrop(info) {
}
// ---- 模块配置加载 ----
async function loadModuleConfig(moduleId) {
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 id = String(moduleId);
const moduleType = findNode(id)?.b_module_type;
let moduleResponse;
let fieldsResponse;
@@ -413,6 +428,8 @@ async function loadModuleConfig(moduleId) {
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 = {
@@ -448,6 +465,23 @@ function clearExpandTimer() {
clearTimeout(expandTimer.value);
expandTimer.value = null;
}
if (expandDelayResolve) {
expandDelayResolve(false);
expandDelayResolve = null;
}
}
function waitForExpandAnimation() {
return new Promise((resolve) => {
expandDelayResolve = (ready) => {
resolve(ready);
};
expandTimer.value = setTimeout(() => {
expandTimer.value = null;
expandDelayResolve = null;
resolve(true);
}, 300);
});
}
function onTreeSelect(keys, info) {
@@ -481,15 +515,15 @@ function onTreeSelect(keys, info) {
expandedTreeKeys.value = expandedKeys.filter((item) => item !== key);
selectedModuleId.value = key;
} else {
// 展开:立即高亮选中节点,动画完毕后再加载数据
// 展开:立即高亮,下一帧发请求;结果回写最多等到展开动画结束。
expandedTreeKeys.value = [...expandedKeys, key];
skipLoad.value = true;
selectedModuleId.value = key; // 立即高亮,watch 中 skipLoad=true 跳过加载
expandTimer.value = setTimeout(() => {
expandTimer.value = null;
loadedModuleId.value = null;
void loadModuleConfig(key);
}, 300);
selectedModuleId.value = key;
loadedModuleId.value = null;
void loadModuleConfig(key, {
waitForPaint: true,
applyReady: waitForExpandAnimation(),
});
}
}
@@ -1056,9 +1090,7 @@ async function syncFields() {
}
// 多语言面板中移除旧字段对应的受管翻译行(_managedId 格式为 field:<b_id>)
if (i18nPanelRef.value) {
const managedIds = new Set(
Array.from(removedFieldIds).map((id) => `field:${id}`),
);
const managedIds = new Set(Array.from(removedFieldIds).map((id) => `field:${id}`));
i18nPanelRef.value.removeByManagedIds(managedIds);
}
}
@@ -1151,192 +1183,196 @@ async function syncFields() {
// ---- 初始化 ----
void loadTree().catch((e) => message.error("模块树加载失败:" + (e.message || e)));
onBeforeUnmount(clearExpandTimer);
</script>
<template>
<div class="mm-page">
<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"
@select="onTreeSelect"
@menu-click="onTreeMenuClick"
>
<template #title="node">
<span class="mm-tree-node">
<span>{{ node.title }}</span>
</span>
</template>
</FmsTree>
</div>
</aside>
</a-splitter-panel>
<a-splitter-panel :min="720">
<!-- 右侧详情 -->
<main class="mm-main">
<FmsLoadingOverlay :loading="configLoading">
<template v-if="selectedModule">
<!-- 页面头部 -->
<div class="mm-header">
<a-space>
<a-button
type="primary"
:loading="configSaving"
:disabled="configLoading || configSyncing"
@click="saveAll"
>保存</a-button
>
<a-button
v-if="selectedModule.b_module_type === 'data'"
:loading="configSyncing"
:disabled="syncDisabled"
@click="syncFields"
>
同步字段
</a-button>
</a-space>
<div class="mm-header-center">
<a-space size="middle">
<span>启用</span>
<a-checkbox
:checked="drafts.module?.b_canuse === 1"
:disabled="busy"
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
/>
<a-divider type="vertical" />
<span>显示菜单</span>
<a-checkbox
:checked="drafts.module?.b_canmenu === 1"
:disabled="busy || drafts.module?.b_module_type === 'data'"
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
/>
<template v-if="drafts.module?.b_module_type === 'data'">
<a-divider type="vertical" />
<span>自动编码</span>
<a-checkbox
:checked="autoCodeEnabled"
:disabled="busy"
@update:checked="setAutoCodeEnabled"
/>
</template>
</a-space>
<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"
@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>
</aside>
</a-splitter-panel>
<!-- 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"
:force-render="tab.key === 'i18n'"
>
<ModuleBasicPanel
v-if="tab.key === '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="tab.key === 'fields'"
:module-id="String(selectedModule.b_id)"
:module-code="selectedModule?.b_code || ''"
:fields="drafts.fields"
:loading="busy"
@update="onFieldsUpdate"
@i18n-saved="onI18nSaved"
/>
<ModuleListConfigPanel
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'"
:module-id="String(selectedModule.b_id)"
:query-config="drafts.queryConfig"
:fields="drafts.fields"
:loading="busy"
@update="(v) => onUpdate('queryConfig', v)"
/>
<ModuleAutoCodePanel
v-else-if="tab.key === 'auto-code'"
:module-id="String(selectedModule.b_id)"
:auto-code="drafts.autoCode"
:fields="drafts.fields"
:loading="busy"
@update="onAutoCodeUpdate"
/>
<ModulePowersPanel
v-else-if="tab.key === '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="tab.key === 'i18n'"
:ref="setI18nPanelRef"
:module-id="String(selectedModule.b_id)"
:i18n-items="i18nItems"
:loading="busy"
/>
</a-tab-pane>
</a-tabs>
</div>
</template>
<a-splitter-panel :min="720">
<!-- 右侧详情 -->
<main class="mm-main">
<FmsLoadingOverlay :loading="configLoading">
<template v-if="selectedModule">
<!-- 页面头部 -->
<div class="mm-header">
<a-space>
<a-button
type="primary"
:loading="configSaving"
:disabled="configLoading || configSyncing"
@click="saveAll"
>保存</a-button
>
<a-button
v-if="selectedModule.b_module_type === 'data'"
:loading="configSyncing"
:disabled="syncDisabled"
@click="syncFields"
>
同步字段
</a-button>
</a-space>
<div class="mm-header-center">
<a-space size="middle">
<span>启用</span>
<a-checkbox
:checked="drafts.module?.b_canuse === 1"
:disabled="busy"
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
/>
<a-divider type="vertical" />
<span>显示菜单</span>
<a-checkbox
:checked="drafts.module?.b_canmenu === 1"
:disabled="busy || drafts.module?.b_module_type === 'data'"
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
/>
<template v-if="drafts.module?.b_module_type === 'data'">
<a-divider type="vertical" />
<span>自动编码</span>
<a-checkbox
:checked="autoCodeEnabled"
:disabled="busy"
@update:checked="setAutoCodeEnabled"
/>
</template>
</a-space>
</div>
</div>
<!-- 未选中模块 -->
<div v-else class="mm-empty">
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
</div>
</FmsLoadingOverlay>
</main>
</a-splitter-panel>
<!-- 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"
:force-render="tab.key === 'i18n'"
>
<ModuleBasicPanel
v-if="tab.key === '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="tab.key === 'fields'"
:module-id="String(selectedModule.b_id)"
:module-code="selectedModule?.b_code || ''"
:fields="drafts.fields"
:loading="busy"
@update="onFieldsUpdate"
@i18n-saved="onI18nSaved"
/>
<ModuleListConfigPanel
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'"
:module-id="String(selectedModule.b_id)"
:query-config="drafts.queryConfig"
:fields="drafts.fields"
:loading="busy"
@update="(v) => onUpdate('queryConfig', v)"
/>
<ModuleAutoCodePanel
v-else-if="tab.key === 'auto-code'"
:module-id="String(selectedModule.b_id)"
:auto-code="drafts.autoCode"
:fields="drafts.fields"
:loading="busy"
@update="onAutoCodeUpdate"
/>
<ModulePowersPanel
v-else-if="tab.key === '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="tab.key === 'i18n'"
:ref="setI18nPanelRef"
:module-id="String(selectedModule.b_id)"
:i18n-items="i18nItems"
:loading="busy"
/>
</a-tab-pane>
</a-tabs>
</div>
</template>
<!-- 未选中模块 -->
<div v-else class="mm-empty">
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
</div>
</FmsLoadingOverlay>
</main>
</a-splitter-panel>
</a-splitter>
</div>
</template>