450 lines
10 KiB
Vue
450 lines
10 KiB
Vue
<template>
|
|
<view class="page profile-page">
|
|
<text class="page-title">我</text>
|
|
<text class="page-subtitle">{{ phone || '未登录' }}</text>
|
|
|
|
<view v-if="message" class="status-message">{{ message }}</view>
|
|
|
|
<view class="panel account-card">
|
|
<view>
|
|
<text class="panel-title">账号</text>
|
|
<text class="account-phone">{{ phone || '未登录' }}</text>
|
|
</view>
|
|
<button v-if="!loggedIn" class="primary-button account-action" @click="goLogin">去登录</button>
|
|
<button v-else class="secondary-button account-action" @click="logout">退出</button>
|
|
</view>
|
|
|
|
<view v-if="profile" class="panel">
|
|
<text class="panel-title">成长摘要</text>
|
|
<view class="score-row">
|
|
<view class="score-main">
|
|
<text class="score-value">{{ profile.score }}</text>
|
|
<text class="score-label">综合能力</text>
|
|
</view>
|
|
<view class="metric-grid">
|
|
<view class="metric">
|
|
<text>{{ profile.completed }}</text>
|
|
<text>已训练</text>
|
|
</view>
|
|
<view class="metric">
|
|
<text>{{ profile.pendingReview }}</text>
|
|
<text>待复盘</text>
|
|
</view>
|
|
<view class="metric">
|
|
<text>{{ profile.trainingMinutes || 0 }}</text>
|
|
<text>分钟</text>
|
|
</view>
|
|
<view class="metric">
|
|
<text>{{ profile.aiLevel || '未评级' }}</text>
|
|
<text>AI等级</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-if="profile.dimensions?.length" class="dimension-list">
|
|
<view v-for="item in profile.dimensions" :key="item.label" class="dimension-row">
|
|
<view>
|
|
<text class="dimension-title">{{ item.label }}</text>
|
|
<text class="dimension-note">{{ item.note }}</text>
|
|
</view>
|
|
<text class="dimension-score">{{ item.value }}</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="profile.growthPath?.length" class="growth-section">
|
|
<text class="section-label">成长路径</text>
|
|
<view v-for="stage in profile.growthPath" :key="stage.level" class="growth-row">
|
|
<view class="growth-copy">
|
|
<text class="growth-title">{{ stage.level }} · {{ stage.status }}</text>
|
|
<text class="growth-desc">{{ stage.evidence }}</text>
|
|
</view>
|
|
<view class="growth-progress">
|
|
<view class="growth-bar">
|
|
<view class="growth-fill" :style="{ width: `${stage.progress}%` }" />
|
|
</view>
|
|
<text>{{ stage.progress }}%</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="evidence" class="panel evidence-panel">
|
|
<text class="panel-title">成长证据包</text>
|
|
<view class="evidence-grid">
|
|
<view class="evidence-metric">
|
|
<text>{{ evidence.currentLevel }}</text>
|
|
<text>当前等级</text>
|
|
</view>
|
|
<view class="evidence-metric">
|
|
<text>{{ evidence.assessmentScore }}</text>
|
|
<text>测评分</text>
|
|
</view>
|
|
<view class="evidence-metric">
|
|
<text>{{ evidence.trainingMinutes }}</text>
|
|
<text>训练分钟</text>
|
|
</view>
|
|
<view class="evidence-metric">
|
|
<text>{{ evidence.contribution }}</text>
|
|
<text>贡献度</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="evidence.evidenceItems?.length" class="evidence-list">
|
|
<text v-for="item in evidence.evidenceItems" :key="item" class="evidence-item">{{ item }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="loggedIn" class="panel">
|
|
<view class="history-head">
|
|
<text class="panel-title">最近练过</text>
|
|
<button class="ghost-button refresh-button" :disabled="loading" @click="loadData">刷新</button>
|
|
</view>
|
|
<view v-if="history.length" class="history-list">
|
|
<view v-for="item in history" :key="item.id || item.sessionId || item.time" class="history-row">
|
|
<view>
|
|
<text class="history-title">{{ item.scene }}</text>
|
|
<text class="history-desc">{{ item.time }} · {{ item.summary }}</text>
|
|
</view>
|
|
<view class="history-score">
|
|
<text>{{ item.score }}</text>
|
|
<text>{{ item.status }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-else class="empty">暂无训练记录</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import { onShow } from '@dcloudio/uni-app';
|
|
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
|
|
import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
|
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
|
|
import { resetPageScroll } from '@/services/navigation';
|
|
|
|
const loggedIn = ref(false);
|
|
const phone = ref('');
|
|
const loading = ref(false);
|
|
const message = ref('');
|
|
const profile = ref<CompetencyProfile | null>(null);
|
|
const history = ref<PracticeRecord[]>([]);
|
|
const evidence = ref<PromotionEvidence | null>(null);
|
|
|
|
const refreshAuth = () => {
|
|
const auth = getAuth();
|
|
loggedIn.value = isLoggedIn();
|
|
phone.value = auth.phone;
|
|
};
|
|
|
|
const clearProfileData = () => {
|
|
profile.value = null;
|
|
history.value = [];
|
|
evidence.value = null;
|
|
};
|
|
|
|
const loadData = async () => {
|
|
refreshAuth();
|
|
if (!loggedIn.value) {
|
|
clearProfileData();
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
message.value = '';
|
|
try {
|
|
const [nextHistory, nextProfile, nextEvidence] = await Promise.all([
|
|
getPracticeHistory(),
|
|
getCompetencyProfile(),
|
|
getPromotionEvidence()
|
|
]);
|
|
history.value = nextHistory || [];
|
|
profile.value = nextProfile || null;
|
|
evidence.value = nextEvidence || null;
|
|
} catch (error) {
|
|
refreshAuth();
|
|
if (!loggedIn.value) {
|
|
clearProfileData();
|
|
message.value = '登录已过期,请重新登录';
|
|
return;
|
|
}
|
|
message.value = error instanceof Error ? error.message : '我的数据加载失败';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const goLogin = () => {
|
|
rememberLoginRedirect('/pages/user/profile/index');
|
|
uni.navigateTo({ url: '/pages/auth/login/index' });
|
|
};
|
|
|
|
const logout = () => {
|
|
clearAuth();
|
|
clearProfileData();
|
|
refreshAuth();
|
|
uni.showToast({ title: '已退出', icon: 'none' });
|
|
};
|
|
|
|
onShow(() => {
|
|
resetPageScroll();
|
|
void loadData();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.profile-page {
|
|
padding-bottom: 92px;
|
|
background: #f4f7fb;
|
|
}
|
|
|
|
.account-card,
|
|
.history-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.account-phone {
|
|
display: block;
|
|
margin-top: 8px;
|
|
color: #64748b;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.account-action {
|
|
min-width: 84px;
|
|
}
|
|
|
|
.score-row {
|
|
display: grid;
|
|
margin-top: 16px;
|
|
grid-template-columns: 104px 1fr;
|
|
gap: 14px;
|
|
}
|
|
|
|
.score-main {
|
|
display: flex;
|
|
min-height: 124px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 18px;
|
|
background: #eff6ff;
|
|
}
|
|
|
|
.score-value {
|
|
color: #1d4ed8;
|
|
font-size: 42px;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
}
|
|
|
|
.score-label {
|
|
margin-top: 8px;
|
|
color: #475569;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.metric-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.metric {
|
|
display: flex;
|
|
min-height: 58px;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
padding: 8px 10px;
|
|
border-radius: 14px;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.metric text:first-child {
|
|
color: #0f172a;
|
|
font-size: 18px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.metric text:last-child {
|
|
margin-top: 4px;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.evidence-panel {
|
|
background: #fff7ed;
|
|
}
|
|
|
|
.evidence-grid {
|
|
display: grid;
|
|
margin-top: 14px;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.evidence-metric {
|
|
display: flex;
|
|
min-height: 58px;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
padding: 8px 10px;
|
|
border-radius: 14px;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.evidence-metric text:first-child {
|
|
color: #9a3412;
|
|
font-size: 18px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.evidence-metric text:last-child {
|
|
margin-top: 4px;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.evidence-list {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.evidence-item {
|
|
display: block;
|
|
padding: 9px 0;
|
|
border-top: 1px solid #fed7aa;
|
|
color: #7c2d12;
|
|
font-size: 13px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.dimension-list,
|
|
.history-list {
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.growth-section {
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.section-label {
|
|
display: block;
|
|
color: #334155;
|
|
font-size: 14px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.growth-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 96px;
|
|
gap: 12px;
|
|
padding: 12px 0;
|
|
border-top: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.growth-row:first-of-type {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.growth-title,
|
|
.growth-desc {
|
|
display: block;
|
|
}
|
|
|
|
.growth-title {
|
|
color: #0f172a;
|
|
font-size: 14px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.growth-desc {
|
|
margin-top: 5px;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.growth-progress {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
color: #1d4ed8;
|
|
font-size: 12px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.growth-bar {
|
|
width: 88px;
|
|
height: 6px;
|
|
overflow: hidden;
|
|
border-radius: 999px;
|
|
background: #dbeafe;
|
|
}
|
|
|
|
.growth-fill {
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
background: #2563eb;
|
|
}
|
|
|
|
.dimension-row,
|
|
.history-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 12px 0;
|
|
border-top: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.dimension-title,
|
|
.history-title {
|
|
display: block;
|
|
color: #0f172a;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.dimension-note,
|
|
.history-desc {
|
|
display: block;
|
|
margin-top: 5px;
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.dimension-score {
|
|
color: #1d4ed8;
|
|
font-size: 20px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.refresh-button {
|
|
min-height: 36px;
|
|
padding: 0 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.history-score {
|
|
display: flex;
|
|
min-width: 52px;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.history-score text:first-child {
|
|
color: #1d4ed8;
|
|
font-size: 20px;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.history-score text:last-child {
|
|
margin-top: 4px;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|