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:
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="header">
|
||||
<text class="title">🏆 成就</text>
|
||||
<text class="title">成就</text>
|
||||
<text class="unlock-count">{{ unlockedCount }}/{{ totalCount }}</text>
|
||||
</view>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else-if="!loading">
|
||||
<text class="empty-icon">🏆</text>
|
||||
<text class="empty-icon">暂无成就</text>
|
||||
<text class="empty-text">还没有获得任何成就</text>
|
||||
<text class="empty-sub">继续学习,解锁属于你的成就吧</text>
|
||||
</view>
|
||||
@@ -88,9 +88,9 @@ const loading = ref(true)
|
||||
|
||||
// 成就分类的默认配置
|
||||
const CATEGORY_CONFIG = [
|
||||
{ name: '知识探索', icon: '📖', color: 'var(--dim1-color)' },
|
||||
{ name: '收藏成就', icon: '⭐', color: 'var(--dim2-color)' },
|
||||
{ name: 'AI 问答', icon: '🤖', color: 'var(--dim3-color)' },
|
||||
{ name: '知识探索', icon: '星', color: 'var(--dim1-color)' },
|
||||
{ name: '收藏成就', icon: '藏', color: 'var(--dim2-color)' },
|
||||
{ name: 'AI 问答', icon: '问', color: 'var(--dim3-color)' },
|
||||
{ name: '社交成就', icon: '👥', color: 'var(--dim4-color)' }
|
||||
]
|
||||
|
||||
@@ -141,7 +141,7 @@ async function loadAchievements() {
|
||||
|
||||
function catIcon(name) {
|
||||
const config = CATEGORY_CONFIG.find((c) => c.name === name)
|
||||
return config ? config.icon : '🏆'
|
||||
return config ? config.icon : '成'
|
||||
}
|
||||
|
||||
function achColor(icon) {
|
||||
|
||||
+242
-234
@@ -2,28 +2,29 @@
|
||||
<view class="page-chat page-layout">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 聊天头部 -->
|
||||
<!-- 聊天头部:维度精灵 -->
|
||||
<view class="chat-header">
|
||||
<view class="chat-avatar">
|
||||
<text class="avatar-text">∞</text>
|
||||
<AppIcon name="planet-white" :size="22" />
|
||||
</view>
|
||||
<view class="chat-info">
|
||||
<text class="chat-name">AI 助手</text>
|
||||
<text class="chat-status">随时为你解答</text>
|
||||
<text class="chat-name">维度精灵</text>
|
||||
<text class="chat-status">你的 AI 知识探索向导</text>
|
||||
</view>
|
||||
<!-- 配额徽章(登录后显示) -->
|
||||
<view class="quota-badge" v-if="userStore.isLoggedIn" @click="goShare">
|
||||
<text class="quota-text">{{ quotaDisplay }}</text>
|
||||
<text class="quota-icon">🎁</text>
|
||||
<view class="quota-badge" @click="onQuotaTap">
|
||||
<AppIcon :name="isGuest ? 'gift' : 'gift-gray'" :size="13" />
|
||||
<text class="quota-text">{{ quotaText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表(始终可见) -->
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view class="msg-list" scroll-y :scroll-into-view="scrollTo">
|
||||
<!-- 欢迎消息 -->
|
||||
<!-- 欢迎 -->
|
||||
<view class="welcome-card">
|
||||
<text class="welcome-icon">💡</text>
|
||||
<text class="welcome-text">试试问我这些问题:</text>
|
||||
<view class="welcome-head">
|
||||
<AppIcon name="sparkle" :size="16" />
|
||||
<text class="welcome-text">{{ welcomeText }}</text>
|
||||
</view>
|
||||
<view class="suggest-list">
|
||||
<view class="suggest-item" @click="sendSuggest(item)" v-for="(item, idx) in suggestions" :key="idx">
|
||||
<text>{{ item }}</text>
|
||||
@@ -32,7 +33,9 @@
|
||||
</view>
|
||||
|
||||
<view v-for="(msg, idx) in messages" :key="idx" :id="'msg-' + idx" class="chat-msg" :class="msg.role">
|
||||
<view v-if="msg.role === 'assistant'" class="mini-avatar">∞</view>
|
||||
<view v-if="msg.role === 'assistant'" class="mini-avatar">
|
||||
<AppIcon name="planet-white" :size="14" />
|
||||
</view>
|
||||
<view class="msg-content">
|
||||
<view class="bubble" v-html="msg.content"></view>
|
||||
</view>
|
||||
@@ -40,7 +43,9 @@
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<view v-if="loading" class="chat-msg assistant">
|
||||
<view class="mini-avatar">∞</view>
|
||||
<view class="mini-avatar">
|
||||
<AppIcon name="planet-white" :size="14" />
|
||||
</view>
|
||||
<view class="msg-content">
|
||||
<view class="bubble loading-dots">
|
||||
<text class="dot"></text>
|
||||
@@ -49,40 +54,42 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区(始终可见,未登录时提示登录) -->
|
||||
<!-- 输入区:游客与登录用户一致,始终可见 -->
|
||||
<view class="chat-input-area">
|
||||
<view class="chat-input" v-if="userStore.isLoggedIn">
|
||||
<view v-if="guestExhausted" class="login-guide" @click="goLogin">
|
||||
<text class="guide-text">{{ guestExhaustedText }}</text>
|
||||
<view class="guide-btn">
|
||||
<text>登录后每天 5 次</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chat-input" v-else>
|
||||
<input
|
||||
v-model="question"
|
||||
class="input-field"
|
||||
placeholder="输入你对 AI 的疑问..."
|
||||
:placeholder="inputPlaceholder"
|
||||
:disabled="loading"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
/>
|
||||
<view class="send-btn" @click="sendMessage">
|
||||
<text>➤</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 未登录:显示登录引导 -->
|
||||
<view class="login-input-placeholder" v-else @click="goLogin">
|
||||
<text class="login-input-text">登录后即可体验 AI 问答</text>
|
||||
<view class="login-input-btn">
|
||||
<text>去登录 →</text>
|
||||
<view class="send-btn" :class="{ disabled: loading || !question.trim() }" @click="sendMessage">
|
||||
<AppIcon name="send-white" :size="16" />
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="isGuest && !guestExhausted" class="guest-hint">游客模式 · 每日 3 次体验</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { aiChatApi } from '@/utils/api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AppIcon from '@/components/AppIcon.vue'
|
||||
|
||||
const question = ref('')
|
||||
const messages = ref([])
|
||||
@@ -90,35 +97,44 @@ const loading = ref(false)
|
||||
const scrollTo = ref('')
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 建议问题
|
||||
const quotaInfo = ref(null) // { remainingQuota, freeQuota, isGuest }
|
||||
const guestExhausted = ref(false)
|
||||
|
||||
const WELCOME_LOGGED = '你好呀,我是维度精灵。任何 AI 概念都能问我,我用大白话讲明白。'
|
||||
const WELCOME_GUEST = '嗨,我是维度精灵。游客身份也能提问,试试下面的问题。'
|
||||
const GUEST_EXHAUSTED = '今日 3 次游客体验已用完,登录后每天可问 5 次,还能点亮知识星球。'
|
||||
const INPUT_LOGGED = '输入你对 AI 的疑问...'
|
||||
const INPUT_GUEST = '游客模式,直接提问试试...'
|
||||
|
||||
const suggestions = ref([
|
||||
'"Transformer 的核心原理是什么?"',
|
||||
'"RAG 和微调有什么区别?"',
|
||||
'"2026 年最值得关注的 AI 趋势?"'
|
||||
'用大白话解释 Transformer 是什么',
|
||||
'RAG 和微调有什么区别?',
|
||||
'2026 年最值得关注的 AI 趋势?'
|
||||
])
|
||||
|
||||
// 配额显示
|
||||
const quotaDisplay = computed(() => {
|
||||
const q = userStore.quota
|
||||
if (!q) return '?次'
|
||||
const used = q.usedQuota || q.dailyUsed || 0
|
||||
const remaining = q.remainingQuota || 0
|
||||
const total = q.freeQuota || remaining + used
|
||||
return `${remaining}/${total}`
|
||||
const isGuest = computed(() => !userStore.isLoggedIn)
|
||||
const welcomeText = computed(() => (isGuest.value ? WELCOME_GUEST : WELCOME_LOGGED))
|
||||
const inputPlaceholder = computed(() => (isGuest.value ? INPUT_GUEST : INPUT_LOGGED))
|
||||
const guestExhaustedText = GUEST_EXHAUSTED
|
||||
|
||||
const quotaText = computed(() => {
|
||||
const q = quotaInfo.value
|
||||
if (!q) return '...'
|
||||
return `${q.remainingQuota} 次`
|
||||
})
|
||||
|
||||
// 监听登录状态变化,加载配额
|
||||
const isLoggedIn = computed(() => userStore.isLoggedIn)
|
||||
import { watch } from 'vue'
|
||||
watch(isLoggedIn, (val) => {
|
||||
if (val) {
|
||||
loadQuota()
|
||||
}
|
||||
guestExhausted.value = false
|
||||
loadQuota()
|
||||
}, { immediate: true })
|
||||
|
||||
async function loadQuota() {
|
||||
if (userStore.token) {
|
||||
await userStore.fetchQuota(userStore.token)
|
||||
try {
|
||||
const data = await aiChatApi.quota(userStore.isLoggedIn ? userStore.token : undefined)
|
||||
quotaInfo.value = data
|
||||
} catch (e) {
|
||||
quotaInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,28 +142,27 @@ async function sendMessage() {
|
||||
const q = question.value.trim()
|
||||
if (!q || loading.value) return
|
||||
|
||||
// 未登录 → 跳登录
|
||||
if (!userStore.isLoggedIn) {
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
messages.value.push({ role: 'user', content: escapeHtml(q) })
|
||||
question.value = ''
|
||||
scrollToLast()
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await aiChatApi.ask(q, 'general', userStore.token)
|
||||
const data = await aiChatApi.ask(q, 'general', userStore.isLoggedIn ? userStore.token : undefined)
|
||||
messages.value.push({ role: 'assistant', content: escapeHtml(data.answer) })
|
||||
await userStore.fetchQuota(userStore.token)
|
||||
if (quotaInfo.value) {
|
||||
quotaInfo.value.remainingQuota = data.remainingQuota
|
||||
}
|
||||
if (isGuest.value && data.remainingQuota <= 0) {
|
||||
guestExhausted.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AI 问答失败:', e)
|
||||
const errorMsg = e.message || 'AI 服务暂时不可用,请稍后再试'
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: escapeHtml(`抱歉,${errorMsg}`)
|
||||
})
|
||||
const msg = e.message || 'AI 服务暂时不可用,请稍后再试'
|
||||
if (msg.includes('游客体验已用完')) {
|
||||
guestExhausted.value = true
|
||||
} else {
|
||||
messages.value.push({ role: 'assistant', content: escapeHtml(`抱歉,${msg}`) })
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
scrollToLast()
|
||||
@@ -161,8 +176,9 @@ function sendSuggest(text) {
|
||||
|
||||
function scrollToLast() {
|
||||
setTimeout(() => {
|
||||
scrollTo.value = 'msg-' + (messages.value.length - 1)
|
||||
}, 100)
|
||||
scrollTo.value = ''
|
||||
setTimeout(() => { scrollTo.value = 'msg-' + (messages.value.length - 1) }, 50)
|
||||
}, 80)
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -177,33 +193,43 @@ function goLogin() {
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
}
|
||||
|
||||
function goShare() {
|
||||
function onQuotaTap() {
|
||||
if (isGuest.value) {
|
||||
uni.showModal({
|
||||
title: '游客体验',
|
||||
content: '每天可免费提问 3 次;登录后每天 5 次,答对检测题还能点亮知识星球',
|
||||
confirmText: '去登录',
|
||||
cancelText: '继续体验',
|
||||
success: (res) => {
|
||||
if (res.confirm) goLogin()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '获取更多次数',
|
||||
content: '将小程序分享给好友,即可获得 5 次额外问答机会',
|
||||
confirmText: '去分享',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 微信小程序分享能力
|
||||
if (res.confirm && userStore.token) {
|
||||
uni.showShareMenu({ withShareTicket: true })
|
||||
// 调用分享获得次数接口
|
||||
if (userStore.token) {
|
||||
aiChatApi.shareGain(userStore.token).then(() => {
|
||||
uni.showToast({ title: '已记录分享', icon: 'success' })
|
||||
loadQuota()
|
||||
}).catch(() => {})
|
||||
}
|
||||
aiChatApi.shareGain(userStore.token).then(() => {
|
||||
uni.showToast({ title: '已记录分享', icon: 'success' })
|
||||
loadQuota()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 微信小程序右上角三点分享
|
||||
// #ifdef MP-WEIXIN
|
||||
onShareAppMessage(() => ({
|
||||
title: 'AI 维度 - AI 问答',
|
||||
title: '维度精灵 · 你的 AI 知识探索向导',
|
||||
path: '/pages/chat/chat'
|
||||
}))
|
||||
onShareTimeline(() => ({
|
||||
title: '维度精灵 · 你的 AI 知识探索向导'
|
||||
}))
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
@@ -212,7 +238,7 @@ onShareAppMessage(() => ({
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px;
|
||||
padding: 0 16px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
@@ -222,248 +248,230 @@ onShareAppMessage(() => ({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0 16px;
|
||||
padding: 10px 0 14px;
|
||||
}
|
||||
|
||||
.chat-avatar,
|
||||
.mini-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, rgba(124, 77, 255, 0.3), rgba(0, 188, 212, 0.2));
|
||||
border: 1px solid rgba(124, 77, 255, 0.15);
|
||||
background: linear-gradient(135deg, var(--color-brand), #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 14px var(--color-brand-glow);
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mini-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.chat-avatar { width: 42px; height: 42px; }
|
||||
.mini-avatar { width: 28px; height: 28px; border-radius: 9px; }
|
||||
|
||||
.chat-info { flex: 1; }
|
||||
|
||||
.chat-name {
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-status {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 配额徽章
|
||||
.quota-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(124, 77, 255, 0.1);
|
||||
border: 1px solid rgba(124, 77, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-brand-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
|
||||
.quota-text {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.quota-text {
|
||||
font-size: 12px;
|
||||
color: var(--dim1-color);
|
||||
font-weight: 600;
|
||||
.msg-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.quota-icon { font-size: 14px; }
|
||||
.list-bottom { height: 16px; }
|
||||
|
||||
// 未登录输入区占位
|
||||
.login-input-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px dashed var(--glass-border);
|
||||
border-radius: 14px;
|
||||
margin-bottom: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-input-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.login-input-btn {
|
||||
padding: 6px 16px;
|
||||
background: var(--color-brand);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.login-input-btn text {
|
||||
font-size: 13px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 欢迎卡片
|
||||
.welcome-card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
.welcome-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.welcome-icon {
|
||||
font-size: 20px;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
.welcome-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.suggest-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.suggest-item {
|
||||
padding: 8px 12px;
|
||||
background: rgba(124, 77, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
padding: 11px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--glass-border);
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
// 消息列表
|
||||
.msg-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.suggest-item:active { border-color: var(--color-brand); color: var(--color-brand); }
|
||||
|
||||
.chat-msg {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.chat-msg.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
&.assistant .msg-content { align-items: flex-start; }
|
||||
|
||||
.chat-msg.assistant {
|
||||
flex-direction: row;
|
||||
}
|
||||
.msg-content {
|
||||
max-width: 82%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
max-width: 82%;
|
||||
&.user {
|
||||
justify-content: flex-end;
|
||||
|
||||
.msg-content { align-items: flex-end; }
|
||||
.bubble {
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
color: #fff;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.assistant .bubble {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: var(--text-primary);
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.7;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chat-msg.assistant .bubble {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: var(--text-primary);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-msg.user .bubble {
|
||||
background: linear-gradient(135deg, rgba(124, 77, 255, 0.3), rgba(124, 77, 255, 0.15));
|
||||
border: 1px solid rgba(124, 77, 255, 0.15);
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 14px 18px;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 16px 14px;
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-brand);
|
||||
animation: dotBounce 1.2s infinite ease-in-out;
|
||||
|
||||
&:nth-child(2) { animation-delay: 0.15s; }
|
||||
&:nth-child(3) { animation-delay: 0.3s; }
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
animation: blink 1.4s infinite;
|
||||
@keyframes dotBounce {
|
||||
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
||||
30% { transform: translateY(-5px); opacity: 1; }
|
||||
}
|
||||
|
||||
.dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes blink {
|
||||
0%, 20% { opacity: 0; transform: scale(0.6); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(0.6); }
|
||||
}
|
||||
|
||||
// 输入区
|
||||
/* 输入区 */
|
||||
.chat-input-area {
|
||||
padding: 12px 0 30px;
|
||||
background: var(--bg-primary);
|
||||
flex-shrink: 0;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
|
||||
.guest-hint {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 14px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 6px 6px 6px 18px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
height: 38px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field::placeholder {
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7c4dff, #b388ff);
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-shadow: 0 0 20px rgba(124, 77, 255, 0.2);
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&.disabled { opacity: 0.45; }
|
||||
}
|
||||
|
||||
.send-btn text {
|
||||
font-size: 16px;
|
||||
/* 用完额度引导 */
|
||||
.login-guide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(135deg, var(--color-brand-bg), rgba(139, 92, 246, 0.06));
|
||||
border: 1px solid var(--glass-hover-border);
|
||||
|
||||
.guide-text {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.guide-btn {
|
||||
padding: 8px 14px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+374
-277
@@ -1,29 +1,35 @@
|
||||
<template>
|
||||
<view class="detail-page page-layout">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">‹</view>
|
||||
<view class="nav-back" @click="goBack">
|
||||
<AppIcon name="back-gray" :size="18" />
|
||||
</view>
|
||||
<text class="nav-title">AI 知识</text>
|
||||
<view class="nav-collect" @click="toggleCollect">
|
||||
<text v-if="isCollected" class="icon icon-collected">★</text>
|
||||
<text v-else class="icon">☆</text>
|
||||
<AppIcon v-if="isCollected" name="star-fill" :size="20" />
|
||||
<AppIcon v-else name="star-gray" :size="20" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 知识头部 -->
|
||||
<view class="detail-header">
|
||||
<view class="dim-tag" :style="{ background: dimColor }">
|
||||
<text class="dim-name">{{ dimensionName }}</text>
|
||||
<view class="dim-tag" :style="{ background: dimInfo.bg, color: dimInfo.color }">
|
||||
{{ dimInfo.name }}
|
||||
</view>
|
||||
<text class="detail-title">{{ detail?.title || '加载中...' }}</text>
|
||||
<view class="meta-row">
|
||||
<text class="meta-item">📖 {{ viewCountText }}</text>
|
||||
<text class="meta-item">⭐ {{ collectionCount }}</text>
|
||||
<text class="meta-item" v-if="detail?.category">
|
||||
{{ categoryLabel }}
|
||||
</text>
|
||||
<view class="meta-item">
|
||||
<AppIcon name="search-gray" :size="12" />
|
||||
<text>{{ viewCountText }} 次阅读</text>
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<AppIcon name="star-gray" :size="12" />
|
||||
<text>{{ collectionCount }} 人收藏</text>
|
||||
</view>
|
||||
<text class="meta-item" v-if="readMinutes">{{ readMinutes }} 分钟读完</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -32,85 +38,140 @@
|
||||
<text class="tag" v-for="tag in detail.tags" :key="tag">#{{ tag }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content-scroll" scroll-y>
|
||||
<!-- 分段内容(如果存在 sections) -->
|
||||
<view class="content-section" v-if="detail?.sections?.length">
|
||||
<view class="section-block" v-for="(section, idx) in detail.sections" :key="idx">
|
||||
<text class="section-title">{{ section.title }}</text>
|
||||
<text class="section-content">{{ section.content }}</text>
|
||||
<view class="section-images" v-if="section.images?.length">
|
||||
<image
|
||||
class="section-img"
|
||||
v-for="(img, i) in section.images"
|
||||
:key="i"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 正文 -->
|
||||
<view class="content-card">
|
||||
<view v-if="!detail" class="loading-block">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 普通内容 -->
|
||||
<view class="content-block" v-else>
|
||||
<text class="content-text">{{ detail?.content || '暂无内容' }}</text>
|
||||
<template v-else-if="paragraphs.length">
|
||||
<text
|
||||
v-for="(p, i) in paragraphs"
|
||||
:key="i"
|
||||
class="para"
|
||||
:class="{ 'para-lead': i === 0 }"
|
||||
>{{ p }}</text>
|
||||
</template>
|
||||
<view v-else class="loading-block">
|
||||
<text>暂无内容</text>
|
||||
</view>
|
||||
|
||||
<!-- 问答区域 -->
|
||||
<view class="qa-section" v-if="detail?.category === 'question' && detail?.question">
|
||||
<view class="qa-question">
|
||||
<text class="qa-label">问题</text>
|
||||
<text class="qa-text">{{ detail.question }}</text>
|
||||
</view>
|
||||
<view class="qa-options" v-if="detail?.options?.length">
|
||||
<view
|
||||
class="qa-option"
|
||||
v-for="(opt, idx) in detail.options"
|
||||
:key="idx"
|
||||
:class="{ correct: selectedOption === idx && opt.isCorrect }"
|
||||
@click="selectedOption = idx"
|
||||
>
|
||||
<text class="option-letter">{{ String.fromCharCode(65 + idx) }}</text>
|
||||
<text class="option-text">{{ opt.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="qa-answer" v-if="selectedOption !== null">
|
||||
<text class="answer-label">
|
||||
{{ detail.options?.[selectedOption]?.isCorrect ? '✅ 正确' : '❌ 错误' }}
|
||||
</text>
|
||||
<text class="answer-text" v-if="detail.answer">{{ detail.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部 AI 问答入口 -->
|
||||
<view class="ai-banner" @click="goToAI">
|
||||
<text class="ai-icon">🤖</text>
|
||||
<text class="ai-text">还有疑问?问问 AI 助手</text>
|
||||
<text class="ai-arrow">→</text>
|
||||
</view>
|
||||
|
||||
<!-- 检测题(读后测,答对点亮星球) -->
|
||||
<view class="quiz-card" v-if="detail?.question && detail?.options?.length">
|
||||
<view class="quiz-head">
|
||||
<AppIcon name="sparkle" :size="16" />
|
||||
<text class="quiz-title">读后检测 · 答对点亮这颗星球</text>
|
||||
</view>
|
||||
<text class="quiz-question">{{ detail.question }}</text>
|
||||
|
||||
<template v-if="!quizResult">
|
||||
<view
|
||||
v-for="(opt, idx) in detail.options"
|
||||
:key="idx"
|
||||
class="quiz-option"
|
||||
:class="{ selected: selectedOption === idx }"
|
||||
@click="selectedOption = idx"
|
||||
>
|
||||
<text class="option-letter">{{ optionLetters[idx] }}</text>
|
||||
<text class="option-text">{{ opt.text }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="quiz-submit"
|
||||
:class="{ disabled: selectedOption === null }"
|
||||
@click="submitQuiz"
|
||||
>
|
||||
<text>{{ selectedOption === null ? '选择你的答案' : '确认作答' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="quiz-result" :class="quizResult.correct ? 'ok' : 'bad'">
|
||||
<view class="result-icon">
|
||||
<AppIcon :name="quizResult.correct ? 'check-green' : 'lock-gray'" :size="18" />
|
||||
</view>
|
||||
<view class="result-text">
|
||||
<text class="result-title">{{ quizResult.correct ? resultOkTitle : resultBadTitle }}</text>
|
||||
<text class="result-desc" v-if="quizResult.answer">{{ quizResult.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="quiz-actions">
|
||||
<view v-if="!quizResult.correct" class="quiz-retry" @click="retryQuiz">
|
||||
<text>再试一次</text>
|
||||
</view>
|
||||
<view class="quiz-next" @click="goChat">
|
||||
<text>还有疑问?问维度精灵</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 底部 AI 导师入口 -->
|
||||
<view class="ai-banner" @click="goChat">
|
||||
<AppIcon name="bulb-white" :size="16" />
|
||||
<text class="ai-text">读完想深入聊聊?问维度精灵</text>
|
||||
<AppIcon name="arrow-white" :size="12" />
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { knowledgeApi } from '@/utils/api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { DIMENSIONS } from '@/utils/dimensions'
|
||||
import AppIcon from '@/components/AppIcon.vue'
|
||||
|
||||
const RESULT_OK_TITLE = '回答正确,星球已点亮'
|
||||
const RESULT_OK_GUEST = '回答正确!登录后可永久记录点亮'
|
||||
const RESULT_BAD_TITLE = '差一点点,再想想'
|
||||
|
||||
const detailId = ref('')
|
||||
const detail = ref(null)
|
||||
const isCollected = ref(false)
|
||||
const selectedOption = ref(null)
|
||||
const quizResult = ref(null)
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 从路由参数获取 ID
|
||||
const optionLetters = ['A', 'B', 'C', 'D', 'E']
|
||||
|
||||
// 正文按段落渲染(兼容全部存量数据:content 字符串)
|
||||
const paragraphs = computed(() => {
|
||||
const raw = detail.value?.content
|
||||
if (!raw) return []
|
||||
return String(raw)
|
||||
.split(/\n+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
const readMinutes = computed(() => {
|
||||
const len = detail.value?.content?.length || 0
|
||||
return len ? Math.max(1, Math.ceil(len / 300)) : 0
|
||||
})
|
||||
|
||||
const dimInfo = computed(() => {
|
||||
const dim = detail.value?.dim
|
||||
return DIMENSIONS.find(d => d.id === dim) || DIMENSIONS[0]
|
||||
})
|
||||
|
||||
const viewCountText = computed(() => {
|
||||
const count = detail.value?.viewCount || 0
|
||||
return count >= 10000 ? (count / 10000).toFixed(1) + '万' : String(count)
|
||||
})
|
||||
const collectionCount = computed(() => detail.value?.collectionCount || 0)
|
||||
|
||||
const resultOkTitle = computed(() =>
|
||||
quizResult.value?.lit ? RESULT_OK_TITLE : RESULT_OK_GUEST
|
||||
)
|
||||
const resultBadTitle = RESULT_BAD_TITLE
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const query = currentPage.$page?.query || {}
|
||||
const query = currentPage.$page?.query || currentPage.options || {}
|
||||
detailId.value = query.id || ''
|
||||
|
||||
if (detailId.value) {
|
||||
@@ -118,41 +179,19 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 维度信息
|
||||
const dimColor = computed(() => {
|
||||
const dim = detail.value?.dim
|
||||
if (!dim) return 'var(--dim1-color)'
|
||||
const dimInfo = DIMENSIONS.find(d => d.id === dim)
|
||||
return dimInfo ? dimInfo.color : 'var(--dim1-color)'
|
||||
})
|
||||
const dimensionName = computed(() => {
|
||||
const dim = detail.value?.dim
|
||||
if (!dim) return ''
|
||||
const dimInfo = DIMENSIONS.find(d => d.id === dim)
|
||||
return dimInfo ? dimInfo.name : ''
|
||||
})
|
||||
|
||||
// 分类标签
|
||||
const categoryLabel = computed(() => {
|
||||
const map = {
|
||||
concept: '📚 概念',
|
||||
history: '📜 历史',
|
||||
application: '🛠️ 应用',
|
||||
question: '❓ 问答'
|
||||
}
|
||||
return map[detail.value?.category] || ''
|
||||
})
|
||||
|
||||
const viewCountText = computed(() => {
|
||||
const count = detail.value?.viewCount || 0
|
||||
return count >= 10000 ? (count / 10000).toFixed(1) + '万' : count
|
||||
})
|
||||
const collectionCount = computed(() => detail.value?.collectionCount || 0)
|
||||
|
||||
async function fetchDetail() {
|
||||
try {
|
||||
detail.value = await knowledgeApi.detail(detailId.value)
|
||||
// 检查是否已收藏
|
||||
|
||||
// 记录"继续探索"
|
||||
try {
|
||||
uni.setStorageSync('last_read', {
|
||||
id: detailId.value,
|
||||
title: detail.value.title || '',
|
||||
dim: detail.value.dim
|
||||
})
|
||||
} catch (e) {}
|
||||
|
||||
if (userStore.isLoggedIn) {
|
||||
const collected = await knowledgeApi.collected(userStore.token)
|
||||
isCollected.value = collected.some(item => item.id === detailId.value)
|
||||
@@ -163,9 +202,33 @@ async function fetchDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitQuiz() {
|
||||
if (selectedOption.value === null || !detailId.value) return
|
||||
try {
|
||||
const res = await knowledgeApi.quiz(
|
||||
detailId.value,
|
||||
selectedOption.value,
|
||||
userStore.isLoggedIn ? userStore.token : ''
|
||||
)
|
||||
quizResult.value = res
|
||||
if (res.correct) {
|
||||
uni.setStorageSync('lit_today', new Date().toDateString())
|
||||
if (res.lit) {
|
||||
uni.showToast({ title: '星球已点亮', icon: 'success' })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function retryQuiz() {
|
||||
selectedOption.value = null
|
||||
quizResult.value = null
|
||||
}
|
||||
|
||||
async function toggleCollect() {
|
||||
if (!userStore.isLoggedIn) {
|
||||
// 未登录:弹窗给用户选择,可取消,不强制跳转
|
||||
uni.showModal({
|
||||
title: '登录后收藏',
|
||||
content: '登录后即可收藏喜欢的 AI 知识',
|
||||
@@ -198,44 +261,45 @@ function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goToAI() {
|
||||
function goChat() {
|
||||
uni.switchTab({ url: '/pages/chat/chat' })
|
||||
}
|
||||
|
||||
// 微信小程序分享
|
||||
// #ifdef MP-WEIXIN
|
||||
onShareAppMessage(() => ({
|
||||
title: 'AI 维度 - 你的AI知识助手',
|
||||
path: '/pages/index/index'
|
||||
title: (detail.value?.title ? detail.value.title + ' · AI 维度' : 'AI 维度 · 知识星球'),
|
||||
path: `/pages/detail/detail?id=${detailId.value}`
|
||||
}))
|
||||
onShareTimeline(() => ({
|
||||
title: detail.value?.title || 'AI 维度 · 知识星球'
|
||||
}))
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 20px 30px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
// 顶部导航
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 50px 20px 12px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 0 14px;
|
||||
|
||||
.nav-back {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
@@ -243,226 +307,259 @@ onShareAppMessage(() => ({
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.nav-collect {
|
||||
width: 30px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.icon-collected {
|
||||
color: var(--dim5-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 知识头部
|
||||
.detail-header {
|
||||
padding: 0 20px 16px;
|
||||
flex-shrink: 0;
|
||||
padding: 4px 0 14px;
|
||||
|
||||
.dim-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.9;
|
||||
|
||||
.dim-name { color: white; }
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-tertiary);
|
||||
|
||||
.meta-item { display: flex; align-items: center; }
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 20px 16px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 0 14px;
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
color: var(--dim1-color);
|
||||
background: rgba(179, 136, 255, 0.1);
|
||||
font-size: 11px;
|
||||
color: var(--color-brand);
|
||||
background: var(--color-brand-bg);
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
}
|
||||
|
||||
// 内容滚动区
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 80px;
|
||||
.content-card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 18px 16px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
|
||||
.content-section,
|
||||
.content-block,
|
||||
.qa-section {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
.loading-block {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.para {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.9;
|
||||
margin-bottom: 14px;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
.para-lead {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 检测题 */
|
||||
.quiz-card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
|
||||
.quiz-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.quiz-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.quiz-question {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quiz-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1.5px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.selected {
|
||||
border-color: var(--color-brand);
|
||||
background: var(--color-brand-bg);
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--dim1-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.section-content {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.section-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
|
||||
.section-img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
.option-letter {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 问答区域
|
||||
.qa-section {
|
||||
.qa-question {
|
||||
.qa-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.qa-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.quiz-submit {
|
||||
margin-top: 6px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.quiz-result {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
&.ok {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
&.bad {
|
||||
background: rgba(229, 28, 28, 0.07);
|
||||
border: 1px solid rgba(229, 28, 28, 0.2);
|
||||
}
|
||||
|
||||
.qa-options {
|
||||
.result-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.result-title {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.result-desc {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.quiz-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
|
||||
.quiz-retry {
|
||||
flex: 1;
|
||||
height: 42px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1.5px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.qa-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.correct {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.option-letter {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.option-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.qa-answer {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(179, 136, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
|
||||
.answer-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.answer-text {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.quiz-next {
|
||||
flex: 1.6;
|
||||
height: 42px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// AI 底部入口
|
||||
/* AI 入口 */
|
||||
.ai-banner {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(179, 136, 255, 0.3);
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand));
|
||||
border-radius: var(--radius-md);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px var(--color-brand-glow);
|
||||
|
||||
.ai-icon { font-size: 18px; }
|
||||
.ai-text { font-size: 14px; font-weight: 600; }
|
||||
.ai-arrow { font-size: 14px; }
|
||||
.ai-text { font-size: 14px; font-weight: 600; color: #fff; }
|
||||
}
|
||||
|
||||
.bottom-spacer { height: 24px; }
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<text class="empty-icon">⭐</text>
|
||||
<text class="empty-icon">还没有收藏</text>
|
||||
<text class="empty-text" v-if="userStore.isLoggedIn">你还没有收藏任何内容</text>
|
||||
<text class="empty-text" v-else>登录后可收藏喜欢的 AI 知识</text>
|
||||
<text class="empty-sub" v-if="userStore.isLoggedIn">去首页发现有趣的 AI 知识吧</text>
|
||||
|
||||
@@ -112,7 +112,7 @@ const userStore = useUserStore()
|
||||
// 反馈类型
|
||||
const feedbackTypes = [
|
||||
{ value: 'bug', name: '问题报告', icon: '🐛' },
|
||||
{ value: 'feature', name: '功能建议', icon: '💡' },
|
||||
{ value: 'feature', name: '功能建议', icon: '★' },
|
||||
{ value: 'content', name: '内容纠错', icon: '📝' },
|
||||
{ value: 'other', name: '其他', icon: '💬' }
|
||||
]
|
||||
@@ -147,7 +147,7 @@ function statusLabel(status) {
|
||||
function typeLabel(type) {
|
||||
const map = {
|
||||
bug: '🐛 问题报告',
|
||||
feature: '💡 功能建议',
|
||||
feature: '功能建议',
|
||||
content: '📝 内容纠错',
|
||||
other: '💬 其他'
|
||||
}
|
||||
|
||||
+254
-186
@@ -1,75 +1,93 @@
|
||||
<template>
|
||||
<view class="page-home page-layout">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="header">
|
||||
<view class="brand">
|
||||
<view class="brand-icon">✦</view>
|
||||
<view class="brand-icon">
|
||||
<AppIcon name="planet-white" :size="20" />
|
||||
</view>
|
||||
<view class="brand-text">
|
||||
<text class="title">AI 维度</text>
|
||||
<text class="subtitle">宇之然 · 探索</text>
|
||||
<text class="subtitle">宇之然 · 知识星图</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="icon-btn" @tap="onNotify">
|
||||
<text>🔔</text>
|
||||
</view>
|
||||
<view class="icon-btn" @tap="goProfile">
|
||||
<text>👤</text>
|
||||
<AppIcon name="user-gray" :size="18" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 问候语 -->
|
||||
<view class="greeting">
|
||||
<text class="greeting-title">探索 </text>
|
||||
<text class="greeting-highlight">人工智能</text>
|
||||
<text class="greeting-desc">从 5 个维度,系统理解 AI 的过去、现在与未来</text>
|
||||
<!-- 连续点亮 streak -->
|
||||
<view class="streak-bar" v-if="streak > 0" @tap="goFirstUnlit">
|
||||
<AppIcon name="flame" :size="16" />
|
||||
<text class="streak-text">已连续点亮 {{ streak }} 天</text>
|
||||
<text class="streak-sub">{{ todayLit ? '今日已点亮,继续保持' : '今日还没点亮,去逛逛' }}</text>
|
||||
<view class="streak-go">
|
||||
<AppIcon name="arrow-white" :size="12" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 继续探索 -->
|
||||
<view class="resume-card" v-if="lastRead" @tap="resumeRead">
|
||||
<view class="resume-icon">
|
||||
<AppIcon name="clock" :size="18" />
|
||||
</view>
|
||||
<view class="resume-info">
|
||||
<text class="resume-label">继续探索</text>
|
||||
<text class="resume-title">{{ lastRead.title }}</text>
|
||||
</view>
|
||||
<AppIcon name="arrow-gray" :size="14" />
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" @tap="onSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<AppIcon name="search-gray" :size="16" />
|
||||
<text class="search-placeholder">搜搜你想了解的 AI 知识...</text>
|
||||
</view>
|
||||
|
||||
<!-- 五维度网格 -->
|
||||
<!-- 五大星域 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">✦ 五个维度</text>
|
||||
<text class="section-title">五大星域</text>
|
||||
<text class="section-more" v-if="userStats">{{ userStats.totalLit }}/{{ statsTotal }} 颗已点亮</text>
|
||||
</view>
|
||||
<view class="dim-grid">
|
||||
<view
|
||||
v-for="dim in dimensions"
|
||||
:key="dim.id"
|
||||
class="dim-card"
|
||||
:class="{ 'dim-full': dim.isFull }"
|
||||
:style="{ background: dim.bg }"
|
||||
@tap="goDimension(dim)"
|
||||
>
|
||||
<view class="dim-icon" :style="{ background: dim.iconBg, color: dim.color }">
|
||||
<text>{{ dim.icon }}</text>
|
||||
<view class="dim-top">
|
||||
<view class="dim-icon" :style="{ background: dim.iconBg }">
|
||||
<AppIcon :name="dim.icon" :size="18" />
|
||||
</view>
|
||||
<ProgressRing
|
||||
:size="34"
|
||||
:num-size="9"
|
||||
:percent="dimPercent(dim.id)"
|
||||
:color="dim.color"
|
||||
/>
|
||||
</view>
|
||||
<text class="dim-name">{{ dim.name }}</text>
|
||||
<text class="dim-desc">{{ dim.tagline }}</text>
|
||||
<view class="dim-foot">
|
||||
<view class="dim-lit" :style="{ background: dim.bg, color: dim.color }">
|
||||
{{ dimLitText(dim.id) }}
|
||||
</view>
|
||||
<text class="dim-count">{{ dimCountText(dim.id) }}</text>
|
||||
</view>
|
||||
<text class="dim-name" :style="{ color: dim.color }">{{ dim.name }}</text>
|
||||
<text class="dim-desc">{{ dim.desc }}</text>
|
||||
<text class="dim-count">
|
||||
{{ dim.isFull ? '今日已更新 8 条' : `${dim.articleCount} 篇 · ${dim.quizCount} 个问答` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日快讯 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">📡 今日快讯</text>
|
||||
<text class="section-more" @tap="goTrend">查看全部 →</text>
|
||||
<text class="section-title">今日快讯</text>
|
||||
<text class="section-more" @tap="goTrend">查看全部</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(item, idx) in newsList"
|
||||
:key="idx"
|
||||
class="news-card"
|
||||
@tap="goNewsDetail(item)"
|
||||
>
|
||||
<view v-for="(item, idx) in newsList" :key="idx" class="news-card" @tap="goNewsDetail(item)">
|
||||
<view class="news-tag" :class="item.tagClass">{{ item.tag }}</view>
|
||||
<text class="news-title">{{ item.title }}</text>
|
||||
<view class="news-meta">
|
||||
@@ -83,34 +101,56 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onPullDownRefresh, onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { ref, computed } from 'vue'
|
||||
import { DIMENSIONS } from '@/utils/dimensions'
|
||||
import { trendApi } from '@/utils/api'
|
||||
import { knowledgeApi, trendApi } from '@/utils/api'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AppIcon from '@/components/AppIcon.vue'
|
||||
import ProgressRing from '@/components/ProgressRing.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const dimensions = ref(DIMENSIONS)
|
||||
|
||||
const newsList = ref([])
|
||||
const loading = ref(true)
|
||||
const byDim = ref({}) // 各星域知识总数
|
||||
const userStats = ref(null) // { totalLit, litByDim, streak }
|
||||
const lastRead = ref(null)
|
||||
|
||||
const TAG_HOT = '热门'
|
||||
const TAG_PICK = '精选'
|
||||
const TAG_NEW = '最新'
|
||||
|
||||
const statsTotal = computed(() => Object.values(byDim.value).reduce((a, b) => a + b, 0))
|
||||
const streak = computed(() => userStats.value?.streak || 0)
|
||||
const todayLit = computed(() => !!uni.getStorageSync('lit_today'))
|
||||
|
||||
onShow(() => {
|
||||
loadStats()
|
||||
loadNews()
|
||||
lastRead.value = uni.getStorageSync('last_read') || null
|
||||
})
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
try {
|
||||
const data = await trendApi.list({ limit: 5 })
|
||||
// 页面已有响应式引用会自动更新(保持既有逻辑)
|
||||
} catch (e) {}
|
||||
await Promise.all([loadStats(), loadNews()])
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchNews()
|
||||
})
|
||||
|
||||
async function fetchNews() {
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await trendApi.list({ limit: 5 })
|
||||
newsList.value = data.list.map(item => ({
|
||||
tag: item.hot ? '🔥 热门' : item.picked ? '📌 精选' : '🚀 最新',
|
||||
const data = await knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '')
|
||||
byDim.value = data.byDim || {}
|
||||
userStats.value = data.user || null
|
||||
} catch (e) {
|
||||
// 静默降级:环形显示 0%
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNews() {
|
||||
try {
|
||||
const data = await trendApi.list({ limit: 3 })
|
||||
newsList.value = (data.list || []).slice(0, 3).map(item => ({
|
||||
tag: item.hot ? TAG_HOT : item.picked ? TAG_PICK : TAG_NEW,
|
||||
tagClass: item.hot ? 'hot' : item.picked ? 'pick' : 'new',
|
||||
title: item.title,
|
||||
source: item.source,
|
||||
@@ -118,17 +158,27 @@ async function fetchNews() {
|
||||
_id: item._id
|
||||
}))
|
||||
} catch (e) {
|
||||
// 降级:本地数据
|
||||
newsList.value = [
|
||||
{ tag: '🔥 热门', tagClass: 'hot', title: 'DeepSeek 被曝自研推理芯片', source: '量子位', time: '2 小时前' },
|
||||
{ tag: '🚀 最新', tagClass: 'new', title: '蚂蚁灵波开源 LingBot-World 2.0', source: '量子位', time: '4 小时前' },
|
||||
{ tag: '📌 精选', tagClass: 'pick', title: '阿里获 ACL 2026 最佳资源论文奖', source: '机器之心', time: '6 小时前' }
|
||||
]
|
||||
} finally {
|
||||
loading.value = false
|
||||
newsList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function dimPercent(dimId) {
|
||||
const total = byDim.value[dimId] || 0
|
||||
if (!total) return 0
|
||||
const lit = userStats.value?.litByDim?.[dimId] || 0
|
||||
return Math.round((lit / total) * 100)
|
||||
}
|
||||
|
||||
function dimLitText(dimId) {
|
||||
const lit = userStats.value?.litByDim?.[dimId] || 0
|
||||
return `${lit} 颗已点亮`
|
||||
}
|
||||
|
||||
function dimCountText(dimId) {
|
||||
const total = byDim.value[dimId] || 0
|
||||
return `共 ${total} 颗星球`
|
||||
}
|
||||
|
||||
function formatTime(dateStr) {
|
||||
const diff = Date.now() - new Date(dateStr).getTime()
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
@@ -141,6 +191,16 @@ function goDimension(dim) {
|
||||
uni.navigateTo({ url: `/pages/dimension/dimension?id=${dim.id}&name=${dim.name}` })
|
||||
}
|
||||
|
||||
function goFirstUnlit() {
|
||||
uni.navigateTo({ url: '/pages/dimension/dimension?id=1' })
|
||||
}
|
||||
|
||||
function resumeRead() {
|
||||
if (lastRead.value?.id) {
|
||||
uni.navigateTo({ url: `/pages/detail/detail?id=${lastRead.value.id}` })
|
||||
}
|
||||
}
|
||||
|
||||
function goTrend() {
|
||||
uni.switchTab({ url: '/pages/trend/trend' })
|
||||
}
|
||||
@@ -153,21 +213,19 @@ function onSearch() {
|
||||
uni.navigateTo({ url: '/pages/search/search' })
|
||||
}
|
||||
|
||||
function onNotify() {
|
||||
uni.showToast({ title: '暂无新消息', icon: 'none' })
|
||||
}
|
||||
|
||||
function goNewsDetail(item) {
|
||||
uni.setStorageSync('__news_item__', JSON.stringify(item))
|
||||
uni.navigateTo({ url: '/pages/news/news' })
|
||||
}
|
||||
|
||||
// 微信小程序分享
|
||||
// #ifdef MP-WEIXIN
|
||||
onShareAppMessage(() => ({
|
||||
title: 'AI 维度 - 你的AI知识助手',
|
||||
title: 'AI 维度 · 5 分钟点亮一颗 AI 知识星球',
|
||||
path: '/pages/index/index'
|
||||
}))
|
||||
onShareTimeline(() => ({
|
||||
title: 'AI 维度 · 5 分钟点亮一颗 AI 知识星球'
|
||||
}))
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
@@ -178,15 +236,13 @@ onShareAppMessage(() => ({
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder {
|
||||
height: 44px;
|
||||
}
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0 20px;
|
||||
padding: 6px 0 18px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
@@ -196,38 +252,32 @@ onShareAppMessage(() => ({
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, rgba(124,77,255,0.3), rgba(0,188,212,0.2));
|
||||
border: 1px solid rgba(124,77,255,0.2);
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--color-brand), #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
box-shadow: 0 0 20px rgba(124,77,255,0.1);
|
||||
box-shadow: 0 4px 14px var(--color-brand-glow);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand-text { display: flex; flex-direction: column; }
|
||||
.brand-text .title {
|
||||
font-size: 20px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.brand-text .subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 2px;
|
||||
color: var(--text-tertiary);
|
||||
letter-spacing: 1.5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.header-actions { display: flex; gap: 10px; }
|
||||
.icon-btn {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 50%;
|
||||
@@ -236,167 +286,185 @@ onShareAppMessage(() => ({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
margin-bottom: 20px;
|
||||
padding: 24px 20px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.04));
|
||||
border: 1px solid rgba(99,102,241,0.1);
|
||||
/* streak */
|
||||
.streak-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, rgba(229, 28, 28, 0.08), rgba(223, 119, 0, 0.06));
|
||||
border: 1px solid rgba(229, 28, 28, 0.15);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.greeting-title {
|
||||
font-size: 26px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
.greeting-highlight {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #b388ff, #7c4dff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.greeting-desc {
|
||||
display: block;
|
||||
.streak-text {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--dim5-color);
|
||||
}
|
||||
.streak-sub {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.streak-go {
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--dim5-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
/* 继续探索 */
|
||||
.resume-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 14px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.resume-icon {
|
||||
width: 38px; height: 38px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-brand-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.resume-info { flex: 1; min-width: 0; }
|
||||
.resume-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.resume-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 搜索 */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.search-icon { color: var(--text-muted); }
|
||||
.search-placeholder { color: var(--text-muted); font-size: 14px; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(99,102,241,0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.section-more {
|
||||
font-size: 12px;
|
||||
.search-placeholder {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 星域网格 */
|
||||
.dim-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.dim-card {
|
||||
border-radius: 18px;
|
||||
padding: 20px 16px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.dim-full {
|
||||
grid-column: 1 / -1;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.dim-card:active { transform: scale(0.97); }
|
||||
|
||||
.dim-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dim-icon {
|
||||
width: 42px; height: 42px;
|
||||
border-radius: 12px;
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dim-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.dim-desc {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dim-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dim-lit {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
.dim-count {
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 快讯 */
|
||||
.news-card {
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
|
||||
.news-tag {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.news-tag.hot {
|
||||
background: rgba(239,83,80,0.12);
|
||||
color: #ef5350;
|
||||
}
|
||||
.news-tag.new {
|
||||
background: rgba(0,188,212,0.12);
|
||||
color: #4dd0e1;
|
||||
}
|
||||
.news-tag.pick {
|
||||
background: rgba(124,77,255,0.12);
|
||||
color: #b388ff;
|
||||
}
|
||||
|
||||
.news-tag.hot { background: var(--color-hot-bg); color: var(--color-hot); }
|
||||
.news-tag.new { background: var(--color-new-bg); color: var(--color-new); }
|
||||
.news-tag.pick { background: var(--color-pick-bg); color: var(--color-pick); }
|
||||
.news-title {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
.bottom-spacer { height: 30px; }
|
||||
</style>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<view class="map-page">
|
||||
<view class="status-bar"></view>
|
||||
<view class="top-bar">
|
||||
<view class="user-info">
|
||||
<text class="lv-icon">{{ level.icon }}</text>
|
||||
<text class="lv-name">{{ level.name }} Lv{{ level.level }}</text>
|
||||
</view>
|
||||
<view class="coin-box">
|
||||
<text class="coin-icon">💎</text>
|
||||
<text class="coin-num">{{ credits }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="map-scroll">
|
||||
<view class="map-body" :style="{ height: mapHeight + rpx }">
|
||||
<view class="map-path-line" :style="{ height: mapHeight + rpx }"></view>
|
||||
<view v-for="(island, i) in islands" :key="i"
|
||||
class="island-node"
|
||||
:class="{ active: island.status === current, done: island.status === done, locked: island.status === locked }"
|
||||
:style="{ top: island.y + rpx, left: island.x + rpx }"
|
||||
@tap="goIsland(island)">
|
||||
<view class="island-icon">{{ island.icon }}</view>
|
||||
<text class="island-name">{{ island.name }}</text>
|
||||
<view v-if="island.status === done" class="done-star">⭐</view>
|
||||
<view v-if="island.status === locked" class="lock-icon">🔒</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="bottom-bar">
|
||||
<button class="ai-btn" :disabled="!aiUnlocked" @tap="goCreate">✨ AI 生成</button>
|
||||
<text v-if="!aiUnlocked" class="ai-hint">完成一个知识关后解锁</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
credits: 0,
|
||||
level: { level: 1, name: "画童", icon: "🖌️" },
|
||||
islands: [
|
||||
{ id: 1, name: "AI 起源", icon: "🏛️", x: 100, y: 180, status: "current" },
|
||||
{ id: 2, name: "发展脉络", icon: "📈", x: 380, y: 360, status: "locked" },
|
||||
{ id: 3, name: "行业格局", icon: "🌍", x: 100, y: 540, status: "locked" },
|
||||
{ id: 4, name: "学习工具", icon: "🛠️", x: 380, y: 720, status: "locked" },
|
||||
{ id: 5, name: "趋势前沿", icon: "🚀", x: 100, y: 900, status: "locked" },
|
||||
],
|
||||
aiUnlocked: false,
|
||||
mapHeight: 1100,
|
||||
}
|
||||
},
|
||||
onShow() { this.loadUser() },
|
||||
methods: {
|
||||
loadUser() {
|
||||
var token = uni.getStorageSync("dim_admin_token")
|
||||
if (!token) return
|
||||
uni.request({
|
||||
url: "/api/me",
|
||||
header: { Authorization: "Bearer " + token },
|
||||
success: function(res) {
|
||||
if (res.data && res.data.user) {
|
||||
this.credits = res.data.user.credits || 0
|
||||
}
|
||||
}.bind(this)
|
||||
})
|
||||
},
|
||||
goIsland(island) {
|
||||
if (island.status !== "current") {
|
||||
uni.showToast({ title: "先完成前面的知识关", icon: "none" })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: "/pages/map/quiz?islandId=" + island.id + "&name=" + encodeURIComponent(island.name) })
|
||||
},
|
||||
goCreate() {
|
||||
uni.switchTab({ url: "/pages/index/index" })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.map-page { background: linear-gradient(180deg, #1a1a3e 0%, #2d1b69 50%, #0d0d1a 100%); min-height: 100vh; }
|
||||
.status-bar { height: 80rpx; }
|
||||
.top-bar { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 30rpx; }
|
||||
.user-info { display: flex; align-items: center; gap: 8rpx; }
|
||||
.lv-icon { font-size: 32rpx; }
|
||||
.lv-name { color: #fff; font-size: 26rpx; font-weight: 600; }
|
||||
.coin-box { display: flex; align-items: center; gap: 6rpx; background: rgba(255,255,255,0.1); border-radius: 999rpx; padding: 6rpx 20rpx; }
|
||||
.coin-icon { font-size: 26rpx; }
|
||||
.coin-num { color: #ffd700; font-size: 26rpx; font-weight: 700; }
|
||||
.map-scroll { height: calc(100vh - 80rpx - 70rpx - 120rpx); }
|
||||
.map-body { position: relative; width: 100%; }
|
||||
.map-path-line { position: absolute; left: 50%; top: 100rpx; width: 4rpx; height: 900rpx; background: rgba(255,255,255,0.1); transform: translateX(-50%); }
|
||||
.island-node { position: absolute; width: 180rpx; text-align: center; }
|
||||
.island-icon { font-size: 56rpx; display: block; margin-bottom: 8rpx; }
|
||||
.island-name { color: #fff; font-size: 24rpx; display: block; }
|
||||
.done-star { position: absolute; top: -10rpx; right: 0; font-size: 28rpx; }
|
||||
.lock-icon { position: absolute; top: -8rpx; right: -8rpx; font-size: 28rpx; }
|
||||
.island-node.active .island-icon { animation: bounce 2s infinite; }
|
||||
@keyframes bounce { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-16rpx) } }
|
||||
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.5); display: flex; flex-direction: column; align-items: center; gap: 8rpx; z-index: 100; }
|
||||
.ai-btn { width: 100%; background: linear-gradient(135deg, #6366f1, #a855f7); color: #fff; font-size: 32rpx; font-weight: 700; border-radius: 999rpx; padding: 20rpx; border: none; }
|
||||
.ai-btn[disabled] { opacity: 0.4; }
|
||||
.ai-hint { color: rgba(255,255,255,0.5); font-size: 22rpx; }
|
||||
</style>
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<view class="quiz-page">
|
||||
<view class="quiz-header">
|
||||
<text class="quiz-title">{{ islandName }} · 知识关</text>
|
||||
<text class="quiz-progress">{{ current + 1 }} / {{ questions.length }}</text>
|
||||
</view>
|
||||
<view class="quiz-body" v-if="currentQ">
|
||||
<text class="quiz-q">{{ currentQ.q }}</text>
|
||||
<view v-for="(opt, i) in currentQ.options" :key="i"
|
||||
class="quiz-opt"
|
||||
:class="{ selected: selected === i, correct: submitted && i === currentQ.answer, wrong: submitted && selected === i && i !== currentQ.answer }"
|
||||
@tap="pick(i)">
|
||||
<text>{{ labels[i] }}. {{ opt }}</text>
|
||||
</view>
|
||||
<view v-if="submitted" class="quiz-result">
|
||||
<text :class="isCorrect ? 'ok' : 'fail'">{{ resultText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="quiz-footer">
|
||||
<button v-if="!submitted && currentQ" class="submit-btn" @tap="submit">提交答案</button>
|
||||
<button v-if="submitted && current < questions.length - 1" class="next-btn" @tap="nextQ">下一题 →</button>
|
||||
<button v-if="submitted && current >= questions.length - 1" class="next-btn done-btn" @tap="finish">完成知识关</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
islandName: '',
|
||||
current: 0,
|
||||
selected: -1,
|
||||
submitted: false,
|
||||
isCorrect: false,
|
||||
resultText: '',
|
||||
labels: ['A', 'B', 'C', 'D'],
|
||||
questions: [
|
||||
{ q: 'AI 的英文全称是什么?', options: ['Artificial Intelligence', 'Automatic Integration', 'Advanced Internet', 'Applied Innovation'], answer: 0 },
|
||||
{ q: '深度学习属于 AI 的哪个领域?', options: ['规则推理', '机器学习', '硬件设计', '网络协议'], answer: 1 },
|
||||
{ q: '以下哪个是当前主流的大语言模型?', options: ['Photoshop', 'GPT', 'Excel', 'Bluetooth'], answer: 1 },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentQ() { return this.questions[this.current] || null },
|
||||
isLast() { return this.current >= this.questions.length - 1 },
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.name) {
|
||||
this.islandName = decodeURIComponent(options.name)
|
||||
uni.setNavigationBarTitle({ title: this.islandName + ' · 知识关' })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pick(i) {
|
||||
if (this.submitted) return
|
||||
this.selected = i
|
||||
this.submitted = true
|
||||
this.isCorrect = this.selected === this.questions[this.current].answer
|
||||
},
|
||||
nextQ() {
|
||||
if (this.current < this.questions.length - 1) {
|
||||
this.current++
|
||||
this.selected = -1
|
||||
this.submitted = false
|
||||
} else {
|
||||
uni.showToast({ title: '知识关完成!', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.quiz-page { background: linear-gradient(180deg, #1a1a3e, #0d0d1a); min-height: 100vh; padding: 30rpx; box-sizing: border-box; }
|
||||
.quiz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
|
||||
.quiz-title { color: #fff; font-size: 32rpx; font-weight: 700; }
|
||||
.quiz-progress { color: rgba(255,255,255,0.5); font-size: 24rpx; }
|
||||
.quiz-q { color: #fff; font-size: 32rpx; font-weight: 600; margin-bottom: 30rpx; display: block; }
|
||||
.quiz-opt { background: rgba(255,255,255,0.08); border: 2rpx solid rgba(255,255,255,0.15); border-radius: 16rpx; padding: 24rpx 28rpx; margin-bottom: 16rpx; color: #fff; font-size: 28rpx; transition: all 0.2s; }
|
||||
.quiz-opt.selected { border-color: #6366f1; background: rgba(99,102,241,0.15); }
|
||||
.quiz-opt.correct { border-color: #22c55e; background: rgba(34,197,94,0.15); }
|
||||
.quiz-opt.wrong { border-color: #ef4444; background: rgba(239,68,68,0.15); }
|
||||
.quiz-result { text-align: center; margin-top: 20rpx; font-size: 30rpx; }
|
||||
.quiz-footer { margin-top: 30rpx; }
|
||||
.submit-btn { width: 100%; background: linear-gradient(135deg, #6366f1, #a855f7); color: #fff; font-size: 30rpx; font-weight: 700; border-radius: 16rpx; padding: 24rpx; border: none; margin-top: 20rpx; }
|
||||
</style>
|
||||
@@ -39,7 +39,7 @@
|
||||
<text class="meta-value">{{ news.viewCount || 0 }} 次</text>
|
||||
</view>
|
||||
<view class="meta-item" v-if="news.hot">
|
||||
<text class="meta-label hot">🔥</text>
|
||||
<text class="meta-label hot">热</text>
|
||||
<text class="meta-value">{{ news.hotCount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -110,7 +110,8 @@ const heroBg = computed(() => {
|
||||
})
|
||||
|
||||
const heroEmoji = computed(() => {
|
||||
return { product: '🚀', paper: '📄', tool: '🔧', company: '🏢' }[news.value.category || 'product'] || '📰'
|
||||
const map = { product: '产品', paper: '论文', tool: '工具', company: '公司' }
|
||||
return map[news.value.category || 'product'] || '资讯'
|
||||
})
|
||||
|
||||
const categoryLabel = computed(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="page-profile page-layout">
|
||||
<view class="status-bar-placeholder"></view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<!-- 学习者档案头 -->
|
||||
<view class="profile-card">
|
||||
<view class="profile-avatar">
|
||||
<image
|
||||
@@ -11,14 +11,48 @@
|
||||
class="avatar-img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text v-else class="avatar-text">
|
||||
{{ (userStore.userInfo?.nickName || '用').charAt(0) }}
|
||||
</text>
|
||||
<view v-else class="avatar-placeholder">
|
||||
<AppIcon name="planet-white" :size="26" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-main">
|
||||
<text class="profile-name">{{ userStore.userInfo?.nickName || '探索者' }}</text>
|
||||
<text class="profile-sub">{{ profileSubText }}</text>
|
||||
</view>
|
||||
<view class="streak-chip" v-if="stats && stats.streak > 0">
|
||||
<AppIcon name="flame" :size="13" />
|
||||
<text>{{ stats.streak }} 天</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 学习数据 -->
|
||||
<view class="stats-row">
|
||||
<view class="stat-tile" @click="goProgress">
|
||||
<text class="stat-num">{{ stats?.totalLit || 0 }}</text>
|
||||
<text class="stat-label">点亮星球</text>
|
||||
</view>
|
||||
<view class="stat-tile">
|
||||
<text class="stat-num">{{ stats?.streak || 0 }}</text>
|
||||
<text class="stat-label">连续天数</text>
|
||||
</view>
|
||||
<view class="stat-tile" @click="goFavorites">
|
||||
<text class="stat-num">{{ collectedCount }}</text>
|
||||
<text class="stat-label">收藏知识</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 五星域点亮进度 -->
|
||||
<view class="dims-progress" v-if="stats">
|
||||
<view v-for="dim in dimensions" :key="dim.id" class="dim-row" @click="goDim(dim.id)">
|
||||
<view class="dim-dot" :style="{ background: dim.iconBg }">
|
||||
<AppIcon :name="dim.icon" :size="13" />
|
||||
</view>
|
||||
<text class="dim-name">{{ dim.name }}</text>
|
||||
<view class="dim-bar">
|
||||
<view class="dim-bar-fill" :style="{ width: dimPercent(dim.id) + '%', background: dim.color }"></view>
|
||||
</view>
|
||||
<text class="dim-frac">{{ stats.litByDim?.[dim.id] || 0 }}/{{ byDim[dim.id] || 0 }}</text>
|
||||
</view>
|
||||
<text class="profile-name">{{ userStore.userInfo?.nickName || '游客' }}</text>
|
||||
<text class="profile-level">
|
||||
{{ subscriptionBadge }} · 已收藏 {{ collectedCount }} 篇
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 订阅卡片 -->
|
||||
@@ -26,14 +60,11 @@
|
||||
<view class="plan-info">
|
||||
<text class="plan-title">{{ planTitle }}</text>
|
||||
<text class="plan-desc">{{ planDesc }}</text>
|
||||
<!-- 进度条(订阅到期倒计时) -->
|
||||
<view class="plan-progress" v-if="userStore.subInfo">
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: planPercent + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-text">
|
||||
{{ planExpiryText }}
|
||||
</text>
|
||||
<text class="progress-text">{{ planExpiryText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="plan-btn" :class="{ disabled: userStore.isSubscribed }">
|
||||
@@ -42,55 +73,52 @@
|
||||
</view>
|
||||
|
||||
<!-- AI 问答配额 -->
|
||||
<view class="quota-card" @click="goChat" v-if="userStore.isLoggedIn">
|
||||
<text class="quota-card-title">🤖 AI 问答配额</text>
|
||||
<view class="quota-card" @click="goChat" v-if="userStore.isLoggedIn && quotaInfo">
|
||||
<view class="quota-head">
|
||||
<AppIcon name="chat" :size="15" />
|
||||
<text class="quota-card-title">AI 问答 · 今日</text>
|
||||
</view>
|
||||
<view class="quota-bar">
|
||||
<view class="quota-fill" :style="{ width: quotaPercent + '%' }"></view>
|
||||
</view>
|
||||
<text class="quota-text">
|
||||
{{ quotaUsed }} / {{ quotaTotal }}
|
||||
<text class="quota-note">今日使用</text>
|
||||
</text>
|
||||
<text class="quota-share" @click.stop="goShare">🎁 分享得次数</text>
|
||||
<view class="quota-foot">
|
||||
<text class="quota-text">剩余 {{ quotaInfo.remainingQuota }} 次</text>
|
||||
<text class="quota-share" @click.stop="goShare">分享 +5 次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="menu-section">
|
||||
<view class="menu-item" @click="goFavorites">
|
||||
<view class="m-icon" style="background:rgba(179,136,255,0.1);color:#b388ff">⭐</view>
|
||||
<view class="m-icon" style="background:var(--dim1-bg)"><AppIcon name="star" :size="16" /></view>
|
||||
<text class="m-label">我的收藏</text>
|
||||
<text class="m-badge">{{ collectedCount }}</text>
|
||||
<text class="m-arrow">→</text>
|
||||
<AppIcon name="arrow-gray" :size="12" />
|
||||
</view>
|
||||
<view class="menu-item" @click="goProgress">
|
||||
<view class="m-icon" style="background:rgba(77,208,225,0.1);color:#4dd0e1">📊</view>
|
||||
<view class="m-icon" style="background:var(--dim2-bg)"><AppIcon name="rocket" :size="16" /></view>
|
||||
<text class="m-label">学习进度</text>
|
||||
<text class="m-arrow">→</text>
|
||||
<AppIcon name="arrow-gray" :size="12" />
|
||||
</view>
|
||||
<view class="menu-item" @click="goAchievements">
|
||||
<view class="m-icon" style="background:rgba(77,182,172,0.1);color:#4db6ac">🏆</view>
|
||||
<view class="m-icon" style="background:var(--dim4-bg)"><AppIcon name="trophy" :size="16" /></view>
|
||||
<text class="m-label">成就</text>
|
||||
<text class="m-arrow">→</text>
|
||||
<AppIcon name="arrow-gray" :size="12" />
|
||||
</view>
|
||||
<view class="menu-item" @click="goLeaderboard">
|
||||
<view class="m-icon" style="background:rgba(255,167,38,0.1);color:#ffa726">🥇</view>
|
||||
<view class="m-icon" style="background:var(--dim3-bg)"><AppIcon name="sparkle" :size="16" /></view>
|
||||
<text class="m-label">排行榜</text>
|
||||
<text class="m-arrow">→</text>
|
||||
<AppIcon name="arrow-gray" :size="12" />
|
||||
</view>
|
||||
<view class="menu-item" @click="goFeedback">
|
||||
<view class="m-icon" style="background:rgba(102,187,106,0.1);color:#66bb6a">💬</view>
|
||||
<view class="m-icon" style="background:var(--dim5-bg)"><AppIcon name="feedback" :size="16" /></view>
|
||||
<text class="m-label">意见反馈</text>
|
||||
<text class="m-arrow">→</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goAbout">
|
||||
<view class="m-icon" style="background:rgba(255,213,79,0.1);color:#ffd54f">ℹ️</view>
|
||||
<text class="m-label">关于 AI 维度</text>
|
||||
<text class="m-arrow">→</text>
|
||||
<AppIcon name="arrow-gray" :size="12" />
|
||||
</view>
|
||||
<view class="menu-item" @click="goSettings">
|
||||
<view class="m-icon" style="background:rgba(239,83,80,0.1);color:#ef5350">⚙️</view>
|
||||
<view class="m-icon" style="background:var(--bg-tertiary)"><AppIcon name="settings-gray" :size="16" /></view>
|
||||
<text class="m-label">设置</text>
|
||||
<text class="m-arrow">→</text>
|
||||
<AppIcon name="arrow-gray" :size="12" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -99,42 +127,98 @@
|
||||
<view class="action-btn primary" @click="goLogin">
|
||||
<text>微信一键登录</text>
|
||||
</view>
|
||||
<!-- 未登录:提供显著有效的「拒绝/暂不登录」入口,避免强制登录 -->
|
||||
<view class="action-btn ghost" @click="goBrowse">
|
||||
<text>暂不登录,先体验</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-btn logout" v-else @click="handleLogout">
|
||||
<text>退出登录</text>
|
||||
<text>退出登录</text>
|
||||
</view>
|
||||
|
||||
<text class="version">宇之然 AI 维度 v1.0.0</text>
|
||||
<text class="version">宇之然 AI 维度 v{{ versionText }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { knowledgeApi, aiChatApi } from '@/utils/api'
|
||||
import { DIMENSIONS } from '@/utils/dimensions'
|
||||
import AppIcon from '@/components/AppIcon.vue'
|
||||
|
||||
const VERSION = '1.3.0'
|
||||
const SUB_GUEST = '游客 · 点亮进度保存在本机,登录后同步'
|
||||
const SUB_USER = '已点亮 {n} 颗星球 · 连续 {d} 天'
|
||||
const PLAN_FREE = '✦ 免费版'
|
||||
const PLAN_DESC_FREE = '每日 5 次 AI 问答 · 答对检测题点亮星球'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const dimensions = DIMENSIONS
|
||||
|
||||
// 已收藏数量
|
||||
const collectedCount = ref(0)
|
||||
const stats = ref(null)
|
||||
const byDim = ref({})
|
||||
const quotaInfo = ref(null)
|
||||
|
||||
const versionText = VERSION
|
||||
|
||||
const profileSubText = computed(() => {
|
||||
if (!userStore.isLoggedIn) return SUB_GUEST
|
||||
if (!stats.value) return '知识星图探索中'
|
||||
return `已点亮 ${stats.value.totalLit} 颗星球 · 连续 ${stats.value.streak} 天`
|
||||
})
|
||||
|
||||
const planTitle = computed(() =>
|
||||
userStore.isSubscribed ? `✦ ${userStore.subscriptionLevel?.toUpperCase() || 'PRO'} 会员` : PLAN_FREE
|
||||
)
|
||||
const planDesc = computed(() =>
|
||||
userStore.isSubscribed ? '全量知识解锁 · AI 问答无限次' : PLAN_DESC_FREE
|
||||
)
|
||||
const planBtnText = computed(() => {
|
||||
if (userStore.isLoggedIn && userStore.isSubscribed) return '已订阅'
|
||||
if (userStore.isLoggedIn) return '升级 Pro'
|
||||
return '登录后升级'
|
||||
})
|
||||
|
||||
const planPercent = computed(() => {
|
||||
const sub = userStore.subInfo
|
||||
if (!sub?.expiresAt || !sub?.startedAt) return 100
|
||||
const total = new Date(sub.expiresAt) - new Date(sub.startedAt)
|
||||
const passed = Date.now() - new Date(sub.startedAt)
|
||||
if (total <= 0) return 100
|
||||
return Math.min(100, Math.max(0, Math.round((passed / total) * 100)))
|
||||
})
|
||||
|
||||
const planExpiryText = computed(() => {
|
||||
if (!userStore.subInfo?.expiresAt) return ''
|
||||
const diffDays = Math.ceil((new Date(userStore.subInfo.expiresAt) - Date.now()) / 86400000)
|
||||
return diffDays <= 0 ? '已过期' : `${diffDays} 天后到期`
|
||||
})
|
||||
|
||||
const quotaPercent = computed(() => {
|
||||
if (!quotaInfo.value) return 0
|
||||
const total = (quotaInfo.value.freeQuota || 5) + (quotaInfo.value.sharedQuota || 0)
|
||||
if (!total) return 0
|
||||
return Math.round((quotaInfo.value.remainingQuota / total) * 100)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
if (userStore.isLoggedIn) {
|
||||
loadUserInfo()
|
||||
userStore.fetchMe()
|
||||
loadCollected()
|
||||
loadQuota()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadUserInfo() {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.token) {
|
||||
await userStore.fetchQuota(userStore.token)
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '')
|
||||
stats.value = data.user
|
||||
byDim.value = data.byDim || {}
|
||||
} catch (e) {
|
||||
// 游客或失败:不显示进度区
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,422 +232,393 @@ async function loadCollected() {
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅信息
|
||||
const planTitle = computed(() => {
|
||||
if (userStore.isSubscribed) {
|
||||
return `✦ ${userStore.subscriptionLevel.toUpperCase()} 会员`
|
||||
}
|
||||
return '✦ 免费版'
|
||||
})
|
||||
|
||||
const planDesc = computed(() => {
|
||||
if (userStore.isSubscribed) {
|
||||
return '全量知识解锁 · AI 问答无限次'
|
||||
}
|
||||
return '每日 5 次免费 AI 问答 · 升级解锁全部知识'
|
||||
})
|
||||
|
||||
const planBtnText = computed(() => {
|
||||
if (userStore.isLoggedIn && userStore.isSubscribed) return '已订阅'
|
||||
if (userStore.isLoggedIn) return '升级 Pro'
|
||||
return '登录后升级'
|
||||
})
|
||||
|
||||
const planExpiryText = computed(() => {
|
||||
if (!userStore.subInfo?.expiresAt) return ''
|
||||
const expires = new Date(userStore.subInfo.expiresAt)
|
||||
const now = new Date()
|
||||
const diffDays = Math.ceil((expires - now) / (1000 * 60 * 60 * 24))
|
||||
if (diffDays <= 0) return '已过期'
|
||||
return `${diffDays} 天后到期`
|
||||
})
|
||||
|
||||
const planPercent = computed(() => {
|
||||
if (!userStore.subInfo?.expiresAt || !userStore.subInfo?.startDate) return 0
|
||||
const start = new Date(userStore.subInfo.startDate).getTime()
|
||||
const expires = new Date(userStore.subInfo.expiresAt).getTime()
|
||||
const now = Date.now()
|
||||
const total = expires - start
|
||||
const elapsed = now - start
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100))
|
||||
})
|
||||
|
||||
// 配额
|
||||
const quotaPercent = computed(() => {
|
||||
const q = userStore.quota
|
||||
if (!q) return 0
|
||||
const remaining = q.remainingQuota || 0
|
||||
const used = q.usedQuota || q.dailyUsed || 0
|
||||
const total = q.freeQuota || remaining + used
|
||||
return total > 0 ? Math.round((used / total) * 100) : 0
|
||||
})
|
||||
|
||||
const quotaUsed = computed(() => userStore.quota?.dailyUsed || userStore.quota?.usedQuota || 0)
|
||||
const quotaTotal = computed(() => {
|
||||
const q = userStore.quota
|
||||
return (q?.freeQuota || 5) + (q?.sharedQuota || 0)
|
||||
})
|
||||
|
||||
const subscriptionBadge = computed(() => {
|
||||
if (userStore.isSubscribed) {
|
||||
return userStore.subscriptionLevel.toUpperCase()
|
||||
}
|
||||
return '免费用户'
|
||||
})
|
||||
|
||||
function goLogin() {
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
async function loadQuota() {
|
||||
try {
|
||||
quotaInfo.value = await aiChatApi.quota(userStore.token)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** 仅浏览:未登录用户可跳过登录直接使用首页 */
|
||||
function goBrowse() {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
function dimPercent(dimId) {
|
||||
const total = byDim.value[dimId] || 0
|
||||
if (!total) return 0
|
||||
return Math.round(((stats.value?.litByDim?.[dimId] || 0) / total) * 100)
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定退出当前账号吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
userStore.logout()
|
||||
collectedCount.value = 0
|
||||
uni.showToast({ title: '已退出', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
function goDim(id) {
|
||||
uni.navigateTo({ url: `/pages/dimension/dimension?id=${id}` })
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
const res = await uni.showModal({ title: '退出登录', content: '确定退出当前账号?' })
|
||||
if (res.confirm) {
|
||||
userStore.logout()
|
||||
stats.value = null
|
||||
uni.showToast({ title: '已退出', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
|
||||
function goBrowse() { uni.switchTab({ url: '/pages/index/index' }) }
|
||||
function goChat() { uni.switchTab({ url: '/pages/chat/chat' }) }
|
||||
function goFavorites() { uni.navigateTo({ url: '/pages/favorites/favorites' }) }
|
||||
function goProgress() { uni.navigateTo({ url: '/pages/progress/progress' }) }
|
||||
function goAchievements() { uni.navigateTo({ url: '/pages/achievements/achievements' }) }
|
||||
function goLeaderboard() { uni.navigateTo({ url: '/pages/leaderboard/leaderboard' }) }
|
||||
function goFeedback() { uni.navigateTo({ url: '/pages/feedback/feedback' }) }
|
||||
function goSettings() { uni.navigateTo({ url: '/pages/settings/settings' }) }
|
||||
function goPayment() {
|
||||
if (!userStore.isLoggedIn) {
|
||||
goLogin()
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
if (userStore.isSubscribed) return
|
||||
uni.navigateTo({ url: '/pages/payment/payment' })
|
||||
}
|
||||
|
||||
function goChat() {
|
||||
uni.switchTab({ url: '/pages/chat/chat' })
|
||||
}
|
||||
|
||||
function goShare() {
|
||||
if (!userStore.token) return
|
||||
uni.showShareMenu({ withShareTicket: true })
|
||||
aiChatApi.shareGain(userStore.token).then(() => {
|
||||
uni.showToast({ title: '已记录分享', icon: 'success' })
|
||||
if (userStore.token) userStore.fetchQuota(userStore.token)
|
||||
}).catch(() => {
|
||||
uni.showToast({ title: '已记录分享', icon: 'success' })
|
||||
})
|
||||
uni.showToast({ title: '已记录分享 +5 次', icon: 'success' })
|
||||
loadQuota()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function goFavorites() {
|
||||
uni.navigateTo({ url: '/pages/favorites/favorites' })
|
||||
}
|
||||
|
||||
function goProgress() {
|
||||
uni.navigateTo({ url: '/pages/progress/progress' })
|
||||
}
|
||||
|
||||
function goAchievements() {
|
||||
uni.navigateTo({ url: '/pages/achievements/achievements' })
|
||||
}
|
||||
|
||||
function goLeaderboard() {
|
||||
uni.navigateTo({ url: '/pages/leaderboard/leaderboard' })
|
||||
}
|
||||
|
||||
function goFeedback() {
|
||||
uni.navigateTo({ url: '/pages/feedback/feedback' })
|
||||
}
|
||||
|
||||
function goAbout() {
|
||||
uni.showModal({
|
||||
title: '关于',
|
||||
content: '宇之然 AI 维度\n从 5 个维度系统理解人工智能\n© 2026 宇之然',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
function goSettings() {
|
||||
uni.navigateTo({ url: '/pages/settings/settings' })
|
||||
}
|
||||
|
||||
// 微信小程序分享
|
||||
// #ifdef MP-WEIXIN
|
||||
onShareAppMessage(() => ({
|
||||
title: 'AI 维度 - 你的AI知识助手',
|
||||
title: '我在 AI 维度点亮了知识星球,来一起探索',
|
||||
path: '/pages/index/index'
|
||||
}))
|
||||
onShareTimeline(() => ({
|
||||
title: 'AI 维度 · 点亮你的知识星球'
|
||||
}))
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-profile {
|
||||
min-height: 100vh;
|
||||
padding: 0 20px 40px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-bar-placeholder { height: 44px; }
|
||||
|
||||
// 用户卡片
|
||||
.profile-card {
|
||||
text-align: center;
|
||||
padding: 20px 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 8px 0 16px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 62px; height: 62px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(124, 77, 255, 0.3), rgba(0, 188, 212, 0.15));
|
||||
border: 2px solid rgba(124, 77, 255, 0.2);
|
||||
margin: 0 auto 14px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 40px rgba(124, 77, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
|
||||
.avatar-img { width: 100%; height: 100%; }
|
||||
.avatar-placeholder {
|
||||
width: 100%; height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-brand), #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-main { flex: 1; min-width: 0; }
|
||||
.profile-name {
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.profile-level {
|
||||
.profile-sub {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// 订阅卡片
|
||||
.plan-card {
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
.streak-chip {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, rgba(124, 77, 255, 0.08), rgba(0, 0, 0, 0.2));
|
||||
border: 1px solid rgba(124, 77, 255, 0.12);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plan-info { flex: 1; }
|
||||
|
||||
.plan-title {
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.plan-desc {
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(229, 28, 28, 0.08);
|
||||
border: 1px solid rgba(229, 28, 28, 0.15);
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
color: var(--dim5-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plan-progress {
|
||||
margin-top: 10px;
|
||||
/* 学习数据 */
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 3px;
|
||||
background: var(--glass-bg);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plan-progress .progress-track {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--dim1-color), var(--dim3-color));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.plan-btn {
|
||||
background: linear-gradient(135deg, #7c4dff, #b388ff);
|
||||
padding: 10px 22px;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 0 20px rgba(124, 77, 255, 0.2);
|
||||
}
|
||||
|
||||
.plan-btn.disabled {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// 配额卡片
|
||||
.quota-card {
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
.stat-tile {
|
||||
flex: 1;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
|
||||
.stat-num {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.quota-card-title {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.quota-bar {
|
||||
height: 4px;
|
||||
/* 五星域进度 */
|
||||
.dims-progress {
|
||||
background: var(--glass-bg);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 6px 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
.dim-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.dim-dot {
|
||||
width: 26px; height: 26px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dim-name {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
width: 56px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dim-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
.dim-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
.dim-frac {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 订阅 */
|
||||
.plan-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(135deg, #1a1a3a, #2d2b5e);
|
||||
border: 1px solid rgba(123, 135, 253, 0.3);
|
||||
}
|
||||
.plan-info { flex: 1; min-width: 0; }
|
||||
.plan-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.plan-desc {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.plan-progress {
|
||||
margin-top: 8px;
|
||||
.progress-track {
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #7b87fd, #a2b2ff);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.progress-text {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 9px 16px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--color-brand), #8b5cf6);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.disabled { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* 配额 */
|
||||
.quota-card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
.quota-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.quota-card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
.quota-bar {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-tertiary);
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.quota-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4dd0e1, #b388ff);
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(90deg, var(--color-brand), var(--color-brand-light));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.quota-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
.quota-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.quota-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.quota-share {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.quota-note {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.quota-share {
|
||||
font-size: 11px;
|
||||
color: var(--dim1-color);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// 菜单
|
||||
/* 菜单 */
|
||||
.menu-section {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--card-shadow, none);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 15px 16px;
|
||||
gap: 12px;
|
||||
padding: 13px 0;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.m-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.m-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.m-arrow {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.m-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(179, 136, 255, 0.12);
|
||||
color: var(--dim1-color);
|
||||
}
|
||||
|
||||
// 按钮
|
||||
.action-row {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
font-size: 14px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #7c4dff, #b388ff);
|
||||
box-shadow: 0 0 20px rgba(124, 77, 255, 0.2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-btn.ghost {
|
||||
color: var(--text-secondary);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.action-btn.logout {
|
||||
margin-top: 20px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 操作 */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 46px;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light));
|
||||
color: #fff;
|
||||
}
|
||||
&.ghost {
|
||||
background: var(--glass-bg);
|
||||
border: 1.5px solid var(--glass-border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
&.logout {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
background: var(--glass-bg);
|
||||
border: 1.5px solid var(--glass-border);
|
||||
color: var(--dim5-color);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.1);
|
||||
margin-top: 12px;
|
||||
color: var(--text-muted);
|
||||
padding: 8px 0 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
@confirm="doSearch"
|
||||
confirm-type="search"
|
||||
/>
|
||||
<view class="search-icon">🔍</view>
|
||||
<view class="search-icon"></view>
|
||||
<view v-if="searchText" class="clear-btn" @click="clearSearch">✕</view>
|
||||
</view>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<!-- 空搜索提示 -->
|
||||
<view class="empty-state" v-else-if="!hasSearched">
|
||||
<text class="empty-icon">🔍</text>
|
||||
<text class="empty-icon">没有找到相关知识</text>
|
||||
<text class="empty-text">输入关键词搜索 AI 知识</text>
|
||||
<text class="empty-hint">支持按标题、摘要、标签搜索</text>
|
||||
</view>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<view class="section-title">偏好设置</view>
|
||||
<view class="card">
|
||||
<view class="setting-item" @click="togglePush">
|
||||
<view class="s-icon" style="background:rgba(255,213,79,0.1);color:#ffd54f">🔔</view>
|
||||
<view class="s-icon" style="background:rgba(91,95,248,0.1)">铃</view>
|
||||
<view class="s-text">
|
||||
<text class="s-label">推送通知</text>
|
||||
<text class="s-desc">接收每日 AI 趋势推送</text>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<view class="card-footer">
|
||||
<text class="card-date">{{ formatDate(item.newsDate) }}</text>
|
||||
<view class="card-stats">
|
||||
<text class="stat-item" v-if="item.hot">🔥 {{ item.hotCount }}</text>
|
||||
<text class="stat-item" v-if="item.hot">{{ item.hotCount }} 人觉得热</text>
|
||||
<text class="stat-item">👁️ {{ item.viewCount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -56,7 +56,7 @@
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="displayItems.length === 0" class="empty-state">
|
||||
<text class="empty-icon">📡</text>
|
||||
<text class="empty-icon">暂时没有快讯</text>
|
||||
<text class="empty-text">暂无趋势资讯</text>
|
||||
<text class="empty-sub">稍后再来看最新 AI 动态</text>
|
||||
</view>
|
||||
@@ -72,7 +72,7 @@ import { trendApi } from '@/utils/api'
|
||||
|
||||
const filters = ref([
|
||||
{ icon: '📰', text: '全部' },
|
||||
{ icon: '🚀', text: '产品' },
|
||||
{ icon: 'rocket', text: '产品' },
|
||||
{ icon: '📄', text: '论文' },
|
||||
{ icon: '🔧', text: '工具' },
|
||||
{ icon: '🏢', text: '公司' }
|
||||
@@ -116,9 +116,9 @@ async function fetchTrends() {
|
||||
}
|
||||
|
||||
function getTagText(item) {
|
||||
if (item.hot) return '🔥 热门'
|
||||
if (item.picked) return '📌 精选'
|
||||
if (item.category === '产品') return '🚀 产品'
|
||||
if (item.hot) return '热门'
|
||||
if (item.picked) return '精选'
|
||||
if (item.category === '产品') return '产品'
|
||||
if (item.category === '论文') return '📄 论文'
|
||||
if (item.category === '工具') return '🔧 工具'
|
||||
if (item.category === '公司') return '🏢 公司'
|
||||
@@ -311,7 +311,7 @@ onShareAppMessage(() => ({
|
||||
|
||||
.card-tag.pick {
|
||||
background: rgba(124, 77, 255, 0.12);
|
||||
color: #b388ff;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.card-tag.product {
|
||||
@@ -321,7 +321,7 @@ onShareAppMessage(() => ({
|
||||
|
||||
.card-tag.paper {
|
||||
background: rgba(179, 136, 255, 0.12);
|
||||
color: #b388ff;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.card-tag.tool {
|
||||
|
||||
Reference in New Issue
Block a user