463 lines
12 KiB
Vue
463 lines
12 KiB
Vue
<template>
|
|
<div v-loading="loading" class="character-detail">
|
|
<template v-if="character">
|
|
<!-- 角色头部 -->
|
|
<div class="character-header">
|
|
<div class="character-avatar-large">
|
|
<img :src="character.avatar || '/default-avatar.png'" :alt="character.name" />
|
|
</div>
|
|
|
|
<div class="character-header-info">
|
|
<h1 class="character-title">{{ character.name }}</h1>
|
|
<div class="character-meta">
|
|
<span v-if="character.creatorName" class="meta-item">
|
|
<el-icon><User /></el-icon>
|
|
{{ character.creatorName }}
|
|
</span>
|
|
<span class="meta-item">
|
|
<el-icon><Clock /></el-icon>
|
|
{{ formatDate(character.createdAt) }}
|
|
</span>
|
|
<span class="meta-item">
|
|
<el-icon><View /></el-icon>
|
|
使用 {{ character.usageCount }} 次
|
|
</span>
|
|
</div>
|
|
|
|
<!-- 操作按钮 -->
|
|
<div class="character-actions">
|
|
<el-button
|
|
type="primary"
|
|
:icon="ChatLineSquare"
|
|
@click="startChat"
|
|
>
|
|
开始对话
|
|
</el-button>
|
|
<el-button
|
|
:type="character.isFavorited ? 'warning' : 'default'"
|
|
:icon="character.isFavorited ? StarFilled : Star"
|
|
@click="handleFavorite"
|
|
>
|
|
{{ character.isFavorited ? '已收藏' : '收藏' }}
|
|
</el-button>
|
|
<el-button
|
|
:icon="Top"
|
|
@click="handleLike"
|
|
>
|
|
点赞 {{ character.totalLikes }}
|
|
</el-button>
|
|
<el-dropdown @command="handleExport">
|
|
<el-button :icon="Download">
|
|
导出
|
|
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
|
</el-button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="json">导出为 JSON</el-dropdown-item>
|
|
<el-dropdown-item command="png">导出为 PNG</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
<el-button
|
|
v-if="isOwner"
|
|
:icon="Edit"
|
|
@click="goToEdit"
|
|
>
|
|
编辑
|
|
</el-button>
|
|
<el-button
|
|
v-if="isOwner"
|
|
type="danger"
|
|
:icon="Delete"
|
|
@click="handleDelete"
|
|
>
|
|
删除
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 标签 -->
|
|
<div v-if="character.tags && character.tags.length > 0" class="character-tags-section">
|
|
<el-tag
|
|
v-for="tag in character.tags"
|
|
:key="tag"
|
|
type="info"
|
|
effect="plain"
|
|
>
|
|
{{ tag }}
|
|
</el-tag>
|
|
</div>
|
|
|
|
<!-- 详细信息 -->
|
|
<div class="character-details">
|
|
<el-tabs>
|
|
<el-tab-pane label="基本信息">
|
|
<div class="detail-section">
|
|
<h3>角色描述</h3>
|
|
<p class="detail-text">{{ character.description || '暂无描述' }}</p>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h3>性格特点</h3>
|
|
<p class="detail-text">{{ character.personality || '暂无性格描述' }}</p>
|
|
</div>
|
|
|
|
<div class="detail-section">
|
|
<h3>场景设定</h3>
|
|
<p class="detail-text">{{ character.scenario || '暂无场景设定' }}</p>
|
|
</div>
|
|
|
|
<div v-if="character.systemPrompt" class="detail-section">
|
|
<h3>系统提示词</h3>
|
|
<div class="message-box">{{ character.systemPrompt }}</div>
|
|
</div>
|
|
|
|
<div v-if="character.postHistoryInstructions" class="detail-section">
|
|
<h3>后置历史指令</h3>
|
|
<div class="message-box">{{ character.postHistoryInstructions }}</div>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="对话示例">
|
|
<div class="detail-section">
|
|
<h3>第一条消息</h3>
|
|
<div class="message-box">
|
|
{{ character.firstMessage || '暂无第一条消息' }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="character.exampleMessages && character.exampleMessages.length > 0" class="detail-section">
|
|
<h3>示例对话</h3>
|
|
<div
|
|
v-for="(msg, index) in character.exampleMessages"
|
|
:key="index"
|
|
class="message-box"
|
|
>
|
|
{{ msg }}
|
|
</div>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane v-if="character.alternateGreetings && character.alternateGreetings.length > 0" label="备用问候语">
|
|
<div class="detail-section">
|
|
<div
|
|
v-for="(greeting, index) in character.alternateGreetings"
|
|
:key="index"
|
|
class="message-box"
|
|
>
|
|
<div class="greeting-label">问候语 {{ index + 1 }}</div>
|
|
{{ greeting }}
|
|
</div>
|
|
</div>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="统计信息">
|
|
<el-descriptions :column="2" border>
|
|
<el-descriptions-item label="对话总数">
|
|
{{ character.totalChats }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="点赞总数">
|
|
{{ character.totalLikes }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="使用次数">
|
|
{{ character.usageCount }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="收藏次数">
|
|
{{ character.favoriteCount }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="Token 数量">
|
|
{{ character.tokenCount }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="角色版本">
|
|
v{{ character.version }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="创建时间">
|
|
{{ formatDateTime(character.createdAt) }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="更新时间">
|
|
{{ formatDateTime(character.updatedAt) }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item v-if="character.extensions && Object.keys(character.extensions).length > 0" label="扩展数据">
|
|
{{ Object.keys(character.extensions).join(', ') }}
|
|
</el-descriptions-item>
|
|
<el-descriptions-item v-if="character.characterBook" label="角色书">
|
|
已配置
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane v-if="character.creatorNotes" label="创建者备注">
|
|
<div class="detail-section">
|
|
<p class="detail-text">{{ character.creatorNotes }}</p>
|
|
</div>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useCharacterStore } from '@/stores/character'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import {
|
|
User,
|
|
Clock,
|
|
View,
|
|
ChatLineSquare,
|
|
Star,
|
|
StarFilled,
|
|
Top,
|
|
Download,
|
|
Edit,
|
|
Delete,
|
|
ArrowDown
|
|
} from '@element-plus/icons-vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const characterStore = useCharacterStore()
|
|
const authStore = useAuthStore()
|
|
|
|
const loading = ref(false)
|
|
const character = computed(() => characterStore.currentCharacter)
|
|
|
|
// 是否是创建者
|
|
const isOwner = computed(() => {
|
|
return authStore.isLoggedIn &&
|
|
character.value?.creatorId === authStore.user?.id
|
|
})
|
|
|
|
// 格式化日期
|
|
function formatDate(dateStr: string) {
|
|
return new Date(dateStr).toLocaleDateString('zh-CN')
|
|
}
|
|
|
|
function formatDateTime(dateStr: string) {
|
|
return new Date(dateStr).toLocaleString('zh-CN')
|
|
}
|
|
|
|
// 开始对话
|
|
function startChat() {
|
|
ElMessage.info('对话功能开发中...')
|
|
// TODO: router.push(`/chat/${character.value?.id}`)
|
|
}
|
|
|
|
// 切换收藏
|
|
async function handleFavorite() {
|
|
if (!authStore.isLoggedIn) {
|
|
ElMessage.warning('请先登录')
|
|
return
|
|
}
|
|
|
|
if (character.value) {
|
|
await characterStore.toggleFavorite(character.value.id)
|
|
}
|
|
}
|
|
|
|
// 点赞
|
|
async function handleLike() {
|
|
if (character.value) {
|
|
await characterStore.likeCharacter(character.value.id)
|
|
}
|
|
}
|
|
|
|
// 导出
|
|
async function handleExport(command: string) {
|
|
if (!character.value) return
|
|
|
|
if (command === 'json') {
|
|
await characterStore.downloadCharacterJSON(character.value.id)
|
|
} else if (command === 'png') {
|
|
await characterStore.downloadCharacterPNG(character.value.id)
|
|
}
|
|
}
|
|
|
|
// 编辑
|
|
function goToEdit() {
|
|
if (character.value) {
|
|
router.push(`/character/${character.value.id}/edit`)
|
|
}
|
|
}
|
|
|
|
// 删除
|
|
async function handleDelete() {
|
|
if (!character.value) return
|
|
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
'确定要删除这个角色卡吗?此操作不可恢复。',
|
|
'确认删除',
|
|
{
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}
|
|
)
|
|
|
|
await characterStore.deleteCharacter(character.value.id)
|
|
router.push('/')
|
|
} catch (error) {
|
|
// 用户取消
|
|
}
|
|
}
|
|
|
|
// 初始化
|
|
onMounted(async () => {
|
|
const characterId = Number(route.params.id)
|
|
if (characterId) {
|
|
loading.value = true
|
|
try {
|
|
await characterStore.fetchCharacterDetail(characterId)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.character-detail {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 24px;
|
|
}
|
|
|
|
.character-header {
|
|
display: flex;
|
|
gap: 32px;
|
|
margin-bottom: 32px;
|
|
padding: 24px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
@media (max-width: 768px) {
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
}
|
|
|
|
.character-avatar-large {
|
|
flex-shrink: 0;
|
|
width: 240px;
|
|
height: 320px;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
width: 100%;
|
|
height: 400px;
|
|
}
|
|
}
|
|
|
|
.character-header-info {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.character-title {
|
|
font-size: 32px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
margin: 0;
|
|
}
|
|
|
|
.character-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
color: #909399;
|
|
font-size: 14px;
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
}
|
|
|
|
.character-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.character-tags-section {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
padding: 16px 24px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.character-details {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
padding: 24px;
|
|
}
|
|
|
|
.detail-section {
|
|
margin-bottom: 24px;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
margin-bottom: 12px;
|
|
}
|
|
}
|
|
|
|
.detail-text {
|
|
font-size: 14px;
|
|
line-height: 1.8;
|
|
color: #606266;
|
|
white-space: pre-wrap;
|
|
margin: 0;
|
|
}
|
|
|
|
.message-box {
|
|
background: #f5f7fa;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
color: #606266;
|
|
white-space: pre-wrap;
|
|
margin-bottom: 12px;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.greeting-label {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
</style>
|