## 首页整体架构
- 新增 src/pages/home/ — 完整首页模块(10 个组件)
- HomePage / HomeContent:三栏弹性布局,拖动分隔条调整列宽
- BannerCarousel:顶部轮播图(antd Carousel,2280×90 长条图片适配)
- LeftPanel:模型选择器(Menu 分组) + 任务记录(Table 分页),纵向拖拽调整比例
- CenterPanel / ModelInputForm:动态表单,根据模型 param_schema/ui_config 渲染控件
- RightPanel / OutputPreview:任务输出预览(Image/Video)
- 新增 Layout 组件:h-screen flex 布局,首页 Banner + NavBar + Outlet
## 数据层(数据与视图分离)
- 新增 src/services/modules/ — API 模块(banner/models/task/auth)
- TaskAPI 静态方法,TaskInfoItemResponseBody 判别联合类型
- ModelAPI / BannerAPI,const 断言路由常量
- 新增 src/hooks/use-async.ts — 通用异步 Hook
- useAsyncData:竞态处理、条件请求(enabled)、自动错误日志
- useAsyncMutation:数据变更,onSuccess/onError
- getErrorMessage():RequestErrorType → 可读错误信息
- 新增 src/hooks/use-api.ts — 业务 Hook
- useBannerList / useModelList / useTaskList / useTaskDetail / useSubmitTask
## 主题与交互
- 重写暗色令牌:科技感蓝紫(#080C24 底 / #0F1340 面板 / #2A3278 边框)
- 三栏分隔线:token.colorBorderSecondary 常驻 + 手柄竖线 hover 高亮
- 左面板纵向拖拽:模型区/任务区可调比例,ResizeObserver 自适应
- 暗色/亮色同步:tokens.ts + antd-theme.ts + globals.css 三处一致
## 基础设施
- EventBus 新增 TASK_SUBMITTED 事件(替代 Context 数字递增 hack)
- HomeContext 状态管理(选中模型、任务分页、折叠状态)
- 所有组件边框使用 token.colorBorderSecondary(主题自适应)
- Table 滚动高度 ResizeObserver 动态计算(修复分页器遮挡)
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
// ============================================================
|
||
// BannerCarousel — 首页顶部轮播图
|
||
// 使用 useBannerList() Hook 获取数据,组件仅负责渲染
|
||
// ============================================================
|
||
|
||
import { useRef, useMemo } from 'react';
|
||
import { Carousel, Skeleton } from 'antd';
|
||
import type { CarouselRef } from 'antd/es/carousel';
|
||
|
||
import { useBannerList } from '@/hooks/use-api';
|
||
import type { Banner } from '@/services/modules';
|
||
|
||
// ---------- 组件 Props ----------
|
||
|
||
interface BannerCarouselProps {
|
||
className?: string;
|
||
}
|
||
|
||
// ---------- 组件 ----------
|
||
|
||
export function BannerCarousel({ className }: BannerCarouselProps) {
|
||
const carouselRef = useRef<CarouselRef>(null);
|
||
const { data: rawBanners, loading } = useBannerList();
|
||
|
||
// 过滤 + 排序(纯计算,无副作用)
|
||
const banners = useMemo<Banner[]>(() => {
|
||
if (!rawBanners) return [];
|
||
return rawBanners
|
||
.filter((b) => b.is_active)
|
||
.filter((b) => b.category === 'banner')
|
||
.sort((a, b) => a.sort_order - b.sort_order);
|
||
}, [rawBanners]);
|
||
|
||
// 无数据时不渲染
|
||
if (!loading && banners.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
// 加载态 — 适配长条比例(2280×90)
|
||
if (loading) {
|
||
return (
|
||
<div className={className} style={{ width: '100%' }}>
|
||
<Skeleton.Input active block style={{ height: 90 }} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={`banner-carousel ${className ?? ''}`}
|
||
style={{ position: 'relative', width: '100%', overflow: 'hidden', lineHeight: 0 }}
|
||
>
|
||
<Carousel
|
||
ref={carouselRef}
|
||
autoplay
|
||
autoplaySpeed={5000}
|
||
effect="fade"
|
||
pauseOnHover
|
||
adaptiveHeight={true}
|
||
dots={banners.length > 1}
|
||
>
|
||
{banners.map((banner) => (
|
||
<div key={banner.id}>
|
||
<img
|
||
src={banner.image_url}
|
||
alt={banner.title}
|
||
style={{ width: '100%', height: 'auto', display: 'block' }}
|
||
/>
|
||
</div>
|
||
))}
|
||
</Carousel>
|
||
</div>
|
||
);
|
||
}
|