settings
This commit is contained in:
@@ -1,12 +1,33 @@
|
|||||||
import { DownOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
|
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 { navigate } from '@/router/routerUtils';
|
||||||
|
import { AdminServices } from '@/services/AdminServices';
|
||||||
import { useUserStore } from '@/store/UserStore';
|
import { useUserStore } from '@/store/UserStore';
|
||||||
import { imgWithPrefix } from '@/utils/common';
|
import { imgWithPrefix } from '@/utils/common';
|
||||||
|
import { useRequest } from '@/utils/useRequest';
|
||||||
import { GapBox } from '../GapBox';
|
import { GapBox } from '../GapBox';
|
||||||
|
|
||||||
export const HeaderUserInfo: React.FC = () => {
|
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 (
|
return (
|
||||||
<GapBox>
|
<GapBox>
|
||||||
@@ -34,7 +55,7 @@ export const HeaderUserInfo: React.FC = () => {
|
|||||||
size={40}
|
size={40}
|
||||||
src={`${imgWithPrefix(userInfo.avatar)}?v=${encodeURIComponent(userInfo.update_date)}`}
|
src={`${imgWithPrefix(userInfo.avatar)}?v=${encodeURIComponent(userInfo.update_date)}`}
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
style={{ marginBottom: 4 }} // 👈 控制头像和名字的间距
|
style={{ marginBottom: 4 }} // 控制头像和名字的间距
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -100,7 +121,16 @@ export const HeaderUserInfo: React.FC = () => {
|
|||||||
<Popconfirm
|
<Popconfirm
|
||||||
title='确定要退出登录吗?'
|
title='确定要退出登录吗?'
|
||||||
onConfirm={() => {
|
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'>
|
<Button size='small' variant='text' color='primary'>
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ const companyMenu: MenuDataItem = {
|
|||||||
icon: <TeamOutlined style={iconStyle} />,
|
icon: <TeamOutlined style={iconStyle} />,
|
||||||
children: [{ name: '企业信息', path: '/company/list', auth: 'SF_ADMIN_COMPANY_VIEW' }],
|
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 = {
|
const authMenu: MenuDataItem = {
|
||||||
name: '权限管理',
|
name: '权限管理',
|
||||||
icon: <BarsOutlined style={iconStyle} />,
|
icon: <BarsOutlined style={iconStyle} />,
|
||||||
@@ -48,6 +53,7 @@ const asideMenuConfig: MenuDataItem[] = [
|
|||||||
},
|
},
|
||||||
userMenu,
|
userMenu,
|
||||||
companyMenu,
|
companyMenu,
|
||||||
|
// configMenu,
|
||||||
authMenu,
|
authMenu,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { lazy } from 'react';
|
|||||||
import AppLayout from '@/layouts/AppLayout';
|
import AppLayout from '@/layouts/AppLayout';
|
||||||
import EmptyLayout from '@/layouts/EmptyLayout';
|
import EmptyLayout from '@/layouts/EmptyLayout';
|
||||||
import CompanyList from '@/pages/Company/List';
|
import CompanyList from '@/pages/Company/List';
|
||||||
|
import ConfigList from '@/pages/Config';
|
||||||
import ErrorPage from '@/pages/Error';
|
import ErrorPage from '@/pages/Error';
|
||||||
import Index from '@/pages/Index';
|
import Index from '@/pages/Index';
|
||||||
import Login from '@/pages/Record/Login';
|
import Login from '@/pages/Record/Login';
|
||||||
@@ -39,6 +40,14 @@ export const routes: IRouteItem[] = [
|
|||||||
// { path: '/group', Component: lazy(() => import('@/pages/Staff/group')) },
|
// { 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',
|
path: '/staff',
|
||||||
Layout: AppLayout,
|
Layout: AppLayout,
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export const CompanyEditModal: React.FC<IProps> = (props) => {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label='密码'
|
label='密码'
|
||||||
name='password'
|
name='password'
|
||||||
tooltip={data ? undefined : '不填写则系统自动生成初始密码'}
|
tooltip={data ? undefined : '初始密码:123456'}
|
||||||
rules={[{ min: 6, max: 18, message: '密码需6~18位' }]}
|
rules={[{ min: 6, max: 18, message: '密码需6~18位' }]}
|
||||||
>
|
>
|
||||||
<Input.Password allowClear placeholder={'不填写则系统自动生成初始密码'} />
|
<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