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:
2026-07-22 00:35:40 +08:00
commit b3b58e66fb
103 changed files with 154759 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
Component({
properties: {
exercise: { type: Object, value: {} },
score: { type: Number, value: 0 },
reason: { type: String, value: '' },
compact: { type: Boolean, value: false }
},
methods: {
onTap() {
const ex = this.data.exercise || {};
this.triggerEvent('tap', { id: ex.id, exercise: ex });
}
}
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,21 @@
<view class="ex-card {{compact ? 'ex-card--compact' : ''}}" bindtap="onTap">
<view class="ex-card__media">
<image
wx:if="{{exercise.gifUrl || exercise.image}}"
class="ex-card__img"
src="{{exercise.gifUrl || exercise.image}}"
mode="aspectFill"
lazy-load="true" />
<view wx:else class="ex-card__ph">🏋️</view>
<view wx:if="{{exercise.typeLabel}}" class="ex-card__type">{{exercise.typeLabel}}</view>
<view wx:if="{{score > 0}}" class="ex-card__score">匹配 {{score}}</view>
</view>
<view class="ex-card__body">
<view class="ex-card__name">{{exercise.name}}</view>
<view class="ex-card__chips">
<text wx:if="{{exercise.targetLabel}}" class="tag tag--gold">{{exercise.targetLabel}}</text>
<text wx:if="{{exercise.equipmentLabel}}" class="tag">{{exercise.equipmentLabel}}</text>
</view>
<view wx:if="{{reason}}" class="ex-card__reason">{{reason}}</view>
</view>
</view>

View File

@@ -0,0 +1,40 @@
.ex-card {
background: var(--surface);
border-radius: var(--radius-sm);
overflow: hidden;
border: 1rpx solid var(--line);
width: 100%;
}
.ex-card__media {
position: relative;
width: 100%;
height: 200rpx;
background: var(--surface-2);
}
.ex-card--compact .ex-card__media { height: 160rpx; }
.ex-card__img { width: 100%; height: 100%; display: block; }
.ex-card__ph {
width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
font-size: 64rpx;
}
.ex-card__type {
position: absolute; left: 12rpx; top: 12rpx;
font-size: 20rpx; color: var(--text-2);
background: rgba(0,0,0,0.45); padding: 4rpx 14rpx; border-radius: 999rpx;
}
.ex-card__score {
position: absolute; right: 12rpx; top: 12rpx;
font-size: 20rpx; color: #0E1110; font-weight: 700;
background: var(--gold); padding: 4rpx 14rpx; border-radius: 999rpx;
}
.ex-card__body { padding: 16rpx 18rpx 20rpx; }
.ex-card__name {
font-size: 28rpx; font-weight: 600; color: var(--text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ex-card__chips { display: flex; gap: 10rpx; margin-top: 12rpx; flex-wrap: wrap; }
.ex-card__reason {
margin-top: 12rpx; font-size: 22rpx; color: var(--text-3);
line-height: 1.45;
}