ykt-wxapp/pages/message/components/ai-assistant-buttons.vue

421 lines
10 KiB
Vue
Raw Normal View History

2026-01-29 18:03:40 +08:00
<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"
/>
<!-- 进度显示弹窗 -->
2026-01-30 10:28:34 +08:00
<medical-case-progress
ref="progressRef"
2026-01-29 18:44:34 +08:00
@regenerate="handleRegenerateFromProgress"
@next="handleNextFromProgress"
/>
2026-01-29 18:03:40 +08:00
</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: "",
},
corpId: {
type: String,
default: "",
},
customerId: {
type: String,
default: "",
},
});
const emit = defineEmits(["streamText", "clearInput"]);
const typeSelectorRef = ref(null);
const progressRef = ref(null);
const buttons = ref([
{
id: "followUp",
text: "追问病情",
loadingText: "AI分析中正在为您生成追问建议…",
icon: "/static/icon/zhuiwen.png",
loading: false,
},
2026-01-30 10:28:34 +08:00
// {
// id: "aiAssistant",
// text: "开启AI助手",
// icon: "/static/icon/kaiqiAI.png",
// loading: false,
// },
2026-01-29 18:03:40 +08:00
{
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");
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);
}
}, 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);
2026-01-29 18:44:34 +08:00
progressRef.value?.updateProgress(10);
2026-01-29 18:03:40 +08:00
2026-01-29 18:44:34 +08:00
// 调用补充病历接口(流式处理)
await requestWithStream({
2026-01-29 18:03:40 +08:00
url: "/getYoucanData/im",
data: {
type: "supplementMedicalCase",
groupId: props.groupId,
patientAccountId: props.patientAccountId || props.customerId,
corpId: props.corpId,
caseType: type.id,
},
2026-01-29 18:44:34 +08:00
onProgress: (data) => {
// 处理流式数据
handleStreamData(data, type.id);
},
onComplete: (finalData) => {
// 完成后跳转
handleComplete(finalData, type.id);
},
onError: (error) => {
progressRef.value?.close();
uni.showToast({
title: error.message || "生成病历失败",
icon: "none",
2026-01-29 18:03:40 +08:00
});
2026-01-30 10:28:34 +08:00
},
2026-01-29 18:44:34 +08:00
});
2026-01-29 18:03:40 +08:00
} catch (error) {
console.error("补充病历失败:", error);
progressRef.value?.close();
uni.showToast({
title: "操作失败,请重试",
icon: "none",
});
}
};
2026-01-29 18:44:34 +08:00
// 流式请求处理
2026-01-30 10:28:34 +08:00
const requestWithStream = async ({
url,
data,
onProgress,
onComplete,
onError,
}) => {
2026-01-29 18:44:34 +08:00
try {
// 调用接口时不显示全局 loading第二个参数为 false
2026-01-30 10:28:34 +08:00
const result = await request(
{
url,
data,
},
false
);
2026-01-29 18:44:34 +08:00
if (result.success && result.data) {
// 模拟流式处理(如果后端返回的是完整数据)
const extractedData = result.data.extractedData || {};
2026-01-30 10:28:34 +08:00
2026-01-29 18:44:34 +08:00
// 逐个字段动态显示(包括空值字段)
let progressValue = 20;
const fields = Object.entries(extractedData);
const delay = 300; // 每个字段显示间隔
2026-01-30 10:28:34 +08:00
2026-01-29 18:44:34 +08:00
for (let i = 0; i < fields.length; i++) {
const [key, value] = fields[i];
2026-01-30 10:28:34 +08:00
2026-01-29 18:44:34 +08:00
// 显示所有字段,包括空值(会在组件中显示为"暂无"
2026-01-30 10:28:34 +08:00
await new Promise((resolve) => setTimeout(resolve, delay));
2026-01-29 18:44:34 +08:00
onProgress({ key, value });
progressValue += Math.floor(60 / fields.length);
progressRef.value?.updateProgress(Math.min(progressValue, 80));
}
2026-01-30 10:28:34 +08:00
2026-01-29 18:44:34 +08:00
// 完成
onComplete(result.data);
} else {
onError(new Error(result.message || "请求失败"));
}
} catch (error) {
onError(error);
}
};
// 处理流式数据
const handleStreamData = (data, caseType) => {
const { key, value } = data;
2026-01-30 10:28:34 +08:00
2026-01-29 18:44:34 +08:00
// 添加检测到的信息
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) => {
// 跳转到病历填写页面
uni.navigateTo({
url: `/pages/case/medical-case-form?caseType=${data.caseType}&customerId=${
props.customerId || props.patientAccountId
}&groupId=${props.groupId}&formData=${encodeURIComponent(
JSON.stringify(data.data?.extractedData || {})
)}`,
});
};
2026-01-29 18:03:40 +08:00
// 监听重新生成事件
onMounted(() => {
uni.$on("regenerateMedicalCase", handleRegenerateMedicalCase);
});
onUnmounted(() => {
uni.$off("regenerateMedicalCase", handleRegenerateMedicalCase);
});
const handleRegenerateMedicalCase = (data) => {
const type = { id: data.caseType };
handleCaseTypeSelect(type);
};
</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>