feat(epidemic): add autoScreen/saveReport/getReportStats endpoints, enhance EpidemicReport entity with screen fields, and create EpidemicReport.vue
This commit is contained in:
@@ -7,4 +7,7 @@ public interface IEpidemicAppService {
|
||||
void confirmReport(Long id, String cdcNo);
|
||||
List<EpidemicReport> getReports(String status);
|
||||
Map<String, Object> getStatistics(String startDate, String endDate);
|
||||
EpidemicReport autoScreen(EpidemicReport r);
|
||||
EpidemicReport saveReport(EpidemicReport r);
|
||||
Map<String, Object> getReportStats(String startDate, String endDate);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,18 @@ import java.util.*;
|
||||
@Service
|
||||
public class EpidemicAppServiceImpl implements IEpidemicAppService {
|
||||
@Autowired private IEpidemicReportService reportService;
|
||||
|
||||
private static final Set<String> NOTIFIABLE_DISEASES = Set.of(
|
||||
"鼠疫", "霍乱", "传染性非典型肺炎", "艾滋病", "病毒性肝炎", "脊髓灰质炎",
|
||||
"人感染高致病性禽流感", "麻疹", "流行性出血热", "狂犬病", "流行性乙型脑炎",
|
||||
"登革热", "炭疽", "细菌性和阿米巴性痢疾", "肺结核", "伤寒和副伤寒",
|
||||
"流行性脑脊髓膜炎", "百日咳", "白喉", "新生儿破伤风", "猩红热",
|
||||
"布鲁氏菌病", "淋病", "梅毒", "钩端螺旋体病", "血吸虫病", "疟疾",
|
||||
"手足口病", "流行性感冒", "流行性腮腺炎", "风疹", "急性出血性结膜炎",
|
||||
"麻风病", "流行性和地方性斑疹伤寒", "黑热病", "包虫病", "丝虫病",
|
||||
"感染性腹泻", "甲型H1N1流感", "新型冠状病毒肺炎"
|
||||
);
|
||||
|
||||
@Override
|
||||
public EpidemicReport report(EpidemicReport r) { r.setStatus("PENDING"); r.setDelFlag("0"); r.setReportDate(new Date()); reportService.save(r); return r; }
|
||||
@Override
|
||||
@@ -27,4 +39,40 @@ public class EpidemicAppServiceImpl implements IEpidemicAppService {
|
||||
r.put("confirmed", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getStatus, "CONFIRMED")));
|
||||
return r;
|
||||
}
|
||||
@Override
|
||||
public EpidemicReport autoScreen(EpidemicReport r) {
|
||||
boolean match = r.getDiseaseName() != null && NOTIFIABLE_DISEASES.stream()
|
||||
.anyMatch(d -> r.getDiseaseName().contains(d));
|
||||
r.setScreenResult(match ? "MATCHED" : "NOT_MATCHED");
|
||||
r.setScreenLevel(match ? "LEVEL_A" : "NORMAL");
|
||||
r.setScreenTime(new Date());
|
||||
if (match && (r.getStatus() == null || "DRAFT".equals(r.getStatus()))) {
|
||||
r.setStatus("PENDING");
|
||||
}
|
||||
r.setDelFlag("0");
|
||||
r.setReportDate(new Date());
|
||||
reportService.save(r);
|
||||
return r;
|
||||
}
|
||||
@Override
|
||||
public EpidemicReport saveReport(EpidemicReport r) {
|
||||
if (r.getId() == null) {
|
||||
r.setStatus("DRAFT"); r.setDelFlag("0"); r.setReportDate(new Date());
|
||||
reportService.save(r);
|
||||
} else {
|
||||
reportService.updateById(r);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@Override
|
||||
public Map<String, Object> getReportStats(String startDate, String endDate) {
|
||||
Map<String, Object> r = new HashMap<>();
|
||||
LambdaQueryWrapper<EpidemicReport> base = new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getDelFlag, "0");
|
||||
r.put("total", reportService.count(base));
|
||||
r.put("pending", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getStatus, "PENDING").eq(EpidemicReport::getDelFlag, "0")));
|
||||
r.put("confirmed", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getStatus, "CONFIRMED").eq(EpidemicReport::getDelFlag, "0")));
|
||||
r.put("screenMatched", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getScreenResult, "MATCHED").eq(EpidemicReport::getDelFlag, "0")));
|
||||
r.put("screenNormal", reportService.count(new LambdaQueryWrapper<EpidemicReport>().eq(EpidemicReport::getScreenResult, "NOT_MATCHED").eq(EpidemicReport::getDelFlag, "0")));
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,32 @@ import com.healthlink.his.web.epidemic.appservice.IEpidemicAppService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@Tag(name = "传染病直报") @RestController @RequestMapping("/api/v1/epidemic")
|
||||
public class EpidemicController {
|
||||
@Autowired private IEpidemicAppService epidemicAppService;
|
||||
|
||||
@Operation(summary = "上报") @PostMapping("/report")
|
||||
@PreAuthorize("hasAuthority('epidemic:edit')")
|
||||
public AjaxResult report(@RequestBody EpidemicReport r) { return AjaxResult.success(epidemicAppService.report(r)); }
|
||||
@Operation(summary = "确认") @PutMapping("/confirm/{id}")
|
||||
@PreAuthorize("hasAuthority('epidemic:edit')")
|
||||
public AjaxResult confirm(@PathVariable Long id, @RequestParam String cdcNo) { epidemicAppService.confirmReport(id, cdcNo); return AjaxResult.success(); }
|
||||
@Operation(summary = "列表") @GetMapping("/list")
|
||||
@PreAuthorize("hasAuthority('epidemic:list')")
|
||||
public AjaxResult list(@RequestParam(required = false) String status) { return AjaxResult.success(epidemicAppService.getReports(status)); }
|
||||
@Operation(summary = "统计") @GetMapping("/statistics")
|
||||
@PreAuthorize("hasAuthority('epidemic:list')")
|
||||
public AjaxResult statistics(@RequestParam(required = false) String s, @RequestParam(required = false) String e) { return AjaxResult.success(epidemicAppService.getStatistics(s, e)); }
|
||||
|
||||
@Operation(summary = "自动筛查") @PostMapping("/auto-screen")
|
||||
@PreAuthorize("hasAuthority('epidemic:edit')")
|
||||
public AjaxResult autoScreen(@RequestBody EpidemicReport r) { return AjaxResult.success(epidemicAppService.autoScreen(r)); }
|
||||
@Operation(summary = "保存报告") @PostMapping("/save")
|
||||
@PreAuthorize("hasAuthority('epidemic:edit')")
|
||||
public AjaxResult saveReport(@RequestBody EpidemicReport r) { return AjaxResult.success(epidemicAppService.saveReport(r)); }
|
||||
@Operation(summary = "报告统计") @GetMapping("/report-stats")
|
||||
@PreAuthorize("hasAuthority('epidemic:list')")
|
||||
public AjaxResult reportStats(@RequestParam(required = false) String s, @RequestParam(required = false) String e) { return AjaxResult.success(epidemicAppService.getReportStats(s, e)); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add auto-screen fields to epidemic_report
|
||||
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_result TEXT;
|
||||
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_time TIMESTAMP;
|
||||
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS screen_level VARCHAR(20);
|
||||
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS address VARCHAR(500);
|
||||
ALTER TABLE healthlink_his.epidemic_report ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(50);
|
||||
@@ -13,4 +13,6 @@ public class EpidemicReport extends HisBaseEntity {
|
||||
private String reporterId; private String reporterName;
|
||||
private Date reportDate; private String status;
|
||||
private String cdcConfirmNo; private String delFlag;
|
||||
private String screenResult; private Date screenTime; private String screenLevel;
|
||||
private String address; private String contactPhone;
|
||||
}
|
||||
|
||||
225
healthlink-his-ui/src/views/epidemic/EpidemicReport.vue
Normal file
225
healthlink-his-ui/src/views/epidemic/EpidemicReport.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="传染病报告" name="list">
|
||||
<el-form :inline="true" :model="queryParams" label-width="100px">
|
||||
<el-form-item label="报告状态">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width:160px">
|
||||
<el-option label="待上报" value="PENDING" />
|
||||
<el-option label="已确认" value="CONFIRMED" />
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<el-option label="已退回" value="RETURNED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="loadReports">查询</el-button>
|
||||
<el-button type="success" icon="Plus" @click="showAddDialog">新增报告</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="reportList" border>
|
||||
<el-table-column label="患者姓名" align="center" prop="patientName" width="120" />
|
||||
<el-table-column label="疾病名称" align="center" prop="diseaseName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="疾病编码" align="center" prop="diseaseCode" width="120" />
|
||||
<el-table-column label="报告类型" align="center" prop="reportType" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ reportTypeText(row.reportType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上报人" align="center" prop="reporterName" width="100" />
|
||||
<el-table-column label="上报日期" align="center" prop="reportDate" width="160" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="筛查结果" align="center" prop="screenResult" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.screenResult" :type="row.screenResult === 'MATCHED' ? 'danger' : 'success'">
|
||||
{{ row.screenResult === 'MATCHED' ? '命中' : '未命中' }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 'PENDING'" link type="success" icon="Check" @click="handleConfirm(row)">确认</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="自动筛查" name="screen">
|
||||
<el-form :model="screenForm" label-width="110px" style="max-width:700px">
|
||||
<el-form-item label="患者姓名">
|
||||
<el-input v-model="screenForm.patientName" placeholder="请输入患者姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="疾病名称">
|
||||
<el-input v-model="screenForm.diseaseName" placeholder="请输入诊断疾病名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="疾病编码">
|
||||
<el-input v-model="screenForm.diseaseCode" placeholder="请输入疾病编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报告类型">
|
||||
<el-select v-model="screenForm.reportType" placeholder="请选择">
|
||||
<el-option label="初次报告" value="INITIAL" />
|
||||
<el-option label="订正报告" value="CORRECTION" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="screenForm.contactPhone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="现住址">
|
||||
<el-input v-model="screenForm.address" placeholder="请输入现住址" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleAutoScreen">开始筛查</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider v-if="screenResult" />
|
||||
<el-alert v-if="screenResult" :title="screenResultTitle" :type="screenResult.screenResult === 'MATCHED' ? 'error' : 'success'" show-icon :closable="false" style="margin-bottom:16px" />
|
||||
<el-descriptions v-if="screenResult" :column="2" border>
|
||||
<el-descriptions-item label="筛查结果">{{ screenResult.screenResult === 'MATCHED' ? '命中法定传染病' : '未命中' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛查等级">{{ screenResult.screenLevel === 'LEVEL_A' ? '甲类' : '正常' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛查时间">{{ screenResult.screenTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报告状态">{{ statusText(screenResult.status) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="报告统计" name="stats">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="报告总数">{{ reportStats.total || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="待上报">{{ reportStats.pending || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已确认">{{ reportStats.confirmed || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛查命中">{{ reportStats.screenMatched || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛查未命中">{{ reportStats.screenNormal || 0 }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="addDialogVisible" title="新增传染病报告" width="650px" append-to-body>
|
||||
<el-form :model="formData" label-width="110px">
|
||||
<el-form-item label="患者姓名">
|
||||
<el-input v-model="formData.patientName" placeholder="请输入患者姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="疾病名称">
|
||||
<el-input v-model="formData.diseaseName" placeholder="请输入疾病名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="疾病编码">
|
||||
<el-input v-model="formData.diseaseCode" placeholder="请输入疾病编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报告类型">
|
||||
<el-select v-model="formData.reportType" placeholder="请选择">
|
||||
<el-option label="初次报告" value="INITIAL" />
|
||||
<el-option label="订正报告" value="CORRECTION" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="现住址">
|
||||
<el-input v-model="formData.address" placeholder="请输入现住址" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="传染病报告详情" width="650px" append-to-body>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="患者姓名">{{ detailData.patientName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="疾病名称">{{ detailData.diseaseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="疾病编码">{{ detailData.diseaseCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报告类型">{{ reportTypeText(detailData.reportType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上报人">{{ detailData.reporterName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上报日期">{{ detailData.reportDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusType(detailData.status)">{{ statusText(detailData.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="CDC确认号">{{ detailData.cdcConfirmNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛查结果">{{ detailData.screenResult === 'MATCHED' ? '命中' : (detailData.screenResult || '—') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="筛查等级">{{ detailData.screenLevel === 'LEVEL_A' ? '甲类' : (detailData.screenLevel || '—') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailData.contactPhone || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="现住址">{{ detailData.address || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="EpidemicReport" lang="js">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const activeTab = ref('list')
|
||||
const loading = ref(false)
|
||||
const reportList = ref([])
|
||||
const addDialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref({})
|
||||
const reportStats = ref({})
|
||||
const screenResult = ref(null)
|
||||
|
||||
const queryParams = reactive({ status: undefined })
|
||||
const formData = reactive({ patientName: '', diseaseName: '', diseaseCode: '', reportType: 'INITIAL', contactPhone: '', address: '' })
|
||||
const screenForm = reactive({ patientName: '', diseaseName: '', diseaseCode: '', reportType: 'INITIAL', contactPhone: '', address: '' })
|
||||
|
||||
const screenResultTitle = computed(() =>
|
||||
screenResult.value?.screenResult === 'MATCHED' ? '已命中法定传染病,请及时上报' : '未命中法定传染病'
|
||||
)
|
||||
|
||||
function reportTypeText(t) { return { INITIAL: '初次报告', CORRECTION: '订正报告' }[t] || t }
|
||||
function statusText(s) { return { PENDING: '待上报', CONFIRMED: '已确认', DRAFT: '草稿', RETURNED: '已退回' }[s] || s }
|
||||
function statusType(s) { return { PENDING: 'warning', CONFIRMED: 'success', DRAFT: 'info', RETURNED: 'danger' }[s] || 'info' }
|
||||
|
||||
function loadReports() {
|
||||
loading.value = true
|
||||
request({ url: '/api/v1/epidemic/list', method: 'get', params: queryParams }).then(res => {
|
||||
reportList.value = res.data || []
|
||||
}).finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
request({ url: '/api/v1/epidemic/report-stats', method: 'get' }).then(res => {
|
||||
reportStats.value = res.data || {}
|
||||
})
|
||||
}
|
||||
|
||||
function showAddDialog() {
|
||||
Object.assign(formData, { patientName: '', diseaseName: '', diseaseCode: '', reportType: 'INITIAL', contactPhone: '', address: '' })
|
||||
addDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleDetail(row) { detailData.value = row; detailVisible.value = true }
|
||||
|
||||
function handleConfirm(row) {
|
||||
request({ url: `/api/v1/epidemic/confirm/${row.id}`, method: 'put', params: { cdcNo: 'CDC-' + Date.now() } }).then(() => {
|
||||
ElMessage.success('确认成功'); loadReports()
|
||||
})
|
||||
}
|
||||
|
||||
function handleAutoScreen() {
|
||||
request({ url: '/api/v1/epidemic/auto-screen', method: 'post', data: screenForm }).then(res => {
|
||||
screenResult.value = res.data
|
||||
ElMessage.success('筛查完成')
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
request({ url: '/api/v1/epidemic/save', method: 'post', data: formData }).then(() => {
|
||||
ElMessage.success('报告保存成功'); addDialogVisible.value = false; loadReports()
|
||||
})
|
||||
}
|
||||
|
||||
function handleTabChange() {
|
||||
if (activeTab.value === 'list') loadReports()
|
||||
else if (activeTab.value === 'stats') loadStats()
|
||||
}
|
||||
|
||||
onMounted(() => { loadReports(); loadStats() })
|
||||
</script>
|
||||
Reference in New Issue
Block a user