20260722173146

This commit is contained in:
oneao committed 2026-07-22 17:31:48 +08:00
1 parent e9f099f044
commit d04fad24ed
24 files changed
+1309 -422

No files matched your search

@@ -3,15 +3,20 @@ import { computed, onMounted, reactive, ref } from "vue";
import { App } from "antdv-next";
import { Database, FileText, RotateCcw, Save, ShieldCheck, Trash2 } from "@lucide/vue";
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
import { createTempId } from "@/utils/tempId";
import { useAuthStore } from "@/stores/auth";
import { usePermissionStore } from "@/stores/permissions";
import { I18N_LOCALE_CODES } from "@/utils/i18n";
// 默认语言:由实体的 b_name 字段承载,不写入 b_i18n 翻译表(方案 A)。
const I18N_DEFAULT_LOCALE = "zh-CN";
import ModuleBasicInfoPanel from "./components/ModuleBasicInfoPanel.vue";
import ModuleFieldDefinitionPanel from "./components/ModuleFieldDefinitionPanel.vue";
import ModulePowerPanel from "./components/ModulePowerPanel.vue";
import ModuleTreePanel from "./components/ModuleTreePanel.vue";
const PROTECTED_MODULE_CODES = ["m_module", "m_i18n", "m_biz"];
// 受保护的模块(禁止删除)。当前放开为可删除,留空即不做保护。
const PROTECTED_MODULE_CODES = [];
const { message } = App.useApp();
const authStore = useAuthStore();
const permissionStore = usePermissionStore();
@@ -22,8 +27,8 @@ const activeTab = ref("basic");
const isNew = ref(false);
const deleteConfirmOpen = ref(false);
const pendingDeleteKey = ref("");
const original = ref("");
const originalTranslationKeys = ref([]);
let originalDraft = null;
const data = reactive({
modules: [],
fields: [],
@@ -38,7 +43,6 @@ const canCreate = computed(() => permissionStore.canPower("m_module", "create"))
const canUpdate = computed(() => permissionStore.canPower("m_module", "update"));
const canDelete = computed(() => permissionStore.canPower("m_module", "delete"));
const canSave = computed(() => (isNew.value ? canCreate.value : canUpdate.value));
const dirty = computed(() => Boolean(draft.module) && serializeDraft() !== original.value);
const parentOptions = computed(() =>
data.modules
.filter((item) => item.b_id !== draft.module?.b_id && item.b_type !== "data")
@@ -61,15 +65,6 @@ function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function serializeDraft() {
return JSON.stringify({
module: draft.module,
fields: draft.fields,
powers: draft.powers,
translations: draft.translations,
});
}
function rowKeys() {
return [
draft.module?.b_i18n,
@@ -85,7 +80,7 @@ function clearDraft() {
draft.fields = [];
draft.powers = [];
draft.translations = {};
original.value = "";
originalDraft = null;
originalTranslationKeys.value = [];
activeTab.value = "basic";
}
@@ -108,12 +103,18 @@ function selectModule(moduleCode) {
data.translations
.filter((item) => keys.has(item.b_key))
.forEach((item) => {
if (item.b_locale === I18N_DEFAULT_LOCALE) return;
draft.translations[item.b_key] ||= {};
draft.translations[item.b_key][item.b_locale] = item.b_value;
});
originalTranslationKeys.value = Array.from(keys);
original.value = serializeDraft();
originalDraft = clone({
module: draft.module,
fields: draft.fields,
powers: draft.powers,
translations: draft.translations,
});
if (module.b_type === "directory") activeTab.value = "basic";
}
@@ -189,12 +190,10 @@ async function addModule(kind, contextKey) {
draft.fields = [];
draft.powers = [];
originalTranslationKeys.value = [];
draft.translations = {
["module." + code]: { "zh-CN": draft.module.b_name },
};
draft.translations = {};
originalDraft = { module: null, fields: [], powers: [], translations: {} };
selectedKey.value = code;
isNew.value = true;
original.value = "";
activeTab.value = "basic";
}
@@ -278,6 +277,10 @@ function diffRows(current, previous) {
};
}
function sameRecord(current, previous) {
return JSON.stringify(current) === JSON.stringify(previous);
}
async function translationChanges(previousKeys) {
const currentKeys = new Set([...rowKeys(), ...Object.keys(draft.translations)]);
const relevantKeys = new Set([...previousKeys, ...currentKeys]);
@@ -285,8 +288,9 @@ async function translationChanges(previousKeys) {
const desired = [];
const missing = [];
const editableLocales = I18N_LOCALE_CODES.filter((locale) => locale !== I18N_DEFAULT_LOCALE);
for (const key of currentKeys) {
for (const locale of I18N_LOCALE_CODES) {
for (const locale of editableLocales) {
const value = draft.translations[key]?.[locale]?.trim();
if (!value) continue;
const old = oldRows.find((item) => item.b_key === key && item.b_locale === locale);
@@ -301,15 +305,10 @@ async function translationChanges(previousKeys) {
.filter((item) => !I18N_LOCALE_CODES.includes(item.b_locale))
.forEach((item) => desired.push(item));
const ids = [];
for (let index = 0; index < missing.length; index += 1) {
const response = await nextIdApi();
ids.push(String(response.data));
}
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
missing.forEach((item, index) => {
missing.forEach((item) => {
desired.push({
b_id: ids[index],
b_id: createTempId(),
b_key: item.key,
b_locale: item.locale,
b_value: item.value,
@@ -339,15 +338,9 @@ async function saveModule() {
saving.value = true;
try {
const oldModule = isNew.value
? null
: data.modules.find((item) => item.b_code === selectedKey.value);
const oldFields = oldModule
? data.fields.filter((item) => item.b_module_id === oldModule.b_id)
: [];
const oldPowers = oldModule
? data.powers.filter((item) => item.b_module_id === oldModule.b_id)
: [];
const oldModule = originalDraft?.module ?? null;
const oldFields = originalDraft?.fields ?? [];
const oldPowers = originalDraft?.powers ?? [];
const previousKeys = new Set(
[
...originalTranslationKeys.value,
@@ -380,18 +373,21 @@ async function saveModule() {
table: "b_module",
key_field: "b_id",
inserts: isNew.value ? [draft.module] : [],
updates: isNew.value ? [] : [draft.module],
updates: !isNew.value && !sameRecord(draft.module, oldModule) ? [draft.module] : [],
deletes: [],
},
{ table: "b_module_field", key_field: "b_id", ...fieldChanges },
{ table: "b_module_power", key_field: "b_id", ...powerChanges },
{ table: "b_i18n", key_field: "b_id", ...i18nChanges },
);
await saveObjectApi(
requests.filter(
(request) => request.inserts.length || request.updates.length || request.deletes.length,
),
const pendingRequests = requests.filter(
(request) => request.inserts.length || request.updates.length || request.deletes.length,
);
if (!pendingRequests.length) {
message.info("没有需要保存的修改");
return;
}
await saveObjectApi(pendingRequests);
const savedKey = draft.module.b_code;
message.success("模块配置已保存");
@@ -553,9 +549,7 @@ onMounted(async () => {
</div>
<div class="header-actions">
<a-button :disabled="!dirty" @click="resetDraft">
<RotateCcw :size="15" />取消
</a-button>
<a-button @click="resetDraft"> <RotateCcw :size="15" />取消 </a-button>
<a-popconfirm
title="确定删除当前模块吗?"
:disabled="
@@ -572,12 +566,7 @@ onMounted(async () => {
<Trash2 :size="15" />删除
</a-button>
</a-popconfirm>
<a-button
type="primary"
:loading="saving"
:disabled="!dirty || !canSave"
@click="saveModule"
>
<a-button type="primary" :loading="saving" :disabled="!canSave" @click="saveModule">
<Save :size="15" />保存
</a-button>
</div>
@@ -604,8 +593,10 @@ onMounted(async () => {
</template>
<ModuleFieldDefinitionPanel
v-model:fields="draft.fields"
v-model:translations="draft.translations"
:module-id="draft.module.b_id"
:module-code="draft.module.b_code"
:view-table="draft.module.b_viewtable || draft.module.b_savetable"
/>
</a-tab-pane>
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="powers">
@@ -616,6 +607,7 @@ onMounted(async () => {
</template>
<ModulePowerPanel
v-model:powers="draft.powers"
v-model:translations="draft.translations"
:module-id="draft.module.b_id"
:module-code="draft.module.b_code"
/>