Files
his/openhis-ui-vue3/src/views/appoinmentmanage/index.vue
2026-01-09 11:33:03 +08:00

1681 lines
46 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="locked">锁定</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-header">
<div class="ticket-number">{{ index + 1 }}</div>
<div class="ticket-type-badge" :class="item.ticketType === 'general' ? 'type-general' : 'type-expert'">
{{ item.ticketType === 'general' ? '普通' : '专家' }}
</div>
</div>
<div class="ticket-time">{{ item.dateTime }}</div>
<div class="ticket-doctor">{{ item.doctor }}</div>
<div class="ticket-status" :class="`status-${item.status}`">
<span class="status-dot"></span>
{{ item.status }}
</div>
<div class="ticket-fee">挂号费: {{ item.fee }}</div>
<!-- 显示患者信息如果已预约或已取号 -->
<div v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName" class="ticket-patient">
<div class="patient-name">{{ item.patientName }}</div>
<div class="patient-card">{{ item.patientId }}</div>
</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>
<button class="action-button cancel-button" @click="confirmCancelConsultation(item)" :disabled="item.status === '已取消'" :class="{ 'disabled': item.status === '已取消' }">
<i class="el-icon-close"></i>
停诊
</button>
<button class="action-button check-in-button" @click="confirmCheckIn(item)" :disabled="item.status !== '已预约'" :class="{ 'disabled': item.status !== '已预约' }">
<i class="el-icon-circle-check"></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">
<thead>
<tr>
<th>序号</th>
<th>患者姓名</th>
<th>就诊卡号</th>
<th>身份证号</th>
<th>手机号</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(patient, index) in patients" :key="patient.id" :class="{ selected: selectedPatientId === patient.id }" @click="selectPatient(patient.id)">
<td>{{ index + 1 }}</td>
<td>{{ patient.name }}</td>
<td>{{ patient.medicalCard }}</td>
<td>{{ patient.idCard }}</td>
<td>{{ patient.phone }}</td>
<td>
<button class="select-btn" @click.stop="selectPatient(patient.id)">选择</button>
</td>
</tr>
</tbody>
</table>
</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 === 'booked' || selectedTicketForCancel.status === 'locked' || selectedTicketForCancel.status === 'checked')" 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 } from 'element-plus'
export default {
name: 'AppoinmentManageIndex',
components: {
ElDatePicker
},
// 添加全局点击事件监听器,用于关闭右键菜单
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': '未预约',
// 'locked': '已锁定',
// '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);
// }
// }
// 按患者信息筛选(姓名、就诊卡号、手机号) - 暂时注释掉以排查问题
// if (this.patientName || this.patientCard || this.patientPhone) {
// filtered = filtered.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;
// });
// }
return filtered;
},
},
methods: {
selectDoctor(doctorId) {
this.selectedDoctorId = doctorId
console.log('Selected doctor:', doctorId)
// 医生选择后,筛选号源
this.onSearch();
},
onTypeChange() {
console.log('Type changed:', this.selectedType)
this.onSearch()
},
onDepartmentChange() {
console.log('Department changed:', this.selectedDepartment)
this.onSearch()
},
onDoctorSearch() {
console.log('Searching doctor:', this.searchQuery)
this.onSearch()
},
openPatientSelectModal(ticketId) {
this.currentTicket = this.tickets.find(ticket => ticket.slot_id === ticketId);
// 打开患者选择弹窗时,查询患者列表
this.searchPatients();
this.showPatientModal = true
},
// 患者搜索
searchPatients() {
// 调用真实API获取患者列表
getPatientList(this.patientSearchParams).then(response => {
if (response.records) {
this.patients = response.records;
} else {
this.patients = [];
}
}).catch(error => {
console.error('获取患者列表失败:', error);
this.patients = [];
});
},
// 监听患者搜索输入变化
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.showPatientModal = true;
}
},
// 右键已预约卡片显示取消预约菜单
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) {
// 二次确认
if (confirm(`确认取消患者的预约?`)) {
// 调用API取消预约
this.cancelAppointment(this.selectedTicketForCancel);
}
this.closeContextMenu();
}
},
// 确认停诊
confirmCancelConsultation(ticket) {
// 二次确认
if (confirm(`确认取消${ticket.doctor}${ticket.dateTime}的看诊吗?`)) {
// 调用API处理停诊
this.cancelConsultation(ticket);
}
},
// 处理停诊
cancelConsultation(ticket) {
// 使用真实API调用处理停诊
cancelConsultation(ticket.slot_id).then(response => {
// API调用成功后重新获取最新的号源数据
this.onSearch();
alert('停诊已处理,该号源已被取消');
}).catch(error => {
console.error('停诊处理失败:', error);
alert('停诊处理失败,请稍后重试。');
});
},
// 确认取号
confirmCheckIn(ticket) {
// 二次确认
if (confirm(`确认${ticket.patientName || ''}取号吗?`)) {
// 调用API处理取号
this.checkIn(ticket);
}
},
// 处理取号
checkIn(ticket) {
// 使用真实API调用处理取号
checkInTicket(ticket.slot_id).then(response => {
// API调用成功后重新获取最新的号源数据
this.onSearch();
alert('取号成功!');
}).catch(error => {
console.error('取号失败:', error);
alert('取号失败,请稍后重试。');
});
},
// 取消预约API调用
cancelAppointment(ticket) {
// 使用真实API调用取消预约
cancelTicket(ticket.slot_id).then(response => {
// API调用成功后重新获取最新的号源数据
this.onSearch();
// 关闭上下文菜单
this.closeContextMenu();
alert('预约已取消,号源已释放');
}).catch(error => {
console.error('取消预约失败:', error);
alert('取消预约失败,请稍后重试。');
});
},
closePatientSelectModal() {
this.showPatientModal = false
this.selectedPatientId = null
},
selectPatient(patientId) {
this.selectedPatientId = patientId;
// 保存选择的患者对象
this.selectedPatient = this.patients.find(patient => patient.id === patientId);
},
confirmPatientSelection() {
if (this.selectedPatientId && this.selectedPatient) {
// 这里可以添加确认患者选择后的逻辑
console.log('Selected patient:', this.selectedPatient, 'for ticket:', this.currentTicket.slot_id)
// 构建预约数据
const appointmentData = {
ticketId: Number(this.currentTicket.slot_id),
patientId: this.selectedPatientId ? Number(this.selectedPatientId) : null,
patientName: this.selectedPatient.name,
medicalCard: this.selectedPatient.medicalCard,
phone: this.selectedPatient.phone
};
// 使用真实API调用进行预约
bookTicket(appointmentData).then(response => {
// API调用成功后重新获取最新的号源数据
this.onSearch();
this.closePatientSelectModal();
alert('预约成功,号源已锁定。患者到院签到时需缴费取号。');
}).catch(error => {
console.error('预约失败:', error);
alert('预约失败,请稍后重试。');
});
} else {
alert('请选择患者')
}
},
onDateChange() {
// 日期变化时刷新号源列表
console.log('Date changed:', this.selectedDate)
// 重置页码并重新加载数据
this.currentPage = 1;
this.onSearch()
},
// 切换侧边栏显示/隐藏
toggleSidebar() {
this.showSidebar = !this.showSidebar;
},
// 检测是否为移动设备
checkMobileDevice() {
this.isMobile = window.innerWidth <= 768;
},
onSearch() {
this.isLoading = true
console.log('Searching with:', {
date: this.selectedDate,
status: this.selectedStatus,
type: this.selectedType,
name: this.patientName,
card: this.patientCard,
phone: this.patientPhone
});
// 调用真实API获取号源数据
const queryParams = {
date: this.selectedDate,
status: this.selectedStatus,
type: this.selectedType,
name: this.patientName,
card: this.patientCard,
phone: this.patientPhone,
page: 1,
limit: this.pageSize
};
// 并行调用号源和科室API - 使用测试接口listAllTickets
Promise.all([
listAllTickets(), // 使用测试接口获取固定数据
listDept()
]).then(([ticketResponse, deptResponse]) => {
// 打印完整的响应数据以便调试
console.log('完整的号源响应数据:', JSON.stringify(ticketResponse, null, 2));
console.log('号源响应data字段类型:', typeof ticketResponse.data);
console.log('号源响应data字段:', JSON.stringify(ticketResponse.data, null, 2));
// 处理号源数据
if (ticketResponse) {
console.log('号源响应code:', ticketResponse.code);
if (ticketResponse.code === 200) {
const responseData = ticketResponse.data;
// 检查响应数据结构
console.log('responseData是否存在:', responseData !== undefined && responseData !== null);
if (responseData) {
console.log('responseData.records:', responseData.records);
console.log('responseData.list:', responseData.list);
console.log('responseData.total:', responseData.total);
// 检查是否有records字段
if (responseData.records) {
console.log('使用records字段作为tickets数据源');
this.tickets = responseData.records;
this.totalTickets = responseData.total;
} else if (responseData.list) {
// 兼容使用list字段的情况
console.log('使用list字段作为tickets数据源');
this.tickets = responseData.list;
this.totalTickets = responseData.total;
} else if (Array.isArray(responseData)) {
// 如果直接是数组
console.log('responseData是数组直接作为tickets数据源');
this.tickets = responseData;
this.totalTickets = responseData.length;
} else {
// 如果没有预期的字段,可能数据直接在对象中
console.log('responseData是对象包装为数组作为tickets数据源');
this.tickets = [responseData];
this.totalTickets = 1;
}
// 保存所有原始数据
this.allTickets = [...this.tickets];
console.log('tickets数据:', JSON.stringify(this.tickets, null, 2));
console.log('tickets长度:', this.tickets.length);
// 更新医生列表
this.updateDoctorsList();
this.hasMore = this.tickets.length < this.totalTickets;
} else {
console.error('responseData为空');
this.tickets = [];
this.allTickets = [];
this.totalTickets = 0;
this.hasMore = false;
}
} else {
console.error('API返回错误code不为200:', ticketResponse);
this.tickets = [];
this.allTickets = [];
this.totalTickets = 0;
this.hasMore = false;
}
} else {
console.error('API返回空响应');
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 (Array.isArray(deptResponse)) {
deptList = deptResponse;
} else if (Array.isArray(deptResponse.data)) {
deptList = deptResponse.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();
}
},
// 获取号源状态文本
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());
// 打印医生列表以便调试
console.log('生成的医生列表:', this.doctors);
},
// 加载更多数据
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 => {
// 打印完整的响应数据以便调试
console.log('加载更多的号源响应数据:', response);
// 处理号源数据
if (response && response.data) {
let newRecords = [];
// 检查是否有records字段
if (response.data.records) {
newRecords = response.data.records;
this.totalTickets = response.data.total;
} else {
// 如果没有records字段可能数据直接在对象中
newRecords = [response.data];
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-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-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;
}
.cancel-button {
background-color: #F56C6C;
color: white;
border: none;
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
margin-top: 8px;
align-self: center;
}
.cancel-button:hover {
background-color: #F78989;
}
.cancel-button:disabled {
background-color: #C0C4CC;
cursor: not-allowed;
}
.check-in-button {
background-color: #67C23A;
color: white;
border: none;
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
margin-top: 8px;
align-self: center;
}
.check-in-button:hover {
background-color: #85ce61;
}
.check-in-button:disabled {
background-color: #C0C4CC;
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;
}
/* 响应式设计 */
@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;
}
}
/* 弹窗样式 */
.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;
max-height: 80vh;
overflow-y: auto;
}
.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: 4px 12px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 右键菜单样式 */
.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;
}
.confirm-btn {
padding: 8px 20px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.cancel-btn {
padding: 8px 20px;
background-color: #f5f5f5;
color: #666;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
}
.loading-text {
display: flex;
align-items: center;
}
.search-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>