20260727173703

This commit is contained in:
oneao committed 2026-07-27 17:37:03 +08:00
1 parent a8478d60ed
commit 9dbaef7787
114 files changed
+959 -2588

No files matched your search

@@ -17,9 +17,10 @@ const props = defineProps({
visible: { type: Boolean, default: false },
moduleId: { type: String, required: true },
groups: { type: Array, default: () => [] },
referencedGroupIds: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:visible", "update"]);
const emit = defineEmits(["update:visible", "saved"]);
const localGroups = ref([]);
const selectedGroupId = ref(null);
@@ -81,7 +82,7 @@ function addGroup(parentId) {
b_parent_id: parentId || null,
b_title: "",
b_i18n: null,
b_xh: localGroups.value.length * 10,
b_xh: (localGroups.value.length + 1) * 10,
b_canuse: 1,
};
localGroups.value.push(newGroup);
@@ -90,6 +91,17 @@ function addGroup(parentId) {
function deleteGroup() {
if (!selectedGroupId.value) return;
const hasChildren = localGroups.value.some(
(group) => String(group.b_parent_id) === selectedGroupId.value,
);
if (hasChildren) {
message.warning("该分组下还有子分组,请先删除子分组");
return;
}
if (props.referencedGroupIds.map(String).includes(selectedGroupId.value)) {
message.warning("该分组仍被列表或编辑配置引用,请先清空引用");
return;
}
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== selectedGroupId.value);
selectedGroupId.value = null;
}
@@ -114,7 +126,7 @@ async function saveAllGroups() {
]);
remapForeignKeys(localGroups.value, ["b_parent_id"], idMapping);
await saveObjectApi([req]);
emit("update", cloneData(localGroups.value));
emit("saved", cloneData(localGroups.value), idMapping);
message.success("分组已保存");
emit("update:visible", false);
} catch (e) {
@@ -10,7 +10,7 @@ const props = defineProps({
parentName: { type: String, default: "" },
});
const emit = defineEmits(["update"]);
const emit = defineEmits(["update", "i18nSaved"]);
const formData = ref(null);
@@ -23,7 +23,6 @@ watch(
{ immediate: true },
);
const isDirectory = computed(() => formData.value?.b_module_type === "directory");
const isPage = computed(() => formData.value?.b_module_type === "page");
const isData = computed(() => formData.value?.b_module_type === "data");
@@ -56,6 +55,7 @@ function openI18nModal() {
function onI18nSaved(key) {
setField("b_i18n", key);
emit("i18nSaved", key);
}
</script>
@@ -65,21 +65,13 @@ function onI18nSaved(key) {
<a-form layout="vertical" class="basic-form" :disabled="loading">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="模块编码" required>
<a-input
:value="formData.b_code"
placeholder="如 m_sea"
@update:value="onCodeInput"
/>
<a-form-item :label="isData ? '模块编码' : '菜单编码'" required>
<a-input :value="formData.b_code" @update:value="onCodeInput" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="模块名称" required>
<a-input
:value="formData.b_name"
placeholder="模块显示名称"
@update:value="(v) => setField('b_name', v)"
/>
<a-input :value="formData.b_name" @update:value="(v) => setField('b_name', v)" />
</a-form-item>
</a-col>
<a-col :span="8">
@@ -100,18 +92,15 @@ function onI18nSaved(key) {
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="上级模块">
<a-input :value="parentName" readonly placeholder="通过左侧树选中后自动设置" />
<a-input :value="parentName" readonly />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="多语言键">
<div class="i18n-key-row">
<a-input :value="formData.b_i18n" readonly placeholder="填写模块编码后自动生成" />
<a-input :value="formData.b_i18n" readonly />
<a-button
class="i18n-key-btn"
type="text"
@@ -123,7 +112,7 @@ function onI18nSaved(key) {
</div>
</a-form-item>
</a-col>
<a-col :span="4">
<a-col :span="8">
<a-form-item label="排序号">
<a-input-number
:value="formData.b_xh"
@@ -132,60 +121,44 @@ function onI18nSaved(key) {
/>
</a-form-item>
</a-col>
<a-col v-if="isPage" :span="8">
<a-form-item label="页面路由" required>
<a-input :value="formData.b_route" @update:value="(v) => setField('b_route', v)" />
</a-form-item>
</a-col>
<a-col v-if="isData" :span="8">
<a-form-item label="查询视图" required>
<a-input
:value="formData.b_viewtable"
@update:value="(v) => setField('b_viewtable', v)"
/>
</a-form-item>
</a-col>
<a-col v-if="isData" :span="8">
<a-form-item label="保存表">
<a-input
:value="formData.b_savetable"
@update:value="(v) => setField('b_savetable', v)"
/>
</a-form-item>
</a-col>
<a-col v-if="isData" :span="8">
<a-form-item label="主键字段">
<a-input
:value="formData.b_keyfield"
@update:value="(v) => setField('b_keyfield', v)"
/>
</a-form-item>
</a-col>
<a-col v-if="isData" :span="8">
<a-form-item label="默认排序">
<a-input
:value="formData.b_orderfield"
@update:value="(v) => setField('b_orderfield', v)"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 页面/数据 专用字段 -->
<template v-if="!isDirectory">
<a-row :gutter="16">
<a-col :span="8" v-if="isPage">
<a-form-item label="页面路由" required>
<a-input
:value="formData.b_route"
placeholder="如 /sea/list"
@update:value="(v) => setField('b_route', v)"
/>
</a-form-item>
</a-col>
<a-col :span="8" v-if="isData">
<a-form-item label="查询视图" required>
<a-input
:value="formData.b_viewtable"
placeholder="如 v_sea"
@update:value="(v) => setField('b_viewtable', v)"
/>
</a-form-item>
</a-col>
<a-col :span="8" v-if="isData">
<a-form-item label="保存表">
<a-input
:value="formData.b_savetable"
placeholder="如 d_sea"
@update:value="(v) => setField('b_savetable', v)"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16" v-if="isData">
<a-col :span="8">
<a-form-item label="主键字段">
<a-input
:value="formData.b_keyfield"
placeholder="如 b_id"
@update:value="(v) => setField('b_keyfield', v)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="默认排序">
<a-input
:value="formData.b_orderfield"
placeholder="如 b_xh ASC"
@update:value="(v) => setField('b_orderfield', v)"
/>
</a-form-item>
</a-col>
</a-row>
</template>
</a-form>
<I18nQuickSetModal ref="i18nModalRef" @saved="onI18nSaved" />
@@ -8,10 +8,11 @@ const props = defineProps({
editConfig: { type: Array, default: () => [] },
fields: { type: Array, default: () => [] },
groups: { type: Array, default: () => [] },
referencedGroupIds: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["update", "updateGroups"]);
const emit = defineEmits(["update", "groupsSaved"]);
const localEdit = ref([]);
const groupManagerVisible = ref(false);
@@ -62,7 +63,7 @@ function makeSwitchCol(field, title, width) {
title,
width: width || 60,
align: "center",
editor: { type: "checkbox", onChange: () => emitUpdate() },
editor: { type: "checkbox" },
};
}
@@ -74,7 +75,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: () => availableFields.value,
onChange: () => emitUpdate(),
},
},
makeSwitchCol("b_required", "必填"),
@@ -87,7 +87,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: () => groupOptions.value,
onChange: () => emitUpdate(),
},
},
{ dataIndex: "b_xh", title: "排序号", width: 80 },
@@ -123,7 +122,8 @@ const columns = shallowRef([
:row-draggable="!loading"
row-key="b_id"
:active-row-key="activeKey"
@row-click="(ev, row) => (activeKey = row.b_id)"
@row-click="(_, row) => (activeKey = String(row.b_id))"
@change="emitUpdate"
@row-order-change="onRowOrderChange"
/>
</div>
@@ -131,7 +131,8 @@ const columns = shallowRef([
v-model:visible="groupManagerVisible"
:module-id="moduleId"
:groups="groups"
@update="(v) => emit('updateGroups', v)"
:referenced-group-ids="referencedGroupIds"
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
/>
</div>
</template>
@@ -1,7 +1,7 @@
<script setup>
import { ref, watch, computed } from "vue";
import { message, Modal } from "antdv-next";
import { ref, watch } from "vue";
import FmsTable from "@/components/fms-table/FmsTable.vue";
import { syncModuleItemI18nKeys } from "@/utils/i18n";
const FIELD_TYPES = [
{ label: "输入框", value: "input" },
@@ -16,44 +16,24 @@ const FIELD_TYPES = [
const props = defineProps({
moduleId: { type: String, required: true },
moduleCode: { type: String, default: "" },
fields: { type: Array, default: () => [] },
viewTable: { type: String, default: null },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["update", "sync"]);
const emit = defineEmits(["update"]);
const localFields = ref([]);
const activeKey = ref(null);
// 同步:未配置查询视图时,手动输入表/视图名
const syncOpen = ref(false);
const syncTable = ref("");
function onSyncClick() {
if (props.viewTable) {
emit("sync", props.viewTable);
return;
}
syncTable.value = "";
syncOpen.value = true;
}
function onSyncConfirm() {
const name = syncTable.value.trim();
if (!name) {
message.warning("请输入表或视图名");
return;
}
syncOpen.value = false;
emit("sync", name);
}
watch(
() => props.fields,
(val) => {
if (val === localFields.value) return;
localFields.value = val?.map((r) => ({ ...r })) || [];
() => [props.fields, props.moduleCode],
([val, moduleCode], previous = []) => {
if (val === localFields.value && moduleCode === previous[1]) return;
const rows = val?.map((row) => ({ ...row })) || [];
const changed = syncModuleItemI18nKeys(rows, "field", moduleCode, "b_field");
localFields.value = rows;
if (changed) emit("update", rows);
},
{ immediate: true },
);
@@ -64,56 +44,17 @@ function emitUpdate() {
// 编辑器已直接更新当前行,这里只向父级同步草稿。
function onCellChange() {
syncModuleItemI18nKeys(localFields.value, "field", props.moduleCode, "b_field");
emitUpdate();
}
// 行多选:selectedRowKeys 由 FmsTable 的选择列回写;
// 传了 rowSelection 时 FmsTable 会在最前列插入复选框列(也是单击进入编辑态所需的机制)。
const selectedRowKeys = ref([]);
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys) => {
selectedRowKeys.value = keys;
},
}));
function deleteRow(record) {
if (props.loading) return;
localFields.value = localFields.value.filter((r) => r.b_id !== record.b_id);
const k = String(record.b_id);
if (selectedRowKeys.value.includes(k)) {
selectedRowKeys.value = selectedRowKeys.value.filter((x) => x !== k);
}
emitUpdate();
}
// 批量删除选中行
function batchDelete() {
if (props.loading) return;
const keys = selectedRowKeys.value;
if (!keys.length) return;
Modal.confirm({
title: `确定删除选中的 ${keys.length} 条记录?`,
okText: "删除",
okType: "danger",
cancelText: "取消",
onOk: () => {
const delSet = new Set(keys);
localFields.value = localFields.value.filter((r) => !delSet.has(r.b_id));
selectedRowKeys.value = [];
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = "";
emitUpdate();
},
});
}
const columns = ref([
{ dataIndex: "b_field", title: "字段名", width: 140 },
{
dataIndex: "b_name",
title: "字段名称",
width: 140,
editor: { type: "input", onChange: () => emitUpdate() },
editor: { type: "input" },
},
{
dataIndex: "b_type",
@@ -122,71 +63,31 @@ const columns = ref([
editor: {
type: "select",
options: FIELD_TYPES,
onChange: () => emitUpdate(),
},
},
{
dataIndex: "b_default_value",
title: "默认值",
width: 120,
editor: { type: "input", onChange: () => emitUpdate() },
editor: { type: "input" },
},
{
dataIndex: "b_i18n",
title: "多语言键",
width: 140,
editor: { type: "input", onChange: () => emitUpdate() },
width: 220,
},
{
dataIndex: "b_canuse",
title: "启用",
width: 70,
align: "center",
editor: { type: "checkbox", onChange: () => emitUpdate() },
},
{
dataIndex: "actions",
title: "操作",
width: 60,
actions: [
{
type: "delete",
label: "删除",
confirm: "确定删除该字段?",
onClick: (row) => deleteRow(row),
},
],
editor: { type: "checkbox" },
},
]);
</script>
<template>
<div class="fields-panel">
<div class="panel-toolbar">
<a-space>
<a-button type="primary" :disabled="loading" @click="onSyncClick">
同步字段{{ viewTable ? ` (${viewTable})` : "" }}
</a-button>
<a-button danger :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
</a-button>
<span v-if="!viewTable" class="panel-hint">未配置查询视图,点击后手动输入表名</span>
</a-space>
</div>
<a-modal
v-model:open="syncOpen"
title="同步字段"
@ok="onSyncConfirm"
:confirm-loading="loading"
>
<p>当前模块未配置查询视图,请填写要同步字段的表或视图名:</p>
<a-input
v-model:value="syncTable"
placeholder="例如 s_log_login 或 v_biz"
@press-enter="onSyncConfirm"
/>
</a-modal>
<div class="panel-body">
<FmsTable
v-model:columns="columns"
@@ -194,7 +95,6 @@ const columns = ref([
:editable="!loading"
row-key="b_id"
:active-row-key="activeKey"
:row-selection="rowSelection"
@row-click="(_, row) => (activeKey = String(row.b_id))"
@change="onCellChange"
/>
@@ -209,13 +109,6 @@ const columns = ref([
height: 100%;
}
.panel-toolbar {
display: flex;
flex: none;
gap: 8px;
padding: 12px 20px 0;
}
.panel-body {
flex: 1;
min-height: 0;
@@ -14,6 +14,7 @@ import { createTempId } from "@/utils/tempId";
const props = defineProps({
moduleId: { type: String, required: true },
i18nKeys: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
});
@@ -28,6 +29,7 @@ const isLoading = ref(false);
const searchKey = ref("");
const activeKey = ref(null);
const tableRef = ref(null);
let loadRequestId = 0;
async function loadLangTypes() {
try {
@@ -61,10 +63,16 @@ function buildPivot(flat) {
}
async function loadI18n() {
const requestId = ++loadRequestId;
isLoading.value = true;
try {
const keys = [...new Set((props.i18nKeys || []).filter(Boolean).map(String))];
const quotedKeys = keys.map((key) => "'" + key.replace(/'/g, "''") + "'");
const where = keys.length ? "b_key IN (" + quotedKeys.join(", ") + ")" : null;
const [i18nRes] = await Promise.all([
loadDataApi("b_i18n", null, "b_key ASC, b_locale ASC"),
where
? loadDataApi("b_i18n", where, "b_key ASC, b_locale ASC")
: Promise.resolve({ data: [] }),
loadLangTypes(),
]);
const flat = (i18nRes.data || []).map((r) => ({
@@ -73,12 +81,34 @@ async function loadI18n() {
b_locale: r.b_locale,
b_value: r.b_value,
}));
if (requestId !== loadRequestId) return;
originalsFlat.value = cloneData(flat);
pivotRows.value = buildPivot(flat);
} catch (e) {
message.error("加载多语言失败:" + (e.message || e));
if (requestId === loadRequestId) message.error("加载多语言失败:" + (e.message || e));
} finally {
isLoading.value = false;
if (requestId === loadRequestId) isLoading.value = false;
}
}
async function refreshKey(key) {
if (!key) return;
const escaped = String(key).replace(/'/g, "''");
try {
const res = await loadDataApi("b_i18n", "b_key = '" + escaped + "'", "b_locale ASC");
const rows = (res.data || []).map((row) => ({
b_id: row.b_id,
b_key: row.b_key,
b_locale: row.b_locale,
b_value: row.b_value,
}));
originalsFlat.value = [
...originalsFlat.value.filter((row) => row.b_key !== key),
...cloneData(rows),
];
pivotRows.value = [...pivotRows.value.filter((row) => row.b_key !== key), ...buildPivot(rows)];
} catch (e) {
message.error("刷新多语言失败:" + (e.message || e));
}
}
@@ -154,6 +184,24 @@ function flatten(rows) {
return flat;
}
// 由父级「同步字段」调用:把默认翻译条目({ key, locale, value })并入草稿行。
// 仅当面板中尚不存在该 b_key 时才追加,避免覆盖用户在面板里已配置的翻译。
function mergeDefaults(entries) {
if (!entries?.length) return;
const existingKeys = new Set(pivotRows.value.map((r) => r.b_key));
const toAdd = entries.filter((e) => e.key && !existingKeys.has(e.key));
if (!toAdd.length) return;
pivotRows.value = [
...pivotRows.value,
...toAdd.map((e) => ({
_id: createTempId(),
b_key: e.key,
_localeIds: {},
[e.locale]: e.value,
})),
];
}
// 由父级统一保存调用:返回 b_i18n 的变更请求(无变更返回 null),保存成功后父级调用 markSaved 同步基线
async function buildSaveRequest() {
const flat = flatten(pivotRows.value);
@@ -168,7 +216,7 @@ function markSaved() {
originalsFlat.value = cloneData(flatten(pivotRows.value));
}
defineExpose({ buildSaveRequest, markSaved });
defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey });
const displayedData = computed(() => {
if (!searchKey.value) return pivotRows.value;
@@ -8,10 +8,11 @@ const props = defineProps({
listConfig: { type: Array, default: () => [] },
fields: { type: Array, default: () => [] },
groups: { type: Array, default: () => [] },
referencedGroupIds: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["update", "updateGroups"]);
const emit = defineEmits(["update", "groupsSaved"]);
const localList = ref([]);
const groupManagerVisible = ref(false);
@@ -64,7 +65,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: () => availableFields.value,
onChange: () => emitUpdate(),
},
},
{ dataIndex: "b_width", title: "列宽", width: 80 },
@@ -75,7 +75,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: () => groupOptions.value,
onChange: () => emitUpdate(),
},
},
{
@@ -83,7 +82,7 @@ const columns = shallowRef([
title: "显示",
width: 60,
align: "center",
editor: { type: "checkbox", onChange: () => emitUpdate() },
editor: { type: "checkbox" },
},
{ dataIndex: "b_xh", title: "排序号", width: 80 },
{
@@ -118,7 +117,8 @@ const columns = shallowRef([
:row-draggable="!loading"
row-key="b_id"
:active-row-key="activeKey"
@row-click="(ev, row) => (activeKey = row.b_id)"
@row-click="(_, row) => (activeKey = String(row.b_id))"
@change="emitUpdate"
@row-order-change="onRowOrderChange"
/>
</div>
@@ -126,7 +126,8 @@ const columns = shallowRef([
v-model:visible="groupManagerVisible"
:module-id="moduleId"
:groups="groups"
@update="(v) => emit('updateGroups', v)"
:referenced-group-ids="referencedGroupIds"
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
/>
</div>
</template>
@@ -3,6 +3,7 @@ import { computed, h, nextTick, ref, watch } from "vue";
import { message, Modal } from "antdv-next";
import { Languages } from "@lucide/vue";
import FmsTable from "@/components/fms-table/FmsTable.vue";
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
import { createTempId } from "@/utils/tempId";
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
@@ -13,7 +14,7 @@ const props = defineProps({
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["update"]);
const emit = defineEmits(["update", "i18nSaved"]);
const localPowers = ref([]);
const activeKey = ref(null);
@@ -24,11 +25,10 @@ watch(
() => [props.powers, props.moduleCode],
([val, moduleCode], previous = []) => {
if (val === localPowers.value && moduleCode === previous[1]) return;
localPowers.value = (val || []).map((r) => ({
...r,
b_i18n:
r.b_code && props.moduleCode ? `power.${props.moduleCode}.${r.b_code}` : (r.b_i18n ?? null),
}));
const rows = (val || []).map((row) => ({ ...row }));
const changed = syncModuleItemI18nKeys(rows, "power", moduleCode, "b_code");
localPowers.value = rows;
if (changed) emit("update", rows);
},
{ immediate: true },
);
@@ -46,7 +46,7 @@ async function addRow() {
b_code: "",
b_name: "",
b_i18n: null,
b_xh: localPowers.value.length * 10,
b_xh: (localPowers.value.length + 1) * 10,
b_canuse: 1,
});
emitUpdate();
@@ -89,7 +89,7 @@ function batchDelete() {
cancelText: "取消",
onOk: () => {
const delSet = new Set(keys);
localPowers.value = localPowers.value.filter((r) => !delSet.has(r.b_id));
localPowers.value = localPowers.value.filter((r) => !delSet.has(String(r.b_id)));
selectedRowKeys.value = [];
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = "";
emitUpdate();
@@ -99,6 +99,7 @@ function batchDelete() {
// 编辑器已直接更新当前行,这里只向父级同步草稿。
function onCellChange() {
syncModuleItemI18nKeys(localPowers.value, "power", props.moduleCode, "b_code");
emitUpdate();
}
@@ -106,8 +107,7 @@ function onCellChange() {
const i18nModalRef = ref(null);
function openI18n(row) {
const key =
row.b_code && props.moduleCode ? `power.${props.moduleCode}.${row.b_code}` : row.b_i18n;
const key = buildModuleItemI18nKey("power", props.moduleCode, row.b_code);
if (!key) {
message.warning("请先填写操作编码");
return;
@@ -208,7 +208,7 @@ const columns = ref([
@change="onCellChange"
/>
</div>
<I18nQuickSetModal ref="i18nModalRef" />
<I18nQuickSetModal ref="i18nModalRef" @saved="(key) => emit('i18nSaved', key)" />
</div>
</template>
@@ -65,7 +65,7 @@ function addRow() {
b_component: "input",
b_operator: "=",
b_default_value: null,
b_xh: localQuery.value.length * 10,
b_xh: (localQuery.value.length + 1) * 10,
b_canuse: 1,
});
emitUpdate();
@@ -85,7 +85,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: () => availableFields.value,
onChange: () => emitUpdate(),
},
},
{
@@ -95,7 +94,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: COMPONENT_OPTIONS,
onChange: () => emitUpdate(),
},
},
{
@@ -105,7 +103,6 @@ const columns = shallowRef([
editor: {
type: "select",
options: OPERATOR_OPTIONS,
onChange: () => emitUpdate(),
},
},
{ dataIndex: "b_default_value", title: "默认查询值", width: 140 },
@@ -114,7 +111,7 @@ const columns = shallowRef([
title: "启用",
width: 60,
align: "center",
editor: { type: "checkbox", onChange: () => emitUpdate() },
editor: { type: "checkbox" },
},
{ dataIndex: "b_xh", title: "排序号", width: 80 },
{
@@ -149,7 +146,8 @@ const columns = shallowRef([
:row-draggable="!loading"
row-key="b_id"
:active-row-key="activeKey"
@row-click="(ev, row) => (activeKey = row.b_id)"
@row-click="(_, row) => (activeKey = String(row.b_id))"
@change="emitUpdate"
@row-order-change="onRowOrderChange"
/>
</div>
@@ -1,16 +1,19 @@
<script setup>
import { computed, ref, watch } from "vue";
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 {
cloneData,
diffRows,
allocateTemporaryIds,
remapForeignKeys,
tableChange,
hasChanges,
} from "@/utils/dataChanges";
import { createTempId } from "@/utils/tempId";
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
import { Plus } from "@lucide/vue";
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
@@ -23,11 +26,11 @@ import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
// ---- 状态 ----
const treeData = ref([]);
const treeLoading = ref(false);
const expandedTreeKeys = ref([]);
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;
@@ -71,17 +74,47 @@ const drafts = ref({
const configLoading = ref(false);
const configSaving = ref(false);
const configSyncing = ref(false);
const i18nPanelRef = ref(null);
const loadedModuleId = ref(null);
let configRequestId = 0;
let operationId = 0;
// ref 写在 v-for 内部会被 Vue 收集为数组,这里统一取单个组件实例
function getI18nPanel() {
const r = i18nPanelRef.value;
return Array.isArray(r) ? r[0] : r;
// 使用函数 ref,避免组件位于 TabPane 的 v-for 内时被 Vue 收集成实例数组。
function setI18nPanelRef(instance) {
i18nPanelRef.value = instance || null;
}
// 当前选中模块对象(从 drafts.module 获取)
const selectedModule = computed(() => drafts.value.module);
const busy = computed(() => configLoading.value || configSaving.value || configSyncing.value);
const syncDisabled = computed(
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
);
const isCurrentModule = computed(
() =>
loadedModuleId.value != null && String(loadedModuleId.value) === String(selectedModuleId.value),
);
const i18nKeys = computed(() => {
const keys = new Set();
const add = (key) => {
if (key) keys.add(String(key));
};
add(drafts.value.module?.b_i18n);
for (const section of ["fields", "groups", "listConfig", "powers"]) {
for (const row of drafts.value[section] || []) add(row.b_i18n);
}
return [...keys];
});
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(() => {
@@ -102,20 +135,20 @@ const parentModuleName = computed(() => {
const visibleTabs = computed(() => {
const type = selectedModule.value?.b_module_type;
if (!type) return [];
const tabs = [{ key: "basic", tab: "基本信息", icon: "Info" }];
const tabs = [{ key: "basic", tab: "基本信息" }];
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" },
{ key: "fields", tab: "字段定义" },
{ key: "list", tab: "列表配置" },
{ key: "edit", tab: "编辑配置" },
{ key: "query", tab: "查询配置" },
);
}
if (type === "page") {
tabs.push({ key: "powers", tab: "权限配置", icon: "Lock" });
tabs.push({ key: "powers", tab: "权限配置" });
}
if (type !== "directory") {
tabs.push({ key: "i18n", tab: "多语言", icon: "Translation" });
tabs.push({ key: "i18n", tab: "多语言" });
}
return tabs;
});
@@ -142,6 +175,38 @@ function buildTree(modules) {
return roots;
}
function sortTree(nodes) {
nodes.sort(
(a, b) =>
Number(a.b_xh || 0) - Number(b.b_xh || 0) || String(a.b_id).localeCompare(String(b.b_id)),
);
for (const node of nodes) {
if (node.children?.length) sortTree(node.children);
}
}
// 普通保存后直接更新现有节点,避免重新请求并重建整棵树导致展开状态丢失。
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;
sortTree(treeData.value);
}
async function loadTree() {
treeLoading.value = true;
try {
@@ -157,32 +222,56 @@ async function loadModuleConfig(moduleId, requestId) {
configLoading.value = true;
try {
const id = String(moduleId);
const [moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, powersRes] =
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"),
const moduleType = findNode(id)?.b_module_type;
const requests = [["module", loadDataApi("s_module", `b_id = ${id}`)]];
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"),
],
);
} else if (moduleType === "page") {
requests.push([
"powers",
loadDataApi("s_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
]);
}
const responses = Object.fromEntries(
await Promise.all(requests.map(async ([key, request]) => [key, await request])),
);
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 || [],
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 || [],
};
if (requestId !== configRequestId) return;
originals.value = original;
drafts.value = cloneData(original);
loadedModuleId.value = id;
activeTab.value = "basic";
} catch (e) {
if (requestId === configRequestId) {
loadedModuleId.value = null;
drafts.value.module = null;
message.error("模块配置加载失败:" + (e.message || e));
}
} finally {
@@ -192,6 +281,7 @@ async function loadModuleConfig(moduleId, requestId) {
// ---- 树节点选择 ----
function onTreeSelect(keys) {
if (busy.value) return;
if (keys.length > 0) {
selectedModuleId.value = String(keys[0]);
}
@@ -199,8 +289,11 @@ function onTreeSelect(keys) {
watch(selectedModuleId, (id) => {
const requestId = ++configRequestId;
operationId++;
if (!id) {
configLoading.value = false;
loadedModuleId.value = null;
drafts.value.module = null;
return;
}
if (skipLoad.value) {
@@ -208,6 +301,7 @@ watch(selectedModuleId, (id) => {
configLoading.value = false;
return;
}
loadedModuleId.value = null;
void loadModuleConfig(id, requestId);
});
@@ -223,11 +317,19 @@ function onSetField(field, value) {
}
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
function onGroupsSaved(newGroups) {
function onGroupsSaved(newGroups, idMapping) {
if (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) {
void i18nPanelRef.value?.refreshKey?.(key);
}
// ---- 新增模块 ----
async function onAddModule(parentId, moduleType = "directory") {
const res = await nextIdApi();
@@ -262,10 +364,12 @@ async function onAddModule(parentId, moduleType = "directory") {
drafts.value = cloneData(originals.value);
skipLoad.value = true;
selectedModuleId.value = newId;
loadedModuleId.value = newId;
activeTab.value = "basic";
// 刷新树
await loadTree();
updateSavedTreeNode(newModule);
if (parentId && !expandedTreeKeys.value.includes(String(parentId))) {
expandedTreeKeys.value = [...expandedTreeKeys.value, String(parentId)];
}
}
const addModuleMenu = {
@@ -279,7 +383,7 @@ function onTreeRightClick({ node }) {
}
function onContextMenuClick({ key }) {
contextMenuOpen.value = false;
if (busy.value) return;
const node = contextNode.value;
contextNode.value = null;
if (!node) return;
@@ -345,7 +449,9 @@ async function onDeleteNodeById(moduleId) {
// ---- 统一保存 ----
async function saveAll() {
if (!selectedModule.value) return;
if (!selectedModule.value || !isCurrentModule.value || busy.value) return;
const moduleId = String(selectedModule.value.b_id);
const currentOperationId = ++operationId;
configSaving.value = true;
try {
@@ -354,8 +460,26 @@ async function saveAll() {
// s_module 主表
const moduleData = drafts.value.module;
const moduleOriginal = originals.value.module;
if (!String(moduleData?.b_code || "").trim()) {
message.warning(`请填写${moduleData?.b_module_type === "data" ? "模块编码" : "菜单编码"}`);
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");
const isNew = !moduleOriginal?.b_code;
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
const isNew = !moduleOriginal?.b_code;
saveReqs.push({
table: "s_module",
key_field: "b_id",
@@ -383,8 +507,10 @@ async function saveAll() {
}
// 多语言:由各子面板收集变更,统一在此保存
const i18nPanel = getI18nPanel();
if (i18nPanel) {
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
return;
const i18nPanel = i18nPanelRef.value;
if (typeof i18nPanel?.buildSaveRequest === "function") {
const i18nReq = await i18nPanel.buildSaveRequest();
if (i18nReq) saveReqs.push(i18nReq);
}
@@ -398,9 +524,9 @@ async function saveAll() {
// 更新 originals
originals.value = cloneData(drafts.value);
getI18nPanel()?.markSaved();
i18nPanelRef.value?.markSaved?.();
updateSavedTreeNode(moduleData);
message.success("保存成功");
await loadTree();
} catch (e) {
message.error("保存失败:" + (e.message || e));
} finally {
@@ -529,19 +655,20 @@ function buildMissingDefaultConfigs(module, fields) {
return generated;
}
async function syncFields(tableName) {
async function syncFields() {
const module = selectedModule.value;
const view = tableName || module?.b_viewtable;
if (!view) {
message.warning("请先配置查询视图或输入表名");
return;
}
const view = String(module?.b_viewtable || "").trim();
if (!view || busy.value) return;
const moduleId = String(module.b_id);
const currentOperationId = ++operationId;
try {
configSaving.value = true;
configSyncing.value = true;
const res = await describeApi(view);
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
return;
const columns = res.data || [];
const existingFields = originals.value.fields || [];
const existingFields = drafts.value.fields || [];
// 构建视图列名集合
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
@@ -580,9 +707,9 @@ async function syncFields(tableName) {
}
}
// 字段多语言键只补空值,保留已有人工配置。
// 多语言键始终按 field.<模块代码>.<字段编码> 自动生成。
mergedFields.forEach((field) => {
if (!field.b_i18n) field.b_i18n = `field.${field.b_field}`;
field.b_i18n = buildModuleItemI18nKey("field", module.b_code, field.b_field);
});
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
@@ -594,6 +721,29 @@ async function syncFields(tableName) {
fieldReq.deletes || [],
]);
// 为"有 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 }));
let primaryLocale = "zh-CN";
try {
const langRes = await loadDataApi("b_i18n_type", "b_canuse = 1", "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;
}
} catch (e) {
// 取不到语言类型时回退默认值
}
const i18nEntries = candidateI18n.map((e) => ({
key: e.key,
locale: primaryLocale,
value: e.value,
}));
const generated = buildMissingDefaultConfigs(module, mergedFields);
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
const generatedCount = generatedSections.reduce(
@@ -601,21 +751,15 @@ async function syncFields(tableName) {
0,
);
if (!hasChanges(fieldReq) && !generatedCount) {
message.info("字段和默认配置已是最新,无需同步");
if (!hasChanges(fieldReq) && !generatedCount && !i18nEntries.length) {
message.success("同步成功");
return;
}
if (hasChanges(fieldReq)) {
// 字段先保存以取得真实 ID,自动生成的配置留在草稿中由顶部“保存”统一提交。
await saveObjectApi([fieldReq]);
const fieldsRes = await loadDataApi(
"s_module_field",
`b_module_id = ${module.b_id}`,
"b_id ASC",
);
originals.value.fields = fieldsRes.data || [];
drafts.value.fields = cloneData(originals.value.fields);
// 多语言默认配置注入 i18n 面板草稿;字段已预分配真实 ID(见上方 allocateTemporaryIds),
// 列表/编辑/查询配置稍后追加到草稿。全部由顶部"保存"统一提交,期间不写库。
if (i18nEntries.length) {
i18nPanelRef.value?.mergeDefaults?.(i18nEntries);
}
for (const section of generatedSections) {
@@ -623,16 +767,13 @@ async function syncFields(tableName) {
drafts.value[section] = [...(drafts.value[section] || []), ...cloneData(generated[section])];
}
const disabledCount = existingFields.filter(
(field) => field.b_canuse === 1 && !viewColumnSet.has(field.b_field?.toLowerCase()),
).length;
message.success(
`字段同步完成:${columns.length} 列,新增 ${fieldReq.inserts?.length || 0},停用 ${disabledCount};自动生成列表 ${generated.listConfig.length}、编辑 ${generated.editConfig.length}、查询 ${generated.queryConfig.length} 项配置${generatedCount ? ",请点击保存" : ""}`,
);
drafts.value.fields = cloneData(mergedFields);
message.success("同步成功");
} catch (e) {
message.error("字段同步失败:" + (e.message || e));
} finally {
configSaving.value = false;
configSyncing.value = false;
}
}
@@ -646,20 +787,23 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
<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 :menu="addModuleMenu" :trigger="['click']" :disabled="busy">
<a-button :disabled="busy"><Plus :size="14" />新增</a-button>
</a-dropdown>
</div>
<div v-if="treeData.length === 0" class="mm-tree-empty">
<div v-if="treeLoading" class="mm-tree-empty">
<a-spin />
</div>
<div v-else-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"
>
<div class="mm-tree-container">
<FmsTree
class="mm-tree"
:disabled="configSaving"
:tree-data="treeData"
v-model:expanded-keys="expandedTreeKeys"
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
:field-names="{ title: 'title', key: 'key', children: 'children' }"
@select="onTreeSelect"
@@ -677,66 +821,84 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
<!-- 右侧详情 -->
<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"
:disabled="configSaving"
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
/>
<a-divider type="vertical" />
<span>显示菜单</span>
<a-switch
:checked="drafts.module?.b_canmenu === 1"
:disabled="configSaving || drafts.module?.b_module_type === 'data'"
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
/>
<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-switch
: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
:checked="drafts.module?.b_canmenu === 1"
:disabled="busy || 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>
<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>
<!-- 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="configSaving"
: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"
:view-table="selectedModule.b_viewtable"
:loading="configSaving"
:loading="busy"
@update="(v) => onUpdate('fields', v)"
@sync="syncFields"
/>
<ModuleListConfigPanel
v-else-if="tab.key === 'list'"
@@ -744,9 +906,10 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:list-config="drafts.listConfig"
:fields="drafts.fields"
:groups="drafts.groups"
:loading="configSaving"
:referenced-group-ids="referencedGroupIds"
:loading="busy"
@update="(v) => onUpdate('listConfig', v)"
@update-groups="onGroupsSaved"
@groups-saved="onGroupsSaved"
/>
<ModuleEditConfigPanel
v-else-if="tab.key === 'edit'"
@@ -754,16 +917,17 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:edit-config="drafts.editConfig"
:fields="drafts.fields"
:groups="drafts.groups"
:loading="configSaving"
:referenced-group-ids="referencedGroupIds"
:loading="busy"
@update="(v) => onUpdate('editConfig', v)"
@update-groups="onGroupsSaved"
@groups-saved="onGroupsSaved"
/>
<ModuleQueryConfigPanel
v-else-if="tab.key === 'query'"
:module-id="String(selectedModule.b_id)"
:query-config="drafts.queryConfig"
:fields="drafts.fields"
:loading="configSaving"
:loading="busy"
@update="(v) => onUpdate('queryConfig', v)"
/>
<ModulePowersPanel
@@ -771,25 +935,27 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:module-id="String(selectedModule.b_id)"
:module-code="selectedModule?.b_code || ''"
:powers="drafts.powers"
:loading="configSaving"
:loading="busy"
@update="(v) => onUpdate('powers', v)"
@i18n-saved="onI18nSaved"
/>
<ModuleI18nPanel
v-else-if="tab.key === 'i18n'"
ref="i18nPanelRef"
:ref="setI18nPanelRef"
:module-id="String(selectedModule.b_id)"
:loading="configSaving"
:i18n-keys="i18nKeys"
:loading="busy"
/>
</template>
</a-tab-pane>
</a-tabs>
</div>
</template>
</a-tab-pane>
</a-tabs>
</div>
</template>
<!-- 未选中模块 -->
<div v-else class="mm-empty">
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
</div>
<!-- 未选中模块 -->
<div v-else class="mm-empty">
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
</div>
</FmsLoadingOverlay>
</main>
</div>
</template>
@@ -835,6 +1001,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
padding: 8px 10px;
}
.mm-tree-container {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
flex-direction: column;
}
.mm-tree-empty {
flex: 1;
display: flex;
@@ -3,5 +3,5 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu
</script>
<template>
<FmsModuleListPage module-code="d_login_log" />
<FmsModuleListPage />
</template>