xunji-moban.vue
12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
<!-- 模板大类详情页 就是进入横栏的模板,然后点击列表的模板,进入到的页面 -->
<!-- 第二层某一个模板大类的模板列表 -->
<template>
<view class="plan-detail-page">
<!-- 顶部安全区占位 -->
<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部导航栏 这里是模板大类的名字-->
<view class="header" :style="{ height: headerHeight + 'px' }">
<view class="back-btn" @click="navigateBack">
<uni-icons class="back-icon" type="left" size="28" color="#fff"></uni-icons>
</view>
<view class="title">{{ templateList.name }}</view>
</view>
<scroll-view class="content" scroll-y enable-backdrop-filter="{{false}}">
<view class="description">
<text>{{ templateList.description }}</text>
</view>
<view class="filter-bar">
<!-- 部位筛选 -->
<view class="filter-wrapper">
<view class="filter-item" @click="togglePartDropdown">
<text>{{ activePart }}</text>
<uni-icons :type="showPartDropdown ? 'up' : 'down'" size="22" color="#333"></uni-icons>
</view>
<!-- 下拉菜单 移动到这里面!! -->
<view class="dropdown-menu" v-if="showPartDropdown">
<view class="dropdown-item" :class="{ selected: activePartId === item.id }" v-for="item in partList"
:key="item.id" @click="selectPart(item)">
{{ item.title }}
</view>
</view>
</view>
<!-- 场景筛选 -->
<view class="filter-wrapper">
<view class="filter-item" @click="toggleSceneDropdown">
<text>{{ activeScene }}</text>
<uni-icons :type="showSceneDropdown ? 'up' : 'down'" size="22" color="#333"></uni-icons>
</view>
<view class="dropdown-menu" v-if="showSceneDropdown">
<view class="dropdown-item" :class="{ selected: activeSceneId === item.id }" v-for="item in sceneList"
:key="item.id" @click="selectScene(item)">
{{ item.title }}
</view>
</view>
</view>
</view>
<!-- 新增:两列网格容器 -->
<view class="grid-container">
<view class="plan-card" v-for="(items, index) in filteredTemplates" :key="index" @click="godetail(items)">
<image :src="items.urlCover || lostImage" mode="aspectFill" class="card-cover" @error="handleImageError">
</image>
<view class="card-info">
<text class="card-title">{{ items.name }}</text>
<view class="card-stats">
<view class="stat-item">
<text class="stat-number">{{ items.exerciseCount }}</text>
<text class="stat-label">动作</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ items.totalSets }}</text>
<text class="stat-label">组</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ items.totalWeight }}</text>
<text class="stat-label">kg</text>
</view>
</view>
<text class="tags-text" :maxLines="1">
{{ items.primaryMuscleNames?.join(' ') || '' }}
</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import TemplatesApi from '@/sheep/api/Template/Templates';
// import { template } from 'lodash-es';
const lostImage = "https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260316/order-empty_1773628059920.png"
const statusBarHeight = ref(0);
const headerHeight = ref(44);
const id = ref();
const templateList = ref({});
// ================== 筛选功能 ==================
const filteredTemplates = ref([]); // 筛选后列表
// 部位(从接口拿)
const partList = ref([]);
const rawPartData = ref([]);
const activePartId = ref('0');
const activePart = ref('不限');
// 场景(固定)
const activeSceneId = ref('0');
const activeScene = ref('不限');
const sceneList = ref([
{ id: '0', title: '不限' },
{ id: '1', title: '健身房' },
{ id: '2', title: '仅哑铃' },
{ id: '3', title: '仅哑铃+杠铃' },
]);
// 下拉显示控制
const showPartDropdown = ref(false);
const showSceneDropdown = ref(false);
// 返回上一页函数
const navigateBack = () => {
uni.navigateBack();
};
// 获取部位分类(接口,和首页完全一样)
const getPartCategories = async () => {
try {
const res = await TemplatesApi.getPartAllCategories();
if (res.code === 0) {
rawPartData.value = res.data;
partList.value = [
{ title: '不限', id: '0' },
...res.data.map(item => ({
title: item.name,
id: String(item.id)
}))
];
}
} catch (error) {
console.log('部位获取失败', error);
}
};
const togglePartDropdown = () => {
showPartDropdown.value = !showPartDropdown.value;
showSceneDropdown.value = false;
};
const toggleSceneDropdown = () => {
showSceneDropdown.value = !showSceneDropdown.value;
showPartDropdown.value = false;
};
const handleImageError = (e) => {
console.error('封面图加载失败,请检查路径:');
// 可选:设置默认图(需提前准备)
// plan.value.cover = '/static/images/default-plan.jpg';
};
// 加载模板大类的模板
const loadTemplates = async (id) => {
const response = await TemplatesApi.selecttemplates(id);
templateList.value = response.data;
// console.log('模板大类查到的模板列表', templateList.value)
console.log('👉 单条模板item完整数据:', JSON.stringify(templateList.value.templates[0], null, 2));
filteredTemplates.value = response.data.templates || [];
};
// 前端筛选:部位 + 场景
const doFilter = () => {
const list = templateList.value.templates || [];
const partId = activePartId.value;
const sceneId = activeSceneId.value;
console.log("👉 当前选中场景id:", sceneId);
// 全部不限 → 显示全部
if (partId === '0' && sceneId === '0') {
filteredTemplates.value = list;
return;
}
// 过滤
const result = list.filter(item => {
console.log("👉 单条场景字段:", item.scene, item.fitScene);
// 1. 部位匹配
let partMatch = true;
if (partId !== '0') {
const targetPart = partList.value.find(p => p.id === partId);
partMatch = item.primaryMuscleNames?.includes(targetPart.title);
}
// 2. 场景匹配
let sceneMatch = true;
if (sceneId !== '0') {
sceneMatch = String(item.scene) === sceneId;
}
return partMatch && sceneMatch;
});
filteredTemplates.value = result;
};
// 选择部位
const selectPart = (item) => {
activePart.value = item.title;
activePartId.value = item.id;
showPartDropdown.value = false;
doFilter();
};
// 选择场景
const selectScene = (item) => {
activeScene.value = item.title;
activeSceneId.value = item.id;
showSceneDropdown.value = false;
doFilter();
};
// 关闭所有下拉
const closeAllDropdowns = () => {
showPartDropdown.value = false;
showSceneDropdown.value = false;
};
//查看模板详情
const godetail = (items) => {
uni.navigateTo({
url: `/pages4/pages/xunji/xunji-moban-xiangqing?id=${items.id}`,
});
};
// 这里收到模板大类传来的id,用它来查询模板大类包含的模板
onLoad((options) => {
id.value = options.id;
loadTemplates(id.value);
});
onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight;
getPartCategories();
});
</script>
<style lang="scss" scoped>
/* 样式保持不变 */
.plan-detail-page {
width: 100%;
height: 100%;
background-color: #f5f5f5;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.status-bar-placeholder {
width: 100%;
background-color: #1a1a1a;
}
.header {
width: 100%;
position: relative;
background-color: #1a1a1a;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
font-weight: 600;
z-index: 10;
}
.back-btn {
position: absolute;
left: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 11;
color: #fff;
}
.back-icon {
font-size: 32rpx;
color: #fff;
font-weight: bold;
}
.content {
flex: 1;
padding-top: 20rpx;
}
/* 新增:两列网格布局 */
.grid-container {
display: grid;
// 核心:两列等宽布局
grid-template-columns: repeat(2, 1fr);
// 列之间的间距(左右两个卡片的空隙)
column-gap: 20rpx;
// 行之间的间距(上下两个卡片的空隙)
row-gap: 20rpx;
// 适配小程序安全区域,避免贴边
padding-bottom: 40rpx;
padding: 0 20rpx 40rpx;
}
/* 调整卡片样式,适配网格布局 */
.plan-card {
// 移除原来的 margin-top,改用 grid 的 row-gap 控制间距
margin-top: 0;
width: 100%;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
position: relative;
}
.card-cover {
width: 100%;
// 保持你原来的高度,和参考图一致
height: 300rpx;
}
.description {
padding: 30rpx 30rpx 20rpx;
color: #666;
font-size: 28rpx;
line-height: 46rpx;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.card-info {
// 核心优化:增强背景对比度 + 视觉层次
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 20rpx 24rpx;
// 渐变遮罩:从下往上由深到浅,完美衬托文字,不遮挡图片主体
background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.6) 60%, rgba(0, 0, 0, 0) 100%);
// 文字阴影:进一步强化文字辨识度
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5);
}
.card-title {
font-size: 35rpx; // 字号微增
font-weight: 700; // 字重加粗
color: #ffffff; // 纯白保证最高对比度
margin-bottom: 30rpx;
line-height: 1.3;
}
.card-stats {
// 原来的横向布局改成纵向
display: flex;
flex-direction: column; // 关键:让三个stat-item从上到下排成一列
gap: 4rpx; // 调整每个项目之间的间距
margin-bottom: 16rpx;
// 去掉原来的align-items: center,改成左对齐
align-items: flex-start;
}
.stat-item {
text-align: left;
display: flex;
align-items: center;
gap: 4rpx;
}
.stat-number {
font-size: 32rpx; // 字号放大
font-weight: 700; // 加粗
color: #ffffff;
line-height: 1.2;
}
.stat-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9); // 提高透明度,更清晰
line-height: 1.2;
}
.card-tags {
margin-top: 8rpx;
}
.tags-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
/* 让文本不换行,超出部分由:maxLines="1"控制省略 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 筛选栏 */
.filter-bar {
padding: 20rpx 30rpx;
display: flex;
gap: 20rpx;
background-color: #fff;
}
.filter-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 30rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30rpx;
border: 1px solid #ddd;
color: #333;
font-size: 28rpx;
font-weight: 400;
gap: 8rpx;
background-color: #fff;
&:active {
background-color: #f5f5f5;
}
}
/* 筛选器容器 */
.filter-wrapper {
position: relative;
z-index: 999;
}
/* 下拉菜单 */
.dropdown-menu {
position: absolute;
top: calc(100% + 8rpx);
left: 0;
background: #fff;
border-radius: 14rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
padding: 12rpx 0;
min-width: 160rpx;
max-height: 320rpx;
overflow-y: auto;
z-index: 9999;
}
/* 下拉菜单文字 */
.dropdown-item {
padding: 16rpx 24rpx;
font-size: 28rpx;
line-height: 1.4;
color: #1a1a1a;
background-color: #fff;
white-space: nowrap;
&.selected {
background: #f0f9f4;
color: #2e9d5a;
font-weight: 500;
}
&:active {
opacity: 0.8;
}
}
</style>