Files
ele-HeiXiu/src/pages/home/components/useUI/ImageInput.tsx
YoungestSongMo 9e8c5b7109 feat: 任务轮询 + Token刷新容错 + 提交防抖 + 文件上传Hook (0.0.20)
### 任务状态轮询
- 新增 useTaskPolling hook:批量轮询非终态任务,递归 setTimeout + next_poll_ms 自适应间隔
- TaskHistory 集成批量轮询,OutputPreview 移除独立轮询改为状态转换监听
- batchTaskStatus 修复数组参数序列化(GET + 逗号拼接,避免 bracket 格式)
- 统一 POLLING_STATUSES 常量管理非终态状态

### Token 刷新容错
- 双轨刷新:主动续期(JWT exp 75% 时间点 setTimeout)+ 被动兜底(handle401)
- 刷新失败分级:永久失败(throw RefreshTokenExpiredError)→清token弹登录;临时失败(return null)→仅拒绝当前请求
- AppProvider 登录/自动登录/退出 启停主动刷新定时器

### 提交按钮防抖
- 2s 节流防抖("你点击的太快了")+ 二次确认弹窗("您已提交,确认要再次提交吗?")
- 修复 useAsyncMutation.execute 返回值判空(原 try/catch 死代码)

### 文件上传 Hook
- 新增 useFileUpload hook:封装 request.upload() 为 antd Upload 兼容 customRequest
- ImageInput/ImageListInput/AudioListInput 集成 customRequest
- uploadValidation beforeUpload 返回值修正(false→true)

### 其他
- AnnouncementDrawer 接入公告 API + EnumResolver
- billing/announcement/banner 服务模块完善
- CLAUDE.md 补充架构约定文档
2026-06-12 19:03:36 +08:00

102 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// ImageInput — imageInput → Upload单张图片
// 用于垫图、参考图等单图上传场景
//
// enable_switch 支持:
// 当 API ui.enable_switch === true 时,默认禁用(灰色),
// 需点击 Switch 开关后才能上传——匹配原 Qt 客户端行为。
// 开关关闭时清除已上传文件。
//
// 表单值语义(配合 constraints_config.requires 依赖禁用):
// undefined → 开关关闭 / 未启用 → isValueEmpty = true → 依赖字段禁用
// [] → 开关开启但未上传文件 → isValueEmpty = false → 依赖字段启用
// [{...}] → 已上传文件 → isValueEmpty = false → 依赖字段启用
// ============================================================
import { useState, useCallback } from 'react';
import { Upload, Switch, Space, Typography } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload';
import type { WidgetProps } from './types';
import { createImageBeforeUpload } from './uploadValidation';
import { useFileUpload } from '@/hooks/use-api';
const { Text } = Typography;
/** 构建图片 accept 字符串 */
function buildAccept(filters?: string[]): string {
if (!filters || filters.length === 0) return 'image/png,image/jpg,image/jpeg,image/webp';
return filters.map((ext) => `image/${ext}`).join(',');
}
/** 标记"已启用但空"的空数组,避免 react 渲染时引用变化 */
const ENABLED_EMPTY: UploadFile[] = [];
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;
// 文件上传 Hook提供 customRequest → antd Upload 集成)
const { customRequest } = useFileUpload();
// 有 enable_switch 时默认禁用,需手动开启
const [switchOn, setSwitchOn] = useState(!hasEnableSwitch);
const isUploadDisabled = disabled || !switchOn;
const handleSwitchChange = useCallback(
(on: boolean) => {
setSwitchOn(on);
if (!on) {
// 关闭开关 → 表单值置为 undefinedisValueEmpty = true依赖字段禁用
onChange?.(undefined);
} else {
// 开启开关 → 空数组 = "已启用但未上传"isValueEmpty = false依赖字段启用
onChange?.(ENABLED_EMPTY 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}
customRequest={customRequest}
onChange={({ fileList: newList }) => {
// 仅当开关开启时上报文件变化
if (switchOn || !hasEnableSwitch) {
onChange?.(newList as unknown as UploadFile[]);
}
}}
disabled={isUploadDisabled}
beforeUpload={createImageBeforeUpload(maxSizeMb)}
>
{fileList.length < 1 && (
<div style={{ opacity: isUploadDisabled ? 0.4 : 1 }}>
<UploadOutlined />
<div style={{ marginTop: 4, fontSize: 12 }}></div>
</div>
)}
</Upload>
</Space>
);
}