feat(消息页面): 重构消息页面UI并添加NutUI组件支持
- 使用NutUI组件重构消息页面布局和样式 - 添加Tab分类功能支持全部、未读、通知和留言 - 实现消息列表的滚动加载功能 - 更新全局组件声明添加NutUI的Col、Row、Tabs等组件 - 修改页面标题从"首页"改为"消息"
Showing
3 changed files
with
479 additions
and
275 deletions
| ... | @@ -9,6 +9,7 @@ declare module 'vue' { | ... | @@ -9,6 +9,7 @@ declare module 'vue' { |
| 9 | export interface GlobalComponents { | 9 | export interface GlobalComponents { |
| 10 | NavBar: typeof import('./src/components/navBar.vue')['default'] | 10 | NavBar: typeof import('./src/components/navBar.vue')['default'] |
| 11 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 11 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 12 | + NutCol: typeof import('@nutui/nutui-taro')['Col'] | ||
| 12 | NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] | 13 | NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] |
| 13 | NutForm: typeof import('@nutui/nutui-taro')['Form'] | 14 | NutForm: typeof import('@nutui/nutui-taro')['Form'] |
| 14 | NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] | 15 | NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] |
| ... | @@ -19,9 +20,12 @@ declare module 'vue' { | ... | @@ -19,9 +20,12 @@ declare module 'vue' { |
| 19 | NutNavbar: typeof import('@nutui/nutui-taro')['Navbar'] | 20 | NutNavbar: typeof import('@nutui/nutui-taro')['Navbar'] |
| 20 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] | 21 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] |
| 21 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] | 22 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] |
| 23 | + NutRow: typeof import('@nutui/nutui-taro')['Row'] | ||
| 22 | NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar'] | 24 | NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar'] |
| 23 | NutSwiper: typeof import('@nutui/nutui-taro')['Swiper'] | 25 | NutSwiper: typeof import('@nutui/nutui-taro')['Swiper'] |
| 24 | NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem'] | 26 | NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem'] |
| 27 | + NutTabPane: typeof import('@nutui/nutui-taro')['TabPane'] | ||
| 28 | + NutTabs: typeof import('@nutui/nutui-taro')['Tabs'] | ||
| 25 | NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] | 29 | NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] |
| 26 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] | 30 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] |
| 27 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | 31 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | ... | ... |
| 1 | +/* | ||
| 2 | + * @Date: 2025-07-01 17:55:00 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-07-02 11:14:06 | ||
| 5 | + * @FilePath: /jgdl/src/pages/messages/index.config.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 1 | export default { | 8 | export default { |
| 2 | - navigationBarTitleText: '首页' | 9 | + navigationBarTitleText: '消息' |
| 3 | } | 10 | } | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <view class="messages-page"> | 2 | <view class="messages-page"> |
| 3 | - <!-- 顶部搜索栏 --> | 3 | + <!-- Header --> |
| 4 | - <view class="search-container"> | 4 | + <view class="bg-orange-400 p-4 pt-8"> |
| 5 | - <view class="search-box"> | 5 | + <view class="text-xl font-bold text-white mb-3">消息</view> |
| 6 | - <Search size="18" color="#9ca3af" /> | 6 | + <!-- Search Bar --> |
| 7 | - <input | 7 | + <nut-searchbar v-model="searchValue" placeholder="搜索消息" shape="round" background="transparent" |
| 8 | - v-model="searchValue" | 8 | + input-background="#ffffff"> |
| 9 | - placeholder="搜索聊天记录..." | 9 | + <template #leftin> |
| 10 | - class="search-input" | 10 | + <Search2 /> |
| 11 | - /> | 11 | + </template> |
| 12 | - </view> | 12 | + </nut-searchbar> |
| 13 | </view> | 13 | </view> |
| 14 | 14 | ||
| 15 | - <!-- 消息列表 --> | 15 | + <!-- Tab Navigation --> |
| 16 | - <view class="messages-list"> | 16 | + <nut-tabs v-model="activeTab" @click="onTabClick"> |
| 17 | - <view | 17 | + <nut-tab-pane title="全部" pane-key="all"> |
| 18 | - v-for="message in filteredMessages" | 18 | + <scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50"> |
| 19 | - :key="message.id" | 19 | + <view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item" |
| 20 | - class="message-item" | 20 | + @click="onConversationClick(conversation)"> |
| 21 | - @click="onMessageClick(message)" | 21 | + <nut-row> |
| 22 | - > | 22 | + <nut-col :span="4" class="avatar-container"> |
| 23 | - <view class="avatar-container"> | 23 | + <view class="relative"> |
| 24 | - <image :src="message.avatar" class="avatar" mode="aspectFill" /> | 24 | + <image v-if="conversation.avatar" :src="conversation.avatar" |
| 25 | - <view v-if="message.unreadCount > 0" class="unread-badge"> | 25 | + class="w-12 h-12 rounded-full object-cover" mode="aspectFill" /> |
| 26 | - <text class="unread-count">{{ message.unreadCount > 99 ? '99+' : message.unreadCount }}</text> | 26 | + <view v-else |
| 27 | + class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center"> | ||
| 28 | + <component :is="conversation.icon" /> | ||
| 29 | + </view> | ||
| 30 | + </view> | ||
| 31 | + </nut-col> | ||
| 32 | + <nut-col :span="20" class="content-container"> | ||
| 33 | + <view class="flex justify-between items-center mb-1"> | ||
| 34 | + <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text> | ||
| 35 | + <view class="flex items-center flex-shrink-0"> | ||
| 36 | + <text class="text-xs text-gray-500">{{ conversation.time }}</text> | ||
| 37 | + <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full"> | ||
| 38 | + </view> | ||
| 39 | + </view> | ||
| 40 | + </view> | ||
| 41 | + <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text> | ||
| 42 | + </nut-col> | ||
| 43 | + </nut-row> | ||
| 27 | </view> | 44 | </view> |
| 28 | - </view> | 45 | + |
| 29 | - | 46 | + <!-- Loading indicator --> |
| 30 | - <view class="message-content"> | 47 | + <view v-if="loading" class="loading-container"> |
| 31 | - <view class="message-header"> | 48 | + <text class="loading-text">加载中...</text> |
| 32 | - <text class="sender-name">{{ message.senderName }}</text> | ||
| 33 | - <text class="message-time">{{ formatTime(message.timestamp) }}</text> | ||
| 34 | </view> | 49 | </view> |
| 35 | - | 50 | + </scroll-view> |
| 36 | - <view class="message-preview"> | 51 | + </nut-tab-pane> |
| 37 | - <text class="preview-text" :class="{ 'unread': message.unreadCount > 0 }"> | 52 | + |
| 38 | - {{ message.lastMessage }} | 53 | + <nut-tab-pane title="未读" pane-key="unread"> |
| 39 | - </text> | 54 | + <scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50"> |
| 40 | - <view v-if="message.type === 'image'" class="message-type-icon"> | 55 | + <view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item" |
| 41 | - <Image size="16" color="#9ca3af" /> | 56 | + @click="onConversationClick(conversation)"> |
| 42 | - </view> | 57 | + <nut-row> |
| 58 | + <nut-col :span="4" class="avatar-container"> | ||
| 59 | + <view class="relative"> | ||
| 60 | + <image v-if="conversation.avatar" :src="conversation.avatar" | ||
| 61 | + class="w-12 h-12 rounded-full object-cover" mode="aspectFill" /> | ||
| 62 | + <view v-else | ||
| 63 | + class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center"> | ||
| 64 | + <component :is="conversation.icon" /> | ||
| 65 | + </view> | ||
| 66 | + </view> | ||
| 67 | + </nut-col> | ||
| 68 | + <nut-col :span="20" class="content-container"> | ||
| 69 | + <view class="flex justify-between items-center mb-1"> | ||
| 70 | + <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text> | ||
| 71 | + <view class="flex items-center flex-shrink-0"> | ||
| 72 | + <text class="text-xs text-gray-500">{{ conversation.time }}</text> | ||
| 73 | + <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full"> | ||
| 74 | + </view> | ||
| 75 | + </view> | ||
| 76 | + </view> | ||
| 77 | + <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text> | ||
| 78 | + </nut-col> | ||
| 79 | + </nut-row> | ||
| 43 | </view> | 80 | </view> |
| 44 | - </view> | ||
| 45 | - </view> | ||
| 46 | - </view> | ||
| 47 | 81 | ||
| 48 | - <!-- 空状态 --> | 82 | + <!-- Loading indicator --> |
| 49 | - <view v-if="filteredMessages.length === 0" class="empty-state"> | 83 | + <view v-if="loading" class="loading-container"> |
| 50 | - <view class="empty-icon"> | 84 | + <text class="loading-text">加载中...</text> |
| 51 | - <Message size="48" color="#d1d5db" /> | 85 | + </view> |
| 52 | - </view> | 86 | + </scroll-view> |
| 53 | - <text class="empty-title">暂无消息</text> | 87 | + </nut-tab-pane> |
| 54 | - <text class="empty-subtitle">开始与买家或卖家聊天吧</text> | 88 | + |
| 55 | - </view> | 89 | + <nut-tab-pane title="通知" pane-key="notification"> |
| 90 | + <scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50"> | ||
| 91 | + <view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item" | ||
| 92 | + @click="onConversationClick(conversation)"> | ||
| 93 | + <nut-row> | ||
| 94 | + <nut-col :span="4" class="avatar-container"> | ||
| 95 | + <view class="relative"> | ||
| 96 | + <image v-if="conversation.avatar" :src="conversation.avatar" | ||
| 97 | + class="w-12 h-12 rounded-full object-cover" mode="aspectFill" /> | ||
| 98 | + <view v-else | ||
| 99 | + class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center"> | ||
| 100 | + <component :is="conversation.icon" /> | ||
| 101 | + </view> | ||
| 102 | + </view> | ||
| 103 | + </nut-col> | ||
| 104 | + <nut-col :span="20" class="content-container"> | ||
| 105 | + <view class="flex justify-between items-center mb-1"> | ||
| 106 | + <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text> | ||
| 107 | + <view class="flex items-center flex-shrink-0"> | ||
| 108 | + <text class="text-xs text-gray-500">{{ conversation.time }}</text> | ||
| 109 | + <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full"> | ||
| 110 | + </view> | ||
| 111 | + </view> | ||
| 112 | + </view> | ||
| 113 | + <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text> | ||
| 114 | + </nut-col> | ||
| 115 | + </nut-row> | ||
| 116 | + </view> | ||
| 117 | + | ||
| 118 | + <!-- Loading indicator --> | ||
| 119 | + <view v-if="loading" class="loading-container"> | ||
| 120 | + <text class="loading-text">加载中...</text> | ||
| 121 | + </view> | ||
| 122 | + </scroll-view> | ||
| 123 | + </nut-tab-pane> | ||
| 124 | + | ||
| 125 | + <nut-tab-pane title="留言" pane-key="message"> | ||
| 126 | + <scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50"> | ||
| 127 | + <view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item" | ||
| 128 | + @click="onConversationClick(conversation)"> | ||
| 129 | + <nut-row> | ||
| 130 | + <nut-col :span="4" class="avatar-container"> | ||
| 131 | + <view class="relative"> | ||
| 132 | + <image v-if="conversation.avatar" :src="conversation.avatar" | ||
| 133 | + class="w-12 h-12 rounded-full object-cover" mode="aspectFill" /> | ||
| 134 | + <view v-else | ||
| 135 | + class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center"> | ||
| 136 | + <component :is="conversation.icon" /> | ||
| 137 | + </view> | ||
| 138 | + </view> | ||
| 139 | + </nut-col> | ||
| 140 | + <nut-col :span="20" class="content-container"> | ||
| 141 | + <view class="flex justify-between items-center mb-1"> | ||
| 142 | + <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text> | ||
| 143 | + <view class="flex items-center flex-shrink-0"> | ||
| 144 | + <text class="text-xs text-gray-500">{{ conversation.time }}</text> | ||
| 145 | + <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full"> | ||
| 146 | + </view> | ||
| 147 | + </view> | ||
| 148 | + </view> | ||
| 149 | + <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text> | ||
| 150 | + </nut-col> | ||
| 151 | + </nut-row> | ||
| 152 | + </view> | ||
| 153 | + | ||
| 154 | + <!-- Loading indicator --> | ||
| 155 | + <view v-if="loading" class="loading-container"> | ||
| 156 | + <text class="loading-text">加载中...</text> | ||
| 157 | + </view> | ||
| 158 | + </scroll-view> | ||
| 159 | + </nut-tab-pane> | ||
| 160 | + </nut-tabs> | ||
| 56 | 161 | ||
| 57 | - <!-- 浮动按钮 --> | ||
| 58 | - <view class="floating-btn" @click="onNewMessage"> | ||
| 59 | - <Plus size="24" color="#ffffff" /> | ||
| 60 | - </view> | ||
| 61 | - | ||
| 62 | <!-- 自定义TabBar --> | 162 | <!-- 自定义TabBar --> |
| 63 | <TabBar /> | 163 | <TabBar /> |
| 64 | </view> | 164 | </view> |
| 65 | </template> | 165 | </template> |
| 66 | 166 | ||
| 67 | <script setup> | 167 | <script setup> |
| 68 | -import { ref, computed } from 'vue' | 168 | +import { ref, computed, onMounted, markRaw } from 'vue' |
| 69 | -import { Search, Message, Plus, Image } from '@nutui/icons-vue-taro' | ||
| 70 | import Taro from '@tarojs/taro' | 169 | import Taro from '@tarojs/taro' |
| 170 | +import { Search2, Notice, Message } from '@nutui/icons-vue-taro' | ||
| 71 | import TabBar from '@/components/TabBar.vue' | 171 | import TabBar from '@/components/TabBar.vue' |
| 72 | 172 | ||
| 73 | -// 响应式数据 | 173 | +// 搜索值 |
| 74 | const searchValue = ref('') | 174 | const searchValue = ref('') |
| 175 | +// 当前激活的Tab | ||
| 176 | +const activeTab = ref('all') | ||
| 177 | +// 加载状态 | ||
| 178 | +const loading = ref(false) | ||
| 179 | +// 页码 | ||
| 180 | +const page = ref(1) | ||
| 181 | +// 每页数量 | ||
| 182 | +const pageSize = 10 | ||
| 183 | +// 是否还有更多数据 | ||
| 184 | +const hasMore = ref(true) | ||
| 185 | + | ||
| 186 | +// 模拟对话数据 | ||
| 187 | +const conversations = ref([]) | ||
| 188 | + | ||
| 189 | +// 初始化数据 | ||
| 190 | +const initData = () => { | ||
| 191 | + const mockData = [ | ||
| 192 | + { | ||
| 193 | + id: 1, | ||
| 194 | + name: '张三', | ||
| 195 | + avatar: 'https://randomuser.me/api/portraits/men/32.jpg', | ||
| 196 | + lastMessage: '你好,这个商品还在吗?', | ||
| 197 | + time: '5分钟前', | ||
| 198 | + unread: true, | ||
| 199 | + type: 'chat' | ||
| 200 | + }, | ||
| 201 | + { | ||
| 202 | + id: 2, | ||
| 203 | + name: '李四', | ||
| 204 | + avatar: 'https://randomuser.me/api/portraits/men/32.jpg', | ||
| 205 | + lastMessage: '价格可以商量吗?', | ||
| 206 | + time: '30分钟前', | ||
| 207 | + unread: false, | ||
| 208 | + type: 'chat' | ||
| 209 | + }, | ||
| 210 | + { | ||
| 211 | + id: 3, | ||
| 212 | + name: '系统通知', | ||
| 213 | + avatar: '', | ||
| 214 | + icon: markRaw(Notice), | ||
| 215 | + lastMessage: '您的商品已通过审核', | ||
| 216 | + time: '1小时前', | ||
| 217 | + unread: true, | ||
| 218 | + type: 'notification' | ||
| 219 | + }, | ||
| 220 | + { | ||
| 221 | + id: 4, | ||
| 222 | + name: '王五', | ||
| 223 | + avatar: 'https://randomuser.me/api/portraits/men/32.jpg', | ||
| 224 | + lastMessage: '[图片]', | ||
| 225 | + time: '2小时前', | ||
| 226 | + unread: true, | ||
| 227 | + type: 'chat' | ||
| 228 | + }, | ||
| 229 | + { | ||
| 230 | + id: 5, | ||
| 231 | + name: '客服留言', | ||
| 232 | + avatar: '', | ||
| 233 | + icon: markRaw(Message), | ||
| 234 | + lastMessage: '感谢您的反馈,我们会尽快处理', | ||
| 235 | + time: '昨天', | ||
| 236 | + unread: false, | ||
| 237 | + type: 'message' | ||
| 238 | + } | ||
| 239 | + ] | ||
| 240 | + | ||
| 241 | + conversations.value = mockData | ||
| 242 | +} | ||
| 75 | 243 | ||
| 76 | -// 消息数据 | 244 | +// 过滤后的对话列表 |
| 77 | -const messages = ref([ | 245 | +const filteredConversations = computed(() => { |
| 78 | - { | 246 | + let filtered = conversations.value |
| 79 | - id: 1, | 247 | + |
| 80 | - senderName: '张同学', | 248 | + // 根据Tab过滤 |
| 81 | - avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop&crop=face', | 249 | + if (activeTab.value === 'unread') { |
| 82 | - lastMessage: '这辆车还在吗?可以看看实物吗?', | 250 | + filtered = filtered.filter(conv => conv.unread) |
| 83 | - timestamp: Date.now() - 300000, // 5分钟前 | 251 | + } else if (activeTab.value === 'notification') { |
| 84 | - unreadCount: 2, | 252 | + filtered = filtered.filter(conv => conv.type === 'notification') |
| 85 | - type: 'text' | 253 | + } else if (activeTab.value === 'message') { |
| 86 | - }, | 254 | + filtered = filtered.filter(conv => conv.type === 'message') |
| 87 | - { | ||
| 88 | - id: 2, | ||
| 89 | - senderName: '李小明', | ||
| 90 | - avatar: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=100&h=100&fit=crop&crop=face', | ||
| 91 | - lastMessage: '价格还能再便宜点吗?', | ||
| 92 | - timestamp: Date.now() - 1800000, // 30分钟前 | ||
| 93 | - unreadCount: 0, | ||
| 94 | - type: 'text' | ||
| 95 | - }, | ||
| 96 | - { | ||
| 97 | - id: 3, | ||
| 98 | - senderName: '王美丽', | ||
| 99 | - avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face', | ||
| 100 | - lastMessage: '[图片]', | ||
| 101 | - timestamp: Date.now() - 3600000, // 1小时前 | ||
| 102 | - unreadCount: 1, | ||
| 103 | - type: 'image' | ||
| 104 | - }, | ||
| 105 | - { | ||
| 106 | - id: 4, | ||
| 107 | - senderName: '陈大华', | ||
| 108 | - avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face', | ||
| 109 | - lastMessage: '好的,谢谢!', | ||
| 110 | - timestamp: Date.now() - 7200000, // 2小时前 | ||
| 111 | - unreadCount: 0, | ||
| 112 | - type: 'text' | ||
| 113 | - }, | ||
| 114 | - { | ||
| 115 | - id: 5, | ||
| 116 | - senderName: '刘小红', | ||
| 117 | - avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face', | ||
| 118 | - lastMessage: '车子的电池还好用吗?大概能跑多远?', | ||
| 119 | - timestamp: Date.now() - 86400000, // 1天前 | ||
| 120 | - unreadCount: 0, | ||
| 121 | - type: 'text' | ||
| 122 | - }, | ||
| 123 | - { | ||
| 124 | - id: 6, | ||
| 125 | - senderName: '赵强', | ||
| 126 | - avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face', | ||
| 127 | - lastMessage: '明天下午有时间看车吗?', | ||
| 128 | - timestamp: Date.now() - 172800000, // 2天前 | ||
| 129 | - unreadCount: 0, | ||
| 130 | - type: 'text' | ||
| 131 | } | 255 | } |
| 132 | -]) | ||
| 133 | 256 | ||
| 134 | -// 过滤后的消息列表 | 257 | + // 根据搜索关键词过滤 |
| 135 | -const filteredMessages = computed(() => { | 258 | + if (searchValue.value) { |
| 136 | - if (!searchValue.value.trim()) { | 259 | + filtered = filtered.filter(conv => |
| 137 | - return messages.value | 260 | + conv.name.includes(searchValue.value) || |
| 261 | + conv.lastMessage.includes(searchValue.value) | ||
| 262 | + ) | ||
| 138 | } | 263 | } |
| 139 | - | 264 | + |
| 140 | - return messages.value.filter(message => | 265 | + return filtered |
| 141 | - message.senderName.includes(searchValue.value) || | ||
| 142 | - message.lastMessage.includes(searchValue.value) | ||
| 143 | - ) | ||
| 144 | }) | 266 | }) |
| 145 | 267 | ||
| 146 | -/** | 268 | +// Tab点击事件 |
| 147 | - * 格式化时间 | 269 | +const onTabClick = (tab) => { |
| 148 | - * @param {number} timestamp - 时间戳 | 270 | + activeTab.value = tab.paneKey |
| 149 | - * @returns {string} 格式化后的时间 | 271 | + // 重置分页 |
| 150 | - */ | 272 | + page.value = 1 |
| 151 | -const formatTime = (timestamp) => { | 273 | + hasMore.value = true |
| 152 | - const now = Date.now() | ||
| 153 | - const diff = now - timestamp | ||
| 154 | - | ||
| 155 | - if (diff < 60000) { // 1分钟内 | ||
| 156 | - return '刚刚' | ||
| 157 | - } else if (diff < 3600000) { // 1小时内 | ||
| 158 | - return `${Math.floor(diff / 60000)}分钟前` | ||
| 159 | - } else if (diff < 86400000) { // 1天内 | ||
| 160 | - return `${Math.floor(diff / 3600000)}小时前` | ||
| 161 | - } else if (diff < 604800000) { // 1周内 | ||
| 162 | - return `${Math.floor(diff / 86400000)}天前` | ||
| 163 | - } else { | ||
| 164 | - const date = new Date(timestamp) | ||
| 165 | - return `${date.getMonth() + 1}/${date.getDate()}` | ||
| 166 | - } | ||
| 167 | } | 274 | } |
| 168 | 275 | ||
| 169 | -/** | 276 | +// 加载更多数据 |
| 170 | - * 消息点击事件 | 277 | +const loadMore = async () => { |
| 171 | - * @param {object} message - 消息对象 | 278 | + if (loading.value || !hasMore.value) return |
| 172 | - */ | 279 | + |
| 173 | -const onMessageClick = (message) => { | 280 | + loading.value = true |
| 174 | - // 清除未读数量 | 281 | + |
| 175 | - message.unreadCount = 0 | 282 | + // 模拟API请求 |
| 176 | - | 283 | + setTimeout(() => { |
| 177 | - // 跳转到聊天详情页面 | 284 | + const newData = generateMockData(page.value + 1) |
| 178 | - Taro.navigateTo({ | 285 | + if (newData.length > 0) { |
| 179 | - url: `/pages/chat/index?userId=${message.id}&userName=${message.senderName}` | 286 | + conversations.value.push(...newData) |
| 180 | - }) | 287 | + page.value++ |
| 288 | + } else { | ||
| 289 | + hasMore.value = false | ||
| 290 | + } | ||
| 291 | + loading.value = false | ||
| 292 | + }, 1000) | ||
| 181 | } | 293 | } |
| 182 | 294 | ||
| 183 | -/** | 295 | +// 生成模拟数据 |
| 184 | - * 新建消息 | 296 | +const generateMockData = (pageNum) => { |
| 185 | - */ | 297 | + if (pageNum > 3) return [] // 模拟只有3页数据 |
| 186 | -const onNewMessage = async () => { | 298 | + |
| 187 | - try { | 299 | + const mockData = [] |
| 188 | - await Taro.showToast({ | 300 | + const startId = (pageNum - 1) * pageSize + conversations.value.length + 1 |
| 189 | - title: '新建消息', | 301 | + |
| 190 | - icon: 'none' | 302 | + for (let i = 0; i < pageSize; i++) { |
| 303 | + mockData.push({ | ||
| 304 | + id: startId + i, | ||
| 305 | + name: `用户${startId + i}`, | ||
| 306 | + avatar: 'https://randomuser.me/api/portraits/men/32.jpg', | ||
| 307 | + lastMessage: `这是第${startId + i}条消息`, | ||
| 308 | + time: `${Math.floor(Math.random() * 24)}小时前`, | ||
| 309 | + unread: Math.random() > 0.5, | ||
| 310 | + type: 'chat' | ||
| 191 | }) | 311 | }) |
| 192 | - } catch (error) { | ||
| 193 | - console.error('新建消息失败:', error) | ||
| 194 | } | 312 | } |
| 313 | + | ||
| 314 | + return mockData | ||
| 315 | +} | ||
| 316 | + | ||
| 317 | +// 点击对话 | ||
| 318 | +const onConversationClick = (conversation) => { | ||
| 319 | + // 跳转到聊天页面 | ||
| 320 | + Taro.navigateTo({ | ||
| 321 | + url: `/pages/chat/index?id=${conversation.id}&name=${conversation.name}` | ||
| 322 | + }) | ||
| 195 | } | 323 | } |
| 324 | + | ||
| 325 | +// 页面加载时初始化数据 | ||
| 326 | +onMounted(() => { | ||
| 327 | + initData() | ||
| 328 | +}) | ||
| 196 | </script> | 329 | </script> |
| 197 | 330 | ||
| 198 | -<style lang="less"> | 331 | +<style scoped> |
| 199 | .messages-page { | 332 | .messages-page { |
| 200 | min-height: 100vh; | 333 | min-height: 100vh; |
| 201 | - background-color: #f9fafb; | 334 | + background-color: #f5f5f5; |
| 202 | - padding-bottom: 100px; | 335 | + padding-bottom: 100rpx; |
| 336 | + /* 为TabBar留出空间 */ | ||
| 203 | } | 337 | } |
| 204 | 338 | ||
| 205 | -.search-container { | 339 | +/* 对话列表样式 */ |
| 206 | - padding: 16px; | 340 | +.conversation-list { |
| 207 | - background-color: #ffffff; | 341 | + height: calc(100vh - 300rpx); |
| 208 | - border-bottom: 1px solid #f3f4f6; | 342 | + background: #ffffff; |
| 209 | } | 343 | } |
| 210 | 344 | ||
| 211 | -.search-box { | 345 | +.conversation-item { |
| 346 | + padding: 24rpx 20rpx; | ||
| 347 | + border-bottom: 1rpx solid #f0f0f0; | ||
| 348 | + transition: background-color 0.2s; | ||
| 349 | +} | ||
| 350 | + | ||
| 351 | +.avatar-container { | ||
| 212 | display: flex; | 352 | display: flex; |
| 213 | - align-items: center; | 353 | + align-items: flex-start; |
| 214 | - background-color: #f9fafb; | 354 | + width: 88rpx; |
| 215 | - border-radius: 24px; | 355 | + margin-right: 24rpx; |
| 216 | - padding: 12px 16px; | ||
| 217 | - gap: 8px; | ||
| 218 | } | 356 | } |
| 219 | 357 | ||
| 220 | -.search-input { | 358 | +.content-container { |
| 221 | - flex: 1; | 359 | + padding-left: 8px; |
| 222 | - border: none; | 360 | +} |
| 223 | - outline: none; | 361 | + |
| 224 | - background: transparent; | 362 | +.conversation-item:active { |
| 225 | - font-size: 14px; | 363 | + background-color: #f8f9fa; |
| 226 | - color: #374151; | ||
| 227 | } | 364 | } |
| 228 | 365 | ||
| 229 | -.messages-list { | 366 | +.conversation-item:last-child { |
| 230 | - background-color: #ffffff; | 367 | + border-bottom: none; |
| 231 | } | 368 | } |
| 232 | 369 | ||
| 233 | -.message-item { | 370 | +/* 加载指示器 */ |
| 371 | +.loading-container { | ||
| 234 | display: flex; | 372 | display: flex; |
| 235 | align-items: center; | 373 | align-items: center; |
| 236 | - padding: 16px; | 374 | + justify-content: center; |
| 237 | - border-bottom: 1px solid #f3f4f6; | 375 | + padding: 40rpx; |
| 238 | - transition: background-color 0.2s; | 376 | + gap: 16rpx; |
| 239 | } | 377 | } |
| 240 | 378 | ||
| 241 | -.message-item:active { | 379 | +.loading-text { |
| 242 | - background-color: #f9fafb; | 380 | + font-size: 28rpx; |
| 381 | + color: #9ca3af; | ||
| 243 | } | 382 | } |
| 244 | 383 | ||
| 245 | -.message-item:last-child { | 384 | +/* Tailwind CSS 类的补充样式 */ |
| 246 | - border-bottom: none; | 385 | +.w-12 { |
| 386 | + width: 88rpx; | ||
| 247 | } | 387 | } |
| 248 | 388 | ||
| 249 | -.avatar-container { | 389 | +.h-12 { |
| 250 | - position: relative; | 390 | + height: 88rpx; |
| 251 | - margin-right: 12px; | ||
| 252 | } | 391 | } |
| 253 | 392 | ||
| 254 | -.avatar { | 393 | +.w-2 { |
| 255 | - width: 48px; | 394 | + width: 8rpx; |
| 256 | - height: 48px; | 395 | +} |
| 396 | + | ||
| 397 | +.h-2 { | ||
| 398 | + height: 8rpx; | ||
| 399 | +} | ||
| 400 | + | ||
| 401 | +.rounded-full { | ||
| 257 | border-radius: 50%; | 402 | border-radius: 50%; |
| 403 | +} | ||
| 404 | + | ||
| 405 | +.object-cover { | ||
| 258 | object-fit: cover; | 406 | object-fit: cover; |
| 259 | } | 407 | } |
| 260 | 408 | ||
| 261 | -.unread-badge { | 409 | +.bg-gray-100 { |
| 262 | - position: absolute; | 410 | + background-color: #f3f4f6; |
| 263 | - top: -4px; | 411 | +} |
| 264 | - right: -4px; | 412 | + |
| 265 | - min-width: 20px; | 413 | +.bg-red-500 { |
| 266 | - height: 20px; | ||
| 267 | background-color: #ef4444; | 414 | background-color: #ef4444; |
| 268 | - border-radius: 10px; | ||
| 269 | - display: flex; | ||
| 270 | - align-items: center; | ||
| 271 | - justify-content: center; | ||
| 272 | - border: 2px solid #ffffff; | ||
| 273 | } | 415 | } |
| 274 | 416 | ||
| 275 | -.unread-count { | 417 | +.flex { |
| 276 | - font-size: 12px; | 418 | + display: flex; |
| 277 | - color: #ffffff; | ||
| 278 | - font-weight: 500; | ||
| 279 | - line-height: 1; | ||
| 280 | } | 419 | } |
| 281 | 420 | ||
| 282 | -.message-content { | 421 | +.flex-1 { |
| 283 | flex: 1; | 422 | flex: 1; |
| 423 | +} | ||
| 424 | + | ||
| 425 | +.flex-shrink-0 { | ||
| 426 | + flex-shrink: 0; | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +.min-w-0 { | ||
| 284 | min-width: 0; | 430 | min-width: 0; |
| 285 | } | 431 | } |
| 286 | 432 | ||
| 287 | -.message-header { | 433 | +.items-center { |
| 288 | - display: flex; | ||
| 289 | - justify-content: space-between; | ||
| 290 | align-items: center; | 434 | align-items: center; |
| 291 | - margin-bottom: 4px; | ||
| 292 | } | 435 | } |
| 293 | 436 | ||
| 294 | -.sender-name { | 437 | +.justify-center { |
| 295 | - font-size: 16px; | 438 | + justify-content: center; |
| 439 | +} | ||
| 440 | + | ||
| 441 | +.justify-between { | ||
| 442 | + justify-content: space-between; | ||
| 443 | +} | ||
| 444 | + | ||
| 445 | +.ml-1 { | ||
| 446 | + margin-left: 4rpx; | ||
| 447 | +} | ||
| 448 | + | ||
| 449 | +.ml-3 { | ||
| 450 | + margin-left: 24rpx; | ||
| 451 | +} | ||
| 452 | + | ||
| 453 | +.mt-1 { | ||
| 454 | + margin-top: 4rpx; | ||
| 455 | +} | ||
| 456 | + | ||
| 457 | +.font-medium { | ||
| 296 | font-weight: 500; | 458 | font-weight: 500; |
| 297 | - color: #111827; | ||
| 298 | } | 459 | } |
| 299 | 460 | ||
| 300 | -.message-time { | 461 | +.text-xs { |
| 301 | - font-size: 12px; | 462 | + font-size: 24rpx; |
| 302 | - color: #9ca3af; | ||
| 303 | } | 463 | } |
| 304 | 464 | ||
| 305 | -.message-preview { | 465 | +.text-sm { |
| 306 | - display: flex; | 466 | + font-size: 28rpx; |
| 307 | - align-items: center; | ||
| 308 | - gap: 4px; | ||
| 309 | } | 467 | } |
| 310 | 468 | ||
| 311 | -.preview-text { | 469 | +.text-xl { |
| 312 | - flex: 1; | 470 | + font-size: 40rpx; |
| 313 | - font-size: 14px; | 471 | +} |
| 472 | + | ||
| 473 | +.font-bold { | ||
| 474 | + font-weight: bold; | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +.text-white { | ||
| 478 | + color: #ffffff; | ||
| 479 | +} | ||
| 480 | + | ||
| 481 | +.text-gray-500 { | ||
| 314 | color: #6b7280; | 482 | color: #6b7280; |
| 483 | +} | ||
| 484 | + | ||
| 485 | +.text-gray-600 { | ||
| 486 | + color: #4b5563; | ||
| 487 | +} | ||
| 488 | + | ||
| 489 | +.bg-orange-400 { | ||
| 490 | + background-color: #fb923c; | ||
| 491 | +} | ||
| 492 | + | ||
| 493 | +.p-4 { | ||
| 494 | + padding: 32rpx; | ||
| 495 | +} | ||
| 496 | + | ||
| 497 | +.pt-8 { | ||
| 498 | + padding-top: 64rpx; | ||
| 499 | +} | ||
| 500 | + | ||
| 501 | +.mb-1 { | ||
| 502 | + margin-bottom: 4rpx; | ||
| 503 | +} | ||
| 504 | + | ||
| 505 | +.mb-3 { | ||
| 506 | + margin-bottom: 24rpx; | ||
| 507 | +} | ||
| 508 | + | ||
| 509 | +.mr-2 { | ||
| 510 | + margin-right: 8rpx; | ||
| 511 | +} | ||
| 512 | + | ||
| 513 | +.truncate { | ||
| 315 | overflow: hidden; | 514 | overflow: hidden; |
| 316 | text-overflow: ellipsis; | 515 | text-overflow: ellipsis; |
| 317 | white-space: nowrap; | 516 | white-space: nowrap; |
| 318 | } | 517 | } |
| 319 | 518 | ||
| 320 | -.preview-text.unread { | 519 | +.block { |
| 321 | - color: #374151; | 520 | + display: block; |
| 322 | - font-weight: 500; | ||
| 323 | } | 521 | } |
| 324 | 522 | ||
| 325 | -.message-type-icon { | 523 | +.relative { |
| 326 | - flex-shrink: 0; | 524 | + position: relative; |
| 327 | } | 525 | } |
| 328 | 526 | ||
| 329 | -.empty-state { | 527 | +/* NutUI 组件样式覆盖 */ |
| 330 | - display: flex; | 528 | +:deep(.nut-searchbar) { |
| 331 | - flex-direction: column; | 529 | + background: transparent !important; |
| 332 | - align-items: center; | ||
| 333 | - justify-content: center; | ||
| 334 | - padding: 80px 20px; | ||
| 335 | - text-align: center; | ||
| 336 | } | 530 | } |
| 337 | 531 | ||
| 338 | -.empty-icon { | 532 | +:deep(.nut-searchbar .nut-searchbar__search-input) { |
| 339 | - margin-bottom: 16px; | 533 | + background: #ffffff !important; |
| 534 | + border-radius: 40rpx !important; | ||
| 340 | } | 535 | } |
| 341 | 536 | ||
| 342 | -.empty-title { | 537 | +:deep(.nut-tabs) { |
| 343 | - font-size: 18px; | 538 | + background: #ffffff; |
| 344 | - font-weight: 500; | ||
| 345 | - color: #374151; | ||
| 346 | - margin-bottom: 8px; | ||
| 347 | - display: block; | ||
| 348 | } | 539 | } |
| 349 | 540 | ||
| 350 | -.empty-subtitle { | 541 | +:deep(.nut-tabs__titles) { |
| 351 | - font-size: 14px; | 542 | + background: #ffffff; |
| 352 | - color: #9ca3af; | 543 | + border-bottom: 1rpx solid #f0f0f0; |
| 353 | - display: block; | ||
| 354 | } | 544 | } |
| 355 | 545 | ||
| 356 | -.floating-btn { | 546 | +:deep(.nut-tab-pane) { |
| 357 | - position: fixed; | 547 | + padding: 0; |
| 358 | - bottom: 100px; | ||
| 359 | - right: 20px; | ||
| 360 | - width: 56px; | ||
| 361 | - height: 56px; | ||
| 362 | - background-color: #f97316; | ||
| 363 | - border-radius: 50%; | ||
| 364 | - display: flex; | ||
| 365 | - align-items: center; | ||
| 366 | - justify-content: center; | ||
| 367 | - box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4); | ||
| 368 | - transition: all 0.2s; | ||
| 369 | } | 548 | } |
| 370 | 549 | ||
| 371 | -.floating-btn:active { | 550 | +/* 响应式设计 */ |
| 372 | - transform: scale(0.95); | 551 | +@media (max-width: 750rpx) { |
| 373 | - box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4); | 552 | + .conversation-item { |
| 553 | + padding: 20rpx 16rpx; | ||
| 554 | + } | ||
| 555 | + | ||
| 556 | + .w-12 { | ||
| 557 | + width: 80rpx; | ||
| 558 | + } | ||
| 559 | + | ||
| 560 | + .h-12 { | ||
| 561 | + height: 80rpx; | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + .text-sm { | ||
| 565 | + font-size: 26rpx; | ||
| 566 | + } | ||
| 374 | } | 567 | } |
| 375 | -</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 568 | +</style> | ... | ... |
-
Please register or login to post a comment