ykt-wxapp/pages/message/components/ai-assistant-buttons.vue
2026-02-08 13:53:22 +08:00

439 lines
10 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="ai-assistant-buttons">
<view
v-for="button in buttons"
:key="button.id"
class="ai-button"
:class="{ loading: button.loading }"
@click="handleButtonClick(button)"
>
<image class="button-icon" :src="button.icon" mode="aspectFit" />
<text class="button-text">{{
button.loading && button.loadingText ? button.loadingText : button.text
}}</text>
</view>
<!-- 病历类型选择弹窗 -->
<medical-case-type-selector
ref="typeSelectorRef"
@select="handleCaseTypeSelect"
/>
<!-- 进度显示弹窗 -->
<medical-case-progress
ref="progressRef"
@regenerate="handleRegenerateFromProgress"
@next="handleNextFromProgress"
/>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import request from "@/utils/http.js";
import api from "@/utils/api.js";
import MedicalCaseTypeSelector from "./medical-case-type-selector.vue";
import MedicalCaseProgress from "./medical-case-progress.vue";
const props = defineProps({
groupId: {
type: String,
required: true,
},
patientAccountId: {
type: String,
default: "",
},
patientId: {
type: String,
default: "",
},
corpId: {
type: String,
default: "",
},
customerId: {
type: String,
default: "",
},
});
const emit = defineEmits(["streamText", "clearInput", "generatingStateChange"]);
const typeSelectorRef = ref(null);
const progressRef = ref(null);
const isGenerating = ref(false);
const buttons = ref([
{
id: "followUp",
text: "追问病情",
loadingText: "AI分析中正在为您生成追问建议…",
icon: "/static/icon/zhuiwen.png",
loading: false,
},
// {
// id: "aiAssistant",
// text: "开启AI助手",
// icon: "/static/icon/kaiqiAI.png",
// loading: false,
// },
{
id: "supplementRecord",
text: "补充病历",
icon: "/static/icon/buchong.png",
loading: false,
},
]);
// 处理按钮点击
const handleButtonClick = async (button) => {
if (button.loading) return;
switch (button.id) {
case "followUp":
await handleFollowUpInquiry(button);
break;
case "aiAssistant":
handleAIAssistant(button);
break;
case "supplementRecord":
handleSupplementRecord(button);
break;
}
};
// 处理追问病情
const handleFollowUpInquiry = async (button) => {
try {
button.loading = true;
// 如果没有提供患者账号ID先获取群组信息
let finalPatientAccountId = props.patientAccountId;
if (!finalPatientAccountId) {
// 从聊天记录中获取患者账号ID非当前用户的发送者
const chatRecordsResult = await api("getChatRecordsByGroupId", {
groupID: props.groupId,
count: 5,
});
if (
chatRecordsResult.success &&
chatRecordsResult.data &&
chatRecordsResult.data.records
) {
const records = chatRecordsResult.data.records;
// 找到第一个非当前用户的发送者作为患者账号
const currentUserId = uni.getStorageSync("openid") || "";
const patientMessage = records.find(
(record) =>
record.From_Account && record.From_Account !== currentUserId
);
if (patientMessage) {
finalPatientAccountId = patientMessage.From_Account;
}
}
}
if (!finalPatientAccountId) {
uni.showToast({
title: "无法获取患者信息",
icon: "none",
});
button.loading = false;
return;
}
// 调用追问病情接口不显示全局loading
const result = await request(
{
url: "/getYoucanData/im",
data: {
type: "followUpInquiry",
groupId: props.groupId,
patientAccountId: finalPatientAccountId,
corpId: props.corpId,
},
},
false
); // 第二个参数false表示不显示loading
if (result.success && result.data && result.data.suggestion) {
// 流式输出文本到输入框
streamTextToInput(result.data.suggestion);
} else {
uni.showToast({
title: result.message || "获取追问建议失败",
icon: "none",
});
}
} catch (error) {
console.error("追问病情失败:", error);
uni.showToast({
title: "操作失败,请重试",
icon: "none",
});
} finally {
button.loading = false;
}
};
// 流式输出文本到输入框
const streamTextToInput = (text) => {
if (!text) return;
// 先清空输入框
emit("clearInput");
isGenerating.value = true;
emit("generatingStateChange", true);
let currentIndex = 0;
const speed = 50; // 每个字符的延迟时间(毫秒)
// 延迟一小段时间后开始流式输出,确保清空操作完成
setTimeout(() => {
const streamInterval = setInterval(() => {
if (currentIndex < text.length) {
const char = text[currentIndex];
emit("streamText", char);
currentIndex++;
} else {
clearInterval(streamInterval);
isGenerating.value = false;
emit("generatingStateChange", false);
}
}, speed);
}, 100);
};
// 处理AI助手
const handleAIAssistant = (button) => {
uni.showToast({
title: "AI助手功能开发中",
icon: "none",
});
};
// 处理补充病历
const handleSupplementRecord = (button) => {
typeSelectorRef.value?.open();
};
// 处理病历类型选择
const handleCaseTypeSelect = async (type) => {
try {
// 打开进度弹窗
progressRef.value?.open(type.id);
progressRef.value?.updateProgress(10);
// 调用补充病历接口(流式处理)
await requestWithStream({
url: "/getYoucanData/im",
data: {
type: "supplementMedicalCase",
groupId: props.groupId,
patientAccountId: props.patientAccountId || props.customerId,
corpId: props.corpId,
caseType: type.id,
},
onProgress: (data) => {
// 处理流式数据
handleStreamData(data, type.id);
},
onComplete: (finalData) => {
// 完成后跳转
handleComplete(finalData, type.id);
},
onError: (error) => {
progressRef.value?.close();
uni.showToast({
title: error.message || "生成病历失败",
icon: "none",
});
},
});
} catch (error) {
console.error("补充病历失败:", error);
progressRef.value?.close();
uni.showToast({
title: "操作失败,请重试",
icon: "none",
});
}
};
// 流式请求处理
const requestWithStream = async ({
url,
data,
onProgress,
onComplete,
onError,
}) => {
try {
// 调用接口时不显示全局 loading第二个参数为 false
const result = await request(
{
url,
data,
},
false
);
if (result.success && result.data) {
// 模拟流式处理(如果后端返回的是完整数据)
const extractedData = result.data.extractedData || {};
// 逐个字段动态显示(包括空值字段)
let progressValue = 20;
const fields = Object.entries(extractedData);
const delay = 300; // 每个字段显示间隔
for (let i = 0; i < fields.length; i++) {
const [key, value] = fields[i];
// 显示所有字段,包括空值(会在组件中显示为"暂无"
await new Promise((resolve) => setTimeout(resolve, delay));
onProgress({ key, value });
progressValue += Math.floor(60 / fields.length);
progressRef.value?.updateProgress(Math.min(progressValue, 80));
}
// 完成
onComplete(result.data);
} else {
onError(new Error(result.message || "请求失败"));
}
} catch (error) {
onError(error);
}
};
// 处理流式数据
const handleStreamData = (data, caseType) => {
const { key, value } = data;
// 添加检测到的信息
progressRef.value?.addDetectedInfo(key, value);
};
// 处理完成
const handleComplete = (finalData, caseType) => {
progressRef.value?.updateProgress(90);
progressRef.value?.setGenerating(true);
// 延迟后完成
setTimeout(() => {
progressRef.value?.updateProgress(100);
progressRef.value?.setGenerating(false);
// 延迟后显示操作按钮(不自动跳转)
setTimeout(() => {
progressRef.value?.setCompleted(true, finalData);
}, 500);
}, 800);
};
// 处理从进度弹窗点击重新生成
const handleRegenerateFromProgress = (data) => {
const type = { id: data.caseType };
handleCaseTypeSelect(type);
};
// 处理从进度弹窗点击下一步
const handleNextFromProgress = (data) => {
// 根据病历类型动态构建表单数据
const extractedData = data.data?.extractedData || {};
// 跳转到病历填写页面
uni.navigateTo({
url: `/pages/case/medical-case-form?caseType=${data.caseType}&patientId=${
props.patientId
}&groupId=${props.groupId}&formData=${encodeURIComponent(
JSON.stringify(extractedData)
)}`,
});
};
// 监听重新生成事件
onMounted(() => {
uni.$on("regenerateMedicalCase", handleRegenerateMedicalCase);
});
onUnmounted(() => {
uni.$off("regenerateMedicalCase", handleRegenerateMedicalCase);
});
const handleRegenerateMedicalCase = (data) => {
const type = { id: data.caseType };
handleCaseTypeSelect(type);
};
// 暴露生成状态给父组件
defineExpose({
isGenerating,
});
</script>
<style scoped lang="scss">
.ai-assistant-buttons {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
background-color: #f8f9fa;
border-bottom: 1rpx solid #e5e5e5;
.ai-button {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
background-color: #ffffff;
border: 1rpx solid #e0e0e0;
border-radius: 40rpx;
transition: all 0.3s ease;
&.loading {
opacity: 0.8;
pointer-events: none;
.loading-icon {
width: 32rpx;
height: 32rpx;
border: 3rpx solid #e0e0e0;
border-top-color: #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
&:active {
background-color: #f0f0f0;
transform: scale(0.98);
}
.button-icon {
width: 32rpx;
height: 32rpx;
}
.button-text {
font-size: 28rpx;
color: #333333;
white-space: nowrap;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>