2412 lines
77 KiB
Vue
2412 lines
77 KiB
Vue
<template>
|
||
<div class="dh5">
|
||
<!-- 顶部:搜索 + 日期/状态 chip 横滚 -->
|
||
<header class="dh5-topbar">
|
||
<div class="dh5-search">
|
||
<el-input
|
||
v-model="formData.keyword"
|
||
placeholder="患者姓名 / 手机号"
|
||
clearable
|
||
:prefix-icon="Search"
|
||
@keyup.enter="doSearch"
|
||
@clear="doSearch"
|
||
/>
|
||
<el-button class="dh5-search-btn" type="primary" @click="doSearch">查询</el-button>
|
||
</div>
|
||
|
||
<div class="dh5-chips dh5-chips--scroll">
|
||
<span
|
||
v-for="tab in dateTabs"
|
||
:key="tab.value || 'all'"
|
||
:class="[
|
||
'dh5-chip',
|
||
{
|
||
active:
|
||
formData.pending_assign !== '1' &&
|
||
formData.pending_booking !== '1' &&
|
||
formData.completed_appointment !== '1' &&
|
||
formData.appointment_date === tab.value
|
||
}
|
||
]"
|
||
@click="handleDateTabClick(tab.value)"
|
||
>
|
||
{{ tab.label }}
|
||
<em v-if="dateCounts[tab.value] !== undefined">{{ dateCounts[tab.value] }}</em>
|
||
</span>
|
||
<span
|
||
:class="['dh5-chip', 'dh5-chip--booking', { active: formData.pending_booking === '1' }]"
|
||
@click="handlePendingBookingTabClick"
|
||
>
|
||
待预约
|
||
<em v-if="pendingBookingCount !== undefined">{{ pendingBookingCount }}</em>
|
||
</span>
|
||
<span
|
||
:class="['dh5-chip', 'dh5-chip--done', { active: formData.completed_appointment === '1' }]"
|
||
@click="handleCompletedVisitTabClick"
|
||
>
|
||
已完成
|
||
<em v-if="completedVisitCount !== undefined">{{ completedVisitCount }}</em>
|
||
</span>
|
||
<span
|
||
v-if="hasPermission(['tcm.diagnosis/assign'])"
|
||
:class="['dh5-chip', 'dh5-chip--pending', { active: formData.pending_assign === '1' }]"
|
||
@click="handlePendingAssignTabClick"
|
||
>
|
||
待分配医助
|
||
<em v-if="pendingAssignCount !== undefined">{{ pendingAssignCount }}</em>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="dh5-quick">
|
||
<div class="dh5-quick-group">
|
||
<span class="dh5-quick-label">挂号</span>
|
||
<span
|
||
v-for="opt in hasAptOptions"
|
||
:key="'a' + opt.value"
|
||
:class="['dh5-pill', { active: formData.has_appointment === opt.value }]"
|
||
@click="setHasAppointment(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</span>
|
||
</div>
|
||
<div class="dh5-quick-group">
|
||
<span class="dh5-quick-label">确认</span>
|
||
<span
|
||
v-for="opt in confirmedOptions"
|
||
:key="'c' + opt.value"
|
||
:class="['dh5-pill', { active: formData.diagnosis_confirmed === opt.value }]"
|
||
@click="setConfirmed(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</span>
|
||
</div>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
size="small"
|
||
class="dh5-more-toggle"
|
||
@click="showMoreFilter = !showMoreFilter"
|
||
>
|
||
{{ showMoreFilter ? '收起' : '更多' }}
|
||
<el-icon :class="{ 'rotate-180': showMoreFilter }"><ArrowDown /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
|
||
<div v-show="showMoreFilter" class="dh5-more-filter">
|
||
<el-select v-model="formData.diagnosis_type" placeholder="诊断类型" clearable size="small" @change="doSearch">
|
||
<el-option v-for="item in diagnosisTypeOptions" :key="item.value" :label="item.name" :value="item.value" />
|
||
</el-select>
|
||
<el-select v-model="formData.syndrome_type" placeholder="证型" clearable size="small" @change="doSearch">
|
||
<el-option v-for="item in syndromeTypeOptions" :key="item.value" :label="item.name" :value="item.value" />
|
||
</el-select>
|
||
<el-select v-model="formData.assistant_id" placeholder="医助" clearable filterable size="small" @change="doSearch">
|
||
<el-option v-for="item in assistantOptions" :key="item.id" :label="item.name" :value="Number(item.id)" />
|
||
</el-select>
|
||
<el-button size="small" @click="handleReset">重置</el-button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 卡片列表 -->
|
||
<main class="dh5-list" v-loading="pager.loading">
|
||
<div v-if="pager.lists.length === 0 && !pager.loading" class="dh5-empty">
|
||
<el-empty description="暂无诊单" :image-size="120" />
|
||
</div>
|
||
|
||
<article
|
||
v-for="row in pager.lists"
|
||
:key="row.id"
|
||
:class="['dh5-card', getCardClass(row)]"
|
||
>
|
||
<!-- 卡片头:姓名 / 性别年龄 / 主状态 -->
|
||
<header class="dh5-card-head" @click="handleDetail(row.id)">
|
||
<div class="dh5-card-avatar">
|
||
{{ (row.patient_name || '患').charAt(0) }}
|
||
</div>
|
||
<div class="dh5-card-id-name">
|
||
<div class="dh5-card-name">
|
||
{{ row.patient_name || '-' }}
|
||
<span v-if="row.gender_desc" class="dh5-meta-gender">{{ row.gender_desc }}</span>
|
||
<span v-if="row.age != null" class="dh5-meta-age">{{ row.age }}岁</span>
|
||
</div>
|
||
<div class="dh5-card-sub">
|
||
#{{ row.id }} · {{ row.diagnosis_date_text || formatDateTime(row.create_time) }}
|
||
<span v-if="row.assign_read_at === null && row.assistant_id > 0" class="dh5-tag-new">NEW</span>
|
||
</div>
|
||
</div>
|
||
<div class="dh5-card-tags">
|
||
<span v-if="isDiagnosisConfirmed(row)" class="dh5-tag dh5-tag--ok">已确认</span>
|
||
<span v-else class="dh5-tag dh5-tag--warn">未确认</span>
|
||
<span v-if="row.has_prescription" class="dh5-tag dh5-tag--info">已开方</span>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 卡片体:挂号 / 复诊 / 助理 / 类型 -->
|
||
<section class="dh5-card-body" @click="handleDetail(row.id)">
|
||
<!-- 挂号块 -->
|
||
<div :class="['dh5-row', 'dh5-row--apt', appointmentRowClass(row)]">
|
||
<span class="dh5-row-key">挂号</span>
|
||
<div class="dh5-row-val">
|
||
<template v-if="row.has_appointment">
|
||
<template v-if="appointmentRows(row).length">
|
||
<div
|
||
v-for="apt in appointmentRows(row)"
|
||
:key="apt.id || `${apt.doctor_id}-${apt.time_text}`"
|
||
:class="['dh5-apt-item', appointmentItemClass(apt)]"
|
||
>
|
||
<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>
|
||
</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>
|
||
</template>
|
||
</template>
|
||
<span v-else class="dh5-apt-none">未挂号</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 复诊块(仅已开方时显示) -->
|
||
<div v-if="row.has_prescription" class="dh5-row dh5-row--followup">
|
||
<span class="dh5-row-key">复诊</span>
|
||
<div class="dh5-row-val">
|
||
<span>{{ row.followup_time_text || '—' }}</span>
|
||
<span class="dh5-meta-doctor">{{ row.followup_doctor_name || '—' }}</span>
|
||
<span v-if="row.followup_rx_voided" class="dh5-tag dh5-tag--gray">处方作废</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 助理 / 类型 -->
|
||
<div class="dh5-row">
|
||
<span class="dh5-row-key">助理</span>
|
||
<span class="dh5-row-val">{{ getAssistantName(row.assistant_id || row.assistant) }}</span>
|
||
</div>
|
||
<div v-if="row.diagnosis_type" class="dh5-row">
|
||
<span class="dh5-row-key">诊断</span>
|
||
<span class="dh5-row-val">
|
||
{{ getDictLabel(diagnosisTypeOptions, row.diagnosis_type) }}
|
||
<span v-if="row.syndrome_type" class="dh5-meta-mute">·</span>
|
||
<span v-if="row.syndrome_type">{{ getDictLabel(syndromeTypeOptions, row.syndrome_type) }}</span>
|
||
</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 视频旁观(受限角色 / 状态时只读说明) -->
|
||
<div v-if="watchCallVisibleForRow(row)" class="dh5-card-watch">
|
||
<template v-if="canWatchCallEntry(row) && watchCallShowEnterButton(row)">
|
||
<el-button type="primary" size="small" :icon="VideoCamera" @click="onWatchCallEntryClick(row)">
|
||
进入旁观
|
||
</el-button>
|
||
<span class="dh5-watch-tip">{{ watchCallEnterTooltip(row) }}</span>
|
||
</template>
|
||
<template v-else>
|
||
<span class="dh5-watch-mute">{{ canWatchCallEntry(row) ? watchCallAssistantIdleText(row) : watchCallPublicStatus(row) }}</span>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 卡片操作:3 个常用 + 更多 -->
|
||
<footer class="dh5-card-foot">
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/edit'])"
|
||
type="primary"
|
||
size="small"
|
||
plain
|
||
@click="handleEdit(row.id)"
|
||
>
|
||
诊单
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/kaifang'])"
|
||
:type="prescriptionActionLabel(row) === '查看' ? 'success' : 'primary'"
|
||
size="small"
|
||
@click="handlePrescription(row)"
|
||
>
|
||
{{ prescriptionActionLabel(row) }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="hasPermission(['tcm.diagnosis/guahao'])"
|
||
type="success"
|
||
size="small"
|
||
plain
|
||
@click="handleAppointment(row)"
|
||
>
|
||
预约
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
class="dh5-more-btn"
|
||
@click="openMoreSheet(row)"
|
||
>
|
||
更多<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||
</el-button>
|
||
</footer>
|
||
</article>
|
||
|
||
<div v-if="hasMore" class="dh5-load-more" @click="loadMore">
|
||
{{ pager.loading ? '加载中…' : '加载更多' }}
|
||
</div>
|
||
<div v-if="!hasMore && pager.lists.length > 0" class="dh5-no-more">— 没有更多了 —</div>
|
||
</main>
|
||
|
||
<!-- 悬浮新增 -->
|
||
<button
|
||
v-if="hasPermission(['tcm.diagnosis/add'])"
|
||
class="dh5-fab"
|
||
type="button"
|
||
aria-label="新增诊单"
|
||
@click="handleAdd"
|
||
>
|
||
<el-icon :size="22"><Plus /></el-icon>
|
||
</button>
|
||
|
||
<!-- 复用业务弹窗 -->
|
||
<edit-popup ref="editRef" @success="refreshListAfterPopupSave" />
|
||
<detail-popup ref="detailRef" />
|
||
<tcm-prescription ref="prescriptionRef" @success="onPrescriptionSuccess" />
|
||
<appointment-popup ref="appointmentRef" @success="refreshListAfterPopupSave" />
|
||
<assistant-watch-call-dialog
|
||
v-model="watchCallDialogVisible"
|
||
:diagnosis-id="watchCallDiagnosisId"
|
||
@closed="getLists"
|
||
/>
|
||
|
||
<!-- 行操作 Action Sheet -->
|
||
<el-drawer
|
||
v-model="moreSheetVisible"
|
||
:title="moreSheetRow ? `${moreSheetRow.patient_name || '诊单'} · 更多操作` : '更多操作'"
|
||
direction="btt"
|
||
size="auto"
|
||
:with-header="true"
|
||
:destroy-on-close="true"
|
||
>
|
||
<div class="dh5-actions" v-if="moreSheetRow">
|
||
<button
|
||
v-if="hasPermission(['tcm.diagnosis/assign'])"
|
||
class="dh5-action"
|
||
@click="runAction('assign')"
|
||
>
|
||
<el-icon><User /></el-icon>
|
||
<span>指派医助</span>
|
||
</button>
|
||
<button
|
||
v-if="isAppointmentActiveForVideo(moreSheetRow) && hasPermission(['tcm.diagnosis/videoQr'])"
|
||
class="dh5-action"
|
||
@click="runAction('videoQr')"
|
||
>
|
||
<el-icon><Picture /></el-icon>
|
||
<span>视频二维码</span>
|
||
</button>
|
||
<button
|
||
v-if="isAppointmentActiveForVideo(moreSheetRow) && hasPermission(['tcm.diagnosis/guahao'])"
|
||
class="dh5-action"
|
||
@click="runAction('confirmQr')"
|
||
>
|
||
<el-icon><Picture /></el-icon>
|
||
<span>诊单二维码</span>
|
||
</button>
|
||
<button
|
||
v-if="canCancelAppointmentRow(moreSheetRow) && hasPermission(['tcm.diagnosis/guahao'])"
|
||
class="dh5-action"
|
||
@click="runAction('cancelApt')"
|
||
>
|
||
<el-icon><CircleClose /></el-icon>
|
||
<span>取消挂号</span>
|
||
</button>
|
||
<button
|
||
v-if="hasPermission(['tcm.diagnosis/edit'])"
|
||
class="dh5-action"
|
||
@click="runAction('fillIdCard')"
|
||
>
|
||
<el-icon><Postcard /></el-icon>
|
||
<span>补全身份证</span>
|
||
</button>
|
||
<button
|
||
v-if="hasPermission(['tcm.diagnosis/order'])"
|
||
class="dh5-action"
|
||
@click="runAction('order')"
|
||
>
|
||
<el-icon><Document /></el-icon>
|
||
<span>创建订单</span>
|
||
</button>
|
||
<button class="dh5-action" @click="runAction('wechat')">
|
||
<el-icon><ChatDotRound /></el-icon>
|
||
<span>企微记录</span>
|
||
</button>
|
||
<button
|
||
v-if="hasPermission(['tcm.diagnosis/delete'])"
|
||
class="dh5-action dh5-action--danger"
|
||
@click="runAction('delete')"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
<span>删除诊单</span>
|
||
</button>
|
||
</div>
|
||
</el-drawer>
|
||
|
||
<!-- 指派医助 Drawer -->
|
||
<el-drawer v-model="assignDialogVisible" title="指派医助" direction="btt" size="46%">
|
||
<el-form :model="assignForm" label-position="top" class="dh5-form">
|
||
<el-form-item label="选择医助" required>
|
||
<el-select
|
||
v-model="assignForm.assistant_id"
|
||
placeholder="请选择医助"
|
||
class="!w-full"
|
||
filterable
|
||
clearable
|
||
>
|
||
<el-option
|
||
v-for="item in assistantOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="Number(item.id)"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="诊单信息">
|
||
<div class="dh5-form-tip">
|
||
<div>患者:{{ currentRow?.patient_name }}</div>
|
||
<div>诊断日期:{{ currentRow?.diagnosis_date_text || '—' }}</div>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="dh5-flex1" @click="assignDialogVisible = false">取消</el-button>
|
||
<el-button class="dh5-flex1" type="primary" :loading="assignLoading" @click="handleConfirmAssign">
|
||
确定指派
|
||
</el-button>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<!-- 创建订单 Drawer -->
|
||
<el-drawer v-model="createOrderVisible" title="创建订单" direction="btt" size="64%" @close="resetOrderForm">
|
||
<el-form
|
||
ref="orderFormRef"
|
||
:model="orderForm"
|
||
:rules="orderRules"
|
||
label-position="top"
|
||
class="dh5-form"
|
||
>
|
||
<el-form-item label="患者">
|
||
<span>{{ orderForm.patient_name }} ({{ maskPhone(orderForm.phone) }})</span>
|
||
</el-form-item>
|
||
<el-form-item label="订单类型" prop="order_type">
|
||
<el-select v-model="orderForm.order_type" placeholder="请选择" class="!w-full">
|
||
<el-option label="挂号费" :value="1" />
|
||
<el-option label="问诊费" :value="2" />
|
||
<el-option label="药品费用" :value="3" />
|
||
<el-option label="首付费用" :value="4" />
|
||
<el-option label="尾款费用" :value="5" />
|
||
<el-option label="其他费用" :value="6" />
|
||
<el-option label="全部费用" :value="7" />
|
||
<el-option label="驼奶费用" :value="8" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="订单金额" prop="amount">
|
||
<el-input-number
|
||
v-model="orderForm.amount"
|
||
:min="0"
|
||
:step="0.01"
|
||
:precision="2"
|
||
placeholder="请输入金额"
|
||
class="!w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input
|
||
v-model="orderForm.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入备注(可选)"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="dh5-flex1" @click="createOrderVisible = false">取消</el-button>
|
||
<el-button class="dh5-flex1" type="primary" :loading="orderLoading" @click="submitOrder">
|
||
创建订单
|
||
</el-button>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<!-- 补全身份证 Dialog -->
|
||
<el-dialog
|
||
v-model="fillIdCardVisible"
|
||
title="补全身份证号"
|
||
:close-on-click-modal="false"
|
||
@close="fillIdCardForm.id_card = ''"
|
||
>
|
||
<el-form
|
||
ref="fillIdCardFormRef"
|
||
:model="fillIdCardForm"
|
||
:rules="fillIdCardRules"
|
||
label-position="top"
|
||
class="dh5-form"
|
||
>
|
||
<el-form-item label="患者">
|
||
<span class="font-medium">{{ fillIdCardForm.patient_name }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="身份证号" prop="id_card">
|
||
<el-input
|
||
v-model="fillIdCardForm.id_card"
|
||
placeholder="请输入15或18位身份证号"
|
||
maxlength="18"
|
||
show-word-limit
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="fillIdCardVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="fillIdCardLoading" @click="submitFillIdCard">
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 二维码 Dialog -->
|
||
<el-dialog
|
||
v-model="qrcodeDialogVisible"
|
||
:title="qrcodeDialogTitle"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="dh5-qr">
|
||
<div v-if="qrcodeLoading" class="dh5-qr-loading">
|
||
<el-icon class="is-loading" :size="36"><Loading /></el-icon>
|
||
<div class="dh5-qr-tip">生成中...</div>
|
||
</div>
|
||
<div v-else-if="qrcodeUrl">
|
||
<img :src="qrcodeUrl" alt="小程序二维码" class="dh5-qr-img" />
|
||
<div class="dh5-qr-meta">
|
||
<div>患者:{{ currentQRCodePatient?.patient_name }}</div>
|
||
<div>挂号时间:{{ qrcodeAppointmentTimeText }}</div>
|
||
<div class="dh5-qr-hint">请使用企业微信扫描二维码,然后转发给患者</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="dh5-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>
|
||
|
||
<!-- 企微记录 Drawer -->
|
||
<el-drawer
|
||
v-model="wechatDrawerVisible"
|
||
:title="`企微记录 - ${wechatCurrentPatient?.patient_name || ''}`"
|
||
direction="rtl"
|
||
size="100%"
|
||
:destroy-on-close="true"
|
||
>
|
||
<div v-if="wechatContactInfo" class="dh5-wx-contact">
|
||
<div class="dh5-wx-contact-row">
|
||
<span class="dh5-wx-key">手机号</span>
|
||
<span class="dh5-wx-val">{{ maskPhone(wechatContactInfo.phone) || '-' }}</span>
|
||
</div>
|
||
<div class="dh5-wx-contact-row">
|
||
<span class="dh5-wx-key">企微 ID</span>
|
||
<span class="dh5-wx-val">
|
||
<span v-if="wechatContactInfo.external_userid">{{ wechatContactInfo.external_userid }}</span>
|
||
<el-tag v-else type="info" size="small">未关联</el-tag>
|
||
</span>
|
||
</div>
|
||
<div class="dh5-wx-contact-row">
|
||
<span class="dh5-wx-key">会话存档</span>
|
||
<span class="dh5-wx-val">
|
||
<el-tag v-if="wechatContactInfo.msgaudit_enabled" type="success" size="small">已开通</el-tag>
|
||
<el-tag v-else type="info" size="small">未配置</el-tag>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dh5-wx-add">
|
||
<el-input
|
||
v-model="wechatNewNote"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="添加沟通记录..."
|
||
/>
|
||
<el-button
|
||
type="primary"
|
||
:loading="wechatAddLoading"
|
||
size="small"
|
||
@click="handleAddRecord"
|
||
>
|
||
添加
|
||
</el-button>
|
||
</div>
|
||
|
||
<div v-loading="wechatRecordsLoading" class="dh5-wx-list-wrap">
|
||
<div v-if="wechatRecords.length === 0 && !wechatRecordsLoading" class="dh5-wx-empty">
|
||
暂无聊天记录
|
||
</div>
|
||
<div v-else class="dh5-wx-list">
|
||
<div v-for="record in wechatRecords" :key="record.id" class="dh5-wx-item">
|
||
<div class="dh5-wx-head">
|
||
<div class="dh5-wx-head-left">
|
||
<el-tag :type="record.direction === 0 ? 'primary' : 'success'" size="small">
|
||
{{ record.direction === 0 ? '员工' : '客户' }}
|
||
</el-tag>
|
||
<span class="dh5-wx-name">
|
||
{{ record.direction === 0 ? record.staff_name : record.external_name || wechatCurrentPatient?.patient_name }}
|
||
</span>
|
||
<el-tag v-if="record.msg_type === 'note'" type="warning" size="small">备注</el-tag>
|
||
</div>
|
||
<div class="dh5-wx-head-right">
|
||
<span class="dh5-wx-time">{{ formatTimestamp(record.chat_time) }}</span>
|
||
<el-button type="danger" link size="small" @click="handleDeleteRecord(record.id)">
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="dh5-wx-content">{{ record.content }}</div>
|
||
<div v-if="record.media_url" class="dh5-wx-media">
|
||
<el-image
|
||
:src="record.media_url"
|
||
style="max-width: 200px"
|
||
fit="contain"
|
||
:preview-src-list="[record.media_url]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="wechatRecordTotal > wechatRecords.length" class="dh5-load-more" @click="loadMoreRecords">
|
||
加载更多
|
||
</div>
|
||
</div>
|
||
</el-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="tcmDiagnosisH5">
|
||
import {
|
||
tcmDiagnosisLists,
|
||
tcmDiagnosisDelete,
|
||
tcmDiagnosisAssign,
|
||
getAssistants,
|
||
generateMiniProgramQrcode,
|
||
generateOrderQrcode,
|
||
getWechatChatRecords,
|
||
addWechatChatRecord,
|
||
deleteWechatChatRecord,
|
||
getWechatExternalContact,
|
||
fillIdCard
|
||
} from '@/api/tcm'
|
||
import { orderCreate } from '@/api/order'
|
||
import { cancelAppointment } from '@/api/doctor'
|
||
import { getDictData } from '@/api/app'
|
||
import { getWeappConfig } from '@/api/channel/weapp'
|
||
import { usePaging } from '@/hooks/usePaging'
|
||
import feedback from '@/utils/feedback'
|
||
import { hasPermission } from '@/utils/perm'
|
||
import {
|
||
Loading,
|
||
Plus,
|
||
Search,
|
||
ArrowDown,
|
||
Picture,
|
||
User,
|
||
Document,
|
||
Delete,
|
||
CircleClose,
|
||
Postcard,
|
||
ChatDotRound,
|
||
VideoCamera
|
||
} from '@element-plus/icons-vue'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import { defineAsyncComponent, computed, reactive, ref, onMounted, onUnmounted } from 'vue'
|
||
import dayjs from 'dayjs'
|
||
|
||
const EditPopup = defineAsyncComponent(() => import('./edit.vue'))
|
||
const DetailPopup = defineAsyncComponent(() => import('./detail.vue'))
|
||
const TcmPrescription = defineAsyncComponent(() => import('@/components/tcm-prescription/index.vue'))
|
||
const AppointmentPopup = defineAsyncComponent(() => import('./appointment.vue'))
|
||
const AssistantWatchCallDialog = defineAsyncComponent(() => import('./components/AssistantWatchCallDialog.vue'))
|
||
|
||
const userStore = useUserStore()
|
||
|
||
// ========== 工具 ==========
|
||
const formatDateTime = (timestamp: number | string) => {
|
||
if (!timestamp) return '-'
|
||
const ts = String(timestamp).length === 10 ? Number(timestamp) * 1000 : Number(timestamp)
|
||
return dayjs(ts).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
const formatTimestamp = (ts: number) => {
|
||
if (!ts) return '-'
|
||
const t = String(ts).length === 10 ? ts * 1000 : ts
|
||
return dayjs(t).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
const maskPhone = (phone: string) => {
|
||
if (!phone || phone.length < 11) return phone
|
||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||
}
|
||
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
const yesterdayStr = computed(() => {
|
||
const t = new Date(); t.setDate(t.getDate() - 1)
|
||
return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
|
||
})
|
||
const dayBeforeStr = computed(() => {
|
||
const t = new Date(); t.setDate(t.getDate() - 2)
|
||
return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
|
||
})
|
||
const todayStr = computed(() => {
|
||
const t = new Date()
|
||
return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
|
||
})
|
||
const tomorrowStr = computed(() => {
|
||
const t = new Date(); t.setDate(t.getDate() + 1)
|
||
return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
|
||
})
|
||
const dayAfterStr = computed(() => {
|
||
const t = new Date(); t.setDate(t.getDate() + 2)
|
||
return `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`
|
||
})
|
||
|
||
// ========== 表单 / 列表 ==========
|
||
const formData = reactive({
|
||
keyword: '',
|
||
diagnosis_type: '',
|
||
syndrome_type: '',
|
||
assistant_id: '',
|
||
diagnosis_confirmed: '' as '' | '0' | '1',
|
||
appointment_date: '' as string,
|
||
has_appointment: '' as '' | '0' | '1',
|
||
pending_assign: '' as '' | '1',
|
||
pending_booking: '' as '' | '1',
|
||
completed_appointment: '' as '' | '1'
|
||
})
|
||
|
||
const showMoreFilter = ref(false)
|
||
const hasAptOptions = [
|
||
{ label: '全部', value: '' },
|
||
{ label: '已挂号', value: '1' },
|
||
{ label: '未挂号', value: '0' }
|
||
]
|
||
const confirmedOptions = [
|
||
{ label: '全部', value: '' },
|
||
{ label: '已确认', value: '1' },
|
||
{ label: '未确认', value: '0' }
|
||
]
|
||
|
||
const dateTabs = computed(() => [
|
||
{ label: '昨天挂号', value: yesterdayStr.value },
|
||
{ label: '前天挂号', value: dayBeforeStr.value },
|
||
{ label: '当天挂号', value: todayStr.value },
|
||
{ label: '明天挂号', value: tomorrowStr.value },
|
||
{ label: '后天挂号', value: dayAfterStr.value },
|
||
{ label: '全部', value: '' }
|
||
])
|
||
|
||
const dateCounts = ref<Record<string, number>>({})
|
||
const pendingBookingCount = ref<number | undefined>(undefined)
|
||
const completedVisitCount = ref<number | undefined>(undefined)
|
||
const pendingAssignCount = ref<number | undefined>(undefined)
|
||
|
||
const { pager, getLists } = usePaging({
|
||
fetchFun: tcmDiagnosisLists,
|
||
params: formData
|
||
})
|
||
|
||
const hasMore = computed(() => pager.lists.length < (pager.count || 0))
|
||
|
||
const refreshListAfterPopupSave = () => getLists({ silent: true })
|
||
|
||
const doSearch = () => {
|
||
pager.page = 1
|
||
getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const handleReset = () => {
|
||
formData.keyword = ''
|
||
formData.diagnosis_type = ''
|
||
formData.syndrome_type = ''
|
||
formData.assistant_id = ''
|
||
formData.diagnosis_confirmed = ''
|
||
formData.appointment_date = todayStr.value
|
||
formData.has_appointment = ''
|
||
formData.pending_assign = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
pager.page = 1
|
||
getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const setHasAppointment = (v: string) => {
|
||
formData.has_appointment = v as '' | '0' | '1'
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
pager.page = 1
|
||
getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const setConfirmed = (v: string) => {
|
||
formData.diagnosis_confirmed = v as '' | '0' | '1'
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
pager.page = 1
|
||
getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const handleDateTabClick = async (value: string) => {
|
||
formData.pending_assign = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
formData.has_appointment = ''
|
||
formData.appointment_date = value
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const handlePendingBookingTabClick = async () => {
|
||
formData.pending_assign = ''
|
||
formData.pending_booking = '1'
|
||
formData.completed_appointment = ''
|
||
formData.appointment_date = ''
|
||
formData.has_appointment = '0'
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const handleCompletedVisitTabClick = async () => {
|
||
formData.pending_assign = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = '1'
|
||
formData.has_appointment = ''
|
||
formData.appointment_date = ''
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const handlePendingAssignTabClick = async () => {
|
||
formData.pending_assign = '1'
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
formData.has_appointment = ''
|
||
formData.appointment_date = ''
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
const fetchDateCounts = async () => {
|
||
try {
|
||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
|
||
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
|
||
tcmDiagnosisLists({ pending_assign: 1, page_no: 1, page_size: 1 })
|
||
])
|
||
dateCounts.value = {
|
||
[yesterdayStr.value]: yesterday?.count ?? 0,
|
||
[dayBeforeStr.value]: dayBefore?.count ?? 0,
|
||
[todayStr.value]: today?.count ?? 0,
|
||
[tomorrowStr.value]: tomorrow?.count ?? 0,
|
||
[dayAfterStr.value]: dayAfter?.count ?? 0,
|
||
'': all?.count ?? 0
|
||
}
|
||
pendingBookingCount.value = noApt?.count ?? 0
|
||
completedVisitCount.value = doneVisit?.count ?? 0
|
||
pendingAssignCount.value = pending?.count ?? 0
|
||
} catch (e) {
|
||
console.error('获取日期数量失败', e)
|
||
}
|
||
}
|
||
|
||
const loadMore = async () => {
|
||
if (pager.loading || !hasMore.value) return
|
||
const next = (pager.page || 1) + 1
|
||
pager.loading = true
|
||
try {
|
||
const res = await tcmDiagnosisLists({
|
||
page_no: next,
|
||
page_size: pager.size,
|
||
...formData
|
||
})
|
||
pager.page = next
|
||
pager.lists.push(...(res?.lists || []))
|
||
pager.count = res?.count ?? pager.count
|
||
} catch (e) {
|
||
console.error('加载更多失败:', e)
|
||
} finally {
|
||
pager.loading = false
|
||
}
|
||
}
|
||
|
||
// ========== 字典 ==========
|
||
const diagnosisTypeOptions = ref<any[]>([])
|
||
const syndromeTypeOptions = ref<any[]>([])
|
||
const assistantOptions = ref<any[]>([])
|
||
|
||
const getDictOptions = async () => {
|
||
try {
|
||
const [diagnosisType, syndromeType, assistants] = await Promise.all([
|
||
getDictData({ type: 'diagnosis_type' }),
|
||
getDictData({ type: 'syndrome_type' }),
|
||
getAssistants()
|
||
])
|
||
diagnosisTypeOptions.value = diagnosisType?.diagnosis_type || []
|
||
syndromeTypeOptions.value = syndromeType?.syndrome_type || []
|
||
assistantOptions.value = assistants || []
|
||
} catch (e) {
|
||
console.error('获取字典数据失败:', e)
|
||
}
|
||
}
|
||
|
||
const getDictLabel = (options: any[], value: string) => {
|
||
const item = options.find(opt => opt.value === value)
|
||
return item ? item.name : value || '-'
|
||
}
|
||
|
||
const getAssistantName = (assistantId: number | string | null | undefined) => {
|
||
if (!assistantId && assistantId !== 0) return '-'
|
||
const id = typeof assistantId === 'string' ? parseInt(assistantId) : (assistantId as number)
|
||
if (assistantOptions.value.length === 0) return '...'
|
||
const a = assistantOptions.value.find(item => {
|
||
const itemId = typeof item.id === 'string' ? parseInt(item.id) : item.id
|
||
return itemId === id
|
||
})
|
||
return a ? a.name : '-'
|
||
}
|
||
|
||
// ========== 列卡片状态 ==========
|
||
const isDiagnosisConfirmed = (row: any) => {
|
||
const records = row.DiagnosisViewRecord || []
|
||
return records.length > 0 && records.some((r: any) => r.is_confirmed == 1)
|
||
}
|
||
|
||
const getCardClass = (row: any) => {
|
||
if (!isDiagnosisConfirmed(row)) return 'dh5-card--unconfirmed'
|
||
if (!row.has_appointment) return 'dh5-card--noapt'
|
||
return ''
|
||
}
|
||
|
||
const appointmentStatusLabel = (row: any) => {
|
||
const s = Number(row.appointment_status)
|
||
if (s === 3) return '已完成'
|
||
if (s === 4) return '已过号'
|
||
return '已挂号'
|
||
}
|
||
|
||
const appointmentStatusLabelByStatus = (status: number | string) => {
|
||
const s = Number(status)
|
||
if (s === 3) return '已完成'
|
||
if (s === 4) return '已过号'
|
||
return '已挂号'
|
||
}
|
||
|
||
const appointmentRows = (row: any) => {
|
||
const list = Array.isArray(row.appointments) ? row.appointments : []
|
||
if (list.length > 0) return list
|
||
if (!row.has_appointment) return []
|
||
return [{
|
||
id: row.appointment_id,
|
||
status: row.appointment_status,
|
||
doctor_id: row.appointment_doctor_id,
|
||
doctor_name: row.appointment_doctor_name,
|
||
time_text: row.appointment_time_text
|
||
}]
|
||
}
|
||
|
||
const appointmentItemClass = (apt: any) => {
|
||
const s = Number(apt?.status)
|
||
return {
|
||
'dh5-apt-item--done': s === 3,
|
||
'dh5-apt-item--missed': s === 4,
|
||
'dh5-apt-item--active': s === 1
|
||
}
|
||
}
|
||
|
||
const appointmentRowClass = (row: any) => {
|
||
const s = Number(row.appointment_status)
|
||
return {
|
||
'has-apt': row.has_appointment,
|
||
'apt-row-done': row.has_appointment && s === 3,
|
||
'apt-row-missed': row.has_appointment && s === 4
|
||
}
|
||
}
|
||
|
||
const isAppointmentActiveForVideo = (row: any) =>
|
||
row.has_appointment && Number(row.appointment_status) === 1
|
||
|
||
const canCancelAppointmentRow = (row: any) => {
|
||
const s = Number(row.appointment_status)
|
||
return !!(row.has_appointment && row.appointment_id && (s === 1 || s === 4))
|
||
}
|
||
|
||
// 视频旁观(与 PC 端一致)
|
||
type VideoCallHint = { state: string; label: string; start_time?: number; end_time?: number }
|
||
const isAssignedAssistant = (row: any) => {
|
||
const aid = row.assistant_id ?? row.assistant
|
||
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 watchCallShowEnterButton = (row: any) => {
|
||
const st = watchCallState(row)
|
||
return st === 'live' || st === 'pending_room'
|
||
}
|
||
const watchCallEnterTooltip = (row: any) => {
|
||
if (watchCallState(row) === 'pending_room') {
|
||
return '医生未接通或未同步房间'
|
||
}
|
||
return '点击进入实时房间'
|
||
}
|
||
const watchCallAssistantIdleText = (row: any) => {
|
||
const h = row.video_call_hint as VideoCallHint | undefined
|
||
if (h?.label) return h.label
|
||
return '暂无可旁观通话'
|
||
}
|
||
const watchCallPublicStatus = (row: any) => {
|
||
const st = watchCallState(row)
|
||
if (st === 'none') return ''
|
||
if (st === 'live') return '通话中'
|
||
if (st === 'pending_room') return '接通中'
|
||
const h = row.video_call_hint as VideoCallHint | undefined
|
||
return h?.label || ''
|
||
}
|
||
/** 是否需要在卡片显示视频旁观条 */
|
||
const watchCallVisibleForRow = (row: any) => {
|
||
if (canWatchCallEntry(row)) return true
|
||
const st = watchCallState(row)
|
||
return st === 'live' || st === 'pending_room'
|
||
}
|
||
const watchCallDialogVisible = ref(false)
|
||
const watchCallDiagnosisId = ref(0)
|
||
const onWatchCallEntryClick = (row: any) => {
|
||
if (watchCallState(row) !== 'live') {
|
||
feedback.msgWarning('医生未接通或未同步房间号,请稍后再试')
|
||
return
|
||
}
|
||
watchCallDiagnosisId.value = Number(row.id) || 0
|
||
watchCallDialogVisible.value = true
|
||
}
|
||
|
||
// ========== 主操作 ==========
|
||
const editRef = ref()
|
||
const detailRef = ref()
|
||
const prescriptionRef = ref()
|
||
const appointmentRef = ref()
|
||
|
||
const handleAdd = () => editRef.value?.open('add')
|
||
const handleEdit = (id: number) => editRef.value?.open('edit', id)
|
||
const handleDetail = (id: number) => detailRef.value?.open(id)
|
||
|
||
const prescriptionActionLabel = (row: any) => {
|
||
if (
|
||
row?.prescription_audit_status === 1 &&
|
||
Number(row?.prescription_void_status) !== 1
|
||
) {
|
||
return '查看'
|
||
}
|
||
return '开方'
|
||
}
|
||
|
||
const handlePrescription = (row: any) => {
|
||
if (!row?.id) {
|
||
feedback.msgWarning('诊单信息不完整')
|
||
return
|
||
}
|
||
prescriptionRef.value?.open({
|
||
...row,
|
||
diagnosis_id: row.id,
|
||
id: row.appointment_id || 0
|
||
})
|
||
}
|
||
|
||
const onPrescriptionSuccess = () => {
|
||
getLists()
|
||
;(editRef.value as { refreshCaseList?: () => void } | undefined)?.refreshCaseList?.()
|
||
}
|
||
|
||
const handleAppointment = (row: any) => {
|
||
if (!row.patient_id) { feedback.msgWarning('患者信息不完整'); return }
|
||
appointmentRef.value?.open(row)
|
||
}
|
||
|
||
const handleDelete = async (id: number) => {
|
||
await feedback.confirm('确定要删除该诊单吗?')
|
||
await tcmDiagnosisDelete({ id })
|
||
getLists()
|
||
}
|
||
|
||
// ========== Action Sheet ==========
|
||
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
|
||
switch (cmd) {
|
||
case 'assign': handleSingleAssign(row); break
|
||
case 'videoQr':
|
||
if (!isAppointmentActiveForVideo(row)) {
|
||
feedback.msgWarning('仅「已预约」状态可生成视频二维码')
|
||
return
|
||
}
|
||
handleVideoQRCode(row); break
|
||
case 'confirmQr': handleMiniProgramQRCode(row); break
|
||
case 'cancelApt': handleCancelAppointment(row); break
|
||
case 'fillIdCard': handleFillIdCard(row); break
|
||
case 'order': handleCreateOrder(row); break
|
||
case 'wechat': handleWechatRecords(row); break
|
||
case 'delete': handleDelete(row.id); break
|
||
}
|
||
}
|
||
|
||
// ========== 指派 ==========
|
||
const assignDialogVisible = ref(false)
|
||
const assignLoading = ref(false)
|
||
const currentRow = ref<any>(null)
|
||
const assignForm = ref({ assistant_id: null as number | null })
|
||
const handleSingleAssign = (row: any) => {
|
||
currentRow.value = row
|
||
assignForm.value.assistant_id = row.assistant_id ? Number(row.assistant_id) : null
|
||
assignDialogVisible.value = true
|
||
}
|
||
const handleConfirmAssign = async () => {
|
||
if (!assignForm.value.assistant_id) { feedback.msgWarning('请选择医助'); return }
|
||
assignLoading.value = true
|
||
try {
|
||
await tcmDiagnosisAssign({
|
||
id: currentRow.value.id,
|
||
assistant_id: assignForm.value.assistant_id
|
||
})
|
||
assignDialogVisible.value = false
|
||
getLists()
|
||
} catch (e) {
|
||
console.error('指派失败:', e)
|
||
} finally {
|
||
assignLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 创建订单 ==========
|
||
const createOrderVisible = ref(false)
|
||
const orderFormRef = ref()
|
||
const orderLoading = ref(false)
|
||
const orderForm = reactive({
|
||
patient_id: '' as string | number,
|
||
patient_name: '',
|
||
phone: '',
|
||
order_type: '' as string | number,
|
||
amount: 0,
|
||
remark: '',
|
||
has_appointment: false,
|
||
appointment_time_text: ''
|
||
})
|
||
const orderRules = {
|
||
order_type: [{ required: true, message: '请选择订单类型', trigger: 'change' }],
|
||
amount: [{ required: true, message: '请输入订单金额', trigger: 'blur' }]
|
||
}
|
||
const handleCreateOrder = (row: any) => {
|
||
orderForm.patient_id = row.patient_id || row.id
|
||
orderForm.patient_name = row.patient_name
|
||
orderForm.phone = row.phone || ''
|
||
orderForm.order_type = ''
|
||
orderForm.amount = 0
|
||
orderForm.remark = ''
|
||
orderForm.has_appointment = !!row.has_appointment
|
||
orderForm.appointment_time_text = row.appointment_time_text || ''
|
||
orderFormRef.value?.clearValidate()
|
||
createOrderVisible.value = true
|
||
}
|
||
const resetOrderForm = () => {
|
||
orderForm.patient_id = ''
|
||
orderForm.patient_name = ''
|
||
orderForm.phone = ''
|
||
orderForm.order_type = ''
|
||
orderForm.amount = 0
|
||
orderForm.remark = ''
|
||
orderForm.has_appointment = false
|
||
orderForm.appointment_time_text = ''
|
||
orderFormRef.value?.clearValidate()
|
||
}
|
||
const submitOrder = async () => {
|
||
try {
|
||
await orderFormRef.value?.validate()
|
||
orderLoading.value = true
|
||
const res = await orderCreate({
|
||
patient_id: orderForm.patient_id,
|
||
order_type: orderForm.order_type,
|
||
amount: orderForm.amount,
|
||
remark: orderForm.remark
|
||
})
|
||
createOrderVisible.value = false
|
||
getLists()
|
||
|
||
const orderNo = res?.order_no
|
||
if (orderNo) {
|
||
qrcodeDialogVisible.value = true
|
||
qrcodeLoading.value = true
|
||
qrcodeUrl.value = ''
|
||
qrcodeDialogTitle.value = '收款二维码'
|
||
currentQRCodePatient.value = {
|
||
patient_name: orderForm.patient_name,
|
||
has_appointment: orderForm.has_appointment,
|
||
appointment_time_text: orderForm.appointment_time_text
|
||
}
|
||
try {
|
||
const qrRes = await generateOrderQrcode({ order_no: orderNo })
|
||
if (qrRes?.qrcode_url) qrcodeUrl.value = qrRes.qrcode_url
|
||
else feedback.msgError('二维码生成失败')
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '生成二维码失败')
|
||
} finally {
|
||
qrcodeLoading.value = false
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('创建订单失败:', e)
|
||
} finally {
|
||
orderLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 补全身份证 ==========
|
||
const fillIdCardVisible = ref(false)
|
||
const fillIdCardLoading = ref(false)
|
||
const fillIdCardFormRef = ref()
|
||
const fillIdCardForm = reactive({ id: 0, patient_name: '', id_card: '' })
|
||
const validateIdCard = (_rule: any, value: string, callback: (e?: Error) => void) => {
|
||
if (!value) { callback(new Error('请输入身份证号')); return }
|
||
const valid18 = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(value)
|
||
const valid15 = /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/.test(value)
|
||
if (valid18 || valid15) callback()
|
||
else callback(new Error('请输入15或18位有效身份证号'))
|
||
}
|
||
const fillIdCardRules = {
|
||
id_card: [{ required: true, validator: validateIdCard, trigger: 'blur' }]
|
||
}
|
||
const handleFillIdCard = (row: any) => {
|
||
fillIdCardForm.id = row.id
|
||
fillIdCardForm.patient_name = row.patient_name || '-'
|
||
fillIdCardForm.id_card = row.id_card || ''
|
||
fillIdCardVisible.value = true
|
||
}
|
||
const submitFillIdCard = async () => {
|
||
await fillIdCardFormRef.value?.validate()
|
||
fillIdCardLoading.value = true
|
||
try {
|
||
await fillIdCard({ id: fillIdCardForm.id, id_card: fillIdCardForm.id_card })
|
||
feedback.msgSuccess('补全成功,年龄已自动更新')
|
||
fillIdCardVisible.value = false
|
||
getLists()
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '补全失败')
|
||
} finally {
|
||
fillIdCardLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 二维码 ==========
|
||
const qrcodeDialogVisible = ref(false)
|
||
const qrcodeLoading = ref(false)
|
||
const qrcodeUrl = ref('')
|
||
const currentQRCodePatient = ref<any>(null)
|
||
const qrcodeDialogTitle = ref('小程序二维码')
|
||
const lastQRCodeType = ref<'video' | 'confirm'>('confirm')
|
||
const qrcodeAppointmentTimeText = computed(() => {
|
||
const p = currentQRCodePatient.value
|
||
if (!p) return '—'
|
||
if (!p.has_appointment) return '未挂号'
|
||
return p.appointment_time_text || '—'
|
||
})
|
||
|
||
const handleVideoQRCode = async (row: any) => {
|
||
if (!isAppointmentActiveForVideo(row)) {
|
||
feedback.msgWarning('仅「已预约」状态可生成视频二维码'); return
|
||
}
|
||
if (!row.patient_id) { feedback.msgWarning('患者信息不完整'); return }
|
||
lastQRCodeType.value = 'video'
|
||
currentQRCodePatient.value = row
|
||
qrcodeDialogVisible.value = true
|
||
qrcodeLoading.value = true
|
||
qrcodeUrl.value = ''
|
||
qrcodeDialogTitle.value = '视频二维码'
|
||
try {
|
||
const config = await getWeappConfig()
|
||
if (!config?.app_id) {
|
||
feedback.msgError('小程序未配置')
|
||
qrcodeDialogVisible.value = false
|
||
return
|
||
}
|
||
const result = await generateMiniProgramQrcode({
|
||
doctor_id: row.appointment_doctor_id,
|
||
diagnosis_id: row.appointment_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 handleMiniProgramQRCode = async (row: any) => {
|
||
if (!isAppointmentActiveForVideo(row)) {
|
||
feedback.msgWarning('仅「已预约」状态可使用诊单二维码'); return
|
||
}
|
||
if (!row.patient_id) { feedback.msgWarning('患者信息不完整'); return }
|
||
lastQRCodeType.value = 'confirm'
|
||
currentQRCodePatient.value = row
|
||
qrcodeDialogVisible.value = true
|
||
qrcodeLoading.value = true
|
||
qrcodeUrl.value = ''
|
||
qrcodeDialogTitle.value = '诊单二维码'
|
||
try {
|
||
const config = await getWeappConfig()
|
||
if (!config?.app_id) {
|
||
feedback.msgError('小程序未配置')
|
||
qrcodeDialogVisible.value = false
|
||
return
|
||
}
|
||
const result = await generateMiniProgramQrcode({
|
||
diagnosis_id: row.id,
|
||
doctor_id: row.appointment_doctor_id,
|
||
patient_id: row.patient_id,
|
||
share_user_id: userStore.userInfo?.id || ''
|
||
})
|
||
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 (currentQRCodePatient.value) {
|
||
lastQRCodeType.value === 'video'
|
||
? handleVideoQRCode(currentQRCodePatient.value)
|
||
: handleMiniProgramQRCode(currentQRCodePatient.value)
|
||
}
|
||
}
|
||
|
||
// ========== 取消挂号 ==========
|
||
const handleCancelAppointment = async (row: any) => {
|
||
if (!canCancelAppointmentRow(row)) {
|
||
const s = Number(row.appointment_status)
|
||
if (row.has_appointment && s === 3) feedback.msgWarning('已完成就诊,无法取消挂号')
|
||
else feedback.msgWarning('该诊单未挂号或挂号信息异常')
|
||
return
|
||
}
|
||
try {
|
||
await feedback.confirm(`确定要取消患者「${row.patient_name}」的挂号吗?`)
|
||
await cancelAppointment({ id: row.appointment_id })
|
||
feedback.msgSuccess('取消挂号成功')
|
||
getLists()
|
||
fetchDateCounts()
|
||
} catch (e: any) {
|
||
if (e !== 'cancel') feedback.msgError(e?.msg || '取消挂号失败')
|
||
}
|
||
}
|
||
|
||
// ========== 企业微信 ==========
|
||
const wechatDrawerVisible = ref(false)
|
||
const wechatCurrentPatient = ref<any>(null)
|
||
const wechatRecords = ref<any[]>([])
|
||
const wechatRecordTotal = ref(0)
|
||
const wechatRecordsLoading = ref(false)
|
||
const wechatContactInfo = ref<any>(null)
|
||
const wechatNewNote = ref('')
|
||
const wechatAddLoading = ref(false)
|
||
const wechatPage = ref(1)
|
||
|
||
const handleWechatRecords = async (row: any) => {
|
||
wechatCurrentPatient.value = row
|
||
wechatDrawerVisible.value = true
|
||
wechatRecords.value = []
|
||
wechatRecordTotal.value = 0
|
||
wechatContactInfo.value = null
|
||
wechatNewNote.value = ''
|
||
wechatPage.value = 1
|
||
wechatRecordsLoading.value = true
|
||
try {
|
||
const [recordsRes, contactRes] = await Promise.all([
|
||
getWechatChatRecords({ diagnosis_id: row.id, patient_id: row.patient_id, page_no: 1, page_size: 20 }),
|
||
getWechatExternalContact({ patient_id: row.patient_id || row.id }).catch(() => null)
|
||
])
|
||
wechatRecords.value = recordsRes?.lists || []
|
||
wechatRecordTotal.value = recordsRes?.count || 0
|
||
wechatContactInfo.value = contactRes
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
wechatRecordsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const loadMoreRecords = async () => {
|
||
if (!wechatCurrentPatient.value) return
|
||
wechatPage.value++
|
||
try {
|
||
const res = await getWechatChatRecords({
|
||
diagnosis_id: wechatCurrentPatient.value.id,
|
||
patient_id: wechatCurrentPatient.value.patient_id,
|
||
page_no: wechatPage.value, page_size: 20
|
||
})
|
||
wechatRecords.value.push(...(res?.lists || []))
|
||
} catch (e) { console.error(e) }
|
||
}
|
||
|
||
const handleAddRecord = async () => {
|
||
if (!wechatNewNote.value.trim()) { feedback.msgWarning('请输入内容'); return }
|
||
wechatAddLoading.value = true
|
||
try {
|
||
await addWechatChatRecord({
|
||
diagnosis_id: wechatCurrentPatient.value.id,
|
||
patient_id: wechatCurrentPatient.value.patient_id,
|
||
content: wechatNewNote.value.trim(),
|
||
msg_type: 'note',
|
||
staff_name: userStore.userInfo?.name || '',
|
||
external_name: wechatCurrentPatient.value.patient_name || ''
|
||
})
|
||
wechatNewNote.value = ''
|
||
handleWechatRecords(wechatCurrentPatient.value)
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '添加失败')
|
||
} finally {
|
||
wechatAddLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleDeleteRecord = async (id: number) => {
|
||
await feedback.confirm('确定删除该记录吗?')
|
||
try {
|
||
await deleteWechatChatRecord({ id })
|
||
wechatRecords.value = wechatRecords.value.filter(r => r.id !== id)
|
||
wechatRecordTotal.value--
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '删除失败')
|
||
}
|
||
}
|
||
|
||
// ========== 生命周期 ==========
|
||
const SILENT_LIST_POLL_MS = 15_000
|
||
let silentListPollTimer: ReturnType<typeof setInterval> | null = null
|
||
const silentRefresh = () => {
|
||
if (typeof document !== 'undefined' && document.hidden) return
|
||
void getLists({ silent: true }).catch(() => {})
|
||
void fetchDateCounts()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
document.body.classList.add('h5-mode')
|
||
formData.appointment_date = todayStr.value
|
||
getDictOptions()
|
||
getLists()
|
||
if (typeof requestIdleCallback === 'function') {
|
||
requestIdleCallback(() => fetchDateCounts(), { timeout: 2500 })
|
||
} else {
|
||
setTimeout(fetchDateCounts, 200)
|
||
}
|
||
silentListPollTimer = setInterval(silentRefresh, SILENT_LIST_POLL_MS)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
document.body.classList.remove('h5-mode')
|
||
if (silentListPollTimer) {
|
||
clearInterval(silentListPollTimer)
|
||
silentListPollTimer = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============= 设计变量(手机端紧凑) ============= */
|
||
$dh5-primary: #2563eb;
|
||
$dh5-primary-soft: #eff6ff;
|
||
$dh5-success: #10b981;
|
||
$dh5-warn: #f59e0b;
|
||
$dh5-danger: #ef4444;
|
||
$dh5-text: #0f172a;
|
||
$dh5-text-mute: #475569;
|
||
$dh5-line: #e2e8f0;
|
||
$dh5-bg: #f4f6fb;
|
||
$dh5-card-bg: #ffffff;
|
||
|
||
.dh5 {
|
||
max-width: 560px;
|
||
margin: 0 auto;
|
||
min-height: 100vh;
|
||
background: $dh5-bg;
|
||
padding-bottom: 88px;
|
||
color: $dh5-text;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* ============= 顶部 ============= */
|
||
.dh5-topbar {
|
||
background: $dh5-card-bg;
|
||
padding: 12px 14px 10px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
box-shadow: 0 1px 0 $dh5-line;
|
||
}
|
||
|
||
.dh5-search {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
|
||
:deep(.el-input__wrapper) {
|
||
border-radius: 10px;
|
||
background: $dh5-bg;
|
||
box-shadow: none !important;
|
||
padding: 4px 10px;
|
||
}
|
||
|
||
.dh5-search-btn {
|
||
border-radius: 10px;
|
||
height: 36px;
|
||
}
|
||
}
|
||
|
||
.dh5-chips {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.dh5-chips--scroll {
|
||
overflow-x: auto;
|
||
flex-wrap: nowrap;
|
||
scrollbar-width: none;
|
||
|
||
&::-webkit-scrollbar { display: none; }
|
||
}
|
||
|
||
.dh5-chip {
|
||
flex: 0 0 auto;
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: $dh5-text-mute;
|
||
background: $dh5-bg;
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
transition: background-color 0.18s, color 0.18s;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
line-height: 1;
|
||
|
||
em {
|
||
font-style: normal;
|
||
margin-left: 4px;
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
&:active {
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
&.active {
|
||
color: #fff;
|
||
background: $dh5-primary;
|
||
|
||
em { opacity: 0.85; }
|
||
}
|
||
}
|
||
|
||
.dh5-chip--booking.active { background: $dh5-warn; }
|
||
.dh5-chip--done.active { background: $dh5-success; }
|
||
.dh5-chip--pending.active { background: $dh5-danger; }
|
||
|
||
.dh5-quick {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.dh5-quick-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.dh5-quick-label {
|
||
font-size: 12px;
|
||
color: $dh5-text-mute;
|
||
}
|
||
|
||
.dh5-pill {
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
color: $dh5-text-mute;
|
||
background: $dh5-bg;
|
||
cursor: pointer;
|
||
transition: background-color 0.18s, color 0.18s;
|
||
line-height: 1.4;
|
||
|
||
&:active { background: #e2e8f0; }
|
||
|
||
&.active {
|
||
color: $dh5-primary;
|
||
background: $dh5-primary-soft;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.dh5-more-toggle {
|
||
margin-left: auto;
|
||
font-size: 12px !important;
|
||
|
||
.el-icon {
|
||
margin-left: 2px;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.rotate-180 { transform: rotate(180deg); }
|
||
}
|
||
|
||
.dh5-more-filter {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed $dh5-line;
|
||
|
||
.el-select { flex: 1 1 calc(50% - 4px); min-width: 120px; }
|
||
}
|
||
|
||
/* ============= 列表 ============= */
|
||
.dh5-list {
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.dh5-empty {
|
||
padding: 60px 0;
|
||
}
|
||
|
||
.dh5-card {
|
||
background: $dh5-card-bg;
|
||
border-radius: 14px;
|
||
margin-bottom: 10px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04);
|
||
border: 1px solid transparent;
|
||
position: relative;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 3px;
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
.dh5-card--unconfirmed::before { background: $dh5-danger; }
|
||
.dh5-card--noapt::before { background: $dh5-warn; }
|
||
|
||
.dh5-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 14px 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.dh5-card-avatar {
|
||
flex: 0 0 36px;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
background: $dh5-primary-soft;
|
||
color: $dh5-primary;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.dh5-card-id-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.dh5-card-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: $dh5-text;
|
||
line-height: 1.2;
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dh5-tag-new {
|
||
display: inline-block;
|
||
background: #ef4444;
|
||
color: #fff;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 1px 4px;
|
||
border-radius: 3px;
|
||
margin-left: 4px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.dh5-meta-gender,
|
||
.dh5-meta-age {
|
||
font-size: 12px;
|
||
color: $dh5-text-mute;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.dh5-card-sub {
|
||
font-size: 12px;
|
||
color: $dh5-text-mute;
|
||
margin-top: 4px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.dh5-card-tags {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.dh5-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 8px;
|
||
font-size: 11px;
|
||
border-radius: 999px;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dh5-tag--ok { background: rgba(16, 185, 129, 0.10); color: #047857; }
|
||
.dh5-tag--warn { background: rgba(239, 68, 68, 0.10); color: #b91c1c; }
|
||
.dh5-tag--info { background: rgba(37, 99, 235, 0.10); color: #1d4ed8; }
|
||
.dh5-tag--gray { background: #f1f5f9; color: #64748b; }
|
||
|
||
.dh5-card-body {
|
||
padding: 4px 14px 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.dh5-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
gap: 8px;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.dh5-row-key {
|
||
flex: 0 0 44px;
|
||
color: $dh5-text-mute;
|
||
font-size: 12px;
|
||
padding-top: 1px;
|
||
}
|
||
|
||
.dh5-row-val {
|
||
flex: 1;
|
||
color: $dh5-text;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
align-items: center;
|
||
}
|
||
|
||
.dh5-meta-doctor,
|
||
.dh5-meta-mute {
|
||
color: $dh5-text-mute;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 挂号块 - 状态色 */
|
||
.dh5-row--apt {
|
||
background: #fafbfc;
|
||
border-radius: 10px;
|
||
padding: 8px 10px;
|
||
margin-bottom: 6px;
|
||
|
||
.dh5-row-key { padding-top: 2px; }
|
||
|
||
&.apt-row-done {
|
||
background: rgba(16, 185, 129, 0.06);
|
||
}
|
||
|
||
&.apt-row-missed {
|
||
background: rgba(148, 163, 184, 0.10);
|
||
}
|
||
}
|
||
|
||
.dh5-apt-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
background: rgba(37, 99, 235, 0.06);
|
||
font-size: 12px;
|
||
width: 100%;
|
||
}
|
||
|
||
.dh5-apt-item--done { background: rgba(16, 185, 129, 0.10); }
|
||
.dh5-apt-item--missed { background: rgba(148, 163, 184, 0.14); }
|
||
.dh5-apt-item--active { background: rgba(37, 99, 235, 0.10); }
|
||
|
||
.dh5-apt-status {
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.dh5-apt-doctor {
|
||
color: $dh5-text;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dh5-apt-time {
|
||
color: $dh5-text-mute;
|
||
font-size: 12px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.dh5-apt-none {
|
||
color: $dh5-warn;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 视频旁观条 */
|
||
.dh5-card-watch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 14px;
|
||
background: linear-gradient(90deg, rgba(37, 99, 235, 0.04), transparent);
|
||
border-top: 1px dashed $dh5-line;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.dh5-watch-tip {
|
||
color: $dh5-text-mute;
|
||
font-size: 11px;
|
||
line-height: 1.3;
|
||
flex: 1;
|
||
}
|
||
|
||
.dh5-watch-mute {
|
||
color: $dh5-text-mute;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 卡片底部按钮 */
|
||
.dh5-card-foot {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 12px;
|
||
border-top: 1px solid #f1f5f9;
|
||
|
||
.el-button {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 0;
|
||
height: 32px;
|
||
font-size: 13px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.dh5-more-btn {
|
||
flex: 0 0 auto;
|
||
min-width: 64px;
|
||
}
|
||
}
|
||
|
||
/* ============= FAB ============= */
|
||
.dh5-fab {
|
||
position: fixed;
|
||
right: 16px;
|
||
bottom: 28px;
|
||
width: 52px;
|
||
height: 52px;
|
||
border: none;
|
||
border-radius: 999px;
|
||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||
color: #fff;
|
||
box-shadow: 0 8px 22px rgba(37, 99, 235, 0.32);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
z-index: 99;
|
||
transition: transform 0.18s ease;
|
||
|
||
&:active { transform: scale(0.94); }
|
||
}
|
||
|
||
/* ============= Load more / empty ============= */
|
||
.dh5-load-more {
|
||
text-align: center;
|
||
padding: 16px;
|
||
font-size: 13px;
|
||
color: $dh5-primary;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.dh5-no-more {
|
||
text-align: center;
|
||
padding: 16px;
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
/* ============= Action Sheet ============= */
|
||
.dh5-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px 8px;
|
||
padding: 8px 4px 8px;
|
||
}
|
||
|
||
.dh5-action {
|
||
border: none;
|
||
background: transparent;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 10px 4px;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
color: $dh5-text;
|
||
font-size: 12px;
|
||
transition: background-color 0.18s;
|
||
|
||
.el-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
font-size: 18px;
|
||
background: $dh5-primary-soft;
|
||
color: $dh5-primary;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
&:active { background: $dh5-bg; }
|
||
|
||
&--danger {
|
||
color: $dh5-danger;
|
||
|
||
.el-icon {
|
||
background: rgba(239, 68, 68, 0.10);
|
||
color: $dh5-danger;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ============= 通用 form / drawer ============= */
|
||
.dh5-form {
|
||
:deep(.el-form-item__label) {
|
||
font-size: 13px !important;
|
||
color: $dh5-text-mute !important;
|
||
font-weight: 500 !important;
|
||
}
|
||
}
|
||
|
||
.dh5-form-tip {
|
||
font-size: 13px;
|
||
color: $dh5-text-mute;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.dh5-flex1 {
|
||
flex: 1;
|
||
}
|
||
|
||
/* ============= QR Dialog ============= */
|
||
.dh5-qr {
|
||
padding: 16px 4px 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.dh5-qr-loading {
|
||
padding: 24px 0;
|
||
color: $dh5-text-mute;
|
||
}
|
||
|
||
.dh5-qr-tip {
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dh5-qr-img {
|
||
width: 220px;
|
||
height: 220px;
|
||
border-radius: 12px;
|
||
border: 1px solid $dh5-line;
|
||
}
|
||
|
||
.dh5-qr-meta {
|
||
margin-top: 12px;
|
||
color: $dh5-text-mute;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
|
||
.dh5-qr-hint { color: $dh5-text-mute; opacity: 0.85; }
|
||
}
|
||
|
||
.dh5-qr-fail {
|
||
padding: 24px;
|
||
color: $dh5-danger;
|
||
}
|
||
|
||
/* ============= 企微 Drawer ============= */
|
||
.dh5-wx-contact {
|
||
background: $dh5-bg;
|
||
border-radius: 10px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.dh5-wx-contact-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.dh5-wx-key { color: $dh5-text-mute; }
|
||
.dh5-wx-val { color: $dh5-text; }
|
||
|
||
.dh5-wx-add {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
|
||
.el-textarea { flex: 1; }
|
||
.el-button { align-self: flex-end; }
|
||
}
|
||
|
||
.dh5-wx-empty {
|
||
text-align: center;
|
||
color: #94a3b8;
|
||
padding: 32px 0;
|
||
}
|
||
|
||
.dh5-wx-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.dh5-wx-item {
|
||
background: #fff;
|
||
border: 1px solid $dh5-line;
|
||
border-radius: 10px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.dh5-wx-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dh5-wx-head-left,
|
||
.dh5-wx-head-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dh5-wx-name { font-size: 13px; font-weight: 500; }
|
||
.dh5-wx-time { font-size: 11px; color: #94a3b8; }
|
||
|
||
.dh5-wx-content {
|
||
font-size: 13px;
|
||
color: $dh5-text;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.dh5-wx-media {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
:deep(.el-drawer__footer) {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
border-top: 1px solid $dh5-line;
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss">
|
||
/* 仅 H5 模式下的全局微调(沿用旧代码的 body.h5-mode 上下文) */
|
||
body.h5-mode {
|
||
.el-drawer {
|
||
width: 100% !important;
|
||
max-width: 100vw !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;
|
||
}
|
||
|
||
.dialog .el-dialog {
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
margin: 0 !important;
|
||
height: 100vh;
|
||
border-radius: 0 !important;
|
||
|
||
.el-dialog__body {
|
||
max-height: calc(100vh - 110px);
|
||
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-input,
|
||
.el-input-number,
|
||
.el-select,
|
||
.el-date-editor,
|
||
.el-textarea {
|
||
width: 100% !important;
|
||
}
|
||
|
||
.el-input-number .el-input__wrapper { width: 100% !important; }
|
||
|
||
.el-input__wrapper,
|
||
.el-textarea__inner {
|
||
border-radius: 8px !important;
|
||
}
|
||
|
||
.el-radio-group {
|
||
display: flex !important;
|
||
flex-wrap: wrap !important;
|
||
gap: 8px !important;
|
||
}
|
||
}
|
||
|
||
.el-divider {
|
||
margin: 14px 0 8px !important;
|
||
border-top: none !important;
|
||
|
||
.el-divider__text {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
background: #fff;
|
||
padding-left: 0 !important;
|
||
}
|
||
|
||
&::before { width: 0 !important; }
|
||
}
|
||
|
||
.checkbox-form-item,
|
||
.compact-form-item {
|
||
.el-checkbox-group,
|
||
.el-radio-group {
|
||
display: flex !important;
|
||
flex-wrap: wrap !important;
|
||
gap: 8px !important;
|
||
}
|
||
|
||
.el-checkbox-button,
|
||
.el-radio-button {
|
||
.el-checkbox-button__inner,
|
||
.el-radio-button__inner {
|
||
padding: 6px 14px !important;
|
||
font-size: 13px !important;
|
||
border-radius: 16px !important;
|
||
border: 1px solid #e0e0e0 !important;
|
||
background: #f9f9f9 !important;
|
||
color: #606266 !important;
|
||
transition: all 0.2s !important;
|
||
}
|
||
|
||
&.is-checked {
|
||
.el-checkbox-button__inner,
|
||
.el-radio-button__inner {
|
||
background: #ecf5ff !important;
|
||
border-color: #2563eb !important;
|
||
color: #2563eb !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.el-checkbox-button + .el-checkbox-button,
|
||
.el-radio-button + .el-radio-button {
|
||
margin-left: 0 !important;
|
||
|
||
.el-checkbox-button__inner,
|
||
.el-radio-button__inner {
|
||
border-left: 1px solid #e0e0e0 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.form-tips {
|
||
font-size: 11px;
|
||
color: #b0b0b0;
|
||
line-height: 1.4;
|
||
margin-top: 4px;
|
||
}
|
||
}
|
||
|
||
.diagnosis-tabs {
|
||
.el-tabs__header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
background: #fff;
|
||
margin: 0 0 6px 0 !important;
|
||
padding-top: 2px;
|
||
}
|
||
|
||
.el-tabs__nav-wrap::after { height: 1px !important; }
|
||
.el-tabs__item { font-size: 14px !important; }
|
||
}
|
||
|
||
/* 预约时间网格(保留) */
|
||
.appointment-form {
|
||
.el-form-item {
|
||
flex-direction: column !important;
|
||
align-items: flex-start !important;
|
||
margin-bottom: 12px !important;
|
||
|
||
.el-form-item__label {
|
||
width: auto !important;
|
||
min-width: 0 !important;
|
||
text-align: left !important;
|
||
padding: 0 0 4px 0 !important;
|
||
font-size: 13px !important;
|
||
font-weight: 500;
|
||
color: #8c8c8c;
|
||
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-radio-group {
|
||
display: flex !important;
|
||
flex-wrap: wrap !important;
|
||
gap: 8px !important;
|
||
}
|
||
|
||
.el-radio { margin-right: 0 !important; }
|
||
|
||
.el-textarea__inner { border-radius: 8px !important; }
|
||
}
|
||
|
||
.appointment-time-container { width: 100% !important; }
|
||
|
||
.date-selector {
|
||
display: grid !important;
|
||
grid-template-columns: repeat(3, 1fr) !important;
|
||
gap: 8px !important;
|
||
|
||
> .el-button,
|
||
> button {
|
||
min-width: 0 !important;
|
||
width: 100% !important;
|
||
height: 40px !important;
|
||
font-size: 13px !important;
|
||
padding: 0 4px !important;
|
||
border-radius: 10px !important;
|
||
box-sizing: border-box !important;
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
|
||
.time-slots-container {
|
||
padding: 12px !important;
|
||
border-radius: 10px !important;
|
||
background-color: #f7f8fa !important;
|
||
}
|
||
|
||
.time-slots-grid {
|
||
grid-template-columns: repeat(3, 1fr) !important;
|
||
gap: 8px !important;
|
||
max-height: 300px !important;
|
||
|
||
.time-slot-item {
|
||
min-height: 56px !important;
|
||
padding: 6px 4px !important;
|
||
border-radius: 10px !important;
|
||
border-width: 1.5px !important;
|
||
}
|
||
}
|
||
|
||
.drawer-footer { padding: 0 !important; }
|
||
|
||
.detail-popup {
|
||
.el-descriptions__body .el-descriptions__table {
|
||
display: block !important;
|
||
|
||
tbody { display: block !important; }
|
||
|
||
.el-descriptions-row {
|
||
display: block !important;
|
||
border-bottom: none !important;
|
||
|
||
.el-descriptions__cell {
|
||
display: block !important;
|
||
width: 100% !important;
|
||
border: none !important;
|
||
|
||
&.el-descriptions__label {
|
||
background: transparent;
|
||
font-weight: 500;
|
||
font-size: 13px;
|
||
color: #999;
|
||
padding: 10px 0 2px 0 !important;
|
||
border-bottom: none !important;
|
||
}
|
||
|
||
&.el-descriptions__content {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
padding: 2px 0 10px 0 !important;
|
||
border-bottom: 1px solid #f5f5f5 !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|