feat(web): ArticleDetail 三段式(评论/译文/原文) + LLM 屏蔽词配置 + .diary-para 兜底
- types: Source / LlmSetting 加 blocklist_tags 字段
- AdminLlmSettings:
- 新增 '全局屏蔽分类(命中即删文章)' 卡片(逗号/换行分隔,双向绑 blocklist_tags)
- 分类 prompt 提示加 {blocklist} / drop 字段说明
- ArticleDetail 三段式:
- 顶部:评论(LLM 点评)
- 中部:文章译文(优先 LLM 排版版 / fallback 原始译文)
- 底部:文章原文
- AI 插图挂在译文卡片下作附属
- style.css: .diary-para 兜底规则(margin 0 0 1.5em 0 / line-height 1.7 / color #3e3e3e)
This commit is contained in:
@@ -15,6 +15,8 @@ export interface Source {
|
||||
last_fetched_at?: string | null
|
||||
last_status?: string | null
|
||||
consecutive_failures: number
|
||||
// 源级屏蔽分类标签;与 llm_settings.blocklist_tags 合并后注入 classify prompt
|
||||
blocklist_tags?: string[]
|
||||
}
|
||||
|
||||
export interface ArticleListItem {
|
||||
@@ -71,6 +73,8 @@ export interface LlmSetting {
|
||||
image_model: string
|
||||
interval_sec: number
|
||||
enabled: boolean
|
||||
// 全局屏蔽分类标签;与 sources.blocklist_tags 合并后注入 classify prompt
|
||||
blocklist_tags?: string[]
|
||||
updated_at?: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -45,3 +45,16 @@ img { max-width: 100%; }
|
||||
.article-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* === 排版段落(.diary-para)兜底规则 ===
|
||||
* 后端 enrichment._wrap_article_body 会内联 style 到 <p class="diary-para">;
|
||||
* 这里做兜底,保证 v-html 渲染时一定有合理的行距和段距。
|
||||
*/
|
||||
.diary-para {
|
||||
margin: 0 0 1.5em 0;
|
||||
line-height: 1.7;
|
||||
color: #3e3e3e;
|
||||
}
|
||||
.diary-para:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { onMounted, ref, watch, computed } from 'vue'
|
||||
import {
|
||||
NCard, NSpace, NButton, NInput, NInputNumber, NSwitch, NAlert, useMessage, NSpin, NDivider, NText, NCode,
|
||||
} from 'naive-ui'
|
||||
@@ -19,6 +19,19 @@ const setting = ref<LlmSetting>({
|
||||
image_model: 'agnes-image-2.1-flash',
|
||||
interval_sec: 2.0,
|
||||
enabled: true,
|
||||
blocklist_tags: [],
|
||||
})
|
||||
|
||||
// === 屏蔽分类标签(文本框 ↔ 数组) ===
|
||||
// UI 用逗号分隔,存到 setting.blocklist_tags(数组)
|
||||
const blocklistText = computed({
|
||||
get: () => (setting.value.blocklist_tags || []).join(' / '),
|
||||
set: (v: string) => {
|
||||
setting.value.blocklist_tags = v
|
||||
.split(/[,,;\n]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
},
|
||||
})
|
||||
|
||||
const testResult = ref<{ ok: boolean; detail: string; configured: boolean } | null>(null)
|
||||
@@ -141,8 +154,8 @@ onMounted(load)
|
||||
|
||||
<NCard title="分类提示词" style="margin-top: 16px">
|
||||
<NText depth="3" style="font-size: 12px">
|
||||
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{summary}</NCode> = 摘要, <NCode>{body}</NCode> = 正文(节选)。<br />
|
||||
期望返回 JSON(多标签,2-5 个),形如 <NCode>{`{"categories": ["时政", "国际", "经济"]}`}</NCode>
|
||||
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{summary}</NCode> = 摘要, <NCode>{body}</NCode> = 正文(节选), <NCode>{blocklist}</NCode> = 屏蔽词列表。<br />
|
||||
期望返回 JSON,形如 <NCode>{`{"categories": ["时政", "国际", "经济"], "drop": false}`}</NCode>;若新闻属于屏蔽词之一,务必将 <NCode>drop</NCode> 设为 true。
|
||||
</NText>
|
||||
<NInput
|
||||
v-model:value="setting.classify_prompt"
|
||||
@@ -153,6 +166,24 @@ onMounted(load)
|
||||
/>
|
||||
</NCard>
|
||||
|
||||
<NCard title="全局屏蔽分类(命中即删文章)" style="margin-top: 16px">
|
||||
<NText depth="3" style="font-size: 12px">
|
||||
逗号或换行分隔,与各源级 <NCode>blocklist_tags</NCode> 合并去重后注入 classify prompt 的 <NCode>{blocklist}</NCode> 变量。<br />
|
||||
匹配规则:LLM 返回的 categories 中任一项等于屏蔽词 → 整篇文章删除(且不再做后续 排版 / 插图 / 点评)。<br />
|
||||
例: <NCode>体育 / 娱乐 / 游戏</NCode> 即可屏蔽这三类文章。
|
||||
</NText>
|
||||
<NInput
|
||||
v-model:value="blocklistText"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
placeholder="体育 / 娱乐 / 游戏"
|
||||
style="margin-top: 8px"
|
||||
/>
|
||||
<NText v-if="(setting.blocklist_tags || []).length === 0" depth="3" style="font-size: 12px; margin-top: 4px; display: block">
|
||||
留空 = 不做屏蔽
|
||||
</NText>
|
||||
</NCard>
|
||||
|
||||
<NCard title="点评提示词" style="margin-top: 16px">
|
||||
<NText depth="3" style="font-size: 12px">
|
||||
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{body}</NCode> = 译文正文
|
||||
|
||||
@@ -156,25 +156,31 @@ onMounted(load)
|
||||
本条翻译失败,可点 "重译" 重试,或查看后端日志。
|
||||
</NAlert>
|
||||
|
||||
<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>
|
||||
<!-- 三段式:评论(顶部) / 译文(中) / 原文(底) -->
|
||||
|
||||
<!-- 1) 评论(LLM 点评) -->
|
||||
<NCard v-if="article.commentary" style="margin-top: 16px" title="💬 评论">
|
||||
<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>
|
||||
|
||||
<!-- 2) 译文(优先 LLM 排版版,fallback 原始译文) -->
|
||||
<div v-if="showTranslation" style="margin-top: 16px">
|
||||
<NCard v-if="article.body_zh_formatted" title="译文(LLM 排版版)">
|
||||
<NCard v-if="article.body_zh_formatted" title="📖 文章译文">
|
||||
<template #header-extra>
|
||||
<NSpace align="center">
|
||||
<NTag size="tiny" :type="statusTagType(article.format_status)">{{ article.format_status || 'n/a' }}</NTag>
|
||||
<NTag size="tiny" :type="statusTagType(article.format_status)">排版:{{ article.format_status || 'n/a' }}</NTag>
|
||||
<NButton text size="tiny" @click="showFormatted = !showFormatted">
|
||||
{{ showFormatted ? '隐藏' : '显示' }}
|
||||
{{ showFormatted ? '隐藏排版' : '显示排版' }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
<div v-if="showFormatted" v-html="article.body_zh_formatted" style="line-height: 1.8" />
|
||||
<div v-if="showFormatted" v-html="article.body_zh_formatted" />
|
||||
<NText v-else depth="3" style="font-size: 12px">已隐藏排版版(点击右上角显示)</NText>
|
||||
</NCard>
|
||||
<NCard v-else title="译文(原始)" style="margin-top: 16px">
|
||||
<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 }}
|
||||
@@ -183,20 +189,21 @@ onMounted(load)
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<!-- AI 插图(挂在译文卡片下,作附属) -->
|
||||
<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>
|
||||
|
||||
<!-- 3) 原文 -->
|
||||
<div v-if="showOriginal" style="margin-top: 16px">
|
||||
<NCard title="原文">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user