2522 lines
90 KiB
Vue
2522 lines
90 KiB
Vue
<template>
|
||
<div class="tcm-diagnosis">
|
||
<el-card class="filter-card !border-none" shadow="never">
|
||
<!-- 第一行:日期 + 患者搜索 -->
|
||
<div class="filter-main">
|
||
<div class="date-chips">
|
||
<span
|
||
v-for="tab in dateTabs"
|
||
:key="tab.value"
|
||
:class="[
|
||
'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="['chip', 'chip-pending-booking', { active: formData.pending_booking === '1' }]"
|
||
@click="handlePendingBookingTabClick"
|
||
>
|
||
待预约
|
||
<em v-if="pendingBookingCount !== undefined">{{ pendingBookingCount }}</em>
|
||
</span>
|
||
<span
|
||
:class="['chip', 'chip-completed-visit', { active: formData.completed_appointment === '1' }]"
|
||
@click="handleCompletedVisitTabClick"
|
||
>
|
||
已完成
|
||
<em v-if="completedVisitCount !== undefined">{{ completedVisitCount }}</em>
|
||
</span>
|
||
<span
|
||
class="pending-assign-wrap"
|
||
v-perms="['tcm.diagnosis/assign']"
|
||
>
|
||
<span
|
||
:class="['chip', 'chip-pending', { active: formData.pending_assign === '1' }]"
|
||
@click="handlePendingAssignTabClick"
|
||
>
|
||
待分配医助
|
||
<em v-if="pendingAssignCount !== undefined">{{ pendingAssignCount }}</em>
|
||
</span>
|
||
<el-date-picker
|
||
v-if="formData.pending_assign === '1'"
|
||
v-model="formData.pending_assign_order_month"
|
||
type="month"
|
||
placeholder="订单月份"
|
||
value-format="YYYY-MM"
|
||
clearable
|
||
class="pending-assign-month"
|
||
@change="onPendingAssignOrderMonthChange"
|
||
/>
|
||
<el-input
|
||
v-if="formData.pending_assign === '1'"
|
||
v-model="formData.pending_assign_keyword"
|
||
placeholder="搜身份证/诊单ID/患者号/备注…"
|
||
clearable
|
||
class="pending-assign-search"
|
||
@keyup.enter="doSearch"
|
||
@clear="doSearch"
|
||
/>
|
||
</span>
|
||
</div>
|
||
<div class="search-row">
|
||
<el-input v-model="formData.keyword" placeholder="患者姓名 / 手机号" clearable class="search-input" @keyup.enter="doSearch" />
|
||
<el-button type="primary" @click="doSearch">查询</el-button>
|
||
</div>
|
||
</div>
|
||
<!-- 第二行:挂号/确认 一键筛选,无需下拉 -->
|
||
<div class="filter-chips">
|
||
<span class="chip-label">挂号:</span>
|
||
<span
|
||
v-for="opt in hasAptOptions"
|
||
:key="opt.value"
|
||
:class="['chip', 'chip-sm', { active: formData.has_appointment === opt.value }]"
|
||
@click="setHasAppointment(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</span>
|
||
<span class="chip-divider" />
|
||
<span class="chip-label">确认:</span>
|
||
<span
|
||
v-for="opt in confirmedOptions"
|
||
:key="opt.value"
|
||
:class="['chip', 'chip-sm', { active: formData.diagnosis_confirmed === opt.value }]"
|
||
@click="setConfirmed(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</span>
|
||
<el-button link type="primary" size="small" class="more-btn" @click="showMoreFilter = !showMoreFilter">
|
||
{{ showMoreFilter ? '收起' : '更多筛选' }}
|
||
<el-icon :class="{ 'rotate-180': showMoreFilter }"><ArrowDown /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
<!-- 更多筛选(折叠) -->
|
||
<div v-show="showMoreFilter" class="filter-more">
|
||
<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>
|
||
</el-card>
|
||
|
||
<el-card class="!border-none mt-3 list-card" shadow="never">
|
||
<div class="list-header">
|
||
<div class="list-actions">
|
||
<el-button type="primary" @click="handleAdd" v-perms="['tcm.diagnosis/add']">新增诊单</el-button>
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/assign']"
|
||
type="success"
|
||
@click="handleBatchAssign"
|
||
:disabled="selectedIds.length === 0"
|
||
>
|
||
批量指派医助
|
||
</el-button>
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/assign']"
|
||
type="warning"
|
||
plain
|
||
@click="handleBatchCancelAssign"
|
||
:disabled="!canBatchCancelAssign"
|
||
>
|
||
批量取消指派
|
||
</el-button>
|
||
</div>
|
||
<div v-if="selectedIds.length > 0" class="list-selected">已选 {{ selectedIds.length }} 条</div>
|
||
</div>
|
||
<div class="table-wrap px-4">
|
||
<el-table
|
||
:data="pager.lists"
|
||
row-key="id"
|
||
size="default"
|
||
v-loading="pager.loading"
|
||
@selection-change="handleSelectionChange"
|
||
@row-dblclick="goReadonly"
|
||
:row-class-name="getRowClassName"
|
||
class="diagnosis-table"
|
||
stripe
|
||
>
|
||
<el-table-column type="selection" width="48" align="center" />
|
||
<el-table-column label="ID" min-width="50">
|
||
<template #default="{ row }">
|
||
<div class="patient-cell">
|
||
<!-- <div class="patient-avatar">{{ (row.patient_name || '患').charAt(0) }}</div> -->
|
||
<div class="patient-info">
|
||
<div class="patient-name">{{ row.id }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="患者" min-width="60">
|
||
<template #default="{ row }">
|
||
<div class="patient-cell">
|
||
<!-- <div class="patient-avatar">{{ (row.patient_name || '患').charAt(0) }}</div> -->
|
||
<div class="patient-info">
|
||
<div class="patient-name">{{ row.patient_name }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label=" 性别 / 年龄" width="100" align="left">
|
||
<template #default="{ row }">
|
||
<div >{{ row.gender_desc || '-' }} · {{ row.age ?? '-' }}岁</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="挂号" min-width="175">
|
||
<template #default="{ row }">
|
||
<div
|
||
class="appointment-cell"
|
||
:class="appointmentCellClasses(row)"
|
||
>
|
||
<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="apt-item"
|
||
:class="appointmentItemClass(apt)"
|
||
>
|
||
<div class="apt-badge">{{ appointmentStatusLabelByStatus(apt.status) }}</div>
|
||
<div class="apt-doctor">{{ apt.doctor_name || '-' }}</div>
|
||
<div class="apt-time">{{ apt.time_text || '-' }}</div>
|
||
<div v-if="canCancelAppointmentItem(apt)" class="apt-actions">
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/guahao']"
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click.stop="handleCancelAppointmentItem(row, apt)"
|
||
>
|
||
取消挂号
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="apt-badge">{{ appointmentStatusLabel(row) }}</div>
|
||
<div class="apt-doctor">{{ row.appointment_doctor_name || '-' }}</div>
|
||
<div class="apt-time">{{ row.appointment_time_text || '-' }}</div>
|
||
</template>
|
||
</template>
|
||
<span v-else class="apt-none">未挂号</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="确认" width="88" align="center">
|
||
<template #default="{ row }">
|
||
<span v-if="isDiagnosisConfirmed(row)" class="status-confirmed">已确认</span>
|
||
<span v-else class="status-unconfirmed">未确认</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="复诊" min-width="120">
|
||
<template #default="{ row }">
|
||
<div v-if="row.has_prescription" class="followup-cell">
|
||
<div class="followup-time">{{ row.followup_time_text || '—' }}</div>
|
||
<div class="followup-doctor">{{ row.followup_doctor_name || '—' }}</div>
|
||
<el-tag
|
||
v-if="row.followup_rx_voided"
|
||
type="info"
|
||
size="small"
|
||
effect="plain"
|
||
class="followup-void-tag"
|
||
>
|
||
处方已作废
|
||
</el-tag>
|
||
</div>
|
||
<span v-else class="followup-none">无</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="助理" width="100" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span class="assistant-cell">{{ getAssistantName(row.assistant_id || row.assistant) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="开方" width="72" align="center">
|
||
<template #default="{ row }">
|
||
<span v-if="row.has_prescription" class="status-prescribed">已开方</span>
|
||
<span v-else class="status-unprescribed">未开方</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="未服务天数" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
v-if="row.last_blood_record_at"
|
||
:content="`最近一次血糖打卡:${row.last_blood_record_at}`"
|
||
placement="top"
|
||
>
|
||
<span class="unserved-days" :class="unservedDaysClass(row.unserved_days)">
|
||
{{ row.unserved_days ?? '—' }}
|
||
</span>
|
||
</el-tooltip>
|
||
<span v-else class="unserved-days unserved-muted">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="视频旁观" width="120" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<div class="video-watch-col">
|
||
<template v-if="canWatchCallEntry(row)">
|
||
<template v-if="watchCallShowEnterButton(row)">
|
||
<el-tooltip :content="watchCallEnterTooltip(row)" placement="top">
|
||
<span class="video-watch-trigger">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="onWatchCallEntryClick(row)"
|
||
>
|
||
进入旁观
|
||
</el-button>
|
||
</span>
|
||
</el-tooltip>
|
||
</template>
|
||
<span v-else class="video-watch-muted">{{ watchCallAssistantIdleText(row) }}</span>
|
||
</template>
|
||
<span v-else class="video-watch-muted">{{ watchCallPublicStatus(row) }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="340" fixed="right" align="left">
|
||
<template #default="{ row }">
|
||
<div class="action-cell" @click.stop @dblclick.stop>
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="goReadonly(row)"
|
||
v-perms="['tcm.diagnosis/readonlyDetail']"
|
||
>
|
||
查看
|
||
</el-button>
|
||
<el-button type="primary" link size="small" @click="handleEdit(row.id)" v-perms="['tcm.diagnosis/edit']">诊单</el-button>
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/kaifang']"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handlePrescription(row)"
|
||
>
|
||
{{ prescriptionActionLabel(row) }}
|
||
</el-button>
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/guahao']"
|
||
type="success"
|
||
link
|
||
size="small"
|
||
@click="handleAppointment(row)"
|
||
>预约</el-button>
|
||
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/edit']"
|
||
type="warning"
|
||
link
|
||
size="small"
|
||
@click="handleFillIdCard(row)"
|
||
>补全身份证</el-button>
|
||
<el-dropdown trigger="click" @command="(cmd) => handleRowAction(cmd, row)" placement="bottom-end">
|
||
<el-button type="info" link size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/assign'])" command="assign"><el-icon><User /></el-icon>指派</el-dropdown-item>
|
||
<el-dropdown-item
|
||
v-if="hasPermission(['tcm.diagnosis/assign']) && hasAssignedAssistant(row)"
|
||
command="cancelAssign"
|
||
>
|
||
<el-icon><Remove /></el-icon>取消指派
|
||
</el-dropdown-item>
|
||
<el-dropdown-item v-if="isAppointmentActiveForVideo(row) && hasPermission(['tcm.diagnosis/videoQr'])" command="videoQr"><el-icon><Picture /></el-icon>视频二维码</el-dropdown-item>
|
||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/guahao']) && isAppointmentActiveForVideo(row)" command="confirmQr"><el-icon><Picture /></el-icon>二维码</el-dropdown-item>
|
||
<el-dropdown-item
|
||
v-if="canCancelAppointmentFromDropdown(row) && hasPermission(['tcm.diagnosis/guahao'])"
|
||
command="cancelApt"
|
||
>
|
||
<el-icon><CircleClose /></el-icon>取消挂号
|
||
</el-dropdown-item>
|
||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/guahaoLogList'])" command="guahaoLogs"><el-icon><List /></el-icon>挂号日志</el-dropdown-item>
|
||
<el-dropdown-item command="order" v-if="hasPermission(['tcm.diagnosis/order'])"><el-icon><Document /></el-icon>创建订单</el-dropdown-item>
|
||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/delete'])" command="delete" divided><el-icon><Delete /></el-icon><span class="text-danger">删除</span></el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pagination-wrap">
|
||
<pagination v-model="pager" @change="getLists" />
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<edit-popup ref="editRef" @success="refreshListAfterPopupSave" />
|
||
<detail-popup ref="detailRef" />
|
||
<tcm-prescription ref="prescriptionRef" @success="onPrescriptionSuccess" />
|
||
<appointment-popup ref="appointmentRef" @success="handleAppointmentSaved" />
|
||
<assistant-watch-call-dialog
|
||
v-model="watchCallVisible"
|
||
:diagnosis-id="watchCallDiagnosisId"
|
||
@closed="getLists"
|
||
/>
|
||
|
||
<el-drawer v-model="guahaoLogDrawerVisible" title="挂号操作日志" size="460px" destroy-on-close @closed="resetGuahaoLogDrawer">
|
||
<div v-loading="guahaoLogLoading" class="min-h-[100px]">
|
||
<div v-if="guahaoLogContextRow" class="text-sm text-gray-600 mb-4 pb-3 border-b border-gray-100">
|
||
<span class="font-medium text-gray-800">{{ guahaoLogContextRow.patient_name }}</span>
|
||
<span class="mx-2 text-gray-300">|</span>
|
||
<span>诊单 #{{ guahaoLogContextRow.id }}</span>
|
||
</div>
|
||
<el-empty v-if="!guahaoLogLoading && guahaoLogRows.length === 0" description="暂无挂号/取消挂号操作记录" />
|
||
<el-timeline v-else-if="guahaoLogRows.length > 0">
|
||
<el-timeline-item
|
||
v-for="(log, idx) in guahaoLogRows"
|
||
:key="log.id ?? idx"
|
||
:timestamp="log.create_time_text || '—'"
|
||
placement="top"
|
||
>
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<el-tag size="small" type="info">{{ log.action_desc }}</el-tag>
|
||
<span class="text-sm font-medium text-gray-800">{{ formatGuahaoLogOperator(log) }}</span>
|
||
</div>
|
||
<div class="text-xs text-gray-600 mt-1.5 leading-relaxed">{{ log.summary }}</div>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
</div>
|
||
</el-drawer>
|
||
|
||
<!-- 小程序二维码弹窗 -->
|
||
<el-dialog
|
||
v-model="qrcodeDialogVisible"
|
||
:title="qrcodeDialogTitle"
|
||
width="400px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="flex flex-col items-center justify-center py-4">
|
||
<div v-if="qrcodeLoading" class="text-center">
|
||
<el-icon class="is-loading" :size="40">
|
||
<Loading />
|
||
</el-icon>
|
||
<div class="mt-4 text-gray-500">生成中...</div>
|
||
</div>
|
||
<div v-else-if="qrcodeUrl" class="text-center">
|
||
<img :src="qrcodeUrl" alt="小程序二维码" class="w-64 h-64 border border-gray-200 rounded" />
|
||
<div class="mt-4 text-sm text-gray-600">
|
||
<div>患者:{{ currentQRCodePatient?.patient_name }}</div>
|
||
<div class="mt-2">挂号时间:{{ qrcodeAppointmentTimeText }}</div>
|
||
<div class="mt-2">请使用企业微信扫描二维码,然后转发给患者</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="text-center text-gray-500">
|
||
生成失败,请重试
|
||
</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="assignDialogVisible"
|
||
:title="assignMode === 'batch' ? '批量指派医助' : '指派医助'"
|
||
width="500px"
|
||
@closed="resetAssignDialog"
|
||
>
|
||
<el-form :model="assignForm" label-width="100px">
|
||
<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 v-if="assignMode === 'batch'" label="已选择">
|
||
<div class="text-gray-500">{{ batchAssignIds.length }} 条诊单记录</div>
|
||
</el-form-item>
|
||
<el-form-item v-else label="诊单信息">
|
||
<div class="text-gray-500">
|
||
<div>患者:{{ currentRow?.patient_name }}</div>
|
||
<div>诊断日期:{{ currentRow?.diagnosis_date_text }}</div>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="assignDialogVisible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleConfirmAssign"
|
||
:loading="assignLoading"
|
||
>
|
||
确定指派
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 创建订单弹窗 -->
|
||
<el-dialog
|
||
v-model="createOrderVisible"
|
||
title="创建订单"
|
||
width="600px"
|
||
@close="resetOrderForm"
|
||
>
|
||
<el-form
|
||
ref="orderFormRef"
|
||
:model="orderForm"
|
||
:rules="orderRules"
|
||
label-width="100px"
|
||
>
|
||
<el-form-item label="患者">
|
||
<span class="font-medium">{{ 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 @click="createOrderVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitOrder" :loading="orderLoading">创建订单</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 补全身份证弹窗 -->
|
||
<el-dialog
|
||
v-model="fillIdCardVisible"
|
||
title="补全身份证号"
|
||
width="400px"
|
||
:close-on-click-modal="false"
|
||
@close="fillIdCardForm.id_card = ''"
|
||
>
|
||
<el-form ref="fillIdCardFormRef" :model="fillIdCardForm" :rules="fillIdCardRules" label-width="80px">
|
||
<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" @click="submitFillIdCard" :loading="fillIdCardLoading">提交(自动更新年龄)</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 企业微信聊天记录抽屉 -->
|
||
<el-drawer
|
||
v-model="wechatDrawerVisible"
|
||
:title="`企业微信记录 - ${wechatCurrentPatient?.patient_name || ''}`"
|
||
size="600px"
|
||
:destroy-on-close="true"
|
||
>
|
||
<!-- 患者与企微信息 -->
|
||
<div v-if="wechatContactInfo" class="mb-4">
|
||
<el-descriptions :column="2" border size="small">
|
||
<el-descriptions-item label="患者姓名">{{ wechatCurrentPatient?.patient_name }}</el-descriptions-item>
|
||
<el-descriptions-item label="手机号">{{ maskPhone(wechatContactInfo.phone) || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="企微联系人ID">
|
||
<span v-if="wechatContactInfo.external_userid">{{ wechatContactInfo.external_userid }}</span>
|
||
<el-tag v-else type="info" size="small">未关联</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="会话存档">
|
||
<el-tag v-if="wechatContactInfo.msgaudit_enabled" type="success" size="small">已开通</el-tag>
|
||
<el-tag v-else type="info" size="small">未配置</el-tag>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
<!-- 存档成员列表 -->
|
||
<div v-if="wechatContactInfo.permit_users?.length" class="mt-2">
|
||
<span class="text-sm text-gray-500 mr-2">存档成员:</span>
|
||
<el-tag
|
||
v-for="uid in wechatContactInfo.permit_users"
|
||
:key="uid"
|
||
size="small"
|
||
class="mr-1"
|
||
>
|
||
{{ uid }}
|
||
</el-tag>
|
||
</div>
|
||
<!-- 同意存档情况 -->
|
||
<div v-if="wechatContactInfo.msgaudit_agree?.length" class="mt-2">
|
||
<span class="text-sm text-gray-500 mr-2">存档同意:</span>
|
||
<el-tag
|
||
v-for="(info, idx) in wechatContactInfo.msgaudit_agree"
|
||
:key="idx"
|
||
:type="info.agree_status === 'Agree' ? 'success' : 'warning'"
|
||
size="small"
|
||
class="mr-1"
|
||
>
|
||
{{ info.userid }}: {{ info.agree_status === 'Agree' ? '已同意' : info.agree_status }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<el-divider />
|
||
|
||
<!-- 添加记录 -->
|
||
<div class="mb-4">
|
||
<div class="flex gap-2">
|
||
<el-input
|
||
v-model="wechatNewNote"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="添加沟通记录..."
|
||
class="flex-1"
|
||
/>
|
||
<el-button type="primary" @click="handleAddRecord" :loading="wechatAddLoading" class="self-end">
|
||
添加
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 记录列表 -->
|
||
<div v-loading="wechatRecordsLoading">
|
||
<div v-if="wechatRecords.length === 0" class="text-center text-gray-400 py-10">
|
||
暂无聊天记录
|
||
</div>
|
||
<div v-else class="record-list">
|
||
<div
|
||
v-for="record in wechatRecords"
|
||
:key="record.id"
|
||
class="record-item"
|
||
>
|
||
<div class="record-header">
|
||
<div class="flex items-center gap-2">
|
||
<el-tag :type="record.direction === 0 ? 'primary' : 'success'" size="small">
|
||
{{ record.direction === 0 ? '员工' : '客户' }}
|
||
</el-tag>
|
||
<span class="font-medium text-sm">
|
||
{{ 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="flex items-center gap-2">
|
||
<span class="text-xs text-gray-400">{{ formatTimestamp(record.chat_time) }}</span>
|
||
<el-button type="danger" link size="small" @click="handleDeleteRecord(record.id)">
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="record-content">{{ record.content }}</div>
|
||
<div v-if="record.media_url" class="mt-1">
|
||
<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="text-center mt-4">
|
||
<el-button link type="primary" @click="loadMoreRecords">加载更多</el-button>
|
||
</div>
|
||
</div>
|
||
</el-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="tcmDiagnosis">
|
||
import { tcmDiagnosisLists, tcmDiagnosisDelete, tcmDiagnosisAssign, tcmDiagnosisGuahaoLogList, getAssistants, generateMiniProgramQrcode, generateOrderQrcode, getWechatChatRecords, addWechatChatRecord, deleteWechatChatRecord, getWechatExternalContact, fillIdCard } from '@/api/tcm'
|
||
import { orderCreate } from '@/api/order'
|
||
import { cancelAppointment } from '@/api/doctor'
|
||
import { usePaging } from '@/hooks/usePaging'
|
||
import { getDictData } from '@/api/app'
|
||
import { getWeappConfig } from '@/api/channel/weapp'
|
||
import feedback from '@/utils/feedback'
|
||
import { hasPermission } from '@/utils/perm'
|
||
import { Loading, Warning, List, CircleCheck, Clock, ArrowDown, Picture, User, Document, Delete, CircleClose, Remove } from '@element-plus/icons-vue'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { computed, defineAsyncComponent, onMounted, onUnmounted, watch } 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 formatDateTime = (timestamp: number | string) => {
|
||
if (!timestamp) return '-'
|
||
// 如果是秒级时间戳(10位),转换为毫秒级
|
||
const ts = String(timestamp).length === 10 ? Number(timestamp) * 1000 : Number(timestamp)
|
||
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
|
||
}
|
||
|
||
// 手机号脱敏
|
||
const maskPhone = (phone: string) => {
|
||
if (!phone || phone.length < 11) return phone
|
||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||
}
|
||
|
||
// 身份证号脱敏
|
||
const maskIdCard = (idCard: string) => {
|
||
if (!idCard) return idCard
|
||
if (idCard.length === 15) {
|
||
return idCard.replace(/(\d{6})\d{5}(\d{4})/, '$1*****$2')
|
||
} else if (idCard.length === 18) {
|
||
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2')
|
||
}
|
||
return idCard
|
||
}
|
||
|
||
const formData = reactive({
|
||
keyword: '',
|
||
diagnosis_type: '',
|
||
syndrome_type: '',
|
||
assistant_id: '',
|
||
start_time: '',
|
||
end_time: '',
|
||
diagnosis_confirmed: '' as '' | '0' | '1',
|
||
appointment_date: '' as string,
|
||
has_appointment: '' as '' | '0' | '1',
|
||
pending_assign: '' as '' | '1',
|
||
/** 顶部「待预约」Tab:仅展示已建诊单且尚未挂号(无有效预约) */
|
||
pending_booking: '' as '' | '1',
|
||
/** 顶部「已完成」Tab:至少有一条挂号记录为已完成 status=3 */
|
||
completed_appointment: '' as '' | '1',
|
||
/** 待分配医助:按业务订单创建月份筛选 YYYY-MM,空表示不限月份 */
|
||
pending_assign_order_month: '' as string,
|
||
/** 待分配医助 Tab:多字段检索(后端扩展至身份证/诊单ID/备注等;不额外套数据权限) */
|
||
pending_assign_keyword: '' as string
|
||
})
|
||
|
||
const activeTab = ref('all')
|
||
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 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()
|
||
}
|
||
|
||
/** 待分配 + 宽搜时:请求只吃分页与关键词,不带其它 Tab/日期/筛选(与 buildTcmDiagnosisListRequestPayload 一致) */
|
||
function isPendingAssignWideKeywordParams(req: Record<string, unknown>): boolean {
|
||
const pa = req.pending_assign === '1' || req.pending_assign === 1
|
||
if (!pa) {
|
||
return false
|
||
}
|
||
const k1 = String(req.pending_assign_keyword ?? '').trim()
|
||
const k2 = String(req.keyword ?? '').trim()
|
||
return k1 !== '' || k2 !== ''
|
||
}
|
||
|
||
/** 待分配宽搜:仅传分页 + pending_assign + 关键词,丢弃其余查询参数 */
|
||
function buildTcmDiagnosisListRequestPayload(req: Record<string, unknown>): Record<string, unknown> {
|
||
if (!isPendingAssignWideKeywordParams(req)) {
|
||
return req
|
||
}
|
||
const k1 = String(req.pending_assign_keyword ?? '').trim()
|
||
const k2 = String(req.keyword ?? '').trim()
|
||
const kw = k1 !== '' ? k1 : k2
|
||
return {
|
||
page_no: req.page_no,
|
||
page_size: req.page_size,
|
||
pending_assign: 1,
|
||
pending_assign_keyword: kw
|
||
}
|
||
}
|
||
|
||
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
|
||
tcmDiagnosisLists(buildTcmDiagnosisListRequestPayload(req) as any)
|
||
|
||
/** 宽搜提交前清理界面上的其它检索,避免 Chip 仍显示「已选」但与实际请求不一致 */
|
||
function clearSecondaryFiltersWhenPendingAssignWideSearch() {
|
||
if (formData.pending_assign !== '1') {
|
||
return
|
||
}
|
||
const kw1 = (formData.pending_assign_keyword ?? '').trim()
|
||
const kw2 = (formData.keyword ?? '').trim()
|
||
if (kw1 === '' && kw2 === '') {
|
||
return
|
||
}
|
||
formData.appointment_date = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
formData.diagnosis_confirmed = ''
|
||
formData.has_appointment = ''
|
||
formData.diagnosis_type = ''
|
||
formData.syndrome_type = ''
|
||
formData.assistant_id = ''
|
||
formData.start_time = ''
|
||
formData.end_time = ''
|
||
formData.pending_assign_order_month = ''
|
||
activeTab.value = 'all'
|
||
if (kw1 !== '') {
|
||
formData.keyword = ''
|
||
}
|
||
}
|
||
|
||
const doSearch = () => {
|
||
clearSecondaryFiltersWhenPendingAssignWideSearch()
|
||
pager.page = 1
|
||
getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
// 重点:当天/明天/后天挂号 Tab
|
||
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 handleDateTabClick = async (value: string) => {
|
||
formData.pending_assign = ''
|
||
formData.pending_assign_order_month = ''
|
||
formData.pending_assign_keyword = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
formData.has_appointment = ''
|
||
formData.appointment_date = value
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
/** 已创建诊单、尚未挂号的记录(与第二行「未挂号」同条件,便于从 Tab 一键进入) */
|
||
const handlePendingBookingTabClick = async () => {
|
||
formData.pending_assign = ''
|
||
formData.pending_assign_order_month = ''
|
||
formData.pending_assign_keyword = ''
|
||
formData.pending_booking = '1'
|
||
formData.completed_appointment = ''
|
||
formData.appointment_date = ''
|
||
formData.has_appointment = '0'
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
/** 至少有一条挂号为「已完成」(后端 appointment.status=3) */
|
||
const handleCompletedVisitTabClick = async () => {
|
||
formData.pending_assign = ''
|
||
formData.pending_assign_order_month = ''
|
||
formData.pending_assign_keyword = ''
|
||
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 = ''
|
||
if (!formData.pending_assign_order_month) {
|
||
formData.pending_assign_order_month = dayjs().format('YYYY-MM')
|
||
}
|
||
pager.page = 1
|
||
await getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
/** 待分配:切换订单月份后刷新列表与角标 */
|
||
const onPendingAssignOrderMonthChange = async (val: string | null) => {
|
||
formData.pending_assign_order_month = val || ''
|
||
if (formData.pending_assign !== '1') return
|
||
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(
|
||
buildTcmDiagnosisListRequestPayload({
|
||
pending_assign: 1,
|
||
page_no: 1,
|
||
page_size: 1,
|
||
pending_assign_order_month: formData.pending_assign_order_month,
|
||
pending_assign_keyword: formData.pending_assign_keyword,
|
||
keyword: formData.keyword
|
||
}) as any
|
||
)
|
||
])
|
||
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)
|
||
}
|
||
}
|
||
|
||
/** 首屏优先主列表,错峰拉各日 tab 条数,减轻与 lists 并发争抢 */
|
||
function scheduleDeferredDateCounts() {
|
||
const run = () => fetchDateCounts()
|
||
if (typeof requestIdleCallback === 'function') {
|
||
requestIdleCallback(run, { timeout: 2500 })
|
||
} else {
|
||
setTimeout(run, 120)
|
||
}
|
||
}
|
||
|
||
const SILENT_LIST_POLL_MS = 10_000
|
||
let silentListPollTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
/** 定时静默刷新:不打断表格 loading,标签页隐藏时不请求 */
|
||
function silentRefreshListsAndDateCounts() {
|
||
if (typeof document !== 'undefined' && document.hidden) return
|
||
void getLists({ silent: true }).catch(() => {})
|
||
void fetchDateCounts()
|
||
}
|
||
|
||
// 日期快捷选项(computed 保证每日刷新)
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
const todayStr = computed(() => {
|
||
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 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())}`
|
||
})
|
||
|
||
// Tab 切换
|
||
const handleTabChange = (tabName: string | number) => {
|
||
formData.diagnosis_confirmed = ''
|
||
formData.has_appointment = ''
|
||
formData.appointment_date = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
if (tabName === 'unconfirmed') {
|
||
formData.diagnosis_confirmed = '0'
|
||
} else if (tabName === 'confirmed') {
|
||
formData.diagnosis_confirmed = '1'
|
||
} else if (tabName === 'no_appointment') {
|
||
formData.has_appointment = '0'
|
||
}
|
||
pager.page = 1
|
||
getLists()
|
||
}
|
||
|
||
// 更多下拉
|
||
const handleMoreCommand = (cmd: string) => {
|
||
if (cmd === 'reset') handleReset()
|
||
}
|
||
|
||
// 获取字典选项
|
||
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 (error) {
|
||
console.error('获取字典数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 获取字典标签
|
||
const getDictLabel = (options: any[], value: string) => {
|
||
const item = options.find(opt => opt.value === value)
|
||
return item ? item.name : value || '-'
|
||
}
|
||
|
||
// 获取医助名称
|
||
const getAssistantName = (assistantId: number | string) => {
|
||
// 如果没有assistant_id,返回"-"
|
||
if (!assistantId && assistantId !== 0) return '-'
|
||
|
||
// 确保类型匹配,转换为数字进行比较
|
||
const id = typeof assistantId === 'string' ? parseInt(assistantId) : assistantId
|
||
|
||
// 如果医助列表还没加载,返回"加载中..."
|
||
if (assistantOptions.value.length === 0) return '...'
|
||
|
||
const assistant = assistantOptions.value.find(item => {
|
||
const itemId = typeof item.id === 'string' ? parseInt(item.id) : item.id
|
||
return itemId === id
|
||
})
|
||
|
||
return assistant ? assistant.name : '-'
|
||
}
|
||
|
||
const { pager, getLists, resetParams, resetPage } = usePaging({
|
||
fetchFun: fetchTcmDiagnosisListsForPaging,
|
||
params: formData
|
||
})
|
||
|
||
/** 弹窗内保存后刷新列表:静默请求,避免 el-table v-loading 白蒙层盖住仍打开的抽屉 */
|
||
const refreshListAfterPopupSave = () => getLists({ silent: true })
|
||
|
||
// 重置
|
||
const handleReset = () => {
|
||
activeTab.value = 'all'
|
||
formData.keyword = ''
|
||
formData.diagnosis_type = ''
|
||
formData.syndrome_type = ''
|
||
formData.assistant_id = ''
|
||
formData.diagnosis_confirmed = ''
|
||
formData.appointment_date = ''
|
||
formData.has_appointment = ''
|
||
formData.pending_assign = ''
|
||
formData.pending_assign_order_month = ''
|
||
formData.pending_assign_keyword = ''
|
||
formData.pending_booking = ''
|
||
formData.completed_appointment = ''
|
||
pager.page = 1
|
||
getLists()
|
||
fetchDateCounts()
|
||
}
|
||
|
||
// 是否已确认诊单
|
||
const isDiagnosisConfirmed = (row: any) => {
|
||
const records = row.DiagnosisViewRecord || []
|
||
return records.length > 0 && records.some((r: any) => r.is_confirmed == 1)
|
||
}
|
||
|
||
/**
|
||
* 「未服务天数」着色规则(计划 T3):
|
||
* null/undefined → 灰;>=7 → 红;3-6 → 橙;<=2 → 绿
|
||
*/
|
||
const unservedDaysClass = (n: unknown) => {
|
||
if (n === null || n === undefined) return 'unserved-muted'
|
||
const num = Number(n)
|
||
if (!Number.isFinite(num)) return 'unserved-muted'
|
||
if (num >= 7) return 'unserved-red'
|
||
if (num >= 3) return 'unserved-orange'
|
||
return 'unserved-green'
|
||
}
|
||
|
||
// 行高亮:未确认诊单优先,其次未挂号
|
||
const getRowClassName = ({ row }: { row: any }) => {
|
||
if (!isDiagnosisConfirmed(row)) return 'row-unconfirmed'
|
||
if (!row.has_appointment) return 'row-no-appointment'
|
||
return ''
|
||
}
|
||
|
||
const editRef = ref()
|
||
const detailRef = ref()
|
||
const prescriptionRef = ref()
|
||
const appointmentRef = ref()
|
||
/** 从路由带 id 进入时,诊单弹窗为异步组件,需在 ref 就绪后再 open */
|
||
const pendingRouteDiagnosisId = ref<number | null>(null)
|
||
const userStore = useUserStore()
|
||
|
||
watch(
|
||
[editRef, pendingRouteDiagnosisId],
|
||
() => {
|
||
const id = pendingRouteDiagnosisId.value
|
||
const opener = editRef.value as { open?: (mode: string, diagnosisId: number) => void } | undefined
|
||
if (id == null || !opener?.open) return
|
||
opener.open('edit', id)
|
||
pendingRouteDiagnosisId.value = null
|
||
},
|
||
{ flush: 'post' }
|
||
)
|
||
|
||
const watchCallVisible = ref(false)
|
||
const watchCallDiagnosisId = ref(0)
|
||
|
||
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 openWatchCall = (row: any) => {
|
||
watchCallDiagnosisId.value = Number(row.id) || 0
|
||
watchCallVisible.value = true
|
||
}
|
||
|
||
type VideoCallHint = {
|
||
state: string
|
||
label: string
|
||
start_time?: number
|
||
end_time?: number
|
||
}
|
||
|
||
const canWatchCallEntry = (row: any) =>
|
||
isAssignedAssistant(row) && hasPermission(['tcm.diagnosis/watchCall'])
|
||
|
||
const watchCallState = (row: any) => (row.video_call_hint?.state as string) || 'none'
|
||
|
||
const watchCallLiveTooltip = (row: any) => {
|
||
const h = row.video_call_hint as VideoCallHint | undefined
|
||
const base = '点击进入实时房间(仅观看,不推流、不上麦)'
|
||
if (h?.start_time) {
|
||
return `${base}。开始时间:${formatDateTime(h.start_time)}`
|
||
}
|
||
return base
|
||
}
|
||
|
||
/** 通话进行中或已发起待同步房间时,都显示「进入旁观」入口(避免仅 live 才有按钮导致看不见) */
|
||
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 watchCallLiveTooltip(row)
|
||
}
|
||
|
||
const onWatchCallEntryClick = (row: any) => {
|
||
if (watchCallState(row) !== 'live') {
|
||
feedback.msgWarning('医生尚未接通或未同步房间号,请稍后再试')
|
||
return
|
||
}
|
||
openWatchCall(row)
|
||
}
|
||
|
||
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 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 = ''
|
||
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 (error) {
|
||
console.error('创建订单失败:', error)
|
||
} finally {
|
||
orderLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 小程序二维码相关
|
||
const qrcodeDialogVisible = ref(false)
|
||
const qrcodeLoading = ref(false)
|
||
const qrcodeUrl = ref('')
|
||
const currentQRCodePatient = ref<any>(null)
|
||
/** 二维码弹窗展示的挂号时间(与列表「挂号」列一致) */
|
||
const qrcodeAppointmentTimeText = computed(() => {
|
||
const p = currentQRCodePatient.value
|
||
if (!p) return '—'
|
||
if (!p.has_appointment) return '未挂号'
|
||
return p.appointment_time_text || '—'
|
||
})
|
||
const qrcodeDialogTitle = ref('小程序二维码')
|
||
const lastQRCodeType = ref<'video' | 'confirm'>('confirm')
|
||
|
||
// 批量指派相关
|
||
const selectedIds = ref<number[]>([])
|
||
/** 批量指派打开弹窗时的诊单 id 快照(弹窗内选医助可能触发表格重绘导致勾选丢失,提交以快照为准) */
|
||
const batchAssignIds = ref<number[]>([])
|
||
const assignDialogVisible = ref(false)
|
||
const assignLoading = ref(false)
|
||
const assignMode = ref<'batch' | 'single'>('batch')
|
||
const currentRow = ref<any>(null)
|
||
const assignForm = ref({
|
||
assistant_id: null as number | null
|
||
})
|
||
|
||
function resetAssignDialog() {
|
||
batchAssignIds.value = []
|
||
assignForm.value.assistant_id = null
|
||
}
|
||
|
||
const handleSelectionChange = (selection: any[]) => {
|
||
selectedIds.value = selection.map(item => item.id)
|
||
}
|
||
|
||
/** 当前行是否已指派医助(用于取消指派入口) */
|
||
const hasAssignedAssistant = (row: any) => {
|
||
const id = Number(row?.assistant_id ?? row?.assistant ?? 0)
|
||
return id > 0
|
||
}
|
||
|
||
/** 当前页勾选记录中是否存在已指派医助的诊单(批量取消指派) */
|
||
const canBatchCancelAssign = computed(() => {
|
||
if (!selectedIds.value.length) return false
|
||
const set = new Set(selectedIds.value)
|
||
return pager.lists.some((r: any) => set.has(r.id) && hasAssignedAssistant(r))
|
||
})
|
||
|
||
// 单独指派
|
||
const handleSingleAssign = (row: any) => {
|
||
assignMode.value = 'single'
|
||
currentRow.value = row
|
||
batchAssignIds.value = []
|
||
// 确保类型一致,转换为数字
|
||
assignForm.value.assistant_id = row.assistant_id ? Number(row.assistant_id) : null
|
||
assignDialogVisible.value = true
|
||
}
|
||
|
||
// 批量指派
|
||
const handleBatchAssign = () => {
|
||
if (selectedIds.value.length === 0) {
|
||
feedback.msgWarning('请先选择要指派的诊单')
|
||
return
|
||
}
|
||
assignMode.value = 'batch'
|
||
currentRow.value = null
|
||
assignForm.value.assistant_id = null
|
||
batchAssignIds.value = [...selectedIds.value]
|
||
assignDialogVisible.value = true
|
||
}
|
||
|
||
const handleConfirmAssign = async () => {
|
||
if (!assignForm.value.assistant_id) {
|
||
feedback.msgWarning('请选择医助')
|
||
return
|
||
}
|
||
|
||
assignLoading.value = true
|
||
try {
|
||
if (assignMode.value === 'single') {
|
||
// 单独指派
|
||
await tcmDiagnosisAssign({
|
||
id: currentRow.value.id,
|
||
assistant_id: assignForm.value.assistant_id
|
||
})
|
||
} else {
|
||
// 批量指派:使用打开弹窗时的 id 快照(避免弹窗内操作导致表格勾选被清空后提交为空)
|
||
const ids = batchAssignIds.value
|
||
if (!ids.length) {
|
||
feedback.msgWarning('没有可指派的诊单,请关闭后重新勾选')
|
||
return
|
||
}
|
||
for (const id of ids) {
|
||
await tcmDiagnosisAssign({
|
||
id,
|
||
assistant_id: assignForm.value.assistant_id!,
|
||
})
|
||
}
|
||
selectedIds.value = []
|
||
batchAssignIds.value = []
|
||
}
|
||
|
||
assignDialogVisible.value = false
|
||
getLists()
|
||
} catch (error) {
|
||
console.error('指派失败:', error)
|
||
} finally {
|
||
assignLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 取消指派:将 assistant_id 置为 0(与后端 assign 一致) */
|
||
const handleCancelAssign = async (row: any) => {
|
||
if (!row?.id || !hasAssignedAssistant(row)) {
|
||
feedback.msgWarning('该诊单未指派医助')
|
||
return
|
||
}
|
||
await feedback.confirm('确定取消该诊单的医助指派吗?')
|
||
await tcmDiagnosisAssign({ id: row.id, assistant_id: 0 })
|
||
getLists()
|
||
}
|
||
|
||
const handleBatchCancelAssign = async () => {
|
||
if (!selectedIds.value.length) {
|
||
feedback.msgWarning('请先选择诊单')
|
||
return
|
||
}
|
||
const set = new Set(selectedIds.value)
|
||
const rows = pager.lists.filter((r: any) => set.has(r.id) && hasAssignedAssistant(r))
|
||
if (!rows.length) {
|
||
feedback.msgWarning('所选诊单中没有已指派医助的记录')
|
||
return
|
||
}
|
||
await feedback.confirm(`确定取消 ${rows.length} 条诊单的医助指派吗?`)
|
||
await Promise.all(rows.map((r: any) => tcmDiagnosisAssign({ id: r.id, assistant_id: 0 })))
|
||
selectedIds.value = []
|
||
getLists()
|
||
}
|
||
|
||
const handleAdd = () => {
|
||
editRef.value.open('add')
|
||
}
|
||
|
||
const handleEdit = (id: number) => {
|
||
editRef.value.open('edit', id)
|
||
}
|
||
|
||
/** 与预约列表开方一致:已通过且未作废 → 查看,否则开方。(待分配医助 Tab 一律显示「开方」,避免误解为只读) */
|
||
function prescriptionActionLabel(row: any) {
|
||
if (formData.pending_assign === '1') {
|
||
return '开方'
|
||
}
|
||
if (
|
||
Number(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 handleDetail = (id: number) => {
|
||
detailRef.value.open(id)
|
||
}
|
||
|
||
const handleDelete = async (id: number) => {
|
||
await feedback.confirm('确定要删除该诊单吗?')
|
||
await tcmDiagnosisDelete({ id })
|
||
getLists()
|
||
}
|
||
|
||
// 行操作下拉
|
||
const handleRowAction = (cmd: string, row: any) => {
|
||
switch (cmd) {
|
||
case 'assign': handleSingleAssign(row); break
|
||
case 'cancelAssign': handleCancelAssign(row); break
|
||
case 'videoQr':
|
||
if (!isAppointmentActiveForVideo(row)) {
|
||
feedback.msgWarning('仅「已预约」状态可生成视频二维码')
|
||
return
|
||
}
|
||
handleVideoQRCode(row)
|
||
break
|
||
case 'confirmQr': handleMiniProgramQRCode(row); break
|
||
case 'cancelApt': handleCancelAppointment(row); break
|
||
case 'guahaoLogs': openGuahaoLogDrawer(row); break
|
||
case 'order': handleCreateOrder(row); break
|
||
case 'delete': handleDelete(row.id); break
|
||
}
|
||
}
|
||
|
||
/** 挂号状态:1=已预约 3=已完成 4=已过号(与后端一致) */
|
||
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 {
|
||
'apt-item-done': s === 3,
|
||
'apt-item-missed': s === 4,
|
||
'apt-item-active': s === 1
|
||
}
|
||
}
|
||
|
||
const appointmentCellClasses = (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
|
||
}
|
||
}
|
||
|
||
/** 仅已预约(1)可进视频/小程序码 */
|
||
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))
|
||
}
|
||
|
||
/** 单条挂号记录是否允许取消(已预约 1 / 已过号 4) */
|
||
const canCancelAppointmentItem = (apt: any) => {
|
||
const s = Number(apt?.status)
|
||
return !!(apt?.id && (s === 1 || s === 4))
|
||
}
|
||
|
||
/**
|
||
* 「更多」里行级取消:仅当列表只展示一条挂号时可点,避免多条时与主键 appointment_id 不对齐
|
||
* 多条时在「挂号」列每条旁单独取消
|
||
*/
|
||
const canCancelAppointmentFromDropdown = (row: any) => {
|
||
if (!canCancelAppointmentRow(row)) return false
|
||
const rows = appointmentRows(row)
|
||
return rows.length <= 1
|
||
}
|
||
|
||
// 取消挂号(行级,唯一一条时)
|
||
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
|
||
}
|
||
const rows = appointmentRows(row)
|
||
const target = rows.length === 1 ? rows[0] : null
|
||
const aptId = target?.id && canCancelAppointmentItem(target) ? target.id : row.appointment_id
|
||
try {
|
||
await feedback.confirm(`确定要取消患者「${row.patient_name}」的挂号吗?`)
|
||
await cancelAppointment({ id: aptId })
|
||
feedback.msgSuccess('取消挂号成功')
|
||
getLists()
|
||
fetchDateCounts()
|
||
if (guahaoLogDrawerVisible.value && guahaoLogContextRow.value?.id === row.id) {
|
||
await fetchGuahaoLogs(row.id)
|
||
}
|
||
} catch (e: any) {
|
||
if (e !== 'cancel') feedback.msgError(e?.msg || '取消挂号失败')
|
||
}
|
||
}
|
||
|
||
/** 取消指定挂号(多条展示时用) */
|
||
const handleCancelAppointmentItem = async (row: any, apt: any) => {
|
||
if (!canCancelAppointmentItem(apt)) {
|
||
const s = Number(apt?.status)
|
||
if (s === 3) {
|
||
feedback.msgWarning('已完成就诊,无法取消挂号')
|
||
} else {
|
||
feedback.msgWarning('该挂号无法取消')
|
||
}
|
||
return
|
||
}
|
||
const doc = apt.doctor_name || '—'
|
||
const t = apt.time_text || '—'
|
||
try {
|
||
await feedback.confirm(`确定取消「${doc}」${t} 的挂号吗?(患者:${row.patient_name || '—'})`)
|
||
await cancelAppointment({ id: apt.id })
|
||
feedback.msgSuccess('取消挂号成功')
|
||
getLists()
|
||
fetchDateCounts()
|
||
if (guahaoLogDrawerVisible.value && guahaoLogContextRow.value?.id === row.id) {
|
||
await fetchGuahaoLogs(row.id)
|
||
}
|
||
} catch (e: any) {
|
||
if (e !== 'cancel') feedback.msgError(e?.msg || '取消挂号失败')
|
||
}
|
||
}
|
||
|
||
const guahaoLogDrawerVisible = ref(false)
|
||
const guahaoLogLoading = ref(false)
|
||
const guahaoLogRows = ref<any[]>([])
|
||
const guahaoLogContextRow = ref<any>(null)
|
||
|
||
function resetGuahaoLogDrawer() {
|
||
guahaoLogRows.value = []
|
||
guahaoLogContextRow.value = null
|
||
}
|
||
|
||
function formatGuahaoLogOperator(log: any) {
|
||
const name = String(log?.admin_name || '').trim()
|
||
const aid = Number(log?.admin_id)
|
||
if (name) return name
|
||
if (aid > 0) return `管理员 #${aid}`
|
||
return '—'
|
||
}
|
||
|
||
async function fetchGuahaoLogs(diagnosisId: number) {
|
||
guahaoLogLoading.value = true
|
||
try {
|
||
const res: any = await tcmDiagnosisGuahaoLogList({ id: diagnosisId })
|
||
guahaoLogRows.value = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []
|
||
} catch {
|
||
guahaoLogRows.value = []
|
||
} finally {
|
||
guahaoLogLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openGuahaoLogDrawer(row: any) {
|
||
if (!row?.id) {
|
||
feedback.msgWarning('诊单信息不完整')
|
||
return
|
||
}
|
||
guahaoLogContextRow.value = row
|
||
guahaoLogDrawerVisible.value = true
|
||
await fetchGuahaoLogs(Number(row.id))
|
||
}
|
||
|
||
/** 预约抽屉保存成功后:静默刷新列表 + 顶部计数;若挂号日志抽屉打开则同步日志 */
|
||
async function handleAppointmentSaved() {
|
||
await getLists({ silent: true })
|
||
fetchDateCounts()
|
||
if (guahaoLogDrawerVisible.value && guahaoLogContextRow.value?.id) {
|
||
await fetchGuahaoLogs(Number(guahaoLogContextRow.value.id))
|
||
}
|
||
}
|
||
|
||
// 患者挂号
|
||
const handleAppointment = (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('患者信息不完整')
|
||
return
|
||
}
|
||
|
||
appointmentRef.value?.open(row)
|
||
}
|
||
|
||
// 补全身份证
|
||
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
|
||
}
|
||
}
|
||
|
||
// 生成视频二维码(跳转登录页)- 仅已预约(1)可生成
|
||
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 (error: any) {
|
||
feedback.msgError(error?.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 currentUser = userStore.userInfo
|
||
|
||
// 调用生成二维码接口
|
||
const result = await generateMiniProgramQrcode({
|
||
diagnosis_id: row.id,
|
||
doctor_id: row.appointment_doctor_id,
|
||
patient_id: row.patient_id,
|
||
share_user_id: currentUser?.id || ''
|
||
})
|
||
|
||
if (result?.qrcode_url) {
|
||
qrcodeUrl.value = result.qrcode_url
|
||
} else {
|
||
feedback.msgError('二维码生成失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('生成小程序二维码失败:', error)
|
||
feedback.msgError(error?.msg || '生成二维码失败,请重试')
|
||
} finally {
|
||
qrcodeLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 重新生成二维码
|
||
const handleRegenerateQRCode = () => {
|
||
if (currentQRCodePatient.value) {
|
||
lastQRCodeType.value === 'video'
|
||
? handleVideoQRCode(currentQRCodePatient.value)
|
||
: handleMiniProgramQRCode(currentQRCodePatient.value)
|
||
}
|
||
}
|
||
|
||
// ========== 企业微信聊天记录 ==========
|
||
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 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 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 route = useRoute()
|
||
const router = useRouter()
|
||
|
||
/** 跳到只读病例页(T1+T2):双击行 / 点「查看」按钮均触发 */
|
||
const goReadonly = (row: any) => {
|
||
// 没分配 readonlyDetail 权限的管理员:双击行也不跳转(按钮已用 v-perms 隐藏)
|
||
if (!hasPermission(['tcm.diagnosis/readonlyDetail'])) return
|
||
const id = Number(row?.id)
|
||
if (!id || id <= 0) return
|
||
router.push({ path: '/tcm/diagnosis-readonly', query: { id: String(id) } })
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 默认展示当天挂号,突出重点
|
||
formData.appointment_date = todayStr.value
|
||
// 字典与列表并行,避免 await 字典阻塞首屏列表(LCP)
|
||
getDictOptions()
|
||
getLists()
|
||
scheduleDeferredDateCounts()
|
||
|
||
silentListPollTimer = setInterval(silentRefreshListsAndDateCounts, SILENT_LIST_POLL_MS)
|
||
|
||
const raw = route.query.id
|
||
if (raw != null && raw !== '') {
|
||
const num = Number(raw)
|
||
if (!Number.isNaN(num)) pendingRouteDiagnosisId.value = num
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (silentListPollTimer != null) {
|
||
clearInterval(silentListPollTimer)
|
||
silentListPollTimer = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.tcm-diagnosis {
|
||
|
||
min-height: 100%;
|
||
background: var(--el-bg-color-page);
|
||
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.filter-card {
|
||
:deep(.el-card__body) {
|
||
padding: 12px 16px;
|
||
}
|
||
}
|
||
|
||
.filter-main {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.date-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
|
||
.chip {
|
||
padding: 6px 12px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-regular);
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
em {
|
||
font-style: normal;
|
||
margin-left: 4px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
&:hover {
|
||
background: var(--el-fill-color);
|
||
}
|
||
|
||
&.active {
|
||
color: #fff;
|
||
background: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
.chip-pending-booking {
|
||
margin-left: 8px;
|
||
color: var(--el-color-info);
|
||
background: var(--el-color-info-light-9);
|
||
|
||
&:hover {
|
||
background: var(--el-color-info-light-8);
|
||
}
|
||
|
||
&.active {
|
||
color: #fff;
|
||
background: var(--el-color-info);
|
||
}
|
||
}
|
||
|
||
.chip-completed-visit {
|
||
margin-left: 8px;
|
||
color: var(--el-color-success);
|
||
background: var(--el-color-success-light-9);
|
||
|
||
&:hover {
|
||
background: var(--el-color-success-light-8);
|
||
}
|
||
|
||
&.active {
|
||
color: #fff;
|
||
background: var(--el-color-success);
|
||
}
|
||
}
|
||
|
||
.chip-pending {
|
||
margin-left: 8px;
|
||
color: var(--el-color-warning);
|
||
background: var(--el-color-warning-light-9);
|
||
|
||
&:hover {
|
||
background: var(--el-color-warning-light-8);
|
||
}
|
||
|
||
&.active {
|
||
color: #fff;
|
||
background: var(--el-color-warning);
|
||
}
|
||
}
|
||
|
||
.pending-assign-wrap {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-left: 8px;
|
||
|
||
.chip-pending {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
.pending-assign-month {
|
||
width: 128px;
|
||
}
|
||
|
||
.pending-assign-search {
|
||
width: 220px;
|
||
max-width: 42vw;
|
||
}
|
||
}
|
||
|
||
.search-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
|
||
.search-input {
|
||
width: 160px;
|
||
}
|
||
}
|
||
|
||
.filter-chips {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding-top: 8px;
|
||
margin-top: 8px;
|
||
border-top: 1px solid var(--el-border-color-lighter);
|
||
|
||
.chip-label {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
margin-right: 2px;
|
||
}
|
||
|
||
.chip {
|
||
padding: 3px 10px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
background: transparent;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
color: var(--el-color-primary);
|
||
background: var(--el-color-primary-light-9);
|
||
}
|
||
|
||
&.active {
|
||
color: var(--el-color-primary);
|
||
font-weight: 500;
|
||
background: var(--el-color-primary-light-9);
|
||
}
|
||
}
|
||
|
||
.chip-sm {
|
||
padding: 4px 10px;
|
||
}
|
||
|
||
.chip-divider {
|
||
width: 1px;
|
||
height: 14px;
|
||
background: var(--el-border-color-lighter);
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.more-btn {
|
||
margin-left: 8px;
|
||
|
||
:deep(.el-icon.rotate-180) {
|
||
transform: rotate(180deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.filter-more {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed var(--el-border-color-lighter);
|
||
}
|
||
|
||
.list-card {
|
||
:deep(.el-card__body) {
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
.list-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 16px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
|
||
.list-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.list-selected {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
|
||
.table-wrap {
|
||
padding: 10px 16px 0;
|
||
}
|
||
|
||
.pagination-wrap {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 10px 16px 16px;
|
||
}
|
||
|
||
.patient-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.patient-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, var(--el-color-primary-light-5), var(--el-color-primary-light-7));
|
||
color: var(--el-color-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.patient-info {
|
||
min-width: 0;
|
||
}
|
||
|
||
.patient-name {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: var(--el-text-color-primary);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.patient-meta {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
margin-top: 2px;
|
||
}
|
||
}
|
||
|
||
.patient-meta-col {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.appointment-cell {
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
min-height: 36px;
|
||
|
||
.apt-item + .apt-item {
|
||
margin-top: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed var(--el-border-color-lighter);
|
||
}
|
||
|
||
.apt-item {
|
||
.apt-badge {
|
||
display: inline-block;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.apt-doctor {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.apt-time {
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.apt-actions {
|
||
margin-top: 4px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
&.apt-item-active {
|
||
.apt-badge {
|
||
color: #3a4acc;
|
||
background: rgba(74, 93, 255, 0.15);
|
||
}
|
||
|
||
.apt-time {
|
||
color: #4A5DFF;
|
||
}
|
||
}
|
||
|
||
&.apt-item-done {
|
||
.apt-badge {
|
||
color: var(--el-color-info-dark-2);
|
||
background: var(--el-color-info-light-7);
|
||
}
|
||
|
||
.apt-time {
|
||
color: var(--el-color-info);
|
||
}
|
||
}
|
||
|
||
&.apt-item-missed {
|
||
.apt-badge {
|
||
color: var(--el-color-warning-dark-2);
|
||
background: var(--el-color-warning-light-7);
|
||
}
|
||
|
||
.apt-time {
|
||
color: var(--el-color-warning);
|
||
}
|
||
}
|
||
}
|
||
|
||
&.has-apt {
|
||
background: linear-gradient(135deg, rgba(74, 93, 255, 0.12), rgba(74, 93, 255, 0.06));
|
||
border: 1px solid rgba(74, 93, 255, 0.3);
|
||
}
|
||
|
||
&.apt-row-missed {
|
||
background: linear-gradient(135deg, rgba(var(--el-color-warning-rgb), 0.12), rgba(var(--el-color-warning-rgb), 0.05));
|
||
border-color: rgba(var(--el-color-warning-rgb), 0.35);
|
||
}
|
||
|
||
&.apt-row-done {
|
||
background: linear-gradient(135deg, rgba(var(--el-color-info-rgb), 0.1), rgba(var(--el-color-info-rgb), 0.04));
|
||
border-color: rgba(var(--el-color-info-rgb), 0.28);
|
||
}
|
||
|
||
.apt-none {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
}
|
||
|
||
.status-confirmed {
|
||
font-size: 13px;
|
||
color: var(--el-color-success);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-unconfirmed {
|
||
font-size: 13px;
|
||
color: var(--el-color-warning);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.status-prescribed {
|
||
font-size: 13px;
|
||
color: #4A5DFF;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-unprescribed {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
/* 未服务天数(T3):null=灰;>=7=红;3-6=橙;<=2=绿 */
|
||
.unserved-days {
|
||
display: inline-block;
|
||
min-width: 28px;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
font-variant-numeric: tabular-nums;
|
||
cursor: default;
|
||
}
|
||
|
||
.unserved-muted {
|
||
color: var(--el-text-color-placeholder);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.unserved-green {
|
||
color: #16a34a;
|
||
}
|
||
|
||
.unserved-orange {
|
||
color: #ea580c;
|
||
}
|
||
|
||
.unserved-red {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.diagnosis-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
.diagnosis-date {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.diagnosis-extra {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
|
||
.followup-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
line-height: 1.35;
|
||
|
||
.followup-time {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.followup-doctor {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.followup-void-tag {
|
||
align-self: flex-start;
|
||
}
|
||
}
|
||
|
||
.followup-none {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.assistant-cell {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.action-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.video-watch-col {
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
padding: 0 2px;
|
||
}
|
||
|
||
/* el-tooltip 单个子节点为行内按钮时,包一层避免触发区域为 0 或错位 */
|
||
.video-watch-trigger {
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.video-watch-muted {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.text-danger {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.diagnosis-table {
|
||
:deep(.el-table__header th) {
|
||
background: var(--el-fill-color-light);
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
:deep(.el-table__row) {
|
||
cursor: default;
|
||
}
|
||
:deep(.el-table__body .el-table__cell) {
|
||
padding: 8px 0;
|
||
}
|
||
:deep(.el-table__row.row-unconfirmed) {
|
||
border-left: 3px solid var(--el-color-warning);
|
||
background: linear-gradient(90deg, rgba(var(--el-color-warning-rgb), 0.06) 0%, transparent 100%) !important;
|
||
}
|
||
:deep(.el-table__row.row-unconfirmed:hover > td) {
|
||
background: linear-gradient(90deg, rgba(var(--el-color-warning-rgb), 0.1) 0%, transparent 100%) !important;
|
||
}
|
||
:deep(.el-table__row.row-no-appointment) {
|
||
border-left: 3px solid var(--el-color-info);
|
||
background: linear-gradient(90deg, rgba(var(--el-color-info-rgb), 0.04) 0%, transparent 100%) !important;
|
||
}
|
||
:deep(.el-table__row.row-no-appointment:hover > td) {
|
||
background: linear-gradient(90deg, rgba(var(--el-color-info-rgb), 0.08) 0%, transparent 100%) !important;
|
||
}
|
||
}
|
||
|
||
.record-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.record-item {
|
||
background: #f9fafb;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.record-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.record-content {
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
</style>
|