hookehuyr

fix(plan): 统一分页起始页码从0开始

......@@ -5,6 +5,29 @@
---
## [2026-02-11] - 统一分页起始页码从0开始
### 修复
- **计划书页**
- 完善状态筛选功能(5个状态tab:全部、待处理、处理中、已生成、已查看)
- 实现查看计划书功能(支持多文件选择,使用 ActionSheet)
- 实现删除计划书功能(调用 deleteAPI)
- 添加状态标记显示(4种状态颜色区分)
- 修复分页从0开始
- **消息页**
- 修复分页从0开始
---
**详细信息**
- **影响文件**: src/pages/plan/index.vue, src/pages/message/index.vue
- **技术栈**: Taro 4, Vue 3, NutUI
- **测试状态**: 已通过 ESLint 检查
- **备注**: 统一项目所有列表页的分页规范,确保与 API 文档一致
---
## [2026-02-11] - 完善计划书 API 接口定义
### 新增
......
......@@ -72,10 +72,10 @@ const go = useGo()
const currentList = ref([])
/**
* 当前页码(从1开始)
* 当前页码(从0开始)
* @type {Ref<number>}
*/
const currentPage = ref(1)
const currentPage = ref(0)
/**
* 每页数量
......@@ -105,7 +105,7 @@ const loadingMore = ref(false)
* 获取消息列表
*
* @param {Object} params - 请求参数
* @param {number} params.page - 页码(从1开始)
* @param {number} params.page - 页码(从0开始)
* @param {number} params.limit - 每页数量
* @param {boolean} isLoadMore - 是否为加载更多
* @returns {Promise<void>}
......@@ -174,11 +174,11 @@ useLoad(async (options) => {
console.log('[Message] 页面参数:', options)
// 重置分页状态
currentPage.value = 1
currentPage.value = 0
hasMore.value = true
// 获取消息列表
await fetchMessageList({ page: 1, limit: pageSize })
await fetchMessageList({ page: 0, limit: pageSize })
})
/**
......@@ -207,11 +207,11 @@ const handleRefresh = async () => {
console.log('[Message] 下拉刷新')
// 重置分页状态
currentPage.value = 1
currentPage.value = 0
hasMore.value = true
// 刷新数据
await fetchMessageList({ page: 1, limit: pageSize })
await fetchMessageList({ page: 0, limit: pageSize })
}
/**
......
......@@ -79,10 +79,13 @@
<view
:class="[
'status-badge',
item.status === 'processing' ? 'status-processing' : 'status-generated'
item.status === 'pending' ? 'status-pending' : '',
item.status === 'processing' ? 'status-processing' : '',
item.status === 'generated' ? 'status-generated' : '',
item.status === 'viewed' ? 'status-viewed' : ''
]"
>
{{ item.status === 'processing' ? '生成中' : '已完成' }}
{{ getStatusText(item.status) }}
</view>
</view>
</view>
......@@ -133,7 +136,7 @@
import { ref, nextTick } from 'vue'
import Taro, { useLoad, useReachBottom } from '@tarojs/taro'
import { useFileOperation } from '@/composables/useFileOperation'
import { listAPI } from '@/api/plan'
import { listAPI, viewAPI, deleteAPI } from '@/api/plan'
import NavHeader from '@/components/navigation/NavHeader.vue'
import ListItemActions from '@/components/list/ListItemActions/index.vue'
import SearchBar from '@/components/forms/SearchBar.vue'
......@@ -154,29 +157,55 @@ const pageSize = 20 // 每页数量
/**
* Tab 数据源
* @description 包含分类信息和对应的计划书列表
* @description 状态值说明:空字符串=全部,"3"=生成中,"5"=已生成
* @description 状态值说明(根据API文档):
* - 空字符串 = 全部
* - "3" = 待处理
* - "5" = 处理中
* - "7" = 已生成
* - "9" = 已查看
*/
const tabsData = ref([
{ id: '', name: '全部', list: [] },
{ id: '3', name: '生成中', list: [] },
{ id: '5', name: '已生成', list: [] },
{ id: '3', name: '待处理', list: [] },
{ id: '5', name: '处理中', list: [] },
{ id: '7', name: '已生成', list: [] },
{ id: '9', name: '已查看', list: [] },
])
/**
* 订单状态映射
* @description 将 API 返回的 order_status 映射到前端使用的状态
* @param {string} orderStatus - API 返回的状态值
* @returns {string} 前端状态:'processing' | 'generated'
* @returns {string} 前端状态:'pending' | 'processing' | 'generated' | 'viewed'
*/
const mapOrderStatus = (orderStatus) => {
// 根据业务需求:
// "3" = 生成中
// "5" = 已生成
// 其他值 = 生成中(默认)
if (orderStatus === '5') {
return 'generated'
// 根据API文档:
// "3" = 待处理
// "5" = 处理中
// "7" = 已生成
// "9" = 已查看
const statusMap = {
'3': 'pending',
'5': 'processing',
'7': 'generated',
'9': 'viewed'
}
return 'processing'
return statusMap[orderStatus] || 'pending'
}
/**
* 获取状态文本
* @param {string} status - 前端状态值
* @returns {string} 状态文本
*/
const getStatusText = (status) => {
const textMap = {
'pending': '待处理',
'processing': '处理中',
'generated': '已生成',
'viewed': '已查看'
}
return textMap[status] || '待处理'
}
/**
......@@ -186,8 +215,11 @@ const mapOrderStatus = (orderStatus) => {
* @returns {Object} 组件使用的计划书对象
*/
const transformApiItem = (apiItem) => {
// 获取第一个文件(如果有)
const firstFile = apiItem.proposal_files && apiItem.proposal_files[0]
// 获取所有文件
const proposalFiles = apiItem.proposal_files || []
// 获取第一个文件(作为默认显示)
const firstFile = proposalFiles[0]
// 获取第一个分类名称(categories 是对象数组)
const categoryName = apiItem.categories && apiItem.categories.length > 0
......@@ -201,8 +233,11 @@ const transformApiItem = (apiItem) => {
date: apiItem.created_time || '',
tag: categoryName,
status: mapOrderStatus(apiItem.order_status),
// 默认显示第一个文件
fileName: firstFile?.file_name || '',
downloadUrl: firstFile?.file_url || '',
// 保存所有文件(用于多文件选择)
proposalFiles: proposalFiles,
// 保存完整的原始数据
_raw: apiItem
}
......@@ -210,54 +245,34 @@ const transformApiItem = (apiItem) => {
/**
* 加载计划书列表(调用真实 API)
* @param {number} page - 页码(从1开始,API 要求)
* @param {number} page - 页码(从0开始,API 要求)
* @param {number} limit - 每页数量
* @param {boolean} isLoadMore - 是否为加载更多
*
* @description 状态过滤说明(前端过滤,因为后端不支持):
* @description 状态筛选说明(使用服务端 status 参数筛选):
* - activeTabId = '':不过滤,显示全部
* - activeTabId = '3':只显示生成中的计划书(order_status = "3")
* - activeTabId = '5':只显示已生成的计划书(order_status = "5")
* - activeTabId = '3':只显示待处理的计划书
* - activeTabId = '5':只显示处理中的计划书
* - activeTabId = '7':只显示已生成的计划书
* - activeTabId = '9':只显示已查看的计划书
*
* @todo ⚠️ 后端API缺少关键查询参数,需要添加以下支持:
*
* 1️⃣ **分页参数**(必须):
* - `page` - 页码,从 1 开始
* - `limit` - 每页数量,默认 20
* - 当前影响:无法实现真正的分页,只能一次性加载所有数据
*
* 2️⃣ **状态筛选参数**(必须):
* - `order_status` - 订单状态筛选
* - ⚠️ **状态值不确定**:当前推测 `"3"` = 生成中,`"5"` = 已生成(需与后端确认)
* - 当前影响:需要前端过滤,性能和分页准确性差
*
* 3️⃣ **搜索参数**(建议):
* - `keyword` - 搜索关键字(搜索申请人、产品名等)
* - 当前影响:搜索功能可能不准确
*
* 🔴 **严重问题**:
* - 当前后端API不支持任何查询参数
* - 前端只能一次性获取全部数据,然后在本地进行分页和过滤
* - 这导致:
* - 性能问题:数据量大时加载慢
* - 分页失效:无法实现真正的服务端分页
* - 内存占用:所有数据都在前端
*
* ✅ **建议的API参数规范**:
* ✅ **API参数说明**:
* ```javascript
* GET /srv/?a=proposal&t=list
* Query Parameters:
* - page: number (必需) - 页码,从 1 开始
* - page: number (必需) - 页码,从 0 开始
* - limit: number (必需) - 每页数量,默认 20
* - order_status: string (可选) - 状态筛选,需与后端确认具体值
* - status: string (可选) - 状态筛选
* - keyword: string (可选) - 搜索关键字
* ```
*
* ⚠️ **重要**:order_status 的具体值需要与后端确认!
* - 当前前端推测:`"3"` = 生成中,`"5"` = 已生成
* - 实际值可能不同,请后端提供准确的状态值定义
* ⚠️ **重要**:status 参数的具体值(根据API文档):
* - "3" = 待处理
* - "5" = 处理中
* - "7" = 已生成
* - "9" = 已查看
*/
const fetchPlanList = async (page = 1, limit = pageSize, isLoadMore = false) => {
const fetchPlanList = async (page = 0, limit = pageSize, isLoadMore = false) => {
try {
// 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
if (isLoadMore) {
......@@ -267,12 +282,16 @@ const fetchPlanList = async (page = 1, limit = pageSize, isLoadMore = false) =>
}
// 构建请求参数
// 注意:后端不支持 order_status 参数,需要在前端过滤
const params = {
page: page,
limit: limit
}
// 状态筛选(使用 status 参数)
if (activeTabId.value !== '') {
params.status = activeTabId.value
}
// 搜索关键字
if (searchValue.value) {
params.keyword = searchValue.value
......@@ -283,12 +302,6 @@ const fetchPlanList = async (page = 1, limit = pageSize, isLoadMore = false) =>
if (res.code === 1 && res.data) {
let apiList = res.data.list || []
// ⚠️ 前端过滤:因为后端不支持 order_status 参数
if (activeTabId.value !== '') {
apiList = apiList.filter(item => item.order_status === activeTabId.value)
}
const transformedList = apiList.map(transformApiItem)
if (isLoadMore) {
......@@ -336,8 +349,8 @@ const onTabClick = (id) => {
activeTabId.value = id
listVisible.value = false
// 重置分页状态(从第 1 页开始)
currentPage.value = 1
// 重置分页状态(从第 0 页开始)
currentPage.value = 0
hasMore.value = true
nextTick(() => {
......@@ -345,7 +358,7 @@ const onTabClick = (id) => {
listVisible.value = true
// 重新加载数据
fetchPlanList(1, pageSize, false)
fetchPlanList(0, pageSize, false)
})
}
......@@ -353,14 +366,14 @@ const onTabClick = (id) => {
* 搜索处理
*/
const onSearch = () => {
// 重置分页状态(从第 1 页开始)
currentPage.value = 1
// 重置分页状态(从第 0 页开始)
currentPage.value = 0
hasMore.value = true
listRenderKey.value += 1
// 重新加载数据
fetchPlanList(1, pageSize, false)
fetchPlanList(0, pageSize, false)
}
/**
......@@ -372,14 +385,14 @@ const onClear = () => {
// 清空搜索值
searchValue.value = ''
// 重置分页状态(从第 1 页开始)
currentPage.value = 1
// 重置分页状态(从第 0 页开始)
currentPage.value = 0
hasMore.value = true
listRenderKey.value += 1
// 重新加载数据
fetchPlanList(1, pageSize, false)
fetchPlanList(0, pageSize, false)
console.log('[Plan Page] onClear 完成,列表已刷新')
}
......@@ -388,8 +401,8 @@ const onClear = () => {
* 页面加载时初始化数据
*/
useLoad(() => {
// 加载第一页数据
fetchPlanList(1, pageSize, false)
// 加载第一页数据(page = 0)
fetchPlanList(0, pageSize, false)
})
/**
......@@ -428,46 +441,119 @@ useReachBottom(() => {
* @param {Object} item - 计划书对象
*/
const onView = async (item) => {
// 检查是否已生成
if (item.status === 'processing') {
// 检查是否已生成(只有"已生成"或"已查看"状态才能查看)
if (item.status === 'pending' || item.status === 'processing') {
Taro.showToast({
title: '计划书尚未生成,请稍后',
icon: 'none'
})
return
}
// 检查是否有计划书文件
if (!item.proposalFiles || item.proposalFiles.length === 0) {
Taro.showToast({
title: '计划书生成中,请稍后',
title: '暂无可查看的计划书',
icon: 'none'
})
return
}
// 使用 useFileOperation 查看文档
await viewFile({
downloadUrl: item.downloadUrl,
fileName: item.fileName
/**
* 处理文件查看
* @param {Object} file - 文件对象
*/
const handleFileView = async (file) => {
try {
// 调用 viewAPI 标记为已查看
const viewRes = await viewAPI({ i: item.id })
if (viewRes.code === 1) {
// 更新本地状态为"已查看"
item.status = 'viewed'
// 显示提示
Taro.showToast({
title: '已标记为查看',
icon: 'success',
duration: 1000
})
}
} catch (error) {
console.error('标记查看失败:', error)
// 即使标记失败,也继续查看文档
}
// 使用 useFileOperation 查看文档
await viewFile({
downloadUrl: file.file_url,
fileName: file.file_name
})
}
// 如果只有一个文件,直接查看
if (item.proposalFiles.length === 1) {
await handleFileView(item.proposalFiles[0])
return
}
// 如果有多个文件,显示选择列表
const fileList = item.proposalFiles.map((file, index) => ({
text: file.file_name || `计划书 ${index + 1}`,
file: file
}))
// 使用 Taro.showActionSheet 显示文件选择列表
Taro.showActionSheet({
itemList: fileList.map(f => f.text),
success: async (res) => {
const selectedIndex = res.tapIndex
if (selectedIndex !== undefined && selectedIndex >= 0) {
const selectedFile = fileList[selectedIndex].file
await handleFileView(selectedFile)
}
}
})
}
/**
* 删除计划书
* @description 注意:当前后端可能未提供删除接口,暂时只做前端提示
* @description 调用删除API删除计划书
*/
const onDelete = (item) => {
const onDelete = async (item) => {
Taro.showModal({
title: '提示',
content: '删除功能需要后端支持,是否继续?',
success: (res) => {
title: '确认删除',
content: `确定要删除"${item.title}"吗?`,
success: async (res) => {
if (res.confirm) {
// TODO: 调用删除 API
// const res = await deletePlanAPI({ id: item.id })
// if (res.code === 1) {
// Taro.showToast({ title: '已删除', icon: 'success' })
// // 重新加载列表
// currentPage.value = 1
// fetchPlanList(1, pageSize, false)
// }
Taro.showToast({
title: '删除功能待实现',
icon: 'none',
duration: 2000
})
try {
// 调用删除API
const deleteRes = await deleteAPI({ i: item.id })
if (deleteRes.code === 1) {
Taro.showToast({
title: '删除成功',
icon: 'success',
duration: 1500
})
// 重新加载列表
currentPage.value = 0
hasMore.value = true
await fetchPlanList(0, pageSize, false)
} else {
Taro.showToast({
title: deleteRes.msg || '删除失败',
icon: 'none'
})
}
} catch (error) {
console.error('删除失败:', error)
Taro.showToast({
title: '网络异常,请重试',
icon: 'none'
})
}
}
}
})
......@@ -585,13 +671,23 @@ const onDelete = (item) => {
white-space: nowrap;
}
.status-processing {
.status-pending {
background-color: #FEF3C7;
color: #D97706;
}
.status-processing {
background-color: #DBEAFE;
color: #2563EB;
}
.status-generated {
background-color: #D1FAE5;
color: #059669;
}
.status-viewed {
background-color: #E5E7EB;
color: #6B7280;
}
</style>
......