2231 lines
70 KiB
Vue
2231 lines
70 KiB
Vue
<template>
|
||
<div class="smart-triage-queue">
|
||
<!-- 顶部标题栏 -->
|
||
<div class="header-section">
|
||
<div class="header-left">
|
||
<span class="title">智能分诊排队管理 - 心内科</span>
|
||
</div>
|
||
<div class="header-right">
|
||
<el-button type="primary" @click="handleRefresh">
|
||
<el-icon><Refresh /></el-icon>
|
||
刷新
|
||
</el-button>
|
||
<el-button @click="handleExit">退出</el-button>
|
||
<el-button @click="handleConfig">后台配置</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="main-content">
|
||
<!-- 左侧:智能候选池 -->
|
||
<div class="left-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">① 智能候选池 (已签到未入队)</span>
|
||
</div>
|
||
<div class="table-container">
|
||
<el-table
|
||
:data="filteredCandidatePoolList"
|
||
stripe
|
||
border
|
||
height="100%"
|
||
style="width: 100%"
|
||
@selection-change="handleCandidateSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<el-table-column prop="sequenceNo" label="序号" width="80" align="center" />
|
||
<el-table-column prop="patientName" label="患者" width="100" align="center" />
|
||
<el-table-column prop="age" label="年龄" width="80" align="center" />
|
||
<el-table-column prop="appointmentType" label="号别" width="100" align="center" />
|
||
<el-table-column prop="room" label="诊室" width="120" align="center" />
|
||
<el-table-column prop="doctor" label="医生" width="120" align="center" />
|
||
<el-table-column prop="matchingRule" label="命中规则" min-width="150" align="center" />
|
||
</el-table>
|
||
</div>
|
||
<div class="candidate-actions">
|
||
<el-button
|
||
type="primary"
|
||
@click="handleAddToQueue"
|
||
:disabled="selectedCandidates.length === 0"
|
||
>
|
||
加入队列 >>
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleAddAllToQueue"
|
||
:disabled="filteredCandidatePoolList.length === 0"
|
||
>
|
||
一键加入队列
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:智能队列 -->
|
||
<div class="right-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">② 智能队列 (全科)</span>
|
||
</div>
|
||
<div class="table-container">
|
||
<el-table
|
||
:data="filteredQueueList"
|
||
stripe
|
||
border
|
||
height="100%"
|
||
style="width: 100%"
|
||
:row-class-name="getRowClassName"
|
||
highlight-current-row
|
||
@row-click="handleQueueRowClick"
|
||
>
|
||
<el-table-column prop="queueOrder" label="队序" width="80" align="center" />
|
||
<el-table-column prop="patientName" label="患者" width="100" align="center" />
|
||
<el-table-column prop="appointmentType" label="号别" width="100" align="center" />
|
||
<el-table-column prop="room" label="诊室" width="120" align="center" />
|
||
<el-table-column prop="doctor" label="医生" width="120" align="center" />
|
||
<el-table-column prop="waitingTime" label="等待" width="100" align="center" />
|
||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-tag :type="getStatusTagType(scope.row.status)">
|
||
{{ scope.row.status }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div class="display-options">
|
||
<div class="queue-actions-left">
|
||
<el-button
|
||
type="danger"
|
||
@click="handleRemoveFromQueue"
|
||
:disabled="!selectedQueueRow"
|
||
size="small"
|
||
>
|
||
<< 移出队列
|
||
</el-button>
|
||
<el-button
|
||
type="info"
|
||
@click="handleMoveUp"
|
||
:disabled="!selectedQueueRow || !canMoveUp"
|
||
size="small"
|
||
>
|
||
↑
|
||
</el-button>
|
||
<el-button
|
||
type="info"
|
||
@click="handleMoveDown"
|
||
:disabled="!selectedQueueRow || !canMoveDown"
|
||
size="small"
|
||
>
|
||
↓
|
||
</el-button>
|
||
</div>
|
||
<div class="queue-actions-right">
|
||
<el-button
|
||
:type="showOnlyWaiting ? 'primary' : ''"
|
||
@click="showOnlyWaiting = true"
|
||
size="small"
|
||
>
|
||
只显示等待
|
||
</el-button>
|
||
<el-button
|
||
:type="!showOnlyWaiting ? 'primary' : ''"
|
||
@click="showOnlyWaiting = false"
|
||
size="small"
|
||
>
|
||
显示全部状态
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部控制面板 -->
|
||
<div class="footer-section">
|
||
<!-- 就诊科室快速过滤栏 -->
|
||
<div class="filter-section">
|
||
<div class="filter-label">③ 就诊科室快速过滤栏</div>
|
||
<div class="filter-select-wrapper">
|
||
<el-select
|
||
v-model="selectedDept"
|
||
placeholder="请选择就诊科室"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
size="default"
|
||
>
|
||
<el-option
|
||
label="全部"
|
||
value="all"
|
||
/>
|
||
<el-option
|
||
v-for="dept in departmentList"
|
||
:key="dept.id"
|
||
:label="dept.name"
|
||
:value="dept.id"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 叫号控制板 -->
|
||
<div class="call-control-section">
|
||
<div class="call-control-label">④ 叫号控制板</div>
|
||
<div class="call-control-content">
|
||
<div class="current-call-display">
|
||
当前呼叫: {{ currentCall.number }} {{ currentCall.name }} 诊室: {{ currentCall.room }}
|
||
</div>
|
||
<div class="control-buttons">
|
||
<el-button type="primary" @click="handleSelectCall">选呼</el-button>
|
||
<el-button type="success" @click="handleNextPatient">下一患者</el-button>
|
||
<el-button type="warning" @click="handleSkip">跳过</el-button>
|
||
<el-button type="primary" @click="handleComplete">完成</el-button>
|
||
<el-button type="info" @click="handleRequeue">过号重排</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LED显示 -->
|
||
<div class="led-section">
|
||
<div class="led-label">⑤ LED:</div>
|
||
<div class="led-display">
|
||
[{{ currentCall.number }}]{{ currentCall.name }}请到{{ currentCall.room }}({{ callType }})
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 语音提示 -->
|
||
<div class="voice-section">
|
||
<span class="voice-label">语音:</span>
|
||
<span class="voice-text">请{{ currentCall.number }}号{{ currentCall.name }}到{{ currentCall.room }}({{ callType }})</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 后台配置弹窗 -->
|
||
<el-dialog
|
||
v-model="configDialogVisible"
|
||
width="90%"
|
||
top="5vh"
|
||
>
|
||
<template #header>
|
||
<div class="config-dialog-header">
|
||
<div class="config-dialog-title">智能分诊规则引擎配置 - 心内科</div>
|
||
<div class="config-topbar-actions">
|
||
<el-button type="primary" @click="handleAddRule">新增规则</el-button>
|
||
<el-button type="primary" @click="handleSaveAllRules">保存全部</el-button>
|
||
<el-button @click="handleTestRule">测试规则</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="config-container">
|
||
<div class="config-left">
|
||
<el-scrollbar height="560px">
|
||
<div
|
||
v-for="(item, idx) in rules"
|
||
:key="idx"
|
||
class="rule-card"
|
||
:class="{ active: idx === editingIndex }"
|
||
@click="handleSelectRule(idx)"
|
||
>
|
||
<div class="rule-title">规则{{ idx + 1 }}</div>
|
||
<div class="rule-sub">prio={{ item.priority }}</div>
|
||
<div class="rule-sub">{{ item.name }}</div>
|
||
<div class="rule-actions">
|
||
<el-button size="small" @click.stop="handleSelectRule(idx)">编辑</el-button>
|
||
<el-button size="small" @click.stop="handleDeleteRule(idx)">删除</el-button>
|
||
<el-button size="small" @click.stop="handleRuleMoveUp(idx)" :disabled="idx === 0">↑</el-button>
|
||
<el-button size="small" @click.stop="handleRuleMoveDown(idx)" :disabled="idx === rules.length - 1">↓</el-button>
|
||
</div>
|
||
</div>
|
||
</el-scrollbar>
|
||
</div>
|
||
|
||
<div class="config-right">
|
||
<el-form label-width="110px" class="config-form">
|
||
<el-form-item label="规则名称:" required>
|
||
<el-input v-model="ruleForm.name" placeholder="请输入规则名称" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="科室:">
|
||
<el-select v-model="ruleForm.dept" class="config-fullwidth" disabled>
|
||
<el-option label="心内科" value="心内科" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="规则描述:">
|
||
<el-input v-model="ruleForm.desc" placeholder="请输入规则描述" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="优先级:" required>
|
||
<el-input v-model="ruleForm.priority" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="周几生效:">
|
||
<el-checkbox-group v-model="ruleForm.weeks">
|
||
<el-checkbox v-for="w in weekOptions" :key="w.value" :label="w.value">
|
||
{{ w.label }}
|
||
</el-checkbox>
|
||
</el-checkbox-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="条件表达式(JSON):">
|
||
<el-input
|
||
v-model="ruleForm.expr"
|
||
type="textarea"
|
||
:rows="8"
|
||
placeholder='{"age":">=60","regType":"专家"}'
|
||
/>
|
||
</el-form-item>
|
||
|
||
<div class="config-inline-actions">
|
||
<el-button @click="handleQuickGenerate">快速生成器</el-button>
|
||
<el-button @click="handleValidateRule">语法检查</el-button>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="config-footer">
|
||
<el-button type="primary" @click="handleSaveCurrentRule">保存</el-button>
|
||
<el-button @click="configDialogVisible = false">取消</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 快速生成器对话框 -->
|
||
<el-dialog
|
||
v-model="quickGeneratorDialogVisible"
|
||
title="快速生成器"
|
||
width="600px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form :model="quickGeneratorForm" label-width="120px">
|
||
<el-form-item label="年龄条件:">
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<el-select v-model="quickGeneratorForm.ageOperator" style="width: 100px;">
|
||
<el-option label=">=" value=">=" />
|
||
<el-option label="<=" value="<=" />
|
||
<el-option label="=" value="=" />
|
||
<el-option label=">" value=">" />
|
||
<el-option label="<" value="<" />
|
||
</el-select>
|
||
<el-input-number
|
||
v-model="quickGeneratorForm.ageValue"
|
||
:min="0"
|
||
:max="150"
|
||
:precision="0"
|
||
placeholder="年龄"
|
||
style="width: 150px;"
|
||
/>
|
||
<el-button
|
||
type="text"
|
||
@click="quickGeneratorForm.ageOperator = null; quickGeneratorForm.ageValue = null"
|
||
>
|
||
清除
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="号别/类型:">
|
||
<el-select
|
||
v-model="quickGeneratorForm.regType"
|
||
placeholder="请选择号别"
|
||
clearable
|
||
style="width: 100%"
|
||
>
|
||
<el-option label="专家" value="专家" />
|
||
<el-option label="普通" value="普通" />
|
||
<el-option label="特需" value="特需" />
|
||
<el-option label="急诊" value="急诊" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="科室:">
|
||
<el-select
|
||
v-model="quickGeneratorForm.dept"
|
||
placeholder="请选择科室"
|
||
clearable
|
||
style="width: 100%"
|
||
>
|
||
<el-option label="心内科" value="心内科" />
|
||
<el-option label="心外科" value="心外科" />
|
||
<el-option label="神经内科" value="神经内科" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="医生:">
|
||
<el-input
|
||
v-model="quickGeneratorForm.doctor"
|
||
placeholder="请输入医生姓名(支持模糊匹配)"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="自定义条件:">
|
||
<el-input
|
||
v-model="quickGeneratorForm.customKey"
|
||
placeholder="字段名(如:gender)"
|
||
style="width: 150px; margin-right: 10px;"
|
||
/>
|
||
<el-input
|
||
v-model="quickGeneratorForm.customValue"
|
||
placeholder="字段值(如:男)"
|
||
style="width: 200px;"
|
||
/>
|
||
<el-button
|
||
type="text"
|
||
@click="quickGeneratorForm.customKey = ''; quickGeneratorForm.customValue = ''"
|
||
>
|
||
清除
|
||
</el-button>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="预览JSON:">
|
||
<el-input
|
||
:value="previewJson"
|
||
type="textarea"
|
||
:rows="4"
|
||
readonly
|
||
style="font-family: monospace;"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div style="text-align: right;">
|
||
<el-button @click="quickGeneratorDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleApplyQuickGenerate">应用</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||
import { Refresh } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
getCandidatePool,
|
||
getLocationTree,
|
||
getTriageQueueList,
|
||
addToQueue,
|
||
removeFromQueue,
|
||
adjustQueueOrder,
|
||
callPatient,
|
||
skipPatient,
|
||
completeCall,
|
||
requeuePatient,
|
||
nextPatient
|
||
} from '../api'
|
||
|
||
// 当前日期 & 统计信息(总已签到/在队列中)
|
||
const currentDate = ref('2025/12/22 上午')
|
||
const totalSignedIn = ref(0)
|
||
const totalInQueue = ref(0)
|
||
|
||
// 当前呼叫信息
|
||
const currentCall = ref({
|
||
number: '1',
|
||
name: '郑华',
|
||
room: '4号诊室'
|
||
})
|
||
|
||
// 当前选中的队列行(用于选呼)
|
||
const selectedQueueRow = ref(null)
|
||
|
||
// 选中的候选池患者(多选)
|
||
const selectedCandidates = ref([])
|
||
|
||
// 显示选项
|
||
const showOnlyWaiting = ref(false)
|
||
|
||
// 科室过滤(改为使用就诊科室)
|
||
const selectedDept = ref('all')
|
||
// 就诊科室列表
|
||
const departmentList = ref([])
|
||
|
||
// 后台配置对话框
|
||
const configDialogVisible = ref(false)
|
||
const rules = ref([
|
||
{
|
||
name: '老年专家优先',
|
||
dept: '心内科',
|
||
desc: '老年专家优先',
|
||
priority: 10,
|
||
weeks: ['1', '2', '3', '4', '5'],
|
||
expr: '{"age":">=60","regType":"专家"}'
|
||
},
|
||
{
|
||
name: '少年优先',
|
||
dept: '心内科',
|
||
desc: '少年优先',
|
||
priority: 20,
|
||
weeks: ['1', '2', '3', '4', '5'],
|
||
expr: '{"age":"<18"}'
|
||
},
|
||
{
|
||
name: '默认',
|
||
dept: '心内科',
|
||
desc: '默认',
|
||
priority: 50,
|
||
weeks: ['1', '2', '3', '4', '5', '6', '0'],
|
||
expr: '{}'
|
||
}
|
||
])
|
||
const editingIndex = ref(0)
|
||
const ruleForm = reactive({
|
||
name: '',
|
||
dept: '心内科',
|
||
desc: '',
|
||
priority: 10,
|
||
weeks: ['1', '2', '3', '4', '5'],
|
||
expr: ''
|
||
})
|
||
|
||
// 快速生成器对话框
|
||
const quickGeneratorDialogVisible = ref(false)
|
||
const quickGeneratorForm = reactive({
|
||
ageOperator: null, // >=, <=, =, >, <
|
||
ageValue: null,
|
||
regType: null, // 号别
|
||
dept: null, // 科室
|
||
doctor: null, // 医生
|
||
customKey: '', // 自定义字段名
|
||
customValue: '' // 自定义字段值
|
||
})
|
||
const weekOptions = [
|
||
{ label: '一', value: '1' },
|
||
{ label: '二', value: '2' },
|
||
{ label: '三', value: '3' },
|
||
{ label: '四', value: '4' },
|
||
{ label: '五', value: '5' },
|
||
{ label: '六', value: '6' },
|
||
{ label: '日', value: '0' }
|
||
]
|
||
|
||
// 叫号类型
|
||
const callType = ref('选呼')
|
||
|
||
// ============ 数据初始化/刷新(前端模拟) ============
|
||
const getInitialCandidatePoolList = () => ([
|
||
{
|
||
sequenceNo: 12,
|
||
patientName: '陈明',
|
||
age: 65,
|
||
appointmentType: '专家',
|
||
room: '3号诊室',
|
||
doctor: '张医生',
|
||
matchingRule: '年龄≥60'
|
||
},
|
||
{
|
||
sequenceNo: 13,
|
||
patientName: '刘芳',
|
||
age: 58,
|
||
appointmentType: '普通',
|
||
room: '4号诊室',
|
||
doctor: '李医生',
|
||
matchingRule: '-'
|
||
},
|
||
{
|
||
sequenceNo: 14,
|
||
patientName: '周强',
|
||
age: 45,
|
||
appointmentType: '普通',
|
||
room: '5号诊室',
|
||
doctor: '王医生',
|
||
matchingRule: '-'
|
||
},
|
||
{
|
||
sequenceNo: 15,
|
||
patientName: '吴伟',
|
||
age: 72,
|
||
appointmentType: '专家',
|
||
room: '3号诊室',
|
||
doctor: '张医生',
|
||
matchingRule: '年龄≥70'
|
||
}
|
||
])
|
||
|
||
const getInitialQueueList = () => ([
|
||
{
|
||
queueOrder: 1,
|
||
patientName: '林静',
|
||
appointmentType: '专家',
|
||
room: '3号诊室',
|
||
doctor: '张医生',
|
||
waitingTime: '05:00',
|
||
status: '等待'
|
||
},
|
||
{
|
||
queueOrder: 2,
|
||
patientName: '郑华',
|
||
appointmentType: '普通',
|
||
room: '4号诊室',
|
||
doctor: '李医生',
|
||
waitingTime: '00:00',
|
||
status: '叫号中'
|
||
},
|
||
{
|
||
queueOrder: 3,
|
||
patientName: '王丽',
|
||
appointmentType: '普通',
|
||
room: '5号诊室',
|
||
doctor: '王医生',
|
||
waitingTime: '08:00',
|
||
status: '等待'
|
||
},
|
||
{
|
||
queueOrder: 4,
|
||
patientName: '张伟',
|
||
appointmentType: '专家',
|
||
room: '3号诊室',
|
||
doctor: '张医生',
|
||
waitingTime: '12:00',
|
||
status: '等待'
|
||
}
|
||
])
|
||
|
||
const syncCurrentCallFromQueue = () => {
|
||
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
|
||
if (!calling) {
|
||
currentCall.value = { number: '-', name: '-', room: '-' }
|
||
selectedQueueRow.value = null
|
||
return
|
||
}
|
||
currentCall.value = {
|
||
number: String(calling.queueOrder),
|
||
name: calling.patientName,
|
||
room: calling.room
|
||
}
|
||
selectedQueueRow.value = calling
|
||
}
|
||
|
||
// ============ 与后端接口联动 ============
|
||
// 注意:后端未实现或出错时会自动退回本地假数据
|
||
// 将分钟数转换为 MM:SS 格式
|
||
const formatMinutesToMmSs = (minutes) => {
|
||
if (minutes == null || minutes < 0) return '00:00'
|
||
const hours = Math.floor(minutes / 60)
|
||
const mins = minutes % 60
|
||
const hoursStr = String(hours).padStart(2, '0')
|
||
const minsStr = String(mins).padStart(2, '0')
|
||
return `${hoursStr}:${minsStr}`
|
||
}
|
||
|
||
// 将年龄字符串转换为数字(处理 "20岁"、"1小时" 等格式)
|
||
const parseAge = (ageStr) => {
|
||
if (!ageStr) return 0
|
||
const match = ageStr.match(/(\d+)/)
|
||
return match ? parseInt(match[1], 10) : 0
|
||
}
|
||
|
||
// 后端队列状态 -> 前端展示状态
|
||
const mapBackendStatusToFrontend = (status) => {
|
||
if (!status) {
|
||
console.warn('【心内科】状态映射:收到空状态值')
|
||
return '等待'
|
||
}
|
||
// 转换为大写并去除空格,确保匹配
|
||
const normalizedStatus = String(status).trim().toUpperCase()
|
||
if (normalizedStatus === 'CALLING') return '叫号中'
|
||
if (normalizedStatus === 'WAITING') return '等待'
|
||
if (normalizedStatus === 'SKIPPED') return '跳过'
|
||
if (normalizedStatus === 'COMPLETED') return '已完成'
|
||
console.warn('【心内科】状态映射:未知状态值', status, '-> 默认返回"等待"')
|
||
return '等待'
|
||
}
|
||
|
||
// 前端状态 -> 后端状态(目前仅展示用)
|
||
const mapFrontendStatusToBackend = (status) => {
|
||
if (!status) return 'WAITING'
|
||
if (status === '叫号中') return 'CALLING'
|
||
if (status === '等待') return 'WAITING'
|
||
if (status === '跳过') return 'SKIPPED'
|
||
if (status === '已完成') return 'COMPLETED'
|
||
return 'WAITING'
|
||
}
|
||
|
||
// 从数据库加载队列
|
||
const loadQueueFromDb = async () => {
|
||
try {
|
||
// 如果选择了具体科室,就按科室加载;否则加载当前登录人科室(后端默认)
|
||
const organizationId = selectedDept.value !== 'all' ? selectedDept.value : undefined
|
||
// 只查询今天的患者
|
||
const today = new Date()
|
||
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
||
console.log('【心内科】loadQueueFromDb 开始:organizationId=', organizationId, 'date=', todayStr, 'selectedDept=', selectedDept.value)
|
||
const res = await getTriageQueueList({ organizationId, date: todayStr }).catch((err) => {
|
||
console.error('【心内科】loadQueueFromDb 请求异常:', err)
|
||
return { code: 500, msg: err?.message || '请求失败', data: null }
|
||
})
|
||
console.log('【心内科】loadQueueFromDb 后端响应:', res)
|
||
|
||
// 检查响应是否成功
|
||
if (res?.code !== 200 && res?.code !== undefined) {
|
||
console.error('【心内科】loadQueueFromDb 后端返回错误:', res?.code, res?.msg)
|
||
// 即使后端返回错误,也不清空队列,避免误删数据
|
||
// originalQueueList.value 保持原值
|
||
return
|
||
}
|
||
|
||
const list = Array.isArray(res?.data) ? res.data : []
|
||
console.log('【心内科】loadQueueFromDb 从后端获取到', list.length, '条队列数据')
|
||
if (list.length === 0) {
|
||
console.log('【心内科】提示:队列数据为空(可能是今天还没有患者加入队列)')
|
||
}
|
||
|
||
originalQueueList.value = list
|
||
.map((it) => {
|
||
const frontendStatus = mapBackendStatusToFrontend(it.status)
|
||
// 调试日志:检查状态映射
|
||
if (list.length <= 5) {
|
||
console.log('【心内科】状态映射:后端状态=', it.status, '-> 前端状态=', frontendStatus, '患者=', it.patientName)
|
||
}
|
||
// 计算等待时间:基于创建时间(createTime)
|
||
let waitingTime = '00:00'
|
||
if (it.createTime) {
|
||
try {
|
||
const createTime = new Date(it.createTime)
|
||
// 检查日期是否有效
|
||
if (!isNaN(createTime.getTime())) {
|
||
const now = new Date()
|
||
const diffSeconds = Math.floor((now - createTime) / 1000)
|
||
waitingTime = formatSecondsToMmSs(Math.max(0, diffSeconds))
|
||
} else {
|
||
console.warn('【心内科】无效的创建时间:', it.createTime, '患者:', it.patientName)
|
||
}
|
||
} catch (e) {
|
||
console.error('【心内科】解析创建时间失败:', it.createTime, '患者:', it.patientName, e)
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: it.id,
|
||
queueOrder: it.queueOrder,
|
||
patientName: it.patientName ?? '-',
|
||
appointmentType: it.healthcareName ?? '普通',
|
||
room: it.organizationName ?? '-',
|
||
doctor: it.practitionerName ?? '-',
|
||
waitingTime: waitingTime,
|
||
createTime: it.createTime, // 保存创建时间,用于定时器计算
|
||
status: frontendStatus,
|
||
// 关联字段(用于去重/追溯)
|
||
encounterId: it.encounterId,
|
||
organizationId: it.organizationId
|
||
}
|
||
})
|
||
.filter((item) => {
|
||
// 过滤掉"已完成"状态的患者,不显示在队列中
|
||
if (item.status === '已完成') {
|
||
console.log('【心内科】过滤掉已完成状态的患者:', item.patientName)
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
// 调试日志:检查查找结果
|
||
const callingCount = originalQueueList.value.filter(i => i.status === '叫号中').length
|
||
const waitingCount = originalQueueList.value.filter(i => i.status === '等待').length
|
||
console.log('【心内科】队列状态统计:总数=', originalQueueList.value.length, '叫号中=', callingCount, '等待=', waitingCount)
|
||
if (originalQueueList.value.length > 0) {
|
||
console.log('【心内科】队列中的 encounterIds:', originalQueueList.value.map(q => ({ name: q.patientName, encounterId: q.encounterId })))
|
||
}
|
||
|
||
// 统计
|
||
totalInQueue.value = originalQueueList.value.length
|
||
syncCurrentCallFromQueue()
|
||
} catch (e) {
|
||
console.error('【心内科】loadQueueFromDb 异常:', e)
|
||
// 不回退本地假数据,避免“看起来有数据但实际没落库”的误导
|
||
originalQueueList.value = []
|
||
totalInQueue.value = 0
|
||
syncCurrentCallFromQueue()
|
||
}
|
||
}
|
||
|
||
// 根据状态枚举文本转换为前端状态
|
||
const mapStatusToFrontend = (statusEnumText) => {
|
||
if (!statusEnumText) return '等待'
|
||
// 状态映射:就诊中 -> 叫号中,其他 -> 等待
|
||
if (statusEnumText.includes('就诊中') || statusEnumText.includes('在诊')) {
|
||
return '叫号中'
|
||
} else if (statusEnumText.includes('已完成') || statusEnumText.includes('已出院')) {
|
||
return '已完成'
|
||
} else if (statusEnumText.includes('已取消') || statusEnumText.includes('退号')) {
|
||
return '已取消'
|
||
}
|
||
return '等待'
|
||
}
|
||
|
||
// 判断日期是否是今天
|
||
const isToday = (date) => {
|
||
if (!date) return false
|
||
const targetDate = new Date(date)
|
||
const today = new Date()
|
||
return targetDate.getFullYear() === today.getFullYear() &&
|
||
targetDate.getMonth() === today.getMonth() &&
|
||
targetDate.getDate() === today.getDate()
|
||
}
|
||
|
||
// 过滤今天的数据
|
||
const filterTodayData = (data) => {
|
||
if (!Array.isArray(data)) return []
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
const tomorrow = new Date(today)
|
||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||
|
||
return data.filter(item => {
|
||
if (!item.registerTime) return false
|
||
const registerDate = new Date(item.registerTime)
|
||
return registerDate >= today && registerDate < tomorrow
|
||
})
|
||
}
|
||
|
||
const loadDataFromApi = async () => {
|
||
// 明确打日志,方便你在浏览器 Console 里看到是否调用到了这里
|
||
console.log('【心内科】loadDataFromApi 被调用了:候选池=门诊挂号接口,队列=数据库接口')
|
||
try {
|
||
const baseParams = {}
|
||
|
||
// 1) 候选池(使用门诊挂号当日已挂号接口)
|
||
|
||
const candidateRes = await getCandidatePool(baseParams).catch((err) => {
|
||
|
||
return null
|
||
})
|
||
|
||
if (candidateRes && candidateRes.data != null) {
|
||
// 门诊挂号接口返回的是分页数据 { records: [], total: 0 }
|
||
const records = candidateRes.data.records || candidateRes.data
|
||
const data = Array.isArray(records) ? records : (Array.isArray(candidateRes.data) ? candidateRes.data : [])
|
||
console.log('【心内科】候选池数据:', data, '类型:', typeof data, '是否为数组:', Array.isArray(data), '数量:', data.length)
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
// 过滤今天的数据
|
||
const todayData = filterTodayData(data)
|
||
console.log('【心内科】过滤后的今天数据:', todayData.length, '条(原始:', data.length, '条)')
|
||
// 门诊挂号 CurrentDayEncounterDto → 候选池
|
||
originalCandidatePoolList.value = todayData.map((item, idx) => ({
|
||
sequenceNo: idx + 1,
|
||
encounterId: item.encounterId,
|
||
patientId: item.patientId,
|
||
organizationId: item.organizationId,
|
||
organizationName: item.organizationName,
|
||
patientName: item.patientName ?? '-',
|
||
age: parseAge(item.age),
|
||
appointmentType: item.healthcareName ?? '普通',
|
||
room: item.organizationName ? `${item.organizationName}` : '-',
|
||
doctor: item.practitionerName ?? '-',
|
||
matchingRule: '-' // 这里先不做智能规则匹配
|
||
}))
|
||
console.log('【心内科】候选池已加载', originalCandidatePoolList.value.length, '条今天的数据')
|
||
} else {
|
||
console.log('【心内科】候选池数据为空数组或非数组,使用默认数据')
|
||
originalCandidatePoolList.value = getInitialCandidatePoolList()
|
||
}
|
||
} else {
|
||
console.log('【心内科】候选池响应为空或格式错误,使用默认数据')
|
||
originalCandidatePoolList.value = getInitialCandidatePoolList()
|
||
}
|
||
|
||
// 2) 队列列表:从数据库读取(可刷新、可恢复)
|
||
await loadQueueFromDb()
|
||
|
||
// 3) 候选池去重:已经在队列里的患者(按 encounterId)不再出现在候选池
|
||
// 注意:确保 encounterId 类型一致(统一转换为字符串进行比较,避免数字和字符串不匹配)
|
||
const queueEncounterIds = new Set(
|
||
originalQueueList.value
|
||
.map((q) => q.encounterId)
|
||
.filter((id) => id != null && id !== undefined && id !== '')
|
||
.map((id) => String(id)) // 统一转换为字符串
|
||
)
|
||
console.log('【心内科】去重前:候选池', originalCandidatePoolList.value.length, '条,队列', originalQueueList.value.length, '条,队列encounterIds=', Array.from(queueEncounterIds))
|
||
if (queueEncounterIds.size > 0) {
|
||
const beforeCount = originalCandidatePoolList.value.length
|
||
originalCandidatePoolList.value = originalCandidatePoolList.value.filter((c) => {
|
||
const candidateId = c.encounterId != null ? String(c.encounterId) : null
|
||
return candidateId == null || !queueEncounterIds.has(candidateId)
|
||
})
|
||
console.log('【心内科】去重后:候选池', originalCandidatePoolList.value.length, '条(移除了', beforeCount - originalCandidatePoolList.value.length, '条)')
|
||
// 如果去重后数量没有变化,可能是类型不匹配问题
|
||
if (beforeCount === originalCandidatePoolList.value.length && beforeCount > 0) {
|
||
console.warn('【心内科】警告:去重前后数量未变化,可能是 encounterId 类型不匹配!')
|
||
console.warn(' 候选池中的 encounterIds:', originalCandidatePoolList.value.slice(0, 3).map(c => ({ name: c.patientName, id: c.encounterId, type: typeof c.encounterId })))
|
||
console.warn(' 队列中的 encounterIds:', originalQueueList.value.slice(0, 3).map(q => ({ name: q.patientName, id: q.encounterId, type: typeof q.encounterId })))
|
||
}
|
||
} else {
|
||
console.warn('【心内科】警告:队列中没有 encounterId,无法进行去重!')
|
||
if (originalQueueList.value.length > 0) {
|
||
console.warn(' 队列数据详情:', originalQueueList.value.slice(0, 3).map(q => ({ name: q.patientName, encounterId: q.encounterId })))
|
||
}
|
||
}
|
||
|
||
// 4) 统计信息(只统计候选池和队列的数据)
|
||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||
console.log('【心内科】统计信息:候选池', totalSignedIn.value, '条,队列', totalInQueue.value, '条')
|
||
|
||
// 当前日期展示(简单用本机时间)
|
||
const now = new Date()
|
||
currentDate.value = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`
|
||
|
||
// 同步当前呼叫(队列从 DB 加载后已同步;这里再兜底一次)
|
||
syncCurrentCallFromQueue()
|
||
console.log('【心内科】数据加载完成:候选池', originalCandidatePoolList.value.length, '条,队列', originalQueueList.value.length, '条')
|
||
ElMessage.success('【心内科】已从门诊挂号接口加载数据')
|
||
} catch (e) {
|
||
console.error('【心内科】loadDataFromApi 执行异常,使用本地假数据:', e)
|
||
// 任何异常:回退本地假数据
|
||
originalCandidatePoolList.value = getInitialCandidatePoolList()
|
||
// 队列不再回退假数据,避免误导
|
||
originalQueueList.value = []
|
||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||
totalInQueue.value = originalQueueList.value.length
|
||
syncCurrentCallFromQueue()
|
||
}
|
||
}
|
||
|
||
// 原始数据存储(用于过滤)
|
||
const originalCandidatePoolList = ref(getInitialCandidatePoolList())
|
||
|
||
// 辅助函数:扁平化科室树形结构
|
||
const flattenDepartmentTree = (tree, result = []) => {
|
||
if (!Array.isArray(tree)) return result
|
||
tree.forEach(node => {
|
||
if (node.id && node.name) {
|
||
result.push({ id: node.id, name: node.name })
|
||
}
|
||
if (node.children && Array.isArray(node.children)) {
|
||
flattenDepartmentTree(node.children, result)
|
||
}
|
||
})
|
||
return result
|
||
}
|
||
|
||
// 加载就诊科室列表
|
||
const loadDepartmentList = async () => {
|
||
try {
|
||
const response = await getLocationTree()
|
||
if (response && response.data) {
|
||
// 扁平化树形结构
|
||
departmentList.value = flattenDepartmentTree(response.data)
|
||
console.log('【心内科】已加载就诊科室列表:', departmentList.value.length, '个科室')
|
||
}
|
||
} catch (error) {
|
||
console.error('【心内科】加载就诊科室列表失败:', error)
|
||
ElMessage.warning('加载就诊科室列表失败,使用默认数据')
|
||
}
|
||
}
|
||
|
||
// 获取选中科室的名称
|
||
const getSelectedDeptName = () => {
|
||
if (selectedDept.value === 'all') return null
|
||
const dept = departmentList.value.find(d => d.id === selectedDept.value)
|
||
return dept ? dept.name : null
|
||
}
|
||
|
||
// 过滤后的智能候选池数据
|
||
const filteredCandidatePoolList = computed(() => {
|
||
if (selectedDept.value === 'all') {
|
||
return originalCandidatePoolList.value
|
||
}
|
||
const deptName = getSelectedDeptName()
|
||
if (!deptName) return originalCandidatePoolList.value
|
||
return originalCandidatePoolList.value.filter(item => item.room === deptName)
|
||
})
|
||
|
||
// 原始队列数据存储(用于过滤)
|
||
const originalQueueList = ref(getInitialQueueList())
|
||
|
||
const parseMmSsToSeconds = (mmss) => {
|
||
if (!mmss || typeof mmss !== 'string') return 0
|
||
const [mm, ss] = mmss.split(':')
|
||
const m = Number(mm)
|
||
const s = Number(ss)
|
||
if (Number.isNaN(m) || Number.isNaN(s)) return 0
|
||
return m * 60 + s
|
||
}
|
||
|
||
const formatSecondsToMmSs = (totalSeconds) => {
|
||
const safe = Math.max(0, Math.floor(totalSeconds || 0))
|
||
const mm = String(Math.floor(safe / 60)).padStart(2, '0')
|
||
const ss = String(safe % 60).padStart(2, '0')
|
||
return `${mm}:${ss}`
|
||
}
|
||
|
||
// 过滤后的智能队列数据(同时考虑科室过滤和状态过滤)
|
||
const filteredQueueList = computed(() => {
|
||
let filtered = originalQueueList.value
|
||
|
||
// 先过滤掉"已完成"状态的患者(无论什么情况都不显示)
|
||
filtered = filtered.filter(item => item.status !== '已完成')
|
||
|
||
// 再按科室过滤
|
||
if (selectedDept.value !== 'all') {
|
||
const deptName = getSelectedDeptName()
|
||
if (deptName) {
|
||
filtered = filtered.filter(item => item.room === deptName)
|
||
}
|
||
}
|
||
|
||
// 再按状态过滤(只显示等待)
|
||
if (showOnlyWaiting.value) {
|
||
filtered = filtered.filter(item => item.status === '等待')
|
||
}
|
||
|
||
return filtered
|
||
})
|
||
|
||
const handleQueueRowClick = (row) => {
|
||
selectedQueueRow.value = row
|
||
}
|
||
|
||
// 候选池选择变化
|
||
const handleCandidateSelectionChange = (selection) => {
|
||
selectedCandidates.value = selection
|
||
}
|
||
|
||
// 获取下一个队序号
|
||
const getNextQueueOrder = () => {
|
||
if (originalQueueList.value.length === 0) return 1
|
||
const maxOrder = Math.max(...originalQueueList.value.map(item => item.queueOrder))
|
||
return maxOrder + 1
|
||
}
|
||
|
||
// 重新计算队序号(保持连续)
|
||
const recalculateQueueOrders = () => {
|
||
originalQueueList.value.forEach((item, index) => {
|
||
item.queueOrder = index + 1
|
||
})
|
||
}
|
||
|
||
// 加入队列(选中的候选池患者)——落库
|
||
const handleAddToQueue = async () => {
|
||
if (selectedCandidates.value.length === 0) {
|
||
ElMessage.warning('请先选择要加入队列的患者')
|
||
return
|
||
}
|
||
|
||
// 记录要加入的患者 encounterId(用于后续删除)
|
||
const toAddEncounterIds = new Set()
|
||
selectedCandidates.value.forEach((c) => {
|
||
if (c.encounterId != null) {
|
||
toAddEncounterIds.add(String(c.encounterId))
|
||
}
|
||
})
|
||
|
||
const beforeCount = originalCandidatePoolList.value.length
|
||
console.log('【心内科】handleAddToQueue 开始:选中', selectedCandidates.value.length, '位患者,候选池当前', beforeCount, '条')
|
||
|
||
try {
|
||
const groups = new Map()
|
||
selectedCandidates.value.forEach((c) => {
|
||
if (!c.organizationId) return
|
||
if (!groups.has(c.organizationId)) {
|
||
groups.set(c.organizationId, {
|
||
organizationId: c.organizationId,
|
||
organizationName: c.organizationName || c.room,
|
||
items: []
|
||
})
|
||
}
|
||
groups.get(c.organizationId).items.push({
|
||
encounterId: c.encounterId,
|
||
patientId: c.patientId,
|
||
patientName: c.patientName,
|
||
healthcareName: c.appointmentType,
|
||
practitionerName: c.doctor
|
||
})
|
||
})
|
||
|
||
let added = 0
|
||
const addedEncounterIds = new Set()
|
||
for (const [, payload] of groups) {
|
||
try {
|
||
const res = await addToQueue(payload)
|
||
if (typeof res?.data === 'number') added += res.data
|
||
// 不论后端是否判重,只要尝试加入过的 encounterId,一律从候选池移除
|
||
payload.items.forEach(it => {
|
||
if (it.encounterId != null) {
|
||
addedEncounterIds.add(String(it.encounterId))
|
||
}
|
||
})
|
||
} catch (err) {
|
||
console.error('【心内科】单个分组加入队列失败:', err)
|
||
// 即使失败,也记录 encounterId,后续尝试删除(避免重复尝试)
|
||
payload.items.forEach(it => {
|
||
if (it.encounterId != null) {
|
||
addedEncounterIds.add(String(it.encounterId))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 立即从候选池中删除已加入的患者(无论API是否成功,只要尝试过就删除)
|
||
// 确保 encounterId 类型一致(统一转换为字符串)
|
||
if (addedEncounterIds.size > 0) {
|
||
const beforeDeleteCount = originalCandidatePoolList.value.length
|
||
originalCandidatePoolList.value = originalCandidatePoolList.value.filter(
|
||
c => {
|
||
const candidateId = c.encounterId != null ? String(c.encounterId) : null
|
||
const shouldKeep = candidateId == null || !addedEncounterIds.has(candidateId)
|
||
return shouldKeep
|
||
}
|
||
)
|
||
const deletedCount = beforeDeleteCount - originalCandidatePoolList.value.length
|
||
console.log('【心内科】从候选池删除', deletedCount, '位患者(尝试加入的', addedEncounterIds.size, '位)')
|
||
|
||
// 同时清空选中状态
|
||
selectedCandidates.value = []
|
||
}
|
||
|
||
// 刷新队列数据(从数据库读取最新队列)
|
||
await loadQueueFromDb()
|
||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||
|
||
const afterCount = originalCandidatePoolList.value.length
|
||
console.log('【心内科】handleAddToQueue 完成:候选池从', beforeCount, '条减少到', afterCount, '条,成功加入', added, '位')
|
||
|
||
if (added > 0) {
|
||
ElMessage.success(`成功将 ${added} 位患者加入队列(已保存)`)
|
||
} else if (addedEncounterIds.size > 0) {
|
||
ElMessage.warning('所选患者已在队列中,已从候选池移除')
|
||
} else {
|
||
ElMessage.warning('加入队列失败,请重试')
|
||
}
|
||
} catch (e) {
|
||
console.error('【心内科】handleAddToQueue 异常:', e)
|
||
// 即使出错,也尝试删除已尝试加入的患者(避免重复操作)
|
||
if (toAddEncounterIds.size > 0) {
|
||
const beforeDeleteCount = originalCandidatePoolList.value.length
|
||
originalCandidatePoolList.value = originalCandidatePoolList.value.filter(
|
||
c => {
|
||
const candidateId = c.encounterId != null ? String(c.encounterId) : null
|
||
return candidateId == null || !toAddEncounterIds.has(candidateId)
|
||
}
|
||
)
|
||
const deletedCount = beforeDeleteCount - originalCandidatePoolList.value.length
|
||
console.log('【心内科】异常情况下从候选池删除', deletedCount, '位患者')
|
||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||
selectedCandidates.value = []
|
||
}
|
||
ElMessage.error('加入队列失败(未保存)')
|
||
}
|
||
}
|
||
|
||
// 一键加入队列(所有候选池患者)——落库
|
||
const handleAddAllToQueue = async () => {
|
||
if (filteredCandidatePoolList.value.length === 0) {
|
||
ElMessage.warning('候选池中没有患者')
|
||
return
|
||
}
|
||
|
||
// 记录要加入的所有患者 encounterId(用于后续删除)
|
||
const toAddEncounterIds = new Set()
|
||
filteredCandidatePoolList.value.forEach((c) => {
|
||
if (c.encounterId != null) {
|
||
toAddEncounterIds.add(String(c.encounterId))
|
||
}
|
||
})
|
||
|
||
const beforeCount = originalCandidatePoolList.value.length
|
||
console.log('【心内科】handleAddAllToQueue 开始:候选池当前', beforeCount, '条,过滤后', filteredCandidatePoolList.value.length, '条')
|
||
|
||
try {
|
||
const groups = new Map()
|
||
filteredCandidatePoolList.value.forEach((c) => {
|
||
if (!c.organizationId) return
|
||
if (!groups.has(c.organizationId)) {
|
||
groups.set(c.organizationId, {
|
||
organizationId: c.organizationId,
|
||
organizationName: c.organizationName || c.room,
|
||
items: []
|
||
})
|
||
}
|
||
groups.get(c.organizationId).items.push({
|
||
encounterId: c.encounterId,
|
||
patientId: c.patientId,
|
||
patientName: c.patientName,
|
||
healthcareName: c.appointmentType,
|
||
practitionerName: c.doctor
|
||
})
|
||
})
|
||
|
||
let added = 0
|
||
const addedEncounterIds = new Set()
|
||
for (const [, payload] of groups) {
|
||
try {
|
||
const res = await addToQueue(payload)
|
||
if (typeof res?.data === 'number') added += res.data
|
||
// 不论后端是否判重,只要尝试加入过的 encounterId,一律从候选池移除
|
||
payload.items.forEach(it => {
|
||
if (it.encounterId != null) {
|
||
addedEncounterIds.add(String(it.encounterId))
|
||
}
|
||
})
|
||
} catch (err) {
|
||
console.error('【心内科】单个分组一键加入队列失败:', err)
|
||
// 即使失败,也记录 encounterId,后续尝试删除(避免重复尝试)
|
||
payload.items.forEach(it => {
|
||
if (it.encounterId != null) {
|
||
addedEncounterIds.add(String(it.encounterId))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 立即从候选池中删除已加入的患者(无论API是否成功,只要尝试过就删除)
|
||
// 确保 encounterId 类型一致(统一转换为字符串)
|
||
if (addedEncounterIds.size > 0) {
|
||
const beforeDeleteCount = originalCandidatePoolList.value.length
|
||
originalCandidatePoolList.value = originalCandidatePoolList.value.filter(
|
||
c => {
|
||
const candidateId = c.encounterId != null ? String(c.encounterId) : null
|
||
const shouldKeep = candidateId == null || !addedEncounterIds.has(candidateId)
|
||
return shouldKeep
|
||
}
|
||
)
|
||
const deletedCount = beforeDeleteCount - originalCandidatePoolList.value.length
|
||
console.log('【心内科】从候选池删除', deletedCount, '位患者(尝试加入的', addedEncounterIds.size, '位)')
|
||
|
||
// 同时清空选中状态
|
||
selectedCandidates.value = []
|
||
}
|
||
|
||
// 刷新队列数据(从数据库读取最新队列)
|
||
await loadQueueFromDb()
|
||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||
|
||
const afterCount = originalCandidatePoolList.value.length
|
||
console.log('【心内科】handleAddAllToQueue 完成:候选池从', beforeCount, '条减少到', afterCount, '条,成功加入', added, '位')
|
||
|
||
if (added > 0) {
|
||
ElMessage.success(`成功将 ${added} 位患者加入队列(已保存)`)
|
||
} else if (addedEncounterIds.size > 0) {
|
||
ElMessage.warning('所有候选池患者已在队列中,已从候选池移除')
|
||
} else {
|
||
ElMessage.warning('一键加入失败,请重试')
|
||
}
|
||
} catch (e) {
|
||
console.error('【心内科】handleAddAllToQueue 异常:', e)
|
||
// 即使出错,也尝试删除已尝试加入的患者(避免重复操作)
|
||
if (toAddEncounterIds.size > 0) {
|
||
const beforeDeleteCount = originalCandidatePoolList.value.length
|
||
originalCandidatePoolList.value = originalCandidatePoolList.value.filter(
|
||
c => {
|
||
const candidateId = c.encounterId != null ? String(c.encounterId) : null
|
||
return candidateId == null || !toAddEncounterIds.has(candidateId)
|
||
}
|
||
)
|
||
const deletedCount = beforeDeleteCount - originalCandidatePoolList.value.length
|
||
console.log('【心内科】异常情况下从候选池删除', deletedCount, '位患者')
|
||
totalSignedIn.value = originalCandidatePoolList.value.length
|
||
selectedCandidates.value = []
|
||
}
|
||
ElMessage.error('一键加入失败(未保存)')
|
||
}
|
||
}
|
||
|
||
// 移出队列——落库
|
||
const handleRemoveFromQueue = async () => {
|
||
if (!selectedQueueRow.value) {
|
||
ElMessage.warning('请先选择要移出队列的患者')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const row = selectedQueueRow.value
|
||
if (!row.id) {
|
||
ElMessage.warning('该队列项缺少ID,无法保存删除,请先刷新页面')
|
||
return
|
||
}
|
||
|
||
const patientName = row.patientName || '患者'
|
||
console.log('【心内科】开始移出队列:', patientName, 'encounterId:', row.encounterId)
|
||
|
||
await removeFromQueue(row.id)
|
||
selectedQueueRow.value = null
|
||
|
||
// 刷新数据(候选池和队列)
|
||
await loadDataFromApi()
|
||
|
||
console.log('【心内科】移出队列完成,患者应已重新出现在候选池中')
|
||
ElMessage.success(`已将 ${patientName} 移出队列,患者已重新出现在候选池中(已保存)`)
|
||
} catch (e) {
|
||
console.error('【心内科】handleRemoveFromQueue 异常:', e)
|
||
ElMessage.error('移出队列失败(未保存)')
|
||
}
|
||
}
|
||
|
||
// 是否可以上移
|
||
const canMoveUp = computed(() => {
|
||
if (!selectedQueueRow.value) return false
|
||
const index = originalQueueList.value.findIndex(
|
||
item => item.queueOrder === selectedQueueRow.value.queueOrder
|
||
)
|
||
return index > 0
|
||
})
|
||
|
||
// 是否可以下移
|
||
const canMoveDown = computed(() => {
|
||
if (!selectedQueueRow.value) return false
|
||
const index = originalQueueList.value.findIndex(
|
||
item => item.queueOrder === selectedQueueRow.value.queueOrder
|
||
)
|
||
return index >= 0 && index < originalQueueList.value.length - 1
|
||
})
|
||
|
||
// 上移——落库
|
||
const handleMoveUp = async () => {
|
||
if (!selectedQueueRow.value || !canMoveUp.value) return
|
||
if (!selectedQueueRow.value.id) {
|
||
ElMessage.warning('该队列项缺少ID,无法保存排序,请先刷新页面')
|
||
return
|
||
}
|
||
try {
|
||
await adjustQueueOrder({ id: selectedQueueRow.value.id, direction: 'up' })
|
||
await loadDataFromApi()
|
||
ElMessage.success('队列顺序已调整(已保存)')
|
||
} catch (e) {
|
||
console.error('【心内科】handleMoveUp 异常:', e)
|
||
ElMessage.error('队列顺序调整失败(未保存)')
|
||
}
|
||
}
|
||
|
||
// 下移——落库
|
||
const handleMoveDown = async () => {
|
||
if (!selectedQueueRow.value || !canMoveDown.value) return
|
||
if (!selectedQueueRow.value.id) {
|
||
ElMessage.warning('该队列项缺少ID,无法保存排序,请先刷新页面')
|
||
return
|
||
}
|
||
try {
|
||
await adjustQueueOrder({ id: selectedQueueRow.value.id, direction: 'down' })
|
||
await loadDataFromApi()
|
||
ElMessage.success('队列顺序已调整(已保存)')
|
||
} catch (e) {
|
||
console.error('【心内科】handleMoveDown 异常:', e)
|
||
ElMessage.error('队列顺序调整失败(未保存)')
|
||
}
|
||
}
|
||
|
||
let waitingTimer = null
|
||
const startWaitingTimer = () => {
|
||
if (waitingTimer) return
|
||
waitingTimer = window.setInterval(() => {
|
||
// 仅"等待"状态计算等待时间(基于创建时间)
|
||
const now = new Date()
|
||
originalQueueList.value.forEach((item) => {
|
||
if (item.status !== '等待') return
|
||
// 如果有创建时间,基于创建时间计算;否则累加(兼容旧数据)
|
||
if (item.createTime) {
|
||
try {
|
||
const createTime = new Date(item.createTime)
|
||
// 检查日期是否有效
|
||
if (!isNaN(createTime.getTime())) {
|
||
const diffSeconds = Math.floor((now - createTime) / 1000)
|
||
item.waitingTime = formatSecondsToMmSs(Math.max(0, diffSeconds))
|
||
}
|
||
} catch (e) {
|
||
// 解析失败时使用累加方式(兼容旧数据)
|
||
const seconds = parseMmSsToSeconds(item.waitingTime)
|
||
item.waitingTime = formatSecondsToMmSs(seconds + 1)
|
||
}
|
||
} else {
|
||
// 兼容旧数据:累加方式
|
||
const seconds = parseMmSsToSeconds(item.waitingTime)
|
||
item.waitingTime = formatSecondsToMmSs(seconds + 1)
|
||
}
|
||
})
|
||
}, 1000)
|
||
}
|
||
|
||
const stopWaitingTimer = () => {
|
||
if (!waitingTimer) return
|
||
window.clearInterval(waitingTimer)
|
||
waitingTimer = null
|
||
}
|
||
|
||
// 获取行样式类名(用于高亮当前叫号)
|
||
const getRowClassName = ({ row }) => {
|
||
const classes = []
|
||
if (row.status === '叫号中') classes.push('calling-row')
|
||
if (selectedQueueRow.value && row.queueOrder === selectedQueueRow.value.queueOrder) {
|
||
classes.push('selected-row')
|
||
}
|
||
return classes.join(' ')
|
||
}
|
||
|
||
// 获取状态标签类型
|
||
const getStatusTagType = (status) => {
|
||
const statusMap = {
|
||
'等待': 'info',
|
||
'叫号中': 'warning',
|
||
'就诊中': 'success',
|
||
'跳过': 'danger',
|
||
'已完成': ''
|
||
}
|
||
return statusMap[status] || ''
|
||
}
|
||
|
||
// 刷新数据(候选池=门诊挂号接口,队列=数据库接口)
|
||
const handleRefresh = async () => {
|
||
callType.value = '刷新'
|
||
console.log('【心内科】点击了刷新按钮,开始重新加载数据(候选池+队列)')
|
||
selectedCandidates.value = []
|
||
await loadDataFromApi()
|
||
|
||
// 确保等待计时器只有一个
|
||
stopWaitingTimer()
|
||
startWaitingTimer()
|
||
|
||
ElMessage.success('已刷新(已从数据库恢复队列)')
|
||
}
|
||
|
||
// 退出
|
||
const handleExit = () => {
|
||
ElMessage.info('退出功能待实现')
|
||
// TODO: 实现退出逻辑
|
||
}
|
||
|
||
// 后台配置
|
||
const handleConfig = () => {
|
||
// 打开配置弹窗并加载当前选中规则
|
||
if (rules.value.length > 0) {
|
||
editingIndex.value = 0
|
||
Object.assign(ruleForm, rules.value[0])
|
||
} else {
|
||
editingIndex.value = -1
|
||
Object.assign(ruleForm, {
|
||
name: '',
|
||
dept: '心内科',
|
||
desc: '',
|
||
priority: 10,
|
||
weeks: ['1', '2', '3', '4', '5'],
|
||
expr: '{}'
|
||
})
|
||
}
|
||
configDialogVisible.value = true
|
||
}
|
||
|
||
// 选呼——落库
|
||
const handleSelectCall = async () => {
|
||
if (!selectedQueueRow.value) {
|
||
ElMessage.warning('请先在右侧队列中选择一个患者')
|
||
return
|
||
}
|
||
|
||
try {
|
||
callType.value = '选呼'
|
||
if (!selectedQueueRow.value.id) {
|
||
ElMessage.warning('该队列项缺少ID,无法保存选呼,请先刷新页面')
|
||
return
|
||
}
|
||
|
||
// 检查患者状态,只有"等待"状态才能选呼
|
||
if (selectedQueueRow.value.status !== '等待') {
|
||
if (selectedQueueRow.value.status === '叫号中') {
|
||
ElMessage.info('该患者已经是"叫号中"状态')
|
||
return
|
||
} else {
|
||
ElMessage.warning('只能选呼"等待"状态的患者,当前患者状态为:"' + selectedQueueRow.value.status + '"')
|
||
return
|
||
}
|
||
}
|
||
|
||
await callPatient({ id: selectedQueueRow.value.id, organizationId: selectedQueueRow.value.organizationId })
|
||
// 只刷新队列数据,不重新加载候选池(避免候选池数据被重置)
|
||
await loadQueueFromDb()
|
||
const latest = originalQueueList.value.find((i) => i.id === selectedQueueRow.value.id)
|
||
if (latest) {
|
||
selectedQueueRow.value = latest
|
||
// 如果有多个"叫号中"的患者,显示当前选中的这个
|
||
currentCall.value = { number: String(latest.queueOrder), name: latest.patientName, room: latest.room }
|
||
}
|
||
|
||
// 统计当前"叫号中"的患者数量
|
||
const callingCount = originalQueueList.value.filter(i => i.status === '叫号中').length
|
||
if (callingCount > 1) {
|
||
ElMessage.success(`已选呼(已保存),当前共有 ${callingCount} 位患者处于"叫号中"状态`)
|
||
} else {
|
||
ElMessage.success('已选呼(已保存)')
|
||
}
|
||
} catch (e) {
|
||
console.error('【心内科】handleSelectCall 异常:', e)
|
||
ElMessage.error('选呼失败(未保存)')
|
||
}
|
||
}
|
||
|
||
// 下一患者——落库
|
||
const handleNextPatient = async () => {
|
||
try {
|
||
callType.value = '下一患者'
|
||
// 关键改进:如果用户选中了患者,优先使用选中患者的ID(像 call 方法一样)
|
||
let reqData = {}
|
||
if (selectedQueueRow.value?.id) {
|
||
// 如果选中了患者,直接使用其ID(后端会通过ID直接获取,不依赖查询条件)
|
||
reqData.id = selectedQueueRow.value.id
|
||
reqData.organizationId = selectedQueueRow.value.organizationId
|
||
} else {
|
||
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
|
||
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
|
||
// "全科"模式:优先用"当前叫号中/第一个等待"所在科室
|
||
if (orgId == null) {
|
||
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
|
||
const waiting = originalQueueList.value.find((i) => i.status === '等待')
|
||
console.log('【心内科】handleNextPatient 查找:叫号中=', calling?.patientName, '等待=', waiting?.patientName)
|
||
orgId = calling?.organizationId ?? waiting?.organizationId
|
||
console.log('【心内科】handleNextPatient 确定的 orgId=', orgId)
|
||
}
|
||
if (orgId != null) {
|
||
reqData.organizationId = orgId
|
||
}
|
||
}
|
||
// 如果还是找不到,后端会自己找全科第一个叫号中/等待的
|
||
await nextPatient(reqData)
|
||
selectedQueueRow.value = null
|
||
await loadDataFromApi()
|
||
ElMessage.success('已呼叫下一位(已保存)')
|
||
} catch (e) {
|
||
console.error('【心内科】handleNextPatient 异常:', e)
|
||
// 从错误对象中提取错误消息
|
||
const errorMsg = e?.message || e?.response?.data?.msg || '当前范围内没有"等待"的患者'
|
||
ElMessage.warning(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 跳过——落库(当前等同于“过号重排”)
|
||
const handleSkip = async () => {
|
||
try {
|
||
callType.value = '跳过'
|
||
// 关键改进:如果用户选中了患者,优先使用选中患者的ID(像 call 方法一样)
|
||
let reqData = {}
|
||
if (selectedQueueRow.value?.id) {
|
||
// 如果选中了患者,直接使用其ID(后端会通过ID直接获取,不依赖查询条件)
|
||
reqData.id = selectedQueueRow.value.id
|
||
reqData.organizationId = selectedQueueRow.value.organizationId
|
||
} else {
|
||
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
|
||
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
|
||
// “全科”模式:优先用“当前叫号中”所在科室
|
||
if (orgId == null) {
|
||
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
|
||
orgId = calling?.organizationId
|
||
}
|
||
if (orgId != null) {
|
||
reqData.organizationId = orgId
|
||
}
|
||
}
|
||
await skipPatient(reqData)
|
||
selectedQueueRow.value = null
|
||
await loadDataFromApi()
|
||
ElMessage.success('已跳过(已保存)')
|
||
} catch (e) {
|
||
console.error('【心内科】handleSkip 异常:', e)
|
||
// 从错误对象中提取错误消息
|
||
const errorMsg = e?.message || e?.response?.data?.msg || '当前没有"叫号中"的患者'
|
||
ElMessage.warning(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 完成——落库
|
||
const handleComplete = async () => {
|
||
try {
|
||
callType.value = '完成'
|
||
// 关键改进:如果用户选中了患者,优先使用选中患者的ID(像 call 方法一样)
|
||
let reqData = {}
|
||
if (selectedQueueRow.value?.id) {
|
||
// 如果选中了患者,直接使用其ID(后端会通过ID直接获取,不依赖查询条件)
|
||
reqData.id = selectedQueueRow.value.id
|
||
reqData.organizationId = selectedQueueRow.value.organizationId
|
||
} else {
|
||
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
|
||
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
|
||
// “全科”模式:优先用“当前叫号中”所在科室
|
||
if (orgId == null) {
|
||
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
|
||
orgId = calling?.organizationId
|
||
}
|
||
if (orgId != null) {
|
||
reqData.organizationId = orgId
|
||
}
|
||
}
|
||
await completeCall(reqData)
|
||
selectedQueueRow.value = null
|
||
await loadDataFromApi()
|
||
ElMessage.success('已完成(已保存)')
|
||
} catch (e) {
|
||
console.error('【心内科】handleComplete 异常:', e)
|
||
// 从错误对象中提取错误消息
|
||
const errorMsg = e?.message || e?.response?.data?.msg || '当前没有"叫号中"的患者'
|
||
ElMessage.warning(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 过号重排——落库
|
||
const handleRequeue = async () => {
|
||
try {
|
||
callType.value = '过号重排'
|
||
// 关键改进:如果用户选中了患者,优先使用选中患者的ID(像 call 方法一样)
|
||
let reqData = {}
|
||
if (selectedQueueRow.value?.id) {
|
||
// 如果选中了患者,直接使用其ID(后端会通过ID直接获取,不依赖查询条件)
|
||
reqData.id = selectedQueueRow.value.id
|
||
reqData.organizationId = selectedQueueRow.value.organizationId
|
||
} else {
|
||
// 如果没有选中患者,使用查询条件(兼容旧逻辑)
|
||
let orgId = selectedDept.value !== 'all' ? selectedDept.value : undefined
|
||
// “全科”模式:优先用“当前叫号中”所在科室
|
||
if (orgId == null) {
|
||
const calling = originalQueueList.value.find((i) => i.status === '叫号中')
|
||
orgId = calling?.organizationId
|
||
}
|
||
if (orgId != null) {
|
||
reqData.organizationId = orgId
|
||
}
|
||
}
|
||
await requeuePatient(reqData)
|
||
// 如果执行到这里,说明操作成功
|
||
selectedQueueRow.value = null
|
||
await loadDataFromApi()
|
||
ElMessage.success('已过号重排(已保存)')
|
||
} catch (e) {
|
||
console.error('【心内科】handleRequeue 异常:', e)
|
||
// 从错误对象中提取错误消息
|
||
// 注意:当后端返回 R.fail() 时,code=500,响应拦截器会 Promise.reject(new Error(msg))
|
||
// 所以错误消息在 e.message 中
|
||
const errorMsg = e?.message || e?.response?.data?.msg || '当前没有等待中的患者'
|
||
ElMessage.warning(errorMsg)
|
||
// 失败时不要刷新数据,避免显示错误的状态
|
||
// await loadDataFromApi() // 注释掉,避免刷新数据
|
||
}
|
||
}
|
||
|
||
// 后台配置:选择规则
|
||
const handleSelectRule = (index) => {
|
||
editingIndex.value = index
|
||
Object.assign(ruleForm, rules.value[index])
|
||
}
|
||
|
||
// 后台配置:保存当前规则到列表
|
||
const persistRuleForm = () => {
|
||
if (!ruleForm.name || !ruleForm.priority) {
|
||
ElMessage.warning('请填写规则名称和优先级')
|
||
return false
|
||
}
|
||
if (!Array.isArray(ruleForm.weeks) || ruleForm.weeks.length === 0) {
|
||
ElMessage.warning('请选择生效周几')
|
||
return false
|
||
}
|
||
const parsed = tryParseExpr(ruleForm.expr)
|
||
if (parsed === null) return false
|
||
|
||
const target = {
|
||
name: ruleForm.name,
|
||
dept: ruleForm.dept || '心内科',
|
||
desc: ruleForm.desc || '',
|
||
priority: Number(ruleForm.priority) || 0,
|
||
weeks: [...ruleForm.weeks],
|
||
expr: ruleForm.expr
|
||
}
|
||
|
||
if (editingIndex.value >= 0 && editingIndex.value < rules.value.length) {
|
||
rules.value.splice(editingIndex.value, 1, target)
|
||
} else {
|
||
rules.value.push(target)
|
||
editingIndex.value = rules.value.length - 1
|
||
}
|
||
return true
|
||
}
|
||
|
||
// JSON 语法检查
|
||
const tryParseExpr = (expr) => {
|
||
try {
|
||
JSON.parse(expr || '{}')
|
||
return true
|
||
} catch (e) {
|
||
ElMessage.error('条件表达式 JSON 语法错误')
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 后台配置:新增规则
|
||
const handleAddRule = () => {
|
||
const ok = persistRuleForm()
|
||
if (!ok) return
|
||
const newRule = {
|
||
name: '新规则',
|
||
dept: '心内科',
|
||
desc: '',
|
||
priority: rules.value.length * 10 + 10,
|
||
weeks: ['1', '2', '3', '4', '5'],
|
||
expr: '{}'
|
||
}
|
||
rules.value.push(newRule)
|
||
editingIndex.value = rules.value.length - 1
|
||
Object.assign(ruleForm, newRule)
|
||
}
|
||
|
||
// 后台配置:删除规则
|
||
const handleDeleteRule = (index) => {
|
||
rules.value.splice(index, 1)
|
||
if (rules.value.length === 0) {
|
||
editingIndex.value = -1
|
||
Object.assign(ruleForm, {
|
||
name: '',
|
||
dept: '心内科',
|
||
desc: '',
|
||
priority: 10,
|
||
weeks: ['1', '2', '3', '4', '5'],
|
||
expr: '{}'
|
||
})
|
||
return
|
||
}
|
||
const newIndex = Math.max(0, Math.min(index, rules.value.length - 1))
|
||
editingIndex.value = newIndex
|
||
Object.assign(ruleForm, rules.value[newIndex])
|
||
}
|
||
|
||
// 后台配置:上移
|
||
const handleRuleMoveUp = (index) => {
|
||
if (index <= 0) return
|
||
;[rules.value[index - 1], rules.value[index]] = [rules.value[index], rules.value[index - 1]]
|
||
editingIndex.value = index - 1
|
||
}
|
||
|
||
// 后台配置:下移
|
||
const handleRuleMoveDown = (index) => {
|
||
if (index >= rules.value.length - 1) return
|
||
;[rules.value[index + 1], rules.value[index]] = [rules.value[index], rules.value[index + 1]]
|
||
editingIndex.value = index + 1
|
||
}
|
||
|
||
// 后台配置:保存当前规则
|
||
const handleSaveCurrentRule = () => {
|
||
const ok = persistRuleForm()
|
||
if (ok) {
|
||
ElMessage.success('当前规则已保存')
|
||
}
|
||
}
|
||
|
||
// 后台配置:保存全部(本地模拟)
|
||
const handleSaveAllRules = () => {
|
||
const ok = persistRuleForm()
|
||
if (!ok) return
|
||
// TODO: 调用后端保存接口
|
||
ElMessage.success('全部规则已保存(前端模拟)')
|
||
}
|
||
|
||
// 后台配置:语法检查
|
||
const handleValidateRule = () => {
|
||
const ok = tryParseExpr(ruleForm.expr)
|
||
if (ok) {
|
||
ElMessage.success('语法检查通过')
|
||
}
|
||
}
|
||
|
||
// 构建条件对象(公共函数)
|
||
const buildConditions = () => {
|
||
const conditions = {}
|
||
|
||
// 年龄条件
|
||
if (quickGeneratorForm.ageOperator && quickGeneratorForm.ageValue !== null) {
|
||
conditions.age = `${quickGeneratorForm.ageOperator}${quickGeneratorForm.ageValue}`
|
||
}
|
||
|
||
// 号别
|
||
if (quickGeneratorForm.regType) {
|
||
conditions.regType = quickGeneratorForm.regType
|
||
}
|
||
|
||
// 科室
|
||
if (quickGeneratorForm.dept) {
|
||
conditions.dept = quickGeneratorForm.dept
|
||
}
|
||
|
||
// 医生
|
||
if (quickGeneratorForm.doctor) {
|
||
conditions.doctor = quickGeneratorForm.doctor
|
||
}
|
||
|
||
// 自定义条件
|
||
if (quickGeneratorForm.customKey && quickGeneratorForm.customValue) {
|
||
conditions[quickGeneratorForm.customKey] = quickGeneratorForm.customValue
|
||
}
|
||
|
||
return conditions
|
||
}
|
||
|
||
// 预览JSON(计算属性)
|
||
const previewJson = computed(() => {
|
||
return JSON.stringify(buildConditions(), null, 2)
|
||
})
|
||
|
||
// 后台配置:快速生成器
|
||
const handleQuickGenerate = () => {
|
||
// 如果当前表达式不为空,尝试解析并填充表单
|
||
if (ruleForm.expr) {
|
||
try {
|
||
const parsed = JSON.parse(ruleForm.expr)
|
||
|
||
// 解析年龄条件
|
||
if (parsed.age) {
|
||
const ageMatch = parsed.age.match(/^(>=|<=|=|>|<)(\d+)$/)
|
||
if (ageMatch) {
|
||
quickGeneratorForm.ageOperator = ageMatch[1]
|
||
quickGeneratorForm.ageValue = parseInt(ageMatch[2])
|
||
}
|
||
}
|
||
|
||
// 填充其他字段
|
||
if (parsed.regType) quickGeneratorForm.regType = parsed.regType
|
||
if (parsed.dept) quickGeneratorForm.dept = parsed.dept
|
||
if (parsed.doctor) quickGeneratorForm.doctor = parsed.doctor
|
||
|
||
// 填充自定义字段(排除已知字段)
|
||
const knownFields = ['age', 'regType', 'dept', 'doctor']
|
||
for (const key in parsed) {
|
||
if (!knownFields.includes(key)) {
|
||
quickGeneratorForm.customKey = key
|
||
quickGeneratorForm.customValue = parsed[key]
|
||
break
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 如果解析失败,清空表单
|
||
resetQuickGeneratorForm()
|
||
}
|
||
} else {
|
||
// 如果表达式为空,重置表单
|
||
resetQuickGeneratorForm()
|
||
}
|
||
|
||
quickGeneratorDialogVisible.value = true
|
||
}
|
||
|
||
// 重置快速生成器表单
|
||
const resetQuickGeneratorForm = () => {
|
||
quickGeneratorForm.ageOperator = null
|
||
quickGeneratorForm.ageValue = null
|
||
quickGeneratorForm.regType = null
|
||
quickGeneratorForm.dept = null
|
||
quickGeneratorForm.doctor = null
|
||
quickGeneratorForm.customKey = ''
|
||
quickGeneratorForm.customValue = ''
|
||
}
|
||
|
||
// 应用快速生成器生成的JSON
|
||
const handleApplyQuickGenerate = () => {
|
||
const conditions = buildConditions()
|
||
|
||
// 检查是否至少有一个条件
|
||
if (Object.keys(conditions).length === 0) {
|
||
ElMessage.warning('请至少设置一个条件')
|
||
return
|
||
}
|
||
|
||
// 应用到规则表单
|
||
ruleForm.expr = JSON.stringify(conditions, null, 2)
|
||
|
||
// 关闭对话框
|
||
quickGeneratorDialogVisible.value = false
|
||
|
||
ElMessage.success('条件表达式已生成并应用')
|
||
}
|
||
|
||
// 后台配置:测试规则(占位)
|
||
const handleTestRule = () => {
|
||
const ok = tryParseExpr(ruleForm.expr)
|
||
if (!ok) return
|
||
ElMessage.info('测试规则功能待接入后端,当前为占位')
|
||
}
|
||
|
||
// 组件挂载
|
||
onMounted(() => {
|
||
// 加载就诊科室列表
|
||
loadDepartmentList()
|
||
// 初始化:优先从后端加载,失败则回退本地假数据
|
||
loadDataFromApi()
|
||
startWaitingTimer()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopWaitingTimer()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.smart-triage-queue {
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f5f5f5;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
|
||
// 顶部标题栏
|
||
.header-section {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
background-color: #fff;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
|
||
.header-left {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.title {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.header-info {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 10px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
|
||
.date-info {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stat-info {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
.current-call-info {
|
||
font-size: 16px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
// 主要内容区域
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
min-height: 0;
|
||
|
||
.left-panel,
|
||
.right-panel {
|
||
flex: 1;
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
|
||
.panel-header {
|
||
padding: 15px 20px;
|
||
border-bottom: 2px solid #409eff;
|
||
background-color: #f8f9fa;
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.table-container {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
padding: 10px;
|
||
}
|
||
|
||
.display-options {
|
||
padding: 10px 20px;
|
||
border-top: 1px solid #ebeef5;
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.queue-actions-left {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.queue-actions-right {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
.candidate-actions {
|
||
padding: 10px 20px;
|
||
border-top: 1px solid #ebeef5;
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-start;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 底部控制面板
|
||
.footer-section {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
|
||
.filter-section {
|
||
margin-bottom: 20px;
|
||
|
||
.filter-label {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.filter-select-wrapper {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.call-control-section {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 6px;
|
||
|
||
.call-control-label {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.call-control-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.current-call-display {
|
||
font-size: 16px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.control-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.led-section {
|
||
margin-bottom: 15px;
|
||
padding: 15px;
|
||
background-color: #1a1a1a;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
|
||
.led-label {
|
||
font-size: 14px;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.led-display {
|
||
flex: 1;
|
||
font-size: 20px;
|
||
color: #00ff00;
|
||
font-weight: bold;
|
||
font-family: 'Courier New', monospace;
|
||
text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
|
||
}
|
||
}
|
||
|
||
.voice-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px;
|
||
background-color: #f0f0f0;
|
||
border-radius: 6px;
|
||
|
||
.voice-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.voice-text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
// 配置弹窗样式
|
||
.config-container {
|
||
display: flex;
|
||
gap: 20px;
|
||
min-height: 500px;
|
||
}
|
||
|
||
.config-topbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 0 12px 0;
|
||
}
|
||
|
||
.config-topbar-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.config-dialog-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
}
|
||
|
||
.config-dialog-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.config-left {
|
||
width: 30%;
|
||
background: #f8f9fb;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.rule-card {
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 6px;
|
||
padding: 14px 14px 12px;
|
||
margin: 0 6px 14px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06);
|
||
position: relative;
|
||
}
|
||
|
||
.rule-card.active {
|
||
border-color: #409eff;
|
||
box-shadow: 0 0 0 2px #409eff22;
|
||
}
|
||
|
||
.rule-card.active::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 10px;
|
||
bottom: 10px;
|
||
width: 4px;
|
||
background: #409eff;
|
||
border-radius: 0 4px 4px 0;
|
||
}
|
||
|
||
.rule-card:hover {
|
||
border-color: #b3d8ff;
|
||
}
|
||
|
||
.rule-title {
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.rule-sub {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.rule-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 6px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #ebeef5;
|
||
}
|
||
|
||
.config-right {
|
||
flex: 1;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
box-sizing: border-box;
|
||
background: #fff;
|
||
}
|
||
|
||
.config-form-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.config-form-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.config-inline-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding-left: 110px; // 对齐 label-width
|
||
margin-top: -6px;
|
||
}
|
||
|
||
.config-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
.config-fullwidth {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 表格行样式
|
||
:deep(.calling-row) {
|
||
background-color: #fffbe6 !important;
|
||
}
|
||
|
||
:deep(.selected-row) {
|
||
background-color: #e8f4ff !important;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
.el-table__body {
|
||
tr.calling-row {
|
||
background-color: #fffbe6;
|
||
}
|
||
tr.selected-row {
|
||
background-color: #e8f4ff;
|
||
}
|
||
}
|
||
}
|
||
</style>
|