Initial Commit
This commit is contained in:
commit
1d71a02738
237 changed files with 64293 additions and 0 deletions
3085
pages/message/chat/chat.js
Normal file
3085
pages/message/chat/chat.js
Normal file
File diff suppressed because it is too large
Load diff
17
pages/message/chat/chat.json
Normal file
17
pages/message/chat/chat.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"navigationBarTitleText": "聊天",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"backgroundTextStyle": "light",
|
||||
"enablePullDownRefresh": false,
|
||||
"onReachBottomDistance": 50,
|
||||
"disableScroll": false,
|
||||
"usingComponents": {
|
||||
"media-preview": "../../../components/media-preview/media-preview",
|
||||
"message-action-menu": "../../../components/message-action-menu/message-action-menu",
|
||||
"mention-selector": "../../../components/mention-selector/mention-selector",
|
||||
"voice-message": "../../../components/voice-message/voice-message",
|
||||
"voice-recorder": "../../../components/voice-recorder/voice-recorder"
|
||||
}
|
||||
}
|
||||
412
pages/message/chat/chat.wxml
Normal file
412
pages/message/chat/chat.wxml
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
<!--聊天页面-->
|
||||
<view class="chat-container {{themeClass}} {{isThemeTransitioning ? 'theme-transitioning' : ''}} {{showEmojiPanel ? 'with-emoji' : ''}}">
|
||||
<!-- 加载状态指示器 -->
|
||||
<view wx:if="{{isLoading}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">正在加载聊天...</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
style="height:100%;"
|
||||
scroll-y="{{true}}"
|
||||
scroll-top="{{scrollTop}}"
|
||||
scroll-into-view="{{scrollIntoView}}"
|
||||
enhanced="{{true}}"
|
||||
show-scrollbar="{{false}}"
|
||||
bindscrolltoupper="loadMoreMessages"
|
||||
bindscroll="onScroll"
|
||||
upper-threshold="50"
|
||||
>
|
||||
<!-- 🔥 顶部加载更多提示 -->
|
||||
<view wx:if="{{hasMore}}" class="load-more-top" bindtap="loadMoreMessages">
|
||||
<text class="load-more-text">{{loadingMessages ? '加载中...' : '上拉加载更多消息'}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:for="{{messages}}" wx:key="messageId" class="message-item" id="msg-{{item.messageId}}">
|
||||
<!-- 日期分隔线(仅日期) -->
|
||||
<view wx:if="{{item.showDateDivider}}" class="time-divider">
|
||||
<text class="time-text">{{item.dateText}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息内容:根据是否为自己,交换头像与消息体的渲染顺序以确保两侧显示正确 -->
|
||||
<view class="message-wrapper {{item.isSelf ? 'self' : 'other'}}">
|
||||
<!-- 自己的消息:先渲染消息体,再渲染头像(头像在右侧) -->
|
||||
<block wx:if="{{item.isSelf}}">
|
||||
<view class="message-body">
|
||||
<!-- 自己的消息不显示昵称 -->
|
||||
|
||||
<!-- 单行(右侧):气泡内显示时间/状态 -->
|
||||
<view class="message-line self">
|
||||
<view class="message-content {{item.msgType}} {{item.isRecalled ? 'recalled' : ''}}" bindlongpress="showMessageMenu" data-message="{{item}}">
|
||||
<!-- 撤回消息显示 -->
|
||||
<view wx:if="{{item.isRecalled}}" class="recalled-content">
|
||||
<text class="recalled-icon">↩️</text>
|
||||
<text class="recalled-text">{{item.content}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 普通消息显示 -->
|
||||
<text wx:elif="{{item.msgType === 'text'}}" class="text-content">{{item.content}}</text>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'image'}}">
|
||||
<view wx:if="{{item.content.type === 'error'}}" class="error-content">
|
||||
<text class="error-text">❌ 图片加载失败</text>
|
||||
<text class="error-detail">{{item.content.error}}</text>
|
||||
<text class="error-original">原始内容: {{item.content.originalContent}}</text>
|
||||
</view>
|
||||
<image wx:else
|
||||
class="image-content"
|
||||
src="{{item.content.url || item.content}}"
|
||||
mode="aspectFit"
|
||||
bindtap="previewImage"
|
||||
data-url="{{item.content.url || item.content}}"
|
||||
data-type="{{item.content.type}}"
|
||||
binderror="onImageError">
|
||||
</image>
|
||||
</view>
|
||||
|
||||
<voice-message wx:elif="{{item.msgType === 'audio' || item.msgType === 'voice'}}"
|
||||
voice-data="{{item.content}}"
|
||||
is-self="{{item.isSelf}}"
|
||||
message-id="{{item.messageId}}">
|
||||
</voice-message>
|
||||
|
||||
<video wx:elif="{{item.msgType === 'video'}}"
|
||||
class="video-content"
|
||||
src="{{item.content}}"
|
||||
poster="{{item.thumbnail}}"
|
||||
controls>
|
||||
</video>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'location'}}" class="location-content" bindtap="showLocation" data-message="{{item}}">
|
||||
<text class="location-icon">📍</text>
|
||||
<view class="location-info">
|
||||
<text class="location-name">{{item.locationName || '位置信息'}}</text>
|
||||
<text class="location-address">{{item.locationAddress}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'file'}}" class="file-content" bindtap="downloadFile" data-message="{{item}}">
|
||||
<text class="file-icon">📄</text>
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{item.fileName || '文件'}}</text>
|
||||
<text class="file-size">{{item.fileSize || '未知大小'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image wx:elif="{{item.msgType === 'sticker'}}"
|
||||
class="sticker-content"
|
||||
src="{{item.content.url || item.content}}"
|
||||
mode="aspectFit">
|
||||
</image>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'card'}}" class="card-content" bindtap="viewCard" data-message="{{item}}">
|
||||
<text class="card-icon">👤</text>
|
||||
<view class="card-info">
|
||||
<text class="card-name">{{item.cardName || '联系人'}}</text>
|
||||
<text class="card-phone">{{item.cardPhone || ''}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'system'}}" class="system-content">
|
||||
<text class="system-text">{{item.content}}</text>
|
||||
</view>
|
||||
|
||||
<text wx:else class="unknown-content">[不支持的消息类型]</text>
|
||||
|
||||
<!-- 气泡内时间与状态 -->
|
||||
<view wx:if="{{item.msgType !== 'system'}}" class="bubble-meta">
|
||||
<text class="bubble-time">{{item.bubbleTime}}</text>
|
||||
<text wx:if="{{item.deliveryStatus === 'read'}}" class="meta-status meta-read">✓✓</text>
|
||||
<text wx:elif="{{item.deliveryStatus === 'delivered' || item.deliveryStatus === 'sent'}}" class="meta-status meta-delivered">✓</text>
|
||||
<text wx:elif="{{item.deliveryStatus === 'sending'}}" class="meta-status meta-sending">●</text>
|
||||
<text wx:elif="{{item.deliveryStatus === 'failed'}}" class="meta-status meta-failed">!</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发送失败的“重发”操作(仅自己消息显示,出现在气泡下方靠右) -->
|
||||
<view wx:if="{{item.isSelf && item.deliveryStatus === 'failed' && !item.isRecalled && item.msgType !== 'system'}}" class="resend-row">
|
||||
<view class="resend-action" bindtap="resendMessage" data-message="{{item}}">
|
||||
<text class="resend-icon">↻</text>
|
||||
<text class="resend-text">重新发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自己的头像(右侧) -->
|
||||
<view class="avatar">
|
||||
<view wx:if="{{userAvatar && userAvatar.length > 0}}" class="avatar-image">
|
||||
<image src="{{userAvatar}}" mode="aspectFill" bindload="onAvatarLoad" binderror="onAvatarError" />
|
||||
</view>
|
||||
<view wx:else class="avatar-placeholder">
|
||||
<text>👤</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 对方的消息:头像在左,消息体在右 -->
|
||||
<block wx:else>
|
||||
<view class="avatar">
|
||||
<view wx:if="{{item.senderAvatar}}" class="avatar-image">
|
||||
<image src="{{item.senderAvatar}}" mode="aspectFill" bindload="onAvatarLoad" binderror="onAvatarError" />
|
||||
</view>
|
||||
<view wx:else class="avatar-placeholder">
|
||||
<text>👥</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="message-body">
|
||||
<view class="sender-name" wx:if="{{!item.isSelf && chatType === 1}}">
|
||||
{{item.senderName}}
|
||||
</view>
|
||||
|
||||
<!-- 单行(左侧):气泡内显示时间(仅时间) -->
|
||||
<view class="message-line other">
|
||||
<view class="message-content {{item.msgType}} {{item.isRecalled ? 'recalled' : ''}}" bindlongpress="showMessageMenu" data-message="{{item}}">
|
||||
<!-- 撤回消息显示 -->
|
||||
<view wx:if="{{item.isRecalled}}" class="recalled-content">
|
||||
<text class="recalled-icon">↩️</text>
|
||||
<text class="recalled-text">{{item.content}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 普通消息显示 -->
|
||||
<text wx:elif="{{item.msgType === 'text'}}" class="text-content">{{item.content}}</text>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'image'}}">
|
||||
<view wx:if="{{item.content.type === 'error'}}" class="error-content">
|
||||
<text class="error-text">❌ 图片加载失败</text>
|
||||
<text class="error-detail">{{item.content.error}}</text>
|
||||
<text class="error-original">原始内容: {{item.content.originalContent}}</text>
|
||||
</view>
|
||||
<image wx:else
|
||||
class="image-content"
|
||||
src="{{item.content.url || item.content}}"
|
||||
mode="aspectFit"
|
||||
bindtap="previewImage"
|
||||
data-url="{{item.content.url || item.content}}"
|
||||
data-type="{{item.content.type}}"
|
||||
binderror="onImageError">
|
||||
</image>
|
||||
</view>
|
||||
|
||||
<voice-message wx:elif="{{item.msgType === 'audio' || item.msgType === 'voice'}}"
|
||||
voice-data="{{item.content}}"
|
||||
is-self="{{item.isSelf}}"
|
||||
message-id="{{item.messageId}}">
|
||||
</voice-message>
|
||||
|
||||
<video wx:elif="{{item.msgType === 'video'}}"
|
||||
class="video-content"
|
||||
src="{{item.content}}"
|
||||
poster="{{item.thumbnail}}"
|
||||
controls>
|
||||
</video>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'location'}}" class="location-content" bindtap="showLocation" data-message="{{item}}">
|
||||
<text class="location-icon">📍</text>
|
||||
<view class="location-info">
|
||||
<text class="location-name">{{item.locationName || '位置信息'}}</text>
|
||||
<text class="location-address">{{item.locationAddress}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'file'}}" class="file-content" bindtap="downloadFile" data-message="{{item}}">
|
||||
<text class="file-icon">📄</text>
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{item.fileName || '文件'}}</text>
|
||||
<text class="file-size">{{item.fileSize || '未知大小'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<image wx:elif="{{item.msgType === 'sticker'}}"
|
||||
class="sticker-content"
|
||||
src="{{item.content.url || item.content}}"
|
||||
mode="aspectFit">
|
||||
</image>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'card'}}" class="card-content" bindtap="viewCard" data-message="{{item}}">
|
||||
<text class="card-icon">👤</text>
|
||||
<view class="card-info">
|
||||
<text class="card-name">{{item.cardName || '联系人'}}</text>
|
||||
<text class="card-phone">{{item.cardPhone || ''}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{item.msgType === 'system'}}" class="system-content">
|
||||
<text class="system-text">{{item.content}}</text>
|
||||
</view>
|
||||
|
||||
<text wx:else class="unknown-content">[不支持的消息类型]</text>
|
||||
|
||||
<!-- 气泡内时间(对方仅时间,无状态) -->
|
||||
<view wx:if="{{item.msgType !== 'system'}}" class="bubble-meta">
|
||||
<text class="bubble-time">{{item.bubbleTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 回到最新消息按钮 -->
|
||||
<view class="scroll-to-bottom-btn {{showScrollToBottom ? 'show' : ''}}"
|
||||
bindtap="scrollToBottom"
|
||||
wx:if="{{showScrollToBottom}}">
|
||||
<view class="btn-icon">↓</view>
|
||||
</view>
|
||||
|
||||
<!-- 输入框区域 -->
|
||||
<view class="input-area" style="padding-bottom: {{safeAreaBottom}}px;">
|
||||
<view class="input-row">
|
||||
<!-- 语音按钮 -->
|
||||
<view class="tool-btn" bindtap="toggleInputType">
|
||||
<text class="{{inputType === 'text' ? 'icon-mic' : 'icon-keyboard'}}">{{inputType === 'text' ? '🎤' : '⌨️'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 文本输入 -->
|
||||
<view wx:if="{{inputType === 'text'}}" class="text-input-wrapper">
|
||||
<textarea
|
||||
class="text-input"
|
||||
placeholder="请输入消息..."
|
||||
value="{{inputText}}"
|
||||
bindinput="onInputChange"
|
||||
bindconfirm="sendTextMessage"
|
||||
bindcompositionstart="onCompositionStart"
|
||||
bindcompositionend="onCompositionEnd"
|
||||
bindlinechange="onLineChange"
|
||||
focus="{{inputFocus}}"
|
||||
maxlength="500"
|
||||
confirm-type="send"
|
||||
adjust-position="{{true}}"
|
||||
hold-keyboard="{{true}}"
|
||||
auto-height="{{true}}"
|
||||
show-confirm-bar="{{false}}"
|
||||
fixed="{{false}}"
|
||||
cursor-spacing="0"
|
||||
disable-default-padding="{{true}}"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 语音录制 -->
|
||||
<view wx:else class="voice-input-wrapper">
|
||||
<button
|
||||
class="voice-btn {{recording ? 'recording' : ''}}"
|
||||
bindtap="openVoiceRecorder"
|
||||
>
|
||||
🎤 录制语音
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 表情按钮 -->
|
||||
<view class="tool-btn" bindtap="toggleEmojiPanel">
|
||||
<text class="icon-emoji">😊</text>
|
||||
</view>
|
||||
|
||||
<!-- 更多按钮 -->
|
||||
<view class="tool-btn" bindtap="toggleMorePanel">
|
||||
<text class="icon-more">➕</text>
|
||||
</view>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<view wx:if="{{inputText.length > 0}}" class="send-btn" bindtap="sendTextMessage">
|
||||
<text>发送</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表情面板 -->
|
||||
<view wx:if="{{showEmojiPanel}}" class="emoji-panel">
|
||||
<scroll-view class="emoji-scroll" scroll-y="{{true}}" show-scrollbar="{{false}}">
|
||||
<view class="emoji-list">
|
||||
<text wx:for="{{commonEmojis}}" wx:key="*this" class="emoji-item" bindtap="selectEmoji" data-emoji="{{item}}">{{item}}</text>
|
||||
</view>
|
||||
<!-- 底部安全区占位,避免最后一排被遮挡,保证能继续滚动到最底部 -->
|
||||
<view class="emoji-bottom-spacer" style="height: {{safeAreaBottom}}px;"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多功能面板:移到 input-area 外,避免被其 overflow 裁切 -->
|
||||
<view wx:if="{{showMorePanel}}" class="more-panel-overlay" bindtap="toggleMorePanel">
|
||||
<view class="more-panel" catchtap="stopPropagation">
|
||||
<view class="more-panel-header">
|
||||
<text class="more-panel-title">选择功能</text>
|
||||
<view class="more-panel-close" bindtap="toggleMorePanel">✕</view>
|
||||
</view>
|
||||
<view class="more-list">
|
||||
<view class="more-item" bindtap="showMediaPicker">
|
||||
<text class="more-icon">📷</text>
|
||||
<text class="more-text">拍照/录像</text>
|
||||
</view>
|
||||
<view class="more-item" bindtap="chooseImageFromAlbum">
|
||||
<text class="more-icon">🖼️</text>
|
||||
<text class="more-text">相册</text>
|
||||
</view>
|
||||
<view class="more-item" bindtap="selectFile">
|
||||
<text class="more-icon">📄</text>
|
||||
<text class="more-text">文件</text>
|
||||
</view>
|
||||
<view class="more-item" bindtap="selectLocation">
|
||||
<text class="more-icon">📍</text>
|
||||
<text class="more-text">位置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 录音提示 -->
|
||||
<view wx:if="{{recording}}" class="recording-tips">
|
||||
<text>正在录音...</text>
|
||||
</view>
|
||||
|
||||
<!-- 语音录制组件 -->
|
||||
<voice-recorder
|
||||
visible="{{showVoiceRecorder}}"
|
||||
max-duration="{{60000}}"
|
||||
min-duration="{{1000}}"
|
||||
bind:send="onVoiceSend"
|
||||
bind:close="closeVoiceRecorder">
|
||||
</voice-recorder>
|
||||
|
||||
<!-- 媒体预览组件 -->
|
||||
<media-preview
|
||||
visible="{{mediaPreviewVisible}}"
|
||||
media-list="{{mediaPreviewList}}"
|
||||
current-index="{{currentMediaIndex}}"
|
||||
bind:close="closeMediaPreview"
|
||||
bind:indexchange="onMediaIndexChange">
|
||||
</media-preview>
|
||||
|
||||
<!-- 消息操作菜单组件 -->
|
||||
<message-action-menu
|
||||
visible="{{messageActionVisible}}"
|
||||
message="{{currentMessage}}"
|
||||
is-own-message="{{isOwnMessage}}"
|
||||
bind:close="closeMessageAction"
|
||||
bind:action="onMessageAction">
|
||||
</message-action-menu>
|
||||
|
||||
<!-- @提醒选择器组件 -->
|
||||
<mention-selector
|
||||
visible="{{mentionSelectorVisible}}"
|
||||
group-id="{{targetId}}"
|
||||
current-user-id="{{userInfo.user.customId}}"
|
||||
bind:close="closeMentionSelector"
|
||||
bind:mention="onMentionSelect">
|
||||
</mention-selector>
|
||||
|
||||
<!-- 主题切换悬浮按钮 -->
|
||||
<view class="theme-toggle" bindtap="toggleTheme" style="position:fixed; left:24rpx; top: {{statusBarHeight+20}}rpx; width:64rpx; height:64rpx; border-radius:32rpx; background:rgba(255,255,255,0.04); display:flex; align-items:center; justify-content:center; z-index:800; color:var(--text-primary);">
|
||||
<text>{{themeClass === 'theme-dark' ? '🌙' : '☀️'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 主题切换径向遮罩动画 -->
|
||||
<view wx:if="{{showThemeOverlay}}" class="theme-overlay">
|
||||
<view class="theme-overlay-circle {{overlayPlaying ? 'play' : ''}}" style="width: {{overlayDiameter}}px; height: {{overlayDiameter}}px; left: {{overlayLeft}}px; top: {{overlayTop}}px;"></view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
1020
pages/message/chat/chat.wxss
Normal file
1020
pages/message/chat/chat.wxss
Normal file
File diff suppressed because it is too large
Load diff
1399
pages/message/message.js
Normal file
1399
pages/message/message.js
Normal file
File diff suppressed because it is too large
Load diff
18
pages/message/message.json
Normal file
18
pages/message/message.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"navigationBarTitleText": "消息",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"disableScroll": true,
|
||||
"navigationBarButtonTap": "onNavigationBarButtonTap",
|
||||
"navigationBarButtons": [
|
||||
{
|
||||
"text": "🔍",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"text": "⋯",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
108
pages/message/message.wxml
Normal file
108
pages/message/message.wxml
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<!--消息页面 - Telegram风格设计-->
|
||||
<view class="telegram-container {{themeClass}} {{isThemeTransitioning ? 'theme-transitioning' : ''}}">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="telegram-header">
|
||||
<view class="header-content">
|
||||
<text class="header-title">消息</text>
|
||||
<view class="header-actions">
|
||||
<view class="header-btn" bindtap="showSearchBar"><text class="header-icon">🔍</text></view>
|
||||
<view class="header-btn" bindtap="markAllRead"><text class="header-icon">✔️</text></view>
|
||||
<view class="header-btn" bindtap="startNewChat"><text class="header-icon">➕</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section" wx:if="{{showSearch}}">
|
||||
<view class="search-bar">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input class="search-input"
|
||||
placeholder="搜索聊天记录"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
focus="{{showSearch}}" />
|
||||
<view class="search-cancel" bindtap="hideSearchBar">
|
||||
<text class="cancel-text">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Telegram风格内容:始终展示会话列表(filteredConversations会随搜索实时过滤) -->
|
||||
<scroll-view class="telegram-content" scroll-y="true"
|
||||
refresher-enabled="true"
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh">
|
||||
<view class="conversations-section">
|
||||
<view class="conversation-item {{item.isPinned ? 'pinned' : ''}} {{item.isMuted ? 'muted' : ''}}"
|
||||
wx:for="{{filteredConversations}}"
|
||||
wx:key="conversationId"
|
||||
bindtap="openChat"
|
||||
bindlongtap="showConversationOptions"
|
||||
data-conversation="{{item}}">
|
||||
|
||||
<!-- 头像 -->
|
||||
<view class="conversation-avatar">
|
||||
<image wx:if="{{item.avatar}}"
|
||||
src="{{item.avatar}}"
|
||||
class="avatar-image"
|
||||
mode="aspectFill" />
|
||||
<view wx:else class="avatar-placeholder">
|
||||
<text class="avatar-text">{{item.name.charAt(0)}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 在线状态角标(仅单聊显示) -->
|
||||
<view wx:if="{{item.type === 'single' || item.chatType === 0}}"
|
||||
class="presence-indicator {{item.isOnline ? 'online' : 'offline'}}"></view>
|
||||
|
||||
<!-- 未读消息徽章 -->
|
||||
<view class="unread-badge" wx:if="{{item.unreadCount > 0}}">
|
||||
<text class="unread-count">{{item.unreadCount > 99 ? '99+' : item.unreadCount}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 置顶标识 -->
|
||||
<view class="pin-indicator" wx:if="{{item.isPinned}}">
|
||||
<text class="pin-icon">📌</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话信息 -->
|
||||
<view class="conversation-info">
|
||||
<view class="conversation-header">
|
||||
<text class="conversation-name">{{item.name}}</text>
|
||||
<view class="conversation-meta">
|
||||
<text class="conversation-time">{{item.lastMessageTime}}</text>
|
||||
<text class="mute-icon" wx:if="{{item.isMuted}}">🔇</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="conversation-preview">
|
||||
<text class="last-message">{{item.lastMessage}}</text>
|
||||
<view class="message-indicators">
|
||||
<!-- 消息状态指示器 -->
|
||||
<text class="status-indicator" wx:if="{{item.messageStatus === 'sent'}}">✓</text>
|
||||
<text class="status-indicator read" wx:if="{{item.messageStatus === 'read'}}">✓✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && filteredConversations.length === 0}}">
|
||||
<text class="empty-icon">💬</text>
|
||||
<text class="empty-title">暂无聊天</text>
|
||||
<text class="empty-subtitle">开始一段新的对话吧</text>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 主题切换悬浮按钮 -->
|
||||
<view class="theme-toggle" bindtap="toggleTheme" style="position:fixed; left:24rpx; top: {{statusBarHeight+20}}rpx; width:64rpx; height:64rpx; border-radius:32rpx; background:rgba(255,255,255,0.04); display:flex; align-items:center; justify-content:center; z-index:800; color:var(--text-primary);">
|
||||
<text>{{themeClass === 'theme-dark' ? '🌙' : '☀️'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 主题切换径向遮罩动画 -->
|
||||
<view wx:if="{{showThemeOverlay}}" class="theme-overlay">
|
||||
<view class="theme-overlay-circle {{overlayPlaying ? 'play' : ''}}" style="width: {{overlayDiameter}}px; height: {{overlayDiameter}}px; left: {{overlayLeft}}px; top: {{overlayTop}}px;"></view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
515
pages/message/message.wxss
Normal file
515
pages/message/message.wxss
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
/* 🎨 现代化消息页面设计 - 深色主题 */
|
||||
|
||||
/* 🎨 CSS变量定义 - 黑色主题 */
|
||||
page {
|
||||
--primary-color: #0A84FF;
|
||||
--primary-light: #3EA8FF;
|
||||
--primary-dark: #0056CC;
|
||||
--background-color: #070709;
|
||||
--surface-color: #0F0F11;
|
||||
--text-primary: #ECECEC;
|
||||
--text-secondary: #A8A8A8;
|
||||
--text-tertiary: #7A7A7A;
|
||||
--border-color: rgba(255,255,255,0.06);
|
||||
--shadow-light: 0 1rpx 6rpx rgba(0,0,0,0.6);
|
||||
--shadow-medium: 0 6rpx 18rpx rgba(0,0,0,0.7);
|
||||
--radius-small: 8rpx;
|
||||
--radius-medium: 12rpx;
|
||||
--radius-large: 20rpx;
|
||||
--safe-area-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 🌙 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
page {
|
||||
--primary-color: #0A84FF;
|
||||
--primary-light: #64D2FF;
|
||||
--primary-dark: #0056CC;
|
||||
--background-color: #000000;
|
||||
--surface-color: #1C1C1E;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #8E8E93;
|
||||
--text-tertiary: #48484A;
|
||||
--border-color: #38383A;
|
||||
--shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3);
|
||||
--shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 浅色主题覆盖(当根元素包含 .theme-light 时生效) ===== */
|
||||
.theme-light {
|
||||
--primary-color: #007AFF;
|
||||
--primary-light: #5AC8FA;
|
||||
--primary-dark: #0051D5;
|
||||
--background-color: #FFFFFF;
|
||||
--surface-color: #FFFFFF;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #8E8E93;
|
||||
--text-tertiary: #C7C7CC;
|
||||
--border-color: #E5E5EA;
|
||||
--shadow-light: 0 1rpx 3rpx rgba(0,0,0,0.1);
|
||||
--shadow-medium: 0 4rpx 12rpx rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* 恢复浅色会话项样式(多数规则使用变量,会自动适配) */
|
||||
.theme-light .telegram-container {
|
||||
background: var(--background-color) !important;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.theme-light .search-section {
|
||||
background: var(--surface-color);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
.theme-light .search-bar {
|
||||
background: var(--background-color);
|
||||
border: 1rpx solid var(--border-color);
|
||||
}
|
||||
.theme-light .conversation-item {
|
||||
background: var(--surface-color) !important;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
.theme-light .conversations-section {
|
||||
background: transparent !important;
|
||||
}
|
||||
.theme-light .avatar-placeholder {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
|
||||
color: white;
|
||||
}
|
||||
.theme-light .unread-badge {
|
||||
border: 3rpx solid var(--surface-color);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
.theme-light .empty-state {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.telegram-container {
|
||||
min-height: 100vh;
|
||||
background: var(--background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--safe-area-bottom);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 280ms ease, color 280ms ease, background 280ms ease;
|
||||
}
|
||||
|
||||
/* === 主题切换过渡(类 Telegram)=== */
|
||||
.telegram-container.theme-transitioning {
|
||||
transition: background 320ms ease, color 320ms ease, filter 320ms ease;
|
||||
}
|
||||
|
||||
/* 关键区域同步过渡,避免突变 */
|
||||
.search-section,
|
||||
.search-bar,
|
||||
.conversation-item,
|
||||
.empty-state,
|
||||
.telegram-header {
|
||||
transition: background-color 280ms ease, color 280ms ease, border-color 280ms ease, box-shadow 280ms ease;
|
||||
}
|
||||
|
||||
/* 🎨 现代化导航栏 */
|
||||
.telegram-header {
|
||||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%);
|
||||
box-shadow: var(--shadow-medium);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(12rpx);
|
||||
}
|
||||
|
||||
/* === 主题切换径向遮罩动画 === */
|
||||
.theme-overlay {
|
||||
position: fixed;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 3000;
|
||||
}
|
||||
.theme-overlay-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: var(--background-color);
|
||||
transform: scale(0.001);
|
||||
opacity: 0;
|
||||
}
|
||||
.theme-overlay-circle.play {
|
||||
animation: themeExpand 380ms ease forwards;
|
||||
}
|
||||
@keyframes themeExpand {
|
||||
0% { transform: scale(0.001); opacity: 0; }
|
||||
20% { opacity: 1; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.header-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: var(--radius-medium);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.header-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 32rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 🎨 现代化内容区域 */
|
||||
.telegram-content {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
/* 让内容自然流式布局,避免覆盖搜索栏与头部 */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 🎨 现代化搜索栏 */
|
||||
.search-section {
|
||||
padding: 20rpx 32rpx;
|
||||
background: var(--background-color);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
animation: searchSlideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes searchSlideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
height: 80rpx;
|
||||
background: var(--background-color);
|
||||
border: 1rpx solid var(--border-color);
|
||||
border-radius: var(--radius-large);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 28rpx;
|
||||
gap: 16rpx;
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-bar:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 6rpx rgba(10,132,255,0.06);
|
||||
}
|
||||
|
||||
.search-icon { color: var(--text-secondary); font-size:32rpx; }
|
||||
.search-input { flex:1; font-size:32rpx; color:var(--text-primary); background:transparent }
|
||||
.search-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.search-cancel {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: var(--radius-small);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-cancel:active {
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.cancel-text {
|
||||
font-size: 32rpx;
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 🎨 现代化会话列表 */
|
||||
.conversations-section {
|
||||
background: transparent;
|
||||
border-radius: var(--radius-large) var(--radius-large) 0 0;
|
||||
margin-top: 16rpx;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding-bottom: 200rpx; /* 保留空间给底部操作栏,避免白条 */
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.03);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.conversation-item:active { background: rgba(255,255,255,0.01); transform: scale(0.995); }
|
||||
|
||||
.conversation-item:last-child { border-bottom: none; }
|
||||
|
||||
/* 🎨 现代化头像设计 */
|
||||
.conversation-avatar { position: relative; margin-right: 28rpx; }
|
||||
.avatar-image { width:108rpx; height:108rpx; border-radius:54rpx; box-shadow: var(--shadow-light); border: 2rpx solid rgba(255,255,255,0.03); }
|
||||
.avatar-placeholder { width:108rpx; height:108rpx; border-radius:54rpx; background: linear-gradient(135deg, #151516 0%, #0F0F11 100%); display:flex; align-items:center; justify-content:center; box-shadow: var(--shadow-light); }
|
||||
.avatar-text { font-size:40rpx; font-weight:600; color:var(--text-primary); }
|
||||
|
||||
/* 在线状态角标(右下角小圆点) */
|
||||
.presence-indicator {
|
||||
position: absolute;
|
||||
right: 2rpx;
|
||||
bottom: 2rpx;
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid var(--surface-color);
|
||||
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.35);
|
||||
z-index: 3;
|
||||
}
|
||||
.presence-indicator.online { background: #2ECC71; }
|
||||
.presence-indicator.offline { background: #9E9E9E; opacity: 0.9; }
|
||||
.theme-light .presence-indicator { border-color: var(--surface-color); }
|
||||
|
||||
/* 🎨 现代化未读徽章 */
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
right: -6rpx;
|
||||
min-width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E8E 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3rpx solid var(--surface-color);
|
||||
box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.6);
|
||||
z-index: 2;
|
||||
}
|
||||
.unread-count { font-size:22rpx; font-weight:700; color:white }
|
||||
|
||||
/* 🎨 现代化会话信息 */
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.conversation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.conversation-name {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-size: 34rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
/* 允许在 flex 容器内正确触发省略号 */
|
||||
min-width: 0;
|
||||
margin-right: 16rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.conversation-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
/* 保留时间+静音的显示空间,避免被名称挤没 */
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 0;
|
||||
min-width: 120rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.conversation-time {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
/* 避免被压缩隐藏 */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mute-icon {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 🎨 现代化会话预览 */
|
||||
.conversation-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.last-message {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
color: var(--text-secondary);
|
||||
font-size: 20rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.status-indicator.read {
|
||||
color: var(--primary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 🎨 现代化空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64rpx;
|
||||
color: rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 📱 响应式设计 - 适配不同屏幕尺寸 */
|
||||
@media screen and (max-width: 375px) {
|
||||
.conversation-item {
|
||||
padding: 24rpx 28rpx;
|
||||
}
|
||||
|
||||
.avatar-image, .avatar-placeholder {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.conversation-name {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.last-message {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 414px) {
|
||||
.conversation-item {
|
||||
padding: 32rpx 36rpx;
|
||||
}
|
||||
|
||||
.avatar-image, .avatar-placeholder {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
}
|
||||
|
||||
.conversation-name {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.last-message {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎨 会话项增强功能 */
|
||||
.conversation-item.pinned {
|
||||
background: linear-gradient(90deg, rgba(0, 122, 255, 0.05) 0%, transparent 100%);
|
||||
border-left: 4rpx solid var(--primary-color);
|
||||
}
|
||||
|
||||
.conversation-item.muted .conversation-name {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.conversation-item.muted .last-message {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 🎨 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid var(--border-color);
|
||||
border-top: 4rpx solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 🎨 搜索结果高亮 */
|
||||
.search-highlight {
|
||||
background: linear-gradient(135deg, rgba(255, 235, 59, 0.3) 0%, rgba(255, 193, 7, 0.3) 100%);
|
||||
border-radius: 4rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue