2026-01-26 18:08:01 +08:00

433 lines
12 KiB
Vue
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.

<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 = "";
};
// 从常用语发送文本消息
const sendTextMessageFromPhrase = async (content) => {
if (!content.trim()) return;
await sendMessage("text", content);
// 发送成功后滚动到底部
nextTick(() => {
emit("scrollToBottom");
});
};
// 暴露方法给父组件调用
defineExpose({
sendTextMessageFromPhrase
});
// 发送图片消息
const sendImageMessage = async (imageFile) => {
console.log("chat-input sendImageMessage 被调用,参数:", imageFile);
await sendMessage("image", imageFile);
};
// 发送语音消息
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(
(file) => {
console.log("选择图片成功,文件对象:", file);
// 直接传递文件对象,不需要额外处理
sendImageMessage(file);
},
(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(
(file) => {
console.log("拍照成功,文件对象:", file);
// 直接传递文件对象,不需要额外处理
sendImageMessage(file);
},
(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 goToCommonPhrases = () => {
uni.navigateTo({
url: '/pages/message/common-phrases'
});
};
// 跳转到宣教文章页面
const goToArticleList = () => {
uni.navigateTo({
url: '/pages/message/article-list'
});
};
// 跳转到问卷列表页面
const goToSurveyList = () => {
uni.navigateTo({
url: '/pages/message/survey-list'
});
};
const morePanelButtons = [
{ text: "照片", icon: "/static/icon/zhaopian.png", action: showImagePicker },
{
text: "回访任务",
icon: "/static/icon/zhaopian.png",
action: showImagePicker,
},
{
text: "常用语",
icon: "/static/icon/changyongyu.png",
action: goToCommonPhrases,
},
{
text: "宣教",
icon: "/static/icon/xuanjiaowenzhang.png",
action: goToArticleList,
},
{
text: "问卷",
icon: "/static/icon/wenjuan.png",
action: goToSurveyList,
},
{
text: "结束问诊",
icon: "/static/icon/jieshuzixun.png",
action: showImagePicker,
},
];
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>