审核入库基础功能

This commit is contained in:
jiangs 2026-09-23 16:26:31 +08:00
parent 199c547199
commit 42047c2013
3 changed files with 634 additions and 1 deletions

3
.gitignore vendored
View File

@ -22,4 +22,5 @@ selenium-debug.log
package-lock.json
yarn.lock
.trae/
.trae/
.qoder/

View File

@ -0,0 +1,49 @@
import request from "@/utils/request";
/**
* 获取已存在送血单列表(下拉框选择)
* 后端路径: /bus/bloodbank/auditInStorage/getExistSQDList
*/
export function getExistSQDList(query?: Object) {
return request({
url: "/bus/bloodbank/auditInStorage/getExistSQDList",
method: "get",
params: query,
});
}
/**
* 获取送血单明细数据
* 后端路径: /bus/bloodbank/auditInStorage/getExistSQDInfo
*/
export function getExistSQDInfo(query?: Object) {
return request({
url: "/bus/bloodbank/auditInStorage/getExistSQDInfo",
method: "get",
params: query,
});
}
/**
* 保存审核入库数据
* 后端路径: /bus/bloodbank/auditInStorage/saveData
*/
export function saveAuditData(data?: Object) {
return request({
url: "/bus/bloodbank/auditInStorage/saveData",
method: "post",
data,
});
}
/**
* 打印入库单
* 后端路径: /bus/bloodbank/auditInStorage/print
*/
export function printInStorage(query?: Object) {
return request({
url: "/bus/bloodbank/auditInStorage/print",
method: "get",
params: query,
});
}

View File

@ -0,0 +1,583 @@
<!--
* 审核入库页面
* @author: AI
* @since: 2026-09-23
* 功能:审核入库管理,选择送血单后扫描血液确认入库
-->
<template>
<div class="audit-in-storage">
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<el-button type="primary" plain @click="loadBillList">刷新出库单</el-button>
<el-button type="warning" plain @click="handleSave" :loading="saveLoading">保存入库</el-button>
<el-button type="info" plain :icon="Printer" @click="handlePrint">打印入库单</el-button>
<el-button type="danger" plain :icon="Close" @click="handleClose">关闭</el-button>
</div>
</div>
<!-- 入库信息表单 -->
<div class="form-section">
<div class="section-title">入库信息</div>
<el-form :model="mainForm" label-width="100px" label-position="left">
<el-row :gutter="15">
<el-col :span="6">
<el-form-item label="入库单号">
<el-input v-model="mainForm.bill_no" disabled placeholder="自动生成" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="入库时间">
<el-date-picker v-model="mainForm.occur_date" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" placeholder="入库时间" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="经手人">
<SelectTable v-model:data="mainForm.handle_person" :tableData="userList"
placeholder="请选择经手人" />
</el-form-item>
</el-col>
<el-col :span="6"></el-col>
</el-row>
<el-row :gutter="15">
<el-col :span="6">
<el-form-item label="入库方式">
<el-input v-model="mainForm.storage_type" disabled placeholder="审核入库" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="血站出库单号">
<SelectTable v-model:data="selectedBillId" :tableData="billList" :fields="billFields"
placeholder="请选择送血单" @update:data="handleBillSelect" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注">
<el-input v-model="mainForm.remark" placeholder="备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<!-- 复选框行 -->
<div class="checkbox-row">
<el-checkbox v-model="cancelBlood">取消血液</el-checkbox>
<el-input v-model="cancelBloodNo" placeholder="输入血液流水号后回车取消" style="width: 260px"
@keyup.enter="handleCancelBlood" :disabled="!cancelBlood" />
</div>
<!-- 三栏布局:血液信息 + 待入库血液 + 已扫描血液 -->
<div class="main-content">
<!-- 左侧:血液信息表单 -->
<div class="left-panel">
<div class="section-title">血液信息</div>
<el-form :model="bloodForm" label-width="70px" label-position="left">
<el-form-item label="血液流水号">
<el-input v-model="bloodForm.blood_no" placeholder="请输入血液流水号" @keyup.enter="handleScanBlood" />
</el-form-item>
<el-form-item label="产品码">
<el-input v-model="bloodForm.product_no" placeholder="产品码" />
</el-form-item>
<el-form-item label="血液品种">
<el-select v-model="bloodForm.blood_breed" placeholder="请选择血液品种" filterable style="width: 100%">
<el-option v-for="item in bloodBreedList" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="血型">
<el-select v-model="bloodForm.blood_type" placeholder="请选择血型" style="width: 100%">
<el-option v-for="item in bloodTypeOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="Rh(D)">
<el-select v-model="bloodForm.rh_blood_type" placeholder="Rh(D)" style="width: 100%">
<el-option label="+" value="+" />
<el-option label="-" value="-" />
</el-select>
</el-form-item>
<el-form-item label="容量">
<div class="capacity-box">
<el-input v-model="bloodForm.capacity" placeholder="容量" style="flex: 1" />
<el-input v-model="bloodForm.unit" placeholder="单位" style="width: 60px" />
</div>
</el-form-item>
<el-form-item label="采血日期">
<el-date-picker v-model="bloodForm.pick_date" type="datetime" value-format="YYYY-MM-DD HH:mm:ss"
placeholder="采血日期" style="width: 100%" />
</el-form-item>
<el-form-item label="制备日期">
<el-date-picker v-model="bloodForm.produce_date" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" placeholder="制备日期" style="width: 100%" />
</el-form-item>
<el-form-item label="有效日期">
<el-date-picker v-model="bloodForm.valid_date" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" placeholder="有效日期" style="width: 100%" />
</el-form-item>
<el-form-item label="特别处理">
<el-input v-model="bloodForm.special_deal" placeholder="特别处理" />
</el-form-item>
</el-form>
</div>
<!-- 中间:待入库血液列表 -->
<div class="mid-panel">
<div class="section-title">待入库血液 ({{ pendingList.length }})</div>
<el-table :data="pendingList" border highlight-current-row height="38vh">
<el-table-column type="index" label="序号" width="50" align="center" />
<el-table-column prop="blood_no" label="血液流水号" min-width="120" align="center" />
<el-table-column prop="blood_breed" label="血液品种" min-width="90" align="center">
<template #default="{ row }">
{{ formatBloodBreed(row.blood_breed) }}
</template>
</el-table-column>
<el-table-column prop="blood_type" label="血型" width="55" align="center" />
<el-table-column prop="rh_blood_type" label="Rh(D)" width="55" align="center" />
<el-table-column prop="capacity" label="容量" width="65" align="center" />
<el-table-column prop="unit" label="单位" width="55" align="center" />
</el-table>
<div class="panel-stat">
<BloodTypeStatTable :data="pendingList" />
</div>
</div>
<!-- 操作按钮 -->
<div class="transfer-buttons">
<el-button type="primary" size="small" @click="handleMoveAllToScanned">>></el-button>
<el-button type="primary" size="small" @click="handleMoveAllToPending">&lt;&lt;</el-button>
</div>
<!-- 右侧:已扫描血液列表 -->
<div class="right-panel">
<div class="section-title">已扫描血液 ({{ scannedList.length }})</div>
<el-table :data="scannedList" border highlight-current-row height="38vh">
<el-table-column type="index" label="序号" width="50" align="center" />
<el-table-column prop="blood_no" label="血液流水号" min-width="110" align="center" />
<el-table-column prop="product_no" label="产品码" min-width="100" align="center" />
<el-table-column prop="blood_breed" label="血液品种" min-width="90" align="center">
<template #default="{ row }">
{{ formatBloodBreed(row.blood_breed) }}
</template>
</el-table-column>
<el-table-column prop="blood_type" label="血型" width="55" align="center" />
<el-table-column prop="rh_blood_type" label="Rh(D)" width="55" align="center" />
<el-table-column prop="capacity" label="容量" width="65" align="center" />
<el-table-column prop="unit" label="单位" width="55" align="center" />
<el-table-column prop="remark" label="备注" min-width="80" align="center" />
</el-table>
<div class="panel-stat">
<BloodTypeStatTable :data="scannedList" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Printer, Close } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import useUserStore from '@/store/modules/user'
import { useCommonDict } from '@/hooks'
import { getExistSQDList, getExistSQDInfo, saveAuditData, printInStorage } from '@/api/blood/auditInStorage'
import BloodTypeStatTable from '@/components/BloodTypeStatTable/index.vue'
const userInfo = useUserStore()
const { getServerTime, bloodBreed: bloodBreedList, getBloodList, userList, getUserData } = useCommonDict()
// 保存按钮 loading 状态
const saveLoading = ref(false)
// 选中的单据ID
const selectedBillId = ref<string>('')
// 送血单列表数据
const billList = ref<any[]>([])
// 送血单下拉框表格列配置
const billFields = [
{ prop: 'label', label: '单据编号', width: 160, enablePinyinSearch: true },
{ prop: 'value', label: '代号', width: 100, enablePinyinSearch: true },
]
// 血型选项
const bloodTypeOptions = [
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
{ label: 'O', value: 'O' },
{ label: 'AB', value: 'AB' }
]
// 取消血液复选框
const cancelBlood = ref(false)
// 取消血液输入的流水号
const cancelBloodNo = ref('')
// 待入库血液列表
const pendingList = ref<any[]>([])
// 已扫描血液列表
const scannedList = ref<any[]>([])
// 主表单数据
const mainForm = ref<any>({
bill_no: '',
occur_date: '',
storage_type: '审核入库',
handle_person: '',
out_bill_no: '',
remark: '',
})
// 血液信息表单
const bloodForm = ref<any>({
blood_no: '',
product_no: '',
blood_breed: '',
blood_type: '',
rh_blood_type: '+',
capacity: '',
unit: '',
pick_date: '',
produce_date: '',
valid_date: '',
special_deal: '',
})
/** 格式化血液品种代码为名称 */
const formatBloodBreed = (code: string) => {
const item = bloodBreedList.value.find((b: any) => b.value === code)
return item ? item.label : code
}
/** 加载送血单列表 */
const loadBillList = async () => {
const res = await getExistSQDList()
billList.value = (res.data || []).map((item: any) => ({
label: item.sqd_no || item.bill_no || item.label,
value: item.sqd_id || item.id || item.value,
}))
}
/** 选择送血单后加载明细数据 */
const handleBillSelect = async (val: any) => {
if (!val) return
const res = await getExistSQDInfo({ sqdId: val })
if (res.data) {
const main = res.data.main || {}
mainForm.value = {
bill_no: main.bill_no || '',
occur_date: main.occur_date || '',
storage_type: '审核入库',
handle_person: main.handle_person || userInfo.name,
out_bill_no: main.out_bill_no || '',
remark: main.remark || '',
}
pendingList.value = res.data.bloodList || []
scannedList.value = []
}
}
/** 扫描血液流水号:在待入库列表中查找,找到则移到已扫描列表 */
const handleScanBlood = () => {
const bloodNo = bloodForm.value.blood_no?.trim()
if (!bloodNo) return
const index = pendingList.value.findIndex(item => item.blood_no === bloodNo)
if (index === -1) {
ElMessage.warning('当前血液列表中无此血液!')
bloodForm.value.blood_no = ''
return
}
// 从待入库移除,放入已扫描
const removed = pendingList.value.splice(index, 1)
scannedList.value.push(removed[0])
bloodForm.value.blood_no = ''
}
/** 取消血液:通过血液流水号从已扫描列表中移除 */
const handleCancelBlood = () => {
const bloodNo = cancelBloodNo.value.trim()
if (!bloodNo) return
const index = scannedList.value.findIndex(item => item.blood_no === bloodNo)
if (index === -1) {
ElMessage.warning('已扫描列表中不存在该血液流水号')
} else {
const removed = scannedList.value.splice(index, 1)
pendingList.value.push(removed[0])
ElMessage.success('已取消血液: ' + bloodNo)
}
cancelBloodNo.value = ''
}
/** 全部移入已扫描:把待入库血液全部放入已扫描列表 */
const handleMoveAllToScanned = () => {
if (pendingList.value.length === 0) {
ElMessage.warning('待入库列表为空')
return
}
scannedList.value.push(...pendingList.value)
pendingList.value = []
}
/** 全部退回待入库:把已扫描血液全部放回待入库列表 */
const handleMoveAllToPending = () => {
if (scannedList.value.length === 0) {
ElMessage.warning('已扫描列表为空')
return
}
ElMessageBox.confirm('确定要将所有已扫描血液退回到待入库列表吗?', '批量操作确认', {
type: 'warning',
}).then(() => {
pendingList.value.push(...scannedList.value)
scannedList.value = []
}).catch(() => { })
}
/** 保存入库 */
const handleSave = async () => {
if (!mainForm.value.handle_person) {
return ElMessage.warning('请选择经手人')
}
// 待入库列表有数据时提醒用户
if (pendingList.value.length > 0) {
try {
await ElMessageBox.confirm(
'有待入库的血液没有操作,是否继续?如果继续,将会把待入库血液列入到异常血液当中',
'提示',
{ type: 'warning' }
)
} catch {
return // 用户取消
}
}
const data = {
main: mainForm.value,
scannedList: scannedList.value,
pendingList: pendingList.value,
}
saveLoading.value = true
try {
const res = await saveAuditData(data)
if (res.code === 0 || res.code === 200) {
ElMessage.success(res.msg || '保存入库成功')
resetPage()
} else {
ElMessage.error(res.msg || '保存失败')
}
} finally {
saveLoading.value = false
}
}
/** 打印入库单 */
const handlePrint = () => {
if (!mainForm.value.bill_no) {
return ElMessage.warning('请先选择单据')
}
printInStorage({ billNo: mainForm.value.bill_no })
}
/** 关闭页面 */
const handleClose = () => {
ElMessageBox.confirm('确定关闭当前页面吗?未保存的数据将丢失', '提示', {
type: 'warning',
}).then(() => {
resetPage()
}).catch(() => { })
}
/** 重置页面:清空所有列表和表单 */
const resetPage = () => {
mainForm.value = {
bill_no: '',
occur_date: '',
storage_type: '审核入库',
handle_person: userInfo.name,
out_bill_no: '',
remark: '',
}
bloodForm.value = {
blood_no: '',
product_no: '',
blood_breed: '',
blood_type: '',
rh_blood_type: '+',
capacity: '',
unit: '',
pick_date: '',
produce_date: '',
valid_date: '',
special_deal: '',
}
selectedBillId.value = ''
pendingList.value = []
scannedList.value = []
cancelBlood.value = false
cancelBloodNo.value = ''
}
onMounted(() => {
getBloodList()
getUserData()
loadBillList()
// 设置默认经手人
mainForm.value.handle_person = userInfo.name
// 获取服务器时间
getServerTime().then((time: string) => {
mainForm.value.occur_date = time
})
})
</script>
<style lang="scss" scoped>
.audit-in-storage {
display: flex;
flex-direction: column;
padding: 10px;
box-sizing: border-box;
height: 100%;
}
.toolbar {
flex: none;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
margin-bottom: 10px;
.toolbar-left {
display: flex;
gap: 8px;
}
}
.form-section {
flex: none;
padding: 10px 15px;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 10px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
padding: 0 0 6px 10px;
margin-bottom: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
bottom: 7px;
width: 4px;
height: 14px;
background-color: #409eff;
border-radius: 0 4px 4px 0;
}
}
.checkbox-row {
flex: none;
display: flex;
align-items: center;
gap: 20px;
padding: 8px 15px;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 10px;
}
.main-content {
flex: 1;
min-height: 0;
display: flex;
gap: 10px;
align-items: stretch;
position: relative;
}
.left-panel {
width: 260px;
flex: 0 0 260px;
display: flex;
flex-direction: column;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
overflow-y: auto;
}
.mid-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
}
.right-panel {
flex: 1.2;
min-width: 0;
display: flex;
flex-direction: column;
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
}
.panel-stat {
flex: none;
margin-top: 8px;
}
.transfer-buttons {
flex: 0 0 44px;
width: 44px;
margin-top: 45px;
margin-bottom: 80px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
}
.transfer-buttons .el-button {
width: 36px !important;
min-width: 36px !important;
padding: 0 !important;
margin: 0 !important;
}
.capacity-box {
display: flex;
gap: 6px;
width: 100%;
}
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.left-panel .el-form-item__label) {
white-space: nowrap;
}
:deep(.form-section .el-form-item__label) {
white-space: nowrap;
}
</style>