88 分诊排队管理-》科室叫号显示屏 表triage_queue_item中添加了联合索引queue_date,organization_id,tenant_id,queue_order。添加了room_no,practitioner_id字段。
This commit is contained in:
@@ -21,7 +21,7 @@ public final class ServiceException extends RuntimeException {
|
|||||||
/**
|
/**
|
||||||
* 错误明细,内部调试错误
|
* 错误明细,内部调试错误
|
||||||
*
|
*
|
||||||
* 和 {@link CommonResult#getDetailMessage()} 一致的设计
|
* 和
|
||||||
*/
|
*/
|
||||||
private String detailMessage;
|
private String detailMessage;
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ public class SecurityConfig {
|
|||||||
// 静态资源,可匿名访问
|
// 静态资源,可匿名访问
|
||||||
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**")
|
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
// WebSocket 握手请求允许匿名访问
|
||||||
|
.antMatchers("/ws/**", "/test-ws")
|
||||||
|
.permitAll()
|
||||||
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**")
|
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.antMatchers("/patientmanage/information/**")
|
.antMatchers("/patientmanage/information/**")
|
||||||
|
|||||||
@@ -69,6 +69,13 @@
|
|||||||
<groupId>org.apache.velocity</groupId>
|
<groupId>org.apache.velocity</groupId>
|
||||||
<artifactId>velocity-engine-core</artifactId>
|
<artifactId>velocity-engine-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- WebSocket 支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- rabbitMQ -->
|
<!-- rabbitMQ -->
|
||||||
<!-- <dependency>
|
<!-- <dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openhis.web.triageandqueuemanage.appservice;
|
package com.openhis.web.triageandqueuemanage.appservice;
|
||||||
|
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||||
@@ -22,6 +23,9 @@ public interface TriageQueueAppService {
|
|||||||
R<?> skip(TriageQueueActionReq req);
|
R<?> skip(TriageQueueActionReq req);
|
||||||
/** 下一患者:当前叫号中 -> 完成,下一位等待 -> 叫号中 */
|
/** 下一患者:当前叫号中 -> 完成,下一位等待 -> 叫号中 */
|
||||||
R<?> next(TriageQueueActionReq req);
|
R<?> next(TriageQueueActionReq req);
|
||||||
|
|
||||||
|
/** 叫号显示屏:获取当前叫号和等候队列信息 */
|
||||||
|
CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,19 +10,20 @@ import com.openhis.triageandqueuemanage.domain.TriageCandidateExclusion;
|
|||||||
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
import com.openhis.triageandqueuemanage.service.TriageQueueItemService;
|
||||||
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
import com.openhis.triageandqueuemanage.service.TriageCandidateExclusionService;
|
||||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||||
|
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueEncounterItem;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueEncounterItem;
|
||||||
|
import com.openhis.web.triageandqueuemanage.websocket.CallNumberWebSocket;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
||||||
@@ -121,6 +122,8 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
.setPatientName(it.getPatientName())
|
.setPatientName(it.getPatientName())
|
||||||
.setHealthcareName(it.getHealthcareName())
|
.setHealthcareName(it.getHealthcareName())
|
||||||
.setPractitionerName(it.getPractitionerName())
|
.setPractitionerName(it.getPractitionerName())
|
||||||
|
.setPractitionerId(it.getPractitionerId()) // ✅ 新增字段(可选)
|
||||||
|
.setRoomNo(it.getRoomNo()) // ✅ 新增字段(可选)
|
||||||
.setStatus(STATUS_WAITING)
|
.setStatus(STATUS_WAITING)
|
||||||
.setQueueOrder(++maxOrder)
|
.setQueueOrder(++maxOrder)
|
||||||
.setDeleteFlag("0")
|
.setDeleteFlag("0")
|
||||||
@@ -238,6 +241,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
if (STATUS_WAITING.equals(selected.getStatus())) {
|
if (STATUS_WAITING.equals(selected.getStatus())) {
|
||||||
selected.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
|
selected.setStatus(STATUS_CALLING).setUpdateTime(LocalDateTime.now());
|
||||||
triageQueueItemService.updateById(selected);
|
triageQueueItemService.updateById(selected);
|
||||||
|
|
||||||
|
// ✅ 叫号后推送 WebSocket 消息
|
||||||
|
pushDisplayUpdate(selected.getOrganizationId(), selected.getQueueDate(), selected.getTenantId());
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
} else if (STATUS_CALLING.equals(selected.getStatus())) {
|
} else if (STATUS_CALLING.equals(selected.getStatus())) {
|
||||||
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
// 如果已经是"叫号中"状态,直接返回成功(不做任何操作)
|
||||||
@@ -321,6 +328,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recalcOrders(actualOrgId, null);
|
recalcOrders(actualOrgId, null);
|
||||||
|
|
||||||
|
// ✅ 完成后推送 WebSocket 消息
|
||||||
|
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +424,10 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
triageQueueItemService.updateById(next);
|
triageQueueItemService.updateById(next);
|
||||||
|
|
||||||
recalcOrders(actualOrgId, null);
|
recalcOrders(actualOrgId, null);
|
||||||
|
|
||||||
|
// ✅ 过号重排后推送 WebSocket 消息
|
||||||
|
pushDisplayUpdate(actualOrgId, calling.getQueueDate(), tenantId);
|
||||||
|
|
||||||
return R.ok(true);
|
return R.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +566,179 @@ public class TriageQueueAppServiceImpl implements TriageQueueAppService {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取叫号显示屏数据
|
||||||
|
* @param organizationId 科室ID
|
||||||
|
* @param date 日期
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return 显示屏数据
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CallNumberDisplayResp getDisplayData(Long organizationId, LocalDate date, Integer tenantId) {
|
||||||
|
// 如果没有传入租户ID,尝试从登录用户获取,否则默认为1
|
||||||
|
if (tenantId == null) {
|
||||||
|
try {
|
||||||
|
tenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
tenantId = 1; // 默认租户ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LocalDate qd = date != null ? date : LocalDate.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有队列项(WAITING 和 CALLING 状态)某天的某个科室的某个状态
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
List<TriageQueueItem> allItems = triageQueueItemService.list(
|
||||||
|
new LambdaQueryWrapper<TriageQueueItem>()
|
||||||
|
.eq(TriageQueueItem::getQueueDate, qd)
|
||||||
|
.eq(TriageQueueItem::getOrganizationId, organizationId)
|
||||||
|
.eq(TriageQueueItem::getTenantId, tenantId)
|
||||||
|
.in(TriageQueueItem::getStatus, STATUS_WAITING, STATUS_CALLING)
|
||||||
|
.eq(TriageQueueItem::getDeleteFlag, "0")
|
||||||
|
.orderByAsc(TriageQueueItem::getQueueOrder)
|
||||||
|
);
|
||||||
|
|
||||||
|
CallNumberDisplayResp resp = new CallNumberDisplayResp();
|
||||||
|
|
||||||
|
// 1. 获取科室名称(从第一条数据中取)
|
||||||
|
if (!allItems.isEmpty()) {
|
||||||
|
resp.setDepartmentName(allItems.get(0).getOrganizationName() + " 叫号显示屏");
|
||||||
|
} else {
|
||||||
|
resp.setDepartmentName("叫号显示屏");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找当前叫号中的患者(CALLING 状态)
|
||||||
|
TriageQueueItem callingItem = allItems.stream()
|
||||||
|
.filter(item -> STATUS_CALLING.equals(item.getStatus()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (callingItem != null) {
|
||||||
|
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||||
|
currentCall.setNumber(callingItem.getQueueOrder());
|
||||||
|
currentCall.setName(maskPatientName(callingItem.getPatientName()));
|
||||||
|
currentCall.setRoom(callingItem.getRoomNo() != null ? callingItem.getRoomNo() : "1号");
|
||||||
|
currentCall.setDoctor(callingItem.getPractitionerName());
|
||||||
|
resp.setCurrentCall(currentCall);
|
||||||
|
} else {
|
||||||
|
// 没有叫号中的患者,返回默认值
|
||||||
|
CallNumberDisplayResp.CurrentCallInfo currentCall = new CallNumberDisplayResp.CurrentCallInfo();
|
||||||
|
currentCall.setNumber(null);
|
||||||
|
currentCall.setName("-");
|
||||||
|
currentCall.setRoom("-");
|
||||||
|
currentCall.setDoctor("-");
|
||||||
|
resp.setCurrentCall(currentCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 按医生分组(包括 CALLING 和 WAITING 状态)
|
||||||
|
Map<Long, List<TriageQueueItem>> groupedByDoctor = allItems.stream()
|
||||||
|
// 严格按医生分组:仅保留有 practitionerId 的记录
|
||||||
|
.filter(item -> item.getPractitionerId() != null)
|
||||||
|
.collect(Collectors.groupingBy(TriageQueueItem::getPractitionerId));
|
||||||
|
|
||||||
|
// 每个医生的等待队列
|
||||||
|
List<CallNumberDisplayResp.DoctorGroup> waitingList = new ArrayList<>();
|
||||||
|
int totalWaiting = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<Long, List<TriageQueueItem>> entry : groupedByDoctor.entrySet()) {
|
||||||
|
|
||||||
|
List<TriageQueueItem> doctorItems = entry.getValue();
|
||||||
|
String doctorName = doctorItems.get(0).getPractitionerName();
|
||||||
|
if (doctorName == null || doctorName.isEmpty()) {
|
||||||
|
doctorName = "未分配";
|
||||||
|
}
|
||||||
|
// 按排队顺序排序
|
||||||
|
doctorItems.sort(Comparator.comparing(TriageQueueItem::getQueueOrder));
|
||||||
|
|
||||||
|
// 该医生 下边的患者列表 和 诊室号
|
||||||
|
CallNumberDisplayResp.DoctorGroup doctorGroup = new CallNumberDisplayResp.DoctorGroup();
|
||||||
|
doctorGroup.setDoctorName(doctorName);
|
||||||
|
|
||||||
|
// 获取诊室号(从该医生的任一患者中取)
|
||||||
|
String roomNo = doctorItems.stream()
|
||||||
|
.map(TriageQueueItem::getRoomNo)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("1号");
|
||||||
|
doctorGroup.setRoomNo(roomNo);
|
||||||
|
|
||||||
|
// 转换患者列表
|
||||||
|
List<CallNumberDisplayResp.PatientInfo> patients = new ArrayList<>();
|
||||||
|
for (TriageQueueItem item : doctorItems) {
|
||||||
|
CallNumberDisplayResp.PatientInfo patient = new CallNumberDisplayResp.PatientInfo();
|
||||||
|
patient.setId(item.getId());
|
||||||
|
patient.setName(maskPatientName(item.getPatientName()));
|
||||||
|
patient.setStatus(item.getStatus());
|
||||||
|
patient.setQueueOrder(item.getQueueOrder());
|
||||||
|
patients.add(patient);
|
||||||
|
|
||||||
|
// 统计等待人数(不包括 CALLING 状态)
|
||||||
|
if (STATUS_WAITING.equals(item.getStatus())) {
|
||||||
|
totalWaiting++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doctorGroup.setPatients(patients);
|
||||||
|
waitingList.add(doctorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 按医生名称排序
|
||||||
|
waitingList.sort(Comparator.comparing(CallNumberDisplayResp.DoctorGroup::getDoctorName));
|
||||||
|
|
||||||
|
resp.setWaitingList(waitingList);
|
||||||
|
resp.setWaitingCount(totalWaiting);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 患者姓名脱敏处理
|
||||||
|
* @param name 原始姓名
|
||||||
|
* @return 脱敏后的姓名(如:张*三)
|
||||||
|
*/
|
||||||
|
private String maskPatientName(String name) {
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
if (name.length() == 1) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (name.length() == 2) {
|
||||||
|
return name.charAt(0) + "*";
|
||||||
|
}
|
||||||
|
// 3个字及以上:保留首尾,中间用*代替
|
||||||
|
return name.charAt(0) + "*" + name.charAt(name.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送显示屏更新消息到 WebSocket
|
||||||
|
* @param organizationId 科室ID
|
||||||
|
* @param queueDate 队列日期
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
*/
|
||||||
|
private void pushDisplayUpdate(Long organizationId, LocalDate queueDate, Integer tenantId) {
|
||||||
|
try {
|
||||||
|
// 获取最新的显示屏数据
|
||||||
|
CallNumberDisplayResp displayData = getDisplayData(organizationId, queueDate, tenantId);
|
||||||
|
|
||||||
|
// 构造推送消息
|
||||||
|
Map<String, Object> message = new HashMap<>();
|
||||||
|
message.put("type", "update");
|
||||||
|
message.put("action", "queue_changed");
|
||||||
|
message.put("data", displayData);
|
||||||
|
message.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 推送到该科室的所有 WebSocket 连接
|
||||||
|
CallNumberWebSocket.pushToOrganization(organizationId, message);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// WebSocket 推送失败不应该影响业务逻辑
|
||||||
|
System.err.println("推送显示屏更新失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.openhis.web.triageandqueuemanage.controller;
|
package com.openhis.web.triageandqueuemanage.controller;
|
||||||
|
|
||||||
|
import com.core.common.annotation.Anonymous;
|
||||||
import com.core.common.core.domain.R;
|
import com.core.common.core.domain.R;
|
||||||
|
import com.core.common.utils.SecurityUtils;
|
||||||
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
import com.openhis.web.triageandqueuemanage.appservice.TriageQueueAppService;
|
||||||
|
import com.openhis.web.triageandqueuemanage.dto.CallNumberDisplayResp;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueActionReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAddReq;
|
||||||
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
import com.openhis.web.triageandqueuemanage.dto.TriageQueueAdjustReq;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -65,6 +69,62 @@ public class TriageQueueController {
|
|||||||
public R<?> next(@RequestBody(required = false) TriageQueueActionReq req) {
|
public R<?> next(@RequestBody(required = false) TriageQueueActionReq req) {
|
||||||
return triageQueueAppService.next(req);
|
return triageQueueAppService.next(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 叫号显示屏:获取当前叫号和等候队列信息
|
||||||
|
* @param organizationId 科室ID
|
||||||
|
* @param date 日期(可选,默认今天)
|
||||||
|
* @param tenantId 租户ID(可选,默认1)
|
||||||
|
* @return 显示屏数据
|
||||||
|
*/
|
||||||
|
@Anonymous // 显示屏不需要登录
|
||||||
|
@GetMapping("/display")
|
||||||
|
public R<CallNumberDisplayResp> getDisplayData(
|
||||||
|
@RequestParam(required = false) String organizationId,
|
||||||
|
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
|
||||||
|
@RequestParam(required = false) Integer tenantId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Long orgId = resolveOrganizationId(organizationId);
|
||||||
|
if (orgId == null) {
|
||||||
|
return R.fail("organizationId参数不合法或未获取到登录用户科室");
|
||||||
|
}
|
||||||
|
Integer actualTenantId = resolveTenantId(tenantId);
|
||||||
|
CallNumberDisplayResp data = triageQueueAppService.getDisplayData(orgId, date, actualTenantId);
|
||||||
|
return R.ok(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取显示屏数据失败", e);
|
||||||
|
return R.fail("获取显示屏数据失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long resolveOrganizationId(String organizationId) {
|
||||||
|
if (!StringUtils.hasText(organizationId)) {
|
||||||
|
try {
|
||||||
|
return SecurityUtils.getLoginUser().getOrgId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(organizationId.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("非法organizationId: {}", organizationId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer resolveTenantId(Integer tenantId) {
|
||||||
|
if (tenantId != null) {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Integer loginTenantId = SecurityUtils.getLoginUser().getTenantId();
|
||||||
|
return loginTenantId != null ? loginTenantId : 1;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ public class TriageQueueEncounterItem {
|
|||||||
private String patientName;
|
private String patientName;
|
||||||
private String healthcareName;
|
private String healthcareName;
|
||||||
private String practitionerName;
|
private String practitionerName;
|
||||||
|
|
||||||
|
// ========== 新增字段(可选,用于叫号显示屏)==========
|
||||||
|
/** 医生ID(可选) */
|
||||||
|
private Long practitionerId;
|
||||||
|
/** 诊室号(可选) */
|
||||||
|
private String roomNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,21 +18,23 @@ public class TriageQueueItem {
|
|||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private Integer tenantId;
|
private Integer tenantId; // 租户ID
|
||||||
private LocalDate queueDate;
|
private LocalDate queueDate; // 队列日期
|
||||||
private Long organizationId;
|
private Long organizationId; // 科室ID
|
||||||
private String organizationName;
|
private String organizationName; // 科室名称
|
||||||
|
|
||||||
private Long encounterId;
|
private Long encounterId;
|
||||||
private Long patientId;
|
private Long patientId; // 患者ID
|
||||||
private String patientName;
|
private String patientName; // 患者姓名(脱敏)
|
||||||
|
|
||||||
private String healthcareName;
|
private String healthcareName; // 挂号类型(普通/专家)
|
||||||
private String practitionerName;
|
private String practitionerName; // 医生姓名
|
||||||
|
private Long practitionerId; // 医生ID(新增字段)
|
||||||
|
private String roomNo; // 诊室号(新增字段)
|
||||||
|
|
||||||
/** WAITING / CALLING / SKIPPED / COMPLETED */
|
/** WAITING / CALLING / SKIPPED / COMPLETED */
|
||||||
private String status;
|
private String status;
|
||||||
private Integer queueOrder;
|
private Integer queueOrder; //“排队序号”,也就是患者在当前科室、当天队列里的 顺序号(从 1 开始递增)。
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="call-number-display">
|
<div class="call-number-display" ref="screenContainer">
|
||||||
<!-- 头部区域 -->
|
<!-- 头部区域 -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>{{ departmentName }}</h1>
|
<h1>{{ departmentName }}</h1>
|
||||||
<div class="time">{{ currentTime }}</div>
|
<div class="header-right">
|
||||||
|
<button class="fullscreen-btn" @click="toggleFullscreen">
|
||||||
|
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||||
|
</button>
|
||||||
|
<div class="time">{{ currentTime }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 当前呼叫区 -->
|
<!-- 当前呼叫区 -->
|
||||||
@@ -31,19 +36,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template v-for="doctorName in paginatedDoctors" :key="doctorName">
|
<template v-for="doctorName in paginatedDoctors">
|
||||||
<template v-if="groupedPatients[doctorName]">
|
<template v-if="groupedPatients[doctorName]">
|
||||||
<!-- 医生分组标题 -->
|
<!-- 医生分组标题 -->
|
||||||
<tr class="doctor-header">
|
<tr class="doctor-header" :key="`doctor-${doctorName}`">
|
||||||
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
|
<td colspan="4">{{ doctorName }} 医生 (诊室: {{ getDoctorRoom(doctorName) }})</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- 患者列表 -->
|
<!-- 患者列表 -->
|
||||||
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="patient.id">
|
<tr v-for="(patient, index) in groupedPatients[doctorName]" :key="`${doctorName}-${patient.id}`">
|
||||||
<td>{{ index + 1 }}</td>
|
<td>{{ index + 1 }}</td>
|
||||||
<td>{{ formatPatientName(patient.name) }}</td>
|
<td>{{ patient.name }}</td>
|
||||||
<td>{{ getDoctorRoom(doctorName) }}</td>
|
<td>{{ getDoctorRoom(doctorName) }}</td>
|
||||||
<td :style="{ color: index === 0 ? '#e74c3c' : '#27ae60' }">
|
<td :style="{ color: patient.status === 'CALLING' ? '#e74c3c' : '#27ae60' }">
|
||||||
{{ index === 0 ? '就诊中' : '等待' }}
|
{{ patient.status === 'CALLING' ? '就诊中' : '等待' }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,49 +93,64 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watchEffect } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import useUserStore from '@/store/modules/user'
|
||||||
|
|
||||||
|
// ========== 配置参数 ==========
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { orgId: userOrgId, tenantId: userTenantId } = storeToRefs(userStore)
|
||||||
|
|
||||||
|
// 从登录用户获取科室ID(避免硬编码;后端已确保 orgId 以字符串返回)
|
||||||
|
const ORGANIZATION_ID = computed(() => (userOrgId.value ? String(userOrgId.value) : ''))
|
||||||
|
const TENANT_ID = computed(() => (userTenantId.value ? Number(userTenantId.value) : 1))
|
||||||
|
const API_BASE_URL = '/triage/queue'
|
||||||
|
// WebSocket 地址(通过 Nginx 代理,路径需要加 /openhis 前缀)
|
||||||
|
const WS_URL = computed(
|
||||||
|
() => `ws://${window.location.hostname}:18080/openhis/ws/call-number-display/${ORGANIZATION_ID.value}`
|
||||||
|
)
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
const currentCall = ref({
|
const currentCall = ref(null)
|
||||||
number: '1',
|
|
||||||
name: '李*四',
|
|
||||||
room: '3号'
|
|
||||||
})
|
|
||||||
const patients = ref([])
|
const patients = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const patientsPerPage = 5
|
const patientsPerPage = 5
|
||||||
const autoScrollInterval = ref(null)
|
const autoScrollInterval = ref(null)
|
||||||
const scrollInterval = 5000 // 5秒自动翻页
|
const scrollInterval = 5000 // 5秒自动翻页
|
||||||
|
const wsConnection = ref(null) // WebSocket 连接
|
||||||
|
const timeInterval = ref(null)
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
const screenContainer = ref(null)
|
||||||
|
let tableContainer = null
|
||||||
|
|
||||||
// 科室名称
|
// 科室名称
|
||||||
const departmentName = ref('心内科叫号显示屏幕')
|
const departmentName = ref('叫号显示屏幕')
|
||||||
|
|
||||||
// 计算属性
|
const applyDefaultDepartmentName = () => {
|
||||||
|
if (userStore.orgName) {
|
||||||
|
departmentName.value = `${userStore.orgName} 叫号显示屏`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待总人数(从后端返回)
|
||||||
|
const waitingCount = ref(0)
|
||||||
|
|
||||||
|
// 计算属性:按医生分组的患者列表
|
||||||
const groupedPatients = computed(() => {
|
const groupedPatients = computed(() => {
|
||||||
const grouped = {}
|
const grouped = {}
|
||||||
patients.value.forEach(patient => {
|
patients.value.forEach(doctorGroup => {
|
||||||
if (!grouped[patient.doctor]) {
|
grouped[doctorGroup.doctorName] = doctorGroup.patients || []
|
||||||
grouped[patient.doctor] = []
|
|
||||||
}
|
|
||||||
grouped[patient.doctor].push(patient)
|
|
||||||
})
|
})
|
||||||
return grouped
|
return grouped
|
||||||
})
|
})
|
||||||
|
|
||||||
const waitingCount = computed(() => {
|
|
||||||
let count = 0
|
|
||||||
Object.values(groupedPatients.value).forEach(group => {
|
|
||||||
count += Math.max(0, group.length - 1) // 排除每个医生组中第一个就诊中的患者
|
|
||||||
})
|
|
||||||
return count
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取排序后的医生列表
|
// 获取排序后的医生列表
|
||||||
const sortedDoctors = computed(() => {
|
const sortedDoctors = computed(() => {
|
||||||
return Object.keys(groupedPatients.value).sort()
|
return patients.value.map(group => group.doctorName)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 按医生分组的分页逻辑
|
// 按医生分组的分页逻辑
|
||||||
@@ -159,6 +179,24 @@ const updateTime = () => {
|
|||||||
currentTime.value = now.format('YYYY-MM-DD HH:mm')
|
currentTime.value = now.format('YYYY-MM-DD HH:mm')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFullscreenState = () => {
|
||||||
|
const isActive = !!document.fullscreenElement
|
||||||
|
isFullscreen.value = isActive
|
||||||
|
document.body.classList.toggle('call-screen-fullscreen', isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = async () => {
|
||||||
|
try {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
await document.exitFullscreen()
|
||||||
|
} else if (screenContainer.value && screenContainer.value.requestFullscreen) {
|
||||||
|
await screenContainer.value.requestFullscreen()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换全屏失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatPatientName = (name) => {
|
const formatPatientName = (name) => {
|
||||||
if (!name || typeof name !== 'string') return '-'
|
if (!name || typeof name !== 'string') return '-'
|
||||||
if (name.length === 0) return '-'
|
if (name.length === 0) return '-'
|
||||||
@@ -166,49 +204,98 @@ const formatPatientName = (name) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDoctorRoom = (doctorName) => {
|
const getDoctorRoom = (doctorName) => {
|
||||||
// 根据医生获取固定诊室
|
// 从后端数据中查找医生的诊室号
|
||||||
const doctorRooms = {
|
const doctorGroup = patients.value.find(group => group.doctorName === doctorName)
|
||||||
'张医生': '3号',
|
return doctorGroup?.roomNo || '1号'
|
||||||
'李医生': '1号',
|
|
||||||
'王医生': '2号'
|
|
||||||
}
|
|
||||||
return doctorRooms[doctorName] || '1号'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateWaitingData = async () => {
|
const ensureUserInfo = async () => {
|
||||||
|
if (!userStore.orgId) {
|
||||||
|
try {
|
||||||
|
await userStore.getInfo()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyDefaultDepartmentName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示屏数据(从后端API)
|
||||||
|
*/
|
||||||
|
const fetchDisplayData = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!ORGANIZATION_ID.value) {
|
||||||
|
ElMessage.warning('未获取到登录用户科室信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// 确保数组已正确初始化
|
console.log('正在获取显示屏数据...', {
|
||||||
if (!Array.isArray(patients.value)) {
|
url: `${API_BASE_URL}/display`,
|
||||||
patients.value = []
|
organizationId: ORGANIZATION_ID.value,
|
||||||
|
tenantId: TENANT_ID.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
url: `${API_BASE_URL}/display`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
organizationId: ORGANIZATION_ID.value,
|
||||||
|
tenantId: TENANT_ID.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('后端响应:', response)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
const data = response.data
|
||||||
|
|
||||||
|
// 更新科室名称
|
||||||
|
if (data.departmentName && data.departmentName !== '叫号显示屏') {
|
||||||
|
departmentName.value = data.departmentName
|
||||||
|
} else {
|
||||||
|
applyDefaultDepartmentName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前叫号信息
|
||||||
|
if (data.currentCall) {
|
||||||
|
currentCall.value = data.currentCall
|
||||||
|
} else {
|
||||||
|
currentCall.value = {
|
||||||
|
number: null,
|
||||||
|
name: '-',
|
||||||
|
room: '-',
|
||||||
|
doctor: '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等候队列(按医生分组)
|
||||||
|
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||||||
|
patients.value = data.waitingList
|
||||||
|
console.log('等候队列数据:', data.waitingList)
|
||||||
|
} else {
|
||||||
|
patients.value = []
|
||||||
|
console.log('等候队列为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等待人数
|
||||||
|
waitingCount.value = data.waitingCount || 0
|
||||||
|
|
||||||
|
console.log('显示屏数据更新成功', data)
|
||||||
|
ElMessage.success('数据加载成功')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.msg || '获取数据失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟API调用获取候诊数据
|
|
||||||
// 实际项目中这里应该调用真实API
|
|
||||||
const mockData = [
|
|
||||||
{ id: 13, name: '李四', type: '专家', doctor: '张医生', status: '就诊中' },
|
|
||||||
{ id: 14, name: '王五', type: '普通', doctor: '李医生', status: '候诊中' },
|
|
||||||
{ id: 15, name: '赵六', type: '专家', doctor: '张医生', status: '候诊中' },
|
|
||||||
{ id: 16, name: '钱七', type: '普通', doctor: '王医生', status: '候诊中' },
|
|
||||||
{ id: 17, name: '孙八', type: '专家', doctor: '李医生', status: '候诊中' },
|
|
||||||
{ id: 18, name: '周九', type: '普通', doctor: '王医生', status: '候诊中' },
|
|
||||||
{ id: 19, name: '吴十', type: '专家', doctor: '张医生', status: '候诊中' },
|
|
||||||
{ id: 20, name: '郑一', type: '普通', doctor: '李医生', status: '候诊中' },
|
|
||||||
{ id: 21, name: '王二', type: '专家', doctor: '王医生', status: '候诊中' },
|
|
||||||
{ id: 22, name: '李三', type: '普通', doctor: '张医生', status: '候诊中' },
|
|
||||||
{ id: 23, name: '赵四', type: '专家', doctor: '李医生', status: '候诊中' },
|
|
||||||
{ id: 24, name: '钱五', type: '普通', doctor: '王医生', status: '候诊中' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟网络延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
|
||||||
patients.value = mockData
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取候诊数据失败:', error)
|
console.error('获取显示屏数据失败:', error)
|
||||||
ElMessage.error('获取候诊数据失败')
|
ElMessage.error('获取显示屏数据失败:' + (error.message || '未知错误'))
|
||||||
// 出错时设置为空数组
|
|
||||||
|
// 出错时设置默认值
|
||||||
patients.value = []
|
patients.value = []
|
||||||
|
currentCall.value = { number: null, name: '-', room: '-', doctor: '-' }
|
||||||
|
waitingCount.value = 0
|
||||||
|
applyDefaultDepartmentName()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -259,40 +346,177 @@ const stopAutoScroll = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 WebSocket 连接
|
||||||
|
*/
|
||||||
|
const initWebSocket = () => {
|
||||||
|
try {
|
||||||
|
if (!ORGANIZATION_ID.value) {
|
||||||
|
console.warn('未获取到科室ID,跳过 WebSocket 连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('正在连接 WebSocket:', WS_URL.value)
|
||||||
|
wsConnection.value = new WebSocket(WS_URL.value)
|
||||||
|
|
||||||
|
wsConnection.value.onopen = () => {
|
||||||
|
console.log('WebSocket 连接成功')
|
||||||
|
ElMessage.success('实时连接已建立')
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConnection.value.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
console.log('收到 WebSocket 消息:', message)
|
||||||
|
|
||||||
|
if (message.type === 'connected') {
|
||||||
|
console.log('WebSocket 连接确认:', message.message)
|
||||||
|
} else if (message.type === 'update') {
|
||||||
|
// 收到更新消息,刷新显示屏数据
|
||||||
|
console.log('收到更新通知,刷新显示屏数据')
|
||||||
|
handleWebSocketUpdate(message.data)
|
||||||
|
} else if (message.type === 'pong') {
|
||||||
|
// 心跳响应
|
||||||
|
console.log('心跳响应')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析 WebSocket 消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConnection.value.onerror = (error) => {
|
||||||
|
console.error('WebSocket 错误:', error)
|
||||||
|
ElMessage.error('实时连接出现错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConnection.value.onclose = () => {
|
||||||
|
console.log('WebSocket 连接关闭,5秒后重连')
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!wsConnection.value || wsConnection.value.readyState === WebSocket.CLOSED) {
|
||||||
|
initWebSocket()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心跳检测:每30秒发送一次 ping
|
||||||
|
setInterval(() => {
|
||||||
|
if (wsConnection.value && wsConnection.value.readyState === WebSocket.OPEN) {
|
||||||
|
wsConnection.value.send('ping')
|
||||||
|
}
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化 WebSocket 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 WebSocket 推送的更新数据
|
||||||
|
*/
|
||||||
|
const handleWebSocketUpdate = (data) => {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
// 更新科室名称
|
||||||
|
if (data.departmentName) {
|
||||||
|
departmentName.value = data.departmentName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新当前叫号信息
|
||||||
|
if (data.currentCall) {
|
||||||
|
currentCall.value = data.currentCall
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等候队列
|
||||||
|
if (data.waitingList && Array.isArray(data.waitingList)) {
|
||||||
|
patients.value = data.waitingList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新等待人数
|
||||||
|
if (data.waitingCount !== undefined) {
|
||||||
|
waitingCount.value = data.waitingCount
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('显示屏数据已更新(来自 WebSocket)')
|
||||||
|
|
||||||
|
// 播放语音(如果有新的叫号)
|
||||||
|
if (data.currentCall && data.currentCall.number) {
|
||||||
|
playVoiceNotification(data.currentCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放语音通知
|
||||||
|
*/
|
||||||
|
const playVoiceNotification = (callInfo) => {
|
||||||
|
if (!callInfo || !callInfo.number) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 Web Speech API 播放语音
|
||||||
|
const utterance = new SpeechSynthesisUtterance(
|
||||||
|
`请${callInfo.number}号${callInfo.name}到${callInfo.room}诊室就诊`
|
||||||
|
)
|
||||||
|
utterance.lang = 'zh-CN'
|
||||||
|
utterance.rate = 0.9 // 语速
|
||||||
|
utterance.pitch = 1.0 // 音调
|
||||||
|
utterance.volume = 1.0 // 音量
|
||||||
|
|
||||||
|
window.speechSynthesis.speak(utterance)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('语音播放失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭 WebSocket 连接
|
||||||
|
*/
|
||||||
|
const closeWebSocket = () => {
|
||||||
|
if (wsConnection.value) {
|
||||||
|
wsConnection.value.close()
|
||||||
|
wsConnection.value = null
|
||||||
|
console.log('WebSocket 连接已关闭')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
document.addEventListener('fullscreenchange', updateFullscreenState)
|
||||||
|
await ensureUserInfo()
|
||||||
// 初始化时间
|
// 初始化时间
|
||||||
updateTime()
|
updateTime()
|
||||||
// 每分钟更新时间
|
// 每分钟更新时间
|
||||||
const timeInterval = setInterval(updateTime, 60000)
|
timeInterval.value = setInterval(updateTime, 60000)
|
||||||
|
|
||||||
// 获取候诊数据
|
// ✅ 获取初始数据(从后端 API)
|
||||||
await generateWaitingData()
|
await fetchDisplayData()
|
||||||
|
|
||||||
|
// ✅ 初始化 WebSocket 连接(实时推送)
|
||||||
|
initWebSocket()
|
||||||
|
|
||||||
// 启动自动滚动
|
// 启动自动滚动
|
||||||
startAutoScroll()
|
startAutoScroll()
|
||||||
|
|
||||||
// 鼠标悬停时暂停自动滚动
|
// 鼠标悬停时暂停自动滚动
|
||||||
const tableContainer = document.querySelector('.table-container')
|
tableContainer = document.querySelector('.table-container')
|
||||||
if (tableContainer) {
|
if (tableContainer) {
|
||||||
tableContainer.addEventListener('mouseenter', stopAutoScroll)
|
tableContainer.addEventListener('mouseenter', stopAutoScroll)
|
||||||
tableContainer.addEventListener('mouseleave', startAutoScroll)
|
tableContainer.addEventListener('mouseleave', startAutoScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(timeInterval)
|
|
||||||
stopAutoScroll()
|
|
||||||
if (tableContainer) {
|
|
||||||
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
|
||||||
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时的清理工作
|
// 组件卸载时的清理工作
|
||||||
|
if (timeInterval.value) {
|
||||||
|
clearInterval(timeInterval.value)
|
||||||
|
timeInterval.value = null
|
||||||
|
}
|
||||||
stopAutoScroll()
|
stopAutoScroll()
|
||||||
|
closeWebSocket() // ✅ 关闭 WebSocket 连接
|
||||||
|
if (tableContainer) {
|
||||||
|
tableContainer.removeEventListener('mouseenter', stopAutoScroll)
|
||||||
|
tableContainer.removeEventListener('mouseleave', startAutoScroll)
|
||||||
|
tableContainer = null
|
||||||
|
}
|
||||||
|
document.removeEventListener('fullscreenchange', updateFullscreenState)
|
||||||
|
document.body.classList.remove('call-screen-fullscreen')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听页面变化,重置滚动位置
|
// 监听页面变化,重置滚动位置
|
||||||
@@ -317,6 +541,43 @@ watchEffect(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen) {
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .sidebar-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .navbar),
|
||||||
|
:global(body.call-screen-fullscreen .tags-view-container),
|
||||||
|
:global(body.call-screen-fullscreen #tags-view-container),
|
||||||
|
:global(body.call-screen-fullscreen .drawer-bg) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .app-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .main-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .content-wrapper),
|
||||||
|
:global(body.call-screen-fullscreen .app-main) {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .app-wrapper) {
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.call-screen-fullscreen .call-number-display) {
|
||||||
|
max-width: none;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 头部样式 */
|
/* 头部样式 */
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
|
background: linear-gradient(135deg, #4a90e2, #5fa3e8);
|
||||||
@@ -342,6 +603,28 @@ watchEffect(() => {
|
|||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
|
border-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当前呼叫区 */
|
/* 当前呼叫区 */
|
||||||
|
|||||||
Reference in New Issue
Block a user