20260728225443

This commit is contained in:
oneao committed 2026-07-28 22:54:43 +08:00
1 parent 5ec7aadef9
commit f6b79ea09a
30 files changed
+1541 -1120

No files matched your search

@@ -4,6 +4,7 @@ import { message, Modal } from "antdv-next";
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 {
cloneData,
diffRows,
@@ -30,6 +31,7 @@ import ModulePowersPanel from "./components/ModulePowersPanel.vue";
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
// ---- 状态 ----
const appStore = useAppStore();
const treeData = ref([]);
const treeLoading = ref(false);
const expandedTreeKeys = ref([]);
@@ -80,8 +82,6 @@ const configSaving = ref(false);
const configSyncing = ref(false);
const i18nPanelRef = ref(null);
const loadedModuleId = ref(null);
let configRequestId = 0;
let operationId = 0;
// 使用函数 ref,避免组件位于 TabPane 的 v-for 内时被 Vue 收集成实例数组。
function setI18nPanelRef(instance) {
@@ -94,10 +94,6 @@ const busy = computed(() => configLoading.value || configSaving.value || configS
const syncDisabled = computed(
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
);
const isCurrentModule = computed(
() =>
loadedModuleId.value != null && String(loadedModuleId.value) === String(selectedModuleId.value),
);
// 构建受管多语言项。managedSource 提供模块/字段/权限的受管键来源,
// plainSource 提供分组/列表标题等非受管键来源(手动填写,需即时反馈)。
function buildI18nItems(managedSource, plainSource) {
@@ -245,64 +241,70 @@ async function loadTree() {
}
// ---- 模块配置加载 ----
async function loadModuleConfig(moduleId, requestId) {
async function loadModuleConfig(moduleId) {
configLoading.value = true;
try {
const id = String(moduleId);
const moduleType = findNode(id)?.b_module_type;
const requests = [["module", loadDataApi("s_module", `b_id = ${id}`)]];
let moduleResponse;
let fieldsResponse;
let groupsResponse;
let listResponse;
let editResponse;
let queryResponse;
let powersResponse;
if (moduleType === "data") {
requests.push(
["fields", loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC")],
[
"groups",
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
],
[
"listConfig",
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
],
[
"editConfig",
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
],
[
"queryConfig",
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
],
);
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"),
]);
moduleResponse = responses[0];
fieldsResponse = responses[1];
groupsResponse = responses[2];
listResponse = responses[3];
editResponse = responses[4];
queryResponse = responses[5];
} else if (moduleType === "page") {
requests.push([
"powers",
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}`);
}
const responses = Object.fromEntries(
await Promise.all(requests.map(async ([key, request]) => [key, await request])),
);
if (String(selectedModuleId.value) !== id) return;
const original = {
module: responses.module.data?.[0] || null,
fields: responses.fields?.data || [],
groups: responses.groups?.data || [],
listConfig: responses.listConfig?.data || [],
editConfig: responses.editConfig?.data || [],
queryConfig: responses.queryConfig?.data || [],
powers: responses.powers?.data || [],
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 || [] : [],
powers: powersResponse ? powersResponse.data || [] : [],
};
if (requestId !== configRequestId) return;
originals.value = original;
drafts.value = cloneData(original);
loadedModuleId.value = id;
activeTab.value = "basic";
} catch (e) {
if (requestId === configRequestId) {
if (String(selectedModuleId.value) === String(moduleId)) {
loadedModuleId.value = null;
drafts.value.module = null;
message.error("模块配置加载失败:" + (e.message || e));
}
} finally {
if (requestId === configRequestId) configLoading.value = false;
if (String(selectedModuleId.value) === String(moduleId)) {
configLoading.value = false;
}
}
}
@@ -315,8 +317,6 @@ function onTreeSelect(keys) {
}
watch(selectedModuleId, (id) => {
const requestId = ++configRequestId;
operationId++;
if (!id) {
configLoading.value = false;
loadedModuleId.value = null;
@@ -329,7 +329,7 @@ watch(selectedModuleId, (id) => {
return;
}
loadedModuleId.value = null;
void loadModuleConfig(id, requestId);
void loadModuleConfig(id);
});
// ---- 子面板数据变更 ----
@@ -345,7 +345,7 @@ function onSetField(field, value) {
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
function onGroupsSaved(newGroups, idMapping) {
if (idMapping?.size) {
if (idMapping && idMapping.size) {
remapForeignKeys(drafts.value.listConfig || [], ["b_group_id"], idMapping);
remapForeignKeys(drafts.value.editConfig || [], ["b_group_id"], idMapping);
}
@@ -354,7 +354,9 @@ function onGroupsSaved(newGroups, idMapping) {
}
function onI18nSaved(key) {
void i18nPanelRef.value?.refreshKey?.(key);
if (i18nPanelRef.value) {
void i18nPanelRef.value.refreshKey(key);
}
}
// ---- 新增模块 ----
@@ -442,7 +444,7 @@ function findNode(key) {
function hasChildNode(moduleId) {
const node = findNode(moduleId);
return !!node?.children?.length;
return Boolean(node && node.children && node.children.length);
}
async function onDeleteNodeById(moduleId) {
@@ -486,9 +488,10 @@ async function onDeleteNodeById(moduleId) {
// ---- 统一保存 ----
async function saveAll() {
if (!selectedModule.value || !isCurrentModule.value || busy.value) return;
if (!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);
const currentOperationId = ++operationId;
configSaving.value = true;
try {
@@ -497,9 +500,13 @@ async function saveAll() {
// s_module 主表
const moduleData = drafts.value.module;
const moduleOriginal = originals.value.module;
const moduleCode = String(moduleData?.b_code || "").trim();
const moduleCode = String(moduleData.b_code || "").trim();
if (!moduleCode) {
message.warning(`请填写${moduleData?.b_module_type === "data" ? "模块编码" : "菜单编码"}`);
if (moduleData.b_module_type === "data") {
message.warning("请填写模块编码");
} else {
message.warning("请填写菜单编码");
}
return;
}
const codeError = validateModuleCode(moduleCode);
@@ -507,7 +514,7 @@ async function saveAll() {
message.error(`编码格式不正确:${codeError}`);
return;
}
if (!String(moduleData?.b_name || "").trim()) {
if (!String(moduleData.b_name || "").trim()) {
message.warning("请填写模块名称");
return;
}
@@ -522,13 +529,18 @@ async function saveAll() {
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 = !moduleOriginal?.b_code;
const isNew = !moduleOriginal || !moduleOriginal.b_code;
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
saveReqs.push({
const moduleRequest = {
table: "s_module",
key_field: "b_id",
[isNew ? "inserts" : "updates"]: [moduleData],
});
};
if (isNew) {
moduleRequest.inserts = [moduleData];
} else {
moduleRequest.updates = [moduleData];
}
saveReqs.push(moduleRequest);
}
// 子表:fields, listConfig, editConfig, queryConfig, powers
@@ -558,14 +570,13 @@ async function saveAll() {
}
// 多语言:由各子面板收集变更,统一在此保存
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
return;
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 (typeof i18nPanel?.buildSaveRequest === "function") {
if (i18nPanel && typeof i18nPanel.buildSaveRequest === "function") {
const i18nReq = await i18nPanel.buildSaveRequest();
if (i18nReq) saveReqs.push(i18nReq);
}
@@ -579,8 +590,11 @@ async function saveAll() {
// 更新 originals
originals.value = cloneData(drafts.value);
i18nPanelRef.value?.markSaved?.();
if (i18nPanelRef.value) {
i18nPanelRef.value.markSaved();
}
updateSavedTreeNode(moduleData);
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
message.success("保存成功");
} catch (e) {
message.error("保存失败:" + (e.message || e));
@@ -591,12 +605,16 @@ async function saveAll() {
// ---- 字段同步与默认配置 ----
function configuredFieldIds(section) {
return new Set(
[...(originals.value[section] || []), ...(drafts.value[section] || [])]
.map((row) => row.b_field_id)
.filter((id) => id != null)
.map(String),
);
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) {
@@ -606,77 +624,84 @@ function buildMissingDefaultConfigs(module, fields) {
const queryFieldIds = configuredFieldIds("queryConfig");
const generated = { listConfig: [], editConfig: [], queryConfig: [] };
fields
.filter((field) => field.b_canuse === 1)
.forEach((field, index) => {
const fieldId = String(field.b_id);
const order = (index + 1) * 10;
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 (!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_group_id: null,
b_xh: order,
});
}
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_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,
});
}
});
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;
const view = String(module?.b_viewtable || "").trim();
if (!module) return;
const view = String(module.b_viewtable || "").trim();
if (!view || busy.value) return;
const moduleId = String(module.b_id);
const currentOperationId = ++operationId;
try {
configSyncing.value = true;
const res = await describeApi(view);
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
return;
if (!selectedModule.value || String(selectedModule.value.b_id) !== moduleId) return;
const columns = res.data || [];
const existingFields = drafts.value.fields || [];
// 构建视图列名集合
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
const viewColumnSet = new Set();
for (const column of columns) {
viewColumnSet.add(column.name.toLowerCase());
}
// 已有字段映射(按字段名)
const existingMap = new Map(existingFields.map((f) => [f.b_field?.toLowerCase(), f]));
const existingMap = new Map();
for (const field of existingFields) {
if (!field.b_field) continue;
existingMap.set(field.b_field.toLowerCase(), field);
}
const mergedFields = [];
@@ -704,15 +729,16 @@ async function syncFields() {
// 视图中不存在的已有字段标记为不可用
for (const existing of existingFields) {
if (!viewColumnSet.has(existing.b_field?.toLowerCase())) {
const fieldName = existing.b_field ? existing.b_field.toLowerCase() : "";
if (!viewColumnSet.has(fieldName)) {
mergedFields.push({ ...existing, b_canuse: 0 });
}
}
// 多语言键始终按 field.<模块代码>.<字段编码> 自动生成。
mergedFields.forEach((field) => {
for (const field of mergedFields) {
field.b_i18n = buildModuleItemI18nKey("field", module.b_code, field.b_field);
});
}
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
const changes = diffRows(mergedFields, existingFields);
@@ -725,38 +751,48 @@ async function syncFields() {
// 为"有 b_i18n 键"的字段生成多语言默认配置(b_i18n 草稿行),注入 i18n 面板,
// 由顶部"保存"统一落库;已存在的键由面板去重,不覆盖用户配置。
const candidateI18n = mergedFields
.filter((f) => f.b_canuse === 1 && f.b_i18n)
.map((f) => ({
key: f.b_i18n,
value: f.b_name || f.b_field,
managedId: `field:${f.b_id}`,
}));
let primaryLocale = "zh-CN";
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_xh ASC, b_id ASC");
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) {
// 默认字段名翻译写到"简体中文"(zh-CN),找不到再退回排序第一的语言
const zh = langs.find((l) => l.b_code === "zh-CN");
primaryLocale = (zh || langs[0]).b_code;
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,
});
}
} catch (e) {
// 取不到语言类型时回退默认值
}
const i18nEntries = candidateI18n.map((e) => ({
key: e.key,
locale: primaryLocale,
value: e.value,
managedId: e.managedId,
}));
const generated = buildMissingDefaultConfigs(module, mergedFields);
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
const generatedCount = generatedSections.reduce(
(total, section) => total + generated[section].length,
0,
);
let generatedCount = 0;
for (const section of generatedSections) {
generatedCount += generated[section].length;
}
if (!hasChanges(fieldReq) && !generatedCount && !i18nEntries.length) {
message.success("同步成功");
@@ -765,8 +801,8 @@ async function syncFields() {
// 多语言默认配置注入 i18n 面板草稿;字段已预分配真实 ID(见上方 allocateTemporaryIds),
// 列表/编辑/查询配置稍后追加到草稿。全部由顶部"保存"统一提交,期间不写库。
if (i18nEntries.length) {
i18nPanelRef.value?.mergeDefaults?.(i18nEntries);
if (i18nEntries.length && i18nPanelRef.value) {
i18nPanelRef.value.mergeDefaults(i18nEntries);
}
for (const section of generatedSections) {
@@ -850,14 +886,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
<div class="mm-header-center">
<a-space size="middle">
<span>启用</span>
<a-switch
<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-switch
<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)"
@@ -891,6 +927,7 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:fields="drafts.fields"
:loading="busy"
@update="(v) => onUpdate('fields', v)"
@i18n-saved="onI18nSaved"
/>
<ModuleListConfigPanel
v-else-if="tab.key === 'list'"