feat: 小程序连接服务器IP + 同步未提交开发
- miniprogram/config.js: BASE_URL 指向 http://192.227.237.8:3001 - 动作标题中文化(nameZh),搜索匹配 name/nameZh/nameEn - 新增计划/收藏/我的 等页面与组件 - 后端 Docker 化(Dockerfile/.dockerignore)与翻译脚本 - .gitignore 补充 .DS_Store
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
const planSvc = require('../../services/plan');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const auth = require('../../utils/auth');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -7,24 +9,25 @@ Page({
|
||||
slug: '',
|
||||
collection: null,
|
||||
exercises: [],
|
||||
total: 0,
|
||||
loading: true,
|
||||
page: 1,
|
||||
pageSize: 30,
|
||||
total: 0
|
||||
favorited: false
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const slug = query.slug || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, slug });
|
||||
this.load();
|
||||
if (auth.isLogin()) {
|
||||
favSvc.planStatus(slug).then((r) => {
|
||||
this.setData({ favorited: !!(r && r.favorited) });
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
svc.getCollectionExercises(this.data.slug, {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize
|
||||
}).then((res) => {
|
||||
planSvc.officialExercises(this.data.slug, 1, 60).then((res) => {
|
||||
this.setData({
|
||||
collection: res.collection,
|
||||
exercises: (res && res.items) || [],
|
||||
@@ -36,7 +39,39 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
// 收藏 / 取消收藏官方计划
|
||||
onToggleFav() {
|
||||
auth.ensureLogin().then(() => {
|
||||
const slug = this.data.slug;
|
||||
const wasFav = this.data.favorited;
|
||||
const op = wasFav ? favSvc.removePlan(slug) : favSvc.addPlan(slug);
|
||||
op.then(() => {
|
||||
this.setData({ favorited: !wasFav });
|
||||
wx.showToast({ title: wasFav ? '已取消收藏' : '已收藏', icon: 'none' });
|
||||
}).catch(() => {});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
// 导入到我的计划
|
||||
onImport() {
|
||||
auth.ensureLogin().then(() => {
|
||||
wx.showLoading({ title: '导入中…', mask: true });
|
||||
planSvc.importOfficial(this.data.slug).then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已导入到我的计划', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({ url: '/pages/my-plans/my-plans' });
|
||||
}, 600);
|
||||
}).catch(() => {
|
||||
wx.hideLoading();
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
wx.navigateTo({
|
||||
url: '/pages/detail/detail?from=collection&slug=' +
|
||||
encodeURIComponent(this.data.slug) + '&id=' + encodeURIComponent(e.detail.id)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index",
|
||||
"level-tag": "/components/level-tag/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,42 @@
|
||||
|
||||
<view class="header" style="background: linear-gradient(135deg, {{collection.color}}26, var(--surface));">
|
||||
<view class="header__emoji" style="background: {{collection.color}}22; color: {{collection.color}};">{{collection.emoji}}</view>
|
||||
<view class="header__name">{{collection.name}}</view>
|
||||
<view class="header__name" style="color: {{collection.color}};">{{collection.name}}</view>
|
||||
<view wx:if="{{collection.nameEn}}" class="header__en">{{collection.nameEn}}</view>
|
||||
<view class="header__desc">{{collection.description}}</view>
|
||||
<view class="header__count" style="color: {{collection.color}};">共 {{total}} 个动作</view>
|
||||
|
||||
<view class="header__meta">
|
||||
<level-tag level="{{collection.level}}" />
|
||||
<text class="header__sep">·</text>
|
||||
<text class="header__m" wx:if="{{collection.sessionsPerWeek}}">每周 {{collection.sessionsPerWeek}} 次</text>
|
||||
<text class="header__sep" wx:if="{{collection.sessionsPerWeek && collection.durationWeeks}}">·</text>
|
||||
<text class="header__m" wx:if="{{collection.durationWeeks}}">约 {{collection.durationWeeks}} 周</text>
|
||||
<text class="header__sep" wx:if="{{collection.focus}}">·</text>
|
||||
<text class="header__m" wx:if="{{collection.focus}}">{{collection.focus}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<section-header eyebrow="EXERCISES" title="动作列表" subtitle="共 {{total}} 个动作" />
|
||||
<view class="grid" wx:if="{{exercises.length}}">
|
||||
<view class="grid__item" wx:for="{{exercises}}" wx:key="id">
|
||||
<exercise-card exercise="{{item}}" bind:tap="onExercise" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty">暂无数据</view>
|
||||
<view wx:elif="{{!loading}}" class="empty">该计划暂无动作</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view class="actionbar">
|
||||
<view class="btn btn--ghost {{favorited ? 'btn--faved' : ''}}" bindtap="onToggleFav">
|
||||
<text>{{favorited ? '♥ 已收藏' : '♡ 收藏'}}</text>
|
||||
</view>
|
||||
<view class="btn btn--gold" bindtap="onImport">
|
||||
<text>+ 导入我的计划</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty-page">
|
||||
<view class="empty">{{ loading ? '加载中…' : '未找到该训练组合' }}</view>
|
||||
<view class="empty">{{ loading ? '加载中…' : '未找到该训练计划' }}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); padding-bottom: 60rpx; }
|
||||
.page { min-height: 100vh; background: var(--bg-grad); padding-bottom: 160rpx; }
|
||||
.header { padding: 32rpx; border-bottom: 1rpx solid var(--line); }
|
||||
.header__emoji {
|
||||
width: 120rpx; height: 120rpx; border-radius: 28rpx;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 64rpx;
|
||||
}
|
||||
.header__name { font-size: 44rpx; font-weight: 700; margin-top: 20rpx; }
|
||||
.header__en { font-size: 24rpx; color: var(--text-3); margin-top: 6rpx; letter-spacing: 1rpx; }
|
||||
.header__desc { font-size: 26rpx; color: var(--text-2); margin-top: 12rpx; line-height: 1.5; }
|
||||
.header__count { font-size: 24rpx; margin-top: 16rpx; }
|
||||
.header__meta { display: flex; align-items: center; flex-wrap: wrap; gap: 12rpx; margin-top: 20rpx; }
|
||||
.header__sep { color: var(--text-3); font-size: 22rpx; }
|
||||
.header__m { font-size: 24rpx; color: var(--text-2); }
|
||||
|
||||
.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: 120rpx 0; }
|
||||
.empty-page { min-height: 100vh; background: var(--bg-grad); }
|
||||
|
||||
.actionbar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
display: flex; gap: 20rpx; padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.btn {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 999rpx; padding: 26rpx; font-size: 30rpx; font-weight: 700;
|
||||
}
|
||||
.btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: #0E1110; }
|
||||
.btn--ghost { background: var(--surface-2); color: var(--text); border: 1rpx solid var(--gold-line); }
|
||||
.btn--faved { color: var(--gold-2); border-color: var(--gold); }
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
collections: [],
|
||||
counts: {},
|
||||
groups: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
@@ -14,27 +13,22 @@ Page({
|
||||
this.load();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 从详情返回(可能收藏/导入变化)时刷新
|
||||
if (!this.data.loading) this.load();
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
svc.listCollections().then((list) => {
|
||||
const collections = list || [];
|
||||
this.setData({ collections, loading: false });
|
||||
// 异步获取每个训练组合的动作数量
|
||||
collections.forEach(c => {
|
||||
svc.getCollectionExercises(c.slug, { pageSize: 1 }).then((res) => {
|
||||
const key = 'counts.' + c.slug;
|
||||
this.setData({ [key]: (res && res.total) || 0 });
|
||||
}).catch(() => {});
|
||||
});
|
||||
planSvc.officialGroups().then((res) => {
|
||||
this.setData({ groups: (res && res.groups) || [], loading: false });
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
this.setData({ groups: [], loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onOpen(e) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/collection-detail/collection-detail?slug=' +
|
||||
encodeURIComponent(e.currentTarget.dataset.slug)
|
||||
});
|
||||
// 计算某计划的跳转地址(供 wxml 使用)
|
||||
planTarget(slug) {
|
||||
return '/pages/collection-detail/collection-detail?slug=' + encodeURIComponent(slug);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
"section-header": "/components/section-header/index",
|
||||
"plan-card": "/components/plan-card/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
<view class="page">
|
||||
<navbar title="训练计划" />
|
||||
<navbar title="官方训练计划" show-back />
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="intro">
|
||||
<view class="intro__eyebrow">PROGRAMS</view>
|
||||
<view class="intro__title">挑选你的训练主题</view>
|
||||
<view class="intro__sub">从推、拉、腿到居家无器械,一键开启</view>
|
||||
<view class="intro__eyebrow">OFFICIAL PROGRAMS</view>
|
||||
<view class="intro__title">选择你的训练阶段</view>
|
||||
<view class="intro__sub">从新手入门到高级挑战,分级规划你的每一次进阶</view>
|
||||
</view>
|
||||
|
||||
<view class="list" wx:if="{{collections.length}}">
|
||||
<view
|
||||
wx:for="{{collections}}"
|
||||
wx:key="slug"
|
||||
class="col-card"
|
||||
style="background: linear-gradient(135deg, {{item.color}}26, var(--surface)); border-color: {{item.color}}55;"
|
||||
data-slug="{{item.slug}}"
|
||||
bindtap="onOpen">
|
||||
<view class="col-card__emoji" style="background: {{item.color}}22;">{{item.emoji}}</view>
|
||||
<view class="col-card__main">
|
||||
<view class="col-card__name">{{item.name}}</view>
|
||||
<view class="col-card__desc">{{item.description}}</view>
|
||||
<block wx:if="{{groups.length}}">
|
||||
<view class="group" wx:for="{{groups}}" 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">
|
||||
<plan-card
|
||||
plan="{{p}}"
|
||||
target="/pages/collection-detail/collection-detail?slug={{p.slug}}" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="col-card__count" style="color: {{item.color}};">{{counts[item.slug] || ''}} 个动作 ›</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty">暂无数据</view>
|
||||
</block>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty">暂无官方计划</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="plan" />
|
||||
|
||||
<view style="height: 60rpx;"></view>
|
||||
</view>
|
||||
|
||||
@@ -4,19 +4,8 @@
|
||||
.intro__title { font-size: 44rpx; font-weight: 700; margin-top: 12rpx; }
|
||||
.intro__sub { font-size: 26rpx; color: var(--text-2); margin-top: 12rpx; }
|
||||
|
||||
.list { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.col-card {
|
||||
display: flex; align-items: center; gap: 24rpx;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 28rpx;
|
||||
}
|
||||
.col-card__emoji {
|
||||
width: 96rpx; height: 96rpx; border-radius: 24rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 52rpx; flex: 0 0 auto;
|
||||
}
|
||||
.col-card__main { flex: 1; min-width: 0; }
|
||||
.col-card__name { font-size: 34rpx; font-weight: 700; color: var(--text); }
|
||||
.col-card__desc { font-size: 24rpx; color: var(--text-2); margin-top: 8rpx; line-height: 1.4; }
|
||||
.col-card__count { font-size: 24rpx; flex: 0 0 auto; }
|
||||
.group { margin-top: 12rpx; }
|
||||
.group + .group { margin-top: 36rpx; }
|
||||
.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; }
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const planSvc = require('../../services/plan');
|
||||
const auth = require('../../utils/auth');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
id: '',
|
||||
planId: '',
|
||||
exercise: null,
|
||||
heroSrc: '',
|
||||
heroError: false,
|
||||
related: [],
|
||||
loading: true
|
||||
loading: true,
|
||||
favorited: false
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const id = query.id || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, id });
|
||||
const planId = query.planId || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, id, planId });
|
||||
this.load();
|
||||
if (auth.isLogin()) {
|
||||
favSvc.exerciseStatus(id).then((r) => {
|
||||
this.setData({ favorited: !!(r && r.favorited) });
|
||||
}).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
svc.getExercise(this.data.id).then((ex) => {
|
||||
this.setData({ exercise: ex, loading: false });
|
||||
// 详情页用 GIF 演示动作;记录当前图源以便失败时降级到 JPG / 占位
|
||||
const heroSrc = ex && (ex.gifUrl || ex.image) ? (ex.gifUrl || ex.image) : '';
|
||||
this.setData({ exercise: ex, heroSrc, heroError: false, loading: false });
|
||||
if (ex && ex.target) {
|
||||
svc.listExercises({ target: ex.target, pageSize: 6 }).then((res) => {
|
||||
const related = ((res && res.items) || [])
|
||||
@@ -33,8 +48,52 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
// 收藏 / 取消收藏(心形按钮)
|
||||
onToggleFav() {
|
||||
auth.ensureLogin().then(() => {
|
||||
const id = this.data.id;
|
||||
const wasFav = this.data.favorited;
|
||||
const op = wasFav ? favSvc.removeExercise(id) : favSvc.addExercise(id);
|
||||
op.then(() => {
|
||||
this.setData({ favorited: !wasFav });
|
||||
wx.showToast({ title: wasFav ? '已取消收藏' : '已收藏', icon: 'none' });
|
||||
}).catch(() => {});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
// 底部主按钮:加入计划(真实选择器)
|
||||
onAdd() {
|
||||
wx.showToast({ title: '已加入今日训练', icon: 'success' });
|
||||
if (this.data.planId) {
|
||||
planSvc.addExercise(this.data.planId, this.data.id).then(() => {
|
||||
wx.showToast({ title: '已加入计划', icon: 'success' });
|
||||
setTimeout(() => wx.navigateBack(), 500);
|
||||
}).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(() => {});
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
onBack() {
|
||||
@@ -46,14 +105,26 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 详情大图加载失败:GIF→JPG→emoji 占位,三级降级保证不空白
|
||||
onHeroError() {
|
||||
const ex = this.data.exercise || {};
|
||||
const showingGif = this.data.heroSrc && ex.gifUrl && this.data.heroSrc === ex.gifUrl;
|
||||
if (showingGif && ex.image) {
|
||||
this.setData({ heroSrc: ex.image });
|
||||
} else {
|
||||
this.setData({ heroError: true });
|
||||
}
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ex = this.data.exercise || {};
|
||||
const name = ex.displayName || ex.name || '';
|
||||
return {
|
||||
title: ex.name ? ('FITCOACH · ' + ex.name) : 'FITCOACH 智能健身教练',
|
||||
title: name ? ('FITCOACH · ' + name) : 'FITCOACH 智能健身教练',
|
||||
path: '/pages/detail/detail?id=' + this.data.id
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<view class="page" wx:if="{{exercise}}">
|
||||
<navbar title="{{exercise.name}}" show-back />
|
||||
<navbar title="{{exercise.displayName || exercise.name}}" show-back />
|
||||
|
||||
<view class="favbtn" style="top: {{statusBarHeight + 12}}px;" bindtap="onToggleFav">
|
||||
<text>{{favorited ? '♥' : '♡'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="hero">
|
||||
<image
|
||||
wx:if="{{exercise.gifUrl || exercise.image}}"
|
||||
wx:if="{{heroSrc && !heroError}}"
|
||||
class="hero__img"
|
||||
src="{{exercise.gifUrl || exercise.image}}"
|
||||
mode="aspectFill" />
|
||||
src="{{heroSrc}}"
|
||||
mode="aspectFill"
|
||||
lazy-load="true"
|
||||
binderror="onHeroError" />
|
||||
<view wx:else class="hero__ph">🏋️</view>
|
||||
</view>
|
||||
|
||||
<view class="body section">
|
||||
<view class="name">{{exercise.name}}</view>
|
||||
<view class="name">{{exercise.displayName || exercise.name}}</view>
|
||||
|
||||
<view class="metas">
|
||||
<view class="meta"><view class="meta__k">部位</view><view class="meta__v">{{exercise.bodyPartLabel}}</view></view>
|
||||
@@ -60,7 +66,7 @@
|
||||
</view>
|
||||
|
||||
<view class="addbar">
|
||||
<view class="cta" bindtap="onAdd">加入今日训练</view>
|
||||
<view class="cta" bindtap="onAdd">{{ planId ? '加入该计划' : '加入计划' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -55,3 +55,13 @@
|
||||
.topbar { height: 88rpx; display: flex; align-items: center; padding: 0 24rpx; }
|
||||
.topbar__back { font-size: 56rpx; color: var(--gold); width: 60rpx; line-height: 1; }
|
||||
.empty { color: var(--text-3); font-size: 28rpx; text-align: center; padding: 120rpx 0; }
|
||||
|
||||
.favbtn {
|
||||
position: fixed; right: 28rpx; z-index: 60;
|
||||
width: 72rpx; height: 72rpx; border-radius: 999rpx;
|
||||
background: rgba(14,17,16,0.55);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1rpx solid var(--gold-line);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 44rpx; color: var(--gold-2); line-height: 1;
|
||||
}
|
||||
|
||||
55
miniprogram/pages/favorites/favorites.js
Normal file
55
miniprogram/pages/favorites/favorites.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const app = getApp();
|
||||
const favSvc = require('../../services/favorite');
|
||||
const auth = require('../../utils/auth');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
list: [],
|
||||
loading: true,
|
||||
managing: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (auth.isLogin()) this.load();
|
||||
else {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => wx.navigateBack(), 600);
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
favSvc.listExercises().then((list) => {
|
||||
this.setData({ list: list || [], loading: false });
|
||||
}).catch(() => {
|
||||
this.setData({ list: [], loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onManage() {
|
||||
this.setData({ managing: !this.data.managing });
|
||||
},
|
||||
|
||||
onRemove(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
favSvc.removeExercise(id).then(() => {
|
||||
const list = this.data.list.filter(i => i.id !== id);
|
||||
this.setData({ list });
|
||||
wx.showToast({ title: '已移除', icon: 'none' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
if (this.data.managing) return;
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
},
|
||||
|
||||
goRecommend() {
|
||||
wx.navigateTo({ url: '/pages/recommend/recommend' });
|
||||
}
|
||||
});
|
||||
7
miniprogram/pages/favorites/favorites.json
Normal file
7
miniprogram/pages/favorites/favorites.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index"
|
||||
}
|
||||
}
|
||||
28
miniprogram/pages/favorites/favorites.wxml
Normal file
28
miniprogram/pages/favorites/favorites.wxml
Normal file
@@ -0,0 +1,28 @@
|
||||
<view class="page">
|
||||
<navbar title="我的收藏" show-back />
|
||||
|
||||
<view class="bar">
|
||||
<view class="bar__count">{{list.length}} 个收藏动作</view>
|
||||
<view wx:if="{{list.length}}" class="bar__manage" bindtap="onManage">{{managing ? '完成' : '管理'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="grid" wx:if="{{list.length}}">
|
||||
<view class="grid__item" wx:for="{{list}}" wx:key="id">
|
||||
<view class="item">
|
||||
<exercise-card exercise="{{item}}" bind:tap="onExercise" />
|
||||
<view wx:if="{{managing}}" class="remove" data-id="{{item.id}}" catchtap="onRemove">移除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">🔍</view>
|
||||
<view class="empty-box__t">还没有收藏,去发现喜欢的动作吧</view>
|
||||
<view class="empty-box__btn" bindtap="goRecommend">去推荐</view>
|
||||
</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 80rpx;"></view>
|
||||
</view>
|
||||
33
miniprogram/pages/favorites/favorites.wxss
Normal file
33
miniprogram/pages/favorites/favorites.wxss
Normal file
@@ -0,0 +1,33 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.bar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
.bar__count { font-size: 28rpx; color: var(--text-2); }
|
||||
.bar__manage {
|
||||
font-size: 28rpx; color: var(--gold); font-weight: 600;
|
||||
padding: 8rpx 28rpx; border: 1rpx solid var(--gold-line);
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
.grid__item { width: calc((100% - 18rpx) / 2); }
|
||||
.item { position: relative; }
|
||||
.remove {
|
||||
position: absolute; right: 12rpx; top: 12rpx; z-index: 5;
|
||||
background: rgba(229,84,75,0.92); color: #fff;
|
||||
font-size: 22rpx; padding: 8rpx 18rpx; border-radius: 999rpx;
|
||||
}
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 120rpx 0; }
|
||||
|
||||
.empty-box { display: flex; flex-direction: column; align-items: center; padding: 120rpx 40rpx; }
|
||||
.empty-box__emoji {
|
||||
width: 140rpx; height: 140rpx; border-radius: 999rpx;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center;
|
||||
font-size: 72rpx;
|
||||
}
|
||||
.empty-box__t { font-size: 28rpx; color: var(--text-2); margin-top: 32rpx; text-align: center; }
|
||||
.empty-box__btn {
|
||||
margin-top: 40rpx; padding: 22rpx 64rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
@@ -14,9 +14,9 @@ Page({
|
||||
{ key: 'recommend', label: '智能推荐', emoji: '✨', url: '/pages/recommend/recommend' },
|
||||
{ key: 'bodyPart', label: '按部位', emoji: '💪', url: '/pages/category/category?dimension=bodyPart' },
|
||||
{ key: 'equipment', label: '按器械', emoji: '🏋️', url: '/pages/category/category?dimension=equipment' },
|
||||
{ key: 'plan', label: '训练计划', emoji: '📋', url: '/pages/collection/collection' },
|
||||
{ key: 'home', label: '居家无器械', emoji: '🏠', url: '/pages/collection-detail/collection-detail?slug=home' },
|
||||
{ key: 'search', label: '搜索', emoji: '🔍', url: '/pages/search/search' }
|
||||
{ key: 'official', label: '官方计划', emoji: '📋', url: '/pages/collection/collection' },
|
||||
{ key: 'myPlans', label: '我的计划', emoji: '🗂️', url: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', emoji: '👤', url: '/pages/profile/profile' }
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
44
miniprogram/pages/my-plans/my-plans.js
Normal file
44
miniprogram/pages/my-plans/my-plans.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
const auth = require('../../utils/auth');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
plans: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (auth.isLogin()) this.load();
|
||||
else {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => wx.navigateBack(), 600);
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
planSvc.listMine().then((plans) => {
|
||||
this.setData({ plans: plans || [], loading: false });
|
||||
}).catch(() => {
|
||||
this.setData({ plans: [], loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onNew() {
|
||||
wx.navigateTo({ url: '/pages/plan-edit/plan-edit?mode=new' });
|
||||
},
|
||||
|
||||
onOpen(e) {
|
||||
wx.navigateTo({ url: '/pages/plan-detail/plan-detail?id=' + encodeURIComponent(e.currentTarget.dataset.id) });
|
||||
},
|
||||
|
||||
goOfficial() {
|
||||
wx.navigateTo({ url: '/pages/collection/collection' });
|
||||
}
|
||||
});
|
||||
8
miniprogram/pages/my-plans/my-plans.json
Normal file
8
miniprogram/pages/my-plans/my-plans.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"plan-card": "/components/plan-card/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
}
|
||||
}
|
||||
30
miniprogram/pages/my-plans/my-plans.wxml
Normal file
30
miniprogram/pages/my-plans/my-plans.wxml
Normal file
@@ -0,0 +1,30 @@
|
||||
<view class="page">
|
||||
<navbar title="我的训练计划" show-back />
|
||||
|
||||
<view class="newbar">
|
||||
<view class="newbar__btn" bindtap="onNew">+ 新建计划</view>
|
||||
</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<block wx:if="{{plans.length}}">
|
||||
<view class="grid">
|
||||
<view class="grid__item" wx:for="{{plans}}" wx:key="id">
|
||||
<plan-card
|
||||
plan="{{item}}"
|
||||
target="/pages/plan-detail/plan-detail?id={{item.id}}" />
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">📋</view>
|
||||
<view class="empty-box__t">还没有计划,创建一个开始训练吧</view>
|
||||
<view class="empty-box__btn" bindtap="onNew">+ 新建计划</view>
|
||||
<view class="empty-box__link" bindtap="goOfficial">或浏览官方训练计划 ›</view>
|
||||
</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="plans" />
|
||||
</view>
|
||||
25
miniprogram/pages/my-plans/my-plans.wxss
Normal file
25
miniprogram/pages/my-plans/my-plans.wxss
Normal file
@@ -0,0 +1,25 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.newbar { padding: 16rpx 32rpx; }
|
||||
.newbar__btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
.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: 120rpx 0; }
|
||||
|
||||
.empty-box { display: flex; flex-direction: column; align-items: center; padding: 120rpx 40rpx; }
|
||||
.empty-box__emoji {
|
||||
width: 160rpx; height: 160rpx; border-radius: 36rpx;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center;
|
||||
font-size: 88rpx;
|
||||
}
|
||||
.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: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
.empty-box__link { margin-top: 28rpx; font-size: 26rpx; color: var(--gold); }
|
||||
74
miniprogram/pages/plan-detail/plan-detail.js
Normal file
74
miniprogram/pages/plan-detail/plan-detail.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
id: '',
|
||||
plan: null,
|
||||
exercises: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const id = query.id || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, id });
|
||||
this.load();
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
planSvc.get(this.data.id).then((plan) => {
|
||||
const raw = plan && plan.updatedAt;
|
||||
const updatedAtText = typeof raw === 'string'
|
||||
? raw.slice(0, 10)
|
||||
: (raw ? String(raw).slice(0, 10) : '');
|
||||
this.setData({
|
||||
plan: { ...plan, updatedAtText },
|
||||
exercises: (plan && plan.exercises) || [],
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onRemoveExercise(e) {
|
||||
const exerciseId = e.currentTarget.dataset.id;
|
||||
planSvc.removeExercise(this.data.id, exerciseId).then(() => {
|
||||
this.setData({ exercises: this.data.exercises.filter(i => i.id !== exerciseId) });
|
||||
wx.showToast({ title: '已移除', icon: 'none' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/detail/detail?planId=' + encodeURIComponent(this.data.id) +
|
||||
'&id=' + encodeURIComponent(e.detail.id)
|
||||
});
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
wx.navigateTo({ url: '/pages/search/search?mode=select&planId=' + encodeURIComponent(this.data.id) });
|
||||
},
|
||||
|
||||
onEdit() {
|
||||
wx.navigateTo({ url: '/pages/plan-edit/plan-edit?mode=edit&id=' + encodeURIComponent(this.data.id) });
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
wx.showModal({
|
||||
title: '删除计划',
|
||||
content: '确定要删除该训练计划吗?此操作不可恢复。',
|
||||
confirmColor: '#E5544B',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
planSvc.remove(this.data.id).then(() => {
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => wx.navigateBack(), 500);
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
8
miniprogram/pages/plan-detail/plan-detail.json
Normal file
8
miniprogram/pages/plan-detail/plan-detail.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index"
|
||||
}
|
||||
}
|
||||
39
miniprogram/pages/plan-detail/plan-detail.wxml
Normal file
39
miniprogram/pages/plan-detail/plan-detail.wxml
Normal file
@@ -0,0 +1,39 @@
|
||||
<view class="page" wx:if="{{plan}}">
|
||||
<navbar title="{{plan.name}}" show-back />
|
||||
|
||||
<view class="header" style="background: linear-gradient(135deg, {{plan.color}}26, var(--surface));">
|
||||
<view class="header__emoji" style="background: {{plan.color}}22; color: {{plan.color}};">{{plan.emoji}}</view>
|
||||
<view class="header__name" style="color: {{plan.color}};">{{plan.name}}</view>
|
||||
<view wx:if="{{plan.description}}" class="header__desc">{{plan.description}}</view>
|
||||
<view class="header__meta">共 {{exercises.length}} 个动作<text wx:if="{{plan.updatedAtText}}"> · 更新于 {{plan.updatedAtText}}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<section-header eyebrow="EXERCISES" title="动作列表" subtitle="点击查看详情 · 可在动作页加入本计划" />
|
||||
|
||||
<view class="list" wx:if="{{exercises.length}}">
|
||||
<view class="list__item" wx:for="{{exercises}}" wx:key="id">
|
||||
<view class="list__card">
|
||||
<exercise-card exercise="{{item}}" bind:tap="onExercise" />
|
||||
</view>
|
||||
<view class="list__remove" data-id="{{item.id}}" catchtap="onRemoveExercise">移除</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">🏋️</view>
|
||||
<view class="empty-box__t">这个计划还没有动作,去添加吧</view>
|
||||
<view class="empty-box__btn" bindtap="onAdd">+ 添加动作</view>
|
||||
</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view class="actionbar">
|
||||
<view class="btn btn--gold" bindtap="onAdd">+ 添加动作</view>
|
||||
<view class="btn btn--ghost" bindtap="onEdit">编辑</view>
|
||||
<view class="btn btn--del" bindtap="onDelete">删除计划</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty-page">
|
||||
<view class="empty">{{ loading ? '加载中…' : '未找到该计划' }}</view>
|
||||
</view>
|
||||
50
miniprogram/pages/plan-detail/plan-detail.wxss
Normal file
50
miniprogram/pages/plan-detail/plan-detail.wxss
Normal file
@@ -0,0 +1,50 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); padding-bottom: 160rpx; }
|
||||
.header { padding: 32rpx; border-bottom: 1rpx solid var(--line); }
|
||||
.header__emoji {
|
||||
width: 120rpx; height: 120rpx; border-radius: 28rpx;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 64rpx;
|
||||
}
|
||||
.header__name { font-size: 44rpx; font-weight: 700; margin-top: 20rpx; }
|
||||
.header__desc { font-size: 26rpx; color: var(--text-2); margin-top: 12rpx; line-height: 1.5; }
|
||||
.header__meta { font-size: 24rpx; color: var(--text-2); margin-top: 16rpx; }
|
||||
|
||||
.list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.list__item { position: relative; }
|
||||
.list__card { width: 100%; }
|
||||
.list__remove {
|
||||
position: absolute; right: 12rpx; top: 12rpx; z-index: 5;
|
||||
background: rgba(229,84,75,0.92); color: #fff;
|
||||
font-size: 22rpx; padding: 8rpx 18rpx; border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 120rpx 0; }
|
||||
.empty-page { min-height: 100vh; background: var(--bg-grad); }
|
||||
|
||||
.empty-box { display: flex; flex-direction: column; align-items: center; padding: 100rpx 40rpx; }
|
||||
.empty-box__emoji {
|
||||
width: 150rpx; height: 150rpx; border-radius: 36rpx;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center;
|
||||
font-size: 80rpx;
|
||||
}
|
||||
.empty-box__t { font-size: 28rpx; color: var(--text-2); margin-top: 32rpx; text-align: center; }
|
||||
.empty-box__btn {
|
||||
margin-top: 40rpx; padding: 22rpx 64rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
|
||||
.actionbar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
display: flex; gap: 16rpx; padding: 20rpx 24rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.btn {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: 999rpx; padding: 24rpx 10rpx; font-size: 28rpx; font-weight: 700;
|
||||
}
|
||||
.btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: #0E1110; }
|
||||
.btn--ghost { background: var(--surface-2); color: var(--text); border: 1rpx solid var(--line); flex: 0 0 auto; width: 180rpx; }
|
||||
.btn--del { background: rgba(229,84,75,0.16); color: #E5544B; border: 1rpx solid rgba(229,84,75,0.4); flex: 0 0 auto; width: 180rpx; }
|
||||
88
miniprogram/pages/plan-edit/plan-edit.js
Normal file
88
miniprogram/pages/plan-edit/plan-edit.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const app = getApp();
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
const EMOJIS = ['📋', '💪', '🔥', '🏋️', '🏃', '🧘', '🤸', '🥊', '🚴', '⚡', '🌟', '🎯'];
|
||||
const COLORS = ['#D9B36C', '#58C9B9', '#9B8CFF', '#F08A5D', '#3FBF7F', '#5B9DF9'];
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
mode: 'new',
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '📋',
|
||||
color: '#D9B36C',
|
||||
emojis: EMOJIS,
|
||||
colors: COLORS,
|
||||
loading: false,
|
||||
dirty: false // 是否有未保存的修改
|
||||
},
|
||||
|
||||
onLoad(query) {
|
||||
const mode = query.mode === 'edit' ? 'edit' : 'new';
|
||||
const id = query.id || '';
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20, mode, id });
|
||||
if (mode === 'edit' && id) {
|
||||
this.setData({ loading: true });
|
||||
planSvc.get(id).then((p) => {
|
||||
this.setData({
|
||||
name: p.name || '',
|
||||
description: p.description || '',
|
||||
emoji: p.emoji || '📋',
|
||||
color: p.color || '#D9B36C',
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onName(e) { this.setData({ name: e.detail.value, dirty: true }); },
|
||||
onDesc(e) { this.setData({ description: e.detail.value, dirty: true }); },
|
||||
onPickEmoji(e) { this.setData({ emoji: e.currentTarget.dataset.emoji, dirty: true }); },
|
||||
onPickColor(e) { this.setData({ color: e.currentTarget.dataset.color, dirty: true }); },
|
||||
|
||||
// 点返回键(navbar 拦截模式)触发:有未保存改动时先确认
|
||||
onBackRequest() {
|
||||
if (!this.data.dirty) {
|
||||
wx.navigateBack();
|
||||
return;
|
||||
}
|
||||
wx.showModal({
|
||||
title: '放弃编辑?',
|
||||
content: '当前修改尚未保存,确定要退出吗?',
|
||||
confirmText: '退出',
|
||||
cancelText: '继续编辑',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateBack();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const name = (this.data.name || '').trim();
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请填写计划名称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const dto = {
|
||||
name,
|
||||
description: (this.data.description || '').trim(),
|
||||
emoji: this.data.emoji,
|
||||
color: this.data.color
|
||||
};
|
||||
wx.showLoading({ title: '保存中…', mask: true });
|
||||
const op = this.data.mode === 'edit'
|
||||
? planSvc.update(this.data.id, dto)
|
||||
: planSvc.create(dto);
|
||||
op.then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已保存', icon: 'success' });
|
||||
setTimeout(() => wx.navigateBack(), 500);
|
||||
}).catch(() => {
|
||||
wx.hideLoading();
|
||||
});
|
||||
}
|
||||
});
|
||||
6
miniprogram/pages/plan-edit/plan-edit.json
Normal file
6
miniprogram/pages/plan-edit/plan-edit.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index"
|
||||
}
|
||||
}
|
||||
64
miniprogram/pages/plan-edit/plan-edit.wxml
Normal file
64
miniprogram/pages/plan-edit/plan-edit.wxml
Normal file
@@ -0,0 +1,64 @@
|
||||
<view class="page">
|
||||
<navbar title="{{mode === 'edit' ? '编辑计划' : '新建计划'}}" show-back intercept-back bind:back="onBackRequest" />
|
||||
|
||||
<view class="section">
|
||||
<view class="preview" style="background: linear-gradient(135deg, {{color}}26, var(--surface));">
|
||||
<view class="preview__emoji" style="background: {{color}}22; color: {{color}};">{{emoji}}</view>
|
||||
<view class="preview__name" style="color: {{color}};">{{name || '计划名称'}}</view>
|
||||
<view class="preview__desc">{{description || '计划描述(选填)'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<view class="field__label">计划名称</view>
|
||||
<input
|
||||
class="field__input"
|
||||
value="{{name}}"
|
||||
placeholder="例如:居家增肌 12 周"
|
||||
placeholder-class="ph"
|
||||
maxlength="20"
|
||||
bindinput="onName" />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<view class="field__label">计划描述</view>
|
||||
<textarea
|
||||
class="field__area"
|
||||
value="{{description}}"
|
||||
placeholder="简单描述你的训练目标(选填)"
|
||||
placeholder-class="ph"
|
||||
maxlength="120"
|
||||
auto-height
|
||||
bindinput="onDesc" />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<view class="field__label">图标</view>
|
||||
<scroll-view scroll-x class="emoji-row">
|
||||
<view
|
||||
wx:for="{{emojis}}"
|
||||
wx:key="*this"
|
||||
class="emoji {{emoji === item ? 'emoji--active' : ''}}"
|
||||
style="{{emoji === item ? 'border-color:' + color + '; background:' + color + '22;' : ''}}"
|
||||
data-emoji="{{item}}"
|
||||
bindtap="onPickEmoji">{{item}}</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<view class="field__label">主题色</view>
|
||||
<view class="colors">
|
||||
<view
|
||||
wx:for="{{colors}}"
|
||||
wx:key="*this"
|
||||
class="swatch {{color === item ? 'swatch--active' : ''}}"
|
||||
style="background: {{item}};"
|
||||
data-color="{{item}}"
|
||||
bindtap="onPickColor"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="savebar">
|
||||
<view class="savebar__btn" bindtap="onSave">保存计划</view>
|
||||
</view>
|
||||
</view>
|
||||
54
miniprogram/pages/plan-edit/plan-edit.wxss
Normal file
54
miniprogram/pages/plan-edit/plan-edit.wxss
Normal file
@@ -0,0 +1,54 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); padding-bottom: 160rpx; }
|
||||
|
||||
.preview {
|
||||
border-radius: var(--radius); padding: 36rpx;
|
||||
border: 1rpx solid var(--line); text-align: center;
|
||||
}
|
||||
.preview__emoji {
|
||||
width: 120rpx; height: 120rpx; border-radius: 28rpx;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 64rpx;
|
||||
}
|
||||
.preview__name { font-size: 38rpx; font-weight: 700; margin-top: 20rpx; }
|
||||
.preview__desc { font-size: 24rpx; color: var(--text-2); margin-top: 10rpx; }
|
||||
|
||||
.field { margin-top: 40rpx; }
|
||||
.field__label { font-size: 26rpx; color: var(--text-2); margin-bottom: 16rpx; }
|
||||
.field__input, .field__area {
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius-sm); padding: 24rpx; color: var(--text);
|
||||
font-size: 30rpx; width: 100%;
|
||||
}
|
||||
.field__area { min-height: 140rpx; }
|
||||
.ph { color: var(--text-3); }
|
||||
|
||||
.emoji-row { white-space: nowrap; }
|
||||
.emoji {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 84rpx; height: 84rpx; margin-right: 14rpx;
|
||||
border-radius: 20rpx; background: var(--surface-2);
|
||||
border: 1rpx solid var(--line); font-size: 44rpx;
|
||||
}
|
||||
.emoji--active { border-width: 2rpx; }
|
||||
|
||||
.colors { display: flex; flex-wrap: wrap; gap: 24rpx; }
|
||||
.swatch {
|
||||
width: 72rpx; height: 72rpx; border-radius: 999rpx;
|
||||
border: 4rpx solid transparent; box-sizing: border-box;
|
||||
}
|
||||
.swatch--active { border-color: var(--text); box-shadow: 0 0 0 2rpx var(--bg); }
|
||||
|
||||
.savebar {
|
||||
position: fixed; left: 0; right: 0; bottom: 0;
|
||||
padding: 20rpx 32rpx;
|
||||
background: rgba(14,17,16,0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1rpx solid var(--line);
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.savebar__btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 26rpx;
|
||||
}
|
||||
36
miniprogram/pages/plan-favorites/plan-favorites.js
Normal file
36
miniprogram/pages/plan-favorites/plan-favorites.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const app = getApp();
|
||||
const favSvc = require('../../services/favorite');
|
||||
const auth = require('../../utils/auth');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
plans: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (auth.isLogin()) this.load();
|
||||
else {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => wx.navigateBack(), 600);
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setData({ loading: true });
|
||||
favSvc.listPlans().then((plans) => {
|
||||
this.setData({ plans: plans || [], loading: false });
|
||||
}).catch(() => {
|
||||
this.setData({ plans: [], loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
goOfficial() {
|
||||
wx.navigateTo({ url: '/pages/collection/collection' });
|
||||
}
|
||||
});
|
||||
7
miniprogram/pages/plan-favorites/plan-favorites.json
Normal file
7
miniprogram/pages/plan-favorites/plan-favorites.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"plan-card": "/components/plan-card/index"
|
||||
}
|
||||
}
|
||||
24
miniprogram/pages/plan-favorites/plan-favorites.wxml
Normal file
24
miniprogram/pages/plan-favorites/plan-favorites.wxml
Normal file
@@ -0,0 +1,24 @@
|
||||
<view class="page">
|
||||
<navbar title="收藏的官方计划" show-back />
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<block wx:if="{{plans.length}}">
|
||||
<view class="grid">
|
||||
<view class="grid__item" wx:for="{{plans}}" wx:key="slug">
|
||||
<plan-card
|
||||
plan="{{item}}"
|
||||
target="/pages/collection-detail/collection-detail?slug={{item.slug}}" />
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view wx:elif="{{!loading}}" class="empty-box">
|
||||
<view class="empty-box__emoji">⭐</view>
|
||||
<view class="empty-box__t">还没有收藏官方计划</view>
|
||||
<view class="empty-box__btn" bindtap="goOfficial">浏览官方计划</view>
|
||||
</view>
|
||||
<view wx:else class="empty">加载中…</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 80rpx;"></view>
|
||||
</view>
|
||||
17
miniprogram/pages/plan-favorites/plan-favorites.wxss
Normal file
17
miniprogram/pages/plan-favorites/plan-favorites.wxss
Normal file
@@ -0,0 +1,17 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
.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: 120rpx 0; }
|
||||
|
||||
.empty-box { display: flex; flex-direction: column; align-items: center; padding: 120rpx 40rpx; }
|
||||
.empty-box__emoji {
|
||||
width: 150rpx; height: 150rpx; border-radius: 36rpx;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center;
|
||||
font-size: 80rpx;
|
||||
}
|
||||
.empty-box__t { font-size: 28rpx; color: var(--text-2); margin-top: 32rpx; text-align: center; }
|
||||
.empty-box__btn {
|
||||
margin-top: 40rpx; padding: 22rpx 64rpx; border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 30rpx;
|
||||
}
|
||||
129
miniprogram/pages/profile/profile.js
Normal file
129
miniprogram/pages/profile/profile.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const app = getApp();
|
||||
const auth = require('../../utils/auth');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
const EMOJIS = ['💪', '🏋️', '🔥', '🏃', '🧘', '🤸', '🥊', '🚴', '⚡', '🌟', '🐯', '🦁'];
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
loggedIn: false,
|
||||
profile: { nickname: '', avatar: '💪' },
|
||||
counts: { fav: 0, plans: 0, favPlans: 0 },
|
||||
loading: false,
|
||||
// 编辑态
|
||||
showEmojiPicker: false,
|
||||
emojis: EMOJIS,
|
||||
draftNickname: '',
|
||||
draftAvatar: '💪'
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
loadData() {
|
||||
if (!auth.isLogin()) {
|
||||
this.setData({ loggedIn: false });
|
||||
return;
|
||||
}
|
||||
const profile = auth.getProfile();
|
||||
this.setData({ loggedIn: true, profile, loading: true });
|
||||
Promise.all([
|
||||
favSvc.listExercises().catch(() => []),
|
||||
planSvc.listMine().catch(() => []),
|
||||
favSvc.listPlans().catch(() => [])
|
||||
]).then(([fav, plans, favPlans]) => {
|
||||
this.setData({
|
||||
counts: {
|
||||
fav: (fav || []).length,
|
||||
plans: (plans || []).length,
|
||||
favPlans: (favPlans || []).length
|
||||
},
|
||||
loading: false
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
onLogin() {
|
||||
auth.login().then(() => {
|
||||
this.loadData();
|
||||
wx.showToast({ title: '登录成功', icon: 'success' });
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
onEdit() {
|
||||
const cur = this.data.profile;
|
||||
wx.showModal({
|
||||
title: '修改昵称',
|
||||
editable: true,
|
||||
placeholderText: '输入昵称',
|
||||
content: cur.nickname || '',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({
|
||||
draftNickname: (res.content || '').trim() || cur.nickname,
|
||||
draftAvatar: cur.avatar || '💪',
|
||||
showEmojiPicker: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onPickEmoji(e) {
|
||||
this.setData({ draftAvatar: e.currentTarget.dataset.emoji });
|
||||
},
|
||||
|
||||
onCancelEdit() {
|
||||
this.setData({ showEmojiPicker: false });
|
||||
},
|
||||
|
||||
onConfirmEdit() {
|
||||
const nickname = (this.data.draftNickname || '').trim();
|
||||
const avatar = this.data.draftAvatar;
|
||||
if (!nickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
wx.showLoading({ title: '保存中…', mask: true });
|
||||
auth.updateProfile({ nickname, avatar }).then((p) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ profile: p, showEmojiPicker: false });
|
||||
wx.showToast({ title: '已保存', icon: 'success' });
|
||||
}).catch(() => {
|
||||
wx.hideLoading();
|
||||
});
|
||||
},
|
||||
|
||||
onLogout() {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出当前账号吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
auth.logout();
|
||||
this.setData({
|
||||
loggedIn: false,
|
||||
profile: { nickname: '', avatar: '💪' },
|
||||
counts: { fav: 0, plans: 0, favPlans: 0 }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
goFav() { wx.navigateTo({ url: '/pages/favorites/favorites' }); },
|
||||
goPlans() { wx.navigateTo({ url: '/pages/my-plans/my-plans' }); },
|
||||
goFavPlans() { wx.navigateTo({ url: '/pages/plan-favorites/plan-favorites' }); },
|
||||
goOfficial() { wx.navigateTo({ url: '/pages/collection/collection' }); },
|
||||
|
||||
noop() {}
|
||||
});
|
||||
8
miniprogram/pages/profile/profile.json
Normal file
8
miniprogram/pages/profile/profile.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"avatar": "/components/avatar/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
}
|
||||
}
|
||||
87
miniprogram/pages/profile/profile.wxml
Normal file
87
miniprogram/pages/profile/profile.wxml
Normal file
@@ -0,0 +1,87 @@
|
||||
<view class="page">
|
||||
<navbar title="我的" />
|
||||
|
||||
<!-- 未登录 -->
|
||||
<view wx:if="{{!loggedIn}}" class="login">
|
||||
<view class="login__logo">💪</view>
|
||||
<view class="login__title">FITCOACH 智能健身教练</view>
|
||||
<view class="login__sub">登录后同步你的收藏与训练计划</view>
|
||||
<button class="login__btn" bindtap="onLogin">微信登录</button>
|
||||
</view>
|
||||
|
||||
<!-- 已登录 -->
|
||||
<view wx:else class="center">
|
||||
<view class="profile">
|
||||
<view class="profile__head">
|
||||
<avatar avatar="{{profile.avatar}}" size="{{120}}" />
|
||||
<view class="profile__info">
|
||||
<view class="profile__name">{{profile.nickname || '健身达人'}}</view>
|
||||
<view class="profile__edit" bindtap="onEdit">编辑 ›</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats">
|
||||
<view class="stat" bindtap="goFav">
|
||||
<view class="stat__n">{{counts.fav}}</view>
|
||||
<view class="stat__l">收藏动作</view>
|
||||
</view>
|
||||
<view class="stat" bindtap="goPlans">
|
||||
<view class="stat__n">{{counts.plans}}</view>
|
||||
<view class="stat__l">我的计划</view>
|
||||
</view>
|
||||
<view class="stat" bindtap="goFavPlans">
|
||||
<view class="stat__n">{{counts.favPlans}}</view>
|
||||
<view class="stat__l">收藏计划</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu card">
|
||||
<view class="cell" bindtap="goFav">
|
||||
<text class="cell__icon">❤️</text>
|
||||
<text class="cell__label">我的收藏动作</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goPlans">
|
||||
<text class="cell__icon">🗂️</text>
|
||||
<text class="cell__label">我的训练计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goFavPlans">
|
||||
<text class="cell__icon">⭐</text>
|
||||
<text class="cell__label">收藏的官方计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goOfficial">
|
||||
<text class="cell__icon">📋</text>
|
||||
<text class="cell__label">官方训练计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout" bindtap="onLogout">退出登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 编辑头像弹层 -->
|
||||
<view wx:if="{{showEmojiPicker}}" class="sheet" bindtap="onCancelEdit">
|
||||
<view class="sheet__panel" catchtap="noop">
|
||||
<view class="sheet__title">选择头像</view>
|
||||
<scroll-view scroll-x class="emoji-row">
|
||||
<view
|
||||
wx:for="{{emojis}}"
|
||||
wx:key="*this"
|
||||
class="emoji {{draftAvatar === item ? 'emoji--active' : ''}}"
|
||||
data-emoji="{{item}}"
|
||||
bindtap="onPickEmoji">{{item}}</view>
|
||||
</scroll-view>
|
||||
<view class="sheet__nick">{{draftNickname || '未命名'}}</view>
|
||||
<view class="sheet__actions">
|
||||
<view class="sheet__btn sheet__btn--ghost" bindtap="onCancelEdit">取消</view>
|
||||
<view class="sheet__btn sheet__btn--gold" bindtap="onConfirmEdit">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="mine" />
|
||||
</view>
|
||||
84
miniprogram/pages/profile/profile.wxss
Normal file
84
miniprogram/pages/profile/profile.wxss
Normal file
@@ -0,0 +1,84 @@
|
||||
.page { min-height: 100vh; background: var(--bg-grad); }
|
||||
|
||||
/* 未登录 */
|
||||
.login {
|
||||
min-height: 70vh; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; padding: 60rpx;
|
||||
}
|
||||
.login__logo {
|
||||
width: 160rpx; height: 160rpx; border-radius: 999rpx;
|
||||
background: var(--gold-soft); border: 1rpx solid var(--gold-line);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 88rpx;
|
||||
}
|
||||
.login__title { font-size: 38rpx; font-weight: 700; margin-top: 32rpx; color: var(--text); }
|
||||
.login__sub { font-size: 26rpx; color: var(--text-2); margin-top: 14rpx; }
|
||||
.login__btn {
|
||||
margin-top: 56rpx; width: 420rpx;
|
||||
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%);
|
||||
color: #0E1110; font-weight: 700; font-size: 32rpx;
|
||||
border-radius: 999rpx; padding: 24rpx;
|
||||
}
|
||||
.login__btn::after { border: none; }
|
||||
|
||||
/* 个人中心 */
|
||||
.center { padding: 32rpx; }
|
||||
.profile {
|
||||
background: linear-gradient(135deg, rgba(217,179,108,0.14), var(--surface));
|
||||
border: 1rpx solid var(--gold-line);
|
||||
border-radius: var(--radius); padding: 36rpx;
|
||||
}
|
||||
.profile__head { display: flex; align-items: center; gap: 28rpx; }
|
||||
.profile__info { flex: 1; min-width: 0; }
|
||||
.profile__name { font-size: 40rpx; font-weight: 700; color: var(--gold); }
|
||||
.profile__edit { font-size: 26rpx; color: var(--text-2); margin-top: 10rpx; }
|
||||
|
||||
.stats { display: flex; margin-top: 36rpx; }
|
||||
.stat { flex: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.stat + .stat { border-left: 1rpx solid var(--line); }
|
||||
.stat__n { font-size: 44rpx; font-weight: 700; color: var(--text); }
|
||||
.stat__l { font-size: 24rpx; color: var(--text-2); margin-top: 8rpx; }
|
||||
|
||||
.menu { margin-top: 28rpx; padding: 8rpx 28rpx; }
|
||||
.cell {
|
||||
display: flex; align-items: center; gap: 24rpx;
|
||||
padding: 30rpx 0; border-bottom: 1rpx solid var(--line);
|
||||
}
|
||||
.cell:last-child { border-bottom: none; }
|
||||
.cell__icon { font-size: 38rpx; width: 48rpx; text-align: center; }
|
||||
.cell__label { flex: 1; font-size: 30rpx; color: var(--text); }
|
||||
.cell__arrow { font-size: 40rpx; color: var(--gold); line-height: 1; }
|
||||
|
||||
.logout {
|
||||
margin-top: 40rpx; text-align: center;
|
||||
font-size: 28rpx; color: var(--text-3);
|
||||
}
|
||||
|
||||
/* 头像选择弹层 */
|
||||
.sheet {
|
||||
position: fixed; inset: 0; z-index: 80;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex; align-items: flex-end;
|
||||
}
|
||||
.sheet__panel {
|
||||
width: 100%; background: var(--surface);
|
||||
border-top-left-radius: 32rpx; border-top-right-radius: 32rpx;
|
||||
padding: 36rpx 32rpx calc(36rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.sheet__title { font-size: 30rpx; font-weight: 700; color: var(--text); }
|
||||
.emoji-row { white-space: nowrap; margin-top: 24rpx; }
|
||||
.emoji {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 88rpx; height: 88rpx; margin-right: 16rpx;
|
||||
border-radius: 20rpx; background: var(--surface-2);
|
||||
border: 1rpx solid var(--line); font-size: 48rpx;
|
||||
}
|
||||
.emoji--active { border-color: var(--gold); background: var(--gold-soft); }
|
||||
.sheet__nick { margin-top: 24rpx; font-size: 28rpx; color: var(--text-2); text-align: center; }
|
||||
.sheet__actions { display: flex; gap: 20rpx; margin-top: 28rpx; }
|
||||
.sheet__btn {
|
||||
flex: 1; text-align: center; padding: 24rpx; border-radius: 999rpx;
|
||||
font-size: 30rpx; font-weight: 700;
|
||||
}
|
||||
.sheet__btn--ghost { background: var(--surface-2); color: var(--text); border: 1rpx solid var(--line); }
|
||||
.sheet__btn--gold { background: linear-gradient(135deg, var(--gold) 0%, var(--gold-2) 100%); color: #0E1110; }
|
||||
@@ -1,5 +1,6 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
const planSvc = require('../../services/plan');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -7,11 +8,24 @@ Page({
|
||||
q: '',
|
||||
results: [],
|
||||
loading: false,
|
||||
searched: false
|
||||
searched: false,
|
||||
selectMode: false,
|
||||
planId: '',
|
||||
navTitle: '搜索'
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(query) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
const selectMode = query.mode === 'select';
|
||||
const planId = query.planId || '';
|
||||
this.setData({
|
||||
selectMode,
|
||||
planId,
|
||||
navTitle: selectMode ? '选择动作' : '搜索'
|
||||
});
|
||||
if (selectMode) {
|
||||
wx.setNavigationBarTitle({ title: '选择动作' });
|
||||
}
|
||||
},
|
||||
|
||||
onInput(e) {
|
||||
@@ -43,6 +57,14 @@ Page({
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id) });
|
||||
const id = e.detail.id;
|
||||
if (this.data.selectMode && this.data.planId) {
|
||||
planSvc.addExercise(this.data.planId, id).then(() => {
|
||||
wx.showToast({ title: '已添加', icon: 'success' });
|
||||
setTimeout(() => wx.navigateBack(), 500);
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(id) });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<view class="page">
|
||||
<navbar title="搜索" />
|
||||
<navbar title="{{navTitle}}" show-back="{{selectMode}}" />
|
||||
<view class="searchbar">
|
||||
<view class="searchbar__box">
|
||||
<text class="searchbar__icon">🔍</text>
|
||||
<input
|
||||
class="searchbar__input"
|
||||
placeholder="搜索动作 / 器械 / 部位"
|
||||
placeholder="{{selectMode ? '选择动作加入计划' : '搜索动作 / 器械 / 部位'}}"
|
||||
placeholder-class="ph"
|
||||
value="{{q}}"
|
||||
confirm-type="search"
|
||||
bindinput="onInput" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{selectMode}}" class="hint">从下方结果中选择动作,加入当前计划</view>
|
||||
|
||||
<view class="section" style="padding-top: 8rpx;">
|
||||
<view class="grid" wx:if="{{results.length}}">
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
.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: 120rpx 0; }
|
||||
.hint { padding: 16rpx 36rpx 0; font-size: 24rpx; color: var(--gold); }
|
||||
|
||||
Reference in New Issue
Block a user