## 首页整体架构
- 新增 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 动态计算(修复分页器遮挡)
138 lines
4.6 KiB
TypeScript
138 lines
4.6 KiB
TypeScript
// ============================================================
|
|
// NavBar — 自定义顶部导航栏(替代系统菜单)
|
|
//
|
|
// 逻辑:
|
|
// - 所有导航项默认全部显示(不再按登录状态隐藏)
|
|
// - 未登录时点击需登录的项 → 弹出提示"请先登录"并触发登录 Modal
|
|
// - 登录/注册 与 退出登录 仍按登录状态互斥显示
|
|
// ============================================================
|
|
|
|
import { useState, useCallback, useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { App } from 'antd';
|
|
|
|
import { useAppContext } from '@/contexts/app-context';
|
|
import { useTheme } from '@/hooks/use-theme';
|
|
import { emit, EVENTS } from '@/utils/event-bus.ts';
|
|
import { NavItem } from './NavItem';
|
|
import { AnnouncementDrawer } from './AnnouncementDrawer';
|
|
import { personalLeft, personalRight, enterpriseLeft, enterpriseRight } from './nav-config';
|
|
import type { NavItemConfig } from './types';
|
|
|
|
const NAVBAR_HEIGHT = 56;
|
|
|
|
export function NavBar() {
|
|
const { platform, edition, isLoggedIn, logout } = useAppContext();
|
|
const { isDark } = useTheme();
|
|
const { message } = App.useApp();
|
|
const navigate = useNavigate();
|
|
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
|
|
// 根据版本选择导航项
|
|
const { leftItems, rightItems } = useMemo(() => {
|
|
const isEnterprise = edition === 'enterprise';
|
|
return {
|
|
leftItems: isEnterprise ? enterpriseLeft : personalLeft,
|
|
rightItems: isEnterprise ? enterpriseRight : personalRight,
|
|
};
|
|
}, [edition]);
|
|
|
|
// 过滤规则:
|
|
// requireGuest → 仅未登录时可见(登录/注册)
|
|
// "退出登录" → 仅已登录时可见
|
|
// 其他所有项 → 始终可见,未登录点击时弹"请先登录"
|
|
const visibleLeft = useMemo(
|
|
() =>
|
|
leftItems.filter((item) => {
|
|
if (item.requireGuest && isLoggedIn) return false;
|
|
return !(item.key === 'logout' && !isLoggedIn);
|
|
}),
|
|
[leftItems, isLoggedIn],
|
|
);
|
|
const visibleRight = useMemo(
|
|
() =>
|
|
rightItems.filter((item) => {
|
|
if (item.requireGuest && isLoggedIn) return false;
|
|
return !(item.key === 'logout' && !isLoggedIn);
|
|
}),
|
|
[rightItems, isLoggedIn],
|
|
);
|
|
|
|
// 点击导航项
|
|
const handleNavClick = useCallback(
|
|
(item: NavItemConfig) => {
|
|
// 需要登录但未登录 → 提示并弹出登录 Modal
|
|
if (item.requireLogin && !isLoggedIn) {
|
|
message.warning('请先登录');
|
|
emit(EVENTS.SHOW_LOGIN);
|
|
return;
|
|
}
|
|
|
|
switch (item.action) {
|
|
case 'drawer':
|
|
if (item.key === 'announcements') {
|
|
setDrawerOpen(true);
|
|
}
|
|
break;
|
|
|
|
case 'button':
|
|
if (item.key === 'logout') {
|
|
logout();
|
|
}
|
|
break;
|
|
|
|
case 'external':
|
|
if (item.target) {
|
|
window.open(item.target, '_blank');
|
|
}
|
|
break;
|
|
|
|
case 'navigate':
|
|
case 'spa':
|
|
if (item.key === 'auth') {
|
|
emit(EVENTS.SHOW_LOGIN);
|
|
} else if (item.target) {
|
|
navigate(item.target);
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
[isLoggedIn, logout, message],
|
|
);
|
|
|
|
const macPaddingLeft = platform === 'darwin' ? 80 : 12;
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className="navbar-drag-region flex items-center justify-between select-none"
|
|
style={{
|
|
height: NAVBAR_HEIGHT,
|
|
paddingLeft: macPaddingLeft,
|
|
paddingRight: 12,
|
|
background: isDark
|
|
? 'linear-gradient(180deg, #1E1B4B 0%, #1A1730 100%)'
|
|
: 'linear-gradient(180deg, #FFFFFF 0%, #F5F3FF 100%)',
|
|
borderBottom: `1px solid ${isDark ? '#2E2A4A' : '#E5E7EB'}`,
|
|
boxShadow: isDark ? '0 1px 4px rgba(0,0,0,0.3)' : '0 1px 4px rgba(99,102,241,0.06)',
|
|
}}
|
|
>
|
|
<div className="navbar-no-drag flex items-center gap-0.5">
|
|
{visibleLeft.map((item) => (
|
|
<NavItem key={item.key} item={item} onClick={handleNavClick} />
|
|
))}
|
|
</div>
|
|
|
|
<div className="navbar-no-drag flex items-center gap-0.5">
|
|
{visibleRight.map((item) => (
|
|
<NavItem key={item.key} item={item} onClick={handleNavClick} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<AnnouncementDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
|
</>
|
|
);
|
|
}
|