xsh_1997 1 vecka sedan
förälder
incheckning
f2f0b4b457
2 ändrade filer med 239 tillägg och 54 borttagningar
  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 247
 	uni.navigateTo({
248 248
 		url:
249 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 1
 <template>
2 2
 	<view class="page-b">
3 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 18
 				</view>
25 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 52
 		<goods-list-block
29 53
 			v-if="activeCategoryId"
@@ -40,15 +64,20 @@
40 64
 <script setup>
41 65
 import { ref, computed } from 'vue'
42 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 70
 import { DEFAULT_SORT_BY } from '@/constants/categorySort'
46 71
 import SearchEntry from '@/components/mall/SearchEntry.vue'
47 72
 import GoodsListBlock from '@/components/category/GoodsListBlock.vue'
48 73
 import { goGoodsDetail } from '@/utils/goodsDetail'
49 74
 
75
+const CATEGORY_PLACEHOLDER = '/static/logo.png'
76
+
50 77
 const level1Id = ref('')
51 78
 const level1Name = ref('')
79
+const level1Pic = ref('')
80
+const level1IsHot = ref(false)
52 81
 const tabList = ref([])
53 82
 const activeTabIndex = ref(0)
54 83
 const tabsLoading = ref(true)
@@ -56,23 +85,68 @@ const tabsFailed = ref(false)
56 85
 const sortBy = ref(DEFAULT_SORT_BY)
57 86
 const scrollTopKey = ref(0)
58 87
 const goodsScrollHeight = ref('500px')
88
+/** 手风琴默认展开 */
89
+const accordionOpen = ref(true)
59 90
 
60 91
 const activeCategoryId = computed(() => {
61 92
 	const tab = tabList.value[activeTabIndex.value]
62 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 116
 function calcScrollHeight() {
66 117
 	try {
67 118
 		const sys = uni.getSystemInfoSync()
68 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 123
 	} catch (e) {
72 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 150
 async function loadTabs() {
77 151
 	if (!level1Id.value) return
78 152
 	tabsLoading.value = true
@@ -105,6 +179,8 @@ function onGoodsClick(item) {
105 179
 onLoad((options) => {
106 180
 	level1Id.value = options.level1Id || ''
107 181
 	level1Name.value = decodeURIComponent(options.level1Name || '')
182
+	level1Pic.value = options.level1Pic ? decodeURIComponent(options.level1Pic) : ''
183
+	level1IsHot.value = options.level1Hot === '1'
108 184
 	if (level1Name.value) {
109 185
 		uni.setNavigationBarTitle({ title: level1Name.value })
110 186
 	}
@@ -112,6 +188,7 @@ onLoad((options) => {
112 188
 })
113 189
 
114 190
 onShow(() => {
191
+	loadLevel1Meta()
115 192
 	loadTabs()
116 193
 })
117 194
 </script>
@@ -120,49 +197,156 @@ onShow(() => {
120 197
 .page-b {
121 198
 	display: flex;
122 199
 	flex-direction: column;
123
-	height: calc(100vh - 188rpx);
200
+	height: calc(100vh - 88rpx);
124 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 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 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 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 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 323
 	color: #2e7d32;
155 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 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 352
 </style>