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

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

View File

@@ -0,0 +1,8 @@
{
"navigationStyle": "custom",
"usingComponents": {
"navbar": "/components/navbar/index",
"avatar": "/components/avatar/index",
"bottom-nav": "/components/bottom-nav/index"
}
}

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

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