From a7ee616afca15d12b75f96eee47e2fbdeb73f9aa Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 17:56:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=8D=87=E7=BA=A7=E9=A1=B6?= =?UTF-8?q?=E6=A0=8F=E4=B8=BA=E6=9E=81=E7=AE=80=E7=BA=A7=E8=81=94=E9=9D=A2?= =?UTF-8?q?=E5=8C=85=E5=B1=91=E6=9E=B6=E6=9E=84=EF=BC=8C=E8=87=AA=E9=80=82?= =?UTF-8?q?=E5=BA=94=20320px=20=E6=9E=81=E7=AA=84=E8=A7=86=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- web/src/components/doc/SlimTabBar.tsx | 585 ++++++++++++++++---------- 1 file changed, 363 insertions(+), 222 deletions(-) diff --git a/web/src/components/doc/SlimTabBar.tsx b/web/src/components/doc/SlimTabBar.tsx index b6f7b03e..3187d206 100644 --- a/web/src/components/doc/SlimTabBar.tsx +++ b/web/src/components/doc/SlimTabBar.tsx @@ -1,7 +1,17 @@ -import React from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { DeliverableItem, DeliverableCategory } from '../../types/workbench'; import { DocLifecycleState } from '../../types/docgov'; -import { FolderKanban, FileDown, Maximize2, Minimize2, Lock, Globe, Shield } from 'lucide-react'; +import { + FileDown, + Maximize2, + Minimize2, + Lock, + Globe, + Shield, + ChevronDown, + Layers, + Check, +} from 'lucide-react'; interface SlimTabBarProps { deliverables: DeliverableItem[]; @@ -26,6 +36,52 @@ export const SlimTabBar: React.FC = ({ isFullscreen, onToggleFullscreen, }) => { + // 下拉菜单显示状态 + const [showCategoryMenu, setShowCategoryMenu] = useState(false); + const [showDocMenu, setShowDocMenu] = useState(false); + + // 容器宽度监听(用于右侧大盘按钮在空间充裕时展示文字,窄栏时优雅收缩为纯数字徽标) + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(600); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width); + } + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + // 点击外部及 Escape 键自动收起下拉浮层 + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest('.dropdown-trigger') && !target.closest('.dropdown-menu')) { + setShowCategoryMenu(false); + setShowDocMenu(false); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setShowCategoryMenu(false); + setShowDocMenu(false); + } + }; + + window.addEventListener('mousedown', handleClickOutside); + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('keydown', handleKeyDown); + }; + }, []); + const getStatusDot = (state: DocLifecycleState) => { switch (state) { case 'approved': @@ -47,6 +103,7 @@ export const SlimTabBar: React.FC = ({ const privateDocs = deliverables.filter((d) => d.category === 'private'); const visibleTabs = deliverables.filter((d) => d.category === activeCategory); + const activeDoc = deliverables.find((d) => d.docId === activeDocId) || visibleTabs[0]; const handleCategoryClick = (cat: DeliverableCategory) => { onSelectCategory(cat); @@ -56,8 +113,38 @@ export const SlimTabBar: React.FC = ({ } }; + // 分类属性映射 + const categoryConfig: Record< + DeliverableCategory, + { label: string; icon: React.ReactNode; color: string; count: number } + > = { + workflow: { + label: '流程受控', + icon: , + color: 'var(--brand-500)', + count: workflowDocs.length, + }, + shared: { + label: '项目共享', + icon: , + color: 'var(--brand-500)', + count: sharedDocs.length, + }, + private: { + label: '我的私密', + icon: , + color: '#D97706', + count: privateDocs.length, + }, + }; + + const currentCatInfo = categoryConfig[activeCategory] || categoryConfig.workflow; + const docStatus = activeDoc ? getStatusDot(activeDoc.lifecycleState) : null; + const isCompact = containerWidth < 460; + return (
= ({ display: 'flex', alignItems: 'center', padding: '0 8px', - gap: '8px', - overflowX: 'auto', + gap: '6px', flexShrink: 0, userSelect: 'none', + position: 'relative', + zIndex: 10, + width: '100%', }} > - {/* 分类切换分段 Pill */} -
+ {/* 1. 分类级联选择器 (Breadcrumb Category Step) */} +
- - - - -
- -
- - {/* 交付物 Tabs (当前分类下的文档) */} -
- {visibleTabs.length === 0 ? ( -
- {activeCategory === 'private' - ? '暂无个人私密生成文档 (在对话中要求助手生成即可)' - : activeCategory === 'shared' - ? '暂无项目共享参考资料' - : '暂无流程文档'} -
- ) : ( - visibleTabs.map((doc) => { - const isActive = doc.docId === activeDocId; - const status = getStatusDot(doc.lifecycleState); - - return ( -
onSelectDoc(doc.docId)} - style={{ - height: '28px', - padding: '0 10px', - borderRadius: '6px 6px 0 0', - background: isActive ? 'var(--bg-panel)' : 'transparent', - border: isActive ? '1px solid var(--border)' : '1px solid transparent', - borderBottom: isActive ? '1px solid var(--bg-panel)' : '1px solid transparent', - display: 'flex', - alignItems: 'center', - gap: '6px', - fontSize: '12px', - fontWeight: isActive ? 600 : 500, - color: isActive ? 'var(--text-strong)' : 'var(--text-muted)', - cursor: 'pointer', - whiteSpace: 'nowrap', - transition: 'all 0.15s ease', - }} - onMouseEnter={(e) => { - if (!isActive) e.currentTarget.style.color = 'var(--text)'; - }} - onMouseLeave={(e) => { - if (!isActive) e.currentTarget.style.color = 'var(--text-muted)'; - }} - title={`${doc.title} (${doc.category === 'private' ? '仅我可见' : doc.category === 'shared' ? '项目共享' : status.label})`} - > - {doc.category === 'private' ? ( - - ) : doc.category === 'shared' ? ( - - ) : ( -
- )} - - {doc.title} - -
- ); - }) - )} -
- - {/* 右侧全局操作区 */} -
- {/* 全部交付物大盘抽屉入口 */} - - {/* 导出受控 Word 按钮 */} + {showCategoryMenu && ( +
+ {(['workflow', 'shared', 'private'] as DeliverableCategory[]).map((cat) => { + const info = categoryConfig[cat]; + const isSelected = activeCategory === cat; + return ( + + ); + })} +
+ )} +
+ + {/* 面包屑斜杠 */} + / + + {/* 2. 当前文档级联下拉选择器 (占满中间自适应空间,带文本截断与状态点) */} +
+ + + {showDocMenu && ( +
+
+ {currentCatInfo.label}清单 ({visibleTabs.length} 份): +
+ {visibleTabs.length === 0 ? ( +
+ 暂无文档 +
+ ) : ( + visibleTabs.map((doc) => { + const isCur = doc.docId === activeDocId; + const st = getStatusDot(doc.lifecycleState); + return ( + + ); + }) + )} +
+ )} +
+ + {/* 3. 右侧全局紧凑操作区 */} +
+ {/* 全景大盘抽屉入口 (宽屏带文字,窄屏收缩为纯图标+数字) */} + + + {/* 导出 DOCX 图标 */} {onExportDocx && ( )} - {/* 全屏切换 */} + {/* 全屏切换图标 */} {onToggleFullscreen && ( )}