hookehuyr

feat(product): 添加 mock 数据支持并修复滚动加载功能

新增功能:
- 添加 mock 数据系统(src/api/mock/product.js)
  - 生成 100 个测试产品数据
  - 支持 5 个产品分类
  - 支持随机图片(Lorem Picsum 服务)
  - 支持搜索和分类过滤
  - 模拟网络延迟(300-800ms)
- 在 get_product.js 中添加 mock 数据开关(USE_MOCK_DATA)
  - 方便开发测试和后端联调

修复问题:
- 修复产品中心页面滚动加载不触发的问题
- 使用 page-level scroll 替代 container scroll
- 使用 sticky 定位固定顶部导航和搜索栏
- useReachBottom 现在可以正确监听页面触底事件

技术细节:
- 移除 h-screen(小程序不支持 100vh)
- 使用 sticky top-0 固定顶部区域
- 列表区域使用页面级滚动
- 添加调试日志方便排查问题

相关文件:
- src/api/mock/product.js(新增)
- src/api/get_product.js(修改)
- src/pages/product-center/index.vue(修改)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
import { fn, fetch } from '@/api/fn';
import { mockListAPI, mockDetailAPI } from './mock/product';
// ⚠️ Mock 数据开关 - 设置为 true 使用 mock 数据,false 使用真实 API
const USE_MOCK_DATA = false;
const Api = {
Detail: '/srv/?a=get_product&t=detail',
......@@ -45,7 +49,13 @@ const Api = {
* };
* }>}
*/
export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
export const detailAPI = (params) => {
// 如果开启 Mock 数据,返回 mock 数据
if (USE_MOCK_DATA) {
return mockDetailAPI(params);
}
return fn(fetch.get(Api.Detail, params));
};
/**
* @description 产品列表
......@@ -87,4 +97,10 @@ export const detailAPI = (params) => fn(fetch.get(Api.Detail, params));
* };
* }>}
*/
export const listAPI = (params) => fn(fetch.get(Api.List, params));
export const listAPI = (params) => {
// 如果开启 Mock 数据,返回 mock 数据
if (USE_MOCK_DATA) {
return mockListAPI(params);
}
return fn(fetch.get(Api.List, params));
};
......
/**
* @description Mock 数据 - 产品中心
* @note 用于测试滚动加载更多功能
*/
// Mock 分类数据
export const mockCategories = [
{ id: 1, name: '寿险' },
{ id: 2, name: '健康险' },
{ id: 3, name: '意外险' },
{ id: 4, name: '年金险' },
{ id: 5, name: '重疾险' }
]
// Mock 标签数据
const mockTags = [
{ id: 1, name: '热销', bg_color: '#FEF3C7', text_color: '#92400E' },
{ id: 2, name: '新品', bg_color: '#DBEAFE', text_color: '#1E40AF' },
{ id: 3, name: '推荐', bg_color: '#D1FAE5', text_color: '#065F46' },
{ id: 4, name: '限时', bg_color: '#FEE2E2', text_color: '#991B1B' }
]
// 生成单个产品数据
const generateProduct = (id) => {
const recommendTypes = ['normal', 'hot']
const recommend = recommendTypes[Math.floor(Math.random() * recommendTypes.length)]
// 随机选择 1-2 个标签
const tagCount = Math.floor(Math.random() * 2) + 1
const tags = []
for (let i = 0; i < tagCount; i++) {
const tag = mockTags[Math.floor(Math.random() * mockTags.length)]
if (!tags.find(t => t.id === tag.id)) {
tags.push(tag)
}
}
// 随机选择 1-2 个分类
const categoryCount = Math.floor(Math.random() * 2) + 1
const categories = []
for (let i = 0; i < categoryCount; i++) {
const category = mockCategories[Math.floor(Math.random() * mockCategories.length)]
if (!categories.find(c => c.id === category.id)) {
categories.push(category)
}
}
return {
id: id,
product_name: `测试产品 ${id} - ${categories.map(c => c.name).join('+')}`,
recommend: recommend,
form_sn: `product_form_${id}`,
created_time: '2025-12-01 12:00:00',
categories: categories,
tags: tags,
// 使用 Lorem Picsum 随机图片服务(基于产品 ID 确保图片固定)
cover_image: `https://picsum.photos/300/200?random=${id}`
}
}
// 生成产品列表
const generateProductList = (page, limit, cid = null) => {
const start = page * limit
const end = start + limit
// 如果指定了分类,只返回该分类的产品
let filteredProducts = []
if (cid) {
// 为每个分类生成固定数量的产品
const categoryProducts = []
for (let i = 1; i <= 50; i++) {
const product = generateProduct(i)
// 强制该产品属于指定分类
product.categories = mockCategories.find(c => String(c.id) === String(cid))
? [mockCategories.find(c => String(c.id) === String(cid))]
: [{ id: parseInt(cid), name: '测试分类' }]
categoryProducts.push(product)
}
filteredProducts = categoryProducts
} else {
// 全部产品
for (let i = 1; i <= 100; i++) {
filteredProducts.push(generateProduct(i))
}
}
const total = filteredProducts.length
const list = filteredProducts.slice(start, end)
return {
list,
total,
hasMore: end < total
}
}
/**
* Mock 产品列表 API
* @param {Object} params 请求参数
* @param {string} params.page 页码(从 0 开始)
* @param {string} params.limit 每页数量
* @param {string} params.cid 分类 ID(可选)
* @param {string} params.keyword 搜索关键词(可选)
* @returns {Promise} 模拟 API 响应
*/
export const mockListAPI = (params) => {
return new Promise((resolve) => {
// 模拟网络延迟(300-800ms)
const delay = Math.floor(Math.random() * 500) + 300
setTimeout(() => {
const page = parseInt(params.page) || 0
const limit = parseInt(params.limit) || 10
const cid = params.cid || null
const keyword = params.keyword || ''
let result = generateProductList(page, limit, cid)
// 如果有搜索关键词,过滤产品
if (keyword) {
result.list = result.list.filter(p =>
p.product_name.includes(keyword)
)
// 搜索时重新计算总数
result.total = result.list.length + Math.floor(Math.random() * 20)
}
resolve({
code: 1,
msg: 'success',
data: {
categories: mockCategories,
list: result.list,
total: result.total
}
})
}, delay)
})
}
/**
* Mock 产品详情 API
* @param {Object} params 请求参数
* @param {string} params.i 产品 ID
* @returns {Promise} 模拟 API 响应
*/
export const mockDetailAPI = (params) => {
return new Promise((resolve) => {
const delay = Math.floor(Math.random() * 500) + 300
setTimeout(() => {
const id = parseInt(params.i) || 1
const product = generateProduct(id)
// 添加额外的详情字段
product.product_description = `这是产品 ${id} 的详细描述。\n\n产品特点:\n1. 保障全面\n2. 灵活配置\n3. 理赔便捷`
product.documents = [
{
file_url: 'https://example.com/file1.pdf',
file_name: '产品条款.pdf',
file_size: '1024000',
file_size_formatted: '1.0 MB'
},
{
file_url: 'https://example.com/file2.pdf',
file_name: '产品说明.pdf',
file_size: '512000',
file_size_formatted: '512 KB'
}
]
product.status = 'active'
product.created_by = 1
product.updated_by = 1
product.updated_time = '2025-12-01 12:00:00'
resolve({
code: 1,
msg: 'success',
data: product
})
}, delay)
})
}
......@@ -3,8 +3,9 @@
* @Description: 产品中心 - API 接口集成版本(含搜索功能)
-->
<template>
<view class="h-screen bg-[#F9FAFB] flex flex-col">
<view class="bg-[#F9FAFB] z-10">
<view class="bg-[#F9FAFB]">
<!-- 固定在顶部的导航和搜索 -->
<view class="bg-[#F9FAFB] sticky top-0 z-10">
<NavHeader title="产品中心" />
<!-- Search Bar -->
......@@ -43,8 +44,8 @@
</view>
</view>
<!-- 列表容器 - 使用原生滚动 -->
<view class="flex-1 min-h-0 overflow-y-auto pb-[calc(160rpx+env(safe-area-inset-bottom))]">
<!-- 列表容器 - 页面级滚动 -->
<view class="pb-[calc(160rpx+env(safe-area-inset-bottom))]">
<!-- 加载状态 -->
<view v-if="loading && products.length === 0" class="flex justify-center items-center py-[100rpx]">
<text class="text-gray-400 text-[28rpx]">加载中...</text>
......@@ -398,9 +399,15 @@ useLoad(() => {
/**
* 触底加载更多
* @description 使用 Taro 的 useReachBottom hook 监听页面滚动到底部
*/
useReachBottom(() => {
if (!hasMore.value || loading.value) return
console.log('滚动到底部,加载更多')
if (!hasMore.value || loading.value) {
console.log('没有更多数据或正在加载中,跳过')
return
}
page.value += 1
fetchProducts(true)
......