feat(lab): T7.2 室间质评+报告打印 - AppService/Controller/前端报告单

This commit is contained in:
2026-06-18 12:36:34 +08:00
parent ec8238ab26
commit 3fcc4c1ee7
5 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.web.lab.appservice;
import com.core.common.core.domain.R;
import com.healthlink.his.lab.domain.LabExternalEqa;
public interface ILabEqaAppService {
R<?> recordEqa(LabExternalEqa eqa);
R<?> getEqaResults(String assessmentName, Integer pageNo, Integer pageSize);
R<?> getEqaStats();
}

View File

@@ -0,0 +1,67 @@
package com.healthlink.his.web.lab.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.lab.domain.LabExternalEqa;
import com.healthlink.his.lab.service.ILabExternalEqaService;
import com.healthlink.his.web.lab.appservice.ILabEqaAppService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class LabEqaAppServiceImpl implements ILabEqaAppService {
@Resource
private ILabExternalEqaService externalEqaService;
@Override
public R<?> recordEqa(LabExternalEqa eqa) {
if (eqa.getTargetValue() != null && eqa.getActualValue() != null) {
try {
BigDecimal target = new BigDecimal(eqa.getTargetValue());
BigDecimal actual = new BigDecimal(eqa.getActualValue());
if (target.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal deviation = actual.subtract(target).abs()
.divide(target, 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
eqa.setDeviationRate(deviation);
eqa.setResult(deviation.compareTo(new BigDecimal("10")) <= 0 ? "合格" : "不合格");
}
} catch (NumberFormatException e) {
log.warn("EQA数值解析失败: target={}, actual={}", eqa.getTargetValue(), eqa.getActualValue());
}
}
externalEqaService.save(eqa);
return R.ok(eqa);
}
@Override
public R<?> getEqaResults(String assessmentName, Integer pageNo, Integer pageSize) {
LambdaQueryWrapper<LabExternalEqa> w = new LambdaQueryWrapper<>();
w.like(StringUtils.hasText(assessmentName), LabExternalEqa::getAssessmentName, assessmentName)
.orderByDesc(LabExternalEqa::getCreateTime);
return R.ok(externalEqaService.page(new Page<>(pageNo, pageSize), w));
}
@Override
public R<?> getEqaStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("total", externalEqaService.count());
LambdaQueryWrapper<LabExternalEqa> wq = new LambdaQueryWrapper<>();
wq.eq(LabExternalEqa::getResult, "合格");
stats.put("qualified", externalEqaService.count(wq));
LambdaQueryWrapper<LabExternalEqa> wf = new LambdaQueryWrapper<>();
wf.eq(LabExternalEqa::getResult, "不合格");
stats.put("unqualified", externalEqaService.count(wf));
return R.ok(stats);
}
}

View File

@@ -0,0 +1,40 @@
package com.healthlink.his.web.lab.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.lab.domain.LabExternalEqa;
import com.healthlink.his.web.lab.appservice.ILabEqaAppService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
@RestController
@RequestMapping("/lab/eqa")
@Slf4j
public class LabEqaController {
@Resource
private ILabEqaAppService labEqaAppService;
@PostMapping("/record")
@PreAuthorize("hasPermi('infection:lab:edit')")
public R<?> recordEqa(@RequestBody LabExternalEqa eqa) {
return labEqaAppService.recordEqa(eqa);
}
@GetMapping("/results")
@PreAuthorize("hasPermi('infection:lab:list')")
public R<?> getEqaResults(
@RequestParam(value = "assessmentName", required = false) String assessmentName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
return labEqaAppService.getEqaResults(assessmentName, pageNo, pageSize);
}
@GetMapping("/stats")
@PreAuthorize("hasPermi('infection:lab:list')")
public R<?> getEqaStats() {
return labEqaAppService.getEqaStats();
}
}

View File

@@ -0,0 +1,24 @@
import request from '@/utils/request';
export function recordEqa(data) {
return request({
url: '/lab/eqa/record',
method: 'post',
data: data,
});
}
export function getEqaResults(query) {
return request({
url: '/lab/eqa/results',
method: 'get',
params: query,
});
}
export function getEqaStats() {
return request({
url: '/lab/eqa/stats',
method: 'get',
});
}

View File

@@ -0,0 +1,206 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px;display:flex;align-items:center;gap:16px">
<span style="font-size:18px;font-weight:bold">室间质评管理</span>
<el-tag v-if="stats.total" type="info"> {{ stats.total }} </el-tag>
<el-tag v-if="stats.qualified" type="success">合格 {{ stats.qualified }}</el-tag>
<el-tag v-if="stats.unqualified" type="danger">不合格 {{ stats.unqualified }}</el-tag>
</div>
<el-row :gutter="16">
<el-col :span="8">
<el-card shadow="never">
<template #header>
<span>录入室间质评</span>
</template>
<el-form :model="form" label-width="90px" size="default">
<el-form-item label="评批名称">
<el-input v-model="form.assessmentName" placeholder="如2026年Q1生化" />
</el-form-item>
<el-form-item label="组织机构">
<el-input v-model="form.assessmentOrg" />
</el-form-item>
<el-form-item label="年度">
<el-input-number v-model="form.assessmentYear" :min="2020" :max="2030" style="width:100%" />
</el-form-item>
<el-form-item label="季度">
<el-select v-model="form.assessmentQuarter" style="width:100%">
<el-option label="Q1" :value="1" />
<el-option label="Q2" :value="2" />
<el-option label="Q3" :value="3" />
<el-option label="Q4" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="样本编号">
<el-input v-model="form.sampleCode" />
</el-form-item>
<el-form-item label="检测项目">
<el-input v-model="form.testItem" />
</el-form-item>
<el-form-item label="靶值">
<el-input v-model="form.targetValue" />
</el-form-item>
<el-form-item label="实测值">
<el-input v-model="form.actualValue" />
</el-form-item>
<el-form-item label="操作人">
<el-input v-model="form.operatorName" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitRecord" :loading="loading">提交</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="never">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center">
<span>室间质评结果</span>
<div style="display:flex;gap:8px">
<el-input v-model="query.assessmentName" placeholder="评批名称" clearable style="width:160px" />
<el-button type="primary" @click="loadResults">查询</el-button>
<el-button type="success" @click="printReport">打印报告单</el-button>
</div>
</div>
</template>
<el-table :data="tableData" border stripe size="small" ref="tableRef">
<el-table-column prop="assessmentName" label="评批名称" width="150" />
<el-table-column prop="assessmentOrg" label="组织机构" width="120" />
<el-table-column prop="assessmentYear" label="年度" width="70" />
<el-table-column prop="assessmentQuarter" label="季度" width="60" />
<el-table-column prop="sampleCode" label="样本编号" width="100" />
<el-table-column prop="testItem" label="检测项目" width="100" />
<el-table-column prop="targetValue" label="靶值" width="90" />
<el-table-column prop="actualValue" label="实测值" width="90" />
<el-table-column prop="deviationRate" label="偏差率%" width="80" />
<el-table-column prop="result" label="判定" width="80">
<template #default="{row}">
<el-tag :type="row.result === '合格' ? 'success' : 'danger'" size="small">{{ row.result }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="80" />
</el-table>
<el-pagination
v-model:current-page="query.pageNo"
v-model:page-size="query.pageSize"
style="margin-top:12px;justify-content:flex-end"
:total="total"
layout="total,prev,pager,next"
@current-change="loadResults"
/>
</el-card>
</el-col>
</el-row>
<div id="printArea" ref="printRef" style="display:none">
<div style="padding:20px;font-family:SimSun;font-size:12px">
<div style="text-align:center;font-size:16px;font-weight:bold;margin-bottom:16px">室间质评报告单</div>
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span>评批名称{{ form.assessmentName || '-' }}</span>
<span>组织机构{{ form.assessmentOrg || '-' }}</span>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:12px">
<span>年度/季度{{ form.assessmentYear || '-' }} Q{{ form.assessmentQuarter || '-' }}</span>
<span>样本编号{{ form.sampleCode || '-' }}</span>
</div>
<table style="width:100%;border-collapse:collapse;border:1px solid #000">
<thead>
<tr style="background:#f0f0f0">
<th style="border:1px solid #000;padding:4px">检测项目</th>
<th style="border:1px solid #000;padding:4px">靶值</th>
<th style="border:1px solid #000;padding:4px">实测值</th>
<th style="border:1px solid #000;padding:4px">偏差率%</th>
<th style="border:1px solid #000;padding:4px">判定</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td style="border:1px solid #000;padding:4px;text-align:center">{{ row.testItem }}</td>
<td style="border:1px solid #000;padding:4px;text-align:center">{{ row.targetValue }}</td>
<td style="border:1px solid #000;padding:4px;text-align:center">{{ row.actualValue }}</td>
<td style="border:1px solid #000;padding:4px;text-align:center">{{ row.deviationRate }}</td>
<td style="border:1px solid #000;padding:4px;text-align:center">{{ row.result }}</td>
</tr>
</tbody>
</table>
<div style="margin-top:16px;display:flex;justify-content:space-between">
<span>操作人{{ form.operatorName || '-' }}</span>
<span>报告日期{{ new Date().toLocaleDateString() }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { recordEqa, getEqaResults, getEqaStats } from '@/api/lab/labEqa'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const stats = ref({ total: 0, qualified: 0, unqualified: 0 })
const tableRef = ref(null)
const printRef = ref(null)
const query = ref({ assessmentName: '', pageNo: 1, pageSize: 20 })
const defaultForm = () => ({
assessmentName: '', assessmentOrg: '', assessmentYear: 2026, assessmentQuarter: 1,
sampleCode: '', testItem: '', targetValue: '', actualValue: '', operatorName: ''
})
const form = ref(defaultForm())
const loadResults = async () => {
const r = await getEqaResults(query.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
}
const loadStats = async () => {
const r = await getEqaStats()
stats.value = r.data || stats.value
}
const submitRecord = async () => {
if (!form.value.assessmentName || !form.value.testItem) {
ElMessage.warning('请填写评批名称和检测项目')
return
}
loading.value = true
try {
const r = await recordEqa(form.value)
if (r.data?.result === '合格') {
ElMessage.success('提交成功,判定: 合格')
} else if (r.data?.result === '不合格') {
ElMessage.warning('提交成功,判定: 不合格')
} else {
ElMessage.success('提交成功')
}
form.value = defaultForm()
await loadResults()
await loadStats()
} finally {
loading.value = false
}
}
const printReport = () => {
if (tableData.value.length === 0) {
ElMessage.warning('暂无数据可打印')
return
}
const printContent = printRef.value.innerHTML
const win = window.open('', '_blank')
win.document.write(`<html><head><title>室间质评报告单</title></head><body>${printContent}</body></html>`)
win.document.close()
win.print()
}
onMounted(async () => {
await loadResults()
await loadStats()
})
</script>