feat(web): 重构组件样式并添加确认对话框组件

- 优化组件视觉风格: 使用渐变色、阴影和圆角增强现代感
- 添加 ConfirmDialog 组件和 useConfirm hook 用于确认操作
- 改进聊天界面: 添加滚动到底部按钮、斜杠命令自动弹出
- 统一组件间距和颜色方案,提升 UI 一致性
This commit is contained in:
2026-05-08 00:41:09 +08:00
parent 66d170066a
commit 0fb33b9f57
12 changed files with 254 additions and 93 deletions

View File

@@ -6,7 +6,7 @@ import { ChatMessage } from './ChatMessage';
import { ChatInput } from './ChatInput';
import { PipelineProgress } from './PipelineProgress';
import { Button } from '@/components/ui/button';
import { RefreshCw, Loader2, StopCircle, X, MessageCircle } from 'lucide-react';
import { RefreshCw, Loader2, StopCircle, X, MessageCircle, ArrowDown } from 'lucide-react';
import { api } from '@/lib/api';
import type { Account, Message } from '@/types';
@@ -19,6 +19,7 @@ export function ChatView() {
const [accounts, setAccounts] = useState<Account[]>([]);
const [quote, setQuote] = useState<string | null>(null);
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
const [showScrollBtn, setShowScrollBtn] = useState(false);
const creatingRef = useRef(false);
const scrollRef = useRef<HTMLDivElement>(null);
const userScrolledUp = useRef(false);
@@ -54,6 +55,7 @@ export function ChatView() {
const onScroll = () => {
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
userScrolledUp.current = !atBottom;
setShowScrollBtn(!atBottom);
};
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
@@ -62,6 +64,7 @@ export function ChatView() {
// Reset scroll state on conversation switch
useEffect(() => {
userScrolledUp.current = false;
setShowScrollBtn(false);
}, [conversationId]);
// After navigating to a new conversation, send the pending message once history is loaded
@@ -137,9 +140,12 @@ export function ChatView() {
if (!conversationId) {
const recentConvs = conversations.slice(0, 5);
return (
<div className="flex-1 flex flex-col items-center justify-center bg-white">
<div className="flex-1 flex flex-col items-center justify-center bg-gradient-to-b from-white to-zinc-50/30">
<div className="w-full max-w-xl px-4">
<div className="text-center mb-6">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-50 to-violet-50 flex items-center justify-center mx-auto mb-4 shadow-sm">
<MessageCircle size={24} className="text-indigo-400" strokeWidth={1.5} />
</div>
<h2 className="text-lg font-semibold text-zinc-800"></h2>
<p className="text-sm text-zinc-400 mt-1"></p>
</div>
@@ -150,7 +156,7 @@ export function ChatView() {
<select
value={selectedAccountId || ''}
onChange={(e) => useAppStore.getState().setSelectedAccountId(e.target.value || null)}
className="h-7 rounded-md border border-zinc-200 bg-white px-2.5 text-xs text-zinc-600"
className="h-7 rounded-lg border border-zinc-100 bg-white px-2.5 text-xs text-zinc-600 hover:border-zinc-200 transition-colors"
>
<option value=""></option>
{accounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
@@ -164,7 +170,7 @@ export function ChatView() {
<button
key={conv.id}
onClick={() => navigate(`/chat/${conv.id}`)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left hover:bg-zinc-50 transition-colors group"
className="w-full flex items-center gap-2 px-3 py-2 rounded-xl text-left hover:bg-zinc-50/80 transition-colors group"
>
<MessageCircle size={13} className="text-zinc-300 flex-shrink-0" />
<span className="text-sm text-zinc-600 truncate flex-1 group-hover:text-zinc-800">{conv.title}</span>
@@ -184,19 +190,19 @@ export function ChatView() {
return (
<div className="flex-1 flex flex-col bg-white h-full">
{/* Header bar */}
<div className="px-4 py-2 border-b border-zinc-200 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-1.5">
<div className={`w-1.5 h-1.5 rounded-full ${connected ? 'bg-green-500' : 'bg-red-400'}`} />
<div className="px-4 py-2 border-b border-zinc-100 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${connected ? 'bg-emerald-400 shadow-sm shadow-emerald-200' : 'bg-red-400 animate-pulse'}`} />
<span className="text-xs text-zinc-400">{connected ? '在线' : '连接中'}</span>
</div>
<div className="flex items-center gap-2">
{manifestPath && (
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={handleResume}>
<Button size="sm" variant="outline" className="h-7 text-xs rounded-lg" onClick={handleResume}>
<RefreshCw size={12} className="mr-1" />
</Button>
)}
{thinking && (
<Button size="sm" variant="outline" className="h-7 text-xs text-red-500 border-red-200 hover:bg-red-50" onClick={handleStop}>
<Button size="sm" variant="outline" className="h-7 text-xs text-red-500 border-red-200 hover:bg-red-50 rounded-lg" onClick={handleStop}>
<StopCircle size={12} className="mr-1" />
</Button>
)}
@@ -204,7 +210,7 @@ export function ChatView() {
</div>
{/* Chat area */}
<div className="flex-1 overflow-hidden">
<div className="flex-1 overflow-hidden relative">
<div className="h-full flex flex-col">
{/* Message list */}
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4">
@@ -235,15 +241,29 @@ export function ChatView() {
{thinking && !pipeline && (
<div className="flex items-center gap-2 text-zinc-400 text-sm py-2">
<Loader2 size={14} className="animate-spin" />
<Loader2 size={14} className="animate-spin text-indigo-400" />
{toolStatus || '思考中...'}
</div>
)}
</div>
{/* Scroll to bottom FAB */}
{showScrollBtn && (
<button
onClick={() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
userScrolledUp.current = false;
setShowScrollBtn(false);
}}
className="absolute bottom-4 left-1/2 -translate-x-1/2 w-9 h-9 rounded-full bg-white border border-zinc-200 shadow-lg shadow-zinc-200/60 flex items-center justify-center text-zinc-400 hover:text-zinc-700 hover:border-zinc-300 hover:shadow-xl transition-all"
>
<ArrowDown size={16} />
</button>
)}
{/* Quote bar */}
{quote && (
<div className="mx-4 px-3 py-1.5 bg-zinc-50 border border-zinc-200 rounded-lg flex items-center gap-2 text-xs text-zinc-500 flex-shrink-0">
<div className="mx-4 px-3 py-1.5 bg-indigo-50/60 border border-indigo-100/50 rounded-xl flex items-center gap-2 text-xs text-zinc-500 flex-shrink-0">
<span className="flex-1 truncate">: {quote}</span>
<button onClick={() => setQuote(null)} className="text-zinc-400 hover:text-zinc-600"><X size={12} /></button>
</div>