feat(config): add system configuration module (Phase 3)
Implement the complete erp-config crate with: - Data dictionaries (CRUD + items management) - Dynamic menus (tree structure with role filtering) - System settings (hierarchical: platform > tenant > org > user) - Numbering rules (concurrency-safe via PostgreSQL advisory_lock) - Theme and language configuration (via settings store) - 6 database migrations (dictionaries, menus, settings, numbering_rules) - Frontend Settings page with 5 tabs (dictionary, menu, numbering, settings, theme) Refactor: move RBAC functions (require_permission) from erp-auth to erp-core to avoid cross-module dependencies. Add 20 new seed permissions for config module operations.
This commit is contained in:
298
apps/web/src/pages/settings/NumberingRules.tsx
Normal file
298
apps/web/src/pages/settings/NumberingRules.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Popconfirm,
|
||||
message,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, NumberOutlined } from '@ant-design/icons';
|
||||
import client from '../../api/client';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface NumberingRule {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
prefix?: string;
|
||||
date_format?: string;
|
||||
seq_length: number;
|
||||
seq_start: number;
|
||||
current_value: number;
|
||||
separator?: string;
|
||||
reset_cycle: 'never' | 'daily' | 'monthly' | 'yearly';
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
const resetCycleOptions = [
|
||||
{ label: '不重置', value: 'never' },
|
||||
{ label: '每天', value: 'daily' },
|
||||
{ label: '每月', value: 'monthly' },
|
||||
{ label: '每年', value: 'yearly' },
|
||||
];
|
||||
|
||||
const resetCycleLabels: Record<string, string> = {
|
||||
never: '不重置',
|
||||
daily: '每天',
|
||||
monthly: '每月',
|
||||
yearly: '每年',
|
||||
};
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export default function NumberingRules() {
|
||||
const [rules, setRules] = useState<NumberingRule[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editRule, setEditRule] = useState<NumberingRule | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchRules = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: resp } = await client.get('/config/numbering-rules');
|
||||
setRules(resp.data ?? resp);
|
||||
} catch {
|
||||
message.error('加载编号规则失败');
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRules();
|
||||
}, [fetchRules]);
|
||||
|
||||
const handleSubmit = async (values: {
|
||||
name: string;
|
||||
code: string;
|
||||
prefix?: string;
|
||||
date_format?: string;
|
||||
seq_length: number;
|
||||
seq_start: number;
|
||||
separator?: string;
|
||||
reset_cycle: 'never' | 'daily' | 'monthly' | 'yearly';
|
||||
}) => {
|
||||
try {
|
||||
if (editRule) {
|
||||
await client.put(`/config/numbering-rules/${editRule.id}`, values);
|
||||
message.success('编号规则更新成功');
|
||||
} else {
|
||||
await client.post('/config/numbering-rules', values);
|
||||
message.success('编号规则创建成功');
|
||||
}
|
||||
closeModal();
|
||||
fetchRules();
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '操作失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await client.delete(`/config/numbering-rules/${id}`);
|
||||
message.success('编号规则已删除');
|
||||
fetchRules();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async (rule: NumberingRule) => {
|
||||
try {
|
||||
const { data: resp } = await client.post(
|
||||
`/config/numbering-rules/${rule.id}/generate`,
|
||||
);
|
||||
const generated = resp.data?.number ?? resp.data ?? resp.number ?? resp;
|
||||
message.success(`生成编号: ${generated}`);
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '生成编号失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setEditRule(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
seq_length: 4,
|
||||
seq_start: 1,
|
||||
separator: '-',
|
||||
reset_cycle: 'never',
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (rule: NumberingRule) => {
|
||||
setEditRule(rule);
|
||||
form.setFieldsValue({
|
||||
name: rule.name,
|
||||
code: rule.code,
|
||||
prefix: rule.prefix,
|
||||
date_format: rule.date_format,
|
||||
seq_length: rule.seq_length,
|
||||
seq_start: rule.seq_start,
|
||||
separator: rule.separator,
|
||||
reset_cycle: rule.reset_cycle,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditRule(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '编码', dataIndex: 'code', key: 'code' },
|
||||
{
|
||||
title: '前缀',
|
||||
dataIndex: 'prefix',
|
||||
key: 'prefix',
|
||||
render: (v?: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '日期格式',
|
||||
dataIndex: 'date_format',
|
||||
key: 'date_format',
|
||||
render: (v?: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '序列长度',
|
||||
dataIndex: 'seq_length',
|
||||
key: 'seq_length',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'current_value',
|
||||
key: 'current_value',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '重置周期',
|
||||
dataIndex: 'reset_cycle',
|
||||
key: 'reset_cycle',
|
||||
width: 100,
|
||||
render: (v: string) => resetCycleLabels[v] ?? v,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (_: unknown, record: NumberingRule) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<NumberOutlined />}
|
||||
onClick={() => handleGenerate(record)}
|
||||
>
|
||||
生成编号
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除此编号规则?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
编号规则管理
|
||||
</Typography.Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={rules}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editRule ? '编辑编号规则' : '新建编号规则'}
|
||||
open={modalOpen}
|
||||
onCancel={closeModal}
|
||||
onOk={() => form.submit()}
|
||||
width={560}
|
||||
>
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入规则名称' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="编码"
|
||||
rules={[{ required: true, message: '请输入规则编码' }]}
|
||||
>
|
||||
<Input disabled={!!editRule} />
|
||||
</Form.Item>
|
||||
<Form.Item name="prefix" label="前缀">
|
||||
<Input placeholder="如 PO、SO" />
|
||||
</Form.Item>
|
||||
<Form.Item name="date_format" label="日期格式">
|
||||
<Input placeholder="如 YYYYMMDD" />
|
||||
</Form.Item>
|
||||
<Form.Item name="separator" label="分隔符">
|
||||
<Input placeholder="默认 -" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="seq_length"
|
||||
label="序列长度"
|
||||
rules={[{ required: true, message: '请输入序列长度' }]}
|
||||
>
|
||||
<InputNumber min={1} max={20} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="seq_start" label="起始值" initialValue={1}>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reset_cycle"
|
||||
label="重置周期"
|
||||
rules={[{ required: true, message: '请选择重置周期' }]}
|
||||
>
|
||||
<Select options={resetCycleOptions} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user