feat: TodoPanel 改为常驻右边栏并支持点击跳转
- TodoPanel 移除浮动定位与拖动逻辑,改为常驻右边栏 - 右边栏与左边栏对称的折叠/展开逻辑,localStorage 持久化 - 待办/记忆/技能三 Tab 切换 - 点击已完成 todo 滚动并高亮对应 tool 消息 - 修复 MessageBubble isMergedTool 分支缺失 data-message-id 导致跳转失效 - ChatContainer 移除 todoPanel prop
This commit is contained in:
parent
4766f838a2
commit
cb0d3d932d
107
web/src/App.tsx
107
web/src/App.tsx
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, PanelLeftClose, PanelLeftOpen, X, Brain, Settings as SettingsIcon, ChevronRight } from 'lucide-react'
|
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelLeftClose, PanelLeftOpen, Brain, Settings as SettingsIcon, ChevronRight } from 'lucide-react'
|
||||||
import { ChatContainer } from './components/Chat/ChatContainer'
|
import { ChatContainer } from './components/Chat/ChatContainer'
|
||||||
import { TopicList } from './components/Sidebar/TopicList'
|
import { TopicList } from './components/Sidebar/TopicList'
|
||||||
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
||||||
@ -103,22 +103,25 @@ function App() {
|
|||||||
setSendMessage(sendMessage)
|
setSendMessage(sendMessage)
|
||||||
}, [setSendMessage, sendMessage])
|
}, [setSendMessage, sendMessage])
|
||||||
|
|
||||||
// ---- 主题状态 ----
|
// ---- 右边栏状态(与左边栏对称的折叠/展开逻辑) ----
|
||||||
|
|
||||||
const [memoryPanelOpen, setMemoryPanelOpen] = useState(() => {
|
const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(() => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem('picobot-memory-panel-open') !== 'false'
|
return localStorage.getItem('picobot-right-sidebar-collapsed') === 'true'
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleMemoryPanel = useCallback((open: boolean) => {
|
const toggleRightSidebar = useCallback(() => {
|
||||||
setMemoryPanelOpen(open)
|
setRightSidebarCollapsed(prev => {
|
||||||
localStorage.setItem('picobot-memory-panel-open', String(open))
|
const next = !prev
|
||||||
|
localStorage.setItem('picobot-right-sidebar-collapsed', String(next))
|
||||||
|
return next
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const [rightPanelTab, setRightPanelTab] = useState<'memory' | 'skill'>('memory')
|
const [rightPanelTab, setRightPanelTab] = useState<'todo' | 'memory' | 'skill'>('todo')
|
||||||
|
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||||
try {
|
try {
|
||||||
@ -417,15 +420,25 @@ function App() {
|
|||||||
|
|
||||||
// 点击待办项后滚动到对应消息
|
// 点击待办项后滚动到对应消息
|
||||||
const handleTodoClick = useCallback((todo: TodoItemSummary) => {
|
const handleTodoClick = useCallback((todo: TodoItemSummary) => {
|
||||||
if (todo.created_by_message_id) {
|
if (!todo.created_by_message_id) {
|
||||||
// 先清再设,确保同一 todo 重复点击也能触发 useEffect
|
|
||||||
setHighlightedMessageId(null)
|
|
||||||
const msgId = todo.created_by_message_id
|
|
||||||
setTimeout(() => setHighlightedMessageId(msgId), 0)
|
|
||||||
} else {
|
|
||||||
alert('该待办的完成记录无法定位,可能是历史数据')
|
alert('该待办的完成记录无法定位,可能是历史数据')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, [setHighlightedMessageId])
|
// 若处于子智能体视图或定时任务视图,先退出回到主会话视图
|
||||||
|
if (subAgentStack.length > 0) {
|
||||||
|
navigateToSubAgentLevel(-1)
|
||||||
|
}
|
||||||
|
if (schedulerView) {
|
||||||
|
exitSchedulerJobView()
|
||||||
|
}
|
||||||
|
// 先清再设,确保同一 todo 重复点击也能触发 useEffect
|
||||||
|
const msgId = todo.created_by_message_id
|
||||||
|
setHighlightedMessageId(null)
|
||||||
|
// 延迟一帧,等视图切换后消息列表渲染完成再滚动
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlightedMessageId(msgId)
|
||||||
|
}, 50)
|
||||||
|
}, [setHighlightedMessageId, subAgentStack.length, schedulerView, navigateToSubAgentLevel, exitSchedulerJobView])
|
||||||
|
|
||||||
const handleRefreshSchedulerJobs = useCallback(() => {
|
const handleRefreshSchedulerJobs = useCallback(() => {
|
||||||
const cmd = requestSchedulerJobList()
|
const cmd = requestSchedulerJobList()
|
||||||
@ -780,23 +793,38 @@ function App() {
|
|||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
viewKey={viewKey}
|
viewKey={viewKey}
|
||||||
highlightedMessageId={highlightedMessageId}
|
highlightedMessageId={highlightedMessageId}
|
||||||
todoPanel={
|
|
||||||
<TodoPanel
|
|
||||||
todos={todos}
|
|
||||||
requestTodoList={refreshTodoList}
|
|
||||||
sendCommand={sendMemoryCommand}
|
|
||||||
onTodoClick={handleTodoClick}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar - Memory & Skill Panel (collapsible, tabbed) */}
|
{/* Right Sidebar - Todo / Memory / Skill Panel (collapsible, tabbed) */}
|
||||||
<div className={`shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)]/50 transition-all duration-300 ease-out overflow-hidden ${memoryPanelOpen ? 'w-80' : 'w-0 border-l-0'}`}>
|
<div
|
||||||
<div className={`w-80 h-full flex flex-col ${memoryPanelOpen ? '' : 'invisible'}`}>
|
className={`shrink-0 border-l border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col overflow-hidden ${rightSidebarCollapsed ? 'w-11' : 'w-80'}`}
|
||||||
|
style={{ transition: 'width 200ms ease-out', willChange: 'width' }}
|
||||||
|
>
|
||||||
|
{rightSidebarCollapsed ? (
|
||||||
|
// 折叠态:窄条 + 展开按钮
|
||||||
|
<button
|
||||||
|
onClick={toggleRightSidebar}
|
||||||
|
className="flex items-center justify-center w-full py-4 text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||||
|
title="展开侧栏"
|
||||||
|
>
|
||||||
|
<PanelLeftOpen className="h-4 w-4 rotate-180" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="w-80 h-full flex flex-col">
|
||||||
{/* Tab 栏 */}
|
{/* Tab 栏 */}
|
||||||
<div className="shrink-0 flex border-b border-[var(--border-color)]">
|
<div className="shrink-0 flex border-b border-[var(--border-color)]">
|
||||||
|
<button
|
||||||
|
onClick={() => setRightPanelTab('todo')}
|
||||||
|
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
||||||
|
rightPanelTab === 'todo'
|
||||||
|
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||||
|
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
待办
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setRightPanelTab('memory')}
|
onClick={() => setRightPanelTab('memory')}
|
||||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
||||||
@ -817,13 +845,20 @@ function App() {
|
|||||||
>
|
>
|
||||||
技能
|
技能
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => toggleMemoryPanel(false)} className="px-2 py-2.5 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors" title="收起">
|
<button onClick={toggleRightSidebar} className="px-2 py-2.5 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors shrink-0" title="收起">
|
||||||
<X className="h-3.5 w-3.5" />
|
<PanelLeftClose className="h-3.5 w-3.5 rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Panel content */}
|
{/* Panel content */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
{rightPanelTab === 'memory' ? (
|
{rightPanelTab === 'todo' ? (
|
||||||
|
<TodoPanel
|
||||||
|
todos={todos}
|
||||||
|
requestTodoList={refreshTodoList}
|
||||||
|
sendCommand={sendMemoryCommand}
|
||||||
|
onTodoClick={handleTodoClick}
|
||||||
|
/>
|
||||||
|
) : rightPanelTab === 'memory' ? (
|
||||||
<MemoryPanel
|
<MemoryPanel
|
||||||
memories={memories}
|
memories={memories}
|
||||||
onRefresh={handleRefreshMemories}
|
onRefresh={handleRefreshMemories}
|
||||||
@ -840,21 +875,9 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reopen button — visible when panel is collapsed */}
|
|
||||||
{!memoryPanelOpen && (
|
|
||||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 z-10">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleMemoryPanel(true)}
|
|
||||||
className="flex items-center gap-1.5 px-2 py-4 rounded-l-xl bg-[var(--bg-secondary)]/80 backdrop-blur-sm border border-r-0 border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:border-[var(--accent-cyan)]/30 transition-all duration-300 shadow-lg"
|
|
||||||
title="展开记忆面板"
|
|
||||||
>
|
|
||||||
<PanelRightOpen className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 系统配置页面 */}
|
{/* 系统配置页面 */}
|
||||||
{configPageOpen && (
|
{configPageOpen && (
|
||||||
|
|||||||
@ -11,8 +11,6 @@ interface ChatContainerProps {
|
|||||||
onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
|
onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
showThinking?: boolean
|
showThinking?: boolean
|
||||||
/** 浮动待办面板,绝对定位在消息区域上方 */
|
|
||||||
todoPanel?: React.ReactNode
|
|
||||||
/** 视图标识,用于保存/恢复滚动位置 */
|
/** 视图标识,用于保存/恢复滚动位置 */
|
||||||
viewKey?: string
|
viewKey?: string
|
||||||
/** 高亮的消息 ID */
|
/** 高亮的消息 ID */
|
||||||
@ -28,7 +26,6 @@ export function ChatContainer({
|
|||||||
onNavigateToSubAgent,
|
onNavigateToSubAgent,
|
||||||
onStop,
|
onStop,
|
||||||
showThinking = true,
|
showThinking = true,
|
||||||
todoPanel,
|
|
||||||
viewKey,
|
viewKey,
|
||||||
highlightedMessageId,
|
highlightedMessageId,
|
||||||
}: ChatContainerProps) {
|
}: ChatContainerProps) {
|
||||||
@ -36,7 +33,6 @@ export function ChatContainer({
|
|||||||
<div className="flex h-full flex-col relative">
|
<div className="flex h-full flex-col relative">
|
||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} highlightedMessageId={highlightedMessageId} />
|
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} viewKey={viewKey} highlightedMessageId={highlightedMessageId} />
|
||||||
{todoPanel}
|
|
||||||
</div>
|
</div>
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSend={onSendMessage}
|
onSend={onSendMessage}
|
||||||
|
|||||||
@ -376,7 +376,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 animate-slide-in">
|
<div data-message-id={message.id} className="flex gap-3 animate-slide-in">
|
||||||
<div className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full mt-0.5 ${
|
<div className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full mt-0.5 ${
|
||||||
isTaskTool ? 'bg-violet-500/20' : statusConfig.avatarBg
|
isTaskTool ? 'bg-violet-500/20' : statusConfig.avatarBg
|
||||||
}`}>
|
}`}>
|
||||||
|
|||||||
@ -127,14 +127,10 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
|||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
// 查找目标消息元素
|
|
||||||
const targetElement = container.querySelector(`[data-message-id="${highlightedMessageId}"]`)
|
const targetElement = container.querySelector(`[data-message-id="${highlightedMessageId}"]`)
|
||||||
if (!targetElement) return
|
if (!targetElement) return
|
||||||
|
|
||||||
// 滚动到目标位置
|
|
||||||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
|
||||||
// 添加高亮样式
|
|
||||||
targetElement.classList.add('todo-highlight')
|
targetElement.classList.add('todo-highlight')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
targetElement.classList.remove('todo-highlight')
|
targetElement.classList.remove('todo-highlight')
|
||||||
|
|||||||
@ -47,45 +47,24 @@ function PulseDot() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── position persistence ─────────────────────────────── */
|
|
||||||
|
|
||||||
const POS_KEY = 'picobot-todo-pos'
|
|
||||||
|
|
||||||
function loadPos(): { x: number; y: number } {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(POS_KEY)
|
|
||||||
if (raw) return JSON.parse(raw)
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
return { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePos(pos: { x: number; y: number }) {
|
|
||||||
try { localStorage.setItem(POS_KEY, JSON.stringify(pos)) } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── TodoPanel ────────────────────────────────────────── */
|
/* ── TodoPanel ────────────────────────────────────────── */
|
||||||
|
|
||||||
export function TodoPanel({ todos, requestTodoList, sendCommand, onTodoClick }: TodoPanelProps) {
|
export function TodoPanel({ todos, requestTodoList, sendCommand, onTodoClick }: TodoPanelProps) {
|
||||||
const [expanded, setExpanded] = useState(() => {
|
|
||||||
try { return localStorage.getItem('picobot-todo-expanded') === 'true' } catch { return false }
|
|
||||||
})
|
|
||||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set(['completed', 'cancelled']))
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set(['completed', 'cancelled']))
|
||||||
const [pos, setPos] = useState(loadPos)
|
|
||||||
const prevTodoIdsRef = useRef<Set<string>>(new Set())
|
const prevTodoIdsRef = useRef<Set<string>>(new Set())
|
||||||
const dragRef = useRef<{ startX: number; startY: number; startPos: { x: number; y: number }; moved: boolean } | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
// 新增待办时自动展开"进行中"分组
|
||||||
localStorage.setItem('picobot-todo-expanded', String(expanded))
|
|
||||||
}, [expanded])
|
|
||||||
|
|
||||||
// auto-expand on new items, auto-collapse when empty
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newIds = new Set(todos.map(t => t.id))
|
const newIds = new Set(todos.map(t => t.id))
|
||||||
if (todos.length === 0) {
|
if (todos.length > 0) {
|
||||||
setExpanded(false)
|
|
||||||
} else {
|
|
||||||
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
||||||
if (hasNewItems) setExpanded(true)
|
if (hasNewItems) {
|
||||||
|
setCollapsedGroups(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.delete('in_progress')
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevTodoIdsRef.current = newIds
|
prevTodoIdsRef.current = newIds
|
||||||
}, [todos])
|
}, [todos])
|
||||||
@ -104,97 +83,10 @@ export function TodoPanel({ todos, requestTodoList, sendCommand, onTodoClick }:
|
|||||||
|
|
||||||
const handleRefresh = useCallback(() => sendCommand(requestTodoList()), [sendCommand, requestTodoList])
|
const handleRefresh = useCallback(() => sendCommand(requestTodoList()), [sendCommand, requestTodoList])
|
||||||
|
|
||||||
/* ── drag handling ──────────────────────────────────── */
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: React.MouseEvent) => {
|
|
||||||
if ((e.target as HTMLElement).closest('button')) return
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const startX = e.clientX
|
|
||||||
const startY = e.clientY
|
|
||||||
const startPos = { ...pos }
|
|
||||||
dragRef.current = { startX, startY, startPos, moved: false }
|
|
||||||
|
|
||||||
const handleMove = (ev: MouseEvent) => {
|
|
||||||
if (!dragRef.current) return
|
|
||||||
const dx = ev.clientX - dragRef.current.startX
|
|
||||||
const dy = ev.clientY - dragRef.current.startY
|
|
||||||
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragRef.current.moved = true
|
|
||||||
setPos({
|
|
||||||
x: Math.max(0, dragRef.current.startPos.x - dx),
|
|
||||||
y: Math.max(0, dragRef.current.startPos.y + dy),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUp = () => {
|
|
||||||
document.removeEventListener('mousemove', handleMove)
|
|
||||||
document.removeEventListener('mouseup', handleUp)
|
|
||||||
document.body.style.userSelect = ''
|
|
||||||
document.body.style.cursor = ''
|
|
||||||
if (dragRef.current) {
|
|
||||||
setPos(prev => { savePos(prev); return prev })
|
|
||||||
dragRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMove)
|
|
||||||
document.addEventListener('mouseup', handleUp)
|
|
||||||
document.body.style.userSelect = 'none'
|
|
||||||
document.body.style.cursor = 'grabbing'
|
|
||||||
}, [pos])
|
|
||||||
|
|
||||||
/* ── minimized: circle button ───────────────────────── */
|
|
||||||
|
|
||||||
if (!expanded) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex h-full flex-col">
|
||||||
className="absolute z-30"
|
{/* title bar */}
|
||||||
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
|
<div className="shrink-0 flex items-center gap-2 px-4 py-2.5 border-b border-[var(--border-color)]/50">
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="relative cursor-grab active:cursor-grabbing select-none"
|
|
||||||
onMouseDown={handleDragStart}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => { if (totalCount > 0) setExpanded(true); else handleRefresh() }}
|
|
||||||
className="relative w-14 h-14 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-xl border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/50 shadow-[0_4px_24px_rgba(0,240,255,0.08)] hover:shadow-[0_4px_32px_var(--shadow-glow-sm)] transition-all duration-300 group"
|
|
||||||
title="待办"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<ClipboardList className="h-5 w-5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
|
|
||||||
</div>
|
|
||||||
{totalCount > 0 && (
|
|
||||||
<span className={`absolute -top-1.5 -right-1.5 min-w-[20px] h-5 px-1 rounded-full flex items-center justify-center text-[11px] font-bold leading-tight ${
|
|
||||||
inProgressCount > 0
|
|
||||||
? 'bg-amber-400 text-black shadow-[0_0_12px_rgba(245,158,11,0.4)]'
|
|
||||||
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-color)]'
|
|
||||||
}`}>
|
|
||||||
{totalCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{inProgressCount > 0 && (
|
|
||||||
<span className="absolute -inset-[3px] rounded-full animate-todo-ring-pulse" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── expanded: full card ────────────────────────────── */
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="absolute z-30"
|
|
||||||
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
|
|
||||||
>
|
|
||||||
<div className="w-80 max-h-[55vh] flex flex-col rounded-2xl bg-[var(--bg-tertiary)]/95 backdrop-blur-md border border-[var(--border-color)] shadow-2xl hover:shadow-[0_8px_40px_var(--shadow-glow-sm)] overflow-hidden animate-todo-card-in">
|
|
||||||
{/* title bar (drag handle) */}
|
|
||||||
<div
|
|
||||||
className="shrink-0 flex items-center gap-2 px-4 py-2.5 border-b border-[var(--border-color)]/50 cursor-grab active:cursor-grabbing select-none rounded-t-2xl"
|
|
||||||
onMouseDown={handleDragStart}
|
|
||||||
>
|
|
||||||
<span className="text-[var(--text-muted)]/40 text-sm tracking-[0.15em] leading-none select-none group-hover:text-[var(--accent-cyan)]/60 transition-colors">⠿</span>
|
|
||||||
<ClipboardList className="h-4 w-4 text-[var(--accent-cyan)]/80" />
|
<ClipboardList className="h-4 w-4 text-[var(--accent-cyan)]/80" />
|
||||||
<span className="text-[13px] font-semibold text-[var(--text-primary)] tracking-tight">待办</span>
|
<span className="text-[13px] font-semibold text-[var(--text-primary)] tracking-tight">待办</span>
|
||||||
<span className="text-[11px] text-[var(--text-muted)]/80 tabular-nums">{totalCount}</span>
|
<span className="text-[11px] text-[var(--text-muted)]/80 tabular-nums">{totalCount}</span>
|
||||||
@ -203,9 +95,6 @@ export function TodoPanel({ todos, requestTodoList, sendCommand, onTodoClick }:
|
|||||||
<button onClick={handleRefresh} className="p-1.5 rounded-lg text-[var(--text-muted)]/50 hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors" title="刷新">
|
<button onClick={handleRefresh} className="p-1.5 rounded-lg text-[var(--text-muted)]/50 hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors" title="刷新">
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setExpanded(false)} className="p-1.5 rounded-lg text-[var(--text-muted)]/50 hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)] transition-colors" title="缩小">
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -268,6 +157,5 @@ export function TodoPanel({ todos, requestTodoList, sendCommand, onTodoClick }:
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user