Files
Reisaye/EyeVue/src/views/read.vue
2025-08-08 16:07:49 +08:00

365 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="flex h-screen w-full bg-gray-100 dark:bg-gray-900 p-6 overflow-y-auto">
<div class="max-w-7xl mx-auto flex w-full">
<!-- 左侧信息区域 -->
<div class="w-3/10 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mr-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">内容统计</h2>
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<tbody class="bg-white dark:bg-gray-700">
<tr class="border-b border-gray-200 dark:border-gray-600">
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300">行数</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-200">{{ lineCount }}</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-600">
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300">语言</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-200">{{ language }}</td>
</tr>
<tr class="border-b border-gray-200 dark:border-gray-600">
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300">字符数</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-200">{{ article?.content?.length || 0 }}</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300">阅读数</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-200">{{ article?.read_count || 0 }}</td>
</tr>
<tr>
<td class="px-4 py-2 font-medium text-gray-700 dark:text-gray-300">标签数</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-200">{{ tagCount }}</td>
</tr>
</tbody>
</table>
<!-- 支持统计饼图 -->
<div v-if="article" class="mt-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">翻译支持比例</h2>
<div class="w-50">
<PieChart :data="supportData" />
</div>
</div>
</div>
<!-- 右侧阅读区域 -->
<div class="w-7/10 w-full bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 overflow-y-auto">
<div v-if="loading" class="text-center text-gray-500">加载中...</div>
<div v-else-if="error" class="text-center text-red-500">{{ error }}</div>
<div v-else class="prose dark:prose-invert max-w-none">
<!-- 标题 -->
<h1 class="text-3xl font-bold text-gray-800 dark:text-white mb-4">{{ article.title }}</h1>
<!-- 作者 -->
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">作者{{ article.author }}</p>
<a class="flex text-sm text-gray-500 dark:text-gray-400 mb-6" :href="`/edit?id=${article.id}`">编辑文章</a>
<hr />
<!-- 内容 -->
<div class="mb-6 whitespace-pre-line text-gray-700 dark:text-gray-300">
{{ article.content }}
</div>
<!-- 人工翻译 -->
<div v-if="article.transform_content_human" class="mb-4">
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">人工翻译内容</h2>
<div class="whitespace-pre-line bg-gray-50 dark:bg-gray-700 p-4 rounded border dark:border-gray-600">
{{ article.transform_content_human }}
</div>
<button
@click="handleLike('human')"
class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 like-button"
>
<svg
:class="{ 'text-red-500 dark:text-red-400': humanLikeStatus }"
class="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.583 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
</svg>
点赞 {{ humanLikeStatus ? '已' : '未' }}完成
</button>
</div>
<!-- 多个机器翻译 -->
<div v-for="(trans, index) in article.transform_content_machine" :key="index" class="mb-4">
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">AI 翻译内容 {{ index + 1 }}</h2>
<!-- 显示 Prompt -->
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1">
提示词{{ article.support_machine_prompt[index] || '未提供' }}
</p>
<!-- 翻译内容 -->
<div class="whitespace-pre-line bg-gray-50 dark:bg-gray-700 p-4 rounded border dark:border-gray-600">
{{ trans }}
</div>
<!-- 点赞按钮 -->
<button
@click="handleLike(`machine-${index}`)"
class="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 like-button"
>
<svg
:class="{ 'text-red-500 dark:text-red-400': machineLikeStatus[index] }"
class="w-4 h-4 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.583 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
</svg>
点赞 {{ machineLikeStatus[index] ? '已' : '未' }}完成
</button>
</div>
<!-- 标签 -->
<div v-if="article.tags && article.tags.length > 0" class="mt-6">
<h2 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">标签</h2>
<div class="flex flex-wrap gap-2">
<span v-for="tagId in article.tags" :key="tagId" class="inline-flex items-center bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 px-3 py-1 rounded-full text-sm">
{{ tagTitles[tagId] || '加载中...' }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watchEffect } from 'vue'
import axios from 'axios'
import { useRoute } from 'vue-router'
import PieChart from '@/components/PieChart.vue'
import router from '@/router'
const route = useRoute()
// 文章数据
const article = ref<any>(null)
const loading = ref(true)
const error = ref<string | null>(null)
// 翻译支持统计
const supportData = ref({
labels: ['人工翻译'],
datasets: [
{
label: '翻译支持比例',
data: [0],
backgroundColor: [
'rgba(54, 162, 235, 0.7)',
'rgba(255, 99, 132, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)'
],
borderColor: [
'rgba(54, 162, 235, 1)',
'rgba(255, 99, 132, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)'
],
borderWidth: 1
}
]
})
// 统计信息计算
const wordCount = ref(0)
const lineCount = ref(0)
const language = ref('未知')
const tagCount = ref(0)
// 标签相关
const tagTitles = ref<Record<string, string>>({}) // 保存 tagId -> title 映射
// 获取标签名称
const fetchTagTitle = async (tagId: string) => {
try {
const response = await axios.get('/api/tags/getTagById', {
params: { id: tagId }
})
tagTitles.value[tagId] = response.data.title
} catch (err) {
tagTitles.value[tagId] = '未知标签'
console.error(`获取标签 ${tagId} 失败`, err)
}
}
// 批量获取所有标签名
const fetchAllTagTitles = () => {
if (!article.value?.tags?.length) return
const uniqueTagIds = [...new Set(article.value.tags)] // 去重
uniqueTagIds.forEach(tagId => {
if (!tagTitles.value[tagId]) {
fetchTagTitle(tagId)
}
})
}
// 加载文章
const fetchArticle = async () => {
const id = route.query.id as string
if (!id) {
error.value = '缺少文章 ID'
loading.value = false
return
}
try {
const response = await axios.get('/api/page/getPageById', {
params: { id }
})
article.value = response.data
// 更新本地 read_count
if (article.value) {
article.value.read_count = (article.value.read_count || 0) + 1
}
// 初始化点赞状态
initLikeStatus()
// 更新统计数据
updateSupportData()
wordCount.value = article.value.content.trim().split(/\s+/).length
// 获取标签名称
fetchAllTagTitles()
} catch (err) {
error.value = '加载文章失败'
console.error(err)
} finally {
loading.value = false
}
}
// 翻译点赞状态
const humanLikeStatus = ref<boolean>(false)
const machineLikeStatus = ref<{ [index: number]: boolean }>({})
const initLikeStatus = () => {
// 人工翻译点赞状态
const humanKey = `like_human_${article.value.id}`
const storedHuman = localStorage.getItem(humanKey)
if (storedHuman) {
humanLikeStatus.value = JSON.parse(storedHuman).liked
}
// 机器翻译点赞状态
const machineLength = article.value?.transform_content_machine?.length || 0
for (let i = 0; i < machineLength; i++) {
const key = `like_machine_${article.value.id}_${i}`
const stored = localStorage.getItem(key)
if (stored) {
machineLikeStatus.value[i] = JSON.parse(stored).liked
}
}
}
// 更新饼图数据
const updateSupportData = () => {
if (!article.value) return
const labels = ['人工翻译']
const data = [article.value.support_human]
if (Array.isArray(article.value.transform_content_machine)) {
article.value.transform_content_machine.forEach((_, index) => {
labels.push(`AI翻译 ${index + 1}${article.value.support_machine_prompt[index] || '无提示词'}`)
data.push(article.value.support_machine[index])
})
}
supportData.value.labels = labels
supportData.value.datasets[0].data = data
}
const handleLike = async (type: 'human' | `machine-${number}`) => {
let needUpdate = false
if (type === 'human') {
if (humanLikeStatus.value) return
article.value.support_human += 1
humanLikeStatus.value = true
localStorage.setItem(`like_human_${article.value.id}`, JSON.stringify({ liked: true, timestamp: Date.now() }))
needUpdate = true
} else {
const match = type.match(/machine-(\d+)/)
if (!match) return
const index = parseInt(match[1], 10)
if (machineLikeStatus.value[index]) return
article.value.support_machine[index] += 1
machineLikeStatus.value[index] = true
localStorage.setItem(`like_machine_${article.value.id}_${index}`, JSON.stringify({ liked: true, timestamp: Date.now() }))
needUpdate = true
}
if (needUpdate) {
try {
await axios.post('/api/page/like', {
id: article.value.id,
human: article.value.support_human,
machine: article.value.support_machine
}, {
headers: { 'Content-Type': 'application/json' }
})
updateSupportData()
} catch (err) {
console.error('点赞失败:', err)
}
}
}
onMounted(() => {
if (window.innerWidth < 768) {
router.push(`/readM?id=${route.query.id}`)
}
fetchArticle()
})
watchEffect(() => {
if (article.value?.content) {
wordCount.value = article.value.content.trim().split(/\s+/).length
lineCount.value = article.value.content.split('\n').length
const text = article.value.content.trim()
if (/[\u4e00-\u9fa5]/.test(text)) {
language.value = '中文'
} else if (/[a-zA-Z]/.test(text)) {
language.value = '英文'
} else {
language.value = '未知'
}
}
updateSupportData()
tagCount.value = article.value?.tags?.length || 0
})
</script>
<style scoped>
.like-button {
transition: all 0.2s ease-in-out;
}
.like-button:hover {
transform: scale(1.05);
}
.like-button:active {
transform: scale(0.95);
}
.flex,
.w-4\/10,
.w-6\/10 {
background-color: transparent !important;
}
.whitespace-pre-line {
white-space: pre-line;
}
</style>