185 lines
7.7 KiB
TypeScript
185 lines
7.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { Plus, MessageSquare, Layers, Hash, Clock, RefreshCw, Trash2, Check, X } from 'lucide-react'
|
|
import type { Topic } from '../../types/protocol'
|
|
|
|
interface TopicListProps {
|
|
sessionId: string | null
|
|
topics: Topic[]
|
|
currentTopicId: string | null
|
|
isReadOnly: boolean
|
|
onCreateTopic: () => void
|
|
onRefresh: () => void
|
|
onSwitchTopic: (topicId: string) => void
|
|
onDeleteTopic: (topicId: string) => void
|
|
}
|
|
|
|
function formatTime(timestamp: number): string {
|
|
const date = new Date(timestamp)
|
|
const now = new Date()
|
|
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24))
|
|
|
|
if (diffDays === 0) {
|
|
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
} else if (diffDays === 1) {
|
|
return '昨天'
|
|
} else if (diffDays < 7) {
|
|
return `${diffDays}天前`
|
|
} else {
|
|
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
|
}
|
|
}
|
|
|
|
export function TopicList({
|
|
sessionId,
|
|
topics,
|
|
currentTopicId,
|
|
isReadOnly,
|
|
onCreateTopic,
|
|
onRefresh,
|
|
onSwitchTopic,
|
|
onDeleteTopic,
|
|
}: TopicListProps) {
|
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b border-[var(--border-color)] px-4 py-3">
|
|
<h2 className="font-semibold text-[var(--text-primary)] flex items-center gap-2 text-sm">
|
|
<Layers className="h-4 w-4 text-[var(--accent-cyan)]" />
|
|
话题列表
|
|
{topics.length > 0 && (
|
|
<span className="text-xs text-[var(--text-muted)]">({topics.length})</span>
|
|
)}
|
|
</h2>
|
|
{isReadOnly ? (
|
|
<button
|
|
onClick={onRefresh}
|
|
disabled={!sessionId}
|
|
className={`flex items-center gap-1 rounded-lg px-2 py-1 text-xs transition-all ${
|
|
!sessionId
|
|
? 'text-[var(--text-muted)] cursor-not-allowed'
|
|
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-subtle)]'
|
|
}`}
|
|
title="刷新话题列表"
|
|
>
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
刷新
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={onCreateTopic}
|
|
disabled={!sessionId}
|
|
className={`flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm transition-all ${
|
|
!sessionId
|
|
? 'bg-[var(--overlay-subtle)] text-[var(--text-muted)] cursor-not-allowed'
|
|
: 'bg-[var(--accent-cyan)]/10 text-[var(--accent-cyan)] hover:bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30'
|
|
}`}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
新建
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Topics 列表 */}
|
|
<div className="flex-1 overflow-y-auto p-3">
|
|
{!sessionId ? (
|
|
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
|
|
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>等待连接...</p>
|
|
</div>
|
|
) : topics.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
|
|
<MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>暂无话题</p>
|
|
<p className="text-xs mt-1">点击上方"新建"创建话题</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{topics.map((topic, index) => (
|
|
<div key={topic.id} className="group relative">
|
|
<button
|
|
onClick={() => onSwitchTopic(topic.id)}
|
|
className={`w-full rounded-xl pl-3 pr-8 py-3 text-left text-sm transition-all ${
|
|
topic.id === currentTopicId
|
|
? 'bg-gradient-to-r from-[var(--accent-cyan)]/20 to-transparent border border-[var(--accent-cyan)]/30'
|
|
: 'hover:bg-[var(--overlay-hover)] border border-transparent'
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="mt-0.5 text-xs text-[var(--text-muted)] font-mono w-4">
|
|
{index + 1}
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className={`truncate font-medium ${
|
|
topic.id === currentTopicId ? 'text-[var(--accent-cyan)]' : 'text-[var(--text-secondary)]'
|
|
}`}>
|
|
{topic.description || topic.title}
|
|
</div>
|
|
<div className="flex items-center gap-3 mt-1.5">
|
|
<span className="text-xs text-[var(--text-muted)] flex items-center gap-1">
|
|
<Hash className="h-3 w-3" />
|
|
{topic.message_count} 条消息
|
|
</span>
|
|
<span className="text-xs text-[var(--text-muted)] flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
{formatTime(topic.updated_at)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{topic.id === currentTopicId && (
|
|
<span className="inline-block h-2 w-2 rounded-full bg-[var(--accent-cyan)] shadow-lg shadow-[var(--shadow-glow-soft)] mt-1.5" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{/* Delete button — visible on group hover */}
|
|
<div className="absolute top-2.5 right-2.5">
|
|
{confirmDeleteId === topic.id ? (
|
|
<span className="flex items-center gap-1.5 rounded-lg bg-[var(--bg-tertiary)] border border-[var(--border-color)] px-2 py-1 shadow-lg animate-scale-in">
|
|
<span className="text-xs text-red-400 whitespace-nowrap">确认删除?</span>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onDeleteTopic(topic.id)
|
|
setConfirmDeleteId(null)
|
|
}}
|
|
className="flex items-center justify-center h-5 w-5 rounded bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 transition-colors"
|
|
title="确认"
|
|
>
|
|
<Check className="h-3 w-3" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setConfirmDeleteId(null)
|
|
}}
|
|
className="flex items-center justify-center h-5 w-5 rounded bg-zinc-500/20 text-zinc-400 hover:bg-zinc-500/30 transition-colors"
|
|
title="取消"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</span>
|
|
) : (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setConfirmDeleteId(topic.id)
|
|
}}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10"
|
|
title="删除话题"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|