Files
zhiyin/zhiyin-app/src/pages/index/index.vue
T

449 lines
22 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">{{ userInfo ? '试试下面的功能,开启你的求职练习' : '登录后免费使用 AI 模拟面试 · 简历优化 · 面经题库' }}</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.gravity ?? 0) > 0 ? '引力值 ' + (userInfo.gravity ?? 0) : '引力值 0' }}</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>
<view class="guest-benefits">
<text class="guest-benefit">🎙 模拟面试</text>
<text class="guest-benefit">📄 简历优化</text>
</view>
</view>
<text class="guest-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-grid">
<view class="fs-card card" @click="goCareer">
<view class="fs-top">
<view class="fs-icon fs-career"><text class="fs-emoji">🎯</text></view>
<text class="fs-name">AI 择业顾问</text>
</view>
<text class="fs-brief">专业分析 · 岗位匹配 · 智能推荐</text>
</view>
<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>
</view>
<!-- 每日一题 -->
<view class="section">
<view class="section-header">
<text class="section-title">📮 每日一题</text>
<text class="section-desc" @click="refreshDaily" v-if="dailyQuestion">换一题</text>
</view>
<view class="daily-card card" v-if="dailyQuestion">
<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 class="daily-guest card" v-else-if="!userInfo" @click="goLogin">
<view class="daily-guest-top">
<text class="daily-guest-icon">🔒</text>
<view class="daily-guest-body">
<text class="daily-guest-title">登录后获取今日面试题</text>
<text class="daily-guest-desc">每日一道校招真题持续积累面试经验</text>
</view>
</view>
<text class="daily-action primary">立即登录 </text>
</view>
</view>
<!-- 热门岗位 - AI 专区 -->
<view class="section">
<view class="section-header">
<view class="section-title-row">
<text class="section-title">🔥 AI 热门岗位</text>
<text class="section-badge">NEW</text>
</view>
<text class="section-desc">AI 时代最热方向点击直接面试</text>
</view>
<view class="ai-banner card" @click="goInterview">
<view class="ai-banner-icon-wrap">
<text class="ai-banner-icon">🤖</text>
</view>
<view class="ai-banner-body">
<text class="ai-banner-title">AI 正在重塑整个行业</text>
<text class="ai-banner-desc">大模型应用 / Agent 开发 / Prompt 工程 顶尖人才缺口巨大现在上车正当时</text>
</view>
<text class="ai-banner-arrow"></text>
</view>
<view class="position-list card" v-if="!positionsLoading && displayAiPositions.length > 0">
<view class="pos-item" v-for="(pos, idx) in displayAiPositions" :key="'ai-' + idx" @click="startInterview(pos)">
<view class="pos-left">
<text class="pos-icon pos-icon-ai">{{ pos.icon || aiPosIcons[idx % aiPosIcons.length] || '🤖' }}</text>
<view class="pos-body">
<text class="pos-name">{{ pos.name }}</text>
<text class="pos-name-tag">AI 方向</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-btn pos-action-ai">开始面试</text>
</view>
</view>
</view>
<!-- 更多岗位 -->
<view class="more-header" @click="showMore = !showMore">
<view class="more-header-left">
<text class="more-icon">🧑💻</text>
<text class="more-title">更多岗位{{ traditionalPositions.length }}</text>
</view>
<text class="more-arrow">{{ showMore ? '收起 ▲' : '展开 ▼' }}</text>
</view>
<view class="position-list card" v-if="showMore && !positionsLoading && traditionalPositions.length > 0">
<view class="pos-item" v-for="(pos, idx) in traditionalPositions" :key="'tr-' + idx" @click="startInterview(pos)">
<view class="pos-left">
<text class="pos-icon">{{ pos.icon || posIcons[(displayAiPositions.length + idx) % posIcons.length] || '💼' }}</text>
<view class="pos-body">
<text class="pos-name">{{ pos.name }}</text>
<text class="pos-name-tag pos-name-tag-tr">{{ pos.category === 'intern' ? '实习' : '校招' }}</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-btn">立即模拟</text>
</view>
</view>
</view>
<view class="loading-tip" v-if="positionsLoading">加载岗位中...</view>
</view>
<view class="bottom-spacer"></view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
// #ifdef MP-WEIXIN
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
// #endif
// #ifndef MP-WEIXIN
import { onShow } from '@dcloudio/uni-app'
// #endif
import { api } from '../../config'
// #ifdef MP-WEIXIN
onShareAppMessage(() => ({ title: '职引 - AI模拟面试 | 校招求职一站式平台', path: '/pages/index/index' }))
onShareTimeline(() => ({ title: '职引 - AI模拟面试 | 校招求职一站式平台' }))
// #endif
const userInfo = ref(null)
const greeting = ref('')
const hotPositions = ref([])
const posIcons = ['💻', '⚙️', '🤖', '📊', '🎨', '🧪', '📱', '🔧']
const aiPosIcons = ['🤖', '🧠', '⚡', '🚀', '💡', '🔬']
const positionsLoading = ref(true)
const dailyQuestion = ref(null)
const showAnswer = ref(false)
const showMore = ref(true)
// 前端兜底 AI 岗位(当 API 返回不足时展示)
const FALLBACK_AI_POSITIONS = [
{ name: 'AI Agent 开发工程师', category: 'ai', company: '热门方向', salary: '30K-60K', icon: '🧠' },
{ name: '大模型应用开发', category: 'ai', company: '热门方向', salary: '25K-50K', icon: '⚡' },
]
const aiPositions = computed(() => hotPositions.value.filter(p => p.category === 'ai'))
const displayAiPositions = computed(() => {
const fromApi = aiPositions.value
if (fromApi.length >= 4) return fromApi
// 补充到至少 4 个,去重
const existingNames = new Set(fromApi.map(p => p.name))
const needed = FALLBACK_AI_POSITIONS.filter(p => !existingNames.has(p.name))
return [...fromApi, ...needed]
})
const traditionalPositions = computed(() => hotPositions.value.filter(p => p.category !== 'ai'))
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 goCareer = () => uni.navigateTo({ url: '/pages/career/career' })
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; min-width: 280rpx; max-width: 340rpx; }
.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.2); backdrop-filter: blur(12rpx);
border: 1rpx solid rgba(255,255,255,0.25);
}
.guest-card .avatar { border-color: rgba(255,255,255,0.35); }
.guest-card .user-name { font-size: 26rpx; color: #FFF; font-weight: 700; }
.guest-benefits { display: flex; gap: 12rpx; margin-top: 6rpx; flex-wrap: wrap; }
.guest-benefit { font-size: 18rpx; color: rgba(255,255,255,0.8); padding: 2rpx 10rpx; background: rgba(255,255,255,0.12); border-radius: 8rpx; }
.guest-arrow { font-size: 36rpx; color: rgba(255,255,255,0.5); margin-left: 12rpx; }
.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-badge { font-size: 18rpx; color: #fff; background: var(--color-primary); padding: 2rpx 12rpx; border-radius: 20rpx; font-weight: 500; }
.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-grid { display: grid; grid-template-columns: 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-career { background: linear-gradient(135deg, #F3E8FF, #D8B4FE); }
.fs-resume { background: linear-gradient(135deg, #ECFDF5, #A7F3D0); }
.fs-progress { background: linear-gradient(135deg, #EEF2FF, #C7D2FE); }
.fs-contribute { background: linear-gradient(135deg, #FFF7ED, #FDBA74); }
.fs-bank { background: linear-gradient(135deg, #FCE7F3, #F9A8D4); }
.fs-internship { background: linear-gradient(135deg, #E0F2FE, #7DD3FC); }
/* 每日一题 */
.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; }
.daily-guest {
padding: 24rpx; border-radius: var(--radius-lg);
background: linear-gradient(135deg, #EEF2FF, #E0E7FF);
display: flex; align-items: center; justify-content: space-between;
cursor: pointer;
}
.daily-guest:active { opacity: 0.8; }
.daily-guest-top { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
.daily-guest-icon { font-size: 32rpx; width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.6); border-radius: 14rpx; flex-shrink: 0; }
.daily-guest-body { flex: 1; min-width: 0; }
.daily-guest-title { font-size: 26rpx; font-weight: 600; color: var(--color-text); }
.daily-guest-desc { font-size: 20rpx; color: var(--color-text-secondary); margin-top: 4rpx; display: block; }
/* AI 岗位专区 */
.ai-banner {
background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 50%, #FCD34D 100%);
padding: 20rpx 24rpx; border-radius: var(--radius-lg); margin-bottom: 16rpx;
display: flex; align-items: center; gap: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(251,191,36,0.2);
cursor: pointer; transition: transform 0.15s;
}
.ai-banner:active { transform: scale(0.97); }
.ai-banner-icon-wrap {
width: 64rpx; height: 64rpx; border-radius: 18rpx;
background: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.ai-banner-icon { font-size: 36rpx; }
.ai-banner-body { flex: 1; min-width: 0; }
.ai-banner-title { font-size: 26rpx; font-weight: 700; color: #92400E; display: block; margin-bottom: 4rpx; }
.ai-banner-desc { font-size: 20rpx; color: #A16207; line-height: 1.5; display: block; }
.ai-banner-arrow { font-size: 28rpx; color: #B45309; font-weight: 600; flex-shrink: 0; }
.position-list { border-radius: var(--radius-lg); overflow: hidden; }
.pos-item { padding: 26rpx 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: #F9FAFB; }
.pos-left { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
.pos-icon { font-size: 32rpx; width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; background: #F3F4F6; border-radius: 16rpx; flex-shrink: 0; }
.pos-icon-ai { background: linear-gradient(135deg, #FEF3C7, #FDE68A); }
.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-name-tag {
font-size: 18rpx; color: #D97706; background: #FFFBEB; padding: 1rpx 10rpx;
border-radius: 6rpx; align-self: flex-start; margin-top: 6rpx;
}
.pos-name-tag-tr { color: var(--color-primary); background: #EEF2FF; }
.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-btn {
font-size: 22rpx; font-weight: 600; color: var(--color-primary);
padding: 10rpx 24rpx; border-radius: var(--radius-round);
background: #EEF2FF; display: inline-block;
}
.pos-action-btn:active { background: #DBEAFE; }
.pos-action-ai { color: #D97706; background: #FFFBEB; }
/* 更多岗位 */
.more-header {
display: flex; justify-content: space-between; align-items: center;
padding: 24rpx 20rpx 16rpx; margin-top: 8rpx; cursor: pointer;
background: #FFFFFF; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.more-header:active { opacity: 0.7; }
.more-header-left { display: flex; align-items: center; gap: 10rpx; }
.more-icon { font-size: 28rpx; }
.more-title { font-size: 26rpx; font-weight: 600; color: var(--color-text); }
.more-arrow { font-size: 22rpx; color: var(--color-primary); font-weight: 500; background: #EEF2FF; padding: 4rpx 16rpx; border-radius: var(--radius-round); }
.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>