|
|
@@ -1,332 +1,1439 @@
|
|
|
-<template>
|
|
|
- <view class="message-page" :class="screenMode">
|
|
|
-
|
|
|
- <!-- 顶部返回 -->
|
|
|
- <!-- <view class="nav-header" v-if="screenMode != 'heng'">
|
|
|
- <view class="back-btn" @click="back"><image src="/static/icon/left.png"></image> 返回</view>
|
|
|
- </view> -->
|
|
|
-
|
|
|
- <!-- 消息列表 -->
|
|
|
- <view class="message-list">
|
|
|
- <view class="message-item ">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration"> <text>你好啊</text></view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- <!-- <view class="message-item right">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration"> <text>注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体注意身体</text></view>
|
|
|
- </view>
|
|
|
- </view> -->
|
|
|
-
|
|
|
- <view class="message-item ">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration"><image src="/static/icon/tingtong.png"></image> <text>通话时长 00:25</text></view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 通话记录 -->
|
|
|
- <view class="message-item right">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration"><image src="/static/icon/tingtong.png"></image> <text>通话时长 00:25</text></view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 语音消息 -->
|
|
|
- <view class="message-item left">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration"> <image src="/static/icon/guangbo.png"></image> <text>24"</text></view>
|
|
|
- <text class="time">17:25</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 语音消息 -->
|
|
|
- <view class="message-item right">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration"><image src="/static/icon/yinbo.png"></image> <text>00:15</text></view>
|
|
|
- <text class="time">18:28</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 语音消息带录音点 -->
|
|
|
- <view class="message-item left">
|
|
|
- <image class="avatar" src="/static/logo.png"></image>
|
|
|
- <view class="voice-message">
|
|
|
- <view class="voice-duration">
|
|
|
- <image src="/static/icon/guangbo.png"></image>
|
|
|
- <text>16"</text>
|
|
|
- <view class="recording-dot"></view>
|
|
|
- </view>
|
|
|
- <text class="time">17:25</text>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 底部操作栏 -->
|
|
|
- <view class="footer">
|
|
|
- <view class="input-box">按住 说话</view>
|
|
|
- <view class="emoji-btn"><image src="/static/icon/emjoy.png"></image></view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script>
|
|
|
-export default {
|
|
|
- data() {
|
|
|
- return {
|
|
|
- screenMode: 'heng',
|
|
|
- }
|
|
|
- },
|
|
|
- onLoad(options) {
|
|
|
- uni.getSystemInfo({
|
|
|
- success: (res) => {
|
|
|
- this.screenMode = res.windowWidth > res.windowHeight ? 'heng' : 'shu'
|
|
|
- }
|
|
|
- })
|
|
|
- // 获取路由参数中的角色信息
|
|
|
- const role = options.role
|
|
|
- console.log(role)
|
|
|
- if (!role) {
|
|
|
- uni.showToast({
|
|
|
- title: '缺少角色信息',
|
|
|
- icon: 'none'
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // 这里可以根据角色获取对应的消息列表
|
|
|
- // this.getMessageList(role)
|
|
|
- },
|
|
|
- methods: {
|
|
|
- back() {
|
|
|
- uni.navigateBack({
|
|
|
- delta: 1
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped lang="less">
|
|
|
-.heng {
|
|
|
- min-height: 100vh;
|
|
|
- background: #f5f5f5;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-
|
|
|
- .message-list {
|
|
|
- flex: 1;
|
|
|
- padding: 5rpx;
|
|
|
- background: #fff;
|
|
|
- }
|
|
|
- .message-item {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- margin: 15rpx 20rpx;
|
|
|
- }
|
|
|
- .avatar {
|
|
|
- width: 30rpx;
|
|
|
- height: 30rpx;
|
|
|
- border-radius: 2rpx;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
- .voice-message {
|
|
|
- position: relative;
|
|
|
- display: flex;
|
|
|
- align-items: flex-end;
|
|
|
- }
|
|
|
- .voice-duration {
|
|
|
- background: #a4b3d4;
|
|
|
- padding: 6rpx;
|
|
|
- border-radius: 4rpx;
|
|
|
- color: #fff;
|
|
|
- font-size: 12rpx;
|
|
|
- position: relative;
|
|
|
- min-width: 50rpx;
|
|
|
- margin: 0 10rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- }
|
|
|
- .voice-duration image {
|
|
|
- width: 18rpx;
|
|
|
- height: 18rpx;
|
|
|
- }
|
|
|
- .voice-duration text {
|
|
|
- margin: 0 5rpx;
|
|
|
- }
|
|
|
- .right {
|
|
|
- flex-direction: row-reverse;
|
|
|
- }
|
|
|
- .right .avatar{
|
|
|
- border-radius: 50%;
|
|
|
- }
|
|
|
- .right .voice-message {
|
|
|
- flex-direction: row-reverse;
|
|
|
- }
|
|
|
- .right .voice-duration {
|
|
|
- background: #e9e9e9;
|
|
|
- color: #5e5e5e;
|
|
|
-
|
|
|
- }
|
|
|
- .time {
|
|
|
- font-size: 12rpx;
|
|
|
- color: #999;
|
|
|
- display: block;
|
|
|
- }
|
|
|
- .recording-dot {
|
|
|
- width: 5rpx;
|
|
|
- height: 5rpx;
|
|
|
- background: #eb6100;
|
|
|
- border-radius: 50%;
|
|
|
- position: absolute;
|
|
|
- right: 3rpx;
|
|
|
- top: 3rpx;
|
|
|
- }
|
|
|
- .footer {
|
|
|
- padding: 10rpx 35rpx;
|
|
|
- background: #f5f5f5;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- border-top: 1rpx solid #eee;
|
|
|
- font-size: 14rpx;
|
|
|
- }
|
|
|
- .input-box {
|
|
|
- flex: 1;
|
|
|
- background: #ffffff;
|
|
|
- height: 30rpx;
|
|
|
- line-height: 30rpx;
|
|
|
- text-align: center;
|
|
|
- border-radius: 4rpx;
|
|
|
- margin-right: 10rpx;
|
|
|
- color: #666;
|
|
|
- border: 1rpx solid #eee;
|
|
|
- }
|
|
|
- .emoji-btn image {
|
|
|
- width: 20rpx;
|
|
|
- height: 20rpx;
|
|
|
- }
|
|
|
-}
|
|
|
-.shu{
|
|
|
- min-height: 100vh;
|
|
|
- background: #f5f5f5;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- .nav-header {
|
|
|
- padding: 20rpx;
|
|
|
- background: #fff;
|
|
|
- border-bottom: 1rpx solid #eee;
|
|
|
- }
|
|
|
- .back-btn {
|
|
|
- width: 100rpx;
|
|
|
- font-size: 28rpx;
|
|
|
- text-align: center;
|
|
|
- color: #333;
|
|
|
- border-radius: 5rpx;
|
|
|
- padding: 10rpx 20rpx;
|
|
|
- border: 1px solid #eee;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10rpx;
|
|
|
- }
|
|
|
- .back-btn image {
|
|
|
- width: 25rpx;
|
|
|
- height: 25rpx;
|
|
|
- }
|
|
|
- .message-list {
|
|
|
- flex: 1;
|
|
|
- padding: 20rpx;
|
|
|
- background: #fff;
|
|
|
- }
|
|
|
- .message-item {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- margin: 30rpx 0;
|
|
|
- }
|
|
|
- .avatar {
|
|
|
- width: 80rpx;
|
|
|
- height: 80rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
- .voice-message {
|
|
|
- position: relative;
|
|
|
- display: flex;
|
|
|
- align-items: flex-end;
|
|
|
- }
|
|
|
- .voice-duration {
|
|
|
- background: #a4b3d4;
|
|
|
- padding: 20rpx 20rpx;
|
|
|
- border-radius: 8rpx;
|
|
|
- color: #fff;
|
|
|
- font-size: 28rpx;
|
|
|
- position: relative;
|
|
|
- min-width: 100rpx;
|
|
|
- margin: 0 20rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- }
|
|
|
- .voice-duration image{
|
|
|
- width: 40rpx;
|
|
|
- height: 40rpx;
|
|
|
- }
|
|
|
- .voice-duration text {
|
|
|
- margin: 0 10rpx;
|
|
|
- }
|
|
|
- .right {
|
|
|
- flex-direction: row-reverse;
|
|
|
- }
|
|
|
- .right .avatar{
|
|
|
- border-radius: 50%;
|
|
|
- }
|
|
|
- .right .voice-message {
|
|
|
- flex-direction: row-reverse;
|
|
|
- }
|
|
|
- .right .voice-duration {
|
|
|
- background: #e9e9e9;
|
|
|
- color: #5e5e5e;
|
|
|
-
|
|
|
- }
|
|
|
- .time {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #999;
|
|
|
- display: block;
|
|
|
- }
|
|
|
- .recording-dot {
|
|
|
- width: 16rpx;
|
|
|
- height: 16rpx;
|
|
|
- background: #eb6100;
|
|
|
- border-radius: 50%;
|
|
|
- position: absolute;
|
|
|
- right: 10rpx;
|
|
|
- top: 10rpx;
|
|
|
- }
|
|
|
- .footer {
|
|
|
- padding: 20rpx 40rpx;
|
|
|
- background: #f5f5f5;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- border-top: 1rpx solid #eee;
|
|
|
- }
|
|
|
- .input-box {
|
|
|
- flex: 1;
|
|
|
- background: #ffffff;
|
|
|
- height: 80rpx;
|
|
|
- line-height: 80rpx;
|
|
|
- text-align: center;
|
|
|
- border-radius: 8rpx;
|
|
|
- margin-right: 20rpx;
|
|
|
- color: #666;
|
|
|
- border: 1px solid #eee;
|
|
|
- }
|
|
|
- .emoji-btn image{
|
|
|
- width: 50rpx;
|
|
|
- height: 50rpx;
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|
|
|
+<template>
|
|
|
+ <view class="message-page">
|
|
|
+ <!-- 消息列表 -->
|
|
|
+ <scroll-view class="message-list" scroll-y :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
|
|
|
+ @touchstart="handleListTouchStart" @touchmove="handleListTouchMove" @touchend="handleListTouchEnd">
|
|
|
+ <view class="message-item" :class="message.direction" v-for="(message, index) in messages" :key="index"
|
|
|
+ :id="'msg-' + index">
|
|
|
+ <!-- 头像+时间区域 -->
|
|
|
+ <view class="avatar-section">
|
|
|
+ <image class="avatar" src="/static/logo.png"></image>
|
|
|
+ <text class="msg-time">{{ message.time }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 内容区域 -->
|
|
|
+ <view class="content-section">
|
|
|
+ <!-- 部门/班级 | 姓名 -->
|
|
|
+ <view class="user-info">{{ message.department }} | {{ message.name }}</view>
|
|
|
+
|
|
|
+ <!-- 消息内容 -->
|
|
|
+ <view class="message-content">
|
|
|
+ <!-- 文字消息 -->
|
|
|
+ <view v-if="message.type === 'text'" class="text-message-wrapper">
|
|
|
+ <!-- 接收的消息:内容在左,按钮在右 -->
|
|
|
+ <template v-if="message.direction === 'left'">
|
|
|
+ <view class="text-message">
|
|
|
+ <text>{{ message.content }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="message.needReceipt" class="inline-receipt-button"
|
|
|
+ :class="message.receiptStatus">
|
|
|
+ {{ message.receiptStatus === 'read' ? '已读' : '确认阅读' }}
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ <!-- 发送的消息:按钮在左,内容在右 -->
|
|
|
+ <template v-else>
|
|
|
+ <!-- 带回执按钮(仅在1分钟内显示) -->
|
|
|
+ <view v-if="message.showReceiptToggle" class="receipt-toggle-wrapper">
|
|
|
+ <SsOnoffButton name="needReceipt" label="带回执" :value="1"
|
|
|
+ :modelValue="message.needReceipt ? 1 : 0"
|
|
|
+ @update:modelValue="toggleReceipt(index, $event)" />
|
|
|
+ </view>
|
|
|
+ <!-- 阅读情况按钮 -->
|
|
|
+ <view v-if="message.needReceipt && !message.showReceiptToggle" class="inline-receipt-button"
|
|
|
+ :class="message.receiptStatus">
|
|
|
+ 阅读情况
|
|
|
+ </view>
|
|
|
+ <!-- 文字内容 -->
|
|
|
+ <view class="text-message">
|
|
|
+ <text>{{ message.content }}</text>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 通话记录 -->
|
|
|
+ <view v-if="message.type === 'call'" class="call-message">
|
|
|
+ <image src="/static/icon/tingtong.png"></image>
|
|
|
+ <text>通话时长 {{ message.duration }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 语音消息 -->
|
|
|
+ <view
|
|
|
+ v-if="message.type === 'voice'"
|
|
|
+ class="voice-message"
|
|
|
+ :style="getVoiceBubbleStyle(message)"
|
|
|
+ @click.stop="toggleVoicePlayback(message, index)"
|
|
|
+ >
|
|
|
+ <image
|
|
|
+ :src="message.direction === 'left' ? '/static/icon/guangbo.png' : '/static/icon/yinbo.png'">
|
|
|
+ </image>
|
|
|
+ <text>{{ message.duration }}" {{ message.voicePreview }}</text>
|
|
|
+ <view v-if="message.isRecording" class="recording-dot"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 图片消息 -->
|
|
|
+ <view v-if="message.type === 'image'" class="image-message" @click.stop="openImagePreview(message)">
|
|
|
+ <image class="image-preview" :src="message.imageUrl || '/static/logo.png'" mode="aspectFill"></image>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 文件消息 -->
|
|
|
+ <view v-if="message.type === 'file'" class="file-message" @click.stop="openFilePreview(message)">
|
|
|
+ <Icon lib="base" name="icon-file" size="39" :color="iconColor" />
|
|
|
+ <text class="file-name">{{ message.fileName }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 视频消息 -->
|
|
|
+ <view v-if="message.type === 'video'" class="video-message" @click.stop="openVideoPreview(message, index)">
|
|
|
+ <video
|
|
|
+ class="video-cover"
|
|
|
+ :src="message.videoUrl"
|
|
|
+ muted
|
|
|
+ :controls="false"
|
|
|
+ :show-center-play-btn="false"
|
|
|
+ :enable-progress-gesture="false"
|
|
|
+ object-fit="cover"
|
|
|
+ ></video>
|
|
|
+ <view class="video-play">
|
|
|
+ <Icon lib="base" name="icon-vid" size="40" color="#ffffff" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 语音转文字区域(仅语音消息且有转文字内容时显示) -->
|
|
|
+ <view v-if="message.type === 'voice' && message.voiceText" class="voice-text-section">
|
|
|
+ <!-- 接收的消息:转写在左,按钮在右 -->
|
|
|
+ <template v-if="message.direction === 'left'">
|
|
|
+ <view class="voice-text-content">{{ message.voiceText }}</view>
|
|
|
+ <view v-if="message.needReceipt" class="receipt-button" :class="message.receiptStatus">
|
|
|
+ {{ message.receiptStatus === 'read' ? '已读' : '确认阅读' }}
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ <!-- 发送的消息:按钮在左,转写在右 -->
|
|
|
+ <template v-else>
|
|
|
+ <view v-if="message.needReceipt" class="receipt-button" :class="message.receiptStatus">
|
|
|
+ 阅读情况
|
|
|
+ </view>
|
|
|
+ <view class="voice-text-content">{{ message.voiceText }}</view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <!-- 底部操作栏 -->
|
|
|
+ <view class="footer" :class="{ active: showMorePanel || showEmojiPanel }" @click.stop>
|
|
|
+ <!-- 左侧:语音/键盘切换 -->
|
|
|
+ <view class="tool-btn" @click="toggleInputMode">
|
|
|
+ <Icon lib="base" :name="inputMode === 'voice' ? 'icon-txt' : 'icon-spk'" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 中间:输入框 / 按住说话 -->
|
|
|
+ <view class="center-area">
|
|
|
+ <view
|
|
|
+ v-if="inputMode === 'voice'"
|
|
|
+ class="press-to-talk"
|
|
|
+ @longpress="handlePressToTalkStart"
|
|
|
+ @touchmove.stop.prevent="handlePressToTalkMove"
|
|
|
+ @touchend.stop.prevent="handlePressToTalkEnd"
|
|
|
+ @touchcancel.stop.prevent="handlePressToTalkCancel"
|
|
|
+ >
|
|
|
+ <Icon lib="base" name="icon-aud-bold" size="36" :color="iconColor" />
|
|
|
+ <text class="press-text">长按说话</text>
|
|
|
+ </view>
|
|
|
+ <textarea
|
|
|
+ v-else
|
|
|
+ class="text-input"
|
|
|
+ :class="{ capped: textLineCount >= 5 }"
|
|
|
+ v-model="draftText"
|
|
|
+ :focus="inputFocus"
|
|
|
+ auto-height
|
|
|
+ confirm-type="send"
|
|
|
+ @confirm="sendTextMessage"
|
|
|
+ maxlength="-1"
|
|
|
+ @linechange="handleTextLineChange"
|
|
|
+ @blur="inputFocus = false"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 右侧:表情 + 更多 -->
|
|
|
+ <view class="tool-btn" @click="toggleEmojiPanel">
|
|
|
+ <Icon lib="base" name="icon-emoji" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+ <view class="tool-btn" @click="toggleMorePanel">
|
|
|
+ <Icon lib="base" name="icon-add" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部扩展面板(更多/表情) -->
|
|
|
+ <view class="bottom-panel" :class="{ open: showMorePanel || showEmojiPanel }" @click.stop>
|
|
|
+ <view v-show="showMorePanel" class="more-panel">
|
|
|
+ <view class="more-grid">
|
|
|
+ <view class="more-item" @click="handleMoreAction('file')">
|
|
|
+ <view class="more-icon">
|
|
|
+ <Icon lib="base" name="icon-file" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+ <text class="more-text">文件</text>
|
|
|
+ </view>
|
|
|
+ <view class="more-item" @click="handleMoreAction('image')">
|
|
|
+ <view class="more-icon">
|
|
|
+ <Icon lib="base" name="icon-img" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+ <text class="more-text">照片</text>
|
|
|
+ </view>
|
|
|
+ <view class="more-item" @click="handleMoreAction('camera')">
|
|
|
+ <view class="more-icon">
|
|
|
+ <Icon lib="base" name="icon-vid-bold" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+ <text class="more-text">拍照</text>
|
|
|
+ </view>
|
|
|
+ <view class="more-item" @click="handleMoreAction('video')">
|
|
|
+ <view class="more-icon">
|
|
|
+ <Icon lib="base" name="icon-vid" size="36" :color="iconColor" />
|
|
|
+ </view>
|
|
|
+ <text class="more-text">视频留言</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-show="showEmojiPanel" class="emoji-panel">
|
|
|
+ <view class="emoji-grid">
|
|
|
+ <view class="emoji-item" v-for="(emoji, index) in emojiList" :key="index"
|
|
|
+ @click="insertEmoji(emoji)">
|
|
|
+ {{ emoji }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="showMediaPreview" class="media-preview-mask" @click="closeMediaPreview">
|
|
|
+ <view class="media-preview-content" :class="previewType === 'image' ? 'image-mode' : 'video-mode'" @click.stop="handlePreviewContentClick">
|
|
|
+ <swiper
|
|
|
+ v-if="previewType === 'image'"
|
|
|
+ class="media-preview-swiper"
|
|
|
+ :current="previewImageIndex"
|
|
|
+ @change="handleImageSwiperChange"
|
|
|
+ >
|
|
|
+ <swiper-item class="media-preview-swiper-item" v-for="(img, idx) in previewImageList" :key="idx">
|
|
|
+ <image class="media-preview-image fit-x" :src="img" mode="widthFix"></image>
|
|
|
+ </swiper-item>
|
|
|
+ </swiper>
|
|
|
+ <video
|
|
|
+ v-if="previewType === 'video'"
|
|
|
+ class="media-preview-video"
|
|
|
+ :src="previewSource"
|
|
|
+ autoplay
|
|
|
+ controls
|
|
|
+ object-fit="contain"
|
|
|
+ @loadedmetadata="handlePreviewVideoLoaded"
|
|
|
+ @error="handlePreviewVideoError"
|
|
|
+ ></video>
|
|
|
+ </view>
|
|
|
+ <view v-if="previewType === 'video'" class="media-preview-exit" @click.stop="closeMediaPreview">退出</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="showFileDialog" class="file-dialog-mask" @click="closeFileDialog">
|
|
|
+ <view class="file-dialog" @click.stop>
|
|
|
+ <view class="file-dialog-header">
|
|
|
+ <Icon lib="base" name="icon-file" size="44" :color="iconColor" />
|
|
|
+ <text class="file-dialog-name">{{ activeFileName }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="file-dialog-actions">
|
|
|
+ <view class="file-dialog-btn ghost" @click="closeFileDialog">取消</view>
|
|
|
+ <view class="file-dialog-btn" @click="downloadFileFromDialog">下载</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="isRecording" class="record-mask">
|
|
|
+ <view class="record-panel">
|
|
|
+ <view class="record-title">正在录音</view>
|
|
|
+ <view class="record-time">{{ recordSeconds }}s</view>
|
|
|
+ <view class="record-tip" :class="{ danger: isRecordCancel }">
|
|
|
+ {{ isRecordCancel ? '松开取消发送' : '手指上滑,取消发送' }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import SsOnoffButton from '@/components/SsOnoffButton/index.vue'
|
|
|
+import Icon from '@/components/icon/index.vue'
|
|
|
+import { EMOJI_LIST } from '@/constants/emoji'
|
|
|
+import { collectImageUrls, getInitialParentMessages, createImageMessage } from '@/utils/parent-message-factory'
|
|
|
+import {
|
|
|
+ buildTextOutgoingMessage,
|
|
|
+ buildVoiceOutgoingMessage,
|
|
|
+ pickFileOutgoingMessage,
|
|
|
+ pickImageOutgoingMessages,
|
|
|
+ pickVideoOutgoingMessage,
|
|
|
+ shootImageOutgoingMessage
|
|
|
+} from '@/utils/parent-message-send'
|
|
|
+
|
|
|
+const RECEIPT_TOGGLE_TTL_MS = 60 * 1000
|
|
|
+
|
|
|
+ export default {
|
|
|
+ components: {
|
|
|
+ SsOnoffButton,
|
|
|
+ Icon
|
|
|
+ },
|
|
|
+ computed: {},
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ scrollTop: 0,
|
|
|
+ scrollIntoView: '',
|
|
|
+ inputMode: 'voice', // voice | text
|
|
|
+ inputFocus: false,
|
|
|
+ draftText: '',
|
|
|
+ iconColor: '#575d6d',
|
|
|
+ showEmojiPanel: false,
|
|
|
+ showMorePanel: false,
|
|
|
+ emojiList: EMOJI_LIST,
|
|
|
+ showMediaPreview: false,
|
|
|
+ previewType: '',
|
|
|
+ previewSource: '',
|
|
|
+ activeVideoIndex: -1,
|
|
|
+ previewImageList: [],
|
|
|
+ previewImageIndex: 0,
|
|
|
+ showFileDialog: false,
|
|
|
+ activeFileName: '',
|
|
|
+ activeFileUrl: '',
|
|
|
+ receiptToggleTimers: {},
|
|
|
+ currentUserMeta: {
|
|
|
+ direction: 'right',
|
|
|
+ department: '家长',
|
|
|
+ name: '我'
|
|
|
+ },
|
|
|
+ textLineCount: 1,
|
|
|
+ listTouchMoved: false,
|
|
|
+ touchStartX: 0,
|
|
|
+ touchStartY: 0,
|
|
|
+ recorderManager: null,
|
|
|
+ isRecording: false,
|
|
|
+ isRecordCancel: false,
|
|
|
+ recordSeconds: 0,
|
|
|
+ recordStartY: 0,
|
|
|
+ recordTickTimer: null,
|
|
|
+ recordGuardTimer: null,
|
|
|
+ audioPlayer: null,
|
|
|
+ playingVoiceIndex: -1,
|
|
|
+ // 消息列表数据
|
|
|
+ messages: getInitialParentMessages()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad(options) {
|
|
|
+ // 获取路由参数中的角色信息
|
|
|
+ const role = options.role
|
|
|
+ console.log(role)
|
|
|
+ if (!role) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '缺少角色信息',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 这里可以根据角色获取对应的消息列表
|
|
|
+ // this.getMessageList(role)
|
|
|
+ },
|
|
|
+ onReady() {
|
|
|
+ // 页面渲染完成后,滚动到最新消息
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.scrollToBottom()
|
|
|
+ this.initReceiptToggleTimers()
|
|
|
+ this.initRecorder()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ onUnload() {
|
|
|
+ this.stopVoicePlayback()
|
|
|
+ if (this.audioPlayer) {
|
|
|
+ this.audioPlayer.destroy()
|
|
|
+ this.audioPlayer = null
|
|
|
+ }
|
|
|
+ this.cleanupRecorder()
|
|
|
+ this.clearReceiptToggleTimers()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ getVoiceBubbleStyle(message) {
|
|
|
+ if (!message || !message.isPlaying) return {}
|
|
|
+ const progress = Math.max(0, Math.min(100, Number(message.playProgress || 0)))
|
|
|
+ if (message.direction === 'right') {
|
|
|
+ return {
|
|
|
+ backgroundImage: `linear-gradient(to right, #7d89b1 ${progress}%, #eeeeee ${progress}%)`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ backgroundImage: `linear-gradient(to right, #eeeeee ${progress}%, #7d89b1 ${progress}%)`
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetVoiceItemState(index) {
|
|
|
+ if (index < 0 || !this.messages[index]) return
|
|
|
+ this.$set(this.messages[index], 'isPlaying', false)
|
|
|
+ this.$set(this.messages[index], 'playProgress', 0)
|
|
|
+ },
|
|
|
+ updateVoicePlayProgress() {
|
|
|
+ const index = this.playingVoiceIndex
|
|
|
+ if (index < 0 || !this.messages[index] || !this.audioPlayer) return
|
|
|
+ const duration = Number(this.audioPlayer.duration || 0)
|
|
|
+ const currentTime = Number(this.audioPlayer.currentTime || 0)
|
|
|
+ if (duration <= 0) return
|
|
|
+ const progress = Math.max(0, Math.min(100, (currentTime / duration) * 100))
|
|
|
+ this.$set(this.messages[index], 'playProgress', progress)
|
|
|
+ },
|
|
|
+ initAudioPlayer() {
|
|
|
+ if (this.audioPlayer || !uni.createInnerAudioContext) return
|
|
|
+ this.audioPlayer = uni.createInnerAudioContext()
|
|
|
+ this.audioPlayer.onTimeUpdate(() => {
|
|
|
+ this.updateVoicePlayProgress()
|
|
|
+ })
|
|
|
+ this.audioPlayer.onEnded(() => {
|
|
|
+ this.resetVoiceItemState(this.playingVoiceIndex)
|
|
|
+ this.playingVoiceIndex = -1
|
|
|
+ })
|
|
|
+ this.audioPlayer.onStop(() => {
|
|
|
+ this.resetVoiceItemState(this.playingVoiceIndex)
|
|
|
+ this.playingVoiceIndex = -1
|
|
|
+ })
|
|
|
+ this.audioPlayer.onError(() => {
|
|
|
+ this.resetVoiceItemState(this.playingVoiceIndex)
|
|
|
+ this.playingVoiceIndex = -1
|
|
|
+ uni.showToast({ title: '语音播放失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ stopVoicePlayback() {
|
|
|
+ if (this.audioPlayer) {
|
|
|
+ this.audioPlayer.stop()
|
|
|
+ }
|
|
|
+ this.resetVoiceItemState(this.playingVoiceIndex)
|
|
|
+ this.playingVoiceIndex = -1
|
|
|
+ },
|
|
|
+ toggleVoicePlayback(message, index) {
|
|
|
+ if (!message || message.type !== 'voice') return
|
|
|
+ if (!message.audioUrl) {
|
|
|
+ uni.showToast({ title: '该语音暂无音频文件', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.initAudioPlayer()
|
|
|
+ if (!this.audioPlayer) {
|
|
|
+ uni.showToast({ title: '当前环境不支持播放', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.playingVoiceIndex === index) {
|
|
|
+ this.stopVoicePlayback()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.playingVoiceIndex > -1) {
|
|
|
+ this.audioPlayer.stop()
|
|
|
+ this.resetVoiceItemState(this.playingVoiceIndex)
|
|
|
+ }
|
|
|
+ this.playingVoiceIndex = index
|
|
|
+ this.$set(this.messages[index], 'isPlaying', true)
|
|
|
+ this.$set(this.messages[index], 'playProgress', 0)
|
|
|
+ this.audioPlayer.src = message.audioUrl
|
|
|
+ this.audioPlayer.play()
|
|
|
+ },
|
|
|
+ initRecorder() {
|
|
|
+ if (!uni.getRecorderManager) return
|
|
|
+ this.recorderManager = uni.getRecorderManager()
|
|
|
+ this.recorderManager.onStop((res) => {
|
|
|
+ const canceled = this.isRecordCancel
|
|
|
+ const durationMs = Number(res && res.duration ? res.duration : 0)
|
|
|
+ const durationSeconds = Math.max(1, Math.round(durationMs / 1000) || this.recordSeconds)
|
|
|
+ this.clearRecordTimers()
|
|
|
+ this.isRecording = false
|
|
|
+ this.isRecordCancel = false
|
|
|
+ this.recordStartY = 0
|
|
|
+
|
|
|
+ if (canceled) {
|
|
|
+ uni.showToast({ title: '已取消发送', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!res || !res.tempFilePath || durationSeconds < 1) {
|
|
|
+ uni.showToast({ title: '录音时间太短', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const voiceMessage = buildVoiceOutgoingMessage({
|
|
|
+ durationSeconds,
|
|
|
+ audioUrl: res.tempFilePath,
|
|
|
+ voiceText: ''
|
|
|
+ }, this.currentUserMeta)
|
|
|
+ this.appendMessage(voiceMessage)
|
|
|
+ })
|
|
|
+ this.recorderManager.onError(() => {
|
|
|
+ this.clearRecordTimers()
|
|
|
+ this.isRecording = false
|
|
|
+ this.isRecordCancel = false
|
|
|
+ uni.showToast({ title: '录音失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ clearRecordTimers() {
|
|
|
+ if (this.recordTickTimer) {
|
|
|
+ clearInterval(this.recordTickTimer)
|
|
|
+ this.recordTickTimer = null
|
|
|
+ }
|
|
|
+ if (this.recordGuardTimer) {
|
|
|
+ clearTimeout(this.recordGuardTimer)
|
|
|
+ this.recordGuardTimer = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ cleanupRecorder() {
|
|
|
+ this.clearRecordTimers()
|
|
|
+ if (this.isRecording && this.recorderManager) {
|
|
|
+ this.isRecordCancel = true
|
|
|
+ try {
|
|
|
+ this.recorderManager.stop()
|
|
|
+ } catch (error) {
|
|
|
+ // noop
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.isRecording = false
|
|
|
+ this.recordSeconds = 0
|
|
|
+ },
|
|
|
+ handlePressToTalkStart(event) {
|
|
|
+ if (this.isRecording) return
|
|
|
+ if (!this.recorderManager) {
|
|
|
+ this.initRecorder()
|
|
|
+ }
|
|
|
+ if (!this.recorderManager) {
|
|
|
+ uni.showToast({ title: '当前环境不支持录音', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const touch = event && event.touches && event.touches[0]
|
|
|
+ this.recordStartY = touch ? touch.clientY : 0
|
|
|
+ this.recordSeconds = 0
|
|
|
+ this.isRecordCancel = false
|
|
|
+ this.isRecording = true
|
|
|
+ this.clearRecordTimers()
|
|
|
+
|
|
|
+ this.recordTickTimer = setInterval(() => {
|
|
|
+ this.recordSeconds = Math.min(60, this.recordSeconds + 1)
|
|
|
+ }, 1000)
|
|
|
+ this.recordGuardTimer = setTimeout(() => {
|
|
|
+ if (!this.isRecording || !this.recorderManager) return
|
|
|
+ this.recorderManager.stop()
|
|
|
+ }, 60000)
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.recorderManager.start({
|
|
|
+ duration: 60000,
|
|
|
+ sampleRate: 16000,
|
|
|
+ numberOfChannels: 1,
|
|
|
+ encodeBitRate: 96000,
|
|
|
+ format: 'mp3'
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ this.clearRecordTimers()
|
|
|
+ this.isRecording = false
|
|
|
+ this.isRecordCancel = false
|
|
|
+ uni.showToast({ title: '录音启动失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handlePressToTalkMove(event) {
|
|
|
+ if (!this.isRecording) return
|
|
|
+ const touch = event && event.touches && event.touches[0]
|
|
|
+ if (!touch) return
|
|
|
+ const deltaY = this.recordStartY - touch.clientY
|
|
|
+ this.isRecordCancel = deltaY > 80
|
|
|
+ },
|
|
|
+ handlePressToTalkEnd() {
|
|
|
+ if (!this.isRecording || !this.recorderManager) return
|
|
|
+ this.recorderManager.stop()
|
|
|
+ },
|
|
|
+ handlePressToTalkCancel() {
|
|
|
+ if (!this.isRecording || !this.recorderManager) return
|
|
|
+ this.isRecordCancel = true
|
|
|
+ this.recorderManager.stop()
|
|
|
+ },
|
|
|
+ back() {
|
|
|
+ uni.navigateBack({
|
|
|
+ delta: 1
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 切换带回执状态
|
|
|
+ toggleReceipt(index, value) {
|
|
|
+ this.messages[index].needReceipt = value === 1 || value === '1'
|
|
|
+ },
|
|
|
+ initReceiptToggleTimers() {
|
|
|
+ this.messages.forEach((message, index) => {
|
|
|
+ if (message.showReceiptToggle) {
|
|
|
+ this.registerReceiptToggleTimer(index)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ registerReceiptToggleTimer(index) {
|
|
|
+ const message = this.messages[index]
|
|
|
+ if (!message || !message.showReceiptToggle) return
|
|
|
+
|
|
|
+ if (!message.receiptToggleCreatedAt) {
|
|
|
+ message.receiptToggleCreatedAt = Date.now()
|
|
|
+ }
|
|
|
+
|
|
|
+ const expireAt = message.receiptToggleCreatedAt + RECEIPT_TOGGLE_TTL_MS
|
|
|
+ const remain = expireAt - Date.now()
|
|
|
+
|
|
|
+ if (remain <= 0) {
|
|
|
+ message.showReceiptToggle = false
|
|
|
+ this.clearReceiptToggleTimer(index)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.clearReceiptToggleTimer(index)
|
|
|
+ this.receiptToggleTimers[index] = setTimeout(() => {
|
|
|
+ const target = this.messages[index]
|
|
|
+ if (target) {
|
|
|
+ target.showReceiptToggle = false
|
|
|
+ }
|
|
|
+ this.clearReceiptToggleTimer(index)
|
|
|
+ }, remain)
|
|
|
+ },
|
|
|
+ clearReceiptToggleTimer(index) {
|
|
|
+ const timerId = this.receiptToggleTimers[index]
|
|
|
+ if (timerId) {
|
|
|
+ clearTimeout(timerId)
|
|
|
+ delete this.receiptToggleTimers[index]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ clearReceiptToggleTimers() {
|
|
|
+ Object.keys(this.receiptToggleTimers).forEach((key) => {
|
|
|
+ this.clearReceiptToggleTimer(key)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 滚动到底部
|
|
|
+ scrollToBottom() {
|
|
|
+ const lastIndex = this.messages.length - 1
|
|
|
+ if (lastIndex >= 0) {
|
|
|
+ this.scrollIntoView = 'msg-' + lastIndex
|
|
|
+ }
|
|
|
+ },
|
|
|
+ toggleInputMode() {
|
|
|
+ if (this.inputMode === 'voice') {
|
|
|
+ this.inputMode = 'text'
|
|
|
+ this.showMorePanel = false
|
|
|
+ this.showEmojiPanel = false
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.inputFocus = true
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.inputMode = 'voice'
|
|
|
+ this.showMorePanel = false
|
|
|
+ this.showEmojiPanel = false
|
|
|
+ this.inputFocus = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ toggleEmojiPanel() {
|
|
|
+ const nextStatus = !this.showEmojiPanel
|
|
|
+ this.showEmojiPanel = nextStatus
|
|
|
+ if (nextStatus) {
|
|
|
+ this.inputMode = 'text'
|
|
|
+ this.showMorePanel = false
|
|
|
+ this.inputFocus = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ toggleMorePanel() {
|
|
|
+ const nextStatus = !this.showMorePanel
|
|
|
+ this.showMorePanel = nextStatus
|
|
|
+ if (nextStatus) {
|
|
|
+ this.inputMode = 'text'
|
|
|
+ this.showEmojiPanel = false
|
|
|
+ this.inputFocus = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handlePageClick() {
|
|
|
+ if (this.showMorePanel || this.showEmojiPanel) {
|
|
|
+ this.showMorePanel = false
|
|
|
+ this.showEmojiPanel = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ insertEmoji(emoji) {
|
|
|
+ this.inputMode = 'text'
|
|
|
+ this.draftText = `${this.draftText}${emoji}`
|
|
|
+ },
|
|
|
+ handleListTouchStart(event) {
|
|
|
+ const touch = event.touches && event.touches[0]
|
|
|
+ if (!touch) return
|
|
|
+ this.listTouchMoved = false
|
|
|
+ this.touchStartX = touch.clientX
|
|
|
+ this.touchStartY = touch.clientY
|
|
|
+ },
|
|
|
+ handleListTouchMove(event) {
|
|
|
+ const touch = event.touches && event.touches[0]
|
|
|
+ if (!touch) return
|
|
|
+ const deltaX = Math.abs(touch.clientX - this.touchStartX)
|
|
|
+ const deltaY = Math.abs(touch.clientY - this.touchStartY)
|
|
|
+ if (deltaX > 8 || deltaY > 8) {
|
|
|
+ this.listTouchMoved = true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleListTouchEnd() {
|
|
|
+ if (!this.listTouchMoved) {
|
|
|
+ this.handlePageClick()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ appendMessages(newMessages = []) {
|
|
|
+ if (!newMessages.length) return
|
|
|
+ const startIndex = this.messages.length
|
|
|
+ this.messages = [...this.messages, ...newMessages]
|
|
|
+ newMessages.forEach((message, offset) => {
|
|
|
+ if (message.showReceiptToggle) {
|
|
|
+ this.registerReceiptToggleTimer(startIndex + offset)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.scrollToBottom()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ appendMessage(newMessage) {
|
|
|
+ if (!newMessage) return
|
|
|
+ this.appendMessages([newMessage])
|
|
|
+ },
|
|
|
+ async handleMoreAction(type) {
|
|
|
+ try {
|
|
|
+ if (type === 'file') {
|
|
|
+ const fileMessage = await pickFileOutgoingMessage(this.currentUserMeta)
|
|
|
+ if (fileMessage) this.appendMessage(fileMessage)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (type === 'image') {
|
|
|
+ const imageMessages = await pickImageOutgoingMessages(this.currentUserMeta)
|
|
|
+ if (imageMessages.length) this.appendMessages(imageMessages)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (type === 'camera') {
|
|
|
+ const imageMessage = await shootImageOutgoingMessage(this.currentUserMeta)
|
|
|
+ if (imageMessage) this.appendMessage(imageMessage)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (type === 'video') {
|
|
|
+ const videoMessage = await pickVideoOutgoingMessage(this.currentUserMeta)
|
|
|
+ if (videoMessage) this.appendMessage(videoMessage)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.showToast({ title: `点击:${type}`, icon: 'none' })
|
|
|
+ } catch (error) {
|
|
|
+ uni.showToast({ title: '操作已取消', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openImagePreview(message) {
|
|
|
+ const imageUrls = collectImageUrls(this.messages)
|
|
|
+ if (!imageUrls.length) return
|
|
|
+ const currentUrl = message.imageUrl || imageUrls[0]
|
|
|
+ const currentIndex = imageUrls.findIndex((item) => item === currentUrl)
|
|
|
+ this.previewType = 'image'
|
|
|
+ this.previewImageList = imageUrls
|
|
|
+ this.previewImageIndex = currentIndex > -1 ? currentIndex : 0
|
|
|
+ this.showMediaPreview = true
|
|
|
+ },
|
|
|
+ openVideoPreview(message, index) {
|
|
|
+ this.previewType = 'video'
|
|
|
+ this.previewSource = message.videoUrl || message.coverUrl || '/static/logo.png'
|
|
|
+ this.activeVideoIndex = typeof index === 'number' ? index : -1
|
|
|
+ this.showMediaPreview = true
|
|
|
+ },
|
|
|
+ openFilePreview(message) {
|
|
|
+ this.activeFileName = message.fileName || '未命名文件'
|
|
|
+ this.activeFileUrl = message.fileUrl || ''
|
|
|
+ this.showFileDialog = true
|
|
|
+ },
|
|
|
+ downloadFileFromDialog() {
|
|
|
+ if (!this.activeFileUrl) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '文件地址不存在',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const lowerUrl = (this.activeFileUrl || '').toLowerCase()
|
|
|
+ const lowerName = (this.activeFileName || '').toLowerCase()
|
|
|
+ const target = lowerName || lowerUrl
|
|
|
+ const isDocType = /(\.pdf|\.doc|\.docx|\.xls|\.xlsx|\.ppt|\.pptx|\.txt)$/.test(target)
|
|
|
+ uni.downloadFile({
|
|
|
+ url: this.activeFileUrl,
|
|
|
+ success: (res) => {
|
|
|
+ if (res.statusCode === 200) {
|
|
|
+ this.closeFileDialog()
|
|
|
+ if (isDocType) {
|
|
|
+ uni.openDocument({
|
|
|
+ filePath: res.tempFilePath,
|
|
|
+ showMenu: true,
|
|
|
+ fail: () => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '文件打开失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '下载成功,当前类型不支持在线打开',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '下载失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '下载失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ closeFileDialog() {
|
|
|
+ this.showFileDialog = false
|
|
|
+ this.activeFileName = ''
|
|
|
+ this.activeFileUrl = ''
|
|
|
+ },
|
|
|
+ closeMediaPreview() {
|
|
|
+ this.showMediaPreview = false
|
|
|
+ this.previewType = ''
|
|
|
+ this.previewSource = ''
|
|
|
+ this.activeVideoIndex = -1
|
|
|
+ this.previewImageList = []
|
|
|
+ this.previewImageIndex = 0
|
|
|
+ },
|
|
|
+ handlePreviewVideoLoaded(event) {
|
|
|
+ const seconds = event?.detail?.duration
|
|
|
+ if (!seconds || this.activeVideoIndex < 0 || !this.messages[this.activeVideoIndex]) return
|
|
|
+ const mins = Math.floor(seconds / 60)
|
|
|
+ const secs = Math.floor(seconds % 60)
|
|
|
+ const durationText = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
|
|
+ this.messages[this.activeVideoIndex].duration = durationText
|
|
|
+ },
|
|
|
+ handlePreviewContentClick() {
|
|
|
+ if (this.previewType === 'image') {
|
|
|
+ this.closeMediaPreview()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleImageSwiperChange(event) {
|
|
|
+ this.previewImageIndex = event.detail && event.detail.current ? event.detail.current : 0
|
|
|
+ },
|
|
|
+ handlePreviewVideoError() {
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频加载失败,请使用H.264编码MP4',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ handleTextLineChange(event) {
|
|
|
+ this.textLineCount = event.detail && event.detail.lineCount ? event.detail.lineCount : 1
|
|
|
+ },
|
|
|
+ sendTextMessage() {
|
|
|
+ const message = buildTextOutgoingMessage(this.draftText, this.currentUserMeta)
|
|
|
+ if (!message) return
|
|
|
+ message.receiptToggleCreatedAt = Date.now()
|
|
|
+ this.appendMessage(message)
|
|
|
+ this.draftText = ''
|
|
|
+ this.textLineCount = 1
|
|
|
+ },
|
|
|
+ addImageFromUrl(url) {
|
|
|
+ if (!url) return
|
|
|
+ this.appendMessage(createImageMessage({
|
|
|
+ ...this.currentUserMeta,
|
|
|
+ imageUrl: url
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+ .message-page {
|
|
|
+ height: 100vh;
|
|
|
+ background: #f5f5f5;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .nav-header {
|
|
|
+ padding: 20rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-bottom: 1rpx solid #eee;
|
|
|
+ }
|
|
|
+
|
|
|
+ .back-btn {
|
|
|
+ width: 100rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ text-align: center;
|
|
|
+ color: #333;
|
|
|
+ border-radius: 5rpx;
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
+ border: 1px solid #eee;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .back-btn image {
|
|
|
+ width: 25rpx;
|
|
|
+ height: 25rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-list {
|
|
|
+ flex: 1;
|
|
|
+ padding: 20rpx;
|
|
|
+ background: #fff;
|
|
|
+ overflow-y: auto;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin: 42rpx 0;
|
|
|
+ gap: 20rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 头像+时间区域 */
|
|
|
+ .avatar-section {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .avatar {
|
|
|
+ width: 92rpx;
|
|
|
+ height: 92rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .msg-time {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #666;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 内容区域 */
|
|
|
+ .content-section {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-info {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 文字消息容器 */
|
|
|
+ .text-message-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 10rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 带回执按钮容器 */
|
|
|
+ .receipt-toggle-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 消息气泡通用样式 */
|
|
|
+ .text-message,
|
|
|
+ .call-message,
|
|
|
+ .voice-message,
|
|
|
+ .file-message {
|
|
|
+ background: #7d89b1;
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 32rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-message,
|
|
|
+ .video-message {
|
|
|
+ border-radius: 8rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1rpx solid #dcdcdc;
|
|
|
+ background: #ffffff;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 文字消息 */
|
|
|
+ .text-message {
|
|
|
+ max-width: 400rpx;
|
|
|
+ word-wrap: break-word;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 通话记录 */
|
|
|
+ .call-message {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .call-message image {
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 语音消息 */
|
|
|
+ .voice-message {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ min-width: 100rpx;
|
|
|
+ transition: background-image 0.12s linear;
|
|
|
+ }
|
|
|
+
|
|
|
+ .voice-message image {
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-message {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12rpx;
|
|
|
+ max-width: 420rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-name {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ font-size: 32rpx;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-preview {
|
|
|
+ width: 280rpx;
|
|
|
+ height: 220rpx;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-message {
|
|
|
+ position: relative;
|
|
|
+ width: 280rpx;
|
|
|
+ height: 220rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-cover {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-play {
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 64rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(0, 0, 0, 0.45);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recording-dot {
|
|
|
+ width: 16rpx;
|
|
|
+ height: 16rpx;
|
|
|
+ background: #eb6100;
|
|
|
+ border-radius: 50%;
|
|
|
+ position: absolute;
|
|
|
+ right: 10rpx;
|
|
|
+ top: 10rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 语音转文字区域 */
|
|
|
+ .voice-text-section {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 20rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .voice-text-content {
|
|
|
+ flex: 1;
|
|
|
+ background: #7d89b1;
|
|
|
+ padding: 20rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #fff;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 回执按钮通用样式 */
|
|
|
+ .inline-receipt-button,
|
|
|
+ .receipt-button {
|
|
|
+ background: #565d6d;
|
|
|
+ color: #fff;
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ white-space: nowrap;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 右侧消息(发送) */
|
|
|
+ .right {
|
|
|
+ flex-direction: row-reverse;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right .avatar {
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right .content-section {
|
|
|
+ align-items: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right .user-info {
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 右侧消息气泡样式 */
|
|
|
+ .right .text-message,
|
|
|
+ .right .call-message,
|
|
|
+ .right .voice-message,
|
|
|
+ .right .file-message,
|
|
|
+ .right .voice-text-content {
|
|
|
+ background: #eeeeee;
|
|
|
+ color: #333333;
|
|
|
+ border: 1rpx solid #dcdcdc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .footer {
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
+ background: #eeeeee;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ gap: 16rpx;
|
|
|
+ border-top: 4rpx solid transparent;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .footer.active {
|
|
|
+ border-top-color: #dcdcdc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tool-btn {
|
|
|
+ width: 78rpx;
|
|
|
+ height: 78rpx;
|
|
|
+ border-radius: 4rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #e5e7eb;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-self: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .center-area {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .press-to-talk {
|
|
|
+ width: 100%;
|
|
|
+ height: 78rpx;
|
|
|
+ border-radius: 4rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #e5e7eb;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .press-text {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 30rpx;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .text-input {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 82rpx;
|
|
|
+ max-height: 234rpx;
|
|
|
+ border-radius: 4rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #e5e7eb;
|
|
|
+ padding: 18rpx 20rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ line-height: 39rpx;
|
|
|
+ color: #111827;
|
|
|
+ overflow-y: auto;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .text-input.capped {
|
|
|
+ height: 234rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bottom-panel {
|
|
|
+ background: #eeeeee;
|
|
|
+ border-top: 1rpx solid transparent;
|
|
|
+ max-height: 0;
|
|
|
+ opacity: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ transform: translateY(24rpx);
|
|
|
+ transition: max-height 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bottom-panel.open {
|
|
|
+ border-top-color: #dcdcdc;
|
|
|
+ max-height: 380rpx;
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ .more-panel {
|
|
|
+ background: #eeeeee;
|
|
|
+ padding: 30rpx 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .emoji-panel {
|
|
|
+ background: #eeeeee;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ max-height: 320rpx;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .emoji-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(8, 1fr);
|
|
|
+ gap: 12rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .emoji-item {
|
|
|
+ height: 52rpx;
|
|
|
+ line-height: 52rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 36rpx;
|
|
|
+ border-radius: 4rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .more-grid {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .more-item {
|
|
|
+ width: 25%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .more-icon {
|
|
|
+ width: 96rpx;
|
|
|
+ height: 96rpx;
|
|
|
+ border-radius: 4rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #e5e7eb;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .more-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-mask {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: rgba(0, 0, 0, 0.78);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 9999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-content.video-mode {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: transparent;
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-content.image-mode {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: transparent;
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-video {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-exit {
|
|
|
+ position: fixed;
|
|
|
+ top: 90rpx;
|
|
|
+ right: 30rpx;
|
|
|
+ height: 56rpx;
|
|
|
+ line-height: 56rpx;
|
|
|
+ padding: 0 18rpx;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #fff;
|
|
|
+ background: rgba(0, 0, 0, 0.45);
|
|
|
+ z-index: 10001;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-swiper {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-swiper-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-preview-image.fit-x {
|
|
|
+ width: 100vw;
|
|
|
+ height: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog-mask {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: rgba(255, 255, 255, 0.96);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 10000;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: transparent;
|
|
|
+ border-radius: 0;
|
|
|
+ padding: 0 60rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog-header {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 18rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog-name {
|
|
|
+ max-width: 620rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 24rpx;
|
|
|
+ padding-top: 40rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog-btn {
|
|
|
+ min-width: 120rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ line-height: 64rpx;
|
|
|
+ text-align: center;
|
|
|
+ background: #575d6d;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 28rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-dialog-btn.ghost {
|
|
|
+ background: #f1f2f4;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-mask {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 12000;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-panel {
|
|
|
+ width: 360rpx;
|
|
|
+ padding: 30rpx 24rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ background: rgba(0, 0, 0, 0.72);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 14rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-time {
|
|
|
+ font-size: 42rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-tip {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-tip.danger {
|
|
|
+ color: #ffb2b2;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+</style>
|