feat: 新增个人身体画像评估与馆主经营助手

把 3 分钟身体状态评估做成独立获客链路,匿名测评后登录认领完整报告,并接入体验预约与今日待办。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-11 22:09:19 +08:00
parent b8c0dd6781
commit ab6602e41d
56 changed files with 4640 additions and 25 deletions

View File

@@ -0,0 +1,76 @@
<template>
<view class="page" :style="{ paddingTop: navBarHeight }">
<CustomNavBar title="你的身体画像" show-back />
<view v-if="teaser" class="card">
<text class="kicker">你的身体画像</text>
<text class="title">{{ teaser.headline }}</text>
<text class="summary">{{ claimed && report ? report.summary : '登录后查看完整个性化报告。问卷画像只能反映生活习惯和主观感受。' }}</text>
<text class="match">问卷依据{{ teaser.matchQuality === 'full' ? '充分' : '一般' }}这表示回答是否足够形成解释不是诊断准确率</text>
</view>
<SafetyNotice v-if="report?.safety.flagged" :message="report.safety.message || ''" />
<PortraitRadar v-if="claimed && report" :scores="report.scores" />
<view v-if="claimed && report" class="block">
<view v-for="(item, index) in report.evidence" :key="item.title" class="evidence">
<text class="etitle">{{ index + 1 }} {{ item.title }}</text>
<text class="eans">根据你的答案{{ item.answers.join('、') }}</text>
<text class="ebody">{{ item.explanation }}</text>
</view>
</view>
<view v-if="claimed && report" class="block">
<text class="etitle">这些问题是有关联的</text>
<text v-for="stepItem in report.chain.steps" :key="stepItem" class="chain">{{ stepItem }}</text>
<text class="ebody">{{ report.chain.takeaway }}</text>
</view>
<text class="disclaimer">非医疗诊断仅用于运动训练参考</text>
<view class="cta" @tap="continueFlow">{{ claimed ? '查看建议' : '登录查看完整报告' }}</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import CustomNavBar from '../../components/CustomNavBar.vue'
import PortraitRadar from '../../components/PortraitRadar.vue'
import SafetyNotice from '../../components/SafetyNotice.vue'
import { getSystemLayout } from '../../utils/system'
import { getErrorMessage } from '../../utils/auth'
import { useBodyPortraitStore } from '../../stores/body-portrait'
import { useUserStore } from '../../stores/user'
const navBarHeight = ref(`${getSystemLayout().navBarHeight}px`)
const portrait = useBodyPortraitStore()
const userStore = useUserStore()
const teaser = portrait.teaser
const report = portrait.report
const claimed = portrait.claimed
onShow(async () => {
if (!portrait.teaser && !portrait.session) {
await portrait.loadMine().catch(() => {})
}
if (portrait.teaser) await portrait.track('report_viewed')
})
async function continueFlow() {
try {
if (!portrait.claimed) {
if (!userStore.loggedIn) await userStore.login()
await portrait.claim()
}
uni.navigateTo({ url: '/pages/portrait/advice' })
} catch (err) {
uni.showToast({ title: getErrorMessage(err, '请先登录后查看完整报告'), icon: 'none' })
}
}
</script>
<style lang="scss" scoped>
.page { min-height: 100vh; background: #fbf9f6; padding: 0 40rpx 120rpx; }
.card, .block, .evidence { background: #fff; border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; }
.kicker { color: #8a7b6e; font-size: 22rpx; }
.title { display: block; margin-top: 12rpx; font-size: 40rpx; color: #4a4035; line-height: 1.4; }
.summary, .match, .ebody, .eans, .chain { display: block; margin-top: 16rpx; color: #7a6a5a; font-size: 26rpx; line-height: 1.7; }
.etitle { display: block; font-size: 30rpx; color: #4a4035; }
.disclaimer { display: block; color: #a09080; font-size: 22rpx; margin: 12rpx 0 24rpx; }
.cta { height: 92rpx; border-radius: 999rpx; background: #6b8276; color: #fff; display: flex; align-items: center; justify-content: center; }
</style>