37cfdfe93c
- 前端:登录页重构,支持密码登录、验证码登录、注册三种模式 - 前端:首页热门岗位添加「参考示例」标签,去虚构数据 - 前端:面试页顶部优化,岗位名+状态标签展示 - 前端:新增用户协议、隐私政策页面及免责声明 - 后端:新增 POST /api/user/register 注册接口 - 后端:新增 POST /api/user/set-password 设置密码接口 - 后端:修复 user.schema.ts unique 索引导致 null 冲突问题 - 后端:新增 payment-order.schema、positions.schema、site-config.schema - 后端:package.json 新增 postbuild 脚本自动复制证书 - 管理后台:新增订单管理 Tab
243 lines
12 KiB
Vue
243 lines
12 KiB
Vue
<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">
|
||
<view class="section-title-row">
|
||
<text class="section-title">热门岗位</text>
|
||
<text class="section-tag-demo">参考示例</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">{{ posIcons[idx] || '💼' }}</text>
|
||
<view class="pos-body">
|
||
<text class="pos-name">{{ pos.name }}</text>
|
||
<view class="pos-meta-row">
|
||
<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 { 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)
|
||
|
||
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: 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-tag-demo { font-size: 18rpx; color: #9CA3AF; background: #F3F4F6; padding: 2rpx 10rpx; border-radius: 6rpx; }
|
||
.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-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> |