feat: 支持新版会员档案
This commit is contained in:
37
packages/app/src/components/BodyAssessmentRecords.vue
Normal file
37
packages/app/src/components/BodyAssessmentRecords.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="assessments">
|
||||
<view class="section-top"><view><text class="eyebrow">POSTURE JOURNAL</text><text class="title">体态与身体评估</text></view><button v-if="admin" class="add" @tap="open()">+ 新建记录</button></view>
|
||||
<text class="intro">从每一个细节,慢慢了解身体。<text>铅垂线 · 侧视 · 正面 · 后视 · 脊柱</text></text>
|
||||
<view v-if="!records.length" class="empty"><view class="empty-symbol">+</view><text class="empty-title">第一份身体观察,从这里开始</text><text>可只记录体态检查,身高、体重等均为选填。</text><button v-if="admin" class="add empty-add" @tap="open()">开始评估</button></view>
|
||||
<view v-for="record in records" :key="record.id" class="record">
|
||||
<view class="record-head"><view class="date"><text class="day">{{ record.recordedAt.slice(8, 10) }}</text><view><text class="month">{{ record.recordedAt.slice(0, 7).replace('-', ' / ') }}</text><text class="record-label">{{ record.assessment ? '体态分析指南检查表' : '身体刻度记录' }}</text></view></view><text class="arrow" @tap="open(record)">查看 ›</text></view>
|
||||
<view v-if="record.assessment" class="scope"><text>{{ postureAnswerCount(record.assessment) }} 项检查已记录</text><text>{{ completedViews(record) }} / 5 个检查分区</text></view>
|
||||
<view v-else class="scope"><text>历史体测记录</text><text>原始数据已保留</text></view>
|
||||
<view v-if="measurements(record).length" class="measurements"><text v-for="item in measurements(record)" :key="item.key">{{ item.label }} <text class="value">{{ record[item.key] }}</text> {{ item.unit }}</text></view>
|
||||
<text v-if="record.remark" class="remark">{{ record.remark }}</text>
|
||||
<view class="record-actions"><button class="text-button" @tap="open(record)">查看完整检查表</button><view v-if="admin" class="admin-actions"><button class="text-button edit" @tap="open(record, true)">编辑</button><button class="text-button delete" @tap="$emit('remove', record.id)">删除</button></view></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BODY_MEASUREMENT_FIELDS, POSTURE_SECTIONS, postureAnswerCount } from '@mp-pilates/shared'
|
||||
import type { BodyMetricRecord } from '@mp-pilates/shared'
|
||||
const props = defineProps<{ records: BodyMetricRecord[]; admin?: boolean; userId?: string }>()
|
||||
defineEmits<{ remove: [id: string] }>()
|
||||
function measurements(record: BodyMetricRecord) { return BODY_MEASUREMENT_FIELDS.filter(field => record[field.key] != null) }
|
||||
function completedViews(record: BodyMetricRecord) {
|
||||
const assessment = record.assessment
|
||||
if (!assessment) return 0
|
||||
return Number(Object.values(assessment.plumb).some(Boolean)) + Number(!!assessment.spine.flat || !!assessment.spine.imbalance)
|
||||
+ POSTURE_SECTIONS.filter(section => section.groups.some(group => group.options.some(option => assessment.answers[option.id]?.length))).length
|
||||
}
|
||||
function open(record?: BodyMetricRecord, edit = false) {
|
||||
const query = [props.admin ? `userId=${encodeURIComponent(props.userId || '')}` : '', record ? `id=${encodeURIComponent(record.id)}` : '', edit ? 'edit=1' : ''].filter(Boolean).join('&')
|
||||
uni.navigateTo({ url: '/pages/profile/body-assessment?' + query })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.section-top{display:flex;justify-content:space-between;align-items:center;gap:16rpx;}.eyebrow{display:block;color:#8ba5b3;font-size:18rpx;letter-spacing:4rpx;}.title{display:block;margin-top:12rpx;font-family:'Songti SC','STSong',serif;font-size:36rpx;color:#4a4035;}.add{margin:0;padding:0 22rpx;line-height:76rpx;background:#e8f0f4;color:#587b90;font-size:24rpx;border-radius:18rpx;flex-shrink:0;}.add::after,.text-button::after{border:0;}.intro{display:block;font-size:24rpx;color:#8e8274;line-height:1.9;margin:22rpx 0 30rpx;}.intro>text{display:block;font-size:21rpx;letter-spacing:1rpx;color:#a29485;}.record{padding:28rpx;margin-bottom:24rpx;background:#fffefa;border:1rpx solid #e8e1d7;border-radius:26rpx;animation:unfold .3s ease both;}.record-head{display:flex;align-items:center;justify-content:space-between;}.date{display:flex;align-items:center;gap:20rpx;}.day{font-family:'Baskerville',serif;font-size:64rpx;color:#4a4035;line-height:1;}.month{display:block;font-size:21rpx;color:#9c8e7e;letter-spacing:2rpx;}.record-label{display:block;margin-top:9rpx;font-size:25rpx;color:#5c5145;}.arrow{font-size:24rpx;color:#7ba5be;padding:20rpx 0 20rpx 24rpx;}.scope{display:flex;justify-content:space-between;gap:12rpx;margin:26rpx 0 0;padding:18rpx 20rpx;background:#f1f5f6;border-radius:14rpx;color:#7390a0;font-size:21rpx;}.measurements{display:flex;flex-wrap:wrap;gap:16rpx 22rpx;margin-top:24rpx;font-size:22rpx;color:#9b8d7d;}.value{color:#5d5246;margin-left:8rpx;font-size:26rpx;}.remark{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:pre-wrap;margin-top:20rpx;font-size:25rpx;line-height:1.8;color:#8a7c6c;}.record-actions{display:flex;justify-content:space-between;align-items:center;border-top:1rpx solid #eee8df;margin-top:24rpx;padding-top:10rpx;}.admin-actions{display:flex;gap:20rpx;}.text-button{background:transparent;padding:0;margin:0;color:#8e8274;font-size:23rpx;line-height:72rpx;}.edit{color:#587b90;}.delete{color:#b09184;}.empty{padding:48rpx 28rpx;text-align:center;border:1rpx dashed #d9d6cc;border-radius:26rpx;color:#a09080;font-size:23rpx;line-height:1.8;}.empty-symbol{font-size:54rpx;font-weight:200;color:#a9bfcc;margin-bottom:12rpx;}.empty-title{display:block;font-size:28rpx;color:#7a6a5a;margin-bottom:12rpx;}.empty-add{display:inline-block;margin-top:28rpx;min-width:200rpx;}@keyframes unfold{from{opacity:0;transform:translateY(10rpx);}to{opacity:1;transform:translateY(0);}}@media(prefers-reduced-motion:reduce){.record{animation:none;}}
|
||||
</style>
|
||||
@@ -23,12 +23,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
/** Transparent bg with white text — for pages with colored header */
|
||||
transparent?: boolean
|
||||
/** Show back arrow (for sub-pages navigated via navigateTo) */
|
||||
showBack?: boolean
|
||||
beforeBack?: () => boolean | Promise<boolean>
|
||||
}>()
|
||||
|
||||
const statusBarHeight = ref(0)
|
||||
@@ -38,7 +39,8 @@ onMounted(() => {
|
||||
statusBarHeight.value = windowInfo.statusBarHeight ?? 20
|
||||
})
|
||||
|
||||
function handleBack() {
|
||||
async function handleBack() {
|
||||
if (props.beforeBack && !await props.beforeBack()) return
|
||||
const pages = getCurrentPages()
|
||||
|
||||
if (pages.length > 1) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
33
packages/app/src/components/PostureChecklist.vue
Normal file
33
packages/app/src/components/PostureChecklist.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<view class="checklist">
|
||||
<view v-for="(group, index) in section.groups" :key="group.id" class="anatomy-card" :style="{ animationDelay: index * 25 + 'ms' }">
|
||||
<view class="group-heading"><view class="group-title"><text class="number">{{ String(index + 1).padStart(2, '0') }}</text><text>{{ group.title }}</text></view><text class="status">{{ groupCount(group) ? groupCount(group) + ' 项已记录' : '未检查' }}</text></view>
|
||||
<view v-for="option in group.options" :key="option.id" class="finding" :class="{ 'finding--selected': answers[option.id]?.length }">
|
||||
<text class="finding-label">{{ option.label }}</text>
|
||||
<view class="marks">
|
||||
<button v-for="mark in option.bilateral ? sideMarks : wholeMarks" :key="mark.key" class="mark" :class="{ selected: answers[option.id]?.includes(mark.key) }" :disabled="readonly" :aria-label="group.title + ',' + option.label + ',' + mark.label + (answers[option.id]?.includes(mark.key) ? ',已选' : ',未选')" @tap="toggle(group, option, mark.key)"><text v-if="answers[option.id]?.includes(mark.key)" class="tick">✓</text>{{ mark.label }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { togglePostureMark } from '@mp-pilates/shared'
|
||||
import type { PostureAssessment, PostureGroup, PostureMark, PostureOption, PostureSection } from '@mp-pilates/shared'
|
||||
const props = defineProps<{ section: PostureSection; answers: PostureAssessment['answers']; readonly?: boolean }>()
|
||||
const emit = defineEmits<{ change: [answers: PostureAssessment['answers']] }>()
|
||||
const sideMarks: { key: PostureMark; label: string }[] = [{ key: 'right', label: '右' }, { key: 'left', label: '左' }]
|
||||
const wholeMarks: { key: PostureMark; label: string }[] = [{ key: 'checked', label: '勾选' }]
|
||||
function groupCount(group: PostureGroup) { return group.options.reduce((sum, option) => sum + (props.answers[option.id]?.length || 0), 0) }
|
||||
function toggle(group: PostureGroup, option: PostureOption, mark: PostureMark) {
|
||||
if (!props.readonly) emit('change', togglePostureMark(props.answers, group, option, mark))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.anatomy-card{margin-bottom:24rpx;padding:8rpx 26rpx 18rpx;border:1rpx solid #e9e3da;border-radius:24rpx;background:#fffefa;animation:arrive .3s ease both;}
|
||||
.group-heading{display:flex;align-items:center;justify-content:space-between;gap:14rpx;padding:24rpx 0;border-bottom:1rpx solid #eee9e2;margin-bottom:8rpx;}.group-title{display:flex;align-items:center;gap:16rpx;font-size:29rpx;color:#4a4035;font-weight:500;}.number{font-family:'Baskerville',serif;font-size:28rpx;color:#8ba5b3;}.status{font-size:20rpx;color:#a09080;}
|
||||
.finding{display:flex;align-items:center;justify-content:space-between;gap:16rpx;min-height:94rpx;padding:6rpx 0;box-sizing:border-box;}.finding-label{flex:1;min-width:0;font-size:25rpx;line-height:1.65;color:#766b5e;}.finding--selected .finding-label{color:#4a4035;}.marks{display:flex;gap:12rpx;flex-shrink:0;}.mark{display:flex;align-items:center;justify-content:center;gap:4rpx;min-width:78rpx;min-height:72rpx;margin:0;padding:0 16rpx;border:1rpx solid #e2e5e3;border-radius:14rpx;background:#f8f8f4;font-size:24rpx;line-height:1.3;color:#9a958c;transition:background .18s,border-color .18s,color .18s,transform .18s;}.mark::after{border:0;}.mark.selected{background:#eaf2f6;border-color:#a9bfcc;color:#51778e;}.mark:active{transform:scale(.95);}.mark[disabled]{opacity:1;}.tick{font-size:20rpx;}@keyframes arrive{from{opacity:0;transform:translateY(12rpx);}to{opacity:1;transform:translateY(0);}}
|
||||
@media(prefers-reduced-motion:reduce){.anatomy-card{animation:none;}.mark{transition:none;}}
|
||||
</style>
|
||||
Reference in New Issue
Block a user