350 lines
10 KiB
Vue
350 lines
10 KiB
Vue
<template>
|
||
<view class="input-section">
|
||
<view class="input-toolbar">
|
||
<view @click="toggleVoiceInput" class="voice-toggle-btn">
|
||
<uni-icons v-if="showVoiceInput" fontFamily="keyboard" :size="28">{{ '' }}</uni-icons>
|
||
<uni-icons v-else type="mic" size="28" color="#666" />
|
||
</view>
|
||
<view class="input-area">
|
||
<input v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
|
||
@confirm="sendTextMessage" @focus="handleInputFocus" />
|
||
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
|
||
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
|
||
</input>
|
||
</view>
|
||
<button v-if="inputText.trim()" class="send-btn" @click="sendTextMessage">
|
||
发送
|
||
</button>
|
||
<view v-else class="plus-btn" @click="toggleMorePanel()">
|
||
<uni-icons type="plusempty" size="28" color="#666" />
|
||
</view>
|
||
</view>
|
||
<view class="more-panel" v-if="showMorePanel">
|
||
<view v-for="btn in morePanelButtons" :key="btn.text" class="more-btn" @click="btn.action">
|
||
<image :src="btn.icon" class="more-icon" mode="aspectFit"></image>
|
||
<text>{{ btn.text }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 录音遮罩层 -->
|
||
<view v-if="isRecording" class="recording-overlay">
|
||
<view class="recording-modal" :class="{ 'cancel-mode': isCancelMode }">
|
||
<view class="recording-icon-container">
|
||
<view v-if="!isCancelMode" class="wave-circle wave-1"></view>
|
||
<view v-if="!isCancelMode" class="wave-circle wave-2"></view>
|
||
<view v-if="!isCancelMode" class="wave-circle wave-3"></view>
|
||
<view class="mic-icon-wrapper" :class="{ 'cancel-icon': isCancelMode }">
|
||
<uni-icons v-if="!isCancelMode" type="mic-filled" size="60" color="#fff" />
|
||
<uni-icons v-else type="closeempty" size="60" color="#fff" />
|
||
</view>
|
||
</view>
|
||
<view class="recording-text" :class="{ 'cancel-text': isCancelMode }">
|
||
{{ isCancelMode ? '松开手指,取消录音' : '正在录音...' }}
|
||
</view>
|
||
<view v-if="!isCancelMode" class="recording-hint">松开发送,上滑取消</view>
|
||
<view v-if="!isCancelMode" class="recording-duration">{{ recordingDuration }}s</view>
|
||
<view v-else class="cancel-hint">
|
||
<uni-icons type="up" size="20" color="#ff4757" />
|
||
<text>已上滑</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||
import {
|
||
chooseImage,
|
||
takePhoto as takePhotoUtil,
|
||
initRecorderManager as initRecorderManagerUtil,
|
||
startRecord as startRecordUtil,
|
||
stopRecord as stopRecordUtil,
|
||
createCustomMessage,
|
||
sendCustomMessage as sendCustomMessageUtil,
|
||
sendMessage as sendMessageUtil,
|
||
checkRecordingDuration,
|
||
validateBeforeSend
|
||
} from '@/utils/chat-utils.js';
|
||
|
||
// Props
|
||
const props = defineProps({
|
||
timChatManager: { type: Object, required: true },
|
||
patientInfo: { type: Object, default: () => ({}) },
|
||
chatRoomBusiness: { type: Object, default: () => ({}) },
|
||
formatTime: { type: Function, required: true }
|
||
});
|
||
|
||
// Emits
|
||
const emit = defineEmits(['messageSent', 'scrollToBottom']);
|
||
|
||
// 输入相关状态
|
||
const inputText = ref("");
|
||
const showVoiceInput = ref(false);
|
||
const showMorePanel = ref(false);
|
||
const isRecording = ref(false);
|
||
const recordingText = ref("录音中...");
|
||
const cloudCustomData = computed(() => {
|
||
const arr = [props.chatRoomBusiness.businessType, props.chatRoomBusiness.businessId];
|
||
return arr.filter(Boolean).join('|');
|
||
})
|
||
|
||
// 录音相关扩展状态(特效 + 取消逻辑)
|
||
const recordingDuration = ref(0);
|
||
let recordingTimer = null;
|
||
const isCancelMode = ref(false);
|
||
let touchStartY = 0;
|
||
const CANCEL_DISTANCE = 100;
|
||
let discardRecording = false; // 取消后丢弃本次录音
|
||
|
||
// 录音管理器
|
||
let recorderManager = null;
|
||
|
||
// 初始化录音管理器
|
||
const initRecorderManager = () => {
|
||
recorderManager = initRecorderManagerUtil(
|
||
{},
|
||
(res) => {
|
||
// 若本次被标记为丢弃(取消录音触发的 stop),则忽略发送
|
||
if (discardRecording) {
|
||
discardRecording = false;
|
||
return;
|
||
}
|
||
|
||
// 检查录音时长
|
||
if (!checkRecordingDuration(res, () => {
|
||
isRecording.value = false;
|
||
recordingText.value = "录音中...";
|
||
})) {
|
||
return;
|
||
}
|
||
|
||
// 重置录音状态
|
||
isRecording.value = false;
|
||
recordingText.value = "录音中...";
|
||
clearDurationTimer();
|
||
|
||
// 发送语音消息
|
||
const duration = Math.floor(res.duration / 1000);
|
||
sendVoiceMessage(res, duration);
|
||
},
|
||
(err) => {
|
||
isRecording.value = false;
|
||
recordingText.value = "录音中...";
|
||
clearDurationTimer();
|
||
discardRecording = false;
|
||
}
|
||
);
|
||
};
|
||
|
||
// 发送文本消息
|
||
const sendTextMessage = async () => {
|
||
if (!inputText.value.trim()) return;
|
||
|
||
await sendMessage('text', inputText.value);
|
||
inputText.value = "";
|
||
};
|
||
|
||
// 发送图片消息
|
||
const sendImageMessage = async (imageUrl) => {
|
||
await sendMessage('image', imageUrl);
|
||
};
|
||
|
||
// 发送语音消息
|
||
const sendVoiceMessage = async (voiceFile, duration) => {
|
||
await sendMessage('voice', { file: voiceFile, duration });
|
||
};
|
||
|
||
// 发送消息的通用方法(文本、图片、语音)
|
||
const sendMessage = async (messageType, data) => {
|
||
await sendMessageUtil(
|
||
messageType,
|
||
data,
|
||
props.timChatManager,
|
||
() => validateBeforeSend(false, false, props.timChatManager),
|
||
() => {
|
||
showMorePanel.value = false;
|
||
emit('messageSent');
|
||
},
|
||
cloudCustomData.value
|
||
);
|
||
};
|
||
|
||
// 发送自定义消息的通用方法
|
||
const sendCustomMessage = async (messageData) => {
|
||
await sendCustomMessageUtil(
|
||
messageData,
|
||
props.timChatManager,
|
||
() => validateBeforeSend(false, false, props.timChatManager),
|
||
() => {
|
||
showMorePanel.value = false;
|
||
emit('messageSent');
|
||
}
|
||
);
|
||
};
|
||
|
||
// 输入相关方法
|
||
const toggleVoiceInput = () => {
|
||
showVoiceInput.value = !showVoiceInput.value;
|
||
showMorePanel.value = false;
|
||
};
|
||
|
||
const toggleMorePanel = () => {
|
||
showMorePanel.value = !showMorePanel.value;
|
||
showVoiceInput.value = false;
|
||
};
|
||
|
||
// 处理图片选择
|
||
const showImagePicker = () => {
|
||
chooseImage(
|
||
(res) => sendImageMessage(res),
|
||
(err) => {
|
||
console.error('选择图片失败:', err);
|
||
if (!err.errMsg?.includes('permission') && !err.errMsg?.includes('auth') && !err.errMsg?.includes('拒绝') && !err.errMsg?.includes('未授权')) {
|
||
uni.showToast({
|
||
title: '选择图片失败,请重试',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
}
|
||
);
|
||
};
|
||
|
||
const takePhoto = () => {
|
||
takePhotoUtil(
|
||
(res) => sendImageMessage(res),
|
||
(err) => {
|
||
console.error('拍照失败:', err);
|
||
if (!err.errMsg?.includes('permission') && !err.errMsg?.includes('auth') && !err.errMsg?.includes('拒绝') && !err.errMsg?.includes('未授权')) {
|
||
uni.showToast({
|
||
title: '拍照失败,请重试',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
}
|
||
);
|
||
};
|
||
|
||
function startDurationTimer() {
|
||
clearDurationTimer();
|
||
recordingDuration.value = 0;
|
||
recordingTimer = setInterval(() => {
|
||
recordingDuration.value++;
|
||
}, 1000);
|
||
}
|
||
|
||
function clearDurationTimer() {
|
||
if (recordingTimer) {
|
||
clearInterval(recordingTimer);
|
||
recordingTimer = null;
|
||
}
|
||
}
|
||
|
||
const startRecord = (e) => {
|
||
isRecording.value = true;
|
||
recordingText.value = "录音中...";
|
||
isCancelMode.value = false;
|
||
discardRecording = false;
|
||
recordingDuration.value = 0;
|
||
if (e && e.touches && e.touches[0]) {
|
||
touchStartY = e.touches[0].clientY;
|
||
}
|
||
|
||
// 确保录音管理器已初始化
|
||
if (!recorderManager) {
|
||
initRecorderManager();
|
||
}
|
||
|
||
startDurationTimer();
|
||
startRecordUtil(recorderManager);
|
||
};
|
||
|
||
const onRecordTouchMove = (e) => {
|
||
if (!isRecording.value) return;
|
||
if (e && e.touches && e.touches[0]) {
|
||
const currentY = e.touches[0].clientY;
|
||
const deltaY = touchStartY - currentY; // 上滑为正
|
||
isCancelMode.value = deltaY > CANCEL_DISTANCE;
|
||
}
|
||
};
|
||
|
||
const stopRecord = () => {
|
||
if (!isRecording.value) return;
|
||
// 如果处于取消模式,则按取消处理
|
||
if (isCancelMode.value) {
|
||
cancelRecord();
|
||
return;
|
||
}
|
||
isRecording.value = false;
|
||
recordingText.value = "录音中...";
|
||
clearDurationTimer();
|
||
stopRecordUtil(recorderManager);
|
||
};
|
||
|
||
const cancelRecord = () => {
|
||
if (!isRecording.value) return;
|
||
isRecording.value = false;
|
||
isCancelMode.value = false;
|
||
recordingText.value = "已取消";
|
||
discardRecording = true; // 标记为丢弃,阻止 onStop 发送
|
||
clearDurationTimer();
|
||
stopRecordUtil(recorderManager);
|
||
};
|
||
|
||
|
||
|
||
|
||
// 发送问卷调查消息
|
||
const sendSurveyMessage = async () => {
|
||
const surveyMessage = createCustomMessage("survey", {
|
||
content: "医生发送了问卷调查",
|
||
surveyTitle: "治疗效果评估",
|
||
surveyDescription: "您好,为了帮助了解您的病情变化,请您如实填写问卷。",
|
||
surveyMessage: "慢性病患者生活质量评估问卷",
|
||
estimatedTime: "约3-5分钟",
|
||
reward: "积分奖励10分",
|
||
note: "问卷内容涉及您的症状变化、用药情况等,请根据实际情况填写。",
|
||
}, props.formatTime);
|
||
|
||
await sendCustomMessage(surveyMessage);
|
||
};
|
||
|
||
// 更多面板按钮配置
|
||
const morePanelButtons = [
|
||
{ text: '照片', icon: '/static/home/photo.png', action: showImagePicker },
|
||
{ text: '拍摄', icon: '/static/home/video.png', action: takePhoto },
|
||
// { text: '病情', icon: '/static/home/doctor.png', action: sendSymptomMessage },
|
||
// { text: '处方', icon: '/static/home/doctor.png', action: sendPrescriptionMessage },
|
||
// { text: '续方', icon: '/static/home/doctor.png', action: sendRefillMessage },
|
||
// { text: '问卷', icon: '/static/home/doctor.png', action: sendSurveyMessage }
|
||
];
|
||
|
||
function handleInputFocus() {
|
||
console.log('handleInputFocus')
|
||
nextTick().then(() => {
|
||
emit('scrollToBottom')
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 初始化录音管理器
|
||
initRecorderManager();
|
||
|
||
// 监听关闭功能栏事件
|
||
uni.$on('closeMorePanel', () => {
|
||
showMorePanel.value = false;
|
||
});
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
// 移除事件监听
|
||
uni.$off('closeMorePanel');
|
||
clearDurationTimer();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "../chat.scss";
|
||
</style> |