更新
This commit is contained in:
@@ -129,12 +129,9 @@
|
||||
</el-button>
|
||||
<el-table :data="formData.herbs" class="mt-2" border>
|
||||
<el-table-column label="序号" type="index" width="60" />
|
||||
<el-table-column label="药材名称" min-width="150">
|
||||
<el-table-column label="药材名称" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.name"
|
||||
placeholder="请输入药材名称"
|
||||
/>
|
||||
<MedicineNameSelect v-model="row.name" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剂量(克)" min-width="120">
|
||||
@@ -301,6 +298,7 @@
|
||||
import { prescriptionAdd } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import MedicineNameSelect from '@/components/medicine-name-select/index.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
size="small"
|
||||
@click="handleChat(row)"
|
||||
>
|
||||
聊天
|
||||
通话
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['doctor.appointment/complete']"
|
||||
@@ -456,7 +456,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑患者弹窗 -->
|
||||
<edit-popup ref="editRef" @success="getLists" />
|
||||
<edit-popup ref="editRef" @success="loadData" />
|
||||
|
||||
<!-- 中医处方单 -->
|
||||
<tcm-prescription ref="prescriptionRef" @success="onPrescriptionSuccess" />
|
||||
@@ -506,14 +506,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { watch } from 'vue'
|
||||
import { defineAsyncComponent, onMounted, watch } from 'vue'
|
||||
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
|
||||
import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import feedback from '@/utils/feedback'
|
||||
import EditPopup from '../diagnosis/edit.vue'
|
||||
import TcmPrescription from '@/components/tcm-prescription/index.vue'
|
||||
import ChatDialog from '@/components/chat-dialog/index.vue'
|
||||
|
||||
// 重组件异步加载,避免与列表同 chunk 阻塞路由进入与首帧渲染
|
||||
const EditPopup = defineAsyncComponent(() => import('../diagnosis/edit.vue'))
|
||||
const TcmPrescription = defineAsyncComponent(() => import('@/components/tcm-prescription/index.vue'))
|
||||
const ChatDialog = defineAsyncComponent(() => import('@/components/chat-dialog/index.vue'))
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { getWeappConfig } from '@/api/channel/weapp'
|
||||
import {
|
||||
@@ -554,7 +556,9 @@ const formData = reactive({
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
date_preset: 'today' as '' | 'today' | 'tomorrow' | 'day_after',
|
||||
diagnosis_confirmed: '' as '' | '0' | '1' // ''=全部 1=已确认 0=未确认
|
||||
diagnosis_confirmed: '' as '' | '0' | '1', // ''=全部 1=已确认 0=未确认
|
||||
/** 为 1 时后端 extend 返回各状态数量,避免额外 4 次列表请求 */
|
||||
include_status_counts: 0 as 0 | 1
|
||||
})
|
||||
|
||||
const activeTab = ref('1')
|
||||
@@ -568,7 +572,8 @@ const statusCount = ref<Record<number, number>>({
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: appointmentLists,
|
||||
params: formData
|
||||
params: formData,
|
||||
firstLoading: true
|
||||
})
|
||||
|
||||
// 手动修改日期范围时清除快捷日期
|
||||
@@ -593,29 +598,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 获取各状态数量
|
||||
const getStatusCount = async () => {
|
||||
try {
|
||||
// 获取各状态的数量
|
||||
const promises = [1, 2, 3, 4].map(async (status) => {
|
||||
const res = await appointmentLists({
|
||||
...formData,
|
||||
status,
|
||||
page_no: 1,
|
||||
page_size: 1
|
||||
})
|
||||
return { status, count: res.count || 0 }
|
||||
})
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
results.forEach(({ status, count }) => {
|
||||
statusCount.value[status] = count
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取状态数量失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 待接诊行高亮
|
||||
const getRowClassName = ({ row }: { row: any }) => {
|
||||
return row.status === 1 ? 'row-pending' : ''
|
||||
@@ -631,10 +613,24 @@ const handleTabChange = (tabName: string | number) => {
|
||||
resetPage()
|
||||
}
|
||||
|
||||
// 重写getLists,同时更新状态数量
|
||||
// 列表 + Tab 角标:一次请求(后端 extend.status_count),翻页等仅 getLists 不重复统计
|
||||
const loadData = async () => {
|
||||
await getLists()
|
||||
await getStatusCount()
|
||||
formData.include_status_counts = 1
|
||||
try {
|
||||
await getLists()
|
||||
const ext = pager.extend as { status_count?: Record<number | string, number> }
|
||||
const sc = ext?.status_count
|
||||
if (sc) {
|
||||
for (const k of [1, 2, 3, 4] as const) {
|
||||
const n = sc[k] ?? sc[String(k)]
|
||||
if (typeof n === 'number') {
|
||||
statusCount.value[k] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
formData.include_status_counts = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 更多下拉命令
|
||||
@@ -730,17 +726,24 @@ const getPastHistoryLabels = (values: string[] | string) => {
|
||||
}
|
||||
|
||||
const loadDetailDictOptions = async () => {
|
||||
const [dt, st, db, ph] = await Promise.all([
|
||||
getDictData({ type: 'diagnosis_type' }),
|
||||
getDictData({ type: 'syndrome_type' }),
|
||||
getDictData({ type: 'diabetes_type' }),
|
||||
getDictData({ type: 'past_history' })
|
||||
])
|
||||
diagnosisTypeOptions.value = dt?.diagnosis_type || []
|
||||
syndromeTypeOptions.value = st?.syndrome_type || []
|
||||
diabetesTypeOptions.value = db?.diabetes_type || []
|
||||
pastHistoryOptions.value = ph?.past_history || []
|
||||
}
|
||||
|
||||
const detailDictReady = ref(false)
|
||||
const ensureDetailDictOptions = async () => {
|
||||
if (detailDictReady.value) return
|
||||
try {
|
||||
const [dt, st, db, ph] = await Promise.all([
|
||||
getDictData({ type: 'diagnosis_type' }),
|
||||
getDictData({ type: 'syndrome_type' }),
|
||||
getDictData({ type: 'diabetes_type' }),
|
||||
getDictData({ type: 'past_history' })
|
||||
])
|
||||
diagnosisTypeOptions.value = dt?.diagnosis_type || []
|
||||
syndromeTypeOptions.value = st?.syndrome_type || []
|
||||
diabetesTypeOptions.value = db?.diabetes_type || []
|
||||
pastHistoryOptions.value = ph?.past_history || []
|
||||
await loadDetailDictOptions()
|
||||
detailDictReady.value = true
|
||||
} catch (e) {
|
||||
console.error('加载字典失败:', e)
|
||||
}
|
||||
@@ -785,7 +788,8 @@ const handleAction = (command: string, row: any) => {
|
||||
// 查看详情(含患者诊单完整信息)
|
||||
const handleDetail = async (row: any) => {
|
||||
try {
|
||||
const res = await appointmentDetail({ id: row.id })
|
||||
const detailBundle = await Promise.all([ensureDetailDictOptions(), appointmentDetail({ id: row.id })])
|
||||
const res = detailBundle[1]
|
||||
detailData.value = res
|
||||
// 有 patient_id(即诊单ID)时,拉取患者完整信息
|
||||
if (res.patient_id) {
|
||||
@@ -966,8 +970,9 @@ formData.start_date = `${_today.getFullYear()}-${_pad(_today.getMonth() + 1)}-${
|
||||
formData.end_date = formData.start_date
|
||||
formData.status = 1
|
||||
|
||||
loadData()
|
||||
loadDetailDictOptions()
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -36,6 +36,20 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="渠道来源:" required>
|
||||
<el-select
|
||||
v-model="form.channel_source"
|
||||
placeholder="请选择渠道来源"
|
||||
class="channel-source-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 预约医生 -->
|
||||
<el-form-item label="预约医生:">
|
||||
<div class="doctor-list">
|
||||
@@ -167,6 +181,7 @@ import { Refresh } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import isoWeek from 'dayjs/plugin/isoWeek'
|
||||
import { getDoctors } from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { getAvailableSlots, createAppointment, rosterLists, appointmentLists } from '@/api/doctor'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
@@ -192,6 +207,7 @@ const timeSlots = ref<TimeSlot[]>([])
|
||||
const lastVisit = ref('')
|
||||
const selectedDoctorId = ref(0)
|
||||
const doctorRosterDates = ref<string[]>([]) // 医生有排班的日期列表
|
||||
const channelOptions = ref<{ name: string; value: string }[]>([])
|
||||
|
||||
const patientInfo = reactive({
|
||||
id: 0,
|
||||
@@ -206,7 +222,8 @@ const form = reactive({
|
||||
patientId: 0,
|
||||
date: '',
|
||||
appointmentTime: '',
|
||||
remark: ''
|
||||
remark: '',
|
||||
channel_source: '' as string
|
||||
})
|
||||
|
||||
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
|
||||
@@ -271,7 +288,12 @@ const filteredTimeSlots = computed(() => {
|
||||
|
||||
// 是否可以提交
|
||||
const canSubmit = computed(() => {
|
||||
return selectedDoctorId.value && form.date && form.appointmentTime
|
||||
return (
|
||||
selectedDoctorId.value &&
|
||||
form.date &&
|
||||
form.appointmentTime &&
|
||||
!!form.channel_source
|
||||
)
|
||||
})
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
@@ -292,10 +314,12 @@ const open = async (patient: any) => {
|
||||
form.date = ''
|
||||
form.appointmentTime = ''
|
||||
form.remark = ''
|
||||
form.channel_source = ''
|
||||
timeSlots.value = []
|
||||
doctorRosterDates.value = []
|
||||
lastVisit.value = ''
|
||||
|
||||
await loadChannelOptions()
|
||||
// 加载医生列表
|
||||
await loadDoctors()
|
||||
|
||||
@@ -327,6 +351,16 @@ const loadLastVisit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadChannelOptions = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'channels' })
|
||||
channelOptions.value = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
} catch (e) {
|
||||
console.error('加载渠道来源失败:', e)
|
||||
channelOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载医生列表
|
||||
const loadDoctors = async () => {
|
||||
try {
|
||||
@@ -511,6 +545,10 @@ const handleConfirm = async () => {
|
||||
feedback.msgWarning('请选择预约时间')
|
||||
return
|
||||
}
|
||||
if (!form.channel_source) {
|
||||
feedback.msgWarning('请选择渠道来源')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
@@ -523,7 +561,8 @@ const handleConfirm = async () => {
|
||||
period: 'all', // 全天时段
|
||||
appointment_time: form.appointmentTime,
|
||||
appointment_type: form.appointmentType,
|
||||
remark: form.remark
|
||||
remark: form.remark,
|
||||
channel_source: form.channel_source
|
||||
}
|
||||
|
||||
console.log('提交预约参数:', params)
|
||||
@@ -549,6 +588,11 @@ defineExpose({ open })
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channel-source-select {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-list {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
class="mb-4"
|
||||
title="视频通话与录制回放"
|
||||
>
|
||||
<p class="call-record-tip">
|
||||
<!-- <p class="call-record-tip">
|
||||
下列为与本诊单关联的通话记录。医生接通并同步房间号后,服务端会尝试自动发起「云端混流录制」(需配置 CAM 密钥与云点播等);录制完成后由腾讯云回调写入回放地址。同时,管理端浏览器会进行「本地录制」,挂断后自动上传并合并到本条记录。
|
||||
</p>
|
||||
<p class="call-record-tip muted">
|
||||
回调 URL 示例:<code>{{ callbackHint }}</code>
|
||||
(若配置了 <code>trtc.recording_callback_token</code>,请在 URL 后附加
|
||||
<code>?token=你的密钥</code>)
|
||||
</p>
|
||||
</p> -->
|
||||
</el-alert>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无通话记录">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="im-chat-record-panel">
|
||||
<el-alert type="info" show-icon :closable="false" class="mb-4" title="说明">
|
||||
<p class="panel-tip">
|
||||
展示腾讯云 IM 单聊记录:已合并患者 与
|
||||
展示单聊记录:已合并患者 与
|
||||
<strong>所有医生 / 医助账号</strong>分别产生的会话,按时间排序。
|
||||
数据来自 IM 漫游;若未开通漫游或某账号与该患者无会话,对应侧无消息。
|
||||
</p>
|
||||
|
||||
@@ -459,6 +459,7 @@
|
||||
v-model="formData.tongue_images"
|
||||
:limit="9"
|
||||
type="image"
|
||||
:exclude-domain="true"
|
||||
/>
|
||||
<div class="form-tips">支持上传多张舌苔照片,最多9张</div>
|
||||
</el-form-item>
|
||||
@@ -468,6 +469,7 @@
|
||||
v-model="formData.report_files"
|
||||
:limit="10"
|
||||
type="image"
|
||||
:exclude-domain="true"
|
||||
/>
|
||||
<div class="form-tips">支持上传检查报告、病历、彩超等影像科检查图片,最多10个文件</div>
|
||||
</el-form-item>
|
||||
@@ -521,8 +523,8 @@
|
||||
|
||||
<el-form-item label="病史补充" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
v-mode="formData.remark"
|
||||
type="ltextarea"
|
||||
:rows="2"
|
||||
placeholder="请输入病史补充"
|
||||
/>
|
||||
|
||||
@@ -490,16 +490,17 @@ import { getDictData } from '@/api/app'
|
||||
import { getWeappConfig } from '@/api/channel/weapp'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import { Loading, Warning, List, CircleCheck, Clock, ArrowDown, RefreshRight, Picture, User, Document, Delete, CircleClose } from '@element-plus/icons-vue'
|
||||
import EditPopup from './edit.vue'
|
||||
import DetailPopup from './detail.vue'
|
||||
import AppointmentPopup from './appointment.vue'
|
||||
import AssistantWatchCallDialog from './components/AssistantWatchCallDialog.vue'
|
||||
import { Loading, Warning, List, CircleCheck, Clock, ArrowDown, Picture, User, Document, Delete, CircleClose } from '@element-plus/icons-vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { nextTick } from 'vue'
|
||||
import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const EditPopup = defineAsyncComponent(() => import('./edit.vue'))
|
||||
const DetailPopup = defineAsyncComponent(() => import('./detail.vue'))
|
||||
const AppointmentPopup = defineAsyncComponent(() => import('./appointment.vue'))
|
||||
const AssistantWatchCallDialog = defineAsyncComponent(() => import('./components/AssistantWatchCallDialog.vue'))
|
||||
|
||||
// 格式化时间戳为日期时间
|
||||
const formatDateTime = (timestamp: number | string) => {
|
||||
if (!timestamp) return '-'
|
||||
@@ -589,6 +590,26 @@ const fetchDateCounts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 首屏优先主列表,错峰拉各日 tab 条数,减轻与 lists 并发争抢 */
|
||||
function scheduleDeferredDateCounts() {
|
||||
const run = () => fetchDateCounts()
|
||||
if (typeof requestIdleCallback === 'function') {
|
||||
requestIdleCallback(run, { timeout: 2500 })
|
||||
} else {
|
||||
setTimeout(run, 120)
|
||||
}
|
||||
}
|
||||
|
||||
const SILENT_LIST_POLL_MS = 10_000
|
||||
let silentListPollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 定时静默刷新:不打断表格 loading,标签页隐藏时不请求 */
|
||||
function silentRefreshListsAndDateCounts() {
|
||||
if (typeof document !== 'undefined' && document.hidden) return
|
||||
void getLists({ silent: true }).catch(() => {})
|
||||
void fetchDateCounts()
|
||||
}
|
||||
|
||||
// 日期快捷选项(computed 保证每日刷新)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const todayStr = computed(() => {
|
||||
@@ -709,8 +730,22 @@ const getRowClassName = ({ row }: { row: any }) => {
|
||||
const editRef = ref()
|
||||
const detailRef = ref()
|
||||
const appointmentRef = ref()
|
||||
/** 从路由带 id 进入时,诊单弹窗为异步组件,需在 ref 就绪后再 open */
|
||||
const pendingRouteDiagnosisId = ref<number | null>(null)
|
||||
const userStore = useUserStore()
|
||||
|
||||
watch(
|
||||
[editRef, pendingRouteDiagnosisId],
|
||||
() => {
|
||||
const id = pendingRouteDiagnosisId.value
|
||||
const opener = editRef.value as { open?: (mode: string, diagnosisId: number) => void } | undefined
|
||||
if (id == null || !opener?.open) return
|
||||
opener.open('edit', id)
|
||||
pendingRouteDiagnosisId.value = null
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
const watchCallVisible = ref(false)
|
||||
const watchCallDiagnosisId = ref(0)
|
||||
|
||||
@@ -1241,16 +1276,27 @@ const handleDeleteRecord = async (id: number) => {
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
onMounted(async () => {
|
||||
await getDictOptions()
|
||||
onMounted(() => {
|
||||
// 默认展示当天挂号,突出重点
|
||||
formData.appointment_date = todayStr.value
|
||||
// 字典与列表并行,避免 await 字典阻塞首屏列表(LCP)
|
||||
getDictOptions()
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
// 从 URL 带 id 进入时自动打开诊单编辑(如:面诊结束通知点击跳转)
|
||||
const id = route.query.id
|
||||
if (id && editRef.value) {
|
||||
nextTick(() => editRef.value.open('edit', Number(id)))
|
||||
scheduleDeferredDateCounts()
|
||||
|
||||
silentListPollTimer = setInterval(silentRefreshListsAndDateCounts, SILENT_LIST_POLL_MS)
|
||||
|
||||
const raw = route.query.id
|
||||
if (raw != null && raw !== '') {
|
||||
const num = Number(raw)
|
||||
if (!Number.isNaN(num)) pendingRouteDiagnosisId.value = num
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (silentListPollTimer != null) {
|
||||
clearInterval(silentListPollTimer)
|
||||
silentListPollTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user