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:
89
miniprogram/pages/category/category.js
Normal file
89
miniprogram/pages/category/category.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
const DIM_MAP = {
|
||||
bodyPart: 'body-parts',
|
||||
equipment: 'equipment',
|
||||
target: 'targets',
|
||||
muscleGroup: 'muscle-groups',
|
||||
type: 'types'
|
||||
};
|
||||
|
||||
function labelForDim(d) {
|
||||
return ({
|
||||
bodyPart: '按部位',
|
||||
equipment: '按器械',
|
||||
target: '按目标',
|
||||
muscleGroup: '按肌群',
|
||||
type: '按类型'
|
||||
})[d] || '分类';
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
dimension: 'bodyPart',
|
||||
title: '分类',
|
||||
value: '',
|
||||
values: [],
|
||||
activeValue: '',
|
||||
exercises: [],
|
||||
loading: true,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const dimension = query.dimension || 'bodyPart';
|
||||
const value = query.value || '';
|
||||
const title = query.title || labelForDim(dimension);
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 20,
|
||||
dimension,
|
||||
value,
|
||||
title
|
||||
});
|
||||
this.loadValues();
|
||||
},
|
||||
|
||||
loadValues() {
|
||||
this.setData({ loading: true });
|
||||
const key = DIM_MAP[this.data.dimension] || 'body-parts';
|
||||
svc.getCategories(key).then((items) => {
|
||||
this.setData({
|
||||
values: items || [],
|
||||
activeValue: this.data.value,
|
||||
loading: false
|
||||
});
|
||||
this.loadExercises();
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
loadExercises() {
|
||||
this.setData({ loading: true });
|
||||
const params = { page: this.data.page, pageSize: this.data.pageSize };
|
||||
params[this.data.dimension] = this.data.activeValue;
|
||||
svc.listExercises(params).then((res) => {
|
||||
this.setData({
|
||||
exercises: (res && res.items) || [],
|
||||
total: (res && res.total) || 0,
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onValue(e) {
|
||||
const value = e.detail.value;
|
||||
if (value === this.data.activeValue) return;
|
||||
this.setData({ activeValue: value, page: 1 }, () => this.loadExercises());
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
}
|
||||
});
|
||||
9
miniprogram/pages/category/category.json
Normal file
9
miniprogram/pages/category/category.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"chip": "/components/chip/index",
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
}
|
||||
}
|
||||
29
miniprogram/pages/category/category.wxml
Normal file
29
miniprogram/pages/category/category.wxml
Normal file
@@ -0,0 +1,29 @@
|
||||
<view class="page">
|
||||
<navbar title="{{title}}" show-back />
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<scroll-view wx:if="{{values.length}}" scroll-x class="filterbar">
|
||||
<chip
|
||||
wx:for="{{values}}"
|
||||
wx:key="value"
|
||||
label="{{item.label}}"
|
||||
value="{{item.value}}"
|
||||
active="{{activeValue === item.value}}"
|
||||
bind:tap="onValue" />
|
||||
<view class="filterbar__spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="count muted" wx:if="{{!loading}}">共 {{total}} 个动作</view>
|
||||
|
||||
<view class="grid" wx:if="{{exercises.length}}">
|
||||
<view class="grid__item" wx:for="{{exercises}}" wx:key="id">
|
||||
<exercise-card exercise="{{item}}" bind:tap="onExercise" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty">暂无数据</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="category" />
|
||||
</view>
|
||||
8
miniprogram/pages/category/category.wxss
Normal file
8
miniprogram/pages/category/category.wxss
Normal file
@@ -0,0 +1,8 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.filterbar { white-space: nowrap; padding: 8rpx 0 4rpx; }
|
||||
.filterbar chip { display: inline-block; margin-right: 16rpx; }
|
||||
.filterbar__spacer { display: inline-block; width: 4rpx; }
|
||||
.count { font-size: 24rpx; margin: 20rpx 0 8rpx; }
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 18rpx; margin-top: 12rpx; }
|
||||
.grid__item { width: calc((100% - 18rpx) / 2); }
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 80rpx 0; }
|
||||
42
miniprogram/pages/collection-detail/collection-detail.js
Normal file
42
miniprogram/pages/collection-detail/collection-detail.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
slug: '',
|
||||
collection: null,
|
||||
exercises: [],
|
||||
loading: true,
|
||||
page: 1,
|
||||
pageSize: 30,
|
||||
total: 0
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const slug = query.slug || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, slug });
|
||||
this.load();
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
svc.getCollectionExercises(this.data.slug, {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize
|
||||
}).then((res) => {
|
||||
this.setData({
|
||||
collection: res.collection,
|
||||
exercises: (res && res.items) || [],
|
||||
total: (res && res.total) || 0,
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
}
|
||||
}
|
||||
24
miniprogram/pages/collection-detail/collection-detail.wxml
Normal file
24
miniprogram/pages/collection-detail/collection-detail.wxml
Normal file
@@ -0,0 +1,24 @@
|
||||
<view class="page" wx:if="{{collection}}">
|
||||
<navbar title="{{collection.name}}" show-back />
|
||||
|
||||
<view class="header" style="background: linear-gradient(135deg, {{collection.color}}26, var(--surface));">
|
||||
<view class="header__emoji" style="background: {{collection.color}}22; color: {{collection.color}};">{{collection.emoji}}</view>
|
||||
<view class="header__name">{{collection.name}}</view>
|
||||
<view class="header__desc">{{collection.description}}</view>
|
||||
<view class="header__count" style="color: {{collection.color}};">共 {{total}} 个动作</view>
|
||||
</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="grid" wx:if="{{exercises.length}}">
|
||||
<view class="grid__item" wx:for="{{exercises}}" wx:key="id">
|
||||
<exercise-card exercise="{{item}}" bind:tap="onExercise" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty">暂无数据</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty-page">
|
||||
<view class="empty">{{ loading ? '加载中…' : '未找到该训练组合' }}</view>
|
||||
</view>
|
||||
14
miniprogram/pages/collection-detail/collection-detail.wxss
Normal file
14
miniprogram/pages/collection-detail/collection-detail.wxss
Normal file
@@ -0,0 +1,14 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); padding-bottom: 60rpx; }
|
||||
.header { padding: 32rpx; border-bottom: 1rpx solid var(--line); }
|
||||
.header__emoji {
|
||||
width: 120rpx; height: 120rpx; border-radius: 28rpx;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 64rpx;
|
||||
}
|
||||
.header__name { font-size: 44rpx; font-weight: 700; margin-top: 20rpx; }
|
||||
.header__desc { font-size: 26rpx; color: var(--text-2); margin-top: 12rpx; line-height: 1.5; }
|
||||
.header__count { font-size: 24rpx; margin-top: 16rpx; }
|
||||
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
.grid__item { width: calc((100% - 18rpx) / 2); }
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 120rpx 0; }
|
||||
.empty-page { min-height: 100vh; background: var(--bg-grad); }
|
||||
40
miniprogram/pages/collection/collection.js
Normal file
40
miniprogram/pages/collection/collection.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
collections: [],
|
||||
counts: {},
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
this.load();
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
svc.listCollections().then((list) => {
|
||||
const collections = list || [];
|
||||
this.setData({ collections, loading: false });
|
||||
// 异步获取每个训练组合的动作数量
|
||||
collections.forEach(c => {
|
||||
svc.getCollectionExercises(c.slug, { pageSize: 1 }).then((res) => {
|
||||
const key = 'counts.' + c.slug;
|
||||
this.setData({ [key]: (res && res.total) || 0 });
|
||||
}).catch(() => {});
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onOpen(e) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/collection-detail/collection-detail?slug=' +
|
||||
encodeURIComponent(e.currentTarget.dataset.slug)
|
||||
});
|
||||
}
|
||||
});
|
||||
7
miniprogram/pages/collection/collection.json
Normal file
7
miniprogram/pages/collection/collection.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
}
|
||||
}
|
||||
31
miniprogram/pages/collection/collection.wxml
Normal file
31
miniprogram/pages/collection/collection.wxml
Normal file
@@ -0,0 +1,31 @@
|
||||
<view class="page">
|
||||
<navbar title="训练计划" />
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="intro">
|
||||
<view class="intro__eyebrow">PROGRAMS</view>
|
||||
<view class="intro__title">挑选你的训练主题</view>
|
||||
<view class="intro__sub">从推、拉、腿到居家无器械,一键开启</view>
|
||||
</view>
|
||||
|
||||
<view class="list" wx:if="{{collections.length}}">
|
||||
<view
|
||||
wx:for="{{collections}}"
|
||||
wx:key="slug"
|
||||
class="col-card"
|
||||
style="background: linear-gradient(135deg, {{item.color}}26, var(--surface)); border-color: {{item.color}}55;"
|
||||
data-slug="{{item.slug}}"
|
||||
bindtap="onOpen">
|
||||
<view class="col-card__emoji" style="background: {{item.color}}22;">{{item.emoji}}</view>
|
||||
<view class="col-card__main">
|
||||
<view class="col-card__name">{{item.name}}</view>
|
||||
<view class="col-card__desc">{{item.description}}</view>
|
||||
</view>
|
||||
<view class="col-card__count" style="color: {{item.color}};">{{counts[item.slug] || ''}} 个动作 ›</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty">暂无数据</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="plan" />
|
||||
</view>
|
||||
22
miniprogram/pages/collection/collection.wxss
Normal file
22
miniprogram/pages/collection/collection.wxss
Normal file
@@ -0,0 +1,22 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.intro { padding: 16rpx 0 28rpx; }
|
||||
.intro__eyebrow { color: var(--gold); font-size: 22rpx; letter-spacing: 4rpx; }
|
||||
.intro__title { font-size: 44rpx; font-weight: 700; margin-top: 12rpx; }
|
||||
.intro__sub { font-size: 26rpx; color: var(--text-2); margin-top: 12rpx; }
|
||||
|
||||
.list { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.col-card {
|
||||
display: flex; align-items: center; gap: 24rpx;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 28rpx;
|
||||
}
|
||||
.col-card__emoji {
|
||||
width: 96rpx; height: 96rpx; border-radius: 24rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 52rpx; flex: 0 0 auto;
|
||||
}
|
||||
.col-card__main { flex: 1; min-width: 0; }
|
||||
.col-card__name { font-size: 34rpx; font-weight: 700; color: var(--text); }
|
||||
.col-card__desc { font-size: 24rpx; color: var(--text-2); margin-top: 8rpx; line-height: 1.4; }
|
||||
.col-card__count { font-size: 24rpx; flex: 0 0 auto; }
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 80rpx 0; }
|
||||
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; }
|
||||
88
miniprogram/pages/index/index.js
Normal file
88
miniprogram/pages/index/index.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
stats: null,
|
||||
bodyParts: [],
|
||||
equipment: [],
|
||||
featured: [],
|
||||
collections: [],
|
||||
loading: true,
|
||||
quickEntries: [
|
||||
{ key: 'recommend', label: '智能推荐', emoji: '✨', url: '/pages/recommend/recommend' },
|
||||
{ key: 'bodyPart', label: '按部位', emoji: '💪', url: '/pages/category/category?dimension=bodyPart' },
|
||||
{ key: 'equipment', label: '按器械', emoji: '🏋️', url: '/pages/category/category?dimension=equipment' },
|
||||
{ key: 'plan', label: '训练计划', emoji: '📋', url: '/pages/collection/collection' },
|
||||
{ key: 'home', label: '居家无器械', emoji: '🏠', url: '/pages/collection-detail/collection-detail?slug=home' },
|
||||
{ key: 'search', label: '搜索', emoji: '🔍', url: '/pages/search/search' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ loading: true });
|
||||
Promise.all([
|
||||
svc.getStats().catch(() => null),
|
||||
svc.listExercises({ pageSize: 8 }).catch(() => ({ items: [] })),
|
||||
svc.listCollections().catch(() => [])
|
||||
]).then(([stats, list, collections]) => {
|
||||
this.setData({
|
||||
stats,
|
||||
bodyParts: (stats && stats.bodyParts) || [],
|
||||
equipment: (stats && stats.equipment) || [],
|
||||
featured: (list && list.items) || [],
|
||||
collections: collections || [],
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
goRecommend() {
|
||||
wx.navigateTo({ url: '/pages/recommend/recommend' });
|
||||
},
|
||||
|
||||
onQuick(e) {
|
||||
wx.navigateTo({ url: e.currentTarget.dataset.url });
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
wx.navigateTo({ url: '/pages/search/search' });
|
||||
},
|
||||
|
||||
onScanBodyPart(e) {
|
||||
const { value, label } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
url: '/pages/category/category?dimension=bodyPart&value=' +
|
||||
encodeURIComponent(value) + '&title=' + encodeURIComponent(label)
|
||||
});
|
||||
},
|
||||
|
||||
onScanEquipment(e) {
|
||||
const { value, label } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
url: '/pages/category/category?dimension=equipment&value=' +
|
||||
encodeURIComponent(value) + '&title=' + encodeURIComponent(label)
|
||||
});
|
||||
},
|
||||
|
||||
onCollection(e) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/collection-detail/collection-detail?slug=' +
|
||||
encodeURIComponent(e.currentTarget.dataset.slug)
|
||||
});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id)
|
||||
});
|
||||
}
|
||||
});
|
||||
8
miniprogram/pages/index/index.json
Normal file
8
miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
}
|
||||
}
|
||||
99
miniprogram/pages/index/index.wxml
Normal file
99
miniprogram/pages/index/index.wxml
Normal file
@@ -0,0 +1,99 @@
|
||||
<view class="home" style="padding-top: {{statusBarHeight}}px;">
|
||||
<!-- Hero -->
|
||||
<view class="hero">
|
||||
<view class="hero__eyebrow">FITCOACH · 智能健身教练</view>
|
||||
<view class="hero__title">今天,练点什么?</view>
|
||||
<view class="hero__sub">精准匹配你的目标肌群,让每一次训练都更高效</view>
|
||||
|
||||
<view class="search-pill" bindtap="onSearch">
|
||||
<text class="search-pill__icon">🔍</text>
|
||||
<text class="search-pill__ph">搜索动作 / 器械 / 部位</text>
|
||||
</view>
|
||||
|
||||
<view class="cta" bindtap="goRecommend">
|
||||
<text class="cta__icon">✨</text>
|
||||
<text class="cta__txt">智能推荐</text>
|
||||
<text class="cta__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口 2x3 -->
|
||||
<view class="section quick">
|
||||
<view class="quick__grid">
|
||||
<view
|
||||
wx:for="{{quickEntries}}"
|
||||
wx:key="key"
|
||||
class="quick__tile"
|
||||
data-url="{{item.url}}"
|
||||
bindtap="onQuick">
|
||||
<text class="quick__emoji">{{item.emoji}}</text>
|
||||
<text class="quick__label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选动作 横向滑动 -->
|
||||
<view class="section">
|
||||
<section-header eyebrow="FEATURED" title="精选动作" subtitle="编辑精选 · 高效入门" action="全部" bind:action="goRecommend" />
|
||||
<scroll-view wx:if="{{featured.length}}" scroll-x class="hscroll">
|
||||
<view class="hscroll__item" wx:for="{{featured}}" wx:key="id">
|
||||
<exercise-card exercise="{{item}}" compact bind:tap="onExercise" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view wx:else class="empty">暂无数据</view>
|
||||
</view>
|
||||
|
||||
<!-- 训练组合 横向 -->
|
||||
<view class="section" wx:if="{{collections.length}}">
|
||||
<section-header eyebrow="PROGRAMS" title="训练组合" subtitle="一键开启主题训练" />
|
||||
<scroll-view scroll-x class="chip-scroll">
|
||||
<view
|
||||
wx:for="{{collections}}"
|
||||
wx:key="slug"
|
||||
class="prog"
|
||||
style="background: {{item.color}}22; border-color: {{item.color}}55;"
|
||||
data-slug="{{item.slug}}"
|
||||
bindtap="onCollection">
|
||||
<text class="prog__emoji">{{item.emoji}}</text>
|
||||
<text class="prog__name" style="color: {{item.color}};">{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 按部位浏览 -->
|
||||
<view class="section">
|
||||
<section-header eyebrow="BY BODY PART" title="按部位浏览" subtitle="选择你的目标区域" />
|
||||
<view class="bp-grid">
|
||||
<view
|
||||
wx:for="{{bodyParts}}"
|
||||
wx:key="value"
|
||||
class="bp"
|
||||
data-value="{{item.value}}"
|
||||
data-label="{{item.label}}"
|
||||
bindtap="onScanBodyPart">
|
||||
<text class="bp__label">{{item.label}}</text>
|
||||
<text class="bp__count">{{item.count}} 个动作</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门器械 -->
|
||||
<view class="section" wx:if="{{equipment.length}}">
|
||||
<section-header eyebrow="EQUIPMENT" title="热门器械" subtitle="按器械筛选动作" />
|
||||
<scroll-view scroll-x class="chip-scroll">
|
||||
<view
|
||||
wx:for="{{equipment}}"
|
||||
wx:key="value"
|
||||
class="eq"
|
||||
data-value="{{item.value}}"
|
||||
data-label="{{item.label}}"
|
||||
bindtap="onScanEquipment">
|
||||
<text class="eq__name">{{item.label}}</text>
|
||||
<text class="eq__count">{{item.count}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="home" />
|
||||
</view>
|
||||
78
miniprogram/pages/index/index.wxss
Normal file
78
miniprogram/pages/index/index.wxss
Normal file
@@ -0,0 +1,78 @@
|
||||
.home { min-height: 100vh; background: var(--bg-grad); }
|
||||
|
||||
.hero {
|
||||
padding: 20rpx 40rpx 48rpx;
|
||||
background: linear-gradient(150deg, #1C2A22 0%, #0E1110 70%);
|
||||
border-bottom-left-radius: 48rpx;
|
||||
border-bottom-right-radius: 48rpx;
|
||||
}
|
||||
.hero__eyebrow { color: var(--gold); font-size: 22rpx; letter-spacing: 4rpx; }
|
||||
.hero__title { font-size: 56rpx; font-weight: 700; letter-spacing: 2rpx; margin-top: 14rpx; color: var(--text); }
|
||||
.hero__sub { font-size: 26rpx; color: var(--text-2); margin-top: 14rpx; }
|
||||
|
||||
.search-pill {
|
||||
margin-top: 36rpx;
|
||||
display: flex; align-items: center;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 22rpx 30rpx;
|
||||
}
|
||||
.search-pill__icon { font-size: 30rpx; margin-right: 16rpx; }
|
||||
.search-pill__ph { color: var(--text-3); font-size: 28rpx; }
|
||||
|
||||
.cta {
|
||||
margin-top: 24rpx;
|
||||
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: 28rpx;
|
||||
box-shadow: 0 12rpx 30rpx rgba(217,179,108,0.28);
|
||||
}
|
||||
.cta__icon { margin-right: 12rpx; }
|
||||
.cta__arrow { margin-left: 12rpx; font-size: 36rpx; }
|
||||
|
||||
.quick__grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
.quick__tile {
|
||||
width: calc((100% - 36rpx) / 3);
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 28rpx 18rpx;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.quick__tile::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: radial-gradient(circle at 50% 0%, var(--gold-soft), transparent 70%);
|
||||
}
|
||||
.quick__emoji { font-size: 48rpx; position: relative; z-index: 1; }
|
||||
.quick__label { font-size: 26rpx; margin-top: 12rpx; color: var(--text); position: relative; z-index: 1; }
|
||||
|
||||
.hscroll { white-space: nowrap; margin-top: 8rpx; }
|
||||
.hscroll__item { display: inline-block; width: 300rpx; margin-right: 20rpx; vertical-align: top; }
|
||||
|
||||
.chip-scroll { white-space: nowrap; margin-top: 8rpx; }
|
||||
.prog {
|
||||
display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-width: 160rpx; padding: 24rpx 28rpx; margin-right: 18rpx;
|
||||
border-radius: var(--radius-sm); border: 1rpx solid;
|
||||
}
|
||||
.prog__emoji { font-size: 44rpx; }
|
||||
.prog__name { font-size: 26rpx; font-weight: 700; margin-top: 10rpx; }
|
||||
|
||||
.bp-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.bp {
|
||||
width: calc((100% - 48rpx) / 4);
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius-sm); padding: 22rpx 12rpx;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.bp__label { font-size: 26rpx; color: var(--text); font-weight: 600; }
|
||||
.bp__count { font-size: 20rpx; color: var(--text-3); margin-top: 6rpx; }
|
||||
|
||||
.eq {
|
||||
display: inline-flex; align-items: center; gap: 12rpx;
|
||||
background: var(--surface-2); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 16rpx 28rpx; margin-right: 16rpx;
|
||||
}
|
||||
.eq__name { font-size: 26rpx; color: var(--text); }
|
||||
.eq__count { font-size: 22rpx; color: var(--gold); }
|
||||
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 40rpx 0; }
|
||||
91
miniprogram/pages/recommend/recommend.js
Normal file
91
miniprogram/pages/recommend/recommend.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
step: 1, // 1: 选择目标肌群 / 2: 展示结果
|
||||
bodyGroups: [],
|
||||
selectedTarget: '',
|
||||
selectedTargetLabel: '',
|
||||
equipments: [],
|
||||
selectedEquipment: '',
|
||||
results: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
this.prepare();
|
||||
},
|
||||
|
||||
prepare() {
|
||||
this.setData({ loading: true });
|
||||
Promise.all([
|
||||
svc.getCategories('targets').catch(() => []),
|
||||
svc.getCategories('equipment').catch(() => []),
|
||||
svc.getCategories('body-parts').catch(() => [])
|
||||
]).then(([targets, equipments, bodyParts]) => {
|
||||
const tList = targets || [];
|
||||
// 按 bodyPart 将目标肌群分组展示;后端未返回分组字段时退化为单组
|
||||
const grouped = {};
|
||||
(bodyParts || []).forEach(bp => { grouped[bp.value] = { label: bp.label, targets: [] }; });
|
||||
tList.forEach(t => {
|
||||
const key = t.bodyPart || 'other';
|
||||
if (!grouped[key]) grouped[key] = { label: t.bodyPartLabel || '其他', targets: [] };
|
||||
grouped[key].targets.push(t);
|
||||
});
|
||||
const bodyGroups = Object.keys(grouped)
|
||||
.map(k => grouped[k])
|
||||
.filter(g => g.targets.length);
|
||||
this.setData({
|
||||
bodyGroups: bodyGroups.length ? bodyGroups : [{ label: '全部目标肌群', targets: tList }],
|
||||
equipments: equipments || [],
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onSelectTarget(e) {
|
||||
this.setData({
|
||||
selectedTarget: e.currentTarget.dataset.value,
|
||||
selectedTargetLabel: e.currentTarget.dataset.label
|
||||
});
|
||||
},
|
||||
|
||||
onSelectEquipment(e) {
|
||||
const value = e.currentTarget.dataset.value;
|
||||
this.setData({ selectedEquipment: this.data.selectedEquipment === value ? '' : value });
|
||||
},
|
||||
|
||||
onConfirm() {
|
||||
if (!this.data.selectedTarget) {
|
||||
wx.showToast({ title: '请先选择目标肌群', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ loading: true, step: 2 });
|
||||
svc.recommend({
|
||||
target: this.data.selectedTarget,
|
||||
equipment: this.data.selectedEquipment,
|
||||
limit: 20
|
||||
}).then((res) => {
|
||||
this.setData({ results: (res && res.items) || [], loading: false });
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onBack() {
|
||||
if (this.data.step === 2) {
|
||||
this.setData({ step: 1 });
|
||||
} else {
|
||||
wx.navigateBack();
|
||||
}
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
}
|
||||
});
|
||||
7
miniprogram/pages/recommend/recommend.json
Normal file
7
miniprogram/pages/recommend/recommend.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
}
|
||||
}
|
||||
80
miniprogram/pages/recommend/recommend.wxml
Normal file
80
miniprogram/pages/recommend/recommend.wxml
Normal file
@@ -0,0 +1,80 @@
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="topbar">
|
||||
<view class="topbar__back" bindtap="onBack">‹</view>
|
||||
<view class="topbar__title">智能推荐</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤 1:选择目标肌群 -->
|
||||
<block wx:if="{{step === 1}}">
|
||||
<view class="hero2">
|
||||
<view class="hero2__eyebrow">SMART MATCH</view>
|
||||
<view class="hero2__title">选择你的目标肌群</view>
|
||||
<view class="hero2__sub">为你智能匹配最合适动作,并给出训练理由</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="group" wx:for="{{bodyGroups}}" wx:key="label" wx:for-item="group">
|
||||
<view class="group__label eyebrow">{{group.label}}</view>
|
||||
<view class="group__chips">
|
||||
<view
|
||||
wx:for="{{group.targets}}"
|
||||
wx:key="value"
|
||||
wx:for-item="t"
|
||||
class="tgt {{selectedTarget === t.value ? 'tgt--active' : ''}}"
|
||||
data-value="{{t.value}}"
|
||||
data-label="{{t.label}}"
|
||||
bindtap="onSelectTarget">
|
||||
<text>{{t.label}}</text>
|
||||
<text class="tgt__count">{{t.count}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="group" wx:if="{{equipments.length}}">
|
||||
<view class="group__label eyebrow">器械偏好(可选)</view>
|
||||
<view class="group__chips">
|
||||
<view
|
||||
wx:for="{{equipments}}"
|
||||
wx:key="value"
|
||||
wx:for-item="eq"
|
||||
class="tgt {{selectedEquipment === eq.value ? 'tgt--active' : ''}}"
|
||||
data-value="{{eq.value}}"
|
||||
bindtap="onSelectEquipment">
|
||||
<text>{{eq.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="confirm">
|
||||
<view class="confirm__bar">
|
||||
<text wx:if="{{selectedTargetLabel}}" class="confirm__sel">已选:{{selectedTargetLabel}}</text>
|
||||
<text wx:else class="confirm__hint">请选择目标肌群</text>
|
||||
</view>
|
||||
<view class="cta" bindtap="onConfirm">
|
||||
<text>智能匹配动作</text>
|
||||
<text class="cta__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 步骤 2:结果列表 -->
|
||||
<block wx:else>
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="result-head">
|
||||
<text class="result-head__title">为你匹配 · {{selectedTargetLabel}}</text>
|
||||
<text class="result-head__count">{{results.length}} 个动作</text>
|
||||
</view>
|
||||
<view class="rlist" wx:if="{{results.length}}">
|
||||
<view class="rlist__item" wx:for="{{results}}" wx:key="id" wx:for-item="item">
|
||||
<exercise-card
|
||||
exercise="{{item}}"
|
||||
score="{{item.score}}"
|
||||
reason="{{item.reason}}"
|
||||
bind:tap="onExercise" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="empty">{{ loading ? '匹配中…' : '暂无匹配结果' }}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
46
miniprogram/pages/recommend/recommend.wxss
Normal file
46
miniprogram/pages/recommend/recommend.wxss
Normal file
@@ -0,0 +1,46 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.topbar { height: 88rpx; display: flex; align-items: center; position: relative; padding: 0 24rpx; }
|
||||
.topbar__back { font-size: 56rpx; color: var(--gold); width: 60rpx; line-height: 1; }
|
||||
.topbar__title { position: absolute; left: 0; right: 0; text-align: center; font-size: 34rpx; font-weight: 700; color: var(--text); }
|
||||
|
||||
.hero2 { padding: 12rpx 40rpx 28rpx; }
|
||||
.hero2__eyebrow { color: var(--gold); font-size: 22rpx; letter-spacing: 4rpx; }
|
||||
.hero2__title { font-size: 48rpx; font-weight: 700; margin-top: 12rpx; }
|
||||
.hero2__sub { font-size: 26rpx; color: var(--text-2); margin-top: 12rpx; }
|
||||
|
||||
.group { margin-bottom: 36rpx; }
|
||||
.group__label { margin-bottom: 18rpx; }
|
||||
.group__chips { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.tgt {
|
||||
display: inline-flex; align-items: center; gap: 10rpx;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 16rpx 28rpx; font-size: 26rpx; color: var(--text);
|
||||
}
|
||||
.tgt--active { background: var(--gold-soft); border-color: var(--gold-line); color: var(--gold-2); }
|
||||
.tgt__count { font-size: 22rpx; color: var(--text-3); }
|
||||
.tgt--active .tgt__count { color: var(--gold); }
|
||||
|
||||
.confirm {
|
||||
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));
|
||||
}
|
||||
.confirm__bar { text-align: center; margin-bottom: 14rpx; }
|
||||
.confirm__sel { color: var(--gold-2); font-size: 26rpx; }
|
||||
.confirm__hint { color: var(--text-3); font-size: 26rpx; }
|
||||
.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;
|
||||
}
|
||||
.cta__arrow { margin-left: 12rpx; font-size: 36rpx; }
|
||||
|
||||
.result-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 20rpx; }
|
||||
.result-head__title { font-size: 36rpx; font-weight: 700; color: var(--text); }
|
||||
.result-head__count { font-size: 24rpx; color: var(--gold); }
|
||||
.rlist { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 80rpx 0; }
|
||||
48
miniprogram/pages/search/search.js
Normal file
48
miniprogram/pages/search/search.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
q: '',
|
||||
results: [],
|
||||
loading: false,
|
||||
searched: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
},
|
||||
|
||||
onInput(e) {
|
||||
const q = e.detail.value;
|
||||
this.setData({ q });
|
||||
this.debouncedSearch(q);
|
||||
},
|
||||
|
||||
debouncedSearch(q) {
|
||||
if (this._timer) clearTimeout(this._timer);
|
||||
this._timer = setTimeout(() => this.doSearch(q), 350);
|
||||
},
|
||||
|
||||
doSearch(q) {
|
||||
if (!q || !q.trim()) {
|
||||
this.setData({ results: [], searched: false, loading: false });
|
||||
return;
|
||||
}
|
||||
this.setData({ loading: true });
|
||||
svc.search(q.trim()).then((res) => {
|
||||
this.setData({
|
||||
results: (res && res.items) || [],
|
||||
searched: true,
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ searched: true, loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
}
|
||||
});
|
||||
7
miniprogram/pages/search/search.json
Normal file
7
miniprogram/pages/search/search.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
}
|
||||
}
|
||||
26
miniprogram/pages/search/search.wxml
Normal file
26
miniprogram/pages/search/search.wxml
Normal file
@@ -0,0 +1,26 @@
|
||||
<view class="page">
|
||||
<navbar title="搜索" />
|
||||
<view class="searchbar">
|
||||
<view class="searchbar__box">
|
||||
<text class="searchbar__icon">🔍</text>
|
||||
<input
|
||||
class="searchbar__input"
|
||||
placeholder="搜索动作 / 器械 / 部位"
|
||||
placeholder-class="ph"
|
||||
value="{{q}}"
|
||||
confirm-type="search"
|
||||
bindinput="onInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="grid" wx:if="{{results.length}}">
|
||||
<view class="grid__item" wx:for="{{results}}" wx:key="id">
|
||||
<exercise-card exercise="{{item}}" bind:tap="onExercise" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{loading}}" class="empty">搜索中…</view>
|
||||
<view wx:elif="{{searched}}" class="empty">未找到相关动作</view>
|
||||
<view wx:else class="empty">输入关键词开始搜索</view>
|
||||
</view>
|
||||
</view>
|
||||
14
miniprogram/pages/search/search.wxss
Normal file
14
miniprogram/pages/search/search.wxss
Normal file
@@ -0,0 +1,14 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.searchbar { padding: 12rpx 32rpx; }
|
||||
.searchbar__box {
|
||||
display: flex; align-items: center;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 20rpx 28rpx;
|
||||
}
|
||||
.searchbar__icon { font-size: 30rpx; margin-right: 16rpx; }
|
||||
.searchbar__input { flex: 1; color: var(--text); font-size: 28rpx; }
|
||||
.ph { color: var(--text-3); }
|
||||
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
.grid__item { width: calc((100% - 18rpx) / 2); }
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 120rpx 0; }
|
||||
Reference in New Issue
Block a user