hookehuyr

fix(material): 修复滚动加载无法触发的问题

移除 h-screen flex-col 布局,改用页面级滚动
- NavHeader 和搜索栏使用 sticky top-0 固定
- 列表容器使用页面级滚动(移除 overflow-y-auto)
- 删除 mock 数据相关代码

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 +/*
2 + * @Date: 2026-02-06 18:10:17
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2026-02-06 18:12:50
5 + * @FilePath: /manulife-weapp/src/api/file.js
6 + * @Description: 文件相关 API
7 + */
1 import { fn, fetch } from '@/api/fn'; 8 import { fn, fetch } from '@/api/fn';
2 9
3 const Api = { 10 const Api = {
...@@ -7,11 +14,11 @@ const Api = { ...@@ -7,11 +14,11 @@ const Api = {
7 14
8 /** 15 /**
9 * @description 文档列表 16 * @description 文档列表
10 - * @remark 17 + * @remark
11 * @param {Object} params 请求参数 18 * @param {Object} params 请求参数
12 * @param {string} params.client_id (可选) 主体id 19 * @param {string} params.client_id (可选) 主体id
13 - * @param {string} params.limit (可选) 20 + * @param {string} params.limit (可选)
14 - * @param {string} params.page (可选) 21 + * @param {string} params.page (可选)
15 * @param {string} params.cid (可选) 分类id 22 * @param {string} params.cid (可选) 分类id
16 * @param {string} params.child_id (可选) 只有一层分类时,筛选数据用 23 * @param {string} params.child_id (可选) 只有一层分类时,筛选数据用
17 * @param {string} params.keyword (可选) 搜索关键词 24 * @param {string} params.keyword (可选) 搜索关键词
...@@ -49,7 +56,7 @@ const Api = { ...@@ -49,7 +56,7 @@ const Api = {
49 }>; 56 }>;
50 }>; 57 }>;
51 list: Array<{ 58 list: Array<{
52 - id: integer; // 59 + id: integer; //
53 name: string; // 附件名称 60 name: string; // 附件名称
54 value: string; // 附件地址 61 value: string; // 附件地址
55 extension: string; // 后缀名 62 extension: string; // 后缀名
...@@ -62,7 +69,9 @@ const Api = { ...@@ -62,7 +69,9 @@ const Api = {
62 * }; 69 * };
63 * }>} 70 * }>}
64 */ 71 */
65 -export const fileListAPI = (params) => fn(fetch.get(Api.FileList, params)); 72 +export const fileListAPI = (params) => {
73 + return fn(fetch.get(Api.FileList, params));
74 +};
66 75
67 /** 76 /**
68 * @description 本周热门资料 77 * @description 本周热门资料
...@@ -81,9 +90,11 @@ export const fileListAPI = (params) => fn(fetch.get(Api.FileList, params)); ...@@ -81,9 +90,11 @@ export const fileListAPI = (params) => fn(fetch.get(Api.FileList, params));
81 size: string; // 文件大小 90 size: string; // 文件大小
82 read_people_count: integer; // 学习人数 91 read_people_count: integer; // 学习人数
83 read_people_percent: number; // 学习人数比例 92 read_people_percent: number; // 学习人数比例
84 - is_favorite: string; // 93 + is_favorite: string; //
85 }>; 94 }>;
86 * }; 95 * };
87 * }>} 96 * }>}
88 */ 97 */
89 -export const weekHotAPI = (params) => fn(fetch.get(Api.WeekHot, params)); 98 +export const weekHotAPI = (params) => {
99 + return fn(fetch.get(Api.WeekHot, params));
100 +};
......
1 import { fn, fetch } from '@/api/fn'; 1 import { fn, fetch } from '@/api/fn';
2 -import { mockListAPI, mockDetailAPI } from './mock/product';
3 -
4 -// ⚠️ Mock 数据开关 - 设置为 true 使用 mock 数据,false 使用真实 API
5 -const USE_MOCK_DATA = false;
6 2
7 const Api = { 3 const Api = {
8 Detail: '/srv/?a=get_product&t=detail', 4 Detail: '/srv/?a=get_product&t=detail',
...@@ -50,10 +46,6 @@ const Api = { ...@@ -50,10 +46,6 @@ const Api = {
50 * }>} 46 * }>}
51 */ 47 */
52 export const detailAPI = (params) => { 48 export const detailAPI = (params) => {
53 - // 如果开启 Mock 数据,返回 mock 数据
54 - if (USE_MOCK_DATA) {
55 - return mockDetailAPI(params);
56 - }
57 return fn(fetch.get(Api.Detail, params)); 49 return fn(fetch.get(Api.Detail, params));
58 }; 50 };
59 51
...@@ -98,9 +90,5 @@ export const detailAPI = (params) => { ...@@ -98,9 +90,5 @@ export const detailAPI = (params) => {
98 * }>} 90 * }>}
99 */ 91 */
100 export const listAPI = (params) => { 92 export const listAPI = (params) => {
101 - // 如果开启 Mock 数据,返回 mock 数据
102 - if (USE_MOCK_DATA) {
103 - return mockListAPI(params);
104 - }
105 return fn(fetch.get(Api.List, params)); 93 return fn(fetch.get(Api.List, params));
106 }; 94 };
......
1 -/**
2 - * @description Mock 数据 - 产品中心
3 - * @note 用于测试滚动加载更多功能
4 - */
5 -
6 -// Mock 分类数据
7 -export const mockCategories = [
8 - { id: 1, name: '寿险' },
9 - { id: 2, name: '健康险' },
10 - { id: 3, name: '意外险' },
11 - { id: 4, name: '年金险' },
12 - { id: 5, name: '重疾险' }
13 -]
14 -
15 -// Mock 标签数据
16 -const mockTags = [
17 - { id: 1, name: '热销', bg_color: '#FEF3C7', text_color: '#92400E' },
18 - { id: 2, name: '新品', bg_color: '#DBEAFE', text_color: '#1E40AF' },
19 - { id: 3, name: '推荐', bg_color: '#D1FAE5', text_color: '#065F46' },
20 - { id: 4, name: '限时', bg_color: '#FEE2E2', text_color: '#991B1B' }
21 -]
22 -
23 -// 生成单个产品数据
24 -const generateProduct = (id) => {
25 - const recommendTypes = ['normal', 'hot']
26 - const recommend = recommendTypes[Math.floor(Math.random() * recommendTypes.length)]
27 -
28 - // 随机选择 1-2 个标签
29 - const tagCount = Math.floor(Math.random() * 2) + 1
30 - const tags = []
31 - for (let i = 0; i < tagCount; i++) {
32 - const tag = mockTags[Math.floor(Math.random() * mockTags.length)]
33 - if (!tags.find(t => t.id === tag.id)) {
34 - tags.push(tag)
35 - }
36 - }
37 -
38 - // 随机选择 1-2 个分类
39 - const categoryCount = Math.floor(Math.random() * 2) + 1
40 - const categories = []
41 - for (let i = 0; i < categoryCount; i++) {
42 - const category = mockCategories[Math.floor(Math.random() * mockCategories.length)]
43 - if (!categories.find(c => c.id === category.id)) {
44 - categories.push(category)
45 - }
46 - }
47 -
48 - return {
49 - id: id,
50 - product_name: `测试产品 ${id} - ${categories.map(c => c.name).join('+')}`,
51 - recommend: recommend,
52 - form_sn: `product_form_${id}`,
53 - created_time: '2025-12-01 12:00:00',
54 - categories: categories,
55 - tags: tags,
56 - // 使用 Lorem Picsum 随机图片服务(基于产品 ID 确保图片固定)
57 - cover_image: `https://picsum.photos/300/200?random=${id}`
58 - }
59 -}
60 -
61 -// 生成产品列表
62 -const generateProductList = (page, limit, cid = null) => {
63 - const start = page * limit
64 - const end = start + limit
65 -
66 - // 如果指定了分类,只返回该分类的产品
67 - let filteredProducts = []
68 - if (cid) {
69 - // 为每个分类生成固定数量的产品
70 - const categoryProducts = []
71 - for (let i = 1; i <= 50; i++) {
72 - const product = generateProduct(i)
73 - // 强制该产品属于指定分类
74 - product.categories = mockCategories.find(c => String(c.id) === String(cid))
75 - ? [mockCategories.find(c => String(c.id) === String(cid))]
76 - : [{ id: parseInt(cid), name: '测试分类' }]
77 - categoryProducts.push(product)
78 - }
79 - filteredProducts = categoryProducts
80 - } else {
81 - // 全部产品
82 - for (let i = 1; i <= 100; i++) {
83 - filteredProducts.push(generateProduct(i))
84 - }
85 - }
86 -
87 - const total = filteredProducts.length
88 - const list = filteredProducts.slice(start, end)
89 -
90 - return {
91 - list,
92 - total,
93 - hasMore: end < total
94 - }
95 -}
96 -
97 -/**
98 - * Mock 产品列表 API
99 - * @param {Object} params 请求参数
100 - * @param {string} params.page 页码(从 0 开始)
101 - * @param {string} params.limit 每页数量
102 - * @param {string} params.cid 分类 ID(可选)
103 - * @param {string} params.keyword 搜索关键词(可选)
104 - * @returns {Promise} 模拟 API 响应
105 - */
106 -export const mockListAPI = (params) => {
107 - return new Promise((resolve) => {
108 - // 模拟网络延迟(300-800ms)
109 - const delay = Math.floor(Math.random() * 500) + 300
110 -
111 - setTimeout(() => {
112 - const page = parseInt(params.page) || 0
113 - const limit = parseInt(params.limit) || 10
114 - const cid = params.cid || null
115 - const keyword = params.keyword || ''
116 -
117 - let result = generateProductList(page, limit, cid)
118 -
119 - // 如果有搜索关键词,过滤产品
120 - if (keyword) {
121 - result.list = result.list.filter(p =>
122 - p.product_name.includes(keyword)
123 - )
124 - // 搜索时重新计算总数
125 - result.total = result.list.length + Math.floor(Math.random() * 20)
126 - }
127 -
128 - resolve({
129 - code: 1,
130 - msg: 'success',
131 - data: {
132 - categories: mockCategories,
133 - list: result.list,
134 - total: result.total
135 - }
136 - })
137 - }, delay)
138 - })
139 -}
140 -
141 -/**
142 - * Mock 产品详情 API
143 - * @param {Object} params 请求参数
144 - * @param {string} params.i 产品 ID
145 - * @returns {Promise} 模拟 API 响应
146 - */
147 -export const mockDetailAPI = (params) => {
148 - return new Promise((resolve) => {
149 - const delay = Math.floor(Math.random() * 500) + 300
150 -
151 - setTimeout(() => {
152 - const id = parseInt(params.i) || 1
153 - const product = generateProduct(id)
154 -
155 - // 添加额外的详情字段
156 - product.product_description = `这是产品 ${id} 的详细描述。\n\n产品特点:\n1. 保障全面\n2. 灵活配置\n3. 理赔便捷`
157 - product.documents = [
158 - {
159 - file_url: 'https://example.com/file1.pdf',
160 - file_name: '产品条款.pdf',
161 - file_size: '1024000',
162 - file_size_formatted: '1.0 MB'
163 - },
164 - {
165 - file_url: 'https://example.com/file2.pdf',
166 - file_name: '产品说明.pdf',
167 - file_size: '512000',
168 - file_size_formatted: '512 KB'
169 - }
170 - ]
171 - product.status = 'active'
172 - product.created_by = 1
173 - product.updated_by = 1
174 - product.updated_time = '2025-12-01 12:00:00'
175 -
176 - resolve({
177 - code: 1,
178 - msg: 'success',
179 - data: product
180 - })
181 - }, delay)
182 - })
183 -}
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
3 * @Description: 资料列表页 - 已改造为 NutTabs 版本 3 * @Description: 资料列表页 - 已改造为 NutTabs 版本
4 --> 4 -->
5 <template> 5 <template>
6 - <view class="h-screen bg-[#F9FAFB] flex flex-col"> 6 + <view class="bg-[#F9FAFB]">
7 - <view class="bg-[#F9FAFB] z-10"> 7 + <!-- 固定在顶部的导航和搜索 -->
8 + <view class="bg-[#F9FAFB] sticky top-0 z-10">
8 <NavHeader :title="pageTitle" /> 9 <NavHeader :title="pageTitle" />
9 10
10 <view class="px-[32rpx] mt-[32rpx] mb-[24rpx]"> 11 <view class="px-[32rpx] mt-[32rpx] mb-[24rpx]">
...@@ -18,10 +19,7 @@ ...@@ -18,10 +19,7 @@
18 :show-clear="true" 19 :show-clear="true"
19 /> 20 />
20 </view> 21 </view>
21 - </view>
22 22
23 - <!-- Tabs Container -->
24 - <view class="flex-1 min-h-0 flex flex-col">
25 <!-- 动态显示 Tabs(仅在有分类时显示) --> 23 <!-- 动态显示 Tabs(仅在有分类时显示) -->
26 <nut-tabs v-if="hasCategories" v-model="activeTabId"> 24 <nut-tabs v-if="hasCategories" v-model="activeTabId">
27 <!-- 自定义标签栏 --> 25 <!-- 自定义标签栏 -->
...@@ -41,73 +39,73 @@ ...@@ -41,73 +39,73 @@
41 </view> 39 </view>
42 </template> 40 </template>
43 </nut-tabs> 41 </nut-tabs>
42 + </view>
44 43
45 - <!-- 列表容器(独立于 nut-tab-pane) --> 44 + <!-- 列表容器 - 页面级滚动 -->
46 - <view 45 + <view
47 - v-if="listVisible" 46 + v-if="listVisible"
48 - :key="listRenderKey" 47 + :key="listRenderKey"
49 - class="flex-1 min-h-0 overflow-y-auto px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border" 48 + class="px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
50 - > 49 + >
51 - <view class="flex flex-col gap-[24rpx]"> 50 + <view class="flex flex-col gap-[24rpx]">
52 - <view v-for="(item, index) in currentList" :key="index" 51 + <view v-for="(item, index) in currentList" :key="index"
53 - class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row" 52 + class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row"
54 - :style="{ animationDelay: `${index * 50}ms` }"> 53 + :style="{ animationDelay: `${index * 50}ms` }">
55 - 54 +
56 - <view 55 + <view
57 - class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start"> 56 + class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start">
58 - <image 57 + <image
59 - :src="getDocumentIcon(item.extension ? `file.${item.extension}` : item.fileName)" 58 + :src="getDocumentIcon(item.extension ? `file.${item.extension}` : item.fileName)"
60 - class="w-[48rpx] h-[48rpx]" 59 + class="w-[48rpx] h-[48rpx]"
61 - mode="aspectFit" 60 + mode="aspectFit"
62 - /> 61 + />
63 - </view> 62 + </view>
64 63
65 - <view class="flex-1 min-w-0"> 64 + <view class="flex-1 min-w-0">
66 - <h3 class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]"> 65 + <h3 class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]">
67 - {{ item.title }} 66 + {{ item.title }}
68 - </h3> 67 + </h3>
69 - <p class="text-[#6B7280] text-[24rpx] leading-[1.4] line-clamp-1 mb-[16rpx]"> 68 + <p class="text-[#6B7280] text-[24rpx] leading-[1.4] line-clamp-1 mb-[16rpx]">
70 - {{ item.desc }} 69 + {{ item.desc }}
71 - </p> 70 + </p>
72 - 71 +
73 - <view class="flex items-center gap-[12rpx] mb-[20rpx]"> 72 + <view class="flex items-center gap-[12rpx] mb-[20rpx]">
74 - <view 73 + <view
75 - class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]"> 74 + class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
76 - {{ getDocumentLabel(item.extension ? `file.${item.extension}` : item.fileName) }} 75 + {{ getDocumentLabel(item.extension ? `file.${item.extension}` : item.fileName) }}
77 - </view> 76 + </view>
78 - <view class="text-[#9CA3AF] text-[22rpx]"> 77 + <view class="text-[#9CA3AF] text-[22rpx]">
79 - {{ item.size }} 78 + {{ item.size }}
80 - </view>
81 </view> 79 </view>
82 -
83 - <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view>
84 -
85 - <ListItemActions
86 - :viewable="true"
87 - :collectable="true"
88 - :collected="item.collected"
89 - :item-id="String(item.meta_id || item.id)"
90 - @view="onView(item)"
91 - @collect="toggleCollect(item)"
92 - @delete="onDelete(item)"
93 - />
94 </view> 80 </view>
95 - </view>
96 81
97 - <!-- 空状态 --> 82 + <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view>
98 - <view v-if="currentList.length === 0 && !loading"> 83 +
99 - <nut-empty description="暂无相关资料" image="empty" /> 84 + <ListItemActions
85 + :viewable="true"
86 + :collectable="true"
87 + :collected="item.collected"
88 + :item-id="String(item.meta_id || item.id)"
89 + @view="onView(item)"
90 + @collect="toggleCollect(item)"
91 + @delete="onDelete(item)"
92 + />
100 </view> 93 </view>
94 + </view>
101 95
102 - <!-- 加载更多状态 --> 96 + <!-- 空状态 -->
103 - <view v-if="currentList.length > 0" class="load-more-container"> 97 + <view v-if="currentList.length === 0 && !loading">
104 - <view v-if="loadingMore" class="load-more-loading"> 98 + <nut-empty description="暂无相关资料" image="empty" />
105 - <view class="loading-spinner"></view> 99 + </view>
106 - <text class="ml-[16rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text> 100 +
107 - </view> 101 + <!-- 加载更多状态 -->
108 - <view v-else-if="!hasMore" class="load-more-finished"> 102 + <view v-if="currentList.length > 0" class="load-more-container">
109 - <text class="text-[#9CA3AF] text-[24rpx]">没有更多了</text> 103 + <view v-if="loadingMore" class="load-more-loading">
110 - </view> 104 + <view class="loading-spinner"></view>
105 + <text class="ml-[16rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
106 + </view>
107 + <view v-else-if="!hasMore" class="load-more-finished">
108 + <text class="text-[#9CA3AF] text-[24rpx]">没有更多了</text>
111 </view> 109 </view>
112 </view> 110 </view>
113 </view> 111 </view>
...@@ -282,9 +280,9 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { ...@@ -282,9 +280,9 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => {
282 loading.value = true 280 loading.value = true
283 } 281 }
284 282
285 - // console.log('[Material List] 请求参数:', params) 283 + console.log('[Material List] 请求参数:', params)
286 284
287 - // 调用接口(直接调用,不使用 fn() 包装) 285 + // 调用接口
288 const res = await fileListAPI(params) 286 const res = await fileListAPI(params)
289 287
290 if (res.code === 1 && res.data) { 288 if (res.code === 1 && res.data) {
......