upload project

This commit is contained in:
unknown 2025-12-27 17:16:03 +08:00
commit 06961cae04
422 changed files with 110626 additions and 0 deletions

View file

@ -0,0 +1,344 @@
<!--聊天页面-->
<page-meta page-style="overflow: hidden;" />
<view class="chat-container theme-dark {{showEmojiPanel ? 'with-emoji' : ''}}"
style="height: {{screenHeight + 'px'}}; bottom: 23px;">
<!-- 加载状态指示器 -->
<view wx:if="{{isLoading}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">正在加载聊天...</text>
</view>
<!-- 消息列表 -->
<scroll-view
class="message-list"
style="bottom: {{inputAreaHeight + keyboardHeight + emojiPanelHeight}}px;"
scroll-y="{{true}}"
scroll-top="{{scrollTop}}"
scroll-into-view="{{scrollIntoView}}"
enhanced="{{true}}"
show-scrollbar="{{false}}"
bindscrolltoupper="loadMoreMessages"
bindscroll="onScroll"
bindtap="hideInputMethod"
upper-threshold="50"
>
<!-- 🔥 顶部加载更多提示 - 只在有消息且有更多消息时显示 -->
<view wx:if="{{hasMore && messages.length > 5}}" 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}}" class="self">
<view class="message-body ">
<!-- 自己的消息不显示昵称 -->
<!-- 单行(右侧):气泡内显示时间/状态 -->
<view class="message-line self">
<view class="message-content {{item.msgType}} {{item.isRecalled ? 'recalled' : ''}}"
bindlongpress="showMessageMenu" bindtap="hideInputMethod" 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="widthFix"
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}}" objectFit="cover" controls>
</video>
<view wx:elif="{{item.msgType === 'location'}}" class="location-content" bindtap="showLocation"
data-message="{{item}}">
<image class="location-icon" src="/images/loca.svg" mode="aspectFit"></image>
<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}}">
<image class="file-icon" src="/images/download.svg" mode="aspectFit"></image>
<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>
<!-- 气泡内时间与状态(撤回消息不显示) -->
<!-- deliveryStatus 使用 NIM SDK V2 枚举值: 0=未知, 1=成功, 2=失败, 3=发送中 -->
<view wx:if="{{item.msgType !== 'system' && !item.isRecalled}}" class="bubble-meta">
<text class="bubble-time">{{item.bubbleTime}}</text>
<text wx:if="{{item.deliveryStatus === 1}}" class="meta-status meta-delivered">✓</text>
<text wx:elif="{{item.deliveryStatus === 3}}" class="meta-status meta-sending">●</text>
<text wx:elif="{{item.deliveryStatus === 2}}" class="meta-status meta-failed">!</text>
</view>
</view>
</view>
<!-- 发送失败的"重发"操作(仅自己消息显示,出现在气泡下方靠右) -->
<!-- deliveryStatus === 2 表示失败 (V2NIM_MESSAGE_SENDING_STATE_FAILED) -->
<view wx:if="{{item.isSelf && item.deliveryStatus === 2 && !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" bindtap="onSelfAvatarTap" data-user-id="{{userInfo.user.customId}}">
<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 class="other">
<view class="avatar" bindtap="onPeerAvatarTap" data-sender-id="{{item.senderId || item.senderCustomId}}">
<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" bindtap="hideInputMethod" 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="widthFix"
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}}">
<image class="location-icon" src="/images/loca.svg" mode="aspectFit"></image>
<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}}">
<image class="file-icon" src="/images/download.svg" mode="aspectFit"></image>
<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' && !item.isRecalled}}" 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="bottom: {{keyboardHeight}}px; padding-bottom: {{safeAreaBottom}}px;">
<view class="input-row">
<!-- 语音按钮 -->
<view class="tool-btn" bindtap="toggleInputType">
<image class="{{inputType === 'text' ? 'icon-mic' : 'icon-keyboard'}}"
src="{{inputType === 'text' ? '/images/emoji/s-input.svg' : '/images/emoji/f-message.svg'}}"
mode="aspectFit" />
</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" bindkeyboardheightchange="onKeyboardHeightChange"
focus="{{inputFocus}}" maxlength="500" confirm-type="send"
adjust-position="{{false}}" hold-keyboard="{{true}}" auto-height="{{true}}" show-confirm-bar="{{false}}"
fixed="{{false}}" cursor-spacing="24" disable-default-padding="{{true}}" />
</view>
<!-- 语音录制(微信风格:按住说话,上滑取消) -->
<view wx:else class="voice-input-wrapper">
<button class="voice-btn {{recording ? 'recording' : ''}}" bindtouchstart="onVoiceTouchStart"
bindtouchmove="onVoiceTouchMove" bindtouchend="onVoiceTouchEnd" bindtouchcancel="onVoiceTouchCancel">
<image class="inline-icon" src="/images/emoji/s-input.svg" mode="aspectFit" />
按住 说话
</button>
</view>
<!-- 表情按钮 -->
<view class="tool-btn" bindtap="toggleEmojiPanel">
<image class="icon-emoji" src="/images/emoji/m-emoji.svg" mode="aspectFit" />
</view>
<!-- 更多按钮 -->
<view wx:if="{{inputText.length === 0}}" class="tool-btn" bindtap="toggleMorePanel">
<image class="icon-more" src="/images/emoji/add-circle.svg" mode="aspectFit" />
</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">
<image class="more-icon" src="/images/cam.svg" mode="aspectFit"></image>
<text class="more-text">拍照/录像</text>
</view>
<view class="more-item" bindtap="chooseImageFromAlbum">
<image class="more-icon" src="/images/Album.svg" mode="aspectFit"></image>
<text class="more-text">相册</text>
</view>
<view class="more-item" bindtap="selectFile">
<image class="more-icon" src="/images/download.svg" mode="aspectFit"></image>
<text class="more-text">文件</text>
</view>
<view class="more-item" bindtap="selectLocation">
<image class="more-icon" src="/images/loca.svg" mode="aspectFit"></image>
<text class="more-text">位置</text>
</view>
</view>
</view>
</view>
<!-- 录音提示 -->
<view wx:if="{{recording}}" class="recording-tips">
<text wx:if="{{!voiceCancel}}">松开 结束, 上滑 取消</text>
<text wx:else>松开手指,取消发送</text>
</view>
</view>