feat(plan): 新增「加入计划」选择器页 + 增强官方计划浏览,修复返回不刷新

1) 新增 pages/plan-select(加入我的计划选择器,替换详情页 actionSheet)
   - 默认展示我全部计划(浏览,不再强制搜索),支持 最近创建/动作最多 排序 + 名称搜索
   - 点选即把动作加入该计划并返回;无计划时引导新建
   - 入口接在 detail 页「加入计划」(无 planId 时 navigate 至此)
2) 增强 pages/collection(官方训练计划)
   - 默认展示全部分级,新增 等级筛选 chips + 名称搜索(均用现有字段,不改后端)
   - 筛选/搜索纯前端,viewGroups 驱动渲染
3) 修复 plan-detail 从「添加动作」返回后列表不刷新(上轮已单独提交 7ff55a6)
4) app.json 注册 plan-select 页面
This commit is contained in:
2026-07-23 14:15:41 +08:00
parent 7ff55a6257
commit ff3a120b55
10 changed files with 282 additions and 33 deletions

View File

@@ -1,10 +1,15 @@
const app = getApp();
const planSvc = require('../../services/plan');
// 官方训练计划浏览:默认展示全部分级,支持按等级筛选 + 名称搜索(均用现有字段)。
Page({
data: {
statusBarHeight: 20,
groups: [],
viewGroups: [],
levels: [{ value: 'all', label: '全部' }],
filterLevel: 'all',
keyword: '',
loading: true
},
@@ -15,20 +20,44 @@ Page({
onShow() {
// 从详情返回(可能收藏/导入变化)时刷新
if (!this.data.loading) this.load();
if (!this.data.loading && !this.data.keyword && this.data.filterLevel === 'all') this.load();
},
load() {
this.setData({ loading: true });
planSvc.officialGroups().then((res) => {
this.setData({ groups: (res && res.groups) || [], loading: false });
const groups = (res && res.groups) || [];
const levels = [{ value: 'all', label: '全部' }].concat(
groups.map((g) => ({ value: g.level, label: g.label }))
);
this.setData({ groups, levels, loading: false });
this.applyFilter();
}).catch(() => {
this.setData({ groups: [], loading: false });
this.setData({ groups: [], viewGroups: [], levels: [{ value: 'all', label: '全部' }], loading: false });
});
},
// 计算某计划的跳转地址(供 wxml 使用)
planTarget(slug) {
return '/pages/collection-detail/collection-detail?slug=' + encodeURIComponent(slug);
applyFilter() {
const f = this.data.filterLevel;
const k = (this.data.keyword || '').trim().toLowerCase();
let groups = (this.data.groups || []).slice();
if (f !== 'all') groups = groups.filter((g) => g.level === f);
if (k) {
groups = groups
.map((g) => ({
...g,
plans: (g.plans || []).filter((p) => (p.name || '').toLowerCase().includes(k))
}))
.filter((g) => (g.plans || []).length);
}
this.setData({ viewGroups: groups });
},
onFilter(e) {
this.setData({ filterLevel: e.detail.value }, () => this.applyFilter());
},
onSearch(e) {
this.setData({ keyword: e.detail.value }, () => this.applyFilter());
}
});

View File

@@ -3,6 +3,8 @@
"usingComponents": {
"navbar": "/components/navbar/index",
"section-header": "/components/section-header/index",
"plan-card": "/components/plan-card/index"
"plan-card": "/components/plan-card/index",
"chip": "/components/chip/index",
"fc-icon": "/components/fc-icon/index"
}
}

View File

@@ -8,8 +8,33 @@
<view class="intro__sub">从新手入门到高级挑战,分级规划你的每一次进阶</view>
</view>
<block wx:if="{{groups.length}}">
<view class="group" wx:for="{{groups}}" wx:for-item="g" wx:key="level">
<view class="filterbar">
<scroll-view scroll-x class="filterbar__scroll">
<chip
wx:for="{{levels}}"
wx:key="value"
label="{{item.label}}"
value="{{item.value}}"
active="{{filterLevel === item.value}}"
bind:select="onFilter" />
</scroll-view>
</view>
<view class="searchbar">
<view class="searchbar__box">
<fc-icon name="search" size="28" color="var(--text-2)" />
<input
class="searchbar__input"
placeholder="搜索计划名称"
placeholder-class="ph"
value="{{keyword}}"
confirm-type="search"
bindinput="onSearch" />
</view>
</view>
<block wx:if="{{viewGroups.length}}">
<view class="group" wx:for="{{viewGroups}}" wx:for-item="g" wx:key="level">
<section-header eyebrow="LEVEL" title="{{g.label}}" />
<view class="grid">
<view class="grid__item" wx:for="{{g.plans}}" wx:for-item="p" wx:key="slug">
@@ -21,7 +46,7 @@
</view>
</block>
<view wx:elif="{{!loading}}" class="empty">暂无官方计划</view>
<view wx:elif="{{!loading}}" class="empty">没有匹配的计划</view>
<view wx:else class="empty">加载中…</view>
</view>

View File

@@ -8,4 +8,17 @@
.group + .group { margin-top: 36rpx; }
.grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
.grid__item { width: calc((100% - 18rpx) / 2); }
.filterbar { padding: 16rpx 32rpx 4rpx; }
.filterbar__scroll { white-space: nowrap; }
.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__input { flex: 1; color: var(--text); font-size: 28rpx; }
.ph { color: var(--text-3); }
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 80rpx 0; }