Commit 26e22007 by 杨子

feat: 新增出库单管理功能及相关组件

refactor: 优化RFID标签显示为字典标签组件

style: 移除冗余ID列并调整表格列宽

fix: 修复入库单物资重复添加问题

perf: 优化货架和标签选择逻辑,增加重复检查

docs: 更新注释和字段标签描述

test: 添加出库单管理相关API测试

chore: 清理无用代码和配置
parent 46e23a8d
import request from '@/utils/request'
// 查询盘点任务明细列表
export function listWmsInventoryTaskDetail(query) {
return request({
url: '/ware/wmsInventoryTaskDetail/list',
method: 'get',
params: query
})
}
// 查询盘点任务明细详细
export function getWmsInventoryTaskDetail(detailId) {
return request({
url: '/ware/wmsInventoryTaskDetail/' + detailId,
method: 'get'
})
}
// 新增盘点任务明细
export function addWmsInventoryTaskDetail(data) {
return request({
url: '/ware/wmsInventoryTaskDetail',
method: 'post',
data: data
})
}
// 修改盘点任务明细
export function updateWmsInventoryTaskDetail(data) {
return request({
url: '/ware/wmsInventoryTaskDetail',
method: 'put',
data: data
})
}
// 删除盘点任务明细
export function delWmsInventoryTaskDetail(detailId) {
return request({
url: '/ware/wmsInventoryTaskDetail/' + detailId,
method: 'delete'
})
}
import request from '@/utils/request'
// 查询出库明细库存关联列表
export function listWmsOutboundItemInventory(query) {
return request({
url: '/ware/wmsOutboundItemInventory/list',
method: 'get',
params: query
})
}
// 查询出库明细库存关联详细
export function getWmsOutboundItemInventory(relationId) {
return request({
url: '/ware/wmsOutboundItemInventory/' + relationId,
method: 'get'
})
}
// 新增出库明细库存关联
export function addWmsOutboundItemInventory(data) {
return request({
url: '/ware/wmsOutboundItemInventory',
method: 'post',
data: data
})
}
// 修改出库明细库存关联
export function updateWmsOutboundItemInventory(data) {
return request({
url: '/ware/wmsOutboundItemInventory',
method: 'put',
data: data
})
}
// 删除出库明细库存关联
export function delWmsOutboundItemInventory(relationId) {
return request({
url: '/ware/wmsOutboundItemInventory/' + relationId,
method: 'delete'
})
}
...@@ -42,3 +42,44 @@ export function delWmsOutboundOrder(orderId) { ...@@ -42,3 +42,44 @@ export function delWmsOutboundOrder(orderId) {
method: 'delete' method: 'delete'
}) })
} }
// 确认出库
export function confirmOutbound(data) {
return request({
url: '/ware/wmsOutboundOrder/rfidPicking',
method: 'post',
data: data
})
}
/**
* 按照出库明细撤销
* @param {*} data 出库单明细
* @param {*} operatorId 操作人ID
* @returns
*/
export function cancelByDetailId(data, operatorId) {
return request({
url: '/ware/wmsOutboundOrder/cancelItem/' + data.itemId,
method: 'post',
data: {
operatorId: operatorId
}
})
}
/**
* 按照出库记录撤销
* @param {*} data 出库记录
* @param {*} operatorId 操作人ID
* @returns
*/
export function cancelByRelationId(data, operatorId) {
return request({
url: '/ware/wmsOutboundOrder/cancelOutboundRelations/' + data.relationId,
method: 'post',
data: {
operatorId: operatorId
}
})
}
\ No newline at end of file
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
append-to-body append-to-body
> >
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px"> <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="操作类型" prop="operationType"> <el-form-item label="流水类型" prop="flowType">
<el-input <el-input
v-model="queryParams.operationType" v-model="queryParams.flowType"
placeholder="请输入操作类型" placeholder="请输入流水类型"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
...@@ -34,14 +34,18 @@ ...@@ -34,14 +34,18 @@
<el-table-column label="物资ID" align="center" prop="materialId" /> <el-table-column label="物资ID" align="center" prop="materialId" />
<el-table-column label="仓库ID" align="center" prop="warehouseId" /> <el-table-column label="仓库ID" align="center" prop="warehouseId" />
<el-table-column label="货架ID" align="center" prop="locationId" /> <el-table-column label="货架ID" align="center" prop="locationId" />
<el-table-column label="操作类型" align="center" prop="operationType" /> <el-table-column label="流水类型" align="center" prop="flowType" />
<el-table-column label="变动数量" align="center" prop="changeQuantity" /> <el-table-column label="流水单号" align="center" prop="flowCode" />
<el-table-column label="变动前数量" align="center" prop="beforeQuantity" /> <el-table-column label="批次号" align="center" prop="batchNo" />
<el-table-column label="变动后数量" align="center" prop="afterQuantity" /> <el-table-column label="变动前数量" align="center" prop="quantityBefore" />
<el-table-column label="操作人" align="center" prop="operator" /> <el-table-column label="变动数量" align="center" prop="quantityChange" />
<el-table-column label="变动后数量" align="center" prop="quantityAfter" />
<el-table-column label="单价" align="center" prop="unitPrice" />
<el-table-column label="总价" align="center" prop="totalPrice" />
<el-table-column label="操作人" align="center" prop="operatorId" />
<el-table-column label="操作时间" align="center" prop="operationTime" width="180"> <el-table-column label="操作时间" align="center" prop="operationTime" width="180">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.operationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> <span>{{ parseTime(scope.row.operationTime, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="备注" align="center" prop="remark" /> <el-table-column label="备注" align="center" prop="remark" />
...@@ -106,7 +110,7 @@ const data = reactive({ ...@@ -106,7 +110,7 @@ const data = reactive({
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
inventoryId: props.inventoryId, inventoryId: props.inventoryId,
operationType: null, flowType: null,
operationTime: null operationTime: null
} }
}) })
...@@ -144,16 +148,12 @@ function handleQuery() { ...@@ -144,16 +148,12 @@ function handleQuery() {
/** 重置按钮操作 */ /** 重置按钮操作 */
function resetQuery() { function resetQuery() {
queryParams.value.operationType = null queryParams.value.flowType = null
queryParams.value.operationTime = null queryParams.value.operationTime = null
queryParams.value.pageNum = 1 queryParams.value.pageNum = 1
getList() getList()
} }
// 暴露方法供外部调用
defineExpose({
handleQuery
})
</script> </script>
<style scoped> <style scoped>
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
<el-table-column label="标签类型" align="center" prop="tagType" /> <el-table-column label="标签类型" align="center" prop="tagType" />
<el-table-column label="标签状态" align="center" prop="tagStatus"> <el-table-column label="标签状态" align="center" prop="tagStatus">
<template #default="scope"> <template #default="scope">
<span>{{ sys_normal_disable[scope.row.tagStatus] }}</span> <dict-tag :options="sys_normal_disable" :value="scope.row.tagStatus" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="最后读取时间" align="center" prop="lastReadTime"> <el-table-column label="最后读取时间" align="center" prop="lastReadTime">
......
...@@ -91,7 +91,6 @@ ...@@ -91,7 +91,6 @@
<el-table v-loading="loading" :data="wmsCustomerList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="wmsCustomerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="客户ID" align="center" prop="customerId" />
<el-table-column label="客户编码" align="center" prop="customerCode" /> <el-table-column label="客户编码" align="center" prop="customerCode" />
<el-table-column label="客户名称" align="center" prop="customerName" /> <el-table-column label="客户名称" align="center" prop="customerName" />
<el-table-column label="联系人" align="center" prop="contactPerson" /> <el-table-column label="联系人" align="center" prop="contactPerson" />
......
...@@ -62,10 +62,10 @@ ...@@ -62,10 +62,10 @@
</template> </template>
</el-table-column> </el-table-column>
<!-- PLT标签 --> <!-- PLT标签 -->
<el-table-column prop="storageLocation" label="PLT标签" width="180"> <el-table-column prop="storageLocation" label="PTL标签" width="180">
<template #default="scope"> <template #default="scope">
<div v-if="scope.row.isEditing"> <div v-if="scope.row.isEditing">
<el-input v-model="scope.row.storageLocation" placeholder="请选择PLT标签" size="small" readonly /> <el-input v-model="scope.row.storageLocation" placeholder="请选择PTL标签" size="small" readonly />
<el-button slot="append" type="primary" size="small" @click="openPtlTagSelect(scope.$index)">选择</el-button> <el-button slot="append" type="primary" size="small" @click="openPtlTagSelect(scope.$index)">选择</el-button>
</div> </div>
<span v-else>{{ scope.row.storageLocation || '-' }}</span> <span v-else>{{ scope.row.storageLocation || '-' }}</span>
...@@ -75,10 +75,10 @@ ...@@ -75,10 +75,10 @@
<el-table-column prop="rfidTagIds" label="RFID标签" width="180"> <el-table-column prop="rfidTagIds" label="RFID标签" width="180">
<template #default="scope"> <template #default="scope">
<div v-if="scope.row.isEditing"> <div v-if="scope.row.isEditing">
<el-input v-model="scope.row.rfidTagIds" placeholder="请选择RFID标签" size="small" readonly /> <el-input v-model="scope.row.rfidTagCodes" placeholder="请选择RFID标签" size="small" readonly />
<el-button slot="append" type="primary" size="small" @click="openRfidTagSelect(scope.$index)">选择</el-button> <el-button slot="append" type="primary" size="small" @click="openRfidTagSelect(scope.$index)">选择</el-button>
</div> </div>
<span v-else>{{ scope.row.rfidTagIds || '-' }}</span> <span v-else>{{ scope.row.rfidTagCodes || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right"> <el-table-column label="操作" width="180" align="center" fixed="right">
...@@ -276,7 +276,8 @@ function openRfidTagSelect(index) { ...@@ -276,7 +276,8 @@ function openRfidTagSelect(index) {
// 处理RFID标签选择确认 // 处理RFID标签选择确认
function handleRfidTagConfirm(selectedTags) { function handleRfidTagConfirm(selectedTags) {
if (currentEditingRowIndex.value >= 0 && adjustInventoryDetailList.value[currentEditingRowIndex.value]) { if (currentEditingRowIndex.value >= 0 && adjustInventoryDetailList.value[currentEditingRowIndex.value]) {
adjustInventoryDetailList.value[currentEditingRowIndex.value].rfidTagIds = selectedTags.map(tag => tag.rfidTagId).join(',') adjustInventoryDetailList.value[currentEditingRowIndex.value].rfidTagIds = selectedTags.map(tag => tag.tagId).join(',')
adjustInventoryDetailList.value[currentEditingRowIndex.value].rfidTagCodes = selectedTags.map(tag => tag.tagCode).join(',')
} }
currentEditingRowIndex.value = -1 currentEditingRowIndex.value = -1
} }
......
...@@ -27,7 +27,28 @@ ...@@ -27,7 +27,28 @@
<span>{{ scope.row.amount }}</span> <span>{{ scope.row.amount }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="wmsLocation.locationName" label="货架" width="100" /> <el-table-column prop="locationId" label="货架" width="300">
<template #default="scope">
<LocationCascaderSelect
:model-value="{ warehouseId: inboundOrder?.warehouseId, areaId: scope.row.areaId, locationId: scope.row.locationId }"
@update:model-value="(value) => {
if (value.locationId) {
// 检查是否有其他行使用了相同的locationId
const duplicateRow = inventoryDetailList.find((item, index) =>
index !== scope.$index && item.locationId === value.locationId
);
if (duplicateRow) {
proxy.$message.warning('该货架已被其他明细行使用,请选择其他货架');
return;
}
}
scope.row.warehouseId = inboundOrder?.warehouseId;
scope.row.areaId = value.areaId;
scope.row.locationId = value.locationId;
}" />
</template>
</el-table-column>
<el-table-column prop="storageLocation" label="PTL标签" width="180"> <el-table-column prop="storageLocation" label="PTL标签" width="180">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.storageLocation" placeholder="请选择PTL标签" size="small" readonly /> <el-input v-model="scope.row.storageLocation" placeholder="请选择PTL标签" size="small" readonly />
...@@ -36,10 +57,15 @@ ...@@ -36,10 +57,15 @@
</el-table-column> </el-table-column>
<el-table-column prop="rfidTagIds" label="RFID标签" width="180"> <el-table-column prop="rfidTagIds" label="RFID标签" width="180">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.rfidTagIds" placeholder="请选择RFID标签" size="small" readonly /> <el-input v-model="scope.row.rfidTagCodes" placeholder="请选择RFID标签" size="small" readonly />
<el-button slot="append" type="primary" size="small" @click="openRfidTagSelect(scope.$index)">选择</el-button> <el-button slot="append" type="primary" size="small" @click="openRfidTagSelect(scope.$index)">选择</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注">
<template #default="scope">
{{ scope.row.remark }}
</template>
</el-table-column>
<el-table-column prop="wmsInboundOrderItemId" label="操作" width="100" fixed="right"> <el-table-column prop="wmsInboundOrderItemId" label="操作" width="100" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button type="primary" size="small" @click="handleExportQrcode(scope.row)">打印二维码</el-button> <el-button type="primary" size="small" @click="handleExportQrcode(scope.row)">打印二维码</el-button>
...@@ -75,6 +101,7 @@ import { addToInventory } from "@/api/ware/wmsInboundOrder" ...@@ -75,6 +101,7 @@ import { addToInventory } from "@/api/ware/wmsInboundOrder"
import RfidTagSelectDialog from "@/components/RfidTagSelectDialog.vue" import RfidTagSelectDialog from "@/components/RfidTagSelectDialog.vue"
import PtlTagSelectDialog from "@/components/PtlTagSelectDialog.vue" import PtlTagSelectDialog from "@/components/PtlTagSelectDialog.vue"
import QrcodePrintDialog from "@/components/QrcodePrintDialog.vue" import QrcodePrintDialog from "@/components/QrcodePrintDialog.vue"
import LocationCascaderSelect from "@/components/LocationCascaderSelect.vue"
const props = defineProps({ const props = defineProps({
visible: { visible: {
...@@ -204,7 +231,21 @@ function openRfidTagSelect(index) { ...@@ -204,7 +231,21 @@ function openRfidTagSelect(index) {
// 处理RFID标签选择确认 // 处理RFID标签选择确认
function handleRfidTagConfirm(selectedTags) { function handleRfidTagConfirm(selectedTags) {
if (currentEditingRowIndex.value >= 0 && inventoryDetailList.value[currentEditingRowIndex.value]) { if (currentEditingRowIndex.value >= 0 && inventoryDetailList.value[currentEditingRowIndex.value]) {
inventoryDetailList.value[currentEditingRowIndex.value].rfidTagIds = selectedTags.map(tag => tag.tagId).join(',') const selectedTagIds = selectedTags.map(tag => tag.tagId).join(',')
const selectedTagCodes = selectedTags.map(tag => tag.tagCode).join(',')
// 检查是否有其他行使用了相同的RFID标签
const duplicateRow = inventoryDetailList.value.find((item, index) =>
index !== currentEditingRowIndex.value && item.rfidTagIds === selectedTagIds
);
if (duplicateRow) {
proxy.$message.warning('该RFID标签已被其他明细行使用,请选择其他标签');
return;
}
inventoryDetailList.value[currentEditingRowIndex.value].rfidTagIds = selectedTagCodes
inventoryDetailList.value[currentEditingRowIndex.value].rfidTagCodes = selectedTagCodes
} }
currentEditingRowIndex.value = -1 currentEditingRowIndex.value = -1
} }
...@@ -218,7 +259,19 @@ function openPtlTagSelect(index) { ...@@ -218,7 +259,19 @@ function openPtlTagSelect(index) {
// 处理PTL标签选择确认 // 处理PTL标签选择确认
function handlePtlTagConfirm(selectedTags) { function handlePtlTagConfirm(selectedTags) {
if (currentEditingRowIndex.value >= 0 && inventoryDetailList.value[currentEditingRowIndex.value]) { if (currentEditingRowIndex.value >= 0 && inventoryDetailList.value[currentEditingRowIndex.value]) {
inventoryDetailList.value[currentEditingRowIndex.value].storageLocation = selectedTags.map(tag => tag.antennaId).join(',') const selectedTagIds = selectedTags.map(tag => tag.antennaId).join(',')
// 检查是否有其他行使用了相同的PTL标签
const duplicateRow = inventoryDetailList.value.find((item, index) =>
index !== currentEditingRowIndex.value && item.storageLocation === selectedTagIds
);
if (duplicateRow) {
proxy.$message.warning('该PTL标签已被其他明细行使用,请选择其他标签');
return;
}
inventoryDetailList.value[currentEditingRowIndex.value].storageLocation = selectedTagIds
} }
currentEditingRowIndex.value = -1 currentEditingRowIndex.value = -1
} }
......
...@@ -242,6 +242,12 @@ ...@@ -242,6 +242,12 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="actualQuantity" label="实际数量" width="100">
<template #default="scope">
<el-input v-model.number="scope.row.actualQuantity" placeholder="请输入实际数量" size="small"
@change="calculateAmount(scope.row)" type="number" :precision="3" :step="0.001" />
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="80"> <el-table-column prop="unit" label="单位" width="80">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.unit" placeholder="请输入单位" size="small" disabled /> <el-input v-model="scope.row.unit" placeholder="请输入单位" size="small" disabled />
...@@ -311,21 +317,8 @@ ...@@ -311,21 +317,8 @@
<el-input v-model="scope.row.unqualifiedReason" placeholder="请输入不合格原因" size="small" /> <el-input v-model="scope.row.unqualifiedReason" placeholder="请输入不合格原因" size="small" />
</template> </template>
</el-table-column> --> </el-table-column> -->
<el-table-column prop="locationId" label="货架" width="300">
<template #default="scope">
<LocationCascaderSelect
:model-value="{warehouseId: form.warehouseId, areaId: scope.row.areaId, locationId: scope.row.locationId}"
@update:model-value="(value) => {
scope.row.warehouseId = value.warehouseId;
scope.row.areaId = value.areaId;
scope.row.locationId = value.locationId;
}"
/> <el-table-column prop="remark" label="备注">
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="150">
<template #default="scope"> <template #default="scope">
<el-input type="textarea" v-model="scope.row.remark" placeholder="请输入备注" size="small" /> <el-input type="textarea" v-model="scope.row.remark" placeholder="请输入备注" size="small" />
</template> </template>
...@@ -719,6 +712,13 @@ function handleMaterialConfirm(selectedMaterials) { ...@@ -719,6 +712,13 @@ function handleMaterialConfirm(selectedMaterials) {
// 将选中的物资添加到明细列表 // 将选中的物资添加到明细列表
selectedMaterials.forEach(material => { selectedMaterials.forEach(material => {
// 检查物资ID是否已经存在于明细列表中
const existingItem = form.value.items.find(item => item.materialId === material.materialId)
if (existingItem) {
proxy.$modal.msgWarning(`物资"${material.materialName}"已存在于明细列表中,请勿重复添加`)
return
}
const newItem = { const newItem = {
materialId: material.materialId, materialId: material.materialId,
materialName: material.materialName, materialName: material.materialName,
......
...@@ -33,18 +33,18 @@ ...@@ -33,18 +33,18 @@
</el-row> </el-row>
<el-table v-loading="loading" :data="wmsInventoryList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="wmsInventoryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" fixed="left" />
<el-table-column label="物资ID" align="center" prop="materialId" /> <el-table-column label="物资ID" align="center" prop="materialId" fixed="left" />
<!-- <el-table-column label="仓库ID" align="center" prop="warehouseId" /> --> <!-- <el-table-column label="仓库ID" align="center" prop="warehouseId" /> -->
<!-- <el-table-column label="库区ID" align="center" prop="areaId" /> --> <!-- <el-table-column label="库区ID" align="center" prop="areaId" /> -->
<el-table-column label="货架ID" align="center" prop="locationId" /> <el-table-column label="货架ID" align="center" prop="locationId" fixed="left" />
<el-table-column label="批次号" align="center" prop="batchNo" width="180" /> <el-table-column label="批次号" align="center" prop="batchNo" width="180" />
<el-table-column label="生产日期" align="center" prop="productionDate" width="180"> <el-table-column label="生产日期" align="center" prop="productionDate" width="180">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.productionDate, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.productionDate, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="效日期" align="center" prop="expirationDate" width="180"> <el-table-column label="效日期" align="center" prop="expirationDate" width="180">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.expirationDate, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.expirationDate, '{y}-{m}-{d}') }}</span>
</template> </template>
...@@ -70,19 +70,13 @@ ...@@ -70,19 +70,13 @@
<span>{{ parseTime(scope.row.lastOutboundTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.lastOutboundTime, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="RFID标签" align="center" prop="rfidTag" /> <el-table-column label="RFID标签" align="center" prop="rfidTagCodes">
<el-table-column label="是否FIFO管理" align="center" prop="isFifo">
<template #default="scope"> <template #default="scope">
<dict-tag :options="sys_yes_no" :value="scope.row.isFifo" /> <span>{{ scope.row.rfidTag || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="备注" align="center" prop="remark" /> <el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="RFID绑定数量" align="center" prop="rfidBindingCount" /> <el-table-column label="RFID绑定数量" align="center" prop="rfidBindingCount" />
<!-- <el-table-column label="最后RFID检测时间" align="center" prop="lastRfidDetectTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.lastRfidDetectTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column> -->
<el-table-column label="RFID覆盖率" align="center" prop="rfidCoverageRate" /> <el-table-column label="RFID覆盖率" align="center" prop="rfidCoverageRate" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right">
<template #default="scope"> <template #default="scope">
......
...@@ -9,110 +9,166 @@ ...@@ -9,110 +9,166 @@
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="物资ID" prop="materialId"> <el-form-item label="盘点计划ID" prop="planId">
<el-input <el-input
v-model="queryParams.materialId" v-model="queryParams.planId"
placeholder="请输入物资ID" placeholder="请输入盘点计划ID"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="货架ID" prop="locationId"> <el-form-item label="仓库ID" prop="warehouseId">
<el-input <el-input
v-model="queryParams.locationId" v-model="queryParams.warehouseId"
placeholder="请输入货架ID" placeholder="请输入仓库ID"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="批次号" prop="batchNo"> <el-form-item label="仓库名称" prop="warehouseName">
<el-input <el-input
v-model="queryParams.batchNo" v-model="queryParams.warehouseName"
placeholder="请输入批次号" placeholder="请输入仓库名称"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="系统数量" prop="systemQuantity"> <el-form-item label="库区ID" prop="areaId">
<el-input <el-input
v-model="queryParams.systemQuantity" v-model="queryParams.areaId"
placeholder="请输入系统数量" placeholder="请输入库区ID"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="实际数量" prop="actualQuantity"> <el-form-item label="库区名称" prop="areaName">
<el-input <el-input
v-model="queryParams.actualQuantity" v-model="queryParams.areaName"
placeholder="请输入实际数量" placeholder="请输入库区名称"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="差异数量" prop="differenceQuantity"> <el-form-item label="盘点员ID" prop="checkerId">
<el-input <el-input
v-model="queryParams.differenceQuantity" v-model="queryParams.checkerId"
placeholder="请输入差异数量" placeholder="请输入盘点员ID"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="差异率" prop="differenceRate"> <el-form-item label="盘点员姓名" prop="checkerName">
<el-input <el-input
v-model="queryParams.differenceRate" v-model="queryParams.checkerName"
placeholder="请输入差异率" placeholder="请输入盘点员姓名"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="差异原因" prop="differenceReason"> <el-form-item label="盘点开始时间" prop="startTime">
<el-date-picker clearable
v-model="queryParams.startTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择盘点开始时间">
</el-date-picker>
</el-form-item>
<el-form-item label="盘点结束时间" prop="endTime">
<el-date-picker clearable
v-model="queryParams.endTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择盘点结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="盘点总项数" prop="totalItems">
<el-input <el-input
v-model="queryParams.differenceReason" v-model="queryParams.totalItems"
placeholder="请输入差异原因" placeholder="请输入盘点总项数"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="盘点员ID" prop="checkerId"> <el-form-item label="已盘项数" prop="countedItems">
<el-input <el-input
v-model="queryParams.checkerId" v-model="queryParams.countedItems"
placeholder="请输入盘点员ID" placeholder="请输入已盘项数"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="盘点时间" prop="checkTime"> <el-form-item label="盘盈项数" prop="surplusItems">
<el-date-picker clearable <el-input
v-model="queryParams.checkTime" v-model="queryParams.surplusItems"
type="date" placeholder="请输入盘盈项数"
value-format="YYYY-MM-DD" clearable
placeholder="请选择盘点时间"> @keyup.enter="handleQuery"
</el-date-picker> />
</el-form-item>
<el-form-item label="盘亏项数" prop="deficitItems">
<el-input
v-model="queryParams.deficitItems"
placeholder="请输入盘亏项数"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="盘盈总量" prop="totalSurplus">
<el-input
v-model="queryParams.totalSurplus"
placeholder="请输入盘盈总量"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="盘亏总量" prop="totalDeficit">
<el-input
v-model="queryParams.totalDeficit"
placeholder="请输入盘亏总量"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="总差异价值" prop="totalDifferenceValue">
<el-input
v-model="queryParams.totalDifferenceValue"
placeholder="请输入总差异价值"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item> </el-form-item>
<el-form-item label="RFID标签" prop="rfidTag"> <el-form-item label="差异率" prop="differenceRate">
<el-input <el-input
v-model="queryParams.rfidTag" v-model="queryParams.differenceRate"
placeholder="请输入RFID标签" placeholder="请输入差异率"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="条形码" prop="barcode"> <el-form-item label="审核人ID" prop="auditorId">
<el-input <el-input
v-model="queryParams.barcode" v-model="queryParams.auditorId"
placeholder="请输入条形码" placeholder="请输入审核人ID"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="是否盲区" prop="isBlindZone"> <el-form-item label="审核人姓名" prop="auditorName">
<el-input <el-input
v-model="queryParams.isBlindZone" v-model="queryParams.auditorName"
placeholder="请输入是否盲区" placeholder="请输入审核人姓名"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="审核时间" prop="auditTime">
<el-date-picker clearable
v-model="queryParams.auditTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择审核时间">
</el-date-picker>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
...@@ -165,24 +221,41 @@ ...@@ -165,24 +221,41 @@
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="盘点结果ID" align="center" prop="resultId" /> <el-table-column label="盘点结果ID" align="center" prop="resultId" />
<el-table-column label="盘点任务ID" align="center" prop="taskId" /> <el-table-column label="盘点任务ID" align="center" prop="taskId" />
<el-table-column label="物资ID" align="center" prop="materialId" /> <el-table-column label="盘点计划ID" align="center" prop="planId" />
<el-table-column label="货架ID" align="center" prop="locationId" /> <el-table-column label="仓库ID" align="center" prop="warehouseId" />
<el-table-column label="批次号" align="center" prop="batchNo" /> <el-table-column label="仓库名称" align="center" prop="warehouseName" />
<el-table-column label="系统数量" align="center" prop="systemQuantity" /> <el-table-column label="库区ID" align="center" prop="areaId" />
<el-table-column label="实际数量" align="center" prop="actualQuantity" /> <el-table-column label="库区名称" align="center" prop="areaName" />
<el-table-column label="差异数量" align="center" prop="differenceQuantity" />
<el-table-column label="差异率" align="center" prop="differenceRate" />
<el-table-column label="差异原因" align="center" prop="differenceReason" />
<el-table-column label="质量状态" align="center" prop="qualityStatus" />
<el-table-column label="盘点员ID" align="center" prop="checkerId" /> <el-table-column label="盘点员ID" align="center" prop="checkerId" />
<el-table-column label="盘点时间" align="center" prop="checkTime" width="180"> <el-table-column label="盘点员姓名" align="center" prop="checkerName" />
<el-table-column label="盘点开始时间" align="center" prop="startTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="盘点结束时间" align="center" prop="endTime" width="180">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.checkTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="RFID标签" align="center" prop="rfidTag" /> <el-table-column label="盘点总项数" align="center" prop="totalItems" />
<el-table-column label="条形码" align="center" prop="barcode" /> <el-table-column label="已盘项数" align="center" prop="countedItems" />
<el-table-column label="是否盲区" align="center" prop="isBlindZone" /> <el-table-column label="盘盈项数" align="center" prop="surplusItems" />
<el-table-column label="盘亏项数" align="center" prop="deficitItems" />
<el-table-column label="盘盈总量" align="center" prop="totalSurplus" />
<el-table-column label="盘亏总量" align="center" prop="totalDeficit" />
<el-table-column label="总差异价值" align="center" prop="totalDifferenceValue" />
<el-table-column label="差异率" align="center" prop="differenceRate" />
<el-table-column label="盘点状态" align="center" prop="resultStatus" />
<el-table-column label="审核状态" align="center" prop="auditStatus" />
<el-table-column label="审核人ID" align="center" prop="auditorId" />
<el-table-column label="审核人姓名" align="center" prop="auditorName" />
<el-table-column label="审核时间" align="center" prop="auditTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.auditTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="审核意见" align="center" prop="auditOpinion" />
<el-table-column label="备注" align="center" prop="remark" /> <el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
...@@ -206,49 +279,83 @@ ...@@ -206,49 +279,83 @@
<el-form-item label="盘点任务ID" prop="taskId"> <el-form-item label="盘点任务ID" prop="taskId">
<el-input v-model="form.taskId" placeholder="请输入盘点任务ID" /> <el-input v-model="form.taskId" placeholder="请输入盘点任务ID" />
</el-form-item> </el-form-item>
<el-form-item label="物资ID" prop="materialId"> <el-form-item label="盘点计划ID" prop="planId">
<el-input v-model="form.materialId" placeholder="请输入物资ID" /> <el-input v-model="form.planId" placeholder="请输入盘点计划ID" />
</el-form-item>
<el-form-item label="仓库ID" prop="warehouseId">
<el-input v-model="form.warehouseId" placeholder="请输入仓库ID" />
</el-form-item>
<el-form-item label="仓库名称" prop="warehouseName">
<el-input v-model="form.warehouseName" placeholder="请输入仓库名称" />
</el-form-item> </el-form-item>
<el-form-item label="货架ID" prop="locationId"> <el-form-item label="库区ID" prop="areaId">
<el-input v-model="form.locationId" placeholder="请输入货架ID" /> <el-input v-model="form.areaId" placeholder="请输入库区ID" />
</el-form-item> </el-form-item>
<el-form-item label="批次号" prop="batchNo"> <el-form-item label="库区名称" prop="areaName">
<el-input v-model="form.batchNo" placeholder="请输入批次号" /> <el-input v-model="form.areaName" placeholder="请输入库区名称" />
</el-form-item> </el-form-item>
<el-form-item label="系统数量" prop="systemQuantity"> <el-form-item label="盘点员ID" prop="checkerId">
<el-input v-model="form.systemQuantity" placeholder="请输入系统数量" /> <el-input v-model="form.checkerId" placeholder="请输入盘点员ID" />
</el-form-item> </el-form-item>
<el-form-item label="实际数量" prop="actualQuantity"> <el-form-item label="盘点员姓名" prop="checkerName">
<el-input v-model="form.actualQuantity" placeholder="请输入实际数量" /> <el-input v-model="form.checkerName" placeholder="请输入盘点员姓名" />
</el-form-item> </el-form-item>
<el-form-item label="差异数量" prop="differenceQuantity"> <el-form-item label="盘点开始时间" prop="startTime">
<el-input v-model="form.differenceQuantity" placeholder="请输入差异数量" /> <el-date-picker clearable
v-model="form.startTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择盘点开始时间">
</el-date-picker>
</el-form-item>
<el-form-item label="盘点结束时间" prop="endTime">
<el-date-picker clearable
v-model="form.endTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择盘点结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="盘点总项数" prop="totalItems">
<el-input v-model="form.totalItems" placeholder="请输入盘点总项数" />
</el-form-item>
<el-form-item label="已盘项数" prop="countedItems">
<el-input v-model="form.countedItems" placeholder="请输入已盘项数" />
</el-form-item>
<el-form-item label="盘盈项数" prop="surplusItems">
<el-input v-model="form.surplusItems" placeholder="请输入盘盈项数" />
</el-form-item>
<el-form-item label="盘亏项数" prop="deficitItems">
<el-input v-model="form.deficitItems" placeholder="请输入盘亏项数" />
</el-form-item>
<el-form-item label="盘盈总量" prop="totalSurplus">
<el-input v-model="form.totalSurplus" placeholder="请输入盘盈总量" />
</el-form-item>
<el-form-item label="盘亏总量" prop="totalDeficit">
<el-input v-model="form.totalDeficit" placeholder="请输入盘亏总量" />
</el-form-item>
<el-form-item label="总差异价值" prop="totalDifferenceValue">
<el-input v-model="form.totalDifferenceValue" placeholder="请输入总差异价值" />
</el-form-item> </el-form-item>
<el-form-item label="差异率" prop="differenceRate"> <el-form-item label="差异率" prop="differenceRate">
<el-input v-model="form.differenceRate" placeholder="请输入差异率" /> <el-input v-model="form.differenceRate" placeholder="请输入差异率" />
</el-form-item> </el-form-item>
<el-form-item label="差异原因" prop="differenceReason"> <el-form-item label="审核人ID" prop="auditorId">
<el-input v-model="form.differenceReason" placeholder="请输入差异原因" /> <el-input v-model="form.auditorId" placeholder="请输入审核人ID" />
</el-form-item> </el-form-item>
<el-form-item label="盘点员ID" prop="checkerId"> <el-form-item label="审核人姓名" prop="auditorName">
<el-input v-model="form.checkerId" placeholder="请输入盘点员ID" /> <el-input v-model="form.auditorName" placeholder="请输入审核人姓名" />
</el-form-item> </el-form-item>
<el-form-item label="盘点时间" prop="checkTime"> <el-form-item label="审核时间" prop="auditTime">
<el-date-picker clearable <el-date-picker clearable
v-model="form.checkTime" v-model="form.auditTime"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择盘点时间"> placeholder="请选择审核时间">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
<el-form-item label="RFID标签" prop="rfidTag"> <el-form-item label="审核意见" prop="auditOpinion">
<el-input v-model="form.rfidTag" placeholder="请输入RFID标签" /> <el-input v-model="form.auditOpinion" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="条形码" prop="barcode">
<el-input v-model="form.barcode" placeholder="请输入条形码" />
</el-form-item>
<el-form-item label="是否盲区" prop="isBlindZone">
<el-input v-model="form.isBlindZone" placeholder="请输入是否盲区" />
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
...@@ -285,34 +392,34 @@ const data = reactive({ ...@@ -285,34 +392,34 @@ const data = reactive({
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
taskId: null, taskId: null,
materialId: null, planId: null,
locationId: null, warehouseId: null,
batchNo: null, warehouseName: null,
systemQuantity: null, areaId: null,
actualQuantity: null, areaName: null,
differenceQuantity: null,
differenceRate: null,
differenceReason: null,
qualityStatus: null,
checkerId: null, checkerId: null,
checkTime: null, checkerName: null,
rfidTag: null, startTime: null,
barcode: null, endTime: null,
isBlindZone: null, totalItems: null,
countedItems: null,
surplusItems: null,
deficitItems: null,
totalSurplus: null,
totalDeficit: null,
totalDifferenceValue: null,
differenceRate: null,
resultStatus: null,
auditStatus: null,
auditorId: null,
auditorName: null,
auditTime: null,
auditOpinion: null,
}, },
rules: { rules: {
taskId: [ taskId: [
{ required: true, message: "盘点任务ID不能为空", trigger: "blur" } { required: true, message: "盘点任务ID不能为空", trigger: "blur" }
], ],
materialId: [
{ required: true, message: "物资ID不能为空", trigger: "blur" }
],
systemQuantity: [
{ required: true, message: "系统数量不能为空", trigger: "blur" }
],
actualQuantity: [
{ required: true, message: "实际数量不能为空", trigger: "blur" }
],
} }
}) })
...@@ -339,21 +446,34 @@ function reset() { ...@@ -339,21 +446,34 @@ function reset() {
form.value = { form.value = {
resultId: null, resultId: null,
taskId: null, taskId: null,
materialId: null, planId: null,
locationId: null, warehouseId: null,
batchNo: null, warehouseName: null,
systemQuantity: null, areaId: null,
actualQuantity: null, areaName: null,
differenceQuantity: null,
differenceRate: null,
differenceReason: null,
qualityStatus: null,
checkerId: null, checkerId: null,
checkTime: null, checkerName: null,
rfidTag: null, startTime: null,
barcode: null, endTime: null,
isBlindZone: null, totalItems: null,
remark: null countedItems: null,
surplusItems: null,
deficitItems: null,
totalSurplus: null,
totalDeficit: null,
totalDifferenceValue: null,
differenceRate: null,
resultStatus: null,
auditStatus: null,
auditorId: null,
auditorName: null,
auditTime: null,
auditOpinion: null,
remark: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null
} }
proxy.resetForm("wmsInventoryResultRef") proxy.resetForm("wmsInventoryResultRef")
} }
......
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="任务ID" prop="taskId">
<el-input
v-model="queryParams.taskId"
placeholder="请输入任务ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="库存ID" prop="inventoryId">
<el-input
v-model="queryParams.inventoryId"
placeholder="请输入库存ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="物资ID" prop="materialId">
<el-input
v-model="queryParams.materialId"
placeholder="请输入物资ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="仓库ID" prop="warehouseId">
<el-input
v-model="queryParams.warehouseId"
placeholder="请输入仓库ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="仓库名称" prop="warehouseName">
<el-input
v-model="queryParams.warehouseName"
placeholder="请输入仓库名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="库区ID" prop="areaId">
<el-input
v-model="queryParams.areaId"
placeholder="请输入库区ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="库区名称" prop="areaName">
<el-input
v-model="queryParams.areaName"
placeholder="请输入库区名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="货架ID" prop="locationId">
<el-input
v-model="queryParams.locationId"
placeholder="请输入货架ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="货架名称" prop="locationName">
<el-input
v-model="queryParams.locationName"
placeholder="请输入货架名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="批次号" prop="batchNo">
<el-input
v-model="queryParams.batchNo"
placeholder="请输入批次号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="系统数量" prop="systemQuantity">
<el-input
v-model="queryParams.systemQuantity"
placeholder="请输入系统数量"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="实际数量" prop="actualQuantity">
<el-input
v-model="queryParams.actualQuantity"
placeholder="请输入实际数量"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="差异数量" prop="differenceQuantity">
<el-input
v-model="queryParams.differenceQuantity"
placeholder="请输入差异数量"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="差异率" prop="differenceRate">
<el-input
v-model="queryParams.differenceRate"
placeholder="请输入差异率"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="盘点员ID" prop="checkerId">
<el-input
v-model="queryParams.checkerId"
placeholder="请输入盘点员ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="盘点员姓名" prop="checkerName">
<el-input
v-model="queryParams.checkerName"
placeholder="请输入盘点员姓名"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="盘点时间" prop="checkTime">
<el-date-picker clearable
v-model="queryParams.checkTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择盘点时间">
</el-date-picker>
</el-form-item>
<el-form-item label="RFID标签" prop="rfidTag">
<el-input
v-model="queryParams.rfidTag"
placeholder="请输入RFID标签"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="条形码" prop="barcode">
<el-input
v-model="queryParams.barcode"
placeholder="请输入条形码"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="是否盲区" prop="isBlindZone">
<el-input
v-model="queryParams.isBlindZone"
placeholder="请输入是否盲区"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['ware:wmsInventoryTaskDetail:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['ware:wmsInventoryTaskDetail:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['ware:wmsInventoryTaskDetail:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['ware:wmsInventoryTaskDetail:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="wmsInventoryTaskDetailList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="明细ID" align="center" prop="detailId" />
<el-table-column label="任务ID" align="center" prop="taskId" />
<el-table-column label="库存ID" align="center" prop="inventoryId" />
<el-table-column label="物资ID" align="center" prop="materialId" />
<el-table-column label="仓库ID" align="center" prop="warehouseId" />
<el-table-column label="仓库名称" align="center" prop="warehouseName" />
<el-table-column label="库区ID" align="center" prop="areaId" />
<el-table-column label="库区名称" align="center" prop="areaName" />
<el-table-column label="货架ID" align="center" prop="locationId" />
<el-table-column label="货架名称" align="center" prop="locationName" />
<el-table-column label="批次号" align="center" prop="batchNo" />
<el-table-column label="系统数量" align="center" prop="systemQuantity" />
<el-table-column label="实际数量" align="center" prop="actualQuantity" />
<el-table-column label="差异数量" align="center" prop="differenceQuantity" />
<el-table-column label="差异率" align="center" prop="differenceRate" />
<el-table-column label="差异原因" align="center" prop="differenceReason" />
<el-table-column label="盘点状态" align="center" prop="checkStatus" />
<el-table-column label="盘点员ID" align="center" prop="checkerId" />
<el-table-column label="盘点员姓名" align="center" prop="checkerName" />
<el-table-column label="盘点时间" align="center" prop="checkTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.checkTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="RFID标签" align="center" prop="rfidTag" />
<el-table-column label="条形码" align="center" prop="barcode" />
<el-table-column label="是否盲区" align="center" prop="isBlindZone" />
<el-table-column label="质量状态" align="center" prop="qualityStatus" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['ware:wmsInventoryTaskDetail:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['ware:wmsInventoryTaskDetail:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改盘点任务明细对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="wmsInventoryTaskDetailRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="任务ID" prop="taskId">
<el-input v-model="form.taskId" placeholder="请输入任务ID" />
</el-form-item>
<el-form-item label="库存ID" prop="inventoryId">
<el-input v-model="form.inventoryId" placeholder="请输入库存ID" />
</el-form-item>
<el-form-item label="物资ID" prop="materialId">
<el-input v-model="form.materialId" placeholder="请输入物资ID" />
</el-form-item>
<el-form-item label="仓库ID" prop="warehouseId">
<el-input v-model="form.warehouseId" placeholder="请输入仓库ID" />
</el-form-item>
<el-form-item label="仓库名称" prop="warehouseName">
<el-input v-model="form.warehouseName" placeholder="请输入仓库名称" />
</el-form-item>
<el-form-item label="库区ID" prop="areaId">
<el-input v-model="form.areaId" placeholder="请输入库区ID" />
</el-form-item>
<el-form-item label="库区名称" prop="areaName">
<el-input v-model="form.areaName" placeholder="请输入库区名称" />
</el-form-item>
<el-form-item label="货架ID" prop="locationId">
<el-input v-model="form.locationId" placeholder="请输入货架ID" />
</el-form-item>
<el-form-item label="货架名称" prop="locationName">
<el-input v-model="form.locationName" placeholder="请输入货架名称" />
</el-form-item>
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="form.batchNo" placeholder="请输入批次号" />
</el-form-item>
<el-form-item label="系统数量" prop="systemQuantity">
<el-input v-model="form.systemQuantity" placeholder="请输入系统数量" />
</el-form-item>
<el-form-item label="实际数量" prop="actualQuantity">
<el-input v-model="form.actualQuantity" placeholder="请输入实际数量" />
</el-form-item>
<el-form-item label="差异数量" prop="differenceQuantity">
<el-input v-model="form.differenceQuantity" placeholder="请输入差异数量" />
</el-form-item>
<el-form-item label="差异率" prop="differenceRate">
<el-input v-model="form.differenceRate" placeholder="请输入差异率" />
</el-form-item>
<el-form-item label="差异原因" prop="differenceReason">
<el-input v-model="form.differenceReason" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="盘点员ID" prop="checkerId">
<el-input v-model="form.checkerId" placeholder="请输入盘点员ID" />
</el-form-item>
<el-form-item label="盘点员姓名" prop="checkerName">
<el-input v-model="form.checkerName" placeholder="请输入盘点员姓名" />
</el-form-item>
<el-form-item label="盘点时间" prop="checkTime">
<el-date-picker clearable
v-model="form.checkTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择盘点时间">
</el-date-picker>
</el-form-item>
<el-form-item label="RFID标签" prop="rfidTag">
<el-input v-model="form.rfidTag" placeholder="请输入RFID标签" />
</el-form-item>
<el-form-item label="条形码" prop="barcode">
<el-input v-model="form.barcode" placeholder="请输入条形码" />
</el-form-item>
<el-form-item label="是否盲区" prop="isBlindZone">
<el-input v-model="form.isBlindZone" placeholder="请输入是否盲区" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="WmsInventoryTaskDetail">
import { listWmsInventoryTaskDetail, getWmsInventoryTaskDetail, delWmsInventoryTaskDetail, addWmsInventoryTaskDetail, updateWmsInventoryTaskDetail } from "@/api/ware/wmsInventoryTaskDetail"
const { proxy } = getCurrentInstance()
const wmsInventoryTaskDetailList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
taskId: null,
inventoryId: null,
materialId: null,
warehouseId: null,
warehouseName: null,
areaId: null,
areaName: null,
locationId: null,
locationName: null,
batchNo: null,
systemQuantity: null,
actualQuantity: null,
differenceQuantity: null,
differenceRate: null,
differenceReason: null,
checkStatus: null,
checkerId: null,
checkerName: null,
checkTime: null,
rfidTag: null,
barcode: null,
isBlindZone: null,
qualityStatus: null,
},
rules: {
taskId: [
{ required: true, message: "任务ID不能为空", trigger: "blur" }
],
materialId: [
{ required: true, message: "物资ID不能为空", trigger: "blur" }
],
warehouseId: [
{ required: true, message: "仓库ID不能为空", trigger: "blur" }
],
areaId: [
{ required: true, message: "库区ID不能为空", trigger: "blur" }
],
locationId: [
{ required: true, message: "货架ID不能为空", trigger: "blur" }
],
systemQuantity: [
{ required: true, message: "系统数量不能为空", trigger: "blur" }
],
}
})
const { queryParams, form, rules } = toRefs(data)
/** 查询盘点任务明细列表 */
function getList() {
loading.value = true
listWmsInventoryTaskDetail(queryParams.value).then(response => {
wmsInventoryTaskDetailList.value = response.rows
total.value = response.total
loading.value = false
})
}
// 取消按钮
function cancel() {
open.value = false
reset()
}
// 表单重置
function reset() {
form.value = {
detailId: null,
taskId: null,
inventoryId: null,
materialId: null,
warehouseId: null,
warehouseName: null,
areaId: null,
areaName: null,
locationId: null,
locationName: null,
batchNo: null,
systemQuantity: null,
actualQuantity: null,
differenceQuantity: null,
differenceRate: null,
differenceReason: null,
checkStatus: null,
checkerId: null,
checkerName: null,
checkTime: null,
rfidTag: null,
barcode: null,
isBlindZone: null,
qualityStatus: null,
remark: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null
}
proxy.resetForm("wmsInventoryTaskDetailRef")
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef")
handleQuery()
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.detailId)
single.value = selection.length != 1
multiple.value = !selection.length
}
/** 新增按钮操作 */
function handleAdd() {
reset()
open.value = true
title.value = "添加盘点任务明细"
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const _detailId = row.detailId || ids.value
getWmsInventoryTaskDetail(_detailId).then(response => {
form.value = response.data
open.value = true
title.value = "修改盘点任务明细"
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["wmsInventoryTaskDetailRef"].validate(valid => {
if (valid) {
if (form.value.detailId != null) {
updateWmsInventoryTaskDetail(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
getList()
})
} else {
addWmsInventoryTaskDetail(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功")
open.value = false
getList()
})
}
}
})
}
/** 删除按钮操作 */
function handleDelete(row) {
const _detailIds = row.detailId || ids.value
proxy.$modal.confirm('是否确认删除盘点任务明细编号为"' + _detailIds + '"的数据项?').then(function() {
return delWmsInventoryTaskDetail(_detailIds)
}).then(() => {
getList()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('ware/wmsInventoryTaskDetail/export', {
...queryParams.value
}, `wmsInventoryTaskDetail_${new Date().getTime()}.xlsx`)
}
getList()
</script>
...@@ -28,12 +28,6 @@ ...@@ -28,12 +28,6 @@
<el-button slot="append" icon="Search" @click="handleSupplierSelect('query')">选择</el-button> <el-button slot="append" icon="Search" @click="handleSupplierSelect('query')">选择</el-button>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="是否RFID" prop="isRfidManaged">
<el-select v-model="queryParams.isRfidManaged" placeholder="请选择是否RFID" clearable @keyup.enter="handleQuery">
<el-option v-for="item in custom_yes_no" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
......
<template>
<div class="outbound-manager-container">
<!-- 顶部导航和操作栏 -->
<div class="header-section">
<el-breadcrumb separator="/">
<el-breadcrumb-item>仓库管理</el-breadcrumb-item>
<el-breadcrumb-item>出库作业</el-breadcrumb-item>
<el-breadcrumb-item>批量出库</el-breadcrumb-item>
</el-breadcrumb>
<div class="header-actions">
<el-button type="primary" @click="createNewOrder">
<el-icon><Plus /></el-icon>新建出库单
</el-button>
<el-button @click="refreshData">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
<!-- 标签页:出库单列表 和 出库执行 -->
<el-tabs v-model="activeTab" type="border-card" class="main-tabs">
<!-- 标签页1: 出库单列表 -->
<el-tab-pane label="出库单管理" name="orderList">
<OrderList
:orders="orders"
@select-order="handleSelectOrder"
@edit-order="handleEditOrder"
@delete-order="handleDeleteOrder"
@start-outbound="handleStartOutbound"
/>
</el-tab-pane>
<!-- 标签页2: 出库执行 (仅在选中订单后可用) -->
<el-tab-pane
label="出库执行"
name="outboundExecution"
:disabled="!selectedOrder"
>
<OutboundExecution
v-if="selectedOrder"
:order="selectedOrder"
:key="selectedOrder.id"
@order-updated="handleOrderUpdated"
@operation-log="handleOperationLog"
/>
</el-tab-pane>
<!-- 标签页3: 操作日志 -->
<el-tab-pane label="操作日志" name="operationLogs">
<OperationLogs :logs="operationLogs" />
</el-tab-pane>
</el-tabs>
<!-- 新建/编辑出库单对话框 -->
<OrderDialog
v-model="showOrderDialog"
:order="editingOrder"
mode="create"
@save="handleSaveOrder"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh } from '@element-plus/icons-vue'
import OrderList from './components/OrderList.vue'
import OutboundExecution from './components/OutboundExecution.vue'
import OperationLogs from './components/OperationLogs.vue'
import OrderDialog from './components/OrderDialog.vue'
import {
fetchOrders,
deleteOrder,
createOrder,
fetchOperationLogs
} from './components/mockApi'
// 响应式数据
const activeTab = ref('orderList')
const orders = ref([])
const selectedOrder = ref(null)
const showOrderDialog = ref(false)
const editingOrder = ref(null)
const operationLogs = ref([])
// 生命周期
onMounted(() => {
loadOrders()
loadOperationLogs()
})
// 方法
const loadOrders = async () => {
try {
orders.value = await fetchOrders()
} catch (error) {
ElMessage.error('加载出库单失败')
}
}
const loadOperationLogs = async () => {
try {
operationLogs.value = await fetchOperationLogs()
} catch (error) {
console.error('加载操作日志失败:', error)
}
}
const refreshData = () => {
loadOrders()
loadOperationLogs()
ElMessage.success('数据已刷新')
}
const createNewOrder = () => {
editingOrder.value = null
showOrderDialog.value = true
}
const handleSelectOrder = (order) => {
selectedOrder.value = order
activeTab.value = 'outboundExecution'
}
const handleEditOrder = (order) => {
editingOrder.value = { ...order }
showOrderDialog.value = true
}
const handleDeleteOrder = async (order) => {
try {
await ElMessageBox.confirm(
`确定要删除出库单 ${order.orderNo} 吗?`,
'确认删除',
{ type: 'warning' }
)
await deleteOrder(order.id)
ElMessage.success('删除成功')
loadOrders()
} catch (error) {
// 用户取消
}
}
const handleStartOutbound = (order) => {
selectedOrder.value = order
activeTab.value = 'outboundExecution'
}
const handleSaveOrder = async (orderData) => {
try {
await createOrder(orderData)
ElMessage.success('出库单创建成功')
showOrderDialog.value = false
loadOrders()
} catch (error) {
ElMessage.error('保存失败')
}
}
const handleOrderUpdated = () => {
loadOrders()
}
const handleOperationLog = (log) => {
operationLogs.value.unshift(log)
}
</script>
<style scoped>
.outbound-manager-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fa;
padding: 20px;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 0 10px;
}
.header-actions {
display: flex;
gap: 10px;
}
.main-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
:deep(.el-tabs__content) {
flex: 1;
overflow: auto;
}
</style>
\ No newline at end of file
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="1200px"
:close-on-click-modal="false"
>
<!-- 物资信息摘要 -->
<div v-if="detail" class="material-summary">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="物资编码">
{{ detail.materialCode }}
</el-descriptions-item>
<el-descriptions-item label="物资名称">
{{ detail.materialName }}
</el-descriptions-item>
<el-descriptions-item label="规格型号">
{{ detail.specification }}
</el-descriptions-item>
<el-descriptions-item label="计划数量">
<span class="planned-quantity">{{ detail.plannedQuantity }}</span>
</el-descriptions-item>
<el-descriptions-item label="实际出库">
<span :class="{
'completed': detail.actualQuantity >= detail.plannedQuantity,
'in-progress': detail.actualQuantity > 0 && detail.actualQuantity < detail.plannedQuantity
}">
{{ detail.actualQuantity }}
</span>
</el-descriptions-item>
<el-descriptions-item label="剩余需求">
<span class="remaining-need">
{{ Math.max(0, detail.plannedQuantity - detail.actualQuantity) }}
</span>
</el-descriptions-item>
</el-descriptions>
<el-progress
:percentage="completionPercentage"
:status="completionPercentage >= 100 ? 'success' : 'primary'"
:stroke-width="12"
style="margin-top: 15px;"
/>
</div>
<!-- 批次出库记录 -->
<div class="batch-transactions">
<div class="section-header">
<h4>批次出库记录</h4>
<span class="total-info">
{{ transactions.length }} 个批次
</span>
</div>
<el-table
:data="transactions"
stripe
border
size="small"
height="300px"
>
<el-table-column type="selection" width="55" align="center" fixed="left" />
<el-table-column type="index" label="序号" width="60" align="center" fixed="left" />
<el-table-column label="关联ID" align="center" prop="relationId" />
<el-table-column label="出库明细ID" align="center" prop="itemId" />
<el-table-column label="出库单ID" align="center" prop="orderId" />
<el-table-column label="库存ID" align="center" prop="inventoryId" />
<el-table-column label="RFID标签ID" align="center" prop="rfidTagId" />
<el-table-column label="RFID编码" align="center" prop="rfidCode" />
<el-table-column label="EPC编码" align="center" prop="epcCode" />
<el-table-column label="TID编码" align="center" prop="tidCode" />
<el-table-column label="物资ID" align="center" prop="materialId" />
<el-table-column label="批次号" align="center" prop="batchNo" width="120" />
<el-table-column label="仓库id" align="center" prop="warehouseId" />
<el-table-column label="库区id" align="center" prop="areaId" />
<el-table-column label="库位ID" align="center" prop="locationId" />
<el-table-column label="数量" align="center" prop="quantity" />
<el-table-column label="单位成本" align="center" prop="unitCost" />
<el-table-column label="出库时间" align="center" prop="outboundTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.outboundTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="是否已撤销" align="center" prop="isCanceled" />
<el-table-column label="撤销时间" align="center" prop="cancelTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.cancelTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="撤销人" align="center" prop="cancelBy" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['ware:wmsOutboundItemInventory:remove']">撤销</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 汇总信息 -->
<div class="summary-section">
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="批次总数">
<span class="summary-value">{{ batchCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="正常出库">
<span class="summary-value success">
{{ normalTransactionCount }}
</span>
</el-descriptions-item>
<el-descriptions-item label="已撤销">
<span class="summary-value danger">
{{ reversedTransactionCount }}
</span>
</el-descriptions-item>
<el-descriptions-item label="合计数量">
<span class="summary-value highlight">
{{ totalAmount }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">关闭</el-button>
<el-button
v-if="detail?.actualQuantity < detail?.plannedQuantity"
type="primary"
@click="handleContinueOutbound"
>
继续出库
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { listWmsOutboundItemInventory } from "@/api/ware/wmsOutboundItemInventory"
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const detail = ref({})
const transactions = ref([])
const emit = defineEmits()
// 响应式数据
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 计算属性
const dialogTitle = computed(() => {
if (!detail.value) return '批次详情'
return `${detail.value.materialName} - 批次出库详情`
})
const completionPercentage = computed(() => {
if (!detail.value || detail.value.plannedQuantity === 0) return 0
return Math.min((detail.value.actualQuantity / detail.value.plannedQuantity) * 100, 100)
})
const batchCount = computed(() => {
const batchNos = new Set(transactions.value.map(t => t.batchNo))
return batchNos.size
})
const normalTransactionCount = computed(() => {
return transactions.value.filter(t => t.isCanceled === '0').length
})
const reversedTransactionCount = computed(() => {
return transactions.value.filter(t => t.status === 'REVERSED').length
})
const totalAmount = computed(() => {
return transactions.value
.filter(t => t.isCanceled === '0')
.reduce((sum, t) => sum + t.quantity, 0)
})
// 方法
const formatTime = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const formatAmount = (amount) => {
return amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
const handleContinueOutbound = () => {
emit('continue-outbound')
visible.value = false
}
const openDialog = (row) => {
detail.value = row
listWmsOutboundItemInventory({
orderId: row.orderId,
pageNum: 1,
pageSize: 1000
}).then(res => {
transactions.value = res.rows || []
})
}
defineExpose({
openDialog
})
</script>
<style scoped>
.material-summary {
margin-bottom: 20px;
padding: 15px;
background: #f8fafc;
border-radius: 6px;
}
.planned-quantity {
font-weight: 600;
color: #303133;
}
.completed {
color: #67c23a;
font-weight: 600;
}
.in-progress {
color: #e6a23c;
font-weight: 600;
}
.remaining-need {
color: #f56c6c;
font-weight: 600;
}
.batch-transactions {
margin-bottom: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.section-header h4 {
margin: 0;
color: #303133;
font-weight: 600;
}
.total-info {
color: #606266;
font-size: 14px;
}
.batch-no-cell {
display: flex;
align-items: center;
gap: 8px;
}
.batch-no {
font-family: 'Monaco', 'Consolas', monospace;
font-size: 13px;
}
.quantity-cell {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
.quantity-cell.reversed {
text-decoration: line-through;
color: #f56c6c;
}
.amount-cell.reversed {
text-decoration: line-through;
color: #f56c6c;
}
.summary-section {
margin-top: 20px;
}
.summary-value {
font-weight: 500;
color: #303133;
}
.summary-value.success {
color: #67c23a;
}
.summary-value.danger {
color: #f56c6c;
}
.summary-value.highlight {
color: #f56c6c;
font-size: 16px;
font-weight: 600;
}
:deep(.el-descriptions__label) {
width: 80px;
font-weight: 500;
}
:deep(.el-descriptions__content) {
font-weight: 400;
}
</style>
\ No newline at end of file
<template>
<el-dialog
v-model="visible"
title="智能批次推荐"
width="700px"
:close-on-click-modal="false"
>
<!-- 推荐策略选择 -->
<div class="strategy-section">
<el-radio-group v-model="selectedStrategy" size="small">
<el-radio-button label="FIFO">先进先出</el-radio-button>
<el-radio-button label="FEFO">先过期先出</el-radio-button>
<el-radio-button label="NEAREST">最近库位</el-radio-button>
<el-radio-button label="MANUAL">手动选择</el-radio-button>
</el-radio-group>
<div class="strategy-description">
<el-icon><InfoFilled /></el-icon>
<span>{{ strategyDescription }}</span>
</div>
</div>
<!-- 推荐结果 -->
<div class="recommendation-section">
<div class="section-header">
<h4>推荐批次列表</h4>
<el-button
type="primary"
size="small"
@click="applyAllRecommendations"
:disabled="!canApplyAll"
>
一键应用全部推荐
</el-button>
</div>
<el-table
:data="recommendations"
stripe
border
size="small"
height="350px"
v-loading="loading"
>
<el-table-column type="index" label="推荐" width="60" align="center">
<template #default="{ $index }">
<el-tag
v-if="$index < 3"
:type="getRankTagType($index)"
effect="dark"
round
>
{{ $index + 1 }}
</el-tag>
<span v-else class="rank-text">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="150">
<template #default="{ row }">
<div class="batch-info">
<span class="batch-no">{{ row.batchNo }}</span>
<el-tag
v-if="isNearExpiry(row.expiryDate)"
type="warning"
size="mini"
>
临期
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="库存信息" width="120">
<template #default="{ row }">
<div class="stock-info">
<div class="current-stock">
库存: <strong>{{ row.currentStock }}</strong>
</div>
<div class="recommended">
推荐: <strong class="recommended-quantity">{{ row.recommendedQuantity }}</strong>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="location" label="库位" width="100">
<template #default="{ row }">
<el-tag size="small" type="primary">
{{ row.location }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="效期" width="120">
<template #default="{ row }">
<div class="expiry-info" :class="{
'expiry-near': isNearExpiry(row.expiryDate),
'expiry-urgent': isUrgentExpiry(row.expiryDate)
}">
{{ formatDate(row.expiryDate) }}
<el-icon
v-if="isNearExpiry(row.expiryDate)"
color="#e6a23c"
size="12"
>
<Warning />
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column prop="priorityScore" label="推荐度" width="100" align="center">
<template #default="{ row }">
<el-progress
:percentage="row.priorityScore"
:stroke-width="10"
:show-text="false"
/>
<span class="score-text">{{ row.priorityScore }}</span>
</template>
</el-table-column>
<el-table-column label="推荐理由">
<template #default="{ row }">
<div class="reasons">
<el-tag
v-for="(reason, index) in row.reason"
:key="index"
size="mini"
type="info"
effect="plain"
class="reason-tag"
>
{{ reason }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="selectBatch(row)"
>
选择
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 推荐说明 -->
<div class="explanation-section">
<el-collapse v-model="activeExplanation">
<el-collapse-item title="推荐说明" name="explanation">
<div class="explanation-content">
<p><strong>先进先出(FIFO)</strong>: 优先出库生产日期早的批次,减少库存积压</p>
<p><strong>先过期先出(FEFO)</strong>: 优先出库效期近的批次,减少过期损失</p>
<p><strong>最近库位</strong>: 优先出库距离操作台近的批次,提高作业效率</p>
<p><strong>手动选择</strong>: 不进行自动推荐,由操作员自主选择</p>
</div>
</el-collapse-item>
</el-collapse>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
:loading="applying"
@click="applyRecommendations"
>
应用推荐
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { InfoFilled, Warning } from '@element-plus/icons-vue'
const props = defineProps()
const emit = defineEmits()
// 响应式数据
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const selectedStrategy = ref('FEFO')
const loading = ref(false)
const applying = ref(false)
const activeExplanation = ref(['explanation'])
// 模拟推荐数据
const recommendations = ref([
{
batchId: 'B001',
batchNo: 'BATCH-202312-001',
materialId: 'MAT001',
materialName: '不锈钢螺丝',
recommendedQuantity: 250,
currentStock: 300,
location: 'A-01-02',
expiryDate: '2026-11-15',
productionDate: '2023-11-15',
supplierName: '上海五金',
priorityScore: 95,
reason: ['先过期先出', '库位最近']
},
{
batchId: 'B002',
batchNo: 'BATCH-202311-015',
materialId: 'MAT001',
materialName: '不锈钢螺丝',
recommendedQuantity: 180,
currentStock: 200,
location: 'A-02-01',
expiryDate: '2026-10-20',
productionDate: '2023-10-20',
supplierName: '浙江紧固件',
priorityScore: 88,
reason: ['先进先出', '库存充足']
},
{
batchId: 'B003',
batchNo: 'BATCH-202312-003',
materialId: 'MAT001',
materialName: '不锈钢螺丝',
recommendedQuantity: 70,
currentStock: 100,
location: 'B-01-03',
expiryDate: '2027-01-10',
productionDate: '2023-12-01',
supplierName: '江苏五金',
priorityScore: 75,
reason: ['新批次', '库存较少']
}
])
// 计算属性
const strategyDescription = computed(() => {
const descriptions = {
'FIFO': '优先出库生产日期早的批次,减少库存积压',
'FEFO': '优先出库效期近的批次,减少过期损失',
'NEAREST': '优先出库距离操作台近的批次,提高作业效率',
'MANUAL': '不进行自动推荐,由操作员自主选择'
}
return descriptions[selectedStrategy.value] || ''
})
const canApplyAll = computed(() => {
if (!props.detail) return false
const remainingNeed = props.detail.plannedQuantity - props.detail.actualQuantity
const totalRecommended = recommendations.value.reduce(
(sum, r) => sum + r.recommendedQuantity, 0
)
return remainingNeed >= totalRecommended
})
// 监听策略变化
watch(selectedStrategy, (newStrategy) => {
if (newStrategy && newStrategy !== 'MANUAL') {
loading.value = true
// 模拟API调用
setTimeout(() => {
// 根据策略重新排序
if (newStrategy === 'FEFO') {
recommendations.value.sort((a, b) => {
const dateA = new Date(a.expiryDate).getTime()
const dateB = new Date(b.expiryDate).getTime()
return dateA - dateB
})
} else if (newStrategy === 'FIFO') {
recommendations.value.sort((a, b) => {
const dateA = new Date(a.productionDate).getTime()
const dateB = new Date(b.productionDate).getTime()
return dateA - dateB
})
}
loading.value = false
}, 500)
}
})
// 方法
const getRankTagType = (index) => {
const types = ['danger', 'warning', 'primary']
return types[index] || 'info'
}
const isNearExpiry = (expiryDate) => {
const expiry = new Date(expiryDate)
const now = new Date()
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 90 && diffDays > 0
}
const isUrgentExpiry = (expiryDate) => {
const expiry = new Date(expiryDate)
const now = new Date()
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 30 && diffDays > 0
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
}
const selectBatch = (batch) => {
emit('select-batch', batch)
visible.value = false
}
const applyAllRecommendations = async () => {
if (!props.detail) return
applying.value = true
try {
// 模拟批量应用推荐
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success('已应用所有推荐批次')
// 触发批次选择事件(选择第一个)
if (recommendations.value.length > 0) {
emit('select-batch', recommendations.value[0])
}
visible.value = false
} catch (error) {
ElMessage.error('应用失败')
} finally {
applying.value = false
}
}
const applyRecommendations = () => {
if (recommendations.value.length > 0) {
selectBatch(recommendations.value[0])
}
}
</script>
<style scoped>
.strategy-section {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #ebeef5;
}
.strategy-description {
margin-top: 10px;
padding: 8px 12px;
background: #f0f9ff;
border-radius: 4px;
font-size: 13px;
color: #409eff;
display: flex;
align-items: center;
gap: 8px;
}
.recommendation-section {
margin-bottom: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.section-header h4 {
margin: 0;
color: #303133;
font-weight: 600;
}
.batch-info {
display: flex;
align-items: center;
gap: 8px;
}
.batch-no {
font-family: 'Monaco', 'Consolas', monospace;
font-size: 13px;
}
.stock-info {
line-height: 1.4;
}
.current-stock {
color: #606266;
font-size: 12px;
}
.recommended {
color: #409eff;
font-size: 12px;
}
.recommended-quantity {
color: #f56c6c;
font-weight: 600;
}
.expiry-info {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
}
.expiry-info.expiry-near {
color: #e6a23c;
font-weight: 500;
}
.expiry-info.expiry-urgent {
color: #f56c6c;
font-weight: 600;
}
.rank-text {
color: #909399;
font-size: 12px;
}
.score-text {
font-size: 11px;
color: #606266;
margin-top: 2px;
display: inline-block;
}
.reasons {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.reason-tag {
margin: 2px 0;
}
.explanation-section {
margin-top: 20px;
}
.explanation-content {
padding: 10px;
background: #f8fafc;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
.explanation-content p {
margin: 5px 0;
}
:deep(.el-collapse-item__header) {
font-weight: 500;
color: #303133;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
</style>
\ No newline at end of file
<template>
<el-dialog
v-model="visible"
title="智能批次推荐"
width="700px"
:close-on-click-modal="false"
>
<!-- 推荐策略选择 -->
<div class="strategy-section">
<el-radio-group v-model="selectedStrategy" size="small">
<el-radio-button label="FIFO">先进先出</el-radio-button>
<el-radio-button label="FEFO">先过期先出</el-radio-button>
<el-radio-button label="NEAREST">最近库位</el-radio-button>
<el-radio-button label="MANUAL">手动选择</el-radio-button>
</el-radio-group>
<div class="strategy-description">
<el-icon><InfoFilled /></el-icon>
<span>{{ strategyDescription }}</span>
</div>
</div>
<!-- 推荐结果 -->
<div class="recommendation-section">
<div class="section-header">
<h4>推荐批次列表</h4>
<el-button
type="primary"
size="small"
@click="applyAllRecommendations"
:disabled="!canApplyAll"
>
一键应用全部推荐
</el-button>
</div>
<el-table
:data="recommendations"
stripe
border
size="small"
height="350px"
v-loading="loading"
>
<el-table-column type="index" label="推荐" width="60" align="center">
<template #default="{ $index }">
<el-tag
v-if="$index < 3"
:type="getRankTagType($index)"
effect="dark"
round
>
{{ $index + 1 }}
</el-tag>
<span v-else class="rank-text">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="150">
<template #default="{ row }">
<div class="batch-info">
<span class="batch-no">{{ row.batchNo }}</span>
<el-tag
v-if="isNearExpiry(row.expiryDate)"
type="warning"
size="mini"
>
临期
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="库存信息" width="120">
<template #default="{ row }">
<div class="stock-info">
<div class="current-stock">
库存: <strong>{{ row.currentStock }}</strong>
</div>
<div class="recommended">
推荐: <strong class="recommended-quantity">{{ row.recommendedQuantity }}</strong>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="location" label="库位" width="100">
<template #default="{ row }">
<el-tag size="small" type="primary">
{{ row.location }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="效期" width="120">
<template #default="{ row }">
<div class="expiry-info" :class="{
'expiry-near': isNearExpiry(row.expiryDate),
'expiry-urgent': isUrgentExpiry(row.expiryDate)
}">
{{ formatDate(row.expiryDate) }}
<el-icon
v-if="isNearExpiry(row.expiryDate)"
color="#e6a23c"
size="12"
>
<Warning />
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column prop="priorityScore" label="推荐度" width="100" align="center">
<template #default="{ row }">
<el-progress
:percentage="row.priorityScore"
:stroke-width="10"
:show-text="false"
/>
<span class="score-text">{{ row.priorityScore }}</span>
</template>
</el-table-column>
<el-table-column label="推荐理由">
<template #default="{ row }">
<div class="reasons">
<el-tag
v-for="(reason, index) in row.reason"
:key="index"
size="mini"
type="info"
effect="plain"
class="reason-tag"
>
{{ reason }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="selectBatch(row)"
>
选择
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 推荐说明 -->
<div class="explanation-section">
<el-collapse v-model="activeExplanation">
<el-collapse-item title="推荐说明" name="explanation">
<div class="explanation-content">
<p><strong>先进先出(FIFO)</strong>: 优先出库生产日期早的批次,减少库存积压</p>
<p><strong>先过期先出(FEFO)</strong>: 优先出库效期近的批次,减少过期损失</p>
<p><strong>最近库位</strong>: 优先出库距离操作台近的批次,提高作业效率</p>
<p><strong>手动选择</strong>: 不进行自动推荐,由操作员自主选择</p>
</div>
</el-collapse-item>
</el-collapse>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
:loading="applying"
@click="applyRecommendations"
>
应用推荐
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { InfoFilled, Warning } from '@element-plus/icons-vue'
const props = defineProps()
const emit = defineEmits()
// 响应式数据
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const selectedStrategy = ref('FEFO')
const loading = ref(false)
const applying = ref(false)
const activeExplanation = ref(['explanation'])
// 模拟推荐数据
const recommendations = ref([
{
batchId: 'B001',
batchNo: 'BATCH-202312-001',
materialId: 'MAT001',
materialName: '不锈钢螺丝',
recommendedQuantity: 250,
currentStock: 300,
location: 'A-01-02',
expiryDate: '2026-11-15',
productionDate: '2023-11-15',
supplierName: '上海五金',
priorityScore: 95,
reason: ['先过期先出', '库位最近']
},
{
batchId: 'B002',
batchNo: 'BATCH-202311-015',
materialId: 'MAT001',
materialName: '不锈钢螺丝',
recommendedQuantity: 180,
currentStock: 200,
location: 'A-02-01',
expiryDate: '2026-10-20',
productionDate: '2023-10-20',
supplierName: '浙江紧固件',
priorityScore: 88,
reason: ['先进先出', '库存充足']
},
{
batchId: 'B003',
batchNo: 'BATCH-202312-003',
materialId: 'MAT001',
materialName: '不锈钢螺丝',
recommendedQuantity: 70,
currentStock: 100,
location: 'B-01-03',
expiryDate: '2027-01-10',
productionDate: '2023-12-01',
supplierName: '江苏五金',
priorityScore: 75,
reason: ['新批次', '库存较少']
}
])
// 计算属性
const strategyDescription = computed(() => {
const descriptions = {
'FIFO': '优先出库生产日期早的批次,减少库存积压',
'FEFO': '优先出库效期近的批次,减少过期损失',
'NEAREST': '优先出库距离操作台近的批次,提高作业效率',
'MANUAL': '不进行自动推荐,由操作员自主选择'
}
return descriptions[selectedStrategy.value] || ''
})
const canApplyAll = computed(() => {
if (!props.detail) return false
const remainingNeed = props.detail.plannedQuantity - props.detail.actualQuantity
const totalRecommended = recommendations.value.reduce(
(sum, r) => sum + r.recommendedQuantity, 0
)
return remainingNeed >= totalRecommended
})
// 监听策略变化
watch(selectedStrategy, (newStrategy) => {
if (newStrategy && newStrategy !== 'MANUAL') {
loading.value = true
// 模拟API调用
setTimeout(() => {
// 根据策略重新排序
if (newStrategy === 'FEFO') {
recommendations.value.sort((a, b) => {
const dateA = new Date(a.expiryDate).getTime()
const dateB = new Date(b.expiryDate).getTime()
return dateA - dateB
})
} else if (newStrategy === 'FIFO') {
recommendations.value.sort((a, b) => {
const dateA = new Date(a.productionDate).getTime()
const dateB = new Date(b.productionDate).getTime()
return dateA - dateB
})
}
loading.value = false
}, 500)
}
})
// 方法
const getRankTagType = (index) => {
const types = ['danger', 'warning', 'primary']
return types[index] || 'info'
}
const isNearExpiry = (expiryDate) => {
const expiry = new Date(expiryDate)
const now = new Date()
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 90 && diffDays > 0
}
const isUrgentExpiry = (expiryDate) => {
const expiry = new Date(expiryDate)
const now = new Date()
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 30 && diffDays > 0
}
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
}
const selectBatch = (batch) => {
emit('select-batch', batch)
visible.value = false
}
const applyAllRecommendations = async () => {
if (!props.detail) return
applying.value = true
try {
// 模拟批量应用推荐
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success('已应用所有推荐批次')
// 触发批次选择事件(选择第一个)
if (recommendations.value.length > 0) {
emit('select-batch', recommendations.value[0])
}
visible.value = false
} catch (error) {
ElMessage.error('应用失败')
} finally {
applying.value = false
}
}
const applyRecommendations = () => {
if (recommendations.value.length > 0) {
selectBatch(recommendations.value[0])
}
}
</script>
<style scoped>
.strategy-section {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #ebeef5;
}
.strategy-description {
margin-top: 10px;
padding: 8px 12px;
background: #f0f9ff;
border-radius: 4px;
font-size: 13px;
color: #409eff;
display: flex;
align-items: center;
gap: 8px;
}
.recommendation-section {
margin-bottom: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.section-header h4 {
margin: 0;
color: #303133;
font-weight: 600;
}
.batch-info {
display: flex;
align-items: center;
gap: 8px;
}
.batch-no {
font-family: 'Monaco', 'Consolas', monospace;
font-size: 13px;
}
.stock-info {
line-height: 1.4;
}
.current-stock {
color: #606266;
font-size: 12px;
}
.recommended {
color: #409eff;
font-size: 12px;
}
.recommended-quantity {
color: #f56c6c;
font-weight: 600;
}
.expiry-info {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
}
.expiry-info.expiry-near {
color: #e6a23c;
font-weight: 500;
}
.expiry-info.expiry-urgent {
color: #f56c6c;
font-weight: 600;
}
.rank-text {
color: #909399;
font-size: 12px;
}
.score-text {
font-size: 11px;
color: #606266;
margin-top: 2px;
display: inline-block;
}
.reasons {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.reason-tag {
margin: 2px 0;
}
.explanation-section {
margin-top: 20px;
}
.explanation-content {
padding: 10px;
background: #f8fafc;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
.explanation-content p {
margin: 5px 0;
}
:deep(.el-collapse-item__header) {
font-weight: 500;
color: #303133;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
</style>
\ No newline at end of file
<template>
<div class="operation-logs-container">
<!-- 日志筛选区域 -->
<div class="filter-section">
<el-form :model="filters" inline label-width="80px">
<el-form-item label="操作类型">
<el-select
v-model="filters.operationType"
placeholder="请选择"
clearable
style="width: 150px;"
>
<el-option label="全部" value="" />
<el-option label="创建" value="CREATE" />
<el-option label="扫描" value="SCAN" />
<el-option label="出库" value="OUTBOUND" />
<el-option label="撤销" value="UNDO" />
<el-option label="确认" value="CONFIRM" />
<el-option label="暂停" value="PAUSE" />
<el-option label="恢复" value="RESUME" />
</el-select>
</el-form-item>
<el-form-item label="操作员">
<el-input
v-model="filters.operatorName"
placeholder="输入操作员姓名"
clearable
style="width: 150px;"
/>
</el-form-item>
<el-form-item label="出库单号">
<el-input
v-model="filters.orderNo"
placeholder="输入出库单号"
clearable
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="操作时间">
<el-date-picker
v-model="filters.dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 380px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
查询
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
<el-button @click="exportLogs">
<el-icon><Download /></el-icon>
导出
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 日志列表 -->
<div class="logs-section">
<el-table
:data="filteredLogs"
stripe
v-loading="loading"
height="calc(100vh - 240px)"
>
<el-table-column type="expand" width="50">
<template #default="{ row }">
<div class="log-details">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="操作详情" span="2">
<pre>{{ JSON.stringify(row.details || {}, null, 2) }}</pre>
</el-descriptions-item>
<el-descriptions-item label="设备信息">
{{ row.deviceInfo || '--' }}
</el-descriptions-item>
<el-descriptions-item label="IP地址">
{{ row.ipAddress || '--' }}
</el-descriptions-item>
</el-descriptions>
</div>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="操作时间" width="160" sortable>
<template #default="{ row }">
<div class="timestamp">
{{ formatDateTime(row.createdAt) }}
</div>
</template>
</el-table-column>
<el-table-column label="操作类型" width="120">
<template #default="{ row }">
<el-tag :type="getOperationTypeTag(row.operationType)" size="small">
{{ getOperationTypeText(row.operationType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderNo" label="出库单号" width="180">
<template #default="{ row }">
<span v-if="row.orderNo" class="order-no">
{{ row.orderNo }}
</span>
<span v-else class="no-data">--</span>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作员" width="120" />
<el-table-column prop="operationDesc" label="操作描述">
<template #default="{ row }">
<div class="operation-desc">
{{ row.operationDesc }}
</div>
</template>
</el-table-column>
<el-table-column label="结果" width="100">
<template #default="{ row }">
<el-icon
v-if="row.success !== false"
color="#67c23a"
:size="18"
>
<SuccessFilled />
</el-icon>
<el-icon v-else color="#f56c6c" :size="18">
<CircleCloseFilled />
</el-icon>
</template>
</el-table-column>
<el-table-column label="用时" width="100" align="right">
<template #default="{ row }">
<span v-if="row.duration !== undefined">
{{ row.duration }}ms
</span>
<span v-else class="no-data">--</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页器 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[20, 50, 100, 200]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
Search,
Refresh,
Download,
SuccessFilled,
CircleCloseFilled
} from '@element-plus/icons-vue'
const props = defineProps({
logs: {
type: Array,
default: () => []
}
})
// 响应式数据
const filters = ref({
operationType: '',
operatorName: '',
orderNo: '',
dateRange: []
})
const pagination = ref({
currentPage: 1,
pageSize: 50,
total: 0
})
const loading = ref(false)
// 模拟更多日志数据
const mockLogs = ref([
{
id: '1',
orderId: '1',
orderNo: 'OUT-20231227-001',
operatorId: 'OP001',
operatorName: '张三',
operationType: 'SCAN',
operationDesc: '扫描批次 BATCH-202312-001,出库50个不锈钢螺丝',
ipAddress: '192.168.1.100',
deviceInfo: 'Chrome/120.0.0.0',
createdAt: '2023-12-27 10:30:15',
success: true,
duration: 320,
details: {
batchNo: 'BATCH-202312-001',
quantity: 50,
materialName: '不锈钢螺丝',
unit: '个'
}
},
{
id: '2',
orderId: '1',
orderNo: 'OUT-20231227-001',
operatorId: 'OP001',
operatorName: '张三',
operationType: 'UNDO',
operationDesc: '撤销批次 BATCH-202312-001 的出库操作',
ipAddress: '192.168.1.100',
deviceInfo: 'Chrome/120.0.0.0',
createdAt: '2023-12-27 10:32:45',
success: true,
duration: 280,
details: {
batchNo: 'BATCH-202312-001',
quantity: 50
}
}
])
// 计算属性
const allLogs = computed(() => {
return [...props.logs, ...mockLogs.value].sort((a, b) => {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
})
})
const filteredLogs = computed(() => {
let result = [...allLogs.value]
// 按操作类型过滤
if (filters.value.operationType) {
result = result.filter(log =>
log.operationType === filters.value.operationType
)
}
// 按操作员过滤
if (filters.value.operatorName) {
result = result.filter(log =>
log.operatorName.toLowerCase().includes(filters.value.operatorName.toLowerCase())
)
}
// 按订单号过滤
if (filters.value.orderNo) {
result = result.filter(log =>
log.orderNo?.toLowerCase().includes(filters.value.orderNo.toLowerCase())
)
}
// 按时间范围过滤
if (filters.value.dateRange?.length === 2) {
const [start, end] = filters.value.dateRange
const startTime = new Date(start).getTime()
const endTime = new Date(end).getTime()
result = result.filter(log => {
const logTime = new Date(log.createdAt).getTime()
return logTime >= startTime && logTime <= endTime
})
}
// 更新分页总数
pagination.value.total = result.length
// 分页处理
const start = (pagination.value.currentPage - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
return result.slice(start, end)
})
// 方法
const handleSearch = () => {
pagination.value.currentPage = 1
loading.value = true
setTimeout(() => {
loading.value = false
}, 500)
}
const handleReset = () => {
filters.value = {
operationType: '',
operatorName: '',
orderNo: '',
dateRange: []
}
pagination.value.currentPage = 1
handleSearch()
}
const exportLogs = () => {
ElMessage.success('导出功能开发中...')
// 实际项目中这里可以实现CSV/Excel导出
}
const handleSizeChange = (size) => {
pagination.value.pageSize = size
}
const handleCurrentChange = (page) => {
pagination.value.currentPage = page
}
const getOperationTypeTag = (type) => {
const map = {
'CREATE': 'primary',
'SCAN': 'success',
'OUTBOUND': 'warning',
'UNDO': 'danger',
'CONFIRM': 'success',
'PAUSE': 'info',
'RESUME': 'warning'
}
return map[type] || 'info'
}
const getOperationTypeText = (type) => {
const map = {
'CREATE': '创建',
'SCAN': '扫描',
'OUTBOUND': '出库',
'UNDO': '撤销',
'CONFIRM': '确认',
'PAUSE': '暂停',
'RESUME': '恢复'
}
return map[type] || type
}
const formatDateTime = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
</script>
<style scoped>
.operation-logs-container {
height: 100%;
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
padding: 20px;
}
.filter-section {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
}
:deep(.el-form--inline .el-form-item) {
margin-right: 20px;
margin-bottom: 10px;
}
.logs-section {
flex: 1;
overflow: hidden;
}
.log-details {
padding: 15px;
background: #f8fafc;
border-radius: 4px;
margin: 5px 0;
}
.log-details pre {
margin: 0;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
color: #333;
background: white;
padding: 10px;
border-radius: 4px;
overflow: auto;
max-height: 200px;
}
.timestamp {
font-family: 'Monaco', 'Consolas', monospace;
font-size: 12px;
color: #606266;
}
.order-no {
font-family: 'Monaco', 'Consolas', monospace;
font-weight: 500;
color: #409eff;
}
.no-data {
color: #909399;
font-style: italic;
}
.operation-desc {
line-height: 1.4;
color: #303133;
}
.pagination-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
display: flex;
justify-content: flex-end;
}
:deep(.el-descriptions__body) {
background: white;
}
</style>
\ No newline at end of file
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="900px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 主表单 -->
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-position="right"
size="default"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="出库单号" prop="orderNo">
<el-input
v-model="formData.orderNo"
placeholder="系统自动生成"
:disabled="mode === 'edit'"
>
<template #append>
<el-button @click="generateOrderNo">
<el-icon><Refresh /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出库仓库" prop="warehouseId">
<el-select
v-model="formData.warehouseId"
placeholder="请选择仓库"
style="width: 100%;"
clearable
>
<el-option
v-for="warehouse in warehouseOptions"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remarks">
<el-input
v-model="formData.remarks"
type="textarea"
:rows="2"
placeholder="请输入出库单备注信息"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<!-- 物资明细 -->
<div class="details-section">
<div class="section-header">
<h4>出库物资明细</h4>
<div class="header-actions">
<el-button type="primary" @click="showMaterialSelector">
<el-icon><Plus /></el-icon>
添加物资
</el-button>
<el-button @click="clearAllDetails">
<el-icon><Delete /></el-icon>
清空
</el-button>
</div>
</div>
<el-table
:data="formData.details"
stripe
border
size="small"
class="details-table"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="materialCode" label="物资编码" width="120">
<template #header>
<span>物资编码</span>
<el-tooltip content="点击选择物资" placement="top">
<el-icon class="header-icon"><InfoFilled /></el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="物资信息" min-width="200">
<template #default="{ row }">
<div class="material-info">
<div class="material-name">{{ row.materialName }}</div>
<div class="material-spec">{{ row.specification }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="80" align="center" />
<el-table-column prop="availableStock" label="可用库存" width="100" align="right">
<template #default="{ row }">
<span :class="{
'stock-low': row.availableStock < row.plannedQuantity,
'stock-zero': row.availableStock === 0
}">
{{ row.availableStock || 0 }}
</span>
</template>
</el-table-column>
<el-table-column label="计划出库数量" width="150">
<template #default="{ row }">
<el-input-number
v-model="row.plannedQuantity"
:min="0.001"
:max="row.availableStock || 999999"
:precision="3"
:step="1"
size="small"
controls-position="right"
style="width: 120px;"
@change="updateRowAmount(row)"
/>
</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价" width="120" align="right">
<template #default="{ row }">
<span>¥{{ formatAmount(row.unitPrice) }}</span>
</template>
</el-table-column>
<el-table-column label="计划金额" width="120" align="right">
<template #default="{ row }">
<span class="amount-cell">
¥{{ formatAmount(row.plannedAmount) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right" align="center">
<template #default="{ $index }">
<el-button
type="danger"
link
size="small"
@click="removeDetail($index)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 汇总信息 -->
<div class="summary-section">
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="物资种类">
<span class="summary-value">{{ formData.details.length }} 种</span>
</el-descriptions-item>
<el-descriptions-item label="总数量">
<span class="summary-value">{{ totalQuantity }} {{ unitText }}</span>
</el-descriptions-item>
<el-descriptions-item label="总金额">
<span class="summary-value highlight">
¥{{ formatAmount(totalAmount) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="创建人">
<span class="summary-value">{{ currentUser.name }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
:loading="submitting"
@click="handleSubmit"
:disabled="formData.details.length === 0"
>
确认保存
</el-button>
</span>
</template>
<!-- 物资选择器 -->
<MaterialSelector
v-model="showMaterialSelectorDialog"
:selected-materials="selectedMaterialIds"
@select="handleMaterialSelect"
/>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
Refresh,
Plus,
Delete,
InfoFilled
} from '@element-plus/icons-vue'
import MaterialSelector from './MaterialSelector.vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
order: {
type: Object,
default: null
},
mode: {
type: String,
default: 'create'
}
})
const emit = defineEmits(['update:modelValue', 'save'])
// 表单引用和规则
const formRef = ref()
const rules = {
warehouseId: [
{ required: true, message: '请选择出库仓库', trigger: 'change' }
]
}
// 响应式数据
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const formData = ref({
orderNo: '',
warehouseId: '',
remarks: '',
details: []
})
const submitting = ref(false)
const showMaterialSelectorDialog = ref(false)
// 模拟数据
const warehouseOptions = ref([
{ id: 'WH001', name: '成品仓A' },
{ id: 'WH002', name: '成品仓B' },
{ id: 'WH003', name: '原材料仓' },
{ id: 'WH004', name: '半成品仓' }
])
const materialOptions = ref([
{
id: 'MAT001',
code: 'P-1001',
name: '不锈钢螺丝',
specification: 'M8*25',
unit: '个',
unitPrice: 2.5,
availableStock: 1500
},
{
id: 'MAT002',
code: 'P-1002',
name: '轴承',
specification: '6202ZZ',
unit: '个',
unitPrice: 15.0,
availableStock: 800
},
{
id: 'MAT003',
code: 'P-1003',
name: '电机',
specification: 'DC-24V-100W',
unit: '台',
unitPrice: 350.0,
availableStock: 120
}
])
const currentUser = ref({
id: 'U001',
name: '张三'
})
// 计算属性
const dialogTitle = computed(() => {
return props.mode === 'edit' ? '编辑出库单' : '新建出库单'
})
const selectedMaterialIds = computed(() => {
return formData.value.details.map(detail => detail.materialId)
})
const totalQuantity = computed(() => {
return formData.value.details.reduce((sum, detail) => sum + detail.plannedQuantity, 0)
})
const totalAmount = computed(() => {
return formData.value.details.reduce((sum, detail) => sum + detail.plannedAmount, 0)
})
const unitText = computed(() => {
const units = [...new Set(formData.value.details.map(d => d.unit))]
return units.length > 1 ? '多种单位' : units[0] || ''
})
// 监听订单数据变化
watch(() => props.order, (order) => {
if (order && props.mode === 'edit') {
formData.value = {
orderNo: order.orderNo,
warehouseId: order.warehouseId || '',
remarks: order.remarks || '',
details: order.details || []
}
}
}, { immediate: true })
// 方法
const generateOrderNo = () => {
const date = new Date()
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0')
formData.value.orderNo = `OUT-${year}${month}${day}-${random}`
}
const showMaterialSelector = () => {
showMaterialSelectorDialog.value = true
}
const handleMaterialSelect = (materials) => {
materials.forEach(material => {
// 避免重复添加
if (!selectedMaterialIds.value.includes(material.id)) {
formData.value.details.push({
id: '',
orderId: '',
materialId: material.id,
materialCode: material.code,
materialName: material.name,
specification: material.specification,
unit: material.unit,
plannedQuantity: 1,
actualQuantity: 0,
unitPrice: material.unitPrice,
plannedAmount: material.unitPrice,
actualAmount: 0,
availableStock: material.availableStock
})
}
})
showMaterialSelectorDialog.value = false
}
const removeDetail = (index) => {
formData.value.details.splice(index, 1)
}
const clearAllDetails = () => {
ElMessageBox.confirm(
'确定要清空所有物资明细吗?',
'确认清空',
{ type: 'warning' }
).then(() => {
formData.value.details = []
}).catch(() => {
// 用户取消
})
}
const updateRowAmount = (row) => {
row.plannedAmount = row.plannedQuantity * row.unitPrice
}
const handleClose = () => {
if (formRef.value) {
formRef.value.resetFields()
}
formData.value = {
orderNo: '',
warehouseId: '',
remarks: '',
details: []
}
visible.value = false
}
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (formData.value.details.length === 0) {
ElMessage.warning('请至少添加一条物资明细')
return
}
// 检查库存
const insufficientStock = formData.value.details.some(detail =>
detail.plannedQuantity > detail.availableStock
)
if (insufficientStock) {
ElMessage.warning('部分物资计划数量超过可用库存,请调整')
return
}
submitting.value = true
// 构建提交数据
const submitData = {
...formData.value,
totalAmount: totalAmount.value,
operatorId: currentUser.value.id,
status: 'DRAFT',
details: formData.value.details.map(detail => ({
materialId: detail.materialId,
plannedQuantity: detail.plannedQuantity,
unitPrice: detail.unitPrice
}))
}
emit('save', submitData)
handleClose()
} catch (error) {
console.error('表单验证失败:', error)
} finally {
submitting.value = false
}
}
const formatAmount = (amount) => {
return amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
</script>
<style scoped>
.details-section {
margin-top: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.section-header h4 {
margin: 0;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 10px;
}
.details-table {
width: 100%;
margin-bottom: 20px;
}
.material-info {
line-height: 1.4;
}
.material-name {
font-weight: 500;
color: #303133;
}
.material-spec {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.stock-low {
color: #e6a23c;
font-weight: 500;
}
.stock-zero {
color: #f56c6c;
font-weight: 500;
}
.amount-cell {
font-weight: 600;
color: #303133;
}
.summary-section {
margin-top: 20px;
padding-top: 15px;
border-top: 2px solid #409eff;
}
.summary-value {
font-weight: 500;
color: #303133;
}
.summary-value.highlight {
color: #f56c6c;
font-size: 16px;
font-weight: 600;
}
.header-icon {
margin-left: 5px;
color: #909399;
cursor: help;
}
:deep(.el-dialog__body) {
padding-bottom: 10px;
}
:deep(.el-table__cell) {
padding: 8px 0;
}
</style>
\ No newline at end of file
<template>
<div class="order-list-container">
<!-- 搜索和筛选区域 -->
<div class="filter-section">
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="filters.orderNo"
placeholder="出库单号"
clearable
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select
v-model="filters.status"
placeholder="状态"
clearable
@change="handleSearch"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="DRAFT" />
<el-option label="出库中" value="EXECUTING" />
<el-option label="已暂停" value="PAUSED" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELED" />
</el-select>
</el-col>
<el-col :span="8">
<el-date-picker
v-model="filters.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleSearch"
/>
</el-col>
<el-col :span="6" class="action-buttons">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
</el-row>
</div>
<!-- 订单列表表格 -->
<div class="table-section">
<el-table
:data="filteredOrders"
stripe
highlight-current-row
@row-click="handleRowClick"
v-loading="loading"
height="calc(100vh - 220px)"
>
<el-table-column prop="orderNo" label="出库单号" width="180" fixed>
<template #default="{ row }">
<div class="order-no-cell">
<span class="order-no">{{ row.orderNo }}</span>
<el-tag
:type="getStatusType(row.status)"
size="small"
effect="light"
>
{{ getStatusText(row.status) }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="warehouseName" label="仓库" width="120" />
<el-table-column prop="operatorName" label="操作员" width="100" />
<el-table-column label="状态进度" width="150">
<template #default="{ row }">
<div class="status-progress">
<el-progress
v-if="row.completionRate !== undefined"
:percentage="row.completionRate"
:status="row.completionRate >= 100 ? 'success' : 'primary'"
:stroke-width="8"
:show-text="false"
/>
<span v-else class="no-data">--</span>
</div>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总金额" width="120" align="right">
<template #default="{ row }">
¥{{ formatAmount(row.totalAmount) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="confirmedAt" label="确认时间" width="160">
<template #default="{ row }">
{{ row.confirmedAt ? formatDateTime(row.confirmedAt) : '--' }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button
v-if="row.status === 'DRAFT'"
type="primary"
size="small"
@click.stop="handleEdit(row)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.status === 'DRAFT' || row.status === 'EXECUTING' || row.status === 'PAUSED'"
type="success"
size="small"
@click.stop="handleStartOutbound(row)"
>
<!-- <el-icon><PlayCircle /></el-icon> -->
{{ row.status === 'EXECUTING' ? '继续出库' : '开始出库' }}
</el-button>
<el-dropdown
v-if="row.status === 'DRAFT'"
@command="(command) => handleCommand(command, row)"
size="small"
>
<el-button type="warning" size="small">
更多<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="cancel">
<el-icon><Close /></el-icon>取消订单
</el-dropdown-item>
<el-dropdown-item command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="row.status === 'COMPLETED'"
type="info"
size="small"
@click.stop="handleViewDetails(row)"
>
<el-icon><View /></el-icon>
查看详情
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页器 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Refresh,
Edit,
ArrowDown,
Close,
Delete,
View
} from '@element-plus/icons-vue'
const props = defineProps({
orders: {
type: Array,
default: () => []
}
})
const emit = defineEmits()
// 响应式数据
const filters = ref({
orderNo: '',
status: '',
dateRange: []
})
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const loading = ref(false)
// 计算属性
const filteredOrders = computed(() => {
let result = [...props.orders]
// 按订单号过滤
if (filters.value.orderNo) {
result = result.filter(order =>
order.orderNo.toLowerCase().includes(filters.value.orderNo.toLowerCase())
)
}
// 按状态过滤
if (filters.value.status) {
result = result.filter(order => order.status === filters.value.status)
}
// 按日期范围过滤
if (filters.value.dateRange?.length === 2) {
const [start, end] = filters.value.dateRange
const startDate = new Date(start)
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
result = result.filter(order => {
const createDate = new Date(order.createdAt)
return createDate >= startDate && createDate <= endDate
})
}
// 计算完成率(模拟数据)
result.forEach(order => {
if (order.status === 'COMPLETED') {
order.completionRate = 100
} else if (order.status === 'DRAFT') {
order.completionRate = 0
} else {
// 模拟计算完成率
order.completionRate = Math.floor(Math.random() * 60) + 20
}
})
// 更新分页总数
pagination.value.total = result.length
// 分页处理
const start = (pagination.value.currentPage - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
return result.slice(start, end)
})
// 方法
const handleSearch = () => {
pagination.value.currentPage = 1
loading.value = true
// 模拟搜索延迟
setTimeout(() => {
loading.value = false
}, 500)
}
const handleReset = () => {
filters.value = {
orderNo: '',
status: '',
dateRange: []
}
pagination.value.currentPage = 1
handleSearch()
}
const handleRowClick = (row) => {
emit('select-order', row)
}
const handleEdit = (order) => {
emit('edit-order', order)
}
const handleStartOutbound = (order) => {
emit('start-outbound', order)
}
const handleViewDetails = (order) => {
ElMessage.info(`查看订单详情: ${order.orderNo}`)
// 实际项目中这里可以跳转到详情页或打开详情对话框
}
const handleCommand = async (command, order) => {
switch (command) {
case 'cancel':
await handleCancelOrder(order)
break
case 'delete':
await handleDeleteOrder(order)
break
}
}
const handleCancelOrder = async (order) => {
try {
await ElMessageBox.confirm(
`确定要取消出库单 ${order.orderNo} 吗?`,
'确认取消',
{ type: 'warning' }
)
ElMessage.success('订单已取消')
// 实际项目中这里应该调用API更新状态
} catch (error) {
// 用户取消
}
}
const handleDeleteOrder = async (order) => {
try {
await ElMessageBox.confirm(
`确定要删除出库单 ${order.orderNo} 吗?删除后无法恢复。`,
'确认删除',
{
type: 'error',
confirmButtonText: '确定删除',
cancelButtonText: '取消'
}
)
emit('delete-order', order)
} catch (error) {
// 用户取消
}
}
const handleSizeChange = (size) => {
pagination.value.pageSize = size
}
const handleCurrentChange = (page) => {
pagination.value.currentPage = page
}
const getStatusType = (status) => {
const map= {
'DRAFT': 'info',
'EXECUTING': 'primary',
'PAUSED': 'warning',
'COMPLETED': 'success',
'CANCELED': 'danger'
}
return map[status] || 'info'
}
const getStatusText = (status) => {
const map= {
'DRAFT': '草稿',
'EXECUTING': '出库中',
'PAUSED': '已暂停',
'COMPLETED': '已完成',
'CANCELED': '已取消'
}
return map[status] || status
}
const formatDateTime = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const formatAmount = (amount) => {
return amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
</script>
<style scoped>
.order-list-container {
height: 100%;
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
padding: 20px;
}
.filter-section {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
}
.action-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.table-section {
flex: 1;
overflow: hidden;
}
.order-no-cell {
display: flex;
flex-direction: column;
gap: 5px;
}
.order-no {
font-weight: 600;
font-family: 'Monaco', 'Consolas', monospace;
}
.status-progress {
width: 120px;
}
.no-data {
color: #909399;
font-style: italic;
}
.pagination-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
display: flex;
justify-content: flex-end;
}
:deep(.el-table__row) {
cursor: pointer;
transition: background-color 0.3s;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
:deep(.el-table__row.current-row) {
background-color: #ecf5ff !important;
box-shadow: 0 0 0 1px #409eff inset;
}
</style>
\ No newline at end of file
<template>
<el-dialog v-model="dialogVisible" title="出库执行" width="90%" :before-close="handleClose" destroy-on-close>
<div class="outbound-execution-container">
<!-- 订单信息头 -->
<div class="order-header">
<div class="order-info">
<div class="order-title">
<h2>出库单: {{ order.orderNo }}</h2>
<dict-tag :options="out_order_status" :value="order.orderStatus" />
</div>
<div class="order-meta">
<span><el-icon>
<User />
</el-icon> 出库人: {{ order.applicantName }}</span>
<span><el-icon>
<Calendar />
</el-icon> 出库时间: {{ formatDate(order.applyTime) }}</span>
</div>
</div>
<div class="order-actions"></div>
</div>
<!-- 双栏布局 -->
<div class="execution-layout">
<!-- 左栏:出库单明细 -->
<div class="detail-section">
<div class="section-header">
<h3><el-icon>
<List />
</el-icon> 出库单明细</h3>
</div>
<el-table :data="orderDetails" stripe highlight-current-row :current-row-key="currentDetailId"
height="calc(100vh - 320px)" v-loading="loadingDetails">
<el-table-column type="expand">
<template #default="props">
<el-table v-loading="loading" :data="props.row.wmsOutboundItemInventoryList">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="关联ID" align="center" prop="relationId" />
<el-table-column label="出库明细ID" align="center" prop="itemId" />
<el-table-column label="出库单ID" align="center" prop="orderId" />
<el-table-column label="库存ID" align="center" prop="inventoryId" />
<el-table-column label="RFID标签ID" align="center" prop="rfidTagId" />
<el-table-column label="RFID编码" align="center" prop="rfidCode" />
<el-table-column label="EPC编码" align="center" prop="epcCode" />
<el-table-column label="TID编码" align="center" prop="tidCode" />
<el-table-column label="物资ID" align="center" prop="materialId" />
<el-table-column label="批次号" align="center" prop="batchNo" />
<el-table-column label="仓库id" align="center" prop="warehouseId" />
<el-table-column label="库区id" align="center" prop="areaId" />
<el-table-column label="库位ID" align="center" prop="locationId" />
<el-table-column label="数量" align="center" prop="quantity" />
<el-table-column label="单位成本" align="center" prop="unitCost" />
<el-table-column label="出库时间" align="center" prop="outboundTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.outboundTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="是否已撤销" align="center" prop="isCanceled" />
<el-table-column label="撤销时间" align="center" prop="cancelTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.cancelTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="撤销人" align="center" prop="cancelBy" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-hasPermi="['ware:wmsOutboundItemInventory:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['ware:wmsOutboundItemInventory:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column prop="materialId" label="物资编码" width="120" />
<el-table-column prop="materialName" label="物资名称" />
<el-table-column prop="material" label="规格型号" />
<el-table-column prop="unit" label="单位" width="80" />
<el-table-column prop="planQuantity" label="计划数量" width="150" align="right" />
<el-table-column prop="actualQuantity" label="实际数量" width="150" align="right" />
<el-table-column prop="amount" label="金额" width="150" align="right" />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleCancel(scope.row)">
撤销
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 右栏:扫码操作区 -->
<div class="scan-section">
<div class="section-header">
<h3>
<!-- <el-icon><Scan /></el-icon> -->
扫码出库
</h3>
<el-button type="info" size="small" @click="showBatchRecommendation">
<el-icon>
<MagicStick />
</el-icon>
智能推荐批次
</el-button>
</div>
<!-- 扫码输入区域 -->
<div class="scan-input-area">
<div class="input-wrapper">
<el-input v-model="scanInput" placeholder="请扫描二维码或输入批次号" size="large" clearable @keyup.enter="handleScan"
@clear="clearScanResult" ref="scanInputRef">
<template #prepend>
<el-icon>
<Search />
</el-icon>
</template>
<template #append>
<el-button type="primary" :loading="scanning" @click="handleScan">
确认
</el-button>
</template>
</el-input>
</div>
<!-- 扫描结果提示 -->
<div v-if="scanMessage" class="scan-message" :class="scanMessage.type">
<el-icon v-if="scanMessage.type === 'success'">
<CircleCheck />
</el-icon>
<el-icon v-if="scanMessage.type === 'error'">
<CircleClose />
</el-icon>
<el-icon v-if="scanMessage.type === 'warning'">
<Warning />
</el-icon>
<span>{{ scanMessage.text }}</span>
</div>
</div>
<!-- 批次信息展示 -->
<div v-if="currentBatch" class="batch-info-card">
<div class="batch-header">
<h4>批次信息</h4>
<el-tag type="info" size="small">
库存: {{ currentBatch.availableQuantity }} {{ currentBatch.unit }}
</el-tag>
</div>
<div class="batch-details">
<div class="detail-row">
<span class="label">批次号:</span>
<span class="value batch-no">{{ currentBatch.batchNo }}</span>
</div>
<div class="detail-row">
<span class="label">物资:</span>
<span class="value">{{ currentBatch.materialId }}</span>
</div>
<div class="detail-row">
<span class="label">货架:</span>
<el-tag size="small" type="primary">
{{ currentBatch.locationId }}
</el-tag>
</div>
<div class="detail-row">
<span class="label">有效期:</span>
<span class="value" :class="{
'expiry-warning': isNearExpiry(currentBatch.expirationDate)
}">
{{ formatDate(currentBatch.expirationDate) }}
<el-icon v-if="isNearExpiry(currentBatch.expirationDate)" color="#e6a23c">
<Warning />
</el-icon>
</span>
</div>
<div class="detail-row">
<span class="label">供应商:</span>
<span class="value">{{ currentBatch.supplierName }}</span>
</div>
</div>
<!-- 出库数量输入 -->
<div class="quantity-input-section">
<div class="input-row">
<span class="label">本次出库数量:</span>
<el-input-number v-model="outboundQuantity" :min="0.01" :precision="3" :step="1" size="large"
controls-position="right" style="width: 200px;" />
<span class="unit">{{ currentBatch.unit }}</span>
</div>
<div class="quantity-tips">
<span v-if="remainingNeed > 0">
还需出库: <strong>{{ remainingNeed }}</strong> {{ currentBatch.unit }}
</span>
<el-button type="text" size="small"
@click="outboundQuantity = Math.min(currentBatch.quantity, remainingNeed)">
填充所需数量
</el-button>
<el-button type="text" size="small" @click="outboundQuantity = currentBatch.quantity">
全部出库
</el-button>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button type="primary" size="large" :loading="processingOutbound" :disabled="!canOutbound"
@click="handleOutbound">
<el-icon>
<Check />
</el-icon>
确认出库
</el-button>
<el-button size="large" @click="clearScanResult">
取消
</el-button>
</div>
</div>
<!-- 本次出库流水记录 -->
<div class="transaction-section">
<div class="section-header">
<h4><el-icon>
<Document />
</el-icon> 本次出库记录</h4>
<el-button type="danger" size="small" :disabled="!lastTransaction" @click="handleUndoLast">
<el-icon>
<Delete />
</el-icon>
撤销最后操作
</el-button>
</div>
<el-table :data="recentTransactions" stripe height="200px" v-loading="loadingTransactions">
<el-table-column label="时间" width="140">
<template #default="{ row }">
{{ formatTime(row.scannedAt) }}
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="150" />
<el-table-column label="出库数量" width="100" align="right">
<template #default="{ row }">
{{ row.quantity }} {{ row.unit }}
</template>
</el-table-column>
<el-table-column prop="location" label="库位" width="100" />
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button type="danger" size="small" link @click="undoTransaction(row.id)">
撤销
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 批次详情对话框 -->
<BatchDetailDialog v-model="showBatchDialog" :detail="selectedDetail" :transactions="detailTransactions" />
<!-- 批次推荐对话框 -->
<BatchRecommendationDialog v-model="showRecommendationDialog" :detail="selectedDetail"
@select-batch="handleSelectRecommendedBatch" />
</div>
</el-dialog>
</template>
<script setup>
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
User,
Calendar,
Check,
VideoPause,
VideoPlay,
List,
Search,
CircleCheck,
CircleClose,
Warning,
Document,
Delete,
MagicStick
} from '@element-plus/icons-vue'
import BatchDetailDialog from './BatchDetailDialog.vue'
import BatchRecommendationDialog from './BatchRecommendationDialog.vue'
import { listWmsOutboundOrderItem, getWmsOutboundOrderItem, updateWmsOutboundOrderItem } from '@/api/ware/wmsOutboundOrderItem'
import { listWmsOperationRecord, addWmsOperationRecord } from '@/api/ware/wmsOperationRecord'
import { updateWmsOutboundOrder, confirmOutbound } from '@/api/ware/wmsOutboundOrder'
import useUserStore from '@/store/modules/user'
import { cancelByDetailId, cancelByRelationId } from '@/api/ware/wmsOutboundOrder'
import { listWmsInventory } from "@/api/ware/wmsInventory"
const { proxy } = getCurrentInstance()
const { outbound_type, out_order_status } = proxy.useDict('outbound_type', 'out_order_status')
const props = defineProps({
visible: Boolean,
order: Object
})
const emit = defineEmits(['update:visible', 'order-updated', 'operation-log'])
const userStore = useUserStore()
// 响应式数据
const orderDetails = ref([])
const currentDetailId = ref('')
const currentBatch = ref(null)
const scanInput = ref('')
const scanMessage = ref(null)
const outboundQuantity = ref(0)
const recentTransactions = ref([])
const selectedDetail = ref(null)
const detailTransactions = ref([])
const lastTransaction = ref(null)
// 对话框控制
const dialogVisible = computed({
get() {
return props.visible
},
set(value) {
emit('update:visible', value)
}
})
// 对话框关闭处理
const handleClose = () => {
emit('update:visible', false)
}
// 状态标志
const loadingDetails = ref(false)
const scanning = ref(false)
const processingOutbound = ref(false)
const confirming = ref(false)
const loadingTransactions = ref(false)
// 对话框控制
const showBatchDialog = ref(false)
const showRecommendationDialog = ref(false)
// 计算属性
const totalPlanned = computed(() => {
return orderDetails.value.reduce((sum, detail) => sum + (detail.planQuantity || 0), 0)
})
const totalActual = computed(() => {
return orderDetails.value.reduce((sum, detail) => sum + (detail.actualQuantity || 0), 0)
})
const completionRate = computed(() => {
return totalPlanned.value > 0 ? Math.min((totalActual.value / totalPlanned.value) * 100, 100) : 0
})
const canConfirm = computed(() => {
if (props.order.status !== 'EXECUTING') return false
return orderDetails.value.every(detail => detail.actualQuantity > 0)
})
const remainingNeed = computed(() => {
if (!currentDetailId.value || !currentBatch.value) return 0
const detail = orderDetails.value.find(d => d.itemId === currentDetailId.value)
if (!detail) return 0
return Math.max(0, (detail.planQuantity || 0) - (detail.actualQuantity || 0))
})
const maxOutboundQuantity = computed(() => {
if (!currentBatch.value) return 0
return Math.min(currentBatch.value.availableQuantity, remainingNeed.value)
})
const canOutbound = computed(() => {
return outboundQuantity.value > 0 &&
outboundQuantity.value <= maxOutboundQuantity.value &&
!processingOutbound.value
})
// 方法
const loadOrderDetails = async () => {
loadingDetails.value = true
try {
const response = await listWmsOutboundOrderItem({ orderId: props.order.orderId, pageNum: 1, pageSize: 1000 })
orderDetails.value = response.rows || []
} catch (error) {
ElMessage.error('加载明细失败')
} finally {
loadingDetails.value = false
}
}
const loadRecentTransactions = async () => {
loadingTransactions.value = true
try {
const response = await listWmsOperationRecord({
orderId: props.order.orderId,
operationType: 'OUTBOUND',
pageSize: 50, // 限制只显示最近的50条记录
sortBy: 'createTime',
sortDesc: true
})
recentTransactions.value = response.data || []
lastTransaction.value = response.data[0] || null
} catch (error) {
console.error('加载交易记录失败:', error)
ElMessage.error('加载交易记录失败')
} finally {
loadingTransactions.value = false
}
}
const focusScanInput = () => {
nextTick(() => {
const input = document.querySelector('.scan-input-area input')
if (input) {
; (input).focus()
}
})
}
const handleScan = async () => {
if (!scanInput.value.trim()) {
showScanMessage('请输入或扫描批次号', 'warning')
return
}
scanning.value = true
scanMessage.value = null
try {
// 调用扫描批次接口
const scanResponse = await listWmsInventory({
rfidTag: scanInput.value
})
const scanItem = scanResponse.rows?.length ? scanResponse.rows[0] : null;
if (!scanItem) {
showScanMessage('未找到对应的批次', 'error')
return
}
// 检查该库存物资是否在出库的明细中
const matchedDetail = orderDetails.value.find(d => d.materialId === scanItem.materialId)
if (matchedDetail) {
currentDetailId.value = matchedDetail.itemId
currentBatch.value = scanItem
// 自动设置出库数量
const maxQty = Math.min(scanItem.availableQuantity, (matchedDetail.planQuantity || 0) - (matchedDetail.actualQuantity || 0))
outboundQuantity.value = maxQty > 0 ? maxQty : 0
showScanMessage(`成功匹配物资: ${matchedDetail.materialId}`, 'success')
} else {
showScanMessage('该物资不在出库单计划中', 'error')
currentBatch.value = null
}
} catch (error) {
showScanMessage(error.message || '扫描失败', 'error')
currentBatch.value = null
} finally {
scanning.value = false
scanInput.value = ''
focusScanInput()
}
}
const handleOutbound = async () => {
if (!currentBatch.value || !currentDetailId.value) return
processingOutbound.value = true
try {
// 获取当前明细
const currentDetail = orderDetails.value.find(d => d.itemId === currentDetailId.value)
if (!currentDetail) {
showScanMessage('未找到对应的出库明细', 'error')
return
}
// 确认出库
const confirmResponse = await confirmOutbound({
rfidTag: currentBatch.value.rfidTag,
num: outboundQuantity.value,
orderId: props.order.orderId,
operatorId: userStore.id,
})
if (confirmResponse.code !== 200) {
showScanMessage(confirmResponse.msg || '出库失败', 'error')
return
}
// 更新出库单明细的实际数量
await updateWmsOutboundOrderItem({
itemId: currentDetail.itemId,
actualQuantity: confirmResponse.data.pickingQuantity + (currentDetail.actualQuantity || 0),
})
showScanMessage(`出库成功: ${outboundQuantity.value} ${currentBatch.value.unit}`, 'success')
// 重新加载数据
await Promise.all([
loadOrderDetails(),
])
// 清空当前批次
clearScanResult()
// 触发更新事件
emit('order-updated')
} catch (error) {
showScanMessage(error.message || '出库失败', 'error')
} finally {
processingOutbound.value = false
}
}
const handleUndoLast = async () => {
if (!lastTransaction.value) return
try {
// 获取最后一次操作记录
const lastRecord = lastTransaction.value
// 找到对应的出库单明细
const currentDetail = orderDetails.value.find(d => d.id === lastRecord.itemId)
if (!currentDetail) {
ElMessage.error('未找到对应的出库明细')
return
}
// 计算新的实际数量(减去已出库的数量)
const newActualQuantity = Math.max(0, (currentDetail.actualQuantity || 0) - (lastRecord.quantity || 0))
// 更新出库单明细
await updateWmsOutboundOrderItem({
id: currentDetail.id,
actualQuantity: newActualQuantity
})
// 记录撤销操作日志
await addWmsOperationRecord({
orderId: props.order.id,
itemId: currentDetail.id,
operationType: 'UNDO_OUTBOUND',
operationContent: `撤销批次 ${lastRecord.batchNo} 的出库操作,数量 ${lastRecord.quantity || 0} ${lastRecord.unit}`,
materialId: currentDetail.materialId,
batchNo: lastRecord.batchNo,
quantity: -(lastRecord.quantity || 0), // 使用负数表示撤销
unit: currentDetail.unit
})
ElMessage.success('已撤销最后操作')
// 重新加载数据
await Promise.all([
loadOrderDetails(),
// loadRecentTransactions()
])
emit('operation-log', {
type: 'UNDO',
message: `撤销批次 ${lastRecord.batchNo} 的出库`,
timestamp: new Date()
})
} catch (error) {
console.error('撤销失败:', error)
ElMessage.error('撤销失败')
}
}
const showBatchRecommendation = async () => {
// if (!selectedDetail.value) {
// const detail = orderDetails.value.find(d => d.id === currentDetailId.value)
// if (!detail) {
// ElMessage.warning('请先选择需要出库的明细行')
// return
// }
// selectedDetail.value = detail
// }
showRecommendationDialog.value = true
}
const handleSelectRecommendedBatch = (batch) => {
currentBatch.value = batch
scanInput.value = batch.batchNo
showRecommendationDialog.value = false
// 自动触发扫描
nextTick(() => {
handleScan()
})
}
const clearScanResult = () => {
currentBatch.value = null
outboundQuantity.value = 0
scanMessage.value = null
focusScanInput()
}
const showScanMessage = (text, type = 'info') => {
scanMessage.value = { text, type }
setTimeout(() => {
scanMessage.value = null
}, 3000)
}
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
}
const formatTime = (date) => {
return new Date(date).toLocaleString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const isNearExpiry = (expiryDate) => {
const expiry = new Date(expiryDate)
const now = new Date()
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 30 && diffDays > 0
}
const handleCancel = async (row) => {
try {
await cancelByDetailId({ itemId: row.itemId }, userStore.id)
ElMessage.success('已撤销成功')
} catch (error) {
console.error('撤销失败:', error)
ElMessage.error('撤销失败')
}
}
/**
* 监听键盘回车键
*/
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleScan()
}
}
/**
* 监听键盘空格键,确认出库
*/
const handleSpaceKeyDown = (e) => {
if (e.key === 'Space') {
handleOutbound()
}
}
// 生命周期
onMounted(() => {
focusScanInput();
// 添加事件监听器
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keydown', handleSpaceKeyDown)
})
onBeforeUnmount(() => {
// 移除事件监听器
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keydown', handleSpaceKeyDown)
})
watch(() => props.order, async (newOrder) => {
if (newOrder) {
await loadOrderDetails()
// await loadRecentTransactions()
}
}, { deep: true })
</script>
<style scoped>
.outbound-execution-container {
height: 100%;
display: flex;
flex-direction: column;
background: white;
border-radius: 8px;
overflow: hidden;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
}
.order-info {
flex: 1;
}
.order-title {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.order-title h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.order-meta {
display: flex;
gap: 25px;
font-size: 14px;
opacity: 0.9;
}
.order-meta span {
display: flex;
align-items: center;
gap: 5px;
}
.order-actions {
display: flex;
gap: 10px;
}
.execution-layout {
flex: 1;
display: flex;
min-height: 0;
}
.detail-section,
.scan-section {
flex: 1;
padding: 20px;
overflow: auto;
border-right: 1px solid #ebeef5;
}
.scan-section {
max-width: 500px;
background: #f8fafc;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
}
.section-header h3,
.section-header h4 {
margin: 0;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.section-header h3 {
color: #303133;
}
.section-header h4 {
color: #606266;
}
.summary-info {
display: flex;
align-items: center;
font-size: 14px;
color: #606266;
}
.material-name {
font-weight: 500;
color: #303133;
}
.material-spec {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.planned-quantity {
font-weight: 600;
color: #303133;
}
.actual-quantity {
display: flex;
align-items: center;
justify-content: flex-end;
}
.actual-quantity span {
font-weight: 600;
}
.actual-quantity .completed {
color: #67c23a;
}
.actual-quantity .warning {
color: #e6a23c;
}
.scan-input-area {
margin-bottom: 20px;
}
.input-wrapper {
margin-bottom: 10px;
}
.scan-message {
padding: 10px 15px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
animation: fadeIn 0.3s;
}
.scan-message.success {
background: #f0f9eb;
color: #67c23a;
border: 1px solid #e1f3d8;
}
.scan-message.error {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #fde2e2;
}
.scan-message.warning {
background: #fdf6ec;
color: #e6a23c;
border: 1px solid #faecd8;
}
.batch-info-card {
background: white;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s;
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.batch-header h4 {
margin: 0;
color: #303133;
}
.batch-details {
margin-bottom: 20px;
}
.detail-row {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
}
.detail-row .label {
width: 80px;
color: #909399;
flex-shrink: 0;
}
.detail-row .value {
flex: 1;
color: #606266;
}
.batch-no {
font-family: 'Monaco', 'Consolas', monospace;
background: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #e1e4e8;
}
.expiry-warning {
color: #e6a23c !important;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
}
.quantity-input-section {
margin: 20px 0;
padding: 15px;
background: #f8fafc;
border-radius: 6px;
}
.input-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.input-row .label {
font-weight: 500;
color: #303133;
}
.quantity-tips {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #606266;
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.transaction-section {
margin-top: 20px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:deep(.el-table__row.current-row) {
background-color: #ecf5ff !important;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa !important;
}
</style>
\ No newline at end of file
import { ref } from 'vue'
// 模拟数据存储
const mockOrders = ref([
{
id: '1',
orderNo: 'OUT-20231227-001',
status: 'EXECUTING',
operatorId: 'OP001',
operatorName: '张三',
totalAmount: 12500.50,
remarks: '紧急出库,生产线急需',
createdAt: '2023-12-27 09:30:00',
warehouseName: '成品仓A'
},
{
id: '2',
orderNo: 'OUT-20231226-005',
status: 'COMPLETED',
operatorId: 'OP002',
operatorName: '李四',
totalAmount: 8500.00,
remarks: '正常销售出库',
createdAt: '2023-12-26 14:20:00',
confirmedAt: '2023-12-26 16:45:00',
warehouseName: '成品仓B'
}
])
const mockOrderDetails = ref([
{
id: '101',
orderId: '1',
materialId: 'MAT001',
materialCode: 'P-1001',
materialName: '不锈钢螺丝',
specification: 'M8*25',
unit: '个',
plannedQuantity: 1000,
actualQuantity: 750,
unitPrice: 2.5,
plannedAmount: 2500,
actualAmount: 1875,
batchCount: 3
},
{
id: '102',
orderId: '1',
materialId: 'MAT002',
materialCode: 'P-1002',
materialName: '轴承',
specification: '6202ZZ',
unit: '个',
plannedQuantity: 500,
actualQuantity: 500,
unitPrice: 15.0,
plannedAmount: 7500,
actualAmount: 7500,
batchCount: 2
}
])
const mockInventoryBatches = ref([
{
id: 'B001',
batchNo: 'BATCH-202312-001',
materialId: 'MAT001',
materialCode: 'P-1001',
materialName: '不锈钢螺丝',
specification: 'M8*25',
unit: '个',
quantity: 300,
unitCost: 2.3,
location: 'A-01-02',
warehouseName: '成品仓A',
productionDate: '2023-11-15',
expiryDate: '2026-11-15',
supplierName: '上海五金',
status: 'NORMAL'
}
])
// 模拟操作日志数据
const mockOperationLogs = ref([
{
id: 'LOG001',
orderId: '1',
orderNo: 'OUT-20231227-001',
operatorId: 'OP001',
operatorName: '张三',
operationType: 'CREATE',
operationDesc: '创建出库单',
success: true,
createdAt: '2023-12-27 09:30:00',
duration: 120
},
{
id: 'LOG002',
orderId: '1',
orderNo: 'OUT-20231227-001',
operatorId: 'OP001',
operatorName: '张三',
operationType: 'SCAN',
operationDesc: '扫描批次 BATCH-202312-001,出库数量 50',
success: true,
createdAt: '2023-12-27 10:30:15',
duration: 850
}
])
// API函数
export const fetchOrders = async () => {
// 模拟API调用
return new Promise(resolve => {
setTimeout(() => {
resolve([...mockOrders.value])
}, 500)
})
}
export const deleteOrder = async (orderId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const index = mockOrders.value.findIndex(o => o.id === orderId)
if (index === -1) {
reject(new Error('订单不存在'))
return
}
// 检查订单状态,只有草稿状态的订单可以删除
const order = mockOrders.value[index]
if (order.status !== 'DRAFT') {
reject(new Error('只有草稿状态的订单可以删除'))
return
}
// 删除订单
mockOrders.value.splice(index, 1)
// 删除相关订单详情
mockOrderDetails.value = mockOrderDetails.value.filter(d => d.orderId !== orderId)
resolve()
}, 800)
})
}
export const createOrder = async (orderData) => {
return new Promise(resolve => {
setTimeout(() => {
// 创建新订单
const newOrder = {
id: Date.now().toString(),
orderNo: `OUT-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
status: 'DRAFT',
operatorId: orderData.operatorId || 'OP001',
operatorName: orderData.operatorName || '张三',
totalAmount: orderData.totalAmount || 0,
remarks: orderData.remarks || '',
createdAt: new Date().toISOString(),
warehouseName: orderData.warehouseName || '成品仓A',
...orderData
}
// 添加到模拟数据
mockOrders.value.unshift(newOrder)
// 创建订单详情
if (orderData.details && Array.isArray(orderData.details)) {
const newDetails = orderData.details.map(detail => ({
id: `${newOrder.id}-${Date.now().toString()}`,
orderId: newOrder.id,
...detail
}))
mockOrderDetails.value.push(...newDetails)
}
resolve(newOrder)
}, 1000)
})
}
export const fetchOperationLogs = async (filters = {}) => {
return new Promise(resolve => {
setTimeout(() => {
let logs = [...mockOperationLogs.value]
// 按操作类型过滤
if (filters.operationType) {
logs = logs.filter(log => log.operationType === filters.operationType)
}
// 按操作员过滤
if (filters.operatorName) {
logs = logs.filter(log =>
log.operatorName.toLowerCase().includes(filters.operatorName.toLowerCase())
)
}
// 按订单号过滤
if (filters.orderNo) {
logs = logs.filter(log =>
log.orderNo.toLowerCase().includes(filters.orderNo.toLowerCase())
)
}
// 按日期范围过滤
if (filters.dateRange && filters.dateRange.length === 2) {
const [start, end] = filters.dateRange
const startDate = new Date(start)
const endDate = new Date(end)
endDate.setHours(23, 59, 59, 999)
logs = logs.filter(log => {
const logDate = new Date(log.createdAt)
return logDate >= startDate && logDate <= endDate
})
}
// 按订单ID过滤
if (filters.orderId) {
logs = logs.filter(log => log.orderId === filters.orderId)
}
// 按成功状态过滤
if (filters.success !== undefined) {
logs = logs.filter(log => log.success === filters.success)
}
// 排序(默认按创建时间倒序)
logs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
resolve(logs)
}, 600)
})
}
export const fetchOrderDetails = async (orderId) => {
return new Promise(resolve => {
setTimeout(() => {
const details = mockOrderDetails.value.filter(d => d.orderId === orderId)
resolve([...details])
}, 300)
})
}
export const scanBatch = async (
batchNo,
orderId,
quantity
) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const batch = mockInventoryBatches.value.find(b => b.batchNo === batchNo)
if (!batch) {
reject(new Error('批次不存在或已失效'))
return
}
if (batch.quantity <= 0) {
reject(new Error('库存不足'))
return
}
if (quantity !== undefined) {
// 模拟出库操作
if (quantity > batch.quantity) {
reject(new Error('出库数量超过库存数量'))
return
}
// 更新库存数量
batch.quantity -= quantity
// 更新订单明细
const detail = mockOrderDetails.value.find(
d => d.orderId === orderId && d.materialId === batch.materialId
)
if (detail) {
detail.actualQuantity += quantity
detail.actualAmount += quantity * batch.unitCost
}
}
resolve({ ...batch })
}, 800)
})
}
export const confirmOutbound = async (orderId) => {
return new Promise(resolve => {
setTimeout(() => {
const order = mockOrders.value.find(o => o.id === orderId)
if (order) {
order.status = 'COMPLETED'
order.confirmedAt = new Date().toISOString()
}
resolve()
}, 1000)
})
}
export const fetchRecentTransactions = async (orderId) => {
// 模拟数据
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
id: 'T001',
orderId,
detailId: '101',
batchId: 'B001',
batchNo: 'BATCH-202312-001',
materialName: '不锈钢螺丝',
quantity: 50,
unit: '个',
unitCost: 2.3,
amount: 115,
location: 'A-01-02',
operatorName: '张三',
scannedAt: '2023-12-27 10:30:15',
status: 'NORMAL'
}
])
}, 300)
})
}
export const undoTransaction = async (transactionId) => {
return new Promise(resolve => {
setTimeout(() => {
console.log(`撤销交易 ${transactionId}`)
resolve()
}, 800)
})
}
export const getBatchRecommendation = async (
materialId
) => {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
batchId: 'B001',
batchNo: 'BATCH-202312-001',
materialId,
materialName: '不锈钢螺丝',
recommendedQuantity: 250,
currentStock: 300,
location: 'A-01-02',
expiryDate: '2026-11-15',
productionDate: '2023-11-15',
supplierName: '上海五金',
priorityScore: 95,
reason: ['先进先出', '库位最近']
}
])
}, 500)
})
}
export const pauseOutbound = async (orderId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const order = mockOrders.value.find(o => o.id === orderId)
if (!order) {
reject(new Error('订单不存在'))
return
}
if (order.status !== 'EXECUTING') {
reject(new Error('订单当前状态无法暂停'))
return
}
order.status = 'PAUSED'
order.pausedAt = new Date().toISOString()
resolve(order)
}, 800)
})
}
export const resumeOutbound = async (orderId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const order = mockOrders.value.find(o => o.id === orderId)
if (!order) {
reject(new Error('订单不存在'))
return
}
if (order.status !== 'PAUSED') {
reject(new Error('订单当前状态无法恢复'))
return
}
order.status = 'EXECUTING'
order.resumedAt = new Date().toISOString()
resolve(order)
}, 800)
})
}
\ No newline at end of file
...@@ -9,13 +9,19 @@ ...@@ -9,13 +9,19 @@
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="关联单号" prop="relatedOrderNo"> <el-form-item label="出库类型" prop="orderType">
<el-input <el-select
v-model="queryParams.relatedOrderNo" v-model="queryParams.orderType"
placeholder="请输入关联单号" placeholder="请选择出库类型"
clearable clearable
@keyup.enter="handleQuery" >
<el-option
v-for="item in outbound_type"
:key="item.value"
:label="item.label"
:value="item.value"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
...@@ -46,7 +52,6 @@ ...@@ -46,7 +52,6 @@
</el-table-column> </el-table-column>
<el-table-column label="仓库ID" align="center" prop="warehouseId" /> <el-table-column label="仓库ID" align="center" prop="warehouseId" />
<el-table-column label="客户ID" align="center" prop="customerId" /> <el-table-column label="客户ID" align="center" prop="customerId" />
<el-table-column label="关联单号" align="center" prop="relatedOrderNo" />
<el-table-column label="总数量" align="center" prop="totalQuantity" /> <el-table-column label="总数量" align="center" prop="totalQuantity" />
<el-table-column label="总金额" align="center" prop="totalAmount" /> <el-table-column label="总金额" align="center" prop="totalAmount" />
<el-table-column label="状态" align="center" prop="orderStatus"> <el-table-column label="状态" align="center" prop="orderStatus">
...@@ -72,11 +77,13 @@ ...@@ -72,11 +77,13 @@
</el-table-column> </el-table-column>
<!-- <el-table-column label="出库策略" align="center" prop="outboundStrategy" /> --> <!-- <el-table-column label="出库策略" align="center" prop="outboundStrategy" /> -->
<el-table-column label="备注" align="center" prop="remark" /> <el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="200"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="250">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:edit']">修改</el-button> <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:remove']">删除</el-button> <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:remove']">删除</el-button>
<el-button link type="primary" icon="Document" @click="handlePrint(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:print']">打印</el-button> <el-button link type="primary" icon="Document" @click="handlePrint(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:print']">打印出库单</el-button>
<el-button link type="primary" icon="Check" @click="handleOutbound(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:execute']">出库</el-button>
<el-button link type="primary" icon="Check" @click="handleInventoryFlow(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:flow']">出库流水</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
...@@ -109,7 +116,7 @@ ...@@ -109,7 +116,7 @@
<el-form-item label="仓库" prop="warehouseId"> <el-form-item label="仓库" prop="warehouseId">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="18"> <el-col :span="18">
<el-input v-model="form.warehouseId" placeholder="请选择仓库" readonly /> <el-input v-model="form.warehouseName" placeholder="请选择仓库" readonly />
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-button type="primary" @click="openWarehouseSelect">选择仓库</el-button> <el-button type="primary" @click="openWarehouseSelect">选择仓库</el-button>
...@@ -117,13 +124,11 @@ ...@@ -117,13 +124,11 @@
</el-row> </el-row>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="客户" prop="customerId"> <el-form-item label="客户" prop="customerId">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="18"> <el-col :span="18">
<el-input v-model="form.customerId" placeholder="请选择客户" readonly /> <el-input v-model="form.customerName" placeholder="请选择客户" readonly />
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-button type="primary" @click="openCustomerSelect">选择客户</el-button> <el-button type="primary" @click="openCustomerSelect">选择客户</el-button>
...@@ -132,18 +137,10 @@ ...@@ -132,18 +137,10 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="关联单号" prop="relatedOrderNo">
<el-input v-model="form.relatedOrderNo" placeholder="请输入关联单号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总数量" prop="totalQuantity"> <el-form-item label="总数量" prop="totalQuantity">
<el-input v-model="form.totalQuantity" placeholder="请输入总数量" disabled /> <el-input v-model="form.totalQuantity" placeholder="请输入总数量" disabled />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="总金额" prop="totalAmount"> <el-form-item label="总金额" prop="totalAmount">
<el-input v-model="form.totalAmount" placeholder="请输入总金额" disabled /> <el-input v-model="form.totalAmount" placeholder="请输入总金额" disabled />
...@@ -151,7 +148,7 @@ ...@@ -151,7 +148,7 @@
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="出库人" prop="applicantId"> <el-form-item label="出库人" prop="applicantId">
<el-input v-model="form.applicantId" placeholder="请输入申请人" /> <el-input v-model="form.applicantName" placeholder="请输入申请人" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
...@@ -164,8 +161,6 @@ ...@@ -164,8 +161,6 @@
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="预计发货日期" prop="expectedDeliveryDate"> <el-form-item label="预计发货日期" prop="expectedDeliveryDate">
<el-date-picker clearable <el-date-picker clearable
...@@ -176,7 +171,7 @@ ...@@ -176,7 +171,7 @@
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <!-- <el-col :span="8">
<el-form-item label="实际发货日期" prop="actualDeliveryDate"> <el-form-item label="实际发货日期" prop="actualDeliveryDate">
<el-date-picker clearable <el-date-picker clearable
v-model="form.actualDeliveryDate" v-model="form.actualDeliveryDate"
...@@ -185,8 +180,7 @@ ...@@ -185,8 +180,7 @@
placeholder="请选择实际发货日期"> placeholder="请选择实际发货日期">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
</el-col> </el-col> -->
</el-row>
<!-- <el-row :gutter="20"> <!-- <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="拣货员" prop="pickingPersonId"> <el-form-item label="拣货员" prop="pickingPersonId">
...@@ -245,9 +239,8 @@ ...@@ -245,9 +239,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> --> </el-row> -->
<el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注" prop="remark"> <el-form-item label="出库原因" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" /> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item> </el-form-item>
</el-col> </el-col>
...@@ -260,20 +253,15 @@ ...@@ -260,20 +253,15 @@
<el-button type="primary" size="small" icon="Plus" @click="addDetail">添加明细</el-button> <el-button type="primary" size="small" icon="Plus" @click="addDetail">添加明细</el-button>
</el-col> </el-col>
</el-row> </el-row>
<el-table :data="form.details" border style="width: 100%"> <el-table :data="form.items" border style="width: 100%">
<el-table-column prop="materialName" label="物资名称" width="150"> <el-table-column prop="materialName" label="物资名称" width="150">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.materialName" placeholder="请输入物资名称" size="small" disabled /> <el-input v-model="scope.row.materialName" placeholder="请输入物资名称" size="small" disabled />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="planQuantity" label="计划数量" width="100"> <el-table-column prop="planQuantity" label="计划数量" width="120">
<template #default="scope">
<el-input v-model.number="scope.row.planQuantity" placeholder="请输入计划数量" size="small" />
</template>
</el-table-column>
<el-table-column prop="actualQuantity" label="实际数量" width="100">
<template #default="scope"> <template #default="scope">
<el-input v-model.number="scope.row.actualQuantity" placeholder="请输入实际数量" size="small" /> <el-input v-model.number="scope.row.planQuantity" placeholder="请输入计划数量" size="small" @change="calculateAmount(scope.row)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="unit" label="单位" width="80"> <el-table-column prop="unit" label="单位" width="80">
...@@ -283,23 +271,19 @@ ...@@ -283,23 +271,19 @@
</el-table-column> </el-table-column>
<el-table-column prop="unitPrice" label="单价" width="100"> <el-table-column prop="unitPrice" label="单价" width="100">
<template #default="scope"> <template #default="scope">
<el-input v-model.number="scope.row.unitPrice" placeholder="请输入单价" size="small" /> <el-input v-model.number="scope.row.unitPrice" placeholder="请输入单价" size="small" @change="calculateAmount(scope.row)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="amount" label="金额" width="100"> <el-table-column prop="amount" label="金额" width="100">
<template #default="scope"> <template #default="scope">
<el-input v-model.number="scope.row.amount" placeholder="请输入金额" size="small" /> <el-input v-model.number="scope.row.amount" placeholder="请输入金额" size="small" disabled />
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="120">
<template #default="scope">
<el-input v-model="scope.row.batchNo" placeholder="请输入批次号" size="small" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="productionDate" label="生产日期" width="150"> <el-table-column prop="productionDate" label="生产日期" width="150">
<template #default="scope"> <template #default="scope">
<el-date-picker clearable <el-date-picker clearable
v-model="scope.row.productionDate" v-model="scope.row.productionDate"
class="!w-full"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择生产日期" placeholder="请选择生产日期"
...@@ -311,6 +295,7 @@ ...@@ -311,6 +295,7 @@
<template #default="scope"> <template #default="scope">
<el-date-picker clearable <el-date-picker clearable
v-model="scope.row.expirationDate" v-model="scope.row.expirationDate"
class="!w-full"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择失效日期" placeholder="请选择失效日期"
...@@ -338,15 +323,6 @@ ...@@ -338,15 +323,6 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 库存选择对话框组件 -->
<InventorySelectDialog
ref="inventorySelectDialogRef"
v-model:visible="inventoryDialogVisible"
title="选择库存"
:warehouse-id="data.form.warehouseId"
@confirm="handleInventoryConfirm"
/>
<!-- 仓库选择对话框组件 --> <!-- 仓库选择对话框组件 -->
<WarehouseSelectDialog <WarehouseSelectDialog
ref="warehouseSelectDialogRef" ref="warehouseSelectDialogRef"
...@@ -362,6 +338,23 @@ ...@@ -362,6 +338,23 @@
title="选择客户" title="选择客户"
@confirm="handleCustomerConfirm" @confirm="handleCustomerConfirm"
/> />
<!-- 物资选择对话框组件 -->
<MaterialSelectDialog
ref="materialSelectDialogRef"
v-model:visible="materialDialogVisible"
title="选择物资"
@confirm="handleMaterialConfirm"
/>
<!-- 出库执行组件 -->
<OutboundExecution
v-model:visible="outboundVisible"
:order="currentOrder"
/>
<!-- 出库流水详情对话框 -->
<BatchDetailDialog v-model="showBatchDialog" ref="batchDetailDialogRef" />
</div> </div>
</template> </template>
...@@ -369,13 +362,18 @@ ...@@ -369,13 +362,18 @@
import { listWmsOutboundOrder, getWmsOutboundOrder, delWmsOutboundOrder, addWmsOutboundOrder, updateWmsOutboundOrder } from "@/api/ware/wmsOutboundOrder" import { listWmsOutboundOrder, getWmsOutboundOrder, delWmsOutboundOrder, addWmsOutboundOrder, updateWmsOutboundOrder } from "@/api/ware/wmsOutboundOrder"
import WarehouseSelectDialog from "@/components/WarehouseSelectDialog.vue" import WarehouseSelectDialog from "@/components/WarehouseSelectDialog.vue"
import CustomerSelectDialog from "@/components/CustomerSelectDialog.vue" import CustomerSelectDialog from "@/components/CustomerSelectDialog.vue"
import InventorySelectDialog from "@/components/InventorySelectDialog.vue" import MaterialSelectDialog from "@/components/MaterialSelectDialog.vue"
import { generateOutboundOrderNo } from "@/api/ware/codeGenerator" import { generateOutboundOrderNo } from "@/api/ware/codeGenerator"
import OutboundExecution from "./components/OutboundExecution.vue"
import useUserStore from '@/store/modules/user'
import {dayjs} from 'element-plus'
import BatchDetailDialog from './components/BatchDetailDialog.vue'
const { proxy } = getCurrentInstance()
const { proxy } = getCurrentInstance()
const {outbound_type, out_order_status} = proxy.useDict('outbound_type', 'out_order_status') const {outbound_type, out_order_status} = proxy.useDict('outbound_type', 'out_order_status')
const userStore = useUserStore()
const wmsOutboundOrderList = ref([]) const wmsOutboundOrderList = ref([])
const open = ref(false) const open = ref(false)
const loading = ref(true) const loading = ref(true)
...@@ -386,6 +384,10 @@ const multiple = ref(true) ...@@ -386,6 +384,10 @@ const multiple = ref(true)
const total = ref(0) const total = ref(0)
const title = ref("") const title = ref("")
// 出库执行弹框控制
const outboundVisible = ref(false)
const currentOrder = ref(null)
// 库存选择对话框状态 // 库存选择对话框状态
const inventoryDialogVisible = ref(false) const inventoryDialogVisible = ref(false)
// 库存选择对话框实例引用 // 库存选择对话框实例引用
...@@ -401,6 +403,24 @@ const customerDialogVisible = ref(false) ...@@ -401,6 +403,24 @@ const customerDialogVisible = ref(false)
// 客户选择对话框实例引用 // 客户选择对话框实例引用
const customerSelectDialogRef = ref(null) const customerSelectDialogRef = ref(null)
// 物资选择对话框状态
const materialDialogVisible = ref(false)
// 物资选择对话框实例引用
const materialSelectDialogRef = ref(null)
// 出库流水详情对话框状态
const showBatchDialog = ref(false)
// 出库流水详情对话框实例引用
const batchDetailDialogRef = ref(null)
// 处理出库按钮点击事件
const handleOutbound = (row) => {
currentOrder.value = row
outboundVisible.value = true
}
const data = reactive({ const data = reactive({
form: {}, form: {},
queryParams: { queryParams: {
...@@ -437,6 +457,18 @@ const data = reactive({ ...@@ -437,6 +457,18 @@ const data = reactive({
warehouseId: [ warehouseId: [
{ required: true, message: "仓库ID不能为空", trigger: "blur" } { required: true, message: "仓库ID不能为空", trigger: "blur" }
], ],
customerId: [
{ required: true, message: "客户ID不能为空", trigger: "blur" }
],
totalQuantity: [
{ required: true, message: "总数量不能为空", trigger: "blur" }
],
totalAmount: [
{ required: true, message: "总金额不能为空", trigger: "blur" }
],
remark: [
{ required: true, message: "出库原因不能为空", trigger: "blur" }
],
} }
}) })
...@@ -487,7 +519,7 @@ function reset() { ...@@ -487,7 +519,7 @@ function reset() {
createTime: null, createTime: null,
updateBy: null, updateBy: null,
updateTime: null, updateTime: null,
details: [] items: []
} }
proxy.resetForm("wmsOutboundOrderRef") proxy.resetForm("wmsOutboundOrderRef")
} }
...@@ -514,6 +546,7 @@ function handleSelectionChange(selection) { ...@@ -514,6 +546,7 @@ function handleSelectionChange(selection) {
/** 新增按钮操作 */ /** 新增按钮操作 */
function handleAdd() { function handleAdd() {
reset() reset()
setApplicantId()
// 调用API生成出库单号 // 调用API生成出库单号
generateOutboundOrderNo().then(response => { generateOutboundOrderNo().then(response => {
form.value.orderNo = response.msg form.value.orderNo = response.msg
...@@ -535,6 +568,7 @@ function handleWarehouseConfirm(selectedWarehouses) { ...@@ -535,6 +568,7 @@ function handleWarehouseConfirm(selectedWarehouses) {
if (selectedWarehouses.length > 0) { if (selectedWarehouses.length > 0) {
const warehouse = selectedWarehouses[0] const warehouse = selectedWarehouses[0]
form.value.warehouseId = warehouse.warehouseId form.value.warehouseId = warehouse.warehouseId
form.value.warehouseName = warehouse.warehouseName
// 可以根据需要添加更多仓库信息字段 // 可以根据需要添加更多仓库信息字段
} }
} }
...@@ -552,6 +586,7 @@ function handleCustomerConfirm(selectedCustomers) { ...@@ -552,6 +586,7 @@ function handleCustomerConfirm(selectedCustomers) {
if (selectedCustomers.length > 0) { if (selectedCustomers.length > 0) {
const customer = selectedCustomers[0] const customer = selectedCustomers[0]
form.value.customerId = customer.customerId form.value.customerId = customer.customerId
form.value.customerName = customer.customerName
// 可以根据需要添加更多客户信息字段 // 可以根据需要添加更多客户信息字段
} }
} }
...@@ -573,14 +608,14 @@ function submitForm() { ...@@ -573,14 +608,14 @@ function submitForm() {
proxy.$refs["wmsOutboundOrderRef"].validate(valid => { proxy.$refs["wmsOutboundOrderRef"].validate(valid => {
if (valid) { if (valid) {
// 验证出库单明细 // 验证出库单明细
if (!form.value.details || form.value.details.length === 0) { if (!form.value.items || form.value.items.length === 0) {
proxy.$modal.msgError("请至少添加一条出库单明细") proxy.$modal.msgError("请至少添加一条出库单明细")
return return
} }
// 验证每条明细的必填字段 // 验证每条明细的必填字段
for (let i = 0; i < form.value.details.length; i++) { for (let i = 0; i < form.value.items.length; i++) {
const detail = form.value.details[i] const detail = form.value.items[i]
if (!detail.materialId) { if (!detail.materialId) {
proxy.$modal.msgError(`第${i + 1}条明细的物资ID不能为空`) proxy.$modal.msgError(`第${i + 1}条明细的物资ID不能为空`)
return return
...@@ -616,54 +651,89 @@ function submitForm() { ...@@ -616,54 +651,89 @@ function submitForm() {
/** 添加明细 */ /** 添加明细 */
function addDetail() { function addDetail() {
if (!form.value.details) { if (!form.value.items) {
form.value.details = [] form.value.items = []
} }
// // 检查是否已选择仓库 // 打开物资选择对话框
// if (!data.form.warehouseId) { materialDialogVisible.value = true
// proxy.$modal.msgWarning("请先选择仓库") // 调用组件方法加载物资列表
// return if (materialSelectDialogRef.value) {
// } materialSelectDialogRef.value.handleQuery()
// 打开库存选择对话框
inventoryDialogVisible.value = true
// 调用组件方法加载库存列表
if (inventorySelectDialogRef.value) {
inventorySelectDialogRef.value.handleQuery()
} }
} }
/** 处理库存选择确认 */ /** 处理物资选择确认 */
function handleInventoryConfirm(selectedInventories) { function handleMaterialConfirm(selectedMaterials) {
if (selectedInventories.length === 0) { if (selectedMaterials.length === 0) {
proxy.$modal.msgWarning("请至少选择一个库存") proxy.$modal.msgWarning("请至少选择一个物资")
return return
} }
// 将选中的库存添加到明细列表 // 为每个选中的物资直接添加到明细列表
selectedInventories.forEach(inventory => { const newItems = selectedMaterials.map(material => {
form.value.details.push({ // 创建新的明细项
materialId: inventory.materialId, return {
materialCode: inventory.materialCode, materialId: material.materialId,
materialName: inventory.materialName, materialCode: material.materialCode,
specification: inventory.specification, materialName: material.materialName,
specification: material.specification,
planQuantity: null, planQuantity: null,
actualQuantity: null, actualQuantity: null,
unit: inventory.unit, unit: material.unit,
unitPrice: inventory.unitPrice, unitPrice: null,
amount: null, amount: null,
batchNo: inventory.batchNo, batchNo: null, // 不自动生成批次号
productionDate: inventory.productionDate, productionDate: null,
expirationDate: inventory.expirationDate, expirationDate: null,
warehouseId: inventory.warehouseId,
locationName: inventory.locationName,
remark: null remark: null
}
}) })
// 将所有新的明细项添加到列表
newItems.forEach(item => {
form.value.items.push(item)
}) })
} }
/** 删除明细 */ /** 删除明细 */
function deleteDetail(index) { function deleteDetail(index) {
form.value.details.splice(index, 1) form.value.items.splice(index, 1)
updateTotal()
}
/** 计算明细金额 */
function calculateAmount(row) {
// 计划数量 * 单价 = 金额
if (row.planQuantity && row.unitPrice) {
// 计算金额并保留2位小数
row.amount = Number((row.planQuantity * row.unitPrice).toFixed(2))
} else {
row.amount = null
}
// 更新主表总数量和总金额
updateTotal()
}
/** 更新主表总数量和总金额 */
function updateTotal() {
if (!form.value.items || form.value.items.length === 0) {
form.value.totalQuantity = 0
form.value.totalAmount = 0
return
}
// 计算总数量(实际数量之和)
const totalQuantity = form.value.items.reduce((sum, item) => {
return sum + (item.planQuantity || 0)
}, 0)
// 计算总金额(金额之和)
const totalAmount = form.value.items.reduce((sum, item) => {
return sum + (item.amount || 0)
}, 0)
form.value.totalQuantity = totalQuantity
form.value.totalAmount = totalAmount
} }
/** 删除按钮操作 */ /** 删除按钮操作 */
...@@ -716,10 +786,14 @@ function handlePrint(row) { ...@@ -716,10 +786,14 @@ function handlePrint(row) {
margin-bottom: 20px; margin-bottom: 20px;
padding: 10px; padding: 10px;
border: 1px solid #eee; border: 1px solid #eee;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
} }
.info-row { .info-row {
display: flex; display: flex;
margin-bottom: 10px; margin-bottom: 10px;
align-items: center;
} }
.info-label { .info-label {
width: 120px; width: 120px;
...@@ -772,14 +846,18 @@ function handlePrint(row) { ...@@ -772,14 +846,18 @@ function handlePrint(row) {
<span>${orderData.totalAmount}</span> <span>${orderData.totalAmount}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">申请人:</span> <span class="info-label">出库人:</span>
<span>${orderData.applicantId}</span> <span>${orderData.applicantId}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">申请时间:</span> <span class="info-label">出库时间:</span>
<span>${orderData.applyTime ? new Date(orderData.applyTime).toLocaleDateString() : ''}</span> <span>${orderData.applyTime ? new Date(orderData.applyTime).toLocaleDateString() : ''}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">预计发货日期:</span>
<span>${orderData.expectedDeliveryDate}</span>
</div>
<div class="info-row">
<span class="info-label">备注:</span> <span class="info-label">备注:</span>
<span>${orderData.remark || ''}</span> <span>${orderData.remark || ''}</span>
</div> </div>
...@@ -792,7 +870,6 @@ function handlePrint(row) { ...@@ -792,7 +870,6 @@ function handlePrint(row) {
<tr> <tr>
<th>物资名称</th> <th>物资名称</th>
<th>计划数量</th> <th>计划数量</th>
<th>实际数量</th>
<th>单位</th> <th>单位</th>
<th>单价</th> <th>单价</th>
<th>金额</th> <th>金额</th>
...@@ -806,13 +883,12 @@ function handlePrint(row) { ...@@ -806,13 +883,12 @@ function handlePrint(row) {
` `
// 添加明细数据 // 添加明细数据
if (orderData.details && orderData.details.length > 0) { if (orderData.items && orderData.items.length > 0) {
orderData.details.forEach(detail => { orderData.items.forEach(detail => {
printContent += ` printContent += `
<tr> <tr>
<td>${detail.materialName}</td> <td>${detail.materialName}</td>
<td>${detail.planQuantity}</td> <td>${detail.planQuantity}</td>
<td>${detail.actualQuantity}</td>
<td>${detail.unit}</td> <td>${detail.unit}</td>
<td>${detail.unitPrice}</td> <td>${detail.unitPrice}</td>
<td>${detail.amount}</td> <td>${detail.amount}</td>
...@@ -856,5 +932,16 @@ function handlePrint(row) { ...@@ -856,5 +932,16 @@ function handlePrint(row) {
}) })
} }
function setApplicantId() {
form.value.applicantId = userStore.id;
form.value.applicantName = userStore.nickName;
form.value.applyTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
}
function handleInventoryFlow(row) {
showBatchDialog.value = true
batchDetailDialogRef.value.openDialog(row)
}
getList() getList()
</script> </script>
...@@ -91,7 +91,6 @@ ...@@ -91,7 +91,6 @@
<el-table v-loading="loading" :data="wmsSupplierList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="wmsSupplierList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="供应商ID" align="center" prop="supplierId" />
<el-table-column label="供应商编码" align="center" prop="supplierCode" /> <el-table-column label="供应商编码" align="center" prop="supplierCode" />
<el-table-column label="供应商名称" align="center" prop="supplierName" /> <el-table-column label="供应商名称" align="center" prop="supplierName" />
<el-table-column label="联系人" align="center" prop="contactPerson" /> <el-table-column label="联系人" align="center" prop="contactPerson" />
......
...@@ -67,7 +67,6 @@ ...@@ -67,7 +67,6 @@
<el-table v-loading="loading" :data="wmsWarehouseList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="wmsWarehouseList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="仓库ID" align="center" prop="warehouseId" />
<el-table-column label="仓库编码" align="center" prop="warehouseCode" /> <el-table-column label="仓库编码" align="center" prop="warehouseCode" />
<el-table-column label="仓库名称" align="center" prop="warehouseName" /> <el-table-column label="仓库名称" align="center" prop="warehouseName" />
<el-table-column label="仓库类型" align="center" prop="warehouseType" /> <el-table-column label="仓库类型" align="center" prop="warehouseType" />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment