feat(mobile+telehealth): 移动护理评估+输液管理+互联网医院

This commit is contained in:
2026-06-19 06:38:07 +08:00
parent 50cabbeb32
commit e117022bb6
18 changed files with 1654 additions and 0 deletions

View File

@@ -11,4 +11,9 @@ public interface INursingMobileAppService {
Map<String, Object> executeOrder(Long requestId, String adviceTable, Long encounterId, Long patientId);
NursingMobileVitalSignDto saveVitalSign(NursingMobileVitalSignDto vitalSign);
NursingMobileVitalSignTrendDto getVitalSignTrend(Long patientId, Integer days);
NursingMobileAssessmentDto submitAssessment(NursingMobileAssessmentDto dto);
List<NursingMobileAssessmentDto> getAssessmentList(Long patientId);
NursingMobileInfusionDto startInfusion(NursingMobileInfusionDto dto);
NursingMobileInfusionDto addPatrol(NursingMobileInfusionDto dto);
List<NursingMobileInfusionDto> getInfusionStatus(Long patientId);
}

View File

@@ -1,11 +1,16 @@
package com.healthlink.his.web.nursing.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.nursing.domain.NursingAssessment;
import com.healthlink.his.nursing.domain.NursingInfusionPatrol;
import com.healthlink.his.nursing.domain.NursingVitalSignsChart;
import com.healthlink.his.nursing.service.INursingAssessmentService;
import com.healthlink.his.nursing.service.INursingInfusionPatrolService;
import com.healthlink.his.nursing.service.INursingVitalSignsChartService;
import com.healthlink.his.web.nursing.appservice.INursingMobileAppService;
import com.healthlink.his.web.nursing.dto.*;
import com.healthlink.his.web.nursing.mapper.NursingMobileAppMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -24,6 +29,14 @@ public class NursingMobileAppServiceImpl implements INursingMobileAppService {
@Resource
private INursingVitalSignsChartService vitalSignsChartService;
@Resource
private INursingAssessmentService assessmentService;
@Resource
private INursingInfusionPatrolService infusionPatrolService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public List<NursingMobilePatientDto> getMobilePatientList(String wardName, String searchKey) {
return mobileMapper.selectMobilePatientList(wardName, searchKey);
@@ -156,4 +169,158 @@ public class NursingMobileAppServiceImpl implements INursingMobileAppService {
return trend;
}
@Override
@Transactional(rollbackFor = Exception.class)
public NursingMobileAssessmentDto submitAssessment(NursingMobileAssessmentDto dto) {
NursingAssessment assessment = new NursingAssessment();
assessment.setEncounterId(dto.getEncounterId());
assessment.setPatientId(dto.getPatientId());
assessment.setPatientName(dto.getPatientName());
assessment.setAssessorId(dto.getAssessorId());
assessment.setAssessorName(dto.getAssessorName());
assessment.setAssessmentType(dto.getAssessmentType());
assessment.setAssessmentTool(dto.getAssessmentTool());
assessment.setTotalScore(dto.getTotalScore());
assessment.setRiskLevel(calculateRiskLevel(dto.getAssessmentTool(), dto.getTotalScore()));
assessment.setDetail(dto.getDetail());
assessment.setAssessmentTime(dto.getAssessmentTime() != null ? dto.getAssessmentTime() : new Date());
assessment.setDeleteFlag("0");
try {
assessment.setItemScores(dto.getItemScores() != null ? objectMapper.writeValueAsString(dto.getItemScores()) : null);
} catch (Exception e) {
assessment.setItemScores(null);
}
assessmentService.save(assessment);
dto.setId(assessment.getId());
dto.setRiskLevel(assessment.getRiskLevel());
return dto;
}
@Override
public List<NursingMobileAssessmentDto> getAssessmentList(Long patientId) {
LambdaQueryWrapper<NursingAssessment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(NursingAssessment::getPatientId, patientId)
.orderByDesc(NursingAssessment::getAssessmentTime);
List<NursingAssessment> records = assessmentService.list(wrapper);
List<NursingMobileAssessmentDto> result = new ArrayList<>();
for (NursingAssessment r : records) {
NursingMobileAssessmentDto dto = new NursingMobileAssessmentDto();
dto.setId(r.getId());
dto.setEncounterId(r.getEncounterId());
dto.setPatientId(r.getPatientId());
dto.setPatientName(r.getPatientName());
dto.setAssessorName(r.getAssessorName());
dto.setAssessmentType(r.getAssessmentType());
dto.setAssessmentTool(r.getAssessmentTool());
dto.setTotalScore(r.getTotalScore());
dto.setRiskLevel(r.getRiskLevel());
dto.setDetail(r.getDetail());
dto.setAssessmentTime(r.getAssessmentTime());
try {
if (r.getItemScores() != null) {
dto.setItemScores(objectMapper.readValue(r.getItemScores(), Map.class));
}
} catch (Exception e) {
dto.setItemScores(null);
}
result.add(dto);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public NursingMobileInfusionDto startInfusion(NursingMobileInfusionDto dto) {
NursingInfusionPatrol patrol = new NursingInfusionPatrol();
patrol.setEncounterId(dto.getEncounterId());
patrol.setPatientId(dto.getPatientId());
patrol.setPatientName(dto.getPatientName());
patrol.setOrderId(dto.getOrderId());
patrol.setDrugName(dto.getDrugName());
patrol.setInfusionRate(dto.getInfusionRate());
patrol.setTotalVolume(dto.getTotalVolume());
patrol.setStartTime(dto.getStartTime() != null ? dto.getStartTime() : new Date());
patrol.setPatencyStatus("NORMAL");
patrol.setPatrolNurseId(dto.getPatrolNurseId());
patrol.setPatrolNurseName(dto.getPatrolNurseName());
patrol.setCreateTime(new Date());
infusionPatrolService.save(patrol);
dto.setId(patrol.getId());
dto.setPatencyStatus("NORMAL");
dto.setStatus("RUNNING");
return dto;
}
@Override
@Transactional(rollbackFor = Exception.class)
public NursingMobileInfusionDto addPatrol(NursingMobileInfusionDto dto) {
NursingInfusionPatrol patrol = new NursingInfusionPatrol();
patrol.setEncounterId(dto.getEncounterId());
patrol.setPatientId(dto.getPatientId());
patrol.setPatientName(dto.getPatientName());
patrol.setOrderId(dto.getOrderId());
patrol.setDrugName(dto.getDrugName());
patrol.setPatrolTime(new Date());
patrol.setDripRate(dto.getDripRate());
patrol.setPatencyStatus(dto.getPatencyStatus());
patrol.setAdverseReaction(dto.getAdverseReaction());
patrol.setPatrolNurseId(dto.getPatrolNurseId());
patrol.setPatrolNurseName(dto.getPatrolNurseName());
patrol.setCreateTime(new Date());
infusionPatrolService.save(patrol);
dto.setId(patrol.getId());
dto.setPatrolTime(patrol.getPatrolTime());
return dto;
}
@Override
public List<NursingMobileInfusionDto> getInfusionStatus(Long patientId) {
LambdaQueryWrapper<NursingInfusionPatrol> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(NursingInfusionPatrol::getPatientId, patientId)
.orderByDesc(NursingInfusionPatrol::getStartTime);
List<NursingInfusionPatrol> records = infusionPatrolService.list(wrapper);
Map<Long, NursingMobileInfusionDto> latestMap = new LinkedHashMap<>();
for (NursingInfusionPatrol r : records) {
Long orderId = r.getOrderId();
if (orderId == null) orderId = r.getId();
if (!latestMap.containsKey(orderId)) {
NursingMobileInfusionDto dto = new NursingMobileInfusionDto();
dto.setId(r.getId());
dto.setEncounterId(r.getEncounterId());
dto.setPatientId(r.getPatientId());
dto.setPatientName(r.getPatientName());
dto.setOrderId(r.getOrderId());
dto.setDrugName(r.getDrugName());
dto.setInfusionRate(r.getInfusionRate());
dto.setTotalVolume(r.getTotalVolume());
dto.setStartTime(r.getStartTime());
dto.setPatrolTime(r.getPatrolTime());
dto.setDripRate(r.getDripRate());
dto.setPatencyStatus(r.getPatencyStatus());
dto.setAdverseReaction(r.getAdverseReaction());
dto.setPatrolNurseName(r.getPatrolNurseName());
dto.setStatus("RUNNING");
latestMap.put(orderId, dto);
}
}
return new ArrayList<>(latestMap.values());
}
private String calculateRiskLevel(String tool, Integer score) {
if (score == null) return "NORMAL";
if ("BRADEN".equals(tool)) {
if (score <= 12) return "HIGH";
if (score <= 14) return "MEDIUM";
return "LOW";
} else if ("MORSE".equals(tool)) {
if (score >= 45) return "HIGH";
if (score >= 25) return "MEDIUM";
return "LOW";
} else if ("NRS2002".equals(tool)) {
if (score >= 3) return "HIGH";
return "LOW";
}
return "NORMAL";
}
}

View File

@@ -69,4 +69,44 @@ public class NursingMobileController {
NursingMobileVitalSignTrendDto trend = mobileAppService.getVitalSignTrend(patientId, days);
return R.ok(trend);
}
@Operation(summary = "提交护理评估")
@PostMapping("/assessment/submit")
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
public R<?> submitAssessment(@RequestBody NursingMobileAssessmentDto assessment) {
NursingMobileAssessmentDto saved = mobileAppService.submitAssessment(assessment);
return R.ok(saved);
}
@Operation(summary = "查询评估记录")
@GetMapping("/assessment/list/{patientId}")
@PreAuthorize("hasAuthority('nursing:nursing:list')")
public R<?> getAssessmentList(@PathVariable Long patientId) {
List<NursingMobileAssessmentDto> list = mobileAppService.getAssessmentList(patientId);
return R.ok(list);
}
@Operation(summary = "开始输液")
@PostMapping("/infusion/start")
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
public R<?> startInfusion(@RequestBody NursingMobileInfusionDto infusion) {
NursingMobileInfusionDto saved = mobileAppService.startInfusion(infusion);
return R.ok(saved);
}
@Operation(summary = "输液巡视记录")
@PostMapping("/infusion/patrol")
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
public R<?> addPatrol(@RequestBody NursingMobileInfusionDto patrol) {
NursingMobileInfusionDto saved = mobileAppService.addPatrol(patrol);
return R.ok(saved);
}
@Operation(summary = "输液状态查询")
@GetMapping("/infusion/status/{patientId}")
@PreAuthorize("hasAuthority('nursing:nursing:list')")
public R<?> getInfusionStatus(@PathVariable Long patientId) {
List<NursingMobileInfusionDto> list = mobileAppService.getInfusionStatus(patientId);
return R.ok(list);
}
}

View File

@@ -0,0 +1,24 @@
package com.healthlink.his.web.nursing.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.Date;
import java.util.Map;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class NursingMobileAssessmentDto {
private Long id;
private Long encounterId;
private Long patientId;
private String patientName;
private Long assessorId;
private String assessorName;
private String assessmentType;
private String assessmentTool;
private Integer totalScore;
private String riskLevel;
private Map<String, Integer> itemScores;
private String detail;
private Date assessmentTime;
}

View File

@@ -0,0 +1,30 @@
package com.healthlink.his.web.nursing.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.Date;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class NursingMobileInfusionDto {
private Long id;
private Long encounterId;
private Long patientId;
private String patientName;
private Long orderId;
private String drugName;
private String infusionRate;
private Integer totalVolume;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date patrolTime;
private Integer dripRate;
private String patencyStatus;
private String adverseReaction;
private Long patrolNurseId;
private String patrolNurseName;
private String status;
private Integer remainingVolume;
}

View File

@@ -0,0 +1,15 @@
package com.healthlink.his.web.telehealth.appservice;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.healthlink.his.web.telehealth.dto.TelehealthConsultationDto;
public interface ITelehealthAppService {
Long createConsultation(TelehealthConsultationDto dto);
Page<TelehealthConsultationDto> pageConsultation(TelehealthConsultationDto dto);
Boolean replyConsultation(TelehealthConsultationDto dto);
Boolean prescribeConsultation(TelehealthConsultationDto dto);
}

View File

@@ -0,0 +1,102 @@
package com.healthlink.his.web.telehealth.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.utils.SecurityUtils;
import com.healthlink.his.web.telehealth.appservice.ITelehealthAppService;
import com.healthlink.his.web.telehealth.domain.TelehealthConsultation;
import com.healthlink.his.web.telehealth.dto.TelehealthConsultationDto;
import com.healthlink.his.web.telehealth.mapper.TelehealthConsultationMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import jakarta.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class TelehealthAppServiceImpl implements ITelehealthAppService {
@Resource
private TelehealthConsultationMapper telehealthConsultationMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createConsultation(TelehealthConsultationDto dto) {
TelehealthConsultation entity = new TelehealthConsultation();
entity.setPatientId(dto.getPatientId());
entity.setDoctorId(dto.getDoctorId());
entity.setConsultationType(dto.getConsultationType());
entity.setStatus("PENDING");
entity.setChiefComplaint(dto.getChiefComplaint());
entity.setConsultationTime(new Date());
entity.setTenantId(SecurityUtils.getLoginUser().getTenantId());
telehealthConsultationMapper.insert(entity);
return entity.getId();
}
@Override
public Page<TelehealthConsultationDto> pageConsultation(TelehealthConsultationDto dto) {
Page<TelehealthConsultation> page = new Page<>(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<TelehealthConsultation> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(dto.getStatus())) {
wrapper.eq(TelehealthConsultation::getStatus, dto.getStatus());
}
if (dto.getDoctorId() != null) {
wrapper.eq(TelehealthConsultation::getDoctorId, dto.getDoctorId());
}
if (dto.getPatientId() != null) {
wrapper.eq(TelehealthConsultation::getPatientId, dto.getPatientId());
}
wrapper.orderByDesc(TelehealthConsultation::getCreateTime);
Page<TelehealthConsultation> result = telehealthConsultationMapper.selectPage(page, wrapper);
Page<TelehealthConsultationDto> dtoPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
List<TelehealthConsultationDto> dtoList = result.getRecords().stream()
.map(this::toDto)
.collect(Collectors.toList());
dtoPage.setRecords(dtoList);
return dtoPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean replyConsultation(TelehealthConsultationDto dto) {
TelehealthConsultation entity = telehealthConsultationMapper.selectById(dto.getId());
if (entity == null) {
throw new RuntimeException("问诊记录不存在");
}
entity.setDiagnosis(dto.getDiagnosis());
entity.setStatus("IN_PROGRESS");
telehealthConsultationMapper.updateById(entity);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean prescribeConsultation(TelehealthConsultationDto dto) {
TelehealthConsultation entity = telehealthConsultationMapper.selectById(dto.getId());
if (entity == null) {
throw new RuntimeException("问诊记录不存在");
}
entity.setPrescription(dto.getPrescription());
entity.setDiagnosis(dto.getDiagnosis());
entity.setStatus("COMPLETED");
entity.setEndTime(new Date());
telehealthConsultationMapper.updateById(entity);
return true;
}
private TelehealthConsultationDto toDto(TelehealthConsultation entity) {
TelehealthConsultationDto dto = new TelehealthConsultationDto();
BeanUtils.copyProperties(entity, dto);
return dto;
}
}

View File

@@ -0,0 +1,75 @@
package com.healthlink.his.web.telehealth.controller;
import com.core.common.core.domain.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.healthlink.his.web.telehealth.appservice.ITelehealthAppService;
import com.healthlink.his.web.telehealth.dto.TelehealthConsultationDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
@Slf4j
@Tag(name = "互联网医院-在线问诊")
@RestController
@RequestMapping("/telehealth/consultation")
public class TelehealthController {
@Resource
private ITelehealthAppService telehealthAppService;
@Operation(summary = "创建问诊")
@PostMapping("/create")
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:edit')")
public R<Long> create(@RequestBody TelehealthConsultationDto dto) {
try {
Long id = telehealthAppService.createConsultation(dto);
return R.ok(id);
} catch (Exception e) {
log.error("创建问诊失败", e);
return R.fail("创建问诊失败: " + e.getMessage());
}
}
@Operation(summary = "问诊列表")
@GetMapping("/page")
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:list')")
public R<Page<TelehealthConsultationDto>> page(TelehealthConsultationDto dto) {
try {
Page<TelehealthConsultationDto> result = telehealthAppService.pageConsultation(dto);
return R.ok(result);
} catch (Exception e) {
log.error("查询问诊列表失败", e);
return R.fail("查询问诊列表失败: " + e.getMessage());
}
}
@Operation(summary = "医生回复")
@PostMapping("/reply")
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:edit')")
public R<String> reply(@RequestBody TelehealthConsultationDto dto) {
try {
telehealthAppService.replyConsultation(dto);
return R.ok("回复成功");
} catch (Exception e) {
log.error("医生回复失败", e);
return R.fail("回复失败: " + e.getMessage());
}
}
@Operation(summary = "复诊开方")
@PostMapping("/prescribe")
@PreAuthorize("@ss.hasPermi('outpatient:telehealth:edit')")
public R<String> prescribe(@RequestBody TelehealthConsultationDto dto) {
try {
telehealthAppService.prescribeConsultation(dto);
return R.ok("开方成功");
} catch (Exception e) {
log.error("复诊开方失败", e);
return R.fail("开方失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,46 @@
package com.healthlink.his.web.telehealth.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("telehealth_consultation")
public class TelehealthConsultation extends HisBaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("patient_id")
private Long patientId;
@TableField("doctor_id")
private Long doctorId;
@TableField("consultation_type")
private String consultationType;
@TableField("status")
private String status;
@TableField("chief_complaint")
private String chiefComplaint;
@TableField("diagnosis")
private String diagnosis;
@TableField("prescription")
private String prescription;
@TableField("consultation_time")
private Date consultationTime;
@TableField("end_time")
private Date endTime;
}

View File

@@ -0,0 +1,43 @@
package com.healthlink.his.web.telehealth.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
@Data
public class TelehealthConsultationDto {
private Long id;
private Long patientId;
private String patientName;
private Long doctorId;
private String doctorName;
private String consultationType;
private String status;
private String chiefComplaint;
private String diagnosis;
private String prescription;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date consultationTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private Integer pageNum = 1;
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,10 @@
package com.healthlink.his.web.telehealth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.web.telehealth.domain.TelehealthConsultation;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TelehealthConsultationMapper extends BaseMapper<TelehealthConsultation> {
}

View File

@@ -0,0 +1,19 @@
-- V85: 互联网医院 - 在线问诊+复诊开方
CREATE TABLE IF NOT EXISTS telehealth_consultation (
id BIGSERIAL PRIMARY KEY,
patient_id BIGINT NOT NULL,
doctor_id BIGINT NOT NULL,
consultation_type VARCHAR(20) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
chief_complaint TEXT,
diagnosis TEXT,
prescription TEXT,
consultation_time TIMESTAMP,
end_time TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64),
update_time TIMESTAMP,
update_by VARCHAR(64)
);

View File

@@ -0,0 +1,33 @@
import request from '@/utils/request'
export function createConsultation(data) {
return request({
url: '/telehealth/consultation/create',
method: 'post',
data: data
})
}
export function pageConsultation(query) {
return request({
url: '/telehealth/consultation/page',
method: 'get',
params: query
})
}
export function replyConsultation(data) {
return request({
url: '/telehealth/consultation/reply',
method: 'post',
data: data
})
}
export function prescribeConsultation(data) {
return request({
url: '/telehealth/consultation/prescribe',
method: 'post',
data: data
})
}

View File

@@ -0,0 +1,320 @@
<template>
<div class="mobile-infusion">
<div class="page-header">
<el-page-header @back="goBack">
<template #content>
<span class="page-title">{{ patientName }} - 输液管理</span>
</template>
</el-page-header>
<el-button type="primary" size="small" @click="showStartDialog">开始输液</el-button>
</div>
<div v-loading="loading" class="infusion-list">
<div
v-for="item in infusionList"
:key="item.id"
class="infusion-card"
>
<div class="card-header">
<span class="drug-name">{{ item.drugName || '输液' }}</span>
<el-tag :type="getStatusType(item.patencyStatus)" size="small">
{{ getStatusText(item.patencyStatus) }}
</el-tag>
</div>
<div class="card-body">
<div class="info-row">
<span class="label">输液速度:</span>
<span class="value">{{ item.infusionRate || '-' }}</span>
</div>
<div class="info-row">
<span class="label">总量:</span>
<span class="value">{{ item.totalVolume ? item.totalVolume + 'ml' : '-' }}</span>
</div>
<div class="info-row">
<span class="label">开始时间:</span>
<span class="value">{{ formatTime(item.startTime) }}</span>
</div>
<div class="info-row" v-if="item.patrolTime">
<span class="label">最近巡视:</span>
<span class="value">{{ formatTime(item.patrolTime) }}</span>
</div>
<div class="info-row" v-if="item.dripRate">
<span class="label">滴速:</span>
<span class="value">{{ item.dripRate }} /</span>
</div>
<div class="info-row" v-if="item.adverseReaction">
<span class="label">不良反应:</span>
<span class="value danger-text">{{ item.adverseReaction }}</span>
</div>
</div>
<div class="card-footer">
<el-button size="small" @click="showPatrolDialog(item)">巡视记录</el-button>
</div>
</div>
<el-empty v-if="!loading && infusionList.length === 0" description="暂无输液记录" />
</div>
<el-dialog v-model="startDialogVisible" title="开始输液" width="400px">
<el-form :model="startForm" label-width="80px">
<el-form-item label="药品名称">
<el-input v-model="startForm.drugName" placeholder="请输入药品名称" />
</el-form-item>
<el-form-item label="输液速度">
<el-input v-model="startForm.infusionRate" placeholder="如: 40滴/分" />
</el-form-item>
<el-form-item label="总量(ml)">
<el-input-number v-model="startForm.totalVolume" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="护士姓名">
<el-input v-model="startForm.patrolNurseName" placeholder="执行护士" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="startDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleStartInfusion">确认开始</el-button>
</template>
</el-dialog>
<el-dialog v-model="patrolDialogVisible" title="输液巡视" width="400px">
<el-form :model="patrolForm" label-width="80px">
<el-form-item label="滴速(滴/分)">
<el-input-number v-model="patrolForm.dripRate" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="通畅状态">
<el-select v-model="patrolForm.patencyStatus" style="width: 100%">
<el-option label="正常" value="NORMAL" />
<el-option label="堵塞" value="OCCLUDED" />
<el-option label="静脉炎" value="PHLEBITIS" />
</el-select>
</el-form-item>
<el-form-item label="不良反应">
<el-input v-model="patrolForm.adverseReaction" placeholder="无则留空" />
</el-form-item>
<el-form-item label="巡视护士">
<el-input v-model="patrolForm.patrolNurseName" placeholder="巡视护士" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="patrolDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handlePatrol">确认记录</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getInfusionStatus, startInfusion, addInfusionPatrol } from './api'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const submitting = ref(false)
const patientName = ref(route.query.patientName || '')
const patientId = ref(route.query.patientId)
const encounterId = ref(route.query.encounterId)
const infusionList = ref([])
const startDialogVisible = ref(false)
const patrolDialogVisible = ref(false)
const currentInfusion = ref(null)
const startForm = reactive({
drugName: '',
infusionRate: '',
totalVolume: 250,
patrolNurseName: ''
})
const patrolForm = reactive({
dripRate: null,
patencyStatus: 'NORMAL',
adverseReaction: '',
patrolNurseName: ''
})
const goBack = () => {
router.push('/nursingmobile/patient-list')
}
const formatTime = (time) => {
if (!time) return '-'
const d = new Date(time)
return d.toLocaleString('zh-CN')
}
const getStatusType = (status) => {
const map = { NORMAL: 'success', OCCLUDED: 'danger', PHLEBITIS: 'warning' }
return map[status] || 'info'
}
const getStatusText = (status) => {
const map = { NORMAL: '正常', OCCLUDED: '堵塞', PHLEBITIS: '静脉炎' }
return map[status] || '未知'
}
const fetchInfusionList = async () => {
if (!patientId.value) return
loading.value = true
try {
const res = await getInfusionStatus(patientId.value)
infusionList.value = res.data || []
} finally {
loading.value = false
}
}
const showStartDialog = () => {
startForm.drugName = ''
startForm.infusionRate = ''
startForm.totalVolume = 250
startForm.patrolNurseName = ''
startDialogVisible.value = true
}
const showPatrolDialog = (item) => {
currentInfusion.value = item
patrolForm.dripRate = item.dripRate || null
patrolForm.patencyStatus = item.patencyStatus || 'NORMAL'
patrolForm.adverseReaction = ''
patrolForm.patrolNurseName = ''
patrolDialogVisible.value = true
}
const handleStartInfusion = async () => {
if (!startForm.drugName) {
ElMessage.warning('请输入药品名称')
return
}
submitting.value = true
try {
await startInfusion({
patientId: patientId.value ? Number(patientId.value) : null,
encounterId: encounterId.value ? Number(encounterId.value) : null,
patientName: patientName.value,
drugName: startForm.drugName,
infusionRate: startForm.infusionRate,
totalVolume: startForm.totalVolume,
patrolNurseName: startForm.patrolNurseName,
startTime: new Date()
})
ElMessage.success('输液已开始')
startDialogVisible.value = false
fetchInfusionList()
} catch (e) {
ElMessage.error('操作失败')
} finally {
submitting.value = false
}
}
const handlePatrol = async () => {
submitting.value = true
try {
await addInfusionPatrol({
patientId: patientId.value ? Number(patientId.value) : null,
encounterId: encounterId.value ? Number(encounterId.value) : null,
patientName: patientName.value,
orderId: currentInfusion.value.orderId || currentInfusion.value.id,
drugName: currentInfusion.value.drugName,
dripRate: patrolForm.dripRate,
patencyStatus: patrolForm.patencyStatus,
adverseReaction: patrolForm.adverseReaction,
patrolNurseName: patrolForm.patrolNurseName,
patrolTime: new Date()
})
ElMessage.success('巡视记录已保存')
patrolDialogVisible.value = false
fetchInfusionList()
} catch (e) {
ElMessage.error('记录失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
fetchInfusionList()
})
</script>
<style scoped>
.mobile-infusion {
padding: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.page-title {
font-size: 16px;
font-weight: 600;
}
.infusion-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.infusion-card {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.drug-name {
font-size: 15px;
font-weight: 600;
}
.card-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
align-items: center;
gap: 8px;
}
.info-row .label {
color: #666;
font-size: 13px;
min-width: 70px;
}
.info-row .value {
font-size: 14px;
}
.danger-text {
color: #f56c6c;
}
.card-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,395 @@
<template>
<div class="mobile-assessment">
<div class="page-header">
<el-page-header @back="goBack">
<template #content>
<span class="page-title">{{ patientName }} - 护理评估</span>
</template>
</el-page-header>
</div>
<div class="tool-selector">
<el-radio-group v-model="selectedTool" size="small" @change="onToolChange">
<el-radio-button value="BRADEN">Braden压疮</el-radio-button>
<el-radio-button value="MORSE">Morse跌倒</el-radio-button>
<el-radio-button value="NRS2002">NRS2002营养</el-radio-button>
</el-radio-group>
</div>
<el-card class="assessment-form" v-loading="submitting">
<template #header>
<span>{{ toolConfig.title }}</span>
</template>
<div class="items-list">
<div v-for="(item, index) in toolConfig.items" :key="index" class="assessment-item">
<div class="item-label">{{ item.label }}</div>
<div class="item-options">
<el-radio-group v-model="itemScores[item.key]" size="small">
<el-radio-button
v-for="opt in item.options"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }} ({{ opt.value }})
</el-radio-button>
</el-radio-group>
</div>
</div>
</div>
<div class="score-summary">
<div class="total-score">
总分: <span class="score-value">{{ totalScore }}</span>
</div>
<div class="risk-level">
风险等级:
<el-tag :type="getRiskType(currentRiskLevel)" size="small">
{{ getRiskText(currentRiskLevel) }}
</el-tag>
</div>
</div>
<el-form-item label="评估备注" style="margin-top: 12px;">
<el-input v-model="detail" type="textarea" :rows="2" placeholder="可选备注" />
</el-form-item>
<el-button type="primary" style="width: 100%" @click="handleSubmit">
提交评估
</el-button>
</el-card>
<el-card class="history-section" v-loading="historyLoading">
<template #header>
<span>评估记录</span>
</template>
<div v-for="record in historyList" :key="record.id" class="history-item">
<div class="history-header">
<span class="history-tool">{{ getToolName(record.assessmentTool) }}</span>
<el-tag :type="getRiskType(record.riskLevel)" size="small">
{{ getRiskText(record.riskLevel) }}
</el-tag>
</div>
<div class="history-body">
<span class="history-score">评分: {{ record.totalScore }}</span>
<span class="history-time">{{ formatTime(record.assessmentTime) }}</span>
</div>
</div>
<el-empty v-if="!historyLoading && historyList.length === 0" description="暂无评估记录" />
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { submitAssessment, getAssessmentList } from './api'
const route = useRoute()
const router = useRouter()
const submitting = ref(false)
const historyLoading = ref(false)
const patientName = ref(route.query.patientName || '')
const patientId = ref(route.query.patientId)
const encounterId = ref(route.query.encounterId)
const selectedTool = ref('BRADEN')
const detail = ref('')
const historyList = ref([])
const bradenItems = [
{ key: 'sensory', label: '感觉感知', options: [
{ label: '完全受限', value: 1 }, { label: '严重受限', value: 2 },
{ label: '轻度受限', value: 3 }, { label: '未受损', value: 4 }
]},
{ key: 'moisture', label: '潮湿程度', options: [
{ label: '持续潮湿', value: 1 }, { label: '经常潮湿', value: 2 },
{ label: '有时潮湿', value: 3 }, { label: '很少潮湿', value: 4 }
]},
{ key: 'activity', label: '活动能力', options: [
{ label: '卧床不起', value: 1 }, { label: '仅限于椅', value: 2 },
{ label: '偶尔步行', value: 3 }, { label: '经常步行', value: 4 }
]},
{ key: 'mobility', label: '移动能力', options: [
{ label: '完全不动', value: 1 }, { label: '严重受限', value: 2 },
{ label: '轻度受限', value: 3 }, { label: '不受限', value: 4 }
]},
{ key: 'nutrition', label: '营养摄取', options: [
{ label: '非常差', value: 1 }, { label: '可能不足', value: 2 },
{ label: '充足', value: 3 }, { label: '丰富', value: 4 }
]},
{ key: 'friction', label: '摩擦力和剪切力', options: [
{ label: '存在问题', value: 1 }, { label: '有潜在问题', value: 2 },
{ label: '无明显问题', value: 3 }
]}
]
const morseItems = [
{ key: 'fallHistory', label: '跌倒史', options: [
{ label: '无', value: 0 }, { label: '有', value: 25 }
]},
{ key: 'diagnosis', label: '超过一个医学诊断', options: [
{ label: '无', value: 0 }, { label: '有', value: 15 }
]},
{ key: 'ambulation', label: '行走辅助', options: [
{ label: '卧床/轮椅', value: 0 }, { label: '拐杖/助行器', value: 15 },
{ label: '扶家具', value: 30 }
]},
{ key: 'ivTherapy', label: '静脉输液', options: [
{ label: '无', value: 0 }, { label: '有', value: 20 }
]},
{ key: 'gait', label: '步态', options: [
{ label: '正常/卧床/轮椅', value: 0 }, { label: '虚弱', value: 10 },
{ label: '受损', value: 20 }
]},
{ key: 'mental', label: '精神状态', options: [
{ label: '正确评估自身能力', value: 0 }, { label: '高估或忘记限制', value: 15 }
]}
]
const nrs2002Items = [
{ key: 'bmi', label: 'BMI(kg/m²)', options: [
{ label: '<18.5', value: 3 }, { label: '18.5-20.5', value: 1 },
{ label: '>20.5', value: 0 }
]},
{ key: 'weightLoss', label: '体重下降', options: [
{ label: '>10%/月', value: 3 }, { label: '5-10%/月', value: 2 },
{ label: '2-5%/月', value: 1 }, { label: '无/1月', value: 0 }
]},
{ key: 'intake', label: '饮食摄入', options: [
{ label: '无', value: 3 }, { label: '差', value: 2 },
{ label: '中等', value: 1 }, { label: '好', value: 0 }
]},
{ key: 'illness', label: '疾病严重程度', options: [
{ label: '大手术/创伤', value: 3 }, { label: '骨盆骨折', value: 2 },
{ label: '慢性病急性发作', value: 1 }, { label: '无/轻度', value: 0 }
]}
]
const toolConfigs = {
BRADEN: { title: 'Braden压疮风险评估', items: bradenItems },
MORSE: { title: 'Morse跌倒风险评估', items: morseItems },
NRS2002: { title: 'NRS2002营养风险筛查', items: nrs2002Items }
}
const itemScores = reactive({})
const toolConfig = computed(() => toolConfigs[selectedTool.value])
const totalScore = computed(() => {
let sum = 0
for (const item of toolConfig.value.items) {
sum += itemScores[item.key] || 0
}
return sum
})
const currentRiskLevel = computed(() => {
const tool = selectedTool.value
const score = totalScore.value
if (tool === 'BRADEN') {
if (score <= 12) return 'HIGH'
if (score <= 14) return 'MEDIUM'
return 'LOW'
} else if (tool === 'MORSE') {
if (score >= 45) return 'HIGH'
if (score >= 25) return 'MEDIUM'
return 'LOW'
} else if (tool === 'NRS2002') {
if (score >= 3) return 'HIGH'
return 'LOW'
}
return 'NORMAL'
})
const onToolChange = () => {
for (const key of Object.keys(itemScores)) {
delete itemScores[key]
}
}
const getRiskType = (level) => {
const map = { HIGH: 'danger', MEDIUM: 'warning', LOW: 'success', NORMAL: 'info' }
return map[level] || 'info'
}
const getRiskText = (level) => {
const map = { HIGH: '高风险', MEDIUM: '中风险', LOW: '低风险', NORMAL: '正常' }
return map[level] || '未知'
}
const getToolName = (tool) => {
const map = { BRADEN: 'Braden压疮', MORSE: 'Morse跌倒', NRS2002: 'NRS2002营养' }
return map[tool] || tool
}
const formatTime = (time) => {
if (!time) return '-'
const d = new Date(time)
return d.toLocaleString('zh-CN')
}
const goBack = () => {
router.push('/nursingmobile/patient-list')
}
const fetchHistory = async () => {
if (!patientId.value) return
historyLoading.value = true
try {
const res = await getAssessmentList(patientId.value)
historyList.value = (res.data || []).slice(0, 20)
} finally {
historyLoading.value = false
}
}
const handleSubmit = async () => {
const filled = toolConfig.value.items.filter(i => itemScores[i.key] != null)
if (filled.length < toolConfig.value.items.length) {
ElMessage.warning('请完成所有评估项目')
return
}
submitting.value = true
try {
await submitAssessment({
patientId: patientId.value ? Number(patientId.value) : null,
encounterId: encounterId.value ? Number(encounterId.value) : null,
patientName: patientName.value,
assessmentType: 'MOBILE',
assessmentTool: selectedTool.value,
totalScore: totalScore.value,
itemScores: { ...itemScores },
detail: detail.value,
assessmentTime: new Date()
})
ElMessage.success('评估提交成功')
detail.value = ''
fetchHistory()
} catch (e) {
ElMessage.error('提交失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
fetchHistory()
})
</script>
<style scoped>
.mobile-assessment {
padding: 16px;
}
.page-header {
margin-bottom: 16px;
}
.page-title {
font-size: 16px;
font-weight: 600;
}
.tool-selector {
margin-bottom: 16px;
}
.assessment-form {
margin-bottom: 16px;
}
.items-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.assessment-item {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 12px;
}
.item-label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.item-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.score-summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #f5f7fa;
border-radius: 6px;
margin-top: 16px;
}
.total-score {
font-size: 15px;
font-weight: 500;
}
.score-value {
font-size: 20px;
font-weight: 700;
color: #409eff;
margin-left: 4px;
}
.risk-level {
font-size: 14px;
}
.history-section {
margin-bottom: 16px;
}
.history-item {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.history-item:last-child {
border-bottom: none;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.history-tool {
font-size: 14px;
font-weight: 500;
}
.history-body {
display: flex;
justify-content: space-between;
align-items: center;
}
.history-score {
font-size: 13px;
color: #666;
}
.history-time {
font-size: 12px;
color: #999;
}
</style>

View File

@@ -19,3 +19,23 @@ export function saveVitalSign(data) {
export function getVitalSignTrend(patientId, params) {
return request({ url: '/nursing/mobile/vital-sign-trend/' + patientId, method: 'get', params })
}
export function submitAssessment(data) {
return request({ url: '/nursing/mobile/assessment/submit', method: 'post', data })
}
export function getAssessmentList(patientId) {
return request({ url: '/nursing/mobile/assessment/list/' + patientId, method: 'get' })
}
export function startInfusion(data) {
return request({ url: '/nursing/mobile/infusion/start', method: 'post', data })
}
export function addInfusionPatrol(data) {
return request({ url: '/nursing/mobile/infusion/patrol', method: 'post', data })
}
export function getInfusionStatus(patientId) {
return request({ url: '/nursing/mobile/infusion/status/' + patientId, method: 'get' })
}

View File

@@ -0,0 +1,159 @@
<template>
<div class="telehealth-doctor">
<div class="page-header">
<span class="tab-title">问诊管理 - 医生端</span>
</div>
<div class="search-section">
<el-form :model="queryParams" inline>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 120px">
<el-option label="待诊" value="PENDING" />
<el-option label="问诊中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column label="问诊ID" prop="id" width="120" />
<el-table-column label="患者ID" prop="patientId" width="120" />
<el-table-column label="问诊类型" prop="consultationType" width="120" />
<el-table-column label="主诉" prop="chiefComplaint" show-overflow-tooltip />
<el-table-column label="诊断" prop="diagnosis" show-overflow-tooltip />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="170" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button v-if="row.status === 'PENDING' || row.status === 'IN_PROGRESS'" type="primary" link @click="handleReply(row)">回复</el-button>
<el-button v-if="row.status === 'IN_PROGRESS'" type="success" link @click="handlePrescribe(row)">开方</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="handleQuery"
@current-change="handleQuery"
style="margin-top: 16px; justify-content: flex-end"
/>
<el-dialog v-model="replyVisible" title="医生回复" width="500px">
<el-form label-width="80px">
<el-form-item label="诊断">
<el-input v-model="replyForm.diagnosis" type="textarea" :rows="4" placeholder="请输入诊断" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="replyVisible = false">取消</el-button>
<el-button type="primary" @click="submitReply">提交</el-button>
</template>
</el-dialog>
<el-dialog v-model="prescribeVisible" title="复诊开方" width="600px">
<el-form label-width="80px">
<el-form-item label="诊断">
<el-input v-model="prescribeForm.diagnosis" type="textarea" :rows="3" placeholder="请输入诊断" />
</el-form-item>
<el-form-item label="处方">
<el-input v-model="prescribeForm.prescription" type="textarea" :rows="6" placeholder="请输入处方内容" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="prescribeVisible = false">取消</el-button>
<el-button type="primary" @click="submitPrescribe">确认开方</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { pageConsultation, replyConsultation, prescribeConsultation } from '@/api/telehealth'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const replyVisible = ref(false)
const prescribeVisible = ref(false)
const queryParams = ref({
status: '',
pageNum: 1,
pageSize: 10
})
const replyForm = ref({ id: null, diagnosis: '' })
const prescribeForm = ref({ id: null, diagnosis: '', prescription: '' })
const statusText = (s) => ({ PENDING: '待诊', IN_PROGRESS: '问诊中', COMPLETED: '已完成' }[s] || s)
const statusType = (s) => ({ PENDING: 'warning', IN_PROGRESS: 'primary', COMPLETED: 'success' }[s] || 'info')
const handleQuery = async () => {
loading.value = true
try {
const res = await pageConsultation(queryParams.value)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
const handleReply = (row) => {
replyForm.value = { id: row.id, diagnosis: row.diagnosis || '' }
replyVisible.value = true
}
const handlePrescribe = (row) => {
prescribeForm.value = { id: row.id, diagnosis: row.diagnosis || '', prescription: '' }
prescribeVisible.value = true
}
const submitReply = async () => {
await replyConsultation(replyForm.value)
ElMessage.success('回复成功')
replyVisible.value = false
handleQuery()
}
const submitPrescribe = async () => {
await prescribeConsultation(prescribeForm.value)
ElMessage.success('开方成功')
prescribeVisible.value = false
handleQuery()
}
onMounted(() => {
handleQuery()
})
</script>
<style scoped>
.telehealth-doctor {
padding: 16px;
}
.page-header {
margin-bottom: 16px;
}
.tab-title {
font-size: 18px;
font-weight: bold;
}
.search-section {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div class="telehealth-patient">
<div class="page-header">
<span class="tab-title">在线问诊</span>
</div>
<div class="search-section">
<el-form :model="queryParams" inline>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 120px">
<el-option label="待诊" value="PENDING" />
<el-option label="问诊中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button type="success" @click="handleCreate">新建问诊</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column label="问诊ID" prop="id" width="120" />
<el-table-column label="问诊类型" prop="consultationType" width="120" />
<el-table-column label="主诉" prop="chiefComplaint" show-overflow-tooltip />
<el-table-column label="诊断" prop="diagnosis" show-overflow-tooltip />
<el-table-column label="处方" prop="prescription" show-overflow-tooltip />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="170" />
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="handleQuery"
@current-change="handleQuery"
style="margin-top: 16px; justify-content: flex-end"
/>
<el-dialog v-model="dialogVisible" title="新建问诊" width="500px" @close="resetForm">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="问诊类型" prop="consultationType">
<el-select v-model="form.consultationType" placeholder="请选择">
<el-option label="图文问诊" value="TEXT" />
<el-option label="视频问诊" value="VIDEO" />
<el-option label="电话问诊" value="PHONE" />
</el-select>
</el-form-item>
<el-form-item label="医生ID" prop="doctorId">
<el-input v-model.number="form.doctorId" placeholder="请输入医生ID" />
</el-form-item>
<el-form-item label="主诉" prop="chiefComplaint">
<el-input v-model="form.chiefComplaint" type="textarea" :rows="4" placeholder="请描述您的症状" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCreate">提交</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { createConsultation, pageConsultation } from '@/api/telehealth'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const formRef = ref(null)
const queryParams = ref({
status: '',
pageNum: 1,
pageSize: 10
})
const form = ref({
patientId: null,
doctorId: null,
consultationType: '',
chiefComplaint: ''
})
const rules = {
consultationType: [{ required: true, message: '请选择问诊类型', trigger: 'change' }],
doctorId: [{ required: true, message: '请输入医生ID', trigger: 'blur' }],
chiefComplaint: [{ required: true, message: '请输入主诉', trigger: 'blur' }]
}
const statusText = (s) => ({ PENDING: '待诊', IN_PROGRESS: '问诊中', COMPLETED: '已完成' }[s] || s)
const statusType = (s) => ({ PENDING: 'warning', IN_PROGRESS: 'primary', COMPLETED: 'success' }[s] || 'info')
const handleQuery = async () => {
loading.value = true
try {
const res = await pageConsultation(queryParams.value)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
const handleCreate = () => {
dialogVisible.value = true
}
const resetForm = () => {
form.value = { patientId: null, doctorId: null, consultationType: '', chiefComplaint: '' }
}
const submitCreate = async () => {
await formRef.value.validate()
await createConsultation(form.value)
ElMessage.success('问诊创建成功')
dialogVisible.value = false
handleQuery()
}
onMounted(() => {
handleQuery()
})
</script>
<style scoped>
.telehealth-patient {
padding: 16px;
}
.page-header {
margin-bottom: 16px;
}
.tab-title {
font-size: 18px;
font-weight: bold;
}
.search-section {
margin-bottom: 16px;
}
</style>