settings
This commit is contained in:
@@ -1,12 +1,33 @@
|
||||
import { DownOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Button, Dropdown, Popconfirm, Space } from 'antd';
|
||||
import { App, Avatar, Button, Dropdown, Popconfirm, Space } from 'antd';
|
||||
import { navigate } from '@/router/routerUtils';
|
||||
import { AdminServices } from '@/services/AdminServices';
|
||||
import { useUserStore } from '@/store/UserStore';
|
||||
import { imgWithPrefix } from '@/utils/common';
|
||||
import { useRequest } from '@/utils/useRequest';
|
||||
import { GapBox } from '../GapBox';
|
||||
|
||||
export const HeaderUserInfo: React.FC = () => {
|
||||
const userInfo = useUserStore().user;
|
||||
const user = useUserStore();
|
||||
const userInfo = user.user;
|
||||
const { notification } = App.useApp();
|
||||
|
||||
const { request: logoutRequest } = useRequest(AdminServices.logout, {
|
||||
onSuccess: (res) => {
|
||||
if (res.err_code == 0) {
|
||||
notification.success({ title: '退出登录成功' });
|
||||
user.updateUser({});
|
||||
// updateCompany({erp_name: DefaultERPName})
|
||||
// const hash = window.location.hash;
|
||||
// const end = hash.indexOf('?') > 1 ? hash.indexOf('?') : hash.length;
|
||||
// const pathname = hash.substring(1, end);
|
||||
// if (!pathname.includes('/login')) {
|
||||
// localStorage.setItem(`u${user.user.user_id}_c${company.company_id}_lastRouter`, pathname);
|
||||
// }
|
||||
navigate('/login');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<GapBox>
|
||||
@@ -34,7 +55,7 @@ export const HeaderUserInfo: React.FC = () => {
|
||||
size={40}
|
||||
src={`${imgWithPrefix(userInfo.avatar)}?v=${encodeURIComponent(userInfo.update_date)}`}
|
||||
icon={<UserOutlined />}
|
||||
style={{ marginBottom: 4 }} // 👈 控制头像和名字的间距
|
||||
style={{ marginBottom: 4 }} // 控制头像和名字的间距
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
@@ -100,7 +121,16 @@ export const HeaderUserInfo: React.FC = () => {
|
||||
<Popconfirm
|
||||
title='确定要退出登录吗?'
|
||||
onConfirm={() => {
|
||||
location.hash = '#/login';
|
||||
new Promise<void>((resolve, reject) => {
|
||||
logoutRequest().then((res) => {
|
||||
if (res?.err_code == 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
// location.hash = '#/login';
|
||||
}}
|
||||
>
|
||||
<Button size='small' variant='text' color='primary'>
|
||||
|
||||
@@ -26,6 +26,11 @@ const companyMenu: MenuDataItem = {
|
||||
icon: <TeamOutlined style={iconStyle} />,
|
||||
children: [{ name: '企业信息', path: '/company/list', auth: 'SF_ADMIN_COMPANY_VIEW' }],
|
||||
};
|
||||
const configMenu: MenuDataItem = {
|
||||
name: '配置管理',
|
||||
icon: <TeamOutlined style={iconStyle} />,
|
||||
children: [{ name: '配置信息', path: '/config/list', auth: '' }],
|
||||
};
|
||||
const authMenu: MenuDataItem = {
|
||||
name: '权限管理',
|
||||
icon: <BarsOutlined style={iconStyle} />,
|
||||
@@ -48,6 +53,7 @@ const asideMenuConfig: MenuDataItem[] = [
|
||||
},
|
||||
userMenu,
|
||||
companyMenu,
|
||||
// configMenu,
|
||||
authMenu,
|
||||
];
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { lazy } from 'react';
|
||||
import AppLayout from '@/layouts/AppLayout';
|
||||
import EmptyLayout from '@/layouts/EmptyLayout';
|
||||
import CompanyList from '@/pages/Company/List';
|
||||
import ConfigList from '@/pages/Config';
|
||||
import ErrorPage from '@/pages/Error';
|
||||
import Index from '@/pages/Index';
|
||||
import Login from '@/pages/Record/Login';
|
||||
@@ -39,6 +40,14 @@ export const routes: IRouteItem[] = [
|
||||
// { path: '/group', Component: lazy(() => import('@/pages/Staff/group')) },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
Layout: AppLayout,
|
||||
children: [
|
||||
{ path: '/list', Component: ConfigList },
|
||||
// { path: '/group', Component: lazy(() => import('@/pages/Staff/group')) },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/staff',
|
||||
Layout: AppLayout,
|
||||
|
||||
@@ -129,7 +129,7 @@ export const CompanyEditModal: React.FC<IProps> = (props) => {
|
||||
<Form.Item
|
||||
label='密码'
|
||||
name='password'
|
||||
tooltip={data ? undefined : '不填写则系统自动生成初始密码'}
|
||||
tooltip={data ? undefined : '初始密码:123456'}
|
||||
rules={[{ min: 6, max: 18, message: '密码需6~18位' }]}
|
||||
>
|
||||
<Input.Password allowClear placeholder={'不填写则系统自动生成初始密码'} />
|
||||
|
||||
310
src/pages/Config/index.tsx
Normal file
310
src/pages/Config/index.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { FileTextOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
message,
|
||||
Select,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// --- 复杂类型:可编辑表格组件 (用于分段价格等 array + table 模式) ---
|
||||
interface EditableTableProps {
|
||||
value?: any[];
|
||||
onChange?: (value: any[]) => void;
|
||||
schema?: {
|
||||
columns: Array<{
|
||||
title: string;
|
||||
dataIndex: string;
|
||||
type: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
const EditableTable = ({ value = [], onChange, schema }: EditableTableProps) => {
|
||||
const columns = (schema?.columns || []).map((col: any) => ({
|
||||
title: col.title,
|
||||
dataIndex: col.dataIndex,
|
||||
render: (text: any, record: any, index: number) => {
|
||||
const inputProps = {
|
||||
value: text,
|
||||
// 这里 val 可能是 Event 对象也可能是数值,antd 的 InputNumber 直接返回 val
|
||||
// 普通 Input 返回 e.target.value
|
||||
onChange: (e: any) => {
|
||||
const val = e?.target ? e.target.value : e;
|
||||
const newData = [...value];
|
||||
newData[index] = { ...newData[index], [col.dataIndex]: val };
|
||||
onChange?.(newData); // 使用可选链调用,安全消灭波浪线
|
||||
},
|
||||
};
|
||||
|
||||
return col.type === 'int' ? <InputNumber {...inputProps} style={{ width: '100%' }} /> : <Input {...inputProps} />;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div style={{ border: '1px solid #f0f0f0', padding: 8, borderRadius: 4 }}>
|
||||
<Table
|
||||
dataSource={value}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size='small'
|
||||
// 显式声明 i 为 number,消灭 rowKey 处的波浪线
|
||||
rowKey={(_: any, i: any) => i.toString()}
|
||||
/>
|
||||
<Button
|
||||
type='dashed'
|
||||
block
|
||||
// 关键点:这里也需要使用 ?. 确保 onChange 存在时才调用
|
||||
onClick={() => onChange?.([...value, {}])}
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
新增一行
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigList = () => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 模拟从 SQL 加载并解析数据
|
||||
useEffect(() => {
|
||||
const rawSqlData = [
|
||||
{
|
||||
id: 1,
|
||||
config_key: 'site_name',
|
||||
label: '站点名称',
|
||||
type: 'string',
|
||||
value: 'ERP系统',
|
||||
props: '{"placeholder":"请输入","maxLength":10}',
|
||||
rules: '{"required":true}',
|
||||
},
|
||||
{ id: 2, config_key: 'site_desc', label: '站点描述', type: 'text', value: '', props: '{"rows":4}' },
|
||||
{ id: 3, config_key: 'qty_precision', label: '数量精度', type: 'int', value: 2, props: '{"min":0,"max":5}' },
|
||||
{ id: 4, config_key: 'is_open', label: '系统开关', type: 'bool', value: 1 },
|
||||
{
|
||||
id: 5,
|
||||
config_key: 'tags',
|
||||
label: '标签',
|
||||
type: 'array',
|
||||
value: ['ERP', 'SaaS'],
|
||||
array_config: '{"mode":"tags"}',
|
||||
options: '[{"label":"ERP","value":"ERP"},{"label":"SaaS","value":"SaaS"}]',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
config_key: 'notify_mode',
|
||||
label: '通知方式',
|
||||
type: 'array',
|
||||
value: [1, 2],
|
||||
array_config: '{"mode":"checkbox"}',
|
||||
options: '[{"label":"短信","value":1},{"label":"邮件","value":2}]',
|
||||
},
|
||||
{ id: 8, config_key: 'birth_date', label: '日期', type: 'date', value: '2026-01-01' },
|
||||
{ id: 9, config_key: 'expire_time', label: '时间', type: 'datetime', value: '2026-01-01 12:00:00' },
|
||||
{ id: 10, config_key: 'logo', label: 'Logo', type: 'file', value: [{ url: '/logo.png', name: 'logo.png' }] },
|
||||
{
|
||||
id: 11,
|
||||
config_key: 'price',
|
||||
label: '分段价格',
|
||||
type: 'array',
|
||||
value: [
|
||||
{ min: 0, max: 18, price: 22 },
|
||||
{ min: 18, max: 25, price: 30 },
|
||||
{ min: 25, max: 39, price: 40 },
|
||||
],
|
||||
array_config: '{"mode":"table"}',
|
||||
schema:
|
||||
'{"columns":[{"title":"最小","dataIndex":"min","type":"int"},{"title":"最大","dataIndex":"max","type":"int"},{"title":"价格","dataIndex":"price","type":"int"}]}',
|
||||
},
|
||||
];
|
||||
|
||||
setData(
|
||||
rawSqlData.map((item: any) => ({
|
||||
...item,
|
||||
props: item.props ? JSON.parse(item.props) : {},
|
||||
rules: item.rules ? JSON.parse(item.rules) : {},
|
||||
options: item.options ? JSON.parse(item.options) : [],
|
||||
array_config: item.array_config ? JSON.parse(item.array_config) : {},
|
||||
schema: item.schema ? JSON.parse(item.schema) : null,
|
||||
value:
|
||||
typeof item.value === 'string' && (item.value.startsWith('[') || item.value.startsWith('{'))
|
||||
? JSON.parse(item.value)
|
||||
: item.value,
|
||||
})),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// --- 核心:全类型渲染器 ---
|
||||
const renderField = (item: any) => {
|
||||
const { type, props, options, array_config, schema } = item;
|
||||
|
||||
// 1. 获取该配置项当前的实时值 (用于 tags 模式下合并临时选项)
|
||||
const currentValue = form.getFieldValue(item.config_key) || [];
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return <Input {...props} />;
|
||||
case 'text':
|
||||
return <Input.TextArea {...props} />;
|
||||
case 'int':
|
||||
return <InputNumber style={{ width: '100%' }} {...props} />;
|
||||
case 'bool':
|
||||
return <Switch />;
|
||||
case 'date':
|
||||
return <DatePicker style={{ width: '100%' }} format='YYYY-MM-DD' />;
|
||||
case 'datetime':
|
||||
return <DatePicker showTime style={{ width: '100%' }} format='YYYY-MM-DD HH:mm:ss' />;
|
||||
case 'file':
|
||||
return (
|
||||
<Upload listType='picture' defaultFileList={form.getFieldValue(item.config_key)} beforeUpload={() => false}>
|
||||
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||
</Upload>
|
||||
);
|
||||
|
||||
case 'array': {
|
||||
const mode = array_config?.mode;
|
||||
const currentValue = form.getFieldValue(item.config_key);
|
||||
|
||||
// 1. 处理表格模式 (EditableTable)
|
||||
if (mode === 'table') {
|
||||
return <EditableTable {...props} value={currentValue} schema={schema} />;
|
||||
}
|
||||
|
||||
// 2. 处理复选框模式 (Checkbox)
|
||||
if (mode === 'checkbox') {
|
||||
return <Checkbox.Group options={options} />;
|
||||
}
|
||||
|
||||
// 3. 处理下拉选择与标签模式 (Select / Tags)
|
||||
if (mode === 'select' || mode === 'tags') {
|
||||
// 【关键】始终以原始 options 为基准,防止预设选项丢失
|
||||
const mergedOptions = [...(options || [])];
|
||||
|
||||
// 如果是 tags 模式,需要把当前已输入但不在 options 里的值临时加入下拉列表
|
||||
// 这样可以保证“已选即显示”,但不会影响原始 options 的持久性
|
||||
if (mode === 'tags' && Array.isArray(currentValue)) {
|
||||
currentValue.forEach((v) => {
|
||||
const exists = mergedOptions.some((opt) => opt.value === v);
|
||||
if (!exists) {
|
||||
// 如果是用户新输入的,动态补全到当前渲染列表里
|
||||
mergedOptions.push({ label: String(v), value: v });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
// 模式判定:tags 优先,其次看是否是多选 select
|
||||
mode={mode === 'tags' ? 'tags' : array_config.multiple ? 'multiple' : undefined}
|
||||
style={{ width: '100%' }}
|
||||
placeholder='请选择或输入'
|
||||
options={mergedOptions}
|
||||
optionFilterProp='label'
|
||||
// 允许清空
|
||||
allowClear
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有任何匹配的模式,默认返回普通 Select
|
||||
return <Select options={options} style={{ width: '100%' }} />;
|
||||
}
|
||||
|
||||
// 这里是 switch 的 default,处理非 array 的未知类型
|
||||
default:
|
||||
return <Input placeholder='请输入内容' />;
|
||||
}
|
||||
};
|
||||
|
||||
const openEdit = (record: any) => {
|
||||
setEditingItem(record);
|
||||
// 预处理值:DatePicker 需要 dayjs 对象
|
||||
let val = record.value;
|
||||
if (record.type === 'date' || record.type === 'datetime') val = dayjs(val);
|
||||
|
||||
form.setFieldsValue({ [record.config_key]: val });
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '配置项', dataIndex: 'label', key: 'label', width: 150 },
|
||||
{ title: 'Key', dataIndex: 'config_key', render: (k: string) => <code style={{ color: '#c41d7f' }}>{k}</code> },
|
||||
{ title: '类型', dataIndex: 'type', render: (t: string) => <Tag>{t}</Tag> },
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'value',
|
||||
ellipsis: true,
|
||||
render: (v: any, record: any) => {
|
||||
if (record.type === 'bool') return v ? '是' : '否';
|
||||
if (record.type === 'file') return <FileTextOutlined />;
|
||||
return JSON.stringify(v);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: any, record: any) => (
|
||||
<Button type='link' onClick={() => openEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Table dataSource={data} columns={columns} rowKey='id' bordered pagination={false} />
|
||||
|
||||
<Modal
|
||||
title={`修改 - ${editingItem?.label || ''}`}
|
||||
open={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
onOk={() =>
|
||||
form.validateFields().then((values) => {
|
||||
console.log('提交数据:', values);
|
||||
// 在这里通常需要调用 API
|
||||
// updateConfig(editingItem.id, values[editingItem.config_key]);
|
||||
setIsModalVisible(false);
|
||||
message.success('保存成功(演示)');
|
||||
})
|
||||
}
|
||||
width={editingItem?.array_config?.mode === 'table' ? 700 : 500} // 表格模式自动加宽
|
||||
>
|
||||
{editingItem && (
|
||||
<Form form={form} layout='vertical'>
|
||||
<Form.Item
|
||||
name={editingItem.config_key}
|
||||
label='配置值'
|
||||
valuePropName={
|
||||
editingItem.type === 'bool' ? 'checked' : editingItem.type === 'file' ? 'fileList' : 'value'
|
||||
}
|
||||
rules={[{ required: editingItem.rules?.required }]}
|
||||
>
|
||||
{renderField(editingItem)}
|
||||
</Form.Item>
|
||||
<Divider style={{ fontSize: 12, color: '#999' }}>详细元数据</Divider>
|
||||
<pre style={{ fontSize: 11, background: '#fafafa', padding: 8 }}>
|
||||
类型: {editingItem.type} | 键: {editingItem.config_key}
|
||||
</pre>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigList;
|
||||
Reference in New Issue
Block a user