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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ export interface SettingInfo {
scope_id?: string;
setting_key: string;
setting_value: unknown;
version: number;
}
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;
}
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 }>(
`/config/settings/${key}`,
{ setting_value: settingValue },
{ setting_value: settingValue, version },
);
return data.data;
}
export async function deleteSetting(key: string) {
await client.delete(`/config/settings/${encodeURIComponent(key)}`);
export async function deleteSetting(key: string, version: number) {
await client.delete(`/config/settings/${encodeURIComponent(key)}`, { data: { version } });
}

View File

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

View File

@@ -3,8 +3,6 @@ import {
Table,
Switch,
Modal,
Form,
Input,
Button,
Space,
Typography,
@@ -25,7 +23,6 @@ export default function LanguageManager() {
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);
@@ -44,15 +41,15 @@ export default function LanguageManager() {
// --- Enable / Disable Toggle ---
const handleToggle = async (record: LanguageInfo, enabled: boolean) => {
const handleToggle = async (record: LanguageInfo, checked: boolean) => {
try {
await updateLanguage(record.code, { enabled });
await updateLanguage(record.code, { is_active: checked });
setLanguages((prev) =>
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 {
message.error('操作失败');
}
@@ -62,45 +59,20 @@ export default function LanguageManager() {
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 }) => {
const handleEditSubmit = async () => {
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,
is_active: editingLang.is_active,
});
setLanguages((prev) =>
prev.map((lang) =>
@@ -134,19 +106,19 @@ export default function LanguageManager() {
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
dataIndex: 'is_active',
key: 'is_active',
width: 120,
render: (enabled: boolean, record: LanguageInfo) => (
<Switch checked={enabled} onChange={(checked) => handleToggle(record, checked)} />
render: (is_active: boolean, record: LanguageInfo) => (
<Switch checked={is_active} onChange={(checked) => handleToggle(record, checked)} />
),
},
{
title: '翻译条目数',
key: 'translationCount',
title: '状态标签',
key: 'statusLabel',
width: 140,
render: (_: unknown, record: LanguageInfo) =>
record.translations ? Object.keys(record.translations).length : 0,
record.is_active ? '已启用' : '已禁用',
},
{
title: '操作',
@@ -183,27 +155,20 @@ export default function LanguageManager() {
{/* Edit Modal */}
<Modal
title={`编辑语言 - ${editingLang?.name ?? ''}`}
title={`语言详情 - ${editingLang?.name ?? ''}`}
open={editModalOpen}
onCancel={closeEdit}
onOk={() => editForm.submit()}
onOk={() => handleEditSubmit()}
>
<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>
<div style={{ marginBottom: 12 }}>
<strong></strong>{editingLang?.code}
</div>
<div style={{ marginBottom: 12 }}>
<strong></strong>{editingLang?.name}
</div>
<div>
<strong></strong>{editingLang?.is_active ? '已启用' : '已禁用'}
</div>
</Modal>
</div>
);

View File

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

View File

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

View File

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