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:
17
miniprogram/components/avatar/index.js
Normal file
17
miniprogram/components/avatar/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// 头像组件:纯 emoji 圆形头像(无二进制图片)。
|
||||
// props: avatar(emoji 字符串), size(rpx, 默认 120), bg(背景色, 默认 gold-soft)
|
||||
Component({
|
||||
properties: {
|
||||
avatar: { type: String, value: '💪' },
|
||||
size: { type: Number, value: 120 },
|
||||
bg: { type: String, value: 'rgba(217,179,108,0.12)' }
|
||||
},
|
||||
data: {
|
||||
fontSize: 60
|
||||
},
|
||||
observers: {
|
||||
size: function (size) {
|
||||
this.setData({ fontSize: Math.round(size * 0.5) });
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/avatar/index.json
Normal file
4
miniprogram/components/avatar/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
5
miniprogram/components/avatar/index.wxml
Normal file
5
miniprogram/components/avatar/index.wxml
Normal file
@@ -0,0 +1,5 @@
|
||||
<view
|
||||
class="avatar"
|
||||
style="width: {{size}}rpx; height: {{size}}rpx; background: {{bg}}; font-size: {{fontSize}}rpx;">
|
||||
{{avatar || '💪'}}
|
||||
</view>
|
||||
9
miniprogram/components/avatar/index.wxss
Normal file
9
miniprogram/components/avatar/index.wxss
Normal file
@@ -0,0 +1,9 @@
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
border: 1rpx solid var(--gold-line);
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
Component({
|
||||
properties: {
|
||||
active: { type: String, value: 'home' } // 'home' | 'category' | 'plan'
|
||||
active: { type: String, value: 'home' } // 'home' | 'category' | 'mine'
|
||||
},
|
||||
data: {
|
||||
items: [
|
||||
{ key: 'home', label: '首页', emoji: '🏠', page: '/pages/index/index' },
|
||||
{ key: 'category', label: '分类', emoji: '📚', page: '/pages/category/category' },
|
||||
{ key: 'plan', label: '计划', emoji: '📋', page: '/pages/collection/collection' }
|
||||
{ key: 'plans', label: '计划', emoji: '📋', page: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', emoji: '👤', page: '/pages/profile/profile' }
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
z-index: 60;
|
||||
}
|
||||
.bn__item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -16,6 +17,15 @@
|
||||
padding: 16rpx 0 14rpx;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.bn__item--active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx; height: 4rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, var(--gold), var(--gold-2));
|
||||
}
|
||||
.bn__item--active { color: var(--gold); }
|
||||
.bn__emoji { font-size: 40rpx; line-height: 1; }
|
||||
.bn__label { font-size: 22rpx; margin-top: 6rpx; }
|
||||
|
||||
@@ -5,7 +5,20 @@ Component({
|
||||
reason: { type: String, value: '' },
|
||||
compact: { type: Boolean, value: false }
|
||||
},
|
||||
data: {
|
||||
imgSrc: '',
|
||||
imgError: false
|
||||
},
|
||||
observers: {
|
||||
// 卡片用轻量静态图(~6KB),避免 91KB 的 GIF 拖慢列表
|
||||
exercise(ex) {
|
||||
this.setData({ imgSrc: (ex && ex.image) || '', imgError: false });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onImgError() {
|
||||
this.setData({ imgError: true });
|
||||
},
|
||||
onTap() {
|
||||
const ex = this.data.exercise || {};
|
||||
this.triggerEvent('tap', { id: ex.id, exercise: ex });
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<view class="ex-card {{compact ? 'ex-card--compact' : ''}}" bindtap="onTap">
|
||||
<view class="ex-card__media">
|
||||
<image
|
||||
wx:if="{{exercise.gifUrl || exercise.image}}"
|
||||
wx:if="{{imgSrc && !imgError}}"
|
||||
class="ex-card__img"
|
||||
src="{{exercise.gifUrl || exercise.image}}"
|
||||
src="{{imgSrc}}"
|
||||
mode="aspectFill"
|
||||
lazy-load="true" />
|
||||
lazy-load="true"
|
||||
binderror="onImgError" />
|
||||
<view wx:else class="ex-card__ph">🏋️</view>
|
||||
<view wx:if="{{exercise.typeLabel}}" class="ex-card__type">{{exercise.typeLabel}}</view>
|
||||
<view wx:if="{{score > 0}}" class="ex-card__score">匹配 {{score}}</view>
|
||||
</view>
|
||||
<view class="ex-card__body">
|
||||
<view class="ex-card__name">{{exercise.name}}</view>
|
||||
<view class="ex-card__name">{{exercise.displayName || exercise.name}}</view>
|
||||
<view class="ex-card__chips">
|
||||
<text wx:if="{{exercise.targetLabel}}" class="tag tag--gold">{{exercise.targetLabel}}</text>
|
||||
<text wx:if="{{exercise.equipmentLabel}}" class="tag">{{exercise.equipmentLabel}}</text>
|
||||
|
||||
29
miniprogram/components/level-tag/index.js
Normal file
29
miniprogram/components/level-tag/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// 难度等级标签:beginner / intermediate / advanced / all
|
||||
const MAP = {
|
||||
beginner: { text: '新手入门', color: '#3FBF7F', bg: 'rgba(63,191,127,0.14)' },
|
||||
intermediate: { text: '进阶提升', color: '#5B9DF9', bg: 'rgba(91,157,249,0.14)' },
|
||||
advanced: { text: '高级挑战', color: '#F08A5D', bg: 'rgba(240,138,93,0.14)' },
|
||||
all: { text: '全阶段', color: '#58C9B9', bg: 'rgba(88,201,185,0.14)' }
|
||||
};
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
level: { type: String, value: 'all' },
|
||||
label: { type: String, value: '' }
|
||||
},
|
||||
data: {
|
||||
text: '全阶段',
|
||||
color: '#58C9B9',
|
||||
bg: 'rgba(88,201,185,0.14)'
|
||||
},
|
||||
observers: {
|
||||
'level, label': function (level, label) {
|
||||
const m = MAP[level] || MAP.all;
|
||||
this.setData({
|
||||
text: label || m.text,
|
||||
color: m.color,
|
||||
bg: m.bg
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/level-tag/index.json
Normal file
4
miniprogram/components/level-tag/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
miniprogram/components/level-tag/index.wxml
Normal file
1
miniprogram/components/level-tag/index.wxml
Normal file
@@ -0,0 +1 @@
|
||||
<view class="lvl" style="color: {{color}}; background: {{bg}};">{{text}}</view>
|
||||
9
miniprogram/components/level-tag/index.wxss
Normal file
9
miniprogram/components/level-tag/index.wxss
Normal file
@@ -0,0 +1,9 @@
|
||||
.lvl {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -3,23 +3,40 @@ const app = getApp();
|
||||
Component({
|
||||
properties: {
|
||||
title: { type: String, value: '' },
|
||||
showBack: { type: Boolean, value: false }
|
||||
showBack: { type: Boolean, value: false },
|
||||
// 拦截返回:为 true 时点击返回仅触发 bind:back 事件,由页面自行处理(如未保存草稿确认)
|
||||
interceptBack: { type: Boolean, value: false }
|
||||
},
|
||||
data: {
|
||||
statusBarHeight: 20
|
||||
statusBarHeight: 20,
|
||||
// 运行期根据页面栈动态决定:只有确实存在上一页时才显示返回键,
|
||||
// 避免在底部 tab 根页(页面栈深度 = 1)误显示返回键
|
||||
canBack: false
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const h = (app.globalData && app.globalData.statusBarHeight) || 20;
|
||||
this.setData({ statusBarHeight: h });
|
||||
const pages = getCurrentPages();
|
||||
this.setData({
|
||||
statusBarHeight: h,
|
||||
canBack: pages.length > 1
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
const pages = getCurrentPages();
|
||||
|
||||
// 页面要求自行处理返回逻辑(典型场景:编辑页有未保存内容需确认)
|
||||
if (this.data.interceptBack) {
|
||||
this.triggerEvent('back');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack();
|
||||
wx.navigateBack({ delta: 1 });
|
||||
} else {
|
||||
// 兜底:确实没有上一页时回到首页(正常 tab 根页不会显示返回键,此为保险)
|
||||
wx.reLaunch({ url: '/pages/index/index' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<view class="navbar">
|
||||
<view class="navbar__status" style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar__bar">
|
||||
<view wx:if="{{showBack}}" class="navbar__back" bindtap="onBack">‹</view>
|
||||
<view
|
||||
wx:if="{{showBack && canBack}}"
|
||||
class="navbar__back"
|
||||
hover-class="navbar__back--hover"
|
||||
hover-stay-time="80"
|
||||
bindtap="onBack">
|
||||
<view class="navbar__arrow"></view>
|
||||
</view>
|
||||
<view wx:if="{{title}}" class="navbar__title">{{title}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -10,20 +10,51 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 28rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
/* 返回键:整块左侧区域可点,热区远大于图标本身 */
|
||||
.navbar__back {
|
||||
font-size: 56rpx;
|
||||
color: var(--gold);
|
||||
line-height: 1;
|
||||
width: 60rpx;
|
||||
position: relative;
|
||||
width: 72rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-left: -10rpx; /* 让箭头更贴近系统返回键位置 */
|
||||
z-index: 2;
|
||||
}
|
||||
/* 扩展可点热区,提升命中率(左右留白也算可点) */
|
||||
.navbar__back::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: -24rpx;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.navbar__back--hover { opacity: 0.5; }
|
||||
|
||||
/* 用 CSS 绘制清晰、较粗的左箭头,避免依赖字体渲染 ‹ 字符 */
|
||||
.navbar__arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 5rpx solid var(--gold);
|
||||
border-bottom: 5rpx solid var(--gold);
|
||||
transform: rotate(45deg);
|
||||
margin-left: 14rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.navbar__title {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
color: var(--text);
|
||||
z-index: 1;
|
||||
pointer-events: none; /* 不拦截返回键的点击 */
|
||||
}
|
||||
|
||||
33
miniprogram/components/plan-card/index.js
Normal file
33
miniprogram/components/plan-card/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// 计划卡片:复用官方计划(CollectionDef)与我自己的计划(PlanSummary)。
|
||||
// props:
|
||||
// plan: { id|slug, emoji, color, name, description, count|exerciseCount, cover? }
|
||||
// target: 点击跳转的完整 url
|
||||
Component({
|
||||
properties: {
|
||||
plan: { type: Object, value: {} },
|
||||
target: { type: String, value: '' }
|
||||
},
|
||||
data: {
|
||||
count: 0,
|
||||
cover: '',
|
||||
hasCover: false,
|
||||
color: '#D9B36C'
|
||||
},
|
||||
observers: {
|
||||
plan: function (plan) {
|
||||
plan = plan || {};
|
||||
this.setData({
|
||||
count: plan.count || plan.exerciseCount || 0,
|
||||
cover: plan.cover || '',
|
||||
hasCover: !!(plan.cover),
|
||||
color: plan.color || '#D9B36C'
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
const url = this.data.target;
|
||||
if (url) wx.navigateTo({ url });
|
||||
}
|
||||
}
|
||||
});
|
||||
4
miniprogram/components/plan-card/index.json
Normal file
4
miniprogram/components/plan-card/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
22
miniprogram/components/plan-card/index.wxml
Normal file
22
miniprogram/components/plan-card/index.wxml
Normal file
@@ -0,0 +1,22 @@
|
||||
<view class="pc">
|
||||
<view
|
||||
wx:if="{{hasCover}}"
|
||||
class="pc__cover"
|
||||
style="background: {{color}}22;">
|
||||
<image class="pc__img" src="{{cover}}" mode="aspectFill" lazy-load="true" />
|
||||
<view class="pc__badge" style="background: {{color}}22; color: {{color}};">{{plan.emoji}}</view>
|
||||
</view>
|
||||
<view wx:else class="pc__top" style="background: linear-gradient(135deg, {{color}}26, var(--surface));">
|
||||
<view class="pc__emoji" style="background: {{color}}22; color: {{color}};">{{plan.emoji}}</view>
|
||||
</view>
|
||||
|
||||
<view class="pc__body">
|
||||
<view class="pc__name" style="color: {{color}};">{{plan.name}}</view>
|
||||
<view wx:if="{{plan.description}}" class="pc__desc">{{plan.description}}</view>
|
||||
</view>
|
||||
|
||||
<view class="pc__foot">
|
||||
<text wx:if="{{count > 0}}" class="pc__count">{{count}} 个动作</text>
|
||||
<text class="pc__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
37
miniprogram/components/plan-card/index.wxss
Normal file
37
miniprogram/components/plan-card/index.wxss
Normal file
@@ -0,0 +1,37 @@
|
||||
.pc {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
border: 1rpx solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
width: 100%;
|
||||
}
|
||||
.pc__cover { position: relative; width: 100%; height: 220rpx; background: var(--surface-2); }
|
||||
.pc__img { width: 100%; height: 100%; display: block; }
|
||||
.pc__badge {
|
||||
position: absolute; left: 16rpx; top: 16rpx;
|
||||
width: 64rpx; height: 64rpx; border-radius: 18rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 38rpx; line-height: 1;
|
||||
}
|
||||
.pc__top { padding: 28rpx; display: flex; }
|
||||
.pc__emoji {
|
||||
width: 96rpx; height: 96rpx; border-radius: 24rpx;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 52rpx; line-height: 1;
|
||||
}
|
||||
.pc__body { padding: 18rpx 22rpx 8rpx; }
|
||||
.pc__name { font-size: 30rpx; font-weight: 700; }
|
||||
.pc__desc {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx; color: var(--text-2); line-height: 1.5;
|
||||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
|
||||
}
|
||||
.pc__foot {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 16rpx 22rpx 22rpx;
|
||||
border-top: 1rpx solid var(--line);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.pc__count { font-size: 24rpx; color: var(--text-2); }
|
||||
.pc__arrow { font-size: 36rpx; color: var(--gold); line-height: 1; }
|
||||
Reference in New Issue
Block a user