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:
20
miniprogram/components/bottom-nav/index.js
Normal file
20
miniprogram/components/bottom-nav/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
Component({
|
||||
properties: {
|
||||
active: { type: String, value: 'home' } // 'home' | 'category' | 'plan'
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
{ key: 'home', label: '首页', emoji: '🏠', page: '/pages/index/index' },
|
||||
{ key: 'category', label: '分类', emoji: '📚', page: '/pages/category/category' },
|
||||
{ key: 'plan', label: '计划', emoji: '📋', page: '/pages/collection/collection' }
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
onTap(e) {
|
||||
const page = e.currentTarget.dataset.page;
|
||||
if (!page) return;
|
||||
// 使用 reLaunch 重置页面栈,避免页面层级堆叠
|
||||
wx.reLaunch({ url: page });
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/bottom-nav/index.json
Normal file
4
miniprogram/components/bottom-nav/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
11
miniprogram/components/bottom-nav/index.wxml
Normal file
11
miniprogram/components/bottom-nav/index.wxml
Normal file
@@ -0,0 +1,11 @@
|
||||
<view class="bn">
|
||||
<view
|
||||
wx:for="{{items}}"
|
||||
wx:key="key"
|
||||
class="bn__item {{active === item.key ? 'bn__item--active' : ''}}"
|
||||
data-page="{{item.page}}"
|
||||
bindtap="onTap">
|
||||
<text class="bn__emoji">{{item.emoji}}</text>
|
||||
<text class="bn__label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
22
miniprogram/components/bottom-nav/index.wxss
Normal file
22
miniprogram/components/bottom-nav/index.wxss
Normal file
@@ -0,0 +1,22 @@
|
||||
.bn {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
background: rgba(20,26,22,0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 60;
|
||||
}
|
||||
.bn__item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 0 14rpx;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.bn__item--active { color: var(--gold); }
|
||||
.bn__emoji { font-size: 40rpx; line-height: 1; }
|
||||
.bn__label { font-size: 22rpx; margin-top: 6rpx; }
|
||||
.bn__item--active .bn__label { color: var(--gold-2); }
|
||||
16
miniprogram/components/chip/index.js
Normal file
16
miniprogram/components/chip/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Component({
|
||||
properties: {
|
||||
label: { type: String, value: '' },
|
||||
active: { type: Boolean, value: false },
|
||||
color: { type: String, value: '' },
|
||||
value: { type: String, value: '' }
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
this.triggerEvent('tap', {
|
||||
value: this.data.value,
|
||||
label: this.data.label
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/chip/index.json
Normal file
4
miniprogram/components/chip/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
6
miniprogram/components/chip/index.wxml
Normal file
6
miniprogram/components/chip/index.wxml
Normal file
@@ -0,0 +1,6 @@
|
||||
<view
|
||||
class="chip {{active ? 'chip--active' : ''}}"
|
||||
style="{{color ? 'border-color:' + color + '66; color:' + color : ''}}"
|
||||
bindtap="onTap">
|
||||
<text>{{label}}</text>
|
||||
</view>
|
||||
18
miniprogram/components/chip/index.wxss
Normal file
18
miniprogram/components/chip/index.wxss
Normal file
@@ -0,0 +1,18 @@
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12rpx 26rpx;
|
||||
border-radius: 999rpx;
|
||||
background: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
font-size: 24rpx;
|
||||
border: 1rpx solid var(--line);
|
||||
transition: all .15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip--active {
|
||||
background: var(--gold-soft);
|
||||
color: var(--gold-2);
|
||||
border-color: var(--gold-line);
|
||||
}
|
||||
14
miniprogram/components/exercise-card/index.js
Normal file
14
miniprogram/components/exercise-card/index.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/exercise-card/index.json
Normal file
4
miniprogram/components/exercise-card/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
21
miniprogram/components/exercise-card/index.wxml
Normal file
21
miniprogram/components/exercise-card/index.wxml
Normal 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>
|
||||
40
miniprogram/components/exercise-card/index.wxss
Normal file
40
miniprogram/components/exercise-card/index.wxss
Normal 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;
|
||||
}
|
||||
27
miniprogram/components/navbar/index.js
Normal file
27
miniprogram/components/navbar/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const app = getApp();
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
title: { type: String, value: '' },
|
||||
showBack: { type: Boolean, value: false }
|
||||
},
|
||||
data: {
|
||||
statusBarHeight: 20
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const h = (app.globalData && app.globalData.statusBarHeight) || 20;
|
||||
this.setData({ statusBarHeight: h });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack();
|
||||
} else {
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/navbar/index.json
Normal file
4
miniprogram/components/navbar/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
7
miniprogram/components/navbar/index.wxml
Normal file
7
miniprogram/components/navbar/index.wxml
Normal file
@@ -0,0 +1,7 @@
|
||||
<view class="navbar">
|
||||
<view class="navbar__status" style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar__bar">
|
||||
<view wx:if="{{showBack}}" class="navbar__back" bindtap="onBack">‹</view>
|
||||
<view wx:if="{{title}}" class="navbar__title">{{title}}</view>
|
||||
</view>
|
||||
</view>
|
||||
29
miniprogram/components/navbar/index.wxss
Normal file
29
miniprogram/components/navbar/index.wxss
Normal file
@@ -0,0 +1,29 @@
|
||||
.navbar {
|
||||
background: linear-gradient(180deg, rgba(22,32,27,0.96) 0%, rgba(14,17,16,0.55) 100%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
.navbar__status { width: 100%; }
|
||||
.navbar__bar {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
.navbar__back {
|
||||
font-size: 56rpx;
|
||||
color: var(--gold);
|
||||
line-height: 1;
|
||||
width: 60rpx;
|
||||
}
|
||||
.navbar__title {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
color: var(--text);
|
||||
}
|
||||
13
miniprogram/components/section-header/index.js
Normal file
13
miniprogram/components/section-header/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Component({
|
||||
properties: {
|
||||
title: { type: String, value: '' },
|
||||
subtitle: { type: String, value: '' },
|
||||
eyebrow: { type: String, value: '' },
|
||||
action: { type: String, value: '' }
|
||||
},
|
||||
methods: {
|
||||
onAction() {
|
||||
this.triggerEvent('action');
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/section-header/index.json
Normal file
4
miniprogram/components/section-header/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
8
miniprogram/components/section-header/index.wxml
Normal file
8
miniprogram/components/section-header/index.wxml
Normal file
@@ -0,0 +1,8 @@
|
||||
<view class="sh">
|
||||
<view class="sh__main">
|
||||
<view wx:if="{{eyebrow}}" class="eyebrow">{{eyebrow}}</view>
|
||||
<view class="sh__title">{{title}}</view>
|
||||
<view wx:if="{{subtitle}}" class="sh__sub">{{subtitle}}</view>
|
||||
</view>
|
||||
<view wx:if="{{action}}" class="sh__action" bindtap="onAction">{{action}} ›</view>
|
||||
</view>
|
||||
23
miniprogram/components/section-header/index.wxss
Normal file
23
miniprogram/components/section-header/index.wxss
Normal file
@@ -0,0 +1,23 @@
|
||||
.sh {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 0 20rpx;
|
||||
}
|
||||
.sh__title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
color: var(--text);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.sh__sub {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-2);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.sh__action {
|
||||
font-size: 26rpx;
|
||||
color: var(--gold);
|
||||
white-space: nowrap;
|
||||
}
|
||||
Reference in New Issue
Block a user