Files
zyt/admin/src/views/tcm/appointment/list.vue
T
2026-03-20 13:56:40 +08:00

1304 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="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>