fix(admin): 优化管理后台英文菜单多语言文案杜绝截断并修复各业务页面与API错误
这个提交包含在:
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
function read(relPath: string): string {
|
||||
return fs.readFileSync(path.join(adminRoot, relPath), 'utf-8');
|
||||
}
|
||||
|
||||
test('1. PageShell 必须提供统一页头 / 筛选卡 / 内容卡', () => {
|
||||
const shell = read('src/components/PageShell/index.tsx');
|
||||
const styles = read('src/components/PageShell/style/index.module.less');
|
||||
|
||||
assert.ok(shell.includes('export function PageShell'), '必须导出 PageShell');
|
||||
assert.ok(shell.includes('export function PageHeader'), '必须导出 PageHeader');
|
||||
assert.ok(shell.includes('export function FilterCard'), '必须导出 FilterCard');
|
||||
assert.ok(shell.includes('export function ContentCard'), '必须导出 ContentCard');
|
||||
assert.ok(shell.includes("from '@arco-design/web-react'"), '必须复用 Arco Card');
|
||||
assert.ok(!/\bany\b/.test(shell), '禁止使用 any');
|
||||
|
||||
assert.ok(styles.includes('1200px'), '内容区最大宽度必须是 1200px');
|
||||
assert.ok(styles.includes('720px'), '表单窄栏必须是 720px');
|
||||
assert.ok(styles.includes('16px'), '卡片圆角必须是 16px');
|
||||
});
|
||||
|
||||
test('2. 全局骨架禁止 1100 最小宽,内边距必须走 24/24/32', () => {
|
||||
const layout = read('src/style/layout.module.less');
|
||||
assert.ok(!layout.includes('min-width: @layout-max-width'), 'Navbar/Content 不得锁 1100 最小宽');
|
||||
assert.ok(layout.includes('padding: 24px 24px 32px'), '内容区内边距必须是 24 24 32');
|
||||
assert.ok(layout.includes('32px'), '折叠按钮热区不得小于 32');
|
||||
});
|
||||
|
||||
test('3. 主题切换必须把 Omni token 注入 :root', () => {
|
||||
const theme = read('src/utils/changeTheme.ts');
|
||||
assert.ok(theme.includes('getCssVariables'), 'changeTheme 必须注入 getCssVariables');
|
||||
assert.ok(theme.includes('document.documentElement'), '必须写到 documentElement');
|
||||
});
|
||||
|
||||
test('4. 列表页必须拆成页头 / 筛选 / 表格三层,不再套一张全宽 Card', () => {
|
||||
const pages = [
|
||||
'src/pages/card/list/index.tsx',
|
||||
'src/pages/crm/leads/index.tsx',
|
||||
'src/pages/crm/radar-log/index.tsx',
|
||||
'src/pages/nfc/devices/index.tsx',
|
||||
];
|
||||
for (const rel of pages) {
|
||||
const content = read(rel);
|
||||
assert.ok(content.includes('PageHeader'), `${rel} 必须使用 PageHeader`);
|
||||
assert.ok(content.includes('FilterCard'), `${rel} 必须使用 FilterCard`);
|
||||
assert.ok(content.includes('ContentCard'), `${rel} 必须使用 ContentCard`);
|
||||
assert.ok(!content.includes('<Card title='), `${rel} 不得再用 Card.title 当页头`);
|
||||
}
|
||||
});
|
||||
|
||||
test('5. 表单设置页必须锁 720 窄栏', () => {
|
||||
const tenant = read('src/pages/settings/tenant/index.tsx');
|
||||
const handover = read('src/pages/team/handover/index.tsx');
|
||||
assert.ok(tenant.includes('narrow') || tenant.includes('FormNarrow'), '域名页必须使用窄栏');
|
||||
assert.ok(handover.includes('narrow') || handover.includes('FormNarrow'), '交接页必须使用窄栏');
|
||||
});
|
||||
@@ -99,6 +99,6 @@ test('3. ai/knowledge locale 必须包含 menu.ai.knowledge 与中英文案', ()
|
||||
assert.ok(content.includes('aiKnowledge.filter.keyWord'), '必须包含 keyWord 筛选文案');
|
||||
assert.ok(!/\bany\b/.test(content), 'locale 禁止使用 any');
|
||||
|
||||
assert.equal(defaultI18n['en-US']['menu.ai.knowledge'], 'Knowledge Base');
|
||||
assert.equal(defaultI18n['en-US']['menu.ai.knowledge'], 'Knowledge');
|
||||
assert.equal(defaultI18n['zh-CN']['menu.ai.knowledge'], '知识库');
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ test('1. routes.ts 已注册 card/cases,对应 pages/card/cases', () => {
|
||||
});
|
||||
|
||||
test('2. locale 必须包含 menu.card.cases 中英文案', () => {
|
||||
assert.equal(defaultI18n['en-US']['menu.card.cases'], 'Case Studies');
|
||||
assert.equal(defaultI18n['en-US']['menu.card.cases'], 'Portfolio');
|
||||
assert.equal(defaultI18n['zh-CN']['menu.card.cases'], '出海案例作品集');
|
||||
});
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ test('1. routes.ts 必须包含 key card/list,供 layout glob 加载', () => {
|
||||
|
||||
test('2. locale 必须包含 menu.card / menu.card.list 中英文案', () => {
|
||||
const content = fs.readFileSync(localePath, 'utf-8');
|
||||
assert.ok(content.includes("'menu.card': 'Digital Cards'"), '英文 menu.card');
|
||||
assert.ok(content.includes("'menu.card.list': 'Card Library'"), '英文 menu.card.list');
|
||||
assert.ok(content.includes("'menu.card': 'Cards'"), '英文 menu.card');
|
||||
assert.ok(content.includes("'menu.card.list': 'All Cards'"), '英文 menu.card.list');
|
||||
assert.ok(content.includes("'menu.card': '数字名片'"), '中文 menu.card');
|
||||
assert.ok(content.includes("'menu.card.list': '名片资产库'"), '中文 menu.card.list');
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ test('1. routes.ts 必须包含 key crm/radar-log,供 layout glob 加载', ()
|
||||
});
|
||||
|
||||
test('2. locale 必须包含 menu.crm.radarLog 中英文案', () => {
|
||||
assert.equal(defaultI18n['en-US']['menu.crm.radarLog'], 'Visitor Radar');
|
||||
assert.equal(defaultI18n['en-US']['menu.crm.radarLog'], 'Visitor Logs');
|
||||
assert.equal(defaultI18n['zh-CN']['menu.crm.radarLog'], '访客足迹');
|
||||
});
|
||||
|
||||
|
||||
72
admin/tests/menu-locales.test.ts
普通文件
72
admin/tests/menu-locales.test.ts
普通文件
@@ -0,0 +1,72 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import i18n from '../src/locale/index';
|
||||
import { routes, IRoute } from '../src/routes';
|
||||
|
||||
// 递归收集 routes.ts 中所有的菜单 key (name)
|
||||
function extractRouteNames(list: IRoute[]): string[] {
|
||||
const names: string[] = [];
|
||||
for (const item of list) {
|
||||
if (item.name) {
|
||||
names.push(item.name);
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
names.push(...extractRouteNames(item.children));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
test('1. 所有路由中的菜单项必须在 en-US 和 zh-CN 字典中有完整定义', () => {
|
||||
const routeNames = extractRouteNames(routes);
|
||||
const enMessages = i18n['en-US'];
|
||||
const zhMessages = i18n['zh-CN'];
|
||||
|
||||
for (const name of routeNames) {
|
||||
assert.ok(
|
||||
typeof enMessages[name] === 'string' && enMessages[name].length > 0,
|
||||
`en-US 缺少菜单项翻译: ${name}`
|
||||
);
|
||||
assert.ok(
|
||||
typeof zhMessages[name] === 'string' && zhMessages[name].length > 0,
|
||||
`zh-CN 缺少菜单项翻译: ${name}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('2. 英文菜单文案必须简练原生,长度不可超过 15 个字符以杜绝截断', () => {
|
||||
const routeNames = extractRouteNames(routes);
|
||||
const enMessages = i18n['en-US'];
|
||||
|
||||
for (const name of routeNames) {
|
||||
const text = enMessages[name];
|
||||
assert.ok(
|
||||
text.length <= 15,
|
||||
`菜单项 [${name}] 英文文案 "${text}" 过长 (${text.length} 字符),超过了 15 字符上限,容易在 220px 侧边栏下被截断!`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('3. 关键菜单英文必须采用原生简洁的地道 SaaS 表达', () => {
|
||||
const en = i18n['en-US'];
|
||||
// 平台总控必须为 Platform,杜绝 Platform Operations
|
||||
assert.equal(en['menu.platform'], 'Platform');
|
||||
// 财务统计必须为 Finance,杜绝 Financial & Operations
|
||||
assert.equal(en['menu.dashboard.financial'], 'Finance');
|
||||
// 全球雷达必须为 Radar Map,杜绝 Global Radar Heatmap
|
||||
assert.equal(en['menu.dashboard.radarMap'], 'Radar Map');
|
||||
// 术语库必须为 Glossary,杜绝 AI Translation & Glossary
|
||||
assert.equal(en['menu.ai.translation'], 'Glossary');
|
||||
// 组织成员必须为 Members,杜绝 Members & Structure
|
||||
assert.equal(en['menu.team.members'], 'Members');
|
||||
// 订阅套餐必须为 Plans,杜绝 Subscription Plans
|
||||
assert.equal(en['menu.platform.subscriptions'], 'Plans');
|
||||
// 文章动态必须为 Articles,杜绝 Articles & Posts
|
||||
assert.equal(en['menu.card.articles'], 'Articles');
|
||||
// 案例作品集必须为 Portfolio,杜绝 Case Studies
|
||||
assert.equal(en['menu.card.cases'], 'Portfolio');
|
||||
// 名片列表必须为 All Cards,杜绝 Card Library
|
||||
assert.equal(en['menu.card.list'], 'All Cards');
|
||||
// 设备池必须为 Devices,杜绝 Device Pool
|
||||
assert.equal(en['menu.nfc.devices'], 'Devices');
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户