初始化:职引项目 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
@@ -0,0 +1,191 @@
<template>
<view class="page fade-in">
<view class="hero">
<text class="hero-title">贡献面经</text>
<text class="hero-sub">分享你的面试经验帮助更多同学</text>
</view>
<view class="form card">
<view class="form-group">
<text class="form-label">公司名称 <text class="required">*</text></text>
<input class="form-input" v-model="form.company" placeholder="如:腾讯、字节跳动、阿里巴巴" />
</view>
<view class="form-group">
<text class="form-label">面试岗位 <text class="required">*</text></text>
<input class="form-input" v-model="form.position" placeholder="如:前端工程师、产品经理" />
</view>
<view class="form-group">
<text class="form-label">面试轮次</text>
<input class="form-input" v-model="form.rounds" placeholder="如:一面(技术面)" />
</view>
<view class="form-group">
<text class="form-label">遇到的面试题每行一题</text>
<textarea
class="form-textarea"
v-model="questionsText"
placeholder="把你记得的面试题写下来,帮大家提前准备:&#10;1. 请介绍一下你最熟悉的项目&#10;2. 解释一下闭包的原理&#10;..."
:maxlength="2000"
></textarea>
<text class="form-hint">{{ questionsText.length }}/2000</text>
</view>
<view class="form-group">
<text class="form-label">面试感受/经验</text>
<textarea
class="form-textarea"
v-model="form.experience"
placeholder="分享一下整体感受、面试官风格、需要特别注意的地方..."
:maxlength="1000"
></textarea>
<text class="form-hint">{{ form.experience.length }}/1000</text>
</view>
<view class="form-group">
<text class="form-label">标签可选</text>
<view class="tag-input-area">
<view class="tag-list">
<view
v-for="tag in presetTags"
:key="tag"
class="tag-item"
:class="{ selected: form.tags.includes(tag) }"
@click="toggleTag(tag)"
>{{ tag }}</view>
</view>
<input class="form-input" v-model="customTag" placeholder="自定义标签,回车添加" @confirm="addCustomTag" />
</view>
</view>
<button class="btn-submit" @click="submit" :disabled="submitting">
{{ submitting ? '提交中...' : '提交面经' }}
</button>
<view class="success-box" v-if="submitted">
<text class="success-icon">🎉</text>
<text class="success-text">感谢你的分享你的面经将帮助更多同学准备面试</text>
<text class="success-action" @click="goBack">返回</text>
</view>
</view>
<view class="bottom-spacer"></view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { api } from '../../config'
const props = defineProps({ interviewId: String, position: String })
const form = ref({ company: '', position: '', rounds: '', experience: '', tags: [] })
const questionsText = ref('')
const customTag = ref('')
const submitting = ref(false)
const submitted = ref(false)
const presetTags = ['算法题多', '重视项目经历', '面试官nice', '压力面', '手撕代码', '系统设计', '行为面试', '八股文']
const token = () => uni.getStorageSync('token') || ''
onMounted(() => {
if (props.position) form.value.position = props.position
})
const toggleTag = (tag) => {
const idx = form.value.tags.indexOf(tag)
if (idx > -1) form.value.tags.splice(idx, 1)
else form.value.tags.push(tag)
}
const addCustomTag = () => {
const t = customTag.value.trim()
if (t && !form.value.tags.includes(t) && form.value.tags.length < 10) {
form.value.tags.push(t)
customTag.value = ''
}
}
const submit = async () => {
if (!form.value.company.trim()) { uni.showToast({ title: '请填写公司名称', icon: 'none' }); return }
if (!form.value.position.trim()) { uni.showToast({ title: '请填写面试岗位', icon: 'none' }); return }
submitting.value = true
try {
const questions = questionsText.value
.split('\n')
.map(q => q.replace(/^\d+[\.\、\s]+/, '').trim())
.filter(q => q.length > 0)
const res = await uni.request({
url: api('/contribution'), method: 'POST',
header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' },
data: {
interviewId: props.interviewId || '',
company: form.value.company.trim(),
position: form.value.position.trim(),
rounds: form.value.rounds.trim(),
questions,
experience: form.value.experience.trim(),
tags: form.value.tags,
},
})
if (res.statusCode >= 200 && res.statusCode < 300) {
submitted.value = true
uni.showToast({ title: '提交成功!', icon: 'success' })
} else {
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '网络错误', icon: 'none' })
} finally {
submitting.value = false
}
}
const goBack = () => uni.navigateBack()
</script>
<style scoped>
.page { min-height: 100vh; background: var(--color-bg); }
.hero { background: linear-gradient(135deg, #10B981, #34D399, #6EE7B7); padding: 48rpx 32rpx 72rpx; border-radius: 0 0 48rpx 48rpx; }
.hero-title { font-size: 40rpx; font-weight: 700; color: #FFF; display: block; }
.hero-sub { font-size: 22rpx; color: rgba(255,255,255,0.8); margin-top: 8rpx; display: block; }
.form { margin: -40rpx 32rpx 0; border-radius: var(--radius-xl); padding: 32rpx; }
.form-group { margin-bottom: 28rpx; }
.form-label { font-size: 26rpx; font-weight: 600; color: var(--color-text); display: block; margin-bottom: 10rpx; }
.required { color: #EF4444; }
.form-input {
width: 100%; height: 72rpx; background: #F9FAFB; border-radius: var(--radius-md);
padding: 0 20rpx; font-size: 26rpx; box-sizing: border-box; border: 1rpx solid var(--color-border);
}
.form-textarea {
width: 100%; min-height: 180rpx; background: #F9FAFB; border-radius: var(--radius-md);
padding: 16rpx 20rpx; font-size: 26rpx; box-sizing: border-box; border: 1rpx solid var(--color-border);
}
.form-hint { font-size: 20rpx; color: var(--color-text-tertiary); text-align: right; display: block; margin-top: 6rpx; }
.tag-input-area { display: flex; flex-direction: column; gap: 12rpx; }
.tag-list { display: flex; flex-wrap: wrap; gap: 10rpx; }
.tag-item {
padding: 6rpx 18rpx; border-radius: var(--radius-round); font-size: 22rpx;
background: #F3F4F6; color: var(--color-text-secondary); border: 1rpx solid #E5E7EB;
transition: all 0.2s;
}
.tag-item.selected { background: linear-gradient(135deg, #D1FAE5, #A7F3D0); color: #065F46; border-color: #10B981; }
.btn-submit {
width: 100%; height: 88rpx; border-radius: var(--radius-lg);
background: linear-gradient(135deg, #10B981, #34D399); color: #FFF;
font-size: 30rpx; font-weight: 700; border: none; margin-top: 8rpx;
}
.btn-submit[disabled] { opacity: 0.6; }
.success-box { display: flex; flex-direction: column; align-items: center; padding: 40rpx 0; gap: 12rpx; background: #ECFDF5; border-radius: var(--radius-lg); margin-top: 24rpx; }
.success-icon { font-size: 56rpx; }
.success-text { font-size: 26rpx; color: #065F46; font-weight: 600; text-align: center; line-height: 1.5; }
.success-action { font-size: 28rpx; color: var(--color-primary); font-weight: 600; padding: 10rpx 40rpx; }
.bottom-spacer { height: 40rpx; }
</style>