Files
his/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue
2026-01-22 15:09:52 +08:00

1927 lines
53 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="ticket-management-container">
<!-- 顶部搜索区域 -->
<div id="topSearchArea" class="top-search-area">
<!-- 搜索标签行 -->
<div class="search-labels">
<div class="search-label">号源日期</div>
<div class="search-label">状态</div>
<div class="search-label">患者姓名</div>
<div class="search-label">就诊卡号</div>
<div class="search-label">手机号</div>
<div class="search-label"></div> <!-- 为查询按钮预留空间 -->
</div>
<!-- 搜索输入行 -->
<div class="search-inputs">
<!-- 移动端汉堡菜单按钮 -->
<div class="hamburger-menu" @click="toggleSidebar" v-if="isMobile">
<i class="el-icon-menu"></i>
</div>
<div id="datePicker" class="date-picker">
<el-date-picker
v-model="selectedDate"
type="date"
placeholder="选择日期"
format="YYYY/MM/DD"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
class="date-picker"
@change="onDateChange"
/>
</div>
<div id="statusFilter" class="status-filter">
<select id="status-select" class="search-select" v-model="selectedStatus">
<option value="all">全部</option>
<option value="unbooked">未预约</option>
<option value="booked">已预约</option>
<option value="checked">已取号</option>
<option value="cancelled">已停诊</option>
</select>
</div>
<div id="patientSearch" class="patient-search">
<input id="patient-name-input" class="search-input" placeholder="姓名" v-model="patientName">
</div>
<div id="cardSearch" class="card-search">
<input id="patient-card-input" class="search-input" placeholder="就诊卡号" v-model="patientCard">
</div>
<div id="phoneSearch" class="phone-search">
<input id="patient-phone-input" class="search-input" placeholder="手机号" v-model="patientPhone">
</div>
<div class="search-button">
<button id="search-button" class="search-btn" @click="onSearch" :disabled="isLoading">
<span v-if="isLoading" class="loading-text">搜索中...</span>
<span v-else>查询</span>
</button>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div id="mainContent" class="main-content">
<!-- 左侧筛选区域 -->
<div id="leftSidebar" class="left-sidebar" :class="{ 'sidebar-hidden': isMobile && !showSidebar, 'sidebar-mobile': isMobile }">
<!-- 科室列表 -->
<div class="section">
<h3 class="section-title">科室列表</h3>
<select id="department-list" class="search-select" v-model="selectedDepartment" @change="onDepartmentChange">
<option value="all">全部科室</option>
<option v-for="department in departments" :key="department.value" :value="department.value">
{{ department.label }}
</option>
</select>
</div>
<!-- 号源类型 -->
<div class="section">
<h3 class="section-title">号源类型</h3>
<div class="radio-group">
<label>
<input type="radio" name="type" value="general" v-model="selectedType" @change="onTypeChange">
<span>普通号</span>
</label>
<label>
<input type="radio" name="type" value="expert" v-model="selectedType" @change="onTypeChange">
<span>专家号</span>
</label>
</div>
</div>
<!-- 医生号源列表 -->
<div class="section">
<h3 class="section-title">医生号源列表</h3>
<input id="doctor-search" class="search-input" placeholder="搜索医生姓名" v-model="searchQuery" @input="onDoctorSearch">
<div class="doctor-list">
<div class="doctor-item" v-for="doctor in filteredDoctors" :key="doctor.id" :class="{ selected: selectedDoctorId === doctor.id }" @click="selectDoctor(doctor.id)">
<div class="doctor-name">{{ doctor.name }} <span class="doctor-available">余号{{ doctor.available }}</span></div>
</div>
</div>
</div>
</div>
<!-- 右侧号源卡片区域 -->
<div class="right-content" :class="{ 'right-content-full': isMobile && !showSidebar }">
<!-- 使用网格布局的号源卡片列表 -->
<div class="ticket-grid" v-if="filteredTickets.length > 0">
<div v-for="(item, index) in filteredTickets" :key="item.slot_id" class="ticket-card" @dblclick="handleDoubleClick(item)" @contextmenu.prevent="handleRightClick($event, item)">
<!-- 序号放在最右侧 -->
<div class="ticket-index">{{ index + 1 }}</div>
<!-- 1.时间 -->
<div class="ticket-id-time">{{ item.dateTime }}</div>
<!-- 2. 状态标签 -->
<div class="ticket-status" :class="`status-${item.status}`">
<span class="status-dot"></span>
{{ item.status }}
</div>
<!-- 3. 医生姓名截断显示悬停展示完整信息 -->
<div class="ticket-doctor" :title="item.doctor">{{ item.doctor }}</div>
<!-- 4. 挂号费 -->
<div class="ticket-fee">挂号费{{ item.fee }}</div>
<!-- 5. 号源类型 -->
<div class="ticket-type">{{ item.ticketType === 'general' ? '普通' : '专家' }}</div>
<!-- 6. 已预约患者信息 -->
<div v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName" class="ticket-patient">
{{ item.patientName }}({{ item.patientId }})
</div>
<!-- 7. 患者电话号码 -->
<div v-if="(item.status === '已预约' || item.status === '已取号') && item.phone" class="ticket-phone">
电话号码 {{ item.phone }}
</div>
<div class="ticket-actions">
<button class="action-button book-button" @click="openPatientSelectModal(item.slot_id)" :disabled="item.status !== '未预约'" :class="{ 'disabled': item.status !== '未预约' }">
<i class="el-icon-tickets"></i>
预约
</button>
</div>
</div>
<div v-if="hasMore" class="loading-more">
加载中...
</div>
<div v-else class="no-more">
没有更多数据了
</div>
</div>
<div v-else class="empty-state">
<div class="empty-text">暂无号源数据</div>
</div>
</div>
</div>
<!-- 患者选择弹窗 -->
<div id="patient-select-modal" class="modal" v-if="showPatientModal">
<div class="modal-content">
<div class="modal-header">
<h3>选择患者</h3>
<button class="close-btn" @click="closePatientSelectModal">&times;</button>
</div>
<div class="modal-body">
<input id="patientName" class="search-input" placeholder="患者姓名" v-model="patientSearchParams.patientName" @input="onPatientSearchInput">
<input id="medicalCard" class="search-input" placeholder="就诊卡号" v-model="patientSearchParams.medicalCard" @input="onPatientSearchInput">
<input id="idCard" class="search-input" placeholder="身份证号" v-model="patientSearchParams.idCard" @input="onPatientSearchInput">
<input id="phone" class="search-input" placeholder="手机号" v-model="patientSearchParams.phone" @input="onPatientSearchInput">
<button id="patient-search-btn" class="search-btn" @click="searchPatients">查询</button>
<div class="patient-table-container">
<table id="patient-table" class="patient-table" v-if="patients.length > 0">
<thead>
<tr>
<th>序号</th>
<th>患者姓名</th>
<th>就诊卡号</th>
<th>性别</th>
<th>身份证号</th>
<th>手机号</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(patient, index) in patients" :key="patient.idCard" :class="{ selected: selectedPatientId === patient.idCard }" @click="selectPatient(patient.idCard)">
<td>{{ index + 1 }}</td>
<td>{{ patient.name }}</td>
<td>{{ patient.medicalCard }}</td>
<td>{{ patient.gender || '-' }}</td>
<td>{{ patient.idCard }}</td>
<td>{{ patient.phone }}</td>
<td>
<button class="select-btn" @click.stop="selectPatient(patient.idCard)" :disabled="!patient.idCard">选择</button>
</td>
</tr>
</tbody>
</table>
<!-- 无搜索结果提示 -->
<div v-else-if="hasSearchCriteria && !isLoading" class="no-results">
未找到符合条件的患者请检查搜索条件
</div>
</div>
</div>
<div class="modal-footer">
<button class="confirm-btn" @click="confirmPatientSelection">确认</button>
<button class="cancel-btn" @click="closePatientSelectModal">取消</button>
</div>
</div>
</div>
<!-- 右键菜单 -->
<div
v-if="contextMenuVisible"
class="context-menu"
:style="{
left: contextMenuPosition.x + 'px',
top: contextMenuPosition.y + 'px'
}"
@click.stop
@contextmenu.prevent
>
<div v-if="selectedTicketForCancel && selectedTicketForCancel.status === '已预约'" class="menu-item" @click="confirmCancelAppointment">
取消预约
</div>
</div>
<!-- 点击页面其他地方关闭右键菜单 -->
<div
v-if="contextMenuVisible"
class="context-menu-overlay"
@click="closeContextMenu"
@contextmenu.prevent
></div>
</div>
</template>
<script>
import { listTicket, bookTicket, cancelTicket, checkInTicket, cancelConsultation, listAllTickets } from '@/api/appoinmentmanage/ticket';
import { getPatientList } from '@/api/cardRenewal/api';
import { listDept } from '@/api/appoinmentmanage/dept';
import { ref } from 'vue'
import { ElDatePicker, ElPagination, ElMessageBox, ElMessage } from 'element-plus'
import useUserStore from '@/store/modules/user'
export default {
name: 'OutpatientAppointment',
components: {
ElDatePicker,
ElPagination
},
// 添加全局点击事件监听器,用于关闭右键菜单
mounted() {
document.addEventListener('click', this.closeContextMenu);
},
// 移除事件监听器
beforeUnmount() {
document.removeEventListener('click', this.closeContextMenu);
},
data() {
return {
selectedDate: new Date().toISOString().split('T')[0],
selectedStatus: 'unbooked',
selectedDepartment: 'all',
selectedType: 'general', // 普通号默认选中
selectedDoctorId: null,
showPatientModal: false,
selectedPatientId: null,
selectedPatient: null,
currentTicket: null,
patientName: '',
patientCard: '',
patientPhone: '',
isLoading: false,
// 移动端相关变量
isMobile: false,
showSidebar: false,
// 分页相关属性
currentPage: 1,
pageSize: 20,
totalTickets: 0,
hasMore: true,
// 右键菜单相关
contextMenuVisible: false,
contextMenuPosition: { x: 0, y: 0 },
selectedTicketForCancel: null,
departments: [
{ value: 'all', label: '全部科室' }
],
doctors: [],
allTickets: [], // 存储所有原始号源数据
tickets: [], // 从API获取的真实号源数据
patients: [], // 从API获取的真实患者数据
searchQuery: '',
// 患者搜索相关
patientSearchParams: {
patientName: '',
medicalCard: '',
idCard: '',
phone: ''
},
dateShortcuts: [
{ text: '今天', value: new Date() },
{ text: '明天', value: () => { const date = new Date(); date.setDate(date.getDate() + 1); return date; } },
{ text: '本周', value: () => { const date = new Date(); const day = date.getDay() || 7; date.setDate(date.getDate() - day + 1); return date; } },
{ text: '下周', value: () => { const date = new Date(); const day = date.getDay() || 7; date.setDate(date.getDate() - day + 8); return date; } },
{ text: '本月', value: () => { const date = new Date(); date.setDate(1); return date; } },
{ text: '下月', value: () => { const date = new Date(); date.setMonth(date.getMonth() + 1); date.setDate(1); return date; } }
]
}
},
computed: {
filteredDoctors() {
let filtered = [...this.doctors];
// 根据号源类型过滤医生列表
if (this.selectedType === 'general') {
filtered = filtered.filter(doctor => doctor.type === 'general');
} else if (this.selectedType === 'expert') {
filtered = filtered.filter(doctor => doctor.type === 'expert');
}
// 根据搜索关键词过滤
if (this.searchQuery) {
filtered = filtered.filter(doctor =>
doctor.name.includes(this.searchQuery)
);
}
return filtered;
},
filteredTickets() {
let filtered = [...this.tickets];
//按日期筛选(如果选择了日期)
if (this.selectedDate) {
filtered = filtered.filter(ticket => {
// 使用后端返回的dateTime字段进行筛选
if (ticket.dateTime) {
return ticket.dateTime.startsWith(this.selectedDate);
}
return true;
});
}
//按状态筛选
if (this.selectedStatus !== 'all') {
// 状态映射表(后端返回中文状态,前端使用英文状态)
const statusMap = {
'unbooked': '未预约',
'booked': '已预约',
'checked': '已取号',
'cancelled': '已停诊'
};
const chineseStatus = statusMap[this.selectedStatus];
filtered = filtered.filter(ticket => ticket.status === chineseStatus);
}
// 按科室筛选
if (this.selectedDepartment !== 'all') {
filtered = filtered.filter(ticket => ticket.department === this.selectedDepartment);
}
// 按号源类型筛选
filtered = filtered.filter(ticket => {
if (this.selectedType === 'general') {
return ticket.ticketType === 'general';
} else if (this.selectedType === 'expert') {
return ticket.ticketType === 'expert';
}
return true;
});
// 按医生筛选 - 暂时注释掉以排查问题
if (this.selectedDoctorId) {
const selectedDoctor = this.doctors.find(d => d.id === this.selectedDoctorId);
if (selectedDoctor) {
filtered = filtered.filter(ticket => ticket.doctor === selectedDoctor.name);
}
}
return filtered;
},
hasSearchCriteria() {
return Object.values(this.patientSearchParams).some(value => value && value.trim() !== '');
},
},
methods: {
selectDoctor(doctorId) {
this.selectedDoctorId = doctorId
// 医生选择后,筛选号源
this.onSearch();
},
onTypeChange() {
// 移除onSearch()调用,让计算属性自动处理类型筛选
},
onDepartmentChange() {
this.onSearch()
},
onDoctorSearch() {
// 使用计算属性filteredDoctors实现实时搜索无需调用API
},
openPatientSelectModal(ticketId) {
this.currentTicket = this.tickets.find(ticket => ticket.slot_id === ticketId);
// 重置搜索参数
this.patientSearchParams = {
patientName: '',
medicalCard: '',
idCard: '',
phone: ''
};
// 清空患者列表
this.patients = [];
this.showPatientModal = true;
// 默认加载所有患者
this.searchPatients();
},
// 患者搜索
searchPatients() {
// 检查是否有搜索条件
const hasSearchCriteria = Object.values(this.patientSearchParams).some(value => value && value.trim() !== '');
// 设置加载状态
this.isLoading = true;
// 准备搜索参数,确保支持模糊匹配
const searchParams = {
patientName: this.patientSearchParams.patientName?.trim() || '',
medicalCard: this.patientSearchParams.medicalCard?.trim() || '',
idCard: this.patientSearchParams.idCard?.trim() || '',
phone: this.patientSearchParams.phone?.trim() || ''
};
// 调用真实API获取患者列表
getPatientList(this.patientSearchParams).then(response => {
// 尝试不同的数据结构解析
let records = [];
if (response.data && response.data.records) {
records = response.data.records;
} else if (response.data && Array.isArray(response.data)) {
records = response.data;
} else if (Array.isArray(response)) {
records = response;
}
this.patients = records;
// 在前端进行额外的模糊匹配过滤以防后端API不支持完整的模糊匹配
if (hasSearchCriteria) {
this.patients = this.patients.filter(patient => {
const nameMatch = !searchParams.patientName ||
(patient.name && patient.name.toLowerCase().includes(searchParams.patientName.toLowerCase()));
const cardMatch = !searchParams.medicalCard ||
(patient.medicalCard && patient.medicalCard.toLowerCase().includes(searchParams.medicalCard.toLowerCase()));
const idMatch = !searchParams.idCard ||
(patient.idCard && patient.idCard.toLowerCase().includes(searchParams.idCard.toLowerCase()));
const phoneMatch = !searchParams.phone ||
(patient.phone && patient.phone.toLowerCase().includes(searchParams.phone.toLowerCase()));
return nameMatch && cardMatch && idMatch && phoneMatch;
});
}
// 验证每个患者是否有idCard字段如果没有尝试使用其他唯一标识
this.patients.forEach((patient, index) => {
// 如果没有idCard尝试设置一个临时唯一标识
if (!patient.idCard) {
patient.idCard = patient.id || patient.medicalCard || `temp_${index}`;
}
});
if (this.patients.length === 0) {
ElMessage.error('没有找到患者数据,请检查搜索条件或联系管理员');
}
// 清除加载状态
this.isLoading = false;
}).catch(error => {
this.patients = [];
ElMessage.error('获取患者列表失败:' + (error.message || '未知错误'));
// 清除加载状态
this.isLoading = false;
});
},
// 监听患者搜索输入变化
onPatientSearchInput(event) {
const field = event.target.id;
const value = event.target.value;
// 根据输入框ID更新对应的搜索参数
switch(field) {
case 'patientName':
this.patientSearchParams.patientName = value;
break;
case 'medicalCard':
this.patientSearchParams.medicalCard = value;
break;
case 'idCard':
this.patientSearchParams.idCard = value;
break;
case 'phone':
this.patientSearchParams.phone = value;
break;
}
},
// 双击未预约卡片触发患者选择流程
handleDoubleClick(ticket) {
if (ticket.status === '未预约') {
this.currentTicket = ticket;
this.selectedPatientId = null;
this.selectedPatient = null;
// 先打开弹窗,再加载患者数据,避免等待
this.showPatientModal = true;
// 调用患者搜索接口,加载患者列表
this.searchPatients();
}
},
// 右键已预约卡片显示取消预约菜单
handleRightClick(event, ticket) {
if (ticket.status === '已预约') {
this.selectedTicketForCancel = ticket;
this.contextMenuPosition = { x: event.clientX, y: event.clientY };
this.contextMenuVisible = true;
}
},
// 关闭右键菜单
closeContextMenu() {
this.contextMenuVisible = false;
this.selectedTicketForCancel = null;
},
// 确认取消预约
confirmCancelAppointment() {
if (this.selectedTicketForCancel) {
// 使用 ElMessageBox.confirm 进行二次确认,显示患者姓名
ElMessageBox.confirm(
`确认取消患者${this.selectedTicketForCancel.patientName || ''}的预约?`,
'系统提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 用户点击确定调用API取消预约
this.cancelAppointment(this.selectedTicketForCancel);
}).catch(() => {
// 用户取消操作
this.closeContextMenu();
});
}
},
// 取消预约API调用
cancelAppointment(ticket) {
if (!ticket || !ticket.slot_id) {
ElMessage.error('取消预约失败:缺少号源信息');
this.closeContextMenu();
return;
}
// 使用真实API调用取消预约传递slot_id
cancelTicket(ticket.slot_id).then(response => {
// 根据后端返回判断是否成功
if (response.code === 200 || response.msg === '取消成功' || response.message === '取消成功') {
console.log('取消预约成功,更新前端状态');
// API调用成功后更新当前卡片状态
const ticketIndex = this.tickets.findIndex(t => t.slot_id === ticket.slot_id);
if (ticketIndex !== -1) {
// 清除该号源关联的所有患者信息
this.tickets[ticketIndex].status = '未预约';
this.tickets[ticketIndex].patientName = null;
this.tickets[ticketIndex].patientId = null;
this.tickets[ticketIndex].patientGender = null;
this.tickets[ticketIndex].medicalCard = null;
this.tickets[ticketIndex].phone = null;
// 更新医生号源列表中的余号数量
this.updateDoctorsList();
}
// 关闭上下文菜单
this.closeContextMenu();
ElMessage.success('预约已取消,号源已释放');
} else {
// 取消失败
const errorMsg = response.msg || response.message || '取消预约失败';
ElMessage.error(`取消预约失败:${errorMsg}`);
this.closeContextMenu();
}
}).catch(error => {
const errorMsg = error.message || '取消预约失败,请稍后重试';
ElMessage.error(`取消预约失败:${errorMsg}`);
this.closeContextMenu();
});
},
closePatientSelectModal() {
this.showPatientModal = false
this.selectedPatientId = null
},
selectPatient(patientId) {
// 确保患者数据已加载
if (this.patients.length === 0) {
ElMessage.error('患者数据未加载,请先搜索患者');
return;
}
this.selectedPatientId = patientId;
// 保存选择的患者对象 - 使用idCard作为唯一标识但增加容错性
this.selectedPatient = this.patients.find(patient => {
// 尝试多种匹配方式
const matchByIdCard = patient.idCard === patientId;
const matchById = patient.id === patientId;
const matchByMedicalCard = patient.medicalCard === patientId;
const match = matchByIdCard || matchById || matchByMedicalCard;
return match;
});
if (this.selectedPatient) {
// 使用 ElMessageBox.confirm 进行二次确认
ElMessageBox.confirm(
`确认选择患者 ${this.selectedPatient.name} 进行预约?`,
'患者确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
).then(() => {
// 用户点击确定,自动滚动到确认按钮
ElMessage.success('已选择患者: ' + this.selectedPatient.name);
const confirmBtn = document.querySelector('.confirm-btn');
if (confirmBtn) {
confirmBtn.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}).catch(() => {
// 用户取消选择,清空已选患者
this.selectedPatientId = null;
this.selectedPatient = null;
ElMessage.info('已取消患者选择');
});
} else {
ElMessage.error('未找到该患者,请重新选择');
this.selectedPatientId = null;
}
},
confirmPatientSelection() {
if (!this.selectedPatientId || !this.selectedPatient) {
ElMessage.error('请选择患者');
return;
}
if (!this.currentTicket) {
ElMessage.error('预约信息错误,请重新选择号源');
this.closePatientSelectModal();
return;
}
try {
const userStore = useUserStore();
const appointmentData = {
slotId: this.currentTicket.slot_id, // 添加号源ID
ticketId: Number(this.currentTicket.slot_id),
patientId: this.selectedPatientId, // 使用身份证号作为患者ID不需要转换为数字
patientName: this.selectedPatient.name,
medicalCard: this.selectedPatient.medicalCard,
phone: this.selectedPatient.phone,
gender: this.selectedPatient.gender,
fee: Number(this.currentTicket.fee) || 0,
regType: this.currentTicket.ticketType === 'general' ? '普通' : '专家',
tenant_id: userStore.tenantId || '1' // 修改为tenant_id以匹配后端数据库字段名
};
// 验证必填字段
if (!appointmentData.ticketId || isNaN(appointmentData.ticketId)) {
ElMessage.error('号源ID无效');
return;
}
if (!appointmentData.patientId) {
ElMessage.error('患者ID无效');
return;
}
// 调用真实API进行预约
bookTicket(appointmentData).then(response => {
const ticketIndex = this.tickets.findIndex(t => t.slot_id === this.currentTicket.slot_id);
if (ticketIndex !== -1) {
this.tickets[ticketIndex].status = '已预约';
this.tickets[ticketIndex].patientName = this.selectedPatient.name;
this.tickets[ticketIndex].patientId = this.selectedPatient.medicalCard;
this.tickets[ticketIndex].patientGender = this.selectedPatient.gender;
// 更新医生号源列表中的余号数量
this.updateDoctorsList();
}
this.closePatientSelectModal();
// 重新加载号源数据,确保显示最新状态
this.onSearch();
ElMessage.success('预约成功,号源已锁定。患者到院签到时需缴费取号。');
}).catch(error => {
ElMessage.error('预约失败,请稍后重试。');
});
} catch (error) {
ElMessage.error('预约信息格式错误,请重新操作。');
}
},
onDateChange() {
// 重置页码并重新加载数据
this.currentPage = 1;
this.onSearch()
},
// 切换侧边栏显示/隐藏
toggleSidebar() {
this.showSidebar = !this.showSidebar;
},
// 检测是否为移动设备
checkMobileDevice() {
this.isMobile = window.innerWidth <= 768;
},
onSearch() {
this.isLoading = true
// 调用真实API获取号源数据
const queryParams = {
date: this.selectedDate,
status: this.selectedStatus,
type: this.selectedType,
name: this.patientName,
card: this.patientCard,
phone: this.patientPhone,
page: this.currentPage,
limit: this.pageSize
};
// 并行调用号源和科室API
Promise.all([
listAllTickets(), // 使用测试接口获取固定的5条专家号数据
listDept()
]).then(([ticketResponse, deptResponse]) => {
// 处理号源数据
if (ticketResponse) {
// 检查是否有data.list字段后端返回的数据结构是{code, data:{list, total}}
if (ticketResponse.data && ticketResponse.data.list) {
this.tickets = ticketResponse.data.list;
this.totalTickets = ticketResponse.data.total;
} else {
// 如果没有data.list字段可能数据直接在对象中
this.tickets = [ticketResponse];
this.totalTickets = 1;
}
// 保存所有原始数据
this.allTickets = [...this.tickets];
// 根据患者信息进行前端筛选
this.filterTicketsByPatientInfo();
// 更新医生列表
this.updateDoctorsList();
this.hasMore = this.tickets.length < this.totalTickets;
} else {
this.tickets = [];
this.allTickets = [];
this.totalTickets = 0;
this.hasMore = false;
}
// 处理科室数据
this.updateDepartmentsListFromApi(deptResponse);
this.isLoading = false;
}).catch(error => {
console.error('获取数据失败:', error);
this.tickets = [];
this.allTickets = [];
this.totalTickets = 0;
this.hasMore = false;
this.departments = [{ value: 'all', label: '全部科室' }];
this.isLoading = false;
});
},
// 从号源数据中更新科室列表(备用方法)
updateDepartmentsList() {
const departmentSet = new Set();
this.tickets.forEach(ticket => {
departmentSet.add(ticket.department);
});
// 转换为数组并添加"全部科室"选项
const departmentArray = Array.from(departmentSet).map(dept => ({
value: dept,
label: dept
}));
this.departments = [
{ value: 'all', label: '全部科室' },
...departmentArray
];
},
// 从API获取完整科室列表
updateDepartmentsListFromApi(deptResponse) {
// 检查后端返回的数据结构
let deptList = [];
if (deptResponse.data) {
// 适配不同的后端数据结构
if (Array.isArray(deptResponse.data)) {
deptList = deptResponse.data;
} else if (Array.isArray(deptResponse.data.data)) {
deptList = deptResponse.data.data;
}
}
if (deptList.length > 0) {
// 从API返回的科室数据中提取科室名称
const departmentArray = deptList
.filter(dept => dept.deptName) // 确保科室名称存在
.map(dept => ({
value: dept.deptName,
label: dept.deptName
}))
.filter((dept, index, self) =>
index === self.findIndex((d) => d.value === dept.value) // 去重
);
this.departments = [
{ value: 'all', label: '全部科室' },
...departmentArray
];
} else {
// 如果API调用失败或没有数据回退到从号源数据中提取
this.updateDepartmentsList();
}
},
// 根据患者信息过滤号源
filterTicketsByPatientInfo() {
// 如果没有输入患者信息,就不进行过滤
if (!this.patientName && !this.patientCard && !this.patientPhone) {
return;
}
// 根据患者信息进行过滤使用AND逻辑全部匹配
this.tickets = this.tickets.filter(ticket => {
const matchesName = !this.patientName || ticket.patientName?.includes(this.patientName);
const matchesCard = !this.patientCard || ticket.patientId?.includes(this.patientCard);
const matchesPhone = !this.patientPhone || ticket.phone?.includes(this.patientPhone);
return matchesName && matchesCard && matchesPhone;
});
console.log('按患者信息过滤后的号源数量:', this.tickets.length);
},
// 获取号源状态文本
getStatusText(status) {
switch (status) {
case 'unbooked':
return '未预约';
case 'booked':
return '已预约';
case 'checked':
return '已取号';
case 'cancelled':
return '已取消';
case 'locked':
return '已锁定';
default:
return '未知状态';
}
},
// 从号源数据中更新医生列表
updateDoctorsList() {
const doctorMap = new Map();
// 统计每个医生的余号数量和类型
this.tickets.forEach(ticket => {
// 检查ticket对象是否有效
if (!ticket) return;
// 统计所有号源的医生,不仅仅是未预约的
if (doctorMap.has(ticket.doctor)) {
// 如果是未预约状态,增加余号数量
if (ticket.status === '未预约') {
doctorMap.get(ticket.doctor).available += 1;
}
} else {
// 根据号源类型设置医生类型
const doctorType = ticket.ticketType || 'general';
doctorMap.set(ticket.doctor, {
id: Date.now() + Math.random(),
name: ticket.doctor,
available: ticket.status === '未预约' ? 1 : 0,
type: doctorType
});
}
});
// 转换为数组
this.doctors = Array.from(doctorMap.values());
},
// 加载更多数据
loadMore() {
// 已经在加载中,不再重复请求
if (this.isLoading || !this.hasMore) {
return;
}
this.isLoading = true;
this.currentPage++;
const queryParams = {
date: this.selectedDate,
status: this.selectedStatus,
name: this.patientName,
card: this.patientCard,
phone: this.patientPhone,
page: this.currentPage,
limit: this.pageSize
};
listTicket(queryParams).then(response => {
// 处理号源数据
if (response) {
let newRecords = [];
// 检查是否有records字段响应拦截器已经返回了res.data所以直接访问response.records
if (response.records) {
newRecords = response.records;
this.totalTickets = response.total;
} else {
// 如果没有records字段可能数据直接在对象中
newRecords = [response];
this.totalTickets = this.tickets.length + 1;
}
// 将API返回的数据添加到tickets数组中
this.tickets = [...this.tickets, ...newRecords];
}
this.isLoading = false;
this.hasMore = this.tickets.length < this.totalTickets;
// 更新科室和医生列表
this.updateDepartmentsList();
this.updateDoctorsList();
}).catch(error => {
console.error('获取号源列表失败:', error);
this.isLoading = false;
});
},
// 滚动事件处理函数,实现滚动加载
},
mounted() {
// 初始化数据
this.selectedDate = new Date().toISOString().split('T')[0];
// 调用onSearch获取初始数据
this.onSearch();
// 检测是否为移动设备
this.checkMobileDevice();
// 监听窗口大小变化
window.addEventListener('resize', this.checkMobileDevice);
},
beforeUnmount() {
// 移除窗口大小变化监听
window.removeEventListener('resize', this.checkMobileDevice);
}
}
</script>
<style scoped>
/* 颜色变量定义 */
:root {
--primary-color: #1890FF;
--secondary-color: #FF6B35;
--status-unbooked: #5A8DEE;
--status-booked: #FAAD14;
--status-checked: #52C41A;
--status-cancelled: #F5222D;
--border-color: #d9d9d9;
--text-primary: #333;
--text-secondary: #666;
--text-tertiary: #999;
--bg-light: #f8f9fa;
--bg-white: #fff;
--shadow: 0 2px 8px rgba(90, 141, 238, 0.08);
}
/* 基础样式 */
.ticket-management-container {
padding: 20px;
background-color: var(--bg-light);
min-height: 100vh;
}
/* 顶部搜索区域 */
.top-search-area {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
padding: 12px 16px;
background-color: #FFFFFF;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
border: 1px solid #E8E8E8;
}
/* 搜索标签行 */
.search-labels {
display: flex;
gap: 12px;
align-items: center;
}
/* 搜索标签样式 */
.search-label {
font-size: 14px;
color: #333333;
font-weight: 500;
text-align: left;
flex-grow: 1;
min-width: 80px;
}
/* 搜索输入行 */
.search-inputs {
display: flex;
gap: 12px;
align-items: center;
}
/* 为输入行的每个容器设置flex-grow使其均匀分布 */
.search-inputs > div {
flex-grow: 1;
min-width: 100px;
}
.hamburger-menu {
display: none;
cursor: pointer;
font-size: 20px;
color: var(--primary-color);
}
.date-picker {
width: 100%;
}
/* 搜索控件样式 */
.search-input {
padding: 6px 12px;
border: 1px solid #E8E8E8;
border-radius: 4px;
font-size: 14px;
background-color: #FFFFFF;
width: 100%;
height: 32px;
box-sizing: border-box;
}
.search-input:focus {
border-color: #1890FF;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.search-select {
padding: 6px 12px;
border: 1px solid #E8E8E8;
border-radius: 4px;
font-size: 14px;
background-color: #FFFFFF;
cursor: pointer;
width: 100%;
height: 32px;
box-sizing: border-box;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 12px;
}
.search-select:focus {
border-color: #1890FF;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.search-btn {
padding: 0 16px;
background-color: #1890FF;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
height: 32px;
box-sizing: border-box;
transition: background-color 0.3s;
}
.search-btn:hover {
background-color: #40a9ff;
}
.search-btn:active {
background-color: #096dd9;
}
.search-btn:disabled {
background-color: #e6f7ff;
color: #91d5ff;
cursor: not-allowed;
}
/* 日期选择器样式 */
.date-picker {
width: 160px;
z-index: 1;
}
/* 状态筛选器样式 */
.status-filter {
z-index: 2;
}
.el-date-editor {
width: 100% !important;
}
.el-date-editor .el-input__inner {
height: 32px !important;
border-radius: 4px;
border: 1px solid #E8E8E8;
font-size: 14px;
padding: 0 36px 0 12px !important;
}
.el-date-editor .el-input__inner:focus {
border-color: #1890FF;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.el-date-editor .el-input__suffix {
right: 8px !important;
top: 50%;
transform: translateY(-50%);
}
.el-date-editor .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
}
/* 主要内容区域 */
.main-content {
display: flex;
gap: 20px;
height: calc(100vh - 104px); /* 减去顶部搜索区域高度和内边距 */
overflow: hidden;
}
.left-sidebar {
width: 220px;
height: 100%;
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
flex-shrink: 0;
transition: all 0.3s ease;
overflow-y: auto; /* 当内容超出高度时显示滚动条 */
}
/* 移动端侧边栏样式 */
.sidebar-mobile {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 1000;
}
/* 侧边栏隐藏样式 */
.sidebar-hidden {
transform: translateX(-100%);
}
.right-content {
flex: 1;
background-color: white;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
}
/* 移动端右侧内容区占满屏幕 */
.right-content-full {
width: 100%;
}
/* 响应式设计 */
@media (max-width: 768px) {
/* 顶部搜索区域改为纵向排列 */
.top-search-area {
flex-direction: column;
height: auto;
padding: 16px;
gap: 16px;
}
/* 汉堡菜单显示 */
.hamburger-menu {
display: block;
align-self: flex-start;
}
/* 搜索区域元素宽度100% */
.date-picker,
.status-filter,
.patient-search,
.card-search,
.phone-search,
.search-button,
.add-patient {
width: 100%;
}
/* 右侧内容区卡片布局调整 */
.virtual-list {
grid-template-columns: 1fr;
gap: 12px;
padding: 12px;
}
/* 左侧边栏默认隐藏 */
.left-sidebar {
transform: translateX(-100%);
}
/* 右侧内容区占满屏幕 */
.right-content {
width: 100%;
}
}
/* 确保卡片样式正确应用 */
.ticket-card {
box-shadow: var(--shadow);
overflow: hidden;
transition: all 0.3s ease;
}
/* 卡片悬停效果 */
.ticket-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(90, 141, 238, 0.12);
}
/* 按钮悬停效果 */
.search-btn,
.add-patient-btn {
transition: all 0.3s ease;
}
.search-btn:hover,
.add-patient-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
/* 输入框焦点效果 */
.search-input {
transition: all 0.3s ease;
}
.search-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 医生列表项悬停效果 */
.doctor-item {
transition: all 0.3s ease;
}
.doctor-item:hover {
background-color: rgba(24, 144, 255, 0.05);
cursor: pointer;
}
/* 医生列表项选中效果 */
.doctor-item.selected {
background-color: rgba(24, 144, 255, 0.2);
border-left: 4px solid var(--primary-color);
font-weight: 600;
}
/* 加载动画 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
gap: 8px;
color: var(--primary-color);
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(24, 144, 255, 0.2);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 左侧筛选区域 */
.section {
margin-bottom: 24px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #333;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.doctor-list {
margin-top: 12px;
max-height: calc(100vh - 300px); /* 限制医生列表高度 */
overflow-y: auto; /* 当内容超出高度时显示滚动条 */
}
.doctor-item {
padding: 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 8px;
}
.doctor-item:hover {
background-color: #f0f7ff;
}
.doctor-item.selected {
background-color: #e6f2ff;
border-left: 4px solid var(--primary-color);
}
.doctor-name {
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
}
.doctor-available {
font-size: 12px;
color: var(--status-unbooked);
font-weight: normal;
}
/* 右侧号源卡片网格布局 */
.ticket-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 8px;
}
/* 加载更多样式 */
.loading-more,
.no-more {
grid-column: 1 / -1;
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 600px;
color: var(--text-tertiary);
}
.empty-text {
font-size: 16px;
}
.ticket-card {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(90, 141, 238, 0.08);
border: 1px solid #e8e8e8;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
min-height: 160px;
}
.ticket-card:hover {
box-shadow: 0 4px 12px rgba(90, 141, 238, 0.12);
transform: translateY(-2px);
border-color: #1890ff;
}
.ticket-number {
position: absolute;
top: 12px;
left: 12px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #e6f7ff;
color: #1890ff;
font-size: 12px;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
}
.ticket-index {
position: absolute;
top: 12px;
left: 12px;
color: #1890ff;
font-size: 14px;
font-weight: bold;
}
.ticket-time {
font-size: 14px;
color: #333;
font-weight: 500;
margin: 8px 0;
}
.ticket-status {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
text-align: center;
min-width: 60px;
margin-bottom: 8px;
}
.status-unbooked {
background-color: #e6f3ff;
color: #1890ff;
}
.status-locked {
background-color: #fff1f0;
color: #f5222d;
}
.status-booked {
background-color: #fff8e6;
color: #fa8c16;
}
.status-checked {
background-color: #f6ffed;
color: #52c41a;
}
.status-cancelled {
background-color: #fff1f0;
color: #ff4d4f;
}
.ticket-doctor {
font-size: 14px;
color: #333;
margin-bottom: 8px;
text-align: center;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ticket-id-time {
font-size: 14px;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.ticket-fee {
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.ticket-type {
font-size: 12px;
color: #999;
margin-bottom: 12px;
}
.book-button {
padding: 4px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
align-self: center;
}
.book-button:hover:not(:disabled) {
background-color: #40a9ff;
}
.book-button:disabled {
background-color: #e6f7ff;
color: #91d5ff;
cursor: not-allowed;
}
/* 患者信息样式 */
.ticket-patient {
margin-top: 8px;
padding: 4px;
background-color: rgba(24, 144, 255, 0.05);
border-radius: 4px;
font-size: 12px;
text-align: center;
color: #333;
}
/* 患者电话号码样式 */
.ticket-phone {
margin-top: 4px;
padding: 4px;
background-color: rgba(34, 177, 76, 0.05);
border-radius: 4px;
font-size: 12px;
text-align: center;
color: #22b14c;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 768px) {
.ticket-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.ticket-card {
padding: 12px;
min-height: 140px;
}
.ticket-doctor {
font-size: 13px;
}
.ticket-id-time {
font-size: 13px;
}
.ticket-index {
font-size: 13px;
}
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 8px;
width: 80%;
max-width: 800px;
/* 使用固定高度计算确保footer始终可见 */
height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
/* 确保弹窗在视口中居中且不被遮挡 */
margin: 20px;
/* 相对定位为绝对定位的footer提供参考 */
position: relative;
}
.modal-header {
flex-shrink: 0;
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 20px;
/* 给body添加底部内边距确保内容不被footer遮挡 */
padding-bottom: 100px;
}
.modal-footer {
flex-shrink: 0;
/* 确保footer始终在底部可见不受其他元素影响 */
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 16px 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 12px;
background-color: white;
/* 添加阴影,增强视觉层次感 */
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
/* 确保footer不被body内容遮挡 */
}
.confirm-btn {
padding: 8px 24px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
font-weight: 500;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
}
.confirm-btn:hover {
background-color: #40a9ff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
.confirm-btn:active {
background-color: #096dd9;
transform: translateY(0);
}
.cancel-btn {
padding: 8px 24px;
background-color: white;
color: var(--text-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.cancel-btn:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 20px;
}
.patient-table-container {
margin-top: 20px;
overflow-x: auto;
}
.patient-table {
width: 100%;
border-collapse: collapse;
}
.patient-table th,
.patient-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.patient-table th {
background-color: #f5f5f5;
font-weight: 600;
}
.patient-table tr:hover {
background-color: #fafafa;
}
.patient-table tr.selected {
background-color: #e6f2ff;
}
.select-btn {
padding: 6px 12px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
}
.select-btn:hover:not(:disabled) {
background-color: #40a9ff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
.select-btn:disabled {
background-color: #d9d9d9;
color: #fff;
cursor: not-allowed;
box-shadow: none;
}
/* 搜索提示样式 */
.search-hint {
text-align: center;
color: #999;
font-size: 14px;
padding: 20px;
background-color: #fafafa;
border-radius: 4px;
margin: 10px 0;
}
/* 加载指示器样式 */
.loading-indicator {
text-align: center;
color: #666;
font-size: 14px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-results {
text-align: center;
color: #666;
font-size: 14px;
padding: 20px;
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
margin: 10px 0;
}
/* 右键菜单样式 */
.context-menu {
position: fixed;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1001;
}
.menu-item {
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.context-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.loading-text {
display: flex;
align-items: center;
}
.search-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 分页组件样式 */
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
padding: 20px 0;
background-color: #fafafa;
border-radius: 4px;
}
.pagination-container .el-pagination {
font-size: 14px;
}
.pagination-container .el-pagination .el-pager li {
min-width: 36px;
height: 36px;
line-height: 36px;
border-radius: 4px;
margin: 0 2px;
}
.pagination-container .el-pagination .el-pager li:hover {
background-color: #e6f7ff;
color: #1890ff;
}
.pagination-container .el-pagination .el-pager li.active {
background-color: #1890ff;
color: white;
}
.pagination-container .el-pagination .btn-prev,
.pagination-container .el-pagination .btn-next {
width: 36px;
height: 36px;
border-radius: 4px;
margin: 0 2px;
}
.pagination-container .el-pagination .el-pagination__sizes {
margin-right: 10px;
}
.pagination-container .el-pagination .el-pagination__sizes .el-input {
width: 100px;
}
.pagination-container .el-pagination .el-pagination__jump {
margin-left: 10px;
}
.pagination-container .el-pagination .el-pagination__jump .el-input {
width: 60px;
}
</style>