Files
zyt/admin/src/views/tcm/appointment/list_h5.vue
T
long 3c35ab67cd update
h5 :问诊列表
add 完成诊单  +医生备注
2026-06-01 17:00:38 +08:00

1831 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="apt-h5">
<template v-if="currentView === 'list'">
<header class="apt-h5-topbar">
<div class="apt-h5-search">
<el-input
v-model="formData.patient_name"
placeholder="患者姓名"
clearable
:prefix-icon="Search"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-button class="apt-h5-search-btn" type="primary" @click="handleSearch">查询</el-button>
</div>
<div class="apt-h5-status apt-h5-scroll">
<span
v-for="tab in statusTabs"
:key="tab.value"
:class="['apt-h5-chip', { active: activeTab === tab.value }]"
@click="handleTabChange(tab.value)"
>
{{ tab.label }}
<em v-if="tab.count">{{ tab.count }}</em>
</span>
</div>
<div class="apt-h5-date apt-h5-scroll">
<span
v-for="item in dateTabs"
:key="item.value || 'all'"
:class="['apt-h5-pill', { active: formData.date_preset === item.value }]"
@click="handleDatePresetChange(item.value)"
>
{{ item.label }}
</span>
</div>
<div class="apt-h5-quick">
<div class="apt-h5-quick-group">
<span class="apt-h5-quick-label">确认</span>
<span
v-for="item in confirmedOptions"
:key="item.value || 'all'"
:class="['apt-h5-pill', { active: formData.diagnosis_confirmed === item.value }]"
@click="handleConfirmedChange(item.value)"
>
{{ item.label }}
</span>
</div>
<el-button link type="primary" size="small" class="apt-h5-more-toggle" @click="showMoreFilter = !showMoreFilter">
{{ showMoreFilter ? '收起' : '更多' }}
<el-icon :class="{ 'rotate-180': showMoreFilter }"><ArrowDown /></el-icon>
</el-button>
</div>
<div v-show="showMoreFilter" class="apt-h5-more-filter">
<el-input
v-if="isAdmin"
v-model="formData.doctor_name"
placeholder="医生"
clearable
size="small"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-date-picker
v-model="customDateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
size="small"
class="apt-h5-date-range"
@change="applyCustomDate"
/>
<el-button size="small" @click="handleReset">重置</el-button>
</div>
</header>
<main class="apt-h5-list" v-loading="pager.loading">
<div v-if="pager.lists.length === 0 && !pager.loading" class="apt-h5-empty">
<el-empty description="暂无问诊" :image-size="120" />
</div>
<article v-for="row in pager.lists" :key="row.id" :class="['apt-h5-card', cardClass(row)]">
<header class="apt-h5-card-head" @click="handleDetail(row)">
<div class="apt-h5-avatar">{{ (row.patient_name || '患').charAt(0) }}</div>
<div class="apt-h5-title">
<div class="apt-h5-name">
{{ row.patient_name || '-' }}
<span v-if="row.gender === 1 || row.gender === 0" class="apt-h5-meta">{{ row.gender === 1 ? '' : '' }}</span>
<span v-if="row.age != null" class="apt-h5-meta">{{ row.age }}</span>
</div>
<div class="apt-h5-sub">#{{ row.id }} · {{ maskPhone(row.patient_phone) || '-' }}</div>
</div>
<div class="apt-h5-tags">
<span :class="['apt-h5-tag', statusTagClass(row.status)]">{{ row.status_desc || statusLabel(row.status) }}</span>
<span v-if="row.diagnosis_confirmed" class="apt-h5-tag apt-h5-tag--ok">已确认</span>
<span v-else class="apt-h5-tag apt-h5-tag--warn">未确认</span>
</div>
</header>
<section class="apt-h5-card-body" @click="handleDetail(row)">
<div class="apt-h5-row">
<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>
<span class="apt-h5-row-val">{{ row.doctor_name || '-' }}</span>
</div>
<div class="apt-h5-row">
<span class="apt-h5-row-key">助理</span>
<span class="apt-h5-row-val">{{ row.assistant_name || '-' }}</span>
</div>
<div class="apt-h5-row">
<span class="apt-h5-row-key">开方</span>
<span class="apt-h5-row-val">
<span v-if="row.has_prescription" class="apt-h5-inline-tag apt-h5-inline-tag--ok">已开方</span>
<span v-else class="apt-h5-inline-tag">未开方</span>
<span v-if="row.has_prescription && row.prescription_is_system_auto === 1" class="apt-h5-inline-tag apt-h5-inline-tag--warn">系统代开</span>
</span>
</div>
<div v-if="row.remark" class="apt-h5-row apt-h5-row--remark">
<span class="apt-h5-row-key">备注</span>
<span class="apt-h5-row-val">{{ row.remark }}</span>
</div>
</section>
<footer class="apt-h5-card-foot">
<el-button
v-if="hasPermission(['tcm.diagnosis/edit'])"
type="primary"
size="small"
plain
@click="handleEdit(row)"
>
编辑患者
</el-button>
<el-button
v-if="row.status !== 3 && hasPermission(['doctor.appointment/prescription'])"
type="primary"
size="small"
:icon="Phone"
@click="handleChat(row)"
>
通话
</el-button>
<el-button
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
:type="prescriptionActionLabel(row) === '查看' ? 'success' : 'primary'"
size="small"
plain
@click="handlePrescription(row)"
>
{{ prescriptionActionLabel(row) }}
</el-button>
<el-button size="small" class="apt-h5-more-btn" @click="openMoreSheet(row)">
更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
</footer>
</article>
<div v-if="hasMore" class="apt-h5-load-more" @click="loadMore">
{{ pager.loading ? '加载中...' : '加载更多' }}
</div>
<div v-if="!hasMore && pager.lists.length > 0" class="apt-h5-no-more">没有更多了</div>
</main>
<el-drawer
v-model="moreSheetVisible"
:title="moreSheetRow ? `${moreSheetRow.patient_name || '问诊'} · 更多操作` : '更多操作'"
direction="btt"
size="auto"
:destroy-on-close="true"
>
<div v-if="moreSheetRow" class="apt-h5-actions">
<button
v-if="moreSheetRow.status !== 3 && hasPermission(['tcm.diagnosis/videoQr'])"
class="apt-h5-action"
@click="runAction('videoQr')"
>
<el-icon><Picture /></el-icon>
<span>视频二维码</span>
</button>
<button
v-if="moreSheetRow.status !== 3 && hasPermission(['doctor.appointment/complete'])"
class="apt-h5-action"
@click="runAction('complete')"
>
<el-icon><CircleCheck /></el-icon>
<span>完成</span>
</button>
<button
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
class="apt-h5-action"
@click="runAction('case')"
>
<el-icon><Document /></el-icon>
<span>病历</span>
</button>
<button
v-if="moreSheetRow.status === 1 && hasPermission(['doctor.appointment/cancel'])"
class="apt-h5-action apt-h5-action--danger"
@click="runAction('cancel')"
>
<el-icon><CircleClose /></el-icon>
<span>取消挂号</span>
</button>
</div>
</el-drawer>
</template>
<template v-else>
<header class="apt-h5-detail-topbar">
<button class="apt-h5-back" @click="backToList">
<span></span>
返回
</button>
<div class="apt-h5-detail-title">
<strong>{{ detailData?.patient_name || detailSourceRow?.patient_name || '问诊详情' }}</strong>
<small>{{ detailData?.appointment_date || detailSourceRow?.appointment_date || '' }} {{ detailData?.appointment_time || detailSourceRow?.appointment_time || '' }}</small>
</div>
</header>
<main v-loading="detailLoading" class="apt-h5-detail-page">
<template v-if="detailData">
<section class="apt-h5-detail-section">
<h3>预约信息</h3>
<div class="apt-h5-detail-grid">
<div><span>挂号ID</span><strong>{{ detailData.id || '-' }}</strong></div>
<div><span>状态</span><strong>{{ detailData.status_desc || statusLabel(detailData.status) }}</strong></div>
<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>
</section>
<section class="apt-h5-detail-section">
<h3>患者信息</h3>
<div class="apt-h5-detail-grid">
<div><span>姓名</span><strong>{{ detailData.patient_name || '-' }}</strong></div>
<div><span>手机号</span><strong>{{ detailData.phone || detailData.patient_phone || '-' }}</strong></div>
<div class="apt-h5-detail-full"><span>身份证号</span><strong>{{ detailData.id_card || '-' }}</strong></div>
<div><span>性别</span><strong>{{ detailData.gender === 1 ? '男' : detailData.gender === 0 ? '女' : '-' }}</strong></div>
<div><span>年龄</span><strong>{{ detailData.age != null ? `${detailData.age}岁` : '-' }}</strong></div>
<div><span>诊断日期</span><strong>{{ detailData.diagnosis_date || '-' }}</strong></div>
<div><span>婚姻状态</span><strong>{{ maritalStatusLabel(detailData.marital_status) }}</strong></div>
<div><span>身高(cm)</span><strong>{{ detailData.height ?? '-' }}</strong></div>
<div><span>体重(kg)</span><strong>{{ detailData.weight ?? '-' }}</strong></div>
<div class="apt-h5-detail-full"><span>地区</span><strong>{{ detailData.region || '-' }}</strong></div>
<div><span>收缩压(mmHg)</span><strong>{{ detailData.systolic_pressure ?? '-' }}</strong></div>
<div><span>舒张压(mmHg)</span><strong>{{ detailData.diastolic_pressure ?? '-' }}</strong></div>
<div><span>空腹血糖(mmol/L)</span><strong>{{ detailData.fasting_blood_sugar ?? '-' }}</strong></div>
<div><span>诊断类型</span><strong>{{ getDictLabel(diagnosisTypeOptions, detailData.diagnosis_type) }}</strong></div>
<div><span>证型</span><strong>{{ getDictLabel(syndromeTypeOptions, detailData.syndrome_type) }}</strong></div>
<div><span>糖尿病期数</span><strong>{{ getDictLabel(diabetesTypeOptions, detailData.diabetes_type) }}</strong></div>
</div>
</section>
<section v-if="hasHistoryDetail(detailData)" class="apt-h5-detail-section">
<h3>病史信息</h3>
<div class="apt-h5-detail-grid">
<div v-if="isDiabetesDiscoveryFilled(detailData.diabetes_discovery_year)">
<span>发现糖尿病病史</span>
<strong>{{ formatDiabetesDiscoveryDisplay(detailData.diabetes_discovery_year) }}</strong>
</div>
<div v-if="detailData.local_hospital_name">
<span>当地就诊医院</span>
<strong>{{ detailData.local_hospital_name }}</strong>
</div>
<div v-if="detailData.symptoms" class="apt-h5-detail-full">
<span>症状补充</span>
<strong>{{ detailData.symptoms }}</strong>
</div>
<div v-if="detailData.past_history && detailData.past_history.length" class="apt-h5-detail-full">
<span>既往史</span>
<strong>{{ getPastHistoryLabels(detailData.past_history) }}</strong>
</div>
</div>
</section>
<section v-if="hasDiagnosisDetail(detailData)" class="apt-h5-detail-section">
<h3>诊断信息</h3>
<div class="apt-h5-detail-grid">
<div v-if="detailData.tongue_coating">
<span>舌苔</span>
<strong>{{ detailData.tongue_coating }}</strong>
</div>
<div v-if="detailData.pulse">
<span>脉象</span>
<strong>{{ detailData.pulse }}</strong>
</div>
<div v-if="detailData.treatment_principle" class="apt-h5-detail-full">
<span>治则</span>
<strong>{{ detailData.treatment_principle }}</strong>
</div>
<div v-if="detailData.prescription" class="apt-h5-detail-full">
<span>处方</span>
<strong>{{ detailData.prescription }}</strong>
</div>
</div>
</section>
<section class="apt-h5-detail-section">
<h3>备注</h3>
<p>{{ detailData.remark || '无' }}</p>
</section>
</template>
</main>
<footer v-if="detailData" class="apt-h5-detail-footer-bar">
<div class="apt-h5-detail-footer">
<el-button
v-if="hasPermission(['tcm.diagnosis/edit'])"
type="primary"
plain
@click="handleEdit(detailActionRow)"
>
编辑患者
</el-button>
<el-button
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
type="primary"
@click="handlePrescription(detailActionRow)"
>
{{ prescriptionActionLabel(detailActionRow) }}
</el-button>
<el-button
v-if="detailActionRow.status !== 3 && hasPermission(['doctor.appointment/prescription'])"
type="primary"
plain
@click="handleChat(detailActionRow)"
>
通话
</el-button>
<el-button
v-if="detailActionRow.status !== 3 && hasPermission(['doctor.appointment/complete'])"
type="success"
plain
@click="handleComplete(detailActionRow)"
>
完成
</el-button>
</div>
</footer>
</template>
<el-dialog
v-model="qrcodeDialogVisible"
title="视频二维码"
:close-on-click-modal="false"
class="apt-h5-qr-dialog"
>
<div class="apt-h5-qr">
<div v-if="qrcodeLoading" class="apt-h5-qr-loading">
<el-icon class="is-loading" :size="36"><Loading /></el-icon>
<div>生成中...</div>
</div>
<div v-else-if="qrcodeUrl">
<img :src="qrcodeUrl" alt="小程序二维码" class="apt-h5-qr-img" />
<div class="apt-h5-qr-meta">
<div>患者{{ currentQRCodeRow?.patient_name || '-' }}</div>
<div>请使用微信扫描二维码</div>
</div>
</div>
<div v-else class="apt-h5-qr-fail">生成失败请重试</div>
</div>
<template #footer>
<el-button @click="qrcodeDialogVisible = false">关闭</el-button>
<el-button v-if="!qrcodeLoading" type="primary" @click="handleRegenerateQRCode">重新生成</el-button>
</template>
</el-dialog>
<el-dialog
v-model="completeDialogVisible"
title="完成问诊"
:close-on-click-modal="false"
class="apt-h5-complete-dialog"
destroy-on-close
>
<div class="apt-h5-complete">
<p>确认患者已就诊完成</p>
<el-input
v-if="canAddCompleteNote"
v-model="completeNoteContent"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="填写完成备注,将追加到医生备注时间轴"
/>
</div>
<template #footer>
<el-button @click="completeDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="completeSubmitting" @click="submitComplete">确认完成</el-button>
</template>
</el-dialog>
<edit-popup ref="editRef" @success="() => loadData({ silent: true })" />
<tcm-prescription ref="prescriptionRef" @success="onPrescriptionSuccess" />
<chat-dialog ref="chatDialogRef" />
</div>
</template>
<script setup lang="ts" name="tcmAppointmentH5">
import { appointmentDetail, appointmentLists, cancelAppointment, completeAppointment } from '@/api/doctor'
import { getWeappConfig } from '@/api/channel/weapp'
import {
generateMiniProgramQrcode,
getCallSignature,
prescriptionGetByAppointment,
tcmDiagnosisDetail
} from '@/api/tcm'
import { getDictData } from '@/api/app'
import { addDoctorNote } from '@/api/patient'
import { usePaging } from '@/hooks/usePaging'
import useUserStore from '@/stores/modules/user'
import {
formatDiabetesDiscoveryDisplay,
isDiabetesDiscoveryFilled
} from '@/utils/diabetes-discovery-display'
import feedback from '@/utils/feedback'
import { hasPermission } from '@/utils/perm'
import {
ArrowDown,
CircleCheck,
CircleClose,
Document,
Loading,
Phone,
Picture,
Search
} from '@element-plus/icons-vue'
import { computed, defineAsyncComponent, onMounted, onUnmounted, reactive, ref } from 'vue'
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'))
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const todayDate = () => {
const t = new Date()
return formatDate(t)
}
const formatDate = (date: Date) => {
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
const dateByOffset = (offset: number) => {
const t = new Date()
t.setDate(t.getDate() + offset)
return formatDate(t)
}
const maskPhone = (phone: string) => {
if (!phone || phone.length < 11) return phone
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
const isAdmin = computed(() => {
const roleId = userInfo.value?.role_id
if (Array.isArray(roleId)) return !roleId.includes(1) && !roleId.includes(2)
return roleId !== 1 && roleId !== 2
})
const formData = reactive({
patient_name: '',
doctor_name: '',
status: 1 as string | number,
start_date: todayDate(),
end_date: todayDate(),
date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after',
diagnosis_confirmed: '' as '' | '0' | '1',
include_status_counts: 0 as 0 | 1
})
const activeTab = ref('1')
const currentView = ref<'list' | 'detail'>('list')
const showMoreFilter = ref(false)
const customDateRange = ref<[string, string] | ''>('')
const statusCount = ref<Record<number, number>>({ 1: 0, 2: 0, 3: 0, 4: 0 })
const { pager, getLists } = usePaging({
fetchFun: appointmentLists,
params: formData,
firstLoading: true
})
const statusTabs = computed(() => [
{ label: '待接诊', value: '1', count: statusCount.value[1] },
{ label: '全部', value: 'all', count: 0 },
{ label: '已过号', value: '4', count: statusCount.value[4] },
{ label: '已取消', value: '2', count: statusCount.value[2] },
{ label: '已完成', value: '3', count: statusCount.value[3] }
])
const dateTabs = [
{ label: '昨天', value: 'yesterday' },
{ label: '前天', value: 'day_before' },
{ label: '今天', value: 'today' },
{ label: '明天', value: 'tomorrow' },
{ label: '后天', value: 'day_after' },
{ label: '不限', value: '' }
] as const
const confirmedOptions = [
{ label: '全部', value: '' },
{ label: '已确认', value: '1' },
{ label: '未确认', value: '0' }
] as const
const hasMore = computed(() => pager.lists.length < (pager.count || 0))
const loadData = async (opts?: { silent?: boolean }) => {
formData.include_status_counts = 1
try {
await getLists(opts)
const sc = (pager.extend as { status_count?: Record<string | number, number> })?.status_count
if (sc) {
;([1, 2, 3, 4] as const).forEach((key) => {
const n = sc[key] ?? sc[String(key)]
statusCount.value[key] = typeof n === 'number' ? n : 0
})
}
} finally {
formData.include_status_counts = 0
}
}
const handleSearch = () => {
pager.page = 1
loadData()
}
const handleTabChange = (tab: string) => {
activeTab.value = tab
formData.status = tab === 'all' ? '' : Number(tab)
pager.page = 1
loadData()
}
const handleDatePresetChange = (value: string) => {
formData.date_preset = value as typeof formData.date_preset
customDateRange.value = ''
if (!value) {
formData.start_date = ''
formData.end_date = ''
} else {
const offsetMap: Record<string, number> = {
yesterday: -1,
day_before: -2,
today: 0,
tomorrow: 1,
day_after: 2
}
const date = dateByOffset(offsetMap[value] ?? 0)
formData.start_date = date
formData.end_date = date
}
pager.page = 1
loadData()
}
const handleConfirmedChange = (value: '' | '0' | '1') => {
formData.diagnosis_confirmed = value
pager.page = 1
loadData()
}
const applyCustomDate = () => {
if (!customDateRange.value) {
handleDatePresetChange('')
return
}
formData.date_preset = ''
formData.start_date = customDateRange.value[0]
formData.end_date = customDateRange.value[1]
pager.page = 1
loadData()
}
const handleReset = () => {
formData.patient_name = ''
formData.doctor_name = ''
formData.diagnosis_confirmed = ''
activeTab.value = '1'
formData.status = 1
handleDatePresetChange('today')
}
const loadMore = async () => {
if (pager.loading || !hasMore.value) return
const next = (pager.page || 1) + 1
pager.loading = true
try {
const res = await appointmentLists({
page_no: next,
page_size: pager.size,
...formData,
include_status_counts: 0
})
pager.page = next
pager.lists.push(...(res?.lists || []))
pager.count = res?.count ?? pager.count
} finally {
pager.loading = false
}
}
const statusLabel = (status: number | string) => {
const s = Number(status)
if (s === 1) return '待接诊'
if (s === 2) return '已取消'
if (s === 3) return '已完成'
if (s === 4) return '已过号'
return '-'
}
const statusTagClass = (status: number | string) => {
const s = Number(status)
return {
'apt-h5-tag--ok': s === 1 || s === 3,
'apt-h5-tag--gray': s === 2,
'apt-h5-tag--danger': s === 4
}
}
const cardClass = (row: any) => ({
'apt-h5-card--done': Number(row.status) === 3,
'apt-h5-card--cancel': Number(row.status) === 2,
'apt-h5-card--missed': Number(row.status) === 4
})
const editRef = ref()
const prescriptionRef = ref()
const chatDialogRef = ref()
const COMPLETE_NOTE_PERMISSION = ['doctor.appointment/addDoctorNote']
const completeDialogVisible = ref(false)
const completeSubmitting = ref(false)
const completeNoteContent = ref('')
const completeRow = ref<any>(null)
const canAddCompleteNote = computed(() => hasPermission(COMPLETE_NOTE_PERMISSION))
const handleEdit = (row: any) => {
if (!row.patient_id) {
feedback.msgWarning('该预约没有关联诊单信息')
return
}
editRef.value?.open('edit', row.patient_id)
}
const handleChat = async (row: any) => {
if (!row.patient_id) {
feedback.msgWarning('患者信息不完整')
return
}
try {
const res = await getCallSignature({
patient_id: row.patient_id,
diagnosis_id: row.diagnosis_id || row.id
})
chatDialogRef.value?.open({
patientId: row.patient_id,
patientName: row.patient_name,
diagnosisId: row.diagnosis_id || row.id,
signatureData: res
})
} catch (e: any) {
feedback.msgError(e?.msg || e?.message || '获取通话签名失败')
}
}
const prescriptionActionLabel = (row: any) => {
if (row?.prescription_audit_status === 1 && Number(row?.prescription_void_status) !== 1) return '查看'
return '开方'
}
const handlePrescription = (row: any) => {
if (!row.diagnosis_id && !row.patient_id) {
feedback.msgWarning('该预约缺少诊单信息,请先编辑患者')
return
}
prescriptionRef.value?.open(row)
}
const onPrescriptionSuccess = () => {
loadData()
editRef.value?.refreshCaseList?.()
}
const moreSheetVisible = ref(false)
const moreSheetRow = ref<any>(null)
const openMoreSheet = (row: any) => {
moreSheetRow.value = row
moreSheetVisible.value = true
}
const runAction = (cmd: string) => {
const row = moreSheetRow.value
moreSheetVisible.value = false
if (!row) return
if (cmd === 'videoQr') handleMiniProgramQRCode(row)
if (cmd === 'complete') handleComplete(row)
if (cmd === 'case') handleViewCase(row)
if (cmd === 'cancel') handleCancel(row)
}
const handleComplete = async (row: any) => {
completeRow.value = row
completeNoteContent.value = ''
if (canAddCompleteNote.value) {
completeDialogVisible.value = true
return
}
try {
await feedback.confirm('确认患者已就诊完成?')
await completeAppointment({ id: row.id })
feedback.msgSuccess('操作成功')
loadData()
} catch (e: any) {
if (e !== 'cancel') feedback.msgError(e?.msg || '操作失败')
}
}
const submitComplete = async () => {
const row = completeRow.value
if (!row?.id || completeSubmitting.value) return
completeSubmitting.value = true
try {
const note = completeNoteContent.value.trim()
await completeAppointment({ id: row.id })
let noteFailureMsg = ''
if (canAddCompleteNote.value && note) {
const diagnosisId = row.patient_id || row.diagnosis_id
if (diagnosisId) {
try {
await addDoctorNote({ diagnosis_id: Number(diagnosisId), content: note })
} catch (e) {
noteFailureMsg = '问诊已完成,但备注未保存'
}
} else {
noteFailureMsg = '问诊已完成,但该预约没有关联诊单,备注未保存'
}
}
noteFailureMsg ? feedback.msgWarning(noteFailureMsg) : feedback.msgSuccess('操作成功')
completeDialogVisible.value = false
completeNoteContent.value = ''
completeRow.value = null
if (currentView.value === 'detail') {
backToList()
}
loadData()
} catch (e: any) {
feedback.msgError(e?.msg || e?.message || '操作失败')
} finally {
completeSubmitting.value = false
}
}
const handleCancel = async (row: any) => {
try {
await feedback.confirm('确定要取消该挂号吗?')
await cancelAppointment({ id: row.id })
feedback.msgSuccess('取消成功')
loadData()
} catch (e: any) {
if (e !== 'cancel') feedback.msgError(e?.msg || '取消失败')
}
}
const handleViewCase = async (row: any) => {
try {
const prescription = await prescriptionGetByAppointment({ appointment_id: row.id })
if (prescription?.id) {
prescriptionRef.value?.openById(prescription.id)
} else {
feedback.msgWarning('该预约暂无病历记录,请先开方')
}
} catch (e: any) {
feedback.msgError(e?.msg || '获取病历失败')
}
}
const qrcodeDialogVisible = ref(false)
const qrcodeLoading = ref(false)
const qrcodeUrl = ref('')
const currentQRCodeRow = ref<any>(null)
const handleMiniProgramQRCode = async (row: any) => {
if (!row.patient_id) {
feedback.msgWarning('患者信息不完整')
return
}
currentQRCodeRow.value = row
qrcodeDialogVisible.value = true
qrcodeLoading.value = true
qrcodeUrl.value = ''
try {
const config = await getWeappConfig()
if (!config?.app_id) {
feedback.msgError('小程序未配置,请先配置小程序信息')
qrcodeDialogVisible.value = false
return
}
const result = await generateMiniProgramQrcode({
diagnosis_id: row.doctor_id || '',
patient_id: row.patient_id,
share_user_id: userStore.userInfo?.id || '',
mini_program_path: 'pages/login/login'
})
if (result?.qrcode_url) qrcodeUrl.value = result.qrcode_url
else feedback.msgError('二维码生成失败')
} catch (e: any) {
feedback.msgError(e?.msg || '生成二维码失败,请重试')
} finally {
qrcodeLoading.value = false
}
}
const handleRegenerateQRCode = () => {
if (currentQRCodeRow.value) handleMiniProgramQRCode(currentQRCodeRow.value)
}
const detailLoading = ref(false)
const detailData = ref<any>(null)
const detailSourceRow = ref<any>(null)
const detailActionRow = computed(() => ({ ...(detailSourceRow.value || {}), ...(detailData.value || {}) }))
const diagnosisTypeOptions = ref<any[]>([])
const syndromeTypeOptions = ref<any[]>([])
const diabetesTypeOptions = ref<any[]>([])
const pastHistoryOptions = ref<any[]>([])
const detailDictReady = ref(false)
const getDictLabel = (options: any[], value: string) => {
if (!value) return '-'
const item = options.find((opt: any) => opt.value === value)
return item ? item.name : value
}
const getPastHistoryLabels = (values: string[] | string) => {
const arr = Array.isArray(values) ? values : typeof values === 'string' ? values.split(/[,,、]/) : []
return arr.map((v: string) => getDictLabel(pastHistoryOptions.value, v.trim())).filter(Boolean).join('、') || '-'
}
const maritalStatusLabel = (value: number | string | undefined) => {
if (Number(value) === 0) return '未婚'
if (Number(value) === 1) return '已婚'
if (Number(value) === 2) return '离异'
return '-'
}
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 ensureDetailDictOptions = async () => {
if (detailDictReady.value) return
await loadDetailDictOptions()
detailDictReady.value = true
}
const hasHistoryDetail = (data: any) =>
isDiabetesDiscoveryFilled(data?.diabetes_discovery_year) ||
!!data?.local_hospital_name ||
!!data?.symptoms ||
(Array.isArray(data?.past_history) ? data.past_history.length > 0 : !!data?.past_history)
const hasDiagnosisDetail = (data: any) =>
!!(data?.tongue_coating || data?.pulse || data?.treatment_principle || data?.prescription)
const handleDetail = async (row: any) => {
currentView.value = 'detail'
detailLoading.value = true
detailData.value = null
detailSourceRow.value = row
try {
const [, appointment] = await Promise.all([
ensureDetailDictOptions().catch((e) => console.warn('加载详情字典失败:', e)),
appointmentDetail({ id: row.id })
])
detailData.value = { ...row, ...appointment, appointment_id: row.id }
if (appointment?.patient_id) {
try {
const diagnosis = await tcmDiagnosisDetail({ id: appointment.patient_id })
detailData.value = {
...row,
...appointment,
...diagnosis,
id: appointment.id || row.id,
appointment_id: row.id,
diagnosis_id: appointment.patient_id
}
} catch (e) {
console.warn('获取患者诊单详情失败:', e)
}
}
} catch (e: any) {
feedback.msgError(e?.msg || '获取详情失败')
currentView.value = 'list'
} finally {
detailLoading.value = false
}
}
const backToList = () => {
currentView.value = 'list'
detailLoading.value = false
}
const LIST_POLL_MS = 20_000
let listPollTimer: ReturnType<typeof setInterval> | null = null
onMounted(() => {
document.body.classList.add('h5-mode')
loadData()
listPollTimer = setInterval(() => {
loadData({ silent: true }).catch(() => undefined)
}, LIST_POLL_MS)
})
onUnmounted(() => {
document.body.classList.remove('h5-mode')
if (listPollTimer !== null) {
clearInterval(listPollTimer)
listPollTimer = null
}
})
</script>
<style scoped lang="scss">
.apt-h5 {
max-width: 560px;
margin: 0 auto;
min-height: 100vh;
background: #f5f7fb;
color: #1f2937;
padding-bottom: 24px;
-webkit-font-smoothing: antialiased;
}
.apt-h5-topbar {
position: sticky;
top: 0;
z-index: 10;
padding: 12px 12px 10px;
background: rgba(245, 247, 251, 0.96);
border-bottom: 1px solid #e5e7eb;
backdrop-filter: blur(8px);
}
.apt-h5-search {
display: flex;
gap: 8px;
}
.apt-h5-search :deep(.el-input__wrapper) {
border-radius: 8px;
}
.apt-h5-search-btn {
flex: 0 0 72px;
border-radius: 8px;
}
.apt-h5-scroll {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 10px 0 0;
scrollbar-width: none;
}
.apt-h5-scroll::-webkit-scrollbar {
display: none;
}
.apt-h5-chip,
.apt-h5-pill {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
min-height: 30px;
padding: 0 12px;
border: 1px solid #d8dee8;
border-radius: 8px;
background: #fff;
color: #4b5563;
font-size: 13px;
line-height: 1;
}
.apt-h5-chip em {
min-width: 18px;
height: 18px;
margin-left: 5px;
padding: 0 5px;
border-radius: 9px;
background: #eef2ff;
color: #2563eb;
font-style: normal;
font-size: 11px;
line-height: 18px;
text-align: center;
}
.apt-h5-chip.active,
.apt-h5-pill.active {
border-color: #2563eb;
background: #2563eb;
color: #fff;
}
.apt-h5-chip.active em {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.apt-h5-quick {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding-top: 10px;
}
.apt-h5-quick-group {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
.apt-h5-quick-group::-webkit-scrollbar {
display: none;
}
.apt-h5-quick-label {
flex: 0 0 auto;
color: #6b7280;
font-size: 12px;
}
.apt-h5-more-toggle {
flex: 0 0 auto;
}
.rotate-180 {
transform: rotate(180deg);
}
.apt-h5-more-filter {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
padding-top: 10px;
}
.apt-h5-date-range {
width: 100% !important;
}
.apt-h5-list {
min-height: calc(100vh - 150px);
padding: 12px;
}
.apt-h5-empty {
margin-top: 48px;
}
.apt-h5-card {
margin-bottom: 10px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05);
overflow: hidden;
}
.apt-h5-card--missed {
border-color: #fecaca;
}
.apt-h5-card--cancel,
.apt-h5-card--done {
opacity: 0.86;
}
.apt-h5-card-head {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 12px 10px;
}
.apt-h5-avatar {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 40px;
width: 40px;
height: 40px;
border-radius: 8px;
background: #eff6ff;
color: #2563eb;
font-weight: 600;
}
.apt-h5-title {
min-width: 0;
flex: 1;
}
.apt-h5-name {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
color: #111827;
font-size: 16px;
font-weight: 600;
line-height: 22px;
}
.apt-h5-meta {
color: #6b7280;
font-size: 12px;
font-weight: 400;
}
.apt-h5-sub {
margin-top: 3px;
color: #8a94a6;
font-size: 12px;
}
.apt-h5-tags {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
flex: 0 0 auto;
}
.apt-h5-tag,
.apt-h5-inline-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 7px;
border-radius: 6px;
background: #eef2f7;
color: #64748b;
font-size: 12px;
}
.apt-h5-tag--ok,
.apt-h5-inline-tag--ok {
background: #ecfdf3;
color: #15803d;
}
.apt-h5-tag--warn,
.apt-h5-inline-tag--warn {
background: #fff7ed;
color: #c2410c;
}
.apt-h5-tag--danger {
background: #fef2f2;
color: #dc2626;
}
.apt-h5-tag--gray {
background: #f3f4f6;
color: #6b7280;
}
.apt-h5-card-body {
padding: 0 12px 10px;
}
.apt-h5-row {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 5px 0;
font-size: 13px;
line-height: 20px;
}
.apt-h5-row-key {
flex: 0 0 38px;
color: #8a94a6;
}
.apt-h5-row-val {
min-width: 0;
flex: 1;
color: #374151;
word-break: break-word;
}
.apt-h5-row--remark .apt-h5-row-val {
color: #6b7280;
}
.apt-h5-card-foot {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 7px;
padding: 10px 12px 12px;
border-top: 1px solid #f1f5f9;
}
.apt-h5-card-foot :deep(.el-button) {
min-width: 0;
margin: 0;
border-radius: 8px;
}
.apt-h5-more-btn {
color: #4b5563;
}
.apt-h5-load-more,
.apt-h5-no-more {
padding: 14px 0 18px;
color: #6b7280;
font-size: 13px;
text-align: center;
}
.apt-h5-load-more {
color: #2563eb;
}
.apt-h5-actions {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
padding: 4px 4px 16px;
}
.apt-h5-action {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 72px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
color: #374151;
font-size: 12px;
}
.apt-h5-action :deep(.el-icon) {
font-size: 22px;
color: #2563eb;
}
.apt-h5-action--danger {
color: #dc2626;
}
.apt-h5-action--danger :deep(.el-icon) {
color: #dc2626;
}
.apt-h5-detail-topbar {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 10px;
min-height: 54px;
padding: 8px 12px;
border-bottom: 1px solid #e5e7eb;
background: rgba(245, 247, 251, 0.98);
backdrop-filter: blur(8px);
}
.apt-h5-back {
display: inline-flex;
align-items: center;
gap: 2px;
height: 36px;
padding: 0 8px 0 4px;
border: 0;
border-radius: 8px;
background: transparent;
color: #2563eb;
font-size: 14px;
}
.apt-h5-back span {
font-size: 26px;
line-height: 1;
transform: translateY(-1px);
}
.apt-h5-detail-title {
min-width: 0;
}
.apt-h5-detail-title strong,
.apt-h5-detail-title small {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.apt-h5-detail-title strong {
color: #111827;
font-size: 16px;
line-height: 22px;
}
.apt-h5-detail-title small {
margin-top: 1px;
color: #8a94a6;
font-size: 12px;
}
.apt-h5-detail-page {
min-height: calc(100vh - 54px);
padding: 12px 12px 82px;
}
.apt-h5-detail-section {
margin-bottom: 12px;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.apt-h5-detail-section h3 {
margin: 0 0 10px;
color: #111827;
font-size: 15px;
font-weight: 600;
}
.apt-h5-detail-section p {
margin: 0 0 8px;
color: #4b5563;
font-size: 13px;
line-height: 20px;
}
.apt-h5-detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.apt-h5-detail-grid div {
min-width: 0;
}
.apt-h5-detail-grid .apt-h5-detail-full {
grid-column: 1 / -1;
}
.apt-h5-detail-grid span {
display: block;
margin-bottom: 3px;
color: #8a94a6;
font-size: 12px;
}
.apt-h5-detail-grid strong {
display: block;
color: #374151;
font-size: 13px;
font-weight: 500;
word-break: break-word;
}
.apt-h5-detail-footer {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
width: 100%;
}
.apt-h5-detail-footer-bar {
position: sticky;
bottom: 0;
z-index: 20;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #e5e7eb;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(8px);
}
.apt-h5-detail-footer :deep(.el-button) {
min-width: 0;
margin: 0;
border-radius: 8px;
}
.apt-h5-qr {
min-height: 160px;
text-align: center;
}
.apt-h5-qr-loading,
.apt-h5-qr-fail {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 160px;
color: #6b7280;
gap: 10px;
}
.apt-h5-qr-img {
width: 220px;
height: 220px;
margin: 0 auto;
}
.apt-h5-qr-meta {
margin-top: 10px;
color: #6b7280;
font-size: 13px;
line-height: 22px;
}
.apt-h5-complete p {
margin: 0 0 12px;
color: #374151;
font-size: 14px;
line-height: 22px;
}
.apt-h5-complete :deep(.el-textarea__inner) {
min-height: 112px !important;
border-radius: 8px;
}
@media (max-width: 374px) {
.apt-h5-card-foot {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.apt-h5-actions {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
<style lang="scss">
body.h5-mode {
background: #eef2f7;
.el-drawer {
width: min(100vw, 560px) !important;
max-width: 560px !important;
}
.el-drawer.rtl {
right: max(0px, calc((100vw - 560px) / 2)) !important;
}
.el-drawer.ltr {
left: max(0px, calc((100vw - 560px) / 2)) !important;
}
.el-drawer.btt,
.el-drawer.ttb {
left: 50% !important;
right: auto !important;
transform: translateX(-50%) !important;
}
.el-drawer__body {
padding: 0 14px 14px !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
}
.el-drawer__header {
padding: 14px 16px !important;
margin-bottom: 0 !important;
border-bottom: 1px solid #eee;
background: #fff;
.el-drawer__title {
font-size: 17px;
font-weight: 600;
}
}
.el-drawer__footer {
padding: 10px 16px !important;
border-top: 1px solid #eee;
background: #fff;
}
.el-overlay-dialog .el-dialog,
.el-dialog {
width: 92% !important;
max-width: 460px !important;
margin: 8vh auto !important;
border-radius: 12px !important;
}
.el-dialog__body {
max-height: 65vh;
overflow-y: auto;
}
.edit-drawer {
.el-row {
display: block !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.el-col {
max-width: 100% !important;
flex: 0 0 100% !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
}
.diagnosis-drawer {
.el-drawer__body { padding-bottom: 60px !important; }
.el-form {
padding-bottom: 10px;
.el-form-item {
flex-direction: column !important;
align-items: flex-start !important;
margin-bottom: 10px !important;
padding: 0 !important;
.el-form-item__label {
width: auto !important;
max-width: 100% !important;
min-width: 0 !important;
text-align: left !important;
padding: 0 0 3px 0 !important;
font-size: 14px !important;
font-weight: 500;
color: #475569;
line-height: 1.4 !important;
height: auto !important;
justify-content: flex-start !important;
}
.el-form-item__content {
margin-left: 0 !important;
width: 100% !important;
}
}
}
.el-divider {
margin: 18px 0 12px !important;
.el-divider__text {
max-width: calc(100% - 32px);
padding: 0 8px !important;
color: #0f172a;
font-size: 15px;
font-weight: 700;
white-space: nowrap;
}
}
.checkbox-form-item {
margin-bottom: 14px !important;
}
.el-radio-group,
.el-checkbox-group {
display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
width: 100%;
align-items: stretch;
}
.checkbox-form-item .el-checkbox-group {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.el-radio,
.el-checkbox,
.el-radio-button,
.el-checkbox-button {
width: 100%;
height: auto !important;
margin: 0 !important;
white-space: normal !important;
}
.el-radio__label,
.el-checkbox__label {
white-space: normal !important;
line-height: 18px !important;
}
.el-radio-button__inner,
.el-checkbox-button__inner {
display: flex !important;
align-items: center;
justify-content: center;
width: 100% !important;
min-height: 34px;
padding: 7px 8px !important;
border: 1px solid #d8dee8 !important;
border-radius: 8px !important;
box-shadow: none !important;
white-space: normal !important;
line-height: 18px !important;
text-align: center;
}
.el-radio-button:first-child .el-radio-button__inner,
.el-checkbox-button:first-child .el-checkbox-button__inner,
.el-radio-button:last-child .el-radio-button__inner,
.el-checkbox-button:last-child .el-checkbox-button__inner {
border-radius: 8px !important;
}
.el-radio-button.is-active .el-radio-button__inner,
.el-checkbox-button.is-checked .el-checkbox-button__inner {
border-color: #2563eb !important;
background: #2563eb !important;
color: #fff !important;
}
@media (max-width: 430px) {
.checkbox-form-item .el-checkbox-group {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
}
.tcm-diagnosis-tabs {
.el-tabs__header {
margin: 0 0 10px !important;
}
.el-tabs__nav-wrap {
overflow-x: auto;
}
}
.tcm-dd-header__title {
font-size: 17px !important;
line-height: 24px !important;
}
.prescription-edit,
.prescription-preview-content {
max-width: 100%;
overflow-x: hidden;
.el-row {
display: block !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.el-col {
max-width: 100% !important;
flex: 0 0 100% !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
.el-form-item {
display: block !important;
margin-bottom: 10px !important;
.el-form-item__label {
display: block !important;
width: auto !important;
height: auto !important;
padding: 0 0 4px !important;
line-height: 1.4 !important;
text-align: left !important;
}
.el-form-item__content {
margin-left: 0 !important;
width: 100% !important;
}
}
.info-section {
padding: 12px !important;
margin-bottom: 12px !important;
border-radius: 8px !important;
}
.rp-toolbar,
.rp-toolbar__lead,
.rp-toolbar__actions,
.herb-editor-card__row,
.usage-formula-row {
display: block !important;
}
.rp-toolbar__actions {
margin-top: 10px;
.el-button {
width: 100%;
margin: 0 0 8px !important;
}
}
.herbs-editor-grid {
display: grid !important;
grid-template-columns: 1fr !important;
gap: 8px !important;
}
.herb-editor-card,
.herb-editor-card__select,
.herb-editor-card__dose,
.el-input-number,
.el-select,
.el-input,
.el-textarea {
width: 100% !important;
max-width: 100% !important;
}
.herb-editor-card__row {
.el-button {
margin-top: 6px;
}
}
}
.chat-floating-window {
position: fixed !important;
right: max(0px, calc((100vw - 560px) / 2)) !important;
bottom: 0 !important;
left: auto !important;
top: auto !important;
width: min(100vw, 560px) !important;
height: min(76vh, 680px) !important;
max-width: 560px !important;
border-radius: 12px 12px 0 0 !important;
transform: none !important;
z-index: 3000 !important;
}
.chat-floating-window--minimized {
height: 48px !important;
}
.chat-window-header {
min-height: 48px;
padding: 0 10px !important;
}
.chat-window-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-container,
.chat-content,
.chat-layout,
.chat-area {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
}
.chat-container {
height: calc(76vh - 48px) !important;
}
.chat-dialog-call-kit-wrapper {
left: 50% !important;
right: auto !important;
top: 8px !important;
width: min(100vw, 560px) !important;
max-width: 560px !important;
transform: translateX(-50%) !important;
}
.call-kit-content {
width: 100% !important;
max-width: 100vw !important;
overflow: hidden;
}
@media (max-width: 560px) {
.el-drawer {
width: 100% !important;
max-width: 100vw !important;
}
.el-drawer.rtl {
right: 0 !important;
}
.el-drawer.ltr,
.el-drawer.btt,
.el-drawer.ttb {
left: 0 !important;
transform: none !important;
}
.chat-floating-window {
right: 0 !important;
width: 100vw !important;
max-width: 100vw !important;
}
.chat-dialog-call-kit-wrapper {
width: 100vw !important;
max-width: 100vw !important;
}
}
}
</style>