style(TodoPanel): 优化待办面板动画和样式细节
- 新增待办卡片及条目动画效果,提升界面动感 - 添加待办分组折叠内容展开/收起过渡样式 - 优化按钮尺寸及颜色悬浮效果,提升交互体验 - 自动展开新待办项,空列表时自动收起 - 调整待办面板大小、圆角及阴影效果,增强视觉层次 - 改进待办标题栏排版及交互样式,支持拖动操作手感 - 为无待办状态提供占位展示及提示文字 - 优化待办分组标题样式及折叠箭头动画 - 改进待办条目列表项样式,支持悬浮高亮显示 - 移除消息气泡中的冗余子智能体标签显示内容
This commit is contained in:
parent
301506a3b1
commit
421714dfa3
@ -395,11 +395,6 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
|||||||
子智能体·{subagentType}
|
子智能体·{subagentType}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isTaskTool && isSubAgent && (
|
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400">
|
|
||||||
子智能体
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
|
<span className="text-xs text-[var(--text-muted)]">{formatTime(message.timestamp)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -77,11 +77,15 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
localStorage.setItem('picobot-todo-expanded', String(expanded))
|
localStorage.setItem('picobot-todo-expanded', String(expanded))
|
||||||
}, [expanded])
|
}, [expanded])
|
||||||
|
|
||||||
// auto-expand on new items
|
// 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))
|
||||||
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
if (todos.length === 0) {
|
||||||
if (hasNewItems && todos.length > 0) setExpanded(true)
|
setExpanded(false)
|
||||||
|
} else {
|
||||||
|
const hasNewItems = todos.some(t => !prevTodoIdsRef.current.has(t.id))
|
||||||
|
if (hasNewItems) setExpanded(true)
|
||||||
|
}
|
||||||
prevTodoIdsRef.current = newIds
|
prevTodoIdsRef.current = newIds
|
||||||
}, [todos])
|
}, [todos])
|
||||||
|
|
||||||
@ -152,26 +156,23 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => { if (totalCount > 0) setExpanded(true); else handleRefresh() }}
|
onClick={() => { if (totalCount > 0) setExpanded(true); else handleRefresh() }}
|
||||||
className="relative w-12 h-12 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-md border border-[var(--border-color)] hover:border-[var(--accent-cyan)]/40 shadow-lg hover:shadow-[0_0_20px_var(--shadow-glow-sm)] transition-all duration-300 group"
|
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="待办"
|
title="待办"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<ClipboardList className="h-3.5 w-3.5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
|
<ClipboardList className="h-5 w-5 text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors" />
|
||||||
<span className="text-[8px] font-bold text-[var(--text-muted)] group-hover:text-[var(--accent-cyan)] transition-colors mt-0.5">
|
|
||||||
ToDo
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{totalCount > 0 && (
|
{totalCount > 0 && (
|
||||||
<span className={`absolute -top-1 -right-1 min-w-[18px] h-[18px] rounded-full flex items-center justify-center text-[10px] font-bold shadow-md ${
|
<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
|
inProgressCount > 0
|
||||||
? 'bg-amber-400 text-black'
|
? '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)]'
|
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-color)]'
|
||||||
}`}>
|
}`}>
|
||||||
{totalCount}
|
{totalCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{inProgressCount > 0 && (
|
{inProgressCount > 0 && (
|
||||||
<span className="absolute inset-0 rounded-full border-2 border-amber-400/30 animate-ping" />
|
<span className="absolute -inset-[3px] rounded-full animate-todo-ring-pulse" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -186,29 +187,44 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
className="absolute z-30"
|
className="absolute z-30"
|
||||||
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
|
style={{ top: `${16 + pos.y}px`, right: `${16 + pos.x}px` }}
|
||||||
>
|
>
|
||||||
<div className="w-72 max-h-[50vh] flex flex-col rounded-xl bg-[var(--bg-tertiary)]/95 backdrop-blur-md border border-[var(--border-color)] shadow-2xl overflow-hidden">
|
<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) */}
|
{/* title bar (drag handle) */}
|
||||||
<div
|
<div
|
||||||
className="shrink-0 flex items-center gap-2 px-3 py-2 border-b border-[var(--border-color)]/50 cursor-grab active:cursor-grabbing select-none"
|
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}
|
onMouseDown={handleDragStart}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--text-muted)]/30 text-[10px] leading-none select-none">⠿</span>
|
<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-3 w-3 text-[var(--accent-cyan)]" />
|
<ClipboardList className="h-4 w-4 text-[var(--accent-cyan)]/80" />
|
||||||
<span className="text-[11px] font-semibold text-[var(--text-secondary)]">待办</span>
|
<span className="text-[13px] font-semibold text-[var(--text-primary)] tracking-tight">待办</span>
|
||||||
<span className="text-[10px] text-[var(--text-muted)]/70 tabular-nums">{totalCount}</span>
|
<span className="text-[11px] text-[var(--text-muted)]/80 tabular-nums">{totalCount}</span>
|
||||||
{inProgressCount > 0 && <PulseDot />}
|
{inProgressCount > 0 && <PulseDot />}
|
||||||
<div className="ml-auto flex items-center gap-0.5">
|
<div className="ml-auto flex items-center gap-0.5">
|
||||||
<button onClick={handleRefresh} className="p-1 rounded text-[var(--text-muted)]/50 hover:text-[var(--accent-cyan)] 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 w-3" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setExpanded(false)} className="p-1 rounded text-[var(--text-muted)]/50 hover:text-[var(--text-secondary)] transition-colors" title="缩小">
|
<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-3 w-3" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* list */}
|
{/* list */}
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-hide px-3 py-2">
|
<div className="flex-1 overflow-y-auto scrollbar-hide px-4 py-3 space-y-2">
|
||||||
|
{totalCount === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 px-4 text-center select-none">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="absolute inset-0 rounded-full bg-[var(--accent-cyan)]/10 blur-xl animate-pulse" />
|
||||||
|
<ClipboardList className="relative h-10 w-10 text-[var(--accent-cyan)]/25" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[12px] text-[var(--text-muted)] leading-relaxed mb-1">
|
||||||
|
暂无待办事项
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-[var(--text-muted)]/60 leading-relaxed">
|
||||||
|
AI 助手可在对话中创建和管理待办
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{GROUP_ORDER.map(status => {
|
{GROUP_ORDER.map(status => {
|
||||||
const items = grouped.get(status)
|
const items = grouped.get(status)
|
||||||
if (!items || items.length === 0) return null
|
if (!items || items.length === 0) return null
|
||||||
@ -220,31 +236,28 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
|||||||
<div key={status} className="mt-1 first:mt-0">
|
<div key={status} className="mt-1 first:mt-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleGroup(status)}
|
onClick={() => toggleGroup(status)}
|
||||||
className="flex items-center gap-1.5 w-full py-0.5 group"
|
className="sticky top-0 z-10 flex items-center gap-2 w-full py-1.5 rounded-lg transition-colors hover:bg-[var(--overlay-hover)] bg-[var(--bg-tertiary)]/95 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot} shrink-0`} />
|
<span className={`h-2 w-2 rounded-full ${cfg.dot} shrink-0`} />
|
||||||
<span className={`text-[10px] font-medium ${cfg.color}`}>{cfg.label}</span>
|
<span className={`text-[12px] font-semibold ${cfg.color}`}>{cfg.label}</span>
|
||||||
<span className={`text-[10px] ${cfg.color} opacity-60 tabular-nums`}>{items.length}</span>
|
<span className={`text-[10px] ${cfg.color} opacity-50 tabular-nums ml-0.5`}>{items.length}</span>
|
||||||
<span className="ml-auto text-[var(--text-muted)]/40">
|
<span className="ml-auto text-[var(--text-muted)]/50">
|
||||||
{isCollapsed
|
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${isCollapsed ? '-rotate-90' : 'rotate-0'}`} />
|
||||||
? <ChevronDown className="h-2.5 w-2.5 rotate-[-90deg]" />
|
|
||||||
: <ChevronDown className="h-2.5 w-2.5" />
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!isCollapsed && (
|
<div className={`todo-group-body ${isCollapsed ? 'todo-group-body-closed' : 'todo-group-body-open'}`}>
|
||||||
<div className="ml-3 border-l border-[var(--border-color)]/30 pl-2 mt-0.5 space-y-px">
|
<div className="ml-[7px] border-l-2 border-[var(--border-color)]/60 pl-3 mt-1.5 space-y-0.5">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<div key={item.id} className="py-[3px] flex items-start gap-1.5">
|
<div key={item.id} className="group/item py-1.5 px-2 -mx-2 rounded-md transition-colors duration-150 hover:bg-[var(--overlay-hover)] flex items-start gap-1.5">
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot} shrink-0 mt-[5px]`} />
|
<span className={`h-2 w-2 rounded-full ${cfg.dot} shrink-0 mt-1.5`} />
|
||||||
<span className="text-[12px] leading-snug text-[var(--text-primary)]/90 break-words">
|
<span className="text-[13px] leading-relaxed text-[var(--text-primary)]/85 group-hover/item:text-[var(--text-primary)] transition-colors break-words">
|
||||||
{item.content}
|
{item.content}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -258,6 +258,60 @@ body {
|
|||||||
.animate-fade-in { animation: fade-in 0.2s ease-out; }
|
.animate-fade-in { animation: fade-in 0.2s ease-out; }
|
||||||
.animate-scale-in { animation: scale-in 0.3s ease-out; }
|
.animate-scale-in { animation: scale-in 0.3s ease-out; }
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TodoPanel animations
|
||||||
|
============================================ */
|
||||||
|
@keyframes todo-card-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.92) translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes todo-item-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-6px);
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes todo-ring-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-todo-card-in { animation: todo-card-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||||
|
.animate-todo-item-in { animation: todo-item-in 0.2s ease-out forwards; }
|
||||||
|
.animate-todo-ring-pulse { animation: todo-ring-pulse 2s ease-in-out infinite; }
|
||||||
|
|
||||||
|
/* 分组折叠内容展开/收起 */
|
||||||
|
.todo-group-body {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.25s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.todo-group-body-open {
|
||||||
|
max-height: 600px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.todo-group-body-closed {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes thinking-reveal {
|
@keyframes thinking-reveal {
|
||||||
from { max-height: 0; opacity: 0; }
|
from { max-height: 0; opacity: 0; }
|
||||||
to { max-height: 300px; opacity: 1; }
|
to { max-height: 300px; opacity: 1; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user