1304 lines
47 KiB
Vue
1304 lines
47 KiB
Vue
<template>
|
||
<div class="appointment-list">
|
||
<el-card class="list-card !border-none" shadow="never">
|
||
<!-- 单行工具栏:Tab(主) + 搜索(辅) -->
|
||
<div class="list-toolbar">
|
||
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="list-tabs">
|
||
<el-tab-pane name="1">
|
||
<template #label>
|
||
<span class="tab-label tab-priority"><el-icon><Clock /></el-icon>待接诊<el-badge v-if="statusCount[1]" :value="statusCount[1]" class="tab-badge tab-badge-success" /></span>
|
||
</template>
|
||
</el-tab-pane>
|
||
<el-tab-pane name="all">
|
||
<template #label>
|
||
<span class="tab-label"><el-icon><List /></el-icon>全部</span>
|
||
</template>
|
||
</el-tab-pane>
|
||
<el-tab-pane name="4">
|
||
<template #label>
|
||
<span class="tab-label"><el-icon><Warning /></el-icon>已过号<el-badge v-if="statusCount[4]" :value="statusCount[4]" class="tab-badge tab-badge-danger" /></span>
|
||
</template>
|
||
</el-tab-pane>
|
||
<el-tab-pane name="2">
|
||
<template #label>
|
||
<span class="tab-label"><el-icon><CircleClose /></el-icon>已取消<el-badge v-if="statusCount[2]" :value="statusCount[2]" class="tab-badge tab-badge-info" /></span>
|
||
</template>
|
||
</el-tab-pane>
|
||
<el-tab-pane name="3">
|
||
<template #label>
|
||
<span class="tab-label"><el-icon><CircleCheck /></el-icon>已完成<el-badge v-if="statusCount[3]" :value="statusCount[3]" class="tab-badge tab-badge-primary" /></span>
|
||
</template>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<div class="toolbar-actions">
|
||
<el-input
|
||
v-model="formData.patient_name"
|
||
placeholder="患者姓名"
|
||
clearable
|
||
size="small"
|
||
class="action-input"
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
<el-button type="primary" size="small" @click="handleSearch">查询</el-button>
|
||
<el-dropdown trigger="click" @command="handleMoreCommand">
|
||
<el-button size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item v-if="isAdmin" class="dropdown-item-with-input" @click.stop>
|
||
<span class="dropdown-label">医生</span>
|
||
<el-input v-model="formData.doctor_name" placeholder="医生" size="small" class="dropdown-input" @click.stop />
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="customDate" divided>
|
||
<el-icon><Calendar /></el-icon>
|
||
自定义日期
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="reset">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
重置
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日期 + 确认诊单 快捷筛选 -->
|
||
<div class="filter-bar">
|
||
<div class="filter-group">
|
||
<span class="filter-label">日期</span>
|
||
<el-radio-group v-model="formData.date_preset" size="small" @change="handleDatePresetChange" class="filter-radio">
|
||
<el-radio-button value="today">当天挂号</el-radio-button>
|
||
<el-radio-button value="tomorrow">明天挂号</el-radio-button>
|
||
<el-radio-button value="day_after">后天挂号</el-radio-button>
|
||
<el-radio-button value="">不限</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<div class="filter-group">
|
||
<span class="filter-label">确认诊单</span>
|
||
<el-radio-group v-model="formData.diagnosis_confirmed" size="small" @change="handleDiagnosisConfirmedChange" class="filter-radio">
|
||
<el-radio-button value="">全部</el-radio-button>
|
||
<el-radio-button value="1">已确认</el-radio-button>
|
||
<el-radio-button value="0">未确认</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog
|
||
v-model="dateCustomVisible"
|
||
title="自定义日期"
|
||
width="400px"
|
||
append-to-body
|
||
:close-on-click-modal="true"
|
||
>
|
||
<daterange-picker
|
||
v-model:startTime="formData.start_date"
|
||
v-model:endTime="formData.end_date"
|
||
/>
|
||
<template #footer>
|
||
<el-button @click="dateCustomVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="applyCustomDate">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<div class="table-wrap">
|
||
<el-table
|
||
v-loading="pager.loading"
|
||
:data="pager.lists"
|
||
size="default"
|
||
class="appointment-table"
|
||
stripe
|
||
:row-class-name="getRowClassName"
|
||
>
|
||
<el-table-column label="ID" prop="id" width="72" align="center" />
|
||
|
||
<el-table-column label="患者信息" min-width="140">
|
||
<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 class="patient-phone">{{ row.patient_phone }}</div>
|
||
<div class="patient-extra">
|
||
{{ [row.gender === 1 ? '男' : row.gender === 0 ? '女' : '', row.age != null ? row.age + '岁' : '', row.height != null ? row.height + 'cm' : '', row.weight != null ? row.weight + 'kg' : ''].filter(Boolean).join(' ') || '-' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="医生" prop="doctor_name" width="90" show-overflow-tooltip />
|
||
|
||
<el-table-column label="预约时间" min-width="120">
|
||
<template #default="{ row }">
|
||
<div class="datetime-cell">
|
||
<div class="date-text">{{ row.appointment_date }}</div>
|
||
<div class="time-text">{{ row.appointment_time }}</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
|
||
|
||
<el-table-column label="预约类型" prop="appointment_type_desc" width="100" show-overflow-tooltip />
|
||
|
||
<el-table-column label="确认诊单" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.diagnosis_confirmed" type="success" size="small" effect="plain">已确认</el-tag>
|
||
<el-tag v-else type="warning" size="small" effect="plain">未确认</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="开方" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.has_prescription" type="success" size="small" effect="plain">已开方</el-tag>
|
||
<el-tag v-else type="info" size="small" effect="plain">未开方</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="row.status === 1 ? 'success' : row.status === 2 ? 'info' : row.status === 3 ? 'primary' : 'danger'"
|
||
size="small"
|
||
effect="light"
|
||
round
|
||
>
|
||
{{ row.status_desc }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
|
||
|
||
|
||
<el-table-column label="操作" width="380" fixed="right" align="left">
|
||
<template #default="{ row }">
|
||
<div class="action-btns">
|
||
<!-- <el-button
|
||
v-perms="['doctor.appointment/detail']"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handleDetail(row)"
|
||
>
|
||
详情
|
||
</el-button> -->
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/edit']"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handleEdit(row)"
|
||
>
|
||
编辑患者
|
||
</el-button>
|
||
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/videoQr']"
|
||
type="warning"
|
||
link
|
||
size="small"
|
||
@click="handleMiniProgramQRCode(row)"
|
||
class="action-qrcode"
|
||
>
|
||
<el-icon><Picture /></el-icon>
|
||
视频二维码
|
||
</el-button>
|
||
<el-button
|
||
v-perms="['doctor.appointment/prescription']"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handleChat(row)"
|
||
>
|
||
聊天
|
||
</el-button>
|
||
<el-button
|
||
v-perms="['doctor.appointment/complete']"
|
||
type="success"
|
||
link
|
||
size="small"
|
||
@click="handleComplete(row)"
|
||
>
|
||
完成
|
||
</el-button>
|
||
<el-button
|
||
v-perms="['tcm.diagnosis/kaifang']"
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handlePrescription(row)"
|
||
>
|
||
开方
|
||
</el-button>
|
||
|
||
<el-dropdown trigger="click" @command="(cmd) => handleAction(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-perms="['tcm.diagnosis/kaifang']"
|
||
command="viewCase"
|
||
>
|
||
<el-icon><Document /></el-icon>
|
||
病历
|
||
</el-dropdown-item>
|
||
<el-dropdown-item
|
||
v-if="row.status === 1"
|
||
v-perms="['doctor.appointment/cancel']"
|
||
command="cancel"
|
||
divided
|
||
>
|
||
<el-icon><CircleClose /></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="flex justify-end py-4 px-4">
|
||
<pagination v-model="pager" @change="getLists" />
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 详情弹窗 -->
|
||
<el-dialog
|
||
v-model="detailVisible"
|
||
title="挂号详情"
|
||
width="720px"
|
||
class="detail-dialog"
|
||
destroy-on-close
|
||
>
|
||
<div v-if="detailData" class="detail-content">
|
||
<!-- 预约信息 -->
|
||
<div class="detail-section">
|
||
<div class="detail-section-title">
|
||
<el-icon><Calendar /></el-icon>
|
||
预约信息
|
||
</div>
|
||
<div class="detail-grid">
|
||
<div class="detail-item">
|
||
<span class="detail-label">挂号ID</span>
|
||
<span class="detail-value">{{ detailData.id }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">状态</span>
|
||
<el-tag
|
||
:type="detailData.status === 1 ? 'success' : detailData.status === 2 ? 'info' : detailData.status === 3 ? 'primary' : 'danger'"
|
||
size="small"
|
||
effect="light"
|
||
>
|
||
{{ detailData.status_desc }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">医生</span>
|
||
<span class="detail-value">{{ detailData.doctor_name }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">预约时间</span>
|
||
<span class="detail-value">{{ detailData.appointment_date }} {{ detailData.appointment_time }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">时段</span>
|
||
<span class="detail-value">{{ detailData.period === 'morning' ? '上午' : '下午' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">预约类型</span>
|
||
<span class="detail-value">{{ detailData.appointment_type_desc }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 患者基本信息(含编辑患者中的字段) -->
|
||
<div class="detail-section">
|
||
<div class="detail-section-title">
|
||
<el-icon><User /></el-icon>
|
||
患者信息
|
||
</div>
|
||
<div class="detail-grid">
|
||
<div class="detail-item">
|
||
<span class="detail-label">姓名</span>
|
||
<span class="detail-value">{{ detailData.patient_name || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">手机号</span>
|
||
<span class="detail-value">{{ detailData.phone || detailData.patient_phone || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">身份证号</span>
|
||
<span class="detail-value">{{ detailData.id_card || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">性别</span>
|
||
<span class="detail-value">{{ detailData.gender === 1 ? '男' : detailData.gender === 0 ? '女' : '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">年龄</span>
|
||
<span class="detail-value">{{ detailData.age != null ? detailData.age + '岁' : '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">诊断日期</span>
|
||
<span class="detail-value">{{ detailData.diagnosis_date || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">婚姻状态</span>
|
||
<span class="detail-value">{{ detailData.marital_status === 0 ? '未婚' : detailData.marital_status === 1 ? '已婚' : detailData.marital_status === 2 ? '离异' : '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">身高(cm)</span>
|
||
<span class="detail-value">{{ detailData.height ?? '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">体重(kg)</span>
|
||
<span class="detail-value">{{ detailData.weight ?? '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">地区</span>
|
||
<span class="detail-value">{{ detailData.region || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">收缩压(mmHg)</span>
|
||
<span class="detail-value">{{ detailData.systolic_pressure ?? '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">舒张压(mmHg)</span>
|
||
<span class="detail-value">{{ detailData.diastolic_pressure ?? '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">空腹血糖(mmol/L)</span>
|
||
<span class="detail-value">{{ detailData.fasting_blood_sugar ?? '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">诊断类型</span>
|
||
<span class="detail-value">{{ getDictLabel(diagnosisTypeOptions, detailData.diagnosis_type) || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">证型</span>
|
||
<span class="detail-value">{{ getDictLabel(syndromeTypeOptions, detailData.syndrome_type) || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">糖尿病期数</span>
|
||
<span class="detail-value">{{ getDictLabel(diabetesTypeOptions, detailData.diabetes_type) || '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主诉/现病史 -->
|
||
<div v-if="detailData.symptoms || detailData.local_hospital_name || (detailData.past_history && detailData.past_history.length)" class="detail-section">
|
||
<div class="detail-section-title">
|
||
<el-icon><Document /></el-icon>
|
||
病史信息
|
||
</div>
|
||
<div class="detail-grid">
|
||
<div v-if="detailData.diabetes_discovery_year != null" class="detail-item">
|
||
<span class="detail-label">发现糖尿病病史</span>
|
||
<span class="detail-value">{{ detailData.diabetes_discovery_year }}年</span>
|
||
</div>
|
||
<div v-if="detailData.local_hospital_name" class="detail-item">
|
||
<span class="detail-label">当地就诊医院</span>
|
||
<span class="detail-value">{{ detailData.local_hospital_name }}</span>
|
||
</div>
|
||
<div v-if="detailData.symptoms" class="detail-item detail-item-full">
|
||
<span class="detail-label">症状补充</span>
|
||
<span class="detail-value">{{ detailData.symptoms }}</span>
|
||
</div>
|
||
<div v-if="detailData.past_history && detailData.past_history.length" class="detail-item detail-item-full">
|
||
<span class="detail-label">既往史</span>
|
||
<span class="detail-value">{{ getPastHistoryLabels(detailData.past_history) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 舌苔/脉象/治则 -->
|
||
<div v-if="detailData.tongue_coating || detailData.pulse || detailData.treatment_principle || detailData.prescription" class="detail-section">
|
||
<div class="detail-section-title">
|
||
<el-icon><Document /></el-icon>
|
||
诊断信息
|
||
</div>
|
||
<div class="detail-grid">
|
||
<div v-if="detailData.tongue_coating" class="detail-item">
|
||
<span class="detail-label">舌苔</span>
|
||
<span class="detail-value">{{ detailData.tongue_coating }}</span>
|
||
</div>
|
||
<div v-if="detailData.pulse" class="detail-item">
|
||
<span class="detail-label">脉象</span>
|
||
<span class="detail-value">{{ detailData.pulse }}</span>
|
||
</div>
|
||
<div v-if="detailData.treatment_principle" class="detail-item detail-item-full">
|
||
<span class="detail-label">治则</span>
|
||
<span class="detail-value">{{ detailData.treatment_principle }}</span>
|
||
</div>
|
||
<div v-if="detailData.prescription" class="detail-item detail-item-full">
|
||
<span class="detail-label">处方</span>
|
||
<span class="detail-value">{{ detailData.prescription }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 备注 -->
|
||
<div class="detail-section">
|
||
<div class="detail-section-title">备注</div>
|
||
<p class="detail-remark">{{ detailData.remark || '无' }}</p>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 编辑患者弹窗 -->
|
||
<edit-popup ref="editRef" @success="getLists" />
|
||
|
||
<!-- 中医处方单 -->
|
||
<tcm-prescription ref="prescriptionRef" @success="onPrescriptionSuccess" />
|
||
|
||
<!-- 聊天对话框 -->
|
||
<chat-dialog ref="chatDialogRef" />
|
||
|
||
<!-- 小程序二维码弹窗 -->
|
||
<el-dialog
|
||
v-model="qrcodeDialogVisible"
|
||
title="小程序二维码"
|
||
width="380px"
|
||
:close-on-click-modal="false"
|
||
class="qrcode-dialog"
|
||
>
|
||
<div class="qrcode-content">
|
||
<div v-if="qrcodeLoading" class="qrcode-loading">
|
||
<el-icon class="is-loading" :size="48">
|
||
<Loading />
|
||
</el-icon>
|
||
<div class="qrcode-loading-text">生成中...</div>
|
||
</div>
|
||
<div v-else-if="qrcodeUrl" class="qrcode-success">
|
||
<div class="qrcode-image-wrap">
|
||
<img :src="qrcodeUrl" alt="小程序二维码" class="qrcode-image" />
|
||
</div>
|
||
<div class="qrcode-info">
|
||
<div class="qrcode-patient">患者:{{ currentQRCodeRow?.patient_name }}</div>
|
||
<div class="qrcode-tip">请使用微信扫描二维码</div>
|
||
<div class="qrcode-path">pages/login/login</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="qrcode-error">
|
||
<el-icon :size="40"><Warning /></el-icon>
|
||
<div>生成失败,请重试</div>
|
||
</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>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { usePaging } from '@/hooks/usePaging'
|
||
import { watch } from 'vue'
|
||
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
|
||
import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm'
|
||
import { getDictData } from '@/api/app'
|
||
import feedback from '@/utils/feedback'
|
||
import EditPopup from '../diagnosis/edit.vue'
|
||
import TcmPrescription from '@/components/tcm-prescription/index.vue'
|
||
import ChatDialog from '@/components/chat-dialog/index.vue'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import { getWeappConfig } from '@/api/channel/weapp'
|
||
import {
|
||
Loading,
|
||
User,
|
||
List,
|
||
Clock,
|
||
Warning,
|
||
CircleClose,
|
||
CircleCheck,
|
||
ArrowDown,
|
||
Edit,
|
||
ChatDotRound,
|
||
Picture,
|
||
Document,
|
||
Calendar,
|
||
RefreshRight
|
||
} from '@element-plus/icons-vue'
|
||
|
||
const userStore = useUserStore()
|
||
const userInfo = computed(() => userStore.userInfo)
|
||
|
||
// 判断是否为管理员(非医生、非医助)
|
||
const isAdmin = computed(() => {
|
||
const roleId = userInfo.value.role_id
|
||
// 如果role_id是数组,检查是否包含医生(1)或医助(2)
|
||
if (Array.isArray(roleId)) {
|
||
return !roleId.includes(1) && !roleId.includes(2)
|
||
}
|
||
// 如果role_id是单个值,检查是否不是医生或医助
|
||
return roleId !== 1 && roleId !== 2
|
||
})
|
||
|
||
const formData = reactive({
|
||
patient_name: '',
|
||
doctor_name: '',
|
||
status: '' as string | number,
|
||
start_date: '',
|
||
end_date: '',
|
||
date_preset: 'today' as '' | 'today' | 'tomorrow' | 'day_after',
|
||
diagnosis_confirmed: '' as '' | '0' | '1' // ''=全部 1=已确认 0=未确认
|
||
})
|
||
|
||
const activeTab = ref('1')
|
||
const dateCustomVisible = ref(false)
|
||
const statusCount = ref<Record<number, number>>({
|
||
1: 0,
|
||
2: 0,
|
||
3: 0,
|
||
4: 0
|
||
})
|
||
|
||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||
fetchFun: appointmentLists,
|
||
params: formData
|
||
})
|
||
|
||
// 手动修改日期范围时清除快捷日期
|
||
watch(
|
||
() => [formData.start_date, formData.end_date],
|
||
([start, end]) => {
|
||
if (!formData.date_preset) return
|
||
const today = new Date()
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`
|
||
const tomorrow = new Date(today)
|
||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||
const tomorrowStr = `${tomorrow.getFullYear()}-${pad(tomorrow.getMonth() + 1)}-${pad(tomorrow.getDate())}`
|
||
const dayAfter = new Date(today)
|
||
dayAfter.setDate(dayAfter.getDate() + 2)
|
||
const dayAfterStr = `${dayAfter.getFullYear()}-${pad(dayAfter.getMonth() + 1)}-${pad(dayAfter.getDate())}`
|
||
const expected = formData.date_preset === 'today' ? todayStr : formData.date_preset === 'tomorrow' ? tomorrowStr : dayAfterStr
|
||
const startStr = (start || '').split(' ')[0]
|
||
if (startStr !== expected) {
|
||
formData.date_preset = ''
|
||
}
|
||
}
|
||
)
|
||
|
||
// 获取各状态数量
|
||
const getStatusCount = async () => {
|
||
try {
|
||
// 获取各状态的数量
|
||
const promises = [1, 2, 3, 4].map(async (status) => {
|
||
const res = await appointmentLists({
|
||
...formData,
|
||
status,
|
||
page_no: 1,
|
||
page_size: 1
|
||
})
|
||
return { status, count: res.count || 0 }
|
||
})
|
||
|
||
const results = await Promise.all(promises)
|
||
results.forEach(({ status, count }) => {
|
||
statusCount.value[status] = count
|
||
})
|
||
} catch (error) {
|
||
console.error('获取状态数量失败:', error)
|
||
}
|
||
}
|
||
|
||
// 待接诊行高亮
|
||
const getRowClassName = ({ row }: { row: any }) => {
|
||
return row.status === 1 ? 'row-pending' : ''
|
||
}
|
||
|
||
// 切换选项卡
|
||
const handleTabChange = (tabName: string | number) => {
|
||
if (tabName === 'all') {
|
||
formData.status = ''
|
||
} else {
|
||
formData.status = Number(tabName)
|
||
}
|
||
resetPage()
|
||
}
|
||
|
||
// 重写getLists,同时更新状态数量
|
||
const loadData = async () => {
|
||
await getLists()
|
||
await getStatusCount()
|
||
}
|
||
|
||
// 更多下拉命令
|
||
const handleMoreCommand = (cmd: string) => {
|
||
if (cmd === 'customDate') dateCustomVisible.value = true
|
||
else if (cmd === 'reset') handleReset()
|
||
}
|
||
|
||
// 自定义日期确定
|
||
const applyCustomDate = () => {
|
||
formData.date_preset = ''
|
||
dateCustomVisible.value = false
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
|
||
// 确认诊单筛选变更
|
||
const handleDiagnosisConfirmedChange = () => {
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
|
||
// 快捷日期变更
|
||
const handleDatePresetChange = (val: string | number | boolean | undefined) => {
|
||
const v = String(val || '')
|
||
if (!v) {
|
||
formData.date_preset = ''
|
||
formData.start_date = ''
|
||
formData.end_date = ''
|
||
loadData()
|
||
return
|
||
}
|
||
const today = new Date()
|
||
const pad = (n: number) => String(n).padStart(2, '0')
|
||
let target: Date
|
||
if (v === 'today') {
|
||
target = today
|
||
} else if (v === 'tomorrow') {
|
||
target = new Date(today)
|
||
target.setDate(target.getDate() + 1)
|
||
} else {
|
||
target = new Date(today)
|
||
target.setDate(target.getDate() + 2)
|
||
}
|
||
const dateStr = `${target.getFullYear()}-${pad(target.getMonth() + 1)}-${pad(target.getDate())}`
|
||
formData.date_preset = v as '' | 'today' | 'tomorrow' | 'day_after'
|
||
formData.start_date = dateStr
|
||
formData.end_date = dateStr
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
|
||
// 重置(恢复默认:待接诊 + 今天 + 全部确认状态)
|
||
const handleReset = () => {
|
||
activeTab.value = '1'
|
||
formData.patient_name = ''
|
||
formData.doctor_name = ''
|
||
formData.date_preset = 'today'
|
||
formData.diagnosis_confirmed = ''
|
||
const t = new Date()
|
||
const p = (n: number) => String(n).padStart(2, '0')
|
||
formData.start_date = `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`
|
||
formData.end_date = formData.start_date
|
||
formData.status = 1
|
||
pager.page = 1
|
||
loadData()
|
||
}
|
||
|
||
const detailVisible = ref(false)
|
||
const detailData = ref<any>(null)
|
||
|
||
// 字典选项(用于详情展示)
|
||
const diagnosisTypeOptions = ref<any[]>([])
|
||
const syndromeTypeOptions = ref<any[]>([])
|
||
const diabetesTypeOptions = ref<any[]>([])
|
||
const pastHistoryOptions = ref<any[]>([])
|
||
|
||
const getDictLabel = (options: any[], value: string) => {
|
||
if (!value) return '-'
|
||
const item = options.find((opt: any) => opt.value === value)
|
||
return item ? item.name : value
|
||
}
|
||
|
||
const getPastHistoryLabels = (values: string[] | string) => {
|
||
const arr = Array.isArray(values) ? values : (typeof values === 'string' ? values.split(/[,,、]/) : [])
|
||
return arr.map((v: string) => getDictLabel(pastHistoryOptions.value, v.trim())).filter(Boolean).join('、') || '-'
|
||
}
|
||
|
||
const loadDetailDictOptions = async () => {
|
||
try {
|
||
const [dt, st, db, ph] = await Promise.all([
|
||
getDictData({ type: 'diagnosis_type' }),
|
||
getDictData({ type: 'syndrome_type' }),
|
||
getDictData({ type: 'diabetes_type' }),
|
||
getDictData({ type: 'past_history' })
|
||
])
|
||
diagnosisTypeOptions.value = dt?.diagnosis_type || []
|
||
syndromeTypeOptions.value = st?.syndrome_type || []
|
||
diabetesTypeOptions.value = db?.diabetes_type || []
|
||
pastHistoryOptions.value = ph?.past_history || []
|
||
} catch (e) {
|
||
console.error('加载字典失败:', e)
|
||
}
|
||
}
|
||
const editRef = ref()
|
||
const prescriptionRef = ref()
|
||
const chatDialogRef = ref()
|
||
|
||
// 小程序二维码相关
|
||
const qrcodeDialogVisible = ref(false)
|
||
const qrcodeLoading = ref(false)
|
||
const qrcodeUrl = ref('')
|
||
const currentQRCodeRow = ref<any>(null)
|
||
|
||
// 下拉操作
|
||
const handleAction = (command: string, row: any) => {
|
||
switch (command) {
|
||
case 'edit':
|
||
handleEditPatient(row)
|
||
break
|
||
case 'viewCase':
|
||
handleViewCase(row)
|
||
break
|
||
case 'complete':
|
||
handleComplete(row)
|
||
break
|
||
case 'chat':
|
||
handleChat(row)
|
||
break
|
||
case 'qrcode':
|
||
handleMiniProgramQRCode(row)
|
||
break
|
||
case 'prescription':
|
||
handlePrescription(row)
|
||
break
|
||
case 'cancel':
|
||
handleCancel(row)
|
||
break
|
||
}
|
||
}
|
||
|
||
// 查看详情(含患者诊单完整信息)
|
||
const handleDetail = async (row: any) => {
|
||
try {
|
||
const res = await appointmentDetail({ id: row.id })
|
||
detailData.value = res
|
||
// 有 patient_id(即诊单ID)时,拉取患者完整信息
|
||
if (res.patient_id) {
|
||
try {
|
||
const diagnosis = await tcmDiagnosisDetail({ id: res.patient_id })
|
||
detailData.value = { ...res, ...diagnosis }
|
||
} catch (e) {
|
||
console.warn('获取患者诊单详情失败:', e)
|
||
}
|
||
}
|
||
detailVisible.value = true
|
||
} catch (error) {
|
||
console.error('获取详情失败:', error)
|
||
}
|
||
}
|
||
|
||
// 取消挂号
|
||
const handleCancel = async (row: any) => {
|
||
try {
|
||
await feedback.confirm('确定要取消该挂号吗?')
|
||
await cancelAppointment({ id: row.id })
|
||
feedback.msgSuccess('取消成功')
|
||
loadData()
|
||
} catch (error) {
|
||
// 用户取消操作
|
||
}
|
||
}
|
||
|
||
// 完成挂号
|
||
const handleComplete = async (row: any) => {
|
||
try {
|
||
await feedback.confirm('确认患者已就诊完成?')
|
||
await completeAppointment({ id: row.id })
|
||
feedback.msgSuccess('操作成功')
|
||
loadData()
|
||
} catch (error) {
|
||
// 用户取消操作
|
||
}
|
||
}
|
||
|
||
// 编辑患者资料
|
||
const handleEditPatient = (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('该预约没有关联诊单信息')
|
||
return
|
||
}
|
||
editRef.value?.open('edit', row.patient_id)
|
||
}
|
||
|
||
// 编辑患者(按钮直接调用)
|
||
const handleEdit = (row: any) => {
|
||
handleEditPatient(row)
|
||
}
|
||
|
||
// 聊天
|
||
const handleChat = async (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('患者信息不完整')
|
||
return
|
||
}
|
||
|
||
// 检查是否有诊单ID
|
||
if (!row.diagnosis_id && !row.id) {
|
||
feedback.msgWarning('预约信息不完整,无法发起聊天')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 获取聊天签名信息
|
||
const res = await getCallSignature({
|
||
patient_id: row.patient_id,
|
||
diagnosis_id: row.diagnosis_id || row.id
|
||
})
|
||
|
||
console.log('获取聊天签名成功:', res)
|
||
|
||
// 直接打开聊天对话框,传入必要的参数
|
||
chatDialogRef.value?.open({
|
||
patientId: row.patient_id,
|
||
patientName: row.patient_name,
|
||
diagnosisId: row.diagnosis_id || row.id,
|
||
signatureData: res // 传入签名数据
|
||
})
|
||
} catch (error: any) {
|
||
console.error('获取聊天签名失败:', error)
|
||
feedback.msgError(error.message || '获取聊天签名失败')
|
||
}
|
||
}
|
||
|
||
// 生成小程序二维码(跳转小程序路径:pages/login/login)
|
||
const handleMiniProgramQRCode = async (row: any) => {
|
||
if (!row.patient_id) {
|
||
feedback.msgWarning('患者信息不完整')
|
||
return
|
||
}
|
||
|
||
currentQRCodeRow.value = row
|
||
qrcodeDialogVisible.value = true
|
||
qrcodeLoading.value = true
|
||
qrcodeUrl.value = ''
|
||
|
||
try {
|
||
// 获取小程序配置
|
||
const config = await getWeappConfig()
|
||
|
||
if (!config?.app_id) {
|
||
feedback.msgError('小程序未配置,请先配置小程序信息')
|
||
qrcodeDialogVisible.value = false
|
||
return
|
||
}
|
||
|
||
const currentUser = userStore.userInfo
|
||
|
||
const result = await generateMiniProgramQrcode({
|
||
diagnosis_id: row.doctor_id ||'',
|
||
patient_id: row.patient_id,
|
||
share_user_id: currentUser?.id || '',
|
||
mini_program_path: 'pages/login/login'
|
||
})
|
||
|
||
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 onPrescriptionSuccess = () => {
|
||
loadData()
|
||
editRef.value?.refreshCaseList?.()
|
||
}
|
||
|
||
// 开处方
|
||
const handlePrescription = (row: any) => {
|
||
if (!row.diagnosis_id && !row.patient_id) {
|
||
feedback.msgWarning('该预约缺少诊单信息,请先编辑患者')
|
||
return
|
||
}
|
||
prescriptionRef.value?.open(row)
|
||
}
|
||
|
||
// 查看病历(开方后显示)
|
||
const handleViewCase = async (row: any) => {
|
||
if (!row.id) {
|
||
feedback.msgWarning('预约信息不完整')
|
||
return
|
||
}
|
||
try {
|
||
const prescription = await prescriptionGetByAppointment({ appointment_id: row.id })
|
||
if (prescription?.id) {
|
||
prescriptionRef.value?.openById(prescription.id)
|
||
} else {
|
||
feedback.msgWarning('该预约暂无病历记录,请先开方')
|
||
}
|
||
} catch (error: any) {
|
||
feedback.msgError(error?.msg || '获取病历失败')
|
||
}
|
||
}
|
||
|
||
// 重新生成二维码
|
||
const handleRegenerateQRCode = () => {
|
||
if (currentQRCodeRow.value) {
|
||
handleMiniProgramQRCode(currentQRCodeRow.value)
|
||
}
|
||
}
|
||
|
||
// 默认:今天 + 待接诊
|
||
const _today = new Date()
|
||
const _pad = (n: number) => String(n).padStart(2, '0')
|
||
formData.start_date = `${_today.getFullYear()}-${_pad(_today.getMonth() + 1)}-${_pad(_today.getDate())}`
|
||
formData.end_date = formData.start_date
|
||
formData.status = 1
|
||
|
||
loadData()
|
||
loadDetailDictOptions()
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.appointment-list {
|
||
|
||
min-height: 100%;
|
||
}
|
||
|
||
.list-card {
|
||
:deep(.el-card__body) {
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
.list-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.list-tabs {
|
||
flex: 1;
|
||
min-width: 0;
|
||
:deep(.el-tabs__header) {
|
||
margin-bottom: 0;
|
||
}
|
||
:deep(.el-tabs__nav-wrap::after) {
|
||
display: none;
|
||
}
|
||
:deep(.el-tabs__item) {
|
||
font-size: 14px;
|
||
padding: 0 16px;
|
||
}
|
||
:deep(.el-tabs__item.is-active) {
|
||
font-weight: 500;
|
||
}
|
||
:deep(.el-tabs__ink-bar) {
|
||
height: 2px;
|
||
border-radius: 2px 2px 0 0;
|
||
}
|
||
:deep(.el-tabs__active-bar) {
|
||
height: 2px;
|
||
}
|
||
:deep(.el-tabs__nav) {
|
||
gap: 4px;
|
||
}
|
||
|
||
.tab-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.tab-badge {
|
||
margin-left: 4px;
|
||
:deep(.el-badge__content) {
|
||
font-size: 11px;
|
||
height: 16px;
|
||
line-height: 16px;
|
||
border: none;
|
||
}
|
||
}
|
||
.tab-badge-success :deep(.el-badge__content) {
|
||
background: var(--el-color-success);
|
||
}
|
||
.tab-badge-danger :deep(.el-badge__content) {
|
||
background: var(--el-color-danger);
|
||
}
|
||
.tab-badge-info :deep(.el-badge__content) {
|
||
background: var(--el-color-info);
|
||
}
|
||
.tab-badge-primary :deep(.el-badge__content) {
|
||
background: var(--el-color-primary);
|
||
}
|
||
.tab-priority {
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.toolbar-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
|
||
.action-input {
|
||
width: 120px;
|
||
}
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 32px;
|
||
padding: 12px 16px;
|
||
background: var(--el-fill-color-lighter);
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.filter-label {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-radio {
|
||
:deep(.el-radio-button__inner) {
|
||
padding: 6px 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.dropdown-item-with-input {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
|
||
.dropdown-label {
|
||
width: 40px;
|
||
flex-shrink: 0;
|
||
}
|
||
.dropdown-input {
|
||
width: 120px;
|
||
}
|
||
}
|
||
|
||
.table-wrap {
|
||
padding: 0 16px 20px;
|
||
}
|
||
|
||
.action-btns {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 0 8px;
|
||
|
||
.action-qrcode {
|
||
color: var(--el-color-warning) !important;
|
||
font-weight: 500;
|
||
&:hover {
|
||
color: var(--el-color-warning-dark-2) !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.appointment-table {
|
||
:deep(.el-table__header th) {
|
||
background: var(--el-fill-color-light);
|
||
font-weight: 500;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
:deep(.el-table__row) {
|
||
cursor: default;
|
||
}
|
||
:deep(.el-table__row.row-pending) {
|
||
background: linear-gradient(90deg, rgba(var(--el-color-success-rgb), 0.06) 0%, transparent 100%) !important;
|
||
}
|
||
:deep(.el-table__row.row-pending:hover > td) {
|
||
background: linear-gradient(90deg, rgba(var(--el-color-success-rgb), 0.1) 0%, transparent 100%) !important;
|
||
}
|
||
}
|
||
|
||
.patient-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.patient-avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
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: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
.patient-phone {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
margin-top: 2px;
|
||
}
|
||
.patient-extra {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.datetime-cell {
|
||
.date-text {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
.time-text {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--el-color-primary);
|
||
margin-top: 4px;
|
||
}
|
||
}
|
||
|
||
.detail-content {
|
||
padding: 8px 0;
|
||
}
|
||
.detail-section {
|
||
margin-bottom: 24px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
.detail-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
|
||
.el-icon {
|
||
font-size: 16px;
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
.detail-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 12px 24px;
|
||
}
|
||
.detail-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.detail-label {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.detail-value {
|
||
font-size: 14px;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
}
|
||
.detail-item-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.detail-remark {
|
||
font-size: 14px;
|
||
color: var(--el-text-color-regular);
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.text-danger {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.qrcode-content {
|
||
padding: 16px 0;
|
||
min-height: 200px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.qrcode-loading,
|
||
.qrcode-success,
|
||
.qrcode-error {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
.qrcode-loading-text {
|
||
margin-top: 16px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.qrcode-image-wrap {
|
||
padding: 12px;
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
.qrcode-image {
|
||
width: 200px;
|
||
height: 200px;
|
||
display: block;
|
||
border-radius: 8px;
|
||
}
|
||
.qrcode-info {
|
||
margin-top: 16px;
|
||
font-size: 14px;
|
||
}
|
||
.qrcode-patient {
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
.qrcode-tip {
|
||
margin-top: 8px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
.qrcode-path {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
.qrcode-error {
|
||
color: var(--el-text-color-secondary);
|
||
gap: 12px;
|
||
}
|
||
</style>
|