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

View File

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

View File

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

View File

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