7 Commits

Author SHA1 Message Date
wangjian963
902ee0587e 修改新增耗材“一次性静脉采血器” 点击保存categoryEnum为空值的问题。 2026-01-22 14:13:10 +08:00
49550fcc2e 诊疗下面没有诊疗项目 2026-01-22 14:03:38 +08:00
sindir
1dd7ee3428 90,分诊排队管理-》医生叫号界面 2026-01-22 12:14:01 +08:00
wangjian963
8dff5d466a Merge remote-tracking branch 'origin/develop' into develop 2026-01-22 09:22:22 +08:00
wangjian963
19ada4ace9 移除调用字典接口功能 2026-01-22 09:20:35 +08:00
c92ff38133 Merge remote-tracking branch 'origin/develop' into develop 2026-01-21 17:50:58 +08:00
1c07108e58 refactor(PatientList): 重构患者列表卡片布局结构
- 将原有的 header-top 和 header-bottom 结构替换为 info-row 统一布局
- 新增姓名、性别年龄、房间床号、住院号、保险类型等独立信息行
- 使用 el-text 组件优化姓名显示效果
- 为性别标签添加女性样式标识
- 调整床位信息展示方式,支持溢出省略
- 修改溢出属性从 hidden 为 visible 确保内容正常显示
- 优化标签样式和间距布局
- 隐藏已废弃的旧布局元素
- 调整 pending 患者列表的换行和对齐方式
2026-01-21 17:50:51 +08:00
31 changed files with 2360 additions and 235 deletions

View File

@@ -1,22 +1,29 @@
package com.core.framework.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.core.common.utils.SecurityUtils;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -149,4 +156,41 @@ public class MybatisPlusConfig {
return result != null ? result : 1; // 默认租户ID
}
/**
* 配置 SqlSessionFactory
* 由于排除了 DataSourceAutoConfiguration需要手动配置
*/
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(
@Qualifier("dynamicDataSource") DataSource dataSource,
MybatisPlusInterceptor mybatisPlusInterceptor) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 设置 mapper 文件位置
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/**/*Mapper.xml"));
// 设置 typeAliases 包路径
sessionFactory.setTypeAliasesPackage("com.core.**.domain,com.openhis.**.domain");
// 配置 MyBatis-Plus
MybatisConfiguration configuration = new MybatisConfiguration();
// 使用驼峰命名法转换字段
configuration.setMapUnderscoreToCamelCase(true);
// 开启缓存
configuration.setCacheEnabled(true);
// 允许JDBC支持自动生成主键
configuration.setUseGeneratedKeys(true);
// 配置默认的执行器
configuration.setDefaultExecutorType(org.apache.ibatis.session.ExecutorType.SIMPLE);
// 配置日志实现
configuration.setLogImpl(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
sessionFactory.setConfiguration(configuration);
// 设置拦截器(通过参数注入避免循环依赖)
sessionFactory.setPlugins(mybatisPlusInterceptor);
return sessionFactory.getObject();
}
}

View File

@@ -73,8 +73,10 @@ public class OutpatientPricingAppServiceImpl implements IOutpatientPricingAppSer
} else {
adviceTypes = List.of(1, 2, 3);
}
// 门诊划价:不要强制 pricingFlag=1 参与过滤wor_activity_definition.pricing_flag 可能为 0
// 否则会导致诊疗项目(adviceType=3)查询结果为空 records=[]
return iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, null,
organizationId, pageNo, pageSize, Whether.YES.getValue(), adviceTypes, null);
organizationId, pageNo, pageSize, null, adviceTypes, null);
}
}

View File

@@ -90,4 +90,10 @@ public interface IDoctorStationMainAppService {
*/
List<ReceptionStatisticsDto> getReceptionStatistics(String startTime,String endTime,Long practitionerId);
/**
* 过号重排
* @param encounterId 就诊ID
* @return 操作结果
*/
R<?> rearrangeMissedEncounter(Long encounterId);
}

View File

@@ -127,8 +127,11 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp
log.info("从数据库查询医嘱基础信息");
// 设置默认科室 (不取前端传的了)
organizationId = SecurityUtils.getLoginUser().getOrgId();
// 设置默认科室:仅当前端/调用方未传 organizationId 时才回退到登录人科室
// 否则会导致门诊划价等场景(按患者挂号科室查询)返回空
if (organizationId == null) {
organizationId = SecurityUtils.getLoginUser().getOrgId();
}
// 医嘱定价来源
String orderPricingSource = TenantOptionUtil.getOptionContent(TenantOptionDict.ORDER_PRICING_SOURCE);

View File

@@ -26,9 +26,11 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -323,4 +325,45 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
practitionerId);
}
/**
* 过号重排核心实现
*/
@Override
@Transactional(rollbackFor = Exception.class) // 事务保证原子性
public R<?> rearrangeMissedEncounter(Long encounterId) {
// 1. 校验就诊记录是否存在
Encounter encounter = encounterMapper.selectById(encounterId);
if (encounter == null) {
return R.fail("就诊记录不存在");
}
// 2. 校验状态仅「在诊IN_PROGRESS=2」可重排
if (!EncounterStatus.IN_PROGRESS.getValue().equals(encounter.getStatusEnum())) {
return R.fail("仅「在诊」状态的患者可执行过号重排");
}
// 3. 核心更新:改回待诊+更新missed_time
Date now = new Date();
Long practitionerId = SecurityUtils.getLoginUser().getPractitionerId();
int updateCount = encounterMapper.update(null,
new LambdaUpdateWrapper<Encounter>()
.eq(Encounter::getId, encounterId)
.set(Encounter::getStatusEnum, EncounterStatus.PLANNED.getValue()) // 改回1-待诊
.set(Encounter::getMissedTime, now) // 新增:设置过号时间为当前时间
.set(Encounter::getUpdateBy, practitionerId.toString()) // 操作医生ID
.eq(Encounter::getStatusEnum, EncounterStatus.IN_PROGRESS.getValue())); // 防并发
if (updateCount == 0) {
return R.fail("过号重排失败:状态更新异常");
}
// 4. 同步更新接诊参与记录
iEncounterParticipantService.update(new LambdaUpdateWrapper<EncounterParticipant>()
.eq(EncounterParticipant::getEncounterId, encounterId)
.eq(EncounterParticipant::getTypeCode, ParticipantType.ADMITTER.getCode())
.set(EncounterParticipant::getStatusEnum, EncounterActivityStatus.COMPLETED.getValue()));
return R.ok("过号重排成功");
}
}

View File

@@ -133,6 +133,29 @@ public class DoctorStationMainController {
}
}
/**
* 过号重排
*
* @param encounterId 就诊id
* @return 结果
*/
@GetMapping(value = "/rearrange-missed-encounter")
public R<?> rearrangeMissedEncounter(@RequestParam(value = "encounterId", required = false) String encounterId) {
// 1. 空值校验(和现有接口保持一致)
if (encounterId == null || "undefined".equals(encounterId) || "null".equals(encounterId)) {
return R.fail("就诊ID不能为空");
}
try {
// 2. 字符串转Long和现有接口保持一致
Long id = Long.parseLong(encounterId);
// 3. 调用AppService的过号重排方法
return iDoctorStationMainAppService.rearrangeMissedEncounter(id);
} catch (NumberFormatException e) {
// 4. 格式错误处理(和现有接口保持一致)
return R.fail("就诊ID格式错误");
}
}
/**
* 查询处方号列表信息
*

View File

@@ -1,5 +1,6 @@
package com.openhis.web.doctorstation.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.openhis.common.annotation.Dict;
@@ -127,4 +128,9 @@ public class PatientInfoDto {
* 就诊卡号
*/
private String identifierNo;
/**
* 过号时间
*/
private Date missedTime;
}

View File

@@ -112,6 +112,7 @@
ON T1.context_enum = #{activity}
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
LEFT JOIN med_medication_definition AS T3
ON T1.context_enum = #{medication}
AND T1.product_id = T3.id
@@ -205,6 +206,7 @@
ON T1.context_enum = #{activity}
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
LEFT JOIN med_medication_definition AS T3
ON T1.context_enum = #{medication}
AND T1.product_id = T3.id

View File

@@ -227,8 +227,11 @@
AND T2.instance_table = #{activityTableName}
LEFT JOIN adm_organization_location AS T3 ON T3.activity_definition_id = T1.ID
AND T3.delete_flag = '0' AND (CURRENT_TIME :: time (6) BETWEEN T3.start_time AND T3.end_time)
<if test="organizationId != null">
AND T3.organization_id = #{organizationId}
</if>
WHERE T1.delete_flag = '0'
<if test="pricingFlag ==1">
<if test="pricingFlag != null and pricingFlag == 1">
AND (T1.pricing_flag = #{pricingFlag} OR T1.pricing_flag IS NULL)
</if>
<if test="adviceDefinitionIdParamList != null and !adviceDefinitionIdParamList.isEmpty()">

View File

@@ -24,7 +24,8 @@
T10.practitioner_user_id,
T10.jz_practitioner_user_id,
T10.bus_no,
T10.identifier_no
T10.identifier_no,
T10.missed_time
from
(
SELECT T1.tenant_id AS tenant_id,
@@ -50,7 +51,8 @@
T1.reception_time AS reception_time,
T1.organization_id AS org_id,
T8.bus_no AS bus_no,
T9.identifier_no AS identifier_no
T9.identifier_no AS identifier_no,
T1.missed_time AS missed_time
FROM adm_encounter AS T1
LEFT JOIN adm_organization AS T2 ON T1.organization_id = T2.ID AND T2.delete_flag = '0'
LEFT JOIN adm_healthcare_service AS T3 ON T1.service_type_id = T3.ID AND T3.delete_flag = '0'
@@ -70,18 +72,18 @@
LEFT JOIN fin_contract AS T7 ON T6.contract_no = T7.bus_no AND T7.delete_flag = '0'
LEFT JOIN adm_patient AS T8 ON T1.patient_id = T8.ID AND T8.delete_flag = '0'
LEFT JOIN (
SELECT patient_id,
identifier_no
FROM (
SELECT patient_id,
identifier_no,
ROW_NUMBER() OVER (PARTITION BY patient_id ORDER BY create_time ASC) AS rn
FROM adm_patient_identifier
WHERE delete_flag = '0'
AND identifier_no IS NOT NULL
AND identifier_no != ''
) t
WHERE rn = 1
SELECT patient_id,
identifier_no
FROM (
SELECT patient_id,
identifier_no,
ROW_NUMBER() OVER (PARTITION BY patient_id ORDER BY create_time ASC) AS rn
FROM adm_patient_identifier
WHERE delete_flag = '0'
AND identifier_no IS NOT NULL
AND identifier_no != ''
) t
WHERE rn = 1
) AS T9 ON T8.id = T9.patient_id
WHERE
T1.delete_flag = '0'

View File

@@ -117,10 +117,15 @@ public class DictAspect {
private String queryDictLabel(String dictTable, String dictCode, String dictText, String dictValue) {
if (!StringUtils.hasText(dictTable)) {
// 场景 1默认字典走DictUtils缓存
// 场景 1默认字典走DictUtils缓存dictTable 为空时)
return DictUtils.getDictLabel(dictCode, dictValue);
} else {
// 场景 2查询指定表
// 场景 2查询指定表dictTable 有值时)
// 必须同时有 dictTable 和 dictText 才能执行 SQL 查询
if (!StringUtils.hasText(dictText)) {
// 如果 dictText 为空,回退到字典缓存查询
return DictUtils.getDictLabel(dictCode, dictValue);
}
String sql = String.format("SELECT %s FROM %s WHERE %s::varchar = ? LIMIT 1", dictText, dictTable, dictCode);
try {
return jdbcTemplate.queryForObject(sql, String.class, dictValue);

View File

@@ -1,8 +1,6 @@
package com.openhis.administration.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
@@ -145,4 +143,9 @@ public class Encounter extends HisBaseEntity {
*/
private Long registrarId;
/**
* 过号时间
*/
@TableField("missed_time")
private Date missedTime;
}

View File

@@ -0,0 +1,94 @@
-- ============================================
-- 检查门诊划价"诊疗判断"下没有数据的原因
-- ============================================
-- 1. 检查是否有诊疗类型的收费项目context_enum = 3
SELECT
COUNT(*) AS total_activity_charge_items,
COUNT(CASE WHEN delete_flag = '0' THEN 1 END) AS active_charge_items
FROM adm_charge_item
WHERE context_enum = 3; -- ACTIVITY = 3
-- 2. 检查诊疗类型的收费项目是否能关联到 wor_activity_definition
SELECT
T1.id AS charge_item_id,
T1.encounter_id,
T1.context_enum,
T1.product_id,
T1.status_enum AS charge_status,
T2.id AS activity_def_id,
T2.name AS activity_name,
T2.status_enum AS activity_status,
T2.delete_flag AS activity_delete_flag,
CASE
WHEN T2.id IS NULL THEN '❌ 无法关联到诊疗项目定义'
WHEN T2.delete_flag != '0' THEN '❌ 诊疗项目定义已删除'
WHEN T2.status_enum != 1 THEN '❌ 诊疗项目定义未激活status_enum != 1'
ELSE '✅ 正常'
END AS match_status
FROM adm_charge_item AS T1
LEFT JOIN wor_activity_definition AS T2
ON T1.context_enum = 3
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1 -- 当前查询条件
WHERE T1.context_enum = 3
AND T1.delete_flag = '0'
LIMIT 20;
-- 3. 检查如果移除 status_enum = 1 条件,能关联多少条
SELECT
COUNT(*) AS total_count,
COUNT(CASE WHEN T2.id IS NOT NULL AND T2.delete_flag = '0' AND T2.status_enum = 1 THEN 1 END) AS matched_active,
COUNT(CASE WHEN T2.id IS NOT NULL AND T2.delete_flag = '0' AND T2.status_enum != 1 THEN 1 END) AS matched_inactive,
COUNT(CASE WHEN T2.id IS NULL THEN 1 END) AS unmatched
FROM adm_charge_item AS T1
LEFT JOIN wor_activity_definition AS T2
ON T1.context_enum = 3
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
WHERE T1.context_enum = 3
AND T1.delete_flag = '0';
-- 4. 检查具体某个就诊的诊疗项目(替换 encounterId 为实际值)
-- SELECT
-- T1.id AS charge_item_id,
-- T1.encounter_id,
-- T1.product_id,
-- T1.status_enum AS charge_status,
-- T2.id AS activity_def_id,
-- T2.name AS activity_name,
-- T2.status_enum AS activity_status,
-- CASE
-- WHEN T2.id IS NULL THEN '无法关联'
-- WHEN T2.status_enum != 1 THEN '诊疗项目未激活'
-- ELSE '正常'
-- END AS status
-- FROM adm_charge_item AS T1
-- LEFT JOIN wor_activity_definition AS T2
-- ON T1.context_enum = 3
-- AND T1.product_id = T2.id
-- AND T2.delete_flag = '0'
-- AND T2.status_enum = 1
-- WHERE T1.encounter_id = :encounterId -- 替换为实际的encounterId
-- AND T1.context_enum = 3
-- AND T1.delete_flag = '0';
-- 5. 如果诊疗项目 status_enum != 1查看这些项目
SELECT
T1.id AS charge_item_id,
T1.encounter_id,
T1.product_id,
T2.name AS activity_name,
T2.status_enum AS activity_status,
'诊疗项目未激活,导致无法显示' AS issue
FROM adm_charge_item AS T1
INNER JOIN wor_activity_definition AS T2
ON T1.context_enum = 3
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
AND T2.status_enum != 1 -- 未激活
WHERE T1.context_enum = 3
AND T1.delete_flag = '0'
LIMIT 20;

View File

@@ -0,0 +1,142 @@
-- ============================================
-- 检查 wor_activity_definition 表中 pricing_flag = 0 的数据
-- 用途:分析哪些项目被标记为"不允许划价",是否符合业务预期
-- ============================================
-- 1. 查看所有 pricing_flag = 0 的项目详情
SELECT
id,
bus_no,
name AS activity_name,
category_code,
type_enum,
pricing_flag,
status_enum,
org_id,
location_id,
description_text,
create_time,
update_time
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '0' -- 注意:字段类型是 char(1),所以是字符串 '0'
ORDER BY id;
-- 2. 统计 pricing_flag = 0 的项目数量(按状态)
SELECT
status_enum,
COUNT(*) AS count,
CASE
WHEN status_enum = 1 THEN '激活'
WHEN status_enum = 2 THEN '停用'
ELSE '其他'
END AS status_desc
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '0'
GROUP BY status_enum
ORDER BY status_enum;
-- 3. 检查 pricing_flag = 0 的项目是否有关联的费用定价
SELECT
T1.id,
T1.bus_no,
T1.name AS activity_name,
T1.pricing_flag,
T1.status_enum,
T2.id AS charge_item_definition_id,
T2.charge_name,
T2.price,
T2.status_enum AS charge_status,
CASE
WHEN T2.id IS NOT NULL THEN '有费用定价但不允许划价(可能异常)'
ELSE '无费用定价,不允许划价(正常)'
END AS analysis
FROM wor_activity_definition AS T1
LEFT JOIN adm_charge_item_definition AS T2
ON T2.instance_id = T1.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
AND T2.instance_table = 'wor_activity_definition'
WHERE T1.delete_flag = '0'
AND T1.pricing_flag = '0'
ORDER BY T1.id;
-- 4. 检查 pricing_flag = 0 的项目是否在收费项目表中被使用
SELECT
T1.id AS activity_id,
T1.bus_no,
T1.name AS activity_name,
T1.pricing_flag,
COUNT(T3.id) AS charge_item_count,
CASE
WHEN COUNT(T3.id) > 0 THEN '已被使用(可能异常:不允许划价但已有收费记录)'
ELSE '未被使用(正常)'
END AS analysis
FROM wor_activity_definition AS T1
LEFT JOIN adm_charge_item AS T3
ON T3.product_id = T1.id
AND T3.context_enum = 3 -- ACTIVITY
AND T3.delete_flag = '0'
WHERE T1.delete_flag = '0'
AND T1.pricing_flag = '0'
GROUP BY T1.id, T1.bus_no, T1.name, T1.pricing_flag
HAVING COUNT(T3.id) > 0 -- 只显示已被使用的
ORDER BY charge_item_count DESC;
-- 5. 按类别统计 pricing_flag = 0 的项目
SELECT
category_code,
COUNT(*) AS count,
STRING_AGG(name, ', ') AS activity_names -- 列出项目名称
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '0'
AND status_enum = 1 -- 只统计激活的
GROUP BY category_code
ORDER BY category_code;
-- 6. 对比pricing_flag = 0 和 pricing_flag = 1 的项目特征
SELECT
pricing_flag,
COUNT(*) AS count,
COUNT(DISTINCT category_code) AS category_count,
COUNT(DISTINCT org_id) AS org_count,
STRING_AGG(DISTINCT category_code::text, ', ') AS categories
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
AND pricing_flag IN ('0', '1')
GROUP BY pricing_flag
ORDER BY pricing_flag;
-- 7. 检查是否有子项的项目childrenJson 不为空)被标记为 pricing_flag = 0
SELECT
id,
bus_no,
name AS activity_name,
pricing_flag,
children_json,
CASE
WHEN children_json IS NOT NULL AND children_json != '' THEN '有子项但不允许划价(可能是套餐子项)'
ELSE '无子项'
END AS analysis
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '0'
AND children_json IS NOT NULL
AND children_json != ''
ORDER BY id;
-- 8. 查看 pricing_flag 字段的所有可能值(包括 NULL
SELECT
COALESCE(pricing_flag::text, 'NULL') AS pricing_flag_value,
COUNT(*) AS count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) AS percentage
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
GROUP BY pricing_flag
ORDER BY pricing_flag NULLS LAST;

View File

@@ -0,0 +1,116 @@
-- 详细检查诊疗项目检索问题
-- 已知wor_activity_definition 表中有4条数据
-- 1. 检查这4条诊疗项目定义的详细信息
SELECT
id,
name,
bus_no,
delete_flag,
tenant_id,
create_time
FROM wor_activity_definition
ORDER BY id;
-- 2. 检查收费项目中是否有诊疗项目类型的记录
-- 注意ChargeItemContext.ACTIVITY.getValue() 返回的是 Integer 3
-- 但数据库中 context_enum 可能是字符串类型,需要检查是 '3' 还是 3
SELECT
COUNT(*) as total_activity_charge_items,
COUNT(CASE WHEN delete_flag = '0' THEN 1 END) as active_activity_charge_items,
COUNT(CASE WHEN delete_flag != '0' THEN 1 END) as deleted_activity_charge_items
FROM adm_charge_item
WHERE context_enum::text = '3' OR context_enum = 3; -- 诊疗项目类型是3
-- 3. 检查收费项目中的product_id是否能匹配到诊疗项目定义
-- 注意:诊疗项目类型的 context_enum = 3ChargeItemContext.ACTIVITY.getValue()
SELECT
aci.id as charge_item_id,
aci.encounter_id,
aci.context_enum,
aci.product_id as charge_product_id,
aci.status_enum,
aci.delete_flag as charge_delete_flag,
wad.id as activity_def_id,
wad.name as activity_name,
wad.delete_flag as activity_delete_flag,
CASE
WHEN wad.id IS NULL THEN '❌ 诊疗项目定义不存在'
WHEN wad.delete_flag != '0' THEN '❌ 诊疗项目定义已删除'
WHEN aci.delete_flag != '0' THEN '❌ 收费项目已删除'
ELSE '✅ 正常'
END as match_status
FROM adm_charge_item aci
LEFT JOIN wor_activity_definition wad
ON aci.product_id = wad.id
WHERE (aci.context_enum::text = '3' OR aci.context_enum = 3) -- 诊疗项目类型是3
LIMIT 20;
-- 4. 检查context_enum的所有可能值确认诊疗项目的枚举值是什么
SELECT DISTINCT
context_enum,
COUNT(*) as count
FROM adm_charge_item
WHERE delete_flag = '0'
GROUP BY context_enum
ORDER BY context_enum;
-- 5. 检查是否有收费项目,但无法匹配到诊疗项目定义
SELECT
COUNT(*) as unmatched_count,
STRING_AGG(DISTINCT CAST(product_id AS VARCHAR), ', ') as unmatched_product_ids,
STRING_AGG(DISTINCT CAST(id AS VARCHAR), ', ') as unmatched_charge_item_ids
FROM adm_charge_item aci
WHERE (aci.context_enum::text = '3' OR aci.context_enum = 3) -- 诊疗项目类型是3
AND aci.delete_flag = '0'
AND NOT EXISTS (
SELECT 1
FROM wor_activity_definition wad
WHERE wad.id = aci.product_id
AND wad.delete_flag = '0'
);
-- 6. 检查具体某个就诊的诊疗项目如果有具体的encounterId
-- SELECT
-- T1.encounter_id,
-- T1.id as charge_item_id,
-- T1.context_enum,
-- T1.product_id,
-- T1.status_enum,
-- T1.delete_flag,
-- T2.id as activity_def_id,
-- T2.name as activity_name,
-- T2.delete_flag as activity_delete_flag
-- FROM adm_charge_item AS T1
-- LEFT JOIN wor_activity_definition AS T2
-- ON T1.product_id = T2.id
-- WHERE T1.encounter_id = :encounterId -- 替换为实际的encounterId
-- AND T1.context_enum IN ('ACTIVITY', '1', 'ACTIVITY_CODE') -- 尝试多种可能的值
-- AND T1.delete_flag = '0';
-- 7. 检查后端代码中使用的context_enum值
-- ChargeItemContext.ACTIVITY.getValue() 的实际返回值需要查看枚举类
-- 可能是:'ACTIVITY', '1', 'ACTIVITY_CODE', 或其他值
-- 建议先运行查询4查看数据库中实际使用的context_enum值
-- 8. 检查status_enum的值查询条件中需要匹配的状态
-- SQL查询中要求 status_enum IN (1, 2, 3, 4, 5, 6)
-- 检查收费项目中的状态值是否在这个范围内
SELECT
status_enum,
COUNT(*) as count,
CASE
WHEN status_enum IN (1, 2, 3, 4, 5, 6) THEN '✅ 在查询范围内'
ELSE '❌ 不在查询范围内'
END as status_check
FROM adm_charge_item
WHERE context_enum IN (
SELECT DISTINCT context_enum
FROM adm_charge_item
WHERE delete_flag = '0'
LIMIT 10
)
AND delete_flag = '0'
GROUP BY status_enum
ORDER BY status_enum;

View File

@@ -0,0 +1,92 @@
-- 诊断:门诊划价检索不出诊疗项目的问题
-- 问题描述:在门诊划价页面,检索不出诊疗项目
-- 1. 检查是否有诊疗项目定义数据
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN delete_flag = '0' THEN 1 END) as active_count,
COUNT(CASE WHEN delete_flag != '0' THEN 1 END) as deleted_count
FROM wor_activity_definition;
-- 2. 检查是否有收费项目(诊疗项目类型)
SELECT
T1.id,
T1.encounter_id,
T1.context_enum,
T1.product_id,
T1.status_enum,
T1.delete_flag,
T2.id as activity_def_id,
T2.name as activity_name,
T2.delete_flag as activity_delete_flag
FROM adm_charge_item AS T1
LEFT JOIN wor_activity_definition AS T2
ON T1.context_enum = 'ACTIVITY' -- 诊疗项目类型
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
WHERE T1.context_enum = 'ACTIVITY'
AND T1.delete_flag = '0'
LIMIT 20;
-- 3. 检查是否有诊疗项目定义但收费项目中的product_id无法匹配
SELECT
'诊疗项目定义存在,但收费项目无法匹配' as issue_type,
COUNT(*) as count
FROM wor_activity_definition wad
WHERE wad.delete_flag = '0'
AND NOT EXISTS (
SELECT 1
FROM adm_charge_item aci
WHERE aci.context_enum = 'ACTIVITY'
AND aci.product_id = wad.id
AND aci.delete_flag = '0'
);
-- 4. 检查收费项目中的诊疗项目,但定义表中没有对应数据
SELECT
'收费项目存在,但诊疗项目定义缺失' as issue_type,
COUNT(*) as count
FROM adm_charge_item aci
WHERE aci.context_enum = 'ACTIVITY'
AND aci.delete_flag = '0'
AND NOT EXISTS (
SELECT 1
FROM wor_activity_definition wad
WHERE wad.id = aci.product_id
AND wad.delete_flag = '0'
);
-- 5. 检查某个具体就诊的诊疗项目替换encounterId为实际值
-- SELECT
-- T1.encounter_id,
-- T1.id as charge_item_id,
-- T1.context_enum,
-- T1.product_id,
-- T1.status_enum,
-- T2.id as activity_def_id,
-- T2.name as activity_name,
-- T2.delete_flag as activity_delete_flag,
-- CASE
-- WHEN T2.id IS NULL THEN '诊疗项目定义不存在或已删除'
-- WHEN T2.delete_flag != '0' THEN '诊疗项目定义已删除'
-- ELSE '正常'
-- END as status
-- FROM adm_charge_item AS T1
-- LEFT JOIN wor_activity_definition AS T2
-- ON T1.context_enum = 'ACTIVITY'
-- AND T1.product_id = T2.id
-- WHERE T1.encounter_id = :encounterId -- 替换为实际的encounterId
-- AND T1.context_enum = 'ACTIVITY'
-- AND T1.delete_flag = '0'
-- AND T1.status_enum IN (1, 2, 3, 4, 5, 6); -- PLANNED, BILLABLE, BILLED, REFUNDING, REFUNDED, PART_REFUND

View File

@@ -0,0 +1,144 @@
-- ============================================
-- 查询诊疗项目 pricing_flag 字段分布情况
-- 用途:了解哪些项目允许划价,哪些不允许
-- ============================================
-- 1. 统计 pricing_flag 字段的分布情况
SELECT
COUNT(*) AS total_count,
COUNT(CASE WHEN pricing_flag = 1 THEN 1 END) AS pricing_flag_1_count, -- 允许划价
COUNT(CASE WHEN pricing_flag = 0 THEN 1 END) AS pricing_flag_0_count, -- 不允许划价
COUNT(CASE WHEN pricing_flag IS NULL THEN 1 END) AS pricing_flag_null_count, -- 未设置
ROUND(COUNT(CASE WHEN pricing_flag = 1 THEN 1 END) * 100.0 / COUNT(*), 2) AS flag_1_percent,
ROUND(COUNT(CASE WHEN pricing_flag = 0 THEN 1 END) * 100.0 / COUNT(*), 2) AS flag_0_percent,
ROUND(COUNT(CASE WHEN pricing_flag IS NULL THEN 1 END) * 100.0 / COUNT(*), 2) AS flag_null_percent
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1; -- 只统计激活状态的项目
-- 2. 查看所有允许划价的项目pricing_flag = 1 或 NULL
SELECT
id,
bus_no,
name AS activity_name,
pricing_flag,
status_enum,
org_id,
category_code,
type_enum,
CASE
WHEN pricing_flag = 1 THEN '允许划价'
WHEN pricing_flag = 0 THEN '不允许划价'
WHEN pricing_flag IS NULL THEN '未设置(默认允许)'
ELSE '未知'
END AS pricing_flag_desc
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
AND (pricing_flag = 1 OR pricing_flag IS NULL) -- 之前过滤条件
ORDER BY id;
-- 3. 查看不允许划价的项目pricing_flag = 0
SELECT
id,
bus_no,
name AS activity_name,
pricing_flag,
status_enum,
org_id,
category_code,
type_enum,
description_text,
'不允许划价' AS pricing_flag_desc
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
AND pricing_flag = 0 -- 这些项目之前不会显示
ORDER BY id;
-- 4. 查看未设置 pricing_flag 的项目NULL
SELECT
id,
bus_no,
name AS activity_name,
pricing_flag,
status_enum,
org_id,
category_code,
type_enum,
'未设置(默认允许)' AS pricing_flag_desc
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
AND pricing_flag IS NULL
ORDER BY id;
-- 5. 按科室统计 pricing_flag 分布
SELECT
org_id,
COUNT(*) AS total_count,
COUNT(CASE WHEN pricing_flag = 1 THEN 1 END) AS flag_1_count,
COUNT(CASE WHEN pricing_flag = 0 THEN 1 END) AS flag_0_count,
COUNT(CASE WHEN pricing_flag IS NULL THEN 1 END) AS flag_null_count
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
GROUP BY org_id
ORDER BY org_id;
-- 6. 按类别统计 pricing_flag 分布
SELECT
category_code,
COUNT(*) AS total_count,
COUNT(CASE WHEN pricing_flag = 1 THEN 1 END) AS flag_1_count,
COUNT(CASE WHEN pricing_flag = 0 THEN 1 END) AS flag_0_count,
COUNT(CASE WHEN pricing_flag IS NULL THEN 1 END) AS flag_null_count
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
GROUP BY category_code
ORDER BY category_code;
-- 7. 检查是否有费用定价关联的项目,但 pricing_flag = 0
SELECT
T1.id,
T1.bus_no,
T1.name AS activity_name,
T1.pricing_flag,
T2.id AS charge_item_definition_id,
T2.charge_name,
T2.price,
'有费用定价但不允许划价' AS issue_desc
FROM wor_activity_definition AS T1
LEFT JOIN adm_charge_item_definition AS T2
ON T2.instance_id = T1.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
AND T2.instance_table = 'wor_activity_definition'
WHERE T1.delete_flag = '0'
AND T1.status_enum = 1
AND T1.pricing_flag = 0
AND T2.id IS NOT NULL -- 有关联的费用定价
ORDER BY T1.id;
-- 8. 检查没有费用定价关联的项目,但 pricing_flag = 1
SELECT
T1.id,
T1.bus_no,
T1.name AS activity_name,
T1.pricing_flag,
T2.id AS charge_item_definition_id,
'允许划价但没有费用定价' AS issue_desc
FROM wor_activity_definition AS T1
LEFT JOIN adm_charge_item_definition AS T2
ON T2.instance_id = T1.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
AND T2.instance_table = 'wor_activity_definition'
WHERE T1.delete_flag = '0'
AND T1.status_enum = 1
AND (T1.pricing_flag = 1 OR T1.pricing_flag IS NULL)
AND T2.id IS NULL -- 没有关联的费用定价
ORDER BY T1.id;

View File

@@ -0,0 +1,86 @@
-- ============================================
-- 快速检查 pricing_flag = '0' 的数据问题
-- 注意pricing_flag 是 char(1) 类型,所以要用字符串 '0'
-- ============================================
-- 问题1检查是否有费用定价但 pricing_flag = '0' 的项目(可能异常)
SELECT
'异常:有费用定价但不允许划价' AS issue_type,
T1.id,
T1.bus_no,
T1.name AS activity_name,
T1.pricing_flag,
T2.charge_name,
T2.price
FROM wor_activity_definition AS T1
INNER JOIN adm_charge_item_definition AS T2
ON T2.instance_id = T1.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
AND T2.instance_table = 'wor_activity_definition'
WHERE T1.delete_flag = '0'
AND T1.status_enum = 1
AND T1.pricing_flag = '0' -- 不允许划价,但有费用定价(矛盾)
ORDER BY T1.id;
-- 问题2检查是否已被使用但 pricing_flag = '0' 的项目(可能异常)
SELECT
'异常:已有收费记录但不允许划价' AS issue_type,
T1.id,
T1.bus_no,
T1.name AS activity_name,
T1.pricing_flag,
COUNT(DISTINCT T3.encounter_id) AS encounter_count,
COUNT(T3.id) AS charge_item_count
FROM wor_activity_definition AS T1
INNER JOIN adm_charge_item AS T3
ON T3.product_id = T1.id
AND T3.context_enum = 3 -- ACTIVITY
AND T3.delete_flag = '0'
WHERE T1.delete_flag = '0'
AND T1.pricing_flag = '0'
GROUP BY T1.id, T1.bus_no, T1.name, T1.pricing_flag
ORDER BY charge_item_count DESC;
-- 问题3查看所有 pricing_flag = '0' 的项目,判断哪些可能应该改为 '1'
SELECT
T1.id,
T1.bus_no,
T1.name AS activity_name,
T1.category_code,
T1.pricing_flag,
T1.status_enum,
CASE
WHEN T2.id IS NOT NULL THEN '有费用定价'
ELSE '无费用定价'
END AS has_charge_definition,
CASE
WHEN EXISTS (
SELECT 1 FROM adm_charge_item
WHERE product_id = T1.id
AND context_enum = 3
AND delete_flag = '0'
) THEN '已被使用'
ELSE '未使用'
END AS usage_status,
CASE
WHEN T2.id IS NOT NULL OR EXISTS (
SELECT 1 FROM adm_charge_item
WHERE product_id = T1.id
AND context_enum = 3
AND delete_flag = '0'
) THEN '建议改为 pricing_flag = ''1'''
ELSE '保持 pricing_flag = ''0''(可能正常)'
END AS suggestion
FROM wor_activity_definition AS T1
LEFT JOIN adm_charge_item_definition AS T2
ON T2.instance_id = T1.id
AND T2.delete_flag = '0'
AND T2.status_enum = 1
AND T2.instance_table = 'wor_activity_definition'
WHERE T1.delete_flag = '0'
AND T1.status_enum = 1
AND T1.pricing_flag = '0'
ORDER BY T1.id;

View File

@@ -0,0 +1,123 @@
-- ============================================
-- 将 wor_activity_definition 表中的一两条记录的 pricing_flag 改成 '1'
-- 注意pricing_flag 是 char(1) 类型,所以要用字符串 '1'
-- ============================================
-- 方式1更新前两条 pricing_flag = '0' 的记录(推荐)
UPDATE wor_activity_definition
SET pricing_flag = '1',
update_time = CURRENT_TIMESTAMP
WHERE id IN (
SELECT id
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '0'
AND status_enum = 1 -- 只更新激活状态的项目
ORDER BY id
LIMIT 2
);
-- 方式2更新指定 ID 的记录(更精确,推荐使用)
-- 请将下面的 ID 替换为实际要更新的记录 ID
UPDATE wor_activity_definition
SET pricing_flag = '1',
update_time = CURRENT_TIMESTAMP
WHERE id IN (1906532604116348929, 1906544768434053121) -- 替换为实际的 ID
AND delete_flag = '0';
-- 方式3更新前两条记录不管当前 pricing_flag 值是什么)
UPDATE wor_activity_definition
SET pricing_flag = '1',
update_time = CURRENT_TIMESTAMP
WHERE id IN (
SELECT id
FROM wor_activity_definition
WHERE delete_flag = '0'
AND status_enum = 1
ORDER BY id
LIMIT 2
);
-- 方式4更新有费用定价但 pricing_flag = '0' 的前两条记录(业务逻辑更合理)
UPDATE wor_activity_definition
SET pricing_flag = '1',
update_time = CURRENT_TIMESTAMP
WHERE id IN (
SELECT wad.id
FROM wor_activity_definition wad
INNER JOIN adm_charge_item_definition acid
ON acid.instance_id = wad.id
AND acid.delete_flag = '0'
AND acid.status_enum = 1
AND acid.instance_table = 'wor_activity_definition'
WHERE wad.delete_flag = '0'
AND wad.status_enum = 1
AND wad.pricing_flag = '0'
ORDER BY wad.id
LIMIT 2
);
-- ============================================
-- 执行前可以先查询要更新的记录(验证用)
-- ============================================
-- 查询前两条 pricing_flag = '0' 的记录
SELECT
id,
bus_no,
name AS activity_name,
pricing_flag,
status_enum,
create_time,
update_time
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '0'
AND status_enum = 1
ORDER BY id
LIMIT 2;
-- 查询有费用定价但 pricing_flag = '0' 的记录
SELECT
wad.id,
wad.bus_no,
wad.name AS activity_name,
wad.pricing_flag,
acid.charge_name,
acid.price
FROM wor_activity_definition wad
INNER JOIN adm_charge_item_definition acid
ON acid.instance_id = wad.id
AND acid.delete_flag = '0'
AND acid.status_enum = 1
AND acid.instance_table = 'wor_activity_definition'
WHERE wad.delete_flag = '0'
AND wad.status_enum = 1
AND wad.pricing_flag = '0'
ORDER BY wad.id
LIMIT 2;
-- ============================================
-- 执行后验证更新结果
-- ============================================
-- 验证更新是否成功
SELECT
id,
bus_no,
name AS activity_name,
pricing_flag,
update_time
FROM wor_activity_definition
WHERE id IN (
-- 这里放刚才更新的 ID或者用子查询
SELECT id
FROM wor_activity_definition
WHERE delete_flag = '0'
AND pricing_flag = '1'
AND status_enum = 1
ORDER BY update_time DESC
LIMIT 2
);

View File

@@ -0,0 +1,86 @@
-- 快速诊断诊疗项目检索问题
-- 已知wor_activity_definition 表中有4条数据
-- 关键ChargeItemContext.ACTIVITY.getValue() = 3整数
-- 步骤1检查这4条诊疗项目定义的delete_flag状态
SELECT
id,
name,
delete_flag,
CASE WHEN delete_flag = '0' THEN '✅ 可用' ELSE '❌ 已删除' END as status
FROM wor_activity_definition
ORDER BY id;
-- 步骤2检查收费项目中是否有诊疗项目类型context_enum = 3的记录
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN delete_flag = '0' THEN 1 END) as active_count
FROM adm_charge_item
WHERE (context_enum::text = '3' OR context_enum = 3);
-- 步骤3检查收费项目能否匹配到诊疗项目定义最关键
SELECT
aci.id as charge_item_id,
aci.encounter_id,
aci.product_id,
aci.status_enum,
wad.id as activity_def_id,
wad.name as activity_name,
CASE
WHEN wad.id IS NULL THEN '❌ 无法匹配product_id=' || aci.product_id || ' 在诊疗项目定义表中不存在'
WHEN wad.delete_flag != '0' THEN '❌ 无法匹配:诊疗项目定义已删除'
WHEN aci.delete_flag != '0' THEN '❌ 无法匹配:收费项目已删除'
WHEN aci.status_enum NOT IN (1,2,3,4,5,6) THEN '❌ 无法匹配:状态不在查询范围内 status=' || aci.status_enum
ELSE '✅ 可以匹配'
END as match_result
FROM adm_charge_item aci
LEFT JOIN wor_activity_definition wad
ON aci.product_id = wad.id AND wad.delete_flag = '0'
WHERE (aci.context_enum::text = '3' OR aci.context_enum = 3)
AND aci.delete_flag = '0'
LIMIT 50;
-- 步骤4统计匹配情况
SELECT
COUNT(*) as total_charge_items,
COUNT(CASE WHEN wad.id IS NOT NULL AND wad.delete_flag = '0' THEN 1 END) as matched_count,
COUNT(CASE WHEN wad.id IS NULL THEN 1 END) as unmatched_def_count,
COUNT(CASE WHEN wad.delete_flag != '0' THEN 1 END) as deleted_def_count,
COUNT(CASE WHEN wad.id IS NOT NULL AND wad.delete_flag = '0' AND aci.status_enum IN (1,2,3,4,5,6) THEN 1 END) as final_matched_count
FROM adm_charge_item aci
LEFT JOIN wor_activity_definition wad
ON aci.product_id = wad.id
WHERE (aci.context_enum::text = '3' OR aci.context_enum = 3)
AND aci.delete_flag = '0';
-- 步骤5检查具体就诊的诊疗项目如果有encounterId取消注释并替换
-- SELECT
-- T1.encounter_id,
-- T1.id as charge_item_id,
-- T1.product_id,
-- T1.status_enum,
-- T2.id as activity_def_id,
-- T2.name as activity_name,
-- CASE
-- WHEN T2.id IS NULL THEN '❌ 无法匹配'
-- WHEN T2.delete_flag != '0' THEN '❌ 定义已删除'
-- WHEN T1.status_enum NOT IN (1,2,3,4,5,6) THEN '❌ 状态不符合'
-- ELSE '✅ 正常'
-- END as status
-- FROM adm_charge_item AS T1
-- LEFT JOIN wor_activity_definition AS T2
-- ON T1.product_id = T2.id AND T2.delete_flag = '0'
-- WHERE T1.encounter_id = :encounterId -- 替换为实际的encounterId
-- AND (T1.context_enum::text = '3' OR T1.context_enum = 3)
-- AND T1.delete_flag = '0';

View File

@@ -0,0 +1,165 @@
# 诊断:门诊划价检索不出诊疗项目问题
## 问题描述
在门诊划价页面,检索不出诊疗项目,显示"暂无数据"。
## 问题分析
### SQL查询逻辑
根据 `OutpatientChargeAppMapper.xml` 中的 `selectEncounterPatientPrescription` 查询:
1. **查询主表**`adm_charge_item`(收费项目表)
2. **关联诊疗项目定义表**`wor_activity_definition`
- 关联条件:`T1.context_enum = #{activity}` AND `T1.product_id = T2.id` AND `T2.delete_flag = '0'`
3. **返回字段**`item_name` 来自 `wor_activity_definition.name`
### 可能的原因
#### 1. 数据库中没有诊疗项目定义数据
- **检查**`wor_activity_definition` 表中是否有数据
- **SQL**
```sql
SELECT COUNT(*) FROM wor_activity_definition WHERE delete_flag = '0';
```
#### 2. 收费项目中的product_id无法匹配到诊疗项目定义
- **检查**`adm_charge_item` 表中的 `product_id` 是否存在于 `wor_activity_definition` 表中
- **SQL**
```sql
SELECT
aci.id,
aci.encounter_id,
aci.product_id,
aci.context_enum,
wad.id as activity_def_id,
wad.name as activity_name
FROM adm_charge_item aci
LEFT JOIN wor_activity_definition wad ON aci.product_id = wad.id AND wad.delete_flag = '0'
WHERE aci.context_enum = 'ACTIVITY'
AND aci.delete_flag = '0'
AND wad.id IS NULL; -- 无法匹配的记录
```
#### 3. 诊疗项目定义被标记为删除
- **检查**`wor_activity_definition` 表中的 `delete_flag` 是否为 '0'
- **SQL**
```sql
SELECT
id,
name,
delete_flag
FROM wor_activity_definition
WHERE delete_flag != '0';
```
#### 4. context_enum 值不匹配
- **检查**`adm_charge_item` 表中的 `context_enum` 值是否正确
- **SQL**
```sql
SELECT DISTINCT context_enum FROM adm_charge_item WHERE delete_flag = '0';
```
- **注意**:后端代码中传入的 `#{activity}` 参数值应该是 `ChargeItemContext.ACTIVITY.getValue()`
## 诊断步骤
### 步骤1检查诊疗项目定义表是否有数据
```sql
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN delete_flag = '0' THEN 1 END) as active_count,
COUNT(CASE WHEN delete_flag != '0' THEN 1 END) as deleted_count
FROM wor_activity_definition;
```
**预期结果**`active_count` 应该 > 0
### 步骤2检查收费项目中的诊疗项目是否能匹配到定义
```sql
SELECT
COUNT(*) as total_charge_items,
COUNT(CASE WHEN wad.id IS NOT NULL THEN 1 END) as matched_count,
COUNT(CASE WHEN wad.id IS NULL THEN 1 END) as unmatched_count
FROM adm_charge_item aci
LEFT JOIN wor_activity_definition wad
ON aci.context_enum = 'ACTIVITY'
AND aci.product_id = wad.id
AND wad.delete_flag = '0'
WHERE aci.context_enum = 'ACTIVITY'
AND aci.delete_flag = '0';
```
**预期结果**`matched_count` 应该 > 0`unmatched_count` 应该 = 0
### 步骤3检查具体就诊的诊疗项目
```sql
-- 替换 :encounterId 为实际的就诊ID
SELECT
T1.encounter_id,
T1.id as charge_item_id,
T1.context_enum,
T1.product_id,
T1.status_enum,
T2.id as activity_def_id,
T2.name as activity_name,
T2.delete_flag as activity_delete_flag,
CASE
WHEN T2.id IS NULL THEN '诊疗项目定义不存在或已删除'
WHEN T2.delete_flag != '0' THEN '诊疗项目定义已删除'
ELSE '正常'
END as status
FROM adm_charge_item AS T1
LEFT JOIN wor_activity_definition AS T2
ON T1.context_enum = 'ACTIVITY'
AND T1.product_id = T2.id
WHERE T1.encounter_id = :encounterId -- 替换为实际的encounterId
AND T1.context_enum = 'ACTIVITY'
AND T1.delete_flag = '0'
AND T1.status_enum IN (1, 2, 3, 4, 5, 6);
```
### 步骤4检查后端传入的参数值
检查 `OutpatientChargeAppServiceImpl.java` 中:
```java
ChargeItemContext.ACTIVITY.getValue()
```
确认这个值是什么(应该是 'ACTIVITY' 或对应的枚举值)
## 解决方案
### 如果是数据问题:
1. **缺少诊疗项目定义数据**
- 需要在 `wor_activity_definition` 表中添加诊疗项目数据
- 或者在系统管理-目录管理-诊疗项目中添加
2. **product_id无法匹配**
- 检查 `adm_charge_item` 表中的 `product_id` 是否正确
- 检查 `wor_activity_definition` 表中的 `id` 是否与 `product_id` 匹配
3. **delete_flag不正确**
-`wor_activity_definition` 表中需要使用的记录的 `delete_flag` 设置为 '0'
### 如果是代码问题:
1. **context_enum值不匹配**
- 检查后端代码中 `ChargeItemContext.ACTIVITY.getValue()` 返回的值
- 确保与数据库中的 `context_enum` 值一致
2. **SQL查询条件错误**
- 检查 SQL 中的关联条件是否正确
- 检查是否有其他过滤条件导致数据被过滤掉
## 快速诊断SQL
运行以下SQL可以快速诊断问题
```sql
-- 见 diagnose_treatment_items_issue.sql 文件
```

View File

@@ -33,18 +33,32 @@
:class="{ actived: activeCardId === item.encounterId }"
>
<div class="patient-card-header">
<div class="header-top">
<div class="bed-container">
<div class="bed">
<div class="bed-info">
<div v-if="item.houseName" class="house-name">{{ item.houseName }}</div>
<div class="bed-name">{{ item.bedName || '未分床' }}</div>
</div>
</div>
<!-- 第1行姓名 -->
<div class="info-row name-row">
<div class="name">
<el-text :text="item.patientName" tclass="name" width="auto">
{{ item.patientName || '-' }}
</el-text>
</div>
<el-space>
</div>
<!-- 第2行性别 年龄 + 入院状态 -->
<div class="info-row gender-age-row">
<div class="age">
<el-tag
size="small"
class="age-tag"
effect="plain"
:class="{ 'age-tag-female': item.genderEnum_enumText === '女性' }"
>
{{ item.genderEnum_enumText || '-' }}
<span v-if="item.age"> · {{ item.age }}</span>
</el-tag>
</div>
<!-- 入院状态放在性别年龄旁边 -->
<div class="status-inline" v-if="item.statusEnum_enumText">
<el-tag
v-if="item.statusEnum_enumText"
size="small"
class="payer-tag-status"
effect="light"
@@ -52,22 +66,31 @@
>
{{ item.statusEnum_enumText }}
</el-tag>
<el-tag
v-if="item.contractName"
size="small"
class="payer-tag"
effect="light"
>
{{ item.contractName }}
</el-tag>
</el-space>
</div>
</div>
<div class="header-bottom">
<!-- 第3行房间号-分床状态 -->
<div class="info-row room-bed-row">
<div class="bed-info">
<div v-if="item.houseName" class="house-name">{{ item.houseName }}</div>
<div class="bed-name">{{ item.bedName || '未分床' }}</div>
</div>
</div>
<!-- 第4行住院号 -->
<div class="info-row busno-row">
<span class="bus-no">住院号{{ item.busNo || '-' }}</span>
<span class="insurance-type" v-if="item.insutype_dictText">
险种类型{{ item.insutype_dictText }}
</span>
</div>
<!-- 第5行居民保险类型 -->
<div class="info-row insurance-row" v-if="item.contractName">
<el-tag
size="small"
class="payer-tag"
effect="light"
>
{{ item.contractName }}
</el-tag>
</div>
</div>
@@ -360,7 +383,7 @@ watch(
<style lang="scss" scoped>
.patient-card {
width: 100%;
overflow: hidden;
overflow: visible; /* 改为visible以确保内容可见 */
background-color: #fff;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.04);
@@ -383,77 +406,128 @@ watch(
display: flex;
flex-direction: column;
padding: 10px 12px 4px;
overflow: visible; /* 确保内容可见 */
gap: 4px; /* 行与行之间的间距 */
.header-top {
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.bed-container {
display: flex;
flex: 1;
&.room-bed-row {
align-items: center;
min-width: 0;
font-weight: 600;
font-size: 16px;
color: #1f2933;
.bed {
flex-grow: 0;
flex-shrink: 1;
min-width: 0;
.bed-info {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
min-width: 0; /* 允许收缩 */
.bed-info {
display: flex;
flex-direction: column;
line-height: 1.4;
.house-name {
color: #1f2933;
font-weight: 600;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.house-name {
color: #1f2933;
font-weight: 600;
font-size: 16px;
white-space: normal;
word-break: break-all;
}
.bed-name {
color: #1f2933;
font-weight: 600;
font-size: 16px;
white-space: normal;
word-break: break-all;
}
.bed-name {
color: #1f2933;
font-weight: 600;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.payer-tag {
max-width: 120px;
font-size: 12px;
border-radius: 999px;
font-weight: bolder;
&.insurance-row {
align-items: center;
margin-left: 8px; /* 添加一点左边距,与第一行对齐 */
}
.payer-tag-status {
font-weight: bolder;
border-radius: 999px;
&.busno-row {
color: #6b7280;
font-size: 12px;
align-items: center;
.bus-no {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&.name-row {
align-items: center;
color: #111827;
.name {
color: #111827;
font-weight: 700 !important; /* 更粗的字体使用important确保优先级 */
font-size: 18px !important; /* 更大的字体使用important确保优先级 */
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
:deep(.el-text),
:deep(.el-text__inner) {
font-weight: 700 !important; /* 确保el-text组件也应用粗体 */
font-size: 18px !important; /* 确保el-text组件也应用更大字体 */
}
}
}
&.gender-age-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.age {
flex-shrink: 0;
.age-tag {
border-radius: 999px;
padding: 0 8px;
}
.age-tag-female {
border-color: rgb(255, 55, 158);
color: rgb(255, 126, 184);
}
}
.status-inline {
flex-shrink: 0;
margin-left: 8px;
}
}
}
.header-bottom {
margin-top: 4px;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
color: #6b7280;
.payer-tag {
max-width: 100%;
font-size: 12px;
border-radius: 999px;
font-weight: bolder;
}
.payer-tag-status {
font-weight: bolder;
border-radius: 999px;
}
.bus-no {
white-space: nowrap;
}
.insurance-type {
white-space: nowrap;
}
/* 隐藏旧的布局结构因为已被新的info-row结构替代 */
.header-top,
.header-bottom,
.bed-container,
.tags-container {
display: none;
}
}
@@ -476,6 +550,7 @@ watch(
justify-content: space-between;
gap: 8px;
width: 100%;
display: none; /* 新的布局中name-container已在header中显示这里隐藏 */
.name {
color: #111827;
@@ -522,15 +597,16 @@ watch(
height: 100%;
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid #ebeef5;
background-color: #ffffff;
width: 240px;
min-width: 240px;
&-unexpand {
width: 84px;
min-width: 84px;
}
.patientList-operate {
display: flex;
align-items: center;
@@ -551,22 +627,14 @@ watch(
flex-direction: column;
height: 0;
width: 240px;
&-unexpand {
width: 84px;
}
.search-operate {
padding: 0 8px;
height: 48px;
display: flex;
align-items: center;
flex: none;
}
.patient-cards {
flex: 1;
padding: 0 8px;
overflow: hidden;
:deep(.patient-cards-scrollbar) {
@@ -610,6 +678,7 @@ watch(
display: flex;
justify-content: flex-end;
padding: 4px 16px 8px;
&-unexpand {
justify-content: center;
}

View File

@@ -187,6 +187,7 @@ const getStatusClass = (item: any) => {
align-items: center;
justify-content: space-between;
width: 100%;
flex-wrap: nowrap; /* 防止换行 */
.bed-container {
display: flex;
@@ -216,9 +217,10 @@ const getStatusClass = (item: any) => {
.header-tags {
flex-shrink: 0;
display: flex;
align-items: flex-end;
align-items: center;
justify-content: flex-end;
gap: 6px;
flex-wrap: nowrap; /* 防止换行 */
}
}

View File

@@ -183,6 +183,23 @@
prop="statusEnum_enumText"
:show-overflow-tooltip="true"
/>
<el-table-column
label="划价标记"
align="center"
key="pricingFlag_enumText"
prop="pricingFlag_enumText"
:show-overflow-tooltip="true"
width="100"
>
<template #default="scope">
<el-tag
:type="scope.row.pricingFlag === 1 ? 'success' : scope.row.pricingFlag === 0 ? 'danger' : 'info'"
size="small"
>
{{ scope.row.pricingFlag_enumText || (scope.row.pricingFlag === 1 ? '允许' : scope.row.pricingFlag === 0 ? '不允许' : '未设置') }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"

View File

@@ -8,6 +8,7 @@
@current-change="handleCurrentChange"
row-key="patientId"
@cell-click="clickRow"
@row-click="clickRow"
>
<el-table-column label="名称" align="center" prop="adviceName" />
<el-table-column label="类型" align="center" prop="activityType_enumText" />
@@ -38,6 +39,10 @@ const props = defineProps({
type: Object,
required: true,
},
popoverVisible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['selectAdviceBase']);
const total = ref(0);
@@ -61,30 +66,80 @@ const throttledGetList = throttle(
watch(
() => props.adviceQueryParams,
(newValue) => {
queryParams.value.searchKey = newValue.searchKey;
queryParams.value.adviceType = newValue.adviceType;
// 只有在弹窗打开时才响应 adviceQueryParams 的变化,避免选择项目后弹窗关闭时触发不必要的请求
if (!props.popoverVisible) {
return;
}
queryParams.value.searchKey = newValue?.searchKey;
queryParams.value.adviceType = newValue?.adviceType;
throttledGetList();
},
{ deep: true }
);
getList();
// 监听弹窗打开状态,当弹窗打开时主动加载数据
watch(
() => props.popoverVisible,
(visible) => {
if (visible) {
// 弹窗打开时,确保 adviceQueryParams 同步到 queryParams
if (props.adviceQueryParams) {
queryParams.value.searchKey = props.adviceQueryParams.searchKey;
queryParams.value.adviceType = props.adviceQueryParams.adviceType;
}
// 主动触发数据加载
getList();
} else {
// 弹窗关闭时,清空列表数据,避免显示错误的数据
adviceBaseList.value = [];
total.value = 0;
}
}
);
// 移除组件初始化时的 getList() 调用,避免在没有 adviceType 时查询所有类型的数据
// getList();
function getList() {
// 验证是否已选择患者
if (!props.patientInfo || Object.keys(props.patientInfo).length === 0) {
console.log('[adviceBaseList] getList() 跳过:未选择患者');
return; // 不执行API调用
}
// 只有在弹窗打开时才执行查询
if (!props.popoverVisible) {
console.log('[adviceBaseList] getList() 跳过:弹窗未打开');
return;
}
// 必须有 adviceType 才查询,避免查询所有类型的数据
if (!queryParams.value.adviceType) {
console.log('[adviceBaseList] getList() 跳过adviceType 未设置,当前值:', queryParams.value.adviceType);
return;
}
queryParams.value.organizationId = props.patientInfo.orgId;
console.log('[adviceBaseList] getList() 请求参数:', JSON.stringify(queryParams.value));
getAdviceBaseInfo(queryParams.value).then((res) => {
adviceBaseList.value = res.data.records;
total.value = res.data.total;
console.log('[adviceBaseList] getList() 响应数据:', {
total: res.data?.total,
recordsCount: res.data?.records?.length || 0,
firstRecord: res.data?.records?.[0]?.adviceName || '无数据',
adviceType: queryParams.value.adviceType
});
adviceBaseList.value = res.data.records || [];
total.value = res.data.total || 0;
nextTick(() => {
currentIndex.value = 0;
if (adviceBaseList.value.length > 0) {
adviceBaseRef.value.setCurrentRow(adviceBaseList.value[0]);
adviceBaseRef.value?.setCurrentRow(adviceBaseList.value[0]);
}
});
}).catch((err) => {
console.error('[adviceBaseList] getList() 请求失败:', err);
adviceBaseList.value = [];
total.value = 0;
});
}
@@ -136,8 +191,12 @@ const handleCurrentChange = (currentRow) => {
currentSelectRow.value = currentRow;
};
function clickRow(row) {
emit('selectAdviceBase', row);
function clickRow(row, column, cell, event) {
// cell-click 事件会传递 row, column, cell, event 四个参数
// 确保传递的是完整的行数据
if (row) {
emit('selectAdviceBase', row);
}
}
defineExpose({

View File

@@ -301,6 +301,7 @@ const nextId = ref(1);
const unitCodeList = ref([]);
const adviceTableRef = ref([]);
const organization = ref([]);
const orgTreeLoaded = ref(false);
const rowRules = ref({
conditionDefinitionId: [{ required: true, message: '请选择诊断', trigger: 'change' }],
dose: [{ required: true, message: '请输入单次剂量', trigger: 'change' }],
@@ -449,6 +450,10 @@ function handleDiagnosisChange(item, row) {
function handleFocus(row, index) {
rowIndex.value = index;
// 打开当前行弹窗前,先关闭其它行,避免多个弹窗同时存在
prescriptionList.value.forEach((r, i) => {
if (i !== index) r.showPopover = false;
});
// 如果当前行已选择adviceType同步到adviceQueryParams
if (row.adviceType !== undefined) {
adviceQueryParams.value.adviceType = row.adviceType;
@@ -457,7 +462,9 @@ function handleFocus(row, index) {
}
function handleBlur(row) {
row.showPopover = false;
// 不能在 input blur 时立刻关闭弹窗:
// 点击弹窗里的表格会先触发 blur导致弹窗瞬间关闭从而“点了项目没反应”
// 弹窗关闭交给 selectAdviceBase()(选中后关闭)以及 handleFocus()(切行时关闭其他行)
}
function handleChange(value) {
@@ -465,10 +472,37 @@ function handleChange(value) {
}
/**
* 选择药品回调
* 选择药品/诊疗项目回调
* 这里恢复为之前“能正常工作”的简单逻辑,只做最小必要的修正
*/
function selectAdviceBase(key, row) {
getOrgList();
if (!row) {
console.error('[selectAdviceBase] row 为空');
return;
}
// rowIndex 理论上由 handleFocus 设置;防御一下越界
if (rowIndex.value < 0 || rowIndex.value >= prescriptionList.value.length) {
const foundIndex = prescriptionList.value.findIndex((item) => item.uniqueKey === key);
if (foundIndex === -1) {
console.error('[selectAdviceBase] 找不到对应行key =', key);
return;
}
rowIndex.value = foundIndex;
}
// 关闭当前行弹窗
const currentRow = prescriptionList.value[rowIndex.value];
if (currentRow) {
currentRow.showPopover = false;
}
// 诊疗(adviceType=3) 才需要加载执行科室树,且只加载一次
if (row.adviceType === 3) {
ensureOrgTreeLoaded();
}
// 构建单位列表(保持原有逻辑)
unitCodeList.value = [];
unitCodeList.value.push({ value: row.unitCode, label: row.unitCode_dictText, type: 'unit' });
if (row.doseUnitCode != row.minUnitCode) {
@@ -488,10 +522,14 @@ function selectAdviceBase(key, row) {
type: 'minUnit',
});
}
// 将选中的基础项“覆盖”到当前处方行(这是之前正常工作的核心逻辑)
prescriptionList.value[rowIndex.value] = {
...prescriptionList.value[rowIndex.value],
...JSON.parse(JSON.stringify(row)),
};
// 后续字段处理保持原样
prescriptionList.value[rowIndex.value].orgId = undefined;
prescriptionList.value[rowIndex.value].dose = undefined;
prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value;
@@ -500,12 +538,11 @@ function selectAdviceBase(key, row) {
prescriptionList.value[rowIndex.value].minUnitCode = JSON.parse(JSON.stringify(row.doseUnitCode));
prescriptionList.value[rowIndex.value].unitCode =
row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
// prescriptionList.value[rowIndex.value].doseUnitCode_dictText = row.minUnitCode_dictText;
prescriptionList.value[rowIndex.value].definitionId = JSON.parse(
JSON.stringify(row)
).chargeItemDefinitionId;
// 库存列表 + 价格列表拼成批次号的下拉框
// 库存列表 + 价格列表拼成批次号的下拉框(非诊疗)
if (row.adviceType != 3) {
if (row.inventoryList && row.inventoryList.length == 0) {
expandOrder.value = [];
@@ -532,9 +569,15 @@ function selectAdviceBase(key, row) {
prescriptionList.value[rowIndex.value].positionName = stock.locationName;
}
} else {
// 诊疗:设置执行科室和价格
prescriptionList.value[rowIndex.value].orgId = JSON.parse(JSON.stringify(row)).positionId;
prescriptionList.value[rowIndex.value].unitPrice = row.priceList[0].price;
if (row.priceList && row.priceList.length > 0) {
prescriptionList.value[rowIndex.value].unitPrice = row.priceList[0].price;
} else {
prescriptionList.value[rowIndex.value].unitPrice = 0;
}
}
expandOrder.value = [key];
nextTick(() => {
if (row.adviceType == 1) {
@@ -549,11 +592,18 @@ function selectAdviceBase(key, row) {
});
}
function getOrgList() {
getOrgTree().then((res) => {
organization.value = res.data.records;
console.log(organization.value,"organization.value")
});
function ensureOrgTreeLoaded() {
if (orgTreeLoaded.value) return;
orgTreeLoaded.value = true;
getOrgTree()
.then((res) => {
// 组织机构树接口通常返回分页 records这里做兼容兜底
organization.value = res?.data?.records ?? res?.data ?? [];
})
.catch(() => {
// 加载失败时允许重试
orgTreeLoaded.value = false;
});
}
function handleDelete() {
@@ -706,6 +756,8 @@ function handleSaveSign(row, index) {
row.contentJson = JSON.stringify(row);
row.dbOpType = row.requestId ? '2' : '1';
row.minUnitQuantity = row.quantity * row.partPercent;
row.categoryEnum = row.adviceType
console.log('row', row)
savePrescription({ adviceSaveList: [row] }).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('保存成功');

View File

@@ -54,6 +54,17 @@ export function leaveEncounter(encounterId) {
});
}
/**
* 重新排序未到诊患者
*/
export const rearrangeMissedNumber = (encounterId) => {
return request({
url: '/doctor-station/main/rearrange-missed-encounter', // 对应Controller的路径
method: 'get', // 注意现有接口都是GET这里和后端保持一致
params: { encounterId } // GET请求用params传参
});
};
/**
* 完诊
*/

View File

@@ -0,0 +1,675 @@
<template>
<el-dialog :model-value="dialogVisible" @update:model-value="val => emit('update:dialogVisible', val)" title=""
width="90%" :close-on-click-modal="false" custom-class="call-dialog-custom">
<!-- 顶部标题栏 -->
<div class="dialog-header">
<div class="header-title">医生叫号界面 - {{ department }}</div>
<div class="header-time">{{ formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss') }}</div>
</div>
<!-- 当前就诊患者区域 -->
<div class="current-patient">
<div class="current-label">当前就诊患者</div>
<div class="current-info">
<div>
<span>患者姓名:</span>
<span class="info-value">{{ getPatientNo(currentCallPatient) }} {{ currentCallPatient.patientName || '暂无'
}}</span>
</div>
<div>
<span>诊室:</span>
<span class="info-value">{{ roomNo || '4号' }}</span>
</div>
</div>
</div>
<!-- 叫号按钮区 -->
<div class="call-buttons">
<el-button class="btn-next" @click="callNextPatient">下一患者</el-button>
<el-button class="btn-recall" @click="showRecallDialog">选呼</el-button>
<el-button class="btn-finish" @click="finishCallPatient">完成</el-button>
<el-button class="btn-skip" @click="skipPatient">跳过</el-button>
<el-button class="btn-requeue" @click="requeuePatient">过号重排</el-button>
<el-button class="btn-seen" @click="markSeenPatient">已就诊</el-button>
</div>
<!-- 选呼对话框 -->
<el-dialog v-model="recallDialogVisible" title="选择患者呼叫" width="600px" :close-on-click-modal="false">
<div style="padding: 20px 0;">
<el-select v-model="selectedPatientEncounterId" filterable remote reserve-keyword placeholder="搜索患者姓名身份证号或就诊ID"
:remote-method="remoteSearchPatient" :loading="patientSearchLoading" clearable
style="width: 100%; margin-bottom: 20px;">
<el-option v-for="patient in patientSearchOptions" :key="patient.encounterId"
:label="`${patient.patientName} (${patient.idCard || '无身份证号'})`" :value="patient.encounterId">
<div style="display: flex; justify-content: space-between;">
<span>{{ patient.patientName }}</span>
<span style="color: #8492a6; font-size: 14px;">{{ patient.idCard || '无身份证号' }}</span>
</div>
</el-option>
</el-select>
<div style="text-align: right;">
<el-button @click="recallDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmRecallPatient"
:disabled="!selectedPatientEncounterId">确定呼叫</el-button>
</div>
</div>
</el-dialog>
<!-- 候诊患者列表标题 -->
<div class="wait-list-title">候诊患者列表</div>
<!-- 候诊患者表格 -->
<el-table :data="sortedWaitPatientList" border style="width: 100%" header-cell-class-name="table-header"
:row-class-name="() => 'table-row'">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column prop="patientName" label="患者" width="180">
<template #default="scope">{{ scope.row.patientName }}</template>
</el-table-column>
<el-table-column prop="typeCode_dictText" label="号别" width="120" />
<el-table-column prop="organizationName" label="诊室" width="120" />
<el-table-column label="医生" width="120">
<template #default="scope">{{ scope.row.jz_practitioner_name || '心内科医生' }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default>等待中</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="scope">
<el-button size="small" class="btn-receive" @click="callThisPatient(scope.row)">
接诊
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 底部信息栏 -->
<div class="footer-info">
<div>当前时间{{ formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss') }}</div>
<div>当前号{{ getPatientNo(currentCallPatient) || '暂无' }}</div>
<div>等待人数{{ currentWaitPatientList.length }}</div>
</div>
</el-dialog>
</template>
<script setup>
// ✅ 核心修复h 从 vue 导入,而非 element-plus
import { ref, watch, onMounted, h, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { receiveEncounter, completeEncounter, leaveEncounter, rearrangeMissedNumber } from '../api.js';
// ===== 1. Props/Emits 定义 =====
const props = defineProps({
dialogVisible: Boolean,
currentPatient: { type: Object, default: () => ({}) },
currentPatientList: { type: Array, default: () => [] }, // 候诊列表
roomNo: { type: String, default: '4号' },
department: { type: String, default: '心内科' } // 科室名称,默认为心内科
});
const emit = defineEmits(['update:dialogVisible', 'callNext', 'reCall', 'finish', 'skip', 'requeue', 'markSeen', 'callThis']);
// ===== 2. 响应式变量 =====
const currentWaitPatientList = ref([]); // 候诊患者列表
const currentCallPatient = ref({}); // 当前就诊患者
const selectedId = ref(''); // 选呼患者ID
// 选呼功能相关变量
const selectedPatientEncounterId = ref('');
const patientSearchOptions = ref([]);
const patientSearchLoading = ref(false);
const patientSearchQuery = ref('');
const recallDialogVisible = ref(false); // 选呼对话框显示/隐藏
// ===== 3. 计算属性 =====
// 按创建时间(create_time)和过号时间(missed_time)排序后的候诊列表
const sortedWaitPatientList = computed(() => {
// 空列表直接返回
if (!currentWaitPatientList.value.length) return [];
// 调试:打印当前时间和排序前的患者列表
const now = new Date();
console.log('当前时间:', now);
console.log('排序前的患者列表:');
currentWaitPatientList.value.forEach(item => {
console.log(`患者 ${item.patientName}:`, {
encounterId: item.encounterId,
missed_time: item.missed_time,
missedTime: item.missedTime,
create_time: item.create_time,
createTime: item.createTime,
statusEnum: item.statusEnum,
registerTime: item.registerTime,
parsed_missed_time: item.missed_time ? new Date(item.missed_time) : null,
parsed_create_time: item.create_time ? new Date(item.create_time) : null,
parsed_registerTime: item.registerTime ? new Date(item.registerTime) : null
});
});
// 判断患者是否为过号患者
const isMissedPatient = (patient) => {
// 检查missed_time或missedTime是否存在且不为null
return patient.missed_time != null || patient.missedTime != null;
};
// 获取患者有效时间的辅助函数
const getPatientTime = (patient) => {
// 调试:打印患者的完整时间信息
console.log(`患者 ${patient.patientName} 的时间信息:`, {
missed_time: patient.missed_time,
missedTime: patient.missedTime,
registerTime: patient.registerTime,
create_time: patient.create_time,
createTime: patient.createTime
});
// 1. 优先使用missed_time过号时间同时处理null和undefined
if (patient.missed_time != null) {
console.log(`患者 ${patient.patientName} 使用missed_time: ${patient.missed_time}`);
return patient.missed_time;
}
if (patient.missedTime != null) {
console.log(`患者 ${patient.patientName} 使用missedTime: ${patient.missedTime}`);
return patient.missedTime;
}
// 2. 其次使用registerTime挂号时间
if (patient.registerTime != null) {
console.log(`患者 ${patient.patientName} 使用registerTime: ${patient.registerTime}`);
return patient.registerTime;
}
// 3. 最后使用create_time或createTime创建时间
if (patient.create_time != null) {
console.log(`患者 ${patient.patientName} 使用create_time: ${patient.create_time}`);
return patient.create_time;
}
if (patient.createTime != null) {
console.log(`患者 ${patient.patientName} 使用createTime: ${patient.createTime}`);
return patient.createTime;
}
// 4. 兜底使用当前时间减去1000年确保排到最前面
console.warn(`患者 ${patient.patientName} 没有有效时间字段,使用默认时间`);
return new Date(0); // 1970-01-01
};
// 核心改进:明确区分过号患者和未过号患者
// 1. 未过号患者排在过号患者前面
// 2. 未过号患者按照registerTime升序排列
// 3. 过号患者按照missed_time升序排列
const sortedList = [...currentWaitPatientList.value].sort((a, b) => {
const aIsMissed = isMissedPatient(a);
const bIsMissed = isMissedPatient(b);
// 调试:打印排序比较
console.log(`排序比较: ${a.patientName}(过号:${aIsMissed}) vs ${b.patientName}(过号:${bIsMissed})`);
// 1. 未过号患者排在过号患者前面
if (aIsMissed !== bIsMissed) {
const result = aIsMissed ? 1 : -1;
console.log(`比较结果: ${result} (${aIsMissed ? 'a是过号患者排后面' : 'b是过号患者排后面'})`);
return result;
}
// 2. 获取a和b的有效时间
const aTime = getPatientTime(a);
const bTime = getPatientTime(b);
// 解析时间确保正确处理UTC时间
const timeA = new Date(aTime);
const timeB = new Date(bTime);
// 确保时间有效
if (isNaN(timeA.getTime())) {
console.log(`患者 ${a.patientName} 时间无效,排后面`);
return 1; // 无效时间排后面
}
if (isNaN(timeB.getTime())) {
console.log(`患者 ${b.patientName} 时间无效,排后面`);
return -1; // 有效时间排前面
}
// 3. 同类型患者按照时间升序排列
const result = timeA - timeB;
console.log(`比较结果: ${result} (时间比较: ${timeA} vs ${timeB})`);
return result;
});
// 调试:打印排序后的患者列表
console.log('排序后的患者列表:');
sortedList.forEach((item, index) => {
const effectiveTime = getPatientTime(item);
const isMissed = isMissedPatient(item);
console.log(`${index + 1}. ${item.patientName}:`, {
is_missed: isMissed,
missed_time: item.missed_time,
registerTime: item.registerTime,
create_time: item.create_time || item.createTime,
effective_time: new Date(effectiveTime)
});
});
return sortedList;
});
// ===== 4. 临时 formatDate 函数(避免外部依赖报错)=====
const formatDate = (date, format = 'YYYY-MM-DD') => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hour = String(d.getHours()).padStart(2, '0');
const minute = String(d.getMinutes()).padStart(2, '0');
const second = String(d.getSeconds()).padStart(2, '0');
if (format === 'YYYY-MM-DD HH:mm:ss') {
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
return `${year}-${month}-${day}`;
};
// ===== 4. 监听 Props 同步数据 =====
watch(() => props.currentPatientList, (newVal) => {
currentWaitPatientList.value = newVal || [];
}, { immediate: true });
watch(() => props.currentPatient, (newVal) => {
currentCallPatient.value = newVal || {};
}, { immediate: true });
// ===== 5. 排队号计算 =====
const getPatientNo = (patient) => {
if (!patient || !patient.encounterId || sortedWaitPatientList.value.length === 0) return '';
const index = sortedWaitPatientList.value.findIndex(item => item.encounterId === patient.encounterId);
return index > -1 ? `${index + 1}` : '';
};
// ===== 6. 按钮核心逻辑 =====
// 6.1 下一患者
const callNextPatient = async () => {
if (sortedWaitPatientList.value.length === 0) {
ElMessage.warning('暂无候诊患者');
return;
}
// 获取第一个患者
const nextPatient = sortedWaitPatientList.value[0];
try {
// 调用接诊API
await receiveEncounter(nextPatient.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = nextPatient;
// 通知父组件
emit('callNext');
ElMessage.success(`已呼叫下一位患者:${nextPatient.patientName}`);
} catch (error) {
console.error('呼叫下一位患者失败:', error);
ElMessage.error(`呼叫下一位患者失败:${error.message || '系统错误'}`);
}
};
// 6.2 显示选呼对话框
const showRecallDialog = () => {
// 清空之前的选择
selectedPatientEncounterId.value = '';
patientSearchOptions.value = [];
// 显示对话框
recallDialogVisible.value = true;
};
// 6.3 远程搜索患者
const remoteSearchPatient = (query) => {
if (query === '') {
patientSearchOptions.value = [];
return;
}
patientSearchLoading.value = true;
// 模拟远程搜索,实际从候诊列表中查找
setTimeout(() => {
// 搜索匹配的患者 - 支持模糊搜索
patientSearchOptions.value = sortedWaitPatientList.value.filter(patient => {
// 患者姓名模糊匹配
const nameMatch = patient.patientName && patient.patientName.toLowerCase().includes(query.toLowerCase());
// 身份证号模糊匹配
const idCardMatch = patient.idCard && patient.idCard.includes(query);
// 就诊ID模糊匹配
const encounterIdMatch = patient.encounterId && patient.encounterId.toString().includes(query);
// 至少满足一个条件
return nameMatch || idCardMatch || encounterIdMatch;
});
patientSearchLoading.value = false;
}, 200);
};
// 6.4 确认呼叫患者
const confirmRecallPatient = async () => {
if (!selectedPatientEncounterId.value) return;
try {
// 查找选中的患者
const selectedPatient = sortedWaitPatientList.value.find(patient => patient.encounterId === selectedPatientEncounterId.value);
if (!selectedPatient) {
ElMessage.warning('未找到选中的患者');
return;
}
// 调用接诊API
await receiveEncounter(selectedPatient.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = selectedPatient;
// 通知父组件
emit('reCall');
// 关闭对话框
recallDialogVisible.value = false;
// 清空选择
selectedPatientEncounterId.value = '';
ElMessage.success(`已呼叫患者:${selectedPatient.patientName}`);
} catch (error) {
console.error('选呼患者失败:', error);
ElMessage.error(`选呼患者失败:${error.message || '系统错误'}`);
}
};
// 6.5 完成
const finishCallPatient = async () => {
if (!currentCallPatient.value.encounterId) {
ElMessage.warning('当前没有就诊患者');
return;
}
try {
await completeEncounter(currentCallPatient.value.encounterId);
emit('finish');
emit('update:dialogVisible', false);
ElMessage.success('患者已完诊');
} catch (error) {
console.error('完诊失败:', error);
ElMessage.error(`完诊失败:${error.message || '系统错误'}`);
}
};
// 6.6 跳过
const skipPatient = async () => {
if (sortedWaitPatientList.value.length === 0) {
ElMessage.warning('暂无候诊患者');
return;
}
// 获取第一个患者
const nextPatient = sortedWaitPatientList.value[0];
try {
// 调用接诊API
await receiveEncounter(nextPatient.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = nextPatient;
// 通知父组件
emit('skip');
ElMessage.success(`已跳过当前患者,呼叫下一位患者:${nextPatient.patientName}`);
} catch (error) {
console.error('跳过患者失败:', error);
ElMessage.error(`跳过患者失败:${error.message || '系统错误'}`);
}
};
// 6.7 过号重排
const requeuePatient = async () => {
// 1. 校验:当前必须有就诊患者才能重排
if (!currentCallPatient.value || !currentCallPatient.value.encounterId) {
ElMessage.warning('当前没有就诊患者,无法过号重排');
return;
}
try {
// 2. 显示确认对话框
await ElMessageBox.confirm(
`确定将患者 ${currentCallPatient.value.patientName} 进行过号重排吗?`,
'过号重排确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
draggable: true
}
);
// 3. 调用过号重排接口
console.log('开始调用过号重排接口患者ID:', currentCallPatient.value.encounterId);
const res = await rearrangeMissedNumber(currentCallPatient.value.encounterId);
// 4. 处理后端返回结果
console.log('过号重排接口返回结果:', res);
if (res.code === 200) {
// 5. 直接通知父组件刷新候诊列表,依赖父组件重新获取数据
emit('requeue');
// 6. 提示用户+清空当前就诊患者
ElMessage.success(res.msg || '过号重排成功,患者已排至队尾');
currentCallPatient.value = {};
} else {
// 7. 处理后端返回的错误信息
console.error('过号重排失败,后端返回错误:', res);
ElMessage.error(res.msg || '过号重排失败');
}
} catch (error) {
// 处理取消操作
if (error !== 'cancel') {
console.error('过号重排失败,捕获异常:', error);
ElMessage.error(`过号重排失败:${error.message || '系统错误'}`);
}
}
};
// 6.8 标记已就诊
const markSeenPatient = async () => {
if (!currentCallPatient.value.encounterId) {
ElMessage.warning('当前没有就诊患者');
return;
}
try {
await leaveEncounter(currentCallPatient.value.encounterId);
emit('markSeen');
emit('update:dialogVisible', false);
ElMessage.success('患者已暂离');
} catch (error) {
console.error('暂离失败:', error);
ElMessage.error(`暂离失败:${error.message || '系统错误'}`);
}
};
// 6.9 接诊指定患者
const callThisPatient = async (row) => {
try {
// 调用接诊API
await receiveEncounter(row.encounterId);
// 更新当前呼叫患者
currentCallPatient.value = row;
// 通知父组件
emit('callThis', row);
ElMessage.success(`已接诊患者:${row.patientName}`);
} catch (error) {
console.error('接诊患者失败:', error);
ElMessage.error(`接诊患者失败:${error.message || '系统错误'}`);
}
};
</script>
<style scoped>
.call-dialog-custom {
--el-dialog-body-padding: 0;
}
.dialog-header {
background-color: #0b8074;
color: #fff;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.header-title {
font-size: 24px;
font-weight: bold;
color: #fff;
}
.header-time {
font-size: 16px;
font-weight: normal;
background-color: rgba(255, 255, 255, 0.2);
padding: 8px 16px;
border-radius: 20px;
color: #fff;
}
.current-patient {
background-color: #f8fafc;
padding: 20px 20px;
margin-bottom: 20px;
border-radius: 8px;
}
.current-label {
font-size: 20px;
font-weight: 700;
color: #1f7a7a;
margin-bottom: 12px;
}
.current-info {
display: flex;
gap: 60px;
font-size: 18px;
}
.info-value {
font-weight: 800 !important;
color: #1f7a7a !important;
font-size: 26px !important;
background-color: #e6f7ff;
padding: 8px 16px;
border-radius: 8px;
margin-left: 8px;
display: inline-block;
min-width: 200px;
text-align: center;
}
.call-buttons {
padding: 0 20px 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn-next {
background-color: #10b981 !important;
border-color: #10b981 !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-recall {
background-color: #f59e0b !important;
border-color: #f59e0b !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-finish {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-skip {
background-color: #f5c518 !important;
border-color: #f5c518 !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-requeue {
background-color: #165dff !important;
border-color: #165dff !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-seen {
background-color: #86909c !important;
border-color: #86909c !important;
color: #fff !important;
font-size: 16px !important;
padding: 14px 24px !important;
font-weight: 600;
border-radius: 6px;
}
.btn-receive {
background-color: #10b981 !important;
border-color: #10b981 !important;
color: #fff !important;
font-size: 14px !important;
padding: 8px 12px !important;
border-radius: 4px;
}
.wait-list-title {
padding: 0 20px 12px;
font-size: 20px;
font-weight: 700;
color: #1f7a7a;
}
:deep(.table-header) {
background-color: #1f7a7a !important;
color: #fff !important;
font-weight: 700 !important;
font-size: 18px !important;
}
:deep(.table-row) {
font-size: 18px !important;
font-weight: 500;
}
:deep(.el-table__cell) {
padding: 16px 0 !important;
}
:deep(.el-table--enable-row-hover .el-table__body tr:hover>td) {
background-color: #f0f9ff !important;
}
.footer-info {
background-color: #1f7a7a;
color: #fff;
padding: 14px 20px;
margin-top: 20px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
</style>

View File

@@ -419,40 +419,10 @@ import {ElMessage, ElMessageBox} from 'element-plus'
import {deleteInspectionApplication, getInspectionApplicationList, saveInspectionApplication} from '../api'
import useUserStore from '@/store/modules/user.js'
import {storeToRefs} from 'pinia'
import {listType} from "@/api/system/dict/type.js";
// const getCurrentDeptCode = async () => {
// // try {
// const res = await listType({ dictType: 'dept' }); // 按字典类型查询科室
// console.log('【检验】获取科室字典数据:', res.data)
// deptOptions.value = res.data;
// console.log('【检验】科室字典数据:',deptOptions.value)
// // 获取当前科室名称(来自患者信息)
// const currentDeptName = props.patientInfo?.organizationName;
// console.log('【检验】当前科室名称:', currentDeptName)
//
// // 根据当前科室名称查找对应的编码
// const deptItem = res.data.find(item => item.dictLabel === currentDeptName);
// console.log('【检验】找到的科室名称:', deptItem)
// if (deptItem) {
// return deptItem.dictValue; // 返回科室编码
// } else {
// console.warn(`未找到科室名称为 ${currentDeptName} 的对应编码`);
// return currentDeptName || ''; // 使用原始名称作为默认值,符合防御性编程原则
// }
// } catch (error) {
// console.error('获取科室字典数据失败:', error);
// return '';
// }
// }
// 在 onMounted 中调用初始化函数
onMounted(async () => {
await initData();
// 设置当前科室编码
// const currentDeptCode = await getCurrentDeptCode();
// formData.applyDeptCode = currentDeptCode || '';
})
// Props
@@ -474,7 +444,6 @@ const showForm = ref(false)
const loading = ref(false)
const total = ref(0)
const leftActiveTab = ref('application')
const deptOptions = ref([])
// 用户信息store
const userStore = useUserStore()

View File

@@ -1,11 +1,16 @@
<template>
<div style="display: flex; justify-content: space-between; height: 90vh">
<div style="width: 15%; height: 100%; border: 1px solid #eee; border-right: 0">
<div style="padding: 10px; border: 1px solid #eee; height: 50px; border-right: 0">
<span>现诊患者</span>
<el-badge :value="waitCount > 0 ? waitCount : ''" :max="10"
style="float: right; color: #409eff; cursor: pointer; margin-right: 10px">
<span @click="openDrawer"> 患者队列 </span>
<div
style="padding: 10px; border: 1px solid #eee; height: 50px; border-right: 0; display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center;">
<span style="margin-right: 20px; font-weight: 600;">现诊患者</span>
<el-button type="primary" size="small" @click.stop="handleOpenCallDialog" title="点击打开叫号界面">
<i class="el-icon-bell"></i> 呼叫
</el-button>
</div>
<el-badge :value="waitCount > 0 ? waitCount : ''" :max="10" style="color: #409eff; cursor: pointer;">
<span @click="openDrawer" style="font-weight: 600;"> 患者队列 </span>
</el-badge>
</div>
<div style="width: 100%; padding: 10px">
@@ -15,16 +20,9 @@
<el-button icon="Search" @click="getPatientList" />
</template>
</el-input>
<el-date-picker
v-model="registerTime"
@change="handleTimeChange"
type="date"
style="width: 100%; margin-bottom: 10px"
:clearable="false"
placeholder="挂号时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
<el-date-picker v-model="registerTime" @change="handleTimeChange" type="date"
style="width: 100%; margin-bottom: 10px" :clearable="false" placeholder="挂号时间" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" />
<el-scrollbar height="700px">
<div v-for="(item, index) in patientList" :class="item.active ? 'patient-card actived' : 'patient-card'"
:key="item.id" @click="handleCardClick(item, index)">
@@ -86,11 +84,11 @@
' / ' +
patientInfo.genderEnum_enumText +
' / ' +
(patientInfo?.contractName ? patientInfo.contractName : '') +
'/' +
patientInfo.phone +
'/' +
patientInfo.busNo
(patientInfo?.contractName ? patientInfo.contractName : '') +
'/' +
patientInfo.phone +
'/' +
patientInfo.busNo
: '-'
}}
</el-descriptions-item>
@@ -110,27 +108,22 @@
<el-button type="primary" plain @click.stop="handleRefund(patientInfo.encounterId)">
退费
</el-button>
<el-button type="primary" plain class="top-layer-btn" @click.stop="getEnPrescription(patientInfo.encounterId)">
<el-button type="primary" plain class="top-layer-btn"
@click.stop="getEnPrescription(patientInfo.encounterId)">
处方单
</el-button>
<el-button type="primary" plain class="top-layer-btn" :disabled="isHospitalizationButtonDisabled" @click.stop="handleHospitalizationClick()" @mouseenter="console.log('办理住院按钮状态:', { patientInfo: patientInfo?.value, hasEncounterId: patientInfo?.value?.encounterId, isDisabled: isHospitalizationButtonDisabled })"> 办理住院 </el-button>
<el-button type="primary" plain class="top-layer-btn" :disabled="isHospitalizationButtonDisabled"
@click.stop="handleHospitalizationClick()"
@mouseenter="console.log('办理住院按钮状态:', { patientInfo: patientInfo?.value, hasEncounterId: patientInfo?.value?.encounterId, isDisabled: isHospitalizationButtonDisabled })">
办理住院 </el-button>
</el-descriptions-item>
</el-descriptions>
</div>
<div style="padding: 10px; position: relative">
<el-tabs
type="card"
style="width: 100%; height: 100%"
v-loading="loading"
v-model="activeTab"
@tab-change="handleClick(activeTab)"
>
<el-tabs type="card" style="width: 100%; height: 100%" v-loading="loading" v-model="activeTab"
@tab-change="handleClick(activeTab)">
<el-tab-pane label="门诊病历" name="hospitalizationEmr">
<hospitalizationEmr
:patientInfo="patientInfo"
:activeTab="activeTab"
@emrSaved="handleEmrSaved"
/>
<hospitalizationEmr :patientInfo="patientInfo" :activeTab="activeTab" @emrSaved="handleEmrSaved" />
</el-tab-pane>
<!-- <el-tab-pane label="病历" name="emr">
<Emr
@@ -151,12 +144,8 @@
" />
</el-tab-pane>
<el-tab-pane label="医嘱" name="prescription">
<prescriptionlist
:patientInfo="patientInfo"
ref="prescriptionRef"
:activeTab="activeTab"
:outpatientEmrSaved="outpatientEmrSaved"
/>
<prescriptionlist :patientInfo="patientInfo" ref="prescriptionRef" :activeTab="activeTab"
:outpatientEmrSaved="outpatientEmrSaved" />
</el-tab-pane>
<el-tab-pane label="中医" name="tcm">
<tcmAdvice :patientInfo="patientInfo" ref="tcmRef" />
@@ -180,24 +169,17 @@
<el-drawer v-model="drawer" title="患者队列" direction="ltr" @open="handleOpen">
<PatientList ref="patientDrawerRef" @toCurrent="handleReceive" />
</el-drawer>
<RefundListDialog
:open="openRefundListDialog"
:encounterId="currentEncounterId"
@close="openRefundListDialog = false"
@refresh="() => prescriptionRef.getListInfo()"
/>
<HospitalizationDialog
:open="openDialog"
:patientInfo="patientInfo"
:encounterId="currentEncounterId"
:mainDiagnosis="mainDiagnosis"
@close="openDialog = false"
/>
<PrescriptionInfo
:open="openPrescriptionDialog"
:precriptionInfo="prescriptionInfo"
@close="openPrescriptionDialog = false"
/>
<RefundListDialog :open="openRefundListDialog" :encounterId="currentEncounterId"
@close="openRefundListDialog = false" @refresh="() => prescriptionRef.getListInfo()" />
<HospitalizationDialog :open="openDialog" :patientInfo="patientInfo" :encounterId="currentEncounterId"
:mainDiagnosis="mainDiagnosis" @close="openDialog = false" />
<PrescriptionInfo :open="openPrescriptionDialog" :precriptionInfo="prescriptionInfo"
@close="openPrescriptionDialog = false" />
<!-- 新增叫号弹窗组件 -->
<DoctorCallDialog v-model:dialogVisible="dialogVisible" :current-patient="currentCallPatient"
:current-patient-list="currentWaitPatientList" :room-no="roomNo"
:department="userStore.orgName || patientInfo.organizationName || '心内科'" @callNext="callNext" @reCall="reCall"
@finish="finishCall" @skip="skip" @requeue="requeue" @markSeen="markSeen" @callThis="callThis" />
</div>
</template>
<script setup>
@@ -221,11 +203,12 @@ import HospitalizationDialog from './components/hospitalizationDialog.vue';
import tcmAdvice from './components/tcm/tcmAdvice.vue';
import inspectionApplication from './components/inspection/inspectionApplication.vue';
import surgeryApplication from './components/surgery/surgeryApplication.vue';
import {formatDate, formatDateStr} from '@/utils/index';
import DoctorCallDialog from './components/callQueue/DoctorCallDialog.vue';
import { formatDate, formatDateStr } from '@/utils/index';
import useUserStore from '@/store/modules/user';
import {nextTick} from 'vue';
import {updatePatientInfo} from './components/store/patient.js';
import {ElMessage, ElMessageBox} from 'element-plus';
import { nextTick } from 'vue';
import { updatePatientInfo } from './components/store/patient.js';
import { ElMessage, ElMessageBox } from 'element-plus';
// // 监听路由离开事件
// onBeforeRouteLeave((to, from, next) => {
@@ -253,6 +236,10 @@ const drawer = ref(false);
const openRefundListDialog = ref(false);
const openDialog = ref(false);
const openPrescriptionDialog = ref(false);
const dialogVisible = ref(false); // 叫号弹窗显示/隐藏
const currentCallPatient = ref({}); // 传给弹窗的【当前就诊患者】
const currentWaitPatientList = ref([]); // 传给弹窗的【候诊患者列表】
const roomNo = ref('4号'); // 诊室号
const saveStatus = ref(false);
const outpatientEmrSaved = ref(false); // 门诊病历保存状态
const currentEncounterId = ref('');
@@ -267,11 +254,11 @@ const visitTypeDisabled = ref(false);
// 计算属性:确定办理住院按钮是否应被禁用
const isHospitalizationButtonDisabled = computed(() => {
return !patientInfo.value ||
typeof patientInfo.value !== 'object' ||
!patientInfo.value.encounterId ||
patientInfo.value.encounterId === '' ||
patientInfo.value.encounterId === null ||
patientInfo.value.encounterId === undefined;
typeof patientInfo.value !== 'object' ||
!patientInfo.value.encounterId ||
patientInfo.value.encounterId === '' ||
patientInfo.value.encounterId === null ||
patientInfo.value.encounterId === undefined;
});
const prescriptionInfo = ref([]);
@@ -327,8 +314,9 @@ const shortcuts = [
const eprescriptionRef = ref();
onMounted(() => {
getWaitPatient();
getWaitPatientList();
getPatientList();
});
getPatientList();
// 获取现诊患者列表
function getPatientList() {
queryParams.value.statusEnum = 2;
@@ -405,6 +393,33 @@ function getWaitPatient() {
waitCount.value = res.data.total;
});
}
// 新增:获取候诊患者完整列表
function getWaitPatientList() {
queryParams.value.registerTimeSTime = formatDateStr(new Date(), 'YYYY-MM-DD') + ' 00:00:00';
queryParams.value.registerTimeETime = formatDateStr(new Date(), 'YYYY-MM-DD') + ' 23:59:59';
queryParams.value.statusEnum = 1; // 筛选【待诊】患者
getList(queryParams.value).then((res) => {
currentWaitPatientList.value = res.data.records;
waitCount.value = res.data.total;
});
}
// 新增:打开叫号弹窗方法
function handleOpenCallDialog() {
// 刷新患者列表和候诊列表,确保数据是最新的
getPatientList();
getWaitPatientList();
// 检查当前选中的患者是否已经完诊,如果已经完诊,就不设置为当前呼叫患者
if (patientInfo.value && patientInfo.value.statusEnum !== 3) { // 3代表已完诊
currentCallPatient.value = patientInfo.value;
} else {
// 如果当前患者已经完诊,就清空当前呼叫患者
currentCallPatient.value = {};
}
dialogVisible.value = true;
}
function handleClick(tab) {
switch (tab) {
@@ -456,10 +471,10 @@ function getEnPrescription(encounterId) {
type: 'error',
message: '暂无处方单',
});
return;
}
return;
}
prescriptionInfo.value = res.data.records;
openPrescriptionDialog.value = true;
openPrescriptionDialog.value = true;
});
}
@@ -716,6 +731,58 @@ const onHospitalization = async () => {
});
}
};
// ========== 叫号弹窗所有回调事件 ==========
const callNext = () => {
// 直接刷新患者列表和候诊列表API调用已经在子组件中完成
getPatientList();
getWaitPatientList();
// 子组件已处理提示,这里无需重复提示
};
const reCall = () => {
// 选呼功能已实现,无需提示
};
const finishCall = () => {
dialogVisible.value = false;
// 刷新患者列表和候诊列表
getPatientList();
getWaitPatientList();
// 清空当前呼叫患者
currentCallPatient.value = {};
// 子组件已处理提示,这里无需重复提示
};
const skip = () => {
// 直接刷新患者列表和候诊列表API调用已经在子组件中完成
getPatientList();
getWaitPatientList();
// 子组件已处理提示,这里无需重复提示
};
const requeue = () => {
// 1. 重新获取候诊患者列表
getWaitPatientList();
// 2. 刷新现诊患者列表
getPatientList();
// 3. 刷新候诊人数统计
getWaitPatient();
// 4. 清空当前呼叫患者
currentCallPatient.value = {};
// 子组件已处理提示,这里无需重复提示
};
const markSeen = async () => {
// 直接刷新患者列表和候诊列表API调用已经在子组件中完成
getPatientList();
getWaitPatientList();
// 清空当前呼叫患者
currentCallPatient.value = {};
};
const callThis = (row) => {
handleCardClick(row);
currentCallPatient.value = row;
dialogVisible.value = false;
// 刷新患者列表和候诊列表
getPatientList();
getWaitPatientList();
};
</script>
<style lang="scss" scoped>
@@ -724,6 +791,7 @@ const onHospitalization = async () => {
:deep(.el-descriptions__label) {
font-size: 16px !important;
}
:deep(.el-descriptions__content) {
font-size: 16px !important;
}
@@ -906,15 +974,18 @@ const onHospitalization = async () => {
left: 0;
width: 100%;
height: calc(100% - 50px);
z-index: 998; /* 降低z-index避免覆盖按钮 */
z-index: 998;
/* 降低z-index避免覆盖按钮 */
/* 确保覆盖在内容上方,但不覆盖顶部按钮区域 */
cursor: not-allowed;
background-color: rgba(255, 255, 255, 0.01);
pointer-events: none; /* 默认不捕获鼠标事件,只在真正需要禁用时才启用 */
pointer-events: none;
/* 默认不捕获鼠标事件,只在真正需要禁用时才启用 */
}
.disabled-wrapper .overlay.overlay-disabled {
pointer-events: auto; /* 当需要真正禁用时启用指针事件 */
pointer-events: auto;
/* 当需要真正禁用时启用指针事件 */
}
/* 顶层按钮样式,确保按钮始终在最上层 */

View File

@@ -282,3 +282,13 @@ public Long saveEncounterByRegister(Encounter encounter) {
**记住**:查询条件使用的字段,必须在数据插入/更新时被设置!