更新
This commit is contained in:
@@ -42,6 +42,13 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="挂号类型">
|
||||
<el-select v-model="queryParams.appointment_type" placeholder="全部" clearable class="!w-[140px]">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="视频问诊" value="video" />
|
||||
<el-option label="图文问诊" value="text" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-[130px]">
|
||||
<el-option label="已预约" :value="1" />
|
||||
@@ -115,7 +122,9 @@
|
||||
<div class="text-gray-500">{{ formatHm(row.appointment_time) }} · {{ row.period_desc }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" prop="appointment_type_desc" width="100" />
|
||||
<el-table-column label="挂号类型" width="110">
|
||||
<template #default="{ row }">{{ appointmentTypeDescription(row.appointment_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div class="text-sm">{{ row.channel_source_desc || '—' }}</div>
|
||||
@@ -178,7 +187,7 @@
|
||||
<el-radio-button label="all">全天</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="问诊类型" required>
|
||||
<el-form-item label="挂号类型" required>
|
||||
<el-select v-model="editForm.appointment_type" class="!w-full">
|
||||
<el-option label="视频问诊" value="video" />
|
||||
<el-option label="图文问诊" value="text" />
|
||||
@@ -292,6 +301,7 @@
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { appointmentAdminEdit, appointmentBatchEditChannel, appointmentLists } from '@/api/doctor'
|
||||
import { appointmentTypeDescription } from '@/utils/appointment-type'
|
||||
import { getAssistants } from '@/api/tcm'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
@@ -302,6 +312,7 @@ const queryParams = reactive({
|
||||
patient_name: '',
|
||||
doctor_name: '',
|
||||
assistant_id: undefined as number | undefined,
|
||||
appointment_type: '',
|
||||
status: '' as number | '',
|
||||
channel_source: '' as string
|
||||
})
|
||||
@@ -312,6 +323,7 @@ const queryInit = {
|
||||
patient_name: '',
|
||||
doctor_name: '',
|
||||
assistant_id: undefined as number | undefined,
|
||||
appointment_type: '',
|
||||
status: '' as number | '',
|
||||
channel_source: ''
|
||||
}
|
||||
|
||||
@@ -125,7 +125,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时间" min-width="165">
|
||||
<el-table-column label="挂号类型" min-width="110">
|
||||
<template #default="{ row }">
|
||||
{{ Number(row.appointment_id) > 0 ? appointmentTypeDescription(row.appointment_type) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时间" min-width="165">
|
||||
<template #default="{ row }">
|
||||
<span :class="['appointment-time', { empty: !row.appointment_time_text }]">
|
||||
{{ row.appointment_time_text || '暂无预约' }}
|
||||
@@ -324,7 +329,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { appointmentTypeDescription } from '@/utils/appointment-type'
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
@@ -451,12 +451,14 @@ const handleCall = async (row: QueueRow) => {
|
||||
try {
|
||||
const res = await getCallSignature({
|
||||
patient_id: sourcePatientId,
|
||||
appointment_id: Number(row.id),
|
||||
diagnosis_id: diagnosisId
|
||||
})
|
||||
chatDialogRef.value?.open({
|
||||
patientId: sourcePatientId,
|
||||
patientName: row.patient_name,
|
||||
diagnosisId,
|
||||
appointmentId: Number(row.id),
|
||||
appointmentType: row.appointment_type,
|
||||
signatureData: res
|
||||
})
|
||||
|
||||
@@ -163,7 +163,9 @@
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<!-- <el-table-column label="预约类型" prop="appointment_type_desc" width="100" show-overflow-tooltip /> -->
|
||||
<el-table-column label="面诊类型" width="110">
|
||||
<template #default="{ row }">{{ appointmentTypeDescription(row.appointment_type) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="确认诊单" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -219,8 +221,9 @@
|
||||
</el-button>
|
||||
|
||||
<template v-if="row.status !== 3">
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/videoQr']"
|
||||
<el-button
|
||||
v-if="canAppointmentVideoCall(row.appointment_type)"
|
||||
v-perms="['tcm.diagnosis/videoQr']"
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
@@ -237,7 +240,7 @@
|
||||
size="small"
|
||||
@click="handleChat(row)"
|
||||
>
|
||||
通话
|
||||
{{ canAppointmentVideoCall(row.appointment_type) ? '通话' : '图文沟通' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['doctor.appointment/complete']"
|
||||
@@ -340,8 +343,8 @@
|
||||
<span class="detail-value">{{ detailData.period === 'morning' ? '上午' : '下午' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">预约类型</span>
|
||||
<span class="detail-value">{{ detailData.appointment_type_desc }}</span>
|
||||
<span class="detail-label">面诊类型</span>
|
||||
<span class="detail-value">{{ appointmentTypeDescription(detailData.appointment_type) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -555,7 +558,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { appointmentTypeDescription, canAppointmentVideoCall } from '@/utils/appointment-type'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
|
||||
@@ -1012,7 +1016,8 @@ const handleChat = async (row: any) => {
|
||||
// 获取聊天签名信息
|
||||
const res = await getCallSignature({
|
||||
patient_id: sourcePatientId,
|
||||
diagnosis_id: diagnosisId
|
||||
diagnosis_id: diagnosisId,
|
||||
appointment_id: Number(row.id)
|
||||
})
|
||||
|
||||
console.log('获取聊天签名成功:', res)
|
||||
@@ -1022,6 +1027,7 @@ const handleChat = async (row: any) => {
|
||||
patientId: sourcePatientId,
|
||||
patientName: row.patient_name,
|
||||
diagnosisId,
|
||||
appointmentId: Number(row.id),
|
||||
appointmentType: row.appointment_type,
|
||||
signatureData: res // 传入签名数据
|
||||
})
|
||||
@@ -1032,7 +1038,11 @@ const handleChat = async (row: any) => {
|
||||
}
|
||||
|
||||
// 生成小程序二维码(跳转小程序路径:pages/login/login)
|
||||
const handleMiniProgramQRCode = async (row: any) => {
|
||||
const handleMiniProgramQRCode = async (row: any) => {
|
||||
if (!canAppointmentVideoCall(row.appointment_type)) {
|
||||
feedback.msgWarning('仅视频问诊可生成视频二维码')
|
||||
return
|
||||
}
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
|
||||
@@ -108,8 +108,12 @@
|
||||
<span class="apt-h5-row-key">预约</span>
|
||||
<span class="apt-h5-row-val">{{ row.appointment_date || '-' }} {{ row.appointment_time || '' }}</span>
|
||||
</div>
|
||||
<div class="apt-h5-row">
|
||||
<span class="apt-h5-row-key">医生</span>
|
||||
<div class="apt-h5-row">
|
||||
<span class="apt-h5-row-key">面诊类型</span>
|
||||
<span class="apt-h5-row-val">{{ appointmentTypeDescription(row.appointment_type) }}</span>
|
||||
</div>
|
||||
<div class="apt-h5-row">
|
||||
<span class="apt-h5-row-key">医生</span>
|
||||
<span class="apt-h5-row-val">{{ row.doctor_name || '-' }}</span>
|
||||
</div>
|
||||
<div class="apt-h5-row">
|
||||
@@ -147,7 +151,7 @@
|
||||
:icon="Phone"
|
||||
@click="handleChat(row)"
|
||||
>
|
||||
通话
|
||||
{{ canAppointmentVideoCall(row.appointment_type) ? '通话' : '图文沟通' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
|
||||
@@ -179,7 +183,7 @@
|
||||
>
|
||||
<div v-if="moreSheetRow" class="apt-h5-actions">
|
||||
<button
|
||||
v-if="moreSheetRow.status !== 3 && hasPermission(['tcm.diagnosis/videoQr'])"
|
||||
v-if="moreSheetRow.status !== 3 && canAppointmentVideoCall(moreSheetRow.appointment_type) && hasPermission(['tcm.diagnosis/videoQr'])"
|
||||
class="apt-h5-action"
|
||||
@click="runAction('videoQr')"
|
||||
>
|
||||
@@ -236,7 +240,7 @@
|
||||
<div><span>医生</span><strong>{{ detailData.doctor_name || '-' }}</strong></div>
|
||||
<div><span>预约时间</span><strong>{{ detailData.appointment_date || '-' }} {{ detailData.appointment_time || '' }}</strong></div>
|
||||
<div><span>时段</span><strong>{{ detailData.period === 'morning' ? '上午' : detailData.period === 'afternoon' ? '下午' : '-' }}</strong></div>
|
||||
<div><span>预约类型</span><strong>{{ detailData.appointment_type_desc || '-' }}</strong></div>
|
||||
<div><span>面诊类型</span><strong>{{ appointmentTypeDescription(detailData.appointment_type) }}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -336,7 +340,7 @@
|
||||
plain
|
||||
@click="handleChat(detailActionRow)"
|
||||
>
|
||||
通话
|
||||
{{ canAppointmentVideoCall(detailActionRow.appointment_type) ? '通话' : '图文沟通' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detailActionRow.status !== 3 && hasPermission(['doctor.appointment/complete'])"
|
||||
@@ -407,7 +411,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="tcmAppointmentH5">
|
||||
<script setup lang="ts" name="tcmAppointmentH5">
|
||||
import { appointmentTypeDescription, canAppointmentVideoCall } from '@/utils/appointment-type'
|
||||
import { appointmentDetail, appointmentLists, cancelAppointment, completeAppointment } from '@/api/doctor'
|
||||
import { getWeappConfig } from '@/api/channel/weapp'
|
||||
import {
|
||||
@@ -656,12 +661,14 @@ const handleChat = async (row: any) => {
|
||||
try {
|
||||
const res = await getCallSignature({
|
||||
patient_id: sourcePatientId,
|
||||
diagnosis_id: diagnosisId
|
||||
diagnosis_id: diagnosisId,
|
||||
appointment_id: Number(row.id)
|
||||
})
|
||||
chatDialogRef.value?.open({
|
||||
patientId: sourcePatientId,
|
||||
patientName: row.patient_name,
|
||||
diagnosisId,
|
||||
appointmentId: Number(row.id),
|
||||
appointmentType: row.appointment_type,
|
||||
signatureData: res
|
||||
})
|
||||
@@ -777,7 +784,11 @@ const qrcodeDialogVisible = ref(false)
|
||||
const qrcodeLoading = ref(false)
|
||||
const qrcodeUrl = ref('')
|
||||
const currentQRCodeRow = ref<any>(null)
|
||||
const handleMiniProgramQRCode = async (row: any) => {
|
||||
const handleMiniProgramQRCode = async (row: any) => {
|
||||
if (!canAppointmentVideoCall(row.appointment_type)) {
|
||||
feedback.msgWarning('仅视频问诊可生成视频二维码')
|
||||
return
|
||||
}
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
@@ -1184,8 +1195,9 @@ onUnmounted(() => {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.apt-h5-row-key {
|
||||
flex: 0 0 38px;
|
||||
.apt-h5-row-key {
|
||||
flex: 0 0 60px;
|
||||
white-space: nowrap;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 预约类型 -->
|
||||
<el-form-item label="预约类型:">
|
||||
<!-- 挂号类型 -->
|
||||
<el-form-item label="挂号类型:">
|
||||
<el-radio-group v-model="form.appointmentType">
|
||||
<el-radio value="video">视频问诊</el-radio>
|
||||
<el-radio value="text">图文问诊</el-radio>
|
||||
|
||||
@@ -2,29 +2,47 @@
|
||||
<div class="im-chat-record-panel">
|
||||
<el-alert type="info" show-icon :closable="false" class="mb-4" title="说明">
|
||||
<p class="panel-tip">
|
||||
展示单聊记录:已合并患者 与
|
||||
展示患者所有诊单中与
|
||||
<strong>所有医生 / 医助账号</strong>分别产生的会话,按时间排序。
|
||||
数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。
|
||||
打开页面后自动加载并同步最新消息,页面可见期间每 30 秒继续检查更新。
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar mb-3">
|
||||
<el-button type="primary" link :loading="syncing" @click="triggerSync">
|
||||
<el-icon class="mr-1"><Promotion /></el-icon>
|
||||
同步最新(后台异步)
|
||||
同步最新
|
||||
</el-button>
|
||||
<el-button type="primary" link :loading="loading" @click="reloadArchived">
|
||||
<el-icon class="mr-1"><Refresh /></el-icon>
|
||||
重新加载已归档
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="chat-wrap">
|
||||
<template v-if="!loading && rows.length">
|
||||
</div>
|
||||
|
||||
<p v-if="syncing" class="sync-status" role="status" aria-live="polite">
|
||||
<template v-if="history.phase === 'checking_accounts'">
|
||||
正在核验聊天账号<span v-if="history.candidateAccounts">,{{ history.checkedAccounts }} / {{ history.candidateAccounts }}</span>
|
||||
</template>
|
||||
<template v-else>正在同步聊天记录<span v-if="history.totalPeers">,已处理 {{ history.processedPeers }} / {{ history.totalPeers }} 个会话</span>,新增 {{ history.inserted }} 条</template>
|
||||
</p>
|
||||
<p v-else-if="history.lastSyncAt" class="sync-status">上次同步:{{ formatTime(history.lastSyncAt) }}</p>
|
||||
<p v-if="history.skippedAccounts" class="sync-status">已跳过 {{ history.skippedAccounts }} 个未注册或已失效的聊天账号,已有归档不受影响。</p>
|
||||
<el-alert v-if="history.readError" type="error" show-icon :closable="false" class="mb-3" :title="`读取聊天记录失败:${history.readError}`" />
|
||||
<el-alert v-if="history.syncError" type="error" show-icon :closable="false" class="mb-3" :title="`同步失败:${history.syncError}`" />
|
||||
<el-alert v-if="history.partialErrors.length" type="warning" show-icon :closable="false" class="mb-3" title="聊天记录未完全同步">
|
||||
<p v-for="error in history.partialErrors.slice(0, 3)" :key="error" class="panel-tip">{{ error }}</p>
|
||||
<details v-if="history.partialErrors.length > 3">
|
||||
<summary>查看其余 {{ history.partialErrors.length - 3 }} 项同步问题</summary>
|
||||
<p v-for="error in history.partialErrors.slice(3)" :key="error" class="panel-tip">{{ error }}</p>
|
||||
</details>
|
||||
</el-alert>
|
||||
|
||||
<div v-loading="loading && !rows.length" class="chat-wrap">
|
||||
<template v-if="rows.length">
|
||||
<div class="chat-list">
|
||||
<div
|
||||
v-for="item in rows"
|
||||
:key="item.raw.msg_id"
|
||||
:key="`${item.raw.from_account || ''}-${item.raw.to_account || ''}-${item.raw.msg_id}`"
|
||||
class="chat-row"
|
||||
:class="item.raw.is_from_doctor ? 'from-doctor' : 'from-patient'"
|
||||
>
|
||||
@@ -64,31 +82,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="暂无 IM 聊天记录" />
|
||||
<el-empty v-else-if="!loading" :description="emptyDescription" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, watch, computed } from 'vue'
|
||||
import { watch, computed, toRefs, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh, Promotion } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getImChatMessages, triggerImChatSync } from '@/api/tcm'
|
||||
import type { FriendlyParse } from '@/utils/im-business-message-parse'
|
||||
import { parseImBusinessPayload } from '@/utils/im-business-message-parse'
|
||||
import { parseImBusinessPayload } from '@/utils/im-business-message-parse'
|
||||
import { createImChatHistory } from '@/utils/im-chat-history'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const rawRows = shallowRef<any[]>([])
|
||||
const patientImId = ref('')
|
||||
const patientName = ref('')
|
||||
|
||||
const patientImHint = computed(() => patientImId.value || 'patient_*')
|
||||
const documentVisible = () => typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
||||
const controller = createImChatHistory({
|
||||
load: (diagnosisId) => getImChatMessages({ diagnosis_id: diagnosisId, only_archived: 1 }),
|
||||
sync: (diagnosisId, syncToken) => triggerImChatSync({ diagnosis_id: diagnosisId, ...(syncToken ? { sync_token: syncToken } : {}) }),
|
||||
notify: (kind, message) => ElMessage[kind](message),
|
||||
visible: documentVisible()
|
||||
})
|
||||
const history = controller.state
|
||||
const { loading, syncing, rows: rawRows, patientName } = toRefs(history)
|
||||
const emptyDescription = computed(() => {
|
||||
if (history.readError) return '聊天记录读取失败,请重试'
|
||||
if (history.syncError || history.partialErrors.length) return '暂未读取到聊天记录,请重试同步'
|
||||
if (syncing.value) return '正在同步聊天记录…'
|
||||
return history.hasLoaded ? '暂无聊天记录' : '正在加载聊天记录…'
|
||||
})
|
||||
|
||||
interface EnrichedRow {
|
||||
raw: any
|
||||
@@ -109,16 +136,21 @@ const MSG_TYPE_LABELS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const rows = computed<EnrichedRow[]>(() =>
|
||||
rawRows.value.map((row) => {
|
||||
const fr = parseImFriendly(row)
|
||||
let tag = ''
|
||||
if (fr?.tag) {
|
||||
tag = fr.tag
|
||||
} else {
|
||||
tag = MSG_TYPE_LABELS[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
||||
}
|
||||
return { raw: row, friendly: fr, tag }
|
||||
})
|
||||
rawRows.value.flatMap((row) => {
|
||||
const messages = row.msg_type === 'composite' && Array.isArray(row.parts)
|
||||
? row.parts.map((part: any, index: number) => ({ ...row, ...part, msg_id: `${row.msg_id}:${index}` }))
|
||||
: [row]
|
||||
return messages.map((row: any) => {
|
||||
const fr = parseImFriendly(row)
|
||||
let tag = ''
|
||||
if (fr?.tag) {
|
||||
tag = fr.tag
|
||||
} else {
|
||||
tag = MSG_TYPE_LABELS[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
||||
}
|
||||
return { raw: row, friendly: fr, tag }
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
function formatTime(ts: number | undefined) {
|
||||
@@ -145,54 +177,37 @@ function senderLabel(row: any) {
|
||||
return patientName.value ? `患者(${patientName.value})` : '患者'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = (await getImChatMessages({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
only_archived: 1
|
||||
})) as {
|
||||
lists?: any[]
|
||||
patient_im_id?: string
|
||||
patient_name?: string
|
||||
}
|
||||
rawRows.value = res?.lists || []
|
||||
patientImId.value = res?.patient_im_id || ''
|
||||
patientName.value = res?.patient_name || ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rawRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reloadArchived() {
|
||||
load()
|
||||
}
|
||||
|
||||
async function triggerSync() {
|
||||
if (!props.diagnosisId) return
|
||||
syncing.value = true
|
||||
try {
|
||||
await triggerImChatSync({ diagnosis_id: props.diagnosisId })
|
||||
ElMessage.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('发起同步失败')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
function reloadArchived() {
|
||||
return controller.reload()
|
||||
}
|
||||
|
||||
function triggerSync() {
|
||||
return controller.sync()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
(diagnosisId) => controller.setDiagnosis(diagnosisId),
|
||||
{ immediate: true }
|
||||
)
|
||||
)
|
||||
|
||||
let activated = true
|
||||
function updateVisibility() {
|
||||
controller.setVisible(activated && documentVisible())
|
||||
}
|
||||
onMounted(() => document.addEventListener('visibilitychange', updateVisibility))
|
||||
onActivated(() => {
|
||||
activated = true
|
||||
updateVisibility()
|
||||
})
|
||||
onDeactivated(() => {
|
||||
activated = false
|
||||
updateVisibility()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('visibilitychange', updateVisibility)
|
||||
controller.dispose()
|
||||
})
|
||||
|
||||
defineExpose({ refresh: reloadArchived })
|
||||
</script>
|
||||
@@ -209,9 +224,15 @@ defineExpose({ refresh: reloadArchived })
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sync-status {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.chat-wrap {
|
||||
min-height: 120px;
|
||||
|
||||
@@ -259,7 +259,20 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="确认" width="88" align="center">
|
||||
<el-table-column label="挂号类型" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="appointmentRows(row).length">
|
||||
<div v-for="apt in appointmentRows(row)" :key="apt.id || `${apt.doctor_id}-${apt.time_text}`" class="mb-2">
|
||||
<div>{{ appointmentTypeDescription(apt.appointment_type) }}</div>
|
||||
<div v-if="appointmentRows(row).length > 1" class="text-xs text-gray-500">
|
||||
{{ apt.doctor_name || '—' }} · {{ apt.time_text || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="apt-none">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="确认" width="88" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="isDiagnosisConfirmed(row)" class="status-confirmed">已确认</span>
|
||||
<span v-else class="status-unconfirmed">未确认</span>
|
||||
@@ -388,7 +401,7 @@
|
||||
<el-icon><Remove /></el-icon>取消指派
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="isAppointmentActiveForVideo(row) && hasPermission(['tcm.diagnosis/videoQr'])" command="videoQr"><el-icon><Picture /></el-icon>视频二维码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/guahao']) && isAppointmentActiveForVideo(row)" command="confirmQr"><el-icon><Picture /></el-icon>二维码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/guahao']) && hasActiveAppointment(row)" command="confirmQr"><el-icon><Picture /></el-icon>二维码</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="canCancelAppointmentFromDropdown(row) && hasPermission(['tcm.diagnosis/guahao'])"
|
||||
command="cancelApt"
|
||||
@@ -731,7 +744,8 @@ import { usePaging } from '@/hooks/usePaging'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { getWeappConfig } from '@/api/channel/weapp'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import { appointmentTypeDescription, canAppointmentVideoCall } from '@/utils/appointment-type'
|
||||
import { Loading, Warning, List, CircleCheck, Clock, ArrowDown, Picture, User, Document, Delete, CircleClose, Remove } from '@element-plus/icons-vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -1377,10 +1391,11 @@ type VideoCallHint = {
|
||||
end_time?: number
|
||||
}
|
||||
|
||||
const canWatchCallEntry = (row: any) =>
|
||||
isAssignedAssistant(row) && hasPermission(['tcm.diagnosis/watchCall'])
|
||||
const canWatchCallEntry = (row: any) =>
|
||||
isAppointmentActiveForVideo(row) && isAssignedAssistant(row) && hasPermission(['tcm.diagnosis/watchCall'])
|
||||
|
||||
const watchCallState = (row: any) => (row.video_call_hint?.state as string) || 'none'
|
||||
const watchCallState = (row: any) =>
|
||||
isAppointmentActiveForVideo(row) ? (row.video_call_hint?.state as string) || 'none' : 'none'
|
||||
|
||||
const watchCallLiveTooltip = (row: any) => {
|
||||
const h = row.video_call_hint as VideoCallHint | undefined
|
||||
@@ -1404,7 +1419,11 @@ const watchCallEnterTooltip = (row: any) => {
|
||||
return watchCallLiveTooltip(row)
|
||||
}
|
||||
|
||||
const onWatchCallEntryClick = (row: any) => {
|
||||
const onWatchCallEntryClick = (row: any) => {
|
||||
if (!canWatchCallEntry(row)) {
|
||||
feedback.msgWarning('仅已预约的视频问诊可进入视频旁观')
|
||||
return
|
||||
}
|
||||
if (watchCallState(row) !== 'live') {
|
||||
feedback.msgWarning('医生尚未接通或未同步房间号,请稍后再试')
|
||||
return
|
||||
@@ -1716,7 +1735,7 @@ const handleRowAction = (cmd: string, row: any) => {
|
||||
case 'cancelAssign': handleCancelAssign(row); break
|
||||
case 'videoQr':
|
||||
if (!isAppointmentActiveForVideo(row)) {
|
||||
feedback.msgWarning('仅「已预约」状态可生成视频二维码')
|
||||
feedback.msgWarning('仅已预约的视频问诊可生成视频二维码')
|
||||
return
|
||||
}
|
||||
handleVideoQRCode(row)
|
||||
@@ -1753,7 +1772,8 @@ const appointmentRows = (row: any) => {
|
||||
status: row.appointment_status,
|
||||
doctor_id: row.appointment_doctor_id,
|
||||
doctor_name: row.appointment_doctor_name,
|
||||
time_text: row.appointment_time_text
|
||||
time_text: row.appointment_time_text,
|
||||
appointment_type: row.appointment_type
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -1779,12 +1799,14 @@ const appointmentCellClasses = (row: any) => {
|
||||
* 找到当前可用的已预约挂号。
|
||||
* 同一诊单当天可能先完成一条挂号、随后又新增一条预约,此时行级 appointment_* 仍可能指向旧记录。
|
||||
*/
|
||||
const activeAppointment = (row: any) =>
|
||||
appointmentRows(row).find((apt: any) => Number(apt?.status) === 1) ?? null
|
||||
const activeAppointment = (row: any, videoOnly = false) =>
|
||||
appointmentRows(row).find((apt: any) =>
|
||||
Number(apt?.status) === 1 && (!videoOnly || canAppointmentVideoCall(apt.appointment_type))
|
||||
) ?? null
|
||||
|
||||
/** 二维码必须使用已预约挂号对应的医生和时间,不能继续沿用行级旧挂号字段。 */
|
||||
const activeAppointmentRow = (row: any) => {
|
||||
const apt = activeAppointment(row)
|
||||
const activeAppointmentRow = (row: any, videoOnly = false) => {
|
||||
const apt = activeAppointment(row, videoOnly)
|
||||
if (!apt) return null
|
||||
|
||||
return {
|
||||
@@ -1794,12 +1816,14 @@ const activeAppointmentRow = (row: any) => {
|
||||
appointment_status: apt.status,
|
||||
appointment_doctor_id: apt.doctor_id,
|
||||
appointment_doctor_name: apt.doctor_name,
|
||||
appointment_time_text: apt.time_text
|
||||
appointment_time_text: apt.time_text,
|
||||
appointment_type: apt.appointment_type
|
||||
}
|
||||
}
|
||||
|
||||
/** 任一挂号记录处于已预约(1)即可进视频/小程序码。 */
|
||||
const isAppointmentActiveForVideo = (row: any) => !!activeAppointment(row)
|
||||
/** 诊单二维码适用于所有已预约挂号;视频入口仅适用于视频问诊。 */
|
||||
const hasActiveAppointment = (row: any) => !!activeAppointment(row)
|
||||
const isAppointmentActiveForVideo = (row: any) => !!activeAppointment(row, true)
|
||||
|
||||
/** 已预约、已过号可取消(后端同步限制),针对行上主字段 */
|
||||
const canCancelAppointmentRow = (row: any) => {
|
||||
@@ -1983,11 +2007,11 @@ const submitFillIdCard = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成视频二维码(跳转登录页)- 仅已预约(1)可生成
|
||||
// 生成视频二维码(跳转登录页)- 仅已预约的视频问诊可生成
|
||||
const handleVideoQRCode = async (row: any) => {
|
||||
const qrcodeRow = activeAppointmentRow(row)
|
||||
const qrcodeRow = activeAppointmentRow(row, true)
|
||||
if (!qrcodeRow) {
|
||||
feedback.msgWarning('仅「已预约」状态可生成视频二维码')
|
||||
feedback.msgWarning('仅已预约的视频问诊可生成视频二维码')
|
||||
return
|
||||
}
|
||||
if (!qrcodeRow.patient_id) {
|
||||
|
||||
@@ -155,13 +155,15 @@
|
||||
>
|
||||
<span class="dh5-apt-status">{{ appointmentStatusLabelByStatus(apt.status) }}</span>
|
||||
<span class="dh5-apt-doctor">{{ apt.doctor_name || '-' }}</span>
|
||||
<span class="dh5-apt-time">{{ apt.time_text || '-' }}</span>
|
||||
<span class="dh5-apt-time">{{ apt.time_text || '-' }}</span>
|
||||
<span class="dh5-apt-type">挂号类型:{{ appointmentTypeDescription(apt.appointment_type) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="dh5-apt-status">{{ appointmentStatusLabel(row) }}</span>
|
||||
<span class="dh5-apt-doctor">{{ row.appointment_doctor_name || '-' }}</span>
|
||||
<span class="dh5-apt-time">{{ row.appointment_time_text || '-' }}</span>
|
||||
<span class="dh5-apt-time">{{ row.appointment_time_text || '-' }}</span>
|
||||
<span class="dh5-apt-type">挂号类型:{{ appointmentTypeDescription(row.appointment_type) }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<span v-else class="dh5-apt-none">未挂号</span>
|
||||
@@ -299,7 +301,7 @@
|
||||
<span>视频二维码</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isAppointmentActiveForVideo(moreSheetRow) && hasPermission(['tcm.diagnosis/guahao'])"
|
||||
v-if="hasActiveAppointment(moreSheetRow) && hasPermission(['tcm.diagnosis/guahao'])"
|
||||
class="dh5-action"
|
||||
@click="runAction('confirmQr')"
|
||||
>
|
||||
@@ -584,7 +586,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="tcmDiagnosisH5">
|
||||
<script setup lang="ts" name="tcmDiagnosisH5">
|
||||
import { appointmentTypeDescription, canAppointmentVideoCall } from '@/utils/appointment-type'
|
||||
import {
|
||||
tcmDiagnosisLists,
|
||||
tcmDiagnosisDelete,
|
||||
@@ -924,7 +927,8 @@ const appointmentRows = (row: any) => {
|
||||
status: row.appointment_status,
|
||||
doctor_id: row.appointment_doctor_id,
|
||||
doctor_name: row.appointment_doctor_name,
|
||||
time_text: row.appointment_time_text
|
||||
time_text: row.appointment_time_text,
|
||||
appointment_type: row.appointment_type
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -949,11 +953,13 @@ const appointmentRowClass = (row: any) => {
|
||||
/**
|
||||
* 同一诊单可能同时有已完成的旧挂号和已预约的新挂号,二维码应使用明细里的有效预约。
|
||||
*/
|
||||
const activeAppointment = (row: any) =>
|
||||
appointmentRows(row).find((apt: any) => Number(apt?.status) === 1) ?? null
|
||||
const activeAppointment = (row: any, videoOnly = false) =>
|
||||
appointmentRows(row).find((apt: any) =>
|
||||
Number(apt?.status) === 1 && (!videoOnly || canAppointmentVideoCall(apt.appointment_type))
|
||||
) ?? null
|
||||
|
||||
const activeAppointmentRow = (row: any) => {
|
||||
const apt = activeAppointment(row)
|
||||
const activeAppointmentRow = (row: any, videoOnly = false) => {
|
||||
const apt = activeAppointment(row, videoOnly)
|
||||
if (!apt) return null
|
||||
|
||||
return {
|
||||
@@ -963,11 +969,13 @@ const activeAppointmentRow = (row: any) => {
|
||||
appointment_status: apt.status,
|
||||
appointment_doctor_id: apt.doctor_id,
|
||||
appointment_doctor_name: apt.doctor_name,
|
||||
appointment_time_text: apt.time_text
|
||||
appointment_time_text: apt.time_text,
|
||||
appointment_type: apt.appointment_type
|
||||
}
|
||||
}
|
||||
|
||||
const isAppointmentActiveForVideo = (row: any) => !!activeAppointment(row)
|
||||
const hasActiveAppointment = (row: any) => !!activeAppointment(row)
|
||||
const isAppointmentActiveForVideo = (row: any) => !!activeAppointment(row, true)
|
||||
|
||||
const canCancelAppointmentRow = (row: any) => {
|
||||
const s = Number(row.appointment_status)
|
||||
@@ -981,9 +989,10 @@ const isAssignedAssistant = (row: any) => {
|
||||
if (aid === null || aid === undefined || aid === '') return false
|
||||
return Number(aid) === Number(userStore.userInfo?.id)
|
||||
}
|
||||
const canWatchCallEntry = (row: any) =>
|
||||
isAssignedAssistant(row) && hasPermission(['tcm.diagnosis/watchCall'])
|
||||
const watchCallState = (row: any) => (row.video_call_hint?.state as string) || 'none'
|
||||
const canWatchCallEntry = (row: any) =>
|
||||
isAppointmentActiveForVideo(row) && isAssignedAssistant(row) && hasPermission(['tcm.diagnosis/watchCall'])
|
||||
const watchCallState = (row: any) =>
|
||||
isAppointmentActiveForVideo(row) ? (row.video_call_hint?.state as string) || 'none' : 'none'
|
||||
const watchCallShowEnterButton = (row: any) => {
|
||||
const st = watchCallState(row)
|
||||
return st === 'live' || st === 'pending_room'
|
||||
@@ -1015,7 +1024,11 @@ const watchCallVisibleForRow = (row: any) => {
|
||||
}
|
||||
const watchCallDialogVisible = ref(false)
|
||||
const watchCallDiagnosisId = ref(0)
|
||||
const onWatchCallEntryClick = (row: any) => {
|
||||
const onWatchCallEntryClick = (row: any) => {
|
||||
if (!canWatchCallEntry(row)) {
|
||||
feedback.msgWarning('仅已预约的视频问诊可进入视频旁观')
|
||||
return
|
||||
}
|
||||
if (watchCallState(row) !== 'live') {
|
||||
feedback.msgWarning('医生未接通或未同步房间号,请稍后再试')
|
||||
return
|
||||
@@ -1087,7 +1100,7 @@ const runAction = (cmd: string) => {
|
||||
case 'assign': handleSingleAssign(row); break
|
||||
case 'videoQr':
|
||||
if (!isAppointmentActiveForVideo(row)) {
|
||||
feedback.msgWarning('仅「已预约」状态可生成视频二维码')
|
||||
feedback.msgWarning('仅已预约的视频问诊可生成视频二维码')
|
||||
return
|
||||
}
|
||||
handleVideoQRCode(row); break
|
||||
@@ -1262,9 +1275,9 @@ const qrcodeAppointmentTimeText = computed(() => {
|
||||
})
|
||||
|
||||
const handleVideoQRCode = async (row: any) => {
|
||||
const qrcodeRow = activeAppointmentRow(row)
|
||||
const qrcodeRow = activeAppointmentRow(row, true)
|
||||
if (!qrcodeRow) {
|
||||
feedback.msgWarning('仅「已预约」状态可生成视频二维码'); return
|
||||
feedback.msgWarning('仅已预约的视频问诊可生成视频二维码'); return
|
||||
}
|
||||
if (!qrcodeRow.patient_id) { feedback.msgWarning('患者信息不完整'); return }
|
||||
lastQRCodeType.value = 'video'
|
||||
@@ -1805,8 +1818,9 @@ $dh5-card-bg: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.dh5-apt-item {
|
||||
display: flex;
|
||||
.dh5-apt-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
@@ -1830,11 +1844,17 @@ $dh5-card-bg: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dh5-apt-time {
|
||||
.dh5-apt-time {
|
||||
color: $dh5-text-mute;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dh5-apt-type {
|
||||
flex-basis: 100%;
|
||||
color: $dh5-text-mute;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dh5-apt-none {
|
||||
color: $dh5-warn;
|
||||
|
||||
Reference in New Issue
Block a user