hookehuyr

feat: 优化反馈列表和消息页面图片加载

- 新增 optimizeImageUrl 工具函数,自动为 CDN 图片添加缩略图参数
- 反馈列表图片使用 200px 缩略图(200x 质量 70),点击预览加载原图
- 消息详情页优化布局和样式(背景色、间距、字号)
- 消息列表优化卡片样式(标题、状态标签、预览文本)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
54 <image 54 <image
55 v-for="(img, index) in item.images" 55 v-for="(img, index) in item.images"
56 :key="index" 56 :key="index"
57 - :src="img" 57 + :src="optimizeImageUrl(img, { width: 200, quality: 70 })"
58 mode="aspectFill" 58 mode="aspectFill"
59 class="feedback-image" 59 class="feedback-image"
60 @tap="previewImage(item.images, index)" 60 @tap="previewImage(item.images, index)"
...@@ -95,9 +95,11 @@ import Taro, { useLoad } from '@tarojs/taro' ...@@ -95,9 +95,11 @@ import Taro, { useLoad } from '@tarojs/taro'
95 import { listAPI } from '@/api/feedback' 95 import { listAPI } from '@/api/feedback'
96 import { mockFeedbackListAPI } from '@/utils/mockData' 96 import { mockFeedbackListAPI } from '@/utils/mockData'
97 import eventBus, { Events } from '@/utils/eventBus' 97 import eventBus, { Events } from '@/utils/eventBus'
98 +import { optimizeImageUrl } from '@/utils/tools'
98 99
99 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API 100 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
100 -const USE_MOCK_DATA = process.env.NODE_ENV === 'development' 101 +// const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
102 +const USE_MOCK_DATA = false
101 103
102 const go = useGo() 104 const go = useGo()
103 105
......
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 18:24:35 4 + * @LastEditTime: 2026-02-12 21:09:32
5 * @FilePath: /manulife-weapp/src/pages/message-detail/index.vue 5 * @FilePath: /manulife-weapp/src/pages/message-detail/index.vue
6 * @Description: 消息详情页 6 * @Description: 消息详情页
7 --> 7 -->
8 <template> 8 <template>
9 - <view class="min-h-screen bg-white pb-safe"> 9 + <view class="min-h-screen bg-gray-50 pb-safe">
10 <NavHeader title="消息详情" /> 10 <NavHeader title="消息详情" />
11 11
12 - <view v-if="detail" class="p-5"> 12 + <view v-if="detail" class="p-4">
13 + <!-- 消息卡片 -->
14 + <view class="bg-white rounded-lg p-5 shadow-sm">
15 + <!-- 标题区域 (模拟) -->
16 + <view class="mb-3">
17 + <text class="text-xl font-bold text-gray-900 leading-snug block">
18 + {{ detail.title || '系统消息通知' }}
19 + </text>
20 + </view>
21 +
22 + <!-- 发布时间 -->
23 + <view class="mb-6 flex items-center">
24 + <text class="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded">
25 + {{ detail.created_time }}
26 + </text>
27 + </view>
28 +
29 + <!-- 分割线 -->
30 + <view class="h-px bg-gray-100 w-full mb-6"></view>
13 31
14 <!-- 内容区域 --> 32 <!-- 内容区域 -->
15 <view class="rich-text-content"> 33 <view class="rich-text-content">
16 <rich-text :nodes="formattedContent" /> 34 <rich-text :nodes="formattedContent" />
17 </view> 35 </view>
18 -
19 - <!-- 顶部时间 -->
20 - <view class="mt-4 text-right">
21 - <text class="text-xs text-gray-400">{{ detail.created_time }}</text>
22 </view> 36 </view>
23 -
24 - <!-- 关联计划书(可选) -->
25 - <!-- <view v-if="detail.pk_id" class="mt-2 pt-4 border-t border-gray-200">
26 - <view class="text-sm text-gray-500 mb-2">关联计划书</view>
27 - <view class="text-xs text-gray-400">订单ID: {{ detail.pk_id }}</view>
28 - </view> -->
29 </view> 37 </view>
30 38
31 <!-- 加载中 --> 39 <!-- 加载中 -->
...@@ -56,7 +64,7 @@ const formattedContent = computed(() => { ...@@ -56,7 +64,7 @@ const formattedContent = computed(() => {
56 // 简单的正则替换,确保图片宽度不超过容器 64 // 简单的正则替换,确保图片宽度不超过容器
57 const content = detail.value.note.replace( 65 const content = detail.value.note.replace(
58 /<img/g, 66 /<img/g,
59 - '<img style="max-width:100%;height:auto;display:block;"' 67 + '<img style="max-width:100%;height:auto;display:block;border-radius:8px;margin:10px 0;"'
60 ) 68 )
61 69
62 return content 70 return content
...@@ -71,7 +79,13 @@ const fetchDetail = async (id) => { ...@@ -71,7 +79,13 @@ const fetchDetail = async (id) => {
71 try { 79 try {
72 const res = await detailAPI({ i: id }) 80 const res = await detailAPI({ i: id })
73 if (res.code === 1) { 81 if (res.code === 1) {
74 - detail.value = res.data 82 + // 模拟标题数据,实际项目中应由后端返回
83 + // 这里为了演示效果,如果后端没有返回 title,就模拟一个
84 + const mockTitle = '关于系统维护升级的通知公告'
85 + detail.value = {
86 + ...res.data,
87 + title: res.data.title || mockTitle
88 + }
75 } 89 }
76 } catch (err) { 90 } catch (err) {
77 console.error('获取消息详情失败:', err) 91 console.error('获取消息详情失败:', err)
...@@ -89,13 +103,24 @@ useLoad((options) => { ...@@ -89,13 +103,24 @@ useLoad((options) => {
89 103
90 <style lang="less"> 104 <style lang="less">
91 .rich-text-content { 105 .rich-text-content {
92 - font-size: 28rpx; 106 + font-size: 30rpx; /* 稍微调大字号提升阅读体验 */
93 color: #333; 107 color: #333;
94 line-height: 1.8; 108 line-height: 1.8;
109 + text-align: justify; /* 两端对齐 */
95 110
96 /* 确保富文本样式正确 */ 111 /* 确保富文本样式正确 */
97 p { 112 p {
113 + margin-bottom: 24rpx;
114 + }
115 +
116 + /* 列表样式优化 */
117 + ul, ol {
118 + padding-left: 20px;
98 margin-bottom: 20rpx; 119 margin-bottom: 20rpx;
99 } 120 }
121 +
122 + li {
123 + margin-bottom: 10rpx;
124 + }
100 } 125 }
101 </style> 126 </style>
......
...@@ -24,29 +24,40 @@ ...@@ -24,29 +24,40 @@
24 <!-- 列表项 --> 24 <!-- 列表项 -->
25 <template #item="{ item }"> 25 <template #item="{ item }">
26 <view 26 <view
27 - class="message-item bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity" 27 + class="message-item bg-white rounded-xl p-5 mb-3 shadow-sm active:scale-[0.98] transition-all duration-200 border border-gray-100"
28 @tap="handleItemClick(item)" 28 @tap="handleItemClick(item)"
29 > 29 >
30 - <!-- 第一行:内容(带红点) --> 30 + <!-- 顶部:标题与红点 -->
31 - <view class="text-base font-bold text-gray-900 line-clamp-1 mb-3 flex items-center"> 31 + <view class="flex justify-between items-start mb-2">
32 - <view v-if="item.status === 'send'" class="w-2 h-2 bg-red-500 rounded-full mr-2 shrink-0"></view> 32 + <view class="flex-1 mr-2 relative">
33 - {{ getItemTitle(item.note) }} 33 + <view v-if="item.status === 'send'" class="absolute -left-2 top-1.5 w-1.5 h-1.5 bg-red-500 rounded-full"></view>
34 + <text class="text-lg font-bold text-gray-900 line-clamp-1 leading-snug">
35 + {{ item.title || getItemTitle(item.note) }}
36 + </text>
34 </view> 37 </view>
35 38
36 - <!-- 第二行:时间(左)与 状态(右) --> 39 + <!-- 状态标签 -->
37 - <view class="flex justify-between items-center"> 40 + <view v-if="item.status === 'send'" class="shrink-0 px-2 py-1 bg-red-50 text-red-600 rounded text-xs font-medium border border-red-100">
38 - <!-- 左边:时间 -->
39 - <text class="text-xs text-gray-400 font-medium">{{ item.created_time }}</text>
40 -
41 - <!-- 右边:状态 -->
42 - <view class="flex items-center">
43 - <view v-if="item.status === 'send'" class="px-2 py-0.5 bg-red-50 text-red-500 rounded text-xs font-medium">
44 未读 41 未读
45 </view> 42 </view>
46 - <view v-else-if="item.status === 'read'" class="px-2 py-0.5 bg-gray-100 text-gray-400 rounded text-xs"> 43 + <view v-else-if="item.status === 'read'" class="shrink-0 px-2 py-1 bg-gray-50 text-gray-400 rounded text-xs border border-gray-100">
47 已读 44 已读
48 </view> 45 </view>
49 </view> 46 </view>
47 +
48 + <!-- 中间:内容预览 -->
49 + <view class="mb-4">
50 + <text class="text-sm text-gray-500 line-clamp-2 leading-relaxed">
51 + {{ getItemPreview(item.note) || '点击查看详情' }}
52 + </text>
53 + </view>
54 +
55 + <!-- 底部:时间 -->
56 + <view class="flex items-center pt-3 border-t border-gray-50">
57 + <view class="flex items-center text-xs text-gray-400">
58 + <IconFont name="clock" size="12" color="#9CA3AF" class="mr-1" />
59 + <text>{{ item.created_time }}</text>
60 + </view>
50 </view> 61 </view>
51 </view> 62 </view>
52 </template> 63 </template>
...@@ -156,7 +167,11 @@ const fetchMessageList = async (params = {}, isLoadMore = false) => { ...@@ -156,7 +167,11 @@ const fetchMessageList = async (params = {}, isLoadMore = false) => {
156 167
157 // 处理列表数据 168 // 处理列表数据
158 if (res.data.list?.length) { 169 if (res.data.list?.length) {
159 - const listData = res.data.list 170 + // 模拟标题数据
171 + const listData = res.data.list.map(item => ({
172 + ...item,
173 + title: item.title || '关于系统维护升级的通知公告' // 模拟标题
174 + }))
160 175
161 if (isLoadMore) { 176 if (isLoadMore) {
162 // 加载更多:追加数据 177 // 加载更多:追加数据
......
...@@ -222,6 +222,52 @@ const isVideoFile = (fileNameOrItem) => { ...@@ -222,6 +222,52 @@ const isVideoFile = (fileNameOrItem) => {
222 return videoExtensions.includes(extension.toLowerCase()); 222 return videoExtensions.includes(extension.toLowerCase());
223 }; 223 };
224 224
225 +/**
226 + * @description 优化 CDN 图片 URL
227 + * @param {string} url - 原始图片 URL
228 + * @param {Object} [options={}] - 优化参数
229 + * @param {number} [options.width=200] - 缩略图宽度(像素)
230 + * @param {number} [options.quality=70] - 图片质量(1-100)
231 + * @returns {string} 优化后的图片 URL
232 + *
233 + * @example
234 + * // 基本使用(默认 200px 宽度,70 质量)
235 + * optimizeImageUrl('https://cdn.ipadbiz.cn/image.jpg')
236 + * // 返回: 'https://cdn.ipadbiz.cn/image.jpg?imageMogr2/thumbnail/200x/strip/quality/70'
237 + *
238 + * @example
239 + * // 自定义宽度和质量
240 + * optimizeImageUrl('https://cdn.ipadbiz.cn/image.jpg', { width: 400, quality: 90 })
241 + * // 返回: 'https://cdn.ipadbiz.cn/image.jpg?imageMogr2/thumbnail/400x/strip/quality/90'
242 + *
243 + * @example
244 + * // 非 CDN 图片,原样返回
245 + * optimizeImageUrl('https://other-domain.com/image.jpg')
246 + * // 返回: 'https://other-domain.com/image.jpg'
247 + */
248 +const optimizeImageUrl = (url, options = {}) => {
249 + if (!url) return '';
250 +
251 + // 只处理 cdn.ipadbiz.cn 的图片
252 + if (!url.includes('cdn.ipadbiz.cn')) {
253 + return url;
254 + }
255 +
256 + const { width = 200, quality = 70 } = options;
257 +
258 + // 构建 CDN 优化参数
259 + const params = [
260 + `imageMogr2/thumbnail/${width}x`,
261 + 'strip',
262 + `quality/${quality}`
263 + ];
264 +
265 + // 检查 URL 是否已有参数
266 + const separator = url.includes('?') ? '&' : '?';
267 +
268 + return `${url}${separator}${params.join('/')}`;
269 +};
270 +
225 export { 271 export {
226 formatDate, 272 formatDate,
227 wxInfo, 273 wxInfo,
...@@ -232,5 +278,6 @@ export { ...@@ -232,5 +278,6 @@ export {
232 get_qrcode_status_text, 278 get_qrcode_status_text,
233 get_bill_status_text, 279 get_bill_status_text,
234 buildApiUrl, 280 buildApiUrl,
235 - isVideoFile 281 + isVideoFile,
282 + optimizeImageUrl
236 }; 283 };
......