hookehuyr

refactor(messages): 替换NutUI标签组件为自定义实现并优化动画效果

移除NutUI的Tabs和TabPane组件,改用自定义的状态筛选标签
添加标签切换时的淡入淡出动画效果
优化消息列表项的入场动画
...@@ -30,8 +30,6 @@ declare module 'vue' { ...@@ -30,8 +30,6 @@ declare module 'vue' {
30 NutSticky: typeof import('@nutui/nutui-taro')['Sticky'] 30 NutSticky: typeof import('@nutui/nutui-taro')['Sticky']
31 NutSwiper: typeof import('@nutui/nutui-taro')['Swiper'] 31 NutSwiper: typeof import('@nutui/nutui-taro')['Swiper']
32 NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem'] 32 NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem']
33 - NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
34 - NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
35 NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] 33 NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
36 NutToast: typeof import('@nutui/nutui-taro')['Toast'] 34 NutToast: typeof import('@nutui/nutui-taro')['Toast']
37 PayCard: typeof import('./src/components/payCard.vue')['default'] 35 PayCard: typeof import('./src/components/payCard.vue')['default']
......
1 <template> 1 <template>
2 <view class="messages-page"> 2 <view class="messages-page">
3 - <nut-sticky>
4 <view class="flex flex-col"> 3 <view class="flex flex-col">
5 <view id="page-header" class="bg-orange-400 p-4 pt-4 pb-4"> 4 <view id="page-header" class="bg-orange-400 p-4 pt-4 pb-4">
6 <nut-row type="flex" justify="center" align="center"> 5 <nut-row type="flex" justify="center" align="center">
...@@ -9,7 +8,8 @@ ...@@ -9,7 +8,8 @@
9 </nut-col> 8 </nut-col>
10 <nut-col span="20"> 9 <nut-col span="20">
11 <!-- Search Bar --> 10 <!-- Search Bar -->
12 - <nut-searchbar v-model="searchValue" placeholder="搜索消息" @blur="onBlurSearch" shape="round" background="transparent" input-background="#ffffff"> 11 + <nut-searchbar v-model="searchValue" placeholder="搜索消息" @blur="onBlurSearch" shape="round"
12 + background="transparent" input-background="#ffffff">
13 <template #leftin> 13 <template #leftin>
14 <Search2 /> 14 <Search2 />
15 </template> 15 </template>
...@@ -18,127 +18,30 @@ ...@@ -18,127 +18,30 @@
18 </nut-row> 18 </nut-row>
19 </view> 19 </view>
20 </view> 20 </view>
21 - </nut-sticky>
22 -
23 - <!-- Tab Navigation -->
24 - <nut-tabs id="page-filter" v-model="activeTab" @click="onTabClick">
25 - <nut-tab-pane title="全部" pane-key="all">
26 - <scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore"
27 - @scroll="scroll" :lower-threshold="50" :enable-flex="false">
28 - <view v-for="conversation in filteredConversations" :key="conversation.id"
29 - class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
30 - @click="onConversationClick(conversation)">
31 - <nut-row>
32 - <nut-col :span="6" class="avatar-container">
33 - <view class="relative">
34 - <image v-if="conversation.avatar" :src="conversation.avatar"
35 - class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
36 - <view v-else
37 - class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center">
38 - <component :is="conversation.icon" />
39 - </view>
40 - </view>
41 - </nut-col>
42 - <nut-col :span="20" class="content-container">
43 - <view class="flex justify-between items-center mb-1">
44 - <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
45 - <view class="flex items-center flex-shrink-0">
46 - <text class="text-xs text-gray-500">{{ conversation.time }}</text>
47 - <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full">
48 - </view>
49 - </view>
50 - </view>
51 - <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text>
52 - </nut-col>
53 - </nut-row>
54 - </view>
55 -
56 - <!-- Loading indicator -->
57 - <view v-if="loading" class="loading-container">
58 - <text class="loading-text">加载中...</text>
59 - </view>
60 - </scroll-view>
61 - </nut-tab-pane>
62 -
63 - <nut-tab-pane title="未读" pane-key="unread">
64 - <scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore"
65 - :lower-threshold="50" :enable-flex="false">
66 - <view v-for="conversation in filteredConversations" :key="conversation.id"
67 - class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
68 - @click="onConversationClick(conversation)">
69 - <nut-row>
70 - <nut-col :span="6" class="avatar-container">
71 - <view class="relative">
72 - <image v-if="conversation.avatar" :src="conversation.avatar"
73 - class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
74 - <view v-else
75 - class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center">
76 - <component :is="conversation.icon" />
77 - </view>
78 - </view>
79 - </nut-col>
80 - <nut-col :span="19" class="content-container">
81 - <view class="flex justify-between items-center mb-1">
82 - <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
83 - <view class="flex items-center flex-shrink-0">
84 - <text class="text-xs text-gray-500">{{ conversation.time }}</text>
85 - <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full">
86 - </view>
87 - </view>
88 - </view>
89 - <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text>
90 - </nut-col>
91 - </nut-row>
92 - </view>
93 21
94 - <!-- Loading indicator -->
95 - <view v-if="loading" class="loading-container">
96 - <text class="loading-text">加载中...</text>
97 - </view>
98 - </scroll-view>
99 - </nut-tab-pane>
100 22
101 - <nut-tab-pane title="通知" pane-key="notification"> 23 + <nut-sticky>
102 - <scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore" 24 + <!-- 状态筛选标签 -->
103 - :lower-threshold="50" :enable-flex="false"> 25 + <view id="status-tabs" class="status-tabs">
104 - <view v-for="conversation in filteredConversations" :key="conversation.id" 26 + <view class="tab-item" :class="{ active: activeTab === 'all' }" @click="setActiveTab('all')">
105 - class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2" 27 + 全部
106 - @click="onConversationClick(conversation)">
107 - <nut-row>
108 - <nut-col :span="6" class="avatar-container">
109 - <view class="relative">
110 - <image v-if="conversation.avatar" :src="conversation.avatar"
111 - class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
112 - <view v-else
113 - class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center">
114 - <component :is="conversation.icon" />
115 - </view>
116 - </view>
117 - </nut-col>
118 - <nut-col :span="19" class="content-container">
119 - <view class="flex justify-between items-center mb-1">
120 - <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
121 - <view class="flex items-center flex-shrink-0">
122 - <text class="text-xs text-gray-500">{{ conversation.time }}</text>
123 - <view v-if="conversation.unread" class="ml-1 w-2 h-2 bg-red-500 rounded-full">
124 </view> 28 </view>
29 + <view class="tab-item" :class="{ active: activeTab === 'unread' }" @click="setActiveTab('unread')">
30 + 未读
125 </view> 31 </view>
32 + <view class="tab-item" :class="{ active: activeTab === 'notification' }"
33 + @click="setActiveTab('notification')">
34 + 通知
126 </view> 35 </view>
127 - <text class="text-sm text-gray-600 truncate block">{{ conversation.lastMessage }}</text> 36 + <view class="tab-item" :class="{ active: activeTab === 'message' }" @click="setActiveTab('message')">
128 - </nut-col> 37 + 留言
129 - </nut-row>
130 </view> 38 </view>
131 -
132 - <!-- Loading indicator -->
133 - <view v-if="loading" class="loading-container">
134 - <text class="loading-text">加载中...</text>
135 </view> 39 </view>
136 - </scroll-view> 40 + </nut-sticky>
137 - </nut-tab-pane>
138 41
139 - <nut-tab-pane title="留言" pane-key="message"> 42 + <!-- 消息列表内容 -->
140 <scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore" 43 <scroll-view class="conversation-list" :style="scrollStyle" :scroll-y="true" @scrolltolower="loadMore"
141 - :lower-threshold="50" :enable-flex="false"> 44 + @scroll="scroll" :lower-threshold="50" :enable-flex="false">
142 <view v-for="conversation in filteredConversations" :key="conversation.id" 45 <view v-for="conversation in filteredConversations" :key="conversation.id"
143 class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2" 46 class="conversation-item mt-2 mb-4 border-b border-gray-100 pb-2"
144 @click="onConversationClick(conversation)"> 47 @click="onConversationClick(conversation)">
...@@ -147,13 +50,12 @@ ...@@ -147,13 +50,12 @@
147 <view class="relative"> 50 <view class="relative">
148 <image v-if="conversation.avatar" :src="conversation.avatar" 51 <image v-if="conversation.avatar" :src="conversation.avatar"
149 class="w-12 h-12 rounded-full object-cover" mode="aspectFill" /> 52 class="w-12 h-12 rounded-full object-cover" mode="aspectFill" />
150 - <view v-else 53 + <view v-else class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center">
151 - class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center">
152 <component :is="conversation.icon" /> 54 <component :is="conversation.icon" />
153 </view> 55 </view>
154 </view> 56 </view>
155 </nut-col> 57 </nut-col>
156 - <nut-col :span="19" class="content-container"> 58 + <nut-col :span="20" class="content-container">
157 <view class="flex justify-between items-center mb-1"> 59 <view class="flex justify-between items-center mb-1">
158 <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text> 60 <text class="font-medium truncate flex-1 mr-2">{{ conversation.name }}</text>
159 <view class="flex items-center flex-shrink-0"> 61 <view class="flex items-center flex-shrink-0">
...@@ -172,8 +74,6 @@ ...@@ -172,8 +74,6 @@
172 <text class="loading-text">加载中...</text> 74 <text class="loading-text">加载中...</text>
173 </view> 75 </view>
174 </scroll-view> 76 </scroll-view>
175 - </nut-tab-pane>
176 - </nut-tabs>
177 77
178 <!-- 自定义TabBar --> 78 <!-- 自定义TabBar -->
179 <TabBar /> 79 <TabBar />
...@@ -217,7 +117,7 @@ ...@@ -217,7 +117,7 @@
217 117
218 <script setup> 118 <script setup>
219 import { ref, computed, onMounted, markRaw } from 'vue' 119 import { ref, computed, onMounted, markRaw } from 'vue'
220 -import Taro from '@tarojs/taro' 120 +
221 import { Search2, Notice, Message } from '@nutui/icons-vue-taro' 121 import { Search2, Notice, Message } from '@nutui/icons-vue-taro'
222 import TabBar from '@/components/TabBar.vue' 122 import TabBar from '@/components/TabBar.vue'
223 import { $ } from '@tarojs/extend' 123 import { $ } from '@tarojs/extend'
...@@ -301,11 +201,30 @@ const filteredConversations = computed(() => { ...@@ -301,11 +201,30 @@ const filteredConversations = computed(() => {
301 }) 201 })
302 202
303 // Tab点击事件 203 // Tab点击事件
304 -const onTabClick = (tab) => { 204 +const setActiveTab = (tabKey) => {
305 - activeTab.value = tab.paneKey 205 + if (activeTab.value === tabKey) return;
306 - // 重置分页 206 +
307 - page.value = 1 207 + // 添加淡出效果
308 - hasMore.value = true 208 + const conversationList = document.querySelector('.conversation-list');
209 + if (conversationList) {
210 + conversationList.style.opacity = '0';
211 + conversationList.style.transform = 'translateY(10rpx)';
212 + }
213 +
214 + setTimeout(() => {
215 + activeTab.value = tabKey;
216 + page.value = 1;
217 + hasMore.value = true;
218 + initData();
219 +
220 + // 添加淡入效果
221 + setTimeout(() => {
222 + if (conversationList) {
223 + conversationList.style.opacity = '1';
224 + conversationList.style.transform = 'translateY(0)';
225 + }
226 + }, 50);
227 + }, 150);
309 } 228 }
310 229
311 // 加载更多数据 230 // 加载更多数据
...@@ -387,7 +306,7 @@ onMounted(() => { ...@@ -387,7 +306,7 @@ onMounted(() => {
387 const windowHeight = wx.getWindowInfo().windowHeight; 306 const windowHeight = wx.getWindowInfo().windowHeight;
388 setTimeout(async () => { 307 setTimeout(async () => {
389 const headerHeight = await $('#page-header').height(); 308 const headerHeight = await $('#page-header').height();
390 - const navHeight = await $('.nut-tabs__list').height(); 309 + const navHeight = await $('#status-tabs').height();
391 scrollStyle.value = { 310 scrollStyle.value = {
392 height: windowHeight - headerHeight - navHeight - 70 + 'px' 311 height: windowHeight - headerHeight - navHeight - 70 + 'px'
393 } 312 }
...@@ -665,27 +584,119 @@ onMounted(() => { ...@@ -665,27 +584,119 @@ onMounted(() => {
665 584
666 } 585 }
667 586
668 -/* NutUI 组件样式覆盖 */ 587 +/* 状态筛选标签 */
669 -:deep(.nut-searchbar) { 588 +.status-tabs {
670 - background: transparent !important; 589 + background: white;
590 + padding: 20rpx 35rpx; /* 增加内边距 */
591 + border-bottom: 1rpx solid #e5e7eb;
592 + display: flex;
593 + position: relative;
594 + overflow-x: auto;
595 + scrollbar-width: none;
596 + -ms-overflow-style: none;
597 +
598 + &::-webkit-scrollbar {
599 + display: none;
600 + }
671 } 601 }
672 602
673 -// :deep(.nut-searchbar .nut-searchbar__search-input) { 603 +.tab-item {
674 -// background: #ffffff !important; 604 + margin-right: 48rpx;
675 -// border-radius: 40rpx !important; 605 + padding-bottom: 16rpx;
676 -// } 606 + font-size: 30rpx; /* 增大字体 */
607 + color: #6b7280;
608 + position: relative;
609 + cursor: pointer;
610 + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
611 + white-space: nowrap;
612 + flex-shrink: 0;
613 + transform: translateX(0);
677 614
678 -.nut-tabs { 615 + &.active {
679 - background: #ffffff; 616 + color: #f97316;
617 + font-weight: 500;
618 + transform: translateY(-2rpx);
619 +
620 + &::after {
621 + content: '';
622 + position: absolute;
623 + bottom: 0;
624 + left: 0;
625 + right: 0;
626 + height: 4rpx;
627 + background: linear-gradient(90deg, #f97316, #fb923c);
628 + border-radius: 2rpx;
629 + animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
630 + }
631 + }
632 +
633 + &:hover {
634 + color: #f97316;
635 + transform: translateY(-1rpx);
636 + }
637 +
638 + &:last-child {
639 + margin-right: 0;
640 + }
680 } 641 }
681 642
682 -.nut-tabs__titles { 643 +
683 - background: #ffffff; 644 +
684 - border-bottom: 1rpx solid #f0f0f0; 645 +/* 滑入动画 */
646 +@keyframes slideIn {
647 + 0% {
648 + transform: scaleX(0);
649 + opacity: 0;
650 + }
651 + 100% {
652 + transform: scaleX(1);
653 + opacity: 1;
654 + }
655 +}
656 +
657 +/* 内容切换动画 */
658 +.conversation-list {
659 + animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
660 + transition: opacity 0.15s ease-out, transform 0.15s ease-out;
661 +}
662 +
663 +@keyframes fadeInUp {
664 + from {
665 + opacity: 0;
666 + transform: translateY(20rpx);
667 + }
668 + to {
669 + opacity: 1;
670 + transform: translateY(0);
671 + }
672 +}
673 +
674 +/* 消息项动画 */
675 +.conversation-item {
676 + animation: fadeInItem 0.3s cubic-bezier(0.4, 0, 0.2, 1);
677 + animation-fill-mode: both;
685 } 678 }
686 679
687 -.nut-tab-pane { 680 +.conversation-item:nth-child(1) { animation-delay: 0.1s; }
688 - padding: 0 !important; 681 +.conversation-item:nth-child(2) { animation-delay: 0.15s; }
682 +.conversation-item:nth-child(3) { animation-delay: 0.2s; }
683 +.conversation-item:nth-child(4) { animation-delay: 0.25s; }
684 +.conversation-item:nth-child(5) { animation-delay: 0.3s; }
685 +
686 +@keyframes fadeInItem {
687 + from {
688 + opacity: 0;
689 + transform: translateX(-20rpx);
690 + }
691 + to {
692 + opacity: 1;
693 + transform: translateX(0);
694 + }
695 +}
696 +
697 +/* NutUI 组件样式覆盖 */
698 +:deep(.nut-searchbar) {
699 + background: transparent !important;
689 } 700 }
690 701
691 /* 响应式设计 */ 702 /* 响应式设计 */
......