Files
ele-HeiXiu/src/modules/navbar/views/NavBar.tsx
YoungestSongMo 136a17c0bf feat: 任务列表右键菜单回调 + 主进程下载 + sharp 缩略图 + IPC 精简重构
- 任务右键菜单:实现打开输出目录(inputs/outputs/.cache 结构)、
    复制资源链接、重新编辑、再次生成、拉取结果、重新下载、收藏、删除记录
  - 主进程下载:通过 Electron net 模块绕过 CORS,支持 CDN 媒体文件下载
  - sharp 缩略图:替换 nativeImage,支持 4K/8K+ 大尺寸素材的流式处理
  - IPC 精简:40+ 冗余通道 → 15 个分组通道,移除未使用的 preload 桥接方法
  - 任务存储:新增收藏字段、schema 迁移、task-store 持久化
  - 清理废弃模块:canvas-ipc / updater / request / pricing

refactor: IPC/Preload/Services 模块化拆分 — 单一职责 + 向后兼容

  主要变更:
  - shared/constants/ipc-channels.ts → ipc/ 模块 (base/storage/canvas/updater)
  - electron/preload.ts → preload/ 模块 (base/safe-storage/file-storage/canvas)
  - electron/main/ipc/canvas-ipc.ts → canvas/ 模块 (types/walk/sidecar/mime/thumbnail)
  - electron/main/ipc/storage-ipc.ts → 提取 utils/ (path/download/thumbnail)
  - electron/main/updater.ts → updater/ 模块 (provider/platform-updaters)
  - src/services/request.ts → request/ 模块 (token/interceptors/methods)
  - src/shared/utils/pricing.ts → pricing/ 模块 (types/helpers/format/calculate/fields)
  - src/modules/home/center/views/ModelInputForm.tsx → 提取 utils + DependentFormItem

  架构优化:
  - 所有拆分均保持向后兼容(旧导入路径仍可用)
  - TypeScript 编译通过 ✓
  - 每个模块单一职责,平均 ~130 行
2026-07-03 11:13:36 +08:00

169 lines
6.6 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.
// ============================================================
// NavBar — 自定义顶部导航栏(替代系统菜单)
//
// 逻辑:
// - 所有导航项默认全部显示(不再按登录状态隐藏)
// - 未登录时点击需登录的项 → 弹出提示"请先登录"并触发登录 Modal
// - 登录/注册 与 退出登录 仍按登录状态互斥显示
// ============================================================
import {useState, useCallback, useMemo} from 'react';
import {App} from 'antd';
import {useAppContext} from '@/app/providers/AppProvider';
import {useTheme} from '@/app/providers/ThemeProvider';
import {emit, EVENTS, safeIpcSend} from '@/shared/utils/bus';
import {RENDERER_TO_MAIN_CANVAS} from '@shared/constants/ipc';
import {useAnnouncements} from '@/shared/hooks/use-announcements';
import {NavItem} from './NavItem';
import {AnnouncementDrawer} from '@/modules/announcement/views/AnnouncementDrawer';
import {personalLeft, personalRight, enterpriseLeft, enterpriseRight} from '../settings/nav-config';
import type {NavItemConfig} from '../model/types';
const NAVBAR_HEIGHT = 56;
export function NavBar() {
const {platform, edition, isLoggedIn, logout} = useAppContext();
const {isDark} = useTheme();
const {message} = App.useApp();
const [drawerOpen, setDrawerOpen] = useState(false);
// 公告轮询 + 未读缓存
const { announcements, unreadCount, loading: announcementsLoading, markAsRead, refetch: refetchAnnouncements } = useAnnouncements();
// 根据版本选择导航项
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: NavItemConfig[] = 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);
refetchAnnouncements();
markAsRead();
}
break;
case 'button':
if (item.key === 'logout') {
logout();
} else if (item.key === 'settings') {
emit(EVENTS.OPEN_SETTINGS);
} else if (item.key === 'auth') {
emit(EVENTS.SHOW_LOGIN);
}
break;
case 'external':
if (item.target) {
window.open(item.target, '_blank');
}
break;
case 'page':
// 项目管理 → 打开独立画布窗口(新 BrowserWindow非主窗口内叠加层
if (item.key === 'project-management') {
console.log('[NavBar] 🖼️ 点击"项目管理" → 发送 OPEN_CANVAS_WINDOW IPC');
safeIpcSend(RENDERER_TO_MAIN_CANVAS.OPEN_CANVAS_WINDOW);
return;
}
if (item.target) {
emit(EVENTS.OPEN_PAGE, {
target: item.target,
modalType: item.modalType ?? 'floating',
label: item.label,
});
}
break;
}
},
[isLoggedIn, logout, message, markAsRead, refetchAnnouncements],
);
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}
badgeCount={item.key === 'announcements' ? unreadCount : undefined}
/>
))}
</div>
<div className="navbar-no-drag flex items-center gap-0.5">
{visibleRight.map((item) => (
<NavItem
key={item.key}
item={item}
onClick={handleNavClick}
badgeCount={item.key === 'announcements' ? unreadCount : undefined}
/>
))}
</div>
</div>
<AnnouncementDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
announcements={announcements}
loading={announcementsLoading}
/>
</>
);
}