v4.2 冲刺版+每日推送+支付修复+全量代码评审
## 新增功能 - 冲刺版 ¥49.9/月:完整支付→激活→权益扣减链路 - 每日一题定时推送(@nestjs/schedule,早8点微信订阅消息) - miniprogram-ci 编译上传脚本(scripts/upload-mp.js) ## Bug修复 - 套餐值统一:vip→growth/sprint(interview轮次限制、analyze次数检查) - member/pay 移除开发绕过:改为订单校验后激活 - progress→report 参数名不匹配:id→interviewId - result.vue resume.create() 参数传错(对象→独立参数) - resume.vue analyze请求缺少Authorization header - bank.vue contribution请求缺少Authorization header - member.vue startPay() 缺少try/catch导致网络错误崩溃 - login.vue 调试面板 v-if="true" 生产泄漏 ## 配置 - 微信支付生产证书就位(商户号1113760598) - .env 清理冗余文件(删除.example/.production) - WX_NOTIFY_URL 更新为 zhiyinwx.yzrcloud.cn ## 文档 - PROJECT-STATUS.md v4.1→v4.2,状态全面更新 - DEPLOYMENT.md 新增小程序编译上传章节、清理检查清单
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input class="search-input" v-model="keyword" placeholder="搜索公司名称..." @confirm="searchCompany" />
|
||||
<button class="search-btn" @tap="searchCompany">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 热门公司 -->
|
||||
<view class="section" v-if="!searching">
|
||||
<view class="section-title">热门公司题库</view>
|
||||
<view class="company-grid">
|
||||
<view
|
||||
class="company-card"
|
||||
v-for="c in hotCompanies"
|
||||
:key="c.name"
|
||||
@tap="selectCompany(c.name)"
|
||||
>
|
||||
<view class="company-name">{{ c.name }}</view>
|
||||
<view class="company-count">{{ c.positions }} 个岗位</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果:岗位列表 -->
|
||||
<view class="section" v-if="selectedCompany && !loadingPositions">
|
||||
<view class="section-title-row">
|
||||
<text class="section-title">{{ selectedCompany }} - 岗位列表</text>
|
||||
<text class="back-link" @tap="selectedCompany = ''">返回</text>
|
||||
</view>
|
||||
<view class="position-list">
|
||||
<view
|
||||
class="position-item"
|
||||
v-for="p in positions"
|
||||
:key="p.position"
|
||||
@tap="selectPosition(p.position)"
|
||||
>
|
||||
<view class="position-name">{{ p.position }}</view>
|
||||
<view class="position-meta">{{ p.questionCount }} 题 · {{ p.contributionCount }} 人贡献</view>
|
||||
</view>
|
||||
<view class="empty-state" v-if="positions.length === 0">
|
||||
<text>暂无该公司的面经数据</text>
|
||||
<text class="sub-text">成为第一个贡献者吧!</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 题目列表 -->
|
||||
<view class="section" v-if="selectedPosition && !loadingQuestions">
|
||||
<view class="section-title-row">
|
||||
<text class="section-title">{{ selectedCompany }} · {{ selectedPosition }}</text>
|
||||
<text class="back-link" @tap="selectedPosition = ''">返回</text>
|
||||
</view>
|
||||
<view class="question-list">
|
||||
<view class="question-item" v-for="(q, i) in questions" :key="i">
|
||||
<view class="q-header">
|
||||
<text class="q-num">#{{ i + 1 }}</text>
|
||||
<text class="q-tag">{{ q.type === 'technical' ? '技术' : '行为' }}</text>
|
||||
<text class="q-diff">{{ difficultyLabel(q.difficulty) }}</text>
|
||||
<text class="q-freq">{{ q.frequency }} 次提及</text>
|
||||
</view>
|
||||
<view class="q-content">{{ q.content }}</view>
|
||||
<view class="q-tags" v-if="q.tags && q.tags.length">
|
||||
<text class="tag" v-for="t in q.tags" :key="t">{{ t }}</text>
|
||||
</view>
|
||||
<view class="q-answer" v-if="q.referenceAnswer">
|
||||
<text class="answer-label">参考思路:</text>
|
||||
<text class="answer-text">{{ q.referenceAnswer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-state" v-if="questions.length === 0">
|
||||
<text>暂无题目数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="loading" v-if="loadingPositions || loadingQuestions">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '../../config'
|
||||
|
||||
const HOT_COMPANIES = [
|
||||
{ name: '腾讯', positions: 5 },
|
||||
{ name: '字节跳动', positions: 4 },
|
||||
{ name: '阿里巴巴', positions: 5 },
|
||||
{ name: '美团', positions: 3 },
|
||||
{ name: '百度', positions: 4 },
|
||||
{ name: '京东', positions: 3 },
|
||||
{ name: '网易', positions: 3 },
|
||||
{ name: '小红书', positions: 2 },
|
||||
]
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
searching: false,
|
||||
hotCompanies: HOT_COMPANIES,
|
||||
selectedCompany: '',
|
||||
selectedPosition: '',
|
||||
positions: [],
|
||||
questions: [],
|
||||
loadingPositions: false,
|
||||
loadingQuestions: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
difficultyLabel(d) {
|
||||
const map = { junior: '简单', medium: '中等', senior: '困难' }
|
||||
return map[d] || d || '中等'
|
||||
},
|
||||
async searchCompany() {
|
||||
const kw = this.keyword.trim()
|
||||
if (!kw) return
|
||||
this.selectedCompany = kw
|
||||
this.selectedPosition = ''
|
||||
await this.loadPositions(kw)
|
||||
},
|
||||
async selectCompany(name) {
|
||||
this.selectedCompany = name
|
||||
this.keyword = name
|
||||
this.selectedPosition = ''
|
||||
await this.loadPositions(name)
|
||||
},
|
||||
async loadPositions(company) {
|
||||
this.loadingPositions = true
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
const header = token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
try {
|
||||
const res = await uni.request({ url: api(`/contribution/company/${encodeURIComponent(company)}`), method: 'GET', header })
|
||||
this.positions = res.data || []
|
||||
} catch (e) {
|
||||
this.positions = []
|
||||
}
|
||||
this.loadingPositions = false
|
||||
},
|
||||
async selectPosition(position) {
|
||||
this.selectedPosition = position
|
||||
await this.loadQuestions(this.selectedCompany, position)
|
||||
},
|
||||
async loadQuestions(company, position) {
|
||||
this.loadingQuestions = true
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
const header = token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: api(`/contribution/company/${encodeURIComponent(company)}/position/${encodeURIComponent(position)}`),
|
||||
method: 'GET',
|
||||
header,
|
||||
})
|
||||
this.questions = res.data?.questions || []
|
||||
} catch (e) {
|
||||
this.questions = []
|
||||
}
|
||||
this.loadingQuestions = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 20rpx; min-height: 100vh; background: #f5f6f7; }
|
||||
.search-bar { display: flex; gap: 20rpx; margin-bottom: 30rpx; }
|
||||
.search-input { flex: 1; height: 80rpx; background: #fff; border-radius: 40rpx; padding: 0 30rpx; font-size: 28rpx; }
|
||||
.search-btn { height: 80rpx; line-height: 80rpx; padding: 0 40rpx; background: #4F46E5; color: #fff; border-radius: 40rpx; font-size: 28rpx; }
|
||||
.section { margin-bottom: 30rpx; background: #fff; border-radius: 16rpx; padding: 30rpx; }
|
||||
.section-title { font-size: 32rpx; font-weight: 600; margin-bottom: 20rpx; display: block; }
|
||||
.section-title-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.back-link { color: #4F46E5; font-size: 26rpx; }
|
||||
.company-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20rpx; }
|
||||
.company-card { background: #F3F0FF; border-radius: 12rpx; padding: 24rpx; text-align: center; }
|
||||
.company-name { font-size: 28rpx; font-weight: 500; color: #333; }
|
||||
.company-count { font-size: 22rpx; color: #999; margin-top: 8rpx; }
|
||||
.position-item { padding: 24rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.position-name { font-size: 28rpx; font-weight: 500; color: #333; }
|
||||
.position-meta { font-size: 24rpx; color: #999; margin-top: 8rpx; }
|
||||
.question-item { padding: 24rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.q-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.q-num { font-size: 24rpx; color: #4F46E5; font-weight: 600; }
|
||||
.q-tag { font-size: 22rpx; background: #E8F5E9; color: #2E7D32; padding: 2rpx 12rpx; border-radius: 6rpx; }
|
||||
.q-diff { font-size: 22rpx; background: #FFF3E0; color: #E65100; padding: 2rpx 12rpx; border-radius: 6rpx; }
|
||||
.q-freq { font-size: 22rpx; color: #999; margin-left: auto; }
|
||||
.q-content { font-size: 28rpx; color: #333; line-height: 1.6; }
|
||||
.q-tags { display: flex; flex-wrap: wrap; gap: 8rpx; margin-top: 12rpx; }
|
||||
.tag { font-size: 22rpx; background: #F3F0FF; color: #4F46E5; padding: 4rpx 16rpx; border-radius: 20rpx; }
|
||||
.q-answer { margin-top: 16rpx; padding: 16rpx; background: #F8F9FA; border-radius: 8rpx; }
|
||||
.answer-label { font-size: 24rpx; color: #4F46E5; font-weight: 500; }
|
||||
.answer-text { font-size: 26rpx; color: #555; line-height: 1.6; }
|
||||
.empty-state { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
|
||||
.sub-text { display: block; margin-top: 12rpx; font-size: 24rpx; color: #bbb; }
|
||||
.loading { text-align: center; padding: 60rpx; color: #999; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user