20260727225141
This commit is contained in:
1 parent
9dbaef7787
commit
63588bd217
97 files changed
+1309
-145
No files matched your search
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import { buildModuleI18nKey } from "@/utils/i18n";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -36,20 +37,27 @@ function setField(field, value) {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
// 多语言键由模块编码自动生成(module.<编码>),无需手动填写。
|
||||
// 目录和页面使用 menu.<编码>,数据模块使用 module.<编码>。
|
||||
// 编码变化时始终以完整编码重新派生,避免只生成首字母后不再更新。
|
||||
function onCodeInput(v) {
|
||||
setField("b_code", v);
|
||||
if (v) {
|
||||
formData.value.b_i18n = `module.${v}`;
|
||||
emitUpdate();
|
||||
}
|
||||
// 编码变化时始终以完整编码重新派生多语言键(清空编码时键也随之清空,保持跟随)。
|
||||
formData.value.b_i18n = buildModuleI18nKey(formData.value.b_module_type, v);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function onModuleTypeChange(v) {
|
||||
formData.value.b_module_type = v;
|
||||
formData.value.b_i18n = buildModuleI18nKey(v, formData.value.b_code);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const i18nModalRef = ref(null);
|
||||
|
||||
function openI18nModal() {
|
||||
const key = formData.value?.b_i18n || `module.${formData.value?.b_code || ""}`;
|
||||
const key =
|
||||
formData.value?.b_i18n ||
|
||||
buildModuleI18nKey(formData.value?.b_module_type, formData.value?.b_code);
|
||||
i18nModalRef.value?.open(key, formData.value?.b_name || "");
|
||||
}
|
||||
|
||||
@@ -83,12 +91,7 @@ function onI18nSaved(key) {
|
||||
{ label: '页面', value: 'page' },
|
||||
{ label: '数据', value: 'data' },
|
||||
]"
|
||||
@update:value="
|
||||
(v) => {
|
||||
formData.b_module_type = v;
|
||||
emitUpdate();
|
||||
}
|
||||
"
|
||||
@update:value="onModuleTypeChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
i18nKeys: { type: Array, default: () => [] },
|
||||
i18nItems: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
@@ -31,6 +31,66 @@ const activeKey = ref(null);
|
||||
const tableRef = ref(null);
|
||||
let loadRequestId = 0;
|
||||
|
||||
const managedItems = computed(() => (props.i18nItems || []).filter((item) => item?.managed));
|
||||
const requestedKeys = computed(() => [
|
||||
...new Set(
|
||||
(props.i18nItems || [])
|
||||
.flatMap((item) => [item?.key, item?.sourceKey])
|
||||
.filter(Boolean)
|
||||
.map(String),
|
||||
),
|
||||
]);
|
||||
|
||||
function isManagedRow(row) {
|
||||
return Boolean(row?._managedId);
|
||||
}
|
||||
|
||||
// 保证模块、字段、权限的受管键始终存在。简体中文为空或仍为上一次默认名称时,
|
||||
// 自动跟随当前名称;人工修改后的翻译不再被名称变化覆盖。
|
||||
// itemsOverride 用于保存时按最终草稿态生成受管键(不传则使用 props.i18nItems)。
|
||||
function reconcileManagedRows(itemsOverride) {
|
||||
const items = itemsOverride || managedItems.value;
|
||||
const rows = [...pivotRows.value];
|
||||
const rowByManagedId = new Map(
|
||||
rows.filter((row) => row._managedId).map((row) => [String(row._managedId), row]),
|
||||
);
|
||||
const unclaimedByKey = new Map(
|
||||
rows.filter((row) => !row._managedId).map((row) => [String(row.b_key), row]),
|
||||
);
|
||||
const currentManagedIds = new Set();
|
||||
|
||||
for (const item of items) {
|
||||
const managedId = String(item.id || item.key);
|
||||
currentManagedIds.add(managedId);
|
||||
let row =
|
||||
rowByManagedId.get(managedId) ||
|
||||
unclaimedByKey.get(String(item.key)) ||
|
||||
unclaimedByKey.get(String(item.sourceKey));
|
||||
if (!row) {
|
||||
row = { _id: createTempId(), b_key: item.key, _localeIds: {} };
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
const previousDefault = row._defaultValue;
|
||||
const nextDefault = item.defaultValue == null ? "" : String(item.defaultValue);
|
||||
if (row["zh-CN"] == null || row["zh-CN"] === "" || row["zh-CN"] === previousDefault) {
|
||||
row["zh-CN"] = nextDefault;
|
||||
}
|
||||
row.b_key = String(item.key);
|
||||
row._managedId = managedId;
|
||||
row._defaultValue = nextDefault;
|
||||
row._sortType = String(item.key).split(".", 1)[0];
|
||||
}
|
||||
|
||||
// 已从模块配置中移除的条目不再受保护,保留其翻译供管理员自行处理。
|
||||
for (const row of rows) {
|
||||
if (!row._managedId || currentManagedIds.has(String(row._managedId))) continue;
|
||||
delete row._managedId;
|
||||
delete row._defaultValue;
|
||||
}
|
||||
pivotRows.value = rows;
|
||||
}
|
||||
|
||||
async function loadLangTypes() {
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
@@ -66,7 +126,7 @@ async function loadI18n() {
|
||||
const requestId = ++loadRequestId;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const keys = [...new Set((props.i18nKeys || []).filter(Boolean).map(String))];
|
||||
const keys = requestedKeys.value;
|
||||
const quotedKeys = keys.map((key) => "'" + key.replace(/'/g, "''") + "'");
|
||||
const where = keys.length ? "b_key IN (" + quotedKeys.join(", ") + ")" : null;
|
||||
const [i18nRes] = await Promise.all([
|
||||
@@ -84,6 +144,7 @@ async function loadI18n() {
|
||||
if (requestId !== loadRequestId) return;
|
||||
originalsFlat.value = cloneData(flat);
|
||||
pivotRows.value = buildPivot(flat);
|
||||
reconcileManagedRows();
|
||||
} catch (e) {
|
||||
if (requestId === loadRequestId) message.error("加载多语言失败:" + (e.message || e));
|
||||
} finally {
|
||||
@@ -107,12 +168,14 @@ async function refreshKey(key) {
|
||||
...cloneData(rows),
|
||||
];
|
||||
pivotRows.value = [...pivotRows.value.filter((row) => row.b_key !== key), ...buildPivot(rows)];
|
||||
reconcileManagedRows();
|
||||
} catch (e) {
|
||||
message.error("刷新多语言失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.moduleId, loadI18n, { immediate: true });
|
||||
watch(() => props.i18nItems, reconcileManagedRows, { deep: true });
|
||||
|
||||
async function addRow() {
|
||||
if (props.loading || isLoading.value) return;
|
||||
@@ -122,11 +185,11 @@ async function addRow() {
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
tableRef.value?.scrollToRow(String(newId));
|
||||
await tableRef.value?.editRow(String(newId), "b_key");
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading || isLoading.value) return;
|
||||
if (props.loading || isLoading.value || isManagedRow(record)) return;
|
||||
pivotRows.value = pivotRows.value.filter((r) => r._id !== record._id);
|
||||
// 同步清理选择状态,避免选中项被删后 keys 残留
|
||||
const k = String(record._id);
|
||||
@@ -140,15 +203,22 @@ function deleteRow(record) {
|
||||
const selectedRowKeys = ref([]);
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
getCheckboxProps: (row) => ({ disabled: isManagedRow(row) }),
|
||||
onChange: (keys, rows) => {
|
||||
const selectableKeys = new Set(
|
||||
(rows || []).filter((row) => !isManagedRow(row)).map((row) => String(row._id)),
|
||||
);
|
||||
selectedRowKeys.value = keys.map(String).filter((key) => selectableKeys.has(key));
|
||||
},
|
||||
}));
|
||||
|
||||
// 批量删除选中行(整条 key,含其全部语言)
|
||||
function batchDelete() {
|
||||
if (props.loading || isLoading.value) return;
|
||||
const keys = selectedRowKeys.value;
|
||||
const managedRowKeys = new Set(
|
||||
pivotRows.value.filter(isManagedRow).map((row) => String(row._id)),
|
||||
);
|
||||
const keys = selectedRowKeys.value.filter((key) => !managedRowKeys.has(String(key)));
|
||||
if (!keys.length) return;
|
||||
Modal.confirm({
|
||||
title: `确定删除选中的 ${keys.length} 条记录?`,
|
||||
@@ -216,16 +286,33 @@ function markSaved() {
|
||||
originalsFlat.value = cloneData(flatten(pivotRows.value));
|
||||
}
|
||||
|
||||
defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey });
|
||||
defineExpose({ buildSaveRequest, markSaved, mergeDefaults, refreshKey, reconcileManagedRows });
|
||||
|
||||
const displayedData = computed(() => {
|
||||
if (!searchKey.value) return pivotRows.value;
|
||||
const kw = searchKey.value.toLowerCase();
|
||||
return pivotRows.value.filter((r) => (r.b_key || "").toLowerCase().includes(kw));
|
||||
const rows = kw
|
||||
? pivotRows.value.filter((row) => (row.b_key || "").toLowerCase().includes(kw))
|
||||
: pivotRows.value;
|
||||
const sortOrder = { menu: 0, module: 0, field: 1, power: 2 };
|
||||
return [...rows].sort((left, right) => {
|
||||
const leftType = left._sortType || String(left.b_key || "").split(".", 1)[0];
|
||||
const rightType = right._sortType || String(right.b_key || "").split(".", 1)[0];
|
||||
return (
|
||||
(sortOrder[leftType] ?? 3) - (sortOrder[rightType] ?? 3) ||
|
||||
String(left.b_key || "").localeCompare(String(right.b_key || ""))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const columns = computed(() => {
|
||||
const cols = [{ dataIndex: "b_key", title: "键", width: 240, editor: { type: "input" } }];
|
||||
const cols = [
|
||||
{
|
||||
dataIndex: "b_key",
|
||||
title: "键",
|
||||
width: 240,
|
||||
editor: { type: "input", disabled: (row) => isManagedRow(row) },
|
||||
},
|
||||
];
|
||||
for (const opt of langOptions.value) {
|
||||
cols.push({
|
||||
dataIndex: opt.value,
|
||||
@@ -244,6 +331,7 @@ const columns = computed(() => {
|
||||
type: "delete",
|
||||
label: "删除",
|
||||
confirm: "确定删除该记录(含其全部语言)?",
|
||||
disabled: (row) => isManagedRow(row),
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -53,7 +53,7 @@ async function addRow() {
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
tableRef.value?.scrollToRow(String(newId));
|
||||
await tableRef.value?.editRow(String(newId), "b_code");
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
hasChanges,
|
||||
} from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import {
|
||||
buildModuleI18nKey,
|
||||
buildModuleItemI18nKey,
|
||||
syncModuleItemI18nKeys,
|
||||
validateModuleCode,
|
||||
} from "@/utils/i18n";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import ModuleBasicPanel from "./components/ModuleBasicPanel.vue";
|
||||
import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
|
||||
@@ -95,17 +100,61 @@ 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));
|
||||
// 构建受管多语言项。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 });
|
||||
};
|
||||
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);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
return [...keys];
|
||||
});
|
||||
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"]) {
|
||||
@@ -435,7 +484,24 @@ async function onDeleteNodeById(moduleId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveObjectApi([{ table: "s_module", key_field: "b_id", deletes: [{ b_id: moduleId }] }]);
|
||||
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_power"),
|
||||
deleteByModuleId("s_module_field"),
|
||||
deleteByModuleId("s_module_field_group"),
|
||||
{ table: "s_module", key_field: "b_id", deletes: [{ b_id: id }] },
|
||||
]);
|
||||
message.success("模块已删除");
|
||||
if (selectedModuleId.value === moduleId) {
|
||||
selectedModuleId.value = null;
|
||||
@@ -460,10 +526,16 @@ async function saveAll() {
|
||||
// s_module 主表
|
||||
const moduleData = drafts.value.module;
|
||||
const moduleOriginal = originals.value.module;
|
||||
if (!String(moduleData?.b_code || "").trim()) {
|
||||
const moduleCode = String(moduleData?.b_code || "").trim();
|
||||
if (!moduleCode) {
|
||||
message.warning(`请填写${moduleData?.b_module_type === "data" ? "模块编码" : "菜单编码"}`);
|
||||
return;
|
||||
}
|
||||
const codeError = validateModuleCode(moduleCode);
|
||||
if (codeError) {
|
||||
message.error(`编码格式不正确:${codeError}`);
|
||||
return;
|
||||
}
|
||||
if (!String(moduleData?.b_name || "").trim()) {
|
||||
message.warning("请填写模块名称");
|
||||
return;
|
||||
@@ -478,6 +550,7 @@ 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;
|
||||
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
||||
saveReqs.push({
|
||||
@@ -502,6 +575,13 @@ async function saveAll() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -510,6 +590,10 @@ async function saveAll() {
|
||||
if (currentOperationId !== operationId || 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") {
|
||||
const i18nReq = await i18nPanel.buildSaveRequest();
|
||||
if (i18nReq) saveReqs.push(i18nReq);
|
||||
@@ -859,19 +943,6 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
<span class="mm-header-title">{{
|
||||
selectedModule.b_name || selectedModule.b_code
|
||||
}}</span>
|
||||
<span class="mm-header-code">{{ selectedModule.b_code || "(未设置编码)" }}</span>
|
||||
<span class="mm-header-type">
|
||||
<a-tag>{{
|
||||
selectedModule.b_module_type === "directory"
|
||||
? "目录"
|
||||
: selectedModule.b_module_type === "page"
|
||||
? "页面"
|
||||
: "数据"
|
||||
}}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Tab 面板 -->
|
||||
@@ -943,7 +1014,7 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
v-else-if="tab.key === 'i18n'"
|
||||
:ref="setI18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:i18n-keys="i18nKeys"
|
||||
:i18n-items="i18nItems"
|
||||
:loading="busy"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
@@ -1051,22 +1122,6 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mm-header-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
.mm-header-code {
|
||||
font-size: 13px;
|
||||
color: var(--fms-text-secondary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.mm-header-type {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.mm-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
Reference in new issue
Block a user