feat(mrhomepage): 病案统计细化 — 科室统计+医生统计
- 新增 IMrStatsDetailAppService + MrStatsDetailAppServiceImpl - 新增 MrStatsDetailController (GET /by-dept, GET /by-doctor) - 新增 V75 迁移脚本: mr_homepage 加 department_id, doctor_id - 前端 MrStatsDetail.vue 统计面板+DRG/诊断分布
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package com.healthlink.his.web.mrhomepage.appservice;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IMrStatsDetailAppService {
|
||||
|
||||
Map<String, Object> getMrStatsByDept(Long deptId);
|
||||
|
||||
Map<String, Object> getMrStatsByDoctor(Long doctorId);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.healthlink.his.web.mrhomepage.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.healthlink.his.mrhomepage.domain.MrHomepage;
|
||||
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
|
||||
import com.healthlink.his.web.mrhomepage.appservice.IMrStatsDetailAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class MrStatsDetailAppServiceImpl implements IMrStatsDetailAppService {
|
||||
|
||||
private final IMrHomepageService mrHomepageService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getMrStatsByDept(Long deptId) {
|
||||
LambdaQueryWrapper<MrHomepage> wrapper = new LambdaQueryWrapper<>();
|
||||
if (deptId != null) {
|
||||
wrapper.eq(MrHomepage::getEncounterId, deptId);
|
||||
}
|
||||
List<MrHomepage> list = mrHomepageService.list(wrapper);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("totalCount", list.size());
|
||||
|
||||
BigDecimal totalCost = list.stream()
|
||||
.map(MrHomepage::getTotalCost)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
result.put("totalCost", totalCost);
|
||||
|
||||
result.put("avgCost", list.isEmpty() ? BigDecimal.ZERO :
|
||||
totalCost.divide(BigDecimal.valueOf(list.size()), 2, RoundingMode.HALF_UP));
|
||||
|
||||
Map<String, Long> byStatus = list.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
h -> h.getQualityStatus() != null ? h.getQualityStatus() : "UNKNOWN",
|
||||
Collectors.counting()));
|
||||
result.put("byStatus", byStatus);
|
||||
|
||||
Map<String, Long> byDrg = list.stream()
|
||||
.filter(h -> h.getDrgGroup() != null)
|
||||
.collect(Collectors.groupingBy(MrHomepage::getDrgGroup, Collectors.counting()));
|
||||
result.put("byDrg", byDrg);
|
||||
|
||||
long totalLos = list.stream()
|
||||
.mapToInt(h -> h.getLosDays() != null ? h.getLosDays() : 0)
|
||||
.sum();
|
||||
result.put("totalLosDays", totalLos);
|
||||
result.put("avgLosDays", list.isEmpty() ? 0 :
|
||||
Math.round(totalLos * 10.0 / list.size()) / 10.0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getMrStatsByDoctor(Long doctorId) {
|
||||
LambdaQueryWrapper<MrHomepage> wrapper = new LambdaQueryWrapper<>();
|
||||
if (doctorId != null) {
|
||||
wrapper.eq(MrHomepage::getPatientId, doctorId);
|
||||
}
|
||||
List<MrHomepage> list = mrHomepageService.list(wrapper);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("totalCount", list.size());
|
||||
|
||||
BigDecimal totalCost = list.stream()
|
||||
.map(MrHomepage::getTotalCost)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
result.put("totalCost", totalCost);
|
||||
|
||||
result.put("avgCost", list.isEmpty() ? BigDecimal.ZERO :
|
||||
totalCost.divide(BigDecimal.valueOf(list.size()), 2, RoundingMode.HALF_UP));
|
||||
|
||||
Map<String, Long> byStatus = list.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
h -> h.getQualityStatus() != null ? h.getQualityStatus() : "UNKNOWN",
|
||||
Collectors.counting()));
|
||||
result.put("byStatus", byStatus);
|
||||
|
||||
Map<String, Long> byDiagnosis = list.stream()
|
||||
.filter(h -> h.getPrimaryDiagnosisName() != null)
|
||||
.collect(Collectors.groupingBy(MrHomepage::getPrimaryDiagnosisName, Collectors.counting()));
|
||||
result.put("byDiagnosis", byDiagnosis);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.healthlink.his.web.mrhomepage.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.mrhomepage.appservice.IMrStatsDetailAppService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "病案统计明细")
|
||||
@RestController
|
||||
@RequestMapping("/mr-stats-detail")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class MrStatsDetailController {
|
||||
|
||||
private final IMrStatsDetailAppService mrStatsDetailAppService;
|
||||
|
||||
@Operation(summary = "科室病案统计")
|
||||
@PreAuthorize("@ss.hasPermi('mrhomepage:mrhomepage:list')")
|
||||
@GetMapping("/by-dept")
|
||||
public R<Map<String, Object>> getMrStatsByDept(
|
||||
@RequestParam(value = "deptId", required = false) Long deptId) {
|
||||
return R.ok(mrStatsDetailAppService.getMrStatsByDept(deptId));
|
||||
}
|
||||
|
||||
@Operation(summary = "医生病案统计")
|
||||
@PreAuthorize("@ss.hasPermi('mrhomepage:mrhomepage:list')")
|
||||
@GetMapping("/by-doctor")
|
||||
public R<Map<String, Object>> getMrStatsByDoctor(
|
||||
@RequestParam(value = "doctorId", required = false) Long doctorId) {
|
||||
return R.ok(mrStatsDetailAppService.getMrStatsByDoctor(doctorId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE mr_homepage ADD COLUMN IF NOT EXISTS department_id BIGINT;
|
||||
ALTER TABLE mr_homepage ADD COLUMN IF NOT EXISTS doctor_id BIGINT;
|
||||
COMMENT ON COLUMN mr_homepage.department_id IS '科室ID';
|
||||
COMMENT ON COLUMN mr_homepage.doctor_id IS '主治医生ID';
|
||||
CREATE INDEX IF NOT EXISTS idx_mr_homepage_dept ON mr_homepage(department_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mr_homepage_doctor ON mr_homepage(doctor_id);
|
||||
9
healthlink-his-ui/src/api/mrhomepage/statsDetail.js
Normal file
9
healthlink-his-ui/src/api/mrhomepage/statsDetail.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getMrStatsByDept(params) {
|
||||
return request({ url: '/mr-stats-detail/by-dept', method: 'get', params })
|
||||
}
|
||||
|
||||
export function getMrStatsByDoctor(params) {
|
||||
return request({ url: '/mr-stats-detail/by-doctor', method: 'get', params })
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="mr-stats-detail-container">
|
||||
<div class="page-header">
|
||||
<span class="tab-title">病案统计明细</span>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span>查询条件</span>
|
||||
<el-radio-group v-model="queryMode" @change="resetQuery">
|
||||
<el-radio-button value="dept">按科室</el-radio-button>
|
||||
<el-radio-button value="doctor">按医生</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="queryParams" inline>
|
||||
<el-form-item v-if="queryMode === 'dept'" label="科室ID">
|
||||
<el-input v-model="queryParams.deptId" placeholder="科室ID" clearable style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="医生ID">
|
||||
<el-input v-model="queryParams.doctorId" placeholder="医生ID" clearable style="width: 160px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadData" :loading="loading">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="16" style="margin-bottom: 16px">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #409eff">{{ stats.totalCount || 0 }}</div>
|
||||
<div class="stat-label">病案总数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #e6a23c">{{ formatCost(stats.totalCost) }}</div>
|
||||
<div class="stat-label">总费用</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #67c23a">{{ formatCost(stats.avgCost) }}</div>
|
||||
<div class="stat-label">平均费用</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #909399">{{ stats.avgLosDays || 0 }}天</div>
|
||||
<div class="stat-label">平均住院日</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>按质控状态分布</span></template>
|
||||
<el-table :data="statusList" border stripe size="small">
|
||||
<el-table-column prop="status" label="状态" />
|
||||
<el-table-column prop="count" label="数量" width="100" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>{{ queryMode === 'dept' ? '按DRG分组' : '按主要诊断' }}</span></template>
|
||||
<el-table :data="groupList" border stripe size="small">
|
||||
<el-table-column prop="name" :label="queryMode === 'dept' ? 'DRG分组' : '诊断'" />
|
||||
<el-table-column prop="count" label="数量" width="100" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getMrStatsByDept, getMrStatsByDoctor } from '@/api/mrhomepage/statsDetail'
|
||||
|
||||
const loading = ref(false)
|
||||
const queryMode = ref('dept')
|
||||
const queryParams = reactive({ deptId: '', doctorId: '' })
|
||||
const stats = ref({})
|
||||
|
||||
const formatCost = (val) => {
|
||||
if (!val || val === '0') return '¥0'
|
||||
return '¥' + Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
const STATUS_MAP = {
|
||||
DRAFT: '草稿', SUBMITTED: '已提交', APPROVED: '已审核',
|
||||
REJECTED: '已驳回', CODING: '编码中', COMPLETE: '已完成'
|
||||
}
|
||||
|
||||
const statusList = computed(() => {
|
||||
const byStatus = stats.value.byStatus || {}
|
||||
return Object.entries(byStatus).map(([status, count]) => ({
|
||||
status: STATUS_MAP[status] || status, count
|
||||
}))
|
||||
})
|
||||
|
||||
const groupList = computed(() => {
|
||||
const source = queryMode.value === 'dept' ? (stats.value.byDrg || {}) : (stats.value.byDiagnosis || {})
|
||||
return Object.entries(source).map(([name, count]) => ({ name, count }))
|
||||
})
|
||||
|
||||
const resetQuery = () => {
|
||||
queryParams.deptId = ''
|
||||
queryParams.doctorId = ''
|
||||
stats.value = {}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {}
|
||||
if (queryMode.value === 'dept') {
|
||||
if (queryParams.deptId) params.deptId = queryParams.deptId
|
||||
const res = await getMrStatsByDept(params)
|
||||
stats.value = res.data || {}
|
||||
} else {
|
||||
if (queryParams.doctorId) params.doctorId = queryParams.doctorId
|
||||
const res = await getMrStatsByDoctor(params)
|
||||
stats.value = res.data || {}
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('加载失败: ' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mr-stats-detail-container { padding: 16px; }
|
||||
.page-header { margin-bottom: 16px; }
|
||||
.tab-title { font-size: 18px; font-weight: bold; }
|
||||
.stat-card { text-align: center; padding: 12px 0; }
|
||||
.stat-value { font-size: 28px; font-weight: bold; }
|
||||
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user