Files
zhiyin/zhiyin-app/src/pages/company-bank/bank.vue
T
yuzhiran 54c21e2953 refactor: rewrite company-bank and internship pages
- bank.vue: Composition API, design tokens, 2-col grid, better UX
- internship.vue: search bar, category tabs, card list layout
2026-06-18 17:27:37 +08:00

260 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="search-bar">
<view class="search-inner">
<text class="search-icon">🔍</text>
<input class="search-input" v-model="keyword" placeholder="搜索公司名称..." @confirm="searchCompany" />
<text class="search-clear" v-if="keyword" @tap="keyword = ''"></text>
</view>
<button class="search-btn" @tap="searchCompany">搜索</button>
</view>
<!-- 热门公司 -->
<view class="section" v-if="!selectedCompany">
<view class="section-header">
<text class="section-title">🏢 热门公司题库</text>
</view>
<view class="loading-bar" v-if="loading">
<view class="loading-text">加载中...</view>
</view>
<view class="company-grid" v-else-if="hotCompanies.length > 0">
<view class="company-card card" v-for="c in hotCompanies" :key="c.name" @tap="selectCompany(c.name)">
<text class="company-name">{{ c.name }}</text>
<text class="company-count">{{ c.positionCount > 0 ? c.positionCount + ' 个岗位' : '暂无题库' }}</text>
</view>
</view>
<view class="empty" v-else>
<text class="empty-icon">📭</text>
<text class="empty-text">暂无公司题库数据</text>
<text class="empty-hint">完成面试后贡献面经帮助更多人</text>
</view>
</view>
<!-- 岗位列表 -->
<view class="section" v-if="selectedCompany">
<view class="section-header">
<view class="section-header-left">
<text class="section-back" @tap="backToCompanies"> 返回</text>
<text class="section-title">{{ selectedCompany }}</text>
</view>
</view>
<view class="loading-bar" v-if="loadingPositions">
<view class="loading-text">加载岗位中...</view>
</view>
<view class="position-list" v-else-if="positions.length > 0">
<view class="pos-item card" v-for="p in positions" :key="p.position" @tap="selectPosition(p.position)">
<view class="pos-left">
<text class="pos-icon">{{ p.icon || '💼' }}</text>
<view class="pos-body">
<text class="pos-name">{{ p.position }}</text>
<text class="pos-meta">{{ p.questionCount }} · {{ p.contributionCount }} 人贡献</text>
</view>
</view>
<text class="pos-arrow"></text>
</view>
</view>
<view class="empty" v-else>
<text class="empty-icon">📭</text>
<text class="empty-text">暂无该公司的面经数据</text>
<text class="empty-hint">成为第一个贡献者吧</text>
</view>
</view>
<!-- 题目列表 -->
<view class="section" v-if="selectedPosition">
<view class="section-header">
<view class="section-header-left">
<text class="section-back" @tap="selectedPosition = ''"> 返回</text>
<text class="section-title">{{ selectedPosition }}</text>
</view>
</view>
<view class="loading-bar" v-if="loadingQuestions">
<view class="loading-text">加载题目中...</view>
</view>
<view class="question-list" v-else-if="questions.length > 0">
<view class="question-item card" v-for="(q, i) in questions" :key="i">
<view class="q-header">
<text class="q-num">#{{ i + 1 }}</text>
<text class="q-tag" :class="'q-tag-' + (q.type === 'technical' ? 'tech' : 'behavior')">{{ q.type === 'technical' ? '技术' : '行为' }}</text>
<text class="q-diff" :class="'q-diff-' + (q.difficulty || 'medium')">{{ difficultyLabel(q.difficulty) }}</text>
<text class="q-freq">{{ q.frequency }} 次提及</text>
</view>
<text class="q-content">{{ q.content }}</text>
<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>
<view class="empty" v-else>
<text class="empty-icon">📭</text>
<text class="empty-text">暂无题目数据</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { api } from '../../config'
const keyword = ref('')
const hotCompanies = ref([])
const selectedCompany = ref('')
const selectedPosition = ref('')
const positions = ref([])
const questions = ref([])
const loading = ref(true)
const loadingPositions = ref(false)
const loadingQuestions = ref(false)
onMounted(() => { loadHotCompanies() })
async function loadHotCompanies() {
loading.value = true
try {
const token = uni.getStorageSync('token') || ''
const header = token ? { 'Authorization': `Bearer ${token}` } : {}
const res = await uni.request({ url: api('/contribution/companies/hot'), method: 'GET', header })
if (res.statusCode === 200) hotCompanies.value = res.data || []
} catch (e) { console.error(e) }
finally { loading.value = false }
}
function difficultyLabel(d) {
const map = { junior: '简单', medium: '中等', senior: '困难' }
return map[d] || d || '中等'
}
function searchCompany() {
const kw = keyword.value.trim()
if (!kw) return
selectedCompany.value = kw
selectedPosition.value = ''
loadPositions(kw)
}
function selectCompany(name) {
selectedCompany.value = name
keyword.value = name
selectedPosition.value = ''
loadPositions(name)
}
function backToCompanies() {
selectedCompany.value = ''
selectedPosition.value = ''
}
async function loadPositions(company) {
loadingPositions.value = 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 })
positions.value = res.data || []
} catch (e) { positions.value = [] }
finally { loadingPositions.value = false }
}
function selectPosition(position) {
selectedPosition.value = position
loadQuestions(selectedCompany.value, position)
}
async function loadQuestions(company, position) {
loadingQuestions.value = 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,
})
questions.value = res.data?.questions || []
} catch (e) { questions.value = [] }
finally { loadingQuestions.value = false }
}
</script>
<style scoped>
.page { min-height: 100vh; background: var(--color-bg); padding: 20rpx 32rpx 40rpx; }
/* 搜索 */
.search-bar { display: flex; gap: 16rpx; margin-bottom: 24rpx; padding-top: 16rpx; }
.search-inner {
flex: 1; height: 76rpx; background: var(--color-surface); border-radius: var(--radius-round);
display: flex; align-items: center; padding: 0 24rpx; gap: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
}
.search-icon { font-size: 26rpx; }
.search-input { flex: 1; font-size: 26rpx; color: var(--color-text); height: 100%; }
.search-clear { font-size: 28rpx; color: var(--color-text-tertiary); padding: 8rpx; }
.search-btn {
height: 76rpx; line-height: 76rpx; padding: 0 36rpx; background: var(--color-primary);
color: #fff; border-radius: var(--radius-round); font-size: 26rpx; font-weight: 600; flex-shrink: 0;
}
/* 区块 */
.section { margin-bottom: 24rpx; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
.section-header-left { display: flex; align-items: center; gap: 16rpx; }
.section-back { font-size: 28rpx; color: var(--color-primary); font-weight: 500; padding: 8rpx 0; }
.section-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
/* 公司网格 */
.company-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; }
.company-card {
padding: 24rpx; border-radius: var(--radius-lg); text-align: center;
background: linear-gradient(135deg, #EEF2FF, #E0E7FF);
}
.company-card:active { opacity: 0.7; }
.company-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); display: block; }
.company-count { font-size: 20rpx; color: var(--color-text-secondary); margin-top: 8rpx; display: block; }
/* 岗位列表 */
.position-list { display: flex; flex-direction: column; gap: 16rpx; }
.pos-item { padding: 24rpx 28rpx; border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: space-between; }
.pos-item:active { transform: scale(0.98); }
.pos-left { display: flex; align-items: center; gap: 16rpx; flex: 1; min-width: 0; }
.pos-icon { font-size: 36rpx; }
.pos-body { flex: 1; min-width: 0; }
.pos-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); }
.pos-meta { font-size: 22rpx; color: var(--color-text-secondary); margin-top: 4rpx; display: block; }
.pos-arrow { font-size: 32rpx; color: var(--color-text-tertiary); flex-shrink: 0; margin-left: 12rpx; }
/* 题目列表 */
.question-list { display: flex; flex-direction: column; gap: 16rpx; }
.question-item { padding: 24rpx; border-radius: var(--radius-lg); }
.q-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; flex-wrap: wrap; }
.q-num { font-size: 22rpx; color: var(--color-primary); font-weight: 700; }
.q-tag { font-size: 20rpx; padding: 2rpx 14rpx; border-radius: var(--radius-round); font-weight: 500; }
.q-tag-tech { background: #DCFCE7; color: #166534; }
.q-tag-behavior { background: #FEF3C7; color: #92400E; }
.q-diff { font-size: 20rpx; padding: 2rpx 14rpx; border-radius: var(--radius-round); font-weight: 500; }
.q-diff-junior { background: #EEF2FF; color: var(--color-primary); }
.q-diff-medium { background: #FEF3C7; color: #92400E; }
.q-diff-senior { background: #FEE2E2; color: #991B1B; }
.q-freq { font-size: 20rpx; color: var(--color-text-tertiary); margin-left: auto; }
.q-content { font-size: 28rpx; color: var(--color-text); line-height: 1.7; }
.q-tags { display: flex; flex-wrap: wrap; gap: 8rpx; margin-top: 12rpx; }
.q-tags .tag { font-size: 20rpx; background: #F3F0FF; color: var(--color-primary); padding: 4rpx 16rpx; border-radius: var(--radius-round); }
.q-answer { margin-top: 16rpx; padding: 20rpx; background: #F9FAFB; border-radius: var(--radius-md); }
.answer-label { font-size: 22rpx; color: var(--color-primary); font-weight: 600; display: block; margin-bottom: 8rpx; }
.answer-text { font-size: 24rpx; color: var(--color-text-secondary); line-height: 1.6; }
/* 空状态 */
.empty { text-align: center; padding: 60rpx 0; background: var(--color-surface); border-radius: var(--radius-lg); }
.empty-icon { font-size: 48rpx; display: block; margin-bottom: 12rpx; }
.empty-text { font-size: 28rpx; color: var(--color-text-secondary); display: block; }
.empty-hint { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 8rpx; display: block; }
/* 加载 */
.loading-bar { text-align: center; padding: 40rpx; background: var(--color-surface); border-radius: var(--radius-lg); }
.loading-text { font-size: 24rpx; color: var(--color-text-tertiary); }
</style>