forked from lxbfYeaa/Reisaye
365 lines
12 KiB
Vue
365 lines
12 KiB
Vue
<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>
|