fix(web): 系统设置 CRUD 修复 — version 乐观锁 + 语言字段映射 + JSON 显示
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- API 层所有 Info/Request 接口添加 version 字段,update 函数传递 version
- delete 函数改为 client.delete(url, { data: { version } }) 发送 JSON body
- LanguageInfo.enabled → is_active,匹配后端 LanguageResp 字段名
- LanguageManager 编辑弹窗简化为只读详情(后端仅支持 is_active 切换)
- SystemSettings 设置值显示改用 JSON.stringify 而非 String()
- SystemSettings updateSetting 发送解析后的 JSON 对象而非字符串
This commit is contained in:
iven
2026-04-26 01:28:13 +08:00
parent b4735213c5
commit 2539e5fc44
10 changed files with 79 additions and 101 deletions

View File

@@ -8,6 +8,7 @@ export interface DictionaryItemInfo {
value: string; value: string;
sort_order: number; sort_order: number;
color?: string; color?: string;
version: number;
} }
export interface DictionaryInfo { export interface DictionaryInfo {
@@ -16,6 +17,7 @@ export interface DictionaryInfo {
code: string; code: string;
description?: string; description?: string;
items: DictionaryItemInfo[]; items: DictionaryItemInfo[];
version: number;
} }
export interface CreateDictionaryRequest { export interface CreateDictionaryRequest {
@@ -27,6 +29,7 @@ export interface CreateDictionaryRequest {
export interface UpdateDictionaryRequest { export interface UpdateDictionaryRequest {
name?: string; name?: string;
description?: string; description?: string;
version: number;
} }
export async function listDictionaries(page = 1, pageSize = 20) { export async function listDictionaries(page = 1, pageSize = 20) {
@@ -53,8 +56,8 @@ export async function updateDictionary(id: string, req: UpdateDictionaryRequest)
return data.data; return data.data;
} }
export async function deleteDictionary(id: string) { export async function deleteDictionary(id: string, version: number) {
await client.delete(`/config/dictionaries/${id}`); await client.delete(`/config/dictionaries/${id}`, { data: { version } });
} }
export async function listItemsByCode(code: string) { export async function listItemsByCode(code: string) {
@@ -77,6 +80,7 @@ export interface UpdateDictionaryItemRequest {
value?: string; value?: string;
sort_order?: number; sort_order?: number;
color?: string; color?: string;
version: number;
} }
export async function createDictionaryItem( export async function createDictionaryItem(
@@ -102,6 +106,6 @@ export async function updateDictionaryItem(
return data.data; return data.data;
} }
export async function deleteDictionaryItem(dictionaryId: string, itemId: string) { export async function deleteDictionaryItem(dictionaryId: string, itemId: string, version: number) {
await client.delete(`/config/dictionaries/${dictionaryId}/items/${itemId}`); await client.delete(`/config/dictionaries/${dictionaryId}/items/${itemId}`, { data: { version } });
} }

View File

@@ -5,14 +5,11 @@ import client from './client';
export interface LanguageInfo { export interface LanguageInfo {
code: string; code: string;
name: string; name: string;
enabled: boolean; is_active: boolean;
translations?: Record<string, string>;
} }
export interface UpdateLanguageRequest { export interface UpdateLanguageRequest {
name?: string; is_active: boolean;
enabled?: boolean;
translations?: Record<string, string>;
} }
// --- API Functions --- // --- API Functions ---

View File

@@ -11,6 +11,7 @@ export interface MenuInfo {
menu_type: string; menu_type: string;
permission?: string; permission?: string;
children: MenuInfo[]; children: MenuInfo[];
version: number;
} }
export interface MenuItemReq { export interface MenuItemReq {
@@ -24,6 +25,7 @@ export interface MenuItemReq {
menu_type?: string; menu_type?: string;
permission?: string; permission?: string;
role_ids?: string[]; role_ids?: string[];
version?: number;
} }
export async function getMenus() { export async function getMenus() {
@@ -51,6 +53,6 @@ export async function updateMenu(id: string, req: MenuItemReq) {
return data.data; return data.data;
} }
export async function deleteMenu(id: string) { export async function deleteMenu(id: string, version: number) {
await client.delete(`/config/menus/${id}`); await client.delete(`/config/menus/${id}`, { data: { version } });
} }

View File

@@ -13,6 +13,7 @@ export interface NumberingRuleInfo {
separator: string; separator: string;
reset_cycle: string; reset_cycle: string;
last_reset_date?: string; last_reset_date?: string;
version: number;
} }
export interface CreateNumberingRuleRequest { export interface CreateNumberingRuleRequest {
@@ -33,6 +34,7 @@ export interface UpdateNumberingRuleRequest {
seq_length?: number; seq_length?: number;
separator?: string; separator?: string;
reset_cycle?: string; reset_cycle?: string;
version: number;
} }
export async function listNumberingRules(page = 1, pageSize = 20) { export async function listNumberingRules(page = 1, pageSize = 20) {
@@ -66,6 +68,6 @@ export async function generateNumber(id: string) {
return data.data; return data.data;
} }
export async function deleteNumberingRule(id: string) { export async function deleteNumberingRule(id: string, version: number) {
await client.delete(`/config/numbering-rules/${id}`); await client.delete(`/config/numbering-rules/${id}`, { data: { version } });
} }

View File

@@ -6,6 +6,7 @@ export interface SettingInfo {
scope_id?: string; scope_id?: string;
setting_key: string; setting_key: string;
setting_value: unknown; setting_value: unknown;
version: number;
} }
export async function getSetting(key: string, scope?: string, scopeId?: string) { export async function getSetting(key: string, scope?: string, scopeId?: string) {
@@ -16,14 +17,14 @@ export async function getSetting(key: string, scope?: string, scopeId?: string)
return data.data; return data.data;
} }
export async function updateSetting(key: string, settingValue: unknown) { export async function updateSetting(key: string, settingValue: unknown, version?: number) {
const { data } = await client.put<{ success: boolean; data: SettingInfo }>( const { data } = await client.put<{ success: boolean; data: SettingInfo }>(
`/config/settings/${key}`, `/config/settings/${key}`,
{ setting_value: settingValue }, { setting_value: settingValue, version },
); );
return data.data; return data.data;
} }
export async function deleteSetting(key: string) { export async function deleteSetting(key: string, version: number) {
await client.delete(`/config/settings/${encodeURIComponent(key)}`); await client.delete(`/config/settings/${encodeURIComponent(key)}`, { data: { version } });
} }

View File

@@ -66,7 +66,7 @@ export default function DictionaryManager() {
const handleDictSubmit = async (values: CreateDictionaryRequest) => { const handleDictSubmit = async (values: CreateDictionaryRequest) => {
try { try {
if (editDict) { if (editDict) {
await updateDictionary(editDict.id, values); await updateDictionary(editDict.id, { ...values, version: editDict.version });
message.success('字典更新成功'); message.success('字典更新成功');
} else { } else {
await createDictionary(values); await createDictionary(values);
@@ -82,9 +82,9 @@ export default function DictionaryManager() {
} }
}; };
const handleDeleteDict = async (id: string) => { const handleDeleteDict = async (id: string, version: number) => {
try { try {
await deleteDictionary(id); await deleteDictionary(id, version);
message.success('字典已删除'); message.success('字典已删除');
fetchDictionaries(); fetchDictionaries();
} catch { } catch {
@@ -139,7 +139,7 @@ export default function DictionaryManager() {
if (!activeDictId) return; if (!activeDictId) return;
try { try {
if (editItem) { if (editItem) {
await updateDictionaryItem(activeDictId, editItem.id, values as UpdateDictionaryItemRequest); await updateDictionaryItem(activeDictId, editItem.id, { ...values, version: editItem.version } as UpdateDictionaryItemRequest);
message.success('字典项更新成功'); message.success('字典项更新成功');
} else { } else {
await createDictionaryItem(activeDictId, values); await createDictionaryItem(activeDictId, values);
@@ -155,9 +155,9 @@ export default function DictionaryManager() {
} }
}; };
const handleDeleteItem = async (dictId: string, itemId: string) => { const handleDeleteItem = async (dictId: string, itemId: string, version: number) => {
try { try {
await deleteDictionaryItem(dictId, itemId); await deleteDictionaryItem(dictId, itemId, version);
message.success('字典项已删除'); message.success('字典项已删除');
fetchDictionaries(); fetchDictionaries();
} catch { } catch {
@@ -196,7 +196,7 @@ export default function DictionaryManager() {
</Button> </Button>
<Popconfirm <Popconfirm
title="确定删除此字典?" title="确定删除此字典?"
onConfirm={() => handleDeleteDict(record.id)} onConfirm={() => handleDeleteDict(record.id, record.version)}
> >
<Button size="small" danger> <Button size="small" danger>
@@ -232,7 +232,7 @@ export default function DictionaryManager() {
</Button> </Button>
<Popconfirm <Popconfirm
title="确定删除此字典项?" title="确定删除此字典项?"
onConfirm={() => handleDeleteItem(dictId, record.id)} onConfirm={() => handleDeleteItem(dictId, record.id, record.version)}
> >
<Button size="small" danger> <Button size="small" danger>

View File

@@ -3,8 +3,6 @@ import {
Table, Table,
Switch, Switch,
Modal, Modal,
Form,
Input,
Button, Button,
Space, Space,
Typography, Typography,
@@ -25,7 +23,6 @@ export default function LanguageManager() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [editingLang, setEditingLang] = useState<LanguageInfo | null>(null); const [editingLang, setEditingLang] = useState<LanguageInfo | null>(null);
const [editForm] = Form.useForm();
const fetchLanguages = useCallback(async () => { const fetchLanguages = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -44,15 +41,15 @@ export default function LanguageManager() {
// --- Enable / Disable Toggle --- // --- Enable / Disable Toggle ---
const handleToggle = async (record: LanguageInfo, enabled: boolean) => { const handleToggle = async (record: LanguageInfo, checked: boolean) => {
try { try {
await updateLanguage(record.code, { enabled }); await updateLanguage(record.code, { is_active: checked });
setLanguages((prev) => setLanguages((prev) =>
prev.map((lang) => prev.map((lang) =>
lang.code === record.code ? { ...lang, enabled } : lang, lang.code === record.code ? { ...lang, is_active: checked } : lang,
), ),
); );
message.success(enabled ? '已启用' : '已禁用'); message.success(checked ? '已启用' : '已禁用');
} catch { } catch {
message.error('操作失败'); message.error('操作失败');
} }
@@ -62,45 +59,20 @@ export default function LanguageManager() {
const openEdit = (lang: LanguageInfo) => { const openEdit = (lang: LanguageInfo) => {
setEditingLang(lang); setEditingLang(lang);
editForm.setFieldsValue({
name: lang.name,
translations: lang.translations
? Object.entries(lang.translations)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
: '',
});
setEditModalOpen(true); setEditModalOpen(true);
}; };
const closeEdit = () => { const closeEdit = () => {
setEditModalOpen(false); setEditModalOpen(false);
setEditingLang(null); setEditingLang(null);
editForm.resetFields();
}; };
const handleEditSubmit = async (values: { name: string; translations: string }) => { const handleEditSubmit = async () => {
if (!editingLang) return; 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 { try {
const updated = await updateLanguage(editingLang.code, { const updated = await updateLanguage(editingLang.code, {
name: values.name, is_active: editingLang.is_active,
translations,
}); });
setLanguages((prev) => setLanguages((prev) =>
prev.map((lang) => prev.map((lang) =>
@@ -134,19 +106,19 @@ export default function LanguageManager() {
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'enabled', dataIndex: 'is_active',
key: 'enabled', key: 'is_active',
width: 120, width: 120,
render: (enabled: boolean, record: LanguageInfo) => ( render: (is_active: boolean, record: LanguageInfo) => (
<Switch checked={enabled} onChange={(checked) => handleToggle(record, checked)} /> <Switch checked={is_active} onChange={(checked) => handleToggle(record, checked)} />
), ),
}, },
{ {
title: '翻译条目数', title: '状态标签',
key: 'translationCount', key: 'statusLabel',
width: 140, width: 140,
render: (_: unknown, record: LanguageInfo) => render: (_: unknown, record: LanguageInfo) =>
record.translations ? Object.keys(record.translations).length : 0, record.is_active ? '已启用' : '已禁用',
}, },
{ {
title: '操作', title: '操作',
@@ -183,27 +155,20 @@ export default function LanguageManager() {
{/* Edit Modal */} {/* Edit Modal */}
<Modal <Modal
title={`编辑语言 - ${editingLang?.name ?? ''}`} title={`语言详情 - ${editingLang?.name ?? ''}`}
open={editModalOpen} open={editModalOpen}
onCancel={closeEdit} onCancel={closeEdit}
onOk={() => editForm.submit()} onOk={() => handleEditSubmit()}
> >
<Form form={editForm} onFinish={handleEditSubmit} layout="vertical"> <div style={{ marginBottom: 12 }}>
<Form.Item <strong></strong>{editingLang?.code}
name="name" </div>
label="语言名称" <div style={{ marginBottom: 12 }}>
rules={[{ required: true, message: '请输入语言名称' }]} <strong></strong>{editingLang?.name}
> </div>
<Input /> <div>
</Form.Item> <strong></strong>{editingLang?.is_active ? '已启用' : '已禁用'}
<Form.Item </div>
name="translations"
label="翻译内容"
extra="每行一条格式key=value"
>
<Input.TextArea rows={10} placeholder={'common.save=保存\ncommon.cancel=取消'} />
</Form.Item>
</Form>
</Modal> </Modal>
</div> </div>
); );

View File

@@ -94,7 +94,7 @@ export default function MenuConfig() {
const handleSubmit = async (values: MenuItemReq) => { const handleSubmit = async (values: MenuItemReq) => {
try { try {
if (editMenu) { if (editMenu) {
await updateMenu(editMenu.id, values); await updateMenu(editMenu.id, { ...values, version: editMenu.version });
message.success('菜单更新成功'); message.success('菜单更新成功');
} else { } else {
await createMenu(values); await createMenu(values);
@@ -110,9 +110,9 @@ export default function MenuConfig() {
} }
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string, version: number) => {
try { try {
await deleteMenu(id); await deleteMenu(id, version);
message.success('菜单已删除'); message.success('菜单已删除');
fetchMenus(); fetchMenus();
} catch { } catch {
@@ -203,7 +203,7 @@ export default function MenuConfig() {
</Button> </Button>
<Popconfirm <Popconfirm
title="确定删除此菜单?" title="确定删除此菜单?"
onConfirm={() => handleDelete(record.id)} onConfirm={() => handleDelete(record.id, record.version)}
> >
<Button size="small" danger> <Button size="small" danger>

View File

@@ -71,7 +71,7 @@ export default function NumberingRules() {
const handleSubmit = async (values: CreateNumberingRuleRequest) => { const handleSubmit = async (values: CreateNumberingRuleRequest) => {
try { try {
if (editRule) { if (editRule) {
await updateNumberingRule(editRule.id, values as UpdateNumberingRuleRequest); await updateNumberingRule(editRule.id, { ...values, version: editRule.version } as UpdateNumberingRuleRequest);
message.success('编号规则更新成功'); message.success('编号规则更新成功');
} else { } else {
await createNumberingRule(values); await createNumberingRule(values);
@@ -87,9 +87,9 @@ export default function NumberingRules() {
} }
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string, version: number) => {
try { try {
await deleteNumberingRule(id); await deleteNumberingRule(id, version);
message.success('编号规则已删除'); message.success('编号规则已删除');
fetchRules(); fetchRules();
} catch { } catch {
@@ -193,7 +193,7 @@ export default function NumberingRules() {
</Button> </Button>
<Popconfirm <Popconfirm
title="确定删除此编号规则?" title="确定删除此编号规则?"
onConfirm={() => handleDelete(record.id)} onConfirm={() => handleDelete(record.id, record.version)}
> >
<Button size="small" danger> <Button size="small" danger>

View File

@@ -21,6 +21,7 @@ import { useThemeMode } from '../../hooks/useThemeMode';
interface SettingEntry { interface SettingEntry {
key: string; key: string;
value: string; value: string;
version: number;
} }
export default function SystemSettings() { export default function SystemSettings() {
@@ -38,16 +39,19 @@ export default function SystemSettings() {
} }
try { try {
const result = await getSetting(searchKey.trim()); const result = await getSetting(searchKey.trim());
const value = String(result.setting_value ?? ''); const value = typeof result.setting_value === 'object' && result.setting_value !== null
? JSON.stringify(result.setting_value, null, 2)
: String(result.setting_value ?? '');
const version = result.version;
setEntries((prev) => { setEntries((prev) => {
const exists = prev.findIndex((e) => e.key === searchKey.trim()); const exists = prev.findIndex((e) => e.key === searchKey.trim());
if (exists >= 0) { if (exists >= 0) {
const updated = [...prev]; const updated = [...prev];
updated[exists] = { ...updated[exists], value }; updated[exists] = { ...updated[exists], value, version };
return updated; return updated;
} }
return [...prev, { key: searchKey.trim(), value }]; return [...prev, { key: searchKey.trim(), value, version }];
}); });
message.success('查询成功'); message.success('查询成功');
} catch (err: unknown) { } catch (err: unknown) {
@@ -71,16 +75,19 @@ export default function SystemSettings() {
return; return;
} }
await updateSetting(key, value); const editVersion = editEntry?.version;
const jsonValue = JSON.parse(value);
const result = await updateSetting(key, jsonValue, editVersion);
setEntries((prev) => { setEntries((prev) => {
const displayValue = typeof jsonValue === 'object' ? JSON.stringify(jsonValue, null, 2) : value;
const exists = prev.findIndex((e) => e.key === key); const exists = prev.findIndex((e) => e.key === key);
if (exists >= 0) { if (exists >= 0) {
const updated = [...prev]; const updated = [...prev];
updated[exists] = { key, value }; updated[exists] = { key, value: displayValue, version: result.version };
return updated; return updated;
} }
return [...prev, { key, value }]; return [...prev, { key, value: displayValue, version: result.version }];
}); });
message.success('设置已保存'); message.success('设置已保存');
@@ -92,9 +99,9 @@ export default function SystemSettings() {
} }
}; };
const handleDelete = async (key: string) => { const handleDelete = async (key: string, version: number) => {
try { try {
await deleteSetting(key); await deleteSetting(key, version);
setEntries((prev) => prev.filter((e) => e.key !== key)); setEntries((prev) => prev.filter((e) => e.key !== key));
message.success('设置已删除'); message.success('设置已删除');
} catch { } catch {
@@ -165,7 +172,7 @@ export default function SystemSettings() {
/> />
<Popconfirm <Popconfirm
title="确定删除此设置?" title="确定删除此设置?"
onConfirm={() => handleDelete(record.key)} onConfirm={() => handleDelete(record.key, record.version)}
> >
<Button <Button
size="small" size="small"