360 lines
11 KiB
Vue
Raw Normal View History

2026-01-20 13:21:50 +08:00
<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">{{ '&#xe61a;' }}</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 = "";
};
// 发送图片消息
2026-01-22 16:35:05 +08:00
const sendImageMessage = async (imageFile) => {
console.log('chat-input sendImageMessage 被调用,参数:', imageFile);
await sendMessage('image', imageFile);
2026-01-20 13:21:50 +08:00
};
// 发送语音消息
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;
2026-01-22 16:35:05 +08:00
// 发送成功后滚动到底部
2026-01-20 13:21:50 +08:00
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(
2026-01-22 17:02:15 +08:00
(file) => {
console.log('选择图片成功,文件对象:', file);
// 直接传递文件对象,不需要额外处理
sendImageMessage(file);
2026-01-22 16:35:05 +08:00
},
2026-01-20 13:21:50 +08:00
(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(
2026-01-22 17:02:15 +08:00
(file) => {
console.log('拍照成功,文件对象:', file);
// 直接传递文件对象,不需要额外处理
sendImageMessage(file);
2026-01-22 16:35:05 +08:00
},
2026-01-20 13:21:50 +08:00
(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 },
2026-01-22 16:35:05 +08:00
// { text: '病情', icon: '/static/home/avatar.svg', action: sendSymptomMessage },
// { text: '处方', icon: '/static/home/avatar.svg', action: sendPrescriptionMessage },
// { text: '续方', icon: '/static/home/avatar.svg', action: sendRefillMessage },
// { text: '问卷', icon: '/static/home/avatar.svg', action: sendSurveyMessage }
2026-01-20 13:21:50 +08:00
];
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>