438 lines
10 KiB
Vue
438 lines
10 KiB
Vue
<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/ai-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;
|
||
|
||
.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>
|