diff --git a/backend/src/exercises/exercises.service.ts b/backend/src/exercises/exercises.service.ts index bdcce3f..08b716c 100644 --- a/backend/src/exercises/exercises.service.ts +++ b/backend/src/exercises/exercises.service.ts @@ -46,8 +46,15 @@ export class ExercisesService implements OnModuleInit { findAll(query: QueryExercisesDto) { let list = this.exercises; - if (query.bodyPart) list = list.filter((e) => e.bodyPart === query.bodyPart); - if (query.equipment) list = list.filter((e) => e.equipment === query.equipment); + // 支持逗号分隔多值(OR 关系),向后兼容单值 + if (query.bodyPart) { + const arr = String(query.bodyPart).split(',').map((s) => s.trim()).filter(Boolean); + if (arr.length) list = list.filter((e) => arr.includes(e.bodyPart)); + } + if (query.equipment) { + const arr = String(query.equipment).split(',').map((s) => s.trim()).filter(Boolean); + if (arr.length) list = list.filter((e) => arr.includes(e.equipment)); + } if (query.type) list = list.filter((e) => e.type === query.type); if (query.target) list = list.filter((e) => e.target === query.target); if (query.muscleGroup) diff --git a/miniprogram/pages/category/category.js b/miniprogram/pages/category/category.js index 4ae43b2..d6148c2 100644 --- a/miniprogram/pages/category/category.js +++ b/miniprogram/pages/category/category.js @@ -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) }); diff --git a/miniprogram/pages/category/category.wxml b/miniprogram/pages/category/category.wxml index a9441d5..fbf3886 100644 --- a/miniprogram/pages/category/category.wxml +++ b/miniprogram/pages/category/category.wxml @@ -2,16 +2,35 @@ - - - - + + + + + + + + + {{secondaryLabel}} + 清除 ({{secondarySelected.length}}) + + + + + + + {{ loading ? '加载分类中…' : '暂无分类' }} 共 {{total}} 个动作 diff --git a/miniprogram/pages/category/category.wxss b/miniprogram/pages/category/category.wxss index 7d95a75..30711df 100644 --- a/miniprogram/pages/category/category.wxss +++ b/miniprogram/pages/category/category.wxss @@ -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; diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 33b5ee2..7b68e5a 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -124,10 +124,10 @@ - + - - + + - {{item.label}} - {{item.count}} + {{item.label}} + {{item.count}} 个动作 - + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index 6404334..b3d8e94 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -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; }