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:
@@ -1,17 +1,19 @@
|
||||
// ============================================================
|
||||
// ModelInputForm — 模型输入表单(纯布局外壳)
|
||||
//
|
||||
// 布局:Header(模型信息+meta) → Body(fields.map 渲染) → Footer(价格+提交)
|
||||
// 布局:Header(模型信息+meta) → Body(fields.map 渲染) → Footer(标签+重置+提交)
|
||||
//
|
||||
// 与旧版的区别:
|
||||
// - 控件渲染逻辑已提取到 useUI/ 组件体系
|
||||
// - param_schema 解析已提取到 useModelUI Hook
|
||||
// - 本组件只负责:布局、Form 容器、提交逻辑
|
||||
// - 字段间条件依赖(constraints_config.requires)自动处理禁用
|
||||
// - 本组件只负责:布局、Form 容器、提交逻辑、依赖禁用
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
import { useState, useLayoutEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Typography,
|
||||
Empty,
|
||||
@@ -20,7 +22,8 @@ import {
|
||||
theme as antTheme,
|
||||
message,
|
||||
} 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 { useHomeContext } from '@/pages/home/HomeContent';
|
||||
@@ -29,17 +32,70 @@ import { useSubmitTask } from '@/hooks/use-api';
|
||||
import { emit, EVENTS } from '@/utils/event-bus';
|
||||
import { OutputTypeMap } 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;
|
||||
|
||||
// ---------- 组件 ----------
|
||||
// ---------- 依赖字段包装组件 ----------
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
const { token } = antTheme.useToken();
|
||||
const { isLoggedIn } = useAppContext();
|
||||
const { selectedModel } = useHomeContext();
|
||||
const [form] = Form.useForm();
|
||||
const [taskTag, setTaskTag] = useState('');
|
||||
|
||||
// 提交突变 Hook
|
||||
const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({
|
||||
@@ -51,9 +107,13 @@ export function ModelInputForm() {
|
||||
|
||||
// ======== useModelUI:param_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 iv: Record<string, unknown> = {};
|
||||
fields.forEach((f) => {
|
||||
@@ -64,12 +124,22 @@ export function ModelInputForm() {
|
||||
return iv;
|
||||
}, [fields]);
|
||||
|
||||
// 选中模型时重置表单
|
||||
useEffect(() => {
|
||||
if (selectedModel) {
|
||||
form.resetFields();
|
||||
|
||||
// 选中模型时加载默认值(useLayoutEffect 在 DOM 更新后、浏览器绘制前执行,避免闪烁)
|
||||
// antd 有外部 form 实例时 initialValues prop 被忽略,必须手动 setFieldsValue
|
||||
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 {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 将表单值全部转为字符串(CreatTaskRequestBody.params 为 Record<string, string>)
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
for (const field of fields) {
|
||||
const value = values[field.name];
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
// 文件上传 → 提取 URL 数组 → JSON 字符串
|
||||
if (field.isFileWidget) {
|
||||
const fileList = value as UploadFile[];
|
||||
const urls = fileList
|
||||
@@ -103,20 +171,22 @@ export function ModelInputForm() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 布尔值 → "true"/"false"
|
||||
if (typeof value === 'boolean') {
|
||||
params[field.name] = String(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 数值 / 字符串 → String
|
||||
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) {
|
||||
if (err && typeof err === 'object' && 'errorFields' in err) {
|
||||
return; // 表单验证错误,antd 自动提示
|
||||
return;
|
||||
}
|
||||
message.error(errorMessage || '任务提交失败,请重试');
|
||||
}
|
||||
@@ -232,12 +302,10 @@ export function ModelInputForm() {
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="small"
|
||||
initialValues={initialValues}
|
||||
>
|
||||
{fields.map((field) => (
|
||||
<Form.Item
|
||||
key={field.index}
|
||||
key={`${selectedModel.id}-${field.name}`}
|
||||
name={field.name}
|
||||
label={
|
||||
<span>
|
||||
@@ -245,7 +313,7 @@ export function ModelInputForm() {
|
||||
{field.tips && (
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 11, marginLeft: 6 }}
|
||||
style={{ fontSize: 12, marginLeft: 6 }}
|
||||
>
|
||||
{field.tips.length > 60
|
||||
? `${field.tips.slice(0, 60)}...`
|
||||
@@ -260,19 +328,18 @@ export function ModelInputForm() {
|
||||
: undefined
|
||||
}
|
||||
valuePropName={field.valuePropName}
|
||||
// 文件控件:从 Upload onChange 事件中提取 fileList
|
||||
{...(field.isFileWidget
|
||||
? { getValueFromEvent: (e: { fileList: UploadFile[] }) => e.fileList }
|
||||
: {})}
|
||||
>
|
||||
<field.component config={field.widgetConfig} />
|
||||
<DependentFormItem field={field} form={form} />
|
||||
</Form.Item>
|
||||
))}
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ======== Footer:价格 + 提交 ======== */}
|
||||
{/* ======== Footer:标签 + 重置 + 提交 ======== */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
@@ -280,24 +347,24 @@ export function ModelInputForm() {
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{price !== undefined && priceType === 'SIMPLE' && (
|
||||
<Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||
¥{price}/{priceUnit || '次'}
|
||||
</Text>
|
||||
)}
|
||||
{priceType === 'MULTI_DIMENSION' && (
|
||||
<Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||
按规格计费
|
||||
</Text>
|
||||
)}
|
||||
{priceType === 'BILLING_ADAPTER' && (
|
||||
<Text type="secondary" style={{ fontSize: 12, whiteSpace: 'nowrap' }}>
|
||||
按用量计费
|
||||
</Text>
|
||||
)}
|
||||
<Input
|
||||
placeholder="自定义任务标签(可选)"
|
||||
value={taskTag}
|
||||
onChange={(e) => setTaskTag(e.target.value)}
|
||||
allowClear
|
||||
maxLength={50}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
disabled={!selectedModel}
|
||||
title="重置参数"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -305,15 +372,18 @@ export function ModelInputForm() {
|
||||
onClick={handleSubmit}
|
||||
loading={submitting}
|
||||
disabled={!isLoggedIn || !selectedModel}
|
||||
block
|
||||
size="large"
|
||||
style={{
|
||||
background: token.colorPrimary,
|
||||
height: 44,
|
||||
fontSize: 16,
|
||||
height: 40,
|
||||
fontSize: 14,
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
开始生成
|
||||
{priceType === 'SIMPLE' && price !== undefined
|
||||
? `消费 ¥${price}/${priceUnit || '次'}`
|
||||
: '消费'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// 职责:
|
||||
// - 遍历模型的 param_schema,解析 spec/ui 字段
|
||||
// - 查 WIDGET_MAP 获取对应的 React 组件
|
||||
// - 解析 constraints_config.requires 计算字段间依赖
|
||||
// - 组装出可直接渲染的 UIFieldConfig 数组
|
||||
//
|
||||
// ModelInputForm 只需遍历 fields 渲染 Form.Item 即可,
|
||||
@@ -43,6 +44,14 @@ export interface UIFieldConfig {
|
||||
valuePropName?: string;
|
||||
/** 是否为文件上传类控件(需要 getValueFromEvent 提取 fileList) */
|
||||
isFileWidget: boolean;
|
||||
/**
|
||||
* 条件依赖:当这些字段的值为空时,本字段应被禁用。
|
||||
*
|
||||
* 来源于 constraints_config.requires 的逆向推导:
|
||||
* { if: "sref", then: ["sw", "sv"] }
|
||||
* → sw.dependsOn = ["sref"], sv.dependsOn = ["sref"]
|
||||
*/
|
||||
dependsOn?: string[];
|
||||
/** 传给 widget 组件的配置(placeholder/options/min/max 等) */
|
||||
widgetConfig: Record<string, unknown>;
|
||||
}
|
||||
@@ -51,49 +60,76 @@ export interface UIFieldConfig {
|
||||
|
||||
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 依赖 sref,sref 为空时它们应被禁用。
|
||||
*/
|
||||
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 ----------
|
||||
|
||||
/**
|
||||
* 根据模型的 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[] — 渲染就绪的字段配置列表
|
||||
*
|
||||
* 使用方式:
|
||||
* ```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(
|
||||
paramSchema: ModelsListResponseBody['param_schema'],
|
||||
constraintsConfig: Record<string, unknown> = {},
|
||||
): UIFieldConfig[] {
|
||||
return useMemo(() => {
|
||||
if (!paramSchema || paramSchema.length === 0) return [];
|
||||
|
||||
const depMap = buildDependencyMap(constraintsConfig);
|
||||
|
||||
return paramSchema.map((item, index) => {
|
||||
const spec = (item.spec || {}) as Record<string, unknown>;
|
||||
const ui = (item.ui || {}) as Record<string, unknown>;
|
||||
const widget = (ui.widget as string) || 'lineEdit';
|
||||
|
||||
// 查找组件(未知 widget 用 LineInput 兜底)
|
||||
const component = WIDGET_MAP[widget] || FALLBACK_WIDGET;
|
||||
|
||||
// 构建传给组件的配置
|
||||
const widgetConfig: Record<string, unknown> = {
|
||||
placeholder: ui.placeholder || `请输入${ui.label || item.map_to}`,
|
||||
required: spec.required,
|
||||
// comboBox
|
||||
enumValues: spec.enum,
|
||||
// spinBox / doubleSpinBox
|
||||
minValue: spec.min_value,
|
||||
maxValue: spec.max_value,
|
||||
step: spec.single_step,
|
||||
// textEdit / lineEdit
|
||||
minLength: spec.min_length,
|
||||
maxLength: spec.max_length,
|
||||
// imageInput / imageList / audioList
|
||||
@@ -101,36 +137,43 @@ export function useModelUI(
|
||||
maxFileSizeMb: spec.max_file_size_mb,
|
||||
minFiles: spec.min_files,
|
||||
maxFiles: spec.max_files,
|
||||
// imageInput
|
||||
enableSwitch: ui.enable_switch,
|
||||
};
|
||||
|
||||
const isFile = FILE_WIDGETS.has(widget);
|
||||
const isCheckbox = widget === 'checkbox';
|
||||
const name = item.map_to;
|
||||
const dependsOn = depMap.get(name);
|
||||
|
||||
return {
|
||||
index,
|
||||
name: item.map_to,
|
||||
name,
|
||||
component,
|
||||
label: (ui.label as string) || item.map_to,
|
||||
label: (ui.label as string) || name,
|
||||
must: (spec.required as boolean) || false,
|
||||
defaultValue: spec.default,
|
||||
tips: (ui.tips as string) || undefined,
|
||||
valuePropName: isCheckbox ? 'checked' : undefined,
|
||||
isFileWidget: isFile,
|
||||
dependsOn: dependsOn && dependsOn.length > 0 ? dependsOn : undefined,
|
||||
widgetConfig,
|
||||
};
|
||||
});
|
||||
}, [paramSchema]);
|
||||
}, [paramSchema, constraintsConfig]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个 widget 类型是否为文件上传类控件。
|
||||
*
|
||||
* 文件控件在 Form.Item 中需要特殊处理:
|
||||
* - getValueFromEvent 提取 e.fileList
|
||||
* - value 类型为 UploadFile[] 而非 string
|
||||
*/
|
||||
/** 判断 widget 类型是否为文件上传类控件 */
|
||||
export function isFileWidget(widgetName: string): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ export function DoubleSpinBox({ value, onChange, disabled, config }: WidgetProps
|
||||
min={config.minValue as number | undefined}
|
||||
max={config.maxValue as number | undefined}
|
||||
step={config.step as number || 0.1}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: '100%', minWidth: 140 }}
|
||||
placeholder={(config.placeholder as string) || undefined}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// ============================================================
|
||||
// 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 type { UploadFile } from 'antd/es/upload';
|
||||
import type { WidgetProps } from './types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
/** 构建图片 accept 字符串 */
|
||||
function buildAccept(filters?: string[]): string {
|
||||
if (!filters || filters.length === 0) return 'image/png,image/jpg,image/jpeg,image/webp';
|
||||
@@ -17,15 +25,47 @@ function buildAccept(filters?: string[]): string {
|
||||
export function ImageInput({ value, onChange, disabled, config }: WidgetProps) {
|
||||
const fileList = (value as UploadFile[]) || [];
|
||||
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 (
|
||||
<Space orientation={"vertical"}
|
||||
style={{ width: '100%' }} size={8}>
|
||||
{hasEnableSwitch && (
|
||||
<Space size={8}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={switchOn}
|
||||
onChange={handleSwitchChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text type={switchOn ? 'success' : 'secondary'} style={{ fontSize: 12 }}>
|
||||
{switchOn ? '已启用' : '点击开关启用上传'}
|
||||
</Text>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
maxCount={1}
|
||||
accept={buildAccept(config.fileFilter as string[] | undefined)}
|
||||
fileList={fileList}
|
||||
onChange={({ fileList: newList }) => onChange?.(newList)}
|
||||
disabled={disabled}
|
||||
onChange={({ fileList: newList }) => onChange?.(newList as unknown as UploadFile[])}
|
||||
disabled={isUploadDisabled}
|
||||
beforeUpload={(file) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
if (!isImage) {
|
||||
@@ -40,11 +80,12 @@ export function ImageInput({ value, onChange, disabled, config }: WidgetProps) {
|
||||
}}
|
||||
>
|
||||
{fileList.length < 1 && (
|
||||
<div>
|
||||
<div style={{ opacity: isUploadDisabled ? 0.4 : 1 }}>
|
||||
<UploadOutlined />
|
||||
<div style={{ marginTop: 4, fontSize: 12 }}>上传</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ export function SpinBox({ value, onChange, disabled, config }: WidgetProps) {
|
||||
min={config.minValue as number | undefined}
|
||||
max={config.maxValue as number | undefined}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: '100%', minWidth: 140 }}
|
||||
placeholder={(config.placeholder as string) || undefined}
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user