245 lines
8.8 KiB
TypeScript
245 lines
8.8 KiB
TypeScript
import {useEffect, useState} from 'react'
|
|
import {Link, useNavigate, useParams} from 'react-router-dom'
|
|
import Navbar from '../components/Navbar'
|
|
import {ArrowLeft, Book, Calendar, Download, Edit, MessageSquare, Tag, User} from 'lucide-react'
|
|
import {type Character, characterApi} from '../api/character'
|
|
|
|
export default function CharacterDetailPage() {
|
|
const { id } = useParams<{ id: string }>()
|
|
const [character, setCharacter] = useState<Character | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const navigate = useNavigate()
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
loadCharacter(parseInt(id))
|
|
}
|
|
}, [id])
|
|
|
|
const loadCharacter = async (characterId: number) => {
|
|
try {
|
|
setLoading(true)
|
|
const response = await characterApi.getCharacterById(characterId)
|
|
setCharacter(response.data)
|
|
} catch (err: any) {
|
|
console.error('获取角色卡详情失败:', err)
|
|
if (err.response?.status === 401) {
|
|
navigate('/login')
|
|
} else {
|
|
alert('角色卡不存在或无权访问')
|
|
navigate('/characters')
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleExport = async () => {
|
|
if (!character) return
|
|
try {
|
|
const response = await characterApi.exportCharacter(character.id)
|
|
const blob = new Blob([response.data], { type: 'application/json' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `${character.name}.json`
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
} catch (err) {
|
|
console.error('导出角色卡失败:', err)
|
|
alert('导出失败,请重试')
|
|
}
|
|
}
|
|
|
|
const handleStartChat = () => {
|
|
if (!character) return
|
|
navigate(`/chat?character=${character.id}`)
|
|
}
|
|
|
|
const getWorldBookStats = () => {
|
|
if (!character?.characterBook) return null
|
|
try {
|
|
const book = typeof character.characterBook === 'string'
|
|
? JSON.parse(character.characterBook)
|
|
: character.characterBook
|
|
return {
|
|
name: book.name || '未命名世界书',
|
|
entryCount: book.entries?.length || 0
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-[#0A0A0F] via-[#1A0B2E] to-[#0A0A0F] flex items-center justify-center">
|
|
<div className="text-white">加载中...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!character) {
|
|
return null
|
|
}
|
|
|
|
const worldBookStats = getWorldBookStats()
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-[#0A0A0F] via-[#1A0B2E] to-[#0A0A0F]">
|
|
<Navbar />
|
|
|
|
<div className="pt-32 pb-20 px-4">
|
|
<div className="max-w-5xl mx-auto">
|
|
<Link
|
|
to="/characters"
|
|
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors mb-6"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
返回角色管理
|
|
</Link>
|
|
|
|
<div className="glass rounded-3xl overflow-hidden">
|
|
<div className="relative h-64 bg-gradient-to-br from-primary/20 to-secondary/20">
|
|
{character.avatar && (
|
|
<img
|
|
src={character.avatar}
|
|
alt={character.name}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
)}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-[#0A0A0F] to-transparent" />
|
|
<div className="absolute bottom-6 left-6 right-6">
|
|
<h1 className="text-4xl font-bold mb-2">{character.name}</h1>
|
|
{character.creator && (
|
|
<div className="flex items-center gap-2 text-white/60">
|
|
<User className="w-4 h-4" />
|
|
<span>{character.creator}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-8">
|
|
<div className="flex gap-3 mb-8">
|
|
<button
|
|
onClick={handleStartChat}
|
|
className="flex-1 px-6 py-3 bg-gradient-to-r from-primary to-secondary rounded-xl font-medium hover:opacity-90 transition-opacity cursor-pointer flex items-center justify-center gap-2"
|
|
>
|
|
<MessageSquare className="w-5 h-5" />
|
|
开始对话
|
|
</button>
|
|
<button
|
|
onClick={() => navigate(`/characters?edit=${character.id}`)}
|
|
className="px-6 py-3 glass-hover rounded-xl cursor-pointer flex items-center gap-2"
|
|
>
|
|
<Edit className="w-5 h-5" />
|
|
编辑
|
|
</button>
|
|
<button
|
|
onClick={handleExport}
|
|
className="px-6 py-3 glass-hover rounded-xl cursor-pointer flex items-center gap-2"
|
|
>
|
|
<Download className="w-5 h-5" />
|
|
导出
|
|
</button>
|
|
</div>
|
|
|
|
{character.tags && character.tags.length > 0 && (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Tag className="w-4 h-4 text-primary" />
|
|
<span className="text-sm font-medium">标签</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{character.tags.map((tag, i) => (
|
|
<span key={i} className="px-3 py-1 glass rounded-lg text-sm">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{worldBookStats && (
|
|
<div className="mb-6 glass rounded-xl p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Book className="w-4 h-4 text-secondary" />
|
|
<span className="text-sm font-medium">世界书</span>
|
|
</div>
|
|
<div className="text-sm text-white/60">
|
|
<div>{worldBookStats.name}</div>
|
|
<div>{worldBookStats.entryCount} 个条目</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
{character.description && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">角色描述</h3>
|
|
<p className="text-white/80 whitespace-pre-wrap">{character.description}</p>
|
|
</div>
|
|
)}
|
|
|
|
{character.personality && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">性格特征</h3>
|
|
<p className="text-white/80 whitespace-pre-wrap">{character.personality}</p>
|
|
</div>
|
|
)}
|
|
|
|
{character.scenario && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">场景设定</h3>
|
|
<p className="text-white/80 whitespace-pre-wrap">{character.scenario}</p>
|
|
</div>
|
|
)}
|
|
|
|
{character.firstMes && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">第一条消息</h3>
|
|
<div className="glass rounded-xl p-4">
|
|
<p className="text-white/80 whitespace-pre-wrap">{character.firstMes}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{character.mesExample && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">示例对话</h3>
|
|
<div className="glass rounded-xl p-4">
|
|
<p className="text-white/80 whitespace-pre-wrap font-mono text-sm">
|
|
{character.mesExample}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{character.systemPrompt && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">系统提示词</h3>
|
|
<div className="glass rounded-xl p-4">
|
|
<p className="text-white/80 whitespace-pre-wrap font-mono text-sm">
|
|
{character.systemPrompt}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-4 text-sm text-white/40 pt-4 border-t border-white/10">
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="w-4 h-4" />
|
|
创建于 {new Date(character.createdAt).toLocaleDateString()}
|
|
</div>
|
|
<div>版本 {character.version || '1.0'}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|