pengjun 3 months ago
parent
commit
50eecf210d
  1. 58
      src/api/api.js
  2. 66
      src/components/doctorCheck/ButtonList.vue
  3. 128
      src/components/sumDoctorCheck/ButtonList.vue
  4. 2
      src/views/Home.vue

58
src/api/api.js

@ -1,4 +1,4 @@
import request from "@/api/request";
import request, { showFullScreenLoading, tryHideFullScreenLoading } from "@/api/request";
import store from "../store/index";
const sysConfig = getSysConfig()
@ -74,4 +74,60 @@ export async function putapi(url, params = {}, config) {
})
.finally(() => {});
});
}
// 浏览器端 fetch 流式读取(用于增量处理服务器 chunked/text-stream 响应)
export async function fetchStream(url, params = {}, onChunk = (chunk) => {}) {
const fullUrl = `${sysConfig.apiurl}${url}`;
showFullScreenLoading();
try {
const token = window.sessionStorage.getItem('token');
let tokentype = window.sessionStorage.getItem("tokentype");
const headers = {
'Content-Type': 'application/json'
};
if (token) headers['Authorization'] = `${tokentype} ${token}`;
const resp = await fetch(fullUrl, {
method: 'POST',
headers,
body: JSON.stringify(params),
});
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(`fetchStream HTTP ${resp.status} ${text}`);
}
if (!resp.body || !resp.body.getReader) {
// 非流式返回,直接读取全部文本并回调一次
const text = await resp.text();
try { onChunk(text); } catch (e) { /* ignore */ }
// 已经拿到首个数据,先隐藏 loading
tryHideFullScreenLoading();
return text;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder('utf-8');
let result = '';
let firstChunkSeen = false;
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
result += chunk;
// 在收到第一个 chunk 时立即隐藏 loading
if (!firstChunkSeen) {
firstChunkSeen = true;
tryHideFullScreenLoading();
}
try { onChunk(chunk); } catch (e) { /* 忽略回调内部错误 */ }
}
return result;
} catch (err) {
throw err;
} finally {
tryHideFullScreenLoading();
}
}

66
src/components/doctorCheck/ButtonList.vue

@ -198,7 +198,7 @@
</template>
<script>
import { mapState, mapActions, mapMutations } from "vuex";
import { getapi, postapi, putapi, deletapi } from "@/api/api";
import { getapi, postapi, putapi, deletapi,fetchStream } from "@/api/api";
import { getPagePriv, checkPagePriv, deepCopy, arrayExistObj, objCopy, arrayFilter } from "../../utlis/proFunc";
import PatientRegisterEdit from "../../components/patientRegister/PatientRegisterEdit.vue";
@ -312,8 +312,6 @@ export default {
diagnosis: 'AI诊断信息',
rawText: "", // markdown
html: "", // HTML
typingIndex: 0,
typingTimer: null
},
};
@ -753,7 +751,7 @@ export default {
},
// AI AI
btnAIdiagnosis(again) {
async btnAIdiagnosis(again) {
if (!again) {
if (this.AI.visible) {
this.btnAImax(false)
@ -786,24 +784,50 @@ export default {
// }
// })
postapi('/api/app/AiMessageWs/GetAIMessageResult', { message })
.then(res => {
if (!res) return;
// 'data: '
let cleaned = String(res)
.replace(/data:\s*\[DONE\]/g, '')
.replace(/^data:\s*/gm, '')
.trim();
this.AI.rawText = '';
this.AI.html = '';
try {
await fetchStream('/api/app/AiMessageWs/GetAIMessageResult', { message }, (chunk) => {
// SSE
let cleaned = String(chunk).replace(/data:\s*\[DONE\]/g, '').replace(/^data:\s*/gm, '');
if (!cleaned) return;
this.AI.typingIndex = 0;
this.AI.visible = true;
this.AI.rawText = cleaned;
// typingIndex
if (!this.AI.typingIndex || this.AI.typingIndex < 0) this.AI.typingIndex = 0;
if (this.AI.typingIndex > this.AI.rawText.length) this.AI.typingIndex = 0;
this.startTyping();
this.btnAImax(false);
})
//
if (!this.AI.visible) {
this.AI.visible = true;
this.btnAImax(false);
}
this.AI.rawText += cleaned;
// Markdown
this.AI.html = md.render(this.AI.rawText);
//
this.$nextTick(() => {
try {
const el = this.$refs.aiContent;
if (el) el.scrollTop = el.scrollHeight;
} catch (e) {}
});
});
} catch (err) {
// 退axios/postapi
try {
const res = await postapi('/api/app/AiMessageWs/GetAIMessageResult', { message });
if (res) {
let cleaned = String(res).replace(/data:\s*\[DONE\]/g, '').replace(/^data:\s*/gm, '');
if (cleaned) {
// 退
if (!this.AI.visible) {
this.AI.visible = true;
this.btnAImax(false);
}
this.AI.rawText += cleaned;
this.AI.html = md.render(this.AI.rawText);
}
}
} catch (e) {
this.$message.error({ showClose: true, message: (err && err.message) || 'AI诊断请求失败' });
}
}
},
startTyping() {

128
src/components/sumDoctorCheck/ButtonList.vue

@ -129,7 +129,7 @@
<script>
import { mapState } from "vuex";
import { getapi, postapi, putapi, deletapi } from "@/api/api";
import { getapi, postapi, putapi, deletapi, fetchStream } from "@/api/api";
import { getPagePriv, checkPagePriv, deepCopy } from "../../utlis/proFunc";
import PatientRegisterList from "../doctorCheck/PatientRegisterList.vue";
@ -166,10 +166,8 @@ export default {
max: true,
visible: false,
diagnosis: 'AI诊断信息',
rawText: "", // markdown
html: "", // HTML
typingIndex: 0,
typingTimer: null
rawText: '',
html: ""
},
};
},
@ -440,61 +438,73 @@ export default {
},
// AI AI
btnAIdiagnosis(again) {
// AI AI使 fetchStream
async btnAIdiagnosis(again) {
if (!again) {
if (this.AI.visible) {
this.btnAImax(false)
return
this.btnAImax(false);
return;
}
}
let message = ''
let linkStr = ';'
let message = '';
let linkStr = ';';
this.sumDoctorCheck.summaryList.forEach(e => {
if (message) {
linkStr = ';'
linkStr = ';';
} else {
linkStr = ''
linkStr = '';
}
message += linkStr + e.summaryTitle + ':'
message += linkStr + e.summaryTitle + ':';
e.details.forEach((e1, i) => {
message += (i + 1) + ')、' + e1.summaryContent
message += (i + 1) + ')、' + e1.summaryContent;
});
});
message = '性别:' + this.doctorCheck.prBase.sexName + ',年龄:' + this.doctorCheck.prBase.age + '岁,检查结果:' + message
// postapi('/api/app/AIMessage/GetAIMessageResult', { message })
// .then(res => {
// if (res.code > -1) {
// this.AI.visible = true
// this.AI.diagnosis = res.data.result
// this.btnAImax(false)
// } else {
// this.$message.error({ showClose: true, message: res.message })
// }
// })
postapi('/api/app/AiMessageWs/GetAIMessageResult', { message })
.then(res => {
if (!res) return;
// 'data: '
let cleaned = String(res)
.replace(/data:\s*\[DONE\]/g, '')
.replace(/^data:\s*/gm, '')
.trim();
message = '性别:' + this.doctorCheck.prBase.sexName + ',年龄:' + this.doctorCheck.prBase.age + '岁,检查结果:' + message;
this.AI.rawText = '';
this.AI.html = '';
try {
await fetchStream('/api/app/AiMessageWs/GetAIMessageResult', { message }, (chunk) => {
// SSE
let cleaned = String(chunk).replace(/data:\s*\[DONE\]/g, '').replace(/^data:\s*/gm, '');
if (!cleaned) return;
this.AI.typingIndex = 0;
this.AI.visible = true;
this.AI.rawText = cleaned;
// typingIndex
if (!this.AI.typingIndex || this.AI.typingIndex < 0) this.AI.typingIndex = 0;
if (this.AI.typingIndex > this.AI.rawText.length) this.AI.typingIndex = 0;
this.startTyping();
this.btnAImax(false);
})
//
if (!this.AI.visible) {
this.AI.visible = true;
this.btnAImax(false);
}
this.AI.rawText += cleaned;
// Markdown
this.AI.html = md.render(this.AI.rawText);
//
this.$nextTick(() => {
try {
const el = this.$refs.aiContent;
if (el) el.scrollTop = el.scrollHeight;
} catch (e) {}
});
});
} catch (err) {
// 退axios/postapi
try {
const res = await postapi('/api/app/AiMessageWs/GetAIMessageResult', { message });
if (res) {
let cleaned = String(res).replace(/data:\s*\[DONE\]/g, '').replace(/^data:\s*/gm, '');
if (cleaned) {
// 退
if (!this.AI.visible) {
this.AI.visible = true;
this.btnAImax(false);
}
this.AI.rawText += cleaned;
this.AI.html = md.render(this.AI.rawText);
}
}
} catch (e) {
this.$message.error({ showClose: true, message: (err && err.message) || 'AI诊断请求失败' });
}
}
},
btnAImax(max) {
@ -507,30 +517,6 @@ export default {
this.AI.height = 24
}
},
startTyping() {
if (this.AI.typingTimer) clearInterval(this.AI.typingTimer);
this.AI.typingTimer = setInterval(() => {
if (this.AI.typingIndex < this.AI.rawText.length) {
const current = this.AI.rawText.slice(0, this.AI.typingIndex);
this.AI.html = md.render(current);
this.AI.typingIndex++;
//
this.$nextTick(() => {
try {
const el = this.$refs.aiContent;
if (el) {
el.scrollTop = el.scrollHeight;
}
} catch (e) {
// ignore
}
});
} else {
clearInterval(this.AI.typingTimer);
}
}, 30); // 30ms
},
//
audit() {
// dataTransOpts.tableS.patient_register.summaryDoctorId

2
src/views/Home.vue

@ -224,8 +224,6 @@ export default {
...mapState(["window", "dialogWin", "sysConfig"]),
},
created() {
// openedTabs
this.$set(this, 'openedTabs', {});
let expires_in = parseInt(window.sessionStorage.getItem("expires_in"));
//console.log("dqtime / expires_in",dqtime,expires_in)

Loading…
Cancel
Save