hookehuyr

fix(api): 修复 OpenAPI 生成器并添加无限滚动功能

## 主要修改

### 1. 修复 OpenAPI 生成器 (scripts/generateApiFromOpenAPI.js)
- 修复 extractAPIInfo is not defined 错误
- 增强 generateReturnJSDoc 函数,支持正确解析数组类型的 data 字段
- 新增对 Array<{...}> 类型的完整字段描述生成

### 2. 优化 API 文档 JSDoc 注释
- news.js: data 字段从 any 改为详细的 Array<{...}> 类型
- home.js: 新增 home 模块 API(首页图标列表)

### 3. 添加无限滚动功能 (src/pages/material-list/index.vue)
- 实现 useReachBottom 触底加载更多
- 添加分页状态管理(currentPage, hasMore, loadingMore)
- 支持各分类独立的分页缓存
- 优化加载状态显示(加载中/没有更多了)
- 添加自定义加载动画

## 技术细节
- 使用防抖(300ms)避免频繁触发加载
- 区分首次加载和加载更多的状态
- 正确处理搜索、子分类、全部列表的分页逻辑
- 保存并恢复各分类的分页状态

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 +# 首页图标列表
2 +
3 +## OpenAPI Specification
4 +
5 +```yaml
6 +openapi: 3.0.1
7 +info:
8 + title: ''
9 + version: 1.0.0
10 +paths:
11 + /srv/:
12 + get:
13 + summary: 首页图标列表
14 + deprecated: false
15 + description: ''
16 + tags: []
17 + parameters:
18 + - name: f
19 + in: query
20 + description: ''
21 + required: true
22 + example: manulife
23 + schema:
24 + type: string
25 + - name: a
26 + in: query
27 + description: ''
28 + required: true
29 + example: home_icon
30 + schema:
31 + type: string
32 + - name: t
33 + in: query
34 + description: ''
35 + required: false
36 + example: icon
37 + schema:
38 + type: string
39 + responses:
40 + '200':
41 + description: ''
42 + content:
43 + application/json:
44 + schema:
45 + type: object
46 + properties:
47 + code:
48 + type: integer
49 + msg:
50 + type: integer
51 + data:
52 + type: array
53 + items:
54 + type: object
55 + properties:
56 + id:
57 + type: integer
58 + name:
59 + type: string
60 + seq:
61 + type: integer
62 + link:
63 + type: string
64 + icon:
65 + type: string
66 + required:
67 + - id
68 + - name
69 + - seq
70 + - link
71 + - icon
72 + x-apifox-orders:
73 + - id
74 + - name
75 + - link
76 + - icon
77 + - seq
78 + required:
79 + - code
80 + - msg
81 + - data
82 + x-apifox-orders:
83 + - code
84 + - msg
85 + - data
86 + example:
87 + code: 1
88 + msg: 0
89 + data:
90 + - id: 3134682
91 + name: 计划书
92 + seq: 1
93 + link: /pages/plan/index
94 + icon: >-
95 + https://cdn.ipadbiz.cn/space_3079606/文件_FsINuaz2bYXHzNoAsNIfTV0SnPC-.png
96 + - id: 3134691
97 + name: 入职相关
98 + seq: 2
99 + link: /pages/category-list/index?cid=3129684
100 + icon: >-
101 + https://cdn.ipadbiz.cn/space_3079606/感叹号_FnljRoegoDuH-kkSum55sKCExoIl.png
102 + - id: 3134697
103 + name: 签单相关
104 + seq: 3
105 + link: /pages/category-list/index?cid=3134692
106 + icon: >-
107 + https://cdn.ipadbiz.cn/space_3079606/勾_Fvc4L0IOPkfDqxCcTHbVtwZCjIaR.png
108 + - id: 3134699
109 + name: 家办相关
110 + seq: 4
111 + link: /pages/category-list/index?cid=3134693
112 + icon: >-
113 + https://cdn.ipadbiz.cn/space_3079606/帮助_Fmf73oRbxLMAB7ptz7DheIWdidV_.png
114 + - id: 3134702
115 + name: 产品知识库
116 + seq: 5
117 + link: /pages/category-list/index?cid=3134694
118 + icon: >-
119 + https://cdn.ipadbiz.cn/space_3079606/file-list-3-fill_Fpv2uhm46FuDgHnmvkMM89eKv4eL.png
120 + - id: 3134711
121 + name: 工具箱
122 + seq: 6
123 + link: /pages/category-list/index?cid=3134695
124 + icon: >-
125 + https://cdn.ipadbiz.cn/space_3079606/服务_Fr6i22Usft4N7F-uEQQu4VRjlWpu.png
126 + headers: {}
127 + x-apifox-name: 成功
128 + x-apifox-ordering: 0
129 + security: []
130 + x-apifox-folder: ''
131 + x-apifox-status: released
132 + x-run-in-apifox: https://app.apifox.com/web/project/7792797/apis/api-415955157-run
133 +components:
134 + schemas: {}
135 + responses: {}
136 + securitySchemes: {}
137 +servers:
138 + - url: https://manulife.onwall.cn
139 + description: 正式环境
140 +security: []
141 +
142 +```
...@@ -381,7 +381,12 @@ function generateReturnJSDoc(responseSchema) { ...@@ -381,7 +381,12 @@ function generateReturnJSDoc(responseSchema) {
381 returnDesc += ' * code: number; // 状态码\n'; 381 returnDesc += ' * code: number; // 状态码\n';
382 returnDesc += ' * msg: string; // 消息\n'; 382 returnDesc += ' * msg: string; // 消息\n';
383 383
384 - if (data && data.properties) { 384 + if (data) {
385 + const dataType = data.type || 'any';
386 + const dataDesc = data.description || data.title || '';
387 +
388 + // 处理对象类型的 data
389 + if (dataType === 'object' && data.properties) {
385 returnDesc += ' * data: {\n'; 390 returnDesc += ' * data: {\n';
386 391
387 Object.entries(data.properties).forEach(([key, value]) => { 392 Object.entries(data.properties).forEach(([key, value]) => {
...@@ -410,6 +415,28 @@ function generateReturnJSDoc(responseSchema) { ...@@ -410,6 +415,28 @@ function generateReturnJSDoc(responseSchema) {
410 }); 415 });
411 416
412 returnDesc += ' * };\n'; 417 returnDesc += ' * };\n';
418 + }
419 + // 处理数组类型的 data(你的情况)
420 + else if (dataType === 'array' && data.items && data.items.properties) {
421 + returnDesc += ' * data: Array<{\n';
422 +
423 + Object.entries(data.items.properties).forEach(([key, value]) => {
424 + const type = value.type || 'any';
425 + const desc = value.description || value.title || '';
426 + returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
427 + });
428 +
429 + returnDesc += ' * }>;\n';
430 + }
431 + // 处理简单数组类型
432 + else if (dataType === 'array' && data.items) {
433 + const itemType = data.items.type || 'any';
434 + returnDesc += ` * data: Array<${itemType}>;\n`;
435 + }
436 + // 其他类型
437 + else {
438 + returnDesc += ` * data: ${dataType};\n`;
439 + }
413 } else { 440 } else {
414 returnDesc += ' * data: any;\n'; 441 returnDesc += ' * data: any;\n';
415 } 442 }
...@@ -709,11 +736,14 @@ function compareAPIChanges(openAPIDir) { ...@@ -709,11 +736,14 @@ function compareAPIChanges(openAPIDir) {
709 try { 736 try {
710 const newDocs = parseOpenAPIPath(moduleDir); 737 const newDocs = parseOpenAPIPath(moduleDir);
711 if (newDocs && newDocs.length > 0) { 738 if (newDocs && newDocs.length > 0) {
712 - // 使用 extractAPIInfo 提取 API 信息 739 + // 显示新增接口信息
713 - const apiInfos = newDocs.map(doc => extractAPIInfo(doc)); 740 + console.log(` 包含 ${newDocs.length} 个新增接口:`);
714 - console.log(` 包含 ${apiInfos.length} 个新增接口:`); 741 + newDocs.forEach(doc => {
715 - apiInfos.forEach(api => { 742 + const path = Object.keys(doc.paths || {})[0] || '';
716 - console.log(` • ${api.method} ${api.path} - ${api.summary || api.name}`); 743 + const method = Object.keys(doc.paths?.[path] || {})[0] || '';
744 + const apiInfo = doc.paths?.[path]?.[method];
745 + const summary = apiInfo?.summary || doc.info?.title || '未命名接口';
746 + console.log(` • ${method?.toUpperCase()} ${path} - ${summary}`);
717 }); 747 });
718 } 748 }
719 } catch (error) { 749 } catch (error) {
......
1 +import { fn, fetch } from '@/api/fn';
2 +
3 +const Api = {
4 + HomeIcon: '/srv/?a=home_icon&t=icon',
5 +}
6 +
7 +/**
8 + * @description 首页图标列表
9 + * @remark
10 + * @param {Object} params 请求参数
11 + * @returns {Promise<{
12 + * code: number; // 状态码
13 + * msg: string; // 消息
14 + * data: Array<{
15 + * id: integer; //
16 + * name: string; //
17 + * seq: integer; //
18 + * link: string; //
19 + * icon: string; //
20 + * }>;
21 + * }>}
22 + */
23 +export const homeIconAPI = (params) => fn(fetch.get(Api.HomeIcon, params));
...@@ -13,7 +13,12 @@ const Api = { ...@@ -13,7 +13,12 @@ const Api = {
13 * @returns {Promise<{ 13 * @returns {Promise<{
14 * code: number; // 状态码 14 * code: number; // 状态码
15 * msg: string; // 消息 15 * msg: string; // 消息
16 - * data: any; 16 + * data: Array<{
17 + * id: integer; // 消息id
18 + * note: string; // 消息内容
19 + * created_time: string; // 发消息的时间
20 + * status: string; // send=以发送未读取,read=已读取
21 + * }>;
17 * }>} 22 * }>}
18 */ 23 */
19 export const detailAPI = (params) => fn(fetch.get(Api.Detail, params)); 24 export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
...@@ -27,7 +32,12 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params)); ...@@ -27,7 +32,12 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
27 * @returns {Promise<{ 32 * @returns {Promise<{
28 * code: number; // 状态码 33 * code: number; // 状态码
29 * msg: string; // 消息 34 * msg: string; // 消息
30 - * data: any; 35 + * data: Array<{
36 + * id: integer; // 消息id
37 + * note: string; // 消息内容
38 + * created_time: string; // 发消息的时间
39 + * status: string; // send=以发送未读取,read=已读取
40 + * }>;
31 * }>} 41 * }>}
32 */ 42 */
33 export const myListAPI = (params) => fn(fetch.get(Api.MyList, params)); 43 export const myListAPI = (params) => fn(fetch.get(Api.MyList, params));
......
...@@ -91,9 +91,20 @@ ...@@ -91,9 +91,20 @@
91 </view> 91 </view>
92 92
93 <!-- 空状态 --> 93 <!-- 空状态 -->
94 - <view v-if="currentList.length === 0"> 94 + <view v-if="currentList.length === 0 && !loading">
95 <nut-empty description="暂无相关资料" image="empty" /> 95 <nut-empty description="暂无相关资料" image="empty" />
96 </view> 96 </view>
97 +
98 + <!-- 加载更多状态 -->
99 + <view v-if="currentList.length > 0" class="load-more-container">
100 + <view v-if="loadingMore" class="load-more-loading">
101 + <view class="loading-spinner"></view>
102 + <text class="ml-[16rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
103 + </view>
104 + <view v-else-if="!hasMore" class="load-more-finished">
105 + <text class="text-[#9CA3AF] text-[24rpx]">没有更多了</text>
106 + </view>
107 + </view>
97 </view> 108 </view>
98 </view> 109 </view>
99 </view> 110 </view>
...@@ -102,7 +113,7 @@ ...@@ -102,7 +113,7 @@
102 113
103 <script setup> 114 <script setup>
104 import { ref, computed, nextTick } from 'vue' 115 import { ref, computed, nextTick } from 'vue'
105 -import { useLoad } from '@tarojs/taro' 116 +import { useLoad, useReachBottom } from '@tarojs/taro'
106 import NavHeader from '@/components/NavHeader.vue' 117 import NavHeader from '@/components/NavHeader.vue'
107 import SearchBar from '@/components/SearchBar.vue' 118 import SearchBar from '@/components/SearchBar.vue'
108 import ListItemActions from '@/components/ListItemActions/index.vue' 119 import ListItemActions from '@/components/ListItemActions/index.vue'
...@@ -123,9 +134,31 @@ const listRenderKey = ref(0) ...@@ -123,9 +134,31 @@ const listRenderKey = ref(0)
123 const loading = ref(false) 134 const loading = ref(false)
124 135
125 /** 136 /**
126 - * API 返回的原始数据 137 + * 加载更多状态
138 + * @description 区分首次加载和加载更多
139 + */
140 +const loadingMore = ref(false)
141 +
142 +/**
143 + * 每页数量
144 + */
145 +const pageSize = 10
146 +
147 +/**
148 + * 当前页码(从0开始)
149 + */
150 +const currentPage = ref(0)
151 +
152 +/**
153 + * 是否有更多数据
127 */ 154 */
128 -const data = ref(null) 155 +const hasMore = ref(true)
156 +
157 +/**
158 + * 各分类的分页状态缓存
159 + * @description Map<categoryId, { currentPage, hasMore }>
160 + */
161 +const categoryPageCache = ref(new Map())
129 162
130 /** 163 /**
131 * 初始分类ID(从页面参数获取) 164 * 初始分类ID(从页面参数获取)
...@@ -217,10 +250,18 @@ const transformDocItem = (doc) => { ...@@ -217,10 +250,18 @@ const transformDocItem = (doc) => {
217 * @param {string} params.cid - 分类ID(可选) 250 * @param {string} params.cid - 分类ID(可选)
218 * @param {string} params.child_id - 子分类ID(可选) 251 * @param {string} params.child_id - 子分类ID(可选)
219 * @param {string} params.keyword - 搜索关键词(可选) 252 * @param {string} params.keyword - 搜索关键词(可选)
253 + * @param {number} params.page - 页码(从0开始)
254 + * @param {number} params.limit - 每页数量
255 + * @param {boolean} isLoadMore - 是否为加载更多
220 */ 256 */
221 -const fetchMaterialList = async (params = {}) => { 257 +const fetchMaterialList = async (params = {}, isLoadMore = false) => {
222 try { 258 try {
259 + // 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
260 + if (isLoadMore) {
261 + loadingMore.value = true
262 + } else {
223 loading.value = true 263 loading.value = true
264 + }
224 265
225 console.log('[Material List] 请求参数:', params) 266 console.log('[Material List] 请求参数:', params)
226 267
...@@ -229,7 +270,7 @@ const fetchMaterialList = async (params = {}) => { ...@@ -229,7 +270,7 @@ const fetchMaterialList = async (params = {}) => {
229 270
230 if (res.code === 1 && res.data) { 271 if (res.code === 1 && res.data) {
231 // 如果是初始请求(没有 child_id),保存完整的分类信息 272 // 如果是初始请求(没有 child_id),保存完整的分类信息
232 - if (!params.child_id) { 273 + if (!params.child_id && !params.keyword) {
233 data.value = res.data 274 data.value = res.data
234 console.log('[Material List] 数据:', res.data) 275 console.log('[Material List] 数据:', res.data)
235 console.log('[Material List] 分类数量:', res.data.children?.length) 276 console.log('[Material List] 分类数量:', res.data.children?.length)
...@@ -238,22 +279,55 @@ const fetchMaterialList = async (params = {}) => { ...@@ -238,22 +279,55 @@ const fetchMaterialList = async (params = {}) => {
238 // 处理并缓存"全部"列表 279 // 处理并缓存"全部"列表
239 if (res.data.list?.length) { 280 if (res.data.list?.length) {
240 const allListData = res.data.list.map(transformDocItem) 281 const allListData = res.data.list.map(transformDocItem)
282 +
283 + if (isLoadMore) {
284 + // 加载更多:追加数据
285 + allList.value = [...allList.value, ...allListData]
286 + categoryListCache.value.set('all', allList.value)
287 + } else {
288 + // 首次加载:替换数据
241 allList.value = allListData 289 allList.value = allListData
242 categoryListCache.value.set('all', allListData) 290 categoryListCache.value.set('all', allListData)
243 } 291 }
292 +
293 + // 判断是否还有更多数据
294 + hasMore.value = allListData.length >= params.limit
295 + } else {
296 + if (isLoadMore) {
297 + hasMore.value = false
298 + } else {
299 + allList.value = []
300 + }
301 + }
244 } else { 302 } else {
245 - // 是子分类请求,缓存该分类的列表数据 303 + // 是子分类请求或搜索请求
246 - const childId = params.child_id 304 + const cacheKey = params.child_id || params.keyword || 'search'
305 +
247 if (res.data.list?.length) { 306 if (res.data.list?.length) {
248 const listData = res.data.list.map(transformDocItem) 307 const listData = res.data.list.map(transformDocItem)
249 - categoryListCache.value.set(childId, listData) 308 +
250 - // 更新当前显示的列表 309 + if (isLoadMore) {
310 + // 加载更多:追加数据
311 + const existingData = categoryListCache.value.get(cacheKey) || []
312 + const newData = [...existingData, ...listData]
313 + categoryListCache.value.set(cacheKey, newData)
314 + currentList.value = newData
315 + } else {
316 + // 首次加载:替换数据
317 + categoryListCache.value.set(cacheKey, listData)
251 currentList.value = listData 318 currentList.value = listData
319 + }
320 +
321 + // 判断是否还有更多数据
322 + hasMore.value = listData.length >= params.limit
323 + } else {
324 + if (isLoadMore) {
325 + hasMore.value = false
252 } else { 326 } else {
253 - // 该分类没有数据
254 currentList.value = [] 327 currentList.value = []
255 } 328 }
256 } 329 }
330 + }
257 } else { 331 } else {
258 Taro.showToast({ 332 Taro.showToast({
259 title: res.msg || '获取资料列表失败', 333 title: res.msg || '获取资料列表失败',
...@@ -269,8 +343,12 @@ const fetchMaterialList = async (params = {}) => { ...@@ -269,8 +343,12 @@ const fetchMaterialList = async (params = {}) => {
269 duration: 2000 343 duration: 2000
270 }) 344 })
271 } finally { 345 } finally {
346 + if (isLoadMore) {
347 + loadingMore.value = false
348 + } else {
272 loading.value = false 349 loading.value = false
273 } 350 }
351 + }
274 } 352 }
275 353
276 /** 354 /**
...@@ -289,8 +367,16 @@ useLoad(async (options) => { ...@@ -289,8 +367,16 @@ useLoad(async (options) => {
289 pageTitle.value = options.title 367 pageTitle.value = options.title
290 } 368 }
291 369
370 + // 重置分页状态
371 + currentPage.value = 0
372 + hasMore.value = true
373 +
292 // 获取资料列表(初始请求) 374 // 获取资料列表(初始请求)
293 - await fetchMaterialList({ cid: options.id }) 375 + await fetchMaterialList({
376 + cid: options.id,
377 + page: 0,
378 + limit: pageSize
379 + })
294 380
295 // 初始化当前列表为"全部"列表(等待请求完成后) 381 // 初始化当前列表为"全部"列表(等待请求完成后)
296 currentList.value = allList.value 382 currentList.value = allList.value
...@@ -304,6 +390,16 @@ const onTabClick = async (id) => { ...@@ -304,6 +390,16 @@ const onTabClick = async (id) => {
304 activeTabId.value = id 390 activeTabId.value = id
305 listVisible.value = false 391 listVisible.value = false
306 392
393 + // 恢复或初始化该分类的分页状态
394 + const pageState = categoryPageCache.value.get(id)
395 + if (pageState) {
396 + currentPage.value = pageState.currentPage
397 + hasMore.value = pageState.hasMore
398 + } else {
399 + currentPage.value = 0
400 + hasMore.value = true
401 + }
402 +
307 // 判断是否是"全部" tab 403 // 判断是否是"全部" tab
308 if (id === 'all') { 404 if (id === 'all') {
309 // 显示"全部"列表(从缓存或 allList) 405 // 显示"全部"列表(从缓存或 allList)
...@@ -315,10 +411,18 @@ const onTabClick = async (id) => { ...@@ -315,10 +411,18 @@ const onTabClick = async (id) => {
315 // 从缓存中获取 411 // 从缓存中获取
316 currentList.value = categoryListCache.value.get(id) 412 currentList.value = categoryListCache.value.get(id)
317 } else { 413 } else {
318 - // 调用接口获取该分类的列表 414 + // 调用接口获取该分类的列表(第一页)
319 await fetchMaterialList({ 415 await fetchMaterialList({
320 cid: initialCategoryId.value, 416 cid: initialCategoryId.value,
321 - child_id: id 417 + child_id: id,
418 + page: 0,
419 + limit: pageSize
420 + })
421 +
422 + // 保存分页状态
423 + categoryPageCache.value.set(id, {
424 + currentPage: 0,
425 + hasMore: hasMore.value
322 }) 426 })
323 } 427 }
324 } 428 }
...@@ -330,6 +434,68 @@ const onTabClick = async (id) => { ...@@ -330,6 +434,68 @@ const onTabClick = async (id) => {
330 } 434 }
331 435
332 /** 436 /**
437 + * 触底加载更多
438 + * @description 使用防抖避免频繁触发
439 + */
440 +let loadMoreTimer = null
441 +useReachBottom(() => {
442 + // 如果正在加载或没有更多数据,不执行
443 + if (loadingMore.value || loading.value || !hasMore.value) {
444 + return
445 + }
446 +
447 + // 防抖:300ms 内只触发一次
448 + if (loadMoreTimer) {
449 + clearTimeout(loadMoreTimer)
450 + }
451 +
452 + loadMoreTimer = setTimeout(async () => {
453 + console.log('[Material List] 触底加载更多')
454 +
455 + // 页码 +1
456 + currentPage.value += 1
457 +
458 + // 构建请求参数
459 + const params = {
460 + cid: initialCategoryId.value,
461 + page: currentPage.value,
462 + limit: pageSize
463 + }
464 +
465 + // 判断当前状态:搜索、子分类、或全部
466 + const isSearching = searchValue.value.trim() !== ''
467 +
468 + if (isSearching) {
469 + // 搜索模式
470 + params.keyword = searchValue.value.trim()
471 + if (activeTabId.value !== 'all') {
472 + params.child_id = activeTabId.value
473 + }
474 + } else {
475 + // 非搜索模式:如果当前选中的是子分类,添加 child_id 参数
476 + if (activeTabId.value !== 'all') {
477 + params.child_id = activeTabId.value
478 + }
479 + }
480 +
481 + // 加载下一页数据
482 + await fetchMaterialList(params, true) // true 表示加载更多
483 +
484 + // 保存更新后的分页状态
485 + let cacheKey
486 + if (isSearching) {
487 + cacheKey = params.keyword
488 + } else {
489 + cacheKey = activeTabId.value !== 'all' ? activeTabId.value : 'all'
490 + }
491 + categoryPageCache.value.set(cacheKey, {
492 + currentPage: currentPage.value,
493 + hasMore: hasMore.value
494 + })
495 + }, 300)
496 +})
497 +
498 +/**
333 * 搜索处理函数 499 * 搜索处理函数
334 * @description 根据 child_id 和 keyword 调用接口查询列表 500 * @description 根据 child_id 和 keyword 调用接口查询列表
335 */ 501 */
...@@ -351,16 +517,27 @@ const onSearch = async () => { ...@@ -351,16 +517,27 @@ const onSearch = async () => {
351 // 如果缓存中没有,调用接口获取 517 // 如果缓存中没有,调用接口获取
352 await fetchMaterialList({ 518 await fetchMaterialList({
353 cid: initialCategoryId.value, 519 cid: initialCategoryId.value,
354 - child_id: activeTabId.value 520 + child_id: activeTabId.value,
521 + page: 0,
522 + limit: pageSize
355 }) 523 })
356 } 524 }
357 } 525 }
526 +
527 + // 恢复分页状态
528 + const pageState = categoryPageCache.value.get(activeTabId.value)
529 + if (pageState) {
530 + currentPage.value = pageState.currentPage
531 + hasMore.value = pageState.hasMore
532 + }
358 return 533 return
359 } 534 }
360 535
361 // 构建请求参数 536 // 构建请求参数
362 const params = { 537 const params = {
363 - cid: initialCategoryId.value 538 + cid: initialCategoryId.value,
539 + page: 0,
540 + limit: pageSize
364 } 541 }
365 542
366 // 如果当前选中的是子分类,添加 child_id 参数 543 // 如果当前选中的是子分类,添加 child_id 参数
...@@ -371,6 +548,10 @@ const onSearch = async () => { ...@@ -371,6 +548,10 @@ const onSearch = async () => {
371 // 添加搜索关键词 548 // 添加搜索关键词
372 params.keyword = searchValue.value.trim() 549 params.keyword = searchValue.value.trim()
373 550
551 + // 重置分页状态
552 + currentPage.value = 0
553 + hasMore.value = true
554 +
374 // 调用接口搜索 555 // 调用接口搜索
375 try { 556 try {
376 loading.value = true 557 loading.value = true
...@@ -380,8 +561,15 @@ const onSearch = async () => { ...@@ -380,8 +561,15 @@ const onSearch = async () => {
380 if (res.data.list?.length) { 561 if (res.data.list?.length) {
381 const listData = res.data.list.map(transformDocItem) 562 const listData = res.data.list.map(transformDocItem)
382 currentList.value = listData 563 currentList.value = listData
564 +
565 + // 缓存搜索结果
566 + categoryListCache.value.set(params.keyword, listData)
567 +
568 + // 判断是否还有更多数据
569 + hasMore.value = listData.length >= pageSize
383 } else { 570 } else {
384 currentList.value = [] 571 currentList.value = []
572 + hasMore.value = false
385 } 573 }
386 } else { 574 } else {
387 Taro.showToast({ 575 Taro.showToast({
...@@ -566,4 +754,41 @@ const onDelete = (item) => { ...@@ -566,4 +754,41 @@ const onDelete = (item) => {
566 :deep(.nut-tabs__content) { 754 :deep(.nut-tabs__content) {
567 display: none; 755 display: none;
568 } 756 }
757 +
758 +// 加载更多容器
759 +.load-more-container {
760 + display: flex;
761 + justify-content: center;
762 + align-items: center;
763 + padding: 40rpx 0;
764 + min-height: 80rpx;
765 +}
766 +
767 +.load-more-loading {
768 + display: flex;
769 + align-items: center;
770 + justify-content: center;
771 +}
772 +
773 +.load-more-finished {
774 + display: flex;
775 + align-items: center;
776 + justify-content: center;
777 +}
778 +
779 +// 自定义加载动画
780 +.loading-spinner {
781 + width: 32rpx;
782 + height: 32rpx;
783 + border: 4rpx solid #E5E7EB;
784 + border-top-color: #2563EB;
785 + border-radius: 50%;
786 + animation: spin 0.8s linear infinite;
787 +}
788 +
789 +@keyframes spin {
790 + to {
791 + transform: rotate(360deg);
792 + }
793 +}
569 </style> 794 </style>
......