feat(plan): 添加消息详情计划书查看功能
- 新增 usePlanView composable 封装计划书查看逻辑 - 支持单文件直接预览,多文件显示选择弹框 - 预览成功后自动标记为已查看 - 消息列表第一页插入3条测试数据(1001/1002/1003) - 更新测试文件地址,使用项目 CDN 资源 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
3 changed files
with
542 additions
and
27 deletions
src/composables/usePlanView.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 计划书查看 Composable | ||
| 3 | + * | ||
| 4 | + * @description 封装计划书查看逻辑,支持: | ||
| 5 | + * - 单文件直接预览 | ||
| 6 | + * - 多文件显示选择弹框 | ||
| 7 | + * - 预览成功后标记为已查看 | ||
| 8 | + * - 传入 proposal 数据自动处理状态和文件 | ||
| 9 | + * | ||
| 10 | + * @example | ||
| 11 | + * const { viewProposal } = usePlanView() | ||
| 12 | + * | ||
| 13 | + * // 方式1:传入完整的 proposal 对象(从消息详情 API 获取) | ||
| 14 | + * viewProposal({ | ||
| 15 | + * id: 123, | ||
| 16 | + * order_status: '7', | ||
| 17 | + * proposal_files: [ | ||
| 18 | + * { file_name: '计划书.pdf', file_url: 'xxx', id: 1 } | ||
| 19 | + * ] | ||
| 20 | + * }) | ||
| 21 | + * | ||
| 22 | + * // 方式2:传入已转换的 item(从计划书列表获取) | ||
| 23 | + * viewProposal(planItem) | ||
| 24 | + * | ||
| 25 | + * @author Claude Code | ||
| 26 | + * @version 1.0.0 | ||
| 27 | + */ | ||
| 28 | + | ||
| 29 | +import { useFileOperation } from './useFileOperation' | ||
| 30 | +import { viewAPI } from '@/api/plan' | ||
| 31 | +import Taro from '@tarojs/taro' | ||
| 32 | + | ||
| 33 | +/** | ||
| 34 | + * 计划书查看 Hook | ||
| 35 | + * | ||
| 36 | + * @returns {Object} 包含 viewProposal 方法的对象 | ||
| 37 | + */ | ||
| 38 | +export function usePlanView() { | ||
| 39 | + const { viewFile } = useFileOperation() | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 订单状态映射 | ||
| 43 | + * | ||
| 44 | + * @param {string} orderStatus - API 返回的状态值 | ||
| 45 | + * @returns {string} 前端状态:'pending' | 'processing' | 'generated' | 'viewed' | ||
| 46 | + * | ||
| 47 | + * @description 状态值说明(根据API文档): | ||
| 48 | + * - "3" = 待处理 (pending) | ||
| 49 | + * - "5" = 处理中 (processing) | ||
| 50 | + * - "7" = 已生成 (generated) | ||
| 51 | + * - "9" = 已查看 (viewed) | ||
| 52 | + */ | ||
| 53 | + const mapOrderStatus = (orderStatus) => { | ||
| 54 | + const statusMap = { | ||
| 55 | + '3': 'pending', // 待处理 | ||
| 56 | + '5': 'processing', // 处理中 | ||
| 57 | + '7': 'generated', // 已生成 | ||
| 58 | + '9': 'viewed' // 已查看 | ||
| 59 | + } | ||
| 60 | + return statusMap[orderStatus] || 'pending' | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 获取状态文本 | ||
| 65 | + * | ||
| 66 | + * @param {string} status - 前端状态值 | ||
| 67 | + * @returns {string} 状态文本 | ||
| 68 | + */ | ||
| 69 | + const getStatusText = (status) => { | ||
| 70 | + const textMap = { | ||
| 71 | + 'pending': '待处理', | ||
| 72 | + 'processing': '处理中', | ||
| 73 | + 'generated': '已生成', | ||
| 74 | + 'viewed': '已查看' | ||
| 75 | + } | ||
| 76 | + return textMap[status] || '待处理' | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + /** | ||
| 80 | + * 查看计划书 | ||
| 81 | + * | ||
| 82 | + * @param {Object} proposal - 计划书对象(支持两种格式) | ||
| 83 | + * @param {number} proposal.id - 计划书 ID(必需) | ||
| 84 | + * @param {string} proposal.order_status - 订单状态(API 格式:'3'|'5'|'7'|'9') | ||
| 85 | + * @param {Array} proposal.proposal_files - 文件列表(API 格式) | ||
| 86 | + * @param {string} proposal.status - 订单状态(前端格式,兼容列表数据) | ||
| 87 | + * @param {Array} proposal.proposalFiles - 文件列表(兼容列表数据) | ||
| 88 | + * @param {Object} callbacks - 回调函数 | ||
| 89 | + * @param {Function} callbacks.onViewSuccess - 查看成功后回调,参数为 proposalId | ||
| 90 | + * @param {Function} callbacks.beforeView - 查看前回调,返回 false 可取消查看 | ||
| 91 | + * @returns {Promise<void>} | ||
| 92 | + */ | ||
| 93 | + const viewProposal = async (proposal, callbacks = {}) => { | ||
| 94 | + const { beforeView, onViewSuccess } = callbacks | ||
| 95 | + | ||
| 96 | + // 1. 状态检查 - 解析两种可能的状态字段 | ||
| 97 | + const status = proposal.status || mapOrderStatus(proposal.order_status) | ||
| 98 | + | ||
| 99 | + if (status === 'pending' || status === 'processing') { | ||
| 100 | + Taro.showToast({ | ||
| 101 | + title: '计划书尚未生成,请稍后', | ||
| 102 | + icon: 'none' | ||
| 103 | + }) | ||
| 104 | + return | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + // 2. 解析文件列表 - 支持两种可能的字段名 | ||
| 108 | + const proposalFiles = proposal.proposal_files || proposal.proposalFiles || [] | ||
| 109 | + | ||
| 110 | + if (!proposalFiles || proposalFiles.length === 0) { | ||
| 111 | + Taro.showToast({ | ||
| 112 | + title: '暂无可查看的计划书', | ||
| 113 | + icon: 'none' | ||
| 114 | + }) | ||
| 115 | + return | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + // 3. 执行查看前回调 | ||
| 119 | + if (beforeView) { | ||
| 120 | + const shouldContinue = await beforeView(proposal) | ||
| 121 | + if (shouldContinue === false) return | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + /** | ||
| 125 | + * 处理单个文件的查看 | ||
| 126 | + * | ||
| 127 | + * @param {Object} file - 文件对象 | ||
| 128 | + * @param {string} file.file_url - 文件 URL | ||
| 129 | + * @param {string} file.file_name - 文件名称 | ||
| 130 | + */ | ||
| 131 | + const handleFileView = async (file) => { | ||
| 132 | + try { | ||
| 133 | + const previewSuccess = await viewFile({ | ||
| 134 | + downloadUrl: file.file_url, | ||
| 135 | + fileName: file.file_name | ||
| 136 | + }) | ||
| 137 | + | ||
| 138 | + if (!previewSuccess) return | ||
| 139 | + | ||
| 140 | + // 4. 预览成功后标记为已查看 | ||
| 141 | + if (status !== 'viewed' && proposal.id) { | ||
| 142 | + const viewRes = await viewAPI({ i: proposal.id }) | ||
| 143 | + | ||
| 144 | + if (viewRes.code === 1) { | ||
| 145 | + Taro.showToast({ | ||
| 146 | + title: '已标记为查看', | ||
| 147 | + icon: 'success', | ||
| 148 | + duration: 1000 | ||
| 149 | + }) | ||
| 150 | + | ||
| 151 | + // 触发成功回调 | ||
| 152 | + if (onViewSuccess) { | ||
| 153 | + onViewSuccess(proposal.id) | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | + } | ||
| 157 | + } catch (error) { | ||
| 158 | + console.error('查看计划书文件失败:', error) | ||
| 159 | + } | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + // 5. 单文件直接查看 | ||
| 163 | + if (proposalFiles.length === 1) { | ||
| 164 | + await handleFileView(proposalFiles[0]) | ||
| 165 | + return | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + // 6. 多文件显示选择弹框 | ||
| 169 | + const fileList = proposalFiles.map((file, index) => ({ | ||
| 170 | + text: file.file_name || `计划书 ${index + 1}`, | ||
| 171 | + file: file | ||
| 172 | + })) | ||
| 173 | + | ||
| 174 | + Taro.showActionSheet({ | ||
| 175 | + itemList: fileList.map(f => f.text), | ||
| 176 | + success: async (res) => { | ||
| 177 | + const selectedIndex = res.tapIndex | ||
| 178 | + if (selectedIndex !== undefined && selectedIndex >= 0) { | ||
| 179 | + const selectedFile = fileList[selectedIndex].file | ||
| 180 | + await handleFileView(selectedFile) | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + }) | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + return { | ||
| 187 | + viewProposal, | ||
| 188 | + mapOrderStatus, | ||
| 189 | + getStatusText | ||
| 190 | + } | ||
| 191 | +} |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date:2026-02-03 21:26:58 | 2 | * @Date:2026-02-03 21:26:58 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-02-12 21:09:32 | 4 | + * @LastEditTime: 2026-02-13 |
| 5 | * @FilePath: /manulife-weapp/src/pages/message-detail/index.vue | 5 | * @FilePath: /manulife-weapp/src/pages/message-detail/index.vue |
| 6 | - * @Description: 消息详情页 | 6 | + * @Description: 消息详情页 - API 新增 title 字段,添加 Mock 支持,新增计划书查看功能 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <view class="min-h-screen bg-gray-50 pb-safe"> | 9 | <view class="min-h-screen bg-gray-50 pb-safe"> |
| ... | @@ -12,7 +12,7 @@ | ... | @@ -12,7 +12,7 @@ |
| 12 | <view v-if="detail" class="p-4"> | 12 | <view v-if="detail" class="p-4"> |
| 13 | <!-- 消息卡片 --> | 13 | <!-- 消息卡片 --> |
| 14 | <view class="bg-white rounded-lg p-5 shadow-sm"> | 14 | <view class="bg-white rounded-lg p-5 shadow-sm"> |
| 15 | - <!-- 标题区域 (模拟) --> | 15 | + <!-- 标题区域:API 返回 title 字段 --> |
| 16 | <view class="mb-3"> | 16 | <view class="mb-3"> |
| 17 | <text class="text-xl font-bold text-gray-900 leading-snug block"> | 17 | <text class="text-xl font-bold text-gray-900 leading-snug block"> |
| 18 | {{ detail.title || '系统消息通知' }} | 18 | {{ detail.title || '系统消息通知' }} |
| ... | @@ -33,6 +33,57 @@ | ... | @@ -33,6 +33,57 @@ |
| 33 | <view class="rich-text-content"> | 33 | <view class="rich-text-content"> |
| 34 | <rich-text :nodes="formattedContent" /> | 34 | <rich-text :nodes="formattedContent" /> |
| 35 | </view> | 35 | </view> |
| 36 | + | ||
| 37 | + <!-- 计划书卡片 --> | ||
| 38 | + <view | ||
| 39 | + v-if="detail.proposal" | ||
| 40 | + class="mt-6 pt-6 border-t border-gray-200" | ||
| 41 | + > | ||
| 42 | + <!-- 卡片标题 --> | ||
| 43 | + <view class="flex justify-between items-center mb-4"> | ||
| 44 | + <text class="text-base font-bold text-gray-900">关联计划书</text> | ||
| 45 | + <!-- 状态标记 --> | ||
| 46 | + <view | ||
| 47 | + :class="[ | ||
| 48 | + 'status-badge', | ||
| 49 | + proposalStatus === 'generated' ? 'status-generated' : '', | ||
| 50 | + proposalStatus === 'viewed' ? 'status-viewed' : '' | ||
| 51 | + ]" | ||
| 52 | + > | ||
| 53 | + {{ getProposalStatusText(proposalStatus) }} | ||
| 54 | + </view> | ||
| 55 | + </view> | ||
| 56 | + | ||
| 57 | + <!-- 计划书信息 --> | ||
| 58 | + <view class="bg-gray-50 rounded-lg p-4 mb-4"> | ||
| 59 | + <view class="flex justify-between text-sm mb-2"> | ||
| 60 | + <text class="text-gray-500">申请人</text> | ||
| 61 | + <text class="text-gray-900 font-medium">{{ detail.proposal.customer_name || '-' }}</text> | ||
| 62 | + </view> | ||
| 63 | + <view class="flex justify-between text-sm mb-2"> | ||
| 64 | + <text class="text-gray-500">产品</text> | ||
| 65 | + <text class="text-gray-900 font-medium">{{ detail.proposal.product_name || '-' }}</text> | ||
| 66 | + </view> | ||
| 67 | + <view class="flex justify-between text-sm"> | ||
| 68 | + <text class="text-gray-500">创建时间</text> | ||
| 69 | + <text class="text-gray-900 font-medium">{{ detail.proposal.created_time || '-' }}</text> | ||
| 70 | + </view> | ||
| 71 | + </view> | ||
| 72 | + | ||
| 73 | + <!-- 查看按钮 --> | ||
| 74 | + <nut-button | ||
| 75 | + v-if="canViewProposal" | ||
| 76 | + type="primary" | ||
| 77 | + color="#2563EB" | ||
| 78 | + block | ||
| 79 | + class="!rounded-lg !text-base" | ||
| 80 | + @tap="handleViewProposal" | ||
| 81 | + > | ||
| 82 | + {{ proposalFiles.length > 1 | ||
| 83 | + ? `查看计划书 (${proposalFiles.length}个文件)` | ||
| 84 | + : '查看计划书' }} | ||
| 85 | + </nut-button> | ||
| 86 | + </view> | ||
| 36 | </view> | 87 | </view> |
| 37 | </view> | 88 | </view> |
| 38 | 89 | ||
| ... | @@ -52,12 +103,75 @@ import { useLoad } from '@tarojs/taro' | ... | @@ -52,12 +103,75 @@ import { useLoad } from '@tarojs/taro' |
| 52 | import NavHeader from '@/components/navigation/NavHeader.vue' | 103 | import NavHeader from '@/components/navigation/NavHeader.vue' |
| 53 | import { detailAPI } from '@/api/news' | 104 | import { detailAPI } from '@/api/news' |
| 54 | import { useUserStore } from '@/stores/user' | 105 | import { useUserStore } from '@/stores/user' |
| 106 | +import { mockDetailAPI } from '@/utils/mockData' | ||
| 107 | +import { USE_MOCK_DATA } from '@/config/app' | ||
| 108 | +import { usePlanView } from '@/composables/usePlanView' | ||
| 55 | 109 | ||
| 56 | const detail = ref(null) | 110 | const detail = ref(null) |
| 57 | const loading = ref(true) | 111 | const loading = ref(true) |
| 58 | 112 | ||
| 113 | +// 使用计划书查看 Composable | ||
| 114 | +const { viewProposal } = usePlanView() | ||
| 115 | + | ||
| 116 | +/** | ||
| 117 | + * 计划书状态 | ||
| 118 | + * | ||
| 119 | + * @description 将 API 返回的 order_status 映射为前端状态 | ||
| 120 | + * @returns {string|null} 'pending' | 'processing' | 'generated' | 'viewed' | null | ||
| 121 | + */ | ||
| 122 | +const proposalStatus = computed(() => { | ||
| 123 | + if (!detail.value?.proposal) return null | ||
| 124 | + | ||
| 125 | + // 状态映射(与 plan/index.vue 保持一致) | ||
| 126 | + const statusMap = { | ||
| 127 | + '3': 'pending', // 待处理 | ||
| 128 | + '5': 'processing', // 处理中 | ||
| 129 | + '7': 'generated', // 已生成 | ||
| 130 | + '9': 'viewed' // 已查看 | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + return statusMap[detail.value.proposal.order_status] || 'pending' | ||
| 134 | +}) | ||
| 135 | + | ||
| 136 | +/** | ||
| 137 | + * 计划书文件列表 | ||
| 138 | + * | ||
| 139 | + * @description 从 proposal 对象中提取文件列表 | ||
| 140 | + * @returns {Array} 文件列表 | ||
| 141 | + */ | ||
| 142 | +const proposalFiles = computed(() => { | ||
| 143 | + return detail.value?.proposal?.proposal_files || [] | ||
| 144 | +}) | ||
| 145 | + | ||
| 59 | /** | 146 | /** |
| 60 | - * @description 格式化富文本内容,处理图片宽度等问题 | 147 | + * 是否可以查看计划书 |
| 148 | + * | ||
| 149 | + * @description 只有"已生成"和"已查看"状态才能查看 | ||
| 150 | + * @returns {boolean} | ||
| 151 | + */ | ||
| 152 | +const canViewProposal = computed(() => { | ||
| 153 | + const status = proposalStatus.value | ||
| 154 | + return status === 'generated' || status === 'viewed' | ||
| 155 | +}) | ||
| 156 | + | ||
| 157 | +/** | ||
| 158 | + * 获取状态文本 | ||
| 159 | + * | ||
| 160 | + * @param {string} status - 前端状态值 | ||
| 161 | + * @returns {string} 状态中文文本 | ||
| 162 | + */ | ||
| 163 | +const getProposalStatusText = (status) => { | ||
| 164 | + const textMap = { | ||
| 165 | + 'pending': '待处理', | ||
| 166 | + 'processing': '处理中', | ||
| 167 | + 'generated': '已生成', | ||
| 168 | + 'viewed': '已查看' | ||
| 169 | + } | ||
| 170 | + return textMap[status] || '待处理' | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +/** | ||
| 174 | + * 格式化富文本内容,处理图片宽度等问题 | ||
| 61 | */ | 175 | */ |
| 62 | const formattedContent = computed(() => { | 176 | const formattedContent = computed(() => { |
| 63 | if (!detail.value?.note) return '' | 177 | if (!detail.value?.note) return '' |
| ... | @@ -72,21 +186,37 @@ const formattedContent = computed(() => { | ... | @@ -72,21 +186,37 @@ const formattedContent = computed(() => { |
| 72 | }) | 186 | }) |
| 73 | 187 | ||
| 74 | /** | 188 | /** |
| 75 | - * @description 获取消息详情 | 189 | + * 查看计划书 |
| 190 | + * | ||
| 191 | + * @description 调用 usePlanView 的 viewProposal 方法查看计划书 | ||
| 192 | + * 支持单文件直接预览,多文件显示选择弹框 | ||
| 193 | + */ | ||
| 194 | +const handleViewProposal = () => { | ||
| 195 | + viewProposal(detail.value.proposal, { | ||
| 196 | + onViewSuccess: (proposalId) => { | ||
| 197 | + // 查看成功后的回调 | ||
| 198 | + console.log('计划书已查看:', proposalId) | ||
| 199 | + } | ||
| 200 | + }) | ||
| 201 | +} | ||
| 202 | + | ||
| 203 | +/** | ||
| 204 | + * 获取消息详情 | ||
| 205 | + * | ||
| 206 | + * @description 根据 ID 获取消息详情,API 已返回 title 字段 | ||
| 76 | * @param {string|number} id 消息ID | 207 | * @param {string|number} id 消息ID |
| 77 | */ | 208 | */ |
| 78 | const fetchDetail = async (id) => { | 209 | const fetchDetail = async (id) => { |
| 79 | loading.value = true | 210 | loading.value = true |
| 80 | try { | 211 | try { |
| 81 | - const res = await detailAPI({ i: id }) | 212 | + // 根据环境选择使用 Mock 数据还是真实 API |
| 213 | + const res = USE_MOCK_DATA | ||
| 214 | + ? await mockDetailAPI({ i: id }) | ||
| 215 | + : await detailAPI({ i: id }) | ||
| 216 | + | ||
| 82 | if (res.code === 1) { | 217 | if (res.code === 1) { |
| 83 | - // 模拟标题数据,实际项目中应由后端返回 | 218 | + // API 已返回 title 字段,直接使用后端数据 |
| 84 | - // 这里为了演示效果,如果后端没有返回 title,就模拟一个 | 219 | + detail.value = res.data |
| 85 | - const mockTitle = '关于系统维护升级的通知公告' | ||
| 86 | - detail.value = { | ||
| 87 | - ...res.data, | ||
| 88 | - title: res.data.title || mockTitle | ||
| 89 | - } | ||
| 90 | 220 | ||
| 91 | // 查看消息后刷新用户信息,更新未读消息数 | 221 | // 查看消息后刷新用户信息,更新未读消息数 |
| 92 | const userStore = useUserStore() | 222 | const userStore = useUserStore() |
| ... | @@ -128,4 +258,23 @@ useLoad((options) => { | ... | @@ -128,4 +258,23 @@ useLoad((options) => { |
| 128 | margin-bottom: 10rpx; | 258 | margin-bottom: 10rpx; |
| 129 | } | 259 | } |
| 130 | } | 260 | } |
| 261 | + | ||
| 262 | +/* 计划书状态标记样式 */ | ||
| 263 | +.status-badge { | ||
| 264 | + padding: 4rpx 12rpx; | ||
| 265 | + border-radius: 9999rpx; | ||
| 266 | + font-size: 22rpx; | ||
| 267 | + font-weight: 500; | ||
| 268 | + white-space: nowrap; | ||
| 269 | +} | ||
| 270 | + | ||
| 271 | +.status-generated { | ||
| 272 | + background-color: #D1FAE5; | ||
| 273 | + color: #059669; | ||
| 274 | +} | ||
| 275 | + | ||
| 276 | +.status-viewed { | ||
| 277 | + background-color: #E5E7EB; | ||
| 278 | + color: #6B7280; | ||
| 279 | +} | ||
| 131 | </style> | 280 | </style> | ... | ... |
| ... | @@ -67,11 +67,17 @@ function mockDelay(min = 100, max = 300) { | ... | @@ -67,11 +67,17 @@ function mockDelay(min = 100, max = 300) { |
| 67 | 67 | ||
| 68 | /** | 68 | /** |
| 69 | * 真实的可预览测试文件地址 | 69 | * 真实的可预览测试文件地址 |
| 70 | - * 来源:GitHub 和其他公开的测试资源 | 70 | + * |
| 71 | + * 来源说明: | ||
| 72 | + * - 项目 CDN: cdn.ipadbiz.cn(项目自有 CDN,最稳定) | ||
| 73 | + * - calibre-ebook.com: Calibre 官方测试文件 | ||
| 74 | + * - filesamples.com: 文件格式测试样本 | ||
| 75 | + * - Microsoft: 官方示例文件 | ||
| 71 | */ | 76 | */ |
| 72 | const TEST_FILES = { | 77 | const TEST_FILES = { |
| 73 | - // PDF 文档 | 78 | + // PDF 文档(优先使用项目 CDN) |
| 74 | pdf: [ | 79 | pdf: [ |
| 80 | + 'https://cdn.ipadbiz.cn/manulife/document/test.pdf', // 项目 CDN(最可靠) | ||
| 75 | 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', | 81 | 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', |
| 76 | 'https://www.africau.edu/images/default/sample.pdf' | 82 | 'https://www.africau.edu/images/default/sample.pdf' |
| 77 | ], | 83 | ], |
| ... | @@ -83,12 +89,14 @@ const TEST_FILES = { | ... | @@ -83,12 +89,14 @@ const TEST_FILES = { |
| 83 | 89 | ||
| 84 | // Excel 表格 (xlsx) | 90 | // Excel 表格 (xlsx) |
| 85 | xlsx: [ | 91 | xlsx: [ |
| 92 | + 'https://filesamples.com/samples/document/xlsx/sample1.xlsx', // filesamples 更稳定 | ||
| 86 | 'https://go.microsoft.com/fwlink/?LinkID=512104&clcid=0x0409' | 93 | 'https://go.microsoft.com/fwlink/?LinkID=512104&clcid=0x0409' |
| 87 | ], | 94 | ], |
| 88 | 95 | ||
| 89 | - // PPT 演示文稿 (pptx) | 96 | + // PPT 演示文稿 (ppt/pptx) |
| 90 | pptx: [ | 97 | pptx: [ |
| 91 | - 'https://www.africau.edu/images/default/sample.pptx' | 98 | + 'https://www.africau.edu/images/default/sample.pptx', |
| 99 | + 'https://filesamples.com/samples/document/ppt/sample1.ppt' | ||
| 92 | ], | 100 | ], |
| 93 | 101 | ||
| 94 | // 图片 | 102 | // 图片 |
| ... | @@ -617,20 +625,24 @@ const MESSAGE_TITLES = [ | ... | @@ -617,20 +625,24 @@ const MESSAGE_TITLES = [ |
| 617 | 625 | ||
| 618 | /** | 626 | /** |
| 619 | * 生成消息列表项 | 627 | * 生成消息列表项 |
| 628 | + * | ||
| 629 | + * @description 按 API 规范生成消息 Mock 数据 | ||
| 630 | + * @param {number} id - 消息 ID | ||
| 631 | + * @returns {Object} 消息对象 | ||
| 620 | */ | 632 | */ |
| 621 | function generateMessageItem(id) { | 633 | function generateMessageItem(id) { |
| 622 | const title = MESSAGE_TITLES[Math.floor(Math.random() * MESSAGE_TITLES.length)] | 634 | const title = MESSAGE_TITLES[Math.floor(Math.random() * MESSAGE_TITLES.length)] |
| 623 | const now = new Date() | 635 | const now = new Date() |
| 624 | const createDate = new Date(now.getTime() - Math.random() * 30 * 24 * 60 * 60 * 1000) | 636 | const createDate = new Date(now.getTime() - Math.random() * 30 * 24 * 60 * 60 * 1000) |
| 637 | + const isUnread = Math.random() > 0.5 | ||
| 625 | 638 | ||
| 626 | return { | 639 | return { |
| 627 | id: id, | 640 | id: id, |
| 628 | - title: title, | 641 | + title: title, // API 返回的标题字段 |
| 629 | - intro: `这是一条关于"${title}"的重要通知,请及时查看详情...`, | 642 | + note: `这是一条关于"${title}"的通知。\n点击查看详情了解更多信息。`, // 消息内容(note 字段) |
| 630 | - content: `这是一条关于"${title}"的重要通知,请及时查看详情。如需了解更多信息,请联系客服。`, | 643 | + created_time: formatDate(createDate), // 发消息时间 |
| 631 | - create_time: formatDate(createDate), | 644 | + status: isUnread ? 'send' : 'read', // send=已发送未读取,read=已读取 |
| 632 | - is_read: Math.random() > 0.5 ? 1 : 0, | 645 | + pk_id: Math.floor(Math.random() * 10000) // 计划书订单 ID(可选) |
| 633 | - type: Math.random() > 0.5 ? 'notice' : 'system' | ||
| 634 | } | 646 | } |
| 635 | } | 647 | } |
| 636 | 648 | ||
| ... | @@ -646,21 +658,52 @@ function formatDate(date) { | ... | @@ -646,21 +658,52 @@ function formatDate(date) { |
| 646 | 658 | ||
| 647 | /** | 659 | /** |
| 648 | * Mock: myListAPI (消息列表) | 660 | * Mock: myListAPI (消息列表) |
| 661 | + * | ||
| 662 | + * @description 第1页(page=0)前面会插入三条测试消息用于测试计划书查看功能 | ||
| 649 | */ | 663 | */ |
| 650 | export async function mockMessageListAPI(params) { | 664 | export async function mockMessageListAPI(params) { |
| 651 | await mockDelay() | 665 | await mockDelay() |
| 652 | 666 | ||
| 653 | - const { page = 1, limit = 10 } = params | 667 | + const { page = 0, limit = 10 } = params // 前端传的是从0开始的页码 |
| 654 | const totalPages = 8 | 668 | const totalPages = 8 |
| 655 | 669 | ||
| 656 | - if (page > totalPages) { | 670 | + if (page >= totalPages) { |
| 657 | return { code: 1, msg: 'success', data: { list: [] } } | 671 | return { code: 1, msg: 'success', data: { list: [] } } |
| 658 | } | 672 | } |
| 659 | 673 | ||
| 660 | const list = [] | 674 | const list = [] |
| 661 | - const startIndex = (page - 1) * limit | ||
| 662 | 675 | ||
| 663 | - for (let i = 0; i < limit; i++) { | 676 | + // 第1页(page=0):在前面插入三条测试消息(用于测试计划书查看) |
| 677 | + if (page === 0) { | ||
| 678 | + list.push( | ||
| 679 | + { | ||
| 680 | + id: '1001', | ||
| 681 | + title: '【测试】已生成计划书(单文件)', | ||
| 682 | + note: '测试场景:状态为"已生成",只有一个计划书文件,可直接查看。', | ||
| 683 | + created_time: '2026-02-13', | ||
| 684 | + status: 'send' | ||
| 685 | + }, | ||
| 686 | + { | ||
| 687 | + id: '1002', | ||
| 688 | + title: '【测试】已生成计划书(多文件)', | ||
| 689 | + note: '测试场景:状态为"已生成",有3个计划书文件,点击后会显示选择弹框。', | ||
| 690 | + created_time: '2026-02-13', | ||
| 691 | + status: 'send' | ||
| 692 | + }, | ||
| 693 | + { | ||
| 694 | + id: '1003', | ||
| 695 | + title: '【测试】已查看计划书', | ||
| 696 | + note: '测试场景:状态为"已查看",查看后不会再次标记。', | ||
| 697 | + created_time: '2026-02-13', | ||
| 698 | + status: 'read' | ||
| 699 | + } | ||
| 700 | + ) | ||
| 701 | + } | ||
| 702 | + | ||
| 703 | + const startIndex = page * limit | ||
| 704 | + const remainingCount = limit - list.length | ||
| 705 | + | ||
| 706 | + for (let i = 0; i < remainingCount; i++) { | ||
| 664 | list.push(generateMessageItem(startIndex + i + 1)) | 707 | list.push(generateMessageItem(startIndex + i + 1)) |
| 665 | } | 708 | } |
| 666 | 709 | ||
| ... | @@ -673,6 +716,136 @@ export async function mockMessageListAPI(params) { | ... | @@ -673,6 +716,136 @@ export async function mockMessageListAPI(params) { |
| 673 | } | 716 | } |
| 674 | } | 717 | } |
| 675 | 718 | ||
| 719 | +/** | ||
| 720 | + * Mock: detailAPI (消息详情) | ||
| 721 | + * | ||
| 722 | + * @description 根据 ID 返回消息详情,包含完整的 proposal 数据 | ||
| 723 | + * @param {Object} params 请求参数 | ||
| 724 | + * @param {string|number} params.i 消息ID | ||
| 725 | + * @returns {Promise} 详情数据 | ||
| 726 | + * | ||
| 727 | + * @description 测试数据说明: | ||
| 728 | + * - id='1001': 已生成 + 单文件 | ||
| 729 | + * - id='1002': 已生成 + 多文件 (3个文件) | ||
| 730 | + * - id='1003': 已查看 + 单文件 | ||
| 731 | + * - 其他ID: 待处理状态 + 2个文件 | ||
| 732 | + */ | ||
| 733 | +export async function mockDetailAPI(params) { | ||
| 734 | + await mockDelay() | ||
| 735 | + | ||
| 736 | + const { i: id } = params | ||
| 737 | + | ||
| 738 | + if (!id) { | ||
| 739 | + return { code: 0, msg: '消息ID不能为空', data: null } | ||
| 740 | + } | ||
| 741 | + | ||
| 742 | + // 生成基础消息数据 | ||
| 743 | + const messageItem = generateMessageItem(id) | ||
| 744 | + | ||
| 745 | + // 根据消息 ID 返回不同状态的计划书数据(用于测试) | ||
| 746 | + let proposal = null | ||
| 747 | + | ||
| 748 | + if (id === '1001') { | ||
| 749 | + // 场景1: 已生成 + 单文件 | ||
| 750 | + proposal = { | ||
| 751 | + id: 1001, | ||
| 752 | + customer_name: '张三', | ||
| 753 | + product_name: '年金险产品A', | ||
| 754 | + categories: [{ id: '1', name: '基本信息' }], | ||
| 755 | + created_time: messageItem.created_time, | ||
| 756 | + order_status: '7', // 已生成 | ||
| 757 | + proposal_files: [ | ||
| 758 | + { | ||
| 759 | + id: 1, | ||
| 760 | + file_name: '计划书.pdf', | ||
| 761 | + file_url: TEST_FILES.pdf[0] // 使用真实的 PDF 测试文件 | ||
| 762 | + } | ||
| 763 | + ] | ||
| 764 | + } | ||
| 765 | + } else if (id === '1002') { | ||
| 766 | + // 场景2: 已生成 + 多文件 (3个文件) | ||
| 767 | + proposal = { | ||
| 768 | + id: 1002, | ||
| 769 | + customer_name: '李四', | ||
| 770 | + product_name: '终身寿险产品B', | ||
| 771 | + categories: [ | ||
| 772 | + { id: '1', name: '基本信息' }, | ||
| 773 | + { id: '2', name: '保障内容' }, | ||
| 774 | + { id: '3', name: '缴费方式' } | ||
| 775 | + ], | ||
| 776 | + created_time: messageItem.created_time, | ||
| 777 | + order_status: '7', // 已生成 | ||
| 778 | + proposal_files: [ | ||
| 779 | + { | ||
| 780 | + id: 1, | ||
| 781 | + file_name: '计划书完整版.pdf', | ||
| 782 | + file_url: TEST_FILES.pdf[0] | ||
| 783 | + }, | ||
| 784 | + { | ||
| 785 | + id: 2, | ||
| 786 | + file_name: '产品条款说明书.pdf', | ||
| 787 | + file_url: TEST_FILES.pdf[0] | ||
| 788 | + }, | ||
| 789 | + { | ||
| 790 | + id: 3, | ||
| 791 | + file_name: '费率表.pdf', | ||
| 792 | + file_url: TEST_FILES.pdf[0] | ||
| 793 | + } | ||
| 794 | + ] | ||
| 795 | + } | ||
| 796 | + } else if (id === '1003') { | ||
| 797 | + // 场景3: 已查看 + 单文件 | ||
| 798 | + proposal = { | ||
| 799 | + id: 1003, | ||
| 800 | + customer_name: '王五', | ||
| 801 | + product_name: '重疾险产品C', | ||
| 802 | + categories: [{ id: '1', name: '基本信息' }], | ||
| 803 | + created_time: messageItem.created_time, | ||
| 804 | + order_status: '9', // 已查看 | ||
| 805 | + proposal_files: [ | ||
| 806 | + { | ||
| 807 | + id: 1, | ||
| 808 | + file_name: '计划书.pdf', | ||
| 809 | + file_url: TEST_FILES.pdf[0] | ||
| 810 | + } | ||
| 811 | + ] | ||
| 812 | + } | ||
| 813 | + } else { | ||
| 814 | + // 默认: 待处理状态 + 2个文件(无法查看) | ||
| 815 | + proposal = { | ||
| 816 | + id: id, | ||
| 817 | + customer_name: '测试用户', | ||
| 818 | + product_name: '测试产品', | ||
| 819 | + categories: [{ id: '1', name: '基本信息' }], | ||
| 820 | + created_time: messageItem.created_time, | ||
| 821 | + order_status: '3', // 待处理 | ||
| 822 | + proposal_files: [ | ||
| 823 | + { | ||
| 824 | + id: 1, | ||
| 825 | + file_name: '计划书文件.pdf', | ||
| 826 | + file_url: TEST_FILES.pdf[0] | ||
| 827 | + }, | ||
| 828 | + { | ||
| 829 | + id: 2, | ||
| 830 | + file_name: '产品说明.pdf', | ||
| 831 | + file_url: TEST_FILES.pdf[0] | ||
| 832 | + } | ||
| 833 | + ] | ||
| 834 | + } | ||
| 835 | + } | ||
| 836 | + | ||
| 837 | + console.log(`[Mock] detailAPI - 消息ID: ${id}, 计划书状态: ${proposal.order_status}`) | ||
| 838 | + | ||
| 839 | + return { | ||
| 840 | + code: 1, | ||
| 841 | + msg: 'success', | ||
| 842 | + data: { | ||
| 843 | + ...messageItem, | ||
| 844 | + proposal | ||
| 845 | + } | ||
| 846 | + } | ||
| 847 | +} | ||
| 848 | + | ||
| 676 | // ============================================================================ | 849 | // ============================================================================ |
| 677 | // 6. 收藏列表 Mock (favoriteListAPI) | 850 | // 6. 收藏列表 Mock (favoriteListAPI) |
| 678 | // ============================================================================ | 851 | // ============================================================================ |
| ... | @@ -1018,6 +1191,8 @@ export async function mockAPI(apiName, params) { | ... | @@ -1018,6 +1191,8 @@ export async function mockAPI(apiName, params) { |
| 1018 | return await mockSearchAPI(params) | 1191 | return await mockSearchAPI(params) |
| 1019 | case 'myListAPI': | 1192 | case 'myListAPI': |
| 1020 | return await mockMessageListAPI(params) | 1193 | return await mockMessageListAPI(params) |
| 1194 | + case 'detailAPI': | ||
| 1195 | + return await mockDetailAPI(params) | ||
| 1021 | case 'favoriteListAPI': | 1196 | case 'favoriteListAPI': |
| 1022 | return await mockFavoriteListAPI(params) | 1197 | return await mockFavoriteListAPI(params) |
| 1023 | case 'feedbackListAPI': | 1198 | case 'feedbackListAPI': | ... | ... |
-
Please register or login to post a comment