fix: 修复模型默认值不加载 + DependentFormItem prop 透传断裂

- DependentFormItem 透传 Form.Item 注入的 value/onChange/disabled prop
- 稳定化 constraintsConfig 空对象引用(EMPTY_OBJ),避免 useMemo 失效导致无限重渲染
- useLayoutEffect 手动 setFieldsValue(initialValues)替代 antd 被忽略的 initialValues prop
- handleReset 使用 resetFields + setFieldsValue 组合确保默认值归位

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 17:00:44 +08:00
parent d5ba7e63cd
commit 4261915679
5 changed files with 287 additions and 131 deletions

View File

@@ -1,17 +1,19 @@
// ============================================================ // ============================================================
// ModelInputForm — 模型输入表单(纯布局外壳) // ModelInputForm — 模型输入表单(纯布局外壳)
// //
// 布局Header模型信息+meta → Bodyfields.map 渲染) → Footer价格+提交) // 布局Header模型信息+meta → Bodyfields.map 渲染) → Footer标签+重置+提交)
// //
// 与旧版的区别: // 与旧版的区别:
// - 控件渲染逻辑已提取到 useUI/ 组件体系 // - 控件渲染逻辑已提取到 useUI/ 组件体系
// - param_schema 解析已提取到 useModelUI Hook // - param_schema 解析已提取到 useModelUI Hook
// - 本组件只负责布局、Form 容器、提交逻辑 // - 字段间条件依赖constraints_config.requires自动处理禁用
// - 本组件只负责布局、Form 容器、提交逻辑、依赖禁用
// ============================================================ // ============================================================
import { useEffect, useMemo, useCallback } from 'react'; import { useState, useLayoutEffect, useMemo, useCallback } from 'react';
import { import {
Form, Form,
Input,
Button, Button,
Typography, Typography,
Empty, Empty,
@@ -20,7 +22,8 @@ import {
theme as antTheme, theme as antTheme,
message, message,
} from 'antd'; } from 'antd';
import { SendOutlined, ClockCircleOutlined } from '@ant-design/icons'; import type { FormInstance } from 'antd';
import { SendOutlined, ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload'; import type { UploadFile } from 'antd/es/upload';
import { useHomeContext } from '@/pages/home/HomeContent'; import { useHomeContext } from '@/pages/home/HomeContent';
@@ -29,17 +32,70 @@ import { useSubmitTask } from '@/hooks/use-api';
import { emit, EVENTS } from '@/utils/event-bus'; import { emit, EVENTS } from '@/utils/event-bus';
import { OutputTypeMap } from '@/services/modules/task'; import { OutputTypeMap } from '@/services/modules/task';
import type { OutputTypeString, TaskInfoItemResponseBody } from '@/services/modules/task'; import type { OutputTypeString, TaskInfoItemResponseBody } from '@/services/modules/task';
import { useModelUI, type UIFieldConfig } from './useModelUI'; import { useModelUI, isValueEmpty, type UIFieldConfig } from './useModelUI';
const { Text, Title } = Typography; const { Text, Title } = Typography;
// ---------- 组件 ---------- // ---------- 依赖字段包装组件 ----------
/**
* DependentFormItem — 为单个字段处理条件依赖禁用。
*
* 通过 Form.useWatch 监听 dependsOn 列表中的字段值,
* 任一依赖为空时自动禁用本字段(注入 disabled: true 到 widgetConfig
*
* 每个 DependentFormItem 实例的 dependsOn 长度固定,
* 切换模型时通过外层 key 变化自动重新挂载,满足 React hooks 规则。
*/
function DependentFormItem({
field,
form,
value,
onChange,
disabled: formItemDisabled,
}: {
field: UIFieldConfig;
form: FormInstance;
/** Form.Item 通过 React.cloneElement 注入,必须透传给 widget */
value?: unknown;
onChange?: (value: unknown) => void;
disabled?: boolean;
}) {
const depValues = (field.dependsOn || []).map((depName) =>
Form.useWatch(depName, form),
);
const isDepDisabled =
field.dependsOn?.some((_, i) => isValueEmpty(depValues[i])) ?? false;
const effectiveDisabled = formItemDisabled || isDepDisabled;
const effectiveConfig = effectiveDisabled
? { ...field.widgetConfig, disabled: true }
: field.widgetConfig;
const Component = field.component;
return (
<Component
value={value}
onChange={onChange}
config={effectiveConfig}
/>
);
}
/** 稳定空数组引用,避免 useMemo 重建 */
const EMPTY_ARRAY: never[] = [];
/** 稳定空对象引用,避免每次渲染创建新 {} 导致 useMemo 失效 */
const EMPTY_OBJ: Record<string, unknown> = {};
// ---------- 主组件 ----------
export function ModelInputForm() { export function ModelInputForm() {
const { token } = antTheme.useToken(); const { token } = antTheme.useToken();
const { isLoggedIn } = useAppContext(); const { isLoggedIn } = useAppContext();
const { selectedModel } = useHomeContext(); const { selectedModel } = useHomeContext();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [taskTag, setTaskTag] = useState('');
// 提交突变 Hook // 提交突变 Hook
const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({ const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({
@@ -51,9 +107,13 @@ export function ModelInputForm() {
// ======== useModelUIparam_schema → UIFieldConfig[] ======== // ======== useModelUIparam_schema → UIFieldConfig[] ========
const fields: UIFieldConfig[] = useModelUI(selectedModel?.param_schema || []); const fields: UIFieldConfig[] = useModelUI(
selectedModel?.param_schema || EMPTY_ARRAY,
(selectedModel?.constraints_config as Record<string, unknown>) || EMPTY_OBJ,
);
// 构建初始值 // 构建初始值(用作 setFieldsValue 的数据源,
// antd Form 在有外部 form 实例时会忽略 initialValues prop
const initialValues = useMemo(() => { const initialValues = useMemo(() => {
const iv: Record<string, unknown> = {}; const iv: Record<string, unknown> = {};
fields.forEach((f) => { fields.forEach((f) => {
@@ -64,12 +124,22 @@ export function ModelInputForm() {
return iv; return iv;
}, [fields]); }, [fields]);
// 选中模型时重置表单
useEffect(() => { // 选中模型时加载默认值useLayoutEffect 在 DOM 更新后、浏览器绘制前执行,避免闪烁)
if (selectedModel) { // antd 有外部 form 实例时 initialValues prop 被忽略,必须手动 setFieldsValue
form.resetFields(); useLayoutEffect(() => {
if (selectedModel && Object.keys(initialValues).length > 0) {
form.setFieldsValue(initialValues);
setTaskTag('');
} }
}, [selectedModel?.id]); // eslint-disable-line react-hooks/exhaustive-deps }, [selectedModel?.id, initialValues]);
// 重置参数(回到默认值 + 清空标签)
const handleReset = useCallback(() => {
form.resetFields();
form.setFieldsValue(initialValues);
setTaskTag('');
}, [form, initialValues]);
// ======== 提交 ======== // ======== 提交 ========
@@ -83,14 +153,12 @@ export function ModelInputForm() {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
// 将表单值全部转为字符串CreatTaskRequestBody.params 为 Record<string, string>
const params: Record<string, string> = {}; const params: Record<string, string> = {};
for (const field of fields) { for (const field of fields) {
const value = values[field.name]; const value = values[field.name];
if (value === undefined || value === null) continue; if (value === undefined || value === null) continue;
// 文件上传 → 提取 URL 数组 → JSON 字符串
if (field.isFileWidget) { if (field.isFileWidget) {
const fileList = value as UploadFile[]; const fileList = value as UploadFile[];
const urls = fileList const urls = fileList
@@ -103,20 +171,22 @@ export function ModelInputForm() {
continue; continue;
} }
// 布尔值 → "true"/"false"
if (typeof value === 'boolean') { if (typeof value === 'boolean') {
params[field.name] = String(value); params[field.name] = String(value);
continue; continue;
} }
// 数值 / 字符串 → String
params[field.name] = String(value); params[field.name] = String(value);
} }
await submitTask({ model_id: selectedModel.id, params }); await submitTask({
model_id: selectedModel.id,
params,
...(taskTag.trim() ? { tags: [taskTag.trim()] } : {}),
});
} catch (err) { } catch (err) {
if (err && typeof err === 'object' && 'errorFields' in err) { if (err && typeof err === 'object' && 'errorFields' in err) {
return; // 表单验证错误antd 自动提示 return;
} }
message.error(errorMessage || '任务提交失败,请重试'); message.error(errorMessage || '任务提交失败,请重试');
} }
@@ -232,47 +302,44 @@ export function ModelInputForm() {
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
size="small"
initialValues={initialValues}
> >
{fields.map((field) => ( {fields.map((field) => (
<Form.Item <Form.Item
key={field.index} key={`${selectedModel.id}-${field.name}`}
name={field.name} name={field.name}
label={ label={
<span> <span>
<Text strong={field.must}>{field.label}</Text> <Text strong={field.must}>{field.label}</Text>
{field.tips && ( {field.tips && (
<Text <Text
type="secondary" type="secondary"
style={{ fontSize: 11, marginLeft: 6 }} style={{ fontSize: 12, marginLeft: 6 }}
> >
{field.tips.length > 60 {field.tips.length > 60
? `${field.tips.slice(0, 60)}...` ? `${field.tips.slice(0, 60)}...`
: field.tips} : field.tips}
</Text> </Text>
)} )}
</span> </span>
} }
rules={ rules={
field.must field.must
? [{ required: true, message: `请输入${field.label}` }] ? [{ required: true, message: `请输入${field.label}` }]
: undefined : undefined
} }
valuePropName={field.valuePropName} valuePropName={field.valuePropName}
// 文件控件:从 Upload onChange 事件中提取 fileList {...(field.isFileWidget
{...(field.isFileWidget ? { getValueFromEvent: (e: { fileList: UploadFile[] }) => e.fileList }
? { getValueFromEvent: (e: { fileList: UploadFile[] }) => e.fileList } : {})}
: {})} >
> <DependentFormItem field={field} form={form} />
<field.component config={field.widgetConfig} /> </Form.Item>
</Form.Item> ))}
))}
</Form> </Form>
)} )}
</div> </div>
{/* ======== Footer价格 + 提交 ======== */} {/* ======== Footer标签 + 重置 + 提交 ======== */}
<div <div
style={{ style={{
padding: '12px 20px', padding: '12px 20px',
@@ -280,24 +347,24 @@ export function ModelInputForm() {
flexShrink: 0, flexShrink: 0,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: 8,
}} }}
> >
{price !== undefined && priceType === 'SIMPLE' && ( <Input
<Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}> placeholder="自定义任务标签(可选)"
¥{price}/{priceUnit || '次'} value={taskTag}
</Text> onChange={(e) => setTaskTag(e.target.value)}
)} allowClear
{priceType === 'MULTI_DIMENSION' && ( maxLength={50}
<Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}> style={{ flex: 1, minWidth: 0 }}
/>
</Text>
)} <Button
{priceType === 'BILLING_ADAPTER' && ( icon={<ReloadOutlined />}
<Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}> onClick={handleReset}
disabled={!selectedModel}
</Text> title="重置参数"
)} />
<Button <Button
type="primary" type="primary"
@@ -305,15 +372,18 @@ export function ModelInputForm() {
onClick={handleSubmit} onClick={handleSubmit}
loading={submitting} loading={submitting}
disabled={!isLoggedIn || !selectedModel} disabled={!isLoggedIn || !selectedModel}
block
size="large" size="large"
style={{ style={{
background: token.colorPrimary, background: token.colorPrimary,
height: 44, height: 40,
fontSize: 16, fontSize: 14,
whiteSpace: 'nowrap',
flexShrink: 0,
}} }}
> >
{priceType === 'SIMPLE' && price !== undefined
? `消费 ¥${price}/${priceUnit || '次'}`
: '消费'}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@
// 职责: // 职责:
// - 遍历模型的 param_schema解析 spec/ui 字段 // - 遍历模型的 param_schema解析 spec/ui 字段
// - 查 WIDGET_MAP 获取对应的 React 组件 // - 查 WIDGET_MAP 获取对应的 React 组件
// - 解析 constraints_config.requires 计算字段间依赖
// - 组装出可直接渲染的 UIFieldConfig 数组 // - 组装出可直接渲染的 UIFieldConfig 数组
// //
// ModelInputForm 只需遍历 fields 渲染 Form.Item 即可, // ModelInputForm 只需遍历 fields 渲染 Form.Item 即可,
@@ -43,6 +44,14 @@ export interface UIFieldConfig {
valuePropName?: string; valuePropName?: string;
/** 是否为文件上传类控件(需要 getValueFromEvent 提取 fileList */ /** 是否为文件上传类控件(需要 getValueFromEvent 提取 fileList */
isFileWidget: boolean; isFileWidget: boolean;
/**
* 条件依赖:当这些字段的值为空时,本字段应被禁用。
*
* 来源于 constraints_config.requires 的逆向推导:
* { if: "sref", then: ["sw", "sv"] }
* → sw.dependsOn = ["sref"], sv.dependsOn = ["sref"]
*/
dependsOn?: string[];
/** 传给 widget 组件的配置placeholder/options/min/max 等) */ /** 传给 widget 组件的配置placeholder/options/min/max 等) */
widgetConfig: Record<string, unknown>; widgetConfig: Record<string, unknown>;
} }
@@ -51,49 +60,76 @@ export interface UIFieldConfig {
const FILE_WIDGETS = new Set(['imageInput', 'imageList', 'audioList']); const FILE_WIDGETS = new Set(['imageInput', 'imageList', 'audioList']);
// ---------- 约束解析 ----------
interface ConstraintRule {
if: string;
then: string[];
}
/**
* 从 constraints_config.requires 构建反向依赖表。
*
* 输入:
* [{ if: "sref", then: ["sw", "sv"] }]
*
* 输出:
* { sw: ["sref"], sv: ["sref"] }
*
* 含义sw 和 sv 依赖 srefsref 为空时它们应被禁用。
*/
function buildDependencyMap(
constraintsConfig: Record<string, unknown>,
): Map<string, string[]> {
const depMap = new Map<string, string[]>();
const requires = constraintsConfig.requires as ConstraintRule[] | undefined;
if (!requires || !Array.isArray(requires)) return depMap;
for (const rule of requires) {
if (!rule.if || !rule.then || !Array.isArray(rule.then)) continue;
for (const target of rule.then) {
const existing = depMap.get(target) || [];
existing.push(rule.if);
depMap.set(target, existing);
}
}
return depMap;
}
// ---------- Hook ---------- // ---------- Hook ----------
/** /**
* 根据模型的 param_schema 构建 UI 字段配置数组。 * 根据模型的 param_schema 和 constraints_config 构建 UI 字段配置数组。
* *
* @param paramSchema — 来自 selectedModel.param_schema(可为空数组) * @param paramSchema — 来自 selectedModel.param_schema
* @param constraintsConfig — 来自 selectedModel.constraints_config
* @returns UIFieldConfig[] — 渲染就绪的字段配置列表 * @returns UIFieldConfig[] — 渲染就绪的字段配置列表
*
* 使用方式:
* ```tsx
* const fields = useModelUI(selectedModel?.param_schema || []);
* const initialValues = useMemo(() => {
* const iv: Record<string, unknown> = {};
* fields.forEach(f => { if (f.defaultValue !== undefined) iv[f.name] = f.defaultValue; });
* return iv;
* }, [fields]);
* ```
*/ */
export function useModelUI( export function useModelUI(
paramSchema: ModelsListResponseBody['param_schema'], paramSchema: ModelsListResponseBody['param_schema'],
constraintsConfig: Record<string, unknown> = {},
): UIFieldConfig[] { ): UIFieldConfig[] {
return useMemo(() => { return useMemo(() => {
if (!paramSchema || paramSchema.length === 0) return []; if (!paramSchema || paramSchema.length === 0) return [];
const depMap = buildDependencyMap(constraintsConfig);
return paramSchema.map((item, index) => { return paramSchema.map((item, index) => {
const spec = (item.spec || {}) as Record<string, unknown>; const spec = (item.spec || {}) as Record<string, unknown>;
const ui = (item.ui || {}) as Record<string, unknown>; const ui = (item.ui || {}) as Record<string, unknown>;
const widget = (ui.widget as string) || 'lineEdit'; const widget = (ui.widget as string) || 'lineEdit';
// 查找组件(未知 widget 用 LineInput 兜底)
const component = WIDGET_MAP[widget] || FALLBACK_WIDGET; const component = WIDGET_MAP[widget] || FALLBACK_WIDGET;
// 构建传给组件的配置
const widgetConfig: Record<string, unknown> = { const widgetConfig: Record<string, unknown> = {
placeholder: ui.placeholder || `请输入${ui.label || item.map_to}`, placeholder: ui.placeholder || `请输入${ui.label || item.map_to}`,
required: spec.required, required: spec.required,
// comboBox
enumValues: spec.enum, enumValues: spec.enum,
// spinBox / doubleSpinBox
minValue: spec.min_value, minValue: spec.min_value,
maxValue: spec.max_value, maxValue: spec.max_value,
step: spec.single_step, step: spec.single_step,
// textEdit / lineEdit
minLength: spec.min_length, minLength: spec.min_length,
maxLength: spec.max_length, maxLength: spec.max_length,
// imageInput / imageList / audioList // imageInput / imageList / audioList
@@ -101,36 +137,43 @@ export function useModelUI(
maxFileSizeMb: spec.max_file_size_mb, maxFileSizeMb: spec.max_file_size_mb,
minFiles: spec.min_files, minFiles: spec.min_files,
maxFiles: spec.max_files, maxFiles: spec.max_files,
// imageInput
enableSwitch: ui.enable_switch, enableSwitch: ui.enable_switch,
}; };
const isFile = FILE_WIDGETS.has(widget); const isFile = FILE_WIDGETS.has(widget);
const isCheckbox = widget === 'checkbox'; const isCheckbox = widget === 'checkbox';
const name = item.map_to;
const dependsOn = depMap.get(name);
return { return {
index, index,
name: item.map_to, name,
component, component,
label: (ui.label as string) || item.map_to, label: (ui.label as string) || name,
must: (spec.required as boolean) || false, must: (spec.required as boolean) || false,
defaultValue: spec.default, defaultValue: spec.default,
tips: (ui.tips as string) || undefined, tips: (ui.tips as string) || undefined,
valuePropName: isCheckbox ? 'checked' : undefined, valuePropName: isCheckbox ? 'checked' : undefined,
isFileWidget: isFile, isFileWidget: isFile,
dependsOn: dependsOn && dependsOn.length > 0 ? dependsOn : undefined,
widgetConfig, widgetConfig,
}; };
}); });
}, [paramSchema]); }, [paramSchema, constraintsConfig]);
} }
/** /** 判断 widget 类型是否为文件上传类控件 */
* 判断一个 widget 类型是否为文件上传类控件。
*
* 文件控件在 Form.Item 中需要特殊处理:
* - getValueFromEvent 提取 e.fileList
* - value 类型为 UploadFile[] 而非 string
*/
export function isFileWidget(widgetName: string): boolean { export function isFileWidget(widgetName: string): boolean {
return FILE_WIDGETS.has(widgetName); return FILE_WIDGETS.has(widgetName);
} }
// ---------- 值判空工具 ----------
/** 判断表单值是否为"空"(用于依赖禁用判断) */
export function isValueEmpty(value: unknown): boolean {
if (value === undefined || value === null) return true;
if (typeof value === 'string' && value.trim() === '') return true;
if (Array.isArray(value) && value.length === 0) return true;
if (typeof value === 'boolean' && !value) return true;
return false;
}

View File

@@ -15,8 +15,9 @@ export function DoubleSpinBox({ value, onChange, disabled, config }: WidgetProps
min={config.minValue as number | undefined} min={config.minValue as number | undefined}
max={config.maxValue as number | undefined} max={config.maxValue as number | undefined}
step={config.step as number || 0.1} step={config.step as number || 0.1}
style={{ width: '100%' }} style={{ width: '100%', minWidth: 140 }}
placeholder={(config.placeholder as string) || undefined} placeholder={(config.placeholder as string) || undefined}
controls
/> />
); );
} }

View File

@@ -1,13 +1,21 @@
// ============================================================ // ============================================================
// ImageInput — imageInput → Upload单张图片 // ImageInput — imageInput → Upload单张图片
// 用于垫图、参考图等单图上传场景 // 用于垫图、参考图等单图上传场景
//
// enable_switch 支持:
// 当 API ui.enable_switch === true 时,默认禁用(灰色),
// 需点击 Switch 开关后才能上传——匹配原 Qt 客户端行为。
// 开关关闭时清除已上传文件。
// ============================================================ // ============================================================
import { Upload, message } from 'antd'; import { useState, useCallback } from 'react';
import { Upload, Switch, Space, Typography, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload'; import type { UploadFile } from 'antd/es/upload';
import type { WidgetProps } from './types'; import type { WidgetProps } from './types';
const { Text } = Typography;
/** 构建图片 accept 字符串 */ /** 构建图片 accept 字符串 */
function buildAccept(filters?: string[]): string { function buildAccept(filters?: string[]): string {
if (!filters || filters.length === 0) return 'image/png,image/jpg,image/jpeg,image/webp'; if (!filters || filters.length === 0) return 'image/png,image/jpg,image/jpeg,image/webp';
@@ -17,34 +25,67 @@ function buildAccept(filters?: string[]): string {
export function ImageInput({ value, onChange, disabled, config }: WidgetProps) { export function ImageInput({ value, onChange, disabled, config }: WidgetProps) {
const fileList = (value as UploadFile[]) || []; const fileList = (value as UploadFile[]) || [];
const maxSizeMb = (config.maxFileSizeMb as number) || 10; const maxSizeMb = (config.maxFileSizeMb as number) || 10;
const hasEnableSwitch = (config.enableSwitch as boolean) || false;
// 有 enable_switch 时默认禁用,需手动开启
const [switchOn, setSwitchOn] = useState(!hasEnableSwitch);
const isUploadDisabled = disabled || !switchOn;
const handleSwitchChange = useCallback(
(on: boolean) => {
setSwitchOn(on);
if (!on) {
// 关闭开关 → 清除已上传文件
onChange?.([] as unknown as UploadFile[]);
}
},
[onChange],
);
return ( return (
<Upload <Space orientation={"vertical"}
listType="picture-card" style={{ width: '100%' }} size={8}>
maxCount={1} {hasEnableSwitch && (
accept={buildAccept(config.fileFilter as string[] | undefined)} <Space size={8}>
fileList={fileList} <Switch
onChange={({ fileList: newList }) => onChange?.(newList)} size="small"
disabled={disabled} checked={switchOn}
beforeUpload={(file) => { onChange={handleSwitchChange}
const isImage = file.type.startsWith('image/'); disabled={disabled}
if (!isImage) { />
message.error('只能上传图片文件'); <Text type={switchOn ? 'success' : 'secondary'} style={{ fontSize: 12 }}>
return Upload.LIST_IGNORE; {switchOn ? '已启用' : '点击开关启用上传'}
} </Text>
if (file.size / 1024 / 1024 >= maxSizeMb) { </Space>
message.error(`图片大小不能超过 ${maxSizeMb}MB`);
return Upload.LIST_IGNORE;
}
return false;
}}
>
{fileList.length < 1 && (
<div>
<UploadOutlined />
<div style={{ marginTop: 4, fontSize: 12 }}></div>
</div>
)} )}
</Upload>
<Upload
listType="picture-card"
maxCount={1}
accept={buildAccept(config.fileFilter as string[] | undefined)}
fileList={fileList}
onChange={({ fileList: newList }) => onChange?.(newList as unknown as UploadFile[])}
disabled={isUploadDisabled}
beforeUpload={(file) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件');
return Upload.LIST_IGNORE;
}
if (file.size / 1024 / 1024 >= maxSizeMb) {
message.error(`图片大小不能超过 ${maxSizeMb}MB`);
return Upload.LIST_IGNORE;
}
return false;
}}
>
{fileList.length < 1 && (
<div style={{ opacity: isUploadDisabled ? 0.4 : 1 }}>
<UploadOutlined />
<div style={{ marginTop: 4, fontSize: 12 }}></div>
</div>
)}
</Upload>
</Space>
); );
} }

View File

@@ -15,8 +15,9 @@ export function SpinBox({ value, onChange, disabled, config }: WidgetProps) {
min={config.minValue as number | undefined} min={config.minValue as number | undefined}
max={config.maxValue as number | undefined} max={config.maxValue as number | undefined}
step={1} step={1}
style={{ width: '100%' }} style={{ width: '100%', minWidth: 140 }}
placeholder={(config.placeholder as string) || undefined} placeholder={(config.placeholder as string) || undefined}
controls
/> />
); );
} }