625 lines
17 KiB
Vue
625 lines
17 KiB
Vue
<template>
|
||
<view class="page-profile page-layout">
|
||
<view class="status-bar-placeholder"></view>
|
||
|
||
<!-- 学习者档案头 -->
|
||
<view class="profile-card">
|
||
<view class="profile-avatar">
|
||
<image
|
||
v-if="userStore.userInfo?.avatarUrl"
|
||
:src="userStore.userInfo.avatarUrl"
|
||
class="avatar-img"
|
||
mode="aspectFill"
|
||
/>
|
||
<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>
|
||
</view>
|
||
|
||
<!-- 订阅卡片 -->
|
||
<view class="plan-card" @click="goPayment">
|
||
<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>
|
||
</view>
|
||
</view>
|
||
<view class="plan-btn" :class="{ disabled: userStore.isSubscribed }">
|
||
<text>{{ planBtnText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI 问答配额 -->
|
||
<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>
|
||
<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:var(--dim1-bg)"><AppIcon name="star" :size="16" /></view>
|
||
<text class="m-label">我的收藏</text>
|
||
<text class="m-badge">{{ collectedCount }}</text>
|
||
<AppIcon name="arrow-gray" :size="12" />
|
||
</view>
|
||
<view class="menu-item" @click="goProgress">
|
||
<view class="m-icon" style="background:var(--dim2-bg)"><AppIcon name="rocket" :size="16" /></view>
|
||
<text class="m-label">学习进度</text>
|
||
<AppIcon name="arrow-gray" :size="12" />
|
||
</view>
|
||
<view class="menu-item" @click="goAchievements">
|
||
<view class="m-icon" style="background:var(--dim4-bg)"><AppIcon name="trophy" :size="16" /></view>
|
||
<text class="m-label">成就</text>
|
||
<AppIcon name="arrow-gray" :size="12" />
|
||
</view>
|
||
<view class="menu-item" @click="goLeaderboard">
|
||
<view class="m-icon" style="background:var(--dim3-bg)"><AppIcon name="sparkle" :size="16" /></view>
|
||
<text class="m-label">排行榜</text>
|
||
<AppIcon name="arrow-gray" :size="12" />
|
||
</view>
|
||
<view class="menu-item" @click="goFeedback">
|
||
<view class="m-icon" style="background:var(--dim5-bg)"><AppIcon name="feedback" :size="16" /></view>
|
||
<text class="m-label">意见反馈</text>
|
||
<AppIcon name="arrow-gray" :size="12" />
|
||
</view>
|
||
<view class="menu-item" @click="goSettings">
|
||
<view class="m-icon" style="background:var(--bg-tertiary)"><AppIcon name="settings-gray" :size="16" /></view>
|
||
<text class="m-label">设置</text>
|
||
<AppIcon name="arrow-gray" :size="12" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 登录/退出 -->
|
||
<view class="action-row" v-if="!userStore.isLoggedIn">
|
||
<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>
|
||
</view>
|
||
|
||
<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.2.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) {
|
||
userStore.fetchMe()
|
||
loadCollected()
|
||
loadQuota()
|
||
}
|
||
})
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const data = await knowledgeApi.stats(userStore.isLoggedIn ? userStore.token : '')
|
||
stats.value = data.user
|
||
byDim.value = data.byDim || {}
|
||
} catch (e) {
|
||
// 游客或失败:不显示进度区
|
||
}
|
||
}
|
||
|
||
async function loadCollected() {
|
||
if (!userStore.token) return
|
||
try {
|
||
const list = await knowledgeApi.collected(userStore.token)
|
||
collectedCount.value = list.length
|
||
} catch (e) {
|
||
collectedCount.value = 0
|
||
}
|
||
}
|
||
|
||
async function loadQuota() {
|
||
try {
|
||
quotaInfo.value = await aiChatApi.quota(userStore.token)
|
||
} catch (e) {}
|
||
}
|
||
|
||
function dimPercent(dimId) {
|
||
const total = byDim.value[dimId] || 0
|
||
if (!total) return 0
|
||
return Math.round(((stats.value?.litByDim?.[dimId] || 0) / total) * 100)
|
||
}
|
||
|
||
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) {
|
||
uni.navigateTo({ url: '/pages/login/login' })
|
||
return
|
||
}
|
||
uni.navigateTo({ url: '/pages/payment/payment' })
|
||
}
|
||
|
||
function goShare() {
|
||
if (!userStore.token) return
|
||
uni.showShareMenu({ withShareTicket: true })
|
||
aiChatApi.shareGain(userStore.token).then(() => {
|
||
uni.showToast({ title: '已记录分享 +5 次', icon: 'success' })
|
||
loadQuota()
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// #ifdef MP-WEIXIN
|
||
onShareAppMessage(() => ({
|
||
title: '我在 AI 维度点亮了知识星球,来一起探索',
|
||
path: '/pages/index/index'
|
||
}))
|
||
onShareTimeline(() => ({
|
||
title: 'AI 维度 · 点亮你的知识星球'
|
||
}))
|
||
// #endif
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-profile {
|
||
min-height: 100vh;
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
.status-bar-placeholder { height: 44px; }
|
||
|
||
.profile-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 8px 0 16px;
|
||
}
|
||
|
||
.profile-avatar {
|
||
width: 62px; height: 62px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.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 {
|
||
display: block;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
margin-bottom: 4px;
|
||
}
|
||
.profile-sub {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: var(--text-tertiary);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.streak-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
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;
|
||
font-weight: 700;
|
||
color: var(--dim5-color);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 学习数据 */
|
||
.stats-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.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);
|
||
}
|
||
}
|
||
|
||
/* 五星域进度 */
|
||
.dims-progress {
|
||
background: var(--glass-bg);
|
||
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%;
|
||
border-radius: 3px;
|
||
background: linear-gradient(90deg, var(--color-brand), var(--color-brand-light));
|
||
transition: width 0.3s ease;
|
||
}
|
||
.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);
|
||
}
|
||
}
|
||
|
||
/* 菜单 */
|
||
.menu-section {
|
||
background: var(--glass-bg);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: var(--radius-lg);
|
||
padding: 4px 16px;
|
||
margin-bottom: 16px;
|
||
box-shadow: var(--card-shadow, none);
|
||
}
|
||
.menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 13px 0;
|
||
border-bottom: 1px solid var(--glass-border);
|
||
|
||
&:last-child { border-bottom: none; }
|
||
}
|
||
.m-icon {
|
||
width: 32px; height: 32px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.m-label {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: var(--text-primary);
|
||
}
|
||
.m-badge {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* 操作 */
|
||
.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: var(--text-muted);
|
||
padding: 8px 0 20px;
|
||
}
|
||
</style>
|