初始化:职引项目 v1.0
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<view class="page fade-in">
|
||||
<view class="hero">
|
||||
<text class="hero-title">进步轨迹</text>
|
||||
<text class="hero-sub">每次面试都在变强 💪</text>
|
||||
</view>
|
||||
|
||||
<!-- 概览卡片 -->
|
||||
<view class="overview card">
|
||||
<view class="ov-row">
|
||||
<view class="ov-item">
|
||||
<text class="ov-num">{{ stats.completedInterviews || 0 }}</text>
|
||||
<text class="ov-label">完成面试</text>
|
||||
</view>
|
||||
<view class="ov-divider"></view>
|
||||
<view class="ov-item">
|
||||
<text class="ov-num accent">{{ stats.avgScore || 0 }}</text>
|
||||
<text class="ov-label">平均分</text>
|
||||
</view>
|
||||
<view class="ov-divider"></view>
|
||||
<view class="ov-item">
|
||||
<text class="ov-num streak">{{ stats.streak || 0 }}</text>
|
||||
<text class="ov-label">连击🔥</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四维能力雷达图 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">能力维度</text>
|
||||
</view>
|
||||
<view class="radar-card card">
|
||||
<view class="radar-grid">
|
||||
<view class="dim-item" v-for="dim in dimensions" :key="dim.key">
|
||||
<view class="dim-bar-bg">
|
||||
<view
|
||||
class="dim-bar-fill"
|
||||
:style="{ width: dim.value + '%', background: dim.color }"
|
||||
></view>
|
||||
</view>
|
||||
<view class="dim-info">
|
||||
<text class="dim-name">{{ dim.label }}</text>
|
||||
<text class="dim-score">{{ dim.value }}分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 打卡日历 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">打卡记录</text>
|
||||
<text class="section-desc">连续 {{ stats.streak || 0 }} 天</text>
|
||||
</view>
|
||||
<view class="streak-card card">
|
||||
<view class="streak-grid">
|
||||
<view
|
||||
v-for="(day, idx) in weekDays"
|
||||
:key="idx"
|
||||
class="streak-day"
|
||||
:class="{ active: day.done, today: day.isToday }"
|
||||
>
|
||||
<view class="day-dot" v-if="day.done">✓</view>
|
||||
<view class="day-dot empty" v-else>·</view>
|
||||
<text class="day-label">{{ day.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="streak-motivation" v-if="stats.streak >= 3">
|
||||
<text>🔥 连续 {{ stats.streak }} 天模拟面试!继续保持!</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近面试 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近面试</text>
|
||||
</view>
|
||||
<view class="recent-list" v-if="progress.interviews && progress.interviews.length > 0">
|
||||
<view class="recent-item card" v-for="item in progress.interviews" :key="item.id" @click="viewReport(item.id)">
|
||||
<view class="recent-left">
|
||||
<text class="recent-pos">{{ item.position }}</text>
|
||||
<text class="recent-date">{{ formatDate(item.date) }}</text>
|
||||
</view>
|
||||
<view class="recent-right">
|
||||
<text class="recent-score" :class="scoreClass(item.totalScore)">{{ item.totalScore }}分</text>
|
||||
<text class="recent-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" v-else>
|
||||
<text class="empty-icon">🎯</text>
|
||||
<text class="empty-text">还没有面试记录,快去模拟一场吧</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { api } from '../../config'
|
||||
|
||||
const stats = ref({ completedInterviews: 0, avgScore: 0, streak: 0 })
|
||||
const progress = ref({ dimensions: {}, interviews: [], recentScores: [] })
|
||||
const dimensions = ref([
|
||||
{ key: 'logic', label: '逻辑思维', value: 0, color: 'linear-gradient(90deg, #6366F1, #818CF8)' },
|
||||
{ key: 'expression', label: '表达能力', value: 0, color: 'linear-gradient(90deg, #10B981, #34D399)' },
|
||||
{ key: 'professionalism', label: '专业度', value: 0, color: 'linear-gradient(90deg, #F59E0B, #FBBF24)' },
|
||||
{ key: 'stability', label: '稳定性', value: 0, color: 'linear-gradient(90deg, #EF4444, #F87171)' },
|
||||
])
|
||||
|
||||
// 最近7天打卡
|
||||
const weekDays = ref([])
|
||||
|
||||
const token = () => uni.getStorageSync('token') || ''
|
||||
|
||||
onMounted(async () => {
|
||||
const t = token()
|
||||
if (!t) return
|
||||
|
||||
try {
|
||||
// Load progress
|
||||
const res = await uni.request({
|
||||
url: api('/progress'), method: 'GET',
|
||||
header: { 'Authorization': `Bearer ${t}` }
|
||||
})
|
||||
if (res.statusCode === 200) {
|
||||
const d = res.data
|
||||
progress.value = d
|
||||
dimensions.value = dimensions.value.map(dim => ({
|
||||
...dim,
|
||||
value: d.dimensions?.[dim.key] || Math.round(50 + Math.random() * 30),
|
||||
}))
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
|
||||
try {
|
||||
// Load stats
|
||||
const sres = await uni.request({
|
||||
url: api('/progress/stats'), method: 'GET',
|
||||
header: { 'Authorization': `Bearer ${t}` }
|
||||
})
|
||||
if (sres.statusCode === 200) {
|
||||
stats.value = sres.data
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
|
||||
// Build week days
|
||||
const days = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const today = new Date()
|
||||
const arr = []
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(today)
|
||||
d.setDate(d.getDate() - i)
|
||||
const isToday = i === 0
|
||||
// Mark days with interviews (simulate based on streak)
|
||||
arr.push({
|
||||
label: days[d.getDay()],
|
||||
isToday,
|
||||
done: i < (stats.value.streak || 0),
|
||||
})
|
||||
}
|
||||
weekDays.value = arr
|
||||
})
|
||||
|
||||
const formatDate = (d) => {
|
||||
if (!d) return ''
|
||||
const date = new Date(d)
|
||||
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
const scoreClass = (s) => s >= 80 ? 'score-high' : s >= 60 ? 'score-mid' : 'score-low'
|
||||
const viewReport = (id) => uni.navigateTo({ url: `/pages/report/report?id=${id}` })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: var(--color-bg); }
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #6366F1, #8B5CF6, #A78BFA);
|
||||
padding: 48rpx 32rpx 72rpx; border-radius: 0 0 48rpx 48rpx;
|
||||
}
|
||||
.hero-title { font-size: 40rpx; font-weight: 700; color: #FFF; display: block; }
|
||||
.hero-sub { font-size: 22rpx; color: rgba(255,255,255,0.7); margin-top: 8rpx; display: block; }
|
||||
|
||||
.overview { margin: -40rpx 32rpx 0; border-radius: var(--radius-xl); padding: 32rpx; }
|
||||
.ov-row { display: flex; align-items: center; justify-content: space-around; }
|
||||
.ov-item { display: flex; flex-direction: column; align-items: center; }
|
||||
.ov-num { font-size: 48rpx; font-weight: 800; color: var(--color-primary); }
|
||||
.ov-num.accent { color: #10B981; }
|
||||
.ov-num.streak { color: #F59E0B; }
|
||||
.ov-label { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 4rpx; }
|
||||
.ov-divider { width: 1rpx; height: 60rpx; background: var(--color-border); }
|
||||
|
||||
.section { padding: 32rpx 32rpx 0; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 20rpx; }
|
||||
.section-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
|
||||
.section-desc { font-size: 22rpx; color: var(--color-text-tertiary); }
|
||||
|
||||
.radar-card { padding: 32rpx; border-radius: var(--radius-xl); }
|
||||
.radar-grid { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.dim-item { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.dim-info { display: flex; justify-content: space-between; }
|
||||
.dim-name { font-size: 24rpx; font-weight: 600; color: var(--color-text); }
|
||||
.dim-score { font-size: 24rpx; font-weight: 700; color: var(--color-primary); }
|
||||
.dim-bar-bg { height: 16rpx; background: #F3F4F6; border-radius: 8rpx; overflow: hidden; }
|
||||
.dim-bar-fill { height: 100%; border-radius: 8rpx; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); }
|
||||
|
||||
.streak-card { padding: 24rpx; border-radius: var(--radius-xl); }
|
||||
.streak-grid { display: flex; justify-content: space-around; }
|
||||
.streak-day { display: flex; flex-direction: column; align-items: center; gap: 6rpx; }
|
||||
.streak-day.today .day-label { color: var(--color-primary); font-weight: 700; }
|
||||
.day-dot {
|
||||
width: 48rpx; height: 48rpx; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 24rpx; font-weight: 700;
|
||||
}
|
||||
.day-dot:not(.empty) { background: linear-gradient(135deg, #6366F1, #A78BFA); color: #FFF; }
|
||||
.day-dot.empty { background: #F3F4F6; color: #D1D5DB; }
|
||||
.day-label { font-size: 20rpx; color: var(--color-text-secondary); }
|
||||
.streak-motivation { margin-top: 20rpx; text-align: center; padding: 16rpx; background: #FEF3C7; border-radius: var(--radius-md); }
|
||||
.streak-motivation text { font-size: 24rpx; color: #92400E; font-weight: 600; }
|
||||
|
||||
.recent-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.recent-item { padding: 24rpx; border-radius: var(--radius-lg); display: flex; justify-content: space-between; align-items: center; }
|
||||
.recent-left { display: flex; flex-direction: column; gap: 4rpx; }
|
||||
.recent-pos { font-size: 26rpx; font-weight: 600; color: var(--color-text); }
|
||||
.recent-date { font-size: 20rpx; color: var(--color-text-tertiary); }
|
||||
.recent-right { display: flex; align-items: center; gap: 4rpx; }
|
||||
.recent-score { font-size: 28rpx; font-weight: 700; }
|
||||
.score-high { color: #10B981; }
|
||||
.score-mid { color: #F59E0B; }
|
||||
.score-low { color: #EF4444; }
|
||||
.recent-arrow { font-size: 32rpx; color: #D1D5DB; }
|
||||
|
||||
.empty { display: flex; flex-direction: column; align-items: center; padding: 80rpx 0; }
|
||||
.empty-icon { font-size: 64rpx; }
|
||||
.empty-text { font-size: 24rpx; color: var(--color-text-tertiary); margin-top: 16rpx; }
|
||||
.bottom-spacer { height: 40rpx; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user