feat: 支持教学案例展示功能
This commit is contained in:
18
packages/app/src/components/TeachingCaseCard.vue
Normal file
18
packages/app/src/components/TeachingCaseCard.vue
Normal 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>
|
||||
40
packages/app/src/components/TeachingCaseGallery.vue
Normal file
40
packages/app/src/components/TeachingCaseGallery.vue
Normal 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>
|
||||
17
packages/app/src/components/TeachingCaseSection.vue
Normal file
17
packages/app/src/components/TeachingCaseSection.vue
Normal 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>
|
||||
Reference in New Issue
Block a user