初始化:职引项目 v1.0

This commit is contained in:
yuzhiran
2026-06-08 16:28:00 +08:00
commit 511f60d0db
111 changed files with 27295 additions and 0 deletions
+242
View File
@@ -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>