2026-03-19 18:00:24 +08:00

657 lines
21 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.

/**
* @file index.vue 科内开单
* @author: w
* @since: 2026-03-09
*/
<template>
<div class="app-container">
<el-row :gutter="10" class="row-box">
<el-col :span="16" style="height: 100%;">
<div class="box-shadow">
<el-row class="top-box">
<el-col :span="7">
<el-button type="primary" size="small" :loading="saveLoading" @click="saveHandle">保存</el-button>
<el-button type="primary" size="small" @click="addHandle">新增</el-button>
<el-button type="primary" size="small" @click="printHandle">打印</el-button>
<el-button type="danger" size="small" @click="deleteHandle">作废</el-button>
</el-col>
<el-col :span="17">
<div class="tips">
项目检索: <el-input v-model="searchKey" size="small" style="width: 150px;" @clear="filterDictData"
@input="filterDictData" /> &nbsp;
</div>
</el-col>
</el-row>
<el-row :gutter="5" style="height: 100%;">
<el-col :span="7">
<Info v-model:labPat="labpat" @getBrdh="getBrdhHandle" />
</el-col>
<el-col :span="17" style="height: 100%;">
<el-tabs v-model="activeName" type="card" class="tabs_box" @tab-click="handleTabClick">
<el-tab-pane v-for="item in xmdlList" :key="item.dictCode" :label="item.dictName" :name="item.dictCode">
</el-tab-pane>
</el-tabs>
<div style="height: calc(60% - 1.875rem)" class="mb5">
<CustomVxeTable :table-data="filterData" :columns="xmColumns" @cell-dblclick="dbRowclick"
:cell-style="xmcellStyle" keyField='applyid'
:scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }" class="mytable-style">
<template #itemclass="{ row }">
{{itemclassList.find((item) => item.itemclassCode == row.itemclass)?.itemclassName}}
</template>
</CustomVxeTable>
</div>
<div style="height: calc(40% - 30px)">
<CustomVxeTable :table-data="sqxmCzList" :columns="sqXmColumns" keyField='uid' :cell-style="cellStyle"
:config="tableConfig" :scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }" ref="xmRef"
@selection-change="selectionChange" class="mytable-style">
<template #newitemclass="{ row }">
{{itemclassList.find((item) => item.itemclassCode == row.newitemclass)?.itemclassName}}
</template>
<template #newbarcode="{ row }">
条码号({{ barcodes }}条)
</template>
<template #itemclass_yblx="{ row }">
<SelectTable v-model:data="row.itemclass_yblx" :tableData="dictData.BT" placeholder="请选择"
:clearable="false" v-if="labpat.status < 20 || !labpat.status" value="label"
@getDataValue="(data) => { getyblxData(data, row) }" class="full-width-input" />
<span v-else> {{ formatDict(row.itemclass_yblx, 'BT') }}</span>
</template>
<template #status="{ row }">
<span v-if="row.rowSeq == 1"> {{ formatDict(labpat.status, 'ZT') }}</span>
</template>
<template #action="{ row }" v-if="labpat.status < 20 || !labpat.status">
<el-button type="danger" :text='true' size="small" @click="delItem(row)">删除</el-button>
</template>
</CustomVxeTable>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="8" style="height: 100%;">
<div class="box-shadow">
<el-form :model="searchForm" inline size="small">
<el-form-item label="" prop="brxm">
<el-input v-model="searchForm.brxm" clearable style="width: 9.5rem;" @change="handleQuery"
placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="" prop="status">
<SelectTable v-model:data="searchForm.status" :tableData="dictData.ZT" clearable style="width: 9.5rem;"
placeholder="请选择状态" size="small" @getDataValue="handleQuery" />
</el-form-item>
<el-form-item label="" prop="begdate">
<div class="radio_box">
<el-radio-group v-model="searchForm.days" @change="changeDays">
<el-radio :value="3">近3天</el-radio>
<el-radio :value="7">近7天</el-radio>
<el-radio :value="0">此日
<el-icon>
<Right />
</el-icon>
</el-radio>
</el-radio-group>
<el-date-picker v-model="today" type="date" value-format="YYYY-MM-DD" style="width: 9rem;"
@change="dateHandle" /> &nbsp;
<!-- <el-tooltip class="box-item" effect="dark" content="打印" placement="top">
<el-button type="primary" circle size="small" icon="Memo" @click="printHandle"></el-button>
</el-tooltip>
<el-tooltip class="box-item" effect="dark" content="作废" placement="top">
<el-button type="danger" circle size="small" icon="Delete" @click="deleteHandle"></el-button>
</el-tooltip> -->
</div>
</el-form-item>
</el-form>
<div class="table-box">
<CustomVxeTable :table-data="labPatList" :loading="loading" :columns="tableColumns"
@cell-click="handleRowClick" size="small" keyField='applyid' :cell-style="sqdcellStyle"
:scrollYConfig="{ enabled: true, rSize: 30, height: '100%', }" class="mytable-style" ref="tableRef">
<template #status="{ row }">
{{ formatDict(row.status, 'ZT') }}
</template>
</CustomVxeTable>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup name="Issueorder">
import Info from './components/info.vue'
import { getDictData, formatDict, dictData } from '@/hooks'
import { getXmItemInfo, saveReqInfo, queryRegRequestInfo, queryReqDetail, reqPrint, cancelReq } from '@/api/liswork/order'
import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getFirstLetter } from '@/utils/pinyin';
import { listregdict } from "@/api/regional/dict/regdict.ts";
import { checkPrintService } from '@/utils/printHelper.ts';
import { listitemclass } from "@/api/regional/dict/itemclass.ts";
import { classCom } from '@/utils/classCom';
import { useGroupTableData } from '@/utils/groupHelper'
const { proxy } = getCurrentInstance();
const labpat = ref({})
const saveLoading = ref(false)
const searchKey = ref('')
const loading = ref(false)
const tableRef = useTemplateRef('tableRef')
const labPatList = ref([])
const xmdlList = ref([])
const activeName = ref('')
const today = ref(dayjs().format('YYYY-MM-DD'))
// 记录初始数据(用于对比是否有变化)
const initialLabPat = ref({})
const initialSqxmCzList = ref([])
const searchForm = ref({
brxm: '',
status: '',
days: 3,
begdate: '',
enddate: '',
})
const tableColumns = ref([
{ field: 'inputDate', title: '登记时间', width: 80, align: 'center', resizable: true },
{ field: 'rowSeq', title: 'No', width: 40, align: 'center', resizable: true },
{ field: 'patName', title: '姓名', width: 80, align: 'center', resizable: true, },
{ field: 'status', title: '状态', width: 80, align: 'center', slotName: 'status', resizable: true, },
{ field: 'userName', title: '登记人', width: 80, align: 'center', resizable: true },
{ field: 'reqDetailName', title: '项目', width: 200, align: 'center', resizable: true },
{ field: 'patId', title: '病人代号', align: 'center', width: 80, resizable: true },
])
const sqdcellStyle = ({ row, column }) => {
if (column.title == "状态") {
return {
backgroundColor: getColor(row.status),
// color: ,
}
}
}
const getColor = (type) => {
switch (type) {
case '10':
return '';
case '20':
return ' rgba(238, 8, 8, 0.25)';
case '30':
return 'rgba(0, 128, 0, 0.25)';
case '40':
return 'rgba(255, 165, 0, 0.25)';
case '50':
return 'rgba(255, 165, 0, 0.25)';
default:
return 'rgba(40, 189, 187, 0.25)';
}
}
const deepClone = (data) => {
return JSON.parse(JSON.stringify(data))
}
// 更新初始数据(在新增/加载数据时调用)
const updateInitialData = () => {
initialLabPat.value = deepClone(labpat.value)
initialSqxmCzList.value = deepClone(sqxmCzList.value)
}
// 判断数据是否有变化
const isDataChanged = () => {
const labPatChanged = JSON.stringify(labpat.value) !== JSON.stringify(initialLabPat.value)
const sqxmChanged = JSON.stringify(sqxmCzList.value) !== JSON.stringify(initialSqxmCzList.value)
return labPatChanged || sqxmChanged
}
const rowInfo = ref({})
const handleRowClick = ({ row }) => {
rowInfo.value = row
if (!row?.applyid) return
queryReqDetail({ applyId: row.applyid }).then(res => {
labpat.value = res.data.regApply
sqXmList.value = res.data.regApplyDetailList.map(item => ({
itemcode: item.orderItemCode,
itemname: item.orderItemName,
itemclass_yblx: item.sampleTypeName,
amount: item.amount,
applyid: item.applyid,
uid: item.uid,
classid: item.classid,
barcode: item.barcode,
itemclass: item.itemclass,
itemclass_tips: item.itemclass_tips
}))
// 异步初始化数据
nextTick(() => {
setTimeout(() => {
updateInitialData()
}, 10);
})
})
}
const changeDays = (val) => {
if (val == 3) {
searchForm.value.begdate = dayjs().subtract(3, 'day').format('YYYY-MM-DD') + ' 00:00:00'
searchForm.value.enddate = dayjs().format('YYYY-MM-DD') + ' 23:59:59'
} else if (val == 7) {
searchForm.value.begdate = dayjs().subtract(7, 'day').format('YYYY-MM-DD') + ' 00:00:00'
searchForm.value.enddate = dayjs().format('YYYY-MM-DD') + ' 23:59:59'
} else {
searchForm.value.begdate = dayjs(today.value).format('YYYY-MM-DD') + ' 00:00:00'
searchForm.value.enddate = dayjs(searchForm.value.begdate).format('YYYY-MM-DD') + ' 23:59:59'
}
handleQuery()
}
const dateHandle = (val) => {
searchForm.value.begdate = dayjs(val).format('YYYY-MM-DD') + ' 00:00:00'
searchForm.value.enddate = dayjs(searchForm.value.begdate).format('YYYY-MM-DD') + ' 23:59:59'
handleQuery()
}
const handleQuery = () => {
rowInfo.value = {}
queryRegRequestInfo(searchForm.value).then(res => {
// 数据分组
const { tableData } = useGroupTableData(
res.data,
'orderDate', // 分组字段
['inputDate',], // 仅第一条显示的字段
'orderDate', // 排序字段
['orderDate'],//原数据赋值对应字段
)
labPatList.value = tableData.value
if (!labpat.value.status) {
const item = res.data.find(item => item.patId == labpat.value.patId)
nextTick(() => {
tableRef.value.setCurrentRow(item)
handleRowClick({ row: item })
})
}
})
}
// 获取病人代号查询信息
const getBrdhHandle = (val) => {
console.log("🚀 ~ getBrdhHandle ~ val:", val)
}
//保存
const saveHandle = () => {
if (!labpat.value.patId) return ElMessage.warning('病人代号不能为空!')
if (!labpat.value.patName) return ElMessage.warning('病人姓名不能为空!')
if (!labpat.value.patSex) return ElMessage.warning('病人性别不能为空!')
if (!labpat.value.patAge && !labpat.value.ageUnit) return ElMessage.warning('病人年龄不能为空!')
if (!sqXmList.value.length) return ElMessage.warning('请选择项目!')
labpat.value.status = labpat.value.status ? labpat.value.status : ''
const data = {
regApply: labpat.value,
regApplyDetailList: sqxmCzList.value.map(item => ({
orderItemCode: item.itemcode,
orderItemName: item.itemname,
sampleTypeCode: item.itemclass_yblx,
sampleTypeName: item.itemclass_yblx,
amount: item.amount,
applyid: labpat.value.applyid,
uid: item.uid
}))
}
saveLoading.value = true
saveReqInfo(data).then(res => {
if (res.code == 0) {
ElMessage.success('保存成功!')
handleQuery()
updateInitialData()
}
}).finally(() => {
saveLoading.value = false
})
}
const addHandle = () => {
labpat.value = {}
sqXmList.value = []
updateInitialData()
}
const deleteHandle = () => {
if (!rowInfo.value.applyid) return ElMessage.warning('请选择要作废的申请单!')
ElMessageBox.confirm('确定要作废此申请单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
cancelReq({ applyId: rowInfo.value.applyid }).then(res => {
if (res.code == 0) {
ElMessage.success(res.msg)
handleQuery()
}
})
}).catch(() => { })
}
const printHandle = () => {
if (isDataChanged()) {
ElMessageBox.confirm('数据已修改,请先保存后再打印!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
saveHandle()
}).catch(() => { })
return
}
const selectList = xmRef.value.allSelection()
let arr = selectList.map(item => {
return {
applyId: item.applyid,
barcode: item.barcode,
}
})
const uniqueData = [];
const tempObj = {}; // 记录已出现的 barcode
arr.forEach(item => {
// 若 tempObj 中无该 barcode,则保留并记录
if (!tempObj[item.barcode]) {
tempObj[item.barcode] = true;
uniqueData.push(item);
}
});
if (labpat.value.status > 10) return ElMessage.warning('申请单已审核,不能打印条码!')
if (!uniqueData.length) return ElMessage.warning('请选择要打印的条码!')
checkPrintService().then((isConnected) => {
if (!isConnected) return
reqPrint(uniqueData).then(res => {
if (res.code == 0) {
handleRowClick({ row: rowInfo.value })
ElMessage.success('正在打印中...')
handleQuery()
}
})
})
}
const filterData = ref([])
const xmColumns = ref([
{ field: 'itemcode', title: '代号', align: 'center', resizable: true },
{ field: 'itemname', title: '简称', align: 'center', width: 280, resizable: true },
{ field: 'price', title: '单价', align: 'center', resizable: true },
{ field: 'itemclass', title: '类别', align: 'center', slotName: 'itemclass', resizable: true },
{ field: 'itemclass_tips', title: '采样提示', align: 'center', resizable: true },
{ field: 'itemclass_yblx', title: '样本', align: 'center', resizable: true },
])
const xmcellStyle = ({ row, column }) => {
if (column.title == "类别") {
const bkcolor = itemclassList.value.find((item) => item.itemclassCode == row.itemclass)?.itemclassBkcolor;
const bgColor = classCom.decimalToHexColor(bkcolor);
const textColor = classCom.getContrastTextColor(bgColor);
return {
backgroundColor: `${bgColor} !important`,
color: textColor,
};
}
}
const searchList = ref([])
const handleTabClick = (tab) => {
const currentTab = xmdlList.value.find(item => item.dictCode === tab.paneName)
if (currentTab) {
searchList.value = processedTableData.value.filter(item => item.classid === currentTab.dictCode)
filterData.value = [...searchList.value]
}
}
const processedTableData = ref([])
const filterDictData = () => {
const key = searchKey.value.trim().toLowerCase();
if (!key) {
// 空搜索时显示全部预处理数据
filterData.value = [...searchList.value]
return;
}
filterData.value = searchList.value.filter((item) => {
const matchName = item.itemname.toString().toLowerCase().includes(key);
const matchLabel = item.pinyin.toString().toLowerCase().includes(key);
return matchLabel || matchName;
})
}
const processTableData = (data) => {
return data.map((item) => {
const pinyinMap = {};
pinyinMap['pinyin'] = getFirstLetter(item.itemname).toLowerCase();
return { ...item, ...pinyinMap };
});
};
const sqXmList = ref([])
const sqXmColumns = ref([
{ type: 'selection', field: '', title: '', resizable: true, align: 'center', width: 40 },
{ field: 'newitemclass', title: '类别', align: 'center', width: 100, slotName: 'newitemclass', resizable: true },
{ field: 'newitemclass_tips', title: '采样提示', align: 'center', width: 80, resizable: true },
{ field: 'itemname', title: '简称', align: 'center', resizable: true },
{ field: 'newbarcode', title: '条码号', align: 'center', width: 120, resizable: true, headerSlot: 'newbarcode' },
{ field: 'amount', title: '单价', align: 'center', width: 60, resizable: true },
{ field: 'itemclass_yblx', title: '样本', align: 'center', width: 80, slotName: 'itemclass_yblx', resizable: true },
{ field: 'status', title: '状态', align: 'center', resizable: true, width: 80, slotName: 'status' },
])
const cellStyle = ({ row, column }) => {
if (column.title == "类别" && row.newitemclass) {
const bkcolor = itemclassList.value.find((item) => item.itemclassCode == row.itemclass)?.itemclassBkcolor;
const bgColor = classCom.decimalToHexColor(bkcolor);
const textColor = classCom.getContrastTextColor(bgColor);
return {
backgroundColor: `${bgColor} !important`,
color: textColor,
};
}
}
const getyblxData = (data, row) => {
sqXmList.value.forEach(item => {
if (row.uid == item.uid) {
item.itemclass_yblx = data.label
}
})
}
// 已申请的项目
const sqxmCzList = computed(() => {
// 数据分组
const { tableData } = useGroupTableData(
sqXmList.value,
'itemclass',
['newitemclass', 'newbarcode', 'newitemclass_tips'],
'orderDate',
['itemclass', 'barcode', 'itemclass_tips'],
)
return tableData.value
})
watch(
sqxmCzList,
() => {
nextTick(() => {
// 等待 DOM 渲染完成后执行勾选
if (labpat.value.status < 20 && xmRef.value) {
sqxmCzList.value.forEach(item => {
xmRef.value.toggleRowSelection(item, true)
});
}
});
},
{ deep: true, immediate: true }
);
const barcodes = computed(() => {
return Array.from(new Set(sqxmCzList.value.map(item => item.barcode))).length
})
const tableConfig = ref({
actionWidth: 60,
// 复选框配置
checkboxConfig: {
// 可选配置项
checkField: 'checked', // 绑定数据中的字段(默认:checked)
range: false, // 是否支持范围选择(按住 Shift 连续选择)
disabled: false, // 是否禁用全部复选框
reserve: false, // 是否保留勾选状态(分页/筛选时)
highlight: false // 是否高亮选中行
}
})
const xmRef = useTemplateRef('xmRef')
const selectionChange = (checked, row, selection) => {
// console.log('==>', checked, row, selection);
sqxmCzList.value.forEach(item => {
if (row.barcode === item.barcode) {
xmRef.value.toggleRowSelection(item, checked)
}
});
}
const delItem = (row) => {
const index = sqXmList.value.findIndex(item => item.itemcode === row.itemcode)
if (index !== -1) {
sqXmList.value.splice(index, 1)
}
}
const dbRowclick = ({ row }) => {
if (labpat.value.status > 10) return ElMessage.warning('申请单已审核,不能添加项目!')
const index = sqXmList.value.findIndex(item => item.itemcode == row.itemcode)
if (index > -1) {
ElMessage.warning('已存在该项目')
return
}
sqXmList.value.push({ ...row, amount: row.price })
}
const getXmList = () => {
getXmItemInfo().then(res => {
processedTableData.value = processTableData(res.data);
filterData.value = [...processedTableData.value]
})
}
const itemclassList = ref([]) // 容器类别
onMounted(() => {
changeDays(3)
getXmList()
getDictData('PT', 'SX', 'AU', 'ZT', 'ZJLX', 'DEPT', 'DOCTOR', 'SLZQ', 'BT')
listregdict({ dictType: 'CLASS' }).then(response => {
xmdlList.value = response.rows;
activeName.value = xmdlList.value.length > 0 ? xmdlList.value[0].dictCode : '';
handleTabClick({ paneName: activeName.value });
});
listitemclass({ pageSize: 9999, pageNum: 1 }).then(response => {
itemclassList.value = response.rows;
})
// 初始化初始值
updateInitialData()
})
</script>
<style scoped lang="scss">
.app-container {
background: #F0F3F8;
}
.box-shadow {
height: 100%;
box-shadow: 2px 2px 4px 0px rgba(206, 217, 234, 0.7);
border-radius: .75rem;
padding: .5rem;
background-color: #fff;
.flex-between {
margin-bottom: .5rem;
}
}
.row-box {
height: 100%;
}
.table-box {
margin-top: 2px;
height: calc(100% - 65px);
}
.el-form-item {
margin-bottom: 5px;
}
.top-box {
border-bottom: 1px solid #E6E9ED;
padding-bottom: 2px;
}
.xmBoxs {
height: 60%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background: #817e7e;
border-radius: 4px;
}
}
.tips {
font-style: 15px;
color: #123d64;
font-weight: 600;
}
.title {
font-size: 14px;
color: #000;
font-weight: 700;
}
.radio_box {
display: flex;
align-items: center;
margin: .125rem 0;
.el-radio {
margin-right: .3125rem;
}
}
</style>