77 门诊挂号-》预约签到

This commit is contained in:
HuangXinQuan
2026-04-03 14:42:13 +08:00
parent cb46461ede
commit 1b3d4e3dc0
17 changed files with 821 additions and 77 deletions

View File

@@ -162,3 +162,61 @@ export const STATUS = {
NORMAL: '0', // 正常/启用
DISABLE: '1' // 停用
};
/**
* 号源槽位状态(与后端 CommonConstants.SlotStatus 保持一致)
* adm_schedule_slot.status 字段
*/
export const SlotStatus = {
/** 可用 / 待预约 */
AVAILABLE: 0,
/** 已预约 */
BOOKED: 1,
/** 已取消 / 已停诊 */
CANCELLED: 2,
/** 已锁定 */
LOCKED: 3,
/** 已签到 / 已取号 */
CHECKED_IN: 4,
};
/**
* 号源槽位状态说明信息
*/
export const SlotStatusDescriptions = {
0: '未预约',
1: '已预约',
2: '已停诊',
3: '已锁定',
4: '已取号',
};
/**
* 号源槽位状态对应的CSS类名
*/
export const SlotStatusClassMap = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled',
'已锁定': 'status-locked',
};
/**
* 获取号源槽位状态的说明
* @param {number} value - 状态值
* @returns {string} - 说明信息
*/
export function getSlotStatusDescription(value) {
return SlotStatusDescriptions[value] || '未知状态';
}
/**
* 获取号源槽位状态对应的CSS类名
* @param {string} status - 状态说明
* @returns {string} - CSS类名
*/
export function getSlotStatusClass(status) {
return SlotStatusClassMap[status] || 'status-unbooked';
}

View File

@@ -37,6 +37,7 @@
<option value="booked">已预约</option>
<option value="checked">已取号</option>
<option value="cancelled">已停诊</option>
<option value="returned">已退号</option>
</select>
</div>
<div id="patientSearch" class="patient-search">
@@ -254,6 +255,7 @@ const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已退号': 'status-returned',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled'
};
@@ -703,10 +705,36 @@ export default {
return;
}
const records = payload.list || payload.records || [];
const filteredRecords = this.applyStatusFilter(records);
const total = Number(payload.total);
this.tickets = [...records];
this.allTickets = [...records];
this.totalTickets = Number.isFinite(total) ? total : this.tickets.length;
this.tickets = [...filteredRecords];
this.allTickets = [...filteredRecords];
// 当按状态筛选时,优先使用前端过滤后的数量,避免后端状态未生效导致“显示全部”
if (this.selectedStatus && this.selectedStatus !== 'all') {
this.totalTickets = this.tickets.length;
} else {
this.totalTickets = Number.isFinite(total) ? total : this.tickets.length;
}
},
applyStatusFilter(records = []) {
if (!Array.isArray(records) || records.length === 0) {
return [];
}
if (!this.selectedStatus || this.selectedStatus === 'all') {
return records;
}
const statusMap = {
unbooked: ['未预约'],
booked: ['已预约'],
checked: ['已取号'],
cancelled: ['已停诊', '已取消'],
returned: ['已退号']
};
const matchedStatusList = statusMap[this.selectedStatus] || [];
if (matchedStatusList.length === 0) {
return records;
}
return records.filter(item => matchedStatusList.includes(item?.status));
},
updateDoctorsListFromApi(doctorResponse) {
let doctorList = [];
@@ -1376,6 +1404,11 @@ export default {
color: #52c41a;
}
.status-returned {
background-color: #fff7e6;
color: #d46b08;
}
.status-cancelled {
background-color: #fff1f0;
color: #ff4d4f;

View File

@@ -7,6 +7,7 @@
<div style="display: flex; align-items: center; width: 100%">
<span style="font-size: 16px; font-weight: bold; margin-right: 20px;">门诊挂号</span>
<div style="flex: 1; display: flex; justify-content: center; align-items: center;">
<el-button type="success" icon="Check" @click="handleCheckIn" size="small">预约签到</el-button>
<el-button type="primary" icon="Document" @click="goToPatientRecord" size="small">档案</el-button>
<el-button type="primary" icon="Plus" @click="handleAddPatient" size="small">新建</el-button>
<el-button type="primary" plain icon="Search" @click="handleSearch" size="small">查询</el-button>
@@ -15,7 +16,7 @@
<el-button type="primary" plain @click="handleReadCard('03')" size="small">医保卡</el-button>
<el-button type="warning" plain icon="CircleClose" @click="handleClear" size="small">清空</el-button>
<el-button type="primary" icon="Plus" @click="handleAdd" size="small">保存挂号</el-button>
<el-button type="success" icon="Printer" @click="handleReprint" size="small">补打挂号</el-button>
<el-button type="info" icon="Printer" @click="handleReprint" size="small">补打挂号</el-button>
</div>
</div>
</template>
@@ -615,6 +616,74 @@
}
"
/>
<!-- 预约签到患者选择弹窗 -->
<el-dialog
v-model="showCheckInPatientModal"
title="请选择预约的患者"
width="1200px"
:close-on-click-modal="false"
>
<div style="margin-bottom: 20px; display: flex; gap: 10px;">
<el-input
v-model="checkInSearchKey"
placeholder="输入患者姓名回车查询"
style="width: 400px"
@keyup.enter="loadCheckInPatientList"
/>
<el-button type="primary" @click="loadCheckInPatientList">查询</el-button>
</div>
<el-table
v-loading="checkInLoading"
:data="checkInPatientList"
border
style="width: 100%"
@row-click="selectRow"
highlight-current-row
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="patientId" label="就诊卡号" width="120" align="center" />
<el-table-column prop="patientName" label="姓名" width="120" align="center">
<template #default="scope">
<span style="color: #ff4d4f">{{ scope.row.patientName }}</span>
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" width="80" align="center" />
<el-table-column label="证件类型" width="150" align="center">
<template #default>居民身份证</template>
</el-table-column>
<el-table-column prop="idCard" label="证件号码" width="200" align="center" />
<el-table-column prop="phone" label="手机号码" width="150" align="center" />
<el-table-column label="号源类型" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.ticketType === 'expert' ? 'danger' : 'success'">
{{ scope.row.ticketType === 'expert' ? '专家号' : '普通号' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fee" label="预约金额" width="100" align="center">
<template #default="scope">
<span style="font-weight: bold; color: #f5222d">{{ scope.row.fee }}</span>
</template>
</el-table-column>
<el-table-column prop="dateTime" label="就诊时间" width="180" align="center" />
</el-table>
<div style="margin-top: 20px; display: flex; justify-content: space-between; align-items: center;">
<el-pagination
v-model:current-page="checkInPage"
v-model:page-size="checkInLimit"
:total="checkInTotal"
layout="prev, pager, next"
@current-change="loadCheckInPatientList"
/>
<div class="dialog-footer">
<el-button @click="showCheckInPatientModal = false">取消</el-button>
<el-button type="primary" @click="confirmCheckIn" :disabled="!selectedCheckInPatient">确定</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
@@ -632,7 +701,8 @@ import {
returnRegister,
updatePatientPhone,
} from './components/outpatientregistration';
import {invokeYbPlugin5000, invokeYbPlugin5001} from '@/api/public';
import { listTicket, checkInTicket } from '@/api/appoinmentmanage/ticket';
import { invokeYbPlugin5000, invokeYbPlugin5001 } from '@/api/public';
import patientInfoDialog from './components/patientInfoDialog';
import PatientAddDialog from './components/patientAddDialog';
import patientList from './components/patientList';
@@ -644,7 +714,7 @@ import {handleColor} from '@/utils/his';
import useUserStore from '@/store/modules/user';
import {formatDateStr} from '@/utils/index';
import {isValidCNPhoneNumber} from '../../../utils/validate';
import {ElMessage} from 'element-plus';
import {ElMessage, ElMessageBox} from 'element-plus';
import {hiprint} from 'vue-plugin-hiprint';
import outpatientRegistrationTemplate from '@/components/Print/OutpatientRegistration.json';
@@ -687,14 +757,25 @@ const ybTypeRef = ref(null);
const openDialog = ref(false);
const openRefundDialog = ref(false);
const openReprintDialog = ref(false);
// 预约签到相关变量
const showCheckInPatientModal = ref(false);
const checkInPatientList = ref([]);
const selectedCheckInPatient = ref(null);
const totalAmount = ref(0);
const chargeItemIdList = ref([]);
const chrgBchnoList = ref([]);
const paymentId = ref('');
const loadingText = ref('');
const checkInSearchKey = ref('');
const checkInPage = ref(1);
const checkInLimit = ref(10);
const checkInTotal = ref(0);
const checkInLoading = ref(false);
const registerInfo = ref({}); // 原挂号记录信息
const queryType = ref('all'); // 查询类型all-全部, normal-正常挂号, returned-退号记录
const guardianAgeConfig = ref(''); // 监护人规定年龄配置
const currentSlotId = ref(null); // 当前预约签到的号源ID
// 使用 ref 定义查询所得用户信息数据
const patientInfoList = ref(undefined);
@@ -1584,6 +1665,189 @@ function handleReprint() {
openReprintDialog.value = true;
}
/** 预约签到 - 打开患者选择弹窗 */
function handleCheckIn() {
// 打开患者选择弹窗,显示已预约但未签到的患者列表
showCheckInPatientModal.value = true;
// 加载已预约未签到的患者列表
loadCheckInPatientList();
}
/** 加载预约签到患者列表 */
function loadCheckInPatientList() {
checkInLoading.value = true;
const today = formatDateStr(new Date(), 'YYYY-MM-DD');
listTicket({
date: today,
status: 'booked',
name: checkInSearchKey.value, // 支持姓名等模糊查询,后端需适配
page: checkInPage.value,
limit: checkInLimit.value
}).then(res => {
const data = res.data?.list || res.list || res.data || [];
const total = res.data?.total || res.total || data.length;
checkInPatientList.value = data.map(item => ({
...item,
appointmentDate: item.scheduleDate + ' ' + (item.expectTime || '')
}));
checkInTotal.value = total;
}).catch(err => {
console.error('加载预约导出失败:', err);
ElMessage.error('获取预约列表失败');
}).finally(() => {
checkInLoading.value = false;
});
}
/** 弹窗行点击处理 */
function selectRow(row) {
selectedCheckInPatient.value = row;
}
/** 确认签到(一键签到:直接构建挂号参数 → 预结算 → 弹收费窗口) */
async function confirmCheckIn() {
if (!selectedCheckInPatient.value) {
ElMessage.warning('请先选择患者');
return;
}
const patient = selectedCheckInPatient.value;
// 每次开始新的签到流程先清理残留 slotId避免历史脏值串单
currentSlotId.value = null;
// 弹出确认提示
try {
await ElMessageBox.confirm(
`确认为患者【${patient.patientName}】办理签到挂号?\n` +
`科室:${patient.department || '-'}\n` +
`医生:${patient.doctor || '-'}\n` +
`费用:¥${patient.fee || '0.00'}`,
'签到确认',
{
confirmButtonText: '确认签到',
cancelButtonText: '取消',
type: 'info',
}
);
} catch {
// 用户点了取消
return;
}
showCheckInPatientModal.value = false;
readCardLoading.value = true;
loadingText.value = '正在处理签到挂号...';
try {
// 1. 用科室ID加载该科室的挂号类型列表获取 serviceTypeId 和 definitionId
const healthcareRes = await getHealthcareMetadata({ organizationId: patient.departmentId });
const healthcareRecords = healthcareRes.data?.records || [];
if (healthcareRecords.length === 0) {
ElMessage.error('该科室未配置挂号类型,无法自动签到');
readCardLoading.value = false;
return;
}
// 2. 按号源类型(专家/普通)模糊匹配挂号类型
const matchTypeName = (patient.ticketType === 'expert') ? '专家' : '普通';
const matchedService = healthcareRecords.find(h => h.name && h.name.includes(matchTypeName));
if (!matchedService) {
// 匹配不到就取第一个作为兜底
ElMessage.warning('未精确匹配到挂号类型,已使用默认类型');
}
const service = matchedService || healthcareRecords[0];
const realPatientId = patient.realPatientId; // 后端新增的真实患者数据库ID
if (!realPatientId) {
ElMessage.error('患者ID缺失请联系管理员检查预约数据');
readCardLoading.value = false;
return;
}
// 3. 构建挂号参数(与 transformFormData 结构一致)
const registrationParam = {
encounterFormData: {
patientId: realPatientId,
priorityEnum: 3, // 默认优先级
serviceTypeId: service.id,
organizationId: patient.departmentId,
},
encounterLocationFormData: {
locationId: null,
},
encounterParticipantFormData: {
practitionerId: patient.doctorId,
},
accountFormData: {
patientId: realPatientId,
typeCode: 1, // 个人现金账户
contractNo: '0000', // 默认自费
},
chargeItemFormData: {
patientId: realPatientId,
definitionId: service.definitionId,
serviceId: service.id,
totalPrice: parseFloat(patient.fee) || ((service.price || 0) + (service.activityPrice || 0)),
},
};
// 4. 设置 patientInfoChargeDialog 需要展示)
patientInfo.value = {
patientId: realPatientId,
patientName: patient.patientName,
genderEnum_enumText: patient.gender || '-',
age: '',
contractName: '自费',
idCard: patient.idCard,
phone: patient.phone,
categoryEnum: '门诊',
organizationName: patient.department || '',
practitionerName: patient.doctor || '',
healthcareName: service.name || '',
};
// 同步设置 form 的 contractNoChargeDialog 的 feeType 会读取它
form.value.contractNo = '0000';
// 5. 调用预结算接口reg-pre-pay
const res = await addOutpatientRegistration(registrationParam);
if (res.code == 200) {
// 仅在预结算成功后记录待签到的号源,避免失败路径残留脏数据
currentSlotId.value = patient.slot_id;
// 6. 设置收费弹窗所需的数据
chrgBchno.value = res.data.chrgBchno;
registerBusNo.value = res.data.busNo;
totalAmount.value = res.data.psnCashPay;
patientInfo.value.encounterId = res.data.encounterId || '';
patientInfo.value.busNo = res.data.busNo || '';
transformedData.value = registrationParam;
chargeItemIdList.value = [];
// 7. 打开收费弹窗
openDialog.value = true;
// 打印挂号单
printRegistrationByHiprint(res.data);
} else {
currentSlotId.value = null;
ElMessage.error(res.msg || '预结算失败');
}
} catch (err) {
currentSlotId.value = null;
console.error('预约签到失败:', err);
ElMessage.error('签到处理失败: ' + (err.message || '未知错误'));
} finally {
readCardLoading.value = false;
}
}
/**
* 点击患者列表给表单赋值
*/
@@ -1656,20 +1920,29 @@ function handleClose(value) {
proxy.$modal.msgSuccess('操作成功');
// 更新患者手机号
updatePhone();
// getList();
// reset();
// addOutpatientRegistration(transformedData.value).then((response) => {
// reset();
// proxy.$modal.msgSuccess('新增成功');
// getList();
// });
// 先取出并清空,避免接口失败/取消等路径导致 slotId 残留污染下一单
const pendingSlotId = currentSlotId.value;
currentSlotId.value = null;
// 如果是预约签到的挂号,执行签到状态更新
if (pendingSlotId) {
checkInTicket(pendingSlotId).then(() => {
console.log('预约状态已更新为已取号');
}).catch(err => {
console.error('更新预约状态失败:', err);
ElMessage.error('预约状态更新失败,请手动签到');
});
}
} else if (value == 'cancel') {
currentSlotId.value = null;
// cancelRegister(patientInfo.value.encounterId).then((res) => {
// if (res.code == 200) {
// getList();
// }
// });
} else {
currentSlotId.value = null;
openRefundDialog.value = false;
}
}