mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-11-02 03:01:52 +08:00
feat(menu): multilingual menu titles (form + render utility)
Replaces the "store an i18n key in menu_ext.content.title and resolve it via frontend locales" design with per-locale titles maintained by the admin in the menu CRUD form. The menu store type gains a titles: Record<string,string> field, and MenuEditForm now asks for Chinese / English display names directly instead of an nav.* key. A new resolveMenuTitle(node) helper in utils/MenuUtil.ts gives the rest of the UI a single place to pick the title with the right fallback chain: titles[currentLocale] → titles.en → any populated locale → legacy content.title (via $t for nav.* keys still in the database) → menuName. Any time a menu is rendered — sidebar, breadcrumb, settings left nav — it should go through this helper. Old rows that still carry content.title keep rendering via the legacy-fallback branch until the dc3-docker migration converts them to titles; no hard cutover required.
This commit is contained in:
@@ -28,6 +28,13 @@ export interface MenuNode {
|
||||
menuIndex?: number;
|
||||
menuExt?: {
|
||||
content?: {
|
||||
// Authoritative locale → display-name map. Backed by
|
||||
// MenuExt.Content.titles on the server; populated via the menu CRUD
|
||||
// form (Chinese Title / English Title inputs).
|
||||
titles?: Record<string, string>;
|
||||
// Legacy i18n key (e.g. "nav.home") retained for backwards
|
||||
// compatibility with rows that pre-date the multilingual-titles
|
||||
// migration. New UI should treat this as a final fallback only.
|
||||
title?: string;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2016-present the IoT DC3 original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import i18n from '@/config/i18n';
|
||||
import type { MenuNode } from '@/store/modules/menu';
|
||||
|
||||
/**
|
||||
* Resolve the display name of a menu node given the currently active locale.
|
||||
*
|
||||
* Priority:
|
||||
* 1. menuExt.content.titles[currentLocale] — authoritative per-locale
|
||||
* title maintained from the menu CRUD form.
|
||||
* 2. menuExt.content.titles.en — English fallback when the locale the
|
||||
* user picked has no explicit entry.
|
||||
* 3. Legacy menuExt.content.title — may be either an i18n key (old seed
|
||||
* data like `"nav.home"`) or plain text; fed through $t so existing
|
||||
* `nav.*` keys keep resolving, and falls through to the raw string
|
||||
* when the key is not registered in locales.
|
||||
* 4. menuName — the English machine name stored on the row, so the UI
|
||||
* never shows a blank label.
|
||||
*/
|
||||
export function resolveMenuTitle(node: Partial<MenuNode> | null | undefined): string {
|
||||
if (!node) return '';
|
||||
const content = node.menuExt?.content;
|
||||
if (content) {
|
||||
const locale = i18n.global.locale.value as string;
|
||||
const titles = content.titles;
|
||||
if (titles) {
|
||||
if (titles[locale]) return titles[locale];
|
||||
if (titles.en) return titles.en;
|
||||
// Any other locale populated on the row — pick the first non-empty
|
||||
// entry so a row authored only in e.g. Japanese doesn't render blank.
|
||||
for (const key of Object.keys(titles)) {
|
||||
if (titles[key]) return titles[key];
|
||||
}
|
||||
}
|
||||
if (content.title) {
|
||||
const translated = i18n.global.t(content.title);
|
||||
return translated && translated !== content.title ? translated : content.title;
|
||||
}
|
||||
}
|
||||
return node.menuName || '';
|
||||
}
|
||||
@@ -52,6 +52,12 @@
|
||||
:placeholder="t('settings.menu.menuCodePlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('settings.menu.titleZh')" prop="titleZh">
|
||||
<el-input v-model="reactiveData.form.titleZh" clearable :placeholder="t('settings.menu.titleZhPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('settings.menu.titleEn')" prop="titleEn">
|
||||
<el-input v-model="reactiveData.form.titleEn" clearable :placeholder="t('settings.menu.titleEnPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('settings.menu.menuType')" prop="menuTypeFlag">
|
||||
<el-select v-model="reactiveData.form.menuTypeFlag">
|
||||
<el-option v-for="opt in MENU_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
|
||||
@@ -32,6 +32,8 @@ const createEmptyForm = () => ({
|
||||
menuTypeFlag: 'COMMON' as string,
|
||||
menuLevel: 'C1' as string,
|
||||
menuIndex: 0 as number,
|
||||
titleZh: '',
|
||||
titleEn: '',
|
||||
icon: '',
|
||||
url: '',
|
||||
enableFlag: 'ENABLE' as string,
|
||||
@@ -63,6 +65,8 @@ export default defineComponent({
|
||||
const rules: FormRules = {
|
||||
menuName: [{ required: true, message: t('settings.menu.menuNamePlaceholder'), trigger: 'blur' }],
|
||||
menuCode: [{ required: true, message: t('settings.menu.menuCodePlaceholder'), trigger: 'blur' }],
|
||||
titleZh: [{ required: true, message: t('settings.menu.titleZhPlaceholder'), trigger: 'blur' }],
|
||||
titleEn: [{ required: true, message: t('settings.menu.titleEnPlaceholder'), trigger: 'blur' }],
|
||||
menuTypeFlag: [{ required: true, trigger: 'change' }],
|
||||
menuLevel: [{ required: true, trigger: 'change' }],
|
||||
};
|
||||
@@ -85,11 +89,20 @@ export default defineComponent({
|
||||
const showEdit = (row: any) => {
|
||||
reactiveData.mode = 'edit';
|
||||
const content = row?.menuExt?.content || {};
|
||||
const titles = content.titles || {};
|
||||
// Legacy rows may still carry content.title (an i18n key like
|
||||
// "nav.home"). Keep the form usable on old data by falling back to
|
||||
// the resolved i18n text, then to menuName, so editors never face a
|
||||
// blank title input.
|
||||
const legacyTitle =
|
||||
typeof content.title === 'string' && content.title ? t(content.title, content.title) : row?.menuName || '';
|
||||
const initial = {
|
||||
...createEmptyForm(),
|
||||
...row,
|
||||
parentMenuId: row?.parentMenuId ?? 0,
|
||||
menuIndex: row?.menuIndex ?? 0,
|
||||
titleZh: titles.zh || legacyTitle,
|
||||
titleEn: titles.en || legacyTitle,
|
||||
icon: content.icon || '',
|
||||
url: content.url || '',
|
||||
};
|
||||
@@ -107,12 +120,18 @@ export default defineComponent({
|
||||
const valid = await formRef.value?.validate().catch(() => false);
|
||||
if (!valid) return;
|
||||
reactiveData.submitting = true;
|
||||
const { icon, url, ...rest } = reactiveData.form;
|
||||
const { icon, url, titleZh, titleEn, ...rest } = reactiveData.form;
|
||||
const payload: Record<string, any> = {
|
||||
...rest,
|
||||
menuExt: {
|
||||
content: {
|
||||
title: rest.menuName,
|
||||
// Authoritative locale → display-name map. Picked up by the
|
||||
// sidebar / breadcrumb / header via content.titles[currentLocale]
|
||||
// with fallback to content.titles.en.
|
||||
titles: {
|
||||
zh: titleZh,
|
||||
en: titleEn,
|
||||
},
|
||||
icon: icon || '',
|
||||
url: url || '',
|
||||
remark: rest.remark || '',
|
||||
|
||||
Reference in New Issue
Block a user