feat: v1.3.0 差异化重构——星图点亮体系
P0 修复: - 详情页正文渲染(content 分段),71 条知识不再空白 - 问答页去登录墙:游客每日 3 次(IP 限流),登录后每日 5 次 - 修正问答配额为真正的每日重置语义 点亮闭环: - KnowledgeLit 模型 + 服务端判题(POST /:id/quiz,答案不下发) - /api/knowledge/stats 星域统计 + 点亮进度 + streak + litIds - AI 批量生成 71 条读后检测题(scripts/generate-quiz.js) 设计系统 2.0: - 70 个统一线性图标替换全部 emoji 图标 - 主题 token mixin 化,修复 H5 暗色级联 bug,MP 跟随系统(darkmode) - 首页星域导航(进度环)、我的页学习者档案、星域列表页重写 - 硬编码色清理,误植 map 页移除 服务端加固: - trust proxy 真实 IP、错误信息脱敏、ask 独立限流、无效 ObjectId 404 - seo.js ESM→CJS(修复容器启动)、AI 导师"维度精灵"人格化
This commit is contained in:
@@ -2,72 +2,96 @@
|
||||
<view class="page-dim page-layout">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<view class="back-btn" @tap="goBack">
|
||||
<text>← 返回</text>
|
||||
<view class="nav-row">
|
||||
<view class="back-btn" @tap="goBack">
|
||||
<AppIcon name="back-gray" :size="16" />
|
||||
<text>返回</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度横幅 -->
|
||||
<!-- 星域横幅 -->
|
||||
<view class="dim-banner" :style="{ borderColor: currentDim?.border }">
|
||||
<text class="dim-banner-icon">{{ currentDim?.icon }}</text>
|
||||
<text class="dim-banner-title">{{ currentDim?.name }}</text>
|
||||
<text class="dim-banner-desc">{{ bannerDesc }}</text>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
|
||||
<view class="banner-top">
|
||||
<view class="dim-icon" :style="{ background: currentDim?.iconBg }">
|
||||
<AppIcon :name="currentDim?.icon || 'planet'" :size="20" />
|
||||
</view>
|
||||
<view class="banner-info">
|
||||
<text class="dim-banner-title">{{ currentDim?.name }}</text>
|
||||
<text class="dim-banner-desc">{{ currentDim?.tagline }}</text>
|
||||
</view>
|
||||
<ProgressRing
|
||||
:size="46"
|
||||
:num-size="10"
|
||||
:percent="litPercent"
|
||||
:color="currentDim?.color"
|
||||
/>
|
||||
</view>
|
||||
<text class="progress-label">探索进度 {{ progress }}% · 已完成 {{ readCount }}/{{ totalCount }} 篇</text>
|
||||
<text class="progress-label">{{ litCount }}/{{ totalCount }} 颗星球已点亮 · {{ litPercent }}%</text>
|
||||
</view>
|
||||
|
||||
<!-- 知识列表 -->
|
||||
<view v-for="item in knowledgeList" :key="item._id" class="knowledge-item" @tap="goDetail(item)">
|
||||
<view class="k-icon" :style="getItemStyle(item)">
|
||||
<text>{{ item.icon || '📄' }}</text>
|
||||
<!-- 星球列表 -->
|
||||
<view
|
||||
v-for="item in knowledgeList"
|
||||
:key="item._id"
|
||||
class="planet-item"
|
||||
@tap="goDetail(item)"
|
||||
>
|
||||
<view class="p-icon" :class="{ lit: item.isLit }" :style="item.isLit ? litStyle : null">
|
||||
<AppIcon :name="item.isLit ? 'star-fill-white' : 'star-gray'" :size="16" />
|
||||
</view>
|
||||
<view class="k-info">
|
||||
<text class="k-title">{{ item.title }}</text>
|
||||
<text class="k-desc">{{ item.desc }}</text>
|
||||
<view class="k-meta">
|
||||
<text>📖 {{ item.readTime || 5 }} 分钟</text>
|
||||
<text v-if="item.collected">⭐ 已收藏</text>
|
||||
<text v-else>🔖 {{ item.collectCount || 0 }} 人收藏</text>
|
||||
<text class="k-badge" :class="item.isPremium ? 'pro' : 'free'">
|
||||
{{ item.isPremium ? 'Pro' : '免费' }}
|
||||
</text>
|
||||
<view class="p-info">
|
||||
<text class="p-title">{{ item.title }}</text>
|
||||
<text class="p-desc">{{ item.desc }}</text>
|
||||
<view class="p-meta">
|
||||
<view class="p-lit" v-if="item.isLit" :style="{ color: currentDim?.color, background: currentDim?.bg }">已点亮</view>
|
||||
<text class="p-read">{{ item.readTime }} 分钟</text>
|
||||
<text class="p-quiz" v-if="item.hasQuiz">含检测题</text>
|
||||
</view>
|
||||
</view>
|
||||
<AppIcon name="arrow-gray" :size="13" />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view v-if="!loading && !knowledgeList.length" class="empty-state">
|
||||
<AppIcon name="planet-gray" :size="40" />
|
||||
<text class="empty-title">这片星域还没有星球</text>
|
||||
<text class="empty-desc">内容正在扩充中,先去别的星域逛逛</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getDimension } from '@/utils/dimensions'
|
||||
import { knowledgeApi } from '@/utils/api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AppIcon from '@/components/AppIcon.vue'
|
||||
import ProgressRing from '@/components/ProgressRing.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const dimId = ref(1)
|
||||
const currentDim = computed(() => getDimension(dimId.value))
|
||||
const knowledgeList = ref([])
|
||||
const loading = ref(false)
|
||||
const progress = ref(35)
|
||||
const readCount = ref(4)
|
||||
const totalCount = ref(12)
|
||||
const litIds = ref([])
|
||||
|
||||
const bannerDesc = computed(() => {
|
||||
const descs = {
|
||||
1: '从 1950 年图灵测试到 2026 年通用人工智能,追溯 70 年演进之路',
|
||||
2: '从深度学习到 Transformer,从 MoE 到多模态,理解每一次技术跃迁',
|
||||
3: '大模型格局、Agent 生态、具身智能——2026 年 AI 世界全景图',
|
||||
4: '提示词工程、RAG 系统、模型微调、工具链——从入门到上手',
|
||||
5: '每日追踪 AI 前沿动态,不错过每一个重要时刻'
|
||||
}
|
||||
return descs[dimId.value] || '探索人工智能的精彩世界'
|
||||
})
|
||||
const totalCount = computed(() => knowledgeList.value.length)
|
||||
const litCount = computed(() => knowledgeList.value.filter(i => i.isLit).length)
|
||||
const litPercent = computed(() =>
|
||||
totalCount.value ? Math.round((litCount.value / totalCount.value) * 100) : 0
|
||||
)
|
||||
|
||||
const litStyle = computed(() => ({
|
||||
background: currentDim.value?.iconBg,
|
||||
borderColor: currentDim.value?.border
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
@@ -81,97 +105,42 @@ onMounted(() => {
|
||||
async function loadKnowledge() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await knowledgeApi.list({ dim: dimId.value })
|
||||
const [data, stats] = await Promise.all([
|
||||
knowledgeApi.list({ dim: dimId.value, limit: 100 }),
|
||||
knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '').catch(() => null)
|
||||
])
|
||||
litIds.value = stats?.user?.litIds || []
|
||||
|
||||
knowledgeList.value = (data.items || []).map(item => ({
|
||||
...item,
|
||||
icon: getDimIcon(item.dim),
|
||||
desc: item.content?.substring(0, 40) + '...' || '',
|
||||
readTime: Math.ceil((item.content?.length || 300) / 200),
|
||||
isPremium: false,
|
||||
collected: false,
|
||||
collectCount: item.collectionCount
|
||||
desc: item.content ? item.content.substring(0, 42) + '...' : '',
|
||||
readTime: Math.max(1, Math.ceil((item.content?.length || 300) / 300)),
|
||||
hasQuiz: !!(item.question && (item.options || []).length),
|
||||
isLit: litIds.value.includes(String(item._id))
|
||||
}))
|
||||
} catch (e) {
|
||||
// 用示例数据填充
|
||||
knowledgeList.value = getSampleData(dimId.value)
|
||||
knowledgeList.value = []
|
||||
uni.showToast({ title: '加载失败,请下拉重试', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getItemStyle(item) {
|
||||
const colors = [
|
||||
{ bg: 'rgba(124,77,255,0.1)', color: '#b388ff' },
|
||||
{ bg: 'rgba(0,188,212,0.1)', color: '#4dd0e1' },
|
||||
{ bg: 'rgba(0,200,150,0.1)', color: '#4db6ac' },
|
||||
{ bg: 'rgba(255,193,7,0.1)', color: '#ffd54f' },
|
||||
{ bg: 'rgba(239,83,80,0.1)', color: '#ef5350' }
|
||||
]
|
||||
const c = colors[(item.dim || 1) - 1] || colors[0]
|
||||
return {
|
||||
background: c.bg,
|
||||
color: c.color,
|
||||
borderColor: c.bg
|
||||
}
|
||||
}
|
||||
|
||||
function getDimIcon(dim) {
|
||||
const icons = ['📜', '🏛', '♟', '🧠', '⚡']
|
||||
return icons[(dim || 1) - 1]
|
||||
}
|
||||
|
||||
function getSampleData(dim) {
|
||||
const samples = {
|
||||
1: [
|
||||
{ title: '图灵测试:人工智能的哲学起点', content: '1950 年,艾伦·图灵提出"机器能思考吗?"' },
|
||||
{ title: '达特茅斯会议:AI 的诞生时刻', content: '1956 年,一群科学家正式命名了"人工智能"' },
|
||||
{ title: '深蓝 vs 卡斯帕罗夫', content: '1997 年,IBM 深蓝首次击败国际象棋世界冠军' },
|
||||
],
|
||||
2: [
|
||||
{ title: '深度学习革命', content: '从神经网络到深度学习的演进之路' },
|
||||
{ title: 'Transformer 架构详解', content: '2017 年 Google 提出的革命性架构' },
|
||||
],
|
||||
3: [
|
||||
{ title: '2026 年大模型格局', content: 'GPT、Claude、Gemini、DeepSeek 全面对比' },
|
||||
{ title: 'Agent 智能体生态', content: '从 Chatbot 到自主 Agent 的进化' },
|
||||
],
|
||||
4: [
|
||||
{ title: '提示词工程入门', content: '如何写出高质量的 prompt' },
|
||||
{ title: 'RAG 系统搭建指南', content: '检索增强生成从零到一' },
|
||||
],
|
||||
5: [
|
||||
{ title: 'DeepSeek 自研推理芯片', content: '降低对英伟达依赖的重要一步' },
|
||||
{ title: 'WAIC 2026 即将开幕', content: '7 月 17 日上海,1100+ 企业参展' },
|
||||
]
|
||||
}
|
||||
const data = samples[dim] || samples[1]
|
||||
return data.map((item, i) => ({
|
||||
...item,
|
||||
_id: `sample-${dim}-${i}`,
|
||||
dim,
|
||||
icon: getDimIcon(dim),
|
||||
readTime: 5 + i * 2,
|
||||
isPremium: i >= 3,
|
||||
collected: i === 0,
|
||||
collectionCount: 10 - i * 2
|
||||
}))
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/detail?id=${item._id}`
|
||||
})
|
||||
uni.navigateTo({ url: `/pages/detail/detail?id=${item._id}` })
|
||||
}
|
||||
|
||||
// 微信小程序分享
|
||||
// #ifdef MP-WEIXIN
|
||||
onShareAppMessage(() => ({
|
||||
title: 'AI 维度 - 你的AI知识助手',
|
||||
path: '/pages/index/index'
|
||||
title: (currentDim.value?.name || 'AI 维度') + '星域 · 每颗星球都是一则 AI 知识',
|
||||
path: `/pages/dimension/dimension?id=${dimId.value}`
|
||||
}))
|
||||
onShareTimeline(() => ({
|
||||
title: (currentDim.value?.name || 'AI 维度') + '星域 · AI 维度'
|
||||
}))
|
||||
// #endif
|
||||
</script>
|
||||
@@ -185,107 +154,148 @@ onShareAppMessage(() => ({
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.nav-row { padding-bottom: 12px; }
|
||||
.back-btn {
|
||||
color: rgba(255,255,255,0.4);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 0 14px;
|
||||
gap: 4px;
|
||||
padding: 8px 14px 8px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dim-banner {
|
||||
border-radius: 18px;
|
||||
padding: 24px 20px;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.1), rgba(0,0,0,0.3));
|
||||
border: 1px solid rgba(124,77,255,0.12);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dim-banner-icon { font-size: 28px; display: block; margin-bottom: 8px; }
|
||||
.dim-banner-title { font-size: 22px; color: var(--text-primary); display: block; margin-bottom: 6px; }
|
||||
.dim-banner-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 18px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--glass-bg);
|
||||
border: 1.5px solid var(--glass-border);
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 3px;
|
||||
background: var(--glass-bg);
|
||||
border-radius: 2px;
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #b388ff, #7c4dff);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.progress-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.2);
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.knowledge-item {
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
.banner-top {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.k-icon {
|
||||
.dim-icon {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
border: 1px solid var(--glass-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.k-info { flex: 1; }
|
||||
.k-title {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
font-weight: 500;
|
||||
.banner-info { flex: 1; min-width: 0; }
|
||||
.dim-banner-title {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.k-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
.dim-banner-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.k-meta {
|
||||
.progress-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.planet-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--card-shadow, none);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.planet-item:active { transform: scale(0.98); }
|
||||
|
||||
.p-icon {
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 13px;
|
||||
border: 1.5px dashed var(--glass-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.lit { border-style: solid; }
|
||||
}
|
||||
|
||||
.p-info { flex: 1; min-width: 0; }
|
||||
.p-title {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.p-desc {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.p-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.15);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.k-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
.p-lit {
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
.p-quiz {
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50px 0 30px;
|
||||
gap: 8px;
|
||||
|
||||
.empty-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
.empty-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
.k-badge.free { background: rgba(76,175,80,0.12); color: #66bb6a; }
|
||||
.k-badge.pro { background: rgba(255,152,0,0.12); color: #ffa726; }
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: rgba(255,255,255,0.2);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.bottom-spacer { height: 20px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user