- 修改 admin/vite.config.js 添加 base: '/admin/' 解决静态资源路径问题 - 修复后台 index.html 资源引用从 /assets/ → /admin/assets/ - 更新优化器默认 API 地址为服务器地址 - 添加友链管理相关代码 - 修复多处分类管理页面
339 lines
8.2 KiB
Vue
339 lines
8.2 KiB
Vue
<template>
|
||
<div class="detail-page" v-if="joke">
|
||
<div class="joke-detail-card">
|
||
<!-- 标签 -->
|
||
<div class="detail-tags">
|
||
<span class="tag tag-type" v-for="(name, i) in (joke.type_names||[])" :key="'t'+i">{{ name }}</span>
|
||
<span class="tag tag-crowd" v-for="(name, i) in (joke.crowd_names||[])" :key="'c'+i">{{ name }}</span>
|
||
</div>
|
||
|
||
<!-- 标题 + AI评价 -->
|
||
<div class="title-row">
|
||
<h1 class="detail-title">{{ joke.title }}</h1>
|
||
<span v-if="joke.ai_level" class="ai-badge" :class="'ai-' + joke.ai_level">
|
||
{{ getLevelText(joke.ai_level) }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 内容:优先显示润色内容 -->
|
||
<div class="detail-content">
|
||
<template v-if="joke.polished_content">
|
||
<div class="polished-label">✨ AI 润色版</div>
|
||
{{ joke.polished_content }}
|
||
</template>
|
||
<template v-else>{{ joke.content }}</template>
|
||
</div>
|
||
|
||
<!-- 原始内容(如果有润色) -->
|
||
<details v-if="joke.polished_content" class="original-content">
|
||
<summary>查看原文</summary>
|
||
{{ joke.content }}
|
||
</details>
|
||
|
||
<!-- 操作栏 -->
|
||
<div class="detail-actions">
|
||
<button class="action-primary" @click="handleNext">
|
||
<span>🎲</span> 再看一条
|
||
</button>
|
||
<button class="action-secondary" @click="$router.push('/')">
|
||
<span>🏠</span> 返回首页
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 统计 -->
|
||
<div class="detail-meta">
|
||
<span>浏览 {{ joke.view_count || 0 }}</span>
|
||
<span class="sep">|</span>
|
||
<button class="like-btn" :class="{ 'liked': liked }" @click.stop="handleLike">
|
||
<span class="heart">❤</span>
|
||
<span>点赞 {{ joke.like_count || 0 }}</span>
|
||
</button>
|
||
<span class="sep">|</span>
|
||
<span>{{ formatDate(joke.created_at) }}</span>
|
||
<span v-if="joke.ai_score" class="sep">|</span>
|
||
<span v-if="joke.ai_score" class="ai-score">评分: {{ joke.ai_score.toFixed(1) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-loading" v-else>
|
||
<div class="spinner"></div>
|
||
<span>加载中...</span>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { getJoke, getJokes, likeJoke } from '@/api/joke'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const joke = ref(null)
|
||
const liked = ref(false)
|
||
const loading = ref(false)
|
||
// 请求 ID 追踪,防止竞态
|
||
let currentRequestId = 0
|
||
|
||
const loadJoke = async () => {
|
||
const requestId = ++currentRequestId
|
||
loading.value = true
|
||
try {
|
||
const res = await getJoke(route.params.id)
|
||
// 只接受最新请求的响应
|
||
if (requestId !== currentRequestId) return
|
||
joke.value = res
|
||
liked.value = false
|
||
loading.value = false
|
||
} catch (e) {
|
||
if (requestId !== currentRequestId) return
|
||
loading.value = false
|
||
// 笑话不存在时返回首页
|
||
if (e?.detail === '笑话不存在' || e?.response?.status === 404) {
|
||
router.push('/')
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleLike = async () => {
|
||
if (liked.value || !joke.value) return
|
||
try {
|
||
const res = await likeJoke(route.params.id)
|
||
if (joke.value && res?.like_count !== undefined) {
|
||
joke.value.like_count = res.like_count
|
||
}
|
||
liked.value = true
|
||
ElMessage.success('点赞成功')
|
||
} catch (e) {
|
||
ElMessage.error('点赞失败,请重试')
|
||
}
|
||
}
|
||
|
||
const handleNext = async () => {
|
||
try {
|
||
const res = await getJokes({ page: 1, page_size: 50 })
|
||
if (res.items && res.items.length) {
|
||
const rnd = res.items[Math.floor(Math.random() * res.items.length)]
|
||
router.push(`/detail/${rnd.id}`)
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
const formatDate = (dateStr) => {
|
||
if (!dateStr) return ''
|
||
return new Date(dateStr).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
|
||
}
|
||
|
||
function getLevelText(level) {
|
||
const map = {
|
||
'excellent': '⭐⭐⭐ 精品',
|
||
'good': '⭐⭐ 良好',
|
||
'ordinary': '⭐ 普通',
|
||
'poor': '⚠️ 待优化'
|
||
}
|
||
return map[level] || ''
|
||
}
|
||
|
||
onMounted(() => loadJoke())
|
||
|
||
watch(() => route.params.id, () => loadJoke())
|
||
</script>
|
||
|
||
<style scoped>
|
||
.detail-page {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 0;
|
||
}
|
||
|
||
.joke-detail-card {
|
||
width: 100%;
|
||
max-width: 780px;
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-lg);
|
||
padding: 40px 48px;
|
||
box-shadow: var(--shadow);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
transition: background 0.3s, box-shadow 0.3s;
|
||
}
|
||
|
||
.detail-tags { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||
|
||
/* 标题行:标题 + AI评价右对齐 */
|
||
.title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
}
|
||
|
||
/* AI 评价徽章 */
|
||
.ai-badge {
|
||
font-size: 13px;
|
||
padding: 4px 14px;
|
||
border-radius: 14px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
.ai-excellent { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #92400e; }
|
||
.ai-good { background: linear-gradient(135deg, #d1fae5, #a7f3d0); color: #065f46; }
|
||
.ai-ordinary { background: #f3f4f6; color: #4b5563; }
|
||
.ai-poor { background: #fee2e2; color: #991b1b; }
|
||
|
||
/* AI 润色标签 */
|
||
.polished-label {
|
||
font-size: 12px;
|
||
color: #9333ea;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
display: inline-block;
|
||
}
|
||
|
||
/* 原文折叠 */
|
||
.original-content {
|
||
margin-top: 16px;
|
||
padding: 12px;
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
}
|
||
.original-content summary {
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* AI 评分 */
|
||
.ai-score {
|
||
color: #9333ea;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.detail-title {
|
||
font-size: 26px;
|
||
font-weight: 800;
|
||
color: var(--text-primary);
|
||
line-height: 1.4;
|
||
letter-spacing: 0.5px;
|
||
transition: color 0.3s;
|
||
margin: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.detail-content {
|
||
font-size: 17px;
|
||
line-height: 2;
|
||
color: var(--text-secondary);
|
||
white-space: pre-wrap;
|
||
letter-spacing: 0.3px;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.detail-actions { display: flex; gap: 14px; padding-top: 8px; }
|
||
|
||
.action-primary {
|
||
padding: 12px 28px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 25px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
}
|
||
.action-primary:hover {
|
||
background: var(--primary-dark);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 16px rgba(255,107,0,0.3);
|
||
}
|
||
|
||
.action-secondary {
|
||
padding: 12px 28px;
|
||
background: var(--bg-card);
|
||
color: var(--text-secondary);
|
||
border: 1.5px solid var(--border);
|
||
border-radius: 25px;
|
||
font-size: 15px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
}
|
||
.action-secondary:hover {
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.detail-meta {
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid var(--border);
|
||
transition: color 0.3s, border-color 0.3s;
|
||
}
|
||
.sep { color: #ddd; }
|
||
|
||
.like-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
padding: 0;
|
||
transition: color 0.2s;
|
||
}
|
||
.like-btn:hover { color: #e74c3c; }
|
||
.like-btn.liked { color: #e74c3c; }
|
||
.like-btn .heart {
|
||
font-size: 14px;
|
||
color: #ccc;
|
||
transition: color 0.2s, transform 0.2s;
|
||
}
|
||
.like-btn:hover .heart { color: #e74c3c; }
|
||
.like-btn.liked .heart {
|
||
color: #e74c3c;
|
||
animation: pop 0.3s ease;
|
||
}
|
||
@keyframes pop {
|
||
0% { transform: scale(1); }
|
||
50% { transform: scale(1.3); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.detail-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
padding: 80px;
|
||
color: var(--text-muted);
|
||
}
|
||
.spinner {
|
||
width: 28px; height: 28px;
|
||
border: 3px solid var(--border);
|
||
border-top-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
</style> |