hookehuyr

feat(消息页面): 优化消息列表布局并添加动态高度计算

- 添加@tarojs/extend依赖用于DOM操作
- 为页面元素添加ID便于定位
- 重构消息列表布局,调整列宽和间距
- 实现动态计算滚动区域高度
- 增加更多模拟数据确保滚动效果
- 优化样式表结构和部分样式覆盖
......@@ -40,6 +40,7 @@
"@nutui/icons-vue-taro": "^0.0.9",
"@nutui/nutui-taro": "^4.3.13",
"@tarojs/components": "4.1.2",
"@tarojs/extend": "^4.1.3",
"@tarojs/helper": "4.1.2",
"@tarojs/plugin-framework-vue3": "4.1.2",
"@tarojs/plugin-html": "4.1.2",
......
<template>
<view class="messages-page">
<!-- Header -->
<view class="bg-orange-400 p-4 pt-8">
<view id="page-header" class="bg-orange-400 p-4 pt-8">
<view class="text-xl font-bold text-white mb-3">消息</view>
<!-- Search Bar -->
<nut-searchbar v-model="searchValue" placeholder="搜索消息" shape="round" background="transparent"
......@@ -13,13 +13,15 @@
</view>
<!-- Tab Navigation -->
<nut-tabs v-model="activeTab" @click="onTabClick">
<nut-tabs id="page-filter" v-model="activeTab" @click="onTabClick">
<nut-tab-pane title="全部" pane-key="all">
<scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50">
<view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
<scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore"
@scroll="scroll" :lower-threshold="50" :enable-flex="false">
<view v-for="conversation in filteredConversations" :key="conversation.id"
class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
@click="onConversationClick(conversation)">
<nut-row>
<nut-col :span="4" class="avatar-container">
<nut-col :span="6" class="avatar-container">
<view class="relative">
<image v-if="conversation.avatar" :src="conversation.avatar"
class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
......@@ -51,11 +53,13 @@
</nut-tab-pane>
<nut-tab-pane title="未读" pane-key="unread">
<scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50">
<view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item"
<scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore" :lower-threshold="50"
:enable-flex="false">
<view v-for="conversation in filteredConversations" :key="conversation.id"
class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
@click="onConversationClick(conversation)">
<nut-row>
<nut-col :span="4" class="avatar-container">
<nut-col :span="6" class="avatar-container">
<view class="relative">
<image v-if="conversation.avatar" :src="conversation.avatar"
class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
......@@ -65,7 +69,7 @@
</view>
</view>
</nut-col>
<nut-col :span="20" class="content-container">
<nut-col :span="19" class="content-container">
<view class="flex justify-between items-center mb-1">
<text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
<view class="flex items-center flex-shrink-0">
......@@ -87,11 +91,13 @@
</nut-tab-pane>
<nut-tab-pane title="通知" pane-key="notification">
<scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50">
<view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item"
<scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore" :lower-threshold="50"
:enable-flex="false">
<view v-for="conversation in filteredConversations" :key="conversation.id"
class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
@click="onConversationClick(conversation)">
<nut-row>
<nut-col :span="4" class="avatar-container">
<nut-col :span="6" class="avatar-container">
<view class="relative">
<image v-if="conversation.avatar" :src="conversation.avatar"
class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
......@@ -101,7 +107,7 @@
</view>
</view>
</nut-col>
<nut-col :span="20" class="content-container">
<nut-col :span="19" class="content-container">
<view class="flex justify-between items-center mb-1">
<text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
<view class="flex items-center flex-shrink-0">
......@@ -123,11 +129,13 @@
</nut-tab-pane>
<nut-tab-pane title="留言" pane-key="message">
<scroll-view class="conversation-list" scroll-y @scrolltolower="loadMore" :lower-threshold="50">
<view v-for="conversation in filteredConversations" :key="conversation.id" class="conversation-item"
<scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore" :lower-threshold="50"
:enable-flex="false">
<view v-for="conversation in filteredConversations" :key="conversation.id"
class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
@click="onConversationClick(conversation)">
<nut-row>
<nut-col :span="4" class="avatar-container">
<nut-col :span="6" class="avatar-container">
<view class="relative">
<image v-if="conversation.avatar" :src="conversation.avatar"
class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
......@@ -137,7 +145,7 @@
</view>
</view>
</nut-col>
<nut-col :span="20" class="content-container">
<nut-col :span="19" class="content-container">
<view class="flex justify-between items-center mb-1">
<text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
<view class="flex items-center flex-shrink-0">
......@@ -169,6 +177,11 @@ import { ref, computed, onMounted, markRaw } from 'vue'
import Taro from '@tarojs/taro'
import { Search2, Notice, Message } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import { $ } from '@tarojs/extend'
const scrollStyle = ref({
height: 'calc(100vh - 500rpx)'
})
// 搜索值
const searchValue = ref('')
......@@ -188,102 +201,24 @@ const conversations = ref([])
// 初始化数据
const initData = () => {
const mockData = [
{
id: 1,
name: '张三',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
lastMessage: '你好,这个商品还在吗?',
time: '5分钟前',
unread: true,
type: 'chat'
},
{
id: 2,
name: '李四',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
lastMessage: '价格可以商量吗?',
time: '30分钟前',
unread: false,
type: 'chat'
},
{
id: 3,
name: '系统通知',
avatar: '',
icon: markRaw(Notice),
lastMessage: '您的商品已通过审核',
time: '1小时前',
unread: true,
type: 'notification'
},
{
id: 4,
name: '王五',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
lastMessage: '[图片]',
time: '2小时前',
unread: true,
type: 'chat'
},
{
id: 5,
name: '客服留言',
avatar: '',
icon: markRaw(Message),
lastMessage: '感谢您的反馈,我们会尽快处理',
time: '昨天',
unread: false,
type: 'message'
},
{
id: 6,
name: '张三',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
lastMessage: '你好,这个商品还在吗?',
time: '5分钟前',
unread: true,
type: 'chat'
},
{
id: 7,
name: '李四',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
lastMessage: '价格可以商量吗?',
time: '30分钟前',
unread: false,
type: 'chat'
},
{
id: 8,
name: '系统通知',
avatar: '',
icon: markRaw(Notice),
lastMessage: '您的商品已通过审核',
time: '1小时前',
unread: true,
type: 'notification'
},
{
id: 9,
name: '王五',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
lastMessage: '[图片]',
time: '2小时前',
unread: true,
type: 'chat'
},
{
id: 10,
name: '客服留言',
avatar: '',
icon: markRaw(Message),
lastMessage: '感谢您的反馈,我们会尽快处理',
time: '昨天',
unread: false,
type: 'message'
},
]
const mockData = []
// 生成更多初始数据确保可以滚动
for (let i = 1; i <= 15; i++) {
const types = ['chat', 'notification', 'message']
const type = types[i % 3]
mockData.push({
id: i,
name: type === 'notification' ? '系统通知' : type === 'message' ? '客服留言' : `用户${i}`,
avatar: type === 'chat' ? `https://randomuser.me/api/portraits/men/${(i % 50) + 1}.jpg` : '',
icon: type === 'notification' ? markRaw(Notice) : type === 'message' ? markRaw(Message) : null,
lastMessage: type === 'notification' ? '您有新的系统通知' : type === 'message' ? '感谢您的反馈' : `这是第${i}条消息内容`,
time: i <= 5 ? `${i * 5}分钟前` : i <= 10 ? `${i}小时前` : `${i - 10}天前`,
unread: Math.random() > 0.5,
type: type
})
}
conversations.value = mockData
}
......@@ -322,13 +257,16 @@ const onTabClick = (tab) => {
// 加载更多数据
const loadMore = async () => {
if (loading.value || !hasMore.value) return
if (loading.value || !hasMore.value) {
return
}
loading.value = true
// 模拟API请求
setTimeout(() => {
const newData = generateMockData(page.value + 1)
if (newData.length > 0) {
conversations.value.push(...newData)
page.value++
......@@ -337,6 +275,8 @@ const loadMore = async () => {
}
loading.value = false
}, 1000)
console.warn('loadMore', page.value, hasMore.value, loading.value);
}
// 生成模拟数据
......@@ -371,228 +311,244 @@ const onConversationClick = (conversation) => {
// 页面加载时初始化数据
onMounted(() => {
// 设置滚动列表可视高度
const windowHeight = wx.getWindowInfo().windowHeight;
setTimeout(async () => {
const headerHeight = await $('#page-header').height();
const navHeight = await $('.nut-tabs__list').height();
scrollStyle.value = {
height: windowHeight - headerHeight - navHeight - 70 + 'px'
}
}, 500);
initData()
})
</script>
<style lang="less" scoped>
<style lang="less">
.messages-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 100rpx;
/* 为TabBar留出空间 */
}
/* 对话列表样式 */
.conversation-list {
height: calc(100vh - 300rpx);
background: #ffffff;
}
/* 对话列表样式 */
.conversation-list {
height: calc(100vh - 500rpx);
background: #ffffff;
overflow: hidden;
box-sizing: border-box;
}
.conversation-item {
padding: 24rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s;
margin: 20rpx 0;
}
/* 确保scroll-view内容区域正确显示 */
:deep(.conversation-list) {
display: block !important;
}
.avatar-container {
display: flex;
align-items: flex-start;
width: 88rpx;
margin-right: 24rpx;
}
.conversation-item {
padding: 24rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s;
margin: 20rpx 0;
}
.content-container {
padding-left: 8px;
}
.avatar-container {
display: flex;
align-items: flex-start;
width: 88rpx;
margin-right: 24rpx;
}
.conversation-item:active {
background-color: #f8f9fa;
}
.content-container {
padding-left: 8px;
}
.conversation-item:last-child {
border-bottom: none;
}
.conversation-item:active {
background-color: #f8f9fa;
}
/* 加载指示器 */
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
gap: 16rpx;
}
.conversation-item:last-child {
border-bottom: none;
}
.loading-text {
font-size: 28rpx;
color: #9ca3af;
}
/* 加载指示器 */
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
gap: 16rpx;
}
/* Tailwind CSS 类的补充样式 */
.w-12 {
width: 88rpx;
}
.loading-text {
font-size: 28rpx;
color: #9ca3af;
}
.h-12 {
height: 88rpx;
}
/* Tailwind CSS 类的补充样式 */
.w-12 {
width: 88rpx;
}
.w-2 {
width: 8rpx;
}
.h-12 {
height: 88rpx;
}
.h-2 {
height: 8rpx;
}
.w-2 {
width: 8rpx;
}
.rounded-full {
border-radius: 50%;
}
.h-2 {
height: 8rpx;
}
.object-cover {
object-fit: cover;
}
// .rounded-full {
// border-radius: 50%;
// }
.bg-gray-100 {
background-color: #f3f4f6;
}
.object-cover {
object-fit: cover;
}
.bg-red-500 {
background-color: #ef4444;
}
.bg-gray-100 {
background-color: #f3f4f6;
}
.flex {
display: flex;
}
.bg-red-500 {
background-color: #ef4444;
}
.flex-1 {
flex: 1;
}
.flex {
display: flex;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-1 {
flex: 1;
}
.min-w-0 {
min-width: 0;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.min-w-0 {
min-width: 0;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.ml-1 {
margin-left: 4rpx;
}
.justify-between {
justify-content: space-between;
}
.ml-3 {
margin-left: 24rpx;
}
.ml-1 {
margin-left: 4rpx;
}
.mt-1 {
margin-top: 4rpx;
}
.ml-3 {
margin-left: 24rpx;
}
.font-medium {
font-weight: 500;
}
.mt-1 {
margin-top: 4rpx;
}
.text-xs {
font-size: 24rpx;
}
.font-medium {
font-weight: 500;
}
.text-sm {
font-size: 28rpx;
}
.text-xs {
font-size: 24rpx;
}
.text-xl {
font-size: 40rpx;
}
.text-sm {
font-size: 28rpx;
}
.font-bold {
font-weight: bold;
}
.text-xl {
font-size: 40rpx;
}
.text-white {
color: #ffffff;
}
.font-bold {
font-weight: bold;
}
.text-gray-500 {
color: #6b7280;
}
.text-white {
color: #ffffff;
}
.text-gray-600 {
color: #4b5563;
}
.text-gray-500 {
color: #6b7280;
}
.bg-orange-400 {
background-color: #fb923c;
}
.text-gray-600 {
color: #4b5563;
}
.p-4 {
padding: 32rpx;
}
.bg-orange-400 {
background-color: #fb923c;
}
.pt-8 {
padding-top: 64rpx;
}
.p-4 {
padding: 32rpx;
}
.mb-1 {
margin-bottom: 4rpx;
}
.pt-8 {
padding-top: 64rpx;
}
.mb-3 {
margin-bottom: 24rpx;
}
.mb-1 {
margin-bottom: 4rpx;
}
.mr-2 {
margin-right: 8rpx;
}
.mb-3 {
margin-bottom: 24rpx;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mr-2 {
margin-right: 8rpx;
}
.block {
display: block;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.relative {
position: relative;
}
.block {
display: block;
}
.relative {
position: relative;
}
}
/* NutUI 组件样式覆盖 */
:deep(.nut-searchbar) {
background: transparent !important;
}
:deep(.nut-searchbar .nut-searchbar__search-input) {
background: #ffffff !important;
border-radius: 40rpx !important;
}
// :deep(.nut-searchbar .nut-searchbar__search-input) {
// background: #ffffff !important;
// border-radius: 40rpx !important;
// }
:deep(.nut-tabs) {
.nut-tabs {
background: #ffffff;
}
:deep(.nut-tabs__titles) {
.nut-tabs__titles {
background: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
:deep(.nut-tab-pane) {
padding: 0;
.nut-tab-pane {
padding: 0 !important;
}
/* 响应式设计 */
......
......@@ -2098,6 +2098,11 @@
swiper "11.1.15"
tslib "^2.6.2"
"@tarojs/extend@^4.1.3":
version "4.1.3"
resolved "https://registry.yarnpkg.com/@tarojs/extend/-/extend-4.1.3.tgz#544bfeca4e7e1c123903dbf4dbafbaaad3600a96"
integrity sha512-59FQgR36XqPGCwCZjaFHpk2BW5aNUbaf3uPOb8e2W8+yHqHJDWbQcGZtM26fb6xhzjb6tm0KS1DCK5T4V1Xtfw==
"@tarojs/helper@4.1.2":
version "4.1.2"
resolved "https://registry.yarnpkg.com/@tarojs/helper/-/helper-4.1.2.tgz#34d9d0b78be720b94b18ba71adf0622e930ed807"
......