配置
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { FileTextOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { DeleteOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -10,14 +10,49 @@ import {
|
||||
Modal,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const EditableForm = ({ value = [], onChange, schema }: any) => {
|
||||
// 兼容性处理:从 columns 中提取字段定义
|
||||
const fields = schema?.columns || [];
|
||||
|
||||
// 假设 form 模式操作的是数组的第一个对象
|
||||
const data = value[0] || {};
|
||||
|
||||
const handleFieldChange = (dataIndex: string, val: any) => {
|
||||
const newData = [{ ...data, [dataIndex]: val }];
|
||||
onChange?.(newData); // 保持数据结构依然是 [{...}]
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
background: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{fields.map((col: any) => (
|
||||
<div key={col.dataIndex} style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: '#333', marginBottom: 4 }}>{col.title}</div>
|
||||
<Input
|
||||
placeholder={col.placeholder}
|
||||
value={data[col.dataIndex]}
|
||||
onChange={(e) => handleFieldChange(col.dataIndex, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- 复杂类型:可编辑表格组件 (用于分段价格等 array + table 模式) ---
|
||||
interface EditableTableProps {
|
||||
value?: any[];
|
||||
@@ -31,19 +66,18 @@ interface EditableTableProps {
|
||||
};
|
||||
}
|
||||
const EditableTable = ({ value = [], onChange, schema }: EditableTableProps) => {
|
||||
const columns = (schema?.columns || []).map((col: any) => ({
|
||||
// --- 1. 字段定义 ---
|
||||
const baseColumns = (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); // 使用可选链调用,安全消灭波浪线
|
||||
onChange?.(newData);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,25 +85,51 @@ const EditableTable = ({ value = [], onChange, schema }: EditableTableProps) =>
|
||||
},
|
||||
}));
|
||||
|
||||
// --- 2. 操作列 (删除) ---
|
||||
const columns = [
|
||||
{
|
||||
title: '操作',
|
||||
width: 70,
|
||||
align: 'center' as const,
|
||||
render: (_: any, __: any, index: number) => (
|
||||
<Button
|
||||
type='link'
|
||||
danger
|
||||
size='small'
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
const newData = [...value];
|
||||
newData.splice(index, 1);
|
||||
onChange?.(newData);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...baseColumns,
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ border: '1px solid #f0f0f0', padding: 8, borderRadius: 4 }}>
|
||||
<div style={{ border: '1px solid #f0f0f0', padding: 12, borderRadius: 6, background: '#fff' }}>
|
||||
{/* --- 3. 顶部操作栏 (全部对齐左侧) --- */}
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Button type='primary' size='small' icon={<span>+ </span>} onClick={() => onChange?.([...value, {}])}>
|
||||
新增一行
|
||||
</Button>
|
||||
<Button type='default' danger size='small' disabled={value.length === 0} onClick={() => onChange?.([])}>
|
||||
全部清空
|
||||
</Button>
|
||||
<span style={{ marginLeft: 8, color: '#999', fontSize: '12px' }}>共 {value.length} 条记录</span>
|
||||
</Space>
|
||||
|
||||
{/* --- 4. 表格主体 --- */}
|
||||
<Table
|
||||
dataSource={value}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size='small'
|
||||
// 显式声明 i 为 number,消灭 rowKey 处的波浪线
|
||||
rowKey={(_: any, i: any) => i.toString()}
|
||||
scroll={{ y: 300 }}
|
||||
/>
|
||||
<Button
|
||||
type='dashed'
|
||||
block
|
||||
// 关键点:这里也需要使用 ?. 确保 onChange 存在时才调用
|
||||
onClick={() => onChange?.([...value, {}])}
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
新增一行
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -110,12 +170,12 @@ const ConfigList = () => {
|
||||
label: '通知方式',
|
||||
type: 'array',
|
||||
value: [1, 2],
|
||||
array_config: '{"mode":"checkbox"}',
|
||||
array_config: '{"mode":"checkbox", "multiple":"true"}',
|
||||
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: 10, config_key: 'logo', label: 'Logo', type: 'file', value: [{ url: '/logo.png', name: 'logo.png' }] },
|
||||
{
|
||||
id: 11,
|
||||
config_key: 'price',
|
||||
@@ -130,6 +190,45 @@ const ConfigList = () => {
|
||||
schema:
|
||||
'{"columns":[{"title":"最小","dataIndex":"min","type":"int"},{"title":"最大","dataIndex":"max","type":"int"},{"title":"价格","dataIndex":"price","type":"int"}]}',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
config_key: 'admin_info',
|
||||
label: '管理员信息',
|
||||
type: 'array',
|
||||
value: [{ name: '张三', phone: '李四' }],
|
||||
array_config: '{"mode":"form"}',
|
||||
schema:
|
||||
'{"columns":[{"title":"管理员姓名","dataIndex":"name","type":"string","placeholder":"请输入姓名"},{"title":"管理员手机号","dataIndex":"phone","type":"string","placeholder":"请输入手机号"}]}',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
config_key: 'audit_status',
|
||||
label: '审核状态',
|
||||
type: 'select',
|
||||
value: 1,
|
||||
options: '[{"label":"待审核","value":1},{"label":"通过","value":2},{"label":"驳回","value":3}]',
|
||||
props: '{"placeholder":"请选择状态"}',
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
config_key: 'user_roles',
|
||||
label: '关联角色',
|
||||
type: 'array',
|
||||
value: ['admin'], // 初始选中的值
|
||||
array_config: '{"mode":"select", "multiple": true}', // 核心标志位
|
||||
options:
|
||||
'[{"label":"超级管理员","value":"admin"},{"label":"运营","value":"editor"},{"label":"财务","value":"finance"}]',
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
config_key: 'single_check',
|
||||
label: '单选确认',
|
||||
type: 'array',
|
||||
value: [1],
|
||||
// 增加 single 属性
|
||||
array_config: '{"mode":"checkbox", "multiple":"false"}',
|
||||
options: '[{"label":"选项A","value":1},{"label":"选项B","value":2}]',
|
||||
},
|
||||
];
|
||||
|
||||
setData(
|
||||
@@ -153,7 +252,7 @@ const ConfigList = () => {
|
||||
const { type, props, options, array_config, schema } = item;
|
||||
|
||||
// 1. 获取该配置项当前的实时值 (用于 tags 模式下合并临时选项)
|
||||
const currentValue = form.getFieldValue(item.config_key) || [];
|
||||
//const currentValue = form.getFieldValue(item.config_key) || [];
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
@@ -162,18 +261,20 @@ const ConfigList = () => {
|
||||
return <Input.TextArea {...props} />;
|
||||
case 'int':
|
||||
return <InputNumber style={{ width: '100%' }} {...props} />;
|
||||
case 'select':
|
||||
return <Select {...props} options={options} style={{ width: '100%' }} allowClear />;
|
||||
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 '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;
|
||||
@@ -184,43 +285,93 @@ const ConfigList = () => {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === 'form') {
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
// 模式判定:tags 优先,其次看是否是多选 select
|
||||
mode={mode === 'tags' ? 'tags' : array_config.multiple ? 'multiple' : undefined}
|
||||
style={{ width: '100%' }}
|
||||
placeholder='请选择或输入'
|
||||
options={mergedOptions}
|
||||
optionFilterProp='label'
|
||||
// 允许清空
|
||||
allowClear
|
||||
<EditableForm
|
||||
schema={schema} // 传入包含 columns 的 schema
|
||||
value={currentValue} // 这里的 currentValue 预期是 [{...}]
|
||||
onChange={(val: any) => {
|
||||
form.setFieldsValue({ [item.config_key]: val });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 处理复选框模式 (Checkbox)
|
||||
if (mode === 'checkbox') {
|
||||
//return <Checkbox.Group options={options} />;
|
||||
return (
|
||||
<Checkbox.Group
|
||||
options={options}
|
||||
value={currentValue}
|
||||
onChange={(checkedValues) => {
|
||||
if (array_config?.multiple === 'false') {
|
||||
if (checkedValues.length === 0) return;
|
||||
|
||||
const nextValue = [checkedValues[checkedValues.length - 1]];
|
||||
form.setFieldValue(item.config_key, nextValue);
|
||||
} else {
|
||||
// 普通多选模式
|
||||
form.setFieldValue(item.config_key, checkedValues);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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}
|
||||
// allowClear
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
if (mode === 'select' || mode === 'tags') {
|
||||
// 1. 不再使用 mergedOptions 逻辑,直接使用原始预设的 options
|
||||
// 因为不允许输入新值,所以不需要把 currentValue 里的值往 options 里塞
|
||||
const selectOptions = options || [];
|
||||
|
||||
// 2. 判定是否为多选模式
|
||||
// 即使 mode 是 'tags',我们也将其转为 'multiple',以达到“标签化显示但禁止输入”的效果
|
||||
const isMultiple = mode === 'tags' || array_config?.multiple;
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
// 关键修改:禁止使用 'tags',改用 'multiple'
|
||||
mode={isMultiple ? 'multiple' : undefined}
|
||||
style={{ width: '100%' }}
|
||||
// 修改 Placeholder 提示,避免误导用户可以输入
|
||||
placeholder={isMultiple ? '请选择(多选)' : '请选择'}
|
||||
options={selectOptions}
|
||||
allowClear
|
||||
// 开启搜索,方便在长列表中查找,但 multiple 模式下搜索非选项内容无法回车添加
|
||||
showSearch
|
||||
/>
|
||||
);
|
||||
}
|
||||
// 如果没有任何匹配的模式,默认返回普通 Select
|
||||
return <Select options={options} style={{ width: '100%' }} />;
|
||||
}
|
||||
@@ -252,7 +403,75 @@ const ConfigList = () => {
|
||||
render: (v: any, record: any) => {
|
||||
if (record.type === 'bool') return v ? '是' : '否';
|
||||
if (record.type === 'file') return <FileTextOutlined />;
|
||||
return JSON.stringify(v);
|
||||
if (record.type === 'select' && record.options) {
|
||||
const option = record.options.find((opt: any) => opt.value === v);
|
||||
return option ? <Tag color='blue'>{option.label}</Tag> : v;
|
||||
}
|
||||
if (record.array_config?.mode === 'table' && Array.isArray(v)) {
|
||||
return (
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{v.length > 0 ? `共 ${v.length} 条记录` : <span style={{ color: '#ccc' }}>暂无记录</span>}
|
||||
{/* 也可以选择渲染前两条记录作为预览 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const presetColors = ['magenta', 'red', 'volcano', 'orange', 'gold', 'lime', 'green', 'cyan', 'blue', 'purple'];
|
||||
if (
|
||||
(record.array_config?.mode === 'checkbox' ||
|
||||
record.array_config?.mode === 'select' ||
|
||||
record.array_config?.mode === 'tags') &&
|
||||
Array.isArray(v)
|
||||
) {
|
||||
// 必须加上 return,否则 render 函数没有返回值
|
||||
return (
|
||||
<Space size={[0, 4]} wrap>
|
||||
{v.map((val: any, index: number) => {
|
||||
const option = record.options?.find((opt: any) => opt.value === val);
|
||||
const label = option ? option.label : val;
|
||||
const color = presetColors[index % presetColors.length];
|
||||
|
||||
return (
|
||||
<Tag
|
||||
key={val}
|
||||
color={color}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
marginRight: '2px',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
if (record.array_config?.mode === 'form' && Array.isArray(v) && v.length > 0) {
|
||||
const data = v[0]; // 获取数组中的对象
|
||||
const fields = record.schema?.columns || []; // 获取定义的字段标题
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
display: 'inline-block',
|
||||
minWidth: '180px',
|
||||
border: '1px solid #eee',
|
||||
}}
|
||||
>
|
||||
{fields.map((col: any) => (
|
||||
<div key={col.dataIndex} style={{ fontSize: '12px', lineHeight: '1.8' }}>
|
||||
<span style={{ color: '#8c8c8c', marginRight: '8px' }}>{col.title}:</span>
|
||||
<span style={{ color: '#333', fontWeight: 500 }}>{data[col.dataIndex] || '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return typeof v === 'object' ? JSON.stringify(v) : String(v);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user