feat(reportmanage): 报表维度扩展 — 多维度报表查询

- 新增 IReportDimensionAppService + ReportDimensionAppServiceImpl
- 新增 ReportDimensionController (GET /query)
- 支持按状态/DRG/诊断维度统计
- 前端 ReportDimension.vue 维度切换+明细表格
This commit is contained in:
2026-06-18 17:37:34 +08:00
parent 0994550f2f
commit 5dda5fe217
5 changed files with 271 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.web.reportmanage.appservice;
import java.util.List;
import java.util.Map;
public interface IReportDimensionAppService {
Map<String, Object> getReportByDimension(String dimension, Map<String, String> filters);
}

View File

@@ -0,0 +1,96 @@
package com.healthlink.his.web.reportmanage.appservice.impl;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
import com.healthlink.his.web.reportmanage.appservice.IReportDimensionAppService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class ReportDimensionAppServiceImpl implements IReportDimensionAppService {
private final IMrHomepageService mrHomepageService;
@Override
public Map<String, Object> getReportByDimension(String dimension, Map<String, String> filters) {
List<MrHomepage> allData = mrHomepageService.list();
String startDate = filters != null ? filters.get("startDate") : null;
String endDate = filters != null ? filters.get("endDate") : null;
if (StringUtils.hasText(startDate)) {
allData = allData.stream()
.filter(h -> h.getDischargeDate() != null &&
h.getDischargeDate().toString().compareTo(startDate) >= 0)
.collect(Collectors.toList());
}
if (StringUtils.hasText(endDate)) {
allData = allData.stream()
.filter(h -> h.getDischargeDate() != null &&
h.getDischargeDate().toString().compareTo(endDate) <= 0)
.collect(Collectors.toList());
}
Map<String, Object> result = new HashMap<>();
result.put("totalCount", allData.size());
BigDecimal totalCost = allData.stream()
.map(MrHomepage::getTotalCost)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
result.put("totalCost", totalCost);
result.put("avgCost", allData.isEmpty() ? BigDecimal.ZERO :
totalCost.divide(BigDecimal.valueOf(allData.size()), 2, RoundingMode.HALF_UP));
Map<String, List<MrHomepage>> grouped;
switch (dimension != null ? dimension : "status") {
case "drg":
grouped = allData.stream()
.filter(h -> h.getDrgGroup() != null)
.collect(Collectors.groupingBy(MrHomepage::getDrgGroup));
break;
case "diagnosis":
grouped = allData.stream()
.filter(h -> h.getPrimaryDiagnosisName() != null)
.collect(Collectors.groupingBy(MrHomepage::getPrimaryDiagnosisName));
break;
case "status":
default:
grouped = allData.stream()
.collect(Collectors.groupingBy(
h -> h.getQualityStatus() != null ? h.getQualityStatus() : "UNKNOWN"));
break;
}
List<Map<String, Object>> dimensionData = new ArrayList<>();
grouped.forEach((key, items) -> {
Map<String, Object> entry = new HashMap<>();
entry.put("dimension", key);
entry.put("count", items.size());
BigDecimal cost = items.stream()
.map(MrHomepage::getTotalCost)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
entry.put("totalCost", cost);
entry.put("avgCost", items.isEmpty() ? BigDecimal.ZERO :
cost.divide(BigDecimal.valueOf(items.size()), 2, RoundingMode.HALF_UP));
long totalLos = items.stream()
.mapToInt(h -> h.getLosDays() != null ? h.getLosDays() : 0)
.sum();
entry.put("avgLosDays", items.isEmpty() ? 0 :
Math.round(totalLos * 10.0 / items.size()) / 10.0);
dimensionData.add(entry);
});
result.put("dimension", dimension);
result.put("data", dimensionData);
return result;
}
}

View File

@@ -0,0 +1,36 @@
package com.healthlink.his.web.reportmanage.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.web.reportmanage.appservice.IReportDimensionAppService;
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.HashMap;
import java.util.Map;
@Tag(name = "多维度报表")
@RestController
@RequestMapping("/report-manage/report-dimension")
@Slf4j
@AllArgsConstructor
public class ReportDimensionController {
private final IReportDimensionAppService reportDimensionAppService;
@Operation(summary = "多维度报表查询")
@PreAuthorize("@ss.hasPermi('reportmanage:report:list')")
@GetMapping("/query")
public R<Map<String, Object>> getReportByDimension(
@RequestParam(value = "dimension", defaultValue = "status") String dimension,
@RequestParam(value = "startDate", required = false) String startDate,
@RequestParam(value = "endDate", required = false) String endDate) {
Map<String, String> filters = new HashMap<>();
if (startDate != null) filters.put("startDate", startDate);
if (endDate != null) filters.put("endDate", endDate);
return R.ok(reportDimensionAppService.getReportByDimension(dimension, filters));
}
}

View File

@@ -0,0 +1,5 @@
import request from '@/utils/request'
export function getReportByDimension(params) {
return request({ url: '/report-manage/report-dimension/query', method: 'get', params })
}

View File

@@ -0,0 +1,125 @@
<template>
<div class="report-dimension-container">
<div class="page-header">
<span class="tab-title">多维度报表</span>
</div>
<el-card shadow="never" style="margin-bottom: 16px">
<template #header>
<span>查询条件</span>
</template>
<el-form :model="queryParams" inline>
<el-form-item label="统计维度">
<el-select v-model="queryParams.dimension" style="width: 140px">
<el-option label="按质控状态" value="status" />
<el-option label="按DRG分组" value="drg" />
<el-option label="按主要诊断" value="diagnosis" />
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="queryParams.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker v-model="queryParams.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
</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="8">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #409eff">{{ reportData.totalCount || 0 }}</div>
<div class="stat-label">总病案数</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #e6a23c">{{ formatCost(reportData.totalCost) }}</div>
<div class="stat-label">总费用</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #67c23a">{{ formatCost(reportData.avgCost) }}</div>
<div class="stat-label">平均费用</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="never">
<template #header>
<span>{{ dimensionLabel }}明细</span>
</template>
<el-table :data="reportData.data || []" v-loading="loading" border stripe style="width: 100%">
<el-table-column prop="dimension" :label="dimensionLabel" min-width="160" />
<el-table-column prop="count" label="病案数" width="100" />
<el-table-column prop="totalCost" label="总费用" width="140">
<template #default="{ row }">{{ formatCost(row.totalCost) }}</template>
</el-table-column>
<el-table-column prop="avgCost" label="平均费用" width="140">
<template #default="{ row }">{{ formatCost(row.avgCost) }}</template>
</el-table-column>
<el-table-column prop="avgLosDays" label="平均住院日" width="120">
<template #default="{ row }">{{ row.avgLosDays }}</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getReportByDimension } from '@/api/reportmanage/dimension'
const loading = ref(false)
const queryParams = reactive({
dimension: 'status',
startDate: '',
endDate: ''
})
const reportData = ref({})
const DIMENSION_LABEL = { status: '质控状态', drg: 'DRG分组', diagnosis: '主要诊断' }
const dimensionLabel = computed(() => DIMENSION_LABEL[queryParams.dimension] || '维度')
const formatCost = (val) => {
if (!val || val === '0') return '¥0'
return '¥' + Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}
const loadData = async () => {
loading.value = true
try {
const params = { dimension: queryParams.dimension }
if (queryParams.startDate) params.startDate = queryParams.startDate
if (queryParams.endDate) params.endDate = queryParams.endDate
const res = await getReportByDimension(params)
reportData.value = res.data || {}
} catch (e) {
ElMessage.error('加载失败: ' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
onMounted(() => loadData())
</script>
<style scoped>
.report-dimension-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>