feat: 联调流式对话
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { FASTAPI_BASE_URL, authHeaders, readError, type ApiEnvelope } from './session'
|
||||
|
||||
export type ExamItem = {
|
||||
item_code: string
|
||||
item_name: string
|
||||
item_type: string
|
||||
}
|
||||
|
||||
export type ExamResult = ExamItem & {
|
||||
result_text: string
|
||||
result_structured?: Record<string, unknown>
|
||||
is_key?: boolean
|
||||
is_abnormal?: boolean
|
||||
context_written?: boolean
|
||||
already_ordered?: boolean
|
||||
}
|
||||
|
||||
type ExamListResponse = {
|
||||
items: ExamItem[]
|
||||
}
|
||||
|
||||
type ExamKind = 'physical-exams' | 'auxiliary-exams'
|
||||
|
||||
export function fetchPhysicalExamItems(sessionId: number) {
|
||||
return fetchExamItems(sessionId, 'physical-exams')
|
||||
}
|
||||
|
||||
export function fetchAuxiliaryExamItems(sessionId: number) {
|
||||
return fetchExamItems(sessionId, 'auxiliary-exams')
|
||||
}
|
||||
|
||||
export function orderPhysicalExamResult(sessionId: number, itemCode: string) {
|
||||
return orderExamResult(sessionId, 'physical-exams', itemCode)
|
||||
}
|
||||
|
||||
export function orderAuxiliaryExamResult(sessionId: number, itemCode: string) {
|
||||
return orderExamResult(sessionId, 'auxiliary-exams', itemCode)
|
||||
}
|
||||
|
||||
function assertSessionId(sessionId: number) {
|
||||
if (!Number.isInteger(sessionId) || sessionId <= 0) {
|
||||
throw new Error('未找到当前会话,请先生成模拟场景')
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchExamItems(sessionId: number, kind: ExamKind) {
|
||||
assertSessionId(sessionId)
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/sessions/${sessionId}/${kind}`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiEnvelope<ExamListResponse>
|
||||
if (result.code !== 'OK' || !Array.isArray(result.data?.items)) {
|
||||
throw new Error(result.message || '检查列表加载失败')
|
||||
}
|
||||
|
||||
return result.data.items
|
||||
}
|
||||
|
||||
async function orderExamResult(sessionId: number, kind: ExamKind, itemCode: string) {
|
||||
assertSessionId(sessionId)
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/sessions/${sessionId}/${kind}/${encodeURIComponent(itemCode)}`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiEnvelope<ExamResult>
|
||||
if (result.code !== 'OK' || !result.data?.item_code) {
|
||||
throw new Error(result.message || '检查结果获取失败')
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { FASTAPI_BASE_URL, authHeaders, readError, type ApiEnvelope } from './session'
|
||||
|
||||
export type LearningAssistantSession = {
|
||||
assistant_session_id: number | string
|
||||
title?: string
|
||||
}
|
||||
|
||||
type ChatStreamPayload = {
|
||||
question: string
|
||||
top_k?: number
|
||||
score_threshold?: number
|
||||
}
|
||||
|
||||
type StreamCallbacks = {
|
||||
onDelta: (delta: string) => void
|
||||
onDone?: (meta: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
export async function createLearningAssistantSession(title?: string) {
|
||||
const body = title?.trim()
|
||||
? {
|
||||
title: title.trim().slice(0, 100)
|
||||
}
|
||||
: {}
|
||||
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/learning-assistant/sessions`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const result = (await response.json()) as ApiEnvelope<Record<string, unknown>>
|
||||
if (result.code !== 'OK' || !result.data) {
|
||||
throw new Error(result.message || '新建会话失败')
|
||||
}
|
||||
|
||||
const assistantSessionId = readAssistantSessionId(result.data)
|
||||
if (!assistantSessionId) {
|
||||
throw new Error('新建会话返回缺少 assistant_session_id')
|
||||
}
|
||||
|
||||
return {
|
||||
...result.data,
|
||||
assistant_session_id: assistantSessionId
|
||||
} as LearningAssistantSession
|
||||
}
|
||||
|
||||
export async function streamLearningAssistantChat(
|
||||
assistantSessionId: number | string,
|
||||
payload: ChatStreamPayload,
|
||||
callbacks: StreamCallbacks,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const response = await fetch(`${FASTAPI_BASE_URL}/learning-assistant/sessions/${assistantSessionId}/chat/stream`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders('text/event-stream'),
|
||||
body: JSON.stringify(payload),
|
||||
signal
|
||||
})
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(await readError(response))
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const blocks = buffer.split('\n\n')
|
||||
buffer = blocks.pop() || ''
|
||||
|
||||
for (const block of blocks) {
|
||||
handleStreamBlock(block, callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
handleStreamBlock(buffer, callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
function readAssistantSessionId(data: Record<string, unknown>) {
|
||||
const value = data.assistant_session_id || data.session_id || data.id
|
||||
if (typeof value === 'number' || typeof value === 'string') return value
|
||||
return ''
|
||||
}
|
||||
|
||||
function handleStreamBlock(block: string, callbacks: StreamCallbacks) {
|
||||
const event = block.match(/^event:\s*(.+)$/m)?.[1] || ''
|
||||
const rawLines = block
|
||||
.split('\n')
|
||||
.filter(line => line.startsWith('data:'))
|
||||
.map(line => line.replace(/^data:\s?/, ''))
|
||||
|
||||
if (rawLines.length === 0) return
|
||||
|
||||
const rawData = rawLines.join('\n')
|
||||
if (rawData === '[DONE]') {
|
||||
callbacks.onDone?.({})
|
||||
return
|
||||
}
|
||||
|
||||
let data: unknown = rawData
|
||||
try {
|
||||
data = JSON.parse(rawData)
|
||||
} catch {}
|
||||
|
||||
if (event === 'error') {
|
||||
const message = typeof data === 'object' && data
|
||||
? (data as Record<string, unknown>).message
|
||||
: data
|
||||
throw new Error(typeof message === 'string' ? message : 'AI 学习助手回复失败')
|
||||
}
|
||||
|
||||
const delta = readDelta(data)
|
||||
if (delta) callbacks.onDelta(delta)
|
||||
|
||||
if (event === 'done' || event === 'message_done') {
|
||||
callbacks.onDone?.(typeof data === 'object' && data ? data as Record<string, unknown> : {})
|
||||
}
|
||||
}
|
||||
|
||||
function readDelta(data: unknown) {
|
||||
if (typeof data === 'string') return data
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
|
||||
const payload = data as Record<string, unknown>
|
||||
const delta = payload.delta || payload.content || payload.answer || payload.text
|
||||
if (typeof delta === 'string') return delta
|
||||
|
||||
const nestedData = payload.data
|
||||
if (nestedData && typeof nestedData === 'object') {
|
||||
const nested = nestedData as Record<string, unknown>
|
||||
const nestedDelta = nested.delta || nested.content || nested.answer || nested.text
|
||||
if (typeof nestedDelta === 'string') return nestedDelta
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
import{C as e}from"./index-CoO0Bu96.js";function t(){return Promise.resolve([{id:"case-31190016",title:"间断四肢多关节肿痛5年,加重1个月",patientName:"郭爱和",gender:"男",age:43,department:"风湿免疫科",scene:"门诊部",caseNo:"31190016",tone:"blue",mode:"training"},{id:"case-31180002",title:"右膝关节疼痛8年,腰背部疼痛2年",patientName:"索航",gender:"男",age:51,department:"风湿免疫科",scene:"住院部",caseNo:"31180002",tone:"teal",mode:"training"},{id:"case-2238015",title:"阴道不规则流血4月。",patientName:"韩爱利",gender:"女",age:52,department:"妇科",scene:"住院部",caseNo:"2238015",tone:"pink",mode:"training"},{id:"case-1006004",title:"持续胸痛3小时",patientName:"陈先生",gender:"男",age:60,department:"心血管内科",scene:"住院部",caseNo:"1006004",tone:"orange",mode:"teaching"},{id:"case-31190042",title:"咳嗽、咳痰10余年,加重1周",patientName:"厉明",gender:"男",age:52,department:"呼吸内科",scene:"普通门诊",caseNo:"31190042",tone:"purple",mode:"training"},{id:"case-2238019",title:"尿频、尿急、尿痛3天",patientName:"刘晓元",gender:"女",age:25,department:"泌尿外科",scene:"急诊留观",caseNo:"2238019",tone:"green",mode:"training"}])}function n(){const t=e("clinical-thinking-selected-case");return t&&"object"==typeof t?t:null}export{t as f,n as r};
|
||||
import{C as e}from"./index-pnQqyMoS.js";function t(){return Promise.resolve([{id:"case-31190016",title:"间断四肢多关节肿痛5年,加重1个月",patientName:"郭爱和",gender:"男",age:43,department:"风湿免疫科",scene:"门诊部",caseNo:"31190016",tone:"blue",mode:"training"},{id:"case-31180002",title:"右膝关节疼痛8年,腰背部疼痛2年",patientName:"索航",gender:"男",age:51,department:"风湿免疫科",scene:"住院部",caseNo:"31180002",tone:"teal",mode:"training"},{id:"case-2238015",title:"阴道不规则流血4月。",patientName:"韩爱利",gender:"女",age:52,department:"妇科",scene:"住院部",caseNo:"2238015",tone:"pink",mode:"training"},{id:"case-1006004",title:"持续胸痛3小时",patientName:"陈先生",gender:"男",age:60,department:"心血管内科",scene:"住院部",caseNo:"1006004",tone:"orange",mode:"teaching"},{id:"case-31190042",title:"咳嗽、咳痰10余年,加重1周",patientName:"厉明",gender:"男",age:52,department:"呼吸内科",scene:"普通门诊",caseNo:"31190042",tone:"purple",mode:"training"},{id:"case-2238019",title:"尿频、尿急、尿痛3天",patientName:"刘晓元",gender:"女",age:25,department:"泌尿外科",scene:"急诊留观",caseNo:"2238019",tone:"green",mode:"training"}])}function n(){const t=e("clinical-thinking-selected-case");return t&&"object"==typeof t?t:null}export{t as f,n as r};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{G as o,D as n,L as e}from"./index-CoO0Bu96.js";function i(n){var i;const l=e(),r=Boolean(null==(i=null==l?void 0:l.vnode.props)?void 0:i.onOpenProfile);return function(){r?n("open-profile"):o({url:"/pages/profile/profile"})}}function l(n){var i;const l=e(),r=Boolean(null==(i=null==l?void 0:l.vnode.props)?void 0:i.onOpenSettings);return function(){r?n("open-settings"):o({url:"/pages/config/config"})}}function r(o){var i;const l=e(),r=Boolean(null==(i=null==l?void 0:l.vnode.props)?void 0:i.onGoHome);return function(){r?o("go-home"):n({url:"/pages/home/home"})}}export{l as a,r as b,i as c};
|
||||
import{G as o,D as n,L as e}from"./index-pnQqyMoS.js";function i(n){var i;const l=e(),r=Boolean(null==(i=null==l?void 0:l.vnode.props)?void 0:i.onOpenProfile);return function(){r?n("open-profile"):o({url:"/pages/profile/profile"})}}function l(n){var i;const l=e(),r=Boolean(null==(i=null==l?void 0:l.vnode.props)?void 0:i.onOpenSettings);return function(){r?n("open-settings"):o({url:"/pages/config/config"})}}function r(o){var i;const l=e(),r=Boolean(null==(i=null==l?void 0:l.vnode.props)?void 0:i.onGoHome);return function(){r?o("go-home"):n({url:"/pages/home/home"})}}export{l as a,r as b,i as c};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{J as a,K as e,L as s,O as l,d as t,r as c,c as o,o as n,b as i,e as u,f as d,w as r,i as m,E as f,l as _,m as p,F as g,j as h,g as v,t as b,n as k,y as C,z as j,I as x,x as y,S as N,s as w,G as $}from"./index-CoO0Bu96.js";import{f as L}from"./cases.DfX6IxCO.js";import{c as V,a as I,b as S}from"./navigation.C05E413Y.js";import{_ as z}from"./_plugin-vue_export-helper.BCo6x5W8.js";const E=((l,t=0)=>(t,c=s())=>{!a&&e(l,t,c)})(l,2),F=z(t({__name:"cases",emits:["open-settings","open-profile","go-home"],setup(a,{emit:e}){const s=e,l=V(s),t=I(s),z=S(s),F=c([]),G=c(""),J=c(""),K=c(!1),O=c(""),U=o((()=>{const a=G.value.trim().toLowerCase(),e=O.value?F.value.filter((a=>a.mode===O.value)):F.value;return a?e.filter((e=>[e.title,e.patientName,e.gender,String(e.age),e.department,e.scene,e.caseNo].some((e=>e.toLowerCase().includes(a))))):e}));return E((a=>{const e=null==a?void 0:a.mode;"teaching"!==e&&"training"!==e||(O.value=e)})),n((function(){L().then((a=>{F.value=a}))})),i((()=>{})),(a,e)=>{const s=C,c=j,o=x,n=y,i=N;return u(),d(s,{class:"cases-page"},{default:r((()=>[m(s,{class:"case-shell"},{default:r((()=>[m(s,{class:"case-header"},{default:r((()=>[m(c,{class:"icon-button","aria-label":"设置",onClick:f(t)},{default:r((()=>[m(s,{class:"settings-icon"})])),_:1},8,["onClick"]),m(c,{class:"icon-button home-button","aria-label":"首页",onClick:f(z)},{default:r((()=>[m(s,{class:"home-icon"})])),_:1},8,["onClick"]),m(s,{class:"header-spacer"}),m(c,{class:"icon-button","aria-label":"个人中心",onClick:f(l)},{default:r((()=>[m(s,{class:"account-icon"})])),_:1},8,["onClick"])])),_:1}),m(i,{class:"case-content","scroll-y":""},{default:r((()=>[m(s,{class:"search-row"},{default:r((()=>[m(s,{class:"search-box"},{default:r((()=>[m(s,{class:"search-icon"}),m(o,{class:"search-input",modelValue:G.value,"onUpdate:modelValue":e[0]||(e[0]=a=>G.value=a),type:"text",placeholder:"科室、主诉模糊搜索","placeholder-class":"search-placeholder"},null,8,["modelValue"])])),_:1})])),_:1}),m(s,{class:"case-list"},{default:r((()=>[(u(!0),_(g,null,p(U.value,(a=>(u(),d(s,{key:a.id,class:k(["case-card",`mode-${a.mode}`]),onClick:e=>function(a){w("clinical-thinking-selected-case",a),w("clinical-thinking-case-mode",a.mode),$({url:"teaching"===a.mode?"/pages/teaching/teaching":"/pages/scenario/scenario"})}(a)},{default:r((()=>[m(s,{class:"case-main"},{default:r((()=>[m(s,{class:k(["patient-avatar",`avatar-${a.tone}`])},{default:r((()=>[m(n,null,{default:r((()=>[h(b(a.patientName.slice(0,1)),1)])),_:2},1024)])),_:2},1032,["class"]),m(s,{class:"case-info"},{default:r((()=>[m(n,{class:"case-title"},{default:r((()=>[h(b(a.title),1)])),_:2},1024),m(n,{class:"case-meta"},{default:r((()=>[h(b(a.patientName)+","+b(a.gender)+","+b(a.age)+"岁,"+b(a.department)+","+b(a.scene),1)])),_:2},1024)])),_:2},1024)])),_:2},1024),m(s,{class:"case-footer"},{default:r((()=>[m(n,{class:"case-no"},{default:r((()=>[h("病例编号: "+b(a.caseNo),1)])),_:2},1024),m(s,{class:k(["mode-badge",`mode-badge-${a.mode}`])},{default:r((()=>[m(s,{class:k(["mode-icon",`mode-icon-${a.mode}`])},null,8,["class"]),m(n,null,{default:r((()=>{return[h(b((e=a.mode,"teaching"===e?"教学模式":"训练模式")),1)];var e})),_:2},1024)])),_:2},1032,["class"])])),_:2},1024)])),_:2},1032,["class","onClick"])))),128)),0===U.value.length?(u(),d(s,{key:0,class:"empty-state"},{default:r((()=>[m(n,null,{default:r((()=>[h("暂无匹配病例")])),_:1})])),_:1})):v("",!0)])),_:1})])),_:1})])),_:1}),m(s,{class:k(["toast",{visible:K.value}])},{default:r((()=>[h(b(J.value),1)])),_:1},8,["class"])])),_:1})}}}),[["__scopeId","data-v-64c7a872"]]);export{F as default};
|
||||
import{J as a,K as e,L as s,O as l,d as t,r as c,c as o,o as n,b as i,e as u,f as d,w as r,i as m,E as f,l as _,m as p,F as g,j as h,g as v,t as b,n as k,y as C,z as j,I as x,x as y,S as N,s as w,G as $}from"./index-pnQqyMoS.js";import{f as L}from"./cases.4_d-snq4.js";import{c as V,a as I,b as S}from"./navigation.DVfJwOxJ.js";import{_ as z}from"./_plugin-vue_export-helper.BCo6x5W8.js";const E=((l,t=0)=>(t,c=s())=>{!a&&e(l,t,c)})(l,2),F=z(t({__name:"cases",emits:["open-settings","open-profile","go-home"],setup(a,{emit:e}){const s=e,l=V(s),t=I(s),z=S(s),F=c([]),G=c(""),J=c(""),K=c(!1),O=c(""),U=o((()=>{const a=G.value.trim().toLowerCase(),e=O.value?F.value.filter((a=>a.mode===O.value)):F.value;return a?e.filter((e=>[e.title,e.patientName,e.gender,String(e.age),e.department,e.scene,e.caseNo].some((e=>e.toLowerCase().includes(a))))):e}));return E((a=>{const e=null==a?void 0:a.mode;"teaching"!==e&&"training"!==e||(O.value=e)})),n((function(){L().then((a=>{F.value=a}))})),i((()=>{})),(a,e)=>{const s=C,c=j,o=x,n=y,i=N;return u(),d(s,{class:"cases-page"},{default:r((()=>[m(s,{class:"case-shell"},{default:r((()=>[m(s,{class:"case-header"},{default:r((()=>[m(c,{class:"icon-button","aria-label":"设置",onClick:f(t)},{default:r((()=>[m(s,{class:"settings-icon"})])),_:1},8,["onClick"]),m(c,{class:"icon-button home-button","aria-label":"首页",onClick:f(z)},{default:r((()=>[m(s,{class:"home-icon"})])),_:1},8,["onClick"]),m(s,{class:"header-spacer"}),m(c,{class:"icon-button","aria-label":"个人中心",onClick:f(l)},{default:r((()=>[m(s,{class:"account-icon"})])),_:1},8,["onClick"])])),_:1}),m(i,{class:"case-content","scroll-y":""},{default:r((()=>[m(s,{class:"search-row"},{default:r((()=>[m(s,{class:"search-box"},{default:r((()=>[m(s,{class:"search-icon"}),m(o,{class:"search-input",modelValue:G.value,"onUpdate:modelValue":e[0]||(e[0]=a=>G.value=a),type:"text",placeholder:"科室、主诉模糊搜索","placeholder-class":"search-placeholder"},null,8,["modelValue"])])),_:1})])),_:1}),m(s,{class:"case-list"},{default:r((()=>[(u(!0),_(g,null,p(U.value,(a=>(u(),d(s,{key:a.id,class:k(["case-card",`mode-${a.mode}`]),onClick:e=>function(a){w("clinical-thinking-selected-case",a),w("clinical-thinking-case-mode",a.mode),$({url:"teaching"===a.mode?"/pages/teaching/teaching":"/pages/scenario/scenario"})}(a)},{default:r((()=>[m(s,{class:"case-main"},{default:r((()=>[m(s,{class:k(["patient-avatar",`avatar-${a.tone}`])},{default:r((()=>[m(n,null,{default:r((()=>[h(b(a.patientName.slice(0,1)),1)])),_:2},1024)])),_:2},1032,["class"]),m(s,{class:"case-info"},{default:r((()=>[m(n,{class:"case-title"},{default:r((()=>[h(b(a.title),1)])),_:2},1024),m(n,{class:"case-meta"},{default:r((()=>[h(b(a.patientName)+","+b(a.gender)+","+b(a.age)+"岁,"+b(a.department)+","+b(a.scene),1)])),_:2},1024)])),_:2},1024)])),_:2},1024),m(s,{class:"case-footer"},{default:r((()=>[m(n,{class:"case-no"},{default:r((()=>[h("病例编号: "+b(a.caseNo),1)])),_:2},1024),m(s,{class:k(["mode-badge",`mode-badge-${a.mode}`])},{default:r((()=>[m(s,{class:k(["mode-icon",`mode-icon-${a.mode}`])},null,8,["class"]),m(n,null,{default:r((()=>{return[h(b((e=a.mode,"teaching"===e?"教学模式":"训练模式")),1)];var e})),_:2},1024)])),_:2},1032,["class"])])),_:2},1024)])),_:2},1032,["class","onClick"])))),128)),0===U.value.length?(u(),d(s,{key:0,class:"empty-state"},{default:r((()=>[m(n,null,{default:r((()=>[h("暂无匹配病例")])),_:1})])),_:1})):v("",!0)])),_:1})])),_:1})])),_:1}),m(s,{class:k(["toast",{visible:K.value}])},{default:r((()=>[h(b(J.value),1)])),_:1},8,["class"])])),_:1})}}}),[["__scopeId","data-v-64c7a872"]]);export{F as default};
|
||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{d as s,a,r as e,o as l,b as t,e as i,f as n,w as o,i as c,E as u,j as d,g as r,t as f,l as m,m as g,F as _,n as b,s as p,G as h,y as v,z as k,x as y,u as j}from"./index-CoO0Bu96.js";import{_ as C}from"./config-doctor.TgARj_nM.js";import{c as I,a as w}from"./navigation.C05E413Y.js";import{_ as x}from"./_plugin-vue_export-helper.BCo6x5W8.js";const T=x(s({__name:"home",emits:["open-settings","open-profile"],setup(s,{emit:x}){const T=x,A=I(T),M=w(T),D=a({greeting:"下午好,医生。",highlight:"让我们继续提升您的临床思维能力吧。",remainingModules:3,doctorName:"王主任"}),E=[{title:"精准补强·薄弱环节训练",icon:"trend-icon"},{title:"实战进阶·科室专项训练",icon:"notes-icon"},{title:"新手入门·教学互动模式模式训练",icon:"school-icon"},{title:"精益管理·老师针对性任务训练",icon:"admin-icon"}],F=e(!1),N=e(""),O=e(!1);let P=null;function S(){F.value||(F.value=!0,Promise.resolve({sessionId:`mock-session-${Date.now()}`,startedAt:(new Date).toISOString()}).then((s=>{p("clinical-thinking-session",s),h({url:"/pages/matching/matching"})})).catch((s=>{!function(s){P&&clearTimeout(P);N.value=s,O.value=!0,P=setTimeout((()=>{O.value=!1}),2200)}(s instanceof Error?s.message:"进入训练失败")})).finally((()=>{setTimeout((()=>{F.value=!1}),300)})))}function z(){h({url:"/pages/learning-assistant/learning-assistant"})}return l((function(){Promise.resolve({greeting:"下午好,医生。",highlight:"让我们继续提升您的临床思维能力吧。",remainingModules:3,doctorName:"王主任"}).then((s=>{Object.assign(D,s)}))})),t((()=>{P&&clearTimeout(P)})),(s,a)=>{const e=v,l=k,t=y,p=j;return i(),n(e,{class:"home-page"},{default:o((()=>[c(e,{class:"home-shell"},{default:o((()=>[c(e,{class:"top-bar"},{default:o((()=>[c(l,{class:"icon-button","aria-label":"配置",onClick:u(M)},{default:o((()=>[c(e,{class:"settings-icon"})])),_:1},8,["onClick"]),c(e,{class:"top-spacer"}),c(l,{class:"icon-button","aria-label":"个人中心",onClick:u(A)},{default:o((()=>[c(e,{class:"account-icon"})])),_:1},8,["onClick"])])),_:1}),c(e,{class:"home-main"},{default:o((()=>[c(e,{class:"speech-bubble"},{default:o((()=>[c(t,{class:"bubble-copy"},{default:o((()=>[d("下午好,医生。准备好开始今天的")])),_:1}),c(t,{class:"bubble-strong"},{default:o((()=>[d("带教模拟")])),_:1}),c(t,{class:"bubble-copy"},{default:o((()=>[d(",精进")])),_:1}),c(t,{class:"bubble-highlight"},{default:o((()=>[d("临床思维")])),_:1}),c(t,{class:"bubble-copy"},{default:o((()=>[d("了吗?")])),_:1})])),_:1}),c(e,{class:"doctor-stage"},{default:o((()=>[c(e,{class:"doctor-shadow"}),c(p,{class:"director-image",src:C,mode:"aspectFit"})])),_:1}),c(e,{class:"training-panel"},{default:o((()=>[c(e,{class:"primary-action"},{default:o((()=>[c(l,{class:"start-button",disabled:F.value,onClick:S},{default:o((()=>[F.value?(i(),n(e,{key:0,class:"spinner"})):r("",!0),c(t,null,{default:o((()=>[d(f(F.value?"正在进入...":"开始训练"),1)])),_:1})])),_:1},8,["disabled"]),c(t,{class:"remaining"},{default:o((()=>[d("今日剩余:"+f(D.remainingModules)+"个模块",1)])),_:1})])),_:1}),c(e,{class:"module-grid"},{default:o((()=>[(i(),m(_,null,g(E,(s=>c(l,{key:s.title,class:"module-card",onClick:S},{default:o((()=>[c(e,{class:b(["module-icon",s.icon])},null,8,["class"]),c(t,{class:"module-title"},{default:o((()=>[d(f(s.title),1)])),_:2},1024)])),_:2},1024))),64))])),_:1}),c(e,{class:"assistant-actions"},{default:o((()=>[c(l,{class:"assistant-button",onClick:z},{default:o((()=>[c(e,{class:"assistant-icon chat-icon"}),c(t,null,{default:o((()=>[d("AI 学习助手(医院知识库)")])),_:1})])),_:1}),c(l,{class:"assistant-button disabled",disabled:"","aria-disabled":"true"},{default:o((()=>[c(e,{class:"assistant-icon forum-icon"}),c(t,null,{default:o((()=>[d("方老师AI教学助手沟通")])),_:1})])),_:1})])),_:1})])),_:1})])),_:1})])),_:1}),c(e,{class:b(["toast",{visible:O.value}])},{default:o((()=>[d(f(N.value),1)])),_:1},8,["class"])])),_:1})}}}),[["__scopeId","data-v-cfd573f5"]]);export{T as default};
|
||||
import{d as s,a,r as e,o as l,b as t,e as i,f as n,w as o,i as c,E as u,j as d,g as r,t as f,l as m,m as g,F as _,n as b,s as p,G as h,y as v,z as k,x as y,u as j}from"./index-pnQqyMoS.js";import{_ as C}from"./config-doctor.TgARj_nM.js";import{c as I,a as w}from"./navigation.DVfJwOxJ.js";import{_ as x}from"./_plugin-vue_export-helper.BCo6x5W8.js";const T=x(s({__name:"home",emits:["open-settings","open-profile"],setup(s,{emit:x}){const T=x,A=I(T),M=w(T),D=a({greeting:"下午好,医生。",highlight:"让我们继续提升您的临床思维能力吧。",remainingModules:3,doctorName:"王主任"}),E=[{title:"精准补强·薄弱环节训练",icon:"trend-icon"},{title:"实战进阶·科室专项训练",icon:"notes-icon"},{title:"新手入门·教学互动模式模式训练",icon:"school-icon"},{title:"精益管理·老师针对性任务训练",icon:"admin-icon"}],F=e(!1),N=e(""),O=e(!1);let P=null;function S(){F.value||(F.value=!0,Promise.resolve({sessionId:`mock-session-${Date.now()}`,startedAt:(new Date).toISOString()}).then((s=>{p("clinical-thinking-session",s),h({url:"/pages/matching/matching"})})).catch((s=>{!function(s){P&&clearTimeout(P);N.value=s,O.value=!0,P=setTimeout((()=>{O.value=!1}),2200)}(s instanceof Error?s.message:"进入训练失败")})).finally((()=>{setTimeout((()=>{F.value=!1}),300)})))}function z(){h({url:"/pages/learning-assistant/learning-assistant"})}return l((function(){Promise.resolve({greeting:"下午好,医生。",highlight:"让我们继续提升您的临床思维能力吧。",remainingModules:3,doctorName:"王主任"}).then((s=>{Object.assign(D,s)}))})),t((()=>{P&&clearTimeout(P)})),(s,a)=>{const e=v,l=k,t=y,p=j;return i(),n(e,{class:"home-page"},{default:o((()=>[c(e,{class:"home-shell"},{default:o((()=>[c(e,{class:"top-bar"},{default:o((()=>[c(l,{class:"icon-button","aria-label":"配置",onClick:u(M)},{default:o((()=>[c(e,{class:"settings-icon"})])),_:1},8,["onClick"]),c(e,{class:"top-spacer"}),c(l,{class:"icon-button","aria-label":"个人中心",onClick:u(A)},{default:o((()=>[c(e,{class:"account-icon"})])),_:1},8,["onClick"])])),_:1}),c(e,{class:"home-main"},{default:o((()=>[c(e,{class:"speech-bubble"},{default:o((()=>[c(t,{class:"bubble-copy"},{default:o((()=>[d("下午好,医生。准备好开始今天的")])),_:1}),c(t,{class:"bubble-strong"},{default:o((()=>[d("带教模拟")])),_:1}),c(t,{class:"bubble-copy"},{default:o((()=>[d(",精进")])),_:1}),c(t,{class:"bubble-highlight"},{default:o((()=>[d("临床思维")])),_:1}),c(t,{class:"bubble-copy"},{default:o((()=>[d("了吗?")])),_:1})])),_:1}),c(e,{class:"doctor-stage"},{default:o((()=>[c(e,{class:"doctor-shadow"}),c(p,{class:"director-image",src:C,mode:"aspectFit"})])),_:1}),c(e,{class:"training-panel"},{default:o((()=>[c(e,{class:"primary-action"},{default:o((()=>[c(l,{class:"start-button",disabled:F.value,onClick:S},{default:o((()=>[F.value?(i(),n(e,{key:0,class:"spinner"})):r("",!0),c(t,null,{default:o((()=>[d(f(F.value?"正在进入...":"开始训练"),1)])),_:1})])),_:1},8,["disabled"]),c(t,{class:"remaining"},{default:o((()=>[d("今日剩余:"+f(D.remainingModules)+"个模块",1)])),_:1})])),_:1}),c(e,{class:"module-grid"},{default:o((()=>[(i(),m(_,null,g(E,(s=>c(l,{key:s.title,class:"module-card",onClick:S},{default:o((()=>[c(e,{class:b(["module-icon",s.icon])},null,8,["class"]),c(t,{class:"module-title"},{default:o((()=>[d(f(s.title),1)])),_:2},1024)])),_:2},1024))),64))])),_:1}),c(e,{class:"assistant-actions"},{default:o((()=>[c(l,{class:"assistant-button",onClick:z},{default:o((()=>[c(e,{class:"assistant-icon chat-icon"}),c(t,null,{default:o((()=>[d("AI 学习助手(医院知识库)")])),_:1})])),_:1}),c(l,{class:"assistant-button disabled",disabled:"","aria-disabled":"true"},{default:o((()=>[c(e,{class:"assistant-icon forum-icon"}),c(t,null,{default:o((()=>[d("方老师AI教学助手沟通")])),_:1})])),_:1})])),_:1})])),_:1})])),_:1})])),_:1}),c(e,{class:b(["toast",{visible:O.value}])},{default:o((()=>[d(f(N.value),1)])),_:1},8,["class"])])),_:1})}}}),[["__scopeId","data-v-cfd573f5"]]);export{T as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{d as a,a as s,r as e,o as l,b as t,e as r,f as n,w as c,i as o,l as i,m as u,F as d,j as g,t as p,H as f,p as m,y as _,x as b,u as v,n as h}from"./index-CoO0Bu96.js";import{_ as y}from"./config-doctor.TgARj_nM.js";import{_ as x}from"./_plugin-vue_export-helper.BCo6x5W8.js";const $=x(a({__name:"matching",setup(a){const x=s({message:"王主任正在为您智能匹配病例",subtitle:"正在通过大模型计算最适合您的临床案例库...",progressTarget:92,tags:[]}),$=e([]),I=e(0);let M=0,w=null,j=null;function k(){Promise.resolve({message:"王主任正在为您智能匹配病例",subtitle:"正在通过大模型计算最适合您的临床案例库...",progressTarget:92,tags:[{label:"薄弱环节",tone:"secondary"},{label:"主治医级别",tone:"primary"},{label:"高匹配度",tone:"tertiary"},{label:"基于历史偏好",tone:"neutral"}]}).then((a=>{Object.assign(x,a),function(){j&&clearInterval(j);I.value=0;const a=Date.now();j=setInterval((()=>{const s=Date.now()-a,e=Math.min(1,s/1e4);if(I.value=Math.round(x.progressTarget*e),e>=1)return j&&clearInterval(j),j=null,void m({url:"/pages/cases/cases"})}),100)}()}))}function T(){const a=M++,s=256*Math.random(),e=256*Math.random(),l=150*(Math.random()-.5),t=150*(Math.random()-.5),r=2+3*Math.random();$.value.push({id:a,style:{left:`${s}px`,top:`${e}px`,"--particle-x":`${l}px`,"--particle-y":`${t}px`,animationDuration:`${r}s`}}),setTimeout((()=>{$.value=$.value.filter((s=>s.id!==a))}),1e3*r)}return l((()=>{k(),function(){for(let a=0;a<12;a+=1)T();w=setInterval(T,300)}()})),t((()=>{w&&clearInterval(w),j&&clearInterval(j)})),(a,s)=>{const e=_,l=b,t=v;return r(),n(e,{class:"matching-page"},{default:c((()=>[o(e,{class:"matching-shell"},{default:c((()=>[o(e,{class:"top-visual"},{default:c((()=>[o(e,{class:"network"},{default:c((()=>[o(e,{class:"ring ring-large"}),o(e,{class:"ring ring-middle"}),o(e,{class:"ring ring-small"}),o(e,{class:"node node-top"}),o(e,{class:"node node-left"}),o(e,{class:"node node-right"}),(r(!0),i(d,null,u($.value,(a=>(r(),n(e,{key:a.id,class:"particle",style:f(a.style)},null,8,["style"])))),128))])),_:1})])),_:1}),o(e,{class:"middle-visual"},{default:c((()=>[o(e,{class:"match-bubble"},{default:c((()=>[o(l,null,{default:c((()=>[g(p(x.message),1)])),_:1}),o(l,{class:"typing-dots"}),o(e,{class:"bubble-tail"})])),_:1}),o(e,{class:"director-card"},{default:c((()=>[o(t,{class:"director-image",src:y,mode:"aspectFit"})])),_:1}),o(e,{class:"intelligence-area"},{default:c((()=>[o(e,{class:"scan-circle"},{default:c((()=>[o(e,{class:"pulse-ring ring-one"}),o(e,{class:"pulse-ring ring-two"}),o(e,{class:"brain-core"},{default:c((()=>[o(e,{class:"scan-bar"}),o(e,{class:"brain-icon"})])),_:1})])),_:1}),(r(!0),i(d,null,u(x.tags,((a,s)=>(r(),n(e,{key:a.label,class:h(["float-tag",[`tag-${a.tone}`,`tag-pos-${s}`]])},{default:c((()=>[o(l,null,{default:c((()=>[g(p(a.label),1)])),_:2},1024)])),_:2},1032,["class"])))),128))])),_:1})])),_:1}),o(e,{class:"bottom-progress"},{default:c((()=>[o(e,{class:"progress-track"},{default:c((()=>[o(e,{class:"progress-fill",style:f({width:`${I.value}%`})},null,8,["style"])])),_:1}),o(l,{class:"progress-subtitle"},{default:c((()=>[g(p(x.subtitle),1)])),_:1}),o(e,{class:"security-icon"})])),_:1})])),_:1})])),_:1})}}}),[["__scopeId","data-v-a610ab33"]]);export{$ as default};
|
||||
import{d as a,a as s,r as e,o as l,b as t,e as r,f as n,w as c,i as o,l as i,m as u,F as d,j as g,t as p,H as f,p as m,y as _,x as b,u as v,n as h}from"./index-pnQqyMoS.js";import{_ as y}from"./config-doctor.TgARj_nM.js";import{_ as x}from"./_plugin-vue_export-helper.BCo6x5W8.js";const $=x(a({__name:"matching",setup(a){const x=s({message:"王主任正在为您智能匹配病例",subtitle:"正在通过大模型计算最适合您的临床案例库...",progressTarget:92,tags:[]}),$=e([]),I=e(0);let M=0,w=null,j=null;function k(){Promise.resolve({message:"王主任正在为您智能匹配病例",subtitle:"正在通过大模型计算最适合您的临床案例库...",progressTarget:92,tags:[{label:"薄弱环节",tone:"secondary"},{label:"主治医级别",tone:"primary"},{label:"高匹配度",tone:"tertiary"},{label:"基于历史偏好",tone:"neutral"}]}).then((a=>{Object.assign(x,a),function(){j&&clearInterval(j);I.value=0;const a=Date.now();j=setInterval((()=>{const s=Date.now()-a,e=Math.min(1,s/1e4);if(I.value=Math.round(x.progressTarget*e),e>=1)return j&&clearInterval(j),j=null,void m({url:"/pages/cases/cases"})}),100)}()}))}function T(){const a=M++,s=256*Math.random(),e=256*Math.random(),l=150*(Math.random()-.5),t=150*(Math.random()-.5),r=2+3*Math.random();$.value.push({id:a,style:{left:`${s}px`,top:`${e}px`,"--particle-x":`${l}px`,"--particle-y":`${t}px`,animationDuration:`${r}s`}}),setTimeout((()=>{$.value=$.value.filter((s=>s.id!==a))}),1e3*r)}return l((()=>{k(),function(){for(let a=0;a<12;a+=1)T();w=setInterval(T,300)}()})),t((()=>{w&&clearInterval(w),j&&clearInterval(j)})),(a,s)=>{const e=_,l=b,t=v;return r(),n(e,{class:"matching-page"},{default:c((()=>[o(e,{class:"matching-shell"},{default:c((()=>[o(e,{class:"top-visual"},{default:c((()=>[o(e,{class:"network"},{default:c((()=>[o(e,{class:"ring ring-large"}),o(e,{class:"ring ring-middle"}),o(e,{class:"ring ring-small"}),o(e,{class:"node node-top"}),o(e,{class:"node node-left"}),o(e,{class:"node node-right"}),(r(!0),i(d,null,u($.value,(a=>(r(),n(e,{key:a.id,class:"particle",style:f(a.style)},null,8,["style"])))),128))])),_:1})])),_:1}),o(e,{class:"middle-visual"},{default:c((()=>[o(e,{class:"match-bubble"},{default:c((()=>[o(l,null,{default:c((()=>[g(p(x.message),1)])),_:1}),o(l,{class:"typing-dots"}),o(e,{class:"bubble-tail"})])),_:1}),o(e,{class:"director-card"},{default:c((()=>[o(t,{class:"director-image",src:y,mode:"aspectFit"})])),_:1}),o(e,{class:"intelligence-area"},{default:c((()=>[o(e,{class:"scan-circle"},{default:c((()=>[o(e,{class:"pulse-ring ring-one"}),o(e,{class:"pulse-ring ring-two"}),o(e,{class:"brain-core"},{default:c((()=>[o(e,{class:"scan-bar"}),o(e,{class:"brain-icon"})])),_:1})])),_:1}),(r(!0),i(d,null,u(x.tags,((a,s)=>(r(),n(e,{key:a.label,class:h(["float-tag",[`tag-${a.tone}`,`tag-pos-${s}`]])},{default:c((()=>[o(l,null,{default:c((()=>[g(p(a.label),1)])),_:2},1024)])),_:2},1032,["class"])))),128))])),_:1})])),_:1}),o(e,{class:"bottom-progress"},{default:c((()=>[o(e,{class:"progress-track"},{default:c((()=>[o(e,{class:"progress-fill",style:f({width:`${I.value}%`})},null,8,["style"])])),_:1}),o(l,{class:"progress-subtitle"},{default:c((()=>[g(p(x.subtitle),1)])),_:1}),o(e,{class:"security-icon"})])),_:1})])),_:1})])),_:1})}}}),[["__scopeId","data-v-a610ab33"]]);export{$ as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{d as a,r as e,c as s,b as l,e as t,f as c,w as r,i as d,N as o,j as u,l as n,m as i,F as f,g as _,n as p,t as m,P as b,Q as h,D as y,y as v,z as g,x as k,I as x,S as j,k as w,G as C}from"./index-CoO0Bu96.js";import{_ as V}from"./_plugin-vue_export-helper.BCo6x5W8.js";const I=V(a({__name:"profile-records",setup(a){const V=e(""),I=e(""),z=e(!1),D=[{label:"总病例",value:"12"},{label:"总时长",value:"128h"},{label:"平均正确率",value:"92%",secondary:!0}],F=[{title:"急性心肌梗死",department:"心内科",date:"2023-11-20",score:"98",abbr:"心",tone:"primary"},{title:"缺血性脑卒中",department:"神经内科",date:"2023-11-18",score:"85",abbr:"神",tone:"secondary"},{title:"重症肺炎伴呼吸衰竭",department:"呼吸科",date:"2023-11-15",score:"92",abbr:"肺",tone:"tertiary"},{title:"急性胰腺炎",department:"消化内科",date:"2023-11-12",score:"78",abbr:"消",tone:"primary",dimmed:!0},{title:"糖尿病肾病五期",department:"肾内科",date:"2023-11-10",score:"95",abbr:"肾",tone:"secondary",dimmed:!0}],G=s((()=>{const a=V.value.trim();return a?F.filter((e=>[e.title,e.department,e.date].some((e=>e.includes(a))))):F}));function N(){"function"==typeof b&&b().length>1?h():y({url:"/pages/profile/profile"})}function P(){C({url:"/pages/assessment/assessment"})}return l((()=>{})),(a,e)=>{const s=v,l=g,b=k,h=x,y=j;return t(),c(s,{class:"records-page"},{default:r((()=>[d(s,{class:"records-shell"},{default:r((()=>[o("header",{class:"top-bar"},[d(l,{class:"icon-button","aria-label":"返回",onClick:N},{default:r((()=>[d(s,{class:"back-icon"})])),_:1}),d(b,{class:"page-title"},{default:r((()=>[u("学习记录")])),_:1})]),d(y,{class:"records-scroll","scroll-y":""},{default:r((()=>[o("main",{class:"records-main"},[o("section",{class:"stats-grid"},[(t(),n(f,null,i(D,(a=>d(s,{key:a.label,class:"stat-card"},{default:r((()=>[d(b,{class:"stat-label"},{default:r((()=>[u(m(a.label),1)])),_:2},1024),d(b,{class:p(["stat-value",{secondary:a.secondary}])},{default:r((()=>[u(m(a.value),1)])),_:2},1032,["class"])])),_:2},1024))),64))]),o("section",{class:"search-section"},[d(s,{class:"search-field"},{default:r((()=>[d(s,{class:"search-icon"}),d(h,{modelValue:V.value,"onUpdate:modelValue":e[0]||(e[0]=a=>V.value=a),class:"search-input",placeholder:"搜索病例标题或科室...","placeholder-class":"search-placeholder",type:"text"},null,8,["modelValue"])])),_:1})]),o("section",{class:"history-section"},[d(b,{class:"section-title"},{default:r((()=>[u("最近训练")])),_:1}),d(s,{class:"record-list"},{default:r((()=>[(t(!0),n(f,null,i(G.value,(a=>(t(),c(s,{key:a.title,class:p(["record-card",{dimmed:a.dimmed}]),onClick:P},{default:r((()=>[d(s,{class:p(["case-icon-wrap",a.tone])},{default:r((()=>[d(b,{class:"case-icon-text"},{default:r((()=>[u(m(a.abbr),1)])),_:2},1024)])),_:2},1032,["class"]),d(s,{class:"case-copy"},{default:r((()=>[d(b,{class:"case-title"},{default:r((()=>[u(m(a.title),1)])),_:2},1024),d(s,{class:"case-meta"},{default:r((()=>[d(b,null,{default:r((()=>[u(m(a.department),1)])),_:2},1024),d(b,{class:"dot"},{default:r((()=>[u("•")])),_:1}),d(b,null,{default:r((()=>[u(m(a.date),1)])),_:2},1024)])),_:2},1024)])),_:2},1024),d(s,{class:"score-block"},{default:r((()=>[d(s,{class:"score-row"},{default:r((()=>[d(b,{class:"score-value"},{default:r((()=>[u(m(a.score),1)])),_:2},1024),d(b,{class:"score-unit"},{default:r((()=>[u("分")])),_:1})])),_:2},1024),d(l,{class:"report-button",onClick:w(P,["stop"])},{default:r((()=>[d(b,null,{default:r((()=>[u("查看报告")])),_:1}),d(s,{class:"small-chevron"})])),_:1})])),_:2},1024)])),_:2},1032,["class"])))),128)),0===G.value.length?(t(),c(s,{key:0,class:"empty-state"},{default:r((()=>[d(b,null,{default:r((()=>[u("没有找到匹配的训练记录")])),_:1})])),_:1})):_("",!0)])),_:1})]),d(s,{class:"bottom-hint"},{default:r((()=>[d(b,null,{default:r((()=>[u("已经到底啦")])),_:1})])),_:1})])])),_:1}),d(s,{class:p(["toast",{visible:z.value}])},{default:r((()=>[u(m(I.value),1)])),_:1},8,["class"])])),_:1})])),_:1})}}}),[["__scopeId","data-v-15539840"]]);export{I as default};
|
||||
import{d as a,r as e,c as s,b as l,e as t,f as c,w as r,i as d,M as o,j as u,l as n,m as i,F as f,g as _,n as p,t as m,N as b,P as h,D as y,y as v,z as g,x as k,I as x,S as j,k as w,G as C}from"./index-pnQqyMoS.js";import{_ as V}from"./_plugin-vue_export-helper.BCo6x5W8.js";const I=V(a({__name:"profile-records",setup(a){const V=e(""),I=e(""),z=e(!1),D=[{label:"总病例",value:"12"},{label:"总时长",value:"128h"},{label:"平均正确率",value:"92%",secondary:!0}],F=[{title:"急性心肌梗死",department:"心内科",date:"2023-11-20",score:"98",abbr:"心",tone:"primary"},{title:"缺血性脑卒中",department:"神经内科",date:"2023-11-18",score:"85",abbr:"神",tone:"secondary"},{title:"重症肺炎伴呼吸衰竭",department:"呼吸科",date:"2023-11-15",score:"92",abbr:"肺",tone:"tertiary"},{title:"急性胰腺炎",department:"消化内科",date:"2023-11-12",score:"78",abbr:"消",tone:"primary",dimmed:!0},{title:"糖尿病肾病五期",department:"肾内科",date:"2023-11-10",score:"95",abbr:"肾",tone:"secondary",dimmed:!0}],G=s((()=>{const a=V.value.trim();return a?F.filter((e=>[e.title,e.department,e.date].some((e=>e.includes(a))))):F}));function M(){"function"==typeof b&&b().length>1?h():y({url:"/pages/profile/profile"})}function N(){C({url:"/pages/assessment/assessment"})}return l((()=>{})),(a,e)=>{const s=v,l=g,b=k,h=x,y=j;return t(),c(s,{class:"records-page"},{default:r((()=>[d(s,{class:"records-shell"},{default:r((()=>[o("header",{class:"top-bar"},[d(l,{class:"icon-button","aria-label":"返回",onClick:M},{default:r((()=>[d(s,{class:"back-icon"})])),_:1}),d(b,{class:"page-title"},{default:r((()=>[u("学习记录")])),_:1})]),d(y,{class:"records-scroll","scroll-y":""},{default:r((()=>[o("main",{class:"records-main"},[o("section",{class:"stats-grid"},[(t(),n(f,null,i(D,(a=>d(s,{key:a.label,class:"stat-card"},{default:r((()=>[d(b,{class:"stat-label"},{default:r((()=>[u(m(a.label),1)])),_:2},1024),d(b,{class:p(["stat-value",{secondary:a.secondary}])},{default:r((()=>[u(m(a.value),1)])),_:2},1032,["class"])])),_:2},1024))),64))]),o("section",{class:"search-section"},[d(s,{class:"search-field"},{default:r((()=>[d(s,{class:"search-icon"}),d(h,{modelValue:V.value,"onUpdate:modelValue":e[0]||(e[0]=a=>V.value=a),class:"search-input",placeholder:"搜索病例标题或科室...","placeholder-class":"search-placeholder",type:"text"},null,8,["modelValue"])])),_:1})]),o("section",{class:"history-section"},[d(b,{class:"section-title"},{default:r((()=>[u("最近训练")])),_:1}),d(s,{class:"record-list"},{default:r((()=>[(t(!0),n(f,null,i(G.value,(a=>(t(),c(s,{key:a.title,class:p(["record-card",{dimmed:a.dimmed}]),onClick:N},{default:r((()=>[d(s,{class:p(["case-icon-wrap",a.tone])},{default:r((()=>[d(b,{class:"case-icon-text"},{default:r((()=>[u(m(a.abbr),1)])),_:2},1024)])),_:2},1032,["class"]),d(s,{class:"case-copy"},{default:r((()=>[d(b,{class:"case-title"},{default:r((()=>[u(m(a.title),1)])),_:2},1024),d(s,{class:"case-meta"},{default:r((()=>[d(b,null,{default:r((()=>[u(m(a.department),1)])),_:2},1024),d(b,{class:"dot"},{default:r((()=>[u("•")])),_:1}),d(b,null,{default:r((()=>[u(m(a.date),1)])),_:2},1024)])),_:2},1024)])),_:2},1024),d(s,{class:"score-block"},{default:r((()=>[d(s,{class:"score-row"},{default:r((()=>[d(b,{class:"score-value"},{default:r((()=>[u(m(a.score),1)])),_:2},1024),d(b,{class:"score-unit"},{default:r((()=>[u("分")])),_:1})])),_:2},1024),d(l,{class:"report-button",onClick:w(N,["stop"])},{default:r((()=>[d(b,null,{default:r((()=>[u("查看报告")])),_:1}),d(s,{class:"small-chevron"})])),_:1})])),_:2},1024)])),_:2},1032,["class"])))),128)),0===G.value.length?(t(),c(s,{key:0,class:"empty-state"},{default:r((()=>[d(b,null,{default:r((()=>[u("没有找到匹配的训练记录")])),_:1})])),_:1})):_("",!0)])),_:1})]),d(s,{class:"bottom-hint"},{default:r((()=>[d(b,null,{default:r((()=>[u("已经到底啦")])),_:1})])),_:1})])])),_:1}),d(s,{class:p(["toast",{visible:z.value}])},{default:r((()=>[u(m(I.value),1)])),_:1},8,["class"])])),_:1})])),_:1})}}}),[["__scopeId","data-v-15539840"]]);export{I as default};
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{C as e,s as t}from"./index-CoO0Bu96.js";const n="/fastapi/api/v1";function s(){const t=e("clinical-thinking-access-token");if("string"!=typeof t||!t.trim())throw new Error("登录已过期,请重新登录");return t}function o(e="application/json"){return{"Content-Type":"application/json",Accept:e,Authorization:`Bearer ${s()}`,"X-Entry-Scene":"vue_frontend"}}async function a(e){const t=await e.text().catch((()=>""));if(!t)return`请求失败(${e.status})`;try{const e=JSON.parse(t),n=e.message||e.detail||e.error;if("string"==typeof n&&n.trim())return n}catch{}return t}function r(){const t=e("clinical-thinking-scenario");return t&&"object"==typeof t?t:null}function i(){var e,t;const n=null==(t=null==(e=r())?void 0:e.session)?void 0:t.session_id;if("number"==typeof n&&Number.isInteger(n)&&n>0)return n;throw new Error("未找到当前会话,请先生成模拟场景")}function c(e){const n=r();(null==n?void 0:n.session)&&t("clinical-thinking-scenario",{...n,session:{...n.session,status:e}})}async function f(e){var t;const n=await fetch("/fastapi/api/v1/sessions",{method:"POST",headers:o(),body:JSON.stringify(e)});if(!n.ok)throw new Error(await a(n));const s=await n.json();if("OK"!==s.code||!(null==(t=s.data)?void 0:t.session_id))throw new Error(s.message||"新建会话失败");return s.data}async function d(e){var t;const n=await fetch(`/fastapi/api/v1/sessions/${e}/complete-inquiry`,{method:"POST",headers:o()});if(!n.ok)throw new Error(await a(n));const s=await n.json();if("OK"!==s.code||!(null==(t=s.data)?void 0:t.session_id))throw new Error(s.message||"完成采集失败");return s.data}async function l(e,t,n,s){var r,i,c;const f=await fetch(`/fastapi/api/v1/sessions/${e}/chat/stream`,{method:"POST",headers:o("text/event-stream"),body:JSON.stringify({message:t}),signal:s});if(!f.ok||!f.body)throw new Error(await a(f));const d=f.body.getReader(),l=new TextDecoder;let u="",w=!1;for(;;){const{value:e,done:t}=await d.read();if(t)break;u+=l.decode(e,{stream:!0});const s=u.split("\n\n");u=s.pop()||"";for(const o of s){const e=null==(r=o.match(/^event:\s*(.+)$/m))?void 0:r[1],t=null==(i=o.match(/^data:\s*(.+)$/m))?void 0:i[1];if(!e||!t)continue;const s=JSON.parse(t);if("message_delta"===e){const e=s.delta;"string"==typeof e&&n.onDelta(e)}else if("message_done"===e)w=!0,null==(c=n.onDone)||c.call(n,s);else if("error"===e)throw new Error("string"==typeof s.message?s.message:"AI 流式回复异常")}}if(!w)throw new Error("AI 流式回复未正常结束,请重试")}async function u(e,t,n,s){var r,i,c;const f={last_user_message:t,scope:"current_conversation"},d=await fetch(`/fastapi/api/v1/sessions/${e}/hints/stream`,{method:"POST",headers:o("text/event-stream"),body:JSON.stringify(f),signal:s});if(!d.ok||!d.body)throw new Error(await a(d));const l=d.body.getReader(),u=new TextDecoder;let w="",h=!1;for(;;){const{value:e,done:t}=await l.read();if(t)break;w+=u.decode(e,{stream:!0});const s=w.split("\n\n");w=s.pop()||"";for(const o of s){const e=null==(r=o.match(/^event:\s*(.+)$/m))?void 0:r[1],t=null==(i=o.match(/^data:\s*(.+)$/m))?void 0:i[1];if(!e||!t)continue;const s=JSON.parse(t);if("hint_delta"===e){const e=s.delta;"string"==typeof e&&n.onDelta(e)}else if("hint_done"===e)h=!0,null==(c=n.onDone)||c.call(n,s);else if("error"===e)throw new Error("string"==typeof s.message?s.message:"练习提示生成失败,请稍后重试")}}if(!h)throw new Error("练习提示未正常结束,请重试")}export{n as F,o as a,r as b,f as c,d,u as e,i as f,a as r,l as s,c as u};
|
||||
import{C as e,s as t}from"./index-pnQqyMoS.js";const n="/fastapi/api/v1";function s(){const t=e("clinical-thinking-access-token");if("string"!=typeof t||!t.trim())throw new Error("登录已过期,请重新登录");return t}function o(e="application/json"){return{"Content-Type":"application/json",Accept:e,Authorization:`Bearer ${s()}`,"X-Entry-Scene":"vue_frontend"}}async function a(e){const t=await e.text().catch((()=>""));if(!t)return`请求失败(${e.status})`;try{const e=JSON.parse(t),n=e.message||e.detail||e.error;if("string"==typeof n&&n.trim())return n}catch{}return t}function r(){const t=e("clinical-thinking-scenario");return t&&"object"==typeof t?t:null}function i(){var e,t;const n=null==(t=null==(e=r())?void 0:e.session)?void 0:t.session_id;if("number"==typeof n&&Number.isInteger(n)&&n>0)return n;throw new Error("未找到当前会话,请先生成模拟场景")}function c(e){const n=r();(null==n?void 0:n.session)&&t("clinical-thinking-scenario",{...n,session:{...n.session,status:e}})}async function f(e){var t;const n=await fetch("/fastapi/api/v1/sessions",{method:"POST",headers:o(),body:JSON.stringify(e)});if(!n.ok)throw new Error(await a(n));const s=await n.json();if("OK"!==s.code||!(null==(t=s.data)?void 0:t.session_id))throw new Error(s.message||"新建会话失败");return s.data}async function d(e){var t;const n=await fetch(`/fastapi/api/v1/sessions/${e}/complete-inquiry`,{method:"POST",headers:o()});if(!n.ok)throw new Error(await a(n));const s=await n.json();if("OK"!==s.code||!(null==(t=s.data)?void 0:t.session_id))throw new Error(s.message||"完成采集失败");return s.data}async function l(e,t,n,s){var r,i,c;const f=await fetch(`/fastapi/api/v1/sessions/${e}/chat/stream`,{method:"POST",headers:o("text/event-stream"),body:JSON.stringify({message:t}),signal:s});if(!f.ok||!f.body)throw new Error(await a(f));const d=f.body.getReader(),l=new TextDecoder;let u="",w=!1;for(;;){const{value:e,done:t}=await d.read();if(t)break;u+=l.decode(e,{stream:!0});const s=u.split("\n\n");u=s.pop()||"";for(const o of s){const e=null==(r=o.match(/^event:\s*(.+)$/m))?void 0:r[1],t=null==(i=o.match(/^data:\s*(.+)$/m))?void 0:i[1];if(!e||!t)continue;const s=JSON.parse(t);if("message_delta"===e){const e=s.delta;"string"==typeof e&&n.onDelta(e)}else if("message_done"===e)w=!0,null==(c=n.onDone)||c.call(n,s);else if("error"===e)throw new Error("string"==typeof s.message?s.message:"AI 流式回复异常")}}if(!w)throw new Error("AI 流式回复未正常结束,请重试")}async function u(e,t,n,s){var r,i,c;const f={last_user_message:t,scope:"current_conversation"},d=await fetch(`/fastapi/api/v1/sessions/${e}/hints/stream`,{method:"POST",headers:o("text/event-stream"),body:JSON.stringify(f),signal:s});if(!d.ok||!d.body)throw new Error(await a(d));const l=d.body.getReader(),u=new TextDecoder;let w="",h=!1;for(;;){const{value:e,done:t}=await l.read();if(t)break;w+=u.decode(e,{stream:!0});const s=w.split("\n\n");w=s.pop()||"";for(const o of s){const e=null==(r=o.match(/^event:\s*(.+)$/m))?void 0:r[1],t=null==(i=o.match(/^data:\s*(.+)$/m))?void 0:i[1];if(!e||!t)continue;const s=JSON.parse(t);if("hint_delta"===e){const e=s.delta;"string"==typeof e&&n.onDelta(e)}else if("hint_done"===e)h=!0,null==(c=n.onDone)||c.call(n,s);else if("error"===e)throw new Error("string"==typeof s.message?s.message:"练习提示生成失败,请稍后重试")}}if(!h)throw new Error("练习提示未正常结束,请重试")}export{n as F,o as a,r as b,f as c,d,u as e,i as f,a as r,l as s,c as u};
|
||||
Vendored
+1
-1
@@ -14,7 +14,7 @@
|
||||
<title>AI思维临床训练</title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
<script type="module" crossorigin src="./assets/index-CoO0Bu96.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-pnQqyMoS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-C7PPi8dw.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+140
-160
@@ -104,56 +104,30 @@
|
||||
<view v-if="physicalPanelVisible" class="exam-mask" @click="physicalPanelVisible = false">
|
||||
<view class="physical-panel" @click.stop>
|
||||
<view class="exam-header">
|
||||
<text class="exam-title">录入体格检查</text>
|
||||
<text class="exam-title">选择体格检查</text>
|
||||
<button class="exam-close" aria-label="关闭" @click="physicalPanelVisible = false">
|
||||
<view class="close-icon"></view>
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view class="physical-form" scroll-y>
|
||||
<view class="vital-grid">
|
||||
<view class="form-field">
|
||||
<text class="field-label">体温</text>
|
||||
<input class="field-input" v-model="physicalForm.temperature" type="digit" placeholder="36.8" placeholder-class="field-placeholder" />
|
||||
<text class="field-unit">℃</text>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="field-label">心率</text>
|
||||
<input class="field-input" v-model="physicalForm.pulse" type="number" placeholder="98" placeholder-class="field-placeholder" />
|
||||
<text class="field-unit">次/分</text>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="field-label">呼吸</text>
|
||||
<input class="field-input" v-model="physicalForm.respiration" type="number" placeholder="22" placeholder-class="field-placeholder" />
|
||||
<text class="field-unit">次/分</text>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="field-label">血氧</text>
|
||||
<input class="field-input" v-model="physicalForm.spo2" type="number" placeholder="96" placeholder-class="field-placeholder" />
|
||||
<text class="field-unit">%</text>
|
||||
</view>
|
||||
<view class="exam-list">
|
||||
<view v-if="physicalExamsLoading" class="exam-empty">
|
||||
<text>体格检查加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="form-field full">
|
||||
<text class="field-label">血压</text>
|
||||
<input class="field-input" v-model="physicalForm.bloodPressure" type="text" placeholder="138/86" placeholder-class="field-placeholder" />
|
||||
<text class="field-unit">mmHg</text>
|
||||
<view v-else-if="physicalExams.length === 0" class="exam-empty">
|
||||
<text>暂无可选体格检查</text>
|
||||
</view>
|
||||
<view class="form-field full">
|
||||
<text class="field-label">意识/面色</text>
|
||||
<input class="field-input" v-model="physicalForm.complexion" type="text" placeholder="清醒,面色苍白,出汗" placeholder-class="field-placeholder" />
|
||||
</view>
|
||||
<view class="form-field textarea-field">
|
||||
<text class="field-label">心肺/腹部查体</text>
|
||||
<textarea class="field-textarea" v-model="physicalForm.examFinding" placeholder="心音、肺部啰音、腹部压痛等" placeholder-class="field-placeholder"></textarea>
|
||||
</view>
|
||||
<view class="form-field textarea-field">
|
||||
<text class="field-label">其他发现</text>
|
||||
<textarea class="field-textarea" v-model="physicalForm.otherFinding" placeholder="如双侧血压差、下肢水肿、神经系统体征等" placeholder-class="field-placeholder"></textarea>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="physical-actions">
|
||||
<button class="physical-cancel" @click="physicalPanelVisible = false">取消</button>
|
||||
<button class="physical-submit" @click="submitPhysicalExam">提交检查结果</button>
|
||||
<block v-else>
|
||||
<button
|
||||
v-for="exam in physicalExams"
|
||||
:key="exam.item_code"
|
||||
class="exam-item"
|
||||
:disabled="examOrdering"
|
||||
@click="selectPhysicalExam(exam)"
|
||||
>
|
||||
<text class="exam-name">{{ exam.item_name }}</text>
|
||||
<view class="chevron-icon"></view>
|
||||
</button>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -167,15 +141,24 @@
|
||||
</button>
|
||||
</view>
|
||||
<view class="exam-list">
|
||||
<button
|
||||
v-for="exam in auxiliaryExams"
|
||||
:key="exam.name"
|
||||
class="exam-item"
|
||||
@click="selectAuxiliaryExam(exam)"
|
||||
>
|
||||
<text class="exam-name">{{ exam.name }}</text>
|
||||
<view class="chevron-icon"></view>
|
||||
</button>
|
||||
<view v-if="auxiliaryExamsLoading" class="exam-empty">
|
||||
<text>辅助检查加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="auxiliaryExams.length === 0" class="exam-empty">
|
||||
<text>暂无可选辅助检查</text>
|
||||
</view>
|
||||
<block v-else>
|
||||
<button
|
||||
v-for="exam in auxiliaryExams"
|
||||
:key="exam.item_code"
|
||||
class="exam-item"
|
||||
:disabled="examOrdering"
|
||||
@click="selectAuxiliaryExam(exam)"
|
||||
>
|
||||
<text class="exam-name">{{ exam.item_name }}</text>
|
||||
<view class="chevron-icon"></view>
|
||||
</button>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -188,6 +171,14 @@
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { readStoredClinicalCase, type ClinicalCase } from '../../api/cases'
|
||||
import { createMockChatSession, sendMockChatMessage, type ChatMessage, type ChatSession } from '../../api/chat'
|
||||
import {
|
||||
fetchAuxiliaryExamItems,
|
||||
fetchPhysicalExamItems,
|
||||
orderAuxiliaryExamResult,
|
||||
orderPhysicalExamResult,
|
||||
type ExamItem,
|
||||
type ExamResult
|
||||
} from '../../api/exams'
|
||||
import { createHomeNavigator, createProfileOpener, createSettingsOpener } from '../../api/navigation'
|
||||
import {
|
||||
completeInquiry,
|
||||
@@ -238,46 +229,16 @@ const examPanelVisible = ref(false)
|
||||
const physicalPanelVisible = ref(false)
|
||||
const activeSessionId = ref<number | null>(null)
|
||||
const storedCase = ref<ClinicalCase | null>(null)
|
||||
const physicalExams = ref<ExamItem[]>([])
|
||||
const auxiliaryExams = ref<ExamItem[]>([])
|
||||
const physicalExamsLoading = ref(false)
|
||||
const auxiliaryExamsLoading = ref(false)
|
||||
const examOrdering = ref(false)
|
||||
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let activeStreamController: AbortController | null = null
|
||||
let activeHintController: AbortController | null = null
|
||||
|
||||
type AuxiliaryExam = {
|
||||
name: string
|
||||
result: string
|
||||
}
|
||||
|
||||
const auxiliaryExams: AuxiliaryExam[] = [
|
||||
{
|
||||
name: '心电图',
|
||||
result: '检查结果:床边12导联心电图提示窦性心律,II、III、aVF 导联 ST 段抬高,提示下壁急性心肌梗死可能。'
|
||||
},
|
||||
{
|
||||
name: '胸部X线',
|
||||
result: '检查结果:胸部X线未见明显气胸或纵隔明显增宽,心影大小基本正常,不能排除急性冠脉综合征。'
|
||||
},
|
||||
{
|
||||
name: '心脏超声',
|
||||
result: '检查结果:心脏超声提示左室下壁节段性运动减低,未见大量心包积液,需结合心电图及心肌标志物判断。'
|
||||
},
|
||||
{
|
||||
name: '冠脉CTA',
|
||||
result: '检查结果:冠脉CTA提示右冠状动脉近段重度狭窄/闭塞可能,建议结合急诊介入评估。'
|
||||
}
|
||||
]
|
||||
|
||||
const physicalForm = reactive({
|
||||
temperature: '',
|
||||
pulse: '',
|
||||
respiration: '',
|
||||
bloodPressure: '',
|
||||
spo2: '',
|
||||
complexion: '',
|
||||
examFinding: '',
|
||||
otherFinding: ''
|
||||
})
|
||||
|
||||
const activeCase = computed(() => props.caseItem || storedCase.value)
|
||||
|
||||
const complaintShort = computed(() => {
|
||||
@@ -342,30 +303,93 @@ async function handleCompleteInquiry() {
|
||||
}
|
||||
}
|
||||
|
||||
function openExamPanel() {
|
||||
async function openExamPanel() {
|
||||
const sessionId = activeSessionId.value
|
||||
if (!sessionId) {
|
||||
showToast('未找到当前会话,请先生成模拟场景')
|
||||
return
|
||||
}
|
||||
|
||||
physicalPanelVisible.value = false
|
||||
examPanelVisible.value = true
|
||||
if (auxiliaryExams.value.length > 0 || auxiliaryExamsLoading.value) return
|
||||
|
||||
auxiliaryExamsLoading.value = true
|
||||
try {
|
||||
auxiliaryExams.value = await fetchAuxiliaryExamItems(sessionId)
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '辅助检查列表加载失败')
|
||||
} finally {
|
||||
auxiliaryExamsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPhysicalPanel() {
|
||||
async function openPhysicalPanel() {
|
||||
const sessionId = activeSessionId.value
|
||||
if (!sessionId) {
|
||||
showToast('未找到当前会话,请先生成模拟场景')
|
||||
return
|
||||
}
|
||||
|
||||
examPanelVisible.value = false
|
||||
physicalPanelVisible.value = true
|
||||
if (physicalExams.value.length > 0 || physicalExamsLoading.value) return
|
||||
|
||||
physicalExamsLoading.value = true
|
||||
try {
|
||||
physicalExams.value = await fetchPhysicalExamItems(sessionId)
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '体格检查列表加载失败')
|
||||
} finally {
|
||||
physicalExamsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectAuxiliaryExam(exam: AuxiliaryExam) {
|
||||
examPanelVisible.value = false
|
||||
async function selectPhysicalExam(exam: ExamItem) {
|
||||
const sessionId = activeSessionId.value
|
||||
if (!sessionId || examOrdering.value) return
|
||||
|
||||
examOrdering.value = true
|
||||
try {
|
||||
const result = await orderPhysicalExamResult(sessionId, exam.item_code)
|
||||
physicalPanelVisible.value = false
|
||||
appendExamResultMessages('体格检查', result)
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '体格检查结果获取失败')
|
||||
} finally {
|
||||
examOrdering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectAuxiliaryExam(exam: ExamItem) {
|
||||
const sessionId = activeSessionId.value
|
||||
if (!sessionId || examOrdering.value) return
|
||||
|
||||
examOrdering.value = true
|
||||
try {
|
||||
const result = await orderAuxiliaryExamResult(sessionId, exam.item_code)
|
||||
examPanelVisible.value = false
|
||||
appendExamResultMessages('辅助检查', result)
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '辅助检查结果获取失败')
|
||||
} finally {
|
||||
examOrdering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function appendExamResultMessages(kindLabel: string, result: ExamResult) {
|
||||
const timestamp = Date.now()
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: `doctor-exam-${timestamp}`,
|
||||
role: 'doctor',
|
||||
content: `选择辅助检查:${exam.name}`,
|
||||
content: `选择${kindLabel}:${result.item_name}`,
|
||||
label: '我'
|
||||
},
|
||||
{
|
||||
id: `mentor-exam-${timestamp + 1}`,
|
||||
role: 'mentor',
|
||||
content: exam.result,
|
||||
content: formatExamResult(result),
|
||||
label: 'AI助手'
|
||||
}
|
||||
]
|
||||
@@ -374,73 +398,14 @@ function selectAuxiliaryExam(exam: AuxiliaryExam) {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
function submitPhysicalExam() {
|
||||
const items = [
|
||||
physicalForm.temperature.trim() ? `体温 ${physicalForm.temperature.trim()}℃` : '',
|
||||
physicalForm.pulse.trim() ? `心率 ${physicalForm.pulse.trim()}次/分` : '',
|
||||
physicalForm.respiration.trim() ? `呼吸 ${physicalForm.respiration.trim()}次/分` : '',
|
||||
physicalForm.bloodPressure.trim() ? `血压 ${physicalForm.bloodPressure.trim()}mmHg` : '',
|
||||
physicalForm.spo2.trim() ? `血氧 ${physicalForm.spo2.trim()}%` : '',
|
||||
physicalForm.complexion.trim() ? `意识/面色:${physicalForm.complexion.trim()}` : '',
|
||||
physicalForm.examFinding.trim() ? `心肺/腹部查体:${physicalForm.examFinding.trim()}` : '',
|
||||
physicalForm.otherFinding.trim() ? `其他发现:${physicalForm.otherFinding.trim()}` : ''
|
||||
function formatExamResult(result: ExamResult) {
|
||||
const flags = [
|
||||
result.is_abnormal ? '异常' : '',
|
||||
result.is_key ? '关键检查' : '',
|
||||
result.already_ordered ? '已检查过' : ''
|
||||
].filter(Boolean)
|
||||
|
||||
if (items.length === 0) {
|
||||
showToast('请至少录入一项体格检查')
|
||||
return
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
const summary = items.join(';')
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
id: `doctor-physical-${timestamp}`,
|
||||
role: 'doctor',
|
||||
content: `录入体格检查:${summary}`,
|
||||
label: '我'
|
||||
},
|
||||
{
|
||||
id: `mentor-physical-${timestamp + 1}`,
|
||||
role: 'mentor',
|
||||
content: buildPhysicalFeedback(),
|
||||
label: 'AI助手'
|
||||
}
|
||||
]
|
||||
|
||||
session.messages.push(...messages)
|
||||
physicalPanelVisible.value = false
|
||||
resetPhysicalForm()
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
function resetPhysicalForm() {
|
||||
physicalForm.temperature = ''
|
||||
physicalForm.pulse = ''
|
||||
physicalForm.respiration = ''
|
||||
physicalForm.bloodPressure = ''
|
||||
physicalForm.spo2 = ''
|
||||
physicalForm.complexion = ''
|
||||
physicalForm.examFinding = ''
|
||||
physicalForm.otherFinding = ''
|
||||
}
|
||||
|
||||
function buildPhysicalFeedback() {
|
||||
const pulse = Number(physicalForm.pulse)
|
||||
const respiration = Number(physicalForm.respiration)
|
||||
const spo2 = Number(physicalForm.spo2)
|
||||
const temperature = Number(physicalForm.temperature)
|
||||
const tips: string[] = []
|
||||
|
||||
if (pulse >= 100) tips.push('心率偏快')
|
||||
if (respiration >= 22) tips.push('呼吸频率偏快')
|
||||
if (spo2 > 0 && spo2 < 95) tips.push('血氧偏低')
|
||||
if (temperature >= 37.3) tips.push('体温偏高')
|
||||
if (physicalForm.complexion.includes('苍白') || physicalForm.complexion.includes('出汗')) tips.push('面色/出汗提示急性病容')
|
||||
if (physicalForm.otherFinding.includes('血压差') || physicalForm.otherFinding.includes('双侧')) tips.push('双侧血压或脉搏差异需警惕主动脉夹层')
|
||||
|
||||
const prefix = tips.length ? `已记录体格检查。当前提示:${tips.join('、')}。` : '已记录体格检查,暂未见明确异常体征。'
|
||||
return `${prefix}建议结合胸痛性质、心电图及心肌标志物进一步判断,并持续监测生命体征变化。`
|
||||
const suffix = flags.length ? `(${flags.join(',')})` : ''
|
||||
return `${result.item_name}${suffix}:${result.result_text || '暂无结果。'}`
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
@@ -1283,6 +1248,21 @@ page {
|
||||
background: #f2f3fb;
|
||||
}
|
||||
|
||||
.exam-empty {
|
||||
min-height: 120px;
|
||||
padding: 24px;
|
||||
border: 1px dashed rgba(194, 198, 212, 0.7);
|
||||
border-radius: 12px;
|
||||
background: rgba(249, 249, 255, 0.75);
|
||||
color: rgba(66, 71, 82, 0.78);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exam-name {
|
||||
color: #191c21;
|
||||
font-size: 18px;
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
class="message-input"
|
||||
v-model="draft"
|
||||
auto-height
|
||||
maxlength="500"
|
||||
maxlength="1000"
|
||||
placeholder="请输入您的问题..."
|
||||
placeholder-class="input-placeholder"
|
||||
@confirm="handleSend"
|
||||
@@ -113,7 +113,13 @@
|
||||
<button class="attach-button" aria-label="附件" @click="showToast('附件上传即将开放')">
|
||||
<view class="attach-icon"></view>
|
||||
</button>
|
||||
<button class="send-button" aria-label="发送" @click="handleSend">
|
||||
<button
|
||||
class="send-button"
|
||||
:class="{ disabled: sending }"
|
||||
:disabled="sending"
|
||||
aria-label="发送"
|
||||
@click="handleSend"
|
||||
>
|
||||
<view class="send-icon"></view>
|
||||
</button>
|
||||
</view>
|
||||
@@ -167,6 +173,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import {
|
||||
createLearningAssistantSession,
|
||||
streamLearningAssistantChat,
|
||||
type LearningAssistantSession
|
||||
} from '../../api/learning-assistant'
|
||||
import { createHomeNavigator } from '../../api/navigation'
|
||||
|
||||
type AssistantMessage = {
|
||||
@@ -192,19 +203,7 @@ const pathwaySteps = [
|
||||
{ index: '3', title: '再灌注策略', description: 'STEMI需紧急PCI。' }
|
||||
]
|
||||
|
||||
const messages = ref<AssistantMessage[]>([
|
||||
{
|
||||
id: 'sample-user',
|
||||
role: 'user',
|
||||
content: '你能解释一下急性冠脉综合征(ACS)的最新临床路径吗?'
|
||||
},
|
||||
{
|
||||
id: 'sample-ai',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
variant: 'acs-pathway'
|
||||
}
|
||||
])
|
||||
const messages = ref<AssistantMessage[]>([])
|
||||
|
||||
const draft = ref('')
|
||||
const modalVisible = ref(false)
|
||||
@@ -212,10 +211,11 @@ const typingVisible = ref(false)
|
||||
const toastMessage = ref('')
|
||||
const toastVisible = ref(false)
|
||||
const scrollTop = ref(0)
|
||||
const assistantSession = ref<LearningAssistantSession | null>(null)
|
||||
const sending = ref(false)
|
||||
|
||||
let typingTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pulseTimer: ReturnType<typeof setInterval> | null = null
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let streamAbortController: AbortController | null = null
|
||||
|
||||
function useQuickAction(action: string) {
|
||||
const prompts: Record<string, string> = {
|
||||
@@ -227,12 +227,13 @@ function useQuickAction(action: string) {
|
||||
draft.value = prompts[action] || action
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
async function handleSend() {
|
||||
const value = draft.value.trim()
|
||||
if (!value) {
|
||||
showToast('请输入问题')
|
||||
return
|
||||
}
|
||||
if (sending.value) return
|
||||
|
||||
messages.value.push({
|
||||
id: `user-${Date.now()}`,
|
||||
@@ -241,19 +242,65 @@ function handleSend() {
|
||||
})
|
||||
draft.value = ''
|
||||
typingVisible.value = true
|
||||
sending.value = true
|
||||
scrollToBottom()
|
||||
|
||||
if (typingTimer) clearTimeout(typingTimer)
|
||||
typingTimer = setTimeout(() => {
|
||||
const assistantMessageIndex = messages.value.length
|
||||
messages.value.push({
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
variant: 'simple',
|
||||
content: ''
|
||||
})
|
||||
|
||||
try {
|
||||
const session = await ensureAssistantSession(value)
|
||||
streamAbortController?.abort()
|
||||
streamAbortController = new AbortController()
|
||||
await streamLearningAssistantChat(
|
||||
session.assistant_session_id,
|
||||
{ question: value },
|
||||
{
|
||||
onDelta: delta => {
|
||||
messages.value[assistantMessageIndex].content += delta
|
||||
scrollToBottom()
|
||||
}
|
||||
},
|
||||
streamAbortController.signal
|
||||
)
|
||||
if (!messages.value[assistantMessageIndex].content.trim()) {
|
||||
messages.value[assistantMessageIndex].content = '暂未生成回复,请稍后重试。'
|
||||
}
|
||||
} catch (error) {
|
||||
messages.value[assistantMessageIndex].content = error instanceof Error ? error.message : 'AI 学习助手回复失败'
|
||||
showToast(messages.value[assistantMessageIndex].content)
|
||||
} finally {
|
||||
typingVisible.value = false
|
||||
messages.value.push({
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
variant: 'simple',
|
||||
content: '已收到。我会结合医院知识库、临床路径和指南证据,为你整理成可用于带教复盘的要点。'
|
||||
})
|
||||
sending.value = false
|
||||
scrollToBottom()
|
||||
}, 900)
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureAssistantSession(title: string) {
|
||||
if (assistantSession.value) return assistantSession.value
|
||||
const session = await createLearningAssistantSession(title)
|
||||
assistantSession.value = session
|
||||
uni.setStorageSync('clinical-thinking-learning-assistant-session', session)
|
||||
return session
|
||||
}
|
||||
|
||||
async function initializeAssistantSession() {
|
||||
try {
|
||||
const storedSession = uni.getStorageSync('clinical-thinking-learning-assistant-session')
|
||||
if (storedSession && typeof storedSession === 'object') {
|
||||
assistantSession.value = storedSession as LearningAssistantSession
|
||||
return
|
||||
}
|
||||
assistantSession.value = await createLearningAssistantSession('AI 学习助手')
|
||||
uni.setStorageSync('clinical-thinking-learning-assistant-session', assistantSession.value)
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : '新建会话失败')
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
@@ -272,18 +319,11 @@ function showToast(message: string) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
pulseTimer = setInterval(() => {
|
||||
if (typingVisible.value) return
|
||||
typingVisible.value = true
|
||||
setTimeout(() => {
|
||||
typingVisible.value = false
|
||||
}, 2400)
|
||||
}, 12000)
|
||||
void initializeAssistantSession()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typingTimer) clearTimeout(typingTimer)
|
||||
if (pulseTimer) clearInterval(pulseTimer)
|
||||
streamAbortController?.abort()
|
||||
if (toastTimer) clearTimeout(toastTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -754,6 +794,10 @@ page {
|
||||
background: #00478d;
|
||||
}
|
||||
|
||||
.send-button.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.send-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user