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:
2026-07-22 14:05:30 +08:00
parent b3b58e66fb
commit 70ff806042
79 changed files with 2397 additions and 146070 deletions

View File

@@ -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
};
},

View File

@@ -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>

View File

@@ -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;
}