feat: 市场调研落地——趋势AI人话解读(plainExplain/takeAway,采集自动生成+SSR+小程序卡)+ 首页今日知识卡
This commit is contained in:
@@ -82,6 +82,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日知识卡:每天一条,读了答对就点亮 -->
|
||||
<view class="kcard" v-if="todayKnowledge" @tap="goTodayKnowledge">
|
||||
<view class="kcard-head">
|
||||
<AppIcon name="book" :size="14" />
|
||||
<text class="kcard-label">今日知识卡</text>
|
||||
<text class="kcard-dim">{{ todayDimName }}</text>
|
||||
</view>
|
||||
<text class="kcard-title">{{ todayKnowledge.title }}</text>
|
||||
<text class="kcard-desc">{{ todayKnowledge.content }}</text>
|
||||
<view class="kcard-foot">
|
||||
<text class="kcard-cta">去读一读 · 答对点亮 →</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日快讯 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日快讯</text>
|
||||
@@ -114,6 +128,18 @@ const dimensions = ref(DIMENSIONS)
|
||||
|
||||
const newsList = ref([])
|
||||
const byDim = ref({}) // 各星域知识总数
|
||||
const todayKnowledge = ref<any>(null)
|
||||
|
||||
const todayDimName = computed(() => {
|
||||
const d = DIMENSIONS.find(x => x.id === todayKnowledge.value?.dim)
|
||||
return d ? d.name : ''
|
||||
})
|
||||
|
||||
function goTodayKnowledge() {
|
||||
if (todayKnowledge.value?._id) {
|
||||
uni.navigateTo({ url: `/pages/detail/detail?id=${todayKnowledge.value._id}` })
|
||||
}
|
||||
}
|
||||
const userStats = ref(null) // { totalLit, litByDim, streak }
|
||||
const lastRead = ref(null)
|
||||
|
||||
@@ -128,9 +154,21 @@ const todayLit = computed(() => !!uni.getStorageSync('lit_today'))
|
||||
onShow(() => {
|
||||
loadStats()
|
||||
loadNews()
|
||||
loadTodayKnowledge()
|
||||
lastRead.value = uni.getStorageSync('last_read') || null
|
||||
})
|
||||
|
||||
// 今日知识卡:按日期取当日第 N 条(同一天所有人看到同一条,形成"每日一词"共识感)
|
||||
async function loadTodayKnowledge() {
|
||||
try {
|
||||
const data = await knowledgeApi.list({ limit: 50, page: 1 })
|
||||
const items = (data.items || []).filter((x) => x.content)
|
||||
if (!items.length) return
|
||||
const dayIndex = Math.floor(Date.now() / 86400000)
|
||||
todayKnowledge.value = items[dayIndex % items.length]
|
||||
} catch (e) { /* 静默 */ }
|
||||
}
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await Promise.all([loadStats(), loadNews()])
|
||||
uni.stopPullDownRefresh()
|
||||
@@ -468,3 +506,42 @@ onShareTimeline(() => ({
|
||||
|
||||
.bottom-spacer { height: 30px; }
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.kcard {
|
||||
background: linear-gradient(135deg, rgba(91, 95, 248, 0.08), rgba(139, 92, 246, 0.05));
|
||||
border: 1px solid var(--glass-hover-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.kcard-head { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
||||
.kcard-label { font-size: 12px; font-weight: 700; color: var(--color-brand); }
|
||||
.kcard-dim {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
background: var(--glass-bg);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
.kcard-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.kcard-desc {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.kcard-foot { margin-top: 8px; }
|
||||
.kcard-cta { font-size: 11px; font-weight: 600; color: var(--color-brand); }
|
||||
</style>
|
||||
|
||||
@@ -41,6 +41,18 @@
|
||||
{{ item.summary }}
|
||||
</text>
|
||||
|
||||
<!-- 人话解读:一句话说清 + 对你意味着什么 -->
|
||||
<view class="card-plain" v-if="item.plainExplain">
|
||||
<view class="plain-line">
|
||||
<text class="plain-tag">人话</text>
|
||||
<text class="plain-text">{{ item.plainExplain }}</text>
|
||||
</view>
|
||||
<view class="plain-line" v-if="item.takeAway">
|
||||
<text class="plain-tag alt">对你</text>
|
||||
<text class="plain-text">{{ item.takeAway }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<view class="card-footer">
|
||||
<text class="card-date">{{ formatDate(item.newsDate) }}</text>
|
||||
@@ -403,3 +415,29 @@ onShareAppMessage(() => {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.card-plain {
|
||||
margin-top: 8px;
|
||||
background: var(--color-brand-bg);
|
||||
border: 1px solid var(--glass-hover-border);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.plain-line { display: flex; align-items: flex-start; gap: 6px; }
|
||||
.plain-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: var(--color-brand);
|
||||
border-radius: 5px;
|
||||
padding: 1px 6px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.plain-tag.alt { background: var(--dim3-color); }
|
||||
.plain-text { font-size: 12px; color: var(--text-primary); line-height: 1.6; }
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,9 @@ const TrendSchema = new Schema({
|
||||
},
|
||||
tags: [String],
|
||||
hot: { type: Boolean, default: false },
|
||||
// 人话解读(AI 生成,治"看不懂AI新闻"的焦虑)
|
||||
plainExplain: { type: String, default: '' }, // 一句话说清这是什么
|
||||
takeAway: { type: String, default: '' }, // 对普通人意味着什么
|
||||
picked: { type: Boolean, default: false },
|
||||
newsDate: { type: Date, default: Date.now, index: true },
|
||||
viewCount: { type: Number, default: 0 },
|
||||
|
||||
@@ -66,7 +66,7 @@ router.get('/sitemap.xml', async (req, res) => {
|
||||
|
||||
|
||||
// ===== SSR 趋势页(SEO/GEO:真实内容直出,搜索引擎可收录) =====
|
||||
const TREND_PAGE_CSS = 'body{font-family:-apple-system,"PingFang SC",sans-serif;color:#1f2333;max-width:720px;margin:0 auto;padding:24px 18px;line-height:1.7}h1{font-size:22px}.meta{color:#9aa0b4;font-size:12px;margin:4px 0 20px}a{color:#5b5ff8;text-decoration:none}.item{padding:14px 0;border-bottom:1px solid #eef0f6}.item h2{font-size:16px;margin:0 0 6px}.item p{margin:0;font-size:13px;color:#6b7080}.item .m{font-size:11px;color:#a6abbb;margin-top:4px}.tag{display:inline-block;background:rgba(91,95,248,.1);color:#5b5ff8;border-radius:99px;padding:1px 8px;font-size:11px;margin-right:6px}.foot{margin-top:28px;color:#b6bac9;font-size:12px}'
|
||||
const TREND_PAGE_CSS = 'body{font-family:-apple-system,"PingFang SC",sans-serif;color:#1f2333;max-width:720px;margin:0 auto;padding:24px 18px;line-height:1.7}h1{font-size:22px}.meta{color:#9aa0b4;font-size:12px;margin:4px 0 20px}a{color:#5b5ff8;text-decoration:none}.item{padding:14px 0;border-bottom:1px solid #eef0f6}.item h2{font-size:16px;margin:0 0 6px}.item p{margin:0;font-size:13px;color:#6b7080}.item .m{font-size:11px;color:#a6abbb;margin-top:4px}.tag{display:inline-block;background:rgba(91,95,248,.1);color:#5b5ff8;border-radius:99px;padding:1px 8px;font-size:11px;margin-right:6px}.foot{margin-top:28px;color:#b6bac9;font-size:12px}.plain{background:#f5f5ff;border-left:3px solid #5b5ff8;padding:8px 12px;margin:8px 0;font-size:14px;color:#3a3f55}.plainbox{background:#f5f5ff;border-left:3px solid #5b5ff8;padding:10px 14px;border-radius:0 8px 8px 0;margin:14px 0}.plainbox p{margin:4px 0;font-size:14px;color:#3a3f55}'
|
||||
|
||||
router.get('/trends', async (req, res) => {
|
||||
const o = origin(req)
|
||||
@@ -76,6 +76,7 @@ router.get('/trends', async (req, res) => {
|
||||
<div class="item">
|
||||
<h2><a href="${o}/trends/${t._id}">${esc(t.title)}</a></h2>
|
||||
${t.summary ? `<p>${esc(String(t.summary).slice(0, 120))}</p>` : ''}
|
||||
${t.plainExplain ? `<p class="plain"><b>人话:</b>${esc(t.plainExplain)}${t.takeAway ? ` <b>对你:</b>${esc(t.takeAway)}` : ''}</p>` : ''}
|
||||
<div class="m"><span class="tag">${esc(t.category || '资讯')}</span>${esc(t.source || '')} · ${(t.newsDate || t.createdAt || '').toString().slice(0, 10)}</div>
|
||||
</div>`).join('')
|
||||
res.type('html').send(`<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -104,6 +105,7 @@ router.get('/trends/:id', async (req, res) => {
|
||||
<h1>${esc(t.title)}</h1>
|
||||
<div class="meta">${esc(t.source || '')} · ${(t.newsDate || t.createdAt || '').toString().slice(0, 10)}</div>
|
||||
${t.summary ? `<p>${esc(t.summary)}</p>` : ''}
|
||||
${t.plainExplain ? `<div class="plainbox"><p><b>💡 一句话说清:</b>${esc(t.plainExplain)}</p>${t.takeAway ? `<p><b>🎯 对你意味着:</b>${esc(t.takeAway)}</p>` : ''}</div>` : ''}
|
||||
${t.body ? `<div>${esc(t.body).split('\n').map((p) => `<p>${p}</p>`).join('')}</div>` : ''}
|
||||
${t.sourceUrl ? `<p><a href="${esc(t.sourceUrl)}" rel="noopener nofollow" target="_blank">阅读原文 →</a></p>` : ''}
|
||||
<div class="foot"><a href="/trends">← 更多 AI 趋势</a> · © 宇之然</div>
|
||||
|
||||
@@ -189,6 +189,33 @@ async function callAI(messages, maxTokens = 1024) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 人话解读:每条趋势生成"一句话说清 + 对普通人意味着什么"(治 AI 焦虑的核心价值层) */
|
||||
async function explainItems(items) {
|
||||
const targets = items.filter(i => i.title && !i.plainExplain).slice(0, 14);
|
||||
if (targets.length === 0) return items;
|
||||
console.log(` → 生成人话解读 ${targets.length} 条...`);
|
||||
for (const item of targets) {
|
||||
const prompt = `你是"维度精灵",帮普通上班族看懂 AI 新闻。基于下面这条资讯,输出 JSON(只输出 JSON):
|
||||
{"plainExplain":"用一句大白话说清这条资讯讲的是什么,30字内,不用术语","takeAway":"这对普通人意味着什么/该注意什么,一句行动向的话,30字内"}
|
||||
|
||||
资讯标题:${item.title}
|
||||
${item.summary ? '摘要:' + String(item.summary).slice(0, 200) : ''}`;
|
||||
const out = await callAI([{ role: 'user', content: prompt }], 300);
|
||||
const m = out.match(/\{[\s\S]*?\}/);
|
||||
if (m) {
|
||||
try {
|
||||
const j = JSON.parse(m[0]);
|
||||
if (j.plainExplain) item.plainExplain = String(j.plainExplain).slice(0, 60);
|
||||
if (j.takeAway) item.takeAway = String(j.takeAway).slice(0, 60);
|
||||
} catch (e) { /* 解析失败留空,前端回退只显示标题 */ }
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
const ok = items.filter(i => i.plainExplain).length;
|
||||
console.log(` ✓ 人话解读完成 ${ok}/${targets.length}`);
|
||||
return items;
|
||||
}
|
||||
|
||||
/** 翻译英文内容到中文 */
|
||||
async function translateItems(items) {
|
||||
const needTranslate = items.filter(i => !hasChinese(i.title));
|
||||
@@ -342,6 +369,9 @@ async function main() {
|
||||
console.log(` ✓ 中文保底补充: ${need} 条`);
|
||||
}
|
||||
|
||||
// AI 人话解读(失败不阻断采集,字段留空前端自动回退)
|
||||
try { await explainItems(items); } catch (e) { console.log(' ⚠ 人话解读失败:', e.message); }
|
||||
|
||||
// 全量刷新:清空旧数据(避免残留英文 / 测试数据)
|
||||
await Trend.deleteMany({});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user