2a111d21c0
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 导师"维度精灵"人格化
302 lines
7.6 KiB
Vue
302 lines
7.6 KiB
Vue
<template>
|
||
<view class="page-dim page-layout">
|
||
<view class="status-bar-placeholder"></view>
|
||
|
||
<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 }">
|
||
<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">{{ litCount }}/{{ totalCount }} 颗星球已点亮 · {{ litPercent }}%</text>
|
||
</view>
|
||
|
||
<!-- 星球列表 -->
|
||
<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="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 litIds = ref([])
|
||
|
||
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()
|
||
const page = pages[pages.length - 1]
|
||
if (page.options?.id) {
|
||
dimId.value = parseInt(page.options.id)
|
||
}
|
||
loadKnowledge()
|
||
})
|
||
|
||
async function loadKnowledge() {
|
||
loading.value = true
|
||
try {
|
||
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,
|
||
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 = []
|
||
uni.showToast({ title: '加载失败,请下拉重试', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
function goDetail(item) {
|
||
uni.navigateTo({ url: `/pages/detail/detail?id=${item._id}` })
|
||
}
|
||
|
||
// #ifdef MP-WEIXIN
|
||
onShareAppMessage(() => ({
|
||
title: (currentDim.value?.name || 'AI 维度') + '星域 · 每颗星球都是一则 AI 知识',
|
||
path: `/pages/dimension/dimension?id=${dimId.value}`
|
||
}))
|
||
onShareTimeline(() => ({
|
||
title: (currentDim.value?.name || 'AI 维度') + '星域 · AI 维度'
|
||
}))
|
||
// #endif
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-dim {
|
||
min-height: 100vh;
|
||
padding: 0 20px 40px;
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
.status-bar-placeholder { height: 44px; }
|
||
|
||
.nav-row { padding-bottom: 12px; }
|
||
.back-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
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: 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);
|
||
}
|
||
|
||
.banner-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.dim-icon {
|
||
width: 44px; height: 44px;
|
||
border-radius: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.banner-info { flex: 1; min-width: 0; }
|
||
.dim-banner-title {
|
||
display: block;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
margin-bottom: 3px;
|
||
}
|
||
.dim-banner-desc {
|
||
font-size: 12px;
|
||
color: var(--text-tertiary);
|
||
line-height: 1.5;
|
||
}
|
||
.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: var(--text-muted);
|
||
}
|
||
.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);
|
||
}
|
||
}
|
||
|
||
.loading-state {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.bottom-spacer { height: 20px; }
|
||
</style>
|