质控录入

This commit is contained in:
wyuu 2026-01-13 17:37:28 +08:00
parent b584460dd8
commit be3703bd7a
8 changed files with 792 additions and 170 deletions

View File

@ -107,3 +107,11 @@ export function queryValXmdh(data?: Object) {
params: data
})
}
// 查询质控品录入
export function getqcval(data?: Object) {
return request({
url: '/qc/getqcval',
method: 'get',
params: data
})
}

View File

@ -30,11 +30,19 @@
<el-empty v-if="filterData.length === 0" description="没有匹配的数据" :image-size="100" />
<div @keydown="handleKeydown" v-else tabindex="0" style="outline: none;">
<!-- 数据存在时显示表格 -->
<el-table ref="tableRef" :data="filterData" :highlight-current-row="props.isHighlight" style="width: 100%"
<!-- <el-table ref="tableRef" :data="filterData" :highlight-current-row="props.isHighlight" style="width: 100%"
:border="props.border" @row-click="handleRowChange" max-height="350px" :row-style="{ height: '25px' }">
<el-table-column v-for="field in props.fields" :prop="field.prop" :label="field.label"
:width="field.width" show-overflow-tooltip align="center" />
</el-table>
</el-table> -->
<vxe-table class="mytable-style" ref="tableRef" border resizable highlight-current-row max-height="350px"
:row-config="{ isHover: true, keyField: 'value', height: 30 }" :data="filterData"
:current-row="currentRow" @current-change="currentChange"
:tooltip-config="{ appendToBody: true, zIndex: 3000 }" :show-overflow="true"
:scroll-y="{ enabled: true, rSize: 30, height: '100%' }">
<vxe-table-column v-for="field in props.fields" :field="field.prop" :title="field.label" align="center"
:width="field.width" />
</vxe-table>
</div>
</div>
</template>
@ -78,7 +86,7 @@ const filterData = ref<{ [key: string]: any; pinyinMap: Record<string, string> }
// 存储预处理后的带拼音数据
const processedTableData = ref<{ [key: string]: any; pinyinMap: Record<string, string> }[]>([]);
const currentIndex = ref(-1);
const currentRow = ref<any>(null)
// 字典数据存储
const dictData = ref<Record<string, any[]>>({});
// 预处理数据:生成拼音信息
@ -113,8 +121,8 @@ const visibleChange = (val: any) => {
currentIndex.value = filterData.value.findIndex(item => item[props.value!] === props.data);
if (currentIndex.value >= 0) {
tableRef.value.setCurrentRow(filterData.value[currentIndex.value]);
tableRef.value.setScrollTop(currentIndex.value * 30);
tableRef.value.setCurrentRow(filterData.value[currentIndex.value])
tableRef.value.scrollToRow(filterData.value[currentIndex.value])
}
}, 10);
}
@ -233,11 +241,8 @@ const handleKeydown = (e: any) => {
currentIndex.value = Math.min(maxIndex, currentIndex.value + 1);
}
tableRef.value.setCurrentRow(filterData.value[currentIndex.value]);
// 滚动到当前行
tableRef.value.setScrollTop(currentIndex.value * 30);
tableRef.value.setCurrentRow(filterData.value[currentIndex.value])
tableRef.value.scrollToRow(filterData.value[currentIndex.value])
// 回车确认
if (e.key === "Enter") {
const currentRow = filterData.value[currentIndex.value];
@ -284,11 +289,18 @@ const filterDataHandle = () => {
});
if (filterData.value.length > 0) {
tableRef.value.setScrollTop(0)
tableRef.value.setCurrentRow(filterData.value[0]);
nextTick(() => {
tableRef.value.setCurrentRow(filterData.value[0])
tableRef.value.scrollToRow(filterData.value[0])
});
}
};
const currentChange = (params: { row: any }) => {
currentRow.value = params.row
handleRowChange(params.row)
}
const handleRowChange = (val: any) => {
emits("update:data", props.value ? val[props.value] : val);
setLabel(val);
@ -316,11 +328,44 @@ const clearHandle = () => {
};
</script>
<style scoped>
<style scoped lang="scss">
.select-table-container {
width: 100%;
}
:deep(.vxe-header--row th) {
border-color: #1F6DD3;
background-color: #1F6DD3;
color: #fff;
}
:deep(.vxe-body--row td) {
border-color: #1F6DD3;
}
:deep(.vxe-table--row-hover) {
background-color: #1F6DD3;
}
:deep(.vxe-table--current-row) {
background-color: #1F6DD3;
}
:deep(.el-table-header-cell) {
cursor: move;
user-select: none;
}
// 表格边框
:deep(.vxe-table--render-default.border--full .vxe-body--column) {
background-image: linear-gradient(#96B8D9, #96B8D9), linear-gradient(#96B8D9, #96B8D9) !important;
}
// 表头边框
:deep(.vxe-table--render-default.border--full .vxe-header--column) {
background-image: linear-gradient(#1F6DD3, #1F6DD3), linear-gradient(#1F6DD3, #1F6DD3) !important;
}
.select-table-dropdown {
padding: 8px;
}

View File

@ -1,6 +1,6 @@
<template>
<div class="app-container ">
<div class="tips">检验科条码打印科室:{{ formatDict(queryParams.ksdh, 'DP') }}</div>
<div class="tips">检验科条码打印 科室:{{ formatDict(queryParams.ksdh, 'DP') }} 登录人:{{ formatUser(queryParams.userid) }}</div>
<div ref="compactForm">
<el-row :gutter="10" class="compact-form" v-show="showSearch">
<el-form :model="queryParams" ref="queryRef" :inline="true" :size="aotuSize" :rules="queryRules">
@ -167,6 +167,8 @@ import CustomTable from '@/components/elTable/index.vue'
import CustomVxeTable from '@/components/vxeTable/index.vue'
import { useRouter, useRoute } from 'vue-router'
import { usePrintStore } from '@/store/modules/printStore'
// @ts-ignore
import { listUser } from '@/api/system/user.js'
import { fetchCodeList, addObj, fetchCodeDetail, fetchCodeExec, fetchCodeReqmain, printSendSample } from '@/api/checkCode/index'
const aotuSize = classCom.useAutoSize();
//@ts-ignore
@ -580,7 +582,7 @@ const sendSampleHandle = () => {
}
})
}
const userList = ref<{ userName: string, nickName: string }[]>([])
onMounted(async () => {
// 获取当前日期(当天)
@ -612,6 +614,10 @@ onMounted(async () => {
getList()
}
listUser({ status: 0, del_flag: 0, pageSize: 1000, pageNum: 1 }).then((res: any) => {
userList.value = res.rows;
});
// 加载病人来源字典
const dictRefs = await comDict('PT', 'HOS', 'ST', 'DP');
@ -629,6 +635,10 @@ const formatDict = (v: string, dictType: string) => {
return dictData.value[dictType]?.find((item: any) => item.value == v)?.label || v;
};
const formatUser = (v: string) => {
return userList.value.find((item: any) => item.userName == v)?.nickName || v;
};
const handletime = (val: Array<string>) => {
// console.log('时间', val);
// 校验日期格式

View File

@ -8,7 +8,6 @@
<div class="table-toolbar mb5">
<el-row>
<el-col :span="8">
<el-button type="primary" plain icon="Check" @click="handleSave">保存</el-button>
<el-button type="primary" plain @click="addHandle">新增</el-button>
<el-button type="warning" plain @click="editHandle">修改</el-button>
<el-button type="danger" plain @click="delBatch">删除</el-button>
@ -16,6 +15,7 @@
<el-col :span="16">
<span class="tips">靶值列表</span> &nbsp;
<el-button type="primary" plain @click="dateClick">复制第一个开始日期到每个项目</el-button>
<el-button type="primary" plain icon="Check" @click="handleSave">保存</el-button>
<el-button type="primary" plain @click="">复制靶值</el-button>
<el-button type="primary" plain @click="">新增靶值</el-button>
<el-button type="danger" plain @click="">删除靶值</el-button>
@ -28,6 +28,9 @@
<CustomVxeTable ref="leftTableRef" class="mytable-style" :table-data="tableData" :columns="columns"
:row-config="{ isHover: true, keyField: 'batchno' }" @current-change="batchClick"
:scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }">
<template #conc="{ row }">
{{concList.find(item => item.value == row.conc)?.label || row.conc}}
</template>
<template #useflag="{ row }">
<el-checkbox :model-value="row.useflag == 1" readonly></el-checkbox>
</template>
@ -66,7 +69,8 @@
<SelectTable v-model:data="row.ff" :tableData="dictData.MD" placeholder="" class="full-width-input" />
</template>
<template #sjph="{ row }">
<SelectTable v-model:data="row.sjph" :tableData="dictData.TK" placeholder="" class="full-width-input" />
<SelectTable v-model:data="row.sjph" :tableData="dictData.LOT" value="label" objKey="label" placeholder=""
class="full-width-input" />
</template>
<template #qccv="{ row }">
<el-input v-model="row.qccv" class="full-width-input" />
@ -83,7 +87,7 @@
{{ formatDict(row.ff, 'MD') }}
</template>
<template #sjph="{ row }">
{{ formatDict(row.sjph, 'TK') }}
{{ formatDict(row.sjph, 'LOT') }}
</template>
</CustomTable>
</div>
@ -106,12 +110,17 @@
</el-form-item>
<el-form-item label="水平" prop="zkph">
<el-select v-model="formData.zkph" placeholder="请选择">
<el-option />
<el-option label="水平1" value="1" />
<el-option label="水平2" value="2" />
<el-option label="水平3" value="3" />
<el-option label="水平4" value="4" />
<el-option label="水平5" value="5" />
<el-option label="水平6" value="6" />
</el-select>
</el-form-item>
<el-form-item label="浓度" prop="conc">
<el-select v-model="formData.conc" placeholder="请选择">
<el-option />
<el-option v-for="item in concList" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="批号(瓶子上的)" prop="batchno">
@ -120,21 +129,19 @@
<el-form-item label="有效期" prop="expiredt">
<el-date-picker v-model="formData.expiredt" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="启用日期" prop="ksrq">
<el-date-picker v-model="formData.ksrq" type="date" value-format="YYYY-MM-DD" />
<el-form-item label="启用日期" prop="startda">
<el-date-picker v-model="formData.startda" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="结束日期" prop="endda">
<el-date-picker v-model="formData.endda" type="date" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="质控厂商" prop="qcsrc">
<el-select v-model="formData.qcsrc" placeholder="请选择">
<el-option />
</el-select>
<SelectTable v-model:data="formData.qcsrc" :tableData="dictData.LOT" value="label" objKey="label"
placeholder="" :fields="fields" />
</el-form-item>
<el-form-item label="校准品厂商" prop="standsrc">
<el-select v-model="formData.standsrc" placeholder="请选择">
<el-option />
</el-select>
<SelectTable v-model:data="formData.standsrc" :tableData="dictData.LOT" value="label" objKey="label"
:fields="fields" placeholder="" />
</el-form-item>
<el-form-item label="备注" prop="bk">
<el-input v-model="formData.bk" type="textarea" :row="5" />
@ -168,7 +175,7 @@ const formData = ref({
useflag: '1',
batchno: '',
expiredt: '',
ksrq: '',
startda: '',
endda: '',
qcsrc: '',
standsrc: '',
@ -177,16 +184,29 @@ const formData = ref({
const rules = ref({
zkpph: [{ required: true, message: '请输入质控批号', trigger: 'blur' }],
zkph: [{ required: true, message: '请选择水平', trigger: 'change' }],
ksrq: [{ required: true, message: '请选择启用日期', trigger: 'change' }],
startda: [{ required: true, message: '请选择启用日期', trigger: 'change' }],
})
const fields = ref(
[
{ prop: 'value', label: '代号', width: '40%', enablePinyinSearch: true },
{ prop: 'label', label: '名称', width: '60%', enablePinyinSearch: true },
],
)
const concList = ref([
{ label: '高', value: 'H' },
{ label: '低', value: 'L' },
{ label: '中', value: 'M' },
])
const loading = ref(false)
const tableData = ref([])
const columns = ref([
{ title: '质控批号', field: 'zkpph', width: 80, resizable: true, align: 'center' },
{ title: '水平', field: 'zkph', resizable: true, align: 'center' },
{ title: '浓度', field: 'conc', resizable: true, align: 'center' },
{ title: '浓度', field: 'conc', resizable: true, align: 'center', slotName: 'conc' },
{ title: '在用', field: 'useflag', resizable: true, align: 'center', slotName: 'useflag' },
{ title: '启用日期', field: 'ksrq', width: 90, resizable: true, align: 'center' },
{ title: '启用日期', field: 'startda', width: 90, resizable: true, align: 'center' },
{ title: '停用日期', field: 'endda', width: 90, resizable: true, align: 'center' },
{ title: '批号(瓶子上的)', field: 'batchno', resizable: true, align: 'center', width: 100 },
{ title: '有效期', field: 'expiredt', width: 90, resizable: true, align: 'center' },
@ -204,8 +224,8 @@ const topColumns = [
{ title: '开始日期', field: 'ksrq', resizable: true, align: 'center', slotName: 'ksrq', width: 100 },
{ title: '结束日期', field: 'stopdate', resizable: true, align: 'center', slotName: 'stopdate', width: 100 },
{ title: '波长', field: 'bc', resizable: true, align: 'center', slotName: 'bc', },
{ title: '方法', field: 'ff', resizable: true, align: 'center', slotName: 'ff', },
{ title: '试剂', field: 'sjph', resizable: true, align: 'center', slotName: 'sjph', },
{ title: '方法', field: 'ff', resizable: true, align: 'center', slotName: 'ff', width: 100 },
{ title: '试剂', field: 'sjph', resizable: true, align: 'center', slotName: 'sjph', width: 100 },
{ title: '目标CV(%)', field: 'qccv', resizable: true, align: 'center', slotName: 'qccv', width: 80 },
]
const bottomTableData = ref([])
@ -280,12 +300,21 @@ const delBatch = () => {
const submit = () => {
queryRef.value.validate((valid: boolean) => {
if (!valid) return false;
return
if (title.value == '修改批号') {
updateBatchno(formData.value).then((res: any) => {
if (res.code == 0) {
ElMessage.success('修改成功')
handleDialogClose()
getBatchList()
}
})
} else {
addBatchno(formData.value).then((res: any) => {
if (res.code == 0) {
ElMessage.success('新增成功')
handleDialogClose()
getBatchList()
}
})
}
@ -302,12 +331,13 @@ const handleDialogClose = () => {
useflag: '1',
batchno: '',
expiredt: '',
ksrq: '',
startda: '',
endda: '',
qcsrc: '',
standsrc: '',
bk: ''
}
visible.value = false
}
const batchInfo: any = ref(null)
const batchClick = (row: any) => {
@ -317,7 +347,7 @@ const batchClick = (row: any) => {
const getBatchList = () => {
batchInfo.value = null
loading.value = true
queryBatch({ yq: yq.value, pageNum: 1, pageSize: 9999 }).then((res: any) => {
queryBatch({ yq: yq.value }).then((res: any) => {
loading.value = false
tableData.value = res.data || []
getXmList(tableData.value[0])
@ -360,7 +390,9 @@ watch(yq, (val) => {
}
}
:deep(.el-input__prefix) {
display: none;
.mytable-style {
:deep(.el-input__prefix) {
display: none;
}
}
</style>

View File

@ -250,7 +250,7 @@ const getDeptTree = async (): Promise<void> => {
onMounted(() => {
getDictData('MD', 'TK')
getDictData('MD', 'LOT')
})
// ========== 初始化 ==========
getDeptTree()

View File

@ -0,0 +1,172 @@
/**
* @file dateTime.vue 日期录入
* @author: w
* @since: 2026-01-13
*/
<template>
<div class="app-container">
<div class="handle-box mb5">
<el-form>
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="日期:">
<el-date-picker v-model="queryParams.zkrq" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="项目:">
<el-select v-model="queryParams.xmdh" placeholder="请选择" style="width:100%">
<el-option />
</el-select>
</el-form-item>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="Check" @click="handleSave">保存</el-button>
</el-col>
<el-col :span="5">
<span class="tips ml10">颜色说明:</span> <span class="color-red">大于3SD</span> <span
class="color-pink">大于2SD</span>
</el-col>
</el-row>
</el-form>
</div>
<el-tabs v-model="activeName" type="card" class="tabs_box">
<el-tab-pane v-for="tab in tabList" :key="tab.key" :label="tab.label" :name="tab.key" />
</el-tabs>
<div class="table-box">
<CustomVxeTable class="mytable-style" ref="tableRef" :table-data="tableData" :columns="columns"
:scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }" :cell-style="cellStyleHd">
<template #xmdh="{ row }">
{{ row.xmdh }} {{ row.xmmc }}
</template>
<template #cs1="{ row }">
<el-input v-model="row.cs1" class="full-width-input" />
</template>
<template #cs2="{ row }">
<el-input v-model="row.cs2" class="full-width-input" />
</template>
<template #cs3="{ row }">
<el-input v-model="row.cs3" class="full-width-input" />
</template>
<template #cs4="{ row }">
<el-input v-model="row.cs4" class="full-width-input" />
</template>
<template #cs5="{ row }">
<el-input v-model="row.cs5" class="full-width-input" />
</template>
<template #cs6="{ row }">
<el-input v-model="row.cs6" class="full-width-input" />
</template>
<template #cs7="{ row }">
<el-input v-model="row.cs7" class="full-width-input" />
</template>
</CustomVxeTable>
</div>
</div>
</template>
<script setup lang="ts">
import { getqcval } from '@/api/liswork/qualityControl/index'
import dayjs from 'dayjs'
const yq = defineModel<string>()
const activeName = ref('1')
const queryParams = ref({
xmdh: '',
zkrq: ref(dayjs().format('YYYY-MM-DD')),
})
const tabList = [
{ key: '1', label: '质控1' },
{ key: '2', label: '质控2' }
]
const tableData = ref([])
const columns = ref([
{ field: 'xmdh', title: '日期', resizable: true, width: 200 },
{ field: 'cs1', title: '次数1', resizable: true, slotName: 'cs1' },
{ field: 'cs2', title: '次数2', resizable: true, slotName: 'cs2' },
{ field: 'cs3', title: '次数3', resizable: true, slotName: 'cs3' },
{ field: 'cs4', title: '次数4', resizable: true, slotName: 'cs4' },
{ field: 'cs5', title: '次数5', resizable: true, slotName: 'cs5' },
{ field: 'cs6', title: '次数6', resizable: true, slotName: 'cs6' },
{ field: 'cs7', title: '次数7', resizable: true, slotName: 'cs7' },
{ field: 'zkbz', title: '靶数', resizable: true, align: 'center' },
{ field: 'qcusepos', title: 'SD', resizable: true, align: 'center' },
])
const cellStyleHd = ({ row, column }: {
row: any;
column: any;
}) => {
const styleRules: any = {
columnMap: { cs1: 'cs1', cs2: 'cs2', cs3: 'cs3', cs4: 'cs4', cs5: 'cs5', cs6: 'cs6', cs7: 'cs7' },
colorRules: [
{ min: 3, color: '#f00 !important' }, // 大于3:红色
{ min: 2, max: 3, color: '#ffc0c0 !important' } // 2~3:粉色
]
}
const targetField = styleRules.columnMap[column.title];
if (!targetField || row.qcusepos === 0) return {};
const deviation = Math.abs(row.zkbz - row[targetField]) / row.qcusepos;
for (const rule of styleRules.colorRules) {
if (deviation > rule.min && (!rule.max || deviation < rule.max)) {
return { background: rule.color };
}
}
return {};
}
const getXmList = () => {
getqcval({ yq: yq.value, ...queryParams.value }).then((res: any) => {
tableData.value = res.data
})
}
const handleSave = () => { }
watch(yq, (val) => {
if (val) {
// getXmList()
}
}, { immediate: true })
</script>
<style scoped lang="scss">
.table-box {
margin-top: 5px;
height: calc(100% - 145px);
}
.tips {
font-family: SourceHanSansCN, SourceHanSansCN;
font-size: 16px;
color: #409eff;
font-weight: 600;
}
.color-red {
background-color: #f00;
color: #fff;
padding: 2px;
}
.color-pink {
background-color: #ffc0c0;
color: #fff;
padding: 2px;
}
:deep(.el-form-item) {
margin-bottom: 5px;
}
</style>

View File

@ -0,0 +1,161 @@
/**
* @file index.vue 质控结果手工录入
* @author: w
* @since: 2026-01-05
*/
<template>
<div class="app-container">
<div class="handle-box mb5">
<el-form>
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="日期:">
<el-date-picker v-model="zkrq" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="Check" @click="handleSave">保存</el-button>
</el-col>
<el-col :span="5">
<span class="tips ml10">颜色说明:</span> <span class="color-red">大于3SD</span> <span
class="color-pink">大于2SD</span>
</el-col>
</el-row>
</el-form>
</div>
<el-tabs v-model="activeName" type="card" class="tabs_box">
<el-tab-pane v-for="tab in tabList" :key="tab.key" :label="tab.label" :name="tab.key" />
</el-tabs>
<div class="table-box">
<CustomVxeTable class="mytable-style" ref="tableRef" :table-data="tableData" :columns="columns"
:scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }" :cell-style="cellStyleHd">
<template #xmdh="{ row }">
{{ row.xmdh }} {{ row.xmmc }}
</template>
<template #cs1="{ row }">
<el-input v-model="row.cs1" class="full-width-input" />
</template>
<template #cs2="{ row }">
<el-input v-model="row.cs2" class="full-width-input" />
</template>
<template #cs3="{ row }">
<el-input v-model="row.cs3" class="full-width-input" />
</template>
<template #cs4="{ row }">
<el-input v-model="row.cs4" class="full-width-input" />
</template>
<template #cs5="{ row }">
<el-input v-model="row.cs5" class="full-width-input" />
</template>
<template #cs6="{ row }">
<el-input v-model="row.cs6" class="full-width-input" />
</template>
<template #cs7="{ row }">
<el-input v-model="row.cs7" class="full-width-input" />
</template>
</CustomVxeTable>
</div>
</div>
</template>
<script setup lang="ts">
import { getqcval } from '@/api/liswork/qualityControl/index'
import dayjs from 'dayjs'
const yq = defineModel<string>()
const activeName = ref('1')
const zkrq = ref(dayjs().format('YYYY-MM-DD'))
const tabList = [
{ key: '1', label: '质控1' },
{ key: '2', label: '质控2' }
]
const tableData = ref([])
const columns = ref([
{ field: 'xmdh', title: '质控项目', resizable: true, slotName: 'xmdh', width: 200 },
{ field: 'cs1', title: '次数1', resizable: true, slotName: 'cs1' },
{ field: 'cs2', title: '次数2', resizable: true, slotName: 'cs2' },
{ field: 'cs3', title: '次数3', resizable: true, slotName: 'cs3' },
{ field: 'cs4', title: '次数4', resizable: true, slotName: 'cs4' },
{ field: 'cs5', title: '次数5', resizable: true, slotName: 'cs5' },
{ field: 'cs6', title: '次数6', resizable: true, slotName: 'cs6' },
{ field: 'cs7', title: '次数7', resizable: true, slotName: 'cs7' },
{ field: 'zkbz', title: '靶数', resizable: true, align: 'center' },
{ field: 'qcusepos', title: 'SD', resizable: true, align: 'center' },
])
const cellStyleHd = ({ row, column }: {
row: any;
column: any;
}) => {
const styleRules: any = {
columnMap: { cs1: 'cs1', cs2: 'cs2', cs3: 'cs3', cs4: 'cs4', cs5: 'cs5', cs6: 'cs6', cs7: 'cs7' },
colorRules: [
{ min: 3, color: '#f00 !important' }, // 大于3:红色
{ min: 2, max: 3, color: '#ffc0c0 !important' } // 2~3:粉色
]
}
const targetField = styleRules.columnMap[column.title];
if (!targetField || row.qcusepos === 0) return {};
const deviation = Math.abs(row.zkbz - row[targetField]) / row.qcusepos;
for (const rule of styleRules.colorRules) {
if (deviation > rule.min && (!rule.max || deviation < rule.max)) {
return { background: rule.color };
}
}
return {};
}
const getXmList = () => {
getqcval({ yq: yq.value, zkrq: zkrq.value }).then((res: any) => {
tableData.value = res.data
})
}
const handleSave = () => { }
watch(yq, (val) => {
if (val) {
getXmList()
}
}, { immediate: true })
</script>
<style scoped lang="scss">
.table-box {
margin-top: 5px;
height: calc(100% - 145px);
}
.tips {
font-family: SourceHanSansCN, SourceHanSansCN;
font-size: 16px;
color: #409eff;
font-weight: 600;
}
.color-red {
background-color: #f00;
color: #fff;
padding: 2px;
}
.color-pink {
background-color: #ffc0c0;
color: #fff;
padding: 2px;
}
:deep(.el-form-item) {
margin-bottom: 5px;
}
</style>

View File

@ -1,163 +1,357 @@
/**
* @file index.vue 质控结果手工录入
* @file index.vue 质控
* @author: w
* @since: 2026-01-05
* @since: 2025-12-29
*/
<template>
<div class="app-container">
<div class="handle-box mb5">
<el-form>
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="仪器:">
<YQSelectTable v-model:data="yq" width="200px" clearable @get-data-value="getXmList" />
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="日期:">
<el-date-picker v-model="jyrq" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="5">
<span class="tips ml10">颜色说明:</span> <span class="color-red">大于3SD</span> <span
class="color-pink">大于2SD</span>
</el-col>
<el-row :gutter="20" class="main-row">
<!-- 左侧树区域 -->
<el-col :span="4" :xs="24" class="tree-col">
<el-input v-model="deptName" placeholder="请输入仪器名称" clearable prefix-icon="Search" class="tree-search-input" />
<el-radio-group v-model="radiotype" @change="radioChange">
<el-radio value="1" size="large">仪器组</el-radio>
<el-radio value="2" size="large">所有仪器</el-radio>
</el-radio-group>
<div class="tree-container">
<el-tree :data="deptOptions" :props="treeProps" expand-on-click-node :filter-node-method="filterNode"
ref="deptTreeRef" node-key="id" highlight-current :expanded-keys="expandedKeys"
@node-click="handleNodeClick">
<!-- 自定义树节点模板:新增newflag徽章 -->
<template #default="{ node, data }">
<span class="tree-node-content">
{{ node.label }}
<!-- 新节点徽章:newflag=1时显示黄色"新"标 -->
<el-badge v-if="data.newflag === '1'" value="new" class="new-node-badge" />
</span>
</template>
</el-tree>
</div>
</el-col>
<!-- 右侧列表区域 -->
<el-col :span="20" :xs="24" class="table-col">
<el-row :gutter="10" class="yq-title-row">
<h1 class="yq-title">仪器: {{ yqmc }}</h1>
</el-row>
</el-form>
</div>
<div class="mb5">
<el-button type="primary" plain icon="Check" @click="handleSave">保存</el-button>
</div>
<el-tabs v-model="activeName" type="card" class="tabs_box">
<el-tab-pane v-for="tab in tabList" :key="tab.key" :label="tab.label" :name="tab.key" />
</el-tabs>
<div class="table-box">
<CustomVxeTable class="mytable-style" ref="tableRef" :table-data="tableData" :columns="columns"
:scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }" :cell-style="cellStyleHd">
<template #xmdh="{ row }">
{{ row.xmdh }} {{ row.xmmc }}
</template>
<template #cs1="{ row }">
<el-input v-model="row.cs1" class="full-width-input" />
</template>
<template #cs2="{ row }">
<el-input v-model="row.cs2" class="full-width-input" />
</template>
<template #cs3="{ row }">
<el-input v-model="row.cs3" class="full-width-input" />
</template>
<template #cs4="{ row }">
<el-input v-model="row.cs4" class="full-width-input" />
</template>
<template #cs5="{ row }">
<el-input v-model="row.cs5" class="full-width-input" />
</template>
<template #cs6="{ row }">
<el-input v-model="row.cs6" class="full-width-input" />
</template>
<template #cs7="{ row }">
<el-input v-model="row.cs7" class="full-width-input" />
</template>
</CustomVxeTable>
</div>
<el-tabs v-model="activeName" type="card" class="tabs_box">
<el-tab-pane v-for="tab in tabList" :key="tab.key" :label="tab.label" :name="tab.key" />
</el-tabs>
<!-- 动态组件渲染 -->
<component :is="currentComponent" :key="activeName" v-model="queryParams.yq" @updateTree="getDeptTree" />
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { queryOldxminfo } from '@/api/liswork/xtwh/xmReqItemVsApi'
import YQSelectTable from '@/components/SelectTable/YQSelectTable/index.vue';
import dayjs from 'dayjs'
import { ref, watch, nextTick, computed } from 'vue'
import { ElMessage, ElTree, TreeNode, ElMessageBox, ElBadge } from 'element-plus'
import { paramList, paramTree, getdef, updateParam } from '@/api/liswork/xtwh/ComOpt'
import { getFirstLetter } from '@/utils/pinyin'
import { formatDict, getDictData, dictData } from '@/hooks';
import { useCommonStore } from '@/store/modules/commonStore';
import Project from './components/project.vue';
import DateTime from './components/dateTime.vue';
const mbStore = useCommonStore();
// ========== TypeScript 类型定义 ==========
interface InstrTreeNode {
id: string
label: string
lx: string
newflag: string // 新增newflag字段
children?: InstrTreeNode[]
}
const yq = ref(mbStore.defaultConfig.defaultinstr || '46')
const activeName = ref('1')
const jyrq = ref(dayjs().format('YYYY-MM-DD'))
const tabList = [
{ key: '1', label: '质控1' },
{ key: '2', label: '质控2' }
]
interface ParamTreeResponse {
code: string
msg: string
data: InstrTreeNode[]
url: null | string
method: null | string
params: null | any
problemId: null | string
}
const tableData = ref([])
const columns = ref([
{ field: 'xmdh', title: '质控项目', resizable: true, slotName: 'xmdh', width: 200 },
{ field: 'cs1', title: '次数1', resizable: true, slotName: 'cs1' },
{ field: 'cs2', title: '次数2', resizable: true, slotName: 'cs2' },
{ field: 'cs3', title: '次数3', resizable: true, slotName: 'cs3' },
{ field: 'cs4', title: '次数4', resizable: true, slotName: 'cs4' },
{ field: 'cs5', title: '次数5', resizable: true, slotName: 'cs5' },
{ field: 'cs6', title: '次数6', resizable: true, slotName: 'cs6' },
{ field: 'cs7', title: '次数7', resizable: true, slotName: 'cs7' },
{ field: 'zkbz', title: '靶数', resizable: true, align: 'center' },
{ field: 'qcusepos', title: 'SD', resizable: true, align: 'center' },
])
const cellStyleHd = ({ row, column }: {
row: any;
column: any;
}) => {
const styleRules: any = {
columnMap: { cs1: 'cs1', cs2: 'cs2', cs3: 'cs3', cs4: 'cs4', cs5: 'cs5', cs6: 'cs6', cs7: 'cs7' },
colorRules: [
{ min: 3, color: '#f00 !important' }, // 大于3:红色
{ min: 2, max: 3, color: '#ffc0c0 !important' } // 2~3:粉色
]
}
const targetField = styleRules.columnMap[column.title];
if (!targetField || row.qcusepos === 0) return {};
const deviation = Math.abs(row.zkbz - row[targetField]) / row.qcusepos;
for (const rule of styleRules.colorRules) {
if (deviation > rule.min && (!rule.max || deviation < rule.max)) {
return { background: rule.color };
}
}
return {};
interface QueryParams {
yq?: string | undefined
xxdh?: string | undefined
xxqz?: string | undefined
groupId?: string | undefined
}
const getXmList = () => {
queryOldxminfo({ yq: yq.value, }).then((res: any) => {
tableData.value = res.data
const radiotype = ref<string>("1")
// ========== 响应式变量 ==========
// 树相关
const deptTreeRef = ref<InstanceType<typeof ElTree>>()
const deptOptions = ref<InstrTreeNode[]>([])
const deptName = ref<string>("")
const yqmc = ref<string>("")
const expandedKeys = ref<string[]>([])
const isFiltering = ref<boolean>(false)
const treeProps = ref({
label: 'label',
children: 'children',
isLeaf: (data: Record<string, any>) => !data.children || data.children.length === 0
})
// 查询参数
const queryParams = ref<QueryParams>({
yq: undefined,
xxdh: undefined,
xxqz: undefined,
groupId: undefined
})
const tabList = [
{ key: 'Project', label: '按项目', component: Project },
{ key: 'DateTime', label: '按日期', component: DateTime },
]
const activeName = ref('Project')
// 计算当前要渲染的组件
const currentComponent = computed(() => {
return tabList.find(tab => tab.key === activeName.value)?.component || 'Project'
})
const result: any = ref([])
const treeData = ref<InstrTreeNode[]>([])
const radioChange = (val: string) => {
if (val == "1") {
deptOptions.value = treeData.value
} else {
deptOptions.value = result.value
}
}
// ========== 树操作核心方法 ==========
const forceExpandSingleNode = (nodeId: string): void => {
expandedKeys.value = []
nextTick(() => {
expandedKeys.value = [nodeId]
// deptTreeRef.value?.expand(nodeId, false)
})
}
const handleSave = () => { }
const handleNodeClick = (data: InstrTreeNode): void => {
if (isFiltering.value) {
queryParams.value.groupId = data.id === "-1" ? undefined : data.id
return
}
getXmList()
if (data.children && data.children.length > 0) {
const isExpanded = expandedKeys.value.includes(data.id)
expandedKeys.value = isExpanded ? [] : [data.id]
!isExpanded && forceExpandSingleNode(data.id)
} else {
queryParams.value.yq = data.id
yqmc.value = data.label
}
}
// ========== 检索过滤逻辑 ==========
const filterNode = (value: string, data: any, _node: TreeNode): boolean => {
if (!value) return true
const keyword = value.trim().toUpperCase()
const nodeLabel = data.label?.toLowerCase() || ''
const nodePinyinFirst = data.label ? getFirstLetter(data.label).toUpperCase() : ''
const currentMatch = nodeLabel.includes(keyword.toLowerCase()) || nodePinyinFirst.includes(keyword)
const childrenMatch = (data.children || []).some((child: any) => filterNode(value, child, _node))
return currentMatch || childrenMatch
}
const debounceFilter = (() => {
let timer: number | undefined
return (val: string): void => {
clearTimeout(timer)
timer = setTimeout(() => {
isFiltering.value = true
deptTreeRef.value?.filter(val)
if (val) {
expandedKeys.value = []
const expandMatched = (nodes: InstrTreeNode[]) => {
nodes.forEach(node => {
if (filterNode(val, node, {} as TreeNode) && node.children?.length) {
expandedKeys.value.push(node.id)
}
node.children && expandMatched(node.children)
})
}
expandMatched(deptOptions.value)
} else {
expandedKeys.value = []
}
isFiltering.value = false
}, 300)
}
})()
watch(deptName, debounceFilter, { immediate: false })
// ========== 业务逻辑 ==========
const getDeptTree = async (): Promise<void> => {
try {
const response = await paramTree() as ParamTreeResponse
result.value = []
if (response.code === "0" && response.data) {
for (let i = 0; i < response.data.length; i++) {
const group = response.data[i];
if (group.children && Array.isArray(group.children)) {
for (let j = 0; j < group.children.length; j++) {
result.value.push(group.children[j]);
}
}
}
treeData.value = response.data
radioChange('1')
if (!expandedKeys.value.length) {
// 默认展开第一个一级节点的子级
nextTick(() => {
if (mbStore.defaultConfig.defaultinstr) {
queryParams.value.yq = mbStore.defaultConfig.defaultinstr
yqmc.value = result.value.find((item: any) => item.id === queryParams.value.yq)?.label
deptTreeRef.value?.setCurrentKey(queryParams.value.yq)
} else {
if (deptOptions.value.length > 0) {
const firstLevelNode = deptOptions.value[0]
if (firstLevelNode && firstLevelNode.children && firstLevelNode.children.length > 0) {
expandedKeys.value = [firstLevelNode.id]
const firstChildNode = firstLevelNode.children[0]
queryParams.value.yq = firstChildNode.id
yqmc.value = firstChildNode.label
deptTreeRef.value?.setCurrentKey(queryParams.value.yq)
}
}
}
})
}
} else {
ElMessage.warning(response.msg || "获取树数据失败")
}
} catch (error) {
ElMessage.error(`获取树数据异常:${(error as Error).message}`)
}
}
onMounted(() => {
})
// ========== 初始化 ==========
getDeptTree()
</script>
<style scoped lang="scss">
.table-box {
margin-top: 5px;
height: calc(100% - 125px);
<style lang="scss" scoped>
.app-container {
width: 100%;
height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 8px;
box-sizing: border-box;
}
.tips {
font-family: SourceHanSansCN, SourceHanSansCN;
font-size: 16px;
color: #409eff;
font-weight: 600;
.main-row {
display: flex;
flex: 1;
height: 100%;
overflow: hidden;
}
.color-red {
background-color: #f00;
color: #fff;
padding: 2px;
.tree-col {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-container {
flex: 1;
height: 100%;
overflow: auto;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 4px;
padding: 8px;
// 核心:树选中节点样式
:deep(.el-tree) {
--el-tree-node-transition-duration: 0.2s;
.el-tree-node__children {
transition: all var(--el-tree-node-transition-duration) ease-in-out;
}
.el-tree-node__expand-icon.is-leaf {
display: none;
}
// 选中节点的背景色(蓝色)+ 文字白色
.el-tree-node.is-current>.el-tree-node__content {
background-color: #409eff !important;
color: #ffffff !important;
}
// 选中节点hover样式(加深蓝色)
.el-tree-node.is-current>.el-tree-node__content:hover {
background-color: #1e88e5 !important;
}
// 树节点内容容器样式
.tree-node-content {
display: inline-flex;
align-items: center;
gap: 4px; // 徽章和文字间距
height: 100%;
}
// 新节点徽章样式
.new-node-badge {
--el-badge-font-size: 10px !important;
--el-badge-height: 16px !important;
--el-badge-padding: 0 4px !important;
--el-badge-warning-color: #FFEC24 !important; // 黄色徽章
--el-badge-warning-bg-color: #fff7e6 !important; // 浅黄色背景
--el-badge-border-radius: 2px !important; // 圆角优化
// 徽章文字样式
.el-badge__content {
font-size: 10px !important;
line-height: 16px !important;
padding: 0 4px !important;
height: 16px !important;
}
}
}
}
}
.color-pink {
background-color: #ffc0c0;
color: #fff;
padding: 2px;
}
.table-col {
height: 100%;
:deep(.el-form-item) {
margin-bottom: 5px;
// 仪器标题行样式
.yq-title-row {
margin-bottom: 8px;
.yq-title {
font-family: SourceHanSansCN, SourceHanSansCN;
font-size: 16px; // 缩小h1字体(默认24px→16px)
color: #409eff; // 蓝色(Element Plus主题色)
font-weight: 600;
margin: 0; // 清除默认margin
line-height: 1.2;
}
}
}
</style>