Files
ele-HeiXiu/src/modules/home/center/views/widgets/upload-widgets.tsx
YoungestSongMo 904edecd90 refactor: Home 模块 VCHSM 三栏重构 + Services 迁回 + 右键菜单体系 + 公告修复
## Home 模块三栏 VCHSM 重组
- 按渲染归属重新分配文件:ModelSelector/TaskHistory → left/,ModelInputForm/widgets → center/,OutputPreview → right/
- BannerCarousel 移至 shared/views/(全局组件)
- controls/ → widgets/ 重命名,消除与 controllers/ 的命名歧义
- 新增 barrel 文件(left/center/right/shared 各级 index.ts)

## Services 层迁回 src/services/modules/
- 全部 API 类型定义与请求函数从 modules/*/controllers/ 迁回 services/modules/{domain}/
- 恢复原始文件名(model-api.ts → models.ts,task-api.ts → task.ts 等)
- 新增 domain 子文件夹(home/auth/announcement/account)
- 更新全局 ~70 处导入路径

## TaskHistory 提取 Controller + Hook 层
- 新建 left/controllers/task-table-config.ts(常量 + 纯工具函数,零 React 依赖)
- 新建 left/hooks/useTaskTable.tsx(状态管理 + 数据获取 + 列定义 + 滚动同步)
- TaskHistory.tsx 从 915 行精简为 265 行纯视图

## 右键菜单体系完善
- 共享 <ContextMenu> 组件(antd Dropdown 封装,防 Select 冲突)
- 四组菜单定义:任务列表 / 文本输入 / 媒体参考 / 预览区域
- 统一的 ContextMenuItems → AntD MenuProps 转换

## 公告功能修复
- useAnnouncements 增加 isLoggedIn 登录门控,避免未登录时 401 双重请求
- 抽屉打开时触发 refetch(),确保拿到最新数据

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-30 13:39:38 +08:00

159 lines
4.7 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.
// ============================================================
// upload-widgets — 单文件上传控件
// 合并自 ImageInput.tsx + VideoInput.tsx
//
// 两者共享 ~90% 的 enable_switch + Upload 模式,
// 差异仅在于accept 类型、beforeUpload 校验、图标、默认 maxSize。
// 提取 useEnableSwitch() + SingleUpload 去重。
// ============================================================
import { useState, useCallback } from 'react';
import { Upload, Switch, Space, Typography } from 'antd';
import { UploadOutlined, VideoCameraOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload';
import type { WidgetProps } from './types';
import { useFileUpload } from '@/shared/hooks/use-api';
import {
buildAccept,
buildVideoAccept,
createMediaBeforeUpload,
createVideoBeforeUpload,
} from '@/shared/utils/media-upload';
const { Text } = Typography;
// ════ enable_switch 共享 Hook ════
const ENABLED_EMPTY: UploadFile[] = [];
function useEnableSwitch(
hasEnableSwitch: boolean,
disabled: boolean,
onChange: WidgetProps['onChange'],
) {
const [switchOn, setSwitchOn] = useState(!hasEnableSwitch);
const isUploadDisabled = disabled || !switchOn;
const handleSwitchChange = useCallback(
(on: boolean) => {
setSwitchOn(on);
if (!on) {
onChange?.(undefined);
} else {
onChange?.(ENABLED_EMPTY as unknown as UploadFile[]);
}
},
[onChange],
);
return { switchOn, isUploadDisabled, handleSwitchChange };
}
// ════ SingleUpload 内部组件 ════
interface SingleUploadProps {
value: unknown;
onChange: WidgetProps['onChange'];
disabled: boolean;
config: Record<string, unknown>;
/** 'image' | 'video' */
mediaType: 'image' | 'video';
icon: React.ReactNode;
uploadLabel: string;
defaultMaxSizeMb: number;
}
function SingleUpload({
value,
onChange,
disabled,
config,
mediaType,
icon,
uploadLabel,
defaultMaxSizeMb,
}: SingleUploadProps) {
const fileList = (value as UploadFile[]) || [];
const maxSizeMb = (config.maxFileSizeMb as number) || defaultMaxSizeMb;
const filters = config.fileFilter as string[] | undefined;
const hasEnableSwitch = (config.enableSwitch as boolean) || false;
const { customRequest } = useFileUpload();
const { switchOn, isUploadDisabled, handleSwitchChange } = useEnableSwitch(hasEnableSwitch, disabled, onChange);
const accept = mediaType === 'video'
? buildVideoAccept(filters)
: buildAccept(filters);
const beforeUpload = mediaType === 'video'
? createVideoBeforeUpload(maxSizeMb)
: createMediaBeforeUpload(maxSizeMb, filters);
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={accept}
fileList={fileList}
customRequest={customRequest}
onChange={({ fileList: newList }) => {
if (switchOn || !hasEnableSwitch) onChange?.(newList as unknown as UploadFile[]);
}}
disabled={isUploadDisabled}
beforeUpload={beforeUpload}
>
{fileList.length < 1 && (
<div style={{ opacity: isUploadDisabled ? 0.4 : 1 }}>
{icon}
<div style={{ marginTop: 4, fontSize: 12 }}>{uploadLabel}</div>
</div>
)}
</Upload>
</Space>
);
}
// ════ 公开导出(保持原组件名兼容 WIDGET_MAP ════
export function ImageInput(props: WidgetProps) {
const { value, onChange, disabled, config } = props;
return (
<SingleUpload
value={value}
onChange={onChange}
disabled={disabled ?? false}
config={config}
mediaType="image"
icon={<UploadOutlined />}
uploadLabel="上传"
defaultMaxSizeMb={10}
/>
);
}
export function VideoInput(props: WidgetProps) {
const { value, onChange, disabled, config } = props;
return (
<SingleUpload
value={value}
onChange={onChange}
disabled={disabled ?? false}
config={config}
mediaType="video"
icon={<VideoCameraOutlined />}
uploadLabel="上传视频"
defaultMaxSizeMb={50}
/>
);
}