feat: init fitness-coach (backend + miniprogram)
- backend: NestJS service with exercise/plan data - miniprogram: WeChat mini program client - exclude node_modules, dist, runtime data-store, local env
This commit is contained in:
67
miniprogram/pages/detail/detail.js
Normal file
67
miniprogram/pages/detail/detail.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
id: '',
|
||||
exercise: null,
|
||||
related: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const id = query.id || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, id });
|
||||
this.load();
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
svc.getExercise(this.data.id).then((ex) => {
|
||||
this.setData({ exercise: ex, loading: false });
|
||||
if (ex && ex.target) {
|
||||
svc.listExercises({ target: ex.target, pageSize: 6 }).then((res) => {
|
||||
const related = ((res && res.items) || [])
|
||||
.filter(i => i.id !== ex.id)
|
||||
.slice(0, 6);
|
||||
this.setData({ related });
|
||||
}).catch(() => {});
|
||||
}
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
wx.showToast({ title: '已加入今日训练', icon: 'success' });
|
||||
},
|
||||
|
||||
onBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack();
|
||||
} else {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ex = this.data.exercise || {};
|
||||
return {
|
||||
title: ex.name ? ('FITCOACH · ' + ex.name) : 'FITCOACH 智能健身教练',
|
||||
path: '/pages/detail/detail?id=' + this.data.id
|
||||
};
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: 'FITCOACH · 精准训练推荐',
|
||||
query: 'id=' + this.data.id
|
||||
};
|
||||
}
|
||||
});
|
||||
7
miniprogram/pages/detail/detail.json
Normal file
7
miniprogram/pages/detail/detail.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
}
|
||||
}
|
||||
72
miniprogram/pages/detail/detail.wxml
Normal file
72
miniprogram/pages/detail/detail.wxml
Normal file
@@ -0,0 +1,72 @@
|
||||
<view class="page" wx:if="{{exercise}}">
|
||||
<navbar title="{{exercise.name}}" show-back />
|
||||
|
||||
<view class="hero">
|
||||
<image
|
||||
wx:if="{{exercise.gifUrl || exercise.image}}"
|
||||
class="hero__img"
|
||||
src="{{exercise.gifUrl || exercise.image}}"
|
||||
mode="aspectFill" />
|
||||
<view wx:else class="hero__ph">🏋️</view>
|
||||
</view>
|
||||
|
||||
<view class="body section">
|
||||
<view class="name">{{exercise.name}}</view>
|
||||
|
||||
<view class="metas">
|
||||
<view class="meta"><view class="meta__k">部位</view><view class="meta__v">{{exercise.bodyPartLabel}}</view></view>
|
||||
<view class="meta"><view class="meta__k">器械</view><view class="meta__v">{{exercise.equipmentLabel}}</view></view>
|
||||
<view class="meta"><view class="meta__k">目标</view><view class="meta__v gold-text">{{exercise.targetLabel}}</view></view>
|
||||
<view class="meta"><view class="meta__k">类型</view><view class="meta__v">{{exercise.typeLabel}}</view></view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{exercise.muscleGroupLabel}}">
|
||||
<view class="block__h eyebrow">主要肌群</view>
|
||||
<view class="chips">
|
||||
<text class="tag tag--gold">{{exercise.muscleGroupLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{exercise.secondaryMusclesLabels.length}}">
|
||||
<view class="block__h eyebrow">协同肌群</view>
|
||||
<view class="chips">
|
||||
<text wx:for="{{exercise.secondaryMusclesLabels}}" wx:key="*this" class="tag">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{exercise.instructionSteps.zh.length}}">
|
||||
<view class="block__h eyebrow">动作步骤</view>
|
||||
<view class="steps">
|
||||
<view class="step" wx:for="{{exercise.instructionSteps.zh}}" wx:key="index">
|
||||
<view class="step__n">{{index + 1}}</view>
|
||||
<view class="step__t">{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{exercise.instructions.zh}}">
|
||||
<view class="block__h eyebrow">要点提示</view>
|
||||
<view class="tips">{{exercise.instructions.zh}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" wx:if="{{related.length}}">
|
||||
<view class="block__h eyebrow">相关推荐</view>
|
||||
<scroll-view scroll-x class="hscroll">
|
||||
<view class="hscroll__item" wx:for="{{related}}" wx:key="id">
|
||||
<exercise-card exercise="{{item}}" compact bind:tap="onExercise" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="addbar">
|
||||
<view class="cta" bindtap="onAdd">加入今日训练</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty-page">
|
||||
<view class="topbar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="topbar__back" bindtap="onBack">‹</view>
|
||||
</view>
|
||||
<view class="empty">{{ loading ? '加载中…' : '未找到该动作' }}</view>
|
||||
</view>
|
||||
57
miniprogram/pages/detail/detail.wxss
Normal file
57
miniprogram/pages/detail/detail.wxss
Normal file
@@ -0,0 +1,57 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); padding-bottom: 180rpx; }
|
||||
.hero { width: 100%; height: 520rpx; background: var(--surface-2); }
|
||||
.hero__img { width: 100%; height: 100%; display: block; }
|
||||
.hero__ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 120rpx; }
|
||||
|
||||
.name { font-size: 48rpx; font-weight: 700; color: var(--text); letter-spacing: 1rpx; }
|
||||
|
||||
.metas { display: flex; flex-wrap: wrap; gap: 14rpx; margin-top: 24rpx; }
|
||||
.meta {
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius-sm); padding: 16rpx 22rpx;
|
||||
min-width: calc((100% - 42rpx) / 4);
|
||||
}
|
||||
.meta__k { font-size: 20rpx; color: var(--text-3); }
|
||||
.meta__v { font-size: 26rpx; color: var(--text); margin-top: 6rpx; font-weight: 600; }
|
||||
|
||||
.block { margin-top: 36rpx; }
|
||||
.block__h { margin-bottom: 16rpx; }
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 12rpx; }
|
||||
|
||||
.steps { display: flex; flex-direction: column; gap: 18rpx; }
|
||||
.step { display: flex; gap: 20rpx; align-items: flex-start; }
|
||||
.step__n {
|
||||
flex: 0 0 auto; width: 48rpx; height: 48rpx; border-radius: 999rpx;
|
||||
background: var(--gold-soft); color: var(--gold); border: 1rpx solid var(--gold-line);
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 26rpx;
|
||||
}
|
||||
.step__t { flex: 1; font-size: 28rpx; color: var(--text); line-height: 1.5; padding-top: 6rpx; }
|
||||
|
||||
.tips {
|
||||
font-size: 28rpx; color: var(--text-2); line-height: 1.6;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius-sm); padding: 24rpx;
|
||||
}
|
||||
|
||||
.hscroll { white-space: nowrap; margin-top: 12rpx; }
|
||||
.hscroll__item { display: inline-block; width: 300rpx; margin-right: 20rpx; vertical-align: top; }
|
||||
|
||||
.addbar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.cta {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
|
||||
.empty-page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.topbar { height: 88rpx; display: flex; align-items: center; padding: 0 24rpx; }
|
||||
.topbar__back { font-size: 56rpx; color: var(--gold); width: 60rpx; line-height: 1; }
|
||||
.empty { color: var(--text-3); font-size: 28rpx; text-align: center; padding: 120rpx 0; }
|
||||
Reference in New Issue
Block a user