Files
his/healthlink-his-ui/src/views/lab/LabQc.vue

195 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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.passed" type="success">合格 {{ stats.passed }}</el-tag>
<el-tag v-if="stats.failed" type="danger">失控 {{ stats.failed }}</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="80px" size="default">
<el-form-item label="质控项目">
<el-input v-model="form.qcItem" placeholder="如ALT, GLU" />
</el-form-item>
<el-form-item label="仪器">
<el-input v-model="form.instrumentName" placeholder="仪器名称" />
</el-form-item>
<el-form-item label="靶值">
<el-input-number v-model="form.targetValue" :precision="4" :step="0.1" style="width:100%" />
</el-form-item>
<el-form-item label="实测值">
<el-input-number v-model="form.actualValue" :precision="4" :step="0.1" style="width:100%" />
</el-form-item>
<el-form-item label="检测日期">
<el-date-picker v-model="form.qcDate" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="操作人">
<el-input v-model="form.operatorName" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remarks" type="textarea" :rows="2" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="doRunWestgard" :loading="loading">执行Westgard判断</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.qcItem" placeholder="质控项目" clearable style="width:140px" />
<el-select v-model="query.isPass" placeholder="结果" clearable style="width:100px">
<el-option label="通过" :value="true" />
<el-option label="失控" :value="false" />
</el-select>
<el-button type="primary" @click="loadResults">查询</el-button>
</div>
</div>
</template>
<div ref="chartRef" style="width:100%;height:300px" />
</el-card>
<el-card shadow="never" style="margin-top:12px">
<template #header>
<span>质控结果列表</span>
</template>
<el-table :data="tableData" border stripe size="small">
<el-table-column prop="qcItem" label="质控项目" width="120" />
<el-table-column prop="instrumentName" label="仪器" width="120" />
<el-table-column prop="targetValue" label="靶值" width="90" />
<el-table-column prop="actualValue" label="实测值" width="90" />
<el-table-column prop="sdValue" label="SD" width="80" />
<el-table-column prop="cvRate" label="CV%" width="80" />
<el-table-column prop="westgardRule" label="Westgard判定" width="180">
<template #default="{row}">
<el-tag :type="row.isPass ? 'success' : 'danger'" size="small">{{ row.westgardRule }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="qcDate" label="日期" width="110" />
<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>
</template>
<script setup>
import { ref, onMounted, nextTick, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import { runWestgard as runWestgardApi, getQcResults, getQcStats } from '@/api/lab/labQc'
const chartRef = ref(null)
let chartInstance = null
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const stats = ref({ total: 0, passed: 0, failed: 0 })
const query = ref({ qcItem: '', isPass: null, pageNo: 1, pageSize: 20 })
const defaultForm = () => ({
qcItem: '', instrumentName: '', targetValue: null, actualValue: null,
qcDate: '', operatorName: '', departmentName: '', departmentId: null, remarks: ''
})
const form = ref(defaultForm())
const loadResults = async () => {
const r = await getQcResults(query.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
renderChart()
}
const loadStats = async () => {
const r = await getQcStats()
stats.value = r.data || stats.value
}
const doRunWestgard = async () => {
if (!form.value.qcItem || !form.value.actualValue) {
ElMessage.warning('请填写质控项目和实测值')
return
}
loading.value = true
try {
const res = await runWestgardApi(form.value)
if (res.data?.isPass) {
ElMessage.success('Westgard判定: ' + res.data.westgardRule)
} else {
ElMessage.error('Westgard判定: ' + res.data.westgardRule)
}
form.value = defaultForm()
await loadResults()
await loadStats()
} finally {
loading.value = false
}
}
const renderChart = () => {
if (!chartRef.value || tableData.value.length === 0) return
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value)
}
const items = [...tableData.value].reverse()
const dates = items.map(i => i.qcDate)
const values = items.map(i => i.actualValue)
const targetValues = items.map(i => i.targetValue)
const mean = targetValues.reduce((a, b) => a + (b || 0), 0) / targetValues.length || 0
const sdArr = items.map(i => i.sdValue || 0)
const sd = sdArr.reduce((a, b) => a + b, 0) / sdArr.length || 1
chartInstance.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['实测值', '靶值', '+2SD', '-2SD', '+3SD', '-3SD'] },
grid: { left: 60, right: 20, top: 40, bottom: 40 },
xAxis: { type: 'category', data: dates },
yAxis: { type: 'value', name: '检测值' },
series: [
{ name: '实测值', type: 'line', data: values, itemStyle: { color: '#409EFF' }, lineStyle: { width: 2 } },
{ name: '靶值', type: 'line', data: targetValues, lineStyle: { type: 'dashed', color: '#67C23A' }, itemStyle: { color: '#67C23A' } },
{ name: '+2SD', type: 'line', data: dates.map(() => mean + 2 * sd), lineStyle: { type: 'dotted', color: '#E6A23C' }, itemStyle: { color: '#E6A23C' }, symbol: 'none' },
{ name: '-2SD', type: 'line', data: dates.map(() => mean - 2 * sd), lineStyle: { type: 'dotted', color: '#E6A23C' }, itemStyle: { color: '#E6A23C' }, symbol: 'none' },
{ name: '+3SD', type: 'line', data: dates.map(() => mean + 3 * sd), lineStyle: { type: 'dotted', color: '#F56C6C' }, itemStyle: { color: '#F56C6C' }, symbol: 'none' },
{ name: '-3SD', type: 'line', data: dates.map(() => mean - 3 * sd), lineStyle: { type: 'dotted', color: '#F56C6C' }, itemStyle: { color: '#F56C6C' }, symbol: 'none' },
]
})
}
const handleResize = () => chartInstance?.resize()
onMounted(async () => {
await loadResults()
await loadStats()
await nextTick()
renderChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
})
</script>