feat(ui): 新增 LLM 智能增强设置页 + 路由/侧栏 + ArticleDetail 展示排版/分类/插图/点评

This commit is contained in:
Mavis
2026-06-08 14:24:25 +08:00
parent ba2298da0a
commit 38609ff36f
5 changed files with 300 additions and 17 deletions

View File

@@ -2,9 +2,9 @@
import { onMounted, ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
NCard, NSpace, NTag, NText, NButton, NSpin, NEmpty, NDivider, NAlert, NSkeleton, useMessage,
NCard, NSpace, NTag, NText, NButton, NSpin, NEmpty, NDivider, NAlert, NSkeleton, NImage, useMessage,
} from 'naive-ui'
import { articlesApi, type ArticleDetail } from '@/api/articles'
import { articlesApi, adminApi, type ArticleDetail } from '@/api/articles'
import { http } from '@/api/client'
import { useAuthStore } from '@/stores/auth'
import dayjs from 'dayjs'
@@ -21,6 +21,8 @@ const loading = ref(true)
const starred = ref(false)
const showOriginal = ref(true)
const showTranslation = ref(true)
const showFormatted = ref(true)
const enriching = ref(false)
async function load() {
loading.value = true
@@ -60,6 +62,7 @@ function fmtTime(s?: string | null) {
const publishedAt = computed(() => article.value?.published_at || article.value?.fetched_at)
const isOwner = computed(() => auth.isOwner)
const categories = computed(() => (article.value?.category || '').split(',').filter(Boolean))
async function rerunTranslation() {
if (!article.value) return
@@ -72,6 +75,30 @@ async function rerunTranslation() {
}
}
async function triggerEnrich() {
if (!article.value) return
enriching.value = true
try {
const out = await adminApi.triggerEnrich(article.value.id)
const r = out.results || {}
const ok = Object.values(r).filter((v) => v === 'ok').length
const fail = Object.values(r).filter((v) => v.startsWith('failed')).length
message.info(`完成 ${ok} 项,失败 ${fail}`)
await load()
} catch (e: any) {
message.error(e?.response?.data?.title || '触发失败')
} finally {
enriching.value = false
}
}
function statusTagType(s?: string | null): 'success' | 'warning' | 'error' | 'default' {
if (s === 'ok') return 'success'
if (s === 'failed') return 'error'
if (s === 'pending') return 'warning'
return 'default'
}
onMounted(load)
</script>
@@ -93,6 +120,7 @@ onMounted(load)
<NTag v-if="article.translation_engine" size="small">
{{ article.translation_engine }}
</NTag>
<NTag v-for="c in categories" :key="c" type="success" size="small">{{ c }}</NTag>
</NSpace>
<h1 style="margin: 0">{{ article.title }}</h1>
<h2 v-if="article.title_zh" style="margin: 0; color: #2080f0">{{ article.title_zh }}</h2>
@@ -109,8 +137,18 @@ onMounted(load)
<NButton v-if="isOwner" type="error" ghost @click="rerunTranslation">
重译
</NButton>
<NButton v-if="isOwner" type="info" ghost :loading="enriching" @click="triggerEnrich">
LLM 增强
</NButton>
<NButton tag="a" :href="article.url" target="_blank" rel="noopener">原文链接 </NButton>
</NSpace>
<NSpace v-if="isOwner" size="small" align="center">
<NText depth="3" style="font-size: 12px">LLM 状态:</NText>
<NTag size="tiny" :type="statusTagType(article.format_status)">排版:{{ article.format_status || 'n/a' }}</NTag>
<NTag size="tiny" :type="statusTagType(article.classify_status)">分类:{{ article.classify_status || 'n/a' }}</NTag>
<NTag size="tiny" :type="statusTagType(article.image_ai_status)">插图:{{ article.image_ai_status || 'n/a' }}</NTag>
<NTag size="tiny" :type="statusTagType(article.commentary_status)">点评:{{ article.commentary_status || 'n/a' }}</NTag>
</NSpace>
</NSpace>
</NCard>
@@ -118,15 +156,25 @@ onMounted(load)
本条翻译失败,可点 "重译" 重试,或查看后端日志
</NAlert>
<div v-if="showOriginal" style="margin-top: 16px">
<NCard title="原文">
<div v-if="article.body_html" v-html="article.body_html" style="line-height: 1.8" />
<div v-else style="white-space: pre-wrap; line-height: 1.8">{{ article.body_text }}</div>
<div v-if="article.image_ai_url" style="margin-top: 16px">
<NCard title="🎨 AI 插图">
<NImage :src="article.image_ai_url" object-fit="cover" style="max-width: 100%; border-radius: 6px" />
</NCard>
</div>
<div v-if="showTranslation" style="margin-top: 16px">
<NCard title="译文">
<NCard v-if="article.body_zh_formatted" title="译文(LLM 排版版)">
<template #header-extra>
<NSpace align="center">
<NTag size="tiny" :type="statusTagType(article.format_status)">{{ article.format_status || 'n/a' }}</NTag>
<NButton text size="tiny" @click="showFormatted = !showFormatted">
{{ showFormatted ? '隐藏' : '显示' }}
</NButton>
</NSpace>
</template>
<div v-if="showFormatted" v-html="article.body_zh_formatted" style="line-height: 1.8" />
</NCard>
<NCard v-else title="译文(原始)" style="margin-top: 16px">
<div v-if="article.body_zh_html" v-html="article.body_zh_html" style="line-height: 1.8" />
<div v-else-if="article.body_zh_text" style="white-space: pre-wrap; line-height: 1.8">
{{ article.body_zh_text }}
@@ -135,15 +183,22 @@ onMounted(load)
</NCard>
</div>
<NCard v-if="article.commentary || article.entities" style="margin-top: 16px" title="智能增强 (预留)">
<div v-if="article.commentary">
<NText strong>点评: </NText>
<span>{{ article.commentary }}</span>
</div>
<div v-if="article.entities" style="margin-top: 8px">
<NText strong>实体: </NText>
<code>{{ JSON.stringify(article.entities) }}</code>
</div>
<div v-if="showOriginal" style="margin-top: 16px">
<NCard title="原文">
<div v-if="article.body_html" v-html="article.body_html" style="line-height: 1.8" />
<div v-else style="white-space: pre-wrap; line-height: 1.8">{{ article.body_text }}</div>
</NCard>
</div>
<NCard v-if="article.commentary" style="margin-top: 16px" title="💬 AI 点评">
<template #header-extra>
<NTag size="tiny" :type="statusTagType(article.commentary_status)">{{ article.commentary_status || 'n/a' }}</NTag>
</template>
<p style="white-space: pre-wrap; line-height: 1.8; margin: 0">{{ article.commentary }}</p>
</NCard>
<NCard v-if="article.entities" style="margin-top: 16px" title="🔍 实体(预留)">
<code style="font-size: 12px">{{ JSON.stringify(article.entities) }}</code>
</NCard>
</div>
</NSpin>