Files
erp/apps/web/src/pages/settings/LanguageManager.tsx
iven e16c1a85d7 feat(web): comprehensive frontend performance and UI/UX optimization
Performance improvements:
- Vite build: manual chunks, terser minification, optimizeDeps
- API response caching with 5s TTL via axios interceptors
- React.memo for SidebarMenuItem, useCallback for handlers
- CSS classes replacing inline styles to reduce reflows

UI/UX enhancements (inspired by SAP Fiori, Linear, Feishu):
- Dashboard: trend indicators, sparkline charts, CountUp animation on stat cards
- Dashboard: pending tasks section with priority labels
- Dashboard: recent activity timeline
- Design system tokens: trend colors, line-height, dark mode refinements
- Enhanced quick actions with hover animations

Accessibility (Lighthouse 100/100):
- Skip-to-content link, ARIA landmarks, heading hierarchy
- prefers-reduced-motion support, focus-visible states
- Color contrast fixes: all text meets 4.5:1 ratio
- Keyboard navigation for stat cards and task items

SEO: meta theme-color, format-detection, robots.txt
2026-04-13 01:37:55 +08:00

211 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, useCallback } from 'react';
import {
Table,
Switch,
Modal,
Form,
Input,
Button,
Space,
Typography,
message,
Card,
} from 'antd';
import { EditOutlined } from '@ant-design/icons';
import {
listLanguages,
updateLanguage,
type LanguageInfo,
} from '../../api/languages';
// --- Component ---
export default function LanguageManager() {
const [languages, setLanguages] = useState<LanguageInfo[]>([]);
const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingLang, setEditingLang] = useState<LanguageInfo | null>(null);
const [editForm] = Form.useForm();
const fetchLanguages = useCallback(async () => {
setLoading(true);
try {
const result = await listLanguages();
setLanguages(result);
} catch {
message.error('加载语言列表失败');
}
setLoading(false);
}, []);
useEffect(() => {
fetchLanguages();
}, [fetchLanguages]);
// --- Enable / Disable Toggle ---
const handleToggle = async (record: LanguageInfo, enabled: boolean) => {
try {
await updateLanguage(record.code, { enabled });
setLanguages((prev) =>
prev.map((lang) =>
lang.code === record.code ? { ...lang, enabled } : lang,
),
);
message.success(enabled ? '已启用' : '已禁用');
} catch {
message.error('操作失败');
}
};
// --- Edit Modal ---
const openEdit = (lang: LanguageInfo) => {
setEditingLang(lang);
editForm.setFieldsValue({
name: lang.name,
translations: lang.translations
? Object.entries(lang.translations)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
: '',
});
setEditModalOpen(true);
};
const closeEdit = () => {
setEditModalOpen(false);
setEditingLang(null);
editForm.resetFields();
};
const handleEditSubmit = async (values: { name: string; translations: string }) => {
if (!editingLang) return;
const translations: Record<string, string> = {};
if (values.translations?.trim()) {
for (const line of values.translations.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
const val = trimmed.slice(eqIndex + 1).trim();
if (key) {
translations[key] = val;
}
}
}
try {
const updated = await updateLanguage(editingLang.code, {
name: values.name,
translations,
});
setLanguages((prev) =>
prev.map((lang) =>
lang.code === editingLang.code ? updated : lang,
),
);
message.success('语言更新成功');
closeEdit();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '更新失败';
message.error(errorMsg);
}
};
// --- Columns ---
const columns = [
{
title: '语言代码',
dataIndex: 'code',
key: 'code',
width: 160,
},
{
title: '语言名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
width: 120,
render: (enabled: boolean, record: LanguageInfo) => (
<Switch checked={enabled} onChange={(checked) => handleToggle(record, checked)} />
),
},
{
title: '翻译条目数',
key: 'translationCount',
width: 140,
render: (_: unknown, record: LanguageInfo) =>
record.translations ? Object.keys(record.translations).length : 0,
},
{
title: '操作',
key: 'actions',
render: (_: unknown, record: LanguageInfo) => (
<Space>
<Button
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
</Space>
),
},
];
return (
<div>
<Typography.Title level={5} style={{ marginBottom: 16 }}>
</Typography.Title>
<Card>
<Table
columns={columns}
dataSource={languages}
rowKey="code"
loading={loading}
pagination={false}
/>
</Card>
{/* Edit Modal */}
<Modal
title={`编辑语言 - ${editingLang?.name ?? ''}`}
open={editModalOpen}
onCancel={closeEdit}
onOk={() => editForm.submit()}
>
<Form form={editForm} onFinish={handleEditSubmit} layout="vertical">
<Form.Item
name="name"
label="语言名称"
rules={[{ required: true, message: '请输入语言名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="translations"
label="翻译内容"
extra="每行一条格式key=value"
>
<Input.TextArea rows={10} placeholder={'common.save=保存\ncommon.cancel=取消'} />
</Form.Item>
</Form>
</Modal>
</div>
);
}