feat: 器械分类网格化 + 分类页标题防乱码 + 按器械/部位多维度筛选
- 首页『热门器械』由横滑 chips 改为与『按部位浏览』一致的网格 - 分类页 navbar 标题改由本地分类枚举回写,避免外部传入标题乱码 - 分类页新增二级维度多选筛选:器械维度交叉按部位,其余维度交叉按器械 - 后端 exercises.service 支持 equipment/bodyPart 逗号分隔多值 OR 过滤(向后兼容)
This commit is contained in:
@@ -9,6 +9,11 @@ const DIM_MAP = {
|
||||
type: 'types'
|
||||
};
|
||||
|
||||
const SECONDARY_LABEL = {
|
||||
bodyPart: '部位',
|
||||
equipment: '器械'
|
||||
};
|
||||
|
||||
function labelForDim(d) {
|
||||
return ({
|
||||
bodyPart: '按部位',
|
||||
@@ -27,6 +32,12 @@ Page({
|
||||
value: '',
|
||||
values: [],
|
||||
activeValue: '',
|
||||
// 二级维度(多维度筛选):进入器械维度时交叉按部位,其余维度交叉按器械
|
||||
secondaryDim: 'equipment',
|
||||
secondaryKey: 'equipment',
|
||||
secondaryLabel: '器械',
|
||||
secondaryList: [],
|
||||
secondarySelected: [],
|
||||
exercises: [],
|
||||
loading: true,
|
||||
loadingMore: false,
|
||||
@@ -39,11 +50,15 @@ Page({
|
||||
onLoad(query) {
|
||||
const dimension = query.dimension || 'bodyPart';
|
||||
const value = query.value || '';
|
||||
const secondaryDim = dimension === 'equipment' ? 'bodyPart' : 'equipment';
|
||||
const title = query.title || labelForDim(dimension);
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 20,
|
||||
dimension,
|
||||
value,
|
||||
secondaryDim,
|
||||
secondaryKey: DIM_MAP[secondaryDim] || 'equipment',
|
||||
secondaryLabel: SECONDARY_LABEL[secondaryDim] || '器械',
|
||||
title
|
||||
});
|
||||
this.loadValues();
|
||||
@@ -52,14 +67,30 @@ Page({
|
||||
loadValues() {
|
||||
this.setData({ loading: true });
|
||||
const key = DIM_MAP[this.data.dimension] || 'body-parts';
|
||||
svc.getCategories(key).then((items) => {
|
||||
const secKey = this.data.secondaryKey;
|
||||
Promise.all([
|
||||
svc.getCategories(key),
|
||||
svc.getCategories(secKey).catch(() => [])
|
||||
]).then(([items, secItems]) => {
|
||||
const list = (items || []).slice();
|
||||
// 头部插入「全部」项:无 value 入参时默认选中,明确当前为「全部」
|
||||
// 头部插入「全部」项:明确当前为「全部」
|
||||
list.unshift({ value: '', label: '全部' });
|
||||
const activeValue = this.data.value || '';
|
||||
// 用分类枚举里的干净中文标签回写标题,避免外部传入的标题出现乱码
|
||||
let title = this.data.title;
|
||||
if (activeValue) {
|
||||
const hit = list.find((it) => it.value === activeValue);
|
||||
if (hit && hit.label) title = hit.label;
|
||||
}
|
||||
const secList = (secItems || []).slice().map((it) => ({
|
||||
...it,
|
||||
active: this.data.secondarySelected.includes(it.value)
|
||||
}));
|
||||
this.setData({
|
||||
values: list,
|
||||
activeValue: activeValue,
|
||||
secondaryList: secList,
|
||||
title,
|
||||
loading: false
|
||||
});
|
||||
this.loadExercises();
|
||||
@@ -68,10 +99,19 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
syncSecondaryActive() {
|
||||
const sel = this.data.secondarySelected;
|
||||
const list = this.data.secondaryList.map((it) => ({ ...it, active: sel.includes(it.value) }));
|
||||
this.setData({ secondaryList: list });
|
||||
},
|
||||
|
||||
loadExercises() {
|
||||
this.setData({ loading: true, page: 1 });
|
||||
const params = { page: 1, pageSize: this.data.pageSize };
|
||||
params[this.data.dimension] = this.data.activeValue;
|
||||
if (this.data.secondarySelected.length) {
|
||||
params[this.data.secondaryDim] = this.data.secondarySelected.join(',');
|
||||
}
|
||||
svc.listExercises(params).then((res) => {
|
||||
const items = (res && res.items) || [];
|
||||
const total = (res && res.total) || 0;
|
||||
@@ -96,6 +136,9 @@ Page({
|
||||
this.setData({ loadingMore: true });
|
||||
const params = { page: next, pageSize: this.data.pageSize };
|
||||
params[this.data.dimension] = this.data.activeValue;
|
||||
if (this.data.secondarySelected.length) {
|
||||
params[this.data.secondaryDim] = this.data.secondarySelected.join(',');
|
||||
}
|
||||
svc.listExercises(params).then((res) => {
|
||||
const items = (res && res.items) || [];
|
||||
const total = (res && res.total) || 0;
|
||||
@@ -118,6 +161,24 @@ Page({
|
||||
this.setData({ activeValue: value, page: 1 }, () => this.loadExercises());
|
||||
},
|
||||
|
||||
onSecondary(e) {
|
||||
const value = e.detail.value;
|
||||
if (!value) return;
|
||||
const cur = this.data.secondarySelected;
|
||||
const next = cur.includes(value) ? cur.filter((v) => v !== value) : cur.concat(value);
|
||||
this.setData({ secondarySelected: next, page: 1 }, () => {
|
||||
this.syncSecondaryActive();
|
||||
this.loadExercises();
|
||||
});
|
||||
},
|
||||
|
||||
onSecondaryClear() {
|
||||
this.setData({ secondarySelected: [], page: 1 }, () => {
|
||||
this.syncSecondaryActive();
|
||||
this.loadExercises();
|
||||
});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
if (!e || !e.detail || !e.detail.id) return;
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
|
||||
@@ -2,16 +2,35 @@
|
||||
<navbar title="{{title}}" show-back />
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<scroll-view wx:if="{{values.length}}" scroll-x class="filterbar" style="top: calc({{statusBarHeight}}px + 88rpx);">
|
||||
<chip
|
||||
wx:for="{{values}}"
|
||||
wx:key="value"
|
||||
label="{{item.label}}"
|
||||
value="{{item.value}}"
|
||||
active="{{activeValue === item.value}}"
|
||||
bind:select="onValue" />
|
||||
<view class="filterbar__spacer"></view>
|
||||
</scroll-view>
|
||||
<view wx:if="{{values.length}}" class="filterbar" style="top: calc({{statusBarHeight}}px + 88rpx);">
|
||||
<scroll-view scroll-x class="filterbar__row">
|
||||
<chip
|
||||
wx:for="{{values}}"
|
||||
wx:key="value"
|
||||
label="{{item.label}}"
|
||||
value="{{item.value}}"
|
||||
active="{{activeValue === item.value}}"
|
||||
bind:select="onValue" />
|
||||
<view class="filterbar__spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<view wx:if="{{secondaryList.length}}" class="filterbar__sub">
|
||||
<view class="filterbar__subhead">
|
||||
<text class="filterbar__sublabel">{{secondaryLabel}}</text>
|
||||
<text wx:if="{{secondarySelected.length}}" class="filterbar__clear" bindtap="onSecondaryClear">清除 ({{secondarySelected.length}})</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="filterbar__row">
|
||||
<chip
|
||||
wx:for="{{secondaryList}}"
|
||||
wx:key="value"
|
||||
label="{{item.label}}"
|
||||
value="{{item.value}}"
|
||||
active="{{item.active}}"
|
||||
bind:select="onSecondary" />
|
||||
<view class="filterbar__spacer"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="filterbar__loading muted">{{ loading ? '加载分类中…' : '暂无分类' }}</view>
|
||||
|
||||
<view class="count muted" wx:if="{{!loading}}">共 {{total}} 个动作</view>
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
.filterbar {
|
||||
position: sticky;
|
||||
z-index: 40;
|
||||
white-space: nowrap;
|
||||
padding: 8rpx 0 4rpx;
|
||||
padding: 8rpx 0 12rpx;
|
||||
background: var(--bg-grad);
|
||||
}
|
||||
.filterbar__row { white-space: nowrap; }
|
||||
.filterbar chip { display: inline-block; margin-right: 16rpx; }
|
||||
.filterbar__spacer { display: inline-block; width: 4rpx; }
|
||||
/* 二级维度(多维度筛选):主维度横列下方再叠一条横列,带标签与清除 */
|
||||
.filterbar__sub { margin-top: 12rpx; padding-top: 14rpx; border-top: 1rpx solid var(--line); }
|
||||
.filterbar__subhead { display: flex; align-items: center; justify-content: space-between; padding: 0 4rpx 10rpx; }
|
||||
.filterbar__sublabel { font-size: 24rpx; color: var(--text-2); font-weight: 600; }
|
||||
.filterbar__clear { font-size: 22rpx; color: var(--accent); }
|
||||
/* 加载 / 空态占位:横列区域常驻,避免加载期间整块消失 */
|
||||
.filterbar__loading {
|
||||
font-size: 24rpx;
|
||||
|
||||
@@ -124,10 +124,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门器械 -->
|
||||
<!-- 热门器械(网格,与按部位浏览一致) -->
|
||||
<view class="section" wx:if="{{equipment.length}}">
|
||||
<section-header eyebrow="EQUIPMENT" title="热门器械" subtitle="按器械筛选动作" />
|
||||
<scroll-view scroll-x class="chip-scroll">
|
||||
<section-header eyebrow="EQUIPMENT" title="按器械浏览" subtitle="选择你的训练器械" />
|
||||
<view class="eq-grid">
|
||||
<view
|
||||
wx:for="{{equipment}}"
|
||||
wx:key="value"
|
||||
@@ -135,10 +135,10 @@
|
||||
data-value="{{item.value}}"
|
||||
data-label="{{item.label}}"
|
||||
bindtap="onScanEquipment">
|
||||
<text class="eq__name">{{item.label}}</text>
|
||||
<text class="eq__count">{{item.count}}</text>
|
||||
<text class="eq__label">{{item.label}}</text>
|
||||
<text class="eq__count">{{item.count}} 个动作</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 160rpx;"></view>
|
||||
|
||||
@@ -100,12 +100,15 @@
|
||||
.bp__label { font-size: 26rpx; color: var(--text); font-weight: 600; }
|
||||
.bp__count { font-size: 20rpx; color: var(--text-3); margin-top: 6rpx; }
|
||||
|
||||
.eq-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.eq {
|
||||
display: inline-flex; align-items: center; gap: 12rpx;
|
||||
background: var(--surface-2); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius-pill); padding: 16rpx 28rpx; margin-right: 16rpx;
|
||||
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;
|
||||
}
|
||||
.eq__name { font-size: 26rpx; color: var(--text); }
|
||||
.eq__count { font-size: 22rpx; color: var(--accent); }
|
||||
.eq:active { background: var(--surface-2); }
|
||||
.eq__label { font-size: 26rpx; color: var(--text); font-weight: 600; }
|
||||
.eq__count { font-size: 20rpx; color: var(--text-3); margin-top: 6rpx; }
|
||||
|
||||
.empty { color: var(--text-3); font-size: var(--fs-sm); text-align: center; padding: 40rpx 0; }
|
||||
|
||||
Reference in New Issue
Block a user