挂号补单功能的完善

This commit is contained in:
2026-01-14 12:56:39 +08:00
parent e8783d9f8f
commit d8080fa22d
4 changed files with 663 additions and 34 deletions

View File

@@ -547,47 +547,21 @@ function formatDateTime(date) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 计算流水号:格式为 YYYYMMDD-XXX其中 XXX 为后端按"科室+医生+当日"自增的 displayOrder
// 确保同一科室同一医生同一天内是 001、002、003... 递增
// 计算流水号:直接使用挂号记录表的主键IDencounterId
function calculateSerialNo(row) {
if (!row) {
return '-';
}
// 获取挂号日期YYYYMMDD格式
let dateStr = '';
if (row.registerTime) {
const date = new Date(row.registerTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
dateStr = `${year}${month}${day}`;
// 直接使用主键ID作为流水号
if (row.encounterId != null && row.encounterId !== undefined) {
return String(row.encounterId);
} else if (row.id != null && row.id !== undefined) {
// 兼容其他可能的ID字段名
return String(row.id);
} else {
// 如果没有挂号时间,使用当前日期
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
dateStr = `${year}${month}${day}`;
return '-';
}
// 获取序号部分3位数字001-999
// 直接使用后端返回的 displayOrder自增逻辑在后端按"科室+医生+当日"保证
let serialNum = 1;
if (row.displayOrder != null && row.displayOrder !== undefined) {
const num = Number(row.displayOrder) || 0;
serialNum = num > 0 ? num : 1;
} else if (row.serialNo) {
// 兼容旧数据:如果有已有的 serialNo 字段
const num = Number(row.serialNo) || 0;
serialNum = num > 0 ? num : 1;
} else {
// 兜底:没有任何序号信息时,给 1
serialNum = 1;
}
// 格式YYYYMMDD-XXX例如20250113-001
return `${dateStr}-${String(serialNum).padStart(3, '0')}`;
}
// 提交补打挂号

View File

@@ -0,0 +1,378 @@
<template>
<div class="cardiology-queue">
<div class="app-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">心内科分诊排队管理</span>
<div class="header-actions">
<el-button type="primary" @click="refreshData" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</template>
<!-- 当前呼叫区 -->
<div class="current-call-section">
<div class="call-box">
<div class="call-text">
<span class="highlight">{{ currentCall?.number || '-' }}</span>
<span class="highlight">{{ currentCall?.name || '-' }}</span>
<span class="highlight">{{ currentCall?.room || '-' }}</span> 诊室就诊
</div>
</div>
</div>
<!-- 候诊队列 -->
<div class="queue-section">
<h3 class="section-title">候诊队列</h3>
<el-table
:data="queueList"
v-loading="loading"
stripe
border
style="width: 100%"
:default-sort="{ prop: 'displayOrder', order: 'ascending' }"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientName" label="患者姓名" width="120" align="center">
<template #default="scope">
{{ formatPatientName(scope.row.patientName) }}
</template>
</el-table-column>
<el-table-column prop="practitionerName" label="医生" width="120" align="center" />
<el-table-column prop="room" label="诊室" width="100" align="center" />
<el-table-column prop="displayOrder" label="流水号" width="120" align="center">
<template #default="scope">
{{ formatSerialNo(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="registerTime" label="挂号时间" width="180" align="center">
<template #default="scope">
{{ formatTime(scope.row.registerTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleCall(scope.row)"
:disabled="scope.row.status === 'CALLED'"
>
叫号
</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"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { parseTime } from '@/utils/his'
import { getOutpatientRegistrationCurrent } from '@/views/charge/outpatientregistration/components/outpatientregistration'
// 响应式数据
const loading = ref(false)
const queueList = ref([])
const total = ref(0)
const currentCall = ref({
number: '-',
name: '-',
room: '-'
})
// 查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 20,
searchKey: '',
organizationId: null // 心内科科室ID可以从路由参数或配置中获取
})
// 计算流水号直接使用挂号记录表的主键IDencounterId
function formatSerialNo(row) {
if (!row) return '-'
// 直接使用主键ID作为流水号
if (row.encounterId != null && row.encounterId !== undefined) {
return String(row.encounterId)
} else if (row.id != null && row.id !== undefined) {
// 兼容其他可能的ID字段名
return String(row.id)
} else {
return '-'
}
}
// 格式化患者姓名(脱敏)
function formatPatientName(name) {
if (!name || typeof name !== 'string') return '-'
if (name.length <= 2) return name.charAt(0) + '*'
return name.charAt(0) + '*' + name.slice(-1)
}
// 格式化时间
function formatTime(time) {
if (!time) return '-'
return parseTime(time, '{y}-{m}-{d} {h}:{i}:{s}')
}
// 获取状态类型
function getStatusType(status) {
const statusMap = {
'WAITING': 'info',
'CALLED': 'warning',
'IN_PROGRESS': 'success',
'COMPLETED': ''
}
return statusMap[status] || ''
}
// 获取状态文本
function getStatusText(status) {
const statusMap = {
'WAITING': '等待',
'CALLED': '已叫号',
'IN_PROGRESS': '就诊中',
'COMPLETED': '已完成'
}
return statusMap[status] || '未知'
}
// 获取队列数据
async function fetchQueueData() {
try {
loading.value = true
// 调用API获取当日就诊数据
const response = await getOutpatientRegistrationCurrent({
searchKey: queryParams.searchKey,
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize
})
if (response && response.code === 200) {
// 获取记录数据
const records = response.data?.records || response.data || []
// 过滤心内科的数据如果有organizationId
let filteredData = records
if (queryParams.organizationId) {
filteredData = records.filter(item => item.organizationId === queryParams.organizationId)
}
// 转换为队列数据格式
queueList.value = filteredData.map(item => ({
id: item.encounterId,
patientName: item.patientName,
practitionerName: item.practitionerName || '-',
organizationName: item.organizationName,
room: item.organizationName || '-', // 使用科室名称作为诊室
displayOrder: item.displayOrder,
registerTime: item.registerTime,
status: 'WAITING' // 默认状态,实际应该从后端获取
}))
total.value = response.data?.total || filteredData.length || 0
} else {
queueList.value = []
total.value = 0
if (response && response.msg) {
ElMessage.warning('获取数据失败: ' + response.msg)
}
}
} catch (error) {
console.error('获取队列数据失败:', error)
ElMessage.error('获取队列数据失败: ' + (error.message || '未知错误'))
queueList.value = []
total.value = 0
} finally {
loading.value = false
}
}
// 刷新数据
function refreshData() {
queryParams.pageNo = 1
fetchQueueData()
}
// 叫号
async function handleCall(row) {
try {
await ElMessageBox.confirm(
`确认叫号:${formatPatientName(row.patientName)}${formatSerialNo(row)}`,
'确认叫号',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
)
// TODO: 调用叫号API
// await callPatient(row.id)
// 更新当前呼叫信息
currentCall.value = {
number: row.displayOrder || '-',
name: formatPatientName(row.patientName),
room: row.room
}
// 更新状态
row.status = 'CALLED'
ElMessage.success('叫号成功')
// 刷新数据
await fetchQueueData()
} catch (error) {
if (error !== 'cancel') {
console.error('叫号失败:', error)
ElMessage.error('叫号失败: ' + (error.message || '未知错误'))
}
}
}
// 分页大小改变
function handleSizeChange(val) {
queryParams.pageSize = val
queryParams.pageNo = 1
fetchQueueData()
}
// 页码改变
function handlePageChange(val) {
queryParams.pageNo = val
fetchQueueData()
}
// 组件挂载时获取数据
onMounted(() => {
fetchQueueData()
// 定时刷新数据每30秒
const refreshInterval = setInterval(() => {
fetchQueueData()
}, 30000)
// 组件卸载时清理定时器
return () => {
clearInterval(refreshInterval)
}
})
</script>
<style lang="scss" scoped>
.cardiology-queue {
.app-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
gap: 10px;
}
}
.current-call-section {
margin: 20px 0;
text-align: center;
.call-box {
display: inline-block;
padding: 20px 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
animation: pulse 2s infinite;
.call-text {
font-size: 24px;
font-weight: bold;
color: white;
letter-spacing: 2px;
.highlight {
color: #ffd700;
font-size: 28px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
}
}
}
.queue-section {
margin-top: 20px;
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #667eea;
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
70% {
box-shadow: 0 0 0 15px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
</style>

116
sql/query_encounter_id.sql Normal file
View File

@@ -0,0 +1,116 @@
-- 查询挂号记录表主键的SQL语句
-- 表名adm_encounter
-- 主键字段id (bigint雪花算法生成)
-- ========================================
-- 1. 查询所有挂号记录的主键(简单查询)
-- ========================================
SELECT id
FROM adm_encounter
WHERE delete_flag = '0'
ORDER BY id DESC;
-- ========================================
-- 2. 查询最近N条挂号记录的主键
-- ========================================
SELECT id
FROM adm_encounter
WHERE delete_flag = '0'
ORDER BY start_time DESC
LIMIT 10;
-- ========================================
-- 3. 查询指定日期范围内的挂号记录主键
-- ========================================
SELECT id
FROM adm_encounter
WHERE delete_flag = '0'
AND DATE(start_time) >= '2026-01-01'
AND DATE(start_time) <= '2026-01-31'
ORDER BY start_time DESC;
-- ========================================
-- 4. 查询指定科室的挂号记录主键
-- ========================================
SELECT id
FROM adm_encounter
WHERE delete_flag = '0'
AND organization_id = 123 -- 替换为实际的科室ID
ORDER BY start_time DESC;
-- ========================================
-- 5. 查询指定患者的挂号记录主键
-- ========================================
SELECT id
FROM adm_encounter
WHERE delete_flag = '0'
AND patient_id = 456 -- 替换为实际的患者ID
ORDER BY start_time DESC;
-- ========================================
-- 6. 查询主键及基本信息(常用)
-- ========================================
SELECT
id AS ID,
bus_no AS ,
patient_id AS ID,
organization_id AS ID,
registrar_id AS ID,
display_order AS ,
start_time AS ,
status_enum AS
FROM adm_encounter
WHERE delete_flag = '0'
ORDER BY start_time DESC
LIMIT 20;
-- ========================================
-- 7. 统计主键数量(按日期分组)
-- ========================================
SELECT
DATE(start_time) AS ,
COUNT(id) AS ,
MIN(id) AS ID,
MAX(id) AS ID
FROM adm_encounter
WHERE delete_flag = '0'
GROUP BY DATE(start_time)
ORDER BY DESC
LIMIT 30;
-- ========================================
-- 8. 查询主键的最大值、最小值、总数
-- ========================================
SELECT
COUNT(*) AS ,
MIN(id) AS ID,
MAX(id) AS ID,
MAX(id) - MIN(id) AS ID范围
FROM adm_encounter
WHERE delete_flag = '0';
-- ========================================
-- 9. 根据主键查询单条记录(精确查询)
-- ========================================
SELECT *
FROM adm_encounter
WHERE id = 1996923066055286785 -- 替换为实际的主键ID
AND delete_flag = '0';
-- ========================================
-- 10. 查询主键及关联的患者信息
-- ========================================
SELECT
e.id AS ID,
e.bus_no AS ,
e.display_order AS ,
e.start_time AS ,
p.id AS ID,
p.name AS ,
p.bus_no AS
FROM adm_encounter e
INNER JOIN adm_patient p ON e.patient_id = p.id AND p.delete_flag = '0'
WHERE e.delete_flag = '0'
ORDER BY e.start_time DESC
LIMIT 20;

161
sql/sequence_explanation.md Normal file
View File

@@ -0,0 +1,161 @@
# PostgreSQL SEQUENCE序列详解
## 语句解析
```sql
DROP SEQUENCE IF EXISTS "public"."adm_encounter_id_seq";
CREATE SEQUENCE "public"."adm_encounter_id_seq"
INCREMENT 1
MINVALUE 1
MAXVALUE 99999999
START 200
CACHE 1;
```
## 逐行解释
### 1. `DROP SEQUENCE IF EXISTS "public"."adm_encounter_id_seq";`
- **作用**:如果序列已存在,先删除它
- **`IF EXISTS`**:如果序列不存在,不会报错,直接跳过
- **`"public"."adm_encounter_id_seq"`**:序列的完整名称
- `public`模式schema默认模式
- `adm_encounter_id_seq`:序列名称
### 2. `CREATE SEQUENCE "public"."adm_encounter_id_seq"`
- **作用**:创建一个新的序列
- **序列名称**`adm_encounter_id_seq`(用于 `adm_encounter` 表的主键自增)
### 3. `INCREMENT 1`
- **作用**:每次递增的步长
- **含义**:每次调用 `nextval()` 时,序列值增加 1
- **示例**:如果当前值是 200下次调用 `nextval()` 返回 201
### 4. `MINVALUE 1`
- **作用**:序列的最小值
- **含义**:序列值不能小于 1
- **注意**:如果序列达到最小值后继续递减,会报错(除非设置了 `CYCLE`
### 5. `MAXVALUE 99999999`
- **作用**:序列的最大值
- **含义**:序列值不能超过 999999998位数
- **注意**:如果序列达到最大值后继续递增,会报错(除非设置了 `CYCLE`
### 6. `START 200`
- **作用**:序列的起始值
- **含义**:序列从 200 开始
- **示例**:第一次调用 `nextval()` 返回 200第二次返回 201以此类推
### 7. `CACHE 1`
- **作用**:缓存大小
- **含义**:每次从数据库获取序列值时,预分配 1 个值到内存
- **说明**
- `CACHE 1`:每次只缓存 1 个值(最安全,但性能较低)
- `CACHE 20`:每次缓存 20 个值(性能更好,但可能跳号)
- **注意**:如果数据库重启,缓存中未使用的序列值会丢失,导致跳号
## 使用示例
### 1. 在表定义中使用(自动自增)
```sql
CREATE TABLE "adm_encounter" (
"id" int8 NOT NULL DEFAULT nextval('adm_encounter_id_seq'::regclass),
...
);
```
### 2. 手动获取下一个值
```sql
-- 获取下一个序列值
SELECT nextval('adm_encounter_id_seq');
-- 返回200第一次调用
SELECT nextval('adm_encounter_id_seq');
-- 返回201第二次调用
```
### 3. 查看当前值(不递增)
```sql
SELECT currval('adm_encounter_id_seq');
-- 返回:当前序列值(不会增加)
```
### 4. 重置序列
```sql
-- 将序列重置为指定值
SELECT setval('adm_encounter_id_seq', 200);
```
## 实际应用场景
### 场景1插入数据时自动生成ID
```sql
INSERT INTO adm_encounter (patient_id, organization_id, ...)
VALUES (123, 456, ...);
-- id 字段会自动使用序列的下一个值(如 200, 201, 202...
```
### 场景2手动指定ID不推荐
```sql
INSERT INTO adm_encounter (id, patient_id, ...)
VALUES (999, 123, ...);
-- 注意如果手动插入的ID与序列值冲突可能导致问题
```
## 注意事项
### 1. 序列与雪花算法的区别
- **序列**简单的数字递增1, 2, 3...
- **雪花算法**分布式ID生成算法1996923066055286785
- **当前项目**:虽然定义了序列,但实际使用的是雪花算法(`IdType.ASSIGN_ID`
### 2. 序列跳号的原因
- 事务回滚:如果事务回滚,序列值不会回退
- 缓存:`CACHE > 1` 时,数据库重启可能导致跳号
- 手动插入手动插入ID后序列不会自动调整
### 3. 序列的优缺点
**优点**
- 简单易用
- 性能好(预分配)
- 保证唯一性
**缺点**
- 可能跳号
- 不适合分布式环境
- 最大值有限制
## 相关查询语句
### 查看序列信息
```sql
-- 查看序列的详细信息
SELECT
sequence_name AS 序列名称,
last_value AS 当前值,
start_value AS 起始值,
increment_by AS 递增步长,
max_value AS 最大值,
min_value AS 最小值,
cache_size AS 缓存大小
FROM information_schema.sequences
WHERE sequence_name = 'adm_encounter_id_seq';
```
### 查看序列的下一个值(不实际使用)
```sql
SELECT last_value, is_called
FROM adm_encounter_id_seq;
```
### 修改序列
```sql
-- 修改序列的起始值
ALTER SEQUENCE adm_encounter_id_seq RESTART WITH 1000;
-- 修改序列的最大值
ALTER SEQUENCE adm_encounter_id_seq MAXVALUE 999999999;
-- 修改序列的缓存大小
ALTER SEQUENCE adm_encounter_id_seq CACHE 20;
```