feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1
去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<yxg-page :title="title || '测评'">
|
||||
<text v-if="loadingScale" class="hint">加载中…</text>
|
||||
<text v-else-if="loadError" class="err">{{ loadError }}</text>
|
||||
<template v-else>
|
||||
<view v-if="phase === 'intro'" class="card">
|
||||
<text class="h1">{{ title }}</text>
|
||||
<text class="desc">{{ description }}</text>
|
||||
<text class="meta">约 {{ estMinutes }} 分钟 · {{ questions.length }} 题</text>
|
||||
<text v-if="locked" class="err">该测评需开通成长会员</text>
|
||||
<view v-if="locked" class="btn" @click="yxgGo('/membership')"><text>去开通</text></view>
|
||||
<view v-else class="btn" @click="startTest"><text>{{ draftRestored ? '继续作答' : '开始测评' }}</text></view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="phase === 'answering' && currentQuestion" class="card">
|
||||
<text class="prog">{{ currentIdx + 1 }} / {{ questions.length }}</text>
|
||||
<view class="bar"><view class="bar-in" :style="{ width: progressPct + '%' }" /></view>
|
||||
<text class="prompt">{{ currentQuestion.body.prompt }}</text>
|
||||
<view
|
||||
v-for="opt in currentQuestion.body.options || []"
|
||||
:key="opt.key"
|
||||
class="opt"
|
||||
:class="{ on: answers[currentQuestion.id] === opt.key }"
|
||||
@click="pick(opt.key)"
|
||||
>
|
||||
<text>{{ opt.text }}</text>
|
||||
</view>
|
||||
<view class="navs">
|
||||
<view v-if="currentIdx > 0" class="btn ghost" @click="currentIdx--"><text>上一题</text></view>
|
||||
<view
|
||||
class="btn"
|
||||
:class="{ off: !answers[currentQuestion.id] || submitting }"
|
||||
@click="onNext"
|
||||
>
|
||||
<text>{{ submitting ? '提交中…' : isLast ? '提交' : '下一题' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="submitError" class="err">{{ submitError }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="phase === 'result'" class="card">
|
||||
<text class="h1">{{ resultLabel }}</text>
|
||||
<text class="desc">{{ shareLine || resultRaw?.summary || '' }}</text>
|
||||
<text v-if="resultRaw?.overview" class="body">{{ resultRaw.overview }}</text>
|
||||
<view class="navs">
|
||||
<view class="btn ghost" @click="retake"><text>再测一次</text></view>
|
||||
<view class="btn" @click="yxgGo('/reports')"><text>我的报告</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</yxg-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import YxgPage from '@/components/yxg/yxg-page.vue'
|
||||
import { yxgApi } from '@/util/yxgApi.js'
|
||||
import { ensureAccount, findSelfProfile } from '@/util/yxgAuth.js'
|
||||
import { yxgGo } from '@/util/yxgNav.js'
|
||||
|
||||
const DRAFT_KEY = 'yxg_scale_draft_'
|
||||
const slug = ref('')
|
||||
const title = ref('')
|
||||
const description = ref('')
|
||||
const questions = ref([])
|
||||
const answers = ref({})
|
||||
const loadingScale = ref(true)
|
||||
const submitting = ref(false)
|
||||
const loadError = ref('')
|
||||
const submitError = ref('')
|
||||
const locked = ref(false)
|
||||
const phase = ref('intro')
|
||||
const resultLabel = ref('')
|
||||
const shareLine = ref('')
|
||||
const resultRaw = ref(null)
|
||||
const draftRestored = ref(false)
|
||||
const currentIdx = ref(0)
|
||||
|
||||
const currentQuestion = computed(() => questions.value[currentIdx.value] || null)
|
||||
const isLast = computed(() => currentIdx.value >= questions.value.length - 1)
|
||||
const progressPct = computed(() => {
|
||||
if (!questions.value.length) return 0
|
||||
return Math.round(((currentIdx.value + 1) / questions.value.length) * 100)
|
||||
})
|
||||
const estMinutes = computed(() => Math.max(1, Math.ceil(questions.value.length / 3)))
|
||||
|
||||
function pick(key) {
|
||||
answers.value = { ...answers.value, [currentQuestion.value.id]: key }
|
||||
uni.setStorageSync(DRAFT_KEY + slug.value, answers.value)
|
||||
}
|
||||
|
||||
function startTest() {
|
||||
if (locked.value) return
|
||||
phase.value = 'answering'
|
||||
if (!draftRestored.value) currentIdx.value = 0
|
||||
}
|
||||
|
||||
function retake() {
|
||||
uni.removeStorageSync(DRAFT_KEY + slug.value)
|
||||
answers.value = {}
|
||||
draftRestored.value = false
|
||||
resultRaw.value = null
|
||||
currentIdx.value = 0
|
||||
phase.value = 'intro'
|
||||
}
|
||||
|
||||
async function onNext() {
|
||||
if (!answers.value[currentQuestion.value.id] || submitting.value) return
|
||||
if (!isLast.value) {
|
||||
currentIdx.value++
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
submitError.value = ''
|
||||
try {
|
||||
const self = await findSelfProfile()
|
||||
if (!self) {
|
||||
submitError.value = '请先完善自己的生日档案'
|
||||
yxgGo('/profile')
|
||||
return
|
||||
}
|
||||
const out = await yxgApi.submitScale(slug.value, self.id, answers.value)
|
||||
uni.removeStorageSync(DRAFT_KEY + slug.value)
|
||||
resultRaw.value = out.result || out
|
||||
resultLabel.value = String(resultRaw.value.label || '探索结果')
|
||||
shareLine.value = String(resultRaw.value.share_line || '')
|
||||
phase.value = 'result'
|
||||
} catch (e) {
|
||||
submitError.value = e instanceof Error ? e.message : '提交失败'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loadingScale.value = true
|
||||
loadError.value = ''
|
||||
if (!(await ensureAccount(`/scales/${slug.value}`))) {
|
||||
loadingScale.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const d = await yxgApi.getScale(slug.value)
|
||||
title.value = d.title
|
||||
description.value = d.description
|
||||
locked.value = !!d.locked
|
||||
questions.value = (d.questions || []).map((q) => ({
|
||||
id: q.id,
|
||||
body: typeof q.body === 'string' ? JSON.parse(q.body) : q.body,
|
||||
}))
|
||||
if (locked.value) {
|
||||
phase.value = 'intro'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const latest = await yxgApi.getScaleResult(slug.value)
|
||||
resultRaw.value = latest.result || latest
|
||||
resultLabel.value = String(resultRaw.value.label || '探索结果')
|
||||
shareLine.value = String(resultRaw.value.share_line || '')
|
||||
phase.value = 'result'
|
||||
return
|
||||
} catch { /* no result */ }
|
||||
const draft = uni.getStorageSync(DRAFT_KEY + slug.value)
|
||||
if (draft && typeof draft === 'object') {
|
||||
answers.value = draft
|
||||
draftRestored.value = Object.keys(draft).length > 0
|
||||
}
|
||||
phase.value = 'intro'
|
||||
} catch (e) {
|
||||
loadError.value = e instanceof Error ? e.message : '加载失败'
|
||||
} finally {
|
||||
loadingScale.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((q) => {
|
||||
slug.value = q.slug || ''
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background-color: #ffd1c7; }
|
||||
</style>
|
||||
<style scoped>
|
||||
.hint, .err { display: block; padding: 16px; font-size: 13px; color: #999; }
|
||||
.err { color: #e54d42; }
|
||||
.card {
|
||||
margin: 12px 16px 0; padding: 18px 16px; background: #fff; border-radius: 18px;
|
||||
}
|
||||
.h1 { display: block; font-size: 20px; font-weight: 700; color: #333; }
|
||||
.desc, .body, .meta { display: block; margin-top: 8px; font-size: 13px; color: #666; line-height: 1.6; }
|
||||
.prog { font-size: 12px; color: #999; }
|
||||
.bar { height: 6px; background: #f3ece8; border-radius: 99px; margin: 8px 0 14px; overflow: hidden; }
|
||||
.bar-in { height: 100%; background: #e54d42; }
|
||||
.prompt { display: block; font-size: 16px; font-weight: 650; color: #222; margin-bottom: 12px; line-height: 1.5; }
|
||||
.opt {
|
||||
padding: 12px; border-radius: 12px; background: #fdfaf8; margin-bottom: 8px; font-size: 14px; color: #333;
|
||||
}
|
||||
.opt.on { background: #ffe4e4; color: #e54d42; font-weight: 650; }
|
||||
.navs { display: flex; gap: 10px; margin-top: 12px; }
|
||||
.btn {
|
||||
flex: 1; height: 42px; border-radius: 999px; background: #e54d42; color: #fff;
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 700;
|
||||
}
|
||||
.btn.ghost { background: #f7f2ef; color: #8a4a3a; }
|
||||
.btn.off { opacity: 0.45; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user