- data_service: export 方法查询匹配行(上限10000),import 方法逐行校验+插入 - data_handler: export_plugin_data / import_plugin_data 处理函数 - module: 注册 GET /export + POST /import 路由 - pluginData.ts: exportPluginData / importPluginData API 函数 - PluginCRUDPage: 根据 entity importable/exportable 标志显示导出/导入按钮 - PluginMarket: 修复 TS 错误 (unused imports, type narrowing) - PluginSettingsForm: 修复 TS 错误 (Rule type, Divider orientation)
233 lines
6.2 KiB
TypeScript
233 lines
6.2 KiB
TypeScript
import React, { useCallback, useMemo } from 'react';
|
|
import {
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Switch,
|
|
Select,
|
|
DatePicker,
|
|
Button,
|
|
message,
|
|
Divider,
|
|
Typography,
|
|
Tooltip,
|
|
} from 'antd';
|
|
import { QuestionCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
|
import type {
|
|
PluginSettingField,
|
|
PluginSettingType,
|
|
} from '../api/plugins';
|
|
|
|
const { Text } = Typography;
|
|
|
|
interface PluginSettingsFormProps {
|
|
/** manifest 中声明的 settings 字段 */
|
|
fields: PluginSettingField[];
|
|
/** 当前存储的配置值 */
|
|
values: Record<string, unknown>;
|
|
/** 插件版本(乐观锁) */
|
|
recordVersion: number;
|
|
/** 保存回调 */
|
|
onSave: (config: Record<string, unknown>, version: number) => Promise<unknown>;
|
|
/** 是否只读 */
|
|
readOnly?: boolean;
|
|
}
|
|
|
|
/** 根据 manifest settings 声明自动渲染配置表单 */
|
|
const PluginSettingsForm: React.FC<PluginSettingsFormProps> = ({
|
|
fields,
|
|
values,
|
|
recordVersion,
|
|
onSave,
|
|
readOnly = false,
|
|
}) => {
|
|
const [form] = Form.useForm();
|
|
const [saving, setSaving] = React.useState(false);
|
|
|
|
const initialValues = useMemo(() => {
|
|
const merged: Record<string, unknown> = {};
|
|
for (const f of fields) {
|
|
merged[f.name] = values[f.name] ?? f.default_value ?? getDefaultForType(f.field_type);
|
|
}
|
|
return merged;
|
|
}, [fields, values]);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
try {
|
|
const formValues = await form.validateFields();
|
|
setSaving(true);
|
|
await onSave(formValues, recordVersion);
|
|
message.success('配置已保存');
|
|
} catch (err: unknown) {
|
|
if (err && typeof err === 'object' && 'errorFields' in err) {
|
|
// antd 表单校验错误,无需额外提示
|
|
return;
|
|
}
|
|
message.error(err instanceof Error ? err.message : '保存失败');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [form, onSave, recordVersion]);
|
|
|
|
const grouped = useMemo(() => {
|
|
const groups = new Map<string, PluginSettingField[]>();
|
|
for (const f of fields) {
|
|
const group = f.group ?? '';
|
|
const list = groups.get(group) ?? [];
|
|
list.push(f);
|
|
groups.set(group, list);
|
|
}
|
|
return groups;
|
|
}, [fields]);
|
|
|
|
const renderField = (field: PluginSettingField) => {
|
|
const label = (
|
|
<span>
|
|
{field.display_name}
|
|
{field.description && (
|
|
<Tooltip title={field.description}>
|
|
<QuestionCircleOutlined style={{ marginLeft: 4, color: '#999' }} />
|
|
</Tooltip>
|
|
)}
|
|
</span>
|
|
);
|
|
|
|
const rules: Array<{ required: boolean; message?: string; type?: 'string' | 'number' | 'boolean' | 'url' | 'email' }> = [];
|
|
if (field.required) {
|
|
rules.push({ required: true, message: `请输入${field.display_name}` });
|
|
}
|
|
|
|
const widget = renderWidget(field, readOnly);
|
|
|
|
return (
|
|
<Form.Item
|
|
key={field.name}
|
|
name={field.name}
|
|
label={label}
|
|
rules={rules}
|
|
valuePropName={field.field_type === 'boolean' ? 'checked' : 'value'}
|
|
>
|
|
{widget}
|
|
</Form.Item>
|
|
);
|
|
};
|
|
|
|
const groupEntries = Array.from(grouped.entries());
|
|
|
|
return (
|
|
<Form
|
|
form={form}
|
|
layout="vertical"
|
|
initialValues={initialValues}
|
|
disabled={readOnly}
|
|
>
|
|
{groupEntries.map(([group, groupFields], gi) => (
|
|
<React.Fragment key={group || `__default_${gi}`}>
|
|
{group ? (
|
|
<Divider type="horizontal" orientationMargin={0} plain>
|
|
<Text strong>{group}</Text>
|
|
</Divider>
|
|
) : null}
|
|
{groupFields.map(renderField)}
|
|
</React.Fragment>
|
|
))}
|
|
|
|
{!readOnly && (
|
|
<Form.Item>
|
|
<Button
|
|
type="primary"
|
|
icon={<SaveOutlined />}
|
|
loading={saving}
|
|
onClick={handleSave}
|
|
>
|
|
保存配置
|
|
</Button>
|
|
</Form.Item>
|
|
)}
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
function renderWidget(field: PluginSettingField, readOnly: boolean): React.ReactNode {
|
|
switch (field.field_type) {
|
|
case 'text':
|
|
return <Input disabled={readOnly} placeholder={`请输入${field.display_name}`} />;
|
|
case 'number': {
|
|
const props: Record<string, unknown> = {
|
|
disabled: readOnly,
|
|
placeholder: `请输入${field.display_name}`,
|
|
style: { width: '100%' },
|
|
};
|
|
if (field.range) {
|
|
props.min = field.range[0];
|
|
props.max = field.range[1];
|
|
}
|
|
return <InputNumber {...props} />;
|
|
}
|
|
case 'boolean':
|
|
return <Switch disabled={readOnly} />;
|
|
case 'select':
|
|
return (
|
|
<Select
|
|
disabled={readOnly}
|
|
placeholder={`请选择${field.display_name}`}
|
|
options={(field.options ?? []).map((o) => {
|
|
if (typeof o === 'object' && o !== null && 'label' in o && 'value' in o) {
|
|
return o as { label: string; value: string };
|
|
}
|
|
return { label: String(o), value: String(o) };
|
|
})}
|
|
/>
|
|
);
|
|
case 'multiselect':
|
|
return (
|
|
<Select
|
|
mode="multiple"
|
|
disabled={readOnly}
|
|
placeholder={`请选择${field.display_name}`}
|
|
options={(field.options ?? []).map((o) => {
|
|
if (typeof o === 'object' && o !== null && 'label' in o && 'value' in o) {
|
|
return o as { label: string; value: string };
|
|
}
|
|
return { label: String(o), value: String(o) };
|
|
})}
|
|
/>
|
|
);
|
|
case 'color':
|
|
return <Input type="color" disabled={readOnly} style={{ width: 80 }} />;
|
|
case 'date':
|
|
return <DatePicker disabled={readOnly} style={{ width: '100%' }} />;
|
|
case 'datetime':
|
|
return <DatePicker showTime disabled={readOnly} style={{ width: '100%' }} />;
|
|
case 'json':
|
|
return <Input.TextArea disabled={readOnly} rows={4} placeholder="JSON 格式" />;
|
|
default:
|
|
return <Input disabled={readOnly} />;
|
|
}
|
|
}
|
|
|
|
function getDefaultForType(type: PluginSettingType): unknown {
|
|
switch (type) {
|
|
case 'text':
|
|
case 'color':
|
|
return '';
|
|
case 'number':
|
|
return 0;
|
|
case 'boolean':
|
|
return false;
|
|
case 'select':
|
|
return undefined;
|
|
case 'multiselect':
|
|
return [];
|
|
case 'date':
|
|
case 'datetime':
|
|
return undefined;
|
|
case 'json':
|
|
return '';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export default PluginSettingsForm;
|