fix: resolve E2E audit findings and add Phase C frontend pages
- Fix audit_log handler multi-tenant bug: use Extension<TenantContext>
instead of hardcoded default_tenant_id
- Fix sendMessage route mismatch: frontend /messages/send → /messages
- Add POST /users/{id}/roles backend route for role assignment
- Add task.completed event payload: started_by + instance_id for
notification delivery
- Add audit log viewer frontend page (AuditLogViewer.tsx)
- Add language management frontend page (LanguageManager.tsx)
- Add api/auditLogs.ts and api/languages.ts modules
This commit is contained in:
210
apps/web/src/pages/settings/LanguageManager.tsx
Normal file
210
apps/web/src/pages/settings/LanguageManager.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { useState, useEffect, 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user