fix(web): 系统设置 CRUD 修复 — version 乐观锁 + 语言字段映射 + JSON 显示
- 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:
@@ -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 } });
|
||||
}
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
删除
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
删除
|
||||
|
||||
@@ -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>
|
||||
删除
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user