Files
zyt/TUICallKit-Vue3/components/dev-training-entry/index.vue
T
long 4932ed7651 refactor: 移除 AI 跟练中没有作用的代码
- 删除从未显示的 idle 手势帧(小程序包内 png 和视频用高清帧),训练分包减少 17KB
- 删除骨架连线 BONES、镜像函数 mirrorPose(骨架已去掉)和未使用的 Bgm 类型
- 只给单测用的 sampleMotion 移到测试目录
- 删除训练菜单里未使用的 .menu-icon-text 样式
- 补全 history 单测数据缺少的 adviceKind 字段(严格类型检查报错)
2026-10-31 10:33:25 +08:00

923 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 页面内嵌模块(如 weekly 统计页主内容区) -->
<view
v-if="visible && props.inline"
class="dev-entry-inline"
:class="{ 'dev-entry-inline--weekly': props.tone === 'weekly' }"
>
<view class="inline-hero">
<view class="inline-hero-icon" aria-hidden="true">
<view class="inline-hero-wave" />
</view>
<view class="inline-hero-text">
<view class="inline-hero-title-row">
<text class="inline-title">居家耗糖训练</text>
<text v-if="props.tone === 'weekly'" class="inline-pill">体验</text>
</view>
<text class="inline-sub">低强度动起来,帮助身体消耗糖分</text>
</view>
</view>
<!-- 训练轮播:当前卡片居中放大,左右露出相邻卡片;自动慢速轮播,手指滑动时暂停 -->
<swiper
class="inline-swiper"
circular
autoplay
:interval="4500"
:duration="480"
easing-function="easeOutCubic"
previous-margin="84rpx"
next-margin="84rpx"
:current="current"
@change="onSwiperChange"
>
<swiper-item v-for="(item, i) in inlineItems" :key="item.url" class="inline-swiper-item">
<view class="slide" :class="['slide--' + item.icon, { 'is-active': i === current }]" @click="onSlideTap(i, item.url)">
<view class="slide-deco" />
<view class="slide-body">
<view class="slide-head">
<text class="slide-title">{{ item.title }}</text>
<text v-if="item.isNew" class="slide-badge">新功能</text>
<text v-else-if="item.featured" class="slide-badge">推荐</text>
</view>
<text class="slide-desc">{{ item.desc }}</text>
<view class="slide-cta">
<text>去训练</text>
<text class="slide-cta-arrow">›</text>
</view>
</view>
<view class="slide-art">
<image
v-if="item.art && !artFailed"
class="slide-coach"
:src="item.art"
mode="heightFix"
:webp="true"
@error="artFailed = true"
/>
<view v-else class="slide-icon menu-icon" :class="item.iconClass">
<view class="slide-glyph">
<view v-if="item.icon === 'dumbbell'" class="dumbbell-shape">
<view class="dumbbell-plate dumbbell-plate-left" />
<view class="dumbbell-bar" />
<view class="dumbbell-plate dumbbell-plate-right" />
</view>
<view v-else-if="item.icon === 'pedal'" class="foot-pedal-shape">
<view class="pedal-bar pedal-bar-left" />
<view class="pedal-bar pedal-bar-right" />
</view>
<view v-else-if="item.icon === 'pilates'" class="pilates-ring-shape" />
<view v-else-if="item.icon === 'grip'" class="grip-ring-shape" />
<text v-else-if="item.icon === 'ai'" class="ai-mark">AI</text>
<view v-else-if="item.icon === 'metro'" class="menu-icon--metro-inner">
<view class="metro-bar metro-bar-1" />
<view class="metro-bar metro-bar-2" />
<view class="metro-bar metro-bar-3" />
</view>
</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<view class="inline-dots">
<view
v-for="(item, i) in inlineItems"
:key="'dot-' + item.url"
class="inline-dot"
:class="{ 'is-on': i === current }"
@click="current = i"
/>
</view>
</view>
<view v-else-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
<!-- 展开后的菜单卡片(从下往上动画) -->
<view v-if="expanded" class="menu-list" @click.stop>
<view class="menu-item" @click="goto('/training/pages/pose-hub')">
<view class="menu-icon menu-icon--ai">
<text class="ai-mark">AI</text>
</view>
<view class="menu-info">
<text class="menu-title">AI 跟练</text>
<text class="menu-sub">识别动作 · 自动计数</text>
</view>
<view class="menu-arrow">›</view>
</view>
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
<view class="menu-icon menu-icon--dumbbell">
<view class="dumbbell-shape">
<view class="dumbbell-plate dumbbell-plate-left" />
<view class="dumbbell-bar" />
<view class="dumbbell-plate dumbbell-plate-right" />
</view>
</view>
<view class="menu-info">
<text class="menu-title">哑铃</text>
<text class="menu-sub">力量塑形</text>
</view>
<view class="menu-arrow">›</view>
</view>
<view class="menu-item" @click="goto('/training/pages/foot-pedal')">
<view class="menu-icon menu-icon--pedal">
<view class="foot-pedal-shape">
<view class="pedal-bar pedal-bar-left" />
<view class="pedal-bar pedal-bar-right" />
</view>
</view>
<view class="menu-info">
<text class="menu-title">脚蹬器</text>
<text class="menu-sub">下肢有氧</text>
</view>
<view class="menu-arrow">›</view>
</view>
<view class="menu-item" @click="goto('/training/pages/pilates-ring')">
<view class="menu-icon menu-icon--pilates">
<view class="pilates-ring-shape" />
</view>
<view class="menu-info">
<text class="menu-title">瑜伽环</text>
<text class="menu-sub">核心塑形</text>
</view>
<view class="menu-arrow">›</view>
</view>
<view class="menu-item" @click="goto('/training/pages/grip-ring')">
<view class="menu-icon menu-icon--grip">
<view class="grip-ring-shape" />
</view>
<view class="menu-info">
<text class="menu-title">握力环</text>
<text class="menu-sub">握力训练</text>
</view>
<view class="menu-arrow">›</view>
</view>
<view class="menu-item" @click="goto('/training/pages/metronome')">
<view class="menu-icon menu-icon--metro">
<view class="metro-bar metro-bar-1" />
<view class="metro-bar metro-bar-2" />
<view class="metro-bar metro-bar-3" />
</view>
<view class="menu-info">
<text class="menu-title">耗糖节拍器</text>
<text class="menu-sub">健走配速</text>
</view>
<view class="menu-arrow">›</view>
</view>
</view>
<!-- 主浮动按钮(单字"练" + 呼吸光环) -->
<view class="fab-container">
<view v-if="!expanded" class="fab-pulse" />
<view v-if="!expanded" class="fab-pulse fab-pulse-2" />
<view class="entry-fab" :class="{ expanded }" @click="toggle">
<view v-if="expanded" class="icon-close">
<view class="cross-bar bar-1" />
<view class="cross-bar bar-2" />
</view>
<!-- 心跳波形 icon: 健康主题,SVG 折线 -->
<view v-else class="fab-icon" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
type TrainingItem = {
url: string
title: string
/** 轮播卡片上的一行介绍(10 字左右) */
desc: string
icon: 'dumbbell' | 'pedal' | 'pilates' | 'grip' | 'metro' | 'ai'
iconClass: string
/** 卡片右侧的插画(网络图),没有或加载失败时显示大图标 */
art?: string
featured?: boolean
isNew?: boolean
}
/** AI 跟练卡片的教练插画:training-videos 导出、上传在 COS 的教练高清帧(开合跳双手举过头) */
const COACH_ART = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/yundong/coach/jl_09.webp'
const trainingItems: TrainingItem[] = [
{ url: '/training/pages/pose-hub', title: 'AI 跟练', desc: 'AI 识别动作,自动计数', icon: 'ai', iconClass: 'menu-icon--ai', art: COACH_ART, isNew: true },
{ url: '/training/pages/dumbbell', title: '哑铃', desc: '小重量多组数,练力量', icon: 'dumbbell', iconClass: 'menu-icon--dumbbell' },
{ url: '/training/pages/foot-pedal', title: '脚蹬器', desc: '坐着就能练,低冲击', icon: 'pedal', iconClass: 'menu-icon--pedal' },
{ url: '/training/pages/pilates-ring', title: '瑜伽环', desc: '夹一夹,练核心和腿部', icon: 'pilates', iconClass: 'menu-icon--pilates' },
{ url: '/training/pages/grip-ring', title: '握力环', desc: '随时握一握,练手部', icon: 'grip', iconClass: 'menu-icon--grip' },
{ url: '/training/pages/metronome', title: '耗糖节拍器', desc: '跟着节拍健走,不费劲', icon: 'metro', iconClass: 'menu-icon--metro', featured: true },
]
const props = withDefaults(
defineProps<{
/** 悬浮按钮距底部 rpx(仅非 inline) */
bottom?: number
/** 内嵌到页面主内容区,非 fixed 悬浮 */
inline?: boolean
/** weekly 页:与统计页 Vital Mint 视觉一致 */
tone?: 'default' | 'weekly'
}>(),
{ bottom: 200, inline: false, tone: 'default' },
)
/** weekly 将健走节拍器置前并标推荐 */
const inlineItems = computed(() => {
if (props.tone !== 'weekly') return trainingItems
const metro = trainingItems.find((i) => i.icon === 'metro')
const rest = trainingItems.filter((i) => i.icon !== 'metro')
return metro ? [metro, ...rest] : trainingItems
})
// 居家耗糖训练是正式功能,开发版和发行版都需要展示。
const visible = ref(true)
const expanded = ref(false)
/** 轮播当前居中的卡片 */
const current = ref(0)
const artFailed = ref(false)
const onSwiperChange = (e: { detail: { current: number } }) => {
current.value = e.detail.current
}
/** 点居中的卡片进入训练;点两侧露出的卡片先把它转到中间 */
const onSlideTap = (index: number, url: string) => {
if (index !== current.value) {
current.value = index
return
}
goto(url)
}
const toggle = () => {
expanded.value = !expanded.value
}
const goto = (url: string) => {
expanded.value = false
uni.navigateTo({ url })
}
</script>
<style lang="scss" scoped>
$brand: #10b981;
$brand-deep: #047857;
$brand-light: #34d399;
.dev-entry-wrap {
position: fixed;
right: 24rpx;
z-index: 999;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 18rpx;
}
/* ============================================================
* 主浮动按钮
* ============================================================ */
.fab-container {
position: relative;
width: 108rpx;
height: 108rpx;
}
.entry-fab {
position: absolute;
inset: 0;
border-radius: 50%;
background: linear-gradient(140deg, $brand-light 0%, $brand-deep 100%);
box-shadow:
0 8rpx 20rpx rgba(16, 185, 129, 0.42),
0 2rpx 6rpx rgba(15, 23, 42, 0.12),
inset 0 -8rpx 16rpx rgba(0, 0, 0, 0.12),
inset 0 8rpx 16rpx rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s;
&:active {
transform: scale(0.92);
}
&.expanded {
background: linear-gradient(140deg, #94a3b8 0%, #475569 100%);
transform: rotate(135deg);
box-shadow:
0 6rpx 16rpx rgba(15, 23, 42, 0.2),
inset 0 -6rpx 14rpx rgba(0, 0, 0, 0.12),
inset 0 6rpx 14rpx rgba(255, 255, 255, 0.12);
}
.fab-icon {
width: 60rpx;
height: 60rpx;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.22));
animation: fab-icon-beat 1.6s ease-in-out infinite;
}
}
@keyframes fab-icon-beat {
0%, 60%, 100% {
transform: scale(1);
}
20% {
transform: scale(1.12);
}
40% {
transform: scale(1);
}
}
/* 关闭态 X 图标 (CSS 几何) */
.icon-close {
position: relative;
width: 36rpx;
height: 36rpx;
.cross-bar {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 5rpx;
background: #fff;
border-radius: 3rpx;
transform-origin: center;
}
.bar-1 {
transform: translateY(-50%) rotate(45deg);
}
.bar-2 {
transform: translateY(-50%) rotate(-45deg);
}
}
/* 呼吸光环(2 圈错相位扩散) */
.fab-pulse {
position: absolute;
inset: 0;
border-radius: 50%;
background: rgba(16, 185, 129, 0.25);
animation: fab-ring 2.4s ease-out infinite;
pointer-events: none;
}
.fab-pulse-2 {
animation-delay: 1.2s;
}
@keyframes fab-ring {
0% {
transform: scale(0.95);
opacity: 0.7;
}
100% {
transform: scale(1.55);
opacity: 0;
}
}
/* ============================================================
* 展开菜单
* ============================================================ */
.menu-list {
display: flex;
flex-direction: column;
gap: 14rpx;
animation: slide-up 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20rpx) scale(0.92);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.menu-item {
display: flex;
align-items: center;
gap: 16rpx;
background: rgba(255, 255, 255, 0.96);
padding: 14rpx 20rpx 14rpx 14rpx;
border-radius: 999rpx;
box-shadow:
0 8rpx 24rpx rgba(15, 23, 42, 0.1),
0 1rpx 2rpx rgba(15, 23, 42, 0.06);
min-width: 280rpx;
transition: transform 0.15s;
backdrop-filter: blur(12rpx);
&:active {
transform: scale(0.97);
background: #f0fdf4;
}
}
.menu-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2rpx 8rpx rgba(16, 185, 129, 0.32);
&--train {
background: linear-gradient(140deg, $brand-light, $brand-deep);
}
&--dumbbell {
background: linear-gradient(140deg, #93c5fd, #3b82f6);
box-shadow: 0 2rpx 8rpx rgba(59, 130, 246, 0.36);
}
&--pedal {
background: linear-gradient(140deg, #fdba74, #f97316);
box-shadow: 0 2rpx 8rpx rgba(249, 115, 22, 0.36);
}
&--pilates {
background: linear-gradient(140deg, #c4b5fd, #8b5cf6);
box-shadow: 0 2rpx 8rpx rgba(139, 92, 246, 0.36);
}
&--grip {
background: linear-gradient(140deg, #5eead4, #14b8a6);
box-shadow: 0 2rpx 8rpx rgba(20, 184, 166, 0.36);
}
&--metro {
background: linear-gradient(140deg, #fbbf24, #d97706);
box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.36);
gap: 4rpx;
}
&--ai {
background: linear-gradient(140deg, #34d399, #006c49);
box-shadow: 0 2rpx 8rpx rgba(0, 108, 73, 0.36);
}
}
/* AI 跟练小 icon */
.ai-mark {
color: #fff;
font-size: 24rpx;
font-weight: 800;
letter-spacing: 1rpx;
line-height: 1;
}
/* 哑铃小 icon */
.dumbbell-shape {
width: 40rpx;
height: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dumbbell-plate {
width: 10rpx;
height: 18rpx;
background: #fff;
border-radius: 3rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
.dumbbell-bar {
width: 16rpx;
height: 5rpx;
background: #fff;
border-radius: 2rpx;
margin: 0 2rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
/* 脚蹬器小 icon: 左右踏板 */
.foot-pedal-shape {
width: 40rpx;
height: 28rpx;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 6rpx;
}
.pedal-bar {
width: 14rpx;
background: #fff;
border-radius: 4rpx 4rpx 2rpx 2rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
.pedal-bar-left {
height: 18rpx;
transform: rotate(-12deg);
}
.pedal-bar-right {
height: 22rpx;
transform: rotate(12deg);
}
/* 瑜伽环小 icon: 双弧阻力环 */
.pilates-ring-shape {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 5rpx solid #fff;
position: relative;
box-shadow:
0 1rpx 2rpx rgba(0, 0, 0, 0.18),
inset 0 1rpx 2rpx rgba(255, 255, 255, 0.6);
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18rpx;
height: 18rpx;
transform: translate(-50%, -50%);
border-radius: 50%;
border: 3rpx solid rgba(255, 255, 255, 0.85);
}
}
/* 握力环小 icon: 白色描边的环(甜甜圈) */
.grip-ring-shape {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 7rpx solid #fff;
box-shadow:
0 1rpx 2rpx rgba(0, 0, 0, 0.18),
inset 0 1rpx 2rpx rgba(255, 255, 255, 0.6);
}
/* 节拍器小 icon: 3 根高低柱模拟均衡器/节拍 */
.metro-bar {
width: 4rpx;
background: #fff;
border-radius: 2rpx;
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.18);
}
.metro-bar-1 {
height: 16rpx;
}
.metro-bar-2 {
height: 28rpx;
}
.metro-bar-3 {
height: 22rpx;
}
.menu-info {
display: flex;
flex-direction: column;
gap: 2rpx;
flex: 1;
}
.menu-title {
font-size: 28rpx;
color: #0f172a;
font-weight: 700;
letter-spacing: 1rpx;
}
.menu-sub {
font-size: 20rpx;
color: #94a3b8;
letter-spacing: 0.5rpx;
}
.menu-arrow {
font-size: 32rpx;
color: #cbd5e1;
font-weight: 300;
line-height: 1;
margin-left: 4rpx;
}
.menu-icon--metro-inner {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4rpx;
width: 100%;
height: 100%;
}
/* ============================================================
* 内嵌模块(weekly 等)
* ============================================================ */
.dev-entry-inline {
width: 100%;
box-sizing: border-box;
}
.dev-entry-inline--weekly {
padding: 32rpx 28rpx 24rpx;
border-radius: 48rpx;
// 轮播两侧露出的卡片在模块圆角边缘处裁掉
overflow: hidden;
background: var(--surface-container-lowest, #fff);
border: 1rpx solid rgba(0, 108, 73, 0.12);
box-shadow:
0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08),
0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
}
.inline-hero {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 24rpx;
}
.inline-hero-icon {
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: rgba(0, 108, 73, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.inline-hero-wave {
width: 52rpx;
height: 52rpx;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23006c49' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.inline-hero-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.inline-hero-title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
}
.inline-title {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
line-height: 1.25;
}
.inline-pill {
font-size: 20rpx;
font-weight: 700;
color: #006c49;
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: rgba(0, 108, 73, 0.1);
line-height: 1.2;
}
.inline-sub {
font-size: 26rpx;
font-weight: 500;
color: #475569;
line-height: 1.4;
}
/* ---------- 训练轮播 ---------- */
// 每种训练一套配色:背景浅色渐变两端、主色、深色
$slide-themes: (
metro: (#fff7e6, #ffe8bd, #f59e0b, #b45309),
ai: (#e6f7ef, #c9eedc, #10b981, #006c49),
dumbbell: (#eaf2ff, #d3e3ff, #3b82f6, #1d4ed8),
pedal: (#fff1e6, #ffdcc4, #f97316, #c2410c),
pilates: (#f3efff, #e3daff, #8b5cf6, #6d28d9),
grip: (#e6f8f5, #c9efe9, #14b8a6, #0f766e),
);
.inline-swiper {
height: 216rpx;
}
// 轮播铺满整个模块宽度(含内边距),当前卡片更宽,两侧露出相邻卡片
.dev-entry-inline--weekly .inline-swiper {
margin: 0 -28rpx;
}
.inline-swiper-item {
box-sizing: border-box;
padding: 10rpx 8rpx;
}
.slide {
position: relative;
height: 196rpx;
border-radius: 28rpx;
overflow: hidden;
display: flex;
align-items: stretch;
transform: scale(0.92);
opacity: 0.55;
transition:
transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.45s ease,
box-shadow 0.45s ease;
&.is-active {
transform: scale(1);
opacity: 1;
}
&.is-active:active {
transform: scale(0.985);
}
}
// 右上角一团柔和的主色光晕,让卡片有层次
.slide-deco {
position: absolute;
right: -50rpx;
top: -70rpx;
width: 250rpx;
height: 250rpx;
border-radius: 50%;
}
.slide-body {
position: relative;
z-index: 1;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 24rpx 0 20rpx 28rpx;
}
.slide-head {
display: flex;
align-items: center;
gap: 10rpx;
max-width: 100%;
}
.slide-title {
font-size: 32rpx;
font-weight: 800;
color: #0f172a;
line-height: 1.2;
letter-spacing: 1rpx;
white-space: nowrap;
}
.slide-badge {
flex-shrink: 0;
padding: 3rpx 12rpx;
border-radius: 999rpx;
font-size: 19rpx;
font-weight: 700;
color: #fff;
line-height: 1.3;
}
.slide-desc {
margin-top: 8rpx;
max-width: 100%;
font-size: 22rpx;
font-weight: 500;
color: #475569;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.slide-cta {
margin-top: auto;
display: flex;
align-items: center;
gap: 4rpx;
padding: 7rpx 18rpx;
border-radius: 999rpx;
text {
font-size: 22rpx;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
}
.slide-cta-arrow {
font-size: 26rpx !important;
margin-top: -2rpx;
}
.slide-art {
position: relative;
z-index: 1;
flex-shrink: 0;
width: 168rpx;
display: flex;
align-items: center;
justify-content: center;
}
// 教练插画:贴着卡片底边站着,头顶留一点空
.slide-coach {
position: absolute;
right: 10rpx;
bottom: -4rpx;
height: 184rpx;
}
.slide-icon {
width: 108rpx;
height: 108rpx;
margin-right: 22rpx;
}
// 小图标的几何图形是按 64rpx 圆画的,放大后居中
.slide-glyph {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
transform: scale(1.5);
}
@each $name, $c in $slide-themes {
.slide--#{$name} {
background: linear-gradient(135deg, nth($c, 1) 0%, nth($c, 2) 100%);
box-shadow: 0 16rpx 32rpx -18rpx rgba(nth($c, 4), 0.45);
.slide-deco {
background: radial-gradient(circle, rgba(nth($c, 3), 0.28) 0%, rgba(nth($c, 3), 0) 70%);
}
.slide-badge {
background: nth($c, 4);
}
.slide-cta {
background: linear-gradient(135deg, nth($c, 3) 0%, nth($c, 4) 100%);
box-shadow: 0 8rpx 18rpx -6rpx rgba(nth($c, 4), 0.55);
}
.slide-icon {
box-shadow:
0 0 0 12rpx rgba(255, 255, 255, 0.45),
0 14rpx 26rpx -10rpx rgba(nth($c, 4), 0.5);
}
}
}
.inline-dots {
display: flex;
justify-content: center;
align-items: center;
gap: 10rpx;
margin-top: 14rpx;
}
.inline-dot {
width: 12rpx;
height: 12rpx;
border-radius: 999rpx;
background: rgba(0, 108, 73, 0.18);
transition:
width 0.35s ease,
background 0.35s ease;
&.is-on {
width: 36rpx;
background: #006c49;
}
}
@media (prefers-reduced-motion: reduce) {
.slide,
.inline-dot {
transition: none;
}
}
</style>