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:
@@ -13,6 +13,7 @@
|
||||
"pages/my-plans/my-plans",
|
||||
"pages/plan-edit/plan-edit",
|
||||
"pages/plan-detail/plan-detail",
|
||||
"pages/plan-select/plan-select",
|
||||
"pages/plan-favorites/plan-favorites"
|
||||
],
|
||||
"window": {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -68,7 +68,9 @@ Page({
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
// 底部主按钮:加入计划(真实选择器)
|
||||
// 底部主按钮:加入计划
|
||||
// - 若从某计划内进入(带 planId),直接加入该计划
|
||||
// - 否则进入「加入计划」选择器页,可浏览全部计划并筛选
|
||||
onAdd() {
|
||||
if (this.data.planId) {
|
||||
planSvc.addExercise(this.data.planId, this.data.id).then(() => {
|
||||
@@ -77,28 +79,13 @@ Page({
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
auth.ensureLogin().then(() => planSvc.listMine()).then((res) => {
|
||||
const plans = (res && res.items) || res || [];
|
||||
if (!plans.length) {
|
||||
wx.showModal({
|
||||
title: '还没有训练计划',
|
||||
content: '先创建一个计划,再把动作加进去吧~',
|
||||
confirmText: '去创建',
|
||||
cancelText: '稍后',
|
||||
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/plan-edit/plan-edit' }); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
const names = plans.slice(0, 6).map((p) => p.name);
|
||||
wx.showActionSheet({
|
||||
itemList: names,
|
||||
success: (sheet) => {
|
||||
const plan = plans[sheet.tapIndex];
|
||||
if (!plan) return;
|
||||
planSvc.addExercise(plan.id, this.data.id).then(() => {
|
||||
wx.showToast({ title: '已加入「' + plan.name + '」', icon: 'success' });
|
||||
}).catch(() => {});
|
||||
}
|
||||
auth.ensureLogin().then(() => {
|
||||
const name = this.data.exercise
|
||||
? (this.data.exercise.displayName || this.data.exercise.name)
|
||||
: '';
|
||||
wx.navigateTo({
|
||||
url: '/pages/plan-select/plan-select?exerciseId=' + encodeURIComponent(this.data.id) +
|
||||
'&name=' + encodeURIComponent(name || '')
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
92
miniprogram/pages/plan-select/plan-select.js
Normal file
92
miniprogram/pages/plan-select/plan-select.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
const auth = require('../../utils/auth');
|
||||
const { iconName } = require('../../utils/icon-name');
|
||||
|
||||
// 加入计划选择器:从动作详情「加入计划」进入,默认展示我全部计划,
|
||||
// 支持 最近创建 / 动作最多 排序与名称搜索,点选即把动作加入该计划并返回。
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
exerciseId: '',
|
||||
exerciseName: '',
|
||||
allPlans: [],
|
||||
plans: [],
|
||||
filter: 'all', // 'all' | 'recent' | 'count'
|
||||
keyword: '',
|
||||
status: 'loading', // 'loading' | 'list' | 'empty' | 'error'
|
||||
errorMsg: ''
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 20,
|
||||
exerciseId: query.exerciseId || '',
|
||||
exerciseName: query.name || ''
|
||||
});
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (auth.isLogin()) this.load();
|
||||
else {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => wx.navigateBack(), 600);
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ status: 'loading', errorMsg: '' });
|
||||
planSvc.listMine().then((res) => {
|
||||
const raw = (res && res.items) || res || [];
|
||||
const plans = raw.map((p) => ({ ...p, icon: iconName(p.emoji, 'folder-open') }));
|
||||
this.setData({ allPlans: plans, status: plans.length ? 'list' : 'empty' });
|
||||
this.applyFilter();
|
||||
}).catch((err) => {
|
||||
const status = err && err.statusCode;
|
||||
const msg = status === 401 ? '登录已失效,请重新登录' : '加载失败,请检查网络后重试';
|
||||
this.setData({ status: 'error', errorMsg: msg });
|
||||
});
|
||||
},
|
||||
|
||||
// 排序 + 名称搜索(纯前端,基于已有字段 createdAt / exerciseCount / name)
|
||||
applyFilter() {
|
||||
const f = this.data.filter;
|
||||
let list = (this.data.allPlans || []).slice();
|
||||
const k = (this.data.keyword || '').trim().toLowerCase();
|
||||
if (k) list = list.filter((p) => (p.name || '').toLowerCase().includes(k));
|
||||
if (f === 'recent') list.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
||||
else if (f === 'count') list.sort((a, b) => b.exerciseCount - a.exerciseCount);
|
||||
this.setData({ plans: list });
|
||||
},
|
||||
|
||||
onFilter(e) {
|
||||
this.setData({ filter: e.detail.value }, () => this.applyFilter());
|
||||
},
|
||||
|
||||
onSearch(e) {
|
||||
this.setData({ keyword: e.detail.value }, () => this.applyFilter());
|
||||
},
|
||||
|
||||
onSelect(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const name = e.currentTarget.dataset.name || '';
|
||||
if (!id || !this.data.exerciseId) return;
|
||||
wx.showLoading({ title: '加入中…', mask: true });
|
||||
planSvc.addExercise(id, this.data.exerciseId).then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: name ? ('已加入「' + name + '」') : '已加入计划', icon: 'success' });
|
||||
setTimeout(() => wx.navigateBack(), 400);
|
||||
}).catch(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '加入失败,请重试', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
wx.navigateTo({ url: '/pages/plan-edit/plan-edit' });
|
||||
},
|
||||
|
||||
onRetry() {
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
9
miniprogram/pages/plan-select/plan-select.json
Normal file
9
miniprogram/pages/plan-select/plan-select.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"chip": "/components/chip/index",
|
||||
"plan-card": "/components/plan-card/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
56
miniprogram/pages/plan-select/plan-select.wxml
Normal file
56
miniprogram/pages/plan-select/plan-select.wxml
Normal file
@@ -0,0 +1,56 @@
|
||||
<view class="page">
|
||||
<navbar title="加入计划" show-back />
|
||||
|
||||
<view class="hint" wx:if="{{exerciseName}}">将动作「{{exerciseName}}」加入以下计划:</view>
|
||||
|
||||
<view class="filterbar">
|
||||
<scroll-view scroll-x class="filterbar__scroll">
|
||||
<chip label="全部" value="all" active="{{filter === 'all'}}" bind:select="onFilter" />
|
||||
<chip label="最近创建" value="recent" active="{{filter === 'recent'}}" bind:select="onFilter" />
|
||||
<chip label="动作最多" value="count" active="{{filter === 'count'}}" 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>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view wx:if="{{status === 'list'}}" class="grid">
|
||||
<view
|
||||
class="grid__item"
|
||||
wx:for="{{plans}}"
|
||||
wx:key="id"
|
||||
bindtap="onSelect"
|
||||
data-id="{{item.id}}"
|
||||
data-name="{{item.name}}">
|
||||
<plan-card plan="{{item}}" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{status === 'empty'}}" class="empty-box">
|
||||
<view class="empty-box__emoji"><fc-icon name="clipboard-list" size="64" color="var(--text-3)" /></view>
|
||||
<view class="empty-box__t">还没有计划,先创建一个吧</view>
|
||||
<view class="empty-box__btn" bindtap="onCreate">+ 新建计划</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{status === 'error'}}" class="empty-box">
|
||||
<view class="empty-box__emoji"><fc-icon name="alert-circle" size="64" color="var(--danger)" /></view>
|
||||
<view class="empty-box__t">{{errorMsg}}</view>
|
||||
<view class="empty-box__btn" bindtap="onRetry">重新加载</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 60rpx;"></view>
|
||||
</view>
|
||||
35
miniprogram/pages/plan-select/plan-select.wxss
Normal file
35
miniprogram/pages/plan-select/plan-select.wxss
Normal file
@@ -0,0 +1,35 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
|
||||
.hint {
|
||||
padding: 16rpx 36rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.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); }
|
||||
|
||||
.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: 80rpx 0; }
|
||||
|
||||
.empty-box { display: flex; flex-direction: column; align-items: center; padding: 100rpx 40rpx; }
|
||||
.empty-box__emoji {
|
||||
width: 160rpx; height: 160rpx; border-radius: 36rpx;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.empty-box__t { font-size: 30rpx; color: var(--text-2); margin-top: 36rpx; text-align: center; }
|
||||
.empty-box__btn {
|
||||
margin-top: 44rpx; padding: 24rpx 88rpx; border-radius: 999rpx;
|
||||
background: var(--gold); color: var(--accent-on); font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user