feat: Kinetic Dark 设计语言 + Lucide 图标迁移 + 首页轻量个性化
- 建立 Lucide 子集 iconfont(49 字形),全站 emoji 替换为 fc-icon 组件 - 重塑 Design Token(冷中性近黑底 + Volt 荧光绿 #B6F24E),消除硬编码颜色 - 首页登录后个性化:我的计划 / 我的收藏 / 今日推荐(纯前端复用现有 API) - 后端 DEFAULT_EMOJI→Lucide 名,PALETTE 对齐 Kinetic Dark 语义色板 - 修复 person-standing 缺失字形;14 个官方组合 seed emoji 全部映射到现有字形
This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
const app = getApp();
|
||||
const svc = require('../../services/exercise');
|
||||
const planSvc = require('../../services/plan');
|
||||
const favSvc = require('../../services/favorite');
|
||||
const { iconName } = require('../../utils/icon-name');
|
||||
|
||||
function greeting() {
|
||||
const h = new Date().getHours();
|
||||
if (h < 6) return '夜深了,';
|
||||
if (h < 11) return '早上好,';
|
||||
if (h < 13) return '中午好,';
|
||||
if (h < 18) return '下午好,';
|
||||
if (h < 22) return '晚上好,';
|
||||
return '夜深了,';
|
||||
}
|
||||
|
||||
function isLogin() {
|
||||
return !!wx.getStorageSync('access_token');
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -11,13 +28,19 @@ Page({
|
||||
collections: [],
|
||||
loading: true,
|
||||
quickEntries: [
|
||||
{ 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: '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' }
|
||||
]
|
||||
{ key: 'recommend', label: '智能推荐', icon: 'sparkles', url: '/pages/recommend/recommend' },
|
||||
{ key: 'bodyPart', label: '按部位', icon: 'accessibility', url: '/pages/category/category?dimension=bodyPart' },
|
||||
{ key: 'equipment', label: '按器械', icon: 'dumbbell', url: '/pages/category/category?dimension=equipment' },
|
||||
{ key: 'official', label: '官方计划', icon: 'clipboard-list', url: '/pages/collection/collection' },
|
||||
{ key: 'myPlans', label: '我的计划', icon: 'folder-open', url: '/pages/my-plans/my-plans' },
|
||||
{ key: 'mine', label: '我的', icon: 'user', url: '/pages/profile/profile' }
|
||||
],
|
||||
// —— 轻量个性化(登录后)——
|
||||
isLogin: false,
|
||||
greeting: '',
|
||||
myPlans: [],
|
||||
myFavorites: [],
|
||||
todayRecommend: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -25,6 +48,11 @@ Page({
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// tab 切回:已登录则静默刷新个性化(优先缓存)
|
||||
if (isLogin()) this.loadPersonal(true);
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ loading: true });
|
||||
Promise.all([
|
||||
@@ -32,19 +60,70 @@ Page({
|
||||
svc.listExercises({ pageSize: 8 }).catch(() => ({ items: [] })),
|
||||
svc.listCollections().catch(() => [])
|
||||
]).then(([stats, list, collections]) => {
|
||||
const cols = (collections || []).map((c) => ({
|
||||
...c,
|
||||
icon: iconName(c.emoji)
|
||||
}));
|
||||
this.setData({
|
||||
stats,
|
||||
bodyParts: (stats && stats.bodyParts) || [],
|
||||
equipment: (stats && stats.equipment) || [],
|
||||
featured: (list && list.items) || [],
|
||||
collections: collections || [],
|
||||
collections: cols,
|
||||
loading: false
|
||||
});
|
||||
if (isLogin()) this.loadPersonal(false);
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
// 轻量个性化:复用现有 API,纯前端聚合;silent=切 tab 静默刷新
|
||||
loadPersonal(silent) {
|
||||
if (!isLogin()) return;
|
||||
this.setData({ isLogin: true, greeting: greeting() });
|
||||
if (!silent) this.setData({ personalLoading: true });
|
||||
|
||||
const CACHE_KEY = 'fc_personal_cache';
|
||||
const cached = wx.getStorageSync(CACHE_KEY);
|
||||
const now = Date.now();
|
||||
if (cached && now - (cached.ts || 0) < 5 * 60 * 1000 && !silent) {
|
||||
this.applyPersonal(cached.data);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
planSvc.listMine().catch(() => []),
|
||||
favSvc.listExercises().catch(() => [])
|
||||
]).then(([plans, favs]) => {
|
||||
const planArr = Array.isArray(plans) ? plans : (plans && plans.items) || [];
|
||||
const favArr = Array.isArray(favs) ? favs : (favs && favs.items) || [];
|
||||
const data = {
|
||||
myPlans: planArr.slice(0, 3).map((p) => ({
|
||||
id: p.id, name: p.name, count: p.exerciseCount || 0
|
||||
})),
|
||||
myFavorites: favArr.slice(0, 6),
|
||||
todayRecommend: (this.data.featured || []).slice(0, 3).map((e) => ({
|
||||
id: e.id, name: e.displayName || e.name,
|
||||
reason: '为你精选 · 高效入门'
|
||||
}))
|
||||
};
|
||||
wx.setStorageSync(CACHE_KEY, { ts: now, data });
|
||||
this.applyPersonal(data);
|
||||
}).catch(() => {
|
||||
this.setData({ personalLoading: false });
|
||||
});
|
||||
},
|
||||
|
||||
applyPersonal(data) {
|
||||
this.setData({
|
||||
myPlans: data.myPlans,
|
||||
myFavorites: data.myFavorites,
|
||||
todayRecommend: data.todayRecommend,
|
||||
personalLoading: false
|
||||
});
|
||||
},
|
||||
|
||||
goRecommend() {
|
||||
wx.navigateTo({ url: '/pages/recommend/recommend' });
|
||||
},
|
||||
@@ -57,6 +136,17 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/search/search' });
|
||||
},
|
||||
|
||||
onPlanTap(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
if (id) wx.navigateTo({ url: '/pages/plan-detail/plan-detail?id=' + encodeURIComponent(id) });
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
const id = (e.detail && e.detail.id) || (e.currentTarget && e.currentTarget.dataset.id);
|
||||
if (!id) return;
|
||||
wx.navigateTo({ url: '/pages/detail/detail?id=' + encodeURIComponent(id) });
|
||||
},
|
||||
|
||||
onScanBodyPart(e) {
|
||||
const { value, label } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
@@ -78,12 +168,5 @@ Page({
|
||||
url: '/pages/collection-detail/collection-detail?slug=' +
|
||||
encodeURIComponent(e.currentTarget.dataset.slug)
|
||||
});
|
||||
},
|
||||
|
||||
onExercise(e) {
|
||||
if (!e || !e.detail || !e.detail.id) return;
|
||||
wx.navigateTo({
|
||||
url: '/pages/detail/detail?id=' + encodeURIComponent(e.detail.id)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"usingComponents": {
|
||||
"exercise-card": "/components/exercise-card/index",
|
||||
"section-header": "/components/section-header/index",
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
"bottom-nav": "/components/bottom-nav/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,64 @@
|
||||
<view class="hero__sub">精准匹配你的目标肌群,让每一次训练都更高效</view>
|
||||
|
||||
<view class="search-pill" bindtap="onSearch">
|
||||
<text class="search-pill__icon">🔍</text>
|
||||
<fc-icon name="search" size="28" color="var(--text-2)" />
|
||||
<text class="search-pill__ph">搜索动作 / 器械 / 部位</text>
|
||||
</view>
|
||||
|
||||
<view class="cta" bindtap="goRecommend">
|
||||
<text class="cta__icon">✨</text>
|
||||
<fc-icon name="sparkles" size="24" />
|
||||
<text class="cta__txt">智能推荐</text>
|
||||
<text class="cta__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 为你准备(登录后个性化) -->
|
||||
<view class="section personal" wx:if="{{isLogin}}">
|
||||
<view class="personal__head">
|
||||
<view class="personal__titlewrap">
|
||||
<view class="eyebrow">FOR YOU</view>
|
||||
<view class="personal__hi">{{greeting}}准备好训练了吗?</view>
|
||||
</view>
|
||||
<fc-icon name="sparkles" size="22" color="var(--accent)" />
|
||||
</view>
|
||||
|
||||
<view class="personal__block" wx:if="{{myPlans.length}}">
|
||||
<view class="personal__sub">我的计划</view>
|
||||
<view class="plan-row">
|
||||
<view class="plan-chip" wx:for="{{myPlans}}" wx:key="id" data-id="{{item.id}}" bindtap="onExercise">
|
||||
<fc-icon name="clipboard-list" size="20" color="var(--accent)" />
|
||||
<view class="plan-chip__body">
|
||||
<text class="plan-chip__name">{{item.name}}</text>
|
||||
<text class="plan-chip__meta">{{item.count}} 个动作</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="personal__block" wx:if="{{todayRecommend.length}}">
|
||||
<view class="personal__sub">今日推荐</view>
|
||||
<view class="rec-row">
|
||||
<view class="rec-item" wx:for="{{todayRecommend}}" wx:key="id" data-id="{{item.id}}" catch:tap="onExercise">
|
||||
<view class="rec-item__body">
|
||||
<text class="rec-item__name">{{item.name}}</text>
|
||||
<text class="rec-item__reason">{{item.reason}}</text>
|
||||
</view>
|
||||
<fc-icon name="chevron-right" size="18" color="var(--text-3)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="personal__block" wx:if="{{myFavorites.length}}">
|
||||
<view class="personal__sub">我的收藏</view>
|
||||
<scroll-view scroll-x class="hscroll fav-scroll">
|
||||
<view class="fav-card" wx:for="{{myFavorites}}" wx:key="id" data-id="{{item.id}}" catch:tap="onExercise">
|
||||
<image class="fav-card__img" src="{{item.image}}" mode="aspectFill" lazy-load />
|
||||
<text class="fav-card__name">{{item.displayName || item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口 2x3 -->
|
||||
<view class="section quick">
|
||||
<view class="quick__grid">
|
||||
@@ -26,7 +73,7 @@
|
||||
class="quick__tile"
|
||||
data-url="{{item.url}}"
|
||||
bindtap="onQuick">
|
||||
<text class="quick__emoji">{{item.emoji}}</text>
|
||||
<fc-icon name="{{item.icon}}" size="28" color="var(--text)" />
|
||||
<text class="quick__label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -54,7 +101,7 @@
|
||||
style="background: {{item.color}}22; border-color: {{item.color}}55;"
|
||||
data-slug="{{item.slug}}"
|
||||
bindtap="onCollection">
|
||||
<text class="prog__emoji">{{item.emoji}}</text>
|
||||
<fc-icon name="{{item.icon}}" size="36" color="{{item.color}}" />
|
||||
<text class="prog__name" style="color: {{item.color}};">{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
@@ -2,59 +2,92 @@
|
||||
|
||||
.hero {
|
||||
padding: 20rpx 40rpx 48rpx;
|
||||
background: linear-gradient(150deg, #1C2A22 0%, #0E1110 70%);
|
||||
background: linear-gradient(160deg, var(--surface) 0%, var(--bg) 70%);
|
||||
border-bottom-left-radius: 48rpx;
|
||||
border-bottom-right-radius: 48rpx;
|
||||
}
|
||||
.hero__eyebrow { color: var(--gold); font-size: 22rpx; letter-spacing: 4rpx; }
|
||||
.hero__title { font-size: 56rpx; font-weight: 700; letter-spacing: 2rpx; margin-top: 14rpx; color: var(--text); }
|
||||
.hero__sub { font-size: 26rpx; color: var(--text-2); margin-top: 14rpx; }
|
||||
.hero__eyebrow { color: var(--accent); font-size: var(--fs-xs); letter-spacing: var(--ls-eyebrow); font-weight: var(--fw-semibold); }
|
||||
.hero__title { font-size: var(--fs-3xl); font-weight: 700; letter-spacing: var(--ls-title); margin-top: 14rpx; color: var(--text); }
|
||||
.hero__sub { font-size: var(--fs-sm); color: var(--text-2); margin-top: 14rpx; }
|
||||
|
||||
.search-pill {
|
||||
margin-top: 36rpx;
|
||||
display: flex; align-items: center;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 22rpx 30rpx;
|
||||
border-radius: var(--radius-pill); padding: 22rpx 30rpx;
|
||||
}
|
||||
.search-pill__icon { font-size: 30rpx; margin-right: 16rpx; }
|
||||
.search-pill__ph { color: var(--text-3); font-size: 28rpx; }
|
||||
.search-pill__ph { color: var(--text-3); font-size: var(--fs-base); }
|
||||
|
||||
.cta {
|
||||
margin-top: 24rpx;
|
||||
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: 28rpx;
|
||||
box-shadow: 0 12rpx 30rpx rgba(217,179,108,0.28);
|
||||
color: var(--accent-on); font-weight: 700; font-size: var(--fs-lg);
|
||||
border-radius: var(--radius-pill); padding: 28rpx;
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
.cta__icon { margin-right: 12rpx; }
|
||||
.cta__arrow { margin-left: 12rpx; font-size: 36rpx; }
|
||||
|
||||
.quick__grid { display: flex; flex-wrap: wrap; gap: 18rpx; }
|
||||
.quick__grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
.quick__tile {
|
||||
width: calc((100% - 36rpx) / 3);
|
||||
width: calc((100% - 32rpx) / 3);
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 28rpx 18rpx;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
position: relative; overflow: hidden;
|
||||
transition: background var(--motion-fast) var(--ease);
|
||||
}
|
||||
.quick__tile:active { background: var(--surface-2); }
|
||||
.quick__tile::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: radial-gradient(circle at 50% 0%, var(--gold-soft), transparent 70%);
|
||||
}
|
||||
.quick__emoji { font-size: 48rpx; position: relative; z-index: 1; }
|
||||
.quick__label { font-size: 26rpx; margin-top: 12rpx; color: var(--text); position: relative; z-index: 1; }
|
||||
|
||||
/* ===== 为你准备(个性化) ===== */
|
||||
.personal__head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.personal__hi { font-size: var(--fs-xl); font-weight: 600; color: var(--text); margin-top: 6rpx; }
|
||||
.personal__sub { font-size: var(--fs-sm); color: var(--text-2); font-weight: 600; margin: 28rpx 0 16rpx; }
|
||||
|
||||
.plan-row { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.plan-chip {
|
||||
display: flex; align-items: center; gap: 16rpx;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 20rpx 24rpx;
|
||||
}
|
||||
.plan-chip:active { background: var(--surface-2); }
|
||||
.plan-chip__body { display: flex; flex-direction: column; }
|
||||
.plan-chip__name { font-size: var(--fs-base); color: var(--text); font-weight: 600; }
|
||||
.plan-chip__meta { font-size: 22rpx; color: var(--text-3); margin-top: 4rpx; }
|
||||
|
||||
.rec-row { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.rec-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
background: var(--surface); border: 1rpx solid var(--line);
|
||||
border-radius: var(--radius); padding: 20rpx 24rpx;
|
||||
}
|
||||
.rec-item:active { background: var(--surface-2); }
|
||||
.rec-item__body { display: flex; flex-direction: column; }
|
||||
.rec-item__name { font-size: var(--fs-base); color: var(--text); font-weight: 600; }
|
||||
.rec-item__reason { font-size: 22rpx; color: var(--accent); margin-top: 4rpx; }
|
||||
|
||||
.fav-scroll { white-space: nowrap; margin-top: 4rpx; }
|
||||
.fav-card { display: inline-flex; flex-direction: column; width: 200rpx; margin-right: 16rpx; vertical-align: top; }
|
||||
.fav-card__img {
|
||||
width: 200rpx; height: 200rpx; border-radius: var(--radius);
|
||||
background: var(--surface-2); border: 1rpx solid var(--line);
|
||||
}
|
||||
.fav-card__name { font-size: 24rpx; color: var(--text-2); margin-top: 10rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.hscroll { white-space: nowrap; margin-top: 8rpx; }
|
||||
.hscroll__item { display: inline-block; width: 300rpx; margin-right: 20rpx; vertical-align: top; }
|
||||
|
||||
.chip-scroll { white-space: nowrap; margin-top: 8rpx; }
|
||||
.prog {
|
||||
display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-width: 160rpx; padding: 24rpx 28rpx; margin-right: 18rpx;
|
||||
min-width: 160rpx; padding: 24rpx 28rpx; margin-right: 16rpx;
|
||||
border-radius: var(--radius-sm); border: 1rpx solid;
|
||||
}
|
||||
.prog__emoji { font-size: 44rpx; }
|
||||
.prog__name { font-size: 26rpx; font-weight: 700; margin-top: 10rpx; }
|
||||
|
||||
.bp-grid { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
||||
@@ -70,9 +103,9 @@
|
||||
.eq {
|
||||
display: inline-flex; align-items: center; gap: 12rpx;
|
||||
background: var(--surface-2); border: 1rpx solid var(--line);
|
||||
border-radius: 999rpx; padding: 16rpx 28rpx; margin-right: 16rpx;
|
||||
border-radius: var(--radius-pill); padding: 16rpx 28rpx; margin-right: 16rpx;
|
||||
}
|
||||
.eq__name { font-size: 26rpx; color: var(--text); }
|
||||
.eq__count { font-size: 22rpx; color: var(--gold); }
|
||||
.eq__count { font-size: 22rpx; color: var(--accent); }
|
||||
|
||||
.empty { color: var(--text-3); font-size: 26rpx; text-align: center; padding: 40rpx 0; }
|
||||
.empty { color: var(--text-3); font-size: var(--fs-sm); text-align: center; padding: 40rpx 0; }
|
||||
|
||||
Reference in New Issue
Block a user