From 16621d9c07e89a10fafc4cb11daf70fed75a3b8e Mon Sep 17 00:00:00 2001 From: wm <2747639460@qq.com> Date: Sun, 2 Aug 2026 22:40:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(category):=20=E4=B8=BB=E5=88=86=E7=B1=BB(?= =?UTF-8?q?=E6=8C=89=E5=99=A8=E6=A2=B0/=E6=8C=89=E9=83=A8=E4=BD=8D)?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=A4=9A=E9=80=89;=20filter-bar=20=E5=86=85?= =?UTF-8?q?=E8=81=94=E9=BB=98=E8=AE=A4=E5=B1=95=E7=A4=BA=E5=87=A0=E4=B8=AA?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E5=85=B6=E4=BD=99=E6=94=B6=E8=BF=9B"?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E7=B1=BB=E5=9E=8B"=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/components/filter-bar/index.js | 108 ++++++++++--------- miniprogram/components/filter-bar/index.wxml | 14 ++- miniprogram/components/filter-bar/index.wxss | 8 ++ miniprogram/pages/category/category.js | 86 ++++++++------- miniprogram/pages/category/category.wxml | 32 +++--- 5 files changed, 126 insertions(+), 122 deletions(-) diff --git a/miniprogram/components/filter-bar/index.js b/miniprogram/components/filter-bar/index.js index e7110b9..d9c0318 100644 --- a/miniprogram/components/filter-bar/index.js +++ b/miniprogram/components/filter-bar/index.js @@ -1,20 +1,23 @@ -// filter-bar:分类筛选器 -// 设计目标:① 选中的项永远排在最前;② 内联条不横向滚动,超出部分收进「▾」下拉面板; -// ③ 单选(主维度)/ 多选(二级维度)两种模式。 -// 父级通过 items([{value,label,active}])传入全部分类;change 事件回传选中的 value / values。 +// filter-bar:分类筛选器(多选友好) +// 设计目标: +// ① 内联条「默认就展示几个分类」(选中优先,最多 PREVIEW 个),其余收进「全部类型」面板; +// ② 多选:内联 chip 直接切换选中态,面板内点选保持打开,选中项永远置顶; +// ③ value='' 的「全部」哨兵始终表示「无筛选」(空数组)。 +// 父级通过 items([{value,label,active}]) 传入全部分类(可含 value='' 的「全部」项); +// change 事件回传 { values: string[], value: string }(选中 value 数组,空数组=全部)。 Component({ properties: { items: { type: Array, value: [] }, - multiple: { type: Boolean, value: false }, + multiple: { type: Boolean, value: true }, title: { type: String, value: '分类' }, - placeholder: { type: String, value: '' } + placeholder: { type: String, value: '全部类型' } }, data: { panelOpen: false, - panelItems: [], // 选中置顶后的全量列表(供面板渲染) - visibleItems: [], // 内联收起态展示的「几个」选中项(单选最多 1,多选最多 3) - moreCount: 0 // 多选超出展示上限的剩余数量(内联 +N) + panelItems: [], + visibleItems: [], + moreCount: 0 }, observers: { @@ -25,36 +28,49 @@ Component({ methods: { recompute(items) { - // 选中置顶:active 的排前面,其余保持原序(面板展开后选中即在最前) - const list = items.slice().sort((a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0)); - const sel = list.filter((i) => i.active); - const MAX = 3; // 收起态最多展示几个选中项 - this.setData({ - panelItems: list, - visibleItems: sel.slice(0, MAX), - moreCount: Math.max(0, sel.length - MAX) + const reals = items.filter((i) => i.value !== ''); + const sel = reals.filter((i) => i.active); + const sorted = reals.slice().sort((a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0)); + const PREVIEW = 3; + const inline = sorted.slice(0, PREVIEW); + const overflow = Math.max(0, sel.length - PREVIEW); + // 面板:顶部放「全部」哨兵(仅当无任何具体选中时为 active),其余按选中优先 + const panel = [{ value: '', label: '全部', active: sel.length === 0 }].concat(sorted); + this.setData({ panelItems: panel, visibleItems: inline, moreCount: overflow }); + }, + + emitChange(items) { + const vals = items.filter((i) => i.active && i.value !== '').map((i) => i.value); + this.triggerEvent('change', { values: vals, value: vals[0] || '' }); + }, + + // 切换某项的选中态:选中具体项时自动取消「全部」;点「全部」则清空所有 + toggle(value, items) { + if (value === '') { + return items.map((i) => ({ ...i, active: i.value === '' })); + } + return items.map((i) => { + if (i.value === value) return { ...i, active: !i.active }; + if (i.value === '') return { ...i, active: false }; + return i; }); }, + // 内联 chip:多选直接切换;单选打开面板 + onChipTapInline(e) { + const value = e.currentTarget.dataset.value; + if (!this.data.multiple) { + this.togglePanel(); + return; + } + const next = this.toggle(value, this.data.items); + this.recompute(next); + this.emitChange(next); + }, + togglePanel() { this.setData({ panelOpen: !this.data.panelOpen }); }, - - // 内联收起态:只展示「几个」选中 chip + 末尾的「全部类型」按钮,不做横滑铺全部 - // 单选:点已选 chip 打开面板切换;多选:点已选 chip 直接移除 - onChipTapInline(e) { - const value = e.currentTarget.dataset.value; - if (this.data.multiple) { - const next = this.data.items.map((i) => - i.value === value ? { ...i, active: false } : i - ); - this.recompute(next); - const vals = next.filter((i) => i.active).map((i) => i.value); - this.triggerEvent('change', { values: vals }); - } else { - this.togglePanel(); - } - }, closePanel() { this.setData({ panelOpen: false }); }, @@ -62,30 +78,16 @@ Component({ onPanelTap(e) { const value = e.currentTarget.dataset.value; - if (this.data.multiple) { - // 多选:本地切换 active,回传全量已选 values,面板保持打开 - const next = this.data.items.map((i) => - i.value === value ? { ...i, active: !i.active } : i - ); + if (!this.data.multiple) { + const next = this.data.items.map((i) => ({ ...i, active: i.value === value })); this.recompute(next); - const vals = next.filter((i) => i.active).map((i) => i.value); - this.triggerEvent('change', { values: vals }); - } else { - // 单选:直接选中并关闭面板 - this.recompute(this.data.items.map((i) => ({ ...i, active: i.value === value }))); + this.emitChange(next); this.setData({ panelOpen: false }); - this.triggerEvent('change', { value }); + return; } - }, - - onTagRemove(e) { - const value = e.currentTarget.dataset.value; - const next = this.data.items.map((i) => - i.value === value ? { ...i, active: false } : i - ); + const next = this.toggle(value, this.data.items); this.recompute(next); - const vals = next.filter((i) => i.active).map((i) => i.value); - this.triggerEvent('change', { values: vals }); + this.emitChange(next); }, onDone() { diff --git a/miniprogram/components/filter-bar/index.wxml b/miniprogram/components/filter-bar/index.wxml index 51aa2fd..2028c10 100644 --- a/miniprogram/components/filter-bar/index.wxml +++ b/miniprogram/components/filter-bar/index.wxml @@ -1,6 +1,6 @@ - + {{item.label}} +{{moreCount}} - {{visibleItems.length ? '全部类型' : (placeholder || '全部类型')}} + {{placeholder || '全部类型'}} - + @@ -27,11 +27,9 @@ - {{item.label}} - + bindtap="onPanelTap">{{item.label}} 完成 diff --git a/miniprogram/components/filter-bar/index.wxss b/miniprogram/components/filter-bar/index.wxss index 9f2c227..5618829 100644 --- a/miniprogram/components/filter-bar/index.wxss +++ b/miniprogram/components/filter-bar/index.wxss @@ -98,6 +98,14 @@ border-color: var(--accent-ring); font-weight: var(--fw-semibold); } +.fb-opt--all { + border-color: var(--accent-ring); + color: var(--accent); +} +.fb-chip--more { + color: var(--text-2); + border-style: dashed; +} .fb-sheet__done { margin: 12rpx 24rpx 28rpx; padding: 22rpx; diff --git a/miniprogram/pages/category/category.js b/miniprogram/pages/category/category.js index d6148c2..a47ab17 100644 --- a/miniprogram/pages/category/category.js +++ b/miniprogram/pages/category/category.js @@ -29,9 +29,10 @@ Page({ statusBarHeight: 20, dimension: 'bodyPart', title: '分类', - value: '', + dimTitle: '分类', + // 主维度支持多选:activeValues 为已选 value 数组(空数组 = 全部) + activeValues: [], values: [], - activeValue: '', // 二级维度(多维度筛选):进入器械维度时交叉按部位,其余维度交叉按器械 secondaryDim: 'equipment', secondaryKey: 'equipment', @@ -50,12 +51,14 @@ Page({ onLoad(query) { const dimension = query.dimension || 'bodyPart'; const value = query.value || ''; + const activeValues = value ? [value] : []; const secondaryDim = dimension === 'equipment' ? 'bodyPart' : 'equipment'; const title = query.title || labelForDim(dimension); this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, dimension, - value, + activeValues, + dimTitle: labelForDim(dimension), secondaryDim, secondaryKey: DIM_MAP[secondaryDim] || 'equipment', secondaryLabel: SECONDARY_LABEL[secondaryDim] || '器械', @@ -75,20 +78,23 @@ Page({ const list = (items || []).slice(); // 头部插入「全部」项:明确当前为「全部」 list.unshift({ value: '', label: '全部' }); - const activeValue = this.data.value || ''; // 用分类枚举里的干净中文标签回写标题,避免外部传入的标题出现乱码 let title = this.data.title; - if (activeValue) { - const hit = list.find((it) => it.value === activeValue); + const { activeValues } = this.data; + if (activeValues.length === 1) { + const hit = list.find((it) => it.value === activeValues[0]); if (hit && hit.label) title = hit.label; + } else if (activeValues.length > 1) { + title = `已选 ${activeValues.length} 个`; } + // 主维度 items 带 active 标志(供 filter-bar 置顶/内联展示) + const primaryItems = list.map((it) => ({ ...it, active: activeValues.includes(it.value) })); const secList = (secItems || []).slice().map((it) => ({ ...it, active: this.data.secondarySelected.includes(it.value) })); this.setData({ - values: list, - activeValue: activeValue, + values: primaryItems, secondaryList: secList, title, loading: false @@ -99,20 +105,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 }); + buildParams(page) { + const params = { page, pageSize: this.data.pageSize }; + const av = this.data.activeValues; + if (av.length) params[this.data.dimension] = av.join(','); + if (this.data.secondarySelected.length) { + params[this.data.secondaryDim] = this.data.secondarySelected.join(','); + } + return params; }, 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) => { + svc.listExercises(this.buildParams(1)).then((res) => { const items = (res && res.items) || []; const total = (res && res.total) || 0; this.setData({ @@ -134,12 +139,7 @@ Page({ loadMore() { const next = this.data.page + 1; 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) => { + svc.listExercises(this.buildParams(next)).then((res) => { const items = (res && res.items) || []; const total = (res && res.total) || 0; const merged = this.data.exercises.concat(items); @@ -155,28 +155,32 @@ Page({ }); }, - onValue(e) { - const value = e.detail.value; - if (value === this.data.activeValue) return; - this.setData({ activeValue: value, page: 1 }, () => this.loadExercises()); + // 主维度(按器械/按部位)多选 + onPrimaryChange(e) { + const vals = e.detail.values || []; + const values = this.data.values.map((it) => ({ ...it, active: vals.includes(it.value) })); + let title = this.data.title; + if (vals.length === 1) { + const hit = values.find((it) => it.value === vals[0]); + title = (hit && hit.label) || labelForDim(this.data.dimension); + } else if (vals.length > 1) { + title = `已选 ${vals.length} 个`; + } else { + title = labelForDim(this.data.dimension); + } + this.setData({ activeValues: vals, values, title, 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(); - }); + // 二级维度(部位/器械)多选 + onSecondaryChange(e) { + const values = e.detail.values || []; + const secondaryList = this.data.secondaryList.map((it) => ({ ...it, active: values.includes(it.value) })); + this.setData({ secondarySelected: values, secondaryList, page: 1 }, () => this.loadExercises()); }, onSecondaryClear() { - this.setData({ secondarySelected: [], page: 1 }, () => { - this.syncSecondaryActive(); - this.loadExercises(); - }); + const secondaryList = this.data.secondaryList.map((it) => ({ ...it, active: false })); + this.setData({ secondarySelected: [], secondaryList, page: 1 }, () => this.loadExercises()); }, onExercise(e) { diff --git a/miniprogram/pages/category/category.wxml b/miniprogram/pages/category/category.wxml index fbf3886..a6d7297 100644 --- a/miniprogram/pages/category/category.wxml +++ b/miniprogram/pages/category/category.wxml @@ -3,32 +3,24 @@ - - - - + {{secondaryLabel}} 清除 ({{secondarySelected.length}}) - - - - + {{ loading ? '加载分类中…' : '暂无分类' }}