🎨 重构用户端前端为vue开发,完善基础类和角色相关接口
This commit is contained in:
426
web-app-vue/src/views/character/Detail.vue
Normal file
426
web-app-vue/src/views/character/Detail.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<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="Like"
|
||||
@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>
|
||||
</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 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>
|
||||
</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,
|
||||
Like,
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user