Merge branch 'long-consultation-desk'

This commit is contained in:
2026-04-30 10:36:25 +08:00
148 changed files with 2944 additions and 22718 deletions
+27
View File
@@ -0,0 +1,27 @@
import request from '@/utils/request'
// 接诊台 - 待接诊队列(复用挂号列表接口)
// 建议调用方式:receptionQueue({ status: 1, start_date: today, end_date: today, page_size: 50 })
export function receptionQueue(params: any) {
return request.get({ url: '/doctor.appointment/lists', params })
}
// 接诊台 - 聚合详情:挂号信息 + 诊单病例 + 血糖血压记录
export function receptionDetail(params: { id: number }) {
return request.get({ url: '/doctor.appointment/reception', params })
}
// 接诊台 - 通知接诊医助(发企业微信)
export function notifyAssistant(params: { id: number }) {
return request.post({ url: '/doctor.appointment/notifyAssistant', params })
}
// 医生备注 - 添加/追加(同一天追加 content + tongue_images
export function addDoctorNote(params: { diagnosis_id: number; content?: string; tongue_images?: string[] }) {
return request.post({ url: '/doctor.appointment/addDoctorNote', params })
}
// 医生备注 - 按诊单查询列表
export function getDoctorNotes(params: { diagnosis_id: number }) {
return request.get({ url: '/doctor.appointment/doctorNotes', params })
}
+8 -4
View File
@@ -129,6 +129,7 @@
import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue'
import { Loading, Close, Rank, Camera, Fold, Expand } from '@element-plus/icons-vue'
import { uploadImageBlob, uploadVideoBlob } from '@/api/file'
import { addDoctorNote } from '@/api/patient'
import {
attachLocalCallRecording,
bindCallRoom,
@@ -1310,10 +1311,10 @@ const handleCallKitScreenshot = async () => {
return
}
const previous: string[] = [...((detail.tongue_images || []) as string[])]
if (previous.length >= 9) {
feedback.msgWarning('舌苔照片已达上限 9 张')
return
}
// if (previous.length >= 9) {
// feedback.msgWarning('舌苔照片已达上限 9 张')
// return
// }
const next = [...previous, path]
const payload = JSON.parse(JSON.stringify(detail)) as Record<string, unknown>
payload.tongue_images = next
@@ -1327,6 +1328,9 @@ const handleCallKitScreenshot = async () => {
}
})
)
try {
await addDoctorNote({ diagnosis_id: diagnosisId.value, tongue_images: [path] })
} catch { /* 写入备注表失败不阻塞主流程 */ }
} catch (e: unknown) {
const err = e as Error
console.error(err)
@@ -0,0 +1,116 @@
<template>
<div v-if="notes.length" class="note-timeline">
<div v-for="note in notes" :key="note.id" class="timeline-node">
<div class="timeline-dot"></div>
<div class="timeline-date">{{ note.note_date }}</div>
<div class="timeline-body">
<div v-if="note.content" class="timeline-content">
<div v-for="(line, idx) in splitLines(note.content)" :key="idx" class="content-line">
{{ line }}
</div>
</div>
<div v-if="note.tongue_images?.length" class="timeline-images">
<el-image
v-for="(url, idx) in note.tongue_images"
:key="idx"
:src="url"
:preview-src-list="note.tongue_images"
:initial-index="idx"
fit="cover"
class="timeline-thumb"
preview-teleported
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
export interface DoctorNoteItem {
id: number
note_date: string
content: string | null
tongue_images: string[]
}
defineProps<{ notes: DoctorNoteItem[] }>()
const splitLines = (content: string | null): string[] => {
if (!content) return []
return content.split('\n').filter(Boolean)
}
</script>
<style scoped lang="scss">
.note-timeline {
position: relative;
padding-left: 22px;
}
.note-timeline::before {
content: '';
position: absolute;
left: 6px;
top: 4px;
bottom: 4px;
width: 2px;
background: #ebeef5;
border-radius: 1px;
}
.timeline-node {
position: relative;
padding-bottom: 16px;
}
.timeline-node:last-child {
padding-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -19px;
top: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #409eff;
border: 2px solid #ecf5ff;
}
.timeline-date {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
font-family: 'IBM Plex Mono', Consolas, monospace;
}
.timeline-body {
display: flex;
flex-direction: column;
gap: 6px;
}
.timeline-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.content-line {
font-size: 12.5px;
color: #606266;
line-height: 1.6;
word-break: break-word;
}
.timeline-images {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.timeline-thumb {
width: 64px;
height: 64px;
border-radius: 6px;
border: 1px solid #ebeef5;
cursor: pointer;
transition: transform 0.15s ease;
}
.timeline-thumb:hover {
transform: scale(1.05);
}
</style>
File diff suppressed because it is too large Load Diff
+20 -1
View File
@@ -527,7 +527,13 @@
</fieldset>
</el-form>
</el-tab-pane>
<el-tab-pane label="医生备注" name="doctorNote" :disabled="!formData.id" lazy>
<div v-if="formData.id" class="p-4">
<NoteTimeline v-if="diagNotes.length" :notes="diagNotes" />
<el-empty v-else description="暂无备注记录" :image-size="48" />
</div>
<el-empty v-else description="请先保存诊单后查看" />
</el-tab-pane>
<!-- 血糖血压记录标签页 -->
<el-tab-pane label="血糖血压记录" name="blood" :disabled="!formData.id">
<blood-record-list
@@ -662,6 +668,7 @@
/>
<el-empty v-else description="请先保存诊单后再查看挂号记录" />
</el-tab-pane>
</el-tabs>
<!-- 处方详情(查看病历) -->
@@ -702,7 +709,9 @@ import CallRecordPanel from './components/CallRecordPanel.vue'
import ImChatRecordPanel from './components/ImChatRecordPanel.vue'
import AssignLogPanel from './components/AssignLogPanel.vue'
import AppointmentRecordPanel from './components/AppointmentRecordPanel.vue'
import NoteTimeline from '@/views/patient/reception/components/NoteTimeline.vue'
import TcmPrescription from '@/components/tcm-prescription/index.vue'
import { getDoctorNotes } from '@/api/patient'
import {
DIAGNOSIS_TONGUE_IMAGES_UPDATED,
type DiagnosisTongueImagesUpdatedDetail
@@ -733,6 +742,16 @@ const drawerTitle = computed(() => {
return mode.value === 'add' ? '新增诊单' : '编辑诊单'
})
const diagNotes = ref<any[]>([])
watch(() => activeTab.value, async (tab) => {
if (tab === 'doctorNote' && formData.value.id) {
try {
diagNotes.value = await getDoctorNotes({ diagnosis_id: Number(formData.value.id) }) || []
} catch { diagNotes.value = [] }
}
})
watch([visible, activeTab], () => {
if (!visible.value) return
if (activeTab.value === 'chat' && !hasPermission(['tcm.diagnosis/chat'])) {