初始化:职引项目 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
+228
View File
@@ -0,0 +1,228 @@
<template>
<view class="page fade-in">
<view class="hero">
<text class="hero-title">{{ greeting }}</text>
<text class="hero-sub">试试下面的功能开启你的求职练习</text>
<view class="user-card card" v-if="userInfo" @click="goProfile">
<image class="avatar" :src="userInfo.avatar || '/static/avatar-default.svg'" 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.remaining || 0 }} </text>
</view>
</view>
<text class="arrow"></text>
</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">模拟面试</text>
<text class="fp-brief">AI 面试官 · 真实场景 · 即时反馈</text>
</view>
</view>
<text class="fp-action">开始</text>
</view>
<view class="feature-secondary">
<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" 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">
<text class="section-title">热门岗位</text>
<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">
<view class="pos-rank">{{ idx + 1 }}</view>
<view class="pos-body">
<text class="pos-name">{{ pos.name }}</text>
<text class="pos-company">{{ pos.company }}</text>
</view>
</view>
<text class="pos-salary">{{ pos.salary }}</text>
</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 { api } from '../../config'
const userInfo = ref(null)
const greeting = ref('')
const hotPositions = ref([])
const positionsLoading = ref(true)
const dailyQuestion = ref(null)
const showAnswer = ref(false)
onMounted(async () => {
try { const s = uni.getStorageSync('userInfo'); if (s) userInfo.value = JSON.parse(s) } catch (e) {}
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 }
})
const refreshDaily = () => { showAnswer.value = false; /* trigger reload */ }
const goProfile = () => uni.switchTab({ url: '/pages/user/user' })
const goInterview = () => uni.navigateTo({ url: '/pages/interview/interview' })
const goProgress = () => uni.navigateTo({ url: '/pages/progress/progress' })
const goContribute = () => uni.navigateTo({ url: '/pages/contribute/contribute' })
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-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 {
background: rgba(255,255,255,0.95); backdrop-filter: blur(20rpx);
border-radius: var(--radius-xl); padding: 24rpx 28rpx;
display: flex; align-items: center; margin-top: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.1);
}
.avatar { width: 88rpx; height: 88rpx; border-radius: 50%; margin-right: 20rpx; 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: baseline; margin-bottom: 20rpx; }
.section-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
.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; }
.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-contribute { background: linear-gradient(135deg, #ECFDF5, #A7F3D0); }
/* 每日一题 */
.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-left { display: flex; align-items: center; gap: 16rpx; }
.pos-rank { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #F3F4F6; display: flex; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 700; color: var(--color-primary); flex-shrink: 0; }
.pos-body { display: flex; flex-direction: column; }
.pos-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.pos-company { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 4rpx; }
.pos-salary { font-size: 24rpx; 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>