Files
zhiyin/zhiyin-app/src/pages/history/history.vue
T
2026-06-08 16:28:00 +08:00

159 lines
7.8 KiB
Vue

<template>
<view class="page fade-in">
<view class="hero">
<text class="hero-title">面试记录</text>
<text class="hero-sub">回顾你的成长轨迹</text>
</view>
<view class="stats-bar card">
<view class="stat">
<text class="stat-val">{{ interviewList.length }}</text>
<text class="stat-lbl">总次数</text>
</view>
<view class="stat-sep"></view>
<view class="stat">
<text class="stat-val">{{ avgScore }}</text>
<text class="stat-lbl">平均分</text>
</view>
<view class="stat-sep"></view>
<view class="stat">
<text class="stat-val">{{ completedCount }}</text>
<text class="stat-lbl">已完成</text>
</view>
</view>
<view class="filter-wrap">
<view class="filter-inner">
<view class="filter-tab" :class="{ active: filter === 'all' }" @click="filter = 'all'">全部</view>
<view class="filter-tab" :class="{ active: filter === 'completed' }" @click="filter = 'completed'">已完成</view>
<view class="filter-tab" :class="{ active: filter === 'analyzing' }" @click="filter = 'analyzing'">进行中</view>
</view>
</view>
<view class="list" v-if="filteredList.length > 0">
<view class="record-card card" v-for="(item, idx) in filteredList" :key="idx">
<view class="record-top" @click="goDetail(item)">
<view class="record-icon">{{ item.score >= 80 ? '🌟' : item.score > 0 ? '📋' : '💬' }}</view>
<view class="record-body">
<view class="record-name">{{ item.position }}</view>
<text class="record-meta">{{ item.time }} · {{ item.duration }}</text>
</view>
<view class="record-score" :class="scoreLevel(item.score)">
{{ item.score ? item.score : '--' }}
</view>
</view>
<view class="record-actions" v-if="item.score > 0">
<text class="rec-action" @click="goContribute(item)">💡 贡献面经</text>
</view>
</view>
</view>
<view class="loading-tip" v-if="loading">加载中...</view>
<view class="empty" v-else>
<text class="empty-icon">{{ filter !== 'all' ? '🔍' : '📭' }}</text>
<text class="empty-title">{{ emptyTitle }}</text>
<text class="empty-desc">{{ emptyDesc }}</text>
<button class="empty-btn btn-gradient" @click="goInterview" v-if="filter === 'all'">开始第一次面试</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { api } from '../../config'
const filter = ref('all')
const interviewList = ref([])
const loading = ref(true)
const completedCount = computed(() => interviewList.value.filter(i => i.score > 0).length)
const avgScore = computed(() => {
const scored = interviewList.value.filter(i => i.score > 0)
if (scored.length === 0) return '--'
return Math.round(scored.reduce((s, i) => s + i.score, 0) / scored.length)
})
const filteredList = computed(() => {
if (filter.value === 'all') return interviewList.value
if (filter.value === 'completed') return interviewList.value.filter(i => i.score > 0)
return interviewList.value.filter(i => i.score === 0)
})
const emptyTitle = computed(() => {
if (filter.value === 'all') return '暂无面试记录'
if (filter.value === 'completed') return '暂无已完成面试'
return '暂无进行中面试'
})
const emptyDesc = computed(() => {
if (filter.value === 'all') return '完成你的第一场模拟面试吧'
if (filter.value === 'completed') return '继续面试练习'
return '所有面试都已评价完成'
})
const formatDate = (d) => {
if (!d) return '--'
const date = new Date(d)
return `${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')} ${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`
}
onMounted(async () => {
const token = uni.getStorageSync('token') || ''
if (!token) { loading.value = false; return }
try {
const res = await uni.request({ url: api('/interview/list/all'), method: 'GET', header: { 'Authorization': `Bearer ${token}` } })
if (res.statusCode === 200 && Array.isArray(res.data)) {
interviewList.value = res.data.map(i => ({
position: i.position || '通用岗位', time: formatDate(i.createdAt || i.time),
score: i.totalScore || 0, duration: `${i.questionCount || 0}`, id: i.id,
}))
}
} catch(e) { console.error(e) }
finally { loading.value = false }
})
const scoreLevel = (s) => { if (!s) return 'pending'; if (s >= 80) return 'good'; if (s >= 60) return 'medium'; return 'poor' }
const goDetail = (item) => { if (item.id) uni.navigateTo({ url: `/pages/report/report?interviewId=${item.id}` }) }
const goContribute = (item) => {
uni.navigateTo({ url: `/pages/contribute/contribute?interviewId=${item.id}&position=${encodeURIComponent(item.position)}` })
}
const goInterview = () => uni.navigateTo({ url: '/pages/interview/interview' })
</script>
<style scoped>
.page { height: 100%; overflow-y: auto; background: var(--color-bg); }
.hero { background: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-mid) 50%, var(--color-gradient-end) 100%); 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; }
.stats-bar { display: flex; align-items: center; padding: 24rpx; margin: -40rpx 32rpx 0; position: relative; z-index: 1; border-radius: var(--radius-lg); }
.stat { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6rpx; }
.stat-val { font-size: 36rpx; font-weight: 700; color: var(--color-primary); }
.stat-lbl { font-size: 20rpx; color: var(--color-text-tertiary); }
.stat-sep { width: 1rpx; height: 40rpx; background: var(--color-border); }
.filter-wrap { padding: 24rpx 32rpx 0; }
.filter-inner { display: flex; gap: 8rpx; background: #FFF; border-radius: var(--radius-md); padding: 6rpx; }
.filter-tab { flex: 1; text-align: center; font-size: 24rpx; color: var(--color-text-secondary); padding: 14rpx 0; border-radius: var(--radius-sm); }
.filter-tab.active { background: var(--color-primary); color: #FFF; font-weight: 600; }
.list { padding: 20rpx 32rpx 48rpx; }
.record-card { padding: 24rpx 28rpx; margin-bottom: 16rpx; border-radius: var(--radius-lg); }
.record-top { display: flex; align-items: center; gap: 16rpx; }
.record-icon { font-size: 40rpx; width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.record-body { flex: 1; min-width: 0; }
.record-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.record-meta { font-size: 20rpx; color: var(--color-text-tertiary); margin-top: 6rpx; display: block; }
.record-score { font-size: 28rpx; font-weight: 700; width: 72rpx; text-align: right; flex-shrink: 0; }
.record-score.good { color: var(--color-success); }
.record-score.medium { color: var(--color-warning); }
.record-score.poor { color: var(--color-error); }
.record-score.pending { color: var(--color-text-tertiary); }
.record-actions { margin-top: 12rpx; padding-top: 12rpx; border-top: 1rpx solid var(--color-border); display: flex; gap: 20rpx; }
.rec-action { font-size: 22rpx; color: var(--color-primary); font-weight: 500; }
.empty { display: flex; flex-direction: column; align-items: center; padding: 120rpx 32rpx 0; }
.empty-icon { font-size: 80rpx; margin-bottom: 20rpx; }
.empty-title { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.empty-desc { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 8rpx; margin-bottom: 36rpx; }
.empty-btn { padding: 18rpx 48rpx; border-radius: var(--radius-round); font-size: 26rpx; }
.loading-tip { text-align: center; padding: 80rpx; font-size: 24rpx; color: var(--color-text-tertiary); }
</style>