feat: 支持教学案例展示功能

This commit is contained in:
richarjiang
2026-09-18 18:26:47 +08:00
parent 95b9f6511c
commit 87afb786b9
28 changed files with 950 additions and 5 deletions

View File

@@ -0,0 +1,18 @@
<template>
<view class="case-card">
<view class="heading" @tap="open"><view class="copy"><text class="name">{{ item.displayName }} · 练习记录</text><text class="title">{{ item.title }}</text></view><view class="lessons"><text class="number">{{ item.lessonCount }}</text><text>节练习</text></view></view>
<TeachingCaseGallery :frames="item.frames" :lesson-count="item.lessonCount" @refresh="$emit('refresh')" />
<text class="achievement">{{ item.achievement }}</text>
<button class="link" @tap="open">看这段改变的过程 <text>↗</text></button>
</view>
</template>
<script setup lang="ts">
import type { TeachingCasePublic } from '@mp-pilates/shared'
import TeachingCaseGallery from './TeachingCaseGallery.vue'
const props = defineProps<{ item: TeachingCasePublic }>()
defineEmits<{ refresh: [] }>()
function open() { uni.navigateTo({ url: `/pages/home/teaching-case?id=${props.item.id}` }) }
</script>
<style scoped>
.case-card{padding:28rpx 0;border-bottom:1rpx solid #e4ded4;}.heading{display:flex;justify-content:space-between;gap:20rpx;align-items:center;margin-bottom:24rpx;}.copy{min-width:0;}.name{display:block;font-size:22rpx;color:#8b8174;margin-bottom:12rpx;}.title{font-size:34rpx;line-height:1.5;color:#4b4940;font-family:'Songti SC','STSong',serif;}.lessons{flex-shrink:0;text-align:center;color:#64705c;font-size:20rpx;}.lessons text{display:block;}.lessons .number{font-family:'Baskerville',serif;font-size:58rpx;line-height:1.1;}.achievement{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:22rpx;font-size:25rpx;line-height:1.8;color:#746d61;white-space:pre-wrap;}.link{padding:0;margin:10rpx 0 0;text-align:left;font-size:25rpx;line-height:80rpx;color:#596b50;background:transparent;}.link text{float:right;}.link::after{border:0;}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<view class="gallery">
<swiper class="slides" :current="current" :autoplay="false" @change="current = $event.detail.current">
<swiper-item v-for="(frame, index) in frames" :key="index">
<view class="frame" :class="{ pair: frame.kind === 'pair' }">
<view class="photo" @tap="preview(frame.beforeUrl)">
<image v-if="frame.beforeUrl" :src="frame.beforeUrl" mode="aspectFit" lazy-load @error="failed = true" />
<text v-if="frame.kind === 'pair'" class="badge">练习前</text>
</view>
<view v-if="frame.kind === 'pair'" class="photo" @tap="preview(frame.afterUrl)">
<image v-if="frame.afterUrl" :src="frame.afterUrl" mode="aspectFit" lazy-load @error="failed = true" />
<text class="badge after">{{ lessonCount }} 节后</text>
</view>
</view>
</swiper-item>
</swiper>
<view class="gallery-footer">
<text class="caption">{{ frames[current]?.caption || '每一次练习,都留下改变的痕迹' }}</text>
<view class="pager"><button v-if="frames.length > 1" aria-label="上一组" @tap="move(-1)">‹</button><text>{{ current + 1 }} / {{ frames.length }}</text><button v-if="frames.length > 1" aria-label="下一组" @tap="move(1)">›</button></view>
</view>
<button v-if="failed" class="retry" @tap="$emit('refresh')">图片未能加载,点击刷新</button>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { TeachingCaseFrame } from '@mp-pilates/shared'
const props = defineProps<{ frames: TeachingCaseFrame[]; lessonCount: number }>()
defineEmits<{ refresh: [] }>()
const current = ref(0), failed = ref(false)
watch(() => props.frames, () => { current.value = 0; failed.value = false })
function move(delta: number) { current.value = (current.value + delta + props.frames.length) % props.frames.length }
function preview(url?: string) {
if (!url) return
const urls = props.frames.flatMap(frame => [frame.beforeUrl, frame.afterUrl].filter((item): item is string => Boolean(item)))
uni.previewImage({ current: url, urls })
}
</script>
<style scoped>
.gallery{overflow:hidden;background:#f0eee8;border-radius:18rpx;}.slides{height:480rpx;}.frame{height:100%;display:flex;}.photo{position:relative;flex:1;min-width:0;height:100%;}.pair .photo+.photo{border-left:4rpx solid #fbf9f6;}.photo image{width:100%;height:100%;}.badge{position:absolute;left:14rpx;bottom:16rpx;padding:8rpx 16rpx;border-radius:8rpx;background:rgba(255,253,249,.93);color:#62584d;font-size:22rpx;}.after{background:#53624f;color:#fff;}.gallery-footer{padding:12rpx 18rpx;display:flex;align-items:center;gap:12rpx;min-height:64rpx;}.caption{flex:1;font-size:22rpx;color:#817566;line-height:1.6;}.pager{display:flex;align-items:center;flex-shrink:0;color:#716353;font-size:22rpx;}.pager button{margin:0;padding:0;width:64rpx;line-height:68rpx;background:transparent;color:#53624f;font-size:36rpx;}.pager button::after,.retry::after{border:0;}.retry{background:#eee5da;color:#765c44;font-size:24rpx;line-height:80rpx;}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<view v-if="items.length" class="section">
<view class="section-head"><view><text class="eyebrow">练习,有迹可循</text><text class="title">看见真实的改变</text></view><button @tap="openAll">全部案例 ›</button></view>
<TeachingCaseCard v-for="item in items" :key="item.id" :item="item" @refresh="$emit('refresh')" />
<text class="footnote">经学员授权分享 · 个体经历,实际变化因人而异</text>
</view>
</template>
<script setup lang="ts">
import type { TeachingCasePublic } from '@mp-pilates/shared'
import TeachingCaseCard from './TeachingCaseCard.vue'
defineProps<{ items: TeachingCasePublic[] }>()
defineEmits<{ refresh: [] }>()
function openAll() { uni.navigateTo({ url: '/pages/home/teaching-cases' }) }
</script>
<style scoped>
.section{margin:28rpx 32rpx 40rpx;}.section-head{display:flex;justify-content:space-between;align-items:center;gap:12rpx;}.eyebrow{display:block;font-size:21rpx;letter-spacing:3rpx;color:#8a876e;margin-bottom:13rpx;}.title{font-family:'Songti SC','STSong',serif;font-size:40rpx;line-height:1.5;color:#494c40;}.section-head button{flex-shrink:0;margin:0;padding:0 10rpx;font-size:23rpx;color:#738064;line-height:88rpx;background:transparent;}.section-head button::after{border:0;}.footnote{display:block;margin-top:24rpx;font-size:21rpx;color:#948b7c;line-height:1.7;}
</style>