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>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted, onUnmounted } from "vue";
|
|
|
|
|
|
import request from "@/utils/http.js";
|
|
|
|
|
|
import api from "@/utils/api.js";
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
groupId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
patientAccountId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "",
|
|
|
|
|
|
},
|
2026-02-02 13:27:48 +08:00
|
|
|
|
patientId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "",
|
|
|
|
|
|
},
|
2026-01-29 18:03:40 +08:00
|
|
|
|
corpId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
customerId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "",
|
|
|
|
|
|
},
|
2026-02-12 14:12:01 +08:00
|
|
|
|
typeSelectorRef: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
progressRef: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null,
|
|
|
|
|
|
},
|
2026-01-29 18:03:40 +08:00
|
|
|
|
});
|
2026-02-12 14:12:01 +08:00
|
|
|
|
const emit = defineEmits([
|
|
|
|
|
|
"streamText",
|
|
|
|
|
|
"clearInput",
|
|
|
|
|
|
"generatingStateChange",
|
|
|
|
|
|
"caseTypeSelect",
|
|
|
|
|
|
"regenerateFromProgress",
|
|
|
|
|
|
"nextFromProgress",
|
|
|
|
|
|
]);
|
2026-01-29 18:03:40 +08:00
|
|
|
|
|
2026-02-08 13:53:22 +08:00
|
|
|
|
const isGenerating = ref(false);
|
|
|
|
|
|
|
2026-01-29 18:03:40 +08:00
|
|
|
|
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");
|
2026-02-08 13:53:22 +08:00
|
|
|
|
isGenerating.value = true;
|
|
|
|
|
|
emit("generatingStateChange", true);
|
2026-01-29 18:03:40 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-02-08 13:53:22 +08:00
|
|
|
|
isGenerating.value = false;
|
|
|
|
|
|
emit("generatingStateChange", false);
|
2026-01-29 18:03:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, speed);
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理AI助手
|
|
|
|
|
|
const handleAIAssistant = (button) => {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "AI助手功能开发中",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理补充病历
|
|
|
|
|
|
const handleSupplementRecord = (button) => {
|
2026-02-12 14:12:01 +08:00
|
|
|
|
props.typeSelectorRef?.open();
|
2026-01-29 18:03:40 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理病历类型选择
|
|
|
|
|
|
const handleCaseTypeSelect = async (type) => {
|
2026-02-12 14:12:01 +08:00
|
|
|
|
emit("caseTypeSelect", type);
|
2026-01-29 18:44:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理从进度弹窗点击重新生成
|
|
|
|
|
|
const handleRegenerateFromProgress = (data) => {
|
2026-02-12 14:12:01 +08:00
|
|
|
|
emit("regenerateFromProgress", data);
|
2026-01-29 18:44:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理从进度弹窗点击下一步
|
|
|
|
|
|
const handleNextFromProgress = (data) => {
|
2026-02-12 14:12:01 +08:00
|
|
|
|
emit("nextFromProgress", data);
|
2026-01-29 18:44:34 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
2026-02-08 13:53:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 暴露生成状态给父组件
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
|
isGenerating,
|
|
|
|
|
|
});
|
2026-01-29 18:03:40 +08:00
|
|
|
|
</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>
|