This commit is contained in:
Your Name
2026-03-27 18:06:12 +08:00
parent 9160c36735
commit 099bc1dd22
645 changed files with 276473 additions and 957 deletions
@@ -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'])
+52 -47
View File
@@ -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">
+47 -3
View File
@@ -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>
+4 -2
View File
@@ -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="请输入病史补充"
/>
+59 -13
View File
@@ -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>