hookehuyr

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -54,7 +54,7 @@
<image
v-for="(img, index) in item.images"
:key="index"
:src="img"
:src="optimizeImageUrl(img, { width: 200, quality: 70 })"
mode="aspectFill"
class="feedback-image"
@tap="previewImage(item.images, index)"
......@@ -95,9 +95,11 @@ import Taro, { useLoad } from '@tarojs/taro'
import { listAPI } from '@/api/feedback'
import { mockFeedbackListAPI } from '@/utils/mockData'
import eventBus, { Events } from '@/utils/eventBus'
import { optimizeImageUrl } from '@/utils/tools'
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
const USE_MOCK_DATA = false
const go = useGo()
......
<!--
* @Date:2026-02-03 21:26:58
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-12 18:24:35
* @LastEditTime: 2026-02-12 21:09:32
* @FilePath: /manulife-weapp/src/pages/message-detail/index.vue
* @Description: 消息详情页
-->
<template>
<view class="min-h-screen bg-white pb-safe">
<view class="min-h-screen bg-gray-50 pb-safe">
<NavHeader title="消息详情" />
<view v-if="detail" class="p-5">
<view v-if="detail" class="p-4">
<!-- 消息卡片 -->
<view class="bg-white rounded-lg p-5 shadow-sm">
<!-- 标题区域 (模拟) -->
<view class="mb-3">
<text class="text-xl font-bold text-gray-900 leading-snug block">
{{ detail.title || '系统消息通知' }}
</text>
</view>
<!-- 发布时间 -->
<view class="mb-6 flex items-center">
<text class="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded">
{{ detail.created_time }}
</text>
</view>
<!-- 分割线 -->
<view class="h-px bg-gray-100 w-full mb-6"></view>
<!-- 内容区域 -->
<view class="rich-text-content">
<rich-text :nodes="formattedContent" />
</view>
<!-- 顶部时间 -->
<view class="mt-4 text-right">
<text class="text-xs text-gray-400">{{ detail.created_time }}</text>
</view>
<!-- 关联计划书(可选) -->
<!-- <view v-if="detail.pk_id" class="mt-2 pt-4 border-t border-gray-200">
<view class="text-sm text-gray-500 mb-2">关联计划书</view>
<view class="text-xs text-gray-400">订单ID: {{ detail.pk_id }}</view>
</view> -->
</view>
<!-- 加载中 -->
......@@ -56,7 +64,7 @@ const formattedContent = computed(() => {
// 简单的正则替换,确保图片宽度不超过容器
const content = detail.value.note.replace(
/<img/g,
'<img style="max-width:100%;height:auto;display:block;"'
'<img style="max-width:100%;height:auto;display:block;border-radius:8px;margin:10px 0;"'
)
return content
......@@ -71,7 +79,13 @@ const fetchDetail = async (id) => {
try {
const res = await detailAPI({ i: id })
if (res.code === 1) {
detail.value = res.data
// 模拟标题数据,实际项目中应由后端返回
// 这里为了演示效果,如果后端没有返回 title,就模拟一个
const mockTitle = '关于系统维护升级的通知公告'
detail.value = {
...res.data,
title: res.data.title || mockTitle
}
}
} catch (err) {
console.error('获取消息详情失败:', err)
......@@ -89,13 +103,24 @@ useLoad((options) => {
<style lang="less">
.rich-text-content {
font-size: 28rpx;
font-size: 30rpx; /* 稍微调大字号提升阅读体验 */
color: #333;
line-height: 1.8;
text-align: justify; /* 两端对齐 */
/* 确保富文本样式正确 */
p {
margin-bottom: 24rpx;
}
/* 列表样式优化 */
ul, ol {
padding-left: 20px;
margin-bottom: 20rpx;
}
li {
margin-bottom: 10rpx;
}
}
</style>
......
......@@ -24,29 +24,40 @@
<!-- 列表项 -->
<template #item="{ item }">
<view
class="message-item bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity"
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"
@tap="handleItemClick(item)"
>
<!-- 第一行:内容(带红点) -->
<view class="text-base font-bold text-gray-900 line-clamp-1 mb-3 flex items-center">
<view v-if="item.status === 'send'" class="w-2 h-2 bg-red-500 rounded-full mr-2 shrink-0"></view>
{{ getItemTitle(item.note) }}
<!-- 顶部:标题与红点 -->
<view class="flex justify-between items-start mb-2">
<view class="flex-1 mr-2 relative">
<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>
<text class="text-lg font-bold text-gray-900 line-clamp-1 leading-snug">
{{ item.title || getItemTitle(item.note) }}
</text>
</view>
<!-- 第二行:时间(左)与 状态(右) -->
<view class="flex justify-between items-center">
<!-- 左边:时间 -->
<text class="text-xs text-gray-400 font-medium">{{ item.created_time }}</text>
<!-- 右边:状态 -->
<view class="flex items-center">
<view v-if="item.status === 'send'" class="px-2 py-0.5 bg-red-50 text-red-500 rounded text-xs font-medium">
<!-- 状态标签 -->
<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">
未读
</view>
<view v-else-if="item.status === 'read'" class="px-2 py-0.5 bg-gray-100 text-gray-400 rounded text-xs">
<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">
已读
</view>
</view>
<!-- 中间:内容预览 -->
<view class="mb-4">
<text class="text-sm text-gray-500 line-clamp-2 leading-relaxed">
{{ getItemPreview(item.note) || '点击查看详情' }}
</text>
</view>
<!-- 底部:时间 -->
<view class="flex items-center pt-3 border-t border-gray-50">
<view class="flex items-center text-xs text-gray-400">
<IconFont name="clock" size="12" color="#9CA3AF" class="mr-1" />
<text>{{ item.created_time }}</text>
</view>
</view>
</view>
</template>
......@@ -156,7 +167,11 @@ const fetchMessageList = async (params = {}, isLoadMore = false) => {
// 处理列表数据
if (res.data.list?.length) {
const listData = res.data.list
// 模拟标题数据
const listData = res.data.list.map(item => ({
...item,
title: item.title || '关于系统维护升级的通知公告' // 模拟标题
}))
if (isLoadMore) {
// 加载更多:追加数据
......
......@@ -222,6 +222,52 @@ const isVideoFile = (fileNameOrItem) => {
return videoExtensions.includes(extension.toLowerCase());
};
/**
* @description 优化 CDN 图片 URL
* @param {string} url - 原始图片 URL
* @param {Object} [options={}] - 优化参数
* @param {number} [options.width=200] - 缩略图宽度(像素)
* @param {number} [options.quality=70] - 图片质量(1-100)
* @returns {string} 优化后的图片 URL
*
* @example
* // 基本使用(默认 200px 宽度,70 质量)
* optimizeImageUrl('https://cdn.ipadbiz.cn/image.jpg')
* // 返回: 'https://cdn.ipadbiz.cn/image.jpg?imageMogr2/thumbnail/200x/strip/quality/70'
*
* @example
* // 自定义宽度和质量
* optimizeImageUrl('https://cdn.ipadbiz.cn/image.jpg', { width: 400, quality: 90 })
* // 返回: 'https://cdn.ipadbiz.cn/image.jpg?imageMogr2/thumbnail/400x/strip/quality/90'
*
* @example
* // 非 CDN 图片,原样返回
* optimizeImageUrl('https://other-domain.com/image.jpg')
* // 返回: 'https://other-domain.com/image.jpg'
*/
const optimizeImageUrl = (url, options = {}) => {
if (!url) return '';
// 只处理 cdn.ipadbiz.cn 的图片
if (!url.includes('cdn.ipadbiz.cn')) {
return url;
}
const { width = 200, quality = 70 } = options;
// 构建 CDN 优化参数
const params = [
`imageMogr2/thumbnail/${width}x`,
'strip',
`quality/${quality}`
];
// 检查 URL 是否已有参数
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}${params.join('/')}`;
};
export {
formatDate,
wxInfo,
......@@ -232,5 +278,6 @@ export {
get_qrcode_status_text,
get_bill_status_text,
buildApiUrl,
isVideoFile
isVideoFile,
optimizeImageUrl
};
......