Files
his/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue
Ranyunqiao c3f1b105e9 301
预约管理-》门诊预约挂号:号源信息的序号未进行取值
316门诊医生站-》医嘱TAB页面:会诊医嘱状态从“已签发”变成“草稿”
317【门诊医生站】已签发会诊医嘱未同步至门诊收费系统生成待收费项目
344
门诊预约挂号:未过滤过期号源,允许预约已过时的时间段
347 医生门诊工作已就诊的病人提示未就诊
2026-04-07 15:36:27 +08:00

2018 lines
55 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" @change="onSearch">
<option value="all">全部</option>
<option value="unbooked">未预约</option>
<option value="booked">已预约</option>
<option value="checked">已取号</option>
<option value="cancelled">已停诊</option>
<option value="returned">已退号</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">{{ item.seqNo != null ? item.seqNo : index + 1 }}</div>
<!-- 1.时间 -->
<div class="ticket-id-time">{{ item.dateTime }}</div>
<!-- 2. 状态标签 -->
<div class="ticket-status" :class="getStatusClass(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-department" :title="item.department || '未知科室'">
科室{{ item.department || '未知科室' }}
</div>
<!-- 5. 挂号费 -->
<div class="ticket-fee">挂号费{{ item.fee }}</div>
<!-- 6. 号源类型 -->
<div class="ticket-type">{{ item.ticketType === 'general' ? '普通' : '专家' }}</div>
<!-- 7. 已预约患者信息 -->
<div v-if="(item.status === '已预约' || item.status === '已取号') && item.patientName" class="ticket-patient">
{{ item.patientName }}({{ item.patientId }},{{ getGenderText(item.gender || item.patientGender) }})
</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>
<div class="pagination-container" v-if="totalTickets > 0">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="totalTickets"
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="[15, 30, 50, 100]"
@current-change="handlePageChange"
@size-change="handlePageSizeChange"
/>
</div>
<div v-if="filteredTickets.length === 0" 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">
<div class="patient-search-toolbar">
<input
id="patient-keyword"
class="search-input patient-keyword-input"
placeholder="姓名/就诊卡号/手机号/证件号回车查询"
v-model.trim="patientKeyword"
@keyup.enter="searchPatients"
>
<button id="patient-search-btn" class="search-btn" @click="searchPatients">查询</button>
<button id="patient-reset-btn" class="reset-btn" @click="resetPatientSearch">重置</button>
</div>
<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._rowKey"
:class="{ selected: selectedPatientId === patient._rowKey }"
@click="selectPatient(patient._rowKey)"
>
<td>{{ index + 1 }}</td>
<td>{{ patient.name }}</td>
<td>{{ patient.identifierNo || patient.medicalCard || patient.id }}</td>
<td>{{ patient.identifierNo }}</td>
<td>{{ getGenderText(patient.genderEnum_enumText || patient.genderEnum || patient.gender || patient.sex) }}</td>
<td>{{ patient.idCard }}</td>
<td>{{ patient.phone }}</td>
<td>
<button class="select-btn" @click.stop="selectPatient(patient._rowKey)" :disabled="!patient._rowKey">选择</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, listDoctorSummary, bookTicket, cancelTicket } from '@/api/appoinmentmanage/ticket';
import { getPatientList } from '@/api/cardRenewal/api';
import { listRegisterOrganizations } from '@/api/appoinmentmanage/dept';
import { ElDatePicker, ElPagination, ElMessageBox, ElMessage } from 'element-plus';
import useUserStore from '@/store/modules/user';
const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已退号': 'status-returned',
'已停诊': 'status-cancelled',
'已取消': 'status-cancelled'
};
export default {
name: 'OutpatientAppointment',
components: {
ElDatePicker,
ElPagination
},
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: 15,
totalTickets: 0,
// 右键菜单相关
contextMenuVisible: false,
contextMenuPosition: { x: 0, y: 0 },
selectedTicketForCancel: null,
departments: [
{ value: 'all', label: '全部科室' }
],
doctors: [],
allTickets: [], // 存储所有原始号源数据
tickets: [], // 从API获取的真实号源数据
patients: [], // 从API获取的真实患者数据
searchQuery: '',
patientKeyword: '',
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)
);
}
// 🎯 实时更新余号数量:统计该医生当前筛选条件下剩余可预约(未预约 + 未过期)号源数量
const availableCountMap = {};
this.filteredAndSortedTickets.forEach(ticket => {
const doctorId = String(ticket.doctorId || ticket.doctor_id);
if (!availableCountMap[doctorId]) {
availableCountMap[doctorId] = 0;
}
// 只有未预约的号源才算作可预约余号
if (ticket.status === '未预约') {
availableCountMap[doctorId]++;
}
});
// 更新每个医生的余号数量
filtered = filtered.map(doctor => {
const actualAvailable = availableCountMap[String(doctor.id)] || 0;
return {
...doctor,
available: actualAvailable
};
});
return filtered;
},
// 过滤并排序后的完整号源列表
filteredAndSortedTickets() {
let filtered = [...this.tickets];
// 🎯 精确过滤:过滤掉早于当前时间的号源
// - 日期早于今天 → 过滤
// - 日期是今天但号源时段已经开始(开始时间早于当前时间) → 过滤
const now = new Date();
filtered = filtered.filter(ticket => {
// dateTime 格式示例:"2024-01-01 08:00-09:00"
const parts = (ticket.dateTime || '').split(' ');
if (parts.length < 2) return true; // 如果格式不正确,保留显示
const dateStr = parts[0];
const timeRangeStr = parts[1];
if (!dateStr || !timeRangeStr) return true;
// 提取开始时间
const startTimeStr = timeRangeStr.split('-')[0]; // "08:00"
if (!startTimeStr) return true;
// 构建号源开始时间的完整 Date 对象
const ticketStartStr = `${dateStr} ${startTimeStr}`;
const ticketStart = new Date(ticketStartStr);
// 只显示开始时间晚于当前时间的号源
return ticketStart > now;
});
// 🎯 按开始时间升序排序 → 较早的号源排在前面
filtered.sort((a, b) => {
const getStartTime = (ticket) => {
const parts = (ticket.dateTime || '').split(' ');
if (parts.length < 2) return new Date(0).getTime();
const dateStr = parts[0];
const timeRangeStr = parts[1];
const startTimeStr = (timeRangeStr || '').split('-')[0];
if (!startTimeStr) return new Date(0).getTime();
const ticketStartStr = `${dateStr} ${startTimeStr}`;
return new Date(ticketStartStr).getTime();
};
const timeA = getStartTime(a);
const timeB = getStartTime(b);
return timeA - timeB;
});
return filtered;
},
// 🎯 分页:按照用户选择的每页条数分页,返回当前页的数据
filteredTickets() {
const filtered = this.filteredAndSortedTickets;
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
return filtered.slice(startIndex, endIndex);
},
// 更新总条数为过滤后的实际条数,分页自动处理
totalTickets() {
return this.filteredAndSortedTickets.length;
},
hasSearchCriteria() {
return !!this.patientKeyword?.trim();
}
},
methods: {
selectDoctor(doctorId) {
this.selectedDoctorId = this.selectedDoctorId === doctorId ? null : doctorId;
this.currentPage = 1;
this.fetchTickets({ refreshDepartments: false, refreshDoctors: false }).catch(() => {});
},
onTypeChange() {
this.onSearch();
},
onDepartmentChange() {
this.onSearch();
},
onDoctorSearch() {
// 使用计算属性实时过滤
},
onDateChange() {
this.onSearch();
},
openPatientSelectModal(ticketId) {
this.currentTicket = this.tickets.find(ticket => ticket.slot_id === ticketId);
this.patientKeyword = '';
this.patients = [];
this.selectedPatientId = null;
this.selectedPatient = null;
this.showPatientModal = true;
this.searchPatients();
},
getPatientUniqueId(patient, index = null) {
return patient.id || patient.patientId || patient.identifierNo || patient.medicalCard || patient.idCard ||
(index !== null ? `temp_${index}` : null);
},
matchPatientKeyword(patient, keyword) {
const normalizedKeyword = String(keyword || '').trim().toLowerCase();
if (!normalizedKeyword) {
return true;
}
const fields = [
patient.name,
patient.id,
patient.patientId,
patient.identifierNo,
patient.medicalCard,
patient.idCard,
patient.phone
];
return fields.some(field => String(field || '').toLowerCase().includes(normalizedKeyword));
},
searchPatients() {
const keyword = this.patientKeyword?.trim() || '';
const requestParams = keyword ? { searchKey: keyword } : {};
this.isLoading = true;
getPatientList(requestParams).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
.filter(patient => this.matchPatientKeyword(patient, keyword))
.map((patient, index) => {
const rowKey = this.getPatientUniqueId(patient, index);
return {
...patient,
// 统一口径:预约场景的就诊卡号使用患者标识表中的 identifierNo
medicalCard: patient.identifierNo || patient.medicalCard || '',
_rowKey: rowKey
};
});
this.patients.forEach((patient, index) => {
if (!patient.idCard) {
patient.idCard = patient._rowKey || `temp_${index}`;
}
});
if (keyword && this.patients.length === 0) {
ElMessage.warning('未找到符合条件的患者');
}
}).catch(error => {
this.patients = [];
ElMessage.error('获取患者列表失败:' + (error.message || '未知错误'));
}).finally(() => {
this.isLoading = false;
});
},
resetPatientSearch() {
this.patientKeyword = '';
this.selectedPatientId = null;
this.selectedPatient = null;
this.searchPatients();
},
// 双击未预约卡片触发患者选择流程
handleDoubleClick(ticket) {
if (ticket.status === '未预约') {
this.currentTicket = ticket;
this.patientKeyword = '';
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].gender = null;
this.tickets[ticketIndex].medicalCard = null;
this.tickets[ticketIndex].phone = null;
}
this.fetchTickets({ refreshDepartments: false, refreshDoctors: true }).catch(() => {});
// 关闭上下文菜单
this.closeContextMenu();
ElMessage.success('预约已取消,号源已释放');
} else {
// 取消失败
const errorMsg = response.msg || response.message || '取消预约失败';
ElMessage.error(`取消预约失败:${errorMsg}`);
this.closeContextMenu();
}
}).catch(error => {
console.error('取消预约失败:', error);
this.closeContextMenu();
});
},
closePatientSelectModal() {
this.showPatientModal = false;
this.selectedPatientId = null;
this.selectedPatient = null;
},
selectPatient(patientId) {
if (this.patients.length === 0) {
ElMessage.error('患者数据未加载,请先搜索患者');
return;
}
this.selectedPatientId = patientId;
this.selectedPatient = this.patients.find(patient => patient._rowKey === patientId);
if (this.selectedPatient) {
ElMessage.success('已选中患者: ' + this.selectedPatient.name);
} else {
ElMessage.error('未找到该患者,请重新选择');
this.selectedPatientId = null;
}
},
confirmPatientSelection() {
if (!this.selectedPatientId || !this.selectedPatient) {
ElMessage.error('请选择患者');
return;
}
if (!this.currentTicket) {
ElMessage.error('预约信息错误,请重新选择号源');
this.closePatientSelectModal();
return;
}
// 再次验证号源是否仍处于可预约状态
if (this.currentTicket.status !== '未预约') {
ElMessage.error('该号源已被预约,请选择其他号源');
this.closePatientSelectModal();
return;
}
try {
const userStore = useUserStore();
const patientPrimaryId = this.selectedPatient.id || this.selectedPatient.patientId;
const medicalCard = this.selectedPatient.identifierNo || this.selectedPatient.medicalCard;
if (!patientPrimaryId) {
ElMessage.error('患者ID缺失无法预约请重新选择患者');
return;
}
if (!medicalCard) {
ElMessage.error('就诊卡号缺失,无法预约,请先维护患者就诊卡号');
return;
}
const appointmentData = {
ticketId: this.currentTicket.slot_id,
slotId: this.currentTicket.slot_id,
patientId: patientPrimaryId,
patientName: this.selectedPatient.name,
medicalCard,
phone: this.selectedPatient.phone,
gender: this.getGenderValueForBackend(this.selectedPatient),
fee: Number(this.currentTicket.fee) || 0,
regType: this.currentTicket.ticketType === 'general' ? '普通' : '专家',
tenant_id: userStore.tenantId || '1'
};
// 验证必填字段
if (!appointmentData.ticketId) {
ElMessage.error('号源ID无效');
return;
}
bookTicket(appointmentData).then(() => {
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 = medicalCard;
this.tickets[ticketIndex].patientGender = this.selectedPatient.genderEnum_enumText || this.selectedPatient.genderEnum || this.selectedPatient.gender || this.selectedPatient.sex;
this.tickets[ticketIndex].gender = this.getGenderText(this.tickets[ticketIndex].patientGender);
}
this.closePatientSelectModal();
// 重新加载号源数据,确保显示最新状态
this.onSearch();
ElMessage.success('预约成功,号源已锁定。患者到院签到时需缴费取号。');
}).catch(error => {
console.error('预约失败:', error);
});
} catch (error) {
console.error('操作异常:', error);
}
},
// 切换侧边栏显示/隐藏
toggleSidebar() {
this.showSidebar = !this.showSidebar;
},
// 获取性别文本
getGenderText(genderValue) {
// 如果值为null或undefined返回"未知"
if (genderValue === null || genderValue === undefined) {
return '未知';
}
// 将值转换为字符串进行比较
const strValue = String(genderValue).toLowerCase();
// 处理男性值
if (strValue === '0' || strValue === '男' || strValue === 'male' || strValue === 'm' ||
strValue === 'malegender' || strValue === 'man' || strValue === 'boy' ||
strValue === '男性' || strValue === '男士') {
return '男';
}
// 处理女性值
if (strValue === '1' || strValue === '女' || strValue === 'female' || strValue === 'f' ||
strValue === 'femalegender' || strValue === 'woman' || strValue === 'girl' ||
strValue === '女性' || strValue === '女士') {
return '女';
}
// 如果都不是,返回"未知"
return '未知';
},
// 获取用于后端的性别值
getGenderValueForBackend(patient) {
// 优先使用数字类型的genderEnum或gender字段
if (patient.genderEnum !== undefined && patient.genderEnum !== null) {
return patient.genderEnum;
}
if (patient.gender !== undefined && patient.gender !== null) {
return patient.gender;
}
// 如果genderEnum_enumText是"男性"或"女性",转换为对应的数字
if (patient.genderEnum_enumText) {
const text = patient.genderEnum_enumText.toLowerCase();
if (text === '男性' || text === '男') {
return 0;
} else if (text === '女性' || text === '女') {
return 1;
}
}
// 默认返回0男性
return 0;
},
// 检测是否为移动设备
checkMobileDevice() {
this.isMobile = window.innerWidth <= 768;
},
getStatusClass(status) {
return STATUS_CLASS_MAP[status] || 'status-unbooked';
},
buildQueryParams(page = this.currentPage) {
const doctorId =
this.selectedDoctorId === null || this.selectedDoctorId === undefined || this.selectedDoctorId === ''
? null
: String(this.selectedDoctorId);
return {
date: this.selectedDate,
status: this.selectedStatus === 'all' ? null : this.selectedStatus,
type: this.selectedType === 'all' ? null : this.selectedType,
department: this.selectedDepartment === 'all' ? null : this.selectedDepartment,
doctorId,
name: this.patientName?.trim() || null,
card: this.patientCard?.trim() || null,
phone: this.patientPhone?.trim() || null,
page,
limit: this.pageSize
};
},
buildDoctorQueryParams() {
return {
date: this.selectedDate,
type: this.selectedType === 'all' ? null : this.selectedType,
department: this.selectedDepartment === 'all' ? null : this.selectedDepartment
};
},
handleTicketResponse(ticketResponse) {
const payload = (() => {
if (!ticketResponse) return null;
if (ticketResponse.list || ticketResponse.records || ticketResponse.total !== undefined) {
return ticketResponse;
}
if (ticketResponse.data?.list || ticketResponse.data?.records || ticketResponse.data?.total !== undefined) {
return ticketResponse.data;
}
if (ticketResponse.data?.data) {
return ticketResponse.data.data;
}
return ticketResponse.data || ticketResponse;
})();
if (!payload) {
this.tickets = [];
this.allTickets = [];
this.totalTickets = 0;
return;
}
const records = payload.list || payload.records || [];
const filteredRecords = this.applyStatusFilter(records);
const total = Number(payload.total);
this.tickets = [...filteredRecords];
this.allTickets = [...filteredRecords];
// 后端已经分页总条数由后端返回始终使用后端返回的total
this.totalTickets = Number.isFinite(total) ? total : this.tickets.length;
},
applyStatusFilter(records = []) {
if (!Array.isArray(records) || records.length === 0) {
return [];
}
if (!this.selectedStatus || this.selectedStatus === 'all') {
return records;
}
const statusMap = {
unbooked: ['未预约'],
booked: ['已预约'],
checked: ['已取号'],
cancelled: ['已停诊', '已取消'],
returned: ['已退号']
};
const matchedStatusList = statusMap[this.selectedStatus] || [];
if (matchedStatusList.length === 0) {
return records;
}
return records.filter(item => matchedStatusList.includes(item?.status));
},
updateDoctorsListFromApi(doctorResponse) {
let doctorList = [];
const data = doctorResponse?.data;
if (Array.isArray(doctorResponse)) {
doctorList = doctorResponse;
} else if (Array.isArray(data)) {
doctorList = data;
} else if (Array.isArray(data?.list)) {
doctorList = data.list;
} else if (Array.isArray(data?.records)) {
doctorList = data.records;
} else if (Array.isArray(data?.data)) {
doctorList = data.data;
}
this.doctors = doctorList
.map((doctor, index) => {
const id = doctor?.id ?? doctor?.doctorId ?? index;
const available = Number(doctor?.available ?? doctor?.availableNum ?? doctor?.available_num ?? 0);
return {
id: String(id),
name: doctor?.name || doctor?.doctorName || '',
available: Number.isFinite(available) ? available : 0,
type: doctor?.type || doctor?.ticketType || 'general'
};
})
.filter(doctor => !!doctor.name);
if (this.selectedDoctorId && !this.doctors.some(d => d.id === this.selectedDoctorId)) {
this.selectedDoctorId = null;
}
},
fetchTickets({ refreshDepartments = false, refreshDoctors = false } = {}) {
this.isLoading = true;
const ticketPromise = listTicket(this.buildQueryParams(this.currentPage));
const deptPromise = refreshDepartments
? listRegisterOrganizations({
pageNum: 1,
pageSize: 200
})
: Promise.resolve(null);
const doctorPromise = refreshDoctors
? listDoctorSummary(this.buildDoctorQueryParams())
: Promise.resolve(null);
return Promise.all([ticketPromise, deptPromise, doctorPromise]).then(([ticketResponse, deptResponse, doctorResponse]) => {
this.handleTicketResponse(ticketResponse);
if (refreshDepartments) {
this.updateDepartmentsListFromApi(deptResponse);
}
if (refreshDoctors) {
this.updateDoctorsListFromApi(doctorResponse);
}
}).catch(error => {
console.error('获取数据失败:', error);
this.tickets = [];
this.allTickets = [];
this.totalTickets = 0;
throw error;
}).finally(() => {
this.isLoading = false;
});
},
onSearch() {
if (this.isLoading) {
return;
}
this.currentPage = 1;
this.fetchTickets({ refreshDepartments: true, refreshDoctors: true }).catch(() => {});
},
handlePageChange(page) {
if (this.isLoading || page === this.currentPage) {
return;
}
this.currentPage = page;
this.fetchTickets({ refreshDepartments: false, refreshDoctors: false }).catch(() => {});
},
handlePageSizeChange(size) {
if (this.isLoading || size === this.pageSize) {
return;
}
this.pageSize = size;
this.currentPage = 1;
this.fetchTickets({ refreshDepartments: false, refreshDoctors: false }).catch(() => {});
},
updateDepartmentsListFromApi(deptResponse) {
let deptList = [];
const data = deptResponse?.data;
if (Array.isArray(data)) {
deptList = data;
} else if (Array.isArray(data?.records)) {
deptList = data.records;
} else if (Array.isArray(data?.list)) {
deptList = data.list;
} else if (Array.isArray(data?.content)) {
deptList = data.content;
} else if (Array.isArray(data?.data)) {
deptList = data.data;
}
if (deptList.length > 0) {
const departmentArray = deptList
.map(item => {
const deptName = item?.deptName || item?.name;
return deptName ? { value: deptName, label: deptName } : null;
})
.filter(Boolean)
.filter((dept, index, self) => index === self.findIndex(d => d.value === dept.value));
this.departments = [{ value: 'all', label: '全部科室' }, ...departmentArray];
} else {
this.departments = [{ value: 'all', label: '全部科室' }];
}
}
},
mounted() {
document.addEventListener('click', this.closeContextMenu);
// 初始化数据
this.selectedDate = new Date().toISOString().split('T')[0];
// 调用onSearch获取初始数据
this.onSearch();
// 检测是否为移动设备
this.checkMobileDevice();
// 监听窗口大小变化
window.addEventListener('resize', this.checkMobileDevice);
},
beforeUnmount() {
document.removeEventListener('click', this.closeContextMenu);
// 移除窗口大小变化监听
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;
display: flex;
flex-direction: column;
min-height: 0;
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: visible;
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;
flex: 1;
min-height: 0;
overflow-y: auto;
align-content: start;
}
/* 加载更多样式 */
.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: stretch;
position: relative;
min-height: 210px;
height: auto;
}
.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-returned {
background-color: #fff7e6;
color: #d46b08;
}
.status-cancelled {
background-color: #fff1f0;
color: #ff4d4f;
}
.ticket-doctor {
font-size: 14px;
color: #333;
margin-bottom: 8px;
text-align: center;
width: 100%;
line-height: 1.35;
white-space: normal;
word-break: break-word;
}
.ticket-department {
font-size: 12px;
color: #666;
margin-bottom: 8px;
text-align: center;
width: 100%;
line-height: 1.35;
white-space: normal;
word-break: break-word;
}
.ticket-id-time {
font-size: 14px;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.ticket-fee {
font-size: 14px;
color: #333;
margin-bottom: 8px;
width: 100%;
text-align: center;
line-height: 1.35;
white-space: normal;
word-break: break-word;
}
.ticket-type {
font-size: 12px;
color: #999;
margin-bottom: 12px;
width: 100%;
text-align: center;
}
.ticket-actions {
margin-top: auto;
padding-top: 8px;
width: 100%;
display: flex;
justify-content: center;
}
.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;
width: 100%;
line-height: 1.35;
white-space: normal;
word-break: break-word;
}
/* 患者电话号码样式 */
.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: 190px;
}
.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-search-toolbar {
display: flex;
align-items: center;
gap: 12px;
}
.patient-keyword-input {
flex: 1;
}
.reset-btn {
padding: 0 16px;
background-color: #f5f5f5;
color: #333;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
height: 32px;
box-sizing: border-box;
transition: all 0.3s;
}
.reset-btn:hover {
border-color: #40a9ff;
color: #1890ff;
background-color: #f0f7ff;
}
.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: #d6e8ff;
}
.patient-table tr.selected td {
background-color: #d6e8ff;
color: #0f3f8f;
font-weight: 600;
}
.patient-table tr.selected td:first-child {
box-shadow: inset 4px 0 0 #1677ff;
}
.patient-table tr.selected:hover td {
background-color: #c9e1ff;
}
.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: 12px;
padding: 20px 0;
background-color: #fafafa;
border-radius: 4px;
flex-shrink: 0;
}
.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>