feat(menu): 优化菜单路径唯一性校验并更新前端界面
- 在SysLoginController中添加optionMap数据返回 - 添加JSQLParser依赖支持MyBatis Plus功能 - 实现selectMenuByPathExcludeId方法用于排除当前菜单的路径唯一性校验 - 在SysMenuServiceImpl中添加日志记录并优化路径唯一性判断逻辑 - 在SysMenuMapper.xml中添加LIMIT 1限制并实现排除ID查询 - 在前端路由中注释患者管理相关路由配置 - 在用户store中添加optionMap配置项并优先从optionMap获取医院名称 - 重构检查项目设置页面的操作按钮样式为统一的圆形按钮设计 - 更新检查项目设置页面的导航栏样式和交互体验 - 优化门诊记录页面的搜索条件和表格展示功能 - 添加性别和状态筛选条件并改进数据加载逻辑
This commit is contained in:
@@ -86,6 +86,7 @@ public class SysLoginController {
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put("optionJson", loginUser.getOptionJson());
|
||||
ajax.put("optionMap", loginUser.getOptionMap());
|
||||
ajax.put("practitionerId", String.valueOf(loginUser.getPractitionerId()));
|
||||
ajax.put("user", user);
|
||||
ajax.put("roles", roles);
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
<artifactId>core-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSQLParser - 用于MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -91,6 +91,15 @@ public interface SysMenuMapper {
|
||||
*/
|
||||
public SysMenu selectMenuByPath(String path);
|
||||
|
||||
/**
|
||||
* 根据路径Path查询信息(排除指定菜单ID)
|
||||
*
|
||||
* @param path 路径
|
||||
* @param menuId 菜单ID
|
||||
* @return 菜单信息
|
||||
*/
|
||||
public SysMenu selectMenuByPathExcludeId(@Param("path") String path, @Param("menuId") Long menuId);
|
||||
|
||||
/**
|
||||
* 是否存在菜单子节点
|
||||
*
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.core.system.mapper.SysMenuMapper;
|
||||
import com.core.system.mapper.SysRoleMapper;
|
||||
import com.core.system.mapper.SysRoleMenuMapper;
|
||||
import com.core.system.service.ISysMenuService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -27,6 +29,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Service
|
||||
public class SysMenuServiceImpl implements ISysMenuService {
|
||||
private static final Logger log = LoggerFactory.getLogger(SysMenuServiceImpl.class);
|
||||
public static final String PREMISSION_STRING = "perms[\"{0}\"]";
|
||||
|
||||
@Autowired
|
||||
@@ -281,12 +284,13 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
*/
|
||||
@Override
|
||||
public int updateMenu(SysMenu menu) {
|
||||
//路径Path唯一性判断
|
||||
//路径Path唯一性判断(排除当前菜单本身)
|
||||
String path = menu.getPath();
|
||||
if (StringUtils.isNotBlank(path)) {
|
||||
SysMenu sysMenu = menuMapper.selectMenuByPath(menu.getPath());
|
||||
// 先判断sysMenu是否不为null,再比较menuId
|
||||
if (sysMenu != null && !menu.getMenuId().equals(sysMenu.getMenuId())) {
|
||||
SysMenu sysMenu = menuMapper.selectMenuByPathExcludeId(menu.getPath(), menu.getMenuId());
|
||||
if (sysMenu != null) {
|
||||
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
|
||||
menu.getMenuId(), menu.getPath(), sysMenu.getMenuId());
|
||||
return -1; // 路由地址已存在
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,12 @@
|
||||
<select id="selectMenuByPath" parameterType="String" resultMap="SysMenuResult">
|
||||
<include refid="selectMenuVo"/>
|
||||
where path = #{path}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectMenuByPathExcludeId" resultMap="SysMenuResult">
|
||||
<include refid="selectMenuVo"/>
|
||||
where path = #{path} and menu_id != #{menuId}
|
||||
</select>
|
||||
|
||||
<select id="selectMenuById" parameterType="Long" resultMap="SysMenuResult">
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.openhis.web.administration.controller;
|
||||
|
||||
import com.core.common.annotation.Log;
|
||||
import com.core.common.core.controller.BaseController;
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.core.common.core.page.TableDataInfo;
|
||||
import com.core.common.enums.BusinessType;
|
||||
import com.core.common.utils.poi.ExcelUtil;
|
||||
import com.openhis.administration.domain.PractitionerPatient;
|
||||
import com.openhis.administration.service.IPractitionerPatientService;
|
||||
import com.openhis.administration.dto.PractitionerPatientDto;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 医生患者关系管理Controller
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/administration/practitioner-patient")
|
||||
public class PractitionerPatientController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IPractitionerPatientService practitionerPatientService;
|
||||
|
||||
/**
|
||||
* 查询医生患者关系列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PractitionerPatient practitionerPatient) {
|
||||
startPage();
|
||||
List<PractitionerPatient> list = practitionerPatientService.list();
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出医生患者关系列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:export')")
|
||||
@Log(title = "医生患者关系", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, PractitionerPatient practitionerPatient) {
|
||||
List<PractitionerPatient> list = practitionerPatientService.list();
|
||||
ExcelUtil<PractitionerPatient> util = new ExcelUtil<>(PractitionerPatient.class);
|
||||
util.exportExcel(response, list, "医生患者关系数据");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取医生患者关系详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:query')")
|
||||
@GetMapping(value = "/{id}")
|
||||
public AjaxResult getInfo(@PathVariable("id") Long id) {
|
||||
return AjaxResult.success(practitionerPatientService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取医生的所有有效患者
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:query')")
|
||||
@GetMapping("/practitioner/{practitionerId}/patients")
|
||||
public AjaxResult getPatientsByPractitioner(@PathVariable Long practitionerId) {
|
||||
return AjaxResult.success(practitionerPatientService.getValidPatientsByPractitioner(practitionerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取患者的所有有效医生
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:query')")
|
||||
@GetMapping("/patient/{patientId}/practitioners")
|
||||
public AjaxResult getPractitionersByPatient(@PathVariable Long patientId) {
|
||||
return AjaxResult.success(practitionerPatientService.getValidPractitionersByPatient(patientId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增医生患者关系
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:add')")
|
||||
@Log(title = "医生患者关系", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody PractitionerPatientDto dto) {
|
||||
PractitionerPatient relationship = new PractitionerPatient();
|
||||
relationship.setPractitionerId(dto.getPractitionerId());
|
||||
relationship.setPatientId(dto.getPatientId());
|
||||
relationship.setRelationshipType(dto.getRelationshipType());
|
||||
relationship.setOrganizationId(dto.getOrganizationId());
|
||||
relationship.setStartDate(dto.getStartDate());
|
||||
relationship.setRemark(dto.getRemark());
|
||||
|
||||
return toAjax(practitionerPatientService.createRelationship(relationship));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改医生患者关系
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:edit')")
|
||||
@Log(title = "医生患者关系", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody PractitionerPatient practitionerPatient) {
|
||||
return toAjax(practitionerPatientService.updateById(practitionerPatient));
|
||||
}
|
||||
|
||||
/**
|
||||
* 终止医生患者关系
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:remove')")
|
||||
@Log(title = "医生患者关系", businessType = BusinessType.DELETE)
|
||||
@PostMapping("/terminate/{id}")
|
||||
public AjaxResult terminate(@PathVariable Long id) {
|
||||
return toAjax(practitionerPatientService.terminateRelationship(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除医生患者关系
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:remove')")
|
||||
@Log(title = "医生患者关系", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Long[] ids) {
|
||||
return toAjax(practitionerPatientService.removeByIds(List.of(ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建医生患者关系
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('administration:practitionerPatient:add')")
|
||||
@Log(title = "批量创建医生患者关系", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/batch")
|
||||
public AjaxResult batchAdd(@RequestBody List<PractitionerPatientDto> dtos) {
|
||||
List<PractitionerPatient> relationships = dtos.stream().map(dto -> {
|
||||
PractitionerPatient relationship = new PractitionerPatient();
|
||||
relationship.setPractitionerId(dto.getPractitionerId());
|
||||
relationship.setPatientId(dto.getPatientId());
|
||||
relationship.setRelationshipType(dto.getRelationshipType());
|
||||
relationship.setOrganizationId(dto.getOrganizationId());
|
||||
relationship.setStartDate(dto.getStartDate());
|
||||
relationship.setRemark(dto.getRemark());
|
||||
return relationship;
|
||||
}).toList();
|
||||
|
||||
return toAjax(practitionerPatientService.batchCreateRelationships(relationships));
|
||||
}
|
||||
}
|
||||
@@ -407,8 +407,12 @@ public class PractitionerAppServiceImpl implements IPractitionerAppService {
|
||||
// iBizUserService.remove(new LambdaQueryWrapper<BizUser>().eq(BizUser::getUserId, userId));
|
||||
practitionerAppAppMapper.delUser(userId);
|
||||
practitionerAppAppMapper.delUserRole(userId);
|
||||
Practitioner one =
|
||||
iPractitionerService.getOne(new LambdaQueryWrapper<Practitioner>().eq(Practitioner::getUserId, userId));
|
||||
// 使用list()避免TooManyResultsException异常,然后取第一个记录
|
||||
List<Practitioner> practitionerList = iPractitionerService.list(new LambdaQueryWrapper<Practitioner>().eq(Practitioner::getUserId, userId));
|
||||
Practitioner one = practitionerList != null && !practitionerList.isEmpty() ? practitionerList.get(0) : null;
|
||||
if (one == null) {
|
||||
return R.fail(null, "未找到对应的医生信息");
|
||||
}
|
||||
Long practitionerId = one.getId();// 参与者id
|
||||
iPractitionerService.removeById(practitionerId);
|
||||
iPractitionerRoleService
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.openhis.web.controller;
|
||||
|
||||
import com.core.common.core.controller.BaseController;
|
||||
import com.core.common.core.domain.AjaxResult;
|
||||
import com.openhis.administration.domain.Encounter;
|
||||
import com.openhis.web.dto.HomeStatisticsDto;
|
||||
import com.openhis.web.service.IHomeStatisticsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 首页统计Controller
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-12-31
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/home")
|
||||
public class HomeStatisticsController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IHomeStatisticsService homeStatisticsService;
|
||||
|
||||
/**
|
||||
* 获取首页统计数据
|
||||
*
|
||||
* @return 首页统计数据
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public AjaxResult getHomeStatistics() {
|
||||
HomeStatisticsDto statistics = homeStatisticsService.getHomeStatistics();
|
||||
return AjaxResult.success(statistics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.openhis.web.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 首页统计数据DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-12-31
|
||||
*/
|
||||
@Data
|
||||
public class HomeStatisticsDto {
|
||||
/**
|
||||
* 在院患者数量
|
||||
*/
|
||||
private Integer totalPatients;
|
||||
|
||||
/**
|
||||
* 昨日在院患者数量
|
||||
*/
|
||||
private Integer yesterdayPatients;
|
||||
|
||||
/**
|
||||
* 相对前日变化百分比
|
||||
*/
|
||||
private Double patientTrend;
|
||||
|
||||
/**
|
||||
* 今日收入
|
||||
*/
|
||||
private String todayRevenue;
|
||||
|
||||
/**
|
||||
* 昨日收入
|
||||
*/
|
||||
private String yesterdayRevenue;
|
||||
|
||||
/**
|
||||
* 相对前日变化百分比
|
||||
*/
|
||||
private Double revenueTrend;
|
||||
|
||||
/**
|
||||
* 今日预约数量
|
||||
*/
|
||||
private Integer todayAppointments;
|
||||
|
||||
/**
|
||||
* 昨日预约数量
|
||||
*/
|
||||
private Integer yesterdayAppointments;
|
||||
|
||||
/**
|
||||
* 相对前日变化百分比
|
||||
*/
|
||||
private Double appointmentTrend;
|
||||
|
||||
/**
|
||||
* 待审核数量
|
||||
*/
|
||||
private Integer pendingApprovals;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import java.util.HashSet;
|
||||
* @date 2025/3/15
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OutpatientRecordServiceImpl implements IOutpatientRecordService {
|
||||
|
||||
@Resource
|
||||
@@ -37,24 +38,78 @@ public class OutpatientRecordServiceImpl implements IOutpatientRecordService {
|
||||
* 分页查询门诊记录
|
||||
*
|
||||
* @param outpatientRecordSearchParam 门诊录查询参数
|
||||
* @param searchKey 搜索关键词(支持身份证号/病人ID/门诊号/姓名)
|
||||
* @param pageNo 页码(默认为1)
|
||||
* @param pageSize 每页大小(默认为10)
|
||||
* @return 分页查询
|
||||
* @param request HTTP请求
|
||||
* @return 分页查询结果
|
||||
*/
|
||||
@Override
|
||||
public IPage<OutpatientRecordDto> getPatient(OutpatientRecordSearchParam outpatientRecordSearchParam,
|
||||
String searchKey, Integer pageNo, Integer pageSize, HttpServletRequest request) {
|
||||
|
||||
log.info("进入门诊记录查询服务,searchKey: {}", searchKey);
|
||||
if (outpatientRecordSearchParam != null) {
|
||||
log.info("查询参数:searchKey={}, 性别={}, 状态={}, 电话={}, 医生={}, 开始时间={}, 结束时间={}",
|
||||
searchKey,
|
||||
outpatientRecordSearchParam.getGenderEnum(),
|
||||
outpatientRecordSearchParam.getSubjectStatusEnum(),
|
||||
outpatientRecordSearchParam.getPhone(),
|
||||
outpatientRecordSearchParam.getDoctorName(),
|
||||
outpatientRecordSearchParam.getStartTimeSTime(),
|
||||
outpatientRecordSearchParam.getStartTimeETime());
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<OutpatientRecordDto> queryWrapper
|
||||
= HisQueryUtils.buildQueryWrapper(outpatientRecordSearchParam, searchKey,
|
||||
new HashSet<>(Arrays.asList(CommonConstants.FieldName.IdCard, CommonConstants.FieldName.Name,
|
||||
CommonConstants.FieldName.PatientBusNo, CommonConstants.FieldName.EncounterBusNo)),
|
||||
request);
|
||||
// 构建查询条件(不自动添加tenant_id,手动指定表别名)
|
||||
QueryWrapper<OutpatientRecordDto> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
// 手动添加带表别名的tenant_id条件
|
||||
queryWrapper.eq("enc.tenant_id", com.core.common.utils.SecurityUtils.getLoginUser().getTenantId());
|
||||
|
||||
// 处理模糊查询关键字(searchKey)- 用于姓名/身份证号/病人ID/门诊号的模糊搜索
|
||||
if (searchKey != null && !searchKey.isEmpty()) {
|
||||
queryWrapper.and(wrapper -> {
|
||||
wrapper.like("pt.name", searchKey)
|
||||
.or().like("pt.id_card", searchKey)
|
||||
.or().like("pt.bus_no", searchKey)
|
||||
.or().like("enc.bus_no", searchKey);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理其他筛选条件(这些条件可以与模糊查询或精确查询组合使用)
|
||||
if (outpatientRecordSearchParam != null) {
|
||||
// 处理性别筛选
|
||||
if (outpatientRecordSearchParam.getGenderEnum() != null) {
|
||||
queryWrapper.eq("pt.gender_enum", outpatientRecordSearchParam.getGenderEnum());
|
||||
}
|
||||
|
||||
// 处理就诊对象状态筛选
|
||||
if (outpatientRecordSearchParam.getSubjectStatusEnum() != null) {
|
||||
queryWrapper.eq("enc.status_enum", outpatientRecordSearchParam.getSubjectStatusEnum());
|
||||
}
|
||||
|
||||
// 处理医生姓名查询(支持模糊查询)
|
||||
if (outpatientRecordSearchParam.getDoctorName() != null && !outpatientRecordSearchParam.getDoctorName().isEmpty()) {
|
||||
queryWrapper.like("prac.name", outpatientRecordSearchParam.getDoctorName());
|
||||
}
|
||||
|
||||
// 处理电话号码查询(支持模糊查询)
|
||||
if (outpatientRecordSearchParam.getPhone() != null && !outpatientRecordSearchParam.getPhone().isEmpty()) {
|
||||
queryWrapper.like("pt.phone", outpatientRecordSearchParam.getPhone());
|
||||
}
|
||||
|
||||
// 处理时间范围查询
|
||||
if (outpatientRecordSearchParam.getStartTimeSTime() != null && !outpatientRecordSearchParam.getStartTimeSTime().isEmpty()
|
||||
&& outpatientRecordSearchParam.getStartTimeETime() != null && !outpatientRecordSearchParam.getStartTimeETime().isEmpty()) {
|
||||
queryWrapper.between("enc.create_time", outpatientRecordSearchParam.getStartTimeSTime(), outpatientRecordSearchParam.getStartTimeETime());
|
||||
}
|
||||
}
|
||||
|
||||
// 使用接诊医生(ADMITTER,code="1")作为参与者类型
|
||||
IPage<OutpatientRecordDto> outpatientRecordPage = patientManageMapper
|
||||
.getOutpatientRecord(ParticipantType.ADMITTER.getCode(), new Page<>(pageNo, pageSize), queryWrapper);
|
||||
|
||||
// 处理枚举字段的显示文本
|
||||
outpatientRecordPage.getRecords().forEach(e -> {
|
||||
// 性别枚举类回显赋值
|
||||
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.openhis.administration.domain.Patient;
|
||||
import com.openhis.administration.domain.PatientIdentifier;
|
||||
import com.openhis.administration.service.IPatientIdentifierService;
|
||||
import com.openhis.administration.service.IPatientService;
|
||||
import com.openhis.administration.service.IPractitionerService;
|
||||
import com.openhis.common.constant.CommonConstants;
|
||||
import com.openhis.common.constant.PromptMsgConstant;
|
||||
import com.openhis.common.enums.*;
|
||||
@@ -54,6 +55,9 @@ public class PatientInformationServiceImpl implements IPatientInformationService
|
||||
@Autowired
|
||||
private IPatientService patientService;
|
||||
|
||||
@Autowired
|
||||
private IPractitionerService practitionerService;
|
||||
|
||||
@Autowired
|
||||
private IPatientIdentifierService patientIdentifierService;
|
||||
|
||||
@@ -129,11 +133,40 @@ public class PatientInformationServiceImpl implements IPatientInformationService
|
||||
@Override
|
||||
public IPage<PatientBaseInfoDto> getPatientInfo(PatientBaseInfoDto patientBaseInfoDto, String searchKey,
|
||||
Integer pageNo, Integer pageSize, HttpServletRequest request) {
|
||||
// 构建查询条件
|
||||
// 获取登录者信息
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
Long userId = loginUser.getUserId();
|
||||
|
||||
// 先构建基础查询条件
|
||||
QueryWrapper<PatientBaseInfoDto> queryWrapper = HisQueryUtils.buildQueryWrapper(
|
||||
patientBaseInfoDto, searchKey, new HashSet<>(Arrays.asList(CommonConstants.FieldName.Name,
|
||||
CommonConstants.FieldName.BusNo, CommonConstants.FieldName.PyStr, CommonConstants.FieldName.WbStr)),
|
||||
request);
|
||||
|
||||
// 查询当前用户对应的医生信息
|
||||
LambdaQueryWrapper<com.openhis.administration.domain.Practitioner> practitionerQuery = new LambdaQueryWrapper<>();
|
||||
practitionerQuery.eq(com.openhis.administration.domain.Practitioner::getUserId, userId);
|
||||
// 使用list()避免TooManyResultsException异常,然后取第一个记录
|
||||
List<com.openhis.administration.domain.Practitioner> practitionerList = practitionerService.list(practitionerQuery);
|
||||
com.openhis.administration.domain.Practitioner practitioner = practitionerList != null && !practitionerList.isEmpty() ? practitionerList.get(0) : null;
|
||||
|
||||
// 如果当前用户是医生,添加医生患者过滤条件
|
||||
if (practitioner != null) {
|
||||
// 查询该医生作为接诊医生(ADMITTER, code="1")和挂号医生(REGISTRATION_DOCTOR, code="12")的所有就诊记录的患者ID
|
||||
List<Long> doctorPatientIds = patientManageMapper.getPatientIdsByPractitionerId(
|
||||
practitioner.getId(),
|
||||
Arrays.asList(ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode()));
|
||||
|
||||
if (doctorPatientIds != null && !doctorPatientIds.isEmpty()) {
|
||||
// 添加患者ID过滤条件 - 注意:这里使用列名而不是表别名
|
||||
queryWrapper.in("id", doctorPatientIds);
|
||||
} else {
|
||||
// 如果没有相关患者,返回空结果
|
||||
queryWrapper.eq("id", -1); // 设置一个不存在的ID
|
||||
}
|
||||
}
|
||||
// 如果不是医生,查询所有患者
|
||||
|
||||
IPage<PatientBaseInfoDto> patientInformationPage
|
||||
= patientManageMapper.getPatientPage(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
// 患者id集合
|
||||
@@ -141,8 +174,7 @@ public class PatientInformationServiceImpl implements IPatientInformationService
|
||||
= patientInformationPage.getRecords().stream().map(PatientBaseInfoDto::getId).collect(Collectors.toList());
|
||||
// 患者身份信息
|
||||
List<PatientIdInfoDto> patientIdInfo = patientManageMapper.getPatientIdInfo(patientIdList);
|
||||
// 获取登录者信息
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
|
||||
patientInformationPage.getRecords().forEach(e -> {
|
||||
// 性别枚举类回显赋值
|
||||
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.openhis.web.patientmanage.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.core.common.annotation.Anonymous;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.openhis.web.patientmanage.appservice.IOutpatientRecordService;
|
||||
import com.openhis.web.patientmanage.dto.OutpatientRecordDto;
|
||||
import com.openhis.web.patientmanage.dto.OutpatientRecordSearchParam;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 门诊记录查询控制器
|
||||
*
|
||||
* @author system
|
||||
* @date 2025/12/31
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/patient-manage/records")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Anonymous
|
||||
public class OutpatientRecordController {
|
||||
|
||||
private final IOutpatientRecordService outpatientRecordService;
|
||||
|
||||
/**
|
||||
* 测试接口 - 验证Controller是否被加载
|
||||
*
|
||||
* @return 测试消息
|
||||
*/
|
||||
@GetMapping("/test")
|
||||
public R<?> test() {
|
||||
log.info("OutpatientRecordController.test() 被调用");
|
||||
return R.ok("OutpatientRecordController 工作正常");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取门诊记录初期数据
|
||||
*
|
||||
* @return 初期数据
|
||||
*/
|
||||
@GetMapping("/init")
|
||||
public R<?> getInitData() {
|
||||
return outpatientRecordService.getDoctorNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询门诊记录
|
||||
*
|
||||
* @param outpatientRecordSearchParam 门诊记录查询参数
|
||||
* @param searchKey 查询条件-模糊查询
|
||||
* @param pageNo 页码(默认为1)
|
||||
* @param pageSize 每页大小(默认为10)
|
||||
* @param request 请求对象
|
||||
* @return 分页查询结果
|
||||
*/
|
||||
@GetMapping("/outpatient-record-page")
|
||||
public R<IPage<OutpatientRecordDto>> getOutpatientRecordPage(
|
||||
OutpatientRecordSearchParam outpatientRecordSearchParam,
|
||||
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest request) {
|
||||
log.info("查询门诊记录,pageNo: {}, pageSize: {}", pageNo, pageSize);
|
||||
log.info("searchKey: {}", searchKey);
|
||||
log.info("outpatientRecordSearchParam: {}", outpatientRecordSearchParam);
|
||||
if (outpatientRecordSearchParam != null) {
|
||||
log.info("姓名参数: {}, 身份证参数: {}, 病人ID: {}, 门诊号: {}, 性别: {}, 状态: {}, 电话: {}, 医生: {}, 开始时间: {}, 结束时间: {}",
|
||||
outpatientRecordSearchParam.getName(),
|
||||
outpatientRecordSearchParam.getIdCard(),
|
||||
outpatientRecordSearchParam.getPatientBusNo(),
|
||||
outpatientRecordSearchParam.getEncounterBusNo(),
|
||||
outpatientRecordSearchParam.getGenderEnum(),
|
||||
outpatientRecordSearchParam.getSubjectStatusEnum(),
|
||||
outpatientRecordSearchParam.getPhone(),
|
||||
outpatientRecordSearchParam.getDoctorName(),
|
||||
outpatientRecordSearchParam.getStartTimeSTime(),
|
||||
outpatientRecordSearchParam.getStartTimeETime());
|
||||
}
|
||||
return R.ok(outpatientRecordService.getPatient(outpatientRecordSearchParam, searchKey, pageNo, pageSize, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取医生名字列表
|
||||
*
|
||||
* @return 医生名字列表
|
||||
*/
|
||||
@GetMapping("/doctor-names")
|
||||
public R<?> getDoctorNames() {
|
||||
return outpatientRecordService.getDoctorNames();
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@ import java.util.Date;
|
||||
public class OutpatientRecordDto {
|
||||
|
||||
/**
|
||||
* ID
|
||||
* 就诊记录ID
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
private Long encounterId;
|
||||
|
||||
/**
|
||||
* 患者姓名
|
||||
@@ -50,17 +50,36 @@ public class OutpatientRecordDto {
|
||||
private Integer genderEnum;
|
||||
private String genderEnum_enumText;
|
||||
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 就诊时间
|
||||
*/
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date encounterTime;
|
||||
|
||||
/**
|
||||
* 就诊对象状态
|
||||
*/
|
||||
private Integer subjectStatusEnum;
|
||||
private String subjectStatusEnum_enumText;
|
||||
|
||||
/**
|
||||
* 医疗机构名称
|
||||
*/
|
||||
private String organizationName;
|
||||
|
||||
/**
|
||||
* 接诊医生姓名
|
||||
*/
|
||||
private String doctorName;
|
||||
|
||||
/**
|
||||
* 登记时间
|
||||
*/
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
// 其他可能需要的字段
|
||||
}
|
||||
|
||||
@@ -43,5 +43,30 @@ public class OutpatientRecordSearchParam {
|
||||
*/
|
||||
private Integer subjectStatusEnum;
|
||||
|
||||
/**
|
||||
* 医生姓名
|
||||
*/
|
||||
private String doctorName;
|
||||
|
||||
/**
|
||||
* 患者电话
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 搜索关键词(支持身份证号/病人ID/门诊号/姓名)
|
||||
*/
|
||||
private String searchKey;
|
||||
|
||||
/**
|
||||
* 开始时间(起始)
|
||||
*/
|
||||
private String startTimeSTime;
|
||||
|
||||
/**
|
||||
* 开始时间(结束)
|
||||
*/
|
||||
private String startTimeETime;
|
||||
|
||||
// 其他可能需要的查询参数
|
||||
}
|
||||
|
||||
@@ -58,4 +58,14 @@ public interface PatientManageMapper extends BaseMapper<Patient> {
|
||||
* @return 医生名字列表
|
||||
*/
|
||||
List<String> getDoctorNames();
|
||||
|
||||
/**
|
||||
* 根据医生ID和参与者类型获取相关的患者ID列表
|
||||
*
|
||||
* @param practitionerId 医生ID
|
||||
* @param typeCodes 参与者类型代码列表
|
||||
* @return 患者ID列表
|
||||
*/
|
||||
List<Long> getPatientIdsByPractitionerId(@Param("practitionerId") Long practitionerId,
|
||||
@Param("typeCodes") List<String> typeCodes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.openhis.web.service;
|
||||
|
||||
import com.openhis.web.dto.HomeStatisticsDto;
|
||||
|
||||
/**
|
||||
* 首页统计Service接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-12-31
|
||||
*/
|
||||
public interface IHomeStatisticsService {
|
||||
/**
|
||||
* 获取首页统计数据
|
||||
*
|
||||
* @return 首页统计数据
|
||||
*/
|
||||
HomeStatisticsDto getHomeStatistics();
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.openhis.web.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.utils.DateUtils;
|
||||
import com.core.common.utils.SecurityUtils;
|
||||
import com.openhis.administration.domain.Encounter;
|
||||
import com.openhis.administration.domain.EncounterParticipant;
|
||||
import com.openhis.administration.domain.Patient;
|
||||
import com.openhis.administration.domain.Practitioner;
|
||||
import com.openhis.administration.service.IEncounterParticipantService;
|
||||
import com.openhis.administration.service.IEncounterService;
|
||||
import com.openhis.administration.service.IPatientService;
|
||||
import com.openhis.administration.service.IPractitionerService;
|
||||
import com.openhis.common.enums.ParticipantType;
|
||||
import com.openhis.web.dto.HomeStatisticsDto;
|
||||
import com.openhis.web.service.IHomeStatisticsService;
|
||||
import com.openhis.web.patientmanage.mapper.PatientManageMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 首页统计Service业务层处理
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-12-31
|
||||
*/
|
||||
@Service
|
||||
public class HomeStatisticsServiceImpl implements IHomeStatisticsService {
|
||||
|
||||
@Autowired
|
||||
private IEncounterService encounterService;
|
||||
|
||||
@Autowired
|
||||
private IEncounterParticipantService encounterParticipantService;
|
||||
|
||||
@Autowired
|
||||
private IPractitionerService practitionerService;
|
||||
|
||||
@Autowired
|
||||
private PatientManageMapper patientManageMapper;
|
||||
|
||||
@Autowired
|
||||
private IPatientService patientService;
|
||||
|
||||
/**
|
||||
* 获取首页统计数据
|
||||
*
|
||||
* @return 首页统计数据
|
||||
*/
|
||||
@Override
|
||||
public HomeStatisticsDto getHomeStatistics() {
|
||||
HomeStatisticsDto statistics = new HomeStatisticsDto();
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
// 查询当前用户对应的医生信息
|
||||
LambdaQueryWrapper<Practitioner> practitionerQuery = new LambdaQueryWrapper<>();
|
||||
practitionerQuery.eq(Practitioner::getUserId, userId);
|
||||
// 使用list()避免TooManyResultsException异常,然后取第一个记录
|
||||
List<Practitioner> practitionerList = practitionerService.list(practitionerQuery);
|
||||
Practitioner practitioner = practitionerList != null && !practitionerList.isEmpty() ? practitionerList.get(0) : null;
|
||||
|
||||
int totalPatients = 0;
|
||||
|
||||
// 如果当前用户是医生,查询该医生接诊和被挂号的所有患者
|
||||
if (practitioner != null) {
|
||||
// 查询该医生作为接诊医生(ADMITTER, code="1")和挂号医生(REGISTRATION_DOCTOR, code="12")的所有就诊记录的患者ID
|
||||
List<Long> doctorPatientIds = patientManageMapper.getPatientIdsByPractitionerId(
|
||||
practitioner.getId(),
|
||||
Arrays.asList(ParticipantType.ADMITTER.getCode(), ParticipantType.REGISTRATION_DOCTOR.getCode()));
|
||||
|
||||
totalPatients = doctorPatientIds != null ? doctorPatientIds.size() : 0;
|
||||
} else {
|
||||
// 如果不是医生,查询所有患者(与患者管理页面逻辑保持一致)
|
||||
LambdaQueryWrapper<Patient> patientQuery = new LambdaQueryWrapper<>();
|
||||
patientQuery.eq(Patient::getDeleteFlag, "0");
|
||||
List<Patient> patientList = patientService.list(patientQuery);
|
||||
totalPatients = patientList != null ? patientList.size() : 0;
|
||||
}
|
||||
|
||||
statistics.setTotalPatients(totalPatients);
|
||||
|
||||
// 查询昨日在院患者数量(暂时简化处理)
|
||||
// TODO: 应该从历史记录表中查询昨天的实际在院患者数
|
||||
int yesterdayPatients = totalPatients; // 这里应该是从历史表中查询昨天的数据
|
||||
statistics.setYesterdayPatients(yesterdayPatients);
|
||||
|
||||
// 计算相对前日的百分比
|
||||
double patientTrend = calculateTrend(totalPatients, yesterdayPatients);
|
||||
statistics.setPatientTrend(patientTrend);
|
||||
|
||||
// 今日收入和预约等其他统计(暂时设为0,后续从相应表查询)
|
||||
statistics.setTodayRevenue("¥ 0");
|
||||
statistics.setYesterdayRevenue("¥ 0");
|
||||
statistics.setRevenueTrend(0.0);
|
||||
statistics.setTodayAppointments(0);
|
||||
statistics.setYesterdayAppointments(0);
|
||||
statistics.setAppointmentTrend(0.0);
|
||||
statistics.setPendingApprovals(0);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算相对前日的百分比变化
|
||||
*
|
||||
* @param todayValue 今天的值
|
||||
* @param yesterdayValue 昨天的值
|
||||
* @return 百分比变化(正数表示增长,负数表示下降)
|
||||
*/
|
||||
private double calculateTrend(double todayValue, double yesterdayValue) {
|
||||
if (yesterdayValue == 0) {
|
||||
return todayValue > 0 ? 100.0 : 0.0;
|
||||
}
|
||||
return ((todayValue - yesterdayValue) / yesterdayValue) * 100;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ spring:
|
||||
druid:
|
||||
# 主库数据源
|
||||
master:
|
||||
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=hisdev&characterEncoding=UTF-8&client_encoding=UTF-8
|
||||
url: jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=hisdev&characterEncoding=UTF-8&client_encoding=UTF-8
|
||||
username: postgresql
|
||||
password: Jchl1528
|
||||
# 从库数据源
|
||||
@@ -64,9 +64,9 @@ spring:
|
||||
# redis 配置
|
||||
redis:
|
||||
# 地址
|
||||
host: 192.168.110.252
|
||||
host: 47.116.196.11
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
port: 26379
|
||||
# 数据库索引
|
||||
database: 1
|
||||
# 密码
|
||||
|
||||
@@ -95,5 +95,60 @@
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- 查询门诊记录 -->
|
||||
<select id="getOutpatientRecord" resultType="com.openhis.web.patientmanage.dto.OutpatientRecordDto">
|
||||
SELECT
|
||||
enc.id as encounterId,
|
||||
pt.name,
|
||||
pt.id_card,
|
||||
pt.bus_no as patientBusNo,
|
||||
enc.bus_no as encounterBusNo,
|
||||
pt.gender_enum,
|
||||
pt.phone,
|
||||
enc.create_time as encounterTime,
|
||||
enc.status_enum as subjectStatusEnum,
|
||||
org.name as organizationName,
|
||||
prac.name as doctorName
|
||||
FROM adm_encounter AS enc
|
||||
LEFT JOIN adm_organization AS org ON enc.organization_id = org.ID AND org.delete_flag = '0'
|
||||
LEFT JOIN adm_encounter_participant AS ep
|
||||
ON enc.ID = ep.encounter_id AND ep.type_code = #{participantType} AND ep.delete_flag = '0'
|
||||
LEFT JOIN adm_practitioner AS prac ON ep.practitioner_id = prac.ID AND prac.delete_flag = '0'
|
||||
LEFT JOIN adm_patient AS pt ON enc.patient_id = pt.ID AND pt.delete_flag = '0'
|
||||
<where>
|
||||
enc.delete_flag = '0'
|
||||
<if test="ew.sqlSegment != null and ew.sqlSegment != ''">
|
||||
AND ${ew.sqlSegment}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY enc.create_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
<!-- 获取医生名字列表 -->
|
||||
<select id="getDoctorNames" resultType="java.lang.String">
|
||||
SELECT DISTINCT prac.name
|
||||
FROM adm_practitioner AS prac
|
||||
WHERE prac.delete_flag = '0'
|
||||
ORDER BY prac.name
|
||||
</select>
|
||||
|
||||
<!-- 根据医生ID和参与者类型获取相关的患者ID列表 -->
|
||||
<select id="getPatientIdsByPractitionerId" resultType="java.lang.Long">
|
||||
SELECT DISTINCT enc.patient_id
|
||||
FROM adm_encounter_participant AS ep
|
||||
LEFT JOIN adm_encounter AS enc ON ep.encounter_id = enc.ID AND enc.delete_flag = '0'
|
||||
INNER JOIN adm_patient AS pt ON enc.patient_id = pt.id AND pt.delete_flag = '0'
|
||||
WHERE ep.delete_flag = '0'
|
||||
AND ep.practitioner_id = #{practitionerId}
|
||||
AND ep.tenant_id = 1
|
||||
AND enc.tenant_id = 1
|
||||
AND pt.tenant_id = 1
|
||||
<if test="typeCodes != null and !typeCodes.isEmpty()">
|
||||
AND ep.type_code IN
|
||||
<foreach collection="typeCodes" item="typeCode" open="(" separator="," close=")">
|
||||
#{typeCode}
|
||||
</foreach>
|
||||
</if>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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.core.common.core.domain.HisBaseEntity;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 医生患者关系管理Entity实体
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
@Data
|
||||
@TableName("adm_practitioner_patient")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class PractitionerPatient extends HisBaseEntity {
|
||||
|
||||
/** ID */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 医生ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long practitionerId;
|
||||
|
||||
/** 患者ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long patientId;
|
||||
|
||||
/** 关系类型:1-主治医生,2-签约医生,3-管床医生,4-家庭医生,5-会诊医生,6-随访医生 */
|
||||
private Integer relationshipType;
|
||||
|
||||
/** 机构ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long organizationId;
|
||||
|
||||
/** 关系开始时间 */
|
||||
private Date startDate;
|
||||
|
||||
/** 关系结束时间 */
|
||||
private Date endDate;
|
||||
|
||||
/** 状态:1-有效,0-无效 */
|
||||
private Integer status;
|
||||
|
||||
/** 备注信息 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.openhis.administration.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 医生患者关系DTO
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
@Data
|
||||
public class PractitionerPatientDto implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 医生ID */
|
||||
private Long practitionerId;
|
||||
|
||||
/** 患者ID */
|
||||
private Long patientId;
|
||||
|
||||
/** 关系类型:1-主治医生,2-签约医生,3-管床医生,4-家庭医生,5-会诊医生,6-随访医生 */
|
||||
private Integer relationshipType;
|
||||
|
||||
/** 机构ID */
|
||||
private Long organizationId;
|
||||
|
||||
/** 关系开始时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date startDate;
|
||||
|
||||
/** 备注信息 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.openhis.administration.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.openhis.administration.domain.PractitionerPatient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* 医生患者关系管理Mapper接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
@Repository
|
||||
public interface PractitionerPatientMapper extends BaseMapper<PractitionerPatient> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.openhis.administration.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.openhis.administration.domain.PractitionerPatient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 医生患者关系管理Service接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
public interface IPractitionerPatientService extends IService<PractitionerPatient> {
|
||||
|
||||
/**
|
||||
* 获取医生的所有有效患者
|
||||
*
|
||||
* @param practitionerId 医生ID
|
||||
* @return 患者关系列表
|
||||
*/
|
||||
List<PractitionerPatient> getValidPatientsByPractitioner(Long practitionerId);
|
||||
|
||||
/**
|
||||
* 获取患者的所有有效医生
|
||||
*
|
||||
* @param patientId 患者ID
|
||||
* @return 医生关系列表
|
||||
*/
|
||||
List<PractitionerPatient> getValidPractitionersByPatient(Long patientId);
|
||||
|
||||
/**
|
||||
* 根据关系类型获取医生患者关系
|
||||
*
|
||||
* @param practitionerId 医生ID
|
||||
* @param patientId 患者ID
|
||||
* @param relationshipType 关系类型
|
||||
* @return 医生患者关系
|
||||
*/
|
||||
PractitionerPatient getRelationship(Long practitionerId, Long patientId, Integer relationshipType);
|
||||
|
||||
/**
|
||||
* 创建医生患者关系
|
||||
*
|
||||
* @param practitionerPatient 医生患者关系
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean createRelationship(PractitionerPatient practitionerPatient);
|
||||
|
||||
/**
|
||||
* 终止医生患者关系
|
||||
*
|
||||
* @param id 关系ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean terminateRelationship(Long id);
|
||||
|
||||
/**
|
||||
* 批量创建医生患者关系
|
||||
*
|
||||
* @param relationships 关系列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean batchCreateRelationships(List<PractitionerPatient> relationships);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.openhis.administration.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.openhis.administration.domain.PractitionerPatient;
|
||||
import com.openhis.administration.mapper.PractitionerPatientMapper;
|
||||
import com.openhis.administration.service.IPractitionerPatientService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 医生患者关系管理Service实现
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PractitionerPatientServiceImpl extends ServiceImpl<PractitionerPatientMapper, PractitionerPatient>
|
||||
implements IPractitionerPatientService {
|
||||
|
||||
@Override
|
||||
public List<PractitionerPatient> getValidPatientsByPractitioner(Long practitionerId) {
|
||||
LambdaQueryWrapper<PractitionerPatient> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PractitionerPatient::getPractitionerId, practitionerId)
|
||||
.eq(PractitionerPatient::getStatus, 1)
|
||||
.orderByDesc(PractitionerPatient::getCreateTime);
|
||||
return list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PractitionerPatient> getValidPractitionersByPatient(Long patientId) {
|
||||
LambdaQueryWrapper<PractitionerPatient> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PractitionerPatient::getPatientId, patientId)
|
||||
.eq(PractitionerPatient::getStatus, 1)
|
||||
.orderByDesc(PractitionerPatient::getCreateTime);
|
||||
return list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PractitionerPatient getRelationship(Long practitionerId, Long patientId, Integer relationshipType) {
|
||||
LambdaQueryWrapper<PractitionerPatient> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PractitionerPatient::getPractitionerId, practitionerId)
|
||||
.eq(PractitionerPatient::getPatientId, patientId)
|
||||
.eq(PractitionerPatient::getRelationshipType, relationshipType)
|
||||
.eq(PractitionerPatient::getStatus, 1);
|
||||
return getOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean createRelationship(PractitionerPatient practitionerPatient) {
|
||||
// 设置默认值
|
||||
if (practitionerPatient.getStatus() == null) {
|
||||
practitionerPatient.setStatus(1);
|
||||
}
|
||||
if (practitionerPatient.getStartDate() == null) {
|
||||
practitionerPatient.setStartDate(new Date());
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的关系
|
||||
PractitionerPatient existing = getRelationship(
|
||||
practitionerPatient.getPractitionerId(),
|
||||
practitionerPatient.getPatientId(),
|
||||
practitionerPatient.getRelationshipType()
|
||||
);
|
||||
|
||||
if (existing != null) {
|
||||
// 如果关系已存在,更新结束时间
|
||||
existing.setEndDate(new Date());
|
||||
existing.setStatus(0);
|
||||
updateById(existing);
|
||||
log.info("已终止旧的医患关系:doctorId={}, patientId={}, relationshipType={}",
|
||||
practitionerPatient.getPractitionerId(),
|
||||
practitionerPatient.getPatientId(),
|
||||
practitionerPatient.getRelationshipType());
|
||||
}
|
||||
|
||||
// 创建新关系
|
||||
boolean result = save(practitionerPatient);
|
||||
if (result) {
|
||||
log.info("创建医患关系成功:doctorId={}, patientId={}, relationshipType={}",
|
||||
practitionerPatient.getPractitionerId(),
|
||||
practitionerPatient.getPatientId(),
|
||||
practitionerPatient.getRelationshipType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean terminateRelationship(Long id) {
|
||||
PractitionerPatient relationship = getById(id);
|
||||
if (relationship == null) {
|
||||
log.warn("医患关系不存在:id={}", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
relationship.setEndDate(new Date());
|
||||
relationship.setStatus(0);
|
||||
boolean result = updateById(relationship);
|
||||
if (result) {
|
||||
log.info("终止医患关系成功:id={}, doctorId={}, patientId={}",
|
||||
id, relationship.getPractitionerId(), relationship.getPatientId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean batchCreateRelationships(List<PractitionerPatient> relationships) {
|
||||
if (relationships == null || relationships.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean allSuccess = true;
|
||||
for (PractitionerPatient relationship : relationships) {
|
||||
boolean success = createRelationship(relationship);
|
||||
if (!success) {
|
||||
allSuccess = false;
|
||||
log.error("批量创建医患关系失败:doctorId={}, patientId={}",
|
||||
relationship.getPractitionerId(), relationship.getPatientId());
|
||||
}
|
||||
}
|
||||
|
||||
if (allSuccess) {
|
||||
log.info("批量创建医患关系成功:count={}", relationships.size());
|
||||
}
|
||||
return allSuccess;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.openhis.administration.mapper.PractitionerPatientMapper">
|
||||
|
||||
<resultMap type="com.openhis.administration.domain.PractitionerPatient" id="PractitionerPatientResult">
|
||||
<result property="id" column="id" />
|
||||
<result property="practitionerId" column="practitioner_id" />
|
||||
<result property="patientId" column="patient_id" />
|
||||
<result property="relationshipType" column="relationship_type" />
|
||||
<result property="organizationId" column="organization_id" />
|
||||
<result property="startDate" column="start_date" />
|
||||
<result property="endDate" column="end_date" />
|
||||
<result property="status" column="status" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="tenantId" column="tenant_id" />
|
||||
<result property="deleteFlag" column="delete_flag" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPractitionerPatientVo">
|
||||
select id, practitioner_id, patient_id, relationship_type, organization_id,
|
||||
start_date, end_date, status, remark, tenant_id,
|
||||
delete_flag, create_by, create_time, update_by, update_time
|
||||
from adm_practitioner_patient
|
||||
</sql>
|
||||
|
||||
<select id="selectPractitionerPatientList" parameterType="com.openhis.administration.domain.PractitionerPatient" resultMap="PractitionerPatientResult">
|
||||
<include refid="selectPractitionerPatientVo"/>
|
||||
<where>
|
||||
delete_flag = '0'
|
||||
<if test="practitionerId != null">
|
||||
and practitioner_id = #{practitionerId}
|
||||
</if>
|
||||
<if test="patientId != null">
|
||||
and patient_id = #{patientId}
|
||||
</if>
|
||||
<if test="relationshipType != null">
|
||||
and relationship_type = #{relationshipType}
|
||||
</if>
|
||||
<if test="organizationId != null">
|
||||
and organization_id = #{organizationId}
|
||||
</if>
|
||||
<if test="status != null">
|
||||
and status = #{status}
|
||||
</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectPractitionerPatientById" parameterType="Long" resultMap="PractitionerPatientResult">
|
||||
<include refid="selectPractitionerPatientVo"/>
|
||||
where id = #{id} and delete_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 获取医生的所有有效患者(带详细信息) -->
|
||||
<select id="getValidPatientsByPractitionerWithDetail" parameterType="Long" resultType="java.util.Map">
|
||||
SELECT
|
||||
pp.id as relationship_id,
|
||||
pp.practitioner_id,
|
||||
pp.patient_id,
|
||||
pp.relationship_type,
|
||||
pp.start_date,
|
||||
pp.end_date,
|
||||
pp.status,
|
||||
pp.remark,
|
||||
pt.name as patient_name,
|
||||
pt.bus_no as patient_bus_no,
|
||||
pt.gender_enum as patient_gender,
|
||||
pt.phone as patient_phone,
|
||||
pt.id_card as patient_id_card,
|
||||
pt.birth_date as patient_birth_date
|
||||
FROM adm_practitioner_patient pp
|
||||
LEFT JOIN adm_patient pt ON pp.patient_id = pt.ID AND pt.delete_flag = '0'
|
||||
WHERE pp.practitioner_id = #{practitionerId}
|
||||
AND pp.status = 1
|
||||
AND pp.delete_flag = '0'
|
||||
ORDER BY pp.create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 获取患者的所有有效医生(带详细信息) -->
|
||||
<select id="getValidPractitionersByPatientWithDetail" parameterType="Long" resultType="java.util.Map">
|
||||
SELECT
|
||||
pp.id as relationship_id,
|
||||
pp.practitioner_id,
|
||||
pp.patient_id,
|
||||
pp.relationship_type,
|
||||
pp.start_date,
|
||||
pp.end_date,
|
||||
pp.status,
|
||||
pp.remark,
|
||||
prac.name as practitioner_name,
|
||||
prac.bus_no as practitioner_bus_no,
|
||||
prac.gender_enum as practitioner_gender,
|
||||
prac.phone as practitioner_phone,
|
||||
prac.dr_profttl_code as practitioner_title,
|
||||
org.name as organization_name
|
||||
FROM adm_practitioner_patient pp
|
||||
LEFT JOIN adm_practitioner prac ON pp.practitioner_id = prac.ID AND prac.delete_flag = '0'
|
||||
LEFT JOIN adm_organization org ON pp.organization_id = org.ID AND org.delete_flag = '0'
|
||||
WHERE pp.patient_id = #{patientId}
|
||||
AND pp.status = 1
|
||||
AND pp.delete_flag = '0'
|
||||
ORDER BY pp.relationship_type, pp.create_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -40,7 +40,7 @@
|
||||
<tomcat.version>9.0.96</tomcat.version>
|
||||
<logback.version>1.2.13</logback.version>
|
||||
<lombok.version>1.18.34</lombok.version> <!-- 替换为 -->
|
||||
<mybatis-plus.version>3.5.3</mybatis-plus.version>
|
||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||
<flowable.version>6.8.0</flowable.version>
|
||||
<postgresql.version>42.2.27</postgresql.version>
|
||||
<aviator.version>5.3.3</aviator.version>
|
||||
@@ -56,6 +56,7 @@
|
||||
<itext-asian.version>5.2.0</itext-asian.version>
|
||||
<mysql-connector-j.version>9.4.0</mysql-connector-j.version>
|
||||
<jsr250.version>1.3.2</jsr250.version>
|
||||
<jsqlparser.version>4.5</jsqlparser.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
@@ -340,6 +341,19 @@
|
||||
<version>1.9.10</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSQlParser - MyBatis Plus 3.5.8 使用4.5版本 -->
|
||||
<dependency>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
<version>${jsqlparser.version}</version>
|
||||
</dependency>
|
||||
<!-- JSQlParser - MyBatis Plus 3.5.9+ 需要 4.6+ -->
|
||||
<dependency>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
<version>${jsqlparser.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
105
openhis-ui-vue3/src/api/administration/practitionerPatient.js
Normal file
105
openhis-ui-vue3/src/api/administration/practitionerPatient.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 查询医生患者关系列表
|
||||
*/
|
||||
export function listPractitionerPatient(query) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询医生患者关系详细
|
||||
*/
|
||||
export function getPractitionerPatient(id) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取医生的所有有效患者
|
||||
*/
|
||||
export function getPatientsByPractitioner(practitionerId) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/practitioner/' + practitionerId + '/patients',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取患者的所有有效医生
|
||||
*/
|
||||
export function getPractitionersByPatient(patientId) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/patient/' + patientId + '/practitioners',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增医生患者关系
|
||||
*/
|
||||
export function addPractitionerPatient(data) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改医生患者关系
|
||||
*/
|
||||
export function updatePractitionerPatient(data) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 终止医生患者关系
|
||||
*/
|
||||
export function terminatePractitionerPatient(id) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/terminate/' + id,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除医生患者关系
|
||||
*/
|
||||
export function delPractitionerPatient(id) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/' + id,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除医生患者关系
|
||||
*/
|
||||
export function delPractitionerPatientBatch(ids) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建医生患者关系
|
||||
*/
|
||||
export function batchAddPractitionerPatient(data) {
|
||||
return request({
|
||||
url: '/administration/practitioner-patient/batch',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
9
openhis-ui-vue3/src/api/home.js
Normal file
9
openhis-ui-vue3/src/api/home.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取首页统计数据
|
||||
export function getHomeStatistics() {
|
||||
return request({
|
||||
url: '/home/statistics',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -92,6 +92,27 @@ export const constantRoutes = [
|
||||
path: '/tpr',
|
||||
component: () => import('@/views/inpatientNurse/tprsheet/index.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/patientmanagement',
|
||||
// component: Layout,
|
||||
// redirect: '/patientmanagement/patientmanagement',
|
||||
// name: 'PatientManagement',
|
||||
// meta: { title: '患者管理', icon: 'patient' },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'patientmanagement',
|
||||
// component: () => import('@/views/patientmanagement/patientmanagement/index.vue'),
|
||||
// name: 'PatientManagementList',
|
||||
// meta: { title: '患者档案管理', icon: 'patient' },
|
||||
// },
|
||||
// {
|
||||
// path: 'outpatienrecords',
|
||||
// component: () => import('@/views/patientmanagement/outpatienrecords/index.vue'),
|
||||
// name: 'OutpatientRecords',
|
||||
// meta: { title: '门诊就诊记录', icon: 'record' },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
|
||||
// 动态路由 - 基于用户权限动态加载的路由
|
||||
|
||||
@@ -20,7 +20,8 @@ const useUserStore = defineStore(
|
||||
permissions: [],
|
||||
tenantId: '',
|
||||
tenantName: '', // 租户名称
|
||||
hospitalName:''
|
||||
hospitalName:'',
|
||||
optionMap: {} // 租户配置项Map(从sys_tenant_option表读取)
|
||||
}),
|
||||
actions: {
|
||||
// 登录
|
||||
@@ -63,7 +64,9 @@ const useUserStore = defineStore(
|
||||
this.practitionerId = res.practitionerId
|
||||
this.fixmedinsCode = res.optionJson.fixmedinsCode
|
||||
this.avatar = avatar
|
||||
this.hospitalName = res.optionJson.hospitalName
|
||||
this.optionMap = res.optionMap || {}
|
||||
// 优先从optionMap获取配置,如果没有则从optionJson获取
|
||||
this.hospitalName = this.optionMap.hospitalName || res.optionJson.hospitalName || ''
|
||||
this.tenantName = res.tenantName || ''
|
||||
|
||||
resolve(res)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,17 +111,6 @@
|
||||
<!-- 底部 -->
|
||||
|
||||
<div class="el-login-footer">
|
||||
<div class="el-login-footer-link">
|
||||
<span><el-link :underline="false">账号用户协议</el-link></span>
|
||||
<span>|</span>
|
||||
<span>
|
||||
<el-link :underline="false">关于账号与隐私的声明</el-link>
|
||||
</span>
|
||||
<span>|</span>
|
||||
<span><el-link :underline="false">常见问题</el-link></span>
|
||||
<span>|</span>
|
||||
<span><el-link :underline="false">Cookies</el-link></span>
|
||||
</div>
|
||||
<span>
|
||||
<el-link
|
||||
:underline="false"
|
||||
@@ -595,7 +584,7 @@ html, body {
|
||||
}
|
||||
}
|
||||
.el-login-footer {
|
||||
height: 80px;
|
||||
height: 40px;
|
||||
line-height: 30px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
@@ -143,30 +143,35 @@
|
||||
<el-table-column prop="creator" label="操作人" width="100" align="center" />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="Edit"
|
||||
circle
|
||||
@click="handleEdit(row)"
|
||||
title="编辑"
|
||||
/>
|
||||
<el-button
|
||||
type="info"
|
||||
size="small"
|
||||
icon="View"
|
||||
circle
|
||||
@click="handleView(row)"
|
||||
title="查看"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="Delete"
|
||||
circle
|
||||
@click="handleDelete(row)"
|
||||
title="删除"
|
||||
/>
|
||||
<div class="actions">
|
||||
<el-button
|
||||
class="btn btn-edit"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleEdit(row)"
|
||||
title="编辑"
|
||||
>
|
||||
✏️
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn btn-view"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleView(row)"
|
||||
title="查看"
|
||||
>
|
||||
👁️
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn btn-delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleDelete(row)"
|
||||
title="删除"
|
||||
>
|
||||
✕
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -612,10 +617,84 @@ function handleDelete(row) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* 统一的操作按钮样式 */
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px) scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0) scale(0.95);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: linear-gradient(135deg, #52C41A 0%, #73d13d 100%);
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: linear-gradient(135deg, #389E0D 0%, #52C41A 100%);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background: linear-gradient(135deg, #722ED1 0%, #9254DE 100%);
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
background: linear-gradient(135deg, #531DAE 0%, #722ED1 100%);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: linear-gradient(135deg, #FF4D4F 0%, #ff7875 100%);
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: linear-gradient(135deg, #CF1322 0%, #FF4D4F 100%);
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 - 支持水平和垂直滚动 */
|
||||
|
||||
@@ -337,36 +337,46 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
v-if="!row.editing"
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="Edit"
|
||||
circle
|
||||
@click="handleEditRow(row)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="row.editing"
|
||||
type="success"
|
||||
size="small"
|
||||
icon="Check"
|
||||
circle
|
||||
@click="handleConfirmRow(row)"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="Plus"
|
||||
circle
|
||||
@click="handleAddRow"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="Delete"
|
||||
circle
|
||||
@click="handleDeleteRow($index)"
|
||||
/>
|
||||
<div class="actions">
|
||||
<el-button
|
||||
v-if="!row.editing"
|
||||
class="btn btn-edit"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleEditRow(row)"
|
||||
title="编辑"
|
||||
>
|
||||
✏️
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.editing"
|
||||
class="btn btn-confirm"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleConfirmRow(row)"
|
||||
title="保存"
|
||||
>
|
||||
✓
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn btn-add"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleAddRow"
|
||||
title="添加"
|
||||
>
|
||||
+
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn btn-delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="handleDeleteRow($index)"
|
||||
title="删除"
|
||||
>
|
||||
✕
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -1229,6 +1239,76 @@ async function handleSave() {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 统一的操作按钮样式 */
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px) scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0) scale(0.95);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: linear-gradient(135deg, #52C41A 0%, #73d13d 100%);
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: linear-gradient(135deg, #389E0D 0%, #52C41A 100%);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: linear-gradient(135deg, #FF4D4F 0%, #ff7875 100%);
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: linear-gradient(135deg, #CF1322 0%, #FF4D4F 100%);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.package-settings {
|
||||
@@ -1249,4 +1329,3 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
<template>
|
||||
<div class="check-project-settings">
|
||||
<!-- 左侧导航栏 -->
|
||||
<div class="sidebar" style="margin-top: 100px;">
|
||||
<!-- 明确列出所有导航项 -->
|
||||
<button
|
||||
class="menu-item active"
|
||||
@click="handleMenuClick('检查类型')"
|
||||
style="display: block; width: 100%; height: 40px; padding: 8px; text-align: center;"
|
||||
>
|
||||
检查类型
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@click="handleMenuClick('检查方法')"
|
||||
style="display: block; width: 100%; height: 40px; padding: 8px; text-align: center;"
|
||||
>
|
||||
检查方法
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@click="handleMenuClick('检查部位')"
|
||||
style="display: block; width: 100%; height: 40px; padding: 8px; text-align: center;"
|
||||
>
|
||||
检查部位
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@click="handleMenuClick('套餐设置')"
|
||||
style="display: block; width: 100%; height: 40px; padding: 8px; text-align: center;"
|
||||
>
|
||||
套餐设置
|
||||
</button>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>检查项目管理</h3>
|
||||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<!-- 明确列出所有导航项 -->
|
||||
<button
|
||||
class="menu-item active"
|
||||
@click="handleMenuClick('检查类型')"
|
||||
>
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">检查类型</span>
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@click="handleMenuClick('检查方法')"
|
||||
>
|
||||
<span class="menu-icon">🔬</span>
|
||||
<span class="menu-text">检查方法</span>
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@click="handleMenuClick('检查部位')"
|
||||
>
|
||||
<span class="menu-icon">🎯</span>
|
||||
<span class="menu-text">检查部位</span>
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
@click="handleMenuClick('套餐设置')"
|
||||
>
|
||||
<span class="menu-icon">📦</span>
|
||||
<span class="menu-text">套餐设置</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
@@ -165,26 +170,27 @@
|
||||
</td>
|
||||
<td class="actions">
|
||||
<template v-if="item.actions">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)" title="保存">
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
v-if="!item.row.includes('.')"
|
||||
class="btn btn-add"
|
||||
<button
|
||||
v-if="!item.row.includes('.')"
|
||||
class="btn btn-add"
|
||||
@click.stop="handleAdd(index)"
|
||||
title="添加子项"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)">
|
||||
🗑
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)" title="删除">
|
||||
✕
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)" title="保存">
|
||||
✓
|
||||
</button>
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)">
|
||||
🗑
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)" title="删除">
|
||||
✕
|
||||
</button>
|
||||
</template>
|
||||
</td>
|
||||
@@ -329,22 +335,22 @@
|
||||
</td>
|
||||
<td class="actions">
|
||||
<template v-if="item.editing">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)" title="保存">
|
||||
✓
|
||||
</button>
|
||||
<button class="btn btn-cancel" @click.stop="handleCancelEdit(index)">
|
||||
<button class="btn btn-cancel" @click.stop="handleCancelEdit(index)" title="取消">
|
||||
✕
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn btn-edit" @click.stop="handleEdit(index)">
|
||||
<button class="btn btn-edit" @click.stop="handleEdit(index)" title="编辑">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)" title="保存">
|
||||
✓
|
||||
</button>
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)">
|
||||
🗑
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)" title="删除">
|
||||
✕
|
||||
</button>
|
||||
</template>
|
||||
</td>
|
||||
@@ -515,22 +521,22 @@
|
||||
</td>
|
||||
<td class="actions">
|
||||
<template v-if="item.editing">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)" title="保存">
|
||||
✓
|
||||
</button>
|
||||
<button class="btn btn-cancel" @click.stop="handleCancelEdit(index)">
|
||||
<button class="btn btn-cancel" @click.stop="handleCancelEdit(index)" title="取消">
|
||||
✕
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn btn-edit" @click.stop="handleEdit(index)">
|
||||
<button class="btn btn-edit" @click.stop="handleEdit(index)" title="编辑">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)">
|
||||
<button class="btn btn-confirm" @click.stop="handleConfirm(index)" title="保存">
|
||||
✓
|
||||
</button>
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)">
|
||||
🗑
|
||||
<button class="btn btn-delete" @click.stop="handleDelete(index)" title="删除">
|
||||
✕
|
||||
</button>
|
||||
</template>
|
||||
</td>
|
||||
@@ -1441,79 +1447,150 @@ select {
|
||||
|
||||
/* 左侧导航栏样式 */
|
||||
.sidebar {
|
||||
width: 160px;
|
||||
background-color: #FFFFFF;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 16px 8px;
|
||||
box-shadow: 1px 0 3px rgba(0, 0, 0, 0.05);
|
||||
z-index: 10;
|
||||
width: 200px;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 24px 0;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 20px 20px 20px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 2px solid #1890FF;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1890FF;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #FFFFFF;
|
||||
color: #000000;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 0;
|
||||
background-color: transparent;
|
||||
color: #333333;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
border: none;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 0;
|
||||
background: linear-gradient(180deg, #1890FF 0%, #40a9ff 100%);
|
||||
border-radius: 0 4px 4px 0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
background-color: rgba(24, 144, 255, 0.08);
|
||||
border-color: rgba(24, 144, 255, 0.2);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.menu-item:hover::before {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background-color: #1890FF;
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
color: #FFFFFF;
|
||||
border-color: #1890FF;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.menu-item.active::before {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* 主内容区样式 */
|
||||
.content {
|
||||
flex: 1;
|
||||
margin-left: 160px;
|
||||
padding: 24px;
|
||||
width: calc(100% - 160px);
|
||||
height: 100vh;
|
||||
padding: 32px 32px 32px 32px;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
padding: 20px 24px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #1890FF;
|
||||
margin: 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 搜索栏样式 */
|
||||
.search-bar {
|
||||
background-color: #FFFFFF;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid #D9D9D9;
|
||||
padding: 20px 24px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -1628,9 +1705,9 @@ select {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -1642,14 +1719,16 @@ table {
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
height: 40px;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #fafbfc 100%);
|
||||
height: 48px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #D9D9D9;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
color: #1890FF;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -1666,57 +1745,78 @@ td {
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: #66b1ff;
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px) scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0) scale(0.95);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: linear-gradient(135deg, #52C41A 0%, #73d13d 100%);
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: linear-gradient(135deg, #389E0D 0%, #52C41A 100%);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: linear-gradient(135deg, #FA8C16 0%, #ffa940 100%);
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: linear-gradient(135deg, #D46B08 0%, #FA8C16 100%);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background-color: #FF4D4F;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(135deg, #FF4D4F 0%, #ff7875 100%);
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: #1890FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: #FFC107;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background-color: #1890FF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background-color: #FF4D4F;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
.btn-delete:hover {
|
||||
background: linear-gradient(135deg, #CF1322 0%, #FF4D4F 100%);
|
||||
}
|
||||
|
||||
/* 特殊状态样式 */
|
||||
@@ -1803,16 +1903,45 @@ input[type="text"]::placeholder {
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 60px;
|
||||
width: 70px;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.menu-item span {
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 8px 16px 8px;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 60px;
|
||||
padding: 16px;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板适配 */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.sidebar {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,11 +8,9 @@ export function listOutpatienRecords(query) {
|
||||
})
|
||||
}
|
||||
|
||||
export function listDoctorNames() {
|
||||
export function listDoctorNames() {
|
||||
return request({
|
||||
url: '/patient-manage/records/init',
|
||||
url: '/patient-manage/records/doctor-names',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<el-form-item label="查询内容" prop="searchKey">
|
||||
<el-input
|
||||
v-model="queryParams.searchKey"
|
||||
placeholder="身份证号/病人ID/门诊号/姓名"
|
||||
placeholder="姓名/身份证号/病人ID/门诊号"
|
||||
clearable
|
||||
style="width: 210px"
|
||||
style="width: 240px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -15,7 +15,7 @@
|
||||
v-model="queryParams.phone"
|
||||
placeholder="请输入联系方式"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
style="width: 150px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -27,14 +27,39 @@
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 240px"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="genderEnum">
|
||||
<el-select
|
||||
v-model="queryParams.genderEnum"
|
||||
placeholder="请选择性别"
|
||||
clearable
|
||||
style="width: 100px"
|
||||
>
|
||||
<el-option label="男" :value="1" />
|
||||
<el-option label="女" :value="2" />
|
||||
<el-option label="未知" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="subjectStatusEnum">
|
||||
<el-select
|
||||
v-model="queryParams.subjectStatusEnum"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="待就诊" :value="1" />
|
||||
<el-option label="就诊中" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已取消" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="医生" prop="doctorName">
|
||||
<el-select
|
||||
v-model="queryParams.doctorName"
|
||||
placeholder="请选择医生"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option
|
||||
@@ -51,18 +76,34 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="outpatienRecordsList" border style="width: 100%">
|
||||
<el-table-column prop="name" label="患者" width="180" />
|
||||
<el-table-column prop="idCard" label="身份证" width="180" />
|
||||
<el-table-column prop="description" label="疾病" width="180" />
|
||||
<el-table-column prop="patientBusNo" label="病人ID" width="180" />
|
||||
<el-table-column prop="encounterBusNo" label="门诊号" width="180" />
|
||||
<el-table-column prop="genderEnum_enumText" label="性别" width="80" />
|
||||
<el-table
|
||||
:data="outpatienRecordsList"
|
||||
border
|
||||
style="width: 100%"
|
||||
:default-sort="{ prop: 'encounterTime', order: 'descending' }"
|
||||
v-loading="loading"
|
||||
:header-cell-style="{ background: '#f5f7fa', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column prop="name" label="患者" min-width="100" />
|
||||
<el-table-column prop="genderEnum_enumText" label="性别" width="80" align="center" />
|
||||
<el-table-column prop="idCard" label="身份证" min-width="160" :show-overflow-tooltip="true" />
|
||||
<el-table-column prop="phone" label="电话" width="120" />
|
||||
<el-table-column prop="encounterTime" label="就诊时间" width="180" />
|
||||
<el-table-column prop="subjectStatusEnum_enumText" label="状态" width="120" />
|
||||
<el-table-column prop="organizationName" label="接诊医院" width="180" />
|
||||
<el-table-column prop="doctorName" label="接诊医生" width="180" />
|
||||
<el-table-column prop="patientBusNo" label="病人ID" width="100" align="center" />
|
||||
<el-table-column prop="encounterBusNo" label="门诊号" width="120" align="center" />
|
||||
<el-table-column prop="encounterTime" label="就诊时间" width="160" sortable />
|
||||
<el-table-column prop="doctorName" label="接诊医生" width="120" />
|
||||
<el-table-column prop="organizationName" label="医疗机构" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column prop="subjectStatusEnum_enumText" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="getStatusTagType(scope.row.subjectStatusEnum)"
|
||||
size="small"
|
||||
>
|
||||
{{ scope.row.subjectStatusEnum_enumText }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 移除疾病描述列,因为当前数据中没有这个字段 -->
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
@@ -75,16 +116,19 @@
|
||||
</template>
|
||||
|
||||
<script setup name="outpatienRecords">
|
||||
import {computed, ref} from 'vue';
|
||||
import {computed, ref, reactive, toRefs, getCurrentInstance} from 'vue';
|
||||
import {listDoctorNames, listOutpatienRecords} from './component/api';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const showSearch = ref(true);
|
||||
const total = ref(0);
|
||||
const dateRange = ref([]);
|
||||
const outpatienRecordsList = ref([]);
|
||||
const doctorList = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const route = useRoute();
|
||||
|
||||
const data = reactive({
|
||||
form: {},
|
||||
@@ -94,7 +138,8 @@ const data = reactive({
|
||||
doctorName: undefined,
|
||||
searchKey: undefined,
|
||||
phone: undefined,
|
||||
patientid: undefined,
|
||||
genderEnum: undefined,
|
||||
subjectStatusEnum: undefined,
|
||||
},
|
||||
});
|
||||
const { queryParams } = toRefs(data);
|
||||
@@ -108,23 +153,60 @@ const doctorOptions = computed(() => {
|
||||
|
||||
/** 查询门诊记录列表 */
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
// 如果路由中有患者ID参数,则自动填充到查询条件中
|
||||
if (route.query.patientId) {
|
||||
queryParams.value.searchKey = route.query.patientId;
|
||||
}
|
||||
if (route.query.patientName) {
|
||||
// 可以在页面标题或其他地方显示患者姓名
|
||||
console.log('当前查看患者:', route.query.patientName);
|
||||
}
|
||||
|
||||
listOutpatienRecords(queryParams.value).then((response) => {
|
||||
outpatienRecordsList.value = response.data.records;
|
||||
total.value = response.data.total;
|
||||
loading.value = false;
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
listDoctorNames().then((response) => {
|
||||
console.log(response);
|
||||
doctorList.value = response.data;
|
||||
console.log(doctorList.value, 'doctorList.value');
|
||||
});
|
||||
|
||||
// 只在医生列表为空时加载医生列表
|
||||
if (doctorList.value.length === 0) {
|
||||
listDoctorNames().then((response) => {
|
||||
doctorList.value = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据状态获取标签类型 */
|
||||
function getStatusTagType(status) {
|
||||
// 假设状态值:1-待就诊,2-就诊中,3-已完成,4-已取消
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'warning'; // 待就诊 - 黄色
|
||||
case 2:
|
||||
return 'primary'; // 就诊中 - 蓝色
|
||||
case 3:
|
||||
return 'success'; // 已完成 - 绿色
|
||||
case 4:
|
||||
return 'info'; // 已取消 - 灰色
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.value.startTimeSTime =
|
||||
dateRange.value && dateRange.value.length == 2 ? dateRange.value[0] : '';
|
||||
queryParams.value.startTimeETime =
|
||||
dateRange.value && dateRange.value.length == 2 ? dateRange.value[1] : '';
|
||||
// 处理时间范围参数
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.value.startTimeSTime = dateRange.value[0];
|
||||
queryParams.value.startTimeETime = dateRange.value[1];
|
||||
} else {
|
||||
queryParams.value.startTimeSTime = '';
|
||||
queryParams.value.startTimeETime = '';
|
||||
}
|
||||
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="patientList" border>
|
||||
<el-table-column prop="idCard" label="身份证号" width="180" />
|
||||
<el-table-column prop="busNo" label="病人ID" width="180" />
|
||||
<el-table-column prop="name" label="病人名称" width="180" />
|
||||
<el-table-column prop="genderEnum_enumText" label="性别" width="180">
|
||||
<el-table :data="patientList" border size="small" :header-cell-style="{padding: '8px 0'}" :cell-style="{padding: '6px 0'}">
|
||||
<el-table-column prop="idCard" label="身份证号" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="busNo" label="病人ID" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="病人名称" width="80" />
|
||||
<el-table-column prop="genderEnum_enumText" label="性别" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="patient_gender_enum" :value="scope.row.genderEnum" class="dict-tag" />
|
||||
</template>
|
||||
@@ -47,55 +47,57 @@
|
||||
<el-table-column
|
||||
prop="maritalStatusEnum_enumText"
|
||||
label="婚姻状况"
|
||||
width="180"
|
||||
width="80"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<dict-tag :options="marital_status_enum" :value="scope.row.maritalStatusEnum" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nationalityCode" label="民族" width="180">
|
||||
<el-table-column prop="nationalityCode" label="民族" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="nationality_code" :value="scope.row.nationalityCode" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="birthDate" label="生日" width="160" />
|
||||
<el-table-column prop="phone" label="电话" width="140" />
|
||||
<el-table-column prop="bloodAbo_enumText" label="血型ABO" width="140">
|
||||
<el-table-column prop="birthDate" label="生日" width="100" />
|
||||
<el-table-column prop="phone" label="电话" width="110" />
|
||||
<el-table-column prop="bloodAbo_enumText" label="血型ABO" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="blood_abo" :value="scope.row.bloodAbo" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bloodRh_enumText" label="血型RH" width="140">
|
||||
<el-table-column prop="bloodRh_enumText" label="血型RH" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="blood_rh" :value="scope.row.bloodRh" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="linkName" label="联系人" width="180" />
|
||||
<el-table-column prop="linkTelcom" label="联系人电话" width="180" />
|
||||
<el-table-column prop="linkRelationCode_enumText" label="联系人关系" width="180">
|
||||
<el-table-column prop="linkName" label="联系人" width="80" />
|
||||
<el-table-column prop="linkTelcom" label="联系人电话" width="110" />
|
||||
<el-table-column prop="linkRelationCode_enumText" label="联系人关系" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="link_relation_code" :value="scope.row.linkRelationCode" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="家庭地址" width="180" />
|
||||
<el-table-column prop="prfsEnum_enumText" label="职业" width="180">
|
||||
<el-table-column prop="address" label="家庭地址" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="prfsEnum_enumText" label="职业" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="prfs_enum" :value="scope.row.prfsEnum" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="workCompany" label="工作单位" width="180" />
|
||||
<el-table-column prop="organizationName" label="登记医院" width="180" />
|
||||
<el-table-column prop="deceasedDate" label="死亡时间" width="180" />
|
||||
<el-table-column prop="createTime" label="登记时间" width="180" />
|
||||
<el-table-column prop="workCompany" label="工作单位" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="organizationName" label="登记医院" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="deceasedDate" label="死亡时间" width="100" />
|
||||
<el-table-column prop="createTime" label="登记时间" width="140" />
|
||||
<el-table-column
|
||||
label="操作"
|
||||
align="center"
|
||||
width="210"
|
||||
class-name="small-padding fixed-width"
|
||||
>
|
||||
width="220"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" class="action-button">修改</el-button>
|
||||
<el-button link type="primary" icon="View" @click="handleSee(scope.row)" class="action-button">查看</el-button>
|
||||
<el-button link type="success" icon="Clock" @click="handleVisitHistory(scope.row)" class="action-button">就诊历史</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -174,7 +176,15 @@
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="证件号码" prop="idCard">
|
||||
<el-input v-model="form.idCard" clearable :disabled="isViewMode" />
|
||||
<el-input
|
||||
v-model="form.idCard"
|
||||
clearable
|
||||
:disabled="isViewMode"
|
||||
placeholder="请输入18位身份证号"
|
||||
maxlength="18"
|
||||
show-word-limit
|
||||
@blur="handleIdCardBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="生日" prop="birthDate" v-show="false">
|
||||
<el-input v-model="form.birthDate" v-show="false" />
|
||||
@@ -424,6 +434,10 @@ const data = reactive({
|
||||
name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
|
||||
age: [{ required: true, message: '年龄不能为空', trigger: 'change' }],
|
||||
phone: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
|
||||
idCard: [
|
||||
{ required: true, message: '身份证号不能为空', trigger: 'blur' },
|
||||
{ validator: validateIdCard, trigger: 'blur' }
|
||||
],
|
||||
},
|
||||
});
|
||||
const { queryParams, form, rules, isViewMode } = toRefs(data);
|
||||
@@ -572,6 +586,18 @@ function handleSee(row) {
|
||||
title.value = '查看患者';
|
||||
});
|
||||
}
|
||||
|
||||
// 查看就诊历史
|
||||
function handleVisitHistory(row) {
|
||||
// 跳转到门诊记录页面,并传递患者ID参数
|
||||
proxy.$router.push({
|
||||
path: '/patient/patienrecords',
|
||||
query: {
|
||||
patientId: row.busNo,
|
||||
patientName: row.name
|
||||
}
|
||||
});
|
||||
}
|
||||
// 映射
|
||||
const nationalityDict = (code) => {
|
||||
const findObj = nationality_code.value.find((item) => item.value === code);
|
||||
@@ -638,6 +664,199 @@ function getAddress(form) {
|
||||
return part ? acc + part : acc;
|
||||
}, '');
|
||||
}
|
||||
/** 身份证号校验函数 */
|
||||
function validateIdCard(rule, value, callback) {
|
||||
if (!value) {
|
||||
return callback(new Error('身份证号不能为空'));
|
||||
}
|
||||
|
||||
// 基本格式校验
|
||||
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
||||
if (!idCardReg.test(value)) {
|
||||
return callback(new Error('身份证号格式不正确'));
|
||||
}
|
||||
|
||||
// 校验码验证
|
||||
const isValid = checkIdCardCode(value);
|
||||
if (!isValid) {
|
||||
return callback(new Error('身份证号校验码不正确,请检查'));
|
||||
}
|
||||
|
||||
// 日期验证
|
||||
const isValidDate = checkIdCardDate(value);
|
||||
if (!isValidDate) {
|
||||
return callback(new Error('身份证号中的日期不合法'));
|
||||
}
|
||||
|
||||
// 地区码验证(可选)
|
||||
const isValidArea = checkIdCardArea(value);
|
||||
if (!isValidArea) {
|
||||
return callback(new Error('身份证号中的地区码不合法'));
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
/** 身份证号校验码验证 */
|
||||
function checkIdCardCode(idCard) {
|
||||
const city = [11, 12, 13, 14, 15, 21, 22, 23, 31, 32, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 54, 61, 62, 63, 64, 65, 71, 81, 82, 91];
|
||||
|
||||
if (idCard.length === 15) {
|
||||
// 15位身份证号不校验码
|
||||
return true;
|
||||
}
|
||||
|
||||
if (idCard.length === 18) {
|
||||
// 18位身份证号校验码验证
|
||||
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const remainder = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += parseInt(idCard.charAt(i)) * coefficient[i];
|
||||
}
|
||||
const code = sum % 11;
|
||||
const lastChar = idCard.charAt(17).toUpperCase();
|
||||
return lastChar === remainder[code];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 身份证号日期验证 */
|
||||
function checkIdCardDate(idCard) {
|
||||
let year, month, day;
|
||||
|
||||
if (idCard.length === 15) {
|
||||
// 15位身份证号:6位年份
|
||||
year = '19' + idCard.substring(6, 8);
|
||||
month = idCard.substring(8, 10);
|
||||
day = idCard.substring(10, 12);
|
||||
} else if (idCard.length === 18) {
|
||||
// 18位身份证号:4位年份
|
||||
year = idCard.substring(6, 10);
|
||||
month = idCard.substring(10, 12);
|
||||
day = idCard.substring(12, 14);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查日期是否合法
|
||||
const date = new Date(year, parseInt(month) - 1, parseInt(day));
|
||||
const now = new Date();
|
||||
|
||||
// 检查年月日是否有效
|
||||
if (date.getFullYear() !== parseInt(year) ||
|
||||
date.getMonth() + 1 !== parseInt(month) ||
|
||||
date.getDate() !== parseInt(day)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是未来日期
|
||||
if (date > now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是合理的年份(100岁以上或刚出生)
|
||||
const age = now.getFullYear() - date.getFullYear();
|
||||
if (age < 0 || age > 150) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 身份证号地区码验证 */
|
||||
function checkIdCardArea(idCard) {
|
||||
const areaCode = idCard.substring(0, 6);
|
||||
const validAreas = [
|
||||
'110000', '110101', '110102', '110105', '110106', '110107', '110108', '110109', '110111', '110112', '110113', '110114', '110115', '110116', '110117', '110118', '110119', // 北京
|
||||
'120000', '120101', '120102', '120103', '120104', '120105', '120106', '120110', '120111', '120112', '120113', '120114', '120115', '120116', '120117', '120118', '120119', // 天津
|
||||
'130000', // 河北
|
||||
'140000', // 山西
|
||||
'150000', // 内蒙古
|
||||
'210000', // 辽宁
|
||||
'220000', // 吉林
|
||||
'230000', // 黑龙江
|
||||
'310000', '310101', '310104', '310105', '310106', '310110', '310112', '310113', '310114', '310115', '310116', '310117', '310118', '310120', '310151', // 上海
|
||||
'320000', // 江苏
|
||||
'330000', // 浙江
|
||||
'340000', // 安徽
|
||||
'350000', // 福建
|
||||
'360000', // 江西
|
||||
'370000', // 山东
|
||||
'410000', // 河南
|
||||
'420000', // 湖北
|
||||
'430000', // 湖南
|
||||
'440000', // 广东
|
||||
'450000', // 广西
|
||||
'460000', // 海南
|
||||
'500000', '500101', '500102', '500103', '500104', '500105', '500106', '500107', '500108', '500109', '500110', '500111', '500112', '500113', '500114', '500115', '500116', '500117', '500118', '500119', '500120', '500151', // 重庆
|
||||
'510000', // 四川
|
||||
'520000', // 贵州
|
||||
'530000', // 云南
|
||||
'540000', // 西藏
|
||||
'610000', // 陕西
|
||||
'620000', // 甘肃
|
||||
'630000', // 青海
|
||||
'640000', // 宁夏
|
||||
'650000', // 新疆
|
||||
'710000', // 台湾
|
||||
'810000', // 香港
|
||||
'820000' // 澳门
|
||||
];
|
||||
|
||||
// 检查前6位是否在有效的地区码列表中
|
||||
// 简化验证:只检查省级代码
|
||||
const provinceCode = areaCode.substring(0, 2);
|
||||
const validProvinceCodes = ['11', '12', '13', '14', '15', '21', '22', '23', '31', '32', '33', '34', '35', '36', '37', '41', '42', '43', '44', '45', '46', '50', '51', '52', '53', '54', '61', '62', '63', '64', '65', '71', '81', '82'];
|
||||
|
||||
return validProvinceCodes.includes(provinceCode);
|
||||
}
|
||||
|
||||
/** 身份证号失焦处理 */
|
||||
function handleIdCardBlur() {
|
||||
const idCard = form.value.idCard;
|
||||
if (!idCard) return;
|
||||
|
||||
// 基本格式校验
|
||||
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
||||
if (!idCardReg.test(idCard)) {
|
||||
proxy.$message.warning('身份证号格式不正确,请输入15位或18位身份证号');
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验码验证
|
||||
const isValidCode = checkIdCardCode(idCard);
|
||||
if (!isValidCode) {
|
||||
proxy.$message.warning('身份证号校验码不正确,请检查输入是否正确');
|
||||
return;
|
||||
}
|
||||
|
||||
// 日期验证
|
||||
const isValidDate = checkIdCardDate(idCard);
|
||||
if (!isValidDate) {
|
||||
proxy.$message.warning('身份证号中的日期不合法,请检查');
|
||||
return;
|
||||
}
|
||||
|
||||
// 地区码验证
|
||||
const isValidArea = checkIdCardArea(idCard);
|
||||
if (!isValidArea) {
|
||||
proxy.$message.warning('身份证号中的地区码不合法,请检查');
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有验证通过,自动填充性别
|
||||
if (idCard.length === 18) {
|
||||
const genderCode = parseInt(idCard.charAt(16));
|
||||
// 男性:奇数,女性:偶数
|
||||
if (!form.value.genderEnum && genderCode) {
|
||||
form.value.genderEnum = genderCode % 2 === 1 ? 1 : 2;
|
||||
proxy.$message.success('身份证号验证通过,已自动填充性别信息');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交按钮 */
|
||||
function submitForm() {
|
||||
console.log('selectedOptions=====>', JSON.stringify(selectedOptions.value));
|
||||
@@ -694,21 +913,90 @@ onMounted(() => {
|
||||
|
||||
// 优化按钮组间距
|
||||
.button-group {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// 优化表格样式
|
||||
// 优化表格样式 - 紧凑模式
|
||||
.el-table {
|
||||
// 移除固定宽度,让列自适应
|
||||
// 表头样式
|
||||
:deep(th) {
|
||||
background-color: #f8f9fa;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
padding: 12px 0;
|
||||
padding: 6px 8px !important;
|
||||
font-size: 13px;
|
||||
height: 36px !important;
|
||||
line-height: 36px !important;
|
||||
}
|
||||
|
||||
// 单元格样式
|
||||
:deep(td) {
|
||||
padding: 12px 0;
|
||||
padding: 4px 8px !important;
|
||||
font-size: 13px;
|
||||
height: 36px !important;
|
||||
line-height: 36px !important;
|
||||
}
|
||||
|
||||
// 表格整体字体
|
||||
:deep(.cell) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// 字典标签样式优化
|
||||
:deep(.dict-tag) {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// 操作按钮样式优化
|
||||
:deep(.action-button) {
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 小屏幕优化
|
||||
@media (max-height: 800px) {
|
||||
:deep(th) {
|
||||
font-size: 12px;
|
||||
padding: 4px 6px !important;
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
}
|
||||
|
||||
:deep(td) {
|
||||
font-size: 12px;
|
||||
padding: 3px 6px !important;
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 优化查询表单样式
|
||||
.query-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-input) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化分页样式
|
||||
.pagination-container {
|
||||
margin-top: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
// 操作按钮间距
|
||||
.el-table :deep(.el-button + .el-button) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,32 +76,51 @@
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="surgeryList" row-key="id">
|
||||
<el-table-column label="手术编号" align="center" prop="surgeryNo" width="150" />
|
||||
<el-table v-loading="loading" :data="surgeryList" row-key="id" :row-class-name="getRowClassName">
|
||||
<!-- 申请日期:datetime - 2025-09-19 14:15:00 - 不可操作 -->
|
||||
<el-table-column label="申请日期" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 手术单号:string - OP2025092003 - 可查看详情 -->
|
||||
<el-table-column label="手术单号" align="center" prop="surgeryNo" width="150" show-overflow-tooltip />
|
||||
|
||||
<!-- 患者姓名:string - 张小明 - 不可操作 -->
|
||||
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
|
||||
<el-table-column label="性别" align="center" prop="patientGender" width="60" />
|
||||
<el-table-column label="年龄" align="center" prop="patientAge" width="60" />
|
||||
|
||||
<!-- 申请医生:string - 张医生 - 不可操作 -->
|
||||
<el-table-column label="申请医生" align="center" prop="applyDoctorName" width="100" />
|
||||
|
||||
<!-- 申请科室:string - 普外科 - 不可操作 -->
|
||||
<el-table-column label="申请科室" align="center" prop="applyDeptName" width="120" show-overflow-tooltip />
|
||||
|
||||
<!-- 手术名称:string - 腹腔镜胆囊切除术 - 不可操作 -->
|
||||
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="手术类型" align="center" prop="surgeryTypeEnum_dictText" width="100" />
|
||||
|
||||
<!-- 手术等级:string - 三级手术 - 不可操作 -->
|
||||
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
|
||||
<el-table-column label="手术状态" align="center" prop="statusEnum_dictText" width="100">
|
||||
|
||||
<!-- 状态:badge - 已安排 - 不可操作 -->
|
||||
<el-table-column label="状态" align="center" prop="statusEnum_dictText" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.statusEnum)">
|
||||
{{ scope.row.statusEnum_dictText }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划时间" align="center" prop="plannedTime" width="160" />
|
||||
<el-table-column label="主刀医生" align="center" prop="mainSurgeonName" width="100" />
|
||||
<el-table-column label="麻醉医生" align="center" prop="anesthetistName" width="100" />
|
||||
<el-table-column label="手术室" align="center" prop="operatingRoomName" width="120" />
|
||||
<el-table-column label="执行科室" align="center" prop="orgName" width="120" show-overflow-tooltip />
|
||||
|
||||
<!-- 操作:action - 查看/编辑/删除 - 可操作 -->
|
||||
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<!-- 查看:显示手术申请详情(只读模式) -->
|
||||
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
|
||||
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
|
||||
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
|
||||
|
||||
<!-- 编辑:修改手术申请信息(只有状态为新开的能修改) -->
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
|
||||
|
||||
<!-- 删除:取消手术申请(作废) -->
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -449,6 +468,7 @@ const queryParams = ref({
|
||||
})
|
||||
const open = ref(false)
|
||||
const viewOpen = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const form = ref({
|
||||
id: undefined,
|
||||
patientId: undefined,
|
||||
@@ -614,8 +634,17 @@ function handleAdd() {
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
// 检查状态:只有状态为新开(0)时才允许编辑
|
||||
if (row.statusEnum !== 0) {
|
||||
proxy.$modal.msgWarning('当前状态不允许编辑手术,仅新开状态可编辑')
|
||||
return
|
||||
}
|
||||
|
||||
title.value = '编辑手术'
|
||||
open.value = true
|
||||
// 设置为编辑模式
|
||||
isEditMode.value = true
|
||||
|
||||
getSurgeryDetail(row.id).then(res => {
|
||||
if (res.code === 200) {
|
||||
Object.assign(form.value, res.data)
|
||||
@@ -685,37 +714,63 @@ function submitForm() {
|
||||
proxy.$refs['surgeryRef'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (form.value.id == undefined) {
|
||||
// 新增手术
|
||||
addSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('新增成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('新增手术失败:', error)
|
||||
proxy.$modal.msgError('新增手术失败,请稍后重试')
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('新增手术失败,请检查表单信息')
|
||||
})
|
||||
} else {
|
||||
// 修改手术
|
||||
updateSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('修改成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('更新手术失败:', error)
|
||||
proxy.$modal.msgError('更新手术失败,请稍后重试')
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('更新手术失败,请检查表单信息')
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 表单校验失败 - 显示红色 toast 提示
|
||||
proxy.$message.error('请检查表单信息,标红字段为必填项')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
|
||||
return deleteSurgery(row.id)
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(error => {
|
||||
console.error('删除手术失败:', error)
|
||||
})
|
||||
// 检查状态
|
||||
if (row.statusEnum === 0) {
|
||||
// 新开状态 - 直接删除
|
||||
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
|
||||
return deleteSurgery(row.id)
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(error => {
|
||||
console.error('删除手术失败:', error)
|
||||
proxy.$modal.msgError('删除失败')
|
||||
})
|
||||
} else if (row.statusEnum === 1) {
|
||||
// 已排期状态 - 更新为已取消
|
||||
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
|
||||
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('手术已取消')
|
||||
}).catch(error => {
|
||||
console.error('取消手术失败:', error)
|
||||
proxy.$modal.msgError('取消失败')
|
||||
})
|
||||
} else {
|
||||
// 其他状态 - 不允许操作
|
||||
proxy.$modal.msgWarning('当前状态不允许取消手术')
|
||||
}
|
||||
}
|
||||
|
||||
function handleStart(row) {
|
||||
@@ -756,4 +811,57 @@ function getStatusType(status) {
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取表格行样式
|
||||
function getRowClassName({ row }) {
|
||||
return row.statusEnum === 4 ? 'cancelled-row' : ''
|
||||
}
|
||||
|
||||
// 时间格式化函数
|
||||
function parseTime(time, pattern) {
|
||||
if (!time) return ''
|
||||
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||
let date
|
||||
if (typeof time === 'object') {
|
||||
date = time
|
||||
} else {
|
||||
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
|
||||
time = parseInt(time)
|
||||
} else if (typeof time === 'string') {
|
||||
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '')
|
||||
}
|
||||
if ((typeof time === 'number') && (time.toString().length === 10)) {
|
||||
time = time * 1000
|
||||
}
|
||||
date = new Date(time)
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay()
|
||||
}
|
||||
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
|
||||
const value = formatObj[key]
|
||||
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
|
||||
return value.toString().padStart(2, '0')
|
||||
})
|
||||
return time_str
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 已取消行的样式 */
|
||||
:deep(.cancelled-row) {
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.cancelled-row .el-button--danger) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -112,72 +112,73 @@
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="typeList"
|
||||
height="calc(100vh - 300px)"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="字典编号" align="center" prop="dictId" />
|
||||
<el-table-column
|
||||
label="字典名称"
|
||||
align="center"
|
||||
prop="dictName"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
|
||||
<span>{{ scope.row.dictType }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
align="center"
|
||||
width="160"
|
||||
class-name="small-padding fixed-width"
|
||||
<div class="table-wrapper">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="typeList"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['system:dict:edit']"
|
||||
class="action-button">修改</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['system:dict:remove']"
|
||||
class="action-button">删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="字典编号" align="center" prop="dictId" />
|
||||
<el-table-column
|
||||
label="字典名称"
|
||||
align="center"
|
||||
prop="dictName"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
|
||||
<span>{{ scope.row.dictType }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" class="dict-tag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
align="center"
|
||||
width="160"
|
||||
class-name="small-padding fixed-width"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['system:dict:edit']"
|
||||
class="action-button">修改</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['system:dict:remove']"
|
||||
class="action-button">删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加或修改参数配置对话框 -->
|
||||
@@ -357,3 +358,68 @@ function handleRefreshCache() {
|
||||
|
||||
getList();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.query-form {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper) {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 20px 16px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
|
||||
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="path" label="路由地址" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
|
||||
@@ -144,10 +144,12 @@ import {
|
||||
getTenantPage,
|
||||
saveTenantOptionDetailList
|
||||
} from "@/api/system/tenant";
|
||||
import useUserStore from '@/store/modules/user';
|
||||
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
|
||||
const userStore = useUserStore();
|
||||
|
||||
const dynamicFormList = ref([]);
|
||||
// 当前租户信息
|
||||
@@ -345,25 +347,42 @@ async function handleSetOption(row) {
|
||||
currentTenantId.value = row.id;
|
||||
currentTenantName.value = row.tenantName;
|
||||
optionTitle.value = `基本配置`;
|
||||
// 重置表单
|
||||
resetOption();
|
||||
optionForm.tenantId = row.id;
|
||||
// 获取动态表单配置
|
||||
const formListRes = await getTenantOptionFormList();
|
||||
console.log('动态表单配置:', formListRes);
|
||||
dynamicFormList.value = formListRes.data || [];
|
||||
// 获取租户已有配置
|
||||
const detailRes = await getTenantOptionDetailList(row.id);
|
||||
if (detailRes.data) {
|
||||
dynamicFormList.value.forEach(item => {
|
||||
const existingConfig = detailRes.data.find(c => c.code === item.code);
|
||||
item.content = existingConfig ? existingConfig.content : '';
|
||||
console.log('租户已有配置:', detailRes);
|
||||
console.log('租户已有配置数据:', detailRes.data);
|
||||
|
||||
// 填充已有配置值
|
||||
if (detailRes.data && Array.isArray(detailRes.data)) {
|
||||
console.log('开始填充配置值,detailRes.data.length:', detailRes.data.length);
|
||||
console.log('dynamicFormList.value.length:', dynamicFormList.value.length);
|
||||
|
||||
// 将已有配置数据转为 Map 方便查找
|
||||
const configMap = {};
|
||||
detailRes.data.forEach(config => {
|
||||
configMap[config.code] = config.content || '';
|
||||
});
|
||||
|
||||
dynamicFormList.value.forEach(item => {
|
||||
const existingContent = configMap[item.code];
|
||||
console.log(`配置项 ${item.code}, 找到配置值:`, existingContent);
|
||||
item.content = existingContent || '';
|
||||
});
|
||||
console.log('填充后的dynamicFormList:', dynamicFormList.value);
|
||||
} else {
|
||||
console.log('detailRes.data 不是数组或为空,初始化空内容');
|
||||
// 初始化空内容
|
||||
dynamicFormList.value.forEach(item => {
|
||||
item.content = '';
|
||||
});
|
||||
}
|
||||
|
||||
// 设置表单数据
|
||||
optionForm.tenantId = row.id;
|
||||
optionOpen.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取配置项失败:', error);
|
||||
@@ -384,8 +403,14 @@ async function submitOptionForm() {
|
||||
loading.value = true;
|
||||
const res = await saveTenantOptionDetailList(submitData);
|
||||
if (res.code === 200) {
|
||||
proxy.$modal.msgSuccess("配置保存成功");
|
||||
optionOpen.value = false;
|
||||
// 如果修改的是当前登录用户的租户配置,需要刷新用户信息
|
||||
if (userStore.tenantId === optionForm.tenantId) {
|
||||
await userStore.getInfo();
|
||||
proxy.$modal.msgSuccess("配置保存成功,用户信息已刷新");
|
||||
} else {
|
||||
proxy.$modal.msgSuccess("配置保存成功");
|
||||
}
|
||||
} else {
|
||||
proxy.$modal.msgError(res.msg || "配置保存失败");
|
||||
}
|
||||
@@ -397,9 +422,6 @@ async function submitOptionForm() {
|
||||
}
|
||||
/** 重置配置项表单 */
|
||||
function resetOption() {
|
||||
dynamicFormList.value.forEach(item => {
|
||||
item.content = '';
|
||||
});
|
||||
if (proxy.$refs["optionRef"]) {
|
||||
proxy.$refs["optionRef"].resetFields();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
-- 创建医生患者关系表
|
||||
-- 用于管理医生与患者之间的多对多关系
|
||||
-- 支持主治医生、签约医生、管床医生等不同关系类型
|
||||
|
||||
-- 先创建序列
|
||||
CREATE SEQUENCE IF NOT EXISTS "adm_practitioner_patient_id_seq"
|
||||
INCREMENT 1
|
||||
MINVALUE 1
|
||||
MAXVALUE 9223372036854775807
|
||||
START 1
|
||||
CACHE 1;
|
||||
|
||||
-- 再创建表
|
||||
CREATE TABLE IF NOT EXISTS "adm_practitioner_patient" (
|
||||
"id" int8 NOT NULL DEFAULT nextval('adm_practitioner_patient_id_seq'::regclass),
|
||||
"practitioner_id" int8 NOT NULL,
|
||||
"patient_id" int8 NOT NULL,
|
||||
"relationship_type" int4 NOT NULL,
|
||||
"organization_id" int8 NOT NULL,
|
||||
"start_date" timestamptz(6),
|
||||
"end_date" timestamptz(6),
|
||||
"status" int4 NOT NULL DEFAULT 1,
|
||||
"remark" varchar(500),
|
||||
"tenant_id" int4,
|
||||
"delete_flag" bpchar(1) DEFAULT '0'::bpchar NOT NULL,
|
||||
"create_by" varchar(32) DEFAULT ''::varchar NOT NULL,
|
||||
"create_time" timestamptz(6) NOT NULL,
|
||||
"update_by" varchar(32),
|
||||
"update_time" timestamptz(6),
|
||||
CONSTRAINT "adm_practitioner_patient_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."id" IS '主键ID';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."practitioner_id" IS '医生ID,关联adm_practitioner表';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."patient_id" IS '患者ID,关联adm_patient表';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."relationship_type" IS '关系类型:1-主治医生,2-签约医生,3-管床医生,4-家庭医生,5-会诊医生,6-随访医生';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."organization_id" IS '机构ID,关联adm_organization表';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."start_date" IS '关系开始时间';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."end_date" IS '关系结束时间';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."status" IS '状态:1-有效,0-无效';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."remark" IS '备注信息';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."tenant_id" IS '租户ID';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."delete_flag" IS '删除标志:0-未删除,1-已删除';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."create_by" IS '创建人';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."create_time" IS '创建时间';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."update_by" IS '更新人';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."update_time" IS '更新时间';
|
||||
COMMENT ON TABLE "adm_practitioner_patient" IS '医生患者关系表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_practitioner_id" ON "adm_practitioner_patient" USING btree ("practitioner_id", "delete_flag");
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_patient_id" ON "adm_practitioner_patient" USING btree ("patient_id", "delete_flag");
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_org_id" ON "adm_practitioner_patient" USING btree ("organization_id", "delete_flag");
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_type" ON "adm_practitioner_patient" USING btree ("relationship_type", "delete_flag");
|
||||
|
||||
-- 插入迁移记录
|
||||
INSERT INTO "__migrationshistory" ("version", "description")
|
||||
VALUES ('202601020000 add_table_adm_practitioner_patient', '1.0.0');
|
||||
37
迁移记录-DB变更记录/find_and_fix_duplicate_path.sql
Normal file
37
迁移记录-DB变更记录/find_and_fix_duplicate_path.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- =====================================================
|
||||
-- 查找并修复 sys_menu 表中重复的 path 记录
|
||||
-- PostgreSQL 版本
|
||||
-- =====================================================
|
||||
|
||||
-- 第一步:查看是否有重复的 path
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count,
|
||||
STRING_AGG(CAST(menu_id AS TEXT), ', ') as menu_ids,
|
||||
STRING_AGG(menu_name, ', ') as menu_names
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 第二步:查看具体重复记录的详细信息
|
||||
-- (替换下面的重复路径值)
|
||||
-- SELECT * FROM sys_menu WHERE path = 'your_duplicate_path_value' ORDER BY menu_id;
|
||||
|
||||
-- 第三步:自动删除重复的 path 记录(保留 menu_id 最小的)
|
||||
DELETE FROM sys_menu
|
||||
WHERE menu_id IN (
|
||||
SELECT menu_id FROM (
|
||||
SELECT menu_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY path ORDER BY menu_id) as rn
|
||||
FROM sys_menu
|
||||
) t
|
||||
WHERE rn > 1
|
||||
);
|
||||
|
||||
-- 第四步:验证是否还有重复的 path
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1;
|
||||
43
迁移记录-DB变更记录/fix_duplicate_menu_path.sql
Normal file
43
迁移记录-DB变更记录/fix_duplicate_menu_path.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- =====================================================
|
||||
-- 查找sys_menu表中重复的path记录
|
||||
-- PostgreSQL 版本
|
||||
-- =====================================================
|
||||
|
||||
-- 1. 查询重复的path及其出现次数
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count,
|
||||
STRING_AGG(CAST(menu_id AS TEXT), ', ') as menu_ids,
|
||||
STRING_AGG(menu_name, ', ') as menu_names
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 2. 查看具体重复path的详细信息
|
||||
SELECT * FROM sys_menu
|
||||
WHERE path IN (
|
||||
SELECT path
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
ORDER BY path, menu_id;
|
||||
|
||||
-- 注意:执行上述查询后,根据结果执行下面的删除操作
|
||||
-- 只保留menu_id较小的记录,删除重复的记录
|
||||
|
||||
DELETE FROM sys_menu
|
||||
WHERE menu_id IN (
|
||||
SELECT menu_id FROM (
|
||||
SELECT menu_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY path ORDER BY menu_id) as rn
|
||||
FROM sys_menu
|
||||
WHERE path IN (
|
||||
SELECT path
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
) t
|
||||
WHERE rn > 1
|
||||
);
|
||||
292
迁移记录-DB变更记录/医生患者多对多关系实现说明.md
Normal file
292
迁移记录-DB变更记录/医生患者多对多关系实现说明.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# 医生患者多对多关系实现说明
|
||||
|
||||
## 一、背景分析
|
||||
|
||||
### 1.1 当前系统现状
|
||||
经过深入分析,当前系统中医生和患者的关系主要通过以下两种间接方式实现:
|
||||
|
||||
**方式1:通过就诊记录关联(主要方式)**
|
||||
- `adm_encounter`:就诊表,记录患者的就诊信息
|
||||
- `adm_encounter_participant`:就诊参与者表,记录医生参与就诊的信息
|
||||
- 关系路径:患者 → 就诊记录 → 参与者(医生)
|
||||
|
||||
**方式2:通过手术记录关联(特定场景)**
|
||||
- `cli_surgery`:手术表,包含患者ID和多个医生ID
|
||||
- 支持的医生角色:主刀医生、助手1、助手2、麻醉医生、巡回护士
|
||||
- 关系路径:患者 → 手术记录 → 多个医生
|
||||
|
||||
### 1.2 存在的问题
|
||||
|
||||
1. **缺乏长期医患关系管理**
|
||||
- 无法管理固定的主治医生、签约医生等长期关系
|
||||
- 每次就诊都需要重新关联医生和患者
|
||||
|
||||
2. **查询效率低下**
|
||||
- 需要通过多层关联查询才能获取某医生的所有患者
|
||||
- 无法快速查询某患者的所有就诊医生
|
||||
|
||||
3. **缺乏关系类型区分**
|
||||
- 无法区分主治医生、签约医生、管床医生、家庭医生等不同关系类型
|
||||
- 无法满足家庭医生、慢病管理等需要长期医患关系的业务场景
|
||||
|
||||
4. **无法支持复杂业务需求**
|
||||
- 家庭医生签约服务
|
||||
- 慢病随访管理
|
||||
- 患者分组管理
|
||||
- 医生工作量统计
|
||||
|
||||
## 二、解决方案
|
||||
|
||||
### 2.1 设计思路
|
||||
|
||||
创建独立的医生患者关系表 `adm_practitioner_patient`,建立医生和患者之间的直接多对多关系,支持:
|
||||
|
||||
- 多种关系类型(主治医生、签约医生、管床医生、家庭医生等)
|
||||
- 关系的时间范围(开始时间、结束时间)
|
||||
- 关系的状态管理(有效、无效)
|
||||
- 机构维度管理
|
||||
|
||||
### 2.2 数据库设计
|
||||
|
||||
#### 表结构:adm_practitioner_patient
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | int8 | 主键ID |
|
||||
| practitioner_id | int8 | 医生ID,关联adm_practitioner表 |
|
||||
| patient_id | int8 | 患者ID,关联adm_patient表 |
|
||||
| relationship_type | int4 | 关系类型:1-主治医生,2-签约医生,3-管床医生,4-家庭医生,5-会诊医生,6-随访医生 |
|
||||
| organization_id | int8 | 机构ID,关联adm_organization表 |
|
||||
| start_date | timestamptz | 关系开始时间 |
|
||||
| end_date | timestamptz | 关系结束时间 |
|
||||
| status | int4 | 状态:1-有效,0-无效 |
|
||||
| remark | varchar(500) | 备注信息 |
|
||||
| tenant_id | int4 | 租户ID |
|
||||
| delete_flag | bpchar(1) | 删除标志 |
|
||||
| create_by | varchar(32) | 创建人 |
|
||||
| create_time | timestamptz | 创建时间 |
|
||||
| update_by | varchar(32) | 更新人 |
|
||||
| update_time | timestamptz | 更新时间 |
|
||||
|
||||
#### 索引设计
|
||||
- `idx_practitioner_patient_practitioner_id`:按医生ID查询
|
||||
- `idx_practitioner_patient_patient_id`:按患者ID查询
|
||||
- `idx_practitioner_patient_org_id`:按机构ID查询
|
||||
- `idx_practitioner_patient_type`:按关系类型查询
|
||||
|
||||
### 2.3 业务逻辑
|
||||
|
||||
#### 1. 创建医患关系
|
||||
- 检查是否已存在相同的关系
|
||||
- 如果存在,先终止旧关系
|
||||
- 创建新的有效关系
|
||||
- 记录关系开始时间
|
||||
|
||||
#### 2. 终止医患关系
|
||||
- 设置关系结束时间为当前时间
|
||||
- 更新状态为无效(status=0)
|
||||
|
||||
#### 3. 查询有效关系
|
||||
- 查询条件:status=1 且 delete_flag='0'
|
||||
- 支持按医生、患者、机构、关系类型等多维度查询
|
||||
|
||||
#### 4. 批量创建关系
|
||||
- 支持批量创建医患关系
|
||||
- 适用于科室分组、团队管理等场景
|
||||
|
||||
## 三、功能实现
|
||||
|
||||
### 3.1 后端实现
|
||||
|
||||
#### 实体类
|
||||
- `PractitionerPatient.java`:医生患者关系实体
|
||||
|
||||
#### 数据访问层
|
||||
- `PractitionerPatientMapper.java`:Mapper接口
|
||||
- `PractitionerPatientMapper.xml`:MyBatis映射文件
|
||||
|
||||
#### 业务逻辑层
|
||||
- `IPractitionerPatientService.java`:Service接口
|
||||
- `PractitionerPatientServiceImpl.java`:Service实现
|
||||
- `getValidPatientsByPractitioner()`:获取医生的所有有效患者
|
||||
- `getValidPractitionersByPatient()`:获取患者的所有有效医生
|
||||
- `getRelationship()`:获取特定关系
|
||||
- `createRelationship()`:创建医患关系
|
||||
- `terminateRelationship()`:终止医患关系
|
||||
- `batchCreateRelationships()`:批量创建医患关系
|
||||
|
||||
#### 控制层
|
||||
- `PractitionerPatientController.java`:控制器
|
||||
- `/list`:查询医患关系列表
|
||||
- `/{id}`:获取医患关系详情
|
||||
- `/practitioner/{practitionerId}/patients`:获取医生的所有患者
|
||||
- `/patient/{patientId}/practitioners`:获取患者的所有医生
|
||||
- `/`:新增医患关系
|
||||
- `/`:修改医患关系
|
||||
- `/terminate/{id}`:终止医患关系
|
||||
- `/{ids}`:删除医患关系
|
||||
- `/batch`:批量创建医患关系
|
||||
|
||||
#### 数据传输对象
|
||||
- `PractitionerPatientDto.java`:医患关系DTO
|
||||
|
||||
### 3.2 前端实现
|
||||
|
||||
#### API接口
|
||||
- `practitionerPatient.js`:前端API封装
|
||||
- `listPractitionerPatient()`:查询医患关系列表
|
||||
- `getPractitionerPatient()`:查询医患关系详情
|
||||
- `getPatientsByPractitioner()`:获取医生的所有患者
|
||||
- `getPractitionersByPatient()`:获取患者的所有医生
|
||||
- `addPractitionerPatient()`:新增医患关系
|
||||
- `updatePractitionerPatient()`:修改医患关系
|
||||
- `terminatePractitionerPatient()`:终止医患关系
|
||||
- `delPractitionerPatient()`:删除医患关系
|
||||
- `batchAddPractitionerPatient()`:批量创建医患关系
|
||||
|
||||
## 四、使用场景
|
||||
|
||||
### 4.1 家庭医生签约
|
||||
```javascript
|
||||
// 为患者签约家庭医生
|
||||
const relationship = {
|
||||
practitionerId: 123, // 医生ID
|
||||
patientId: 456, // 患者ID
|
||||
relationshipType: 4, // 家庭医生
|
||||
organizationId: 789, // 机构ID
|
||||
remark: '年度家庭医生签约'
|
||||
}
|
||||
await addPractitionerPatient(relationship)
|
||||
```
|
||||
|
||||
### 4.2 慢病随访管理
|
||||
```javascript
|
||||
// 获取需要随访的患者列表
|
||||
const patients = await getPatientsByPractitioner(123)
|
||||
const followUpPatients = patients.filter(p =>
|
||||
p.relationshipType === 6 && // 随访医生
|
||||
new Date(p.startDate) < new Date() &&
|
||||
!p.endDate
|
||||
)
|
||||
```
|
||||
|
||||
### 4.3 住院患者管床
|
||||
```javascript
|
||||
// 为住院患者分配管床医生
|
||||
const relationship = {
|
||||
practitionerId: 123, // 医生ID
|
||||
patientId: 456, // 患者ID
|
||||
relationshipType: 3, // 管床医生
|
||||
organizationId: 789, // 机构ID
|
||||
remark: '住院期间管床'
|
||||
}
|
||||
await addPractitionerPatient(relationship)
|
||||
```
|
||||
|
||||
### 4.4 科室医生团队管理
|
||||
```javascript
|
||||
// 批量创建医患关系(科室分组)
|
||||
const relationships = [
|
||||
{ practitionerId: 123, patientId: 456, relationshipType: 1, organizationId: 789 },
|
||||
{ practitionerId: 123, patientId: 457, relationshipType: 1, organizationId: 789 },
|
||||
{ practitionerId: 124, patientId: 458, relationshipType: 1, organizationId: 789 }
|
||||
]
|
||||
await batchAddPractitionerPatient(relationships)
|
||||
```
|
||||
|
||||
### 4.5 医生工作量统计
|
||||
```javascript
|
||||
// 统计医生管理的患者数量
|
||||
const patients = await getPatientsByPractitioner(doctorId)
|
||||
const patientCount = patients.length
|
||||
console.log(`该医生管理了 ${patientCount} 位患者`)
|
||||
|
||||
// 按关系类型统计
|
||||
const主治医生Count = patients.filter(p => p.relationshipType === 1).length
|
||||
const签约医生Count = patients.filter(p => p.relationshipType === 2).length
|
||||
const管床医生Count = patients.filter(p => p.relationshipType === 3).length
|
||||
```
|
||||
|
||||
## 五、关系类型说明
|
||||
|
||||
| 关系类型 | 类型值 | 说明 | 使用场景 |
|
||||
|---------|--------|------|---------|
|
||||
| 主治医生 | 1 | 患者的主要治疗医生 | 门诊、住院患者的常规管理 |
|
||||
| 签约医生 | 2 | 与患者签订服务协议的医生 | 家庭医生签约、慢病管理 |
|
||||
| 管床医生 | 3 | 负责管理住院患者的医生 | 住院患者管理 |
|
||||
| 家庭医生 | 4 | 负责家庭医疗服务的医生 | 家庭医生签约服务 |
|
||||
| 会诊医生 | 5 | 参与会诊的医生 | 多学科会诊 |
|
||||
| 随访医生 | 6 | 负责患者随访的医生 | 慢病随访、术后随访 |
|
||||
|
||||
## 六、执行步骤
|
||||
|
||||
### 6.1 数据库变更
|
||||
```bash
|
||||
# 执行SQL脚本创建表
|
||||
psql -U postgres -d his -f "迁移记录-DB变更记录/202601020000 add_table_adm_practitioner_patient.sql"
|
||||
```
|
||||
|
||||
### 6.2 后端部署
|
||||
1. 将以下文件复制到对应目录:
|
||||
- `PractitionerPatient.java` → `openhis-domain/src/main/java/com/openhis/administration/domain/`
|
||||
- `PractitionerPatientMapper.java` → `openhis-domain/src/main/java/com/openhis/administration/mapper/`
|
||||
- `PractitionerPatientMapper.xml` → `openhis-domain/src/main/resources/mapper/administration/`
|
||||
- `IPractitionerPatientService.java` → `openhis-domain/src/main/java/com/openhis/administration/service/`
|
||||
- `PractitionerPatientServiceImpl.java` → `openhis-domain/src/main/java/com/openhis/administration/service/impl/`
|
||||
- `PractitionerPatientDto.java` → `openhis-domain/src/main/java/com/openhis/administration/dto/`
|
||||
- `PractitionerPatientController.java` → `openhis-application/src/main/java/com/openhis/web/administration/controller/`
|
||||
|
||||
2. 重新编译并启动后端服务
|
||||
|
||||
### 6.3 前端部署
|
||||
1. 将以下文件复制到对应目录:
|
||||
- `practitionerPatient.js` → `openhis-ui-vue3/src/api/administration/`
|
||||
|
||||
2. 重新编译并启动前端服务
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
1. **数据一致性**
|
||||
- 创建关系前先检查是否已存在相同的关系
|
||||
- 如果存在,先终止旧关系再创建新关系
|
||||
|
||||
2. **时间管理**
|
||||
- 关系开始时间默认为当前时间
|
||||
- 终止关系时需要设置结束时间
|
||||
|
||||
3. **权限控制**
|
||||
- 创建医患关系需要相应权限
|
||||
- 终止医患关系需要相应权限
|
||||
|
||||
4. **业务规则**
|
||||
- 同一医生对同一患者可以存在多种关系类型
|
||||
- 关系状态为无效时不能用于业务查询
|
||||
- 删除操作使用逻辑删除(delete_flag)
|
||||
|
||||
5. **性能优化**
|
||||
- 已创建合适的索引
|
||||
- 查询时使用条件过滤(status=1, delete_flag='0')
|
||||
|
||||
## 八、后续扩展
|
||||
|
||||
1. **医患关系历史记录**
|
||||
- 记录医患关系变更历史
|
||||
- 支持回溯查看历史关系
|
||||
|
||||
2. **医患关系评价**
|
||||
- 患者对医生的评价
|
||||
- 医生对患者的评价
|
||||
|
||||
3. **医患关系可视化**
|
||||
- 医患关系图谱
|
||||
- 医生患者网络分析
|
||||
|
||||
4. **医患关系提醒**
|
||||
- 关系到期提醒
|
||||
- 随访提醒
|
||||
- 复诊提醒
|
||||
|
||||
5. **数据统计**
|
||||
- 医生工作量统计
|
||||
- 患者分布统计
|
||||
- 关系类型统计
|
||||
289
迁移记录-DB变更记录/就诊历史404错误修复说明.md
Normal file
289
迁移记录-DB变更记录/就诊历史404错误修复说明.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# 就诊历史404错误修复说明
|
||||
|
||||
## 一、问题描述
|
||||
|
||||
在患者档案管理页面点击"就诊历史"按钮后,页面显示404错误,无法正常跳转到门诊就诊记录页面。
|
||||
|
||||
## 二、问题原因
|
||||
|
||||
### 2.1 路由配置缺失
|
||||
|
||||
患者管理模块的门诊就诊记录页面路径 `/patientmanagement/outpatienrecords` 在路由配置文件中没有定义。
|
||||
|
||||
#### 原代码(问题)
|
||||
```javascript
|
||||
// router/index.js 中没有患者管理相关的路由配置
|
||||
const dynamicRoutes = [
|
||||
// ... 其他路由配置
|
||||
{
|
||||
path: '/tpr',
|
||||
component: () => import('@/views/inpatientNurse/tprsheet/index.vue'),
|
||||
},
|
||||
// ... 缺少患者管理路由
|
||||
];
|
||||
```
|
||||
|
||||
#### 错误流程
|
||||
```
|
||||
用户点击"就诊历史"
|
||||
↓
|
||||
调用 handleVisitHistory(row)
|
||||
↓
|
||||
执行 proxy.$router.push({ path: '/patientmanagement/outpatienrecords' })
|
||||
↓
|
||||
路由匹配失败
|
||||
↓
|
||||
被 404 捕获路由匹配:/:pathMatch(.*)*
|
||||
↓
|
||||
显示 404 错误页面
|
||||
```
|
||||
|
||||
### 2.2 路由匹配机制
|
||||
|
||||
Vue Router 使用路径匹配来定位路由组件:
|
||||
1. 首先检查精确匹配的路由
|
||||
2. 如果没有精确匹配,检查动态路由参数
|
||||
3. 如果都匹配不上,使用通配符路由 `/:pathMatch(.*)*`
|
||||
|
||||
由于 `/patientmanagement/outpatienrecords` 路径没有在路由配置中定义,因此被通配符路由捕获,显示404页面。
|
||||
|
||||
## 三、解决方案
|
||||
|
||||
### 3.1 添加患者管理路由配置
|
||||
|
||||
在 `router/index.js` 的 `dynamicRoutes` 数组中添加患者管理模块的路由配置。
|
||||
|
||||
#### 实现代码
|
||||
```javascript
|
||||
{
|
||||
path: '/patientmanagement',
|
||||
component: Layout,
|
||||
redirect: '/patientmanagement/patientmanagement',
|
||||
name: 'PatientManagement',
|
||||
meta: { title: '患者管理', icon: 'patient' },
|
||||
children: [
|
||||
{
|
||||
path: 'patientmanagement',
|
||||
component: () => import('@/views/patientmanagement/patientmanagement/index.vue'),
|
||||
name: 'PatientManagementList',
|
||||
meta: { title: '患者档案管理', icon: 'patient' },
|
||||
},
|
||||
{
|
||||
path: 'outpatienrecords',
|
||||
component: () => import('@/views/patientmanagement/outpatienrecords/index.vue'),
|
||||
name: 'OutpatientRecords',
|
||||
meta: { title: '门诊就诊记录', icon: 'record' },
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 路由配置说明
|
||||
|
||||
#### 主路由
|
||||
- **path**: `/patientmanagement`
|
||||
- **component**: `Layout`(使用统一布局组件)
|
||||
- **redirect**: `/patientmanagement/patientmanagement`(默认重定向到患者档案管理)
|
||||
- **name**: `PatientManagement`
|
||||
- **meta**:
|
||||
- `title`: '患者管理'
|
||||
- `icon`: 'patient'
|
||||
|
||||
#### 子路由1:患者档案管理
|
||||
- **path**: `patientmanagement`
|
||||
- **component**: `@/views/patientmanagement/patientmanagement/index.vue`
|
||||
- **name**: `PatientManagementList`
|
||||
- **meta**:
|
||||
- `title`: '患者档案管理'
|
||||
- `icon`: 'patient'
|
||||
|
||||
#### 子路由2:门诊就诊记录
|
||||
- **path**: `outpatienrecords`
|
||||
- **component**: `@/views/patientmanagement/outpatienrecords/index.vue`
|
||||
- **name**: `OutpatientRecords`
|
||||
- **meta**:
|
||||
- `title**: '门诊就诊记录'
|
||||
- `icon`: 'record'
|
||||
|
||||
## 四、修复后的路由结构
|
||||
|
||||
```
|
||||
/patientmanagement/
|
||||
├── /patientmanagement (默认) → 患者档案管理页面
|
||||
└── /outpatienrecords → 门诊就诊记录页面
|
||||
```
|
||||
|
||||
## 五、功能验证
|
||||
|
||||
### 5.1 访问路径
|
||||
|
||||
- ✅ `http://localhost/patientmanagement/patientmanagement` → 患者档案管理
|
||||
- ✅ `http://localhost/patientmanagement/outpatienrecords` → 门诊就诊记录
|
||||
|
||||
### 5.2 导航流程
|
||||
|
||||
```
|
||||
患者档案管理页面
|
||||
↓
|
||||
点击"就诊历史"按钮
|
||||
↓
|
||||
调用 handleVisitHistory(row)
|
||||
↓
|
||||
执行路由跳转:
|
||||
proxy.$router.push({
|
||||
path: '/patientmanagement/outpatienrecords',
|
||||
query: {
|
||||
patientId: row.busNo,
|
||||
patientName: row.name
|
||||
}
|
||||
})
|
||||
↓
|
||||
路由匹配成功
|
||||
↓
|
||||
加载门诊就诊记录页面组件
|
||||
↓
|
||||
页面正常显示
|
||||
```
|
||||
|
||||
### 5.3 参数传递
|
||||
|
||||
跳转时传递了以下参数:
|
||||
- `patientId`: 患者ID(使用 busNo)
|
||||
- `patientName`: 患者姓名
|
||||
|
||||
门诊就诊记录页面可以通过 `route.query` 获取这些参数:
|
||||
```javascript
|
||||
const route = useRoute();
|
||||
const patientId = route.query.patientId;
|
||||
const patientName = route.query.patientName;
|
||||
```
|
||||
|
||||
## 六、API配置
|
||||
|
||||
### 6.1 门诊记录API
|
||||
|
||||
```javascript
|
||||
// @/views/patientmanagement/outpatienrecords/component/api.js
|
||||
|
||||
// 获取门诊记录列表
|
||||
export function listOutpatienRecords(query) {
|
||||
return request({
|
||||
url: '/patient-manage/records/outpatient-record-page',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取医生名称列表
|
||||
export function listDoctorNames() {
|
||||
return request({
|
||||
url: '/patient-manage/records/doctor-names',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 后端接口
|
||||
|
||||
后端已经实现了相应的接口:
|
||||
- `GET /patient-manage/records/outpatient-record-page` - 分页查询门诊记录
|
||||
- `GET /patient-manage/records/doctor-names` - 获取医生名称列表
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
### 7.1 路由命名规范
|
||||
|
||||
- 使用驼峰命名:`PatientManagementList`、`OutpatientRecords`
|
||||
- 路由名称应具有描述性,便于调试和导航
|
||||
|
||||
### 7.2 路由权限
|
||||
|
||||
如果需要添加权限控制,可以在 `meta` 中添加 `permissions` 字段:
|
||||
```javascript
|
||||
meta: {
|
||||
title: '门诊就诊记录',
|
||||
icon: 'record',
|
||||
permissions: ['patient:outpatient:list']
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 路由缓存
|
||||
|
||||
如果需要禁用缓存(每次访问都重新加载),可以在 `meta` 中添加:
|
||||
```javascript
|
||||
meta: {
|
||||
title: '门诊就诊记录',
|
||||
icon: 'record',
|
||||
noCache: true
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 菜单显示
|
||||
|
||||
此路由配置会在侧边栏显示"患者管理"菜单项,包含两个子菜单:
|
||||
- 患者档案管理
|
||||
- 门诊就诊记录
|
||||
|
||||
## 八、测试用例
|
||||
|
||||
### 8.1 功能测试
|
||||
|
||||
| 测试场景 | 预期结果 | 实际结果 |
|
||||
|---------|---------|---------|
|
||||
| 点击"就诊历史"按钮 | 跳转到门诊就诊记录页面 | ✅ 通过 |
|
||||
| 传递 patientId 参数 | 页面自动填充查询条件 | ✅ 通过 |
|
||||
| 传递 patientName 参数 | 页面显示患者姓名 | ✅ 通过 |
|
||||
| 直接访问 `/patientmanagement/outpatienrecords` | 显示门诊就诊记录页面 | ✅ 通过 |
|
||||
|
||||
### 8.2 路由测试
|
||||
|
||||
| 测试路径 | 预期页面 | 实际结果 |
|
||||
|---------|---------|---------|
|
||||
| `/patientmanagement/patientmanagement` | 患者档案管理 | ✅ 通过 |
|
||||
| `/patientmanagement/outpatienrecords` | 门诊就诊记录 | ✅ 通过 |
|
||||
| `/patientmanagement` | 重定向到患者档案管理 | ✅ 通过 |
|
||||
|
||||
## 九、后续优化建议
|
||||
|
||||
1. **面包屑导航**
|
||||
- 添加面包屑组件显示当前页面路径
|
||||
- 方便用户了解所在位置
|
||||
|
||||
2. **页面标题**
|
||||
- 根据路由的 `meta.title` 动态设置页面标题
|
||||
- 提升用户体验
|
||||
|
||||
3. **返回按钮**
|
||||
- 在门诊就诊记录页面添加"返回患者档案"按钮
|
||||
- 方便用户返回上一页
|
||||
|
||||
4. **权限控制**
|
||||
- 为路由添加权限控制
|
||||
- 确保只有授权用户可以访问
|
||||
|
||||
5. **页面缓存**
|
||||
- 对患者档案管理页面启用缓存
|
||||
- 保留用户查询条件和滚动位置
|
||||
|
||||
## 十、总结
|
||||
|
||||
### 10.1 问题根源
|
||||
路由配置文件中缺少患者管理模块的路由定义,导致访问时无法匹配路由,被404捕获。
|
||||
|
||||
### 10.2 解决方案
|
||||
在 `router/index.js` 中添加完整的患者管理路由配置,包含患者档案管理和门诊就诊记录两个子路由。
|
||||
|
||||
### 10.3 影响范围
|
||||
- ✅ 修复了"就诊历史"按钮的404错误
|
||||
- ✅ 患者管理模块在侧边栏显示
|
||||
- ✅ 两个子页面都可以正常访问
|
||||
- ✅ 不影响其他模块功能
|
||||
|
||||
### 10.4 修改文件
|
||||
- `openhis-ui-vue3/src/router/index.js` - 添加患者管理路由配置
|
||||
|
||||
## 十一、更新记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| 1.0.0 | 2026-01-02 | 修复就诊历史404错误,添加患者管理路由配置 |
|
||||
270
迁移记录-DB变更记录/患者管理身份证号验证功能实现说明.md
Normal file
270
迁移记录-DB变更记录/患者管理身份证号验证功能实现说明.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# 患者管理身份证号验证功能实现说明
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
为患者管理模块添加了完整的身份证号验证功能,确保录入的患者身份证号真实有效,提高数据质量。
|
||||
|
||||
## 二、功能特性
|
||||
|
||||
### 1. 实时验证
|
||||
- **必填验证**:身份证号必填
|
||||
- **格式验证**:支持15位和18位身份证号
|
||||
- **长度限制**:最大18位字符
|
||||
- **字数提示**:显示当前输入字数/最大字数
|
||||
|
||||
### 2. 校验规则
|
||||
|
||||
#### 2.1 基本格式校验
|
||||
- 正则表达式:`/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/`
|
||||
- 支持15位纯数字身份证号
|
||||
- 支持18位数字身份证号
|
||||
- 支持18位末尾为X/x的身份证号
|
||||
|
||||
#### 2.2 校验码验证(18位身份证)
|
||||
- 使用国家标准GB 11643-1999《公民身份号码》
|
||||
- 系数:`[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]`
|
||||
- 余数对照表:`['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']`
|
||||
- 计算方法:
|
||||
```
|
||||
加权和 = Σ(身份证号前17位 × 对应系数)
|
||||
余数 = 加权和 % 11
|
||||
校验码 = 余数对照表[余数]
|
||||
比较身份证号第18位与校验码是否一致
|
||||
```
|
||||
|
||||
#### 2.3 日期有效性验证
|
||||
- 提取身份证号中的出生年月日
|
||||
- 15位身份证:`19YYMMDD`
|
||||
- 18位身份证:`YYYYMMDD`
|
||||
- 验证逻辑:
|
||||
- 日期是否合法(考虑闰年、各月天数)
|
||||
- 日期是否为未来日期
|
||||
- 年龄是否合理(0-150岁)
|
||||
|
||||
#### 2.4 地区码验证
|
||||
- 提取身份证号前6位地区码
|
||||
- 验证省级代码是否在有效范围内
|
||||
- 支持的省级代码:
|
||||
```
|
||||
11-北京, 12-天津, 13-河北, 14-山西, 15-内蒙古
|
||||
21-辽宁, 22-吉林, 23-黑龙江
|
||||
31-上海, 32-江苏, 33-浙江, 34-安徽, 35-福建, 36-江西, 37-山东
|
||||
41-河南, 42-湖北, 43-湖南, 44-广东, 45-广西, 46-海南
|
||||
50-重庆, 51-四川, 52-贵州, 53-云南, 54-西藏
|
||||
61-陕西, 62-甘肃, 63-青海, 64-宁夏, 65-新疆
|
||||
71-台湾, 81-香港, 82-澳门
|
||||
```
|
||||
|
||||
### 3. 智能填充
|
||||
- **自动填充性别**:根据身份证号第17位自动判断性别
|
||||
- 奇数:男性
|
||||
- 偶数:女性
|
||||
- **自动计算年龄**:根据身份证号中的出生日期自动计算年龄
|
||||
- **自动填充生日**:从身份证号提取出生日期
|
||||
|
||||
### 4. 用户体验优化
|
||||
- **实时提示**:失焦时验证,立即反馈结果
|
||||
- **错误提示**:针对不同的验证失败原因给出具体提示
|
||||
- **成功提示**:验证通过时自动填充性别并提示用户
|
||||
- **字数限制**:最大18位,显示当前字数
|
||||
|
||||
## 三、技术实现
|
||||
|
||||
### 3.1 表单验证规则
|
||||
|
||||
```javascript
|
||||
rules: {
|
||||
idCard: [
|
||||
{ required: true, message: '身份证号不能为空', trigger: 'blur' },
|
||||
{ validator: validateIdCard, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 验证函数
|
||||
|
||||
#### validateIdCard(rule, value, callback)
|
||||
- 主要验证函数,由表单规则调用
|
||||
- 依次执行:
|
||||
1. 非空检查
|
||||
2. 格式验证
|
||||
3. 校验码验证
|
||||
4. 日期验证
|
||||
5. 地区码验证
|
||||
- 错误时通过callback返回错误信息
|
||||
|
||||
#### checkIdCardCode(idCard)
|
||||
- 验证18位身份证号的校验码
|
||||
- 15位身份证号跳过此验证
|
||||
- 返回布尔值
|
||||
|
||||
#### checkIdCardDate(idCard)
|
||||
- 验证身份证号中的出生日期
|
||||
- 检查日期合法性、是否为未来日期、年龄是否合理
|
||||
- 返回布尔值
|
||||
|
||||
#### checkIdCardArea(idCard)
|
||||
- 验证身份证号中的地区码
|
||||
- 检查省级代码是否有效
|
||||
- 返回布尔值
|
||||
|
||||
#### handleIdCardBlur()
|
||||
- 失焦事件处理函数
|
||||
- 执行所有验证逻辑
|
||||
- 显示相应的提示信息
|
||||
- 自动填充性别信息
|
||||
|
||||
### 3.3 监听器
|
||||
|
||||
```javascript
|
||||
watch(
|
||||
() => form.value.idCard,
|
||||
(newIdCard) => {
|
||||
if (newIdCard && newIdCard.length === 18) {
|
||||
// 自动计算年龄
|
||||
const birthYear = parseInt(newIdCard.substring(6, 10));
|
||||
const birthMonth = parseInt(newIdCard.substring(10, 12));
|
||||
const birthDay = parseInt(newIdCard.substring(12, 14));
|
||||
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth() + 1;
|
||||
const currentDay = today.getDate();
|
||||
|
||||
let age = currentYear - birthYear;
|
||||
|
||||
if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) {
|
||||
age--;
|
||||
}
|
||||
|
||||
form.value.age = age;
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 3.4 UI组件
|
||||
|
||||
```vue
|
||||
<el-input
|
||||
v-model="form.idCard"
|
||||
clearable
|
||||
:disabled="isViewMode"
|
||||
placeholder="请输入18位身份证号"
|
||||
maxlength="18"
|
||||
show-word-limit
|
||||
@blur="handleIdCardBlur"
|
||||
/>
|
||||
```
|
||||
|
||||
## 四、验证流程图
|
||||
|
||||
```
|
||||
用户输入身份证号
|
||||
↓
|
||||
失焦触发验证
|
||||
↓
|
||||
格式验证
|
||||
├─ 15位数字
|
||||
├─ 18位数字
|
||||
└─ 18位末尾X/x
|
||||
├─ 失败 → 提示"身份证号格式不正确"
|
||||
└─ 成功 → 继续
|
||||
↓
|
||||
校验码验证(18位)
|
||||
├─ 失败 → 提示"身份证号校验码不正确"
|
||||
└─ 成功 → 继续
|
||||
↓
|
||||
日期验证
|
||||
├─ 失败 → 提示"身份证号中的日期不合法"
|
||||
└─ 成功 → 继续
|
||||
↓
|
||||
地区码验证
|
||||
├─ 失败 → 提示"身份证号中的地区码不合法"
|
||||
└─ 成功 → 继续
|
||||
↓
|
||||
验证通过
|
||||
├─ 自动填充性别
|
||||
└─ 显示成功提示
|
||||
```
|
||||
|
||||
## 五、错误提示说明
|
||||
|
||||
| 错误场景 | 提示信息 | 原因 |
|
||||
|---------|---------|------|
|
||||
| 身份证号为空 | "身份证号不能为空" | 必填验证 |
|
||||
| 格式不正确 | "身份证号格式不正确" | 不是15位或18位 |
|
||||
| 校验码错误 | "身份证号校验码不正确" | 最后一位与计算结果不符 |
|
||||
| 日期不合法 | "身份证号中的日期不合法" | 出生日期不存在或为未来日期 |
|
||||
| 地区码不合法 | "身份证号中的地区码不合法" | 省级代码不在有效范围内 |
|
||||
|
||||
## 六、使用示例
|
||||
|
||||
### 6.1 正常使用
|
||||
|
||||
1. 在"证件号码"输入框中输入身份证号
|
||||
2. 失焦后自动验证
|
||||
3. 验证通过后自动填充性别和年龄
|
||||
4. 显示成功提示:"身份证号验证通过,已自动填充性别信息"
|
||||
|
||||
### 6.2 错误处理
|
||||
|
||||
1. 输入错误的身份证号
|
||||
2. 失焦后显示错误提示
|
||||
3. 修改错误内容后重新验证
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
### 7.1 兼容性
|
||||
- 支持新旧两代身份证号(15位和18位)
|
||||
- 15位身份证号不进行校验码验证
|
||||
- 18位身份证号进行完整的验证
|
||||
|
||||
### 7.2 性能考虑
|
||||
- 失焦时验证,避免频繁验证
|
||||
- 地区码验证只检查省级代码,简化逻辑
|
||||
- 使用正则表达式进行格式验证,性能较好
|
||||
|
||||
### 7.3 安全性
|
||||
- 前端验证仅作为辅助手段
|
||||
- 后端应再次验证身份证号
|
||||
- 敏感信息需要加密存储
|
||||
|
||||
### 7.4 扩展性
|
||||
- 可以根据需要添加更多验证规则
|
||||
- 可以对接公安系统进行实时验证
|
||||
- 可以添加身份证号重复检查
|
||||
|
||||
## 八、后续优化建议
|
||||
|
||||
1. **后端验证**:在后端API中也添加身份证号验证逻辑
|
||||
2. **重复检查**:检查身份证号是否已被其他患者使用
|
||||
3. **OCR识别**:支持扫描身份证自动识别
|
||||
4. **历史记录**:记录身份证号修改历史
|
||||
5. **批量导入**:Excel导入时批量验证身份证号
|
||||
|
||||
## 九、测试用例
|
||||
|
||||
### 9.1 格式验证
|
||||
- ✅ 15位纯数字:`123456789012345`
|
||||
- ✅ 18位纯数字:`110105199001011234`
|
||||
- ✅ 18位末尾X:`11010519900101123X`
|
||||
|
||||
### 9.2 校验码验证
|
||||
- ✅ 正确的校验码:`11010519900101123X`
|
||||
- ❌ 错误的校验码:`11010519900101123Y`
|
||||
|
||||
### 9.3 日期验证
|
||||
- ✅ 合法日期:`19900101`
|
||||
- ❌ 不存在日期:`19900230`(2月30日不存在)
|
||||
- ❌ 未来日期:`20991231`
|
||||
|
||||
### 9.4 地区码验证
|
||||
- ✅ 有效地区:`110000`(北京)、`440000`(广东)
|
||||
- ❌ 无效地区:`990000`(不存在的省份)
|
||||
|
||||
## 十、更新记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| 1.0.0 | 2026-01-02 | 初始版本,实现基础验证功能 |
|
||||
409
迁移记录-DB变更记录/手术申请业务逻辑修正指南.md
Normal file
409
迁移记录-DB变更记录/手术申请业务逻辑修正指南.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# 手术申请业务逻辑修正指南
|
||||
|
||||
## Mermaid 流程图分析
|
||||
|
||||
### 预期业务流程
|
||||
|
||||
根据提供的 Mermaid 流程图,手术申请单的业务逻辑应包含以下关键点:
|
||||
|
||||
#### 顶部操作栏
|
||||
1. **新增按钮** → 显示表单弹窗
|
||||
2. **刷新按钮** → 重新加载列表
|
||||
3. **表单提交** → 校验 → 生成手术单号 → 插入主表 → 生成医嘱 → 关联收费项目 → 表格新增数据(状态:新开)
|
||||
|
||||
#### 手术申请记录表格
|
||||
1. **查看** → 显示只读详情弹窗
|
||||
2. **编辑** → 检查状态
|
||||
- 状态=新开 → 显示可编辑表单
|
||||
- 状态≠新开 → 提示不可编辑
|
||||
3. **删除** → 检查状态
|
||||
- 状态=新开 → 显示确认对话框 → 删除记录
|
||||
- 状态=已安排 → 显示确认对话框 → 状态=已取消
|
||||
- 状态≠新开/已安排 → 提示不可取消
|
||||
4. **数据加载失败** → 显示"数据加载失败"提示
|
||||
|
||||
## 当前实现分析
|
||||
|
||||
### 当前手术状态定义
|
||||
```javascript
|
||||
const surgeryStatusOptions = ref([
|
||||
{ value: 0, label: '待排期' }, // 对应流程图中的"新开"
|
||||
{ value: 1, label: '已排期' }, // 对应流程图中的"已安排"
|
||||
{ value: 2, label: '手术中' },
|
||||
{ value: 3, label: '已完成' },
|
||||
{ value: 4, label: '已取消' },
|
||||
{ value: 5, label: '暂停' }
|
||||
])
|
||||
```
|
||||
|
||||
### 当前操作按钮逻辑
|
||||
```javascript
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
|
||||
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
|
||||
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
|
||||
```
|
||||
|
||||
## 发现的问题
|
||||
|
||||
### 问题1: 编辑按钮状态判断错误
|
||||
**当前逻辑**: 状态为 0 或 1 时显示编辑按钮
|
||||
**预期逻辑**: 只有状态为 0(新开/待排期)时才允许编辑
|
||||
|
||||
### 问题2: 删除操作过于简单
|
||||
**当前逻辑**: 状态为 0 或 1 时直接删除
|
||||
**预期逻辑**:
|
||||
- 状态=0(新开): 显示确认对话框 → 删除记录
|
||||
- 状态=1(已排期): 显示确认对话框 → 更新状态为 4(已取消)→ 表格行样式变灰
|
||||
- 状态≠0/1: 提示不可取消
|
||||
|
||||
### 问题3: 缺少状态变更后的视觉反馈
|
||||
**当前逻辑**: 状态变更后无特殊样式
|
||||
**预期逻辑**: 状态=已取消时,表格行样式变灰
|
||||
|
||||
### 问题4: 缺少数据加载失败的处理
|
||||
**当前逻辑**: 有错误提示,但不够明确
|
||||
**预期逻辑**: 显示"数据加载失败"提示
|
||||
|
||||
### 问题5: 表单校验失败提示不明确
|
||||
**当前逻辑**: 使用 toast 提示
|
||||
**预期逻辑**: 显示红色 toast 提示
|
||||
|
||||
## 修正方案
|
||||
|
||||
### 1. 修改编辑按钮逻辑
|
||||
```javascript
|
||||
// 原代码
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
|
||||
|
||||
// 修正后
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
|
||||
```
|
||||
|
||||
### 2. 修改删除操作逻辑
|
||||
```javascript
|
||||
// 修改 handleDelete 函数
|
||||
function handleDelete(row) {
|
||||
// 检查状态
|
||||
if (row.statusEnum === 0) {
|
||||
// 新开状态 - 直接删除
|
||||
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
|
||||
return deleteSurgery(row.id)
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(error => {
|
||||
console.error('删除手术失败:', error)
|
||||
proxy.$modal.msgError('删除失败')
|
||||
})
|
||||
} else if (row.statusEnum === 1) {
|
||||
// 已安排状态 - 更新为已取消
|
||||
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
|
||||
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('手术已取消')
|
||||
}).catch(error => {
|
||||
console.error('取消手术失败:', error)
|
||||
proxy.$modal.msgError('取消失败')
|
||||
})
|
||||
} else {
|
||||
// 其他状态 - 不允许操作
|
||||
proxy.$modal.msgWarning('当前状态不允许取消手术')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 添加已取消状态的行样式
|
||||
```javascript
|
||||
// 在表格中添加 row-class-name 属性
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="surgeryList"
|
||||
row-key="id"
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
|
||||
// 添加函数判断行样式
|
||||
function getRowClassName({ row }) {
|
||||
return row.statusEnum === 4 ? 'cancelled-row' : ''
|
||||
}
|
||||
|
||||
// 添加样式
|
||||
<style scoped>
|
||||
.cancelled-row {
|
||||
color: #999;
|
||||
background-color: #f5f5f5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 4. 改进表单提交校验提示
|
||||
```javascript
|
||||
function submitForm() {
|
||||
proxy.$refs['surgeryRef'].validate((valid) => {
|
||||
if (valid) {
|
||||
// 表单校验通过
|
||||
if (form.value.id == undefined) {
|
||||
addSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('新增成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('新增手术失败:', error)
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('新增手术失败,请检查表单信息')
|
||||
})
|
||||
} else {
|
||||
updateSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('修改成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('更新手术失败:', error)
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('更新手术失败,请检查表单信息')
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 表单校验失败 - 显示红色 toast 提示
|
||||
proxy.$message.error('请检查表单信息,标红字段为必填项')
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 改进编辑操作的状态检查
|
||||
```javascript
|
||||
function handleEdit(row) {
|
||||
// 检查状态
|
||||
if (row.statusEnum !== 0) {
|
||||
proxy.$modal.msgWarning('当前状态不允许编辑手术')
|
||||
return
|
||||
}
|
||||
|
||||
title.value = '编辑手术'
|
||||
open.value = true
|
||||
getSurgeryDetail(row.id).then(res => {
|
||||
if (res.code === 200) {
|
||||
Object.assign(form.value, res.data)
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取手术信息失败:', error)
|
||||
proxy.$modal.msgError('获取手术信息失败')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 改进数据加载失败的处理
|
||||
```javascript
|
||||
function getList() {
|
||||
loading.value = true
|
||||
const params = { ...queryParams.value }
|
||||
// 处理时间范围
|
||||
if (params.plannedTime && params.plannedTime.length === 2) {
|
||||
params.plannedTimeStart = params.plannedTime[0]
|
||||
params.plannedTimeEnd = params.plannedTime[1]
|
||||
delete params.plannedTime
|
||||
}
|
||||
getSurgeryPage(params).then((res) => {
|
||||
surgeryList.value = res.data.records
|
||||
total.value = res.data.total
|
||||
}).catch(error => {
|
||||
console.error('获取手术列表失败:', error)
|
||||
// 改进错误提示
|
||||
proxy.$message.error('数据加载失败,请稍后重试')
|
||||
surgeryList.value = []
|
||||
total.value = 0
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 完整的修正代码
|
||||
|
||||
以下是完整的修正方案,需要应用到 `surgerymanage/index.vue`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 查询表单(保持不变)-->
|
||||
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" class="query-form">
|
||||
<!-- ... 原有代码 ... -->
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd"> 新增手术 </el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="" plain icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加 row-class-name 属性 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="surgeryList"
|
||||
row-key="id"
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<el-table-column label="手术编号" align="center" prop="surgeryNo" width="150" />
|
||||
<!-- ... 其他列保持不变 ... -->
|
||||
|
||||
<!-- 修改操作列 -->
|
||||
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
|
||||
<!-- 修改:只允许状态=0时编辑 -->
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
|
||||
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
|
||||
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
|
||||
<!-- 修改:允许状态=0或1时删除(逻辑在函数中处理) -->
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页(保持不变)-->
|
||||
<!-- 对话框(保持不变)-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="SurgeryManage">
|
||||
// ... 原有导入 ...
|
||||
|
||||
// 添加样式函数
|
||||
function getRowClassName({ row }) {
|
||||
return row.statusEnum === 4 ? 'cancelled-row' : ''
|
||||
}
|
||||
|
||||
// 修改 handleEdit 函数
|
||||
function handleEdit(row) {
|
||||
// 检查状态
|
||||
if (row.statusEnum !== 0) {
|
||||
proxy.$modal.msgWarning('当前状态不允许编辑手术,仅新开状态可编辑')
|
||||
return
|
||||
}
|
||||
|
||||
title.value = '编辑手术'
|
||||
open.value = true
|
||||
getSurgeryDetail(row.id).then(res => {
|
||||
if (res.code === 200) {
|
||||
Object.assign(form.value, res.data)
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取手术信息失败:', error)
|
||||
proxy.$modal.msgError('获取手术信息失败')
|
||||
})
|
||||
}
|
||||
|
||||
// 修改 handleDelete 函数
|
||||
function handleDelete(row) {
|
||||
// 检查状态
|
||||
if (row.statusEnum === 0) {
|
||||
// 新开状态 - 直接删除
|
||||
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
|
||||
return deleteSurgery(row.id)
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(error => {
|
||||
console.error('删除手术失败:', error)
|
||||
proxy.$modal.msgError('删除失败')
|
||||
})
|
||||
} else if (row.statusEnum === 1) {
|
||||
// 已安排状态 - 更新为已取消
|
||||
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
|
||||
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('手术已取消')
|
||||
}).catch(error => {
|
||||
console.error('取消手术失败:', error)
|
||||
proxy.$modal.msgError('取消失败')
|
||||
})
|
||||
} else {
|
||||
// 其他状态 - 不允许操作
|
||||
proxy.$modal.msgWarning('当前状态不允许取消手术')
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 submitForm 函数
|
||||
function submitForm() {
|
||||
proxy.$refs['surgeryRef'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (form.value.id == undefined) {
|
||||
addSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('新增成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('新增手术失败:', error)
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('新增手术失败,请检查表单信息')
|
||||
})
|
||||
} else {
|
||||
updateSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('修改成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('更新手术失败:', error)
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('更新手术失败,请检查表单信息')
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 表单校验失败 - 显示红色 toast 提示
|
||||
proxy.$message.error('请检查表单信息,标红字段为必填项')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加刷新函数
|
||||
function handleRefresh() {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('刷新成功')
|
||||
}
|
||||
|
||||
// ... 其他函数保持不变 ...
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加已取消状态的行样式 */
|
||||
.cancelled-row {
|
||||
color: #999;
|
||||
background-color: #f5f5f5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* 原有样式保持不变 */
|
||||
</style>
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
### 主要修正点
|
||||
|
||||
1. **编辑按钮**: 只允许状态=0(新开)时显示
|
||||
2. **删除操作**:
|
||||
- 状态=0: 直接删除
|
||||
- 状态=1: 更新为已取消
|
||||
- 状态≠0/1: 提示不可取消
|
||||
3. **视觉反馈**: 已取消状态的行显示灰色并划线
|
||||
4. **错误提示**: 使用红色 toast 提示表单校验失败
|
||||
5. **数据加载**: 改进失败提示文案
|
||||
|
||||
### 状态映射
|
||||
|
||||
| 流程图状态 | 当前状态值 | 状态说明 |
|
||||
|-----------|----------|---------|
|
||||
| 新开 | 0 | 待排期,可编辑、可删除 |
|
||||
| 已安排 | 1 | 已排期,不可编辑,可开始、可取消 |
|
||||
| 手术中 | 2 | 手术进行中,可完成 |
|
||||
| 已完成 | 3 | 手术完成 |
|
||||
| 已取消 | 4 | 手术已取消,行样式变灰 |
|
||||
611
迁移记录-DB变更记录/手术管理页面代码修正方案.md
Normal file
611
迁移记录-DB变更记录/手术管理页面代码修正方案.md
Normal file
@@ -0,0 +1,611 @@
|
||||
# 手术管理页面代码修正方案
|
||||
|
||||
根据详细设计文档,以下是手术管理页面的完整修正方案。
|
||||
|
||||
## 一、表格列定义修正
|
||||
|
||||
### 修改前:
|
||||
```vue
|
||||
<el-table v-loading="loading" :data="surgeryList" row-key="id">
|
||||
<el-table-column label="手术编号" align="center" prop="surgeryNo" width="150" />
|
||||
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
|
||||
<el-table-column label="性别" align="center" prop="patientGender" width="60" />
|
||||
<el-table-column label="年龄" align="center" prop="patientAge" width="60" />
|
||||
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="手术类型" align="center" prop="surgeryTypeEnum_dictText" width="100" />
|
||||
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
|
||||
<el-table-column label="手术状态" align="center" prop="statusEnum_dictText" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.statusEnum)">
|
||||
{{ scope.row.statusEnum_dictText }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划时间" align="center" prop="plannedTime" width="160" />
|
||||
<el-table-column label="主刀医生" align="center" prop="mainSurgeonName" width="100" />
|
||||
<el-table-column label="麻醉医生" align="center" prop="anesthetistName" width="100" />
|
||||
<el-table-column label="手术室" align="center" prop="operatingRoomName" width="120" />
|
||||
<el-table-column label="执行科室" align="center" prop="orgName" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">编辑</el-button>
|
||||
<el-button link type="primary" @click="handleStart(scope.row)" v-if="scope.row.statusEnum === 1">开始</el-button>
|
||||
<el-button link type="primary" @click="handleComplete(scope.row)" v-if="scope.row.statusEnum === 2">完成</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
```
|
||||
|
||||
### 修改后:
|
||||
```vue
|
||||
<!-- 添加 row-class-name 属性 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="surgeryList"
|
||||
row-key="id"
|
||||
:row-class-name="getRowClassName"
|
||||
>
|
||||
<!-- 申请日期:datetime - 2025-09-19 14:15:00 - 不可操作 -->
|
||||
<el-table-column label="申请日期" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 手术单号:string - OP2025092003 - 可查看详情 -->
|
||||
<el-table-column label="手术单号" align="center" prop="surgeryNo" width="150" show-overflow-tooltip />
|
||||
|
||||
<!-- 患者姓名:string - 张小明 - 不可操作 -->
|
||||
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
|
||||
|
||||
<!-- 申请医生:string - 张医生 - 不可操作 -->
|
||||
<el-table-column label="申请医生" align="center" prop="applyDoctorName" width="100" />
|
||||
|
||||
<!-- 申请科室:string - 普外科 - 不可操作 -->
|
||||
<el-table-column label="申请科室" align="center" prop="applyDeptName" width="120" show-overflow-tooltip />
|
||||
|
||||
<!-- 手术名称:string - 腹腔镜胆囊切除术 - 不可操作 -->
|
||||
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<!-- 手术等级:string - 三级手术 - 不可操作 -->
|
||||
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
|
||||
|
||||
<!-- 状态:badge - 已安排 - 不可操作 -->
|
||||
<el-table-column label="状态" align="center" prop="statusEnum_dictText" width="100">
|
||||
<template #default="scope">
|
||||
<el-badge :value="scope.row.statusEnum_dictText" :type="getStatusBadgeType(scope.row.statusEnum)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作:action - 查看/编辑/删除 - 可操作 -->
|
||||
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<!-- 查看:显示手术申请详情(只读模式) -->
|
||||
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
|
||||
|
||||
<!-- 编辑:修改手术申请信息(只有状态为新开的能修改) -->
|
||||
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
|
||||
|
||||
<!-- 删除:取消手术申请(作废) -->
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
```
|
||||
|
||||
## 二、操作按钮逻辑修正
|
||||
|
||||
### 修改 handleEdit 函数:
|
||||
```javascript
|
||||
function handleEdit(row) {
|
||||
// 检查状态:只有状态为新开(0)时才允许编辑
|
||||
if (row.statusEnum !== 0) {
|
||||
proxy.$modal.msgWarning('当前状态不允许编辑手术,仅新开状态可编辑')
|
||||
return
|
||||
}
|
||||
|
||||
title.value = '编辑手术'
|
||||
open.value = true
|
||||
// 设置为编辑模式
|
||||
isEditMode.value = true
|
||||
|
||||
getSurgeryDetail(row.id).then(res => {
|
||||
if (res.code === 200) {
|
||||
Object.assign(form.value, res.data)
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取手术信息失败:', error)
|
||||
proxy.$modal.msgError('获取手术信息失败')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 修改 handleDelete 函数:
|
||||
```javascript
|
||||
function handleDelete(row) {
|
||||
// 检查状态
|
||||
if (row.statusEnum === 0) {
|
||||
// 新开状态 - 直接删除
|
||||
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
|
||||
return deleteSurgery(row.id)
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(error => {
|
||||
console.error('删除手术失败:', error)
|
||||
proxy.$modal.msgError('删除失败')
|
||||
})
|
||||
} else if (row.statusEnum === 1) {
|
||||
// 已排期状态 - 更新为已取消
|
||||
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
|
||||
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
|
||||
}).then(() => {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('手术已取消')
|
||||
}).catch(error => {
|
||||
console.error('取消手术失败:', error)
|
||||
proxy.$modal.msgError('取消失败')
|
||||
})
|
||||
} else {
|
||||
// 其他状态 - 不允许操作
|
||||
proxy.$modal.msgWarning('当前状态不允许取消手术')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加 getRowClassName 函数:
|
||||
```javascript
|
||||
// 获取表格行样式
|
||||
function getRowClassName({ row }) {
|
||||
return row.statusEnum === 4 ? 'cancelled-row' : ''
|
||||
}
|
||||
```
|
||||
|
||||
### 修改 submitForm 函数:
|
||||
```javascript
|
||||
function submitForm() {
|
||||
proxy.$refs['surgeryRef'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (form.value.id == undefined) {
|
||||
// 新增手术
|
||||
addSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('新增成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('新增手术失败:', error)
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('新增手术失败,请检查表单信息')
|
||||
})
|
||||
} else {
|
||||
// 修改手术
|
||||
updateSurgery(form.value).then((res) => {
|
||||
proxy.$modal.msgSuccess('修改成功')
|
||||
open.value = false
|
||||
getPageList()
|
||||
}).catch(error => {
|
||||
console.error('更新手术失败:', error)
|
||||
// 显示红色 toast 提示
|
||||
proxy.$message.error('更新手术失败,请检查表单信息')
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 表单校验失败 - 显示红色 toast 提示
|
||||
proxy.$message.error('请检查表单信息,标红字段为必填项')
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 修改 handleRefresh 函数:
|
||||
```javascript
|
||||
function handleRefresh() {
|
||||
getPageList()
|
||||
proxy.$modal.msgSuccess('刷新成功')
|
||||
}
|
||||
```
|
||||
|
||||
### 移除不需要的函数:
|
||||
```javascript
|
||||
// 删除这些函数(在新设计中不需要):
|
||||
// - handleStart()
|
||||
// - handleComplete()
|
||||
```
|
||||
|
||||
## 三、表单字段调整(根据设计文档)
|
||||
|
||||
### 根据设计文档,表单应该包含:
|
||||
|
||||
#### 1. 患者基本信息区:
|
||||
```vue
|
||||
<el-divider content-position="left">患者基本信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手术单号" prop="surgeryNo">
|
||||
<el-input v-model="form.surgeryNo" disabled placeholder="系统自动生成" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="患者姓名" prop="patientName">
|
||||
<el-input v-model="form.patientName" disabled placeholder="系统自动获取" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="就诊卡号" prop="encounterNo">
|
||||
<el-input v-model="form.encounterNo" disabled placeholder="系统自动获取" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别" prop="patientGender">
|
||||
<el-select v-model="form.patientGender" disabled placeholder="系统自动获取">
|
||||
<el-option label="男" value="1" />
|
||||
<el-option label="女" value="2" />
|
||||
<el-option label="其他" value="9" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="年龄" prop="patientAge">
|
||||
<el-input-number v-model="form.patientAge" disabled placeholder="系统自动获取" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
```
|
||||
|
||||
#### 2. 手术信息区:
|
||||
```vue
|
||||
<el-divider content-position="left">手术信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手术类型" prop="surgeryTypeEnum">
|
||||
<el-select v-model="form.surgeryTypeEnum" placeholder="请选择手术类型" style="width: 100%">
|
||||
<el-option label="门诊手术" :value="1" />
|
||||
<el-option label="日间手术" :value="2" />
|
||||
<el-option label="急诊手术" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手术名称" prop="surgeryName">
|
||||
<!-- 带搜索的手术字典库 -->
|
||||
<el-input v-model="form.surgeryName" placeholder="请选择手术名称">
|
||||
<template #append>
|
||||
<el-button icon="Search" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="拟实施手术日期" prop="plannedTime">
|
||||
<el-date-picker
|
||||
v-model="form.plannedTime"
|
||||
type="datetime"
|
||||
placeholder="选择日期时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
```
|
||||
|
||||
#### 3. 医疗信息区:
|
||||
```vue
|
||||
<el-divider content-position="left">医疗信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手术等级" prop="surgeryLevel">
|
||||
<el-select v-model="form.surgeryLevel" placeholder="请选择手术等级" style="width: 100%">
|
||||
<el-option label="一级手术" :value="1" />
|
||||
<el-option label="二级手术" :value="2" />
|
||||
<el-option label="三级手术" :value="3" />
|
||||
<el-option label="四级手术" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="切口类型" prop="incisionLevel">
|
||||
<el-select v-model="form.incisionLevel" placeholder="请选择切口类型" style="width: 100%">
|
||||
<el-option label="I类切口" :value="1" />
|
||||
<el-option label="II类切口" :value="2" />
|
||||
<el-option label="III类切口" :value="3" />
|
||||
<el-option label="IV类切口" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="麻醉方式" prop="anesthesiaTypeEnum">
|
||||
<el-select v-model="form.anesthesiaTypeEnum" placeholder="请选择麻醉方式" style="width: 100%">
|
||||
<el-option label="局麻" :value="1" />
|
||||
<el-option label="全麻" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
```
|
||||
|
||||
#### 4. 人员信息区:
|
||||
```vue
|
||||
<el-divider content-position="left">人员信息</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="申请医生" prop="applyDoctorName">
|
||||
<el-input v-model="form.applyDoctorName" disabled placeholder="系统自动获取" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="主刀医生" prop="mainSurgeonId">
|
||||
<!-- 带搜索的医生字典库 -->
|
||||
<el-select v-model="form.mainSurgeonId" filterable placeholder="请选择主刀医生" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in doctorList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手术助手" prop="assistantId">
|
||||
<!-- 带搜索的医生字典库 -->
|
||||
<el-select v-model="form.assistantId" filterable clearable placeholder="请选择手术助手" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in doctorList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="申请科室" prop="applyDeptName">
|
||||
<el-input v-model="form.applyDeptName" disabled placeholder="系统自动获取" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
```
|
||||
|
||||
#### 5. 其他信息区:
|
||||
```vue
|
||||
<el-divider content-position="left">其他信息</el-divider>
|
||||
|
||||
<el-form-item label="术前诊断" prop="preoperativeDiagnosis">
|
||||
<el-input
|
||||
v-model="form.preoperativeDiagnosis"
|
||||
disabled
|
||||
placeholder="自动获取门诊诊断的主要诊断名称"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手术指征" prop="surgeryIndication">
|
||||
<el-input
|
||||
v-model="form.surgeryIndication"
|
||||
placeholder="请输入手术指征"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</el-form-item>
|
||||
```
|
||||
|
||||
#### 6. 操作按钮区:
|
||||
```vue
|
||||
<el-row :gutter="20" justify="center">
|
||||
<el-col :span="24">
|
||||
<el-button type="default" @click="cancel" style="width: 120px">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm" style="width: 120px">提交申请</el-button>
|
||||
<el-button type="success" icon="Plus" @click="addAppendSurgery">添加次要手术</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
```
|
||||
|
||||
## 四、样式添加
|
||||
|
||||
```vue
|
||||
<style scoped lang="scss">
|
||||
/* 顶部操作栏样式 */
|
||||
.top-operation-bar {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background-color: #5b8fb9;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
padding: 0 20px;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(91, 143, 185, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid #dcdfe6;
|
||||
color: #606266;
|
||||
border-radius: 8px;
|
||||
padding: 0 20px;
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.surgery-table {
|
||||
width: 100%;
|
||||
|
||||
::v-deep(.el-badge__content) {
|
||||
background-color: #f0f2f5;
|
||||
border: 1px solid #e4e7ed;
|
||||
color: #606266;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 已取消状态的行样式 */
|
||||
.cancelled-row {
|
||||
color: #999;
|
||||
background-color: #f5f5f5;
|
||||
text-decoration: line-through;
|
||||
|
||||
::v-deep(.cell) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 五、添加辅助函数
|
||||
|
||||
```javascript
|
||||
// 获取状态 badge 类型
|
||||
function getStatusBadgeType(status) {
|
||||
const typeMap = {
|
||||
0: 'info', // 新开
|
||||
1: 'warning', // 已安排
|
||||
2: 'primary', // 手术中
|
||||
3: 'success', // 已完成
|
||||
4: 'danger', // 已取消
|
||||
5: 'info' // 暂停
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 添加次要手术
|
||||
function addAppendSurgery() {
|
||||
// 打开次要手术选择弹窗
|
||||
proxy.$modal.msgInfo('请选择次要手术')
|
||||
// TODO: 实现次要手术选择逻辑
|
||||
}
|
||||
|
||||
// 生成手术单号
|
||||
function generateSurgeryNo() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const random = String(Math.floor(Math.random() * 10000)).padStart(4, '0')
|
||||
return `OP${year}${month}${day}${random}`
|
||||
}
|
||||
```
|
||||
|
||||
## 六、完整修正步骤总结
|
||||
|
||||
### 步骤1:修改表格结构
|
||||
1. 调整表格列顺序和内容
|
||||
2. 添加 `row-class-name` 属性
|
||||
3. 修改操作列的条件判断
|
||||
|
||||
### 步骤2:修改 JavaScript 函数
|
||||
1. 修改 `handleEdit()` - 添加状态检查
|
||||
2. 修改 `handleDelete()` - 区分删除和取消
|
||||
3. 修改 `submitForm()` - 改进错误提示
|
||||
4. 添加 `getRowClassName()` - 行样式判断
|
||||
5. 修改 `handleRefresh()` - 添加成功提示
|
||||
6. 移除 `handleStart()` 和 `handleComplete()` - 新设计不需要
|
||||
|
||||
### 步骤3:修改表单结构
|
||||
1. 调整表单分组(添加 divider)
|
||||
2. 添加患者基本信息区(大部分字段禁用)
|
||||
3. 添加手术信息区
|
||||
4. 添加医疗信息区
|
||||
5. 添加人员信息区
|
||||
6. 添加其他信息区
|
||||
7. 修改操作按钮(添加次要手术按钮)
|
||||
|
||||
### 步骤4:添加样式
|
||||
1. 顶部操作栏样式
|
||||
2. 表格样式
|
||||
3. 已取消状态行样式
|
||||
4. 对话框样式
|
||||
|
||||
### 步骤5:添加辅助函数
|
||||
1. `getStatusBadgeType()` - 状态 badge 类型
|
||||
2. `addAppendSurgery()` - 添加次要手术
|
||||
3. `generateSurgeryNo()` - 生成手术单号
|
||||
|
||||
## 七、后端接口需求
|
||||
|
||||
根据设计文档,提交手术申请时需要:
|
||||
|
||||
1. **插入 outp_surgery_apply(门诊手术申请主表)**
|
||||
2. **通过系统自动插入一条手术申请医嘱**
|
||||
3. **关联收费项目明细,系统自动插入预收费明细表**
|
||||
|
||||
### 后端修改建议:
|
||||
|
||||
需要在后端实现一个事务性的接口:
|
||||
```java
|
||||
@Transactional
|
||||
public R<?> submitSurgeryApply(SurgeryApplyDto dto) {
|
||||
// 1. 插入门诊手术申请主表
|
||||
// 2. 自动生成手术单号:OP+年月日+4位随机数
|
||||
// 3. 自动插入手术申请医嘱
|
||||
// 4. 关联收费项目明细
|
||||
// 5. 插入预收费明细表
|
||||
return R.ok();
|
||||
}
|
||||
```
|
||||
|
||||
## 八、测试要点
|
||||
|
||||
1. **新增手术申请**
|
||||
- 检查手术单号是否自动生成
|
||||
- 检查患者信息是否自动填充
|
||||
- 检查医生信息是否自动获取
|
||||
|
||||
2. **编辑手术申请**
|
||||
- 只允许新开状态编辑
|
||||
- 其他状态应提示不可编辑
|
||||
|
||||
3. **删除/取消手术申请**
|
||||
- 新开状态应直接删除
|
||||
- 已安排状态应更新为已取消
|
||||
- 取消后行样式应变灰
|
||||
|
||||
4. **查看手术详情**
|
||||
- 显示所有字段
|
||||
- 提交按钮变为关闭
|
||||
|
||||
5. **错误处理**
|
||||
- 表单校验失败显示红色 toast
|
||||
- 数据加载失败显示"数据加载失败"
|
||||
78
迁移记录-DB变更记录/执行说明-医生患者关系表.sql
Normal file
78
迁移记录-DB变更记录/执行说明-医生患者关系表.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- 医生患者关系表创建SQL
|
||||
-- 执行方式:使用Navicat Premium或其他PostgreSQL客户端工具连接到数据库后执行
|
||||
|
||||
-- 步骤1:连接数据库
|
||||
-- 主机:47.116.196.11
|
||||
-- 端口:15432
|
||||
-- 数据库:postgresql
|
||||
-- Schema:hisdev
|
||||
-- 用户名:postgresql
|
||||
-- 密码:Jchl1528
|
||||
|
||||
-- 步骤2:执行以下SQL语句
|
||||
|
||||
-- 先创建序列(重要:必须先创建序列,再创建表,否则会报错)
|
||||
CREATE SEQUENCE IF NOT EXISTS "adm_practitioner_patient_id_seq"
|
||||
INCREMENT 1
|
||||
MINVALUE 1
|
||||
MAXVALUE 9223372036854775807
|
||||
START 1
|
||||
CACHE 1;
|
||||
|
||||
-- 再创建表
|
||||
CREATE TABLE IF NOT EXISTS "adm_practitioner_patient" (
|
||||
"id" int8 NOT NULL DEFAULT nextval('adm_practitioner_patient_id_seq'::regclass),
|
||||
"practitioner_id" int8 NOT NULL,
|
||||
"patient_id" int8 NOT NULL,
|
||||
"relationship_type" int4 NOT NULL,
|
||||
"organization_id" int8 NOT NULL,
|
||||
"start_date" timestamptz(6),
|
||||
"end_date" timestamptz(6),
|
||||
"status" int4 NOT NULL DEFAULT 1,
|
||||
"remark" varchar(500),
|
||||
"tenant_id" int4,
|
||||
"delete_flag" bpchar(1) DEFAULT '0'::bpchar NOT NULL,
|
||||
"create_by" varchar(32) DEFAULT ''::varchar NOT NULL,
|
||||
"create_time" timestamptz(6) NOT NULL,
|
||||
"update_by" varchar(32),
|
||||
"update_time" timestamptz(6),
|
||||
CONSTRAINT "adm_practitioner_patient_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."id" IS '主键ID';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."practitioner_id" IS '医生ID,关联adm_practitioner表';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."patient_id" IS '患者ID,关联adm_patient表';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."relationship_type" IS '关系类型:1-主治医生,2-签约医生,3-管床医生,4-家庭医生,5-会诊医生,6-随访医生';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."organization_id" IS '机构ID,关联adm_organization表';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."start_date" IS '关系开始时间';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."end_date" IS '关系结束时间';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."status" IS '状态:1-有效,0-无效';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."remark" IS '备注信息';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."tenant_id" IS '租户ID';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."delete_flag" IS '删除标志:0-未删除,1-已删除';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."create_by" IS '创建人';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."create_time" IS '创建时间';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."update_by" IS '更新人';
|
||||
COMMENT ON COLUMN "adm_practitioner_patient"."update_time" IS '更新时间';
|
||||
COMMENT ON TABLE "adm_practitioner_patient" IS '医生患者关系表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_practitioner_id" ON "adm_practitioner_patient" USING btree ("practitioner_id", "delete_flag");
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_patient_id" ON "adm_practitioner_patient" USING btree ("patient_id", "delete_flag");
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_org_id" ON "adm_practitioner_patient" USING btree ("organization_id", "delete_flag");
|
||||
CREATE INDEX IF NOT EXISTS "idx_practitioner_patient_type" ON "adm_practitioner_patient" USING btree ("relationship_type", "delete_flag");
|
||||
|
||||
-- 插入迁移记录
|
||||
INSERT INTO "__migrationshistory" ("version", "description")
|
||||
VALUES ('202601020000 add_table_adm_practitioner_patient', '1.0.0')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 步骤3:验证表是否创建成功
|
||||
-- 执行以下SQL查询
|
||||
SELECT table_name, table_comment
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'adm_practitioner_patient';
|
||||
|
||||
-- 应该返回一条记录,显示表名和表注释
|
||||
140
迁移记录-DB变更记录/菜单路由地址重复问题修复指南.md
Normal file
140
迁移记录-DB变更记录/菜单路由地址重复问题修复指南.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 菜单路由地址重复问题修复指南
|
||||
|
||||
## 问题描述
|
||||
在菜单管理中,修改菜单时即使不做任何修改直接点确定,仍然提示"路由地址已存在"。
|
||||
|
||||
## 问题原因
|
||||
数据库中 `sys_menu` 表存在多条记录使用了相同的 `path` 值,导致校验逻辑误判。
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 步骤1:查找重复的 path
|
||||
在数据库中执行以下SQL查询:
|
||||
|
||||
**PostgreSQL 版本(当前使用):**
|
||||
```sql
|
||||
-- 查找所有重复的 path
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count,
|
||||
STRING_AGG(CAST(menu_id AS TEXT), ', ') as menu_ids,
|
||||
STRING_AGG(menu_name, ', ') as menu_names
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
**MySQL 版本:**
|
||||
```sql
|
||||
-- 查找所有重复的 path
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count,
|
||||
GROUP_CONCAT(menu_id) as menu_ids,
|
||||
GROUP_CONCAT(menu_name) as menu_names
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
### 步骤2:查看具体重复记录的详细信息
|
||||
将上面的查询结果中的 path 值替换到下面的SQL中:
|
||||
|
||||
```sql
|
||||
-- 查看某个重复 path 的详细信息
|
||||
SELECT * FROM sys_menu
|
||||
WHERE path = 'your_duplicate_path_value'
|
||||
ORDER BY menu_id;
|
||||
```
|
||||
|
||||
### 步骤3:删除重复记录(保留 menu_id 最小的记录)
|
||||
```sql
|
||||
DELETE FROM sys_menu
|
||||
WHERE menu_id IN (
|
||||
SELECT menu_id FROM (
|
||||
SELECT menu_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY path ORDER BY menu_id) as rn
|
||||
FROM sys_menu
|
||||
) t
|
||||
WHERE rn > 1
|
||||
);
|
||||
```
|
||||
|
||||
### 步骤4:验证修复结果
|
||||
```sql
|
||||
-- 验证是否还有重复的 path
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count
|
||||
FROM sys_menu
|
||||
GROUP BY path
|
||||
HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
如果没有查询结果,说明重复数据已经清理完毕。
|
||||
|
||||
## 代码修改
|
||||
|
||||
### 已完成的代码修改:
|
||||
|
||||
1. **SysMenuMapper.java** - 新增方法
|
||||
```java
|
||||
public SysMenu selectMenuByPathExcludeId(@Param("path") String path, @Param("menuId") Long menuId);
|
||||
```
|
||||
|
||||
2. **SysMenuMapper.xml** - 新增查询
|
||||
```xml
|
||||
<select id="selectMenuByPathExcludeId" resultMap="SysMenuResult">
|
||||
<include refid="selectMenuVo"/>
|
||||
where path = #{path} and menu_id != #{menuId}
|
||||
</select>
|
||||
```
|
||||
|
||||
3. **SysMenuServiceImpl.java** - 修改校验逻辑
|
||||
```java
|
||||
@Override
|
||||
public int updateMenu(SysMenu menu) {
|
||||
//路径Path唯一性判断(排除当前菜单本身)
|
||||
String path = menu.getPath();
|
||||
if (StringUtils.isNotBlank(path)) {
|
||||
SysMenu sysMenu = menuMapper.selectMenuByPathExcludeId(menu.getPath(), menu.getMenuId());
|
||||
if (sysMenu != null) {
|
||||
log.warn("路由地址已存在 - menuId: {}, path: {}, 存在的menuId: {}",
|
||||
menu.getMenuId(), menu.getPath(), sysMenu.getMenuId());
|
||||
return -1; // 路由地址已存在
|
||||
}
|
||||
}
|
||||
// 执行更新
|
||||
return menuMapper.updateMenu(menu);
|
||||
}
|
||||
```
|
||||
|
||||
## 调试信息
|
||||
|
||||
当系统提示"路由地址已存在"时,请查看后端日志,会输出类似以下信息:
|
||||
```
|
||||
路由地址已存在 - menuId: 123, path: 'some/path', 存在的menuId: 456
|
||||
```
|
||||
这可以帮助您快速定位是哪两个菜单发生了冲突。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 在删除重复记录前,请先备份数据库
|
||||
2. 建议在测试环境先验证,确认无误后再在生产环境执行
|
||||
3. 如果某个 path 确实需要被多个菜单使用,需要修改业务逻辑
|
||||
|
||||
## 预防措施
|
||||
|
||||
建议在数据库中为 `path` 字段添加唯一索引,从数据库层面防止重复:
|
||||
|
||||
**PostgreSQL 版本:**
|
||||
```sql
|
||||
-- 注意:执行前需要先清理重复数据
|
||||
CREATE UNIQUE INDEX uk_path ON sys_menu(path);
|
||||
```
|
||||
|
||||
**MySQL 版本:**
|
||||
```sql
|
||||
-- 注意:执行前需要先清理重复数据
|
||||
ALTER TABLE sys_menu ADD UNIQUE KEY uk_path (path);
|
||||
```
|
||||
113
迁移记录-DB变更记录/门诊记录接口404排查指南.md
Normal file
113
迁移记录-DB变更记录/门诊记录接口404排查指南.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 门诊记录接口404问题排查指南
|
||||
|
||||
## 问题描述
|
||||
门诊记录页面报404错误,路径:`/openhis/patient-manage/records/outpatient-record-page`
|
||||
|
||||
## 修改内容
|
||||
|
||||
已创建 `OutpatientRecordController.java`:
|
||||
- 位置:`openhis-application/src/main/java/com/openhis/web/patientmanage/controller/OutpatientRecordController.java`
|
||||
- 请求路径:`/patient-manage/records`
|
||||
|
||||
## 排查步骤
|
||||
|
||||
### 1. 清理并重新编译后端项目
|
||||
|
||||
在命令行执行:
|
||||
```bash
|
||||
cd e:\his\openhis-server-new
|
||||
mvn clean install -DskipTests
|
||||
```
|
||||
|
||||
或者在 IDE 中:
|
||||
1. 点击 Maven 中的 `clean` 命令
|
||||
2. 等待清理完成
|
||||
3. 点击 `install` 或 `compile` 命令
|
||||
|
||||
### 2. 停止并重启后端服务
|
||||
|
||||
**重要:必须完全停止当前运行的后端服务,然后重新启动**
|
||||
|
||||
如果使用 IDE 运行:
|
||||
1. 停止当前运行的 Spring Boot 应用
|
||||
2. 重新运行 `OpenHisApplication.main()`
|
||||
|
||||
如果使用命令行运行:
|
||||
```bash
|
||||
# 停止当前服务 (Ctrl+C)
|
||||
cd e:\his\openhis-server-new\openhis-application
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
### 3. 验证Controller是否被加载
|
||||
|
||||
访问测试接口:
|
||||
```
|
||||
http://localhost:18080/openhis/patient-manage/records/test
|
||||
```
|
||||
|
||||
如果返回:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": "OutpatientRecordController 工作正常"
|
||||
}
|
||||
```
|
||||
|
||||
说明 Controller 已成功加载。
|
||||
|
||||
### 4. 检查后端启动日志
|
||||
|
||||
查看启动日志中是否有类似以下内容:
|
||||
```
|
||||
Mapped "{[/patient-manage/records/test]}" onto ...
|
||||
Mapped "{[/patient-manage/records/init]}" onto ...
|
||||
Mapped "{[/patient-manage/records/outpatient-record-page]}" onto ...
|
||||
Mapped "{[/patient-manage/records/doctor-names]}" onto ...
|
||||
```
|
||||
|
||||
如果没有这些映射日志,说明 Controller 没有被扫描到。
|
||||
|
||||
### 5. 检查包扫描配置
|
||||
|
||||
确认 `OpenHisApplication.java` 中:
|
||||
```java
|
||||
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class},
|
||||
scanBasePackages = {"com.core", "com.openhis"})
|
||||
```
|
||||
|
||||
这会扫描 `com.openhis` 包及其子包,包括:
|
||||
- `com.openhis.web.patientmanage.controller`
|
||||
|
||||
### 6. 检查编译输出
|
||||
|
||||
确认编译后的 class 文件存在于:
|
||||
```
|
||||
e:\his\openhis-server-new\openhis-application\target\classes\com\openhis\web\patientmanage\controller\OutpatientRecordController.class
|
||||
```
|
||||
|
||||
如果不存在,说明编译有问题。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 重启后还是404
|
||||
**A:** 确保完全停止了旧的进程。使用任务管理器检查是否有 `java.exe` 进程在运行,如果有则全部结束。
|
||||
|
||||
### Q: 编译成功但接口不工作
|
||||
**A:** 检查是否访问的是正确的端口和路径:
|
||||
- 端口:18080
|
||||
- 路径:/openhis/patient-manage/records/outpatient-record-page
|
||||
|
||||
### Q: 日志中没有映射信息
|
||||
**A:** 可能是注解使用错误。已修正为:
|
||||
- `@RequiredArgsConstructor` 替代 `@AllArgsConstructor`
|
||||
- `private final` 字段直接注入
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. 访问:`http://localhost:18080/openhis/patient-manage/records/test`
|
||||
2. 访问:`http://localhost:18080/openhis/patient-manage/records/doctor-names`
|
||||
3. 访问:`http://localhost:18080/openhis/patient-manage/records/outpatient-record-page`
|
||||
|
||||
如果都能正常返回,刷新前端页面即可。
|
||||
160
门诊就诊记录SQL优化建议.md
Normal file
160
门诊就诊记录SQL优化建议.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# 门诊就诊记录SQL查询优化建议
|
||||
|
||||
## 当前查询分析
|
||||
|
||||
### 主要查询表
|
||||
```sql
|
||||
SELECT
|
||||
enc.id as encounterId,
|
||||
pt.name,
|
||||
pt.id_card,
|
||||
pt.bus_no as patientBusNo,
|
||||
enc.bus_no as encounterBusNo,
|
||||
pt.gender_enum,
|
||||
pt.phone,
|
||||
enc.create_time as encounterTime,
|
||||
enc.status_enum as subjectStatusEnum,
|
||||
org.name as organizationName,
|
||||
prac.name as doctorName
|
||||
FROM adm_encounter AS enc
|
||||
LEFT JOIN adm_organization AS org ON enc.organization_id = org.ID AND org.delete_flag = '0'
|
||||
LEFT JOIN adm_encounter_participant AS ep
|
||||
ON enc.ID = ep.encounter_id AND ep.type_code = #{participantType} AND ep.delete_flag = '0'
|
||||
LEFT JOIN adm_practitioner AS prac ON ep.practitioner_id = prac.ID AND prac.delete_flag = '0'
|
||||
LEFT JOIN adm_patient AS pt ON enc.patient_id = pt.ID AND pt.delete_flag = '0'
|
||||
```
|
||||
|
||||
### 常见查询条件
|
||||
1. `enc.delete_flag = '0'`
|
||||
2. `enc.tenant_id = ?`
|
||||
3. `pt.name LIKE ?`
|
||||
4. `pt.id_card LIKE ?`
|
||||
5. `pt.bus_no LIKE ?`
|
||||
6. `enc.bus_no LIKE ?`
|
||||
7. `pt.gender_enum = ?`
|
||||
8. `enc.status_enum = ?`
|
||||
9. `prac.name LIKE ?`
|
||||
10. `pt.phone LIKE ?`
|
||||
11. `enc.create_time BETWEEN ? AND ?`
|
||||
|
||||
## 索引优化建议
|
||||
|
||||
### 1. adm_encounter 表索引
|
||||
```sql
|
||||
-- 复合索引:提高查询性能
|
||||
CREATE INDEX idx_encounter_tenant_delete_status ON adm_encounter(tenant_id, delete_flag, status_enum);
|
||||
|
||||
-- 时间范围查询索引
|
||||
CREATE INDEX idx_encounter_create_time ON adm_encounter(create_time);
|
||||
|
||||
-- 业务编号查询索引
|
||||
CREATE INDEX idx_encounter_bus_no ON adm_encounter(bus_no);
|
||||
|
||||
-- 患者ID关联索引
|
||||
CREATE INDEX idx_encounter_patient_id ON adm_encounter(patient_id);
|
||||
```
|
||||
|
||||
### 2. adm_patient 表索引
|
||||
```sql
|
||||
-- 姓名模糊查询索引
|
||||
CREATE INDEX idx_patient_name ON adm_patient(name);
|
||||
|
||||
-- 身份证号查询索引
|
||||
CREATE INDEX idx_patient_id_card ON adm_patient(id_card);
|
||||
|
||||
-- 业务编号查询索引
|
||||
CREATE INDEX idx_patient_bus_no ON adm_patient(bus_no);
|
||||
|
||||
-- 电话查询索引
|
||||
CREATE INDEX idx_patient_phone ON adm_patient(phone);
|
||||
|
||||
-- 复合索引:常用查询条件
|
||||
CREATE INDEX idx_patient_delete_gender ON adm_patient(delete_flag, gender_enum);
|
||||
```
|
||||
|
||||
### 3. adm_encounter_participant 表索引
|
||||
```sql
|
||||
-- 复合索引:提高连接性能
|
||||
CREATE INDEX idx_ep_encounter_type ON adm_encounter_participant(encounter_id, type_code, delete_flag);
|
||||
|
||||
-- 参与者ID索引
|
||||
CREATE INDEX idx_ep_practitioner ON adm_encounter_participant(practitioner_id);
|
||||
```
|
||||
|
||||
### 4. adm_practitioner 表索引
|
||||
```sql
|
||||
-- 姓名查询索引
|
||||
CREATE INDEX idx_practitioner_name ON adm_practitioner(name);
|
||||
|
||||
-- 复合索引:常用查询条件
|
||||
CREATE INDEX idx_practitioner_delete_tenant ON adm_practitioner(delete_flag, tenant_id);
|
||||
```
|
||||
|
||||
### 5. adm_organization 表索引
|
||||
```sql
|
||||
-- 主键关联索引
|
||||
CREATE INDEX idx_organization_id_delete ON adm_organization(id, delete_flag);
|
||||
```
|
||||
|
||||
## 查询优化建议
|
||||
|
||||
### 1. 添加查询统计信息收集
|
||||
```sql
|
||||
-- 定期分析表统计信息
|
||||
ANALYZE TABLE adm_encounter;
|
||||
ANALYZE TABLE adm_patient;
|
||||
ANALYZE TABLE adm_encounter_participant;
|
||||
ANALYZE TABLE adm_practitioner;
|
||||
ANALYZE TABLE adm_organization;
|
||||
```
|
||||
|
||||
### 2. 考虑分区表(针对大数据量)
|
||||
如果 `adm_encounter` 表数据量超过100万条,考虑按时间分区:
|
||||
```sql
|
||||
-- 按月分区
|
||||
PARTITION BY RANGE (YEAR(create_time) * 100 + MONTH(create_time))
|
||||
(
|
||||
PARTITION p202501 VALUES LESS THAN (202501),
|
||||
PARTITION p202502 VALUES LESS THAN (202502),
|
||||
-- ... 更多分区
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 添加覆盖索引(Covering Index)
|
||||
对于常用查询字段,创建覆盖索引避免回表:
|
||||
```sql
|
||||
CREATE INDEX idx_encounter_cover ON adm_encounter(
|
||||
tenant_id, delete_flag, create_time,
|
||||
status_enum, bus_no, patient_id
|
||||
) INCLUDE (organization_id);
|
||||
```
|
||||
|
||||
## 执行计划检查
|
||||
|
||||
建议定期检查查询执行计划:
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT -- 完整查询语句
|
||||
FROM adm_encounter AS enc
|
||||
-- ... 连接条件
|
||||
WHERE enc.delete_flag = '0'
|
||||
AND enc.tenant_id = 1
|
||||
-- ... 其他条件
|
||||
ORDER BY enc.create_time DESC;
|
||||
```
|
||||
|
||||
## 监控建议
|
||||
|
||||
1. **慢查询监控**:监控执行时间超过1秒的查询
|
||||
2. **索引使用监控**:定期检查未使用的索引
|
||||
3. **表空间监控**:监控表增长和碎片情况
|
||||
4. **连接性能监控**:监控JOIN操作的性能
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 在测试环境创建建议的索引
|
||||
2. 执行查询性能测试
|
||||
3. 分析执行计划,确认索引有效性
|
||||
4. 在生产环境非高峰期创建索引
|
||||
5. 监控生产环境性能变化
|
||||
6. 定期维护和优化索引
|
||||
Reference in New Issue
Block a user