miniprogramme/pages/message/chat/chat.wxml
2025-09-12 16:08:17 +08:00

412 lines
No EOL
18 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--聊天页面-->
<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>