新增功能
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="appointment-record-panel">
|
||||
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无挂号记录">
|
||||
<el-table-column label="ID" prop="id" width="72" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="
|
||||
row.status === 1
|
||||
? 'success'
|
||||
: row.status === 2
|
||||
? 'info'
|
||||
: row.status === 3
|
||||
? 'primary'
|
||||
: 'danger'
|
||||
"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ row.status_desc || '—' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="患者(挂号人)" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="font-medium">{{ row.patient_name || '—' }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ row.patient_phone || '' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂号医生" prop="doctor_name" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="挂号助理" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.assistant_name || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时间" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span>{{ row.appointment_date || '—' }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ formatTime(row.appointment_time) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时段" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ periodText(row.period) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.appointment_type_desc || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ channelLabel(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="确认诊单" width="92" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.diagnosis_confirmed" type="success" size="small" effect="plain">已确认</el-tag>
|
||||
<el-tag v-else type="warning" size="small" effect="plain">未确认</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开方" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.has_prescription" type="success" size="small" effect="plain">已开方</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未开方</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="165" prop="create_time" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { appointmentLists } from '@/api/doctor'
|
||||
import { getDictData } from '@/api/app'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
const channelMap = ref<Record<string, string>>({})
|
||||
|
||||
function formatTime(t: string | undefined) {
|
||||
if (!t) return ''
|
||||
return String(t).length >= 8 ? String(t).slice(0, 5) : t
|
||||
}
|
||||
|
||||
function periodText(p: string | undefined) {
|
||||
if (p === 'morning') return '上午'
|
||||
if (p === 'afternoon') return '下午'
|
||||
if (p === 'all') return '全天'
|
||||
return p || '—'
|
||||
}
|
||||
|
||||
function channelLabel(row: Record<string, any>) {
|
||||
const raw = row.channel_source ?? row.channels ?? ''
|
||||
if (raw === '' || raw === null || raw === undefined) return '—'
|
||||
const key = String(raw)
|
||||
return channelMap.value[key] || key
|
||||
}
|
||||
|
||||
function sortRows(list: any[]) {
|
||||
return [...list].sort((a, b) => {
|
||||
const da = String(a.appointment_date || '')
|
||||
const db = String(b.appointment_date || '')
|
||||
if (da !== db) return db.localeCompare(da)
|
||||
const ta = String(a.appointment_time || '')
|
||||
const tb = String(b.appointment_time || '')
|
||||
if (ta !== tb) return tb.localeCompare(ta)
|
||||
return Number(b.id || 0) - Number(a.id || 0)
|
||||
})
|
||||
}
|
||||
|
||||
const loadChannels = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'channels' })
|
||||
const opts = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
const map: Record<string, string> = {}
|
||||
for (const row of opts) {
|
||||
if (row.value != null) map[String(row.value)] = row.name || String(row.value)
|
||||
}
|
||||
channelMap.value = map
|
||||
} catch {
|
||||
channelMap.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
await loadChannels()
|
||||
const res = await appointmentLists({
|
||||
patient_id: props.diagnosisId,
|
||||
page_no: 1,
|
||||
page_size: 500
|
||||
})
|
||||
const list = res?.lists || []
|
||||
rows.value = sortRows(list)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="assign-log-panel">
|
||||
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无指派记录">
|
||||
<el-table-column label="操作时间" width="175" prop="create_time_text" />
|
||||
<el-table-column label="原医助" min-width="120" prop="from_assistant_name" />
|
||||
<el-table-column label="新医助" min-width="120" prop="to_assistant_name" />
|
||||
<el-table-column label="操作人" width="110" prop="operator_name" />
|
||||
<el-table-column label="操作账号" width="120" prop="operator_account" show-overflow-tooltip />
|
||||
<el-table-column label="IP" width="130" prop="ip" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { tcmDiagnosisAssignLogList } from '@/api/tcm'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await tcmDiagnosisAssignLogList({ id: props.diagnosisId })
|
||||
rows.value = Array.isArray(data) ? data : []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
</script>
|
||||
@@ -72,6 +72,8 @@ import { ref, watch, computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getImChatMessages } from '@/api/tcm'
|
||||
import type { FriendlyParse } from '@/utils/im-business-message-parse'
|
||||
import { parseImBusinessPayload } from '@/utils/im-business-message-parse'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
@@ -90,109 +92,6 @@ function formatTime(ts: number | undefined) {
|
||||
return dayjs.unix(sec).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
type FriendlyParse = { main: string; sub?: string; tag?: string }
|
||||
|
||||
/** 业务时间:支持秒级时间戳或毫秒(字符串数字) */
|
||||
function formatBizTime(t: unknown): string | undefined {
|
||||
if (t == null || t === '') return undefined
|
||||
const n =
|
||||
typeof t === 'string' && /^\d+$/.test(t.trim())
|
||||
? parseInt(t.trim(), 10)
|
||||
: Number(t)
|
||||
if (!Number.isFinite(n) || n <= 0) return undefined
|
||||
return n > 1e12
|
||||
? dayjs(n).format('YYYY-MM-DD HH:mm:ss')
|
||||
: dayjs.unix(n).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
/** 将 IM 自定义 / 信令 JSON 解析为可读文案(医生进诊室、音视频通话等) */
|
||||
function parseImBusinessPayload(raw: string): FriendlyParse | null {
|
||||
const s = raw.trim()
|
||||
if (!s.startsWith('{')) return null
|
||||
let o: any
|
||||
try {
|
||||
o = JSON.parse(s)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!('businessID' in o) && !('cmd' in o)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (o.businessID === 'doctor_entered_consult_room') {
|
||||
const t = formatBizTime(o.time)
|
||||
return { main: '医生已进入诊室', sub: t, tag: '诊室' }
|
||||
}
|
||||
|
||||
/** 小程序端:患者打开与医生的会话时发送(与 TUIKit/chat.vue 一致) */
|
||||
if (o.businessID === 'patient_opened_chat') {
|
||||
const parts: string[] = []
|
||||
if (o.patientName) parts.push(`患者:${o.patientName}`)
|
||||
if (o.patientId != null && o.patientId !== '') parts.push(`患者 ID:${o.patientId}`)
|
||||
if (o.doctorId != null && o.doctorId !== '') parts.push(`关联医生:${o.doctorId}`)
|
||||
const t = formatBizTime(o.time)
|
||||
if (t) parts.push(t)
|
||||
return { main: '患者进入聊天', sub: parts.length ? parts.join(' · ') : undefined, tag: '诊室' }
|
||||
}
|
||||
|
||||
if (o.businessID === 'user_typing_status') {
|
||||
return { main: '对方正在输入…', tag: '状态' }
|
||||
}
|
||||
|
||||
if (o.businessID === 'consultation_complete') {
|
||||
return { main: '问诊已完成', sub: formatBizTime(o.time), tag: '诊室' }
|
||||
}
|
||||
|
||||
if (o.businessID === 1 || o.businessID === '1') {
|
||||
let inner: any = o.data
|
||||
if (typeof inner === 'string') {
|
||||
try {
|
||||
inner = JSON.parse(inner)
|
||||
} catch {
|
||||
return { main: '通话信令', sub: '内层数据解析失败', tag: '通话' }
|
||||
}
|
||||
}
|
||||
if (inner && typeof inner === 'object') {
|
||||
return parseRtcInner(inner)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (o.businessID === 'rtc_call' || o.cmd) {
|
||||
return parseRtcInner(o)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseRtcInner(inner: any): FriendlyParse {
|
||||
const callType = inner.call_type
|
||||
const callTypeLabel =
|
||||
callType === 2 ? '视频通话' : callType === 1 ? '语音通话' : '通话'
|
||||
const cmd = String(inner.cmd || '')
|
||||
const cmdMap: Record<string, string> = {
|
||||
hangup: '已结束',
|
||||
invite: '发起通话',
|
||||
linebusy: '对方忙线',
|
||||
cancel: '已取消',
|
||||
reject: '已拒绝',
|
||||
accept: '已接听',
|
||||
timeout: '无人接听'
|
||||
}
|
||||
const action = cmdMap[cmd] || (cmd ? `状态:${cmd}` : '')
|
||||
const main = action ? `${callTypeLabel} · ${action}` : callTypeLabel
|
||||
const parts: string[] = []
|
||||
if (inner.inviter) parts.push(`发起方 ${inner.inviter}`)
|
||||
if (inner.invitee) parts.push(`对方 ${inner.invitee}`)
|
||||
if (inner.groupID) parts.push(`群组 ${inner.groupID}`)
|
||||
return {
|
||||
main,
|
||||
sub: parts.length ? parts.join(',') : undefined,
|
||||
tag: '通话'
|
||||
}
|
||||
}
|
||||
|
||||
function parseImFriendly(row: any): FriendlyParse | null {
|
||||
const raw = (row.text || '').trim()
|
||||
if (!raw) return null
|
||||
|
||||
Reference in New Issue
Block a user