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
211 lines
5.2 KiB
TypeScript
211 lines
5.2 KiB
TypeScript
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>
|
||
);
|
||
}
|