2026-02-09 17:34:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @file index.vue 质控图westGard
|
|
|
|
|
|
* @author: w
|
|
|
|
|
|
* @since: 2026-02-04
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
|
<el-row :gutter="10">
|
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
|
<div class="mb5">
|
|
|
|
|
|
<el-button type="primary" @click="getList">画图</el-button>
|
|
|
|
|
|
<el-button type="warning">打印</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-form :model="queryParams" ref="queryRef" size="small">
|
|
|
|
|
|
<el-form-item label="组别:" prop="instrGroup">
|
|
|
|
|
|
<SelectTable v-model:data="queryParams.instrGroup" :tableData="instrGroupOptions" placeholder="请选择"
|
|
|
|
|
|
@getDataValue="getYqConfig" size="small" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="仪器:" prop="yq">
|
|
|
|
|
|
<SelectTable v-model:data="queryParams.yq" :tableData="instrOptions" placeholder="请选择仪器"
|
|
|
|
|
|
@getDataValue="getXmList" size="small" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="月份:" prop="zkrq1">
|
|
|
|
|
|
<el-date-picker v-model="queryParams.zkrq1" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
|
|
|
|
|
|
style="width: 47%;" @change="handletime" /> -
|
|
|
|
|
|
<el-date-picker v-model="queryParams.zkrq2" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
|
|
|
|
|
|
@change="handletime" style="width: 47%;" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="次数:" prop="cs">
|
|
|
|
|
|
<SelectTable v-model:data="queryParams.cs" :tableData="csData" placeholder="请选择"
|
|
|
|
|
|
@getDataValue="() => { queryParams.zkph = '' }" size="small" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="19">
|
|
|
|
|
|
<CustomTable ref="tableRef" :data="tableData" :columns="columns" :config="tableConfig" @row-click="rowClick"
|
|
|
|
|
|
@selection-change="selectChange">
|
|
|
|
|
|
<template #action="scope">
|
|
|
|
|
|
<el-button type="text" @click="handleEdit(scope.row, $event)">计算均值</el-button>
|
|
|
|
|
|
<el-button type="text" @click="handlePj(scope.row, $event)">评价</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</CustomTable>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="10" class="table-row">
|
|
|
|
|
|
<el-col :span="5" class="table-col">
|
|
|
|
|
|
<div class="table-main">
|
|
|
|
|
|
<el-table ref="xmRef" :data="xmList" height="100%" border highlightCurrentRow @row-click="xmHandle"
|
|
|
|
|
|
show-overflow-tooltip>
|
|
|
|
|
|
<el-table-column label="项目代号" prop="xmdh">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ row.xmdh }} {{ row.xmmc }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="19" class="table-col2">
|
|
|
|
|
|
<div ref="chartRef" style="width: 100%; height:300px"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="tips">标准:</div>
|
|
|
|
|
|
<el-table :data="zklist" height="150" border>
|
|
|
|
|
|
<el-table-column label="质控品" prop="zkph" align="center">
|
|
|
|
|
|
<template #default="{ row, $index }">
|
|
|
|
|
|
{{ row.zkph }} <span class="radio" :style="{ backgroundColor: colorMap[$index] }"></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="𝑋̄" prop="avg0" align="center" />
|
|
|
|
|
|
<el-table-column label="SD" prop="sd0" align="center" />
|
|
|
|
|
|
<el-table-column label="CV%" prop="cv0" align="center" />
|
|
|
|
|
|
<el-table-column label="本月𝑋̄" prop="avg1" align="center" />
|
|
|
|
|
|
<el-table-column label="本月SD" prop="sd1" align="center" />
|
|
|
|
|
|
<el-table-column label="本月CV" prop="cv1" align="center" />
|
|
|
|
|
|
<el-table-column label="批号" prop="zkpph" align="center" />
|
|
|
|
|
|
<el-table-column label="累计𝑋̄" prop="avg3" align="center" />
|
|
|
|
|
|
<el-table-column label="累计SD" prop="sd3" align="center" />
|
|
|
|
|
|
<el-table-column label="累计CV" prop="cv3" align="center" />
|
|
|
|
|
|
<el-table-column label="失控判断" prop="skpd" align="center" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="tips">当前图表数据:</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="result-box">
|
|
|
|
|
|
<div v-for="item in resultData" class="item">
|
|
|
|
|
|
<div class="time">{{ item.zkrqMonth }}</div>
|
|
|
|
|
|
<div v-for="v in zkKeys">
|
2026-02-10 17:36:45 +08:00
|
|
|
|
<div :class="[item['zkphFlag' + v] ? 'red_title' : '', 'no-select']" @dblclick="dbSKHandle(v, item)">
|
|
|
|
|
|
{{ item['zkjg' + v] }}
|
|
|
|
|
|
</div>
|
2026-02-09 17:34:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>{{ item.yhdh }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 质控值、SD计算 -->
|
2026-02-10 17:36:45 +08:00
|
|
|
|
<el-dialog v-model="dialogVisible" title="质控靶值、SD计算" draggable :close-on-click-modal="false" width="600px">
|
2026-02-09 17:34:49 +08:00
|
|
|
|
<el-form :model="form" ref="queryRef" label-width="90px">
|
|
|
|
|
|
<el-form-item label="计算时间:" prop="xmdh">
|
2026-02-10 17:36:45 +08:00
|
|
|
|
<el-date-picker v-model="form.zkrq1" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
|
|
|
|
|
|
style="width: 40%;" @change="handletime" /> -
|
|
|
|
|
|
<el-date-picker v-model="form.zkrq2" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
|
|
|
|
|
|
@change="handletime" style="width: 40%;" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-button type="primary" class="ml5" @click="jsTime">计算</el-button>
|
2026-02-09 17:34:49 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="质控项目:" prop="xmdh">
|
|
|
|
|
|
<el-input v-model="form.xmdh" readonly />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="质控批号:" prop="zkph">
|
|
|
|
|
|
<el-input v-model="form.zkph" readonly />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="计算靶值:" prop="zkbz">
|
|
|
|
|
|
<el-input v-model="form.zkbz" readonly />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="SD:" prop="sd">
|
|
|
|
|
|
<el-input v-model="form.sd" readonly />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div class="tips">
|
|
|
|
|
|
提示:请选择计算起始和截止日期,系统自动计算出靶值和SD,计算完毕后可以按“确定”按钮返回计算值,也可以按“取消”按钮直接关闭窗口。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="subHandle">确定</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="pjShow" title="本月质控评价" draggable :close-on-click-modal="false" width="500px">
|
|
|
|
|
|
<el-form :model="pjForm" label-width="90px">
|
|
|
|
|
|
<el-form-item label="质控批号:" prop="zkph">
|
|
|
|
|
|
<el-input v-model="pjForm.zkph" readonly />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="评价:" prop="zkph">
|
|
|
|
|
|
<el-input v-model="pjForm.bz1" type="textarea" :rows="10" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
|
<el-button @click="pjShow = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="pjHandle">确定</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { getComDicts } from "@/api/liswork/dict/ComDict";
|
|
|
|
|
|
import { getGroupInstrdList } from "@/api/liswork/work/LisWork";
|
2026-02-10 17:36:45 +08:00
|
|
|
|
import { qcgraphList, querySample, qcCalc, saveBtQcSample } from "@/api/liswork/qualityControl/index";
|
2026-02-09 17:34:49 +08:00
|
|
|
|
import { classCom } from "@/utils/classCom";
|
|
|
|
|
|
import { useCommonStore } from '@/store/modules/commonStore';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
import { queryBatch, queryBatchXm, } from '@/api/liswork/qualityControl'
|
|
|
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
|
import { use } from "vxe-pc-ui";
|
|
|
|
|
|
|
|
|
|
|
|
const mbStore = useCommonStore();
|
|
|
|
|
|
|
|
|
|
|
|
const pjShow = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const pjForm = ref({
|
|
|
|
|
|
zkph: '',
|
|
|
|
|
|
bz1: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const dialogVisible = ref(false)
|
|
|
|
|
|
const form = ref({
|
|
|
|
|
|
xmdh: '',
|
2026-02-10 17:36:45 +08:00
|
|
|
|
zkrq1: '',
|
|
|
|
|
|
zkrq2: '',
|
2026-02-09 17:34:49 +08:00
|
|
|
|
zkph: '',
|
|
|
|
|
|
zkbz: '',
|
|
|
|
|
|
sd: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const queryParams = ref({
|
|
|
|
|
|
// instrGroup: mbStore.defaultConfig.lisgroupid || 'ALL',
|
|
|
|
|
|
instrGroup: 'ALL',
|
2026-02-10 17:36:45 +08:00
|
|
|
|
yq: mbStore.defaultConfig.defaultinstr || '1',
|
2026-02-09 17:34:49 +08:00
|
|
|
|
zkrq1: '',
|
|
|
|
|
|
zkrq2: '',
|
|
|
|
|
|
cs: '0',
|
|
|
|
|
|
xmdh: '',
|
|
|
|
|
|
zkph: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const chartRef = useTemplateRef('chartRef')
|
|
|
|
|
|
const tableRef = useTemplateRef('tableRef')
|
|
|
|
|
|
const tableData = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
const columns = ref([
|
|
|
|
|
|
{ type: 'selection', label: '画图', width: 30, visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '质控品', prop: 'zkph', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '起始日期', prop: 'ksrq', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '质控批号', prop: 'zkpph', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '浓度', prop: 'conc', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '平均值', prop: 'zkbz', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '试剂', prop: 'sjph', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '波长', prop: 'bc', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: '方法', prop: 'ff', visible: true, align: 'center' },
|
2026-02-10 17:36:45 +08:00
|
|
|
|
{ label: 'cv(%)', prop: 'cv', visible: true, align: 'center' },
|
|
|
|
|
|
{ label: 'sd', prop: 'sd', visible: true, align: 'center' },
|
2026-02-09 17:34:49 +08:00
|
|
|
|
{ label: '备注', prop: 'bz1', visible: true, align: 'center' },
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const tableConfig = ref({
|
|
|
|
|
|
border: true,
|
|
|
|
|
|
height: '156px',
|
|
|
|
|
|
actionWidth: 150,
|
|
|
|
|
|
highlightCurrentRow: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
const instrGroupOptions = ref([])
|
|
|
|
|
|
const instrOptions = ref([])
|
|
|
|
|
|
const csData = ref([
|
|
|
|
|
|
{ value: '9', label: '最好1次' },
|
|
|
|
|
|
{ value: '0', label: '平均值' },
|
|
|
|
|
|
{ value: '8', label: '最后一次' },
|
|
|
|
|
|
{ value: '10', label: '所有点' },
|
|
|
|
|
|
{ value: '1', label: '第1次' },
|
|
|
|
|
|
{ value: '2', label: '第2次' },
|
|
|
|
|
|
{ value: '3', label: '第3次' },
|
|
|
|
|
|
{ value: '4', label: '第4次' },
|
|
|
|
|
|
{ value: '5', label: '第5次' },
|
|
|
|
|
|
{ value: '6', label: '第6次' },
|
|
|
|
|
|
{ value: '7', label: '第7次' },
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const zklist = ref([])
|
|
|
|
|
|
const qcData = ref([])
|
|
|
|
|
|
const resultData = ref([])
|
|
|
|
|
|
const zkKeys = ref([])
|
|
|
|
|
|
const xmList = ref([])
|
|
|
|
|
|
const colorMap = ['#333', '#00b42a', '#ff9900', '#ff0', '#2f6dd1', '#a65']
|
|
|
|
|
|
let mockRawDataMap = {};
|
|
|
|
|
|
const loadinstrGroupOptions = () => {
|
|
|
|
|
|
getComDicts({ zdlb: 'GROUP' }).then((res) => {
|
|
|
|
|
|
instrGroupOptions.value = res.data.map((item) => ({
|
|
|
|
|
|
value: item.zddh,
|
|
|
|
|
|
label: item.zdmc
|
|
|
|
|
|
}))
|
|
|
|
|
|
instrGroupOptions.value.push({ value: 'ALL', label: '所有仪器' })
|
|
|
|
|
|
getYqConfig()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 仪器
|
|
|
|
|
|
const getYqConfig = () => {
|
|
|
|
|
|
queryParams.value.zkph = ''
|
|
|
|
|
|
getGroupInstrdList({ lisgroup: queryParams.value.instrGroup })
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
if (res.code == 0) {
|
|
|
|
|
|
instrOptions.value = res.data.map((item) => ({
|
|
|
|
|
|
value: item.yq.trim(),
|
|
|
|
|
|
label: item.yqmc
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 17:36:45 +08:00
|
|
|
|
// 获取上个月的第一天和最后一天
|
|
|
|
|
|
|
|
|
|
|
|
const getLastMonthRange = (format = 'YYYY-MM-DD') => {
|
|
|
|
|
|
const lastMonth = dayjs().subtract(1, 'month');
|
|
|
|
|
|
return {
|
|
|
|
|
|
firstDay: lastMonth.startOf('month').format(format),
|
|
|
|
|
|
lastDay: lastMonth.endOf('month').format(format)
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const info = ref({})
|
2026-02-09 17:34:49 +08:00
|
|
|
|
const handleEdit = (row, event) => {
|
2026-02-10 17:36:45 +08:00
|
|
|
|
const { firstDay, lastDay } = getLastMonthRange();
|
|
|
|
|
|
info.value = row
|
2026-02-09 17:34:49 +08:00
|
|
|
|
event.stopPropagation();
|
2026-02-10 17:36:45 +08:00
|
|
|
|
form.value.zkrq1 = firstDay
|
|
|
|
|
|
form.value.zkrq2 = lastDay
|
2026-02-09 17:34:49 +08:00
|
|
|
|
form.value.xmdh = row.xmdh
|
|
|
|
|
|
form.value.zkph = row.zkph
|
2026-02-10 17:36:45 +08:00
|
|
|
|
// row.cv = form.value.sd * 100 / form.value.zkph
|
2026-02-09 17:34:49 +08:00
|
|
|
|
dialogVisible.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-10 17:36:45 +08:00
|
|
|
|
const jsTime = () => {
|
|
|
|
|
|
qcCalc({ ...form.value, yq: queryParams.value.yq, cs: queryParams.value.cs }).then((res) => {
|
|
|
|
|
|
form.value.zkbz = res.data[0].avg1
|
|
|
|
|
|
form.value.sd = res.data[0].sd1
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 17:34:49 +08:00
|
|
|
|
const handlePj = (row, event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
pjForm.value = {
|
|
|
|
|
|
zkph: row.zkph,
|
|
|
|
|
|
bz1: row.bz1,
|
|
|
|
|
|
}
|
2026-02-10 17:36:45 +08:00
|
|
|
|
info.value = row
|
2026-02-09 17:34:49 +08:00
|
|
|
|
pjShow.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 17:36:45 +08:00
|
|
|
|
const dbSKHandle = (val, item) => {
|
|
|
|
|
|
console.log('val==>', val, item);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 17:34:49 +08:00
|
|
|
|
const pjHandle = () => {
|
2026-02-10 17:36:45 +08:00
|
|
|
|
info.value.bz1 = pjForm.value.bz1
|
|
|
|
|
|
saveBtQcSample([info.value]).then((res) => {
|
|
|
|
|
|
if (res.code == 0) {
|
|
|
|
|
|
pjShow.value = false
|
|
|
|
|
|
tableData.value = tableData.value.map((item, i) => item.zkph == info.value.zkph ? { ...info.value } : item);
|
|
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
tableRef.value.toggleAllSelection()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-09 17:34:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const subHandle = () => {
|
2026-02-10 17:36:45 +08:00
|
|
|
|
if (!form.value.zkbz && !form.value.sd) return ElMessage.warning('请先计算!')
|
|
|
|
|
|
|
|
|
|
|
|
info.value.cv = (form.value.sd * 100 / form.value.zkbz).toFixed(3)
|
|
|
|
|
|
info.value.zkbz = form.value.zkbz
|
|
|
|
|
|
info.value.sd = form.value.sd
|
|
|
|
|
|
|
|
|
|
|
|
saveBtQcSample([info.value]).then((res) => {
|
|
|
|
|
|
if (res.code == 0) {
|
|
|
|
|
|
dialogVisible.value = false
|
|
|
|
|
|
tableData.value = tableData.value.map((item, i) => item.zkph == info.value.zkph ? { ...info.value } : item);
|
|
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
tableRef.value.toggleAllSelection()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-02-09 17:34:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rowClick = (row) => {
|
|
|
|
|
|
queryParams.value.zkph = row.zkph
|
|
|
|
|
|
getList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selectChange = (value) => {
|
|
|
|
|
|
const arr = value.map(item => item.zkph)
|
|
|
|
|
|
queryParams.value.zkph = arr.join(',')
|
|
|
|
|
|
}
|
|
|
|
|
|
const xmHandle = (row) => {
|
|
|
|
|
|
queryParams.value.xmdh = row.xmdh
|
|
|
|
|
|
queryParams.value.zkph = ''
|
|
|
|
|
|
getSampleList()
|
|
|
|
|
|
}
|
|
|
|
|
|
const handletime = () => {
|
|
|
|
|
|
queryParams.value.zkph = ''
|
|
|
|
|
|
const dayFlag = classCom.getDayDiff(queryParams.value.zkrq1, queryParams.value.zkrq2)
|
|
|
|
|
|
if (dayFlag === -1 || dayFlag === -2) return ElMessage.warning('请选择有效的日期范围')
|
|
|
|
|
|
}
|
|
|
|
|
|
const xmRef = useTemplateRef('xmRef')
|
|
|
|
|
|
const getXmList = () => {
|
|
|
|
|
|
queryParams.value.zkph = ''
|
|
|
|
|
|
queryBatchXm({ yq: queryParams.value.yq }).then((res) => {
|
|
|
|
|
|
xmList.value = res.data
|
|
|
|
|
|
queryParams.value.xmdh = res.data[0].xmdh
|
|
|
|
|
|
xmRef.value.setCurrentRow(res.data[0])
|
|
|
|
|
|
getSampleList()
|
|
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getSampleList = () => {
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
yq: queryParams.value.yq,
|
|
|
|
|
|
xmdh: queryParams.value.xmdh,
|
|
|
|
|
|
zkrq1: queryParams.value.zkrq1,
|
|
|
|
|
|
zkrq2: queryParams.value.zkrq2,
|
|
|
|
|
|
cs: queryParams.value.cs
|
|
|
|
|
|
}
|
|
|
|
|
|
querySample(data).then((res) => {
|
|
|
|
|
|
tableData.value = res.data || []
|
|
|
|
|
|
getList()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
tableRef.value.toggleAllSelection()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let myChart = null
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
|
const initChart = () => {
|
|
|
|
|
|
if (!chartRef.value) return;
|
|
|
|
|
|
if (myChart) myChart.dispose();
|
|
|
|
|
|
const el = chartRef.value;
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
myChart = echarts.init(el);
|
|
|
|
|
|
|
|
|
|
|
|
// 将原始值转换为SD映射值(-4~4)
|
|
|
|
|
|
const handleDataToSD = (rawData, X, sd) => {
|
|
|
|
|
|
const renderSDData = [];
|
|
|
|
|
|
const originValue = [];
|
|
|
|
|
|
rawData.forEach(val => {
|
|
|
|
|
|
if (val === null) {
|
|
|
|
|
|
// 无数据时,SD值也设为 null,折线跳过该点
|
|
|
|
|
|
renderSDData.push(null);
|
|
|
|
|
|
originValue.push(null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
originValue.push(val);
|
|
|
|
|
|
const sdValue = (val - X) / sd;
|
|
|
|
|
|
renderSDData.push(Math.max(-4, Math.min(4, sdValue)));
|
|
|
|
|
|
});
|
|
|
|
|
|
return { renderSDData, originValue };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const series = [];
|
|
|
|
|
|
// 存储所有系列的原始值(供tooltip使用)
|
|
|
|
|
|
const originValueMap = {};
|
|
|
|
|
|
|
|
|
|
|
|
zklist.value.forEach((item, index) => {
|
|
|
|
|
|
const { zkph, avg0, sd0 } = item;
|
|
|
|
|
|
const rawData = mockRawDataMap[zkph] || []; // 获取当前质控品的原始数据
|
|
|
|
|
|
const { renderSDData, originValue } = handleDataToSD(rawData, avg0, sd0);
|
|
|
|
|
|
|
|
|
|
|
|
// 存储原始值
|
|
|
|
|
|
originValueMap[zkph] = originValue;
|
|
|
|
|
|
|
|
|
|
|
|
// 系列配置
|
|
|
|
|
|
series.push({
|
|
|
|
|
|
name: `质控品${zkph}`,
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: renderSDData,
|
|
|
|
|
|
smooth: true,
|
|
|
|
|
|
symbol: 'circle',
|
|
|
|
|
|
symbolSize: 6,
|
|
|
|
|
|
lineWidth: 1.5,
|
|
|
|
|
|
connectNulls: true, // 开启 null 点连接
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
// 按数据点数值动态设置转折点颜色
|
|
|
|
|
|
color: (params) => getSymbolColor(params.value, colorMap[index]),
|
|
|
|
|
|
shadowColor: colorMap[index],
|
|
|
|
|
|
},
|
|
|
|
|
|
lineStyle: { color: colorMap[index] || '#666' },
|
|
|
|
|
|
z: 3,
|
|
|
|
|
|
// 仅第一个系列显示SD参考线(避免重复)
|
|
|
|
|
|
markLine: zkph === zklist.value[0].zkph ? {
|
|
|
|
|
|
silent: true,
|
|
|
|
|
|
symbol: ['none', 'none'],
|
|
|
|
|
|
z: 2,
|
|
|
|
|
|
lineStyle: { type: 'solid', width: 1.2 },
|
|
|
|
|
|
data: [
|
|
|
|
|
|
{ yAxis: 3, lineStyle: { color: '#f53f3f' }, label: { show: false } },
|
|
|
|
|
|
{ yAxis: 2, lineStyle: { color: '#00b42a' }, label: { show: false } },
|
|
|
|
|
|
{ yAxis: 1, lineStyle: { color: '#1890ff' }, label: { show: false } },
|
|
|
|
|
|
{ yAxis: 0, lineStyle: { color: '#333' }, label: { show: false } },
|
|
|
|
|
|
{ yAxis: -1, lineStyle: { color: '#1890ff' }, label: { show: false } },
|
|
|
|
|
|
{ yAxis: -2, lineStyle: { color: '#00b42a' }, label: { show: false } },
|
|
|
|
|
|
{ yAxis: -3, lineStyle: { color: '#f53f3f' }, label: { show: false } }
|
|
|
|
|
|
]
|
|
|
|
|
|
} : undefined
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// X轴数据
|
|
|
|
|
|
const xAxisData = Array.from(new Set(qcData.value.map(item => item.zkrqMonth))).splice(0, 31)
|
|
|
|
|
|
// 核心配置
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
formatter: (params) => {
|
|
|
|
|
|
// 过滤掉 null 值的系列
|
|
|
|
|
|
const validParams = params.filter(p => p.data !== null);
|
|
|
|
|
|
if (validParams.length === 0) return ''; // 无有效数据时不展示 tooltip
|
|
|
|
|
|
|
|
|
|
|
|
let res = `<div style="font-size:12px; font-weight:500;">${validParams[0].axisValue}</div>`;
|
|
|
|
|
|
validParams.forEach((item, index) => {
|
|
|
|
|
|
const zkph = item.seriesName.replace('质控品', '');
|
|
|
|
|
|
const originVal = originValueMap[zkph][item.dataIndex];
|
|
|
|
|
|
if (originVal === null) return; // 跳过 null 值
|
|
|
|
|
|
const color = colorMap[index] || '#666';
|
|
|
|
|
|
const currentItem = zklist.value.find(item => item.zkph === zkph);
|
|
|
|
|
|
const originalSD = currentItem ? (originVal - currentItem.avg0) / currentItem.sd0 : 0;
|
|
|
|
|
|
res += `<div style="margin-top:4px; font-size:12px;">
|
|
|
|
|
|
<span style="display: inline-block; width: 10px; height: 10px; background: ${color};"></span>
|
|
|
|
|
|
${item.seriesName}:<b>${originVal.toFixed(2)}</b>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
return res;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-10 17:36:45 +08:00
|
|
|
|
grid: { left: '1%', right: '2%', bottom: '10%', top: '10%', containLabel: true },
|
2026-02-09 17:34:49 +08:00
|
|
|
|
dataZoom: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "slider",
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
realtime: true,
|
|
|
|
|
|
bottom: 20,
|
|
|
|
|
|
height: 10,
|
|
|
|
|
|
textStyle: { color: "transparent" },
|
|
|
|
|
|
fillerColor: "#7ECFFF", // 浅蓝填充
|
|
|
|
|
|
handleSize: "120%",
|
|
|
|
|
|
handleStyle: {
|
|
|
|
|
|
color: "#2DE2C4", // 滑块颜色与曲线主色一致
|
|
|
|
|
|
borderWidth: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
backgroundColor: "#E5F3FF", // 滑条底色浅蓝
|
|
|
|
|
|
borderColor: "transparent",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
boundaryGap: false,
|
|
|
|
|
|
data: xAxisData,
|
|
|
|
|
|
axisLabel: {
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
// interval: 0, // 强制显示所有标签
|
|
|
|
|
|
// width: 80, // 每个标签固定宽度80px(可根据需求调整)
|
|
|
|
|
|
// align: 'center', // 标签居中
|
|
|
|
|
|
// // rotate: 30, // 旋转避免文字重叠(可选)
|
|
|
|
|
|
// overflow: 'truncate' // 文字过长时截断
|
|
|
|
|
|
},
|
|
|
|
|
|
axisLine: { lineStyle: { color: '#000' } },
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
min: -4,
|
|
|
|
|
|
max: 4,
|
|
|
|
|
|
axisLabel: {
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
formatter: (v) => v === 0 ? '𝑋̄' : `${v}SD`
|
|
|
|
|
|
},
|
|
|
|
|
|
interval: 1,
|
|
|
|
|
|
axisLine: { show: true, lineStyle: { color: '#000' } },
|
|
|
|
|
|
splitLine: { lineStyle: { type: 'dashed', color: '#000' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
series: series // 使用动态生成的系列
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
myChart.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
// 封装转折点颜色判断函数
|
|
|
|
|
|
const getSymbolColor = (value, defaultColor) => {
|
|
|
|
|
|
if (Math.abs(value) > 3) {
|
|
|
|
|
|
return '#ff0000'; // 红色
|
|
|
|
|
|
} else if (Math.abs(value) > 2) {
|
|
|
|
|
|
return '#ffc0cb'; // 粉色
|
|
|
|
|
|
}
|
|
|
|
|
|
return defaultColor;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getList = () => {
|
|
|
|
|
|
qcgraphList(queryParams.value).then((res) => {
|
|
|
|
|
|
mockRawDataMap = {};
|
|
|
|
|
|
if (res.data) {
|
|
|
|
|
|
zklist.value = res.data.QcSamplelist;
|
|
|
|
|
|
qcData.value = res.data.QcValList.map((item) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
zkrqMonth: dayjs(item.zkrq).format('YY/M/D')
|
|
|
|
|
|
}));
|
|
|
|
|
|
const xAxisData = Array.from(new Set(qcData.value.map(item => item.zkrqMonth))).splice(0, 31);
|
|
|
|
|
|
const zkphs = Array.from(new Set(zklist.value.map(item => item.zkph)));
|
|
|
|
|
|
const dateZkphValueMap = new Map();
|
|
|
|
|
|
qcData.value.forEach((data) => {
|
|
|
|
|
|
const key = `${data.zkrqMonth}_${data.zkph}`;
|
|
|
|
|
|
dateZkphValueMap.set(key, data.zkjg ?? null); // 无值则存 null
|
|
|
|
|
|
});
|
|
|
|
|
|
zkphs.forEach((zkph) => {
|
|
|
|
|
|
mockRawDataMap[zkph] = xAxisData.map((date) => {
|
|
|
|
|
|
const key = `${date}_${zkph}`;
|
|
|
|
|
|
return dateZkphValueMap.get(key) ?? null; // 无数据 = null
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
zkKeys.value = Object.keys(mockRawDataMap);
|
|
|
|
|
|
|
|
|
|
|
|
resultData.value = xAxisData.map((item, i) => {
|
|
|
|
|
|
const obj = {};
|
|
|
|
|
|
obj['zkrqMonth'] = item;
|
|
|
|
|
|
zkKeys.value.map(v => {
|
|
|
|
|
|
obj['zkjg' + v] = mockRawDataMap[v][i] ?? '';
|
|
|
|
|
|
});
|
|
|
|
|
|
return obj;
|
|
|
|
|
|
}).splice(0, 31);
|
|
|
|
|
|
|
|
|
|
|
|
resultData.value.map(item => {
|
|
|
|
|
|
qcData.value.map(v => {
|
|
|
|
|
|
if (item.zkrqMonth == v.zkrqMonth) {
|
|
|
|
|
|
item.yhdh = v.yhdh;
|
|
|
|
|
|
// 判断是否为失控
|
2026-02-10 17:36:45 +08:00
|
|
|
|
item['zkphFlag' + v.zkph] = Math.abs((v.zkbz - v.zkjg) / v.sd) > 3 ? true : false;
|
2026-02-09 17:34:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
// console.log('resultData.value==>', resultData.value);
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
initChart();
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
qcData.value = [];
|
|
|
|
|
|
zklist.value = [];
|
|
|
|
|
|
zkKeys.value = [];
|
|
|
|
|
|
resultData.value = [];
|
|
|
|
|
|
initChart()
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const timeDate = () => {
|
|
|
|
|
|
const today = dayjs();
|
|
|
|
|
|
const startOfWeek = today.subtract(6, 'day');
|
|
|
|
|
|
// queryParams.zkrq1 = startOfWeek.format('YYYY-MM-DD');
|
|
|
|
|
|
// queryParams.zkrq2 = today.format('YYYY-MM-DD');
|
|
|
|
|
|
queryParams.value.zkrq1 = '2025-02-01';
|
|
|
|
|
|
queryParams.value.zkrq2 = today.format('YYYY-MM-DD');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
getXmList()
|
|
|
|
|
|
timeDate()
|
|
|
|
|
|
loadinstrGroupOptions()
|
|
|
|
|
|
// getList()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.el-form-item {
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-row {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
.table-col {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
.table-main {
|
|
|
|
|
|
height: calc(100% - 165px);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-col2 {
|
|
|
|
|
|
height: calc(100% - 165px);
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tips {
|
|
|
|
|
|
font-family: SourceHanSansCN, SourceHanSansCN;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.radio {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-box {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
|
|
|
|
.item {
|
|
|
|
|
|
|
|
|
|
|
|
&>div {
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
line-height: 24px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.time {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.red_title {
|
|
|
|
|
|
color: #f00;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|