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>
This commit is contained in:
158
src/modules/home/center/views/widgets/upload-widgets.tsx
Normal file
158
src/modules/home/center/views/widgets/upload-widgets.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
// ============================================================
|
||||
// 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user