components/ 拆为三层:
- providers/ — 状态管理(App/Theme/SettingsProvider)
- shell/ — 应用外壳(Layout/NavBar/PageDispatcher)
- ui/ — 通用 UI 原子(FloatingPanel/LegalTextModal)
pages/ 重命名 + 重组:
- headers/ → panels/ NavBar 触发的面板页
- login/ → auth/ 认证模块(含注册/忘记密码)
- settings/blocks/ → sections/
- home/components/ → panels/ features/ controls/ hooks/
按职责分:panels(布局壳) + features(功能面板) + controls(表单控件) + hooks
其他清理:
- 删除空占位 Test3.tsx
- HomeContext 从 HomeContent 拆出独立文件
- uploadValidation 内联到 ImageInput
- 修复过时注释(useUI → controls)
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
// ============================================================
|
||
// 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, message } from 'antd';
|
||
import { UploadOutlined } from '@ant-design/icons';
|
||
import type { UploadFile, RcFile } from 'antd/es/upload';
|
||
import type { WidgetProps } from './types';
|
||
import { useFileUpload } from '@/hooks/use-api';
|
||
|
||
const { Text } = Typography;
|
||
|
||
/**
|
||
* 创建图片上传前的类型 & 大小校验函数(ImageInput / ImageListInput 共用)
|
||
* @param maxSizeMb 最大文件大小(MB),默认 10
|
||
*/
|
||
export function createImageBeforeUpload(maxSizeMb: number = 10) {
|
||
return (file: RcFile) => {
|
||
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 true;
|
||
};
|
||
}
|
||
|
||
/** 构建图片 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) {
|
||
// 关闭开关 → 表单值置为 undefined(isValueEmpty = 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>
|
||
);
|
||
}
|