feat(surgery): 完善手术管理功能模块

- 添加手术申请相关API接口,包括根据患者ID查询就诊列表功能
- 在医生工作站界面集成手术申请功能选项卡
- 实现手术管理页面的完整功能,包括手术申请的增删改查
- 添加手术排期、开始、完成等状态流转功能
- 优化手术管理页面表格展示,增加手术类型、等级、计划时间等字段
- 实现手术申请表单的完整编辑和查看模式
- 集成患者信息和就诊记录关联功能
- 添加手术室、医生、护士等资源选择功能
- 更新系统依赖配置,添加core-common模块
- 优化图标资源和manifest配置文件
- 调整患者档案和门诊记录相关状态枚举
This commit is contained in:
2026-01-06 16:23:15 +08:00
parent fa2884b320
commit b0850257c8
66 changed files with 7683 additions and 313 deletions

View File

@@ -0,0 +1,633 @@
<template>
<div class="today-outpatient-patient-list">
<!-- 搜索过滤区域 -->
<div class="filter-section">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form-item label="搜索" prop="searchKey">
<el-input
v-model="queryParams.searchKey"
placeholder="姓名/身份证/手机号/就诊号"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="statusEnum">
<el-select
v-model="queryParams.statusEnum"
placeholder="全部状态"
clearable
style="width: 120px"
>
<el-option label="待就诊" :value="1" />
<el-option label="就诊中" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已取消" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="患者类型" prop="typeCode">
<el-select
v-model="queryParams.typeCode"
placeholder="全部类型"
clearable
style="width: 120px"
>
<el-option label="普通" :value="1" />
<el-option label="急诊" :value="2" />
<el-option label="VIP" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-button type="warning" icon="Download" @click="exportData">导出</el-button>
</el-form-item>
</el-form>
</div>
<!-- 患者列表 -->
<div class="table-section">
<el-table
:data="patientList"
border
style="width: 100%"
v-loading="loading"
:header-cell-style="{ background: '#f5f7fa', fontWeight: 'bold' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="patientName" label="患者" min-width="100">
<template #default="scope">
<div class="patient-name-cell">
<span class="name">{{ scope.row.patientName }}</span>
<el-tag
v-if="scope.row.importantFlag"
size="small"
type="danger"
class="important-tag"
>
重点
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="genderEnumEnumText" label="性别" width="80" align="center" />
<el-table-column prop="age" label="年龄" width="80" align="center" />
<el-table-column prop="phone" label="联系电话" width="120" />
<el-table-column prop="encounterBusNo" label="就诊号" width="120" align="center" />
<el-table-column prop="registerTime" label="挂号时间" width="160" sortable />
<el-table-column prop="waitingDuration" label="候诊时长" width="100" align="center">
<template #default="scope">
<span :class="getWaitingDurationClass(scope.row.waitingDuration)">
{{ scope.row.waitingDuration || 0 }} 分钟
</span>
</template>
</el-table-column>
<el-table-column prop="statusEnumEnumText" label="就诊状态" width="100" align="center">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.statusEnum)"
size="small"
class="status-tag"
>
{{ scope.row.statusEnumEnumText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-button
v-if="scope.row.statusEnum === 1"
type="primary"
size="small"
@click="handleReceive(scope.row.encounterId)"
class="action-button"
>
<el-icon><VideoPlay /></el-icon>
接诊
</el-button>
<el-button
v-if="scope.row.statusEnum === 2"
type="success"
size="small"
@click="handleComplete(scope.row.encounterId)"
class="action-button"
>
<el-icon><CircleCheck /></el-icon>
完成
</el-button>
<el-button
v-if="scope.row.statusEnum !== 4"
type="warning"
size="small"
@click="handleCancel(scope.row.encounterId)"
class="action-button"
>
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
type="info"
size="small"
@click="handleViewDetail(scope.row)"
class="action-button"
>
<el-icon><View /></el-icon>
详情
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 批量操作 -->
<div class="batch-section" v-if="selectedPatients.length > 0">
<el-card shadow="always" class="batch-card">
<div class="batch-content">
<el-space>
<el-text>已选择 {{ selectedPatients.length }} 个患者</el-text>
<el-button-group>
<el-button
v-if="selectedPatients.some(p => p.statusEnum === 1)"
type="primary"
size="small"
@click="batchReceive"
>
<el-icon><VideoPlay /></el-icon>
批量接诊
</el-button>
<el-button
v-if="selectedPatients.some(p => p.statusEnum === 2)"
type="success"
size="small"
@click="batchComplete"
>
<el-icon><CircleCheck /></el-icon>
批量完成
</el-button>
<el-button
type="warning"
size="small"
@click="batchCancel"
>
<el-icon><Close /></el-icon>
批量取消
</el-button>
<el-button
type="info"
size="small"
@click="clearSelection"
>
<el-icon><Delete /></el-icon>
清空选择
</el-button>
</el-button-group>
</el-space>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, defineEmits, defineExpose } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
VideoPlay, CircleCheck, Close, View, Delete
} from '@element-plus/icons-vue'
import {
getTodayOutpatientPatients,
receivePatient,
completeVisit,
cancelVisit,
batchUpdatePatientStatus
} from './api.js'
// 数据
const patientList = ref([])
const total = ref(0)
const loading = ref(false)
const selectedPatients = ref([])
// 查询参数
const queryParams = reactive({
searchKey: '',
statusEnum: null,
typeCode: null,
importantFlag: null,
hasPrescription: null,
hasExamination: null,
hasLaboratory: null,
pageNo: 1,
pageSize: 10,
sortField: 1,
sortOrder: 2
})
// 定义事件
const emit = defineEmits(['refresh'])
// 暴露方法给父组件
defineExpose({
refreshList
})
// 页面加载
onMounted(() => {
loadPatients()
})
// 加载患者列表
const loadPatients = () => {
loading.value = true
getTodayOutpatientPatients(queryParams)
.then(res => {
if (res.code === 200) {
patientList.value = res.data?.records || []
total.value = res.data?.total || 0
}
})
.finally(() => {
loading.value = false
})
}
// 刷新列表
function refreshList() {
loadPatients()
emit('refresh')
}
// 搜索
const handleQuery = () => {
queryParams.pageNo = 1
loadPatients()
}
// 重置搜索
const resetQuery = () => {
queryParams.searchKey = ''
queryParams.statusEnum = null
queryParams.typeCode = null
queryParams.importantFlag = null
queryParams.hasPrescription = null
queryParams.hasExamination = null
queryParams.hasLaboratory = null
handleQuery()
}
// 分页大小变化
const handleSizeChange = (val) => {
queryParams.pageSize = val
loadPatients()
}
// 当前页变化
const handleCurrentChange = (val) => {
queryParams.pageNo = val
loadPatients()
}
// 选择变化
const handleSelectionChange = (val) => {
selectedPatients.value = val
}
// 清空选择
const clearSelection = () => {
selectedPatients.value = []
}
// 接诊患者
const handleReceive = (encounterId) => {
ElMessageBox.confirm('确定接诊该患者吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
receivePatient(encounterId).then(res => {
if (res.code === 200) {
ElMessage.success('接诊成功')
refreshList()
} else {
ElMessage.error(res.msg || '接诊失败')
}
})
})
}
// 完成就诊
const handleComplete = (encounterId) => {
ElMessageBox.confirm('确定完成该患者的就诊吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
completeVisit(encounterId).then(res => {
if (res.code === 200) {
ElMessage.success('就诊完成')
refreshList()
} else {
ElMessage.error(res.msg || '操作失败')
}
})
})
}
// 取消就诊
const handleCancel = (encounterId) => {
ElMessageBox.prompt('请输入取消原因', '取消就诊', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.{1,200}$/,
inputErrorMessage: '取消原因长度在1到200个字符之间'
}).then(({ value }) => {
cancelVisit(encounterId, value).then(res => {
if (res.code === 200) {
ElMessage.success('就诊已取消')
refreshList()
} else {
ElMessage.error(res.msg || '操作失败')
}
})
})
}
// 查看详情
const handleViewDetail = (patient) => {
// 在实际应用中,可以打开详细对话框或跳转到详情页面
ElMessage.info(`查看患者 ${patient.patientName} 的详情`)
}
// 批量接诊
const batchReceive = () => {
const waitingIds = selectedPatients.value
.filter(p => p.statusEnum === 1)
.map(p => p.encounterId)
if (waitingIds.length === 0) {
ElMessage.warning('请选择待就诊的患者')
return
}
ElMessageBox.confirm(`确定批量接诊 ${waitingIds.length} 个患者吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchUpdatePatientStatus(waitingIds, 2).then(res => {
if (res.code === 200) {
ElMessage.success(`成功接诊 ${waitingIds.length} 个患者`)
refreshList()
selectedPatients.value = []
} else {
ElMessage.error(res.msg || '批量接诊失败')
}
})
})
}
// 批量完成
const batchComplete = () => {
const inProgressIds = selectedPatients.value
.filter(p => p.statusEnum === 2)
.map(p => p.encounterId)
if (inProgressIds.length === 0) {
ElMessage.warning('请选择就诊中的患者')
return
}
ElMessageBox.confirm(`确定批量完成 ${inProgressIds.length} 个患者的就诊吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchUpdatePatientStatus(inProgressIds, 3).then(res => {
if (res.code === 200) {
ElMessage.success(`成功完成 ${inProgressIds.length} 个患者的就诊`)
refreshList()
selectedPatients.value = []
} else {
ElMessage.error(res.msg || '批量完成失败')
}
})
})
}
// 批量取消
const batchCancel = () => {
ElMessageBox.prompt('请输入批量取消的原因', '批量取消就诊', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.{1,200}$/,
inputErrorMessage: '取消原因长度在1到200个字符之间'
}).then(({ value }) => {
const cancelIds = selectedPatients.value
.filter(p => p.statusEnum !== 4)
.map(p => p.encounterId)
if (cancelIds.length === 0) {
ElMessage.warning('没有符合条件的患者可以取消')
return
}
batchUpdatePatientStatus(cancelIds, 4).then(res => {
if (res.code === 200) {
ElMessage.success(`成功取消 ${cancelIds.length} 个患者的就诊`)
refreshList()
selectedPatients.value = []
} else {
ElMessage.error(res.msg || '批量取消失败')
}
})
})
}
// 导出数据
const exportData = () => {
ElMessage.info('导出功能开发中...')
}
// 获取状态标签类型
const getStatusTagType = (status) => {
switch (status) {
case 1: // 待就诊
return 'warning'
case 2: // 就诊中
return 'primary'
case 3: // 已完成
return 'success'
case 4: // 已取消
return 'info'
default:
return ''
}
}
// 获取候诊时长样式
const getWaitingDurationClass = (duration) => {
if (!duration) return ''
if (duration > 60) return 'waiting-long' // 超过1小时
if (duration > 30) return 'waiting-medium' // 超过30分钟
return 'waiting-short' // 30分钟以内
}
</script>
<style lang="scss" scoped>
.today-outpatient-patient-list {
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.05);
}
.table-section {
margin-bottom: 20px;
.patient-name-cell {
display: flex;
align-items: center;
gap: 8px;
.name {
font-weight: 500;
}
.important-tag {
font-size: 10px;
padding: 2px 6px;
}
}
.status-tag {
font-weight: bold;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
.action-button {
display: flex;
align-items: center;
gap: 4px;
.el-icon {
font-size: 12px;
}
}
}
// 候诊时长颜色
.waiting-short {
color: #67c23a;
font-weight: 500;
}
.waiting-medium {
color: #e6a23c;
font-weight: 500;
}
.waiting-long {
color: #f56c6c;
font-weight: 500;
}
}
.pagination-section {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
.batch-section {
position: sticky;
bottom: 20px;
z-index: 1000;
.batch-card {
background: linear-gradient(135deg, #f6f9fc, #ffffff);
border: 1px solid #e4e7ed;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
.batch-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
.el-text {
font-weight: bold;
color: #333;
}
.el-button-group {
gap: 8px;
.el-button {
display: flex;
align-items: center;
gap: 4px;
.el-icon {
font-size: 12px;
}
}
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.today-outpatient-patient-list {
.filter-section {
padding: 12px;
.el-form-item {
margin-bottom: 12px;
width: 100%;
.el-input,
.el-select {
width: 100%;
}
}
}
.table-section {
overflow-x: auto;
.el-table {
min-width: 800px;
}
}
.pagination-section {
overflow-x: auto;
}
}
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<div class="today-outpatient-stats">
<el-row :gutter="20">
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="stats-card total-registered">
<div class="stats-icon">
<el-icon><User /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ stats.totalRegistered || 0 }}</div>
<div class="stats-label">今日总挂号</div>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="stats-card waiting">
<div class="stats-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ stats.waitingCount || 0 }}</div>
<div class="stats-label">待就诊</div>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="stats-card in-progress">
<div class="stats-icon">
<el-icon><VideoPlay /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ stats.inProgressCount || 0 }}</div>
<div class="stats-label">就诊中</div>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="stats-card completed">
<div class="stats-icon">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stats-info">
<div class="stats-value">{{ stats.completedCount || 0 }}</div>
<div class="stats-label">已完成</div>
</div>
</div>
</el-col>
</el-row>
<!-- 时间统计 -->
<div class="time-stats">
<el-row :gutter="20">
<el-col :xs="12" :sm="12" :md="12" :lg="12">
<div class="time-card waiting-time">
<el-icon><Timer /></el-icon>
<div class="time-info">
<div class="time-value">{{ stats.averageWaitingTime || 0 }} 分钟</div>
<div class="time-label">平均候诊时间</div>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :md="12" :lg="12">
<div class="time-card visit-time">
<el-icon><Watch /></el-icon>
<div class="time-info">
<div class="time-value">{{ stats.averageVisitTime || 0 }} 分钟</div>
<div class="time-label">平均就诊时间</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, defineEmits, defineExpose } from 'vue'
import { User, Clock, VideoPlay, CircleCheck, Timer, Watch } from '@element-plus/icons-vue'
import { getTodayOutpatientStats } from './api.js'
// 数据
const stats = ref({})
// 定义事件
const emit = defineEmits(['refresh'])
// 暴露方法给父组件
defineExpose({
refreshStats
})
// 页面加载
onMounted(() => {
loadStats()
})
// 加载统计信息
const loadStats = () => {
getTodayOutpatientStats().then(res => {
if (res.code === 200) {
stats.value = res.data || {}
}
})
}
// 刷新统计信息
function refreshStats() {
loadStats()
emit('refresh')
}
</script>
<style lang="scss" scoped>
.today-outpatient-stats {
.stats-card {
background: white;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
}
.stats-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
.el-icon {
color: white;
font-size: 24px;
}
}
.stats-info {
flex: 1;
.stats-value {
font-size: 24px;
font-weight: bold;
color: #333;
line-height: 1.2;
}
.stats-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
}
// 不同统计卡片的颜色
&.total-registered {
.stats-icon {
background: linear-gradient(135deg, #409eff, #79bbff);
}
}
&.waiting {
.stats-icon {
background: linear-gradient(135deg, #e6a23c, #fab85c);
}
}
&.in-progress {
.stats-icon {
background: linear-gradient(135deg, #67c23a, #95d475);
}
}
&.completed {
.stats-icon {
background: linear-gradient(135deg, #909399, #b1b3b8);
}
}
}
.time-stats {
margin-top: 20px;
.time-card {
background: white;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15);
}
.el-icon {
font-size: 32px;
margin-right: 16px;
color: #409eff;
}
.time-info {
.time-value {
font-size: 20px;
font-weight: bold;
color: #333;
line-height: 1.2;
}
.time-label {
font-size: 14px;
color: #666;
margin-top: 4px;
}
}
// 不同时间统计卡片的颜色
&.waiting-time {
.el-icon {
color: #e6a23c;
}
}
&.visit-time {
.el-icon {
color: #67c23a;
}
}
}
}
// 响应式间距
.el-row {
margin-bottom: 0;
}
.el-col {
margin-bottom: 20px;
}
}
// 响应式设计
@media (max-width: 768px) {
.today-outpatient-stats {
.stats-card {
padding: 16px;
.stats-icon {
width: 40px;
height: 40px;
margin-right: 10px;
.el-icon {
font-size: 20px;
}
}
.stats-info {
.stats-value {
font-size: 20px;
}
.stats-label {
font-size: 12px;
}
}
}
.time-stats {
.time-card {
padding: 16px;
.el-icon {
font-size: 24px;
margin-right: 12px;
}
.time-info {
.time-value {
font-size: 18px;
}
.time-label {
font-size: 12px;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
import request from '@/utils/request'
// 获取今日门诊统计信息
export function getTodayOutpatientStats() {
return request({
url: '/today-outpatient/stats',
method: 'get'
})
}
// 分页查询今日门诊患者列表
export function getTodayOutpatientPatients(queryParams) {
return request({
url: '/today-outpatient/patients',
method: 'get',
params: queryParams
})
}
// 获取今日待就诊患者队列
export function getWaitingPatients() {
return request({
url: '/today-outpatient/patients/waiting',
method: 'get'
})
}
// 获取今日就诊中患者列表
export function getInProgressPatients() {
return request({
url: '/today-outpatient/patients/in-progress',
method: 'get'
})
}
// 获取今日已完成就诊患者列表
export function getCompletedPatients() {
return request({
url: '/today-outpatient/patients/completed',
method: 'get'
})
}
// 获取患者就诊详情
export function getPatientDetail(encounterId) {
return request({
url: `/today-outpatient/patients/${encounterId}`,
method: 'get'
})
}
// 批量更新患者状态
export function batchUpdatePatientStatus(encounterIds, targetStatus) {
return request({
url: '/today-outpatient/patients/batch-update-status',
method: 'post',
params: {
encounterIds: encounterIds.join(','),
targetStatus
}
})
}
// 接诊患者
export function receivePatient(encounterId) {
return request({
url: `/today-outpatient/patients/${encounterId}/receive`,
method: 'post'
})
}
// 完成就诊
export function completeVisit(encounterId) {
return request({
url: `/today-outpatient/patients/${encounterId}/complete`,
method: 'post'
})
}
// 取消就诊
export function cancelVisit(encounterId, reason) {
return request({
url: `/today-outpatient/patients/${encounterId}/cancel`,
method: 'post',
params: { reason }
})
}
// 快速接诊
export function quickReceivePatient(encounterId) {
return request({
url: `/today-outpatient/quick-receive/${encounterId}`,
method: 'post'
})
}

View File

@@ -0,0 +1,753 @@
<template>
<div class="today-outpatient-container">
<!-- 统计卡片区域 -->
<div class="stats-cards">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="stats-card" shadow="hover">
<div class="stats-content">
<div class="stats-icon" style="background-color: #409eff;">
<el-icon><User /></el-icon>
</div>
<div class="stats-info">
<div class="stats-label">今日总挂号</div>
<div class="stats-value">{{ stats.totalRegistered || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stats-card" shadow="hover">
<div class="stats-content">
<div class="stats-icon" style="background-color: #e6a23c;">
<el-icon><Clock /></el-icon>
</div>
<div class="stats-info">
<div class="stats-label">待就诊</div>
<div class="stats-value">{{ stats.waitingCount || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stats-card" shadow="hover">
<div class="stats-content">
<div class="stats-icon" style="background-color: #67c23a;">
<el-icon><VideoPlay /></el-icon>
</div>
<div class="stats-info">
<div class="stats-label">就诊中</div>
<div class="stats-value">{{ stats.inProgressCount || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stats-card" shadow="hover">
<div class="stats-content">
<div class="stats-icon" style="background-color: #909399;">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stats-info">
<div class="stats-label">已完成</div>
<div class="stats-value">{{ stats.completedCount || 0 }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 时间统计 -->
<el-row :gutter="20" class="time-stats">
<el-col :span="12">
<el-card class="time-card" shadow="hover">
<div class="time-content">
<el-icon class="time-icon"><Timer /></el-icon>
<div class="time-info">
<div class="time-label">平均候诊时间</div>
<div class="time-value">{{ stats.averageWaitingTime || 0 }} 分钟</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="time-card" shadow="hover">
<div class="time-content">
<el-icon class="time-icon"><Watch /></el-icon>
<div class="time-info">
<div class="time-label">平均就诊时间</div>
<div class="time-value">{{ stats.averageVisitTime || 0 }} 分钟</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 搜索和过滤区域 -->
<div class="search-filter">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form-item label="搜索" prop="searchKey">
<el-input
v-model="queryParams.searchKey"
placeholder="姓名/身份证/手机号/就诊号"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="statusEnum">
<el-select
v-model="queryParams.statusEnum"
placeholder="全部状态"
clearable
style="width: 120px"
>
<el-option label="待就诊" :value="1" />
<el-option label="就诊中" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已取消" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="患者类型" prop="typeCode">
<el-select
v-model="queryParams.typeCode"
placeholder="全部类型"
clearable
style="width: 120px"
>
<el-option label="普通" :value="1" />
<el-option label="急诊" :value="2" />
<el-option label="VIP" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 患者列表 -->
<div class="patient-list">
<el-table
:data="patientList"
border
style="width: 100%"
v-loading="loading"
:header-cell-style="{ background: '#f5f7fa', fontWeight: 'bold' }"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="patientName" label="患者" min-width="100" />
<el-table-column prop="genderEnumEnumText" label="性别" width="80" align="center" />
<el-table-column prop="age" label="年龄" width="80" align="center" />
<el-table-column prop="phone" label="联系电话" width="120" />
<el-table-column prop="encounterBusNo" label="就诊号" width="120" align="center" />
<el-table-column prop="registerTime" label="挂号时间" width="160" sortable />
<el-table-column prop="waitingDuration" label="候诊时长" width="100" align="center">
<template #default="scope">
{{ scope.row.waitingDuration || 0 }} 分钟
</template>
</el-table-column>
<el-table-column prop="statusEnumEnumText" label="就诊状态" width="100" align="center">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.statusEnum)"
size="small"
>
{{ scope.row.statusEnumEnumText }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="subjectStatusEnumEnumText" label="就诊对象状态" width="120" align="center" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button
v-if="scope.row.statusEnum === 1"
type="primary"
size="small"
@click="handleReceive(scope.row.encounterId)"
>
接诊
</el-button>
<el-button
v-if="scope.row.statusEnum === 2"
type="success"
size="small"
@click="handleComplete(scope.row.encounterId)"
>
完成
</el-button>
<el-button
v-if="scope.row.statusEnum !== 4"
type="warning"
size="small"
@click="handleCancel(scope.row.encounterId)"
>
取消
</el-button>
<el-button
type="info"
size="small"
@click="handleViewDetail(scope.row.encounterId)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 批量操作 -->
<div class="batch-actions" v-if="selectedPatients.length > 0">
<el-space>
<el-text>已选择 {{ selectedPatients.length }} 个患者</el-text>
<el-button
v-if="selectedPatients.some(p => p.statusEnum === 1)"
type="primary"
size="small"
@click="batchReceive"
>
批量接诊
</el-button>
<el-button
v-if="selectedPatients.some(p => p.statusEnum === 2)"
type="success"
size="small"
@click="batchComplete"
>
批量完成
</el-button>
<el-button
type="warning"
size="small"
@click="batchCancel"
>
批量取消
</el-button>
</el-space>
</div>
<!-- 患者详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="患者就诊详情"
width="800px"
:before-close="handleDetailDialogClose"
>
<div v-loading="detailLoading">
<el-descriptions v-if="patientDetail" :column="2" border>
<el-descriptions-item label="患者姓名">{{ patientDetail.patientName }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ patientDetail.genderEnumEnumText }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ patientDetail.age }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ patientDetail.idCard }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ patientDetail.phone }}</el-descriptions-item>
<el-descriptions-item label="就诊号">{{ patientDetail.encounterBusNo }}</el-descriptions-item>
<el-descriptions-item label="挂号时间">{{ patientDetail.registerTime }}</el-descriptions-item>
<el-descriptions-item label="接诊时间">{{ patientDetail.receptionTime || '未接诊' }}</el-descriptions-item>
<el-descriptions-item label="就诊状态">
<el-tag :type="getStatusTagType(patientDetail.statusEnum)" size="small">
{{ patientDetail.statusEnumEnumText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="就诊对象状态">{{ patientDetail.subjectStatusEnumEnumText }}</el-descriptions-item>
<el-descriptions-item label="候诊时长">{{ patientDetail.waitingDuration || 0 }} 分钟</el-descriptions-item>
<el-descriptions-item label="就诊时长">{{ patientDetail.visitDuration || 0 }} 分钟</el-descriptions-item>
<el-descriptions-item label="是否重点患者">
<el-tag :type="patientDetail.importantFlag ? 'danger' : 'info'" size="small">
{{ patientDetail.importantFlag ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="是否已开药">
<el-tag :type="patientDetail.hasPrescription ? 'success' : 'info'" size="small">
{{ patientDetail.hasPrescription ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="是否已检查">
<el-tag :type="patientDetail.hasExamination ? 'success' : 'info'" size="small">
{{ patientDetail.hasExamination ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="是否已检验">
<el-tag :type="patientDetail.hasLaboratory ? 'success' : 'info'" size="small">
{{ patientDetail.hasLaboratory ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无患者详情" />
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
<el-button
v-if="patientDetail && patientDetail.statusEnum === 1"
type="primary"
@click="handleReceive(patientDetail.encounterId)"
>
接诊患者
</el-button>
<el-button
v-if="patientDetail && patientDetail.statusEnum === 2"
type="success"
@click="handleComplete(patientDetail.encounterId)"
>
完成就诊
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
User, Clock, VideoPlay, CircleCheck, Timer, Watch
} from '@element-plus/icons-vue'
import {
getTodayOutpatientStats,
getTodayOutpatientPatients,
receivePatient,
completeVisit,
cancelVisit,
batchUpdatePatientStatus
} from './api.js'
// 数据
const stats = ref({})
const patientList = ref([])
const total = ref(0)
const loading = ref(false)
const detailLoading = ref(false)
const selectedPatients = ref([])
const patientDetail = ref(null)
// 对话框控制
const detailDialogVisible = ref(false)
// 查询参数
const queryParams = reactive({
searchKey: '',
statusEnum: null,
typeCode: null,
importantFlag: null,
hasPrescription: null,
hasExamination: null,
hasLaboratory: null,
doctorId: null,
departmentId: null,
queryDate: null,
sortField: 1,
sortOrder: 2,
pageNo: 1,
pageSize: 10
})
// 页面加载
onMounted(() => {
loadStats()
loadPatients()
})
// 加载统计信息
const loadStats = () => {
getTodayOutpatientStats().then(res => {
if (res.code === 200) {
stats.value = res.data || {}
}
})
}
// 加载患者列表
const loadPatients = () => {
loading.value = true
getTodayOutpatientPatients(queryParams)
.then(res => {
if (res.code === 200) {
patientList.value = res.data?.records || []
total.value = res.data?.total || 0
}
})
.finally(() => {
loading.value = false
})
}
// 搜索
const handleQuery = () => {
queryParams.pageNo = 1
loadPatients()
}
// 重置搜索
const resetQuery = () => {
queryParams.searchKey = ''
queryParams.statusEnum = null
queryParams.typeCode = null
queryParams.importantFlag = null
queryParams.hasPrescription = null
queryParams.hasExamination = null
queryParams.hasLaboratory = null
queryParams.sortField = 1
queryParams.sortOrder = 2
handleQuery()
}
// 分页大小变化
const handleSizeChange = (val) => {
queryParams.pageSize = val
loadPatients()
}
// 当前页变化
const handleCurrentChange = (val) => {
queryParams.pageNo = val
loadPatients()
}
// 选择变化
const handleSelectionChange = (val) => {
selectedPatients.value = val
}
// 接诊患者
const handleReceive = (encounterId) => {
ElMessageBox.confirm('确定接诊该患者吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
receivePatient(encounterId).then(res => {
if (res.code === 200) {
ElMessage.success('接诊成功')
loadStats()
loadPatients()
if (detailDialogVisible.value) {
handleViewDetail(encounterId)
}
} else {
ElMessage.error(res.msg || '接诊失败')
}
})
})
}
// 完成就诊
const handleComplete = (encounterId) => {
ElMessageBox.confirm('确定完成该患者的就诊吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
completeVisit(encounterId).then(res => {
if (res.code === 200) {
ElMessage.success('就诊完成')
loadStats()
loadPatients()
if (detailDialogVisible.value) {
handleViewDetail(encounterId)
}
} else {
ElMessage.error(res.msg || '操作失败')
}
})
})
}
// 取消就诊
const handleCancel = (encounterId) => {
ElMessageBox.prompt('请输入取消原因', '取消就诊', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.{1,200}$/,
inputErrorMessage: '取消原因长度在1到200个字符之间'
}).then(({ value }) => {
cancelVisit(encounterId, value).then(res => {
if (res.code === 200) {
ElMessage.success('就诊已取消')
loadStats()
loadPatients()
if (detailDialogVisible.value) {
handleViewDetail(encounterId)
}
} else {
ElMessage.error(res.msg || '操作失败')
}
})
})
}
// 查看详情
const handleViewDetail = (encounterId) => {
detailDialogVisible.value = true
detailLoading.value = true
// 模拟获取详情数据实际应该调用API
setTimeout(() => {
const patient = patientList.value.find(p => p.encounterId === encounterId)
if (patient) {
patientDetail.value = { ...patient }
}
detailLoading.value = false
}, 500)
}
// 关闭详情对话框
const handleDetailDialogClose = (done) => {
patientDetail.value = null
done()
}
// 批量接诊
const batchReceive = () => {
const waitingIds = selectedPatients.value
.filter(p => p.statusEnum === 1)
.map(p => p.encounterId)
if (waitingIds.length === 0) {
ElMessage.warning('请选择待就诊的患者')
return
}
ElMessageBox.confirm(`确定批量接诊 ${waitingIds.length} 个患者吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchUpdatePatientStatus(waitingIds, 2).then(res => {
if (res.code === 200) {
ElMessage.success(`成功接诊 ${waitingIds.length} 个患者`)
loadStats()
loadPatients()
selectedPatients.value = []
} else {
ElMessage.error(res.msg || '批量接诊失败')
}
})
})
}
// 批量完成
const batchComplete = () => {
const inProgressIds = selectedPatients.value
.filter(p => p.statusEnum === 2)
.map(p => p.encounterId)
if (inProgressIds.length === 0) {
ElMessage.warning('请选择就诊中的患者')
return
}
ElMessageBox.confirm(`确定批量完成 ${inProgressIds.length} 个患者的就诊吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
batchUpdatePatientStatus(inProgressIds, 3).then(res => {
if (res.code === 200) {
ElMessage.success(`成功完成 ${inProgressIds.length} 个患者的就诊`)
loadStats()
loadPatients()
selectedPatients.value = []
} else {
ElMessage.error(res.msg || '批量完成失败')
}
})
})
}
// 批量取消
const batchCancel = () => {
ElMessageBox.prompt('请输入批量取消的原因', '批量取消就诊', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.{1,200}$/,
inputErrorMessage: '取消原因长度在1到200个字符之间'
}).then(({ value }) => {
const cancelIds = selectedPatients.value
.filter(p => p.statusEnum !== 4)
.map(p => p.encounterId)
if (cancelIds.length === 0) {
ElMessage.warning('没有符合条件的患者可以取消')
return
}
batchUpdatePatientStatus(cancelIds, 4).then(res => {
if (res.code === 200) {
ElMessage.success(`成功取消 ${cancelIds.length} 个患者的就诊`)
loadStats()
loadPatients()
selectedPatients.value = []
} else {
ElMessage.error(res.msg || '批量取消失败')
}
})
})
}
// 获取状态标签类型
const getStatusTagType = (status) => {
switch (status) {
case 1: // 待就诊
return 'warning'
case 2: // 就诊中
return 'primary'
case 3: // 已完成
return 'success'
case 4: // 已取消
return 'info'
default:
return ''
}
}
</script>
<style lang="scss" scoped>
.today-outpatient-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
.stats-cards {
margin-bottom: 20px;
.stats-card {
border-radius: 8px;
.stats-content {
display: flex;
align-items: center;
.stats-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
.el-icon {
color: white;
font-size: 24px;
}
}
.stats-info {
.stats-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.stats-value {
font-size: 24px;
font-weight: bold;
color: #333;
}
}
}
}
.time-stats {
margin-top: 20px;
.time-card {
border-radius: 8px;
.time-content {
display: flex;
align-items: center;
.time-icon {
font-size: 32px;
color: #409eff;
margin-right: 12px;
}
.time-info {
.time-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.time-value {
font-size: 20px;
font-weight: bold;
color: #333;
}
}
}
}
}
}
.search-filter {
background: white;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.patient-list {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
.batch-actions {
position: fixed;
bottom: 20px;
right: 20px;
background: white;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
z-index: 1000;
}
}
// 响应式设计
@media (max-width: 768px) {
.today-outpatient-container {
padding: 10px;
.stats-cards {
.el-col {
margin-bottom: 10px;
}
}
.search-filter {
.el-form-item {
margin-bottom: 10px;
}
}
}
}
</style>