1927 lines
53 KiB
Vue
1927 lines
53 KiB
Vue
<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">×</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> |