feat: latest code update
This commit is contained in:
@@ -64,14 +64,14 @@
|
||||
<text class="user-name">{{ u.nickname || '--' }}</text>
|
||||
</view>
|
||||
<view class="user-badges">
|
||||
<text class="user-plan" :class="{ vip: u.plan === 'growth' || u.plan === 'vip' }">{{ u.plan === 'growth' || u.plan === 'vip' ? '会员' : '免费' }}</text>
|
||||
<text class="user-plan" :class="{ vip: u.plan === 'growth' || u.plan === 'sprint' }">{{ u.plan === 'growth' || u.plan === 'sprint' ? '会员' : '免费' }}</text>
|
||||
<text class="user-credit">面试:{{ u.interviewCredits ?? 0 }}</text>
|
||||
<text class="user-credit">优化:{{ u.resumeOptimizeCredits ?? 0 }}</text>
|
||||
<text class="user-credit">下载:{{ u.resumeDownloadCredits ?? 0 }}</text>
|
||||
<text class="user-credit share">分享:{{ u.shareCredits ?? 0 }}</text>
|
||||
</view>
|
||||
<view class="user-actions">
|
||||
<text class="user-action-btn" v-if="u.plan !== 'growth' && u.plan !== 'vip'" @click="setVip(u._id)">设为会员</text>
|
||||
<text class="user-action-btn" v-if="u.plan === 'free'" @click="setVip(u._id)">设为会员</text>
|
||||
<text class="user-action-btn credit" @click="openCreditModal(u)">调整额度</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -339,7 +339,7 @@ const resumeLoading = ref(false)
|
||||
const adminKeyword = ref('')
|
||||
const adminList = ref([])
|
||||
const searchResult = ref(null)
|
||||
const memberConfig = ref({ interview: { maxRoundsFree: 5, maxRoundsVip: 10, dailyFreeLimit: 3 }, diagnosis: { dailyFreeLimit: 2 }, optimize: { dailyFreeLimit: 2 }, price: { monthly: 2900 } })
|
||||
const memberConfig = ref({ interview: { maxRoundsFree: 5, maxRoundsVip: 10, dailyFreeLimit: 3 }, diagnosis: { dailyFreeLimit: 2 }, optimize: { dailyFreeLimit: 2 }, price: { monthly: 1990 } })
|
||||
const cfgLoading = ref(false)
|
||||
const pricing = ref({
|
||||
interview: { pricePerSession: 500 },
|
||||
@@ -360,7 +360,7 @@ const growthPriceDisplay = computed(() => growthPriceTemp.value.toFixed(1))
|
||||
const sprintPriceDisplay = computed(() => sprintPriceTemp.value.toFixed(1))
|
||||
|
||||
const calcInterviewPrice = () => {
|
||||
// Convert to 分 on save
|
||||
// Handled in savePricing via growthPriceTemp / sprintPriceTemp
|
||||
}
|
||||
const orders = ref([])
|
||||
const ordersTotal = ref(0)
|
||||
@@ -399,7 +399,7 @@ const doVerify = async () => {
|
||||
try {
|
||||
const res = await apiAdmin('/check')
|
||||
if (res.statusCode === 200 && res.data?.isAdmin) {
|
||||
adminName.value = '管理员'
|
||||
adminName.value = res.data.nickname || res.data.username || '管理员'
|
||||
verified.value = true
|
||||
loadOverview()
|
||||
} else throw new Error('无管理员权限')
|
||||
@@ -500,15 +500,6 @@ const savePricing = async () => {
|
||||
finally { pricingLoading.value = false }
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
cfgLoading.value = true
|
||||
try {
|
||||
const res = await apiAdmin('/config')
|
||||
if (res.statusCode === 200) memberConfig.value = res.data
|
||||
} catch(e) { console.error(e) }
|
||||
finally { cfgLoading.value = false }
|
||||
}
|
||||
|
||||
const loadOrders = async () => {
|
||||
orderLoading.value = true
|
||||
ordersPage.value = 1
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@tap="selectCompany(c.name)"
|
||||
>
|
||||
<view class="company-name">{{ c.name }}</view>
|
||||
<view class="company-count">{{ c.positions }} 个岗位</view>
|
||||
<view class="company-count">{{ c.positionCount > 0 ? c.positionCount + ' 个岗位' : '暂无题库' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -84,23 +84,12 @@
|
||||
<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,
|
||||
hotCompanies: [],
|
||||
selectedCompany: '',
|
||||
selectedPosition: '',
|
||||
positions: [],
|
||||
@@ -109,7 +98,18 @@ export default {
|
||||
loadingQuestions: false,
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadHotCompanies()
|
||||
},
|
||||
methods: {
|
||||
async loadHotCompanies() {
|
||||
try {
|
||||
const res = await uni.request({ url: api('/contribution/companies/hot'), method: 'GET' })
|
||||
if (res.statusCode === 200) this.hotCompanies = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
difficultyLabel(d) {
|
||||
const map = { junior: '简单', medium: '中等', senior: '困难' }
|
||||
return map[d] || d || '中等'
|
||||
|
||||
@@ -75,10 +75,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { api } from '../../config'
|
||||
|
||||
const props = defineProps({ interviewId: String, position: String })
|
||||
const interviewId = ref('')
|
||||
const urlPosition = ref('')
|
||||
const form = ref({ company: '', position: '', rounds: '', experience: '', tags: [] })
|
||||
const questionsText = ref('')
|
||||
const customTag = ref('')
|
||||
@@ -89,8 +91,14 @@ const presetTags = ['算法题多', '重视项目经历', '面试官nice', '压
|
||||
|
||||
const token = () => uni.getStorageSync('token') || ''
|
||||
|
||||
onMounted(() => {
|
||||
if (props.position) form.value.position = props.position
|
||||
onLoad((options) => {
|
||||
if (options?.position) {
|
||||
urlPosition.value = decodeURIComponent(options.position)
|
||||
form.value.position = urlPosition.value
|
||||
}
|
||||
if (options?.interviewId) {
|
||||
interviewId.value = options.interviewId
|
||||
}
|
||||
})
|
||||
|
||||
const toggleTag = (tag) => {
|
||||
@@ -122,7 +130,7 @@ const submit = async () => {
|
||||
url: api('/contribution'), method: 'POST',
|
||||
header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' },
|
||||
data: {
|
||||
interviewId: props.interviewId || '',
|
||||
interviewId: interviewId.value || '',
|
||||
company: form.value.company.trim(),
|
||||
position: form.value.position.trim(),
|
||||
rounds: form.value.rounds.trim(),
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<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>
|
||||
<text class="tag tag-remaining">{{ userInfo.interviewCredits > 0 ? '剩余 ' + userInfo.interviewCredits + ' 次' : '已用完' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="arrow">›</text>
|
||||
@@ -112,19 +112,18 @@
|
||||
<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>
|
||||
<text class="pos-icon">{{ pos.icon || posIcons[idx % posIcons.length] || '💼' }}</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 class="pos-meta-row" v-if="pos.company || pos.salary">
|
||||
<text class="pos-company">{{ pos.company }}</text>
|
||||
<text class="pos-salary">{{ pos.salary }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -190,7 +189,19 @@ onMounted(async () => {
|
||||
|
||||
onShow(loadUserInfo)
|
||||
|
||||
const refreshDaily = () => { showAnswer.value = false; /* trigger reload */ }
|
||||
const refreshDaily = async () => {
|
||||
showAnswer.value = false
|
||||
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 */ }
|
||||
}
|
||||
|
||||
const goProfile = () => uni.switchTab({ url: '/pages/user/user' })
|
||||
const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
|
||||
@@ -242,7 +253,6 @@ const startInterview = (pos) => uni.navigateTo({ url: `/pages/interview/intervie
|
||||
.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; }
|
||||
|
||||
@@ -100,12 +100,13 @@ let recorder = null
|
||||
let timerSeconds = 0
|
||||
let timerInterval = null
|
||||
|
||||
const progressPercent = computed(() => Math.min((answeredCount.value / 5) * 100, 100))
|
||||
let MAX_QUESTIONS = 10
|
||||
const progressPercent = computed(() => Math.min((answeredCount.value / MAX_QUESTIONS) * 100, 100))
|
||||
const formatTime = computed(() => {
|
||||
const m = Math.floor(timerSeconds / 60); const s = timerSeconds % 60
|
||||
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||
})
|
||||
const token = computed(() => uni.getStorageSync('token') || '')
|
||||
const token = () => uni.getStorageSync('token') || ''
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.position) {
|
||||
@@ -117,7 +118,7 @@ onLoad((options) => {
|
||||
|
||||
onMounted(() => {
|
||||
timerInterval = setInterval(() => timerSeconds++, 1000)
|
||||
if (token.value) startInterview()
|
||||
if (token()) startInterview()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -125,7 +126,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const checkLogin = () => {
|
||||
if (!token.value) {
|
||||
if (!token()) {
|
||||
uni.showModal({
|
||||
title: '请先登录', content: '登录后即可开始 AI 模拟面试', confirmText: '去登录',
|
||||
success: (r) => { if (r.confirm) uni.navigateTo({ url: '/pages/login/login' }) },
|
||||
@@ -141,18 +142,22 @@ const startInterview = async () => {
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: api('/interview/create'), method: 'POST',
|
||||
header: { 'Authorization': `Bearer ${token.value}`, 'Content-Type': 'application/json' },
|
||||
header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' },
|
||||
data: { position: position.value },
|
||||
})
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
interviewId.value = res.data.id
|
||||
messages.value = res.data.messages || messages.value
|
||||
answeredCount.value = res.data.questionCount || 0
|
||||
if (res.data.totalQuestions) MAX_QUESTIONS = res.data.totalQuestions
|
||||
// Speak first question in avatar mode
|
||||
if (avatarMode.value && res.data.messages?.length) {
|
||||
const last = res.data.messages[res.data.messages.length - 1]
|
||||
if (last?.role === 'ai') await speakAiText(last.content)
|
||||
}
|
||||
} else {
|
||||
const msg = res.data?.message || '创建面试失败'
|
||||
messages.value.push({ role: 'ai', content: msg })
|
||||
}
|
||||
} catch {
|
||||
messages.value.push({ role: 'ai', content: '创建面试失败,请重试' })
|
||||
@@ -164,8 +169,11 @@ const startInterview = async () => {
|
||||
|
||||
const sendAnswer = async () => {
|
||||
if (!inputText.value.trim() || aiLoading.value || isComplete.value) return
|
||||
if (!token.value) { checkLogin(); return }
|
||||
if (!interviewId.value) { await startInterview(); return }
|
||||
if (!token()) { checkLogin(); return }
|
||||
if (!interviewId.value) {
|
||||
await startInterview()
|
||||
if (!interviewId.value) return // creation failed, don't discard answer
|
||||
}
|
||||
|
||||
const answer = inputText.value.trim()
|
||||
messages.value.push({ role: 'user', content: answer })
|
||||
@@ -176,19 +184,24 @@ const sendAnswer = async () => {
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: api(`/interview/${interviewId.value}/answer`), method: 'POST',
|
||||
header: { 'Authorization': `Bearer ${token.value}`, 'Content-Type': 'application/json' },
|
||||
header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' },
|
||||
data: avatarMode.value ? { answer, avatar: true } : { answer },
|
||||
})
|
||||
if (res.statusCode === 200 && res.data?.messages) {
|
||||
const aiMsg = res.data.messages.find(m => m.role === 'ai')
|
||||
messages.value.push(...res.data.messages)
|
||||
if (avatarMode.value && aiMsg) {
|
||||
// Only push AI messages from response to avoid duplicating the user message already added above
|
||||
const newAiMessages = res.data.messages.filter(m => m.role === 'ai')
|
||||
if (newAiMessages.length > 0) messages.value.push(...newAiMessages)
|
||||
if (avatarMode.value && aiMsg) {
|
||||
await speakAiText(aiMsg.content, res.data.ttsHash, res.data.ttsAmplitude)
|
||||
}
|
||||
answeredCount.value = res.data.questionCount || answeredCount.value + 1
|
||||
if (res.data.ttsHash && !avatarMode.value) {
|
||||
// Still got TTS but not in avatar mode, just show text
|
||||
}
|
||||
if (res.data.totalQuestions) MAX_QUESTIONS = res.data.totalQuestions
|
||||
} else if (res.statusCode === 403) {
|
||||
messages.value.push({ role: 'ai', content: res.data?.message || '面试次数已用完' })
|
||||
isComplete.value = true
|
||||
} else {
|
||||
messages.value.push({ role: 'ai', content: res.data?.message || '回答提交失败' })
|
||||
}
|
||||
} catch {
|
||||
messages.value.push({ role: 'ai', content: '回答提交失败,请重试' })
|
||||
@@ -207,7 +220,7 @@ async function speakAiText(text, ttsHash, ttsAmplitude) {
|
||||
try {
|
||||
const synthRes = await uni.request({
|
||||
url: api('/tts/synthesize'), method: 'POST',
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token()}` },
|
||||
data: { text },
|
||||
})
|
||||
if (synthRes.statusCode === 200 && synthRes.data?.hash) {
|
||||
@@ -240,12 +253,17 @@ const confirmExit = () => {
|
||||
|
||||
function startRecord() {
|
||||
if (aiLoading.value || isComplete.value) return
|
||||
// #ifdef MP-WEIXIN
|
||||
isRecording.value = true
|
||||
recorder = uni.getRecorderManager()
|
||||
recorder.onStart(() => {})
|
||||
recorder.onError(() => { isRecording.value = false })
|
||||
recorder.start({ format: 'mp3' })
|
||||
uni.vibrateShort({ type: 'medium' })
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.showToast({ title: '语音输入仅支持小程序', icon: 'none' })
|
||||
// #endif
|
||||
}
|
||||
|
||||
function stopRecord() {
|
||||
@@ -260,7 +278,7 @@ function stopRecord() {
|
||||
url: api(API_ENDPOINTS.TTS.ASR),
|
||||
filePath: audioPath,
|
||||
name: 'audio',
|
||||
header: { 'Authorization': `Bearer ${token.value}` },
|
||||
header: { 'Authorization': `Bearer ${token()}` },
|
||||
})
|
||||
console.log('[ASR] upload response:', uploadRes.statusCode, typeof uploadRes.data === 'string' ? uploadRes.data.slice(0, 200) : JSON.stringify(uploadRes.data).slice(0, 200))
|
||||
if (uploadRes.statusCode === 200 && uploadRes.data) {
|
||||
|
||||
@@ -147,7 +147,7 @@ onMounted(() => {
|
||||
// #endif
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => { if (timer) clearInterval(timer) })
|
||||
onBeforeUnmount(() => { if (timer) { clearTimeout(timer); timer = null } })
|
||||
|
||||
// 辅助
|
||||
const showToast = (title, icon = 'none') => uni.showToast({ title, icon })
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<view class="plan-badge sprint-badge">🚀 冲刺</view>
|
||||
<view class="plan-header">
|
||||
<text class="plan-name">冲刺版</text>
|
||||
<text class="plan-price"><text class="price-num price-sprint">¥49.9</text><text class="price-unit">/月</text></text>
|
||||
<text class="plan-price"><text class="price-num price-sprint">{{ sprintPriceText }}</text><text class="price-unit">/月</text></text>
|
||||
</view>
|
||||
<view class="plan-features">
|
||||
<text class="feat" v-for="f in sprintFeatures" :key="f">✓ {{ f }}</text>
|
||||
@@ -50,7 +50,7 @@
|
||||
<view class="plan-action" v-if="!isLoggedIn" @click="goLogin">登录后开通</view>
|
||||
<view class="plan-action owned" v-else-if="plan === 'sprint'">✅ 已开通</view>
|
||||
<view class="plan-action" v-else-if="plan === 'growth'" @click="startPay('sprint')">升级至冲刺版</view>
|
||||
<view class="plan-action" v-else @click="startPay('sprint')">¥49.9/月 立即开通</view>
|
||||
<view class="plan-action" v-else @click="startPay('sprint')">{{ sprintPriceText }}/月 立即开通</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -104,16 +104,11 @@ const payError = ref('')
|
||||
const payingPlanName = ref('')
|
||||
const payingPlan = ref('')
|
||||
const growthPriceText = ref('¥19.9')
|
||||
const sprintPriceText = ref('¥49.9')
|
||||
const currentOutTradeNo = ref('')
|
||||
const freeFeatures = ['每日 2 次 AI 模拟面试', '基础面试报告', '通用题库随机出题', '简历诊断(限 3 次)']
|
||||
const growthFeatures = [
|
||||
'免费版全部权益', '无限面试次数', '详细面试报告(四维评分)',
|
||||
'进步轨迹雷达图 + 打卡', '每日一题推送', '参考回答思路', '公司真题库',
|
||||
]
|
||||
const sprintFeatures = [
|
||||
'成长版全部权益', 'AI 语音分析(语气词/语速检测)', '技能缺口分析报告',
|
||||
'学习路径推荐', '真人导师 1v1 点评(每月 1 次)', '简历精修(每月 1 次)', '内推优先',
|
||||
]
|
||||
const freeFeatures = ref(['AI 模拟面试 1 次(体验)', '基础面试报告', '通用题库随机出题', '简历优化(限 3 次免费)'])
|
||||
const growthFeatures = ref(['免费版全部权益', 'AI 数字人面试无限次', '详细面试报告(四维评分)', '进步轨迹雷达图 + 打卡', '每日一题推送', '参考回答思路', '公司真题库', '每场最多 10 轮 AI 对话'])
|
||||
const sprintFeatures = ref(['成长版全部权益', 'AI 语音分析(语气词/语速检测)', '技能缺口分析报告', '学习路径推荐', '真人导师 1v1 点评(每月 1 次)', '简历精修(每月 1 次)', '内推优先', 'AI 实时提示功能'])
|
||||
|
||||
const token = () => uni.getStorageSync('token') || ''
|
||||
|
||||
@@ -136,9 +131,19 @@ onMounted(async () => {
|
||||
plan.value = d.plan || 'free'
|
||||
currentPlanName.value = d.planName || '免费版'
|
||||
}
|
||||
if (lres.statusCode === 200 && lres.data?.price) {
|
||||
const p = lres.data.price
|
||||
growthPriceText.value = `¥${(p.monthly / 100).toFixed(1)}`
|
||||
if (lres.statusCode === 200 && lres.data) {
|
||||
const plans = Array.isArray(lres.data.plans) ? lres.data.plans : (Array.isArray(lres.data) ? lres.data : [])
|
||||
const growth = plans.find((p) => p.id === 'growth')
|
||||
const sprint = plans.find((p) => p.id === 'sprint')
|
||||
if (growth) {
|
||||
growthPriceText.value = `¥${(growth.price / 100).toFixed(1)}`
|
||||
if (growth.features?.length) growthFeatures.value = growth.features
|
||||
}
|
||||
if (sprint?.features?.length) sprintFeatures.value = sprint.features
|
||||
if (sprint) sprintPriceText.value = `¥${(sprint.price / 100).toFixed(1)}`
|
||||
if (lres.data.price?.monthly) {
|
||||
growthPriceText.value = `¥${(lres.data.price.monthly / 100).toFixed(1)}`
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
})
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { api } from '../../config'
|
||||
|
||||
const stats = ref({ completedInterviews: 0, avgScore: 0, streak: 0 })
|
||||
@@ -155,6 +156,16 @@ onMounted(async () => {
|
||||
const t = token()
|
||||
if (!t) return
|
||||
|
||||
await loadProgressData()
|
||||
})
|
||||
|
||||
onShow(async () => {
|
||||
if (token()) await loadProgressData()
|
||||
})
|
||||
|
||||
async function loadProgressData() {
|
||||
const t = token()
|
||||
if (!t) return
|
||||
try {
|
||||
// Load progress
|
||||
const res = await uni.request({
|
||||
@@ -166,7 +177,7 @@ onMounted(async () => {
|
||||
progress.value = d
|
||||
dimensions.value = dimensions.value.map(dim => ({
|
||||
...dim,
|
||||
value: d.dimensions?.[dim.key] || Math.round(50 + Math.random() * 30),
|
||||
value: d.dimensions?.[dim.key] || 0,
|
||||
}))
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
@@ -191,23 +202,29 @@ onMounted(async () => {
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
|
||||
// Build week days
|
||||
buildWeekDays()
|
||||
}
|
||||
|
||||
function buildWeekDays() {
|
||||
const days = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const today = new Date()
|
||||
const arr = []
|
||||
const checkinDates = (progress.value.checkins || []).map((c) => {
|
||||
const d = new Date(c.date || c.createdAt)
|
||||
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
|
||||
})
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(today)
|
||||
d.setDate(d.getDate() - i)
|
||||
const isToday = i === 0
|
||||
// Mark days with interviews (simulate based on streak)
|
||||
const key = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
|
||||
arr.push({
|
||||
label: days[d.getDay()],
|
||||
isToday,
|
||||
done: i < (stats.value.streak || 0),
|
||||
isToday: i === 0,
|
||||
done: checkinDates.includes(key),
|
||||
})
|
||||
}
|
||||
weekDays.value = arr
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (d) => {
|
||||
if (!d) return ''
|
||||
|
||||
@@ -156,7 +156,9 @@ onLoad(async (options) => {
|
||||
}))
|
||||
}
|
||||
}
|
||||
}).catch(() => {})
|
||||
}).catch(e => {
|
||||
console.error('[report] auto-complete failed:', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch(e) { console.error(e) }
|
||||
@@ -288,15 +290,12 @@ async function generateCard() {
|
||||
ctx.setFillStyle('rgba(165,180,252,0.5)')
|
||||
ctx.fillText('扫码开始你的模拟面试 → 在微信搜索"职引"小程序', w / 2, 690)
|
||||
|
||||
// QR code hint (simulated)
|
||||
// QR text hint
|
||||
ctx.setFillStyle('#FFFFFF')
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFontSize(16)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.fillText('⬛ ⬛ ⬛ ⬛ ⬛', w / 2, 760)
|
||||
ctx.fillText('⬛ ⬛ ⬛ ⬛ ⬛', w / 2, 780)
|
||||
ctx.fillText('⬛ ⬛ ⬛ ⬛ ⬛', w / 2, 800)
|
||||
ctx.fillText('⬛ ⬛ ⬛ ⬛ ⬛', w / 2, 820)
|
||||
ctx.fillText('微信小程序', w / 2, 855)
|
||||
ctx.fillText('在微信搜索「职引」小程序', w / 2, 760)
|
||||
ctx.fillText('查看完整面试报告', w / 2, 790)
|
||||
|
||||
ctx.draw(false, async () => {
|
||||
try {
|
||||
@@ -306,7 +305,9 @@ async function generateCard() {
|
||||
itemList: ['保存到相册', '分享给好友'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.saveImageToPhotosAlbum({ filePath: tempRes.tempFilePath })
|
||||
uni.saveImageToPhotosAlbum({ filePath: tempRes.tempFilePath, success: () => uni.showToast({ title: '已保存到相册', icon: 'success' }) })
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.shareAppMessage ? uni.shareAppMessage({ title: '我的面试报告', imageUrl: tempRes.tempFilePath }) : uni.showToast({ title: '请截图后分享', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<text class="score-num">{{ diagnosisResult.score }}</text>
|
||||
<text class="score-label">/100</text>
|
||||
</view>
|
||||
<text class="summary-text">{{ diagnosisResult.summary }}</text>
|
||||
<text class="summary-text" v-if="diagnosisResult.summary">{{ diagnosisResult.summary }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 岗位匹配度(诊断模式) -->
|
||||
@@ -136,16 +136,19 @@ onLoad(async (options: any) => {
|
||||
function applyResult(data: any) {
|
||||
loading.value = false;
|
||||
if (isOptimize.value) {
|
||||
optimizedContent.value = data.optimizedContent || '';
|
||||
changes.value = data.changes || [];
|
||||
highlights.value = data.highlights || [];
|
||||
optimizedContent.value = data.optimized || '';
|
||||
changes.value = (data.changes || []).map((c: any) =>
|
||||
typeof c === 'string' ? { section: c, description: c } : c
|
||||
);
|
||||
highlights.value = [];
|
||||
} else {
|
||||
diagnosisResult.value = data;
|
||||
changes.value = (data.issues || []).map((i: any) => ({
|
||||
...i,
|
||||
typeLabel: i.type === 'structure' ? '结构' : i.type === 'content' ? '内容' : i.type === 'keywords' ? '关键词' : i.type === 'achievement' ? '成就' : '格式',
|
||||
typeLabel: i.level === 'high' ? '严重' : i.level === 'medium' ? '中等' : '轻微',
|
||||
description: i.desc || i.description,
|
||||
}));
|
||||
highlights.value = data.strengths || [];
|
||||
highlights.value = data.suggestions || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,10 +125,12 @@ const todayStats = computed(() => ({
|
||||
credited: stats.value.todayCredited || 0,
|
||||
}))
|
||||
|
||||
let isWechat = false
|
||||
const isWechat = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
isWechat = navigator?.userAgent?.toLowerCase().includes('micromessenger') || false
|
||||
// #ifdef H5
|
||||
isWechat.value = navigator?.userAgent?.toLowerCase().includes('micromessenger') || false
|
||||
// #endif
|
||||
loadData()
|
||||
})
|
||||
|
||||
|
||||
@@ -99,6 +99,18 @@ const refreshState = () => {
|
||||
try { const s = uni.getStorageSync('userInfo'); if (s) userInfo.value = JSON.parse(s) } catch(e) {}
|
||||
loadStats()
|
||||
checkAdmin()
|
||||
// Fetch fresh user info from API to update stale cache (e.g. credits changed after interview)
|
||||
fetchUserInfo()
|
||||
}
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const res = await uni.request({ url: api('/user/info'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } })
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
userInfo.value = res.data
|
||||
uni.setStorageSync('userInfo', JSON.stringify(res.data))
|
||||
}
|
||||
} catch(e) { /* silent - cached data is fallback */ }
|
||||
}
|
||||
|
||||
onMounted(refreshState)
|
||||
|
||||
Reference in New Issue
Block a user