xsh_1997 1 неделя назад
Родитель
Сommit
f2f0b4b457
2 измененных файлов с 239 добавлено и 54 удалено
  1. 2 1
      shop-app/pages/index/index.vue
  2. 237 53
      shop-app/subpackage/category/level1.vue

+ 2 - 1
shop-app/pages/index/index.vue

@@ -247,7 +247,8 @@ function onCategoryTap(item) {
247
 	uni.navigateTo({
247
 	uni.navigateTo({
248
 		url:
248
 		url:
249
 			`${PAGE_CATEGORY_LEVEL1}?level1Id=${item.categoryId}` +
249
 			`${PAGE_CATEGORY_LEVEL1}?level1Id=${item.categoryId}` +
250
-			`&level1Name=${encodeURIComponent(item.categoryName || '')}`
250
+			`&level1Name=${encodeURIComponent(item.categoryName || '')}` +
251
+			`&level1Pic=${encodeURIComponent(item.categoryPic || '')}`
251
 	})
252
 	})
252
 }
253
 }
253
 
254
 

+ 237 - 53
shop-app/subpackage/category/level1.vue

@@ -1,29 +1,53 @@
1
 <template>
1
 <template>
2
 	<view class="page-b">
2
 	<view class="page-b">
3
 		<search-entry />
3
 		<search-entry />
4
-		<view v-if="level1Name" class="page-b__title">
5
-			<text>{{ level1Name }}</text>
6
-		</view>
7
 
4
 
8
-		<view v-if="tabsLoading" class="page-b__hint">
9
-			<u-loading-icon mode="circle" size="20" />
10
-		</view>
11
-		<view v-else-if="!tabList.length" class="page-b__hint">
12
-			<u-empty mode="list" :text="tabsFailed ? '分类加载失败' : '暂无子分类'" icon-size="64" />
13
-		</view>
14
-		<scroll-view v-else class="page-b__tabs" scroll-x show-scrollbar="false">
15
-			<view class="tabs-inner">
16
-				<view
17
-					v-for="(tab, index) in tabList"
18
-					:key="tab.categoryId"
19
-					class="tab-item"
20
-					:class="{ 'tab-item--active': activeTabIndex === index }"
21
-					@click="onTabChange(index)"
22
-				>
23
-					<text>{{ tab.categoryName }}</text>
5
+		<view v-if="level1Name" class="cat-panel">
6
+			<!-- 手风琴标题:一级分类,点击展开/收起二级 -->
7
+			<view class="l1-hero" @click="toggleAccordion">
8
+				<image class="l1-hero__pic" :src="level1DisplayPic" mode="aspectFill" />
9
+				<view class="l1-hero__info">
10
+					<view class="l1-hero__title-row">
11
+						<text class="l1-hero__name">{{ level1Name }}</text>
12
+						<text v-if="level1IsHot" class="l1-hero__hot">热门</text>
13
+					</view>
14
+					<text class="l1-hero__sub">{{ level1SubText }}</text>
15
+				</view>
16
+				<view class="l1-hero__arrow" :class="{ 'l1-hero__arrow--open': accordionOpen }">
17
+					<u-icon name="arrow-down" color="#689f38" size="14" />
24
 				</view>
18
 				</view>
25
 			</view>
19
 			</view>
26
-		</scroll-view>
20
+
21
+			<!-- 手风琴内容:二级分类宫格 -->
22
+			<view
23
+				class="accordion-body"
24
+				:class="{ 'accordion-body--open': accordionOpen }"
25
+			>
26
+				<view class="accordion-body__inner">
27
+					<view v-if="tabsLoading" class="cat-panel__hint">
28
+						<u-loading-icon mode="circle" size="20" />
29
+					</view>
30
+					<view v-else-if="!tabList.length" class="cat-panel__hint">
31
+						<u-empty mode="list" :text="tabsFailed ? '分类加载失败' : '暂无子分类'" icon-size="64" />
32
+					</view>
33
+					<view v-else class="l2-grid">
34
+						<view
35
+							v-for="(tab, index) in tabList"
36
+							:key="tab.categoryId"
37
+							class="l2-item"
38
+							:class="{ 'l2-item--active': activeTabIndex === index }"
39
+							@click.stop="onTabChange(index)"
40
+						>
41
+							<view class="l2-item__pic-wrap">
42
+								<image class="l2-item__pic" :src="tab.displayPic" mode="aspectFill" />
43
+								<text v-if="tab.isHot" class="l2-item__hot">热</text>
44
+							</view>
45
+							<text class="l2-item__name">{{ tab.categoryName }}</text>
46
+						</view>
47
+					</view>
48
+				</view>
49
+			</view>
50
+		</view>
27
 
51
 
28
 		<goods-list-block
52
 		<goods-list-block
29
 			v-if="activeCategoryId"
53
 			v-if="activeCategoryId"
@@ -40,15 +64,20 @@
40
 <script setup>
64
 <script setup>
41
 import { ref, computed } from 'vue'
65
 import { ref, computed } from 'vue'
42
 import { onLoad, onShow } from '@dcloudio/uni-app'
66
 import { onLoad, onShow } from '@dcloudio/uni-app'
43
-import { getLevel2Tabs } from '@/api/category'
44
-import { mapLevel2Tabs } from '@/utils/categoryDisplay'
67
+import { getCategoryTree, getLevel2Tabs } from '@/api/category'
68
+import { mapCategoryTree, mapLevel2Tabs } from '@/utils/categoryDisplay'
69
+import { resolveFileUrl } from '@/utils/image'
45
 import { DEFAULT_SORT_BY } from '@/constants/categorySort'
70
 import { DEFAULT_SORT_BY } from '@/constants/categorySort'
46
 import SearchEntry from '@/components/mall/SearchEntry.vue'
71
 import SearchEntry from '@/components/mall/SearchEntry.vue'
47
 import GoodsListBlock from '@/components/category/GoodsListBlock.vue'
72
 import GoodsListBlock from '@/components/category/GoodsListBlock.vue'
48
 import { goGoodsDetail } from '@/utils/goodsDetail'
73
 import { goGoodsDetail } from '@/utils/goodsDetail'
49
 
74
 
75
+const CATEGORY_PLACEHOLDER = '/static/logo.png'
76
+
50
 const level1Id = ref('')
77
 const level1Id = ref('')
51
 const level1Name = ref('')
78
 const level1Name = ref('')
79
+const level1Pic = ref('')
80
+const level1IsHot = ref(false)
52
 const tabList = ref([])
81
 const tabList = ref([])
53
 const activeTabIndex = ref(0)
82
 const activeTabIndex = ref(0)
54
 const tabsLoading = ref(true)
83
 const tabsLoading = ref(true)
@@ -56,23 +85,68 @@ const tabsFailed = ref(false)
56
 const sortBy = ref(DEFAULT_SORT_BY)
85
 const sortBy = ref(DEFAULT_SORT_BY)
57
 const scrollTopKey = ref(0)
86
 const scrollTopKey = ref(0)
58
 const goodsScrollHeight = ref('500px')
87
 const goodsScrollHeight = ref('500px')
88
+/** 手风琴默认展开 */
89
+const accordionOpen = ref(true)
59
 
90
 
60
 const activeCategoryId = computed(() => {
91
 const activeCategoryId = computed(() => {
61
 	const tab = tabList.value[activeTabIndex.value]
92
 	const tab = tabList.value[activeTabIndex.value]
62
 	return tab ? tab.categoryId : ''
93
 	return tab ? tab.categoryId : ''
63
 })
94
 })
64
 
95
 
96
+const level1DisplayPic = computed(
97
+	() => resolveFileUrl(level1Pic.value) || CATEGORY_PLACEHOLDER
98
+)
99
+
100
+const activeTabName = computed(() => {
101
+	const tab = tabList.value[activeTabIndex.value]
102
+	return tab ? tab.categoryName : ''
103
+})
104
+
105
+const level1SubText = computed(() => {
106
+	if (tabsLoading.value) return '子分类加载中…'
107
+	if (tabsFailed.value) return '子分类加载失败'
108
+	if (!tabList.value.length) return '暂无子分类'
109
+	if (!accordionOpen.value && activeTabName.value) {
110
+		return `当前:${activeTabName.value} · 点击展开子分类`
111
+	}
112
+	const n = tabList.value.length
113
+	return `共 ${n} 个子分类,点击选择`
114
+})
115
+
65
 function calcScrollHeight() {
116
 function calcScrollHeight() {
66
 	try {
117
 	try {
67
 		const sys = uni.getSystemInfoSync()
118
 		const sys = uni.getSystemInfoSync()
68
 		const h = sys.windowHeight || 600
119
 		const h = sys.windowHeight || 600
69
-		// 搜索 + 标题 + Tab 约 200px
70
-		goodsScrollHeight.value = `${h - 200}px`
120
+		// 搜索 + 手风琴面板(展开更高)+ 排序栏
121
+		const panelPx = accordionOpen.value ? 300 : 170
122
+		goodsScrollHeight.value = `${h - panelPx}px`
71
 	} catch (e) {
123
 	} catch (e) {
72
 		goodsScrollHeight.value = '500px'
124
 		goodsScrollHeight.value = '500px'
73
 	}
125
 	}
74
 }
126
 }
75
 
127
 
128
+function toggleAccordion() {
129
+	accordionOpen.value = !accordionOpen.value
130
+	calcScrollHeight()
131
+}
132
+
133
+/** 补全一级分类图标(路由未带图时从分类树读取) */
134
+async function loadLevel1Meta() {
135
+	if (!level1Id.value) return
136
+	if (level1Pic.value && level1Name.value) return
137
+	try {
138
+		const res = await getCategoryTree()
139
+		const tree = mapCategoryTree(res.data || [])
140
+		const node = tree.find((n) => String(n.categoryId) === String(level1Id.value))
141
+		if (!node) return
142
+		if (!level1Name.value) level1Name.value = node.categoryName
143
+		if (!level1Pic.value) level1Pic.value = node.categoryPic || ''
144
+		if (!level1IsHot.value) level1IsHot.value = node.isHot
145
+	} catch (e) {
146
+		// 静默失败,保留路由传入的名称
147
+	}
148
+}
149
+
76
 async function loadTabs() {
150
 async function loadTabs() {
77
 	if (!level1Id.value) return
151
 	if (!level1Id.value) return
78
 	tabsLoading.value = true
152
 	tabsLoading.value = true
@@ -105,6 +179,8 @@ function onGoodsClick(item) {
105
 onLoad((options) => {
179
 onLoad((options) => {
106
 	level1Id.value = options.level1Id || ''
180
 	level1Id.value = options.level1Id || ''
107
 	level1Name.value = decodeURIComponent(options.level1Name || '')
181
 	level1Name.value = decodeURIComponent(options.level1Name || '')
182
+	level1Pic.value = options.level1Pic ? decodeURIComponent(options.level1Pic) : ''
183
+	level1IsHot.value = options.level1Hot === '1'
108
 	if (level1Name.value) {
184
 	if (level1Name.value) {
109
 		uni.setNavigationBarTitle({ title: level1Name.value })
185
 		uni.setNavigationBarTitle({ title: level1Name.value })
110
 	}
186
 	}
@@ -112,6 +188,7 @@ onLoad((options) => {
112
 })
188
 })
113
 
189
 
114
 onShow(() => {
190
 onShow(() => {
191
+	loadLevel1Meta()
115
 	loadTabs()
192
 	loadTabs()
116
 })
193
 })
117
 </script>
194
 </script>
@@ -120,49 +197,156 @@ onShow(() => {
120
 .page-b {
197
 .page-b {
121
 	display: flex;
198
 	display: flex;
122
 	flex-direction: column;
199
 	flex-direction: column;
123
-	height: calc(100vh - 188rpx);
200
+	height: calc(100vh - 88rpx);
124
 	background: #f5f6f8;
201
 	background: #f5f6f8;
125
 }
202
 }
126
-.page-b__title {
127
-	padding: 8rpx 24rpx 0;
128
-	font-size: 30rpx;
129
-	font-weight: 600;
130
-	color: #222;
203
+/* 一二级分类合并面板 */
204
+.cat-panel {
205
+	margin: 16rpx 24rpx 0;
206
+	background: #fff;
207
+	border-radius: 16rpx;
208
+	overflow: hidden;
209
+	box-shadow: 0 4rpx 20rpx rgba(46, 125, 50, 0.08);
131
 }
210
 }
132
-.page-b__hint {
133
-	padding: 40rpx 24rpx;
211
+.l1-hero {
134
 	display: flex;
212
 	display: flex;
135
-	justify-content: center;
213
+	align-items: center;
214
+	padding: 20rpx 24rpx;
215
+	background: linear-gradient(135deg, #e8f5e9 0%, #f9fdf9 55%, #fff 100%);
136
 }
216
 }
137
-.page-b__tabs {
217
+.l1-hero__pic {
218
+	width: 80rpx;
219
+	height: 80rpx;
220
+	border-radius: 18rpx;
138
 	background: #fff;
221
 	background: #fff;
222
+	flex-shrink: 0;
223
+	box-shadow: 0 4rpx 12rpx rgba(46, 125, 50, 0.12);
224
+}
225
+.l1-hero__info {
226
+	flex: 1;
227
+	min-width: 0;
228
+	margin-left: 24rpx;
229
+}
230
+.l1-hero__title-row {
231
+	display: flex;
232
+	align-items: center;
233
+	gap: 12rpx;
234
+}
235
+.l1-hero__name {
236
+	flex: 1;
237
+	min-width: 0;
238
+	font-size: 32rpx;
239
+	font-weight: 700;
240
+	color: #1b5e20;
139
 	white-space: nowrap;
241
 	white-space: nowrap;
140
-	border-bottom: 1rpx solid #f0f0f0;
242
+	overflow: hidden;
243
+	text-overflow: ellipsis;
244
+}
245
+.l1-hero__arrow {
246
+	flex-shrink: 0;
247
+	margin-left: 12rpx;
248
+	width: 40rpx;
249
+	height: 40rpx;
250
+	display: flex;
251
+	align-items: center;
252
+	justify-content: center;
253
+	border-radius: 50%;
254
+	background: rgba(104, 159, 56, 0.12);
255
+	transition: transform 0.25s ease;
256
+}
257
+.l1-hero__arrow--open {
258
+	transform: rotate(180deg);
259
+}
260
+.l1-hero__hot {
261
+	flex-shrink: 0;
262
+	padding: 4rpx 14rpx;
263
+	font-size: 20rpx;
264
+	color: #fff;
265
+	background: linear-gradient(90deg, #ff9800, #f57c00);
266
+	border-radius: 20rpx;
267
+}
268
+.l1-hero__sub {
269
+	display: block;
270
+	margin-top: 10rpx;
271
+	font-size: 24rpx;
272
+	color: #689f38;
273
+	line-height: 1.4;
274
+}
275
+.cat-panel__hint {
276
+	padding: 24rpx;
277
+	display: flex;
278
+	justify-content: center;
279
+}
280
+/* 手风琴展开区 */
281
+.accordion-body {
282
+	max-height: 0;
283
+	overflow: hidden;
284
+	transition: max-height 0.28s ease;
285
+	border-top: 0 solid #f0f0f0;
141
 }
286
 }
142
-.tabs-inner {
143
-	display: inline-flex;
144
-	padding: 0 16rpx;
287
+.accordion-body--open {
288
+	max-height: 1200rpx;
289
+	border-top-width: 1rpx;
145
 }
290
 }
146
-.tab-item {
147
-	display: inline-flex;
291
+.accordion-body__inner {
292
+	background: #fafbfa;
293
+}
294
+/* 二级分类:手风琴内四列小图标宫格 */
295
+.l2-grid {
296
+	display: flex;
297
+	flex-wrap: wrap;
298
+	padding: 16rpx 8rpx 12rpx;
299
+}
300
+.l2-item {
301
+	width: 25%;
302
+	display: flex;
303
+	flex-direction: column;
148
 	align-items: center;
304
 	align-items: center;
149
-	padding: 24rpx 28rpx;
150
-	font-size: 28rpx;
151
-	color: #666;
305
+	padding: 10rpx 6rpx 14rpx;
306
+	box-sizing: border-box;
152
 }
307
 }
153
-.tab-item--active {
308
+.l2-item__pic-wrap {
309
+	position: relative;
310
+	width: 72rpx;
311
+	height: 72rpx;
312
+	border-radius: 16rpx;
313
+	overflow: hidden;
314
+	background: #f0f2f0;
315
+	border: 2rpx solid transparent;
316
+	box-sizing: border-box;
317
+}
318
+.l2-item--active .l2-item__pic-wrap {
319
+	border-color: #2e7d32;
320
+	background: #e8f5e9;
321
+}
322
+.l2-item--active .l2-item__name {
154
 	color: #2e7d32;
323
 	color: #2e7d32;
155
 	font-weight: 600;
324
 	font-weight: 600;
156
-	position: relative;
157
 }
325
 }
158
-.tab-item--active::after {
159
-	content: '';
326
+.l2-item__pic {
327
+	width: 100%;
328
+	height: 100%;
329
+}
330
+.l2-item__hot {
160
 	position: absolute;
331
 	position: absolute;
161
-	left: 28rpx;
162
-	right: 28rpx;
163
-	bottom: 8rpx;
164
-	height: 4rpx;
165
-	background: #2e7d32;
166
-	border-radius: 2rpx;
332
+	top: 0;
333
+	right: 0;
334
+	padding: 0 6rpx;
335
+	font-size: 16rpx;
336
+	color: #fff;
337
+	background: #ff9800;
338
+	border-radius: 0 0 0 6rpx;
339
+	line-height: 1.3;
340
+}
341
+.l2-item__name {
342
+	margin-top: 8rpx;
343
+	width: 100%;
344
+	font-size: 22rpx;
345
+	color: #333;
346
+	text-align: center;
347
+	line-height: 1.3;
348
+	white-space: nowrap;
349
+	overflow: hidden;
350
+	text-overflow: ellipsis;
167
 }
351
 }
168
 </style>
352
 </style>