feat: 新增消息中心功能模块
添加消息列表页和消息详情页,支持查看系统消息详情 在个人中心菜单中增加消息入口,并更新组件类型声明
Showing
7 changed files
with
256 additions
and
1 deletions
| ... | @@ -16,8 +16,8 @@ declare module 'vue' { | ... | @@ -16,8 +16,8 @@ declare module 'vue' { |
| 16 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] | 16 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] |
| 17 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] | 17 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] |
| 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 19 | + NutEmpty: typeof import('@nutui/nutui-taro')['Empty'] | ||
| 19 | NutInput: typeof import('@nutui/nutui-taro')['Input'] | 20 | NutInput: typeof import('@nutui/nutui-taro')['Input'] |
| 20 | - NutLoading: typeof import('@nutui/nutui-taro')['Loading'] | ||
| 21 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] | 21 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] |
| 22 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] | 22 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] |
| 23 | NutRadio: typeof import('@nutui/nutui-taro')['Radio'] | 23 | NutRadio: typeof import('@nutui/nutui-taro')['Radio'] | ... | ... |
| ... | @@ -26,6 +26,8 @@ const pages = [ | ... | @@ -26,6 +26,8 @@ const pages = [ |
| 26 | 'pages/feedback/index', | 26 | 'pages/feedback/index', |
| 27 | 'pages/login/index', | 27 | 'pages/login/index', |
| 28 | 'pages/help-center/index', | 28 | 'pages/help-center/index', |
| 29 | + 'pages/message/index', | ||
| 30 | + 'pages/message-detail/index', | ||
| 29 | ] | 31 | ] |
| 30 | 32 | ||
| 31 | if (process.env.NODE_ENV === 'development') { | 33 | if (process.env.NODE_ENV === 'development') { | ... | ... |
src/pages/message-detail/index.config.js
0 → 100644
src/pages/message-detail/index.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2026-02-03 21:26:58 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-02-03 21:30:21 | ||
| 5 | + * @FilePath: /manulife-weapp/src/pages/message-detail/index.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <view class="min-h-screen bg-white pb-safe"> | ||
| 10 | + <NavHeader title="消息详情" /> | ||
| 11 | + | ||
| 12 | + <view v-if="detail" class="p-5"> | ||
| 13 | + <!-- 标题 --> | ||
| 14 | + <view class="text-xl font-bold text-gray-900 mb-3 leading-tight"> | ||
| 15 | + {{ detail.title }} | ||
| 16 | + </view> | ||
| 17 | + | ||
| 18 | + <!-- 元信息 --> | ||
| 19 | + <view class="flex items-center text-xs text-gray-400 mb-6"> | ||
| 20 | + <text>{{ detail.create_time }}</text> | ||
| 21 | + <text class="mx-2">·</text> | ||
| 22 | + <text>Manulife</text> | ||
| 23 | + </view> | ||
| 24 | + | ||
| 25 | + <!-- 内容区域 --> | ||
| 26 | + <view class="rich-text-content"> | ||
| 27 | + <rich-text :nodes="formattedContent" /> | ||
| 28 | + </view> | ||
| 29 | + </view> | ||
| 30 | + | ||
| 31 | + <!-- 加载中 --> | ||
| 32 | + <view v-else-if="loading" class="flex justify-center py-10"> | ||
| 33 | + <view class="w-8 h-8 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></view> | ||
| 34 | + </view> | ||
| 35 | + | ||
| 36 | + <!-- 错误/空状态 --> | ||
| 37 | + <nut-empty v-else description="未找到消息内容" image="error" /> | ||
| 38 | + </view> | ||
| 39 | +</template> | ||
| 40 | + | ||
| 41 | +<script setup> | ||
| 42 | +import { ref, computed } from 'vue' | ||
| 43 | +import { useLoad } from '@tarojs/taro' | ||
| 44 | +import NavHeader from '@/components/NavHeader.vue' | ||
| 45 | +import { detailAPI } from '@/api/news' | ||
| 46 | + | ||
| 47 | +const detail = ref(null) | ||
| 48 | +const loading = ref(true) | ||
| 49 | + | ||
| 50 | +/** | ||
| 51 | + * @description 格式化富文本内容,处理图片宽度等问题 | ||
| 52 | + */ | ||
| 53 | +const formattedContent = computed(() => { | ||
| 54 | + if (!detail.value?.content) return '' | ||
| 55 | + // 简单的正则替换,确保图片宽度不超过容器 | ||
| 56 | + return detail.value.content.replace( | ||
| 57 | + /<img/g, | ||
| 58 | + '<img style="max-width:100%;height:auto;display:block;"' | ||
| 59 | + ) | ||
| 60 | +}) | ||
| 61 | + | ||
| 62 | +/** | ||
| 63 | + * @description 获取消息详情 | ||
| 64 | + * @param {string|number} id 消息ID | ||
| 65 | + */ | ||
| 66 | +const fetchDetail = async (id) => { | ||
| 67 | + loading.value = true | ||
| 68 | + try { | ||
| 69 | + const res = await detailAPI({ i: id }) | ||
| 70 | + if (res.code === 1) { | ||
| 71 | + detail.value = res.data | ||
| 72 | + } | ||
| 73 | + } catch (err) { | ||
| 74 | + console.error('获取消息详情失败:', err) | ||
| 75 | + } finally { | ||
| 76 | + loading.value = false | ||
| 77 | + } | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +useLoad((options) => { | ||
| 81 | + if (options.id) { | ||
| 82 | + fetchDetail(options.id) | ||
| 83 | + } | ||
| 84 | +}) | ||
| 85 | +</script> | ||
| 86 | + | ||
| 87 | +<style lang="less"> | ||
| 88 | +.rich-text-content { | ||
| 89 | + font-size: 28rpx; | ||
| 90 | + color: #333; | ||
| 91 | + line-height: 1.8; | ||
| 92 | + | ||
| 93 | + /* 确保富文本样式正确 */ | ||
| 94 | + p { | ||
| 95 | + margin-bottom: 20rpx; | ||
| 96 | + } | ||
| 97 | +} | ||
| 98 | +</style> |
src/pages/message/index.config.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-02-03 21:26:43 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-02-03 21:33:27 | ||
| 5 | + * @FilePath: /manulife-weapp/src/pages/message/index.config.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +export default { | ||
| 9 | + navigationBarTitleText: '我的消息', | ||
| 10 | + enablePullDownRefresh: true, | ||
| 11 | + backgroundColor: '#f9fafb', | ||
| 12 | + navigationStyle: 'custom' | ||
| 13 | +} |
src/pages/message/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="min-h-screen bg-[#F9FAFB] pb-safe"> | ||
| 3 | + <NavHeader title="我的消息" /> | ||
| 4 | + | ||
| 5 | + <!-- 列表区域 --> | ||
| 6 | + <view class="p-4"> | ||
| 7 | + <template v-if="messageList.length > 0"> | ||
| 8 | + <view | ||
| 9 | + v-for="item in messageList" | ||
| 10 | + :key="item.id" | ||
| 11 | + class="bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity" | ||
| 12 | + @tap="handleItemClick(item)" | ||
| 13 | + > | ||
| 14 | + <view class="flex justify-between items-start mb-2"> | ||
| 15 | + <view class="flex-1 mr-2"> | ||
| 16 | + <view class="text-base font-bold text-gray-900 line-clamp-1"> | ||
| 17 | + {{ item.title }} | ||
| 18 | + </view> | ||
| 19 | + </view> | ||
| 20 | + <text class="text-xs text-gray-400 shrink-0 mt-1"> | ||
| 21 | + {{ item.create_time }} | ||
| 22 | + </text> | ||
| 23 | + </view> | ||
| 24 | + | ||
| 25 | + <view class="text-sm text-gray-600 line-clamp-2 leading-relaxed"> | ||
| 26 | + {{ item.intro || item.content || '暂无简介' }} | ||
| 27 | + </view> | ||
| 28 | + </view> | ||
| 29 | + | ||
| 30 | + <!-- 加载更多/没有更多 --> | ||
| 31 | + <view class="py-4 text-center text-xs text-gray-400"> | ||
| 32 | + <text v-if="loading">加载中...</text> | ||
| 33 | + <text v-else-if="!hasMore">没有更多了</text> | ||
| 34 | + <text v-else>上拉加载更多</text> | ||
| 35 | + </view> | ||
| 36 | + </template> | ||
| 37 | + | ||
| 38 | + <!-- 空状态 --> | ||
| 39 | + <nut-empty | ||
| 40 | + v-else-if="!loading && messageList.length === 0" | ||
| 41 | + description="暂无消息" | ||
| 42 | + image="empty" | ||
| 43 | + /> | ||
| 44 | + </view> | ||
| 45 | + </view> | ||
| 46 | +</template> | ||
| 47 | + | ||
| 48 | +<script setup> | ||
| 49 | +import { ref } from 'vue' | ||
| 50 | +import { useLoad, usePullDownRefresh, useReachBottom, stopPullDownRefresh } from '@tarojs/taro' | ||
| 51 | +import { useGo } from '@/hooks/useGo' | ||
| 52 | +import NavHeader from '@/components/NavHeader.vue' | ||
| 53 | +import { myListAPI } from '@/api/news' | ||
| 54 | + | ||
| 55 | +const go = useGo() | ||
| 56 | + | ||
| 57 | +const messageList = ref([]) | ||
| 58 | +const page = ref(1) | ||
| 59 | +const limit = ref(10) | ||
| 60 | +const hasMore = ref(true) | ||
| 61 | +const loading = ref(false) | ||
| 62 | + | ||
| 63 | +/** | ||
| 64 | + * @description 加载消息列表 | ||
| 65 | + * @param {boolean} refresh 是否刷新 | ||
| 66 | + */ | ||
| 67 | +const fetchMessageList = async (refresh = false) => { | ||
| 68 | + if (loading.value) return | ||
| 69 | + | ||
| 70 | + if (refresh) { | ||
| 71 | + page.value = 1 | ||
| 72 | + hasMore.value = true | ||
| 73 | + } else if (!hasMore.value) { | ||
| 74 | + return | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + loading.value = true | ||
| 78 | + | ||
| 79 | + try { | ||
| 80 | + const res = await myListAPI({ | ||
| 81 | + page: page.value, | ||
| 82 | + limit: limit.value | ||
| 83 | + }) | ||
| 84 | + | ||
| 85 | + if (res.code === 1) { | ||
| 86 | + const list = res.data?.list || [] | ||
| 87 | + | ||
| 88 | + if (refresh) { | ||
| 89 | + messageList.value = list | ||
| 90 | + } else { | ||
| 91 | + messageList.value = [...messageList.value, ...list] | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + if (list.length < limit.value) { | ||
| 95 | + hasMore.value = false | ||
| 96 | + } else { | ||
| 97 | + page.value++ | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + } catch (err) { | ||
| 101 | + console.error('获取消息列表失败:', err) | ||
| 102 | + } finally { | ||
| 103 | + loading.value = false | ||
| 104 | + if (refresh) { | ||
| 105 | + stopPullDownRefresh() | ||
| 106 | + } | ||
| 107 | + } | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +/** | ||
| 111 | + * @description 跳转到详情页 | ||
| 112 | + * @param {Object} item 消息对象 | ||
| 113 | + */ | ||
| 114 | +const handleItemClick = (item) => { | ||
| 115 | + go('/pages/message-detail/index', { id: item.id }) | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +// 页面加载 | ||
| 119 | +useLoad(() => { | ||
| 120 | + fetchMessageList(true) | ||
| 121 | +}) | ||
| 122 | + | ||
| 123 | +// 下拉刷新 | ||
| 124 | +usePullDownRefresh(() => { | ||
| 125 | + fetchMessageList(true) | ||
| 126 | +}) | ||
| 127 | + | ||
| 128 | +// 上拉加载更多 | ||
| 129 | +useReachBottom(() => { | ||
| 130 | + fetchMessageList() | ||
| 131 | +}) | ||
| 132 | +</script> | ||
| 133 | + | ||
| 134 | +<style lang="less"> | ||
| 135 | +/* Scoped styles if needed */ | ||
| 136 | +</style> |
| ... | @@ -128,6 +128,7 @@ useDidShow(() => { | ... | @@ -128,6 +128,7 @@ useDidShow(() => { |
| 128 | 128 | ||
| 129 | const menuItems = [ | 129 | const menuItems = [ |
| 130 | { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, | 130 | { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, |
| 131 | + { title: '我的消息', icon: 'message', path: '/pages/message/index' }, | ||
| 131 | { title: '我的收藏', icon: 'star', path: '/pages/favorites/index' }, | 132 | { title: '我的收藏', icon: 'star', path: '/pages/favorites/index' }, |
| 132 | { title: '帮助中心', icon: 'service', path: '/pages/help-center/index' }, | 133 | { title: '帮助中心', icon: 'service', path: '/pages/help-center/index' }, |
| 133 | { title: '意见反馈', icon: 'edit', path: '/pages/feedback-list/index' } | 134 | { title: '意见反馈', icon: 'edit', path: '/pages/feedback-list/index' } | ... | ... |
-
Please register or login to post a comment