增加折点维护字典

This commit is contained in:
tangw 2026-09-16 18:14:31 +08:00
parent 409d625a95
commit 0daeb17792
6 changed files with 695 additions and 44 deletions

View File

@ -0,0 +1,55 @@
// 抗生素折点范围字典 API
import request from '@/utils/request'
// 获取菌属列表(从 xm_meddetail 表)
export function getGermClassList(params?: any) {
return request({
url: '/xm/meddetail/germclass/list',
method: 'get',
params
})
}
// 获取抗生素字典列表(从 xm_med 表)
export function getAntibioticList(params?: any) {
return request({
url: '/xm/med/list',
method: 'get',
params
})
}
// 获取折点范围数据
export function getBreakpointList(params: any) {
return request({
url: '/xm/breakpoint/list',
method: 'get',
params
})
}
// 保存折点范围
export function saveBreakpoint(data: any) {
return request({
url: '/xm/breakpoint',
method: 'post',
data
})
}
// 批量保存折点范围
export function batchSaveBreakpoint(data: any[]) {
return request({
url: '/xm/breakpoint/batch',
method: 'post',
data
})
}
// 删除折点范围
export function deleteBreakpoint(id: number) {
return request({
url: `/xm/breakpoint/${id}`,
method: 'delete'
})
}

View File

@ -222,6 +222,28 @@ export const dynamicRoutes = [{
}
}
]
},
// ========== 抗生素折点范围字典 ==========
{
path: '/liswork/xtwh/antibioticBreakpoint',
component: Layout,
hidden: false,
permissions: ['liswork:xtwh:antibioticBreakpoint:list'],
meta: {
title: '抗生素折点范围字典',
icon: 'el-icon-menu'
},
children: [
{
path: '',
name: 'AntibioticBreakpoint',
component: () => import('@/views/liswork/xtwh/antibioticBreakpoint/index'),
meta: {
title: '抗生素折点范围字典',
noCache: true
}
}
]
}
]

View File

@ -101,34 +101,38 @@ function base64ToBlob(base64: string, mimeType: string) {
* @param type 可选,'pdf' | 'excel',不传默认 pdf
* @param fileName 可选,excel下载文件名
*/
function printBase64PDF(base64: string, type: 'pdf' | 'excel' = 'pdf', fileName: string = '文件') {
if (type === 'pdf') {
// PDF打印逻辑
const blob = base64ToBlob(base64, 'application/pdf');
const pdfUrl = URL.createObjectURL(blob);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = pdfUrl;
document.body.appendChild(iframe);
iframe.onload = () => {
iframe.contentWindow?.print();
setTimeout(() => {
URL.revokeObjectURL(pdfUrl);
iframe.remove(); // 修复原代码iframe残留dom
}, 1000);
};
} else if (type === 'excel') {
// excel导出下载逻辑
const blob = base64ToBlob(base64, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = encodeURIComponent(fileName + '.xlsx');
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(downloadUrl);
}
function printBase64PDF(base64: string) {
// PDF打印逻辑
const blob = base64ToBlob(base64, 'application/pdf');
const pdfUrl = URL.createObjectURL(blob);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = pdfUrl;
document.body.appendChild(iframe);
iframe.onload = () => {
iframe.contentWindow?.print();
setTimeout(() => {
URL.revokeObjectURL(pdfUrl);
iframe.remove(); // 修复原代码iframe残留dom
}, 1000);
};
}
/**
* 后端返回Blob文件流
* @param pdfBlob 后端接口返回的文件流blob,responseType:blob
*/
function printBlobPDF(pdfBlob: Blob, fileName: string) {
const pdfUrl = URL.createObjectURL(pdfBlob);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = pdfUrl;
document.body.appendChild(iframe);
iframe.onload = () => {
iframe.contentWindow?.print();
setTimeout(() => URL.revokeObjectURL(pdfUrl), 1000); // 释放内存
};
}
@ -209,5 +213,35 @@ function calcMeanWithSymbols(arr) {
// 保留2位小数返回
return Math.round(avg * 100) / 100
}
/**
* 导出文件(Excel下载)
* @param blob 文件二进制blob
* @param fileName 文件名
*/
function exportFile(blob: Blob, fileName: string) {
const a = document.createElement('a')
const url = URL.createObjectURL(blob)
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
// 释放内存
URL.revokeObjectURL(url)
a.remove()
}
export const classCom = { decimalToHexColor, convertHexToNumber, getContrastTextColor, printBase64PDF, printPDF, useAutoSize, getDayDiff, calcMeanWithSymbols }
export const classCom = {
decimalToHexColor,
convertHexToNumber,
getContrastTextColor,
printBase64PDF,
printPDF,
useAutoSize,
getDayDiff,
calcMeanWithSymbols,
exportFile,
printBlobPDF
}

View File

@ -77,7 +77,6 @@
<script setup lang="ts">
import SelectTable from '@/components/SelectTable/index.vue';
import { getDictData, formatDict, dictData } from '@/hooks'
import { E } from 'vue-router/dist/router-CWoNjPRp.mjs';
const props = defineProps({
labPat: {

View File

@ -0,0 +1,549 @@
/**
* @file index.vue 抗生素折点范围字典
* @author: w
* @since: 2026-09-16
*/
<template>
<div class="app-container">
<div class="unified-panel">
<div class="btn-bar">
<el-button type="primary" icon="Search" @click="search">查询</el-button>
<el-button type="primary" icon="Check" @click="saveBreakpoints">保存</el-button>
<el-button type="warning" icon="Refresh" @click="reset">重置</el-button>
</div>
</div>
<div class="content-container">
<!-- 左栏:菌属树 -->
<div class="panel left-panel">
<div class="panel-header">菌属</div>
<div class="panel-content">
<el-input v-model="treeFilter" placeholder="搜索菌属" size="small" clearable style="padding: 4px;" />
<el-tree
ref="treeRef"
:data="germClassTree"
:props="{ label: 'label', children: 'children' }"
node-key="zddh"
highlight-current
default-expand-all
:filter-node-method="filterNode"
@node-click="handleTreeNodeClick"
>
<template #default="{ node, data }">
<span class="tree-node">
<span class="tree-code">{{ data.zddh }}</span>
<span class="tree-name">{{ data.zdmc }}</span>
</span>
</template>
</el-tree>
</div>
</div>
<!-- 中栏:折点范围编辑 -->
<div class="panel middle-panel">
<div class="panel-header">折点范围编辑</div>
<div class="panel-content">
<el-table
:data="breakpointList"
border
size="mini"
height="100%"
style="width: 100%"
>
<el-table-column prop="ywdh" label="药物代号" width="90" fixed />
<el-table-column label="抗生素" width="130" fixed show-overflow-tooltip>
<template #default="scope">{{ getMedName(scope.row.ywdh) }}</template>
</el-table-column>
<el-table-column label="S(MIC<=)" width="75">
<template #default="scope">
<el-input v-model="scope.row.mic1" size="small" />
</template>
</el-table-column>
<el-table-column label="R(MIC>=)" width="75">
<template #default="scope">
<el-input v-model="scope.row.mic2" size="small" />
</template>
</el-table-column>
<el-table-column prop="micref" label="MIC折点" width="130" show-overflow-tooltip />
<el-table-column label="S(KB>=)" width="75">
<template #default="scope">
<el-input v-model="scope.row.rad1" size="small" />
</template>
</el-table-column>
<el-table-column label="R(KB<=)" width="75">
<template #default="scope">
<el-input v-model="scope.row.rad2" size="small" />
</template>
</el-table-column>
<el-table-column prop="radref" label="KB折点" width="130" show-overflow-tooltip />
<el-table-column label="I=SDD" width="45" align="center">
<template #default="scope">
<el-checkbox v-model="scope.row.sddfalg" :true-value="1" :false-value="null" class="sdd-checkbox" />
</template>
</el-table-column>
<el-table-column prop="xh" label="序号" width="55" />
<el-table-column label="操作" width="65" fixed="right">
<template #default="scope">
<el-button type="danger" link size="small" @click="removeBreakpoint(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 右栏:抗生素字典 -->
<div class="panel right-panel">
<div class="panel-header">抗生素字典</div>
<div class="panel-content right-content">
<el-input v-model="antibioticFilter" placeholder="搜索抗生素" size="small" clearable style="padding: 4px;" />
<el-table
:data="filteredAntibioticList"
border
size="mini"
height="100%"
style="width: 100%"
@row-dblclick="handleAddBreakpoint"
>
<el-table-column prop="ywdh" label="代号" width="85" show-overflow-tooltip />
<el-table-column prop="ywmc" label="药物名称" width="140" show-overflow-tooltip />
<el-table-column prop="zjf" label="简码" width="70" show-overflow-tooltip />
<el-table-column prop="yydh" label="英文代码" width="130" show-overflow-tooltip />
<el-table-column prop="zjmic" label="中介MIC" width="90" show-overflow-tooltip />
<el-table-column prop="nymic" label="尿MIC" width="85" show-overflow-tooltip />
<el-table-column prop="multimic" label="多级参数" width="75" show-overflow-tooltip />
<el-table-column prop="yf" label="用法" width="80" show-overflow-tooltip />
<el-table-column prop="xyfz" label="血药浓度" width="80" show-overflow-tooltip />
<el-table-column prop="seq" label="序号" width="55" show-overflow-tooltip />
</el-table>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
getGermClassList,
getAntibioticList,
getBreakpointList,
batchSaveBreakpoint
} from '@/api/liswork/xtwh/AntibioticBreakpointApi'
const treeRef = ref(null)
const treeFilter = ref('')
const antibioticFilter = ref('')
const germClassList = ref([])
const antibioticList = ref([])
const breakpointList = ref([])
const currentGermClass = ref(null)
const loading = ref(false)
let rowWatchers = []
const germClassTree = computed(() => {
return germClassList.value.map(item => ({
...item,
label: `${item.zddh} ${item.zdmc}`
}))
})
const getMedName = (ywdh) => {
return antibioticList.value.find(m => m.ywdh === ywdh)?.ywmc || ''
}
const filteredAntibioticList = computed(() => {
const kw = antibioticFilter.value.trim().toLowerCase()
if (!kw) return antibioticList.value
return antibioticList.value.filter(m =>
m.ywmc?.toLowerCase().includes(kw) ||
m.ywdh?.toLowerCase().includes(kw) ||
m.zjf?.toLowerCase().includes(kw) ||
m.yydh?.toLowerCase().includes(kw)
)
})
const calcMicRef = (mic1, mic2) => {
const s = mic1 != null && mic1 !== '' ? Number(mic1) : null
const r = mic2 != null && mic2 !== '' ? Number(mic2) : null
if (s == null && r == null) return ''
if (s != null && r != null) {
const mid = buildMicMiddle(s, r)
return `<=${s}${mid ? ', ' + mid : ''}, >=${r}`
}
return s != null ? `<=${s}` : `>=${r}`
}
const calcRadRef = (rad1, rad2) => {
const s = rad1 != null && rad1 !== '' ? Number(rad1) : null
const r = rad2 != null && rad2 !== '' ? Number(rad2) : null
if (s == null && r == null) return ''
if (s != null && r != null) {
const mid = buildRadMiddle(s, r)
return `>=${r}${mid ? ', ' + mid : ''}, <=${s}`
}
return r != null ? `>=${r}` : `<=${s}`
}
const buildMicMiddle = (s, r) => {
if (r - s <= 1) return ''
if (Number.isInteger(s) && Number.isInteger(r)) {
return `${s + 1}-${r - 1}`
}
const vals = []
let v = s + 1
while (v < r) {
vals.push(Number.isInteger(v) ? v.toString() : v.toFixed(1))
v = Math.round((v + 1) * 10) / 10
}
return vals.join(',')
}
const buildRadMiddle = (s, r) => {
if (r - s <= 1) return ''
return `${s + 1}-${r - 1}`
}
const setupBreakpointWatchers = (row) => {
return [
watch(() => row.mic1, val => { row.micref = calcMicRef(val, row.mic2) }),
watch(() => row.mic2, val => { row.micref = calcMicRef(row.mic1, val) }),
watch(() => row.rad1, val => { row.radref = calcRadRef(val, row.rad2) }),
watch(() => row.rad2, val => { row.radref = calcRadRef(row.rad1, val) })
]
}
watch(treeFilter, val => {
treeRef.value?.filter(val)
})
const filterNode = (value, data) => {
if (!value) return true
return data.zdmc.includes(value) || data.zddh.toLowerCase().includes(value.toLowerCase())
}
const loadGermClassList = () => {
getGermClassList().then(res => {
germClassList.value = res.data || []
if (germClassList.value.length > 0) {
currentGermClass.value = germClassList.value[0]
loadBreakpoints()
}
})
}
const loadAntibioticList = () => {
getAntibioticList().then(res => {
antibioticList.value = res.data || []
})
}
const loadBreakpoints = () => {
if (!currentGermClass.value) return
loading.value = true
getBreakpointList({ germClassCode: currentGermClass.value.zddh }).then(res => {
rowWatchers.forEach(stop => stop())
rowWatchers = []
breakpointList.value = res.data || []
breakpointList.value.forEach(row => {
rowWatchers.push(...setupBreakpointWatchers(row))
})
}).finally(() => {
loading.value = false
})
}
const handleTreeNodeClick = (data) => {
currentGermClass.value = data
loadBreakpoints()
}
const handleAddBreakpoint = (row) => {
if (!currentGermClass.value) {
ElMessage.warning('请先选择菌属')
return
}
const exists = breakpointList.value.some(item => item.ywdh === row.ywdh)
if (exists) {
ElMessage.warning(`${row.ywmc} 已在折点列表中`)
return
}
const maxXh = breakpointList.value.reduce((max, item) => Math.max(max, item.xh || 0), 0)
const newRow = {
ywdh: row.ywdh,
mic1: null,
mic2: null,
rad1: null,
rad2: null,
micref: '',
radref: '',
xh: maxXh + 10,
sddfalg: null,
bz: null
}
breakpointList.value.push(newRow)
rowWatchers.push(...setupBreakpointWatchers(newRow))
}
const removeBreakpoint = (index) => {
breakpointList.value.splice(index, 1)
}
const search = () => {
loadGermClassList()
loadAntibioticList()
if (currentGermClass.value) {
loadBreakpoints()
}
}
const saveBreakpoints = () => {
if (!currentGermClass.value) {
ElMessage.warning('请先选择菌属')
return
}
if (breakpointList.value.length === 0) {
ElMessage.warning('没有可保存的数据')
return
}
const data = breakpointList.value.map(item => ({
yq: '_med_',
xmdh: currentGermClass.value.zddh,
ywzmc: '菌属',
ywdh: item.ywdh,
mic1: item.mic1 != null ? Number(item.mic1) : null,
mic2: item.mic2 != null ? Number(item.mic2) : null,
rad1: item.rad1 != null ? Number(item.rad1) : null,
rad2: item.rad2 != null ? Number(item.rad2) : null,
xh: item.xh,
micref: item.micref,
radref: item.radref,
sddfalg: item.sddfalg,
bz: item.bz
}))
batchSaveBreakpoint(data).then(() => {
ElMessage.success('保存成功')
loadBreakpoints()
}).catch(() => {
ElMessage.error('保存失败')
})
}
const reset = () => {
if (currentGermClass.value) {
loadBreakpoints()
}
}
onMounted(() => {
search()
})
</script>
<style scoped lang="scss">
.app-container {
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #d5d5d5;
}
.unified-panel {
background: #e6f7fa;
border-right: 1px solid #d9e8fb;
border-bottom: 1px solid #d9e8fb;
border-left: 1px solid #d9e8fb;
border-radius: 0 0 8px 8px;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
margin-top: -10px;
}
.btn-bar {
padding: 8px 12px 8px 12px;
}
.content-container {
display: flex;
flex: 1;
gap: 5px;
padding: 10px;
overflow: hidden;
min-height: 0;
}
.panel {
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid #d9e8fb;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.panel-header {
padding: 8px 12px;
background: #f0f8ff;
border-bottom: 1px solid #d9e8fb;
font-size: 14px;
font-weight: 600;
color: #3282F6;
font-family: SimSun, "宋体", serif;
}
.panel-content {
flex: 1;
overflow: hidden;
min-height: 0;
}
.left-panel {
width: 220px;
flex-shrink: 0;
}
.middle-panel {
flex: 1;
min-width: 400px;
}
.right-panel {
width: 560px;
flex-shrink: 0;
}
.right-content {
display: flex;
flex-direction: column;
:deep(.el-table) {
flex: 1;
min-height: 0;
}
}
// 表格样式
:deep(.el-table) {
--el-table-row-height: 20px;
--el-table-header-row-height: 26px;
}
:deep(.el-table__header-wrapper th) {
height: 26px !important;
min-height: 26px !important;
max-height: 26px !important;
line-height: 26px !important;
padding: 0 4px !important;
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
font-family: SimSun, "宋体", serif;
}
:deep(.el-table__body-wrapper td) {
height: 20px !important;
min-height: 20px !important;
max-height: 20px !important;
line-height: 20px !important;
padding: 0 !important;
font-family: SimSun, "宋体", serif;
}
:deep(.el-table__cell) {
font-family: SimSun, "宋体", serif;
}
:deep(.el-table__cell .cell) {
display: flex;
align-items: center;
height: 100%;
padding: 0 2px;
line-height: 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.el-table__body-wrapper .el-input) {
width: 100%;
height: 100%;
}
:deep(.el-table__body-wrapper .el-input__wrapper) {
height: 100%;
box-shadow: none;
border-radius: 0;
margin: 0;
padding: 0 4px;
}
:deep(.el-table__body-wrapper .el-input__inner) {
height: 100%;
line-height: 20px;
padding: 0;
font-size: 12px;
}
.tree-node {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-family: SimSun, "宋体", serif;
}
.tree-code {
color: #3282F6;
font-weight: 600;
min-width: 36px;
}
.tree-name {
color: #333;
}
:deep(.el-tree) {
font-size: 12px;
}
:deep(.el-tree-node__content) {
height: 26px;
padding-left: 4px !important;
}
:deep(.el-tree-node__content:hover) {
background: #ecf5ff;
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #409eff;
color: #fff;
font-weight: 600;
}
:deep(.el-tree-node.is-current > .el-tree-node__content .tree-code) {
color: #fff;
}
:deep(.el-tree-node.is-current > .el-tree-node__content .tree-name) {
color: #fff;
}
:deep(.sdd-checkbox) {
margin: 0 auto;
display: flex;
justify-content: center;
}
:deep(.sdd-checkbox .el-checkbox__input) {
transform: scale(1.15);
}
.right-panel :deep(.el-table__row) {
cursor: pointer;
}
</style>

View File

@ -130,7 +130,8 @@
<el-input v-model="form.paramName" :placeholder="`请输入${menuname}名称`" />
</el-form-item>
<el-form-item :label="`${menuname}默认值`" prop="paramDefvalue">
<el-select v-model="form.paramDefvalue" :placeholder="`请选择${menuname}默认值`" style="width: 100%" clearable>
<el-select v-model="form.paramDefvalue" :placeholder="`请选择${menuname}默认值`" style="width: 100%" clearable
filterable allow-create default-first-option>
<el-option v-for="item in defaultOptions" :key="item.value" :label="item.label + ' ' + item.value"
:value="item.value" />
</el-select>
@ -233,9 +234,8 @@ import {
updatestatsparam
} from "@/api/liswork/xtwh/StatsparameterApi.ts";
import { liststatstemp, copyParameter } from "@/api/liswork/xtwh/StatstemplateApi.ts";
import { copy } from "clipboard";
const route = useRoute();
// ========== 基础变量声明(避免提前访问) ==========
// 菜单名称
const menuname = ref("参数");
const statsCode = ref("1");
@ -349,7 +349,6 @@ const multiple = ref(true); // 是否多选
const total = ref(0); // 总条数
const title = ref(""); // 对话框标题
// ========== 工具方法 ==========
// 获取类别颜色
function getparamTypeColor(type) {
const item = paramTypeOptions.value.find(item => item.value === type);
@ -380,7 +379,6 @@ function resetForm() {
}
}
// ========== 核心业务方法 ==========
// 查询列表
function getList() {
loading.value = true;
@ -389,18 +387,12 @@ function getList() {
.then(response => {
regdictList.value = response?.rows || [];
total.value = response?.total || 0;
})
.catch(() => {
regdictList.value = [];
total.value = 0;
proxy && proxy.$modal.msgError("数据加载失败");
})
.finally(() => {
}).finally(() => {
loading.value = false;
});
liststatstemp().then(response => {
//获取模板数据
liststatstemp({ pageSize: 100, pageNum: 1 }).then(response => {
copyTemplateOptions.value = response?.rows || [];
})
}