feat(分诊队列): 实现分诊队列核心功能与日志记录

新增分诊队列相关服务接口与实现,包括队列管理、叫号操作和日志记录
添加DivLogService和CallRecordService用于记录分诊操作和叫号历史
在CurrentDayEncounterDto和TriageQueueItem中增加seqNo字段用于显示预约序号
实现分诊操作日志记录功能,包括添加队列、移除队列、叫号、完成等操作
新增CallType枚举定义叫号类型,并实现叫号记录功能
优化队列状态映射逻辑,支持更多状态类型显示
This commit is contained in:
wangjian963
2026-04-29 17:05:17 +08:00
parent 2b0acce1db
commit d4d05267ad
19 changed files with 933 additions and 303 deletions

View File

@@ -37,7 +37,7 @@ export function getCandidatePool(params) {
pageNo: params?.pageNo || 1,
pageSize: params?.pageSize || 10000,
searchKey: params?.searchKey || '',
statusEnum: params?.statusEnum || -1 // -1表示排除退号记录正常挂号
statusEnum: params?.statusEnum ?? 1 // 1=PLANNED(待诊),已挂号未接诊的患者;不传或传-1会返回已接诊的患者
},
skipErrorMsg: true // 跳过错误提示,由组件处理
})
@@ -169,4 +169,4 @@ export function getLocationTree(query) {
method: 'get',
params: query
})
}
}

View File

@@ -3,15 +3,22 @@
<!-- 顶部标题栏 -->
<div class="header-section">
<div class="header-left">
<span class="title">智能分诊排队管理 - {{ currentDeptName }}</span>
<span class="title">智能分诊排队管理 - {{ currentDeptName }}</span>
</div>
<div class="header-right">
<el-button type="primary" @click="handleRefresh">
<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>
<el-button @click="handleExit">
退出
</el-button>
<el-button @click="handleConfig">
后台配置
</el-button>
</div>
</div>
@@ -31,28 +38,67 @@
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-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"
@click="handleAddToQueue"
>
加入队列 >>
</el-button>
<el-button
type="primary"
@click="handleAddAllToQueue"
:disabled="filteredCandidatePoolList.length === 0"
@click="handleAddAllToQueue"
>
一键加入队列
</el-button>
@@ -75,13 +121,48 @@
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">
<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 }}
@@ -94,25 +175,25 @@
<div class="queue-actions-left">
<el-button
type="danger"
@click="handleRemoveFromQueue"
:disabled="!selectedQueueRow"
size="small"
@click="handleRemoveFromQueue"
>
<< 移出队列
&lt;&lt; 移出队列
</el-button>
<el-button
type="info"
@click="handleMoveUp"
:disabled="!selectedQueueRow || !canMoveUp"
size="small"
@click="handleMoveUp"
>
</el-button>
<el-button
type="info"
@click="handleMoveDown"
:disabled="!selectedQueueRow || !canMoveDown"
size="small"
@click="handleMoveDown"
>
</el-button>
@@ -120,15 +201,15 @@
<div class="queue-actions-right">
<el-button
:type="showOnlyWaiting ? 'primary' : ''"
@click="showOnlyWaiting = true"
size="small"
@click="showOnlyWaiting = true"
>
只显示等待
</el-button>
<el-button
:type="!showOnlyWaiting ? 'primary' : ''"
@click="showOnlyWaiting = false"
size="small"
@click="showOnlyWaiting = false"
>
显示全部状态
</el-button>
@@ -141,7 +222,9 @@
<div class="footer-section">
<!-- 就诊科室快速过滤栏 -->
<div class="filter-section">
<div class="filter-label"> 就诊科室快速过滤栏</div>
<div class="filter-label">
就诊科室快速过滤栏
</div>
<div class="filter-select-wrapper">
<el-select
v-model="selectedDept"
@@ -167,24 +250,53 @@
<!-- 叫号控制板 -->
<div class="call-control-section">
<div class="call-control-label"> 叫号控制板</div>
<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>
<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-label">
LED:
</div>
<div class="led-display">
[{{ currentCall.number }}]{{ currentCall.name }}请到{{ currentCall.room }}({{ callType }})
</div>
@@ -197,205 +309,333 @@
</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>
<!-- 后台配置弹窗 -->
<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"
:disabled="idx === 0"
@click.stop="handleRuleMoveUp(idx)"
>
</el-button>
<el-button
size="small"
:disabled="idx === rules.length - 1"
@click.stop="handleRuleMoveDown(idx)"
>
</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="{&quot;age&quot;:&quot;>=60&quot;,&quot;regType&quot;:&quot;专家&quot;}"
/>
</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>
<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)"
<template #footer>
<div class="config-footer">
<el-button
type="primary"
@click="handleSaveCurrentRule"
>
<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>
保存
</el-button>
<el-button @click="configDialogVisible = false">
取消
</el-button>
</div>
</template>
</el-dialog>
<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-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-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-input-number
v-model="quickGeneratorForm.ageValue"
:min="0"
:max="150"
:precision="0"
placeholder="年龄"
style="width: 150px;"
/>
</el-form-item>
<div class="config-inline-actions">
<el-button @click="handleQuickGenerate">快速生成器</el-button>
<el-button @click="handleValidateRule">语法检查</el-button>
<el-button
type="text"
@click="quickGeneratorForm.ageOperator = null; quickGeneratorForm.ageValue = null"
>
清除
</el-button>
</div>
</el-form>
</div>
</div>
</el-form-item>
<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-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-input-number
v-model="quickGeneratorForm.ageValue"
:min="0"
:max="150"
:precision="0"
placeholder="年龄"
style="width: 150px;"
</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.ageOperator = null; quickGeneratorForm.ageValue = null"
@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>
</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>
</template>
</el-dialog>
</div>
</template>
@@ -627,29 +867,40 @@ const parseAge = (ageStr) => {
}
// 后端队列状态 -> 前端展示状态
// 后端状态码0=WAITING, 10=CALLING, 20=IN_CLINIC, 30=COMPLETED, 40=SKIPPED, 50=REFUNDED, 60=FOLLOW
const mapBackendStatusToFrontend = (status) => {
if (!status) {
if (status === null || status === undefined) {
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 numStatus = Number(status)
switch (numStatus) {
case 0: return '等待'
case 10: return '叫号中'
case 20: return '诊中'
case 30: return '已完成'
case 40: return '跳过'
case 50: return '已退费'
case 60: return '已随访'
default:
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'
if (!status) return 0
switch (status) {
case '叫号中': return 10
case '等待': return 0
case '诊中': return 20
case '已完成': return 30
case '跳过': return 40
case '已退费': return 50
case '已随访': return 60
default: return 0
}
}
// 从数据库加载队列