Files
zhiyin/zhiyin-app/src/pages/index/index.vue
T
2026-06-16 13:18:36 +08:00

310 lines
15 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page fade-in">
<view class="hero">
<view class="hero-row">
<view class="hero-left">
<text class="hero-title">{{ greeting }}</text>
<text class="hero-sub">试试下面的功能开启你的求职练习</text>
</view>
<view class="hero-right">
<view class="user-card card" v-if="userInfo" @click="goProfile">
<image class="avatar" :src="userInfo.avatar || '/static/avatar-default.png'" mode="aspectFill" />
<view class="user-meta">
<text class="user-name">{{ userInfo.nickname || '同学' }}</text>
<view class="user-tags">
<text class="tag tag-plan">{{ userInfo.plan || '免费版' }}</text>
<text class="tag tag-remaining">{{ userInfo.interviewCredits > 0 ? '剩余 ' + userInfo.interviewCredits + ' 次' : '已用完' }}</text>
</view>
</view>
<text class="arrow"></text>
</view>
<view class="guest-card card" v-else @click="goLogin">
<image class="avatar" src="/static/avatar-default.png" mode="aspectFill" />
<view class="user-meta">
<text class="user-name">立即登录</text>
<text class="guest-hint">登录后体验全部功能</text>
</view>
<text class="arrow"></text>
</view>
</view>
</view>
</view>
<!-- 功能入口 -->
<view class="section">
<view class="feature-list">
<view class="feature-primary card" @click="goInterview">
<view class="fp-left">
<view class="fp-icon fp-interview"><text class="fp-emoji">🎙</text></view>
<view class="fp-body">
<text class="fp-name">AI数字人面试</text>
<text class="fp-brief">数字人考官 · 真实场景 · 语音互动 · 即时反馈</text>
</view>
</view>
<text class="fp-action">开始</text>
</view>
<view class="feature-tertiary">
<view class="fs-card card" @click="goResume">
<view class="fs-top">
<view class="fs-icon fs-resume"><text class="fs-emoji">📄</text></view>
<text class="fs-name">简历优化</text>
</view>
<text class="fs-brief">AI 诊断 · 智能优化 · 一键下载</text>
</view>
<view class="fs-card card" @click="goProgress">
<view class="fs-top">
<view class="fs-icon fs-progress"><text class="fs-emoji">📊</text></view>
<text class="fs-name">进步轨迹</text>
</view>
<text class="fs-brief">能力雷达 · 打卡记录 · 成长曲线</text>
</view>
<view class="fs-card card" @click="goContribute">
<view class="fs-top">
<view class="fs-icon fs-contribute"><text class="fs-emoji">💡</text></view>
<text class="fs-name">贡献面经</text>
</view>
<text class="fs-brief">分享经验 · 共建题库 · 帮更多人</text>
</view>
</view>
<view class="feature-secondary">
<view class="fs-card card" @click="goBank">
<view class="fs-top">
<view class="fs-icon fs-progress"><text class="fs-emoji">📚</text></view>
<text class="fs-name">公司真题库</text>
</view>
<text class="fs-brief">大厂真题 · 岗位分类 · 参考思路</text>
</view>
<view class="fs-card card" @click="goInternship">
<view class="fs-top">
<view class="fs-icon fs-contribute"><text class="fs-emoji">🔍</text></view>
<text class="fs-name">实习搜索</text>
</view>
<text class="fs-brief">热门实习 · 一键搜索 · 精准匹配</text>
</view>
</view>
</view>
</view>
<!-- 每日一题 -->
<view class="section" v-if="dailyQuestion">
<view class="section-header">
<text class="section-title">📮 每日一题</text>
<text class="section-desc" @click="refreshDaily">换一题</text>
</view>
<view class="daily-card card">
<text class="daily-tag">{{ dailyQuestion.category || '综合' }}</text>
<text class="daily-question">{{ dailyQuestion.question }}</text>
<view class="daily-answer" v-if="showAnswer">
<text class="daily-answer-label">💡 参考思路</text>
<text class="daily-answer-text">{{ dailyQuestion.referenceAnswer }}</text>
</view>
<view class="daily-actions">
<text class="daily-action" @click="showAnswer = !showAnswer">
{{ showAnswer ? '收起思路' : '查看思路' }}
</text>
<text class="daily-action primary" @click="goInterview">模拟练习 </text>
</view>
</view>
</view>
<!-- 热门岗位 -->
<view class="section">
<view class="section-header">
<view class="section-title-row">
<text class="section-title">热门岗位</text>
</view>
<text class="section-desc">点击直接面试</text>
</view>
<view class="position-list card" v-if="!positionsLoading">
<view class="pos-item" v-for="(pos, idx) in hotPositions" :key="idx" @click="startInterview(pos)">
<view class="pos-left">
<text class="pos-icon">{{ pos.icon || posIcons[idx % posIcons.length] || '💼' }}</text>
<view class="pos-body">
<text class="pos-name">{{ pos.name }}</text>
<view class="pos-meta-row" v-if="pos.company || pos.salary">
<text class="pos-company">{{ pos.company }}</text>
<text class="pos-salary">{{ pos.salary }}</text>
</view>
</view>
</view>
<view class="pos-action">
<text class="pos-action-text">立即模拟</text>
</view>
</view>
</view>
<view class="loading-tip" v-if="positionsLoading">加载岗位中...</view>
</view>
<view class="bottom-spacer"></view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { api } from '../../config'
const userInfo = ref(null)
const greeting = ref('')
const hotPositions = ref([])
const posIcons = ['💻', '⚙️', '🤖', '📊', '🎨', '🧪', '📱', '🔧']
const positionsLoading = ref(true)
const dailyQuestion = ref(null)
const showAnswer = ref(false)
const loadUserInfo = () => {
try { const s = uni.getStorageSync('userInfo'); if (s) userInfo.value = JSON.parse(s); else userInfo.value = null } catch (e) { userInfo.value = null }
}
onMounted(async () => {
loadUserInfo()
const h = new Date().getHours()
if (h < 6) greeting.value = '夜深了,早点休息 🌙'
else if (h < 12) greeting.value = '早上好 ☀️'
else if (h < 14) greeting.value = '中午好 🌤'
else if (h < 18) greeting.value = '下午好 🌥'
else greeting.value = '晚上好 🌆'
// 每日一题
try {
const t = uni.getStorageSync('token')
if (t) {
const qres = await uni.request({
url: api('/daily-question'), method: 'GET',
header: { 'Authorization': `Bearer ${t}` }
})
if (qres.statusCode === 200 && qres.data) {
dailyQuestion.value = qres.data
}
}
} catch (e) { /* silent */ }
// 热门岗位
try {
const res = await uni.request({ url: api('/positions/hot'), method: 'GET' })
if (res.statusCode === 200) hotPositions.value = res.data || []
} catch (e) { console.error(e) }
finally { positionsLoading.value = false }
})
onShow(loadUserInfo)
const refreshDaily = async () => {
showAnswer.value = false
try {
const t = uni.getStorageSync('token')
if (t) {
const qres = await uni.request({
url: api('/daily-question'), method: 'GET',
header: { 'Authorization': `Bearer ${t}` }
})
if (qres.statusCode === 200 && qres.data) dailyQuestion.value = qres.data
}
} catch (e) { /* silent */ }
}
const goProfile = () => uni.switchTab({ url: '/pages/user/user' })
const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
const goInterview = () => uni.navigateTo({ url: '/pages/interview/interview' })
const goResume = () => uni.navigateTo({ url: '/pages/resume/resume' })
const goProgress = () => uni.navigateTo({ url: '/pages/progress/progress' })
const goContribute = () => uni.navigateTo({ url: '/pages/contribute/contribute' })
const goBank = () => uni.navigateTo({ url: '/pages/company-bank/bank' })
const goInternship = () => uni.navigateTo({ url: '/pages/internship/internship' })
const startInterview = (pos) => uni.navigateTo({ url: `/pages/interview/interview?position=${encodeURIComponent(pos.name)}` })
</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-row { display: flex; align-items: flex-start; gap: 24rpx; }
.hero-left { flex: 1; min-width: 0; padding-top: 8rpx; }
.hero-right { flex-shrink: 0; width: 320rpx; }
.hero-title { font-size: 40rpx; font-weight: 700; color: #FFF; display: block; line-height: 1.3; }
.hero-sub { font-size: 22rpx; color: rgba(255,255,255,0.7); margin-top: 8rpx; display: block; }
.user-card, .guest-card {
background: rgba(255,255,255,0.95); backdrop-filter: blur(20rpx);
border-radius: var(--radius-xl); padding: 20rpx 24rpx;
display: flex; align-items: center;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.1);
}
.guest-card { background: rgba(255,255,255,0.15); backdrop-filter: blur(10rpx); }
.guest-card .avatar { border-color: rgba(255,255,255,0.3); }
.guest-card .user-name { font-size: 26rpx; color: #FFF; }
.guest-hint { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 4rpx; display: block; }
.guest-card .arrow { color: rgba(255,255,255,0.4); }
.avatar { width: 72rpx; height: 72rpx; border-radius: 50%; margin-right: 16rpx; border: 3rpx solid var(--color-primary-light); flex-shrink: 0; }
.user-meta { flex: 1; min-width: 0; }
.user-name { font-size: 30rpx; font-weight: 600; color: var(--color-text); }
.user-tags { display: flex; gap: 10rpx; margin-top: 10rpx; }
.tag { font-size: 20rpx; padding: 4rpx 14rpx; border-radius: var(--radius-round); font-weight: 500; }
.tag-plan { background: #EEF2FF; color: var(--color-primary); }
.tag-remaining { background: #ECFDF5; color: var(--color-success); }
.arrow { font-size: 36rpx; color: #D1D5DB; margin-left: 12rpx; }
.section { padding: 32rpx 32rpx 0; }
.section:first-of-type { margin-top: -40rpx; padding-top: 0; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.section-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
.section-title-row { display: flex; align-items: center; gap: 12rpx; }
.section-desc { font-size: 22rpx; color: var(--color-primary); }
.feature-list { display: flex; flex-direction: column; gap: 16rpx; }
.feature-primary {
display: flex; align-items: center; justify-content: space-between;
padding: 24rpx 28rpx; border-radius: var(--radius-lg);
background: linear-gradient(135deg, #EEF2FF, #DBEAFE);
}
.fp-left { display: flex; align-items: center; gap: 20rpx; flex: 1; }
.fp-icon { width: 64rpx; height: 64rpx; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.fp-interview { background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); }
.fp-emoji { font-size: 32rpx; }
.fp-body { flex: 1; min-width: 0; }
.fp-name { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
.fp-brief { font-size: 20rpx; color: var(--color-text-secondary); margin-top: 4rpx; display: block; }
.fp-action { font-size: 28rpx; color: var(--color-primary); font-weight: 600; flex-shrink: 0; }
.feature-secondary { display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; }
.feature-tertiary { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16rpx; }
.fs-card { padding: 20rpx; border-radius: var(--radius-lg); }
.fs-top { display: flex; align-items: center; gap: 10rpx; }
.fs-icon { width: 44rpx; height: 44rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.fs-emoji { font-size: 20rpx; }
.fs-name { font-size: 26rpx; font-weight: 600; color: var(--color-text); }
.fs-brief { font-size: 18rpx; color: var(--color-text-secondary); margin-top: 10rpx; display: block; }
.fs-progress { background: linear-gradient(135deg, #EEF2FF, #C7D2FE); }
.fs-resume { background: linear-gradient(135deg, #ECFDF5, #A7F3D0); }
.fs-contribute { background: linear-gradient(135deg, #FFF7ED, #FDBA74); }
/* 每日一题 */
.daily-card { padding: 24rpx; border-radius: var(--radius-lg); }
.daily-tag { display: inline-block; padding: 4rpx 14rpx; background: #EEF2FF; color: var(--color-primary); font-size: 20rpx; border-radius: var(--radius-round); margin-bottom: 12rpx; }
.daily-question { font-size: 28rpx; font-weight: 600; color: var(--color-text); line-height: 1.6; display: block; }
.daily-answer { margin-top: 16rpx; padding: 20rpx; background: #F9FAFB; border-radius: var(--radius-md); }
.daily-answer-label { font-size: 22rpx; font-weight: 600; color: var(--color-text-secondary); display: block; margin-bottom: 8rpx; }
.daily-answer-text { font-size: 24rpx; color: var(--color-text-secondary); line-height: 1.6; }
.daily-actions { display: flex; justify-content: space-between; margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid var(--color-border); }
.daily-action { font-size: 24rpx; color: var(--color-text-secondary); }
.daily-action.primary { color: var(--color-primary); font-weight: 600; }
.position-list { border-radius: var(--radius-lg); overflow: hidden; }
.pos-item { padding: 24rpx 28rpx; border-bottom: 1rpx solid var(--color-border); display: flex; justify-content: space-between; align-items: center; }
.pos-item:last-child { border-bottom: none; }
.pos-item:active { background: var(--color-bg); }
.pos-left { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
.pos-icon { font-size: 36rpx; width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; background: #F3F4F6; border-radius: 14rpx; flex-shrink: 0; }
.pos-body { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.pos-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.pos-meta-row { display: flex; align-items: center; gap: 10rpx; margin-top: 4rpx; }
.pos-company { font-size: 20rpx; color: var(--color-text-tertiary); }
.pos-salary { font-size: 20rpx; color: var(--color-primary); background: #EEF2FF; padding: 2rpx 10rpx; border-radius: 6rpx; }
.pos-action { flex-shrink: 0; margin-left: 16rpx; }
.pos-action-text { font-size: 22rpx; color: var(--color-primary); font-weight: 600; }
.loading-tip { text-align: center; padding: 40rpx; font-size: 24rpx; color: var(--color-text-tertiary); background: #FFF; border-radius: var(--radius-lg); }
.bottom-spacer { height: 40rpx; }
</style>