2026-07-16 18:08:54 +08:00

731 lines
23 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="8" class="col_box">
<el-button type="success" :size="aotuSize" :loading="saveLoading" plain @click="saveHandle">保存</el-button>
<el-button type="primary" :size="aotuSize" @click="addHandle" plain>新增</el-button>
<el-button type="warning" :size="aotuSize" :loading="printLoading" plain
@click="printHandle">打印</el-button>
<el-button type="danger" :size="aotuSize" plain @click="deleteHandle">作废</el-button>
</el-col>
<el-col :span="16">
<div class="tips">
项目检索: <el-input v-model="searchKey" 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" :visbileMask="visbileMask" @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% - 55px)" 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 #itemclass_yblx="{ row }">
<SelectTable v-model:data="row.itemclass_yblx" :tableData="dictData.BT" placeholder="请选择"
:clearable="false" v-if="row.status == 10 || !row.status" value="label"
@getDataValue="(data) => { getyblxData(data, row) }" class="full-width-input" />
<span v-else style="padding-left: 5px;"> {{ formatDict(row.itemclass_yblx, 'BT') }}</span>
</template>
<template #status="{ row }">
<span v-if="row.rowSeq == 1"> {{ formatDict(row.status, 'ZT') }}</span>
</template>
<template #action="{ row }">
<el-button type="danger" :text='true' v-if="row.status == 10 || !row.status" size="small"
@click="delItem(row)">删除</el-button>
</template>
</CustomVxeTable>
</div>
<div class="yblxtext">共有{{ barcodes }}个样本,合计金额:<span>{{ totalAmount }}</span></div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="8" style="height: 100%;">
<div class="box-shadow">
<el-form :model="searchForm" inline>
<el-form-item label="" prop="brxm">
<el-input v-model="searchForm.brxm" clearable style="width: 8rem;" @change="handleQuery" :size="aotuSize"
placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="" prop="status">
<div class="radio_box">
<el-radio-group v-model="searchForm.status" @change="handleQuery" :size="aotuSize">
<el-radio :value="''">所有</el-radio>
<el-radio :value="item.value" v-for="item in dictData.SQDS">{{ item.label }}</el-radio>
</el-radio-group>
</div>
</el-form-item>
<el-form-item label="" prop="begdate">
<div class="radio_box">
<el-radio-group v-model="searchForm.days" @change="changeDays" :size="aotuSize">
<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;"
:size="aotuSize" @change="dateHandle" /> &nbsp;
<el-button type="primary" icon="RefreshRight" :size="aotuSize" @click="handleQuery">刷新</el-button>
</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, 'SQDS') }}
</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 { saveReqInfo, queryPatInfo } 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";
import { checkPrintService } from '@/utils/printHelper.ts';
import { listitemclass } from "@/api/regional/dict/itemclass";
import { classCom } from '@/utils/classCom';
import { useGroupTableData } from '@/utils/groupHelper'
const { proxy } = getCurrentInstance();
const aotuSize = classCom.useAutoSize();
const labpat = ref({
ageUnit: '1'
})
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 visbileMask = ref(false)
const handleRowClick = ({ row }) => {
rowInfo.value = row
if (!row?.applyid) return
// queryReqDetail({ applyId: row.applyid }).then(res => {
// labpat.value = res.data.regApply
// visbileMask.value = res.data.regApplyDetailList.some(item => { return item.status > 10 })
// 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,
// status: item.status,
// }))
// // 异步初始化数据
// 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
// const item = res.data.find(item => item.patId == labpat.value.patId)
// nextTick(() => {
// tableRef.value.setCurrentRow(item)
// handleRowClick({ row: item })
// })
// })
}
// 获取病人代号查询信息
const getBrdhHandle = (val) => {
queryPatInfo({ patId: val }).then(res => {
if (res.data) {
labpat.value = res.data
}
})
}
//保存
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 = {
ageUnit: '1'
}
sqXmList.value = []
visbileMask.value = false
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 printLoading = ref(false)
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 (!uniqueData.length) return ElMessage.warning('请选择要打印的条码!')
checkPrintService().then((isConnected) => {
if (!isConnected) return
printLoading.value = true
// reqPrint(uniqueData).then(res => {
// if (res.code == 0) {
// sendWebSocketMessage(res.data)
// handleRowClick({ row: rowInfo.value })
// ElMessage.success('正在打印中...')
// handleQuery()
// }
// }).finally(() => {
// printLoading.value = false
// })
})
}
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) => {
if (tab.paneName) {
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]
}
} else {
searchList.value = [...processedTableData.value]
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', slotName: 'newitemclass', resizable: true },
{ field: 'newitemclass_tips', title: '采样提示', align: 'center', width: 60, resizable: true },
{ field: 'itemname', title: '项目名称', align: 'center', resizable: true, width: 220, },
{ field: 'newbarcode', title: '条码号', align: 'center', width: 120, resizable: true, },
{ field: 'amount', title: '单价', align: 'center', width: 60, resizable: true },
{ field: 'itemclass_yblx', title: '样本', 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', 'barcode'],
['newitemclass', 'newbarcode', 'newitemclass_tips'],
'orderDate',
['itemclass', 'barcode', 'itemclass_tips'],
)
return tableData.value
})
watch(
sqxmCzList,
() => {
nextTick(() => {
// 等待 DOM 渲染完成后执行勾选
sqxmCzList.value.forEach(item => {
if (item.status < 30) {
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 totalAmount = computed(() => {
return sqxmCzList.value.reduce((total, item) => {
return total + (item.amount || 0);
}, 0)
})
const tableConfig = ref({
actionWidth: 60,
// 复选框配置
checkboxConfig: {
// 可选配置项
checkField: 'checked', // 绑定数据中的字段(默认:checked)
range: false, // 是否支持范围选择(按住 Shift 连续选择)
disabled: false, // 是否禁用全部复选框
reserve: false, // 是否保留勾选状态(分页/筛选时)
highlight: false, // 是否高亮选中行
checkMethod: ({ row }) => {
//标本送检之前状态才能勾选
return row.status < 30;
}
}
})
const xmRef = useTemplateRef('xmRef')
const selectionChange = (checked, row, selection) => {
// console.log('==>', checked, row, selection);
sqxmCzList.value.forEach(item => {
if (row.barcode === item.barcode && row.status < 30) {
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 (visbileMask.value) 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]
listregdict({ dictType: 'CLASS' }).then(response => {
xmdlList.value = response.rows;
xmdlList.value.unshift({ dictCode: '', dictName: '全部' })
activeName.value = xmdlList.value.length > 0 ? xmdlList.value[0].dictCode : '';
handleTabClick({ paneName: activeName.value });
});
// })
}
const itemclassList = ref([]) // 容器类别
const initSocket = () => {
initWebSocket({
fullUrl: `ws://localhost:9801`,
onOpen: () => {
console.log('WebSocket连接成功');
},
onMessage: (data) => {
console.log(`收到消息: `, JSON.parse(data));
},
onError: (error) => {
console.log(`连接错误: ${error.type}`);
},
onClose: () => {
console.log('WebSocket连接已关闭');
},
reconnectInterval: 3000 // 重连间隔(毫秒)
});
}
onMounted(() => {
getXmList()
changeDays(3)
getDictData('PT', 'SX', 'AU', 'ZT', 'ZJLX', 'DEPT', 'DOCTOR', 'SLZQ', 'BT', 'SQDS')
listitemclass({ pageSize: 9999, pageNum: 1 }).then(response => {
itemclassList.value = response.rows;
})
// 初始化初始值
updateInitialData()
// initSocket()
})
// 页面卸载时清理
onUnmounted(() => {
// closeWebSocket();
});
</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% - 85px);
}
.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;
}
.yblxtext {
font-style: 15px;
color: #123d64;
font-weight: 600;
text-align: center;
}
.title {
font-size: 14px;
color: #000;
font-weight: 700;
}
.radio_box {
display: flex;
align-items: center;
margin: .125rem 0;
.el-radio {
margin-right: .3125rem;
:deep(.el-radio__label) {
padding-left: .3125rem;
}
}
}
.col_box {
.el-button+.el-button {
margin-left: .5rem;
}
}
</style>