{{ STATUS[row.status] || row.status }}
@@ -130,6 +133,7 @@ async function del(row) {
try { await apiDelete(`/api/admin/knowledge/${row._id}`); ElMessage.success('已删除'); load() }
catch (e) { ElMessage.error(e.message || '删除失败') }
}
+function fmt(t) { return t ? new Date(t).toLocaleString('zh-CN', { hour12: false }) : '—' }
onMounted(load)
diff --git a/admin/src/views/Trends.vue b/admin/src/views/Trends.vue
index 72ccbb9..69e50c8 100644
--- a/admin/src/views/Trends.vue
+++ b/admin/src/views/Trends.vue
@@ -3,6 +3,8 @@
搜索
+ 待补 {{ missingCount }}
+ AI 补齐人话解读
新增资讯
@@ -18,7 +20,7 @@
{{ row.plainExplain }} | {{ row.takeAway }}
- 待补
+ 待补(点击筛选)
@@ -65,12 +67,26 @@ const list = ref([]), total = ref(0), page = ref(1), pageSize = 20, q = ref(''),
const dlg = ref(false)
const form = ref({})
function search() { page.value = 1; load() }
+const missingOnly = ref(false)
+const missingCount = ref(0)
+const filling = ref(false)
+function filterMissing() { missingOnly.value = !missingOnly.value; page.value = 1; load() }
+async function fillMissing() {
+ if (!missingCount.value || filling.value) return
+ filling.value = true
+ try {
+ const r = await apiPost('/api/admin/trends/fill-plain', {})
+ ElMessage.success(`已补齐 ${r.filled || 0} 条人话解读`)
+ load()
+ } catch (e) { ElMessage.error(e.message || '补齐失败') } finally { filling.value = false }
+}
async function load() {
loading.value = true
try {
- const r = await apiGet(`/api/admin/trends?page=${page.value}&limit=${pageSize}&keyword=${encodeURIComponent(q.value)}`)
+ const r = await apiGet(`/api/admin/trends?page=${page.value}&limit=${pageSize}&keyword=${encodeURIComponent(q.value)}${missingOnly.value ? '&missing=1' : ''}`)
list.value = r.items || []
total.value = r.pagination?.total || 0
+ missingCount.value = r.missingCount ?? 0
} finally { loading.value = false }
}
function openEdit(row) {
diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js
index 4626f61..b064fd2 100755
--- a/server/src/routes/admin.js
+++ b/server/src/routes/admin.js
@@ -1157,18 +1157,68 @@ router.get('/login-logs', requirePermission('user:read'), async (req, res) => {
*/
router.get('/trends', requirePermission('trend:read'), async (req, res) => {
try {
- const { page = 1, limit = 20, keyword = '' } = req.query
+ const { page = 1, limit = 20, keyword = '', missing } = req.query
const query = keyword ? { title: { $regex: escapeRegExp(keyword), $options: 'i' } } : {}
+ if (missing === '1') query.plainExplain = { $in: ['', null, undefined] }
const total = await Trend.countDocuments(query)
+ const missingCount = await Trend.countDocuments({ plainExplain: { $in: ['', null, undefined] } })
const items = await Trend.find(query).sort({ newsDate: -1, createdAt: -1 })
.skip((parseInt(page) - 1) * parseInt(limit)).limit(parseInt(limit)).lean()
- return ApiResponse.paginated(res, items, { page: parseInt(page), limit: parseInt(limit), total })
+ return res.status(200).json({
+ success: true,
+ message: '查询成功',
+ data: { items, missingCount, pagination: { page: parseInt(page), limit: parseInt(limit), total, totalPages: Math.ceil(total / parseInt(limit)) } },
+ })
} catch (error) {
logger.error('获取趋势列表失败:', error)
return ApiResponse.serverError(res, '获取趋势列表失败')
}
})
+// AI 批量补齐人话解读(复用 aiChat 的模型配置:数据库活跃模型或环境变量)
+router.post('/trends/fill-plain', requirePermission('trend:write'), async (req, res) => {
+ try {
+ const limit = Math.min(30, parseInt(req.body?.limit) || 20)
+ const missing = await Trend.find({ plainExplain: { $in: ['', null, undefined] } })
+ .sort({ newsDate: -1 }).limit(limit).lean()
+ if (!missing.length) return ApiResponse.success(res, { filled: 0, total: 0 }, '没有待补条目')
+
+ // 取模型配置
+ const active = await AIModel.getActiveModels().catch(() => [])
+ const model = (active && active[0]) || {
+ apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
+ apiKey: process.env.OPENAI_API_KEY,
+ modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
+ }
+ if (!model.apiKey) return ApiResponse.error(res, '未配置 AI 模型(系统设置→AI模型)', 400)
+
+ let filled = 0
+ for (const t of missing) {
+ try {
+ const prompt = `给下面这条AI行业新闻写两句中文解读。要求:1) 第一句"人话":用不超过40字的大白话说清这件事;2) 第二句"对你":说明对普通用户/从业者意味着什么,不超过40字。只输出两行,格式:人话内容\n对你内容\n\n新闻标题:${t.title}\n摘要:${(t.summary || '').slice(0, 200)}`
+ const r = await fetch(model.apiUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${model.apiKey}` },
+ body: JSON.stringify({ model: model.modelId, messages: [{ role: 'user', content: prompt }], max_tokens: 300, temperature: 0.5 }),
+ signal: AbortSignal.timeout(60_000),
+ })
+ const j = await r.json()
+ const text = String(j?.choices?.[0]?.message?.content || '').trim()
+ const lines = text.split('\n').map(x => x.replace(/^(人话|对你)[::]?\s*/, '').trim()).filter(Boolean)
+ if (lines.length >= 2) {
+ await Trend.updateOne({ _id: t._id }, { plainExplain: lines[0].slice(0, 60), takeAway: lines[1].slice(0, 60) })
+ filled++
+ }
+ } catch (e) { logger.warn('补齐失败:', t.title, e.message) }
+ }
+ const remain = await Trend.countDocuments({ plainExplain: { $in: ['', null, undefined] } })
+ return ApiResponse.success(res, { filled, remain }, `已补齐 ${filled} 条,剩余 ${remain} 条待补`)
+ } catch (error) {
+ logger.error('批量补齐失败:', error)
+ return ApiResponse.serverError(res, '批量补齐失败')
+ }
+})
+
router.post('/trends', requirePermission('trend:write'), async (req, res) => {
try {
const { title, summary, source, sourceUrl, category, tags, hot, picked, newsDate, plainExplain, takeAway } = req.body