feat: add exploration scale list, questions, and scoring
Seed communication-style scale, expose /api/v1/scales APIs, and wire Explore/Scale H5 pages for the P1 探索测试 path. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,20 +1,42 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<h1>探索</h1>
|
||||
<p class="sub">性格探索 · 人格测评 · 关系理解 · 个人画像</p>
|
||||
<p class="sub">性格探索 · 探索测试 · 关系理解 · 个人画像</p>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
<ul class="list">
|
||||
<li><router-link to="/portrait">性格探索 / 个人画像</router-link></li>
|
||||
<li>人格测评库(对接 /api/v1/scales)</li>
|
||||
<li><router-link to="/relation">关系理解</router-link></li>
|
||||
<li>身心探索(第二阶段)</li>
|
||||
<li v-for="s in scales" :key="s.slug">
|
||||
<router-link :to="`/scales/${s.slug}`">{{ s.title }}</router-link>
|
||||
<span class="desc">{{ s.description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const scales = ref<{ slug: string; title: string; description: string }[]>([])
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await api.listScales()
|
||||
scales.value = res.items || []
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:24px 16px}
|
||||
h1{font-size:22px}
|
||||
.sub{color:var(--yxg-sub);font-size:13px;margin:6px 0 16px}
|
||||
.list{background:#fff;border-radius:16px;padding:8px 16px;line-height:2.2;font-size:14px;color:#555}
|
||||
.list a{color:inherit;text-decoration:none}
|
||||
.err{color:var(--yxg-pri);font-size:13px}
|
||||
.list{background:#fff;border-radius:16px;padding:8px 16px;line-height:1.8;font-size:14px;color:#555}
|
||||
.list a{color:inherit;text-decoration:none;font-weight:600}
|
||||
.desc{display:block;font-size:12px;color:#aaa;font-weight:400;margin-bottom:8px}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<button class="back" type="button" @click="$router.back()">← 返回</button>
|
||||
<h1>{{ title || '探索测试' }}</h1>
|
||||
<p class="sub">{{ description }}</p>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
|
||||
<div v-if="!done" class="card">
|
||||
<div v-for="q in questions" :key="q.id" class="q">
|
||||
<p class="prompt">{{ q.body.prompt }}</p>
|
||||
<label v-for="opt in q.body.options" :key="opt.key" class="opt">
|
||||
<input v-model="answers[q.id]" type="radio" :value="opt.key" />
|
||||
{{ opt.text }}
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" :disabled="loading" @click="submit">查看探索结果</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="card">
|
||||
<h2>{{ resultLabel }}</h2>
|
||||
<p>{{ resultSummary }}</p>
|
||||
<p class="share">{{ shareLine }}</p>
|
||||
<router-link class="link" to="/relation">用结果去做关系理解 →</router-link>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const route = useRoute()
|
||||
const slug = String(route.params.slug || '')
|
||||
const title = ref('')
|
||||
const description = ref('')
|
||||
const questions = ref<{ id: string; body: { prompt: string; options: { key: string; text: string }[] } }[]>([])
|
||||
const answers = reactive<Record<string, string>>({})
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const done = ref(false)
|
||||
const resultLabel = ref('')
|
||||
const resultSummary = ref('')
|
||||
const shareLine = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const d = await api.getScale(slug)
|
||||
title.value = d.title
|
||||
description.value = d.description
|
||||
questions.value = d.questions.map((q) => ({
|
||||
id: q.id,
|
||||
body: typeof q.body === 'string' ? JSON.parse(q.body) : q.body,
|
||||
}))
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
}
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
for (const q of questions.value) {
|
||||
if (!answers[q.id]) throw new Error('请完成全部题目')
|
||||
}
|
||||
const { items } = await api.listProfiles()
|
||||
let self = (items || []).find((p) => p.relation === 'self')
|
||||
if (!self) {
|
||||
self = await api.createProfile({ relation: 'self', birth_date: '1990-01-01', display_name: '我' })
|
||||
}
|
||||
const out = await api.submitScale(slug, self.id, { ...answers })
|
||||
done.value = true
|
||||
resultLabel.value = String(out.result.label || '探索结果')
|
||||
resultSummary.value = String(out.result.summary || '')
|
||||
shareLine.value = String(out.result.share_line || '')
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '提交失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:16px}
|
||||
.back{border:none;background:#fff;border-radius:10px;padding:8px 12px;margin-bottom:12px}
|
||||
h1{font-size:22px}
|
||||
h2{font-size:18px;margin-bottom:8px}
|
||||
.sub{color:var(--yxg-sub);font-size:13px;margin:8px 0 14px}
|
||||
.err{color:var(--yxg-pri);font-size:13px}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;line-height:1.7;color:#555}
|
||||
.q{margin-bottom:18px}
|
||||
.prompt{font-weight:600;color:#333;margin-bottom:8px}
|
||||
.opt{display:block;margin:6px 0;cursor:pointer}
|
||||
button{
|
||||
margin-top:8px;padding:10px 16px;border:none;border-radius:22px;color:#fff;font-weight:600;
|
||||
background:linear-gradient(135deg,#ff7a6e,var(--yxg-pri));
|
||||
}
|
||||
.share{margin-top:12px;color:var(--yxg-gold,#c8923a)}
|
||||
.link{display:inline-block;margin-top:14px;color:var(--yxg-pri)}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ const router = createRouter({
|
||||
{ path: '/portrait', name: 'portrait', component: () => import('../pages/PortraitPage.vue'), meta: { tab: false } },
|
||||
{ path: '/relation', name: 'relation', component: () => import('../pages/RelationPage.vue'), meta: { tab: false } },
|
||||
{ path: '/membership', name: 'membership', component: () => import('../pages/MembershipPage.vue'), meta: { tab: false } },
|
||||
{ path: '/scales/:slug', name: 'scale', component: () => import('../pages/ScalePage.vue'), meta: { tab: false } },
|
||||
{ path: '/decode', redirect: (to) => ({ path: '/portrait', query: to.query }) },
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user