227 lines
8.3 KiB
TypeScript
227 lines
8.3 KiB
TypeScript
|
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||
|
|
import { useDocument, useDocumentAnalysis, useAnalyzeDocument } from '@/hooks/useDocuments';
|
||
|
|
import { Button } from '@/components/Button';
|
||
|
|
import { Card } from '@/components/Card';
|
||
|
|
import { ArrowLeft, FileText, Sparkles, Tag, FolderOpen, Trash2, Edit2, CheckSquare } from 'lucide-react';
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
|
||
|
|
export default function DocumentDetailPage() {
|
||
|
|
const { id } = useParams<{ id: string }>();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const { data: document, isLoading: docLoading, error: docError } = useDocument(id || '');
|
||
|
|
const { data: analysis, isLoading: analysisLoading } = useDocumentAnalysis(id || '');
|
||
|
|
const analyzeMutation = useAnalyzeDocument();
|
||
|
|
const [isEditing, setIsEditing] = useState(false);
|
||
|
|
const [editedTitle, setEditedTitle] = useState('');
|
||
|
|
const [editedContent, setEditedContent] = useState('');
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (document) {
|
||
|
|
setEditedTitle(document.title || '');
|
||
|
|
setEditedContent(document.content);
|
||
|
|
}
|
||
|
|
}, [document]);
|
||
|
|
|
||
|
|
const handleAnalyze = async () => {
|
||
|
|
if (!id) return;
|
||
|
|
try {
|
||
|
|
await analyzeMutation.mutateAsync({ id });
|
||
|
|
} catch (err: any) {
|
||
|
|
alert(err.message || 'AI 分析失败');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSave = () => {
|
||
|
|
// TODO: 实现保存功能
|
||
|
|
setIsEditing(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (docLoading) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center min-h-screen">
|
||
|
|
<div className="text-gray-600">加载中...</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (docError || !document) {
|
||
|
|
return (
|
||
|
|
<div className="flex items-center justify-center min-h-screen">
|
||
|
|
<Card variant="bordered" className="max-w-md">
|
||
|
|
<div className="text-center py-8">
|
||
|
|
<p className="text-red-600 mb-4">文档不存在或无权访问</p>
|
||
|
|
<Link to="/documents">
|
||
|
|
<Button>返回文档列表</Button>
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="max-w-4xl mx-auto space-y-6">
|
||
|
|
{/* 头部导航 */}
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<Link to="/documents">
|
||
|
|
<button className="p-2 hover:bg-gray-100 rounded-lg">
|
||
|
|
<ArrowLeft className="h-5 w-5 text-gray-600" />
|
||
|
|
</button>
|
||
|
|
</Link>
|
||
|
|
<div className="flex-1">
|
||
|
|
<h1 className="text-2xl font-bold text-gray-900">文档详情</h1>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 文档内容卡片 */}
|
||
|
|
<Card variant="bordered">
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* 标题和操作栏 */}
|
||
|
|
<div className="flex items-start justify-between">
|
||
|
|
{isEditing ? (
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={editedTitle}
|
||
|
|
onChange={(e) => setEditedTitle(e.target.value)}
|
||
|
|
className="flex-1 mr-4 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||
|
|
placeholder="文档标题"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<h2 className="flex-1 text-xl font-semibold text-gray-900">
|
||
|
|
{document.title || '无标题'}
|
||
|
|
</h2>
|
||
|
|
)}
|
||
|
|
<div className="flex gap-2">
|
||
|
|
{isEditing ? (
|
||
|
|
<>
|
||
|
|
<Button size="sm" onClick={handleSave}>
|
||
|
|
保存
|
||
|
|
</Button>
|
||
|
|
<Button size="sm" variant="secondary" onClick={() => setIsEditing(false)}>
|
||
|
|
取消
|
||
|
|
</Button>
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<button
|
||
|
|
onClick={() => setIsEditing(true)}
|
||
|
|
className="p-2 hover:bg-gray-100 rounded-lg"
|
||
|
|
title="编辑"
|
||
|
|
>
|
||
|
|
<Edit2 className="h-4 w-4 text-gray-600" />
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleAnalyze}
|
||
|
|
disabled={analyzeMutation.isPending || analysisLoading}
|
||
|
|
className="p-2 hover:bg-purple-100 rounded-lg disabled:opacity-50"
|
||
|
|
title="AI 分析"
|
||
|
|
>
|
||
|
|
<Sparkles className={`h-4 w-4 ${analyzeMutation.isPending ? 'animate-pulse' : ''} text-purple-600`} />
|
||
|
|
</button>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 文档内容 */}
|
||
|
|
{isEditing ? (
|
||
|
|
<textarea
|
||
|
|
value={editedContent}
|
||
|
|
onChange={(e) => setEditedContent(e.target.value)}
|
||
|
|
className="w-full min-h-[300px] px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-sm"
|
||
|
|
placeholder="文档内容"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||
|
|
<p className="whitespace-pre-wrap text-sm text-gray-700">{document.content}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 元数据 */}
|
||
|
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||
|
|
<span>创建时间: {new Date(document.created_at).toLocaleString()}</span>
|
||
|
|
<span>更新时间: {new Date(document.updated_at).toLocaleString()}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* AI 分析结果 */}
|
||
|
|
{(analysis || analyzeMutation.isPending || analysisLoading) && (
|
||
|
|
<Card title="AI 分析结果" variant="bordered">
|
||
|
|
{analyzeMutation.isPending || analysisLoading ? (
|
||
|
|
<div className="flex items-center justify-center py-8">
|
||
|
|
<Sparkles className="h-6 w-6 text-purple-600 animate-pulse mr-2" />
|
||
|
|
<span className="text-gray-600">AI 分析中...</span>
|
||
|
|
</div>
|
||
|
|
) : analysis ? (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* 标签 */}
|
||
|
|
{analysis.suggested_tags && analysis.suggested_tags.length > 0 && (
|
||
|
|
<div className="flex items-start gap-2">
|
||
|
|
<Tag className="h-4 w-4 text-gray-500 mt-0.5" />
|
||
|
|
<div className="flex flex-wrap gap-2">
|
||
|
|
{analysis.suggested_tags.map((tag, idx) => (
|
||
|
|
<span
|
||
|
|
key={idx}
|
||
|
|
className="inline-flex items-center rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800"
|
||
|
|
>
|
||
|
|
{tag}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 分类建议 */}
|
||
|
|
{analysis.suggested_category && (
|
||
|
|
<div className="flex items-start gap-2">
|
||
|
|
<FolderOpen className="h-4 w-4 text-gray-500 mt-0.5" />
|
||
|
|
<span className="text-gray-700">
|
||
|
|
建议分类:{' '}
|
||
|
|
<span className="font-medium text-gray-900">{analysis.suggested_category}</span>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 摘要 */}
|
||
|
|
{analysis.summary && (
|
||
|
|
<div className="bg-purple-50 rounded-lg p-4">
|
||
|
|
<div className="flex items-start gap-2 mb-2">
|
||
|
|
<Sparkles className="h-4 w-4 text-purple-600 mt-0.5" />
|
||
|
|
<span className="font-medium text-gray-900">摘要</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-sm text-gray-700 ml-6">{analysis.summary}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 提供商信息 */}
|
||
|
|
<p className="text-xs text-gray-500">
|
||
|
|
由 {analysis.provider} ({analysis.model}) 分析
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 转为待办 */}
|
||
|
|
<Card variant="bordered">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<CheckSquare className="h-5 w-5 text-green-600" />
|
||
|
|
<div>
|
||
|
|
<h3 className="font-medium text-gray-900">转为待办事项</h3>
|
||
|
|
<p className="text-xs text-gray-500">将此文档内容转换为待办任务</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
onClick={() => navigate(`/todos?createFromDocument=${id}`)}
|
||
|
|
variant="secondary"
|
||
|
|
>
|
||
|
|
创建待办
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|