20260728225443
This commit is contained in:
1 parent
5ec7aadef9
commit
f6b79ea09a
30 files changed
+1541
-1120
No files matched your search
@@ -44,9 +44,12 @@ const treeData = computed(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedGroup = computed(() =>
|
||||
localGroups.value.find((g) => String(g.b_id) === selectedGroupId.value),
|
||||
);
|
||||
const selectedGroup = computed(() => {
|
||||
for (const group of localGroups.value) {
|
||||
if (String(group.b_id) === selectedGroupId.value) return group;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function onTreeSelect(keys) {
|
||||
if (keys.length > 0) {
|
||||
@@ -72,14 +75,25 @@ function addGroup(parentId) {
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
const hasChildren = localGroups.value.some(
|
||||
(group) => String(group.b_parent_id) === selectedGroupId.value,
|
||||
);
|
||||
let hasChildren = false;
|
||||
for (const group of localGroups.value) {
|
||||
if (String(group.b_parent_id) === selectedGroupId.value) {
|
||||
hasChildren = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasChildren) {
|
||||
message.warning("该分组下还有子分组,请先删除子分组");
|
||||
return;
|
||||
}
|
||||
if (props.referencedGroupIds.map(String).includes(selectedGroupId.value)) {
|
||||
let isReferenced = false;
|
||||
for (const groupId of props.referencedGroupIds) {
|
||||
if (String(groupId) === selectedGroupId.value) {
|
||||
isReferenced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isReferenced) {
|
||||
message.warning("该分组仍被列表或编辑配置引用,请先清空引用");
|
||||
return;
|
||||
}
|
||||
@@ -88,7 +102,14 @@ function deleteGroup() {
|
||||
}
|
||||
|
||||
async function saveAllGroups() {
|
||||
if (localGroups.value.some((group) => !String(group.b_title || "").trim())) {
|
||||
let hasEmptyTitle = false;
|
||||
for (const group of localGroups.value) {
|
||||
if (!String(group.b_title || "").trim()) {
|
||||
hasEmptyTitle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasEmptyTitle) {
|
||||
message.warning("请填写分组标题");
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +182,7 @@ function onClose() {
|
||||
<a-input-number v-model:value="selectedGroup.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch
|
||||
<a-checkbox
|
||||
:checked="selectedGroup.b_canuse === 1"
|
||||
@update:checked="(v) => (selectedGroup.b_canuse = v ? 1 : 0)"
|
||||
/>
|
||||
|
||||
@@ -2,30 +2,18 @@
|
||||
import { ref } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
// 快速设置多语言弹窗:按 key 加载 b_i18n 各语言值,保存后 emit("saved", key)。
|
||||
// 调用方通过 ref 调用 open(key, defaultValue),defaultValue 用于默认语言的初始值。
|
||||
|
||||
const emit = defineEmits(["saved"]);
|
||||
const appStore = useAppStore();
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const i18nKey = ref("");
|
||||
const entries = ref([]);
|
||||
const langTypes = ref([]);
|
||||
|
||||
async function loadLangTypes() {
|
||||
try {
|
||||
const res = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
langTypes.value = res.data || [];
|
||||
} catch (e) {
|
||||
message.error("加载语言类型失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
async function open(key, defaultValue = "") {
|
||||
if (!key) {
|
||||
@@ -35,22 +23,38 @@ async function open(key, defaultValue = "") {
|
||||
i18nKey.value = key;
|
||||
const escaped = key.replace(/'/g, "''");
|
||||
try {
|
||||
const [i18nRes] = await Promise.all([
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("b_i18n", `b_key = '${escaped}'`, "b_locale ASC"),
|
||||
loadLangTypes(),
|
||||
loadDataApi("b_i18n_type", "b_canuse = 1", "b_default DESC, b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
const i18nRes = responses[0];
|
||||
const langRes = responses[1];
|
||||
const rows = i18nRes.data || [];
|
||||
const byLocale = Object.fromEntries(rows.map((r) => [r.b_locale, r]));
|
||||
entries.value = langTypes.value.map((t) => {
|
||||
const ex = byLocale[t.b_code];
|
||||
const isDefault = t.b_default === 1 || t.b_code === "zh-CN";
|
||||
return {
|
||||
b_id: ex ? ex.b_id : null,
|
||||
b_locale: t.b_code,
|
||||
b_name: t.b_name,
|
||||
b_value: ex ? ex.b_value : isDefault ? defaultValue : "",
|
||||
};
|
||||
});
|
||||
const languages = langRes.data || [];
|
||||
const byLocale = {};
|
||||
for (const row of rows) {
|
||||
byLocale[row.b_locale] = row;
|
||||
}
|
||||
|
||||
const defaultLanguage = languages.find((item) => Number(item.b_default) === 1);
|
||||
const defaultLocale = String(defaultLanguage?.b_code || languages[0]?.b_code || "");
|
||||
const values = [];
|
||||
for (const language of languages) {
|
||||
const existing = byLocale[language.b_code];
|
||||
let value = "";
|
||||
if (existing) {
|
||||
value = existing.b_value;
|
||||
} else if (language.b_code === defaultLocale) {
|
||||
value = defaultValue;
|
||||
}
|
||||
values.push({
|
||||
b_id: existing ? existing.b_id : null,
|
||||
b_locale: language.b_code,
|
||||
b_name: language.b_name,
|
||||
b_value: value,
|
||||
});
|
||||
}
|
||||
entries.value = values;
|
||||
} catch {
|
||||
entries.value = [];
|
||||
}
|
||||
@@ -66,7 +70,7 @@ async function save() {
|
||||
try {
|
||||
const req = { table: "b_i18n", key_field: "b_id", inserts: [], updates: [] };
|
||||
for (const e of entries.value) {
|
||||
const value = e.b_value ?? "";
|
||||
const value = e.b_value == null ? "" : e.b_value;
|
||||
if (!e.b_id && !value) continue;
|
||||
const row = { b_key: i18nKey.value, b_locale: e.b_locale, b_value: value };
|
||||
if (e.b_id) req.updates.push({ ...row, b_id: e.b_id });
|
||||
@@ -74,6 +78,7 @@ async function save() {
|
||||
}
|
||||
if (req.inserts.length || req.updates.length) {
|
||||
await saveObjectApi([req]);
|
||||
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
|
||||
}
|
||||
message.success("多语言已保存");
|
||||
visible.value = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import FmsMemoInput from "@/components/FmsMemoInput.vue";
|
||||
import { buildModuleI18nKey } from "@/utils/i18n";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
@@ -24,8 +25,12 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isPage = computed(() => formData.value?.b_module_type === "page");
|
||||
const isData = computed(() => formData.value?.b_module_type === "data");
|
||||
const isPage = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "page";
|
||||
});
|
||||
const isData = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "data";
|
||||
});
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit("update", formData.value);
|
||||
@@ -55,10 +60,12 @@ function onModuleTypeChange(v) {
|
||||
const i18nModalRef = ref(null);
|
||||
|
||||
function openI18nModal() {
|
||||
const key =
|
||||
formData.value?.b_i18n ||
|
||||
buildModuleI18nKey(formData.value?.b_module_type, formData.value?.b_code);
|
||||
i18nModalRef.value?.open(key, formData.value?.b_name || "");
|
||||
if (!formData.value || !i18nModalRef.value) return;
|
||||
let key = formData.value.b_i18n;
|
||||
if (!key) {
|
||||
key = buildModuleI18nKey(formData.value.b_module_type, formData.value.b_code);
|
||||
}
|
||||
i18nModalRef.value.open(key, formData.value.b_name || "");
|
||||
}
|
||||
|
||||
function onI18nSaved(key) {
|
||||
@@ -155,9 +162,10 @@ function onI18nSaved(key) {
|
||||
</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)"
|
||||
<FmsMemoInput
|
||||
:model-value="formData.b_orderfield || ''"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v) => setField('b_orderfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
@@ -22,7 +22,11 @@ watch(
|
||||
() => props.editConfig,
|
||||
(val) => {
|
||||
if (val === localEdit.value) return;
|
||||
localEdit.value = val?.map((r) => ({ ...r })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
localEdit.value = rows;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -32,24 +36,31 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localEdit.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
const sortedRows = [];
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
sortedRows.push({ ...rows[index], b_xh: (index + 1) * 10 });
|
||||
}
|
||||
localEdit.value = sortedRows;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
for (const field of props.fields || []) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
const groupOptions = computed(() => {
|
||||
const options = [];
|
||||
for (const group of props.groups || []) {
|
||||
if (group.b_canuse !== 1) continue;
|
||||
options.push({ label: group.b_title || `分组-${group.b_id}`, value: String(group.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
@@ -57,16 +68,6 @@ function deleteRow(record) {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function makeSwitchCol(field, title, width) {
|
||||
return {
|
||||
dataIndex: field,
|
||||
title,
|
||||
width: width || 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
};
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
@@ -77,9 +78,27 @@ const columns = shallowRef([
|
||||
options: () => availableFields.value,
|
||||
},
|
||||
},
|
||||
makeSwitchCol("b_required", "必填"),
|
||||
makeSwitchCol("b_readonly", "只读"),
|
||||
makeSwitchCol("b_disabled", "禁用"),
|
||||
{
|
||||
dataIndex: "b_required",
|
||||
title: "必填",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_readonly",
|
||||
title: "只读",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_disabled",
|
||||
title: "禁用",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "编辑分组",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { h, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ label: "输入框", value: "input" },
|
||||
@@ -10,7 +13,7 @@ const FIELD_TYPES = [
|
||||
{ label: "金额", value: "money" },
|
||||
{ label: "日期", value: "date" },
|
||||
{ label: "日期时间", value: "datetime" },
|
||||
{ label: "开关", value: "switch" },
|
||||
{ label: "复选框", value: "checkbox" },
|
||||
{ label: "下拉选择", value: "select" },
|
||||
];
|
||||
|
||||
@@ -21,16 +24,20 @@ const props = defineProps({
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
const emit = defineEmits(["update", "i18nSaved"]);
|
||||
|
||||
const localFields = ref([]);
|
||||
const activeKey = ref(null);
|
||||
const i18nModalRef = ref(null);
|
||||
|
||||
watch(
|
||||
() => [props.fields, props.moduleCode],
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localFields.value && moduleCode === previous[1]) return;
|
||||
const rows = val?.map((row) => ({ ...row })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
const changed = syncModuleItemI18nKeys(rows, "field", moduleCode, "b_field");
|
||||
localFields.value = rows;
|
||||
if (changed) emit("update", rows);
|
||||
@@ -48,6 +55,59 @@ function onCellChange() {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function openI18n(row) {
|
||||
const key = buildModuleItemI18nKey("field", props.moduleCode, row.b_field);
|
||||
if (!key) {
|
||||
message.warning("请先填写字段名和模块编码");
|
||||
return;
|
||||
}
|
||||
if (i18nModalRef.value) {
|
||||
i18nModalRef.value.open(key, row.b_name || row.b_field || "");
|
||||
}
|
||||
}
|
||||
|
||||
const I18nKeyCell = ({ row, cellValue }) =>
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "4px",
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
"span",
|
||||
{ style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } },
|
||||
cellValue || "",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
title: "设置多语言",
|
||||
style: {
|
||||
flex: "none",
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
cursor: "pointer",
|
||||
padding: "2px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
openI18n(row);
|
||||
},
|
||||
},
|
||||
[h(Languages, { size: 14 })],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||
{
|
||||
@@ -75,6 +135,7 @@ const columns = ref([
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 220,
|
||||
customCell: I18nKeyCell,
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
@@ -99,6 +160,7 @@ const columns = ref([
|
||||
@change="onCellChange"
|
||||
/>
|
||||
</div>
|
||||
<I18nQuickSetModal ref="i18nModalRef" @saved="(key) => emit('i18nSaved', key)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -24,49 +24,59 @@ const langOptions = ref([]);
|
||||
// 扁平原始数据(b_id, b_key, b_locale, b_value),仅用于保存时差量对比。
|
||||
const originalsFlat = ref([]);
|
||||
// 透视后的行数据:一行一个 key,每种语言的值挂在对应 locale 字段上。
|
||||
// 形如 { _id, b_key, "zh-CN": "…", "en-US": "…", _localeIds: { "zh-CN": b_id, … } }
|
||||
const pivotRows = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const searchKey = ref("");
|
||||
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),
|
||||
),
|
||||
]);
|
||||
const managedItems = computed(() => {
|
||||
const items = [];
|
||||
for (const item of props.i18nItems || []) {
|
||||
if (item && item.managed) items.push(item);
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const requestedKeys = computed(() => {
|
||||
const keys = new Set();
|
||||
for (const item of props.i18nItems || []) {
|
||||
if (!item) continue;
|
||||
if (item.key) keys.add(String(item.key));
|
||||
if (item.sourceKey) keys.add(String(item.sourceKey));
|
||||
}
|
||||
return Array.from(keys);
|
||||
});
|
||||
|
||||
function isManagedRow(row) {
|
||||
return Boolean(row?._managedId);
|
||||
return Boolean(row && 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 rowByManagedId = new Map();
|
||||
const unclaimedByKey = new Map();
|
||||
for (const row of rows) {
|
||||
if (row._managedId) {
|
||||
rowByManagedId.set(String(row._managedId), row);
|
||||
} else {
|
||||
unclaimedByKey.set(String(row.b_key), row);
|
||||
}
|
||||
}
|
||||
const currentManagedIds = new Set();
|
||||
const defaultLocale =
|
||||
langOptions.value.find((item) => item.default)?.value || langOptions.value[0]?.value || "";
|
||||
|
||||
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));
|
||||
let row = rowByManagedId.get(managedId);
|
||||
if (!row) row = unclaimedByKey.get(String(item.key));
|
||||
if (!row && item.sourceKey) row = unclaimedByKey.get(String(item.sourceKey));
|
||||
if (!row) {
|
||||
row = { _id: createTempId(), b_key: item.key, _localeIds: {} };
|
||||
rows.push(row);
|
||||
@@ -74,8 +84,13 @@ function reconcileManagedRows(itemsOverride) {
|
||||
|
||||
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;
|
||||
if (
|
||||
defaultLocale &&
|
||||
(row[defaultLocale] == null ||
|
||||
row[defaultLocale] === "" ||
|
||||
row[defaultLocale] === previousDefault)
|
||||
) {
|
||||
row[defaultLocale] = nextDefault;
|
||||
}
|
||||
row.b_key = String(item.key);
|
||||
row._managedId = managedId;
|
||||
@@ -103,10 +118,15 @@ async function loadLangTypes() {
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
langOptions.value = (res.data || []).map((t) => ({
|
||||
label: `${t.b_name} (${t.b_code})`,
|
||||
value: t.b_code,
|
||||
}));
|
||||
const options = [];
|
||||
for (const language of res.data || []) {
|
||||
options.push({
|
||||
label: `${language.b_name} (${language.b_code})`,
|
||||
value: language.b_code,
|
||||
default: Number(language.b_default) === 1,
|
||||
});
|
||||
}
|
||||
langOptions.value = options;
|
||||
} catch (e) {
|
||||
message.error("加载语言类型失败:" + (e.message || e));
|
||||
}
|
||||
@@ -132,32 +152,40 @@ function buildPivot(flat) {
|
||||
}
|
||||
|
||||
async function loadI18n() {
|
||||
const requestId = ++loadRequestId;
|
||||
const moduleId = String(props.moduleId);
|
||||
isLoading.value = true;
|
||||
try {
|
||||
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([
|
||||
const responses = await Promise.all([
|
||||
where
|
||||
? loadDataApi("b_i18n", where, "b_key ASC, b_locale ASC")
|
||||
: Promise.resolve({ data: [] }),
|
||||
loadLangTypes(),
|
||||
]);
|
||||
const flat = (i18nRes.data || []).map((r) => ({
|
||||
b_id: r.b_id,
|
||||
b_key: r.b_key,
|
||||
b_locale: r.b_locale,
|
||||
b_value: r.b_value,
|
||||
}));
|
||||
if (requestId !== loadRequestId) return;
|
||||
const i18nRes = responses[0];
|
||||
const flat = [];
|
||||
for (const row of i18nRes.data || []) {
|
||||
flat.push({
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
});
|
||||
}
|
||||
if (String(props.moduleId) !== moduleId) return;
|
||||
originalsFlat.value = cloneData(flat);
|
||||
pivotRows.value = buildPivot(flat);
|
||||
reconcileManagedRows();
|
||||
} catch (e) {
|
||||
if (requestId === loadRequestId) message.error("加载多语言失败:" + (e.message || e));
|
||||
if (String(props.moduleId) === moduleId) {
|
||||
message.error("加载多语言失败:" + (e.message || e));
|
||||
}
|
||||
} finally {
|
||||
if (requestId === loadRequestId) isLoading.value = false;
|
||||
if (String(props.moduleId) === moduleId) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,12 +194,15 @@ async function refreshKey(key) {
|
||||
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,
|
||||
}));
|
||||
const rows = [];
|
||||
for (const row of res.data || []) {
|
||||
rows.push({
|
||||
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),
|
||||
@@ -194,7 +225,9 @@ async function addRow() {
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
await tableRef.value?.editRow(String(newId), "b_key");
|
||||
if (tableRef.value) {
|
||||
await tableRef.value.editRow(String(newId), "b_key");
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
@@ -272,7 +305,7 @@ function flatten(rows) {
|
||||
// 由父级「同步字段」调用:把默认翻译条目({ key, locale, value, managedId })并入草稿行。
|
||||
// 已存在同 b_key 的行只补全空值(不覆盖用户配置),避免新增重复键;逻辑见 utils/i18n.mergeI18nDefaults。
|
||||
function mergeDefaults(entries) {
|
||||
if (!entries?.length) return;
|
||||
if (!entries || entries.length === 0) return;
|
||||
pivotRows.value = mergeI18nDefaults(pivotRows.value, entries);
|
||||
}
|
||||
|
||||
@@ -296,7 +329,11 @@ async function loadExistingByKeyLocale(keys) {
|
||||
// 由父级统一保存调用:返回 b_i18n 的变更请求(无变更返回 null),保存成功后父级调用 markSaved 同步基线
|
||||
async function buildSaveRequest() {
|
||||
// 保存涉及的键(受管键可能在新建/改名模块时才生成,打开面板时未被加载)
|
||||
const keys = [...new Set(pivotRows.value.map((r) => String(r.b_key)).filter(Boolean))];
|
||||
const keySet = new Set();
|
||||
for (const row of pivotRows.value) {
|
||||
if (row.b_key) keySet.add(String(row.b_key));
|
||||
}
|
||||
const keys = Array.from(keySet);
|
||||
if (keys.length) {
|
||||
const existing = await loadExistingByKeyLocale(keys);
|
||||
if (existing.size) {
|
||||
@@ -362,7 +399,6 @@ const columns = computed(() => {
|
||||
title: "键",
|
||||
width: 240,
|
||||
align: "left",
|
||||
headerAlign: "left",
|
||||
editor: { type: "input", disabled: (row) => isManagedRow(row) },
|
||||
},
|
||||
];
|
||||
@@ -372,7 +408,6 @@ const columns = computed(() => {
|
||||
title: opt.label,
|
||||
width: 180,
|
||||
align: "left",
|
||||
headerAlign: "left",
|
||||
editor: { type: "input" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ watch(
|
||||
() => props.listConfig,
|
||||
(val) => {
|
||||
if (val === localList.value) return;
|
||||
localList.value = val?.map((r) => ({ ...r })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
localList.value = rows;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -32,24 +36,31 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localList.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
const sortedRows = [];
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
sortedRows.push({ ...rows[index], b_xh: (index + 1) * 10 });
|
||||
}
|
||||
localList.value = sortedRows;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
for (const field of props.fields || []) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
const groupOptions = computed(() => {
|
||||
const options = [];
|
||||
for (const group of props.groups || []) {
|
||||
if (group.b_canuse !== 1) continue;
|
||||
options.push({ label: group.b_title || `分组-${group.b_id}`, value: String(group.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
|
||||
@@ -25,7 +25,10 @@ watch(
|
||||
() => [props.powers, props.moduleCode],
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localPowers.value && moduleCode === previous[1]) return;
|
||||
const rows = (val || []).map((row) => ({ ...row }));
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
const changed = syncModuleItemI18nKeys(rows, "power", moduleCode, "b_code");
|
||||
localPowers.value = rows;
|
||||
if (changed) emit("update", rows);
|
||||
@@ -40,7 +43,7 @@ function emitUpdate() {
|
||||
async function addRow() {
|
||||
if (props.loading) return;
|
||||
const newId = createTempId();
|
||||
localPowers.value.push({
|
||||
const newRow = {
|
||||
b_id: newId,
|
||||
b_module_id: props.moduleId,
|
||||
b_code: "",
|
||||
@@ -48,12 +51,17 @@ async function addRow() {
|
||||
b_i18n: null,
|
||||
b_xh: (localPowers.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
};
|
||||
const rows = localPowers.value.slice();
|
||||
rows.push(newRow);
|
||||
localPowers.value = rows;
|
||||
emitUpdate();
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
await tableRef.value?.editRow(String(newId), "b_code");
|
||||
if (tableRef.value) {
|
||||
await tableRef.value.editRow(String(newId), "b_code");
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
@@ -112,7 +120,9 @@ function openI18n(row) {
|
||||
message.warning("请先填写操作编码");
|
||||
return;
|
||||
}
|
||||
i18nModalRef.value?.open(key, row.b_name || "");
|
||||
if (i18nModalRef.value) {
|
||||
i18nModalRef.value.open(key, row.b_name || "");
|
||||
}
|
||||
}
|
||||
|
||||
// 多语言键单元格:值 + 右侧翻译 icon 按钮(原生 button,避免 antdv Button 组件开销)
|
||||
@@ -159,10 +169,30 @@ const I18nKeyCell = ({ row, cellValue }) =>
|
||||
);
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160, editor: { type: "input" } },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160, editor: { type: "input" } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160, customCell: I18nKeyCell },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, editor: { type: "input" } },
|
||||
{
|
||||
dataIndex: "b_code",
|
||||
title: "操作编码",
|
||||
width: 160,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_name",
|
||||
title: "操作名称",
|
||||
width: 160,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 160,
|
||||
customCell: I18nKeyCell,
|
||||
},
|
||||
{
|
||||
dataIndex: "b_xh",
|
||||
title: "排序",
|
||||
width: 80,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
|
||||
+18
-10
@@ -34,7 +34,11 @@ watch(
|
||||
() => props.queryConfig,
|
||||
(val) => {
|
||||
if (val === localQuery.value) return;
|
||||
localQuery.value = val?.map((r) => ({ ...r })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
localQuery.value = rows;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -44,18 +48,22 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localQuery.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
const sortedRows = [];
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
sortedRows.push({ ...rows[index], b_xh: (index + 1) * 10 });
|
||||
}
|
||||
localQuery.value = sortedRows;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
for (const field of props.fields || []) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
function addRow() {
|
||||
if (props.loading) return;
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -1,163 +1,143 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref, shallowRef } from "vue";
|
||||
import { message, Switch, Button, Popconfirm, Tag } from "antdv-next";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi } from "@/services/api";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import { computed, ref } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const appStore = useAppStore();
|
||||
const listRef = ref(null);
|
||||
const selectedRowKeys = ref([]);
|
||||
const saving = ref(false);
|
||||
|
||||
const canCreate = computed(() => permissionStore.canPower("m_i18n_type", "create"));
|
||||
const canUpdate = computed(() => permissionStore.canPower("m_i18n_type", "update"));
|
||||
const canDelete = computed(() => permissionStore.canPower("m_i18n_type", "delete"));
|
||||
const rowSelection = computed(() => {
|
||||
return {
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
getCheckboxProps: (row) => ({ disabled: Number(row.b_default) === 1 }),
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
const rowActions = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: "edit",
|
||||
label: appStore.t("common.edit", "编辑"),
|
||||
disabled: () => saving.value,
|
||||
onClick: openEdit,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
type: "delete",
|
||||
label: appStore.t("common.delete", "删除"),
|
||||
confirm: appStore.t("common.confirmDeleteLanguage", "确定删除该语言?"),
|
||||
disabled: ({ record }) => saving.value || Number(record.b_default) === 1,
|
||||
onClick: removeRow,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
async function reloadList() {
|
||||
selectedRowKeys.value = [];
|
||||
if (listRef.value) await listRef.value.reload();
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n_type", "", "b_xh ASC, b_id ASC");
|
||||
data.value = res.data || [];
|
||||
} catch (e) {
|
||||
message.error("加载多语言类型失败:" + (e?.message || e));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
await appStore.loadLanguages();
|
||||
} catch (error) {
|
||||
message.error("刷新语言配置失败:" + (error?.message || error));
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
|
||||
/* ---------- 行内单元格渲染 ---------- */
|
||||
function createDefaultCell() {
|
||||
return (props) =>
|
||||
props.row.b_default === 1
|
||||
? h(Tag, { color: "blue" }, "默认")
|
||||
: h("span", { class: "muted" }, "—");
|
||||
}
|
||||
|
||||
function createCanuseCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
return h(Switch, {
|
||||
checked: row.b_canuse === 1,
|
||||
size: "small",
|
||||
disabled: !canUpdate.value,
|
||||
"onUpdate:checked": (val) => {
|
||||
const next = val ? 1 : 0;
|
||||
if (row.b_canuse === next) return;
|
||||
row.b_canuse = next;
|
||||
saveRow(row, { b_canuse: next });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createActionsCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
const isDefault = row.b_default === 1;
|
||||
const children = [];
|
||||
if (canUpdate.value) {
|
||||
children.push(
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
type: "link",
|
||||
size: "small",
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
openEdit(row);
|
||||
},
|
||||
},
|
||||
"编辑",
|
||||
),
|
||||
);
|
||||
}
|
||||
if (canDelete.value && !isDefault) {
|
||||
children.push(
|
||||
h(
|
||||
Popconfirm,
|
||||
{
|
||||
title: "确定删除该语言?",
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
onConfirm: () => removeRow(row),
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", danger: true, onClick: (e) => e.stopPropagation() },
|
||||
"删除",
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return h("span", { class: "actions-cell" }, children);
|
||||
};
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
{ dataIndex: "b_code", title: "简称 / 编码", width: 150 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
||||
{
|
||||
dataIndex: "b_default",
|
||||
title: "默认",
|
||||
width: 80,
|
||||
align: "center",
|
||||
customCell: createDefaultCell(),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "是否启用",
|
||||
width: 100,
|
||||
align: "center",
|
||||
customCell: createCanuseCell(),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, align: "center" },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 130,
|
||||
align: "center",
|
||||
customCell: createActionsCell(),
|
||||
},
|
||||
]);
|
||||
|
||||
/* ---------- 行内开关即时保存 ---------- */
|
||||
async function saveRow(row, patch) {
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n_type", key_field: "b_id", updates: [{ b_id: row.b_id, ...patch }] },
|
||||
]);
|
||||
message.success("已更新");
|
||||
} catch (e) {
|
||||
message.error("更新失败:" + (e?.message || e));
|
||||
await loadData();
|
||||
async function getOtherDefaultUpdates(currentId) {
|
||||
const response = await loadDataApi("b_i18n_type", "b_default = 1", "b_id ASC");
|
||||
const updates = [];
|
||||
for (const row of response.data || []) {
|
||||
if (String(row.b_id) === String(currentId)) continue;
|
||||
updates.push({ b_id: row.b_id, b_default: 0 });
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
async function removeRow(row) {
|
||||
if (row.b_default === 1) return;
|
||||
async function updateCheckbox(row, field, checked) {
|
||||
if (saving.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const value = checked ? 1 : 0;
|
||||
const updates = [{ b_id: row.b_id, [field]: value }];
|
||||
if (field === "b_default" && value === 1) {
|
||||
updates.push(...(await getOtherDefaultUpdates(row.b_id)));
|
||||
}
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n_type", key_field: "b_id", deletes: [{ b_id: row.b_id }] },
|
||||
{ table: "b_i18n_type", key_field: "b_id", inserts: [], updates, deletes: [] },
|
||||
]);
|
||||
message.success("删除成功");
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
message.error("删除失败:" + (e?.message || e));
|
||||
message.success(appStore.t("common.updateSuccess", "更新成功"));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(appStore.t("common.updateFailed", "更新失败") + ":" + (error?.message || error));
|
||||
await reloadList();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 新增 / 编辑弹窗 ---------- */
|
||||
async function removeRow(row) {
|
||||
if (saving.value || Number(row.b_default) === 1) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_i18n_type",
|
||||
key_field: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [{ b_id: row.b_id }],
|
||||
},
|
||||
]);
|
||||
message.success(appStore.t("common.deleteSuccess", "删除成功"));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(appStore.t("common.deleteFailed", "删除失败") + ":" + (error?.message || error));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function batchDelete() {
|
||||
if (saving.value || !selectedRowKeys.value.length) return;
|
||||
const keys = [...selectedRowKeys.value];
|
||||
Modal.confirm({
|
||||
title: `${appStore.t("common.confirmBatchDelete", "确定删除选中的记录?")} (${keys.length})`,
|
||||
okText: appStore.t("common.delete", "删除"),
|
||||
okType: "danger",
|
||||
cancelText: appStore.t("common.cancel", "取消"),
|
||||
onOk: async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
const deletes = keys.map((b_id) => ({ b_id }));
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_i18n_type",
|
||||
key_field: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes,
|
||||
},
|
||||
]);
|
||||
message.success(appStore.t("common.deleteSuccess", "删除成功"));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(
|
||||
appStore.t("common.deleteFailed", "删除失败") + ":" + (error?.message || error),
|
||||
);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalMode = ref("create");
|
||||
const editingId = ref(null);
|
||||
@@ -174,63 +154,69 @@ function openCreate() {
|
||||
modalMode.value = "create";
|
||||
editingId.value = null;
|
||||
editingDefault.value = false;
|
||||
form.value = { b_code: "", b_name: "", b_xh: 0, defaultOn: false, canuseOn: true };
|
||||
form.value = {
|
||||
b_code: "",
|
||||
b_name: "",
|
||||
b_xh: 0,
|
||||
defaultOn: false,
|
||||
canuseOn: true,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
modalMode.value = "edit";
|
||||
editingId.value = row.b_id;
|
||||
editingDefault.value = row.b_default === 1;
|
||||
editingDefault.value = Number(row.b_default) === 1;
|
||||
form.value = {
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_code: row.b_code || "",
|
||||
b_name: row.b_name || "",
|
||||
b_xh: row.b_xh ?? 0,
|
||||
defaultOn: row.b_default === 1,
|
||||
canuseOn: row.b_canuse === 1,
|
||||
defaultOn: Number(row.b_default) === 1,
|
||||
canuseOn: Number(row.b_canuse) === 1,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveForm() {
|
||||
const f = form.value;
|
||||
if (!f.b_code || !f.b_code.trim()) {
|
||||
message.warning("请填写语言编码");
|
||||
const current = form.value;
|
||||
if (!String(current.b_code || "").trim()) {
|
||||
message.warning(appStore.t("common.enterLanguageCode", "请填写语言编码"));
|
||||
return;
|
||||
}
|
||||
if (!f.b_name || !f.b_name.trim()) {
|
||||
message.warning("请填写语言名称");
|
||||
if (!String(current.b_name || "").trim()) {
|
||||
message.warning(appStore.t("common.enterLanguageName", "请填写语言名称"));
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const isNew = modalMode.value === "create" || !editingId.value;
|
||||
const b_id = isNew ? (await nextIdApi(1)).data : editingId.value;
|
||||
const isNew = modalMode.value === "create";
|
||||
const b_id = isNew ? (await nextIdApi()).data : editingId.value;
|
||||
const row = {
|
||||
b_id,
|
||||
b_code: f.b_code.trim(),
|
||||
b_name: f.b_name.trim(),
|
||||
b_canuse: f.canuseOn ? 1 : 0,
|
||||
b_default: f.defaultOn ? 1 : 0,
|
||||
b_xh: f.b_xh ?? 0,
|
||||
b_code: current.b_code.trim(),
|
||||
b_name: current.b_name.trim(),
|
||||
b_default: current.defaultOn ? 1 : 0,
|
||||
b_canuse: current.canuseOn ? 1 : 0,
|
||||
b_xh: current.b_xh ?? 0,
|
||||
};
|
||||
const request = {
|
||||
table: "b_i18n_type",
|
||||
key_field: "b_id",
|
||||
inserts: isNew ? [row] : [],
|
||||
updates: isNew ? [] : [row],
|
||||
deletes: [],
|
||||
};
|
||||
const req = { table: "b_i18n_type", key_field: "b_id", inserts: [], updates: [], deletes: [] };
|
||||
if (isNew) req.inserts.push(row);
|
||||
else req.updates.push(row);
|
||||
// 保证仅一个默认语言:若本次设为默认,则将其它语言取消默认
|
||||
if (row.b_default === 1) {
|
||||
for (const r of data.value) {
|
||||
if (r.b_id !== b_id && r.b_default === 1) {
|
||||
req.updates.push({ b_id: r.b_id, b_default: 0 });
|
||||
}
|
||||
}
|
||||
request.updates.push(...(await getOtherDefaultUpdates(b_id)));
|
||||
}
|
||||
await saveObjectApi([req]);
|
||||
message.success("保存成功");
|
||||
await saveObjectApi([request]);
|
||||
message.success(appStore.t("common.saveSuccess", "保存成功"));
|
||||
modalVisible.value = false;
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e?.message || e));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(appStore.t("common.saveFailed", "保存失败") + ":" + (error?.message || error));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -239,106 +225,90 @@ async function saveForm() {
|
||||
|
||||
<template>
|
||||
<div class="i18n-type-page">
|
||||
<div class="panel-card">
|
||||
<div class="panel-toolbar">
|
||||
<a-button v-if="canCreate" type="primary" @click="openCreate">
|
||||
<Plus :size="16" /> 新增语言
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="data"
|
||||
row-key="b_id"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
<FmsModuleListPage
|
||||
ref="listRef"
|
||||
:row-actions="rowActions"
|
||||
:row-selection="rowSelection"
|
||||
@loaded="selectedRowKeys = []"
|
||||
>
|
||||
<template #actions="{ loading }">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="loading || saving"
|
||||
@click="openCreate"
|
||||
>
|
||||
<Plus :size="16" />
|
||||
{{ appStore.t("common.create", "新增") }}
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
:disabled="loading || saving || !selectedRowKeys.length"
|
||||
@click="batchDelete"
|
||||
>
|
||||
<Trash2 :size="16" />
|
||||
{{ appStore.t("common.batchDelete", "批量删除")
|
||||
}}{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<template #cell-b_canuse="{ value, record }">
|
||||
<a-checkbox
|
||||
:checked="Number(value) === 1"
|
||||
:disabled="saving"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@update:checked="(checked) => updateCheckbox(record, 'b_canuse', checked)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-b_default="{ value, record }">
|
||||
<a-checkbox
|
||||
:checked="Number(value) === 1"
|
||||
:disabled="saving"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@update:checked="(checked) => updateCheckbox(record, 'b_default', checked)"
|
||||
/>
|
||||
</template>
|
||||
</FmsModuleListPage>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalMode === 'create' ? '新增语言' : '编辑语言'"
|
||||
:title="
|
||||
modalMode === 'create'
|
||||
? appStore.t('common.createLanguage', '新增语言')
|
||||
: appStore.t('common.editLanguage', '编辑语言')
|
||||
"
|
||||
:confirm-loading="saving"
|
||||
@ok="saveForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="语言编码(简称)" required>
|
||||
<a-form-item :label="appStore.t('common.languageCode', '语言编码')" required>
|
||||
<a-input v-model:value="form.b_code" :disabled="editingDefault" />
|
||||
</a-form-item>
|
||||
<a-form-item label="语言名称" required>
|
||||
<a-form-item :label="appStore.t('common.languageName', '语言名称')" required>
|
||||
<a-input v-model:value="form.b_name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否默认">
|
||||
<a-switch v-model:checked="form.defaultOn" :disabled="editingDefault" />
|
||||
<span class="form-tip">设为默认后,其它语言将自动取消默认</span>
|
||||
<a-form-item :label="appStore.t('common.defaultLanguage', '默认语言')">
|
||||
<a-checkbox v-model:checked="form.defaultOn" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否启用">
|
||||
<a-switch v-model:checked="form.canuseOn" />
|
||||
<a-form-item :label="appStore.t('common.enabled', '启用')">
|
||||
<a-checkbox v-model:checked="form.canuseOn" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序">
|
||||
<a-input-number v-model:value="form.b_xh" :min="0" style="width: 100%" />
|
||||
<a-form-item :label="appStore.t('common.sort', '排序')">
|
||||
<a-input-number v-model:value="form.b_xh" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.i18n-type-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user