This commit is contained in:
董宏凯 committed 2026-05-17 19:29:10 +08:00
1 parent 6cab62d140
commit 918e65d8d3
16 files changed
+1143 -26

No files matched your search

@@ -0,0 +1,202 @@
import { useState, useRef, useEffect } from 'react';
import { History, Trash2, Check, X, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import type { AIWritingSessionSummary } from '../../api/ai-writing-sessions';
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
in_progress: { label: '进行中', color: 'text-blue-500' },
draft_ready: { label: '草稿', color: 'text-amber-500' },
review_ready: { label: '审核', color: 'text-purple-500' },
done: { label: '完成', color: 'text-green-500' },
error: { label: '错误', color: 'text-destructive' },
};
function formatRelative(isoStr: string): string {
const ms = Date.now() - new Date(isoStr).getTime();
const mins = Math.floor(ms / 60000);
if (mins < 1) return '刚刚';
if (mins < 60) return `${mins} 分钟前`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs} 小时前`;
const days = Math.floor(hrs / 24);
if (days < 7) return `${days} 天前`;
return new Date(isoStr).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
interface SessionItemProps {
session: AIWritingSessionSummary;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onRename: (title: string) => void;
}
function SessionItem({ session, isSelected, onSelect, onDelete, onRename }: SessionItemProps) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(session.title);
const inputRef = useRef<HTMLInputElement>(null);
const { label, color } = STATUS_LABELS[session.status] ?? { label: session.status, color: 'text-muted-foreground' };
useEffect(() => {
if (editing) inputRef.current?.focus();
}, [editing]);
function commitRename() {
const trimmed = editValue.trim();
if (trimmed && trimmed !== session.title) onRename(trimmed);
setEditing(false);
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter') commitRename();
else if (e.key === 'Escape') { setEditValue(session.title); setEditing(false); }
}
return (
<div
className={cn(
'group relative flex items-start gap-2 px-3 py-2.5 cursor-pointer transition-colors',
isSelected ? 'bg-accent' : 'hover:bg-accent/60',
)}
onClick={!editing ? onSelect : undefined}
>
<div className="flex-1 min-w-0">
{editing ? (
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<input
ref={inputRef}
className="flex-1 text-xs bg-background border rounded px-1.5 py-0.5 outline-none focus:ring-1 focus:ring-ring"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button className="shrink-0 text-green-500 hover:text-green-600" onClick={commitRename}>
<Check className="w-3 h-3" />
</button>
<button className="shrink-0 text-muted-foreground hover:text-foreground" onClick={() => { setEditValue(session.title); setEditing(false); }}>
<X className="w-3 h-3" />
</button>
</div>
) : (
<p
className="text-xs font-medium truncate leading-snug"
onDoubleClick={(e) => { e.stopPropagation(); setEditing(true); }}
title="双击重命名"
>
{session.title}
</p>
)}
<div className="flex items-center gap-1.5 mt-0.5">
<span className={cn('text-[10px] font-medium', color)}>{label}</span>
<span className="text-[10px] text-muted-foreground">·</span>
<span className="text-[10px] text-muted-foreground">{formatRelative(session.updated_at)}</span>
</div>
</div>
{/* 删除按钮,hover 时显示 */}
{!editing && (
<button
className="opacity-0 group-hover:opacity-100 shrink-0 p-0.5 text-muted-foreground hover:text-destructive transition-opacity"
onClick={(e) => { e.stopPropagation(); onDelete(); }}
title="删除"
>
<Trash2 className="w-3 h-3" />
</button>
)}
</div>
);
}
interface AIWritingHistoryDropdownProps {
sessions: AIWritingSessionSummary[];
selectedId: string | null;
loading: boolean;
onSelect: (session: AIWritingSessionSummary) => void;
onDelete: (id: string) => void;
onRename: (id: string, title: string) => void;
onRefresh: () => void;
}
export function AIWritingHistoryDropdown({
sessions,
selectedId,
loading,
onSelect,
onDelete,
onRename,
onRefresh,
}: AIWritingHistoryDropdownProps) {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
function handleClickOutside(e: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
function handleOpen() {
setOpen((v) => {
if (!v) onRefresh();
return !v;
});
}
return (
<div ref={containerRef} className="relative">
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs gap-1"
onClick={handleOpen}
title="查看历史记录"
>
<History className="size-3" />
历史
</Button>
{open && (
<div className="absolute right-0 top-full mt-1 z-50 w-64 rounded-lg border bg-popover shadow-lg overflow-hidden">
{/* 头部 */}
<div className="flex items-center justify-between px-3 py-2 border-b">
<span className="text-xs font-semibold">写作历史</span>
<button
className={cn('text-muted-foreground hover:text-foreground transition-transform', loading && 'animate-spin')}
onClick={onRefresh}
title="刷新"
>
<RefreshCw className="w-3 h-3" />
</button>
</div>
{/* 列表 */}
<div className="max-h-72 overflow-y-auto">
{sessions.length === 0 ? (
<div className="text-center py-6 text-xs text-muted-foreground">
{loading ? '加载中…' : '暂无历史记录'}
</div>
) : (
sessions.map((s) => (
<SessionItem
key={s.id}
session={s}
isSelected={s.id === selectedId}
onSelect={() => { onSelect(s); setOpen(false); }}
onDelete={() => onDelete(s.id)}
onRename={(title) => onRename(s.id, title)}
/>
))
)}
</div>
</div>
)}
</div>
);
}