🎨 重构用户端前端为vue开发,完善基础类和角色相关接口

This commit is contained in:
2026-02-10 21:55:45 +08:00
parent db934ebed7
commit 56e821b222
92 changed files with 18377 additions and 21 deletions

View 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>