Commit cbf9128a by 杨子

feat(入库/出库): 新增入库/出库管理模块

refactor(库存管理): 优化库存管理界面显示和交互

style(组件): 调整多个组件样式和布局

fix(出库单): 修正出库单详情显示问题

perf(对话框): 优化对话框性能和用户体验

docs(注释): 更新部分代码注释和文档
parent 26e22007
import request from '@/utils/request'
// 查询入库单详情列表
export function listWmsInboundOrderItemDetail(query) {
return request({
url: '/ware/wmsInboundOrderItemDetail/list',
method: 'get',
params: query
})
}
// 查询入库单详情详细
export function getWmsInboundOrderItemDetail(itemDetailId) {
return request({
url: '/ware/wmsInboundOrderItemDetail/' + itemDetailId,
method: 'get'
})
}
// 新增入库单详情
export function addWmsInboundOrderItemDetail(data) {
return request({
url: '/ware/wmsInboundOrderItemDetail',
method: 'post',
data: data
})
}
// 修改入库单详情
export function updateWmsInboundOrderItemDetail(data) {
return request({
url: '/ware/wmsInboundOrderItemDetail',
method: 'put',
data: data
})
}
// 删除入库单详情
export function delWmsInboundOrderItemDetail(itemDetailId) {
return request({
url: '/ware/wmsInboundOrderItemDetail/' + itemDetailId,
method: 'delete'
})
}
......@@ -2,7 +2,7 @@
<el-dialog
:title="title"
v-model="dialogVisible"
width="1200px"
width="1500px"
append-to-body
>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
......@@ -28,15 +28,14 @@
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="wmsInventoryFlowList">
<el-table-column label="流水ID" align="center" prop="flowId" />
<el-table v-loading="loading" :data="wmsInventoryFlowList" height="600px">
<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="货架ID" align="center" prop="locationId" />
<el-table-column label="流水类型" align="center" prop="flowType" />
<el-table-column label="流水单号" align="center" prop="flowCode" />
<el-table-column label="批次号" align="center" prop="batchNo" />
<el-table-column label="流水单号" align="center" prop="flowCode" width="150" />
<el-table-column label="批次号" align="center" prop="batchNo" width="150" />
<el-table-column label="变动前数量" align="center" prop="quantityBefore" />
<el-table-column label="变动数量" align="center" prop="quantityChange" />
<el-table-column label="变动后数量" align="center" prop="quantityAfter" />
......
......@@ -25,13 +25,13 @@
<el-table v-loading="loading" :data="inventoryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="物资编码" align="center" prop="materialCode" />
<el-table-column label="物资名称" align="center" prop="materialName" />
<el-table-column label="规格型号" align="center" prop="specification" />
<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="areaId" /> -->
<el-table-column label="货架ID" align="center" prop="locationId" />
<el-table-column label="物资编码" align="center" prop="material.materialCode" />
<el-table-column label="物资名称" align="center" prop="material.materialName" />
<el-table-column label="规格型号" align="center" prop="material.specification" />
<el-table-column label="计量单位" align="center" prop="material.unit" />
<el-table-column label="仓库" align="center" prop="warehouse.warehouseName" />
<el-table-column label="库区" align="center" prop="area.areaName" />
<el-table-column label="货架" align="center" prop="location.locationName" />
<el-table-column label="批次号" align="center" prop="batchNo" width="180" />
<el-table-column label="生产日期" align="center" prop="productionDate" width="180">
<template #default="scope">
......@@ -46,9 +46,8 @@
<el-table-column label="库存数量" align="center" prop="quantity" />
<el-table-column label="可用数量" align="center" prop="availableQuantity" />
<el-table-column label="锁定数量" align="center" prop="lockedQuantity" />
<el-table-column label="单位成本" align="center" prop="unitCost" />
<el-table-column label="入库单价" align="center" prop="unitCost" />
<el-table-column label="总成本" align="center" prop="totalCost" />
<!-- <el-table-column label="质量状态" align="center" prop="qualityStatus" /> -->
<el-table-column label="库存状态" align="center" prop="inventoryStatus">
<template #default="scope">
<dict-tag :options="inventory_status" :value="scope.row.inventoryStatus" />
......
<template>
<div class="location-cascader">
<el-form :model="form" label-width="80px">
<el-form-item label="仓库" v-if="!modelValue.warehouseId">
<el-select v-model="form.warehouseId" placeholder="请选择仓库" @change="handleWarehouseChange">
<el-form :model="form" label-width="50px">
<el-form-item label="仓库">
<el-select v-model="form.warehouseId" placeholder="请选择仓库" @change="handleWarehouseChange" size="small">
<el-option
v-for="warehouse in warehouseList"
:key="warehouse.warehouseId"
......@@ -13,7 +13,7 @@
</el-form-item>
<el-form-item label="库区">
<el-select v-model="form.areaId" placeholder="请选择库区" @change="handleAreaChange" :disabled="!form.warehouseId">
<el-select v-model="form.areaId" placeholder="请选择库区" @change="handleAreaChange" :disabled="!form.warehouseId" size="small">
<el-option
v-for="area in areaList"
:key="area.areaId"
......@@ -24,7 +24,7 @@
</el-form-item>
<el-form-item label="货架">
<el-select v-model="form.locationId" placeholder="请选择货架" :disabled="!form.areaId">
<el-select v-model="form.locationId" placeholder="请选择货架" :disabled="!form.areaId" size="small">
<el-option
v-for="location in locationList"
:key="location.locationId"
......@@ -177,4 +177,7 @@ if (props.modelValue.warehouseId) {
background-color: #f5f7fa;
border-radius: 4px;
}
:deep(.el-form-item) {
margin-bottom: 10px;
}
</style>
\ No newline at end of file
......@@ -17,30 +17,6 @@
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="所属库区" prop="areaId">
<el-input
v-model="queryParams.areaId"
placeholder="请输入所属库区ID"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="X坐标" prop="xCoordinate">
<el-input
v-model="queryParams.xCoordinate"
placeholder="请输入X坐标"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="Y坐标" prop="yCoordinate">
<el-input
v-model="queryParams.yCoordinate"
placeholder="请输入Y坐标"
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>
......@@ -51,7 +27,7 @@
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="货架编码" align="center" prop="locationCode" />
<el-table-column label="货架名称" align="center" prop="locationName" />
<el-table-column label="所属库区" align="center" prop="areaId" />
<el-table-column label="所属库区" align="center" prop="area.areaName" />
<el-table-column label="货架状态" align="center" prop="status" >
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
......
......@@ -34,12 +34,9 @@
/>
</el-form-item>
<el-form-item label="标签状态" prop="tagStatus">
<el-input
v-model="queryParams.tagStatus"
placeholder="请输入标签状态"
clearable
@keyup.enter="handleQuery"
/>
<el-select v-model="queryParams.tagStatus" placeholder="请选择标签状态">
<el-option v-for="item in is_used" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
......@@ -49,19 +46,13 @@
<el-table v-loading="loading" :data="rfidTagList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="标签ID" align="center" prop="tagId" />
<el-table-column label="标签编码" align="center" prop="tagCode" />
<el-table-column label="EPC编码" align="center" prop="epcCode" />
<el-table-column label="TID编码" align="center" prop="tidCode" />
<el-table-column label="标签类型" align="center" prop="tagType" />
<el-table-column label="标签状态" align="center" prop="tagStatus">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.tagStatus" />
</template>
</el-table-column>
<el-table-column label="最后读取时间" align="center" prop="lastReadTime">
<template #default="scope">
<span>{{ parseTime(scope.row.lastReadTime, '{y}-{m}-{d}') }}</span>
<dict-tag :options="is_used" :value="scope.row.tagStatus" />
</template>
</el-table-column>
</el-table>
......@@ -88,7 +79,7 @@ import { ref, reactive, watch } from 'vue'
import { listWmsRfidTag } from "@/api/ware/wmsRfidTag"
import { parseTime } from "@/utils/ruoyi"
const {proxy} = getCurrentInstance()
const { sys_normal_disable } = proxy.useDict('sys_normal_disable')
const { is_used } = proxy.useDict('is_used')
const props = defineProps({
visible: {
type: Boolean,
......@@ -129,7 +120,7 @@ const queryParams = reactive({
epcCode: undefined,
tidCode: undefined,
tagType: undefined,
tagStatus: undefined
tagStatus: '0'
})
// 表格数据
......@@ -148,7 +139,7 @@ const init = () => {
queryParams.epcCode = undefined
queryParams.tidCode = undefined
queryParams.tagType = undefined
queryParams.tagStatus = undefined
queryParams.tagStatus = '0'
}
// 查询数据
......
<template>
<!-- 添加或修改入库单明细对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="wmsInboundOrderItemRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="物资" prop="materialId">
<el-input v-model="form.materialName" placeholder="请输入物资名称" disabled />
</el-form-item>
<el-form-item label="实际数量" prop="actualQuantity">
<el-input v-model="form.actualQuantity" placeholder="请输入实际数量" @change="calculateAmount" />
</el-form-item>
<el-form-item label="计量单位" prop="unit">
<el-input v-model="form.unit" placeholder="请输入计量单位" disabled />
</el-form-item>
<el-form-item label="单价" prop="unitPrice">
<el-input v-model="form.unitPrice" placeholder="请输入单价" @change="calculateAmount" />
</el-form-item>
<el-form-item label="金额" prop="amount">
<el-input v-model="form.amount" placeholder="请输入金额" disabled />
</el-form-item>
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="form.batchNo" placeholder="请输入批次号" />
</el-form-item>
<el-form-item label="生产日期" prop="productionDate">
<el-date-picker clearable v-model="form.productionDate" type="date" value-format="YYYY-MM-DD"
placeholder="请选择生产日期">
</el-date-picker>
</el-form-item>
<el-form-item label="失效日期" prop="expirationDate">
<el-date-picker clearable v-model="form.expirationDate" type="date" value-format="YYYY-MM-DD"
placeholder="请选择失效日期">
</el-date-picker>
</el-form-item>
<el-form-item label="货架" prop="locationId">
<LocationCascaderSelect
class="w-full"
:model-value="{ warehouseId: form.warehouseId, areaId: form.areaId, locationId: form.locationId }"
@update:model-value="(value) => {
if (value.locationId) {
// 检查是否有其他行使用了相同的locationId(这里是单行,所以不需要检查)
}
form.warehouseId = value.warehouseId;
form.areaId = value.areaId;
form.locationId = value.locationId;
}" />
</el-form-item>
<el-form-item label="PTL标签" prop="storageLocation">
<el-input v-model="form.storageLocation" placeholder="请选择PTL标签" readonly >
<template #append>
<el-button slot="append" type="primary" size="small" @click="openPtlTagSelect">选择</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="RFID标签" prop="rfidTagIds">
<el-input v-model="form.rfidTagCodes" placeholder="请选择RFID标签" readonly >
<template #append>
<el-button slot="append" type="primary" size="small" @click="openRfidTagSelect">选择</el-button>
</template>
</el-input>
</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>
<!-- RFID标签选择对话框组件 -->
<RfidTagSelectDialog ref="rfidTagSelectDialogRef" v-model:visible="rfidTagDialogVisible" title="选择RFID标签"
@confirm="handleRfidTagConfirm" />
<!-- PTL标签选择对话框组件 -->
<PtlTagSelectDialog ref="ptlTagSelectDialogRef" v-model:visible="ptlTagDialogVisible" title="选择PTL标签"
@confirm="handlePtlTagConfirm" />
</el-dialog>
</template>
<script setup name="WmsInboundOrderItem">
import { ref, reactive, toRefs, getCurrentInstance } from "vue"
import { getWmsInboundOrderItem } from "@/api/ware/wmsInboundOrderItem"
import { generateBatchNo } from "@/api/ware/codeGenerator" // 假设批次号生成接口在这个文件中
import LocationCascaderSelect from "@/components/LocationCascaderSelect.vue"
import RfidTagSelectDialog from "@/components/RfidTagSelectDialog.vue"
import PtlTagSelectDialog from "@/components/PtlTagSelectDialog.vue"
const { proxy } = getCurrentInstance()
const open = ref(false)
const ids = ref([])
const title = ref("")
// RFID标签选择对话框状态
const rfidTagDialogVisible = ref(false)
const rfidTagSelectDialogRef = ref(null)
// PTL标签选择对话框状态
const ptlTagDialogVisible = ref(false)
const ptlTagSelectDialogRef = ref(null)
const emit = defineEmits(['submit'])
const data = reactive({
form: {
itemId: null,
orderId: null,
materialId: null,
materialName: null,
planQuantity: null,
actualQuantity: null,
unit: null,
unitPrice: null,
amount: null,
batchNo: null,
productionDate: null,
expirationDate: null,
qualityStatus: null,
checkQuantity: null,
unqualifiedQuantity: null,
unqualifiedReason: null,
storageLocation: null,
putawayStatus: null,
remark: null,
warehouseId: null,
areaId: null,
locationId: null,
rfidTagIds: null,
rfidTagCodes: null
},
queryParams: {
pageNum: 1,
pageSize: 10,
orderId: null,
materialId: null,
planQuantity: null,
actualQuantity: null,
unit: null,
unitPrice: null,
amount: null,
batchNo: null,
productionDate: null,
expirationDate: null,
qualityStatus: null,
checkQuantity: null,
unqualifiedQuantity: null,
unqualifiedReason: null,
storageLocation: null,
putawayStatus: null,
},
rules: {
orderId: [
{ required: true, message: "入库单ID不能为空", trigger: "blur" }
],
materialId: [
{ required: true, message: "物资ID不能为空", trigger: "blur" }
],
planQuantity: [
{ required: true, message: "计划数量不能为空", trigger: "blur" }
],
actualQuantity: [
{ required: true, message: "实际数量不能为空", trigger: "blur" }
],
unit: [
{ required: true, message: "计量单位不能为空", trigger: "blur" }
],
unitPrice: [
{ required: true, message: "单价不能为空", trigger: "blur" }
],
amount: [
{ required: true, message: "金额不能为空", trigger: "blur" }
],
batchNo: [
{ required: true, message: "批次号不能为空", trigger: "blur" }
],
productionDate: [
{ required: true, message: "生产日期不能为空", trigger: "blur" }
],
expirationDate: [
{ required: true, message: "过期日期不能为空", trigger: "blur" }
],
warehouseId: [
{ required: true, message: "仓库ID不能为空", trigger: "blur" }
],
areaId: [
{ required: true, message: "库区ID不能为空", trigger: "blur" }
],
locationId: [
{ required: true, message: "存储位置ID不能为空", trigger: "blur" }
],
rfidTagIds: [
{ required: true, message: "RFID标签ID不能为空", trigger: "blur" }
],
}
})
const { form, rules } = toRefs(data)
// 取消按钮
function cancel() {
open.value = false
reset()
}
// 表单重置
function reset() {
form.value = {
itemId: null,
orderId: null,
materialId: null,
planQuantity: null,
actualQuantity: null,
unit: null,
unitPrice: null,
amount: null,
batchNo: null,
productionDate: null,
expirationDate: null,
qualityStatus: null,
checkQuantity: null,
unqualifiedQuantity: null,
unqualifiedReason: null,
storageLocation: null,
putawayStatus: null,
remark: null
}
proxy.resetForm("wmsInboundOrderItemRef")
}
/** 新增按钮操作 */
function handleAdd(inBoundOrderItemDetail) {
reset()
open.value = true
title.value = "执行入库"
// 填充物资信息
if (inBoundOrderItemDetail) {
form.value.orderId = inBoundOrderItemDetail.orderId
form.value.materialId = inBoundOrderItemDetail.materialId || inBoundOrderItemDetail.wmsMaterial?.materialId
form.value.materialName = inBoundOrderItemDetail.materialName || inBoundOrderItemDetail.wmsMaterial?.materialName
form.value.unit = inBoundOrderItemDetail.unit
form.value.unitPrice = inBoundOrderItemDetail.unitPrice
form.value.actualQuantity = inBoundOrderItemDetail.planQuantity // 默认实际数量等于计划数量
generateBatchNo().then(response => {
form.value.batchNo = response.msg
}).catch(error => {
proxy.$message.error('批次号生成失败:' + error.message)
})
// 计算金额
calculateAmount()
}
}
/** 计算金额(数量 * 单价) */
function calculateAmount() {
if (form.value && form.value.actualQuantity !== undefined && form.value.unitPrice !== undefined) {
// 确保数量和单价都是数字
const quantity = parseFloat(form.value.actualQuantity) || 0;
const unitPrice = parseFloat(form.value.unitPrice) || 0;
// 计算金额,保留2位小数
form.value.amount = parseFloat((quantity * unitPrice).toFixed(2));
}
}
/** 打开RFID标签选择对话框 */
function openRfidTagSelect() {
rfidTagDialogVisible.value = true
}
/** 处理RFID标签选择确认 */
function handleRfidTagConfirm(selectedTags) {
const selectedTagIds = selectedTags.map(tag => tag.tagId).join(',')
const selectedTagCodes = selectedTags.map(tag => tag.tagCode).join(',')
form.value.rfidTagIds = selectedTagIds
form.value.rfidTagCodes = selectedTagCodes
}
/** 打开PTL标签选择对话框 */
function openPtlTagSelect() {
ptlTagDialogVisible.value = true
}
/** 处理PTL标签选择确认 */
function handlePtlTagConfirm(selectedTags) {
const selectedTagIds = selectedTags.map(tag => tag.antennaId).join(',')
form.value.storageLocation = selectedTagIds
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const _itemId = row.itemId || ids.value
getWmsInboundOrderItem(_itemId).then(response => {
form.value = response.data
open.value = true
title.value = "修改入库单明细"
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["wmsInboundOrderItemRef"].validate(valid => {
if (valid) {
open.value = false
emit('submit', form.value)
}
})
}
defineExpose({
submitForm,
handleAdd
})
</script>
......@@ -6,7 +6,7 @@
append-to-body
>
<el-table :data="adjustInventoryDetailList" border style="width: 100%" height="600px">
<el-table-column prop="wmsMaterial.materialName" label="物资名称" width="200" fixed="left" />
<el-table-column prop="wmsMaterial.materialName" label="物资名称" width="120" fixed="left" />
<el-table-column prop="batchNo" label="批次号" width="150" />
<el-table-column prop="actualQuantity" label="实际数量" width="120">
<template #default="scope">
......@@ -42,12 +42,12 @@
<span v-else>{{ scope.row.unitPrice }}</span>
</template>
</el-table-column>
<el-table-column label="金额" width="100">
<el-table-column label="金额" width="80">
<template #default="scope">
<span>{{ scope.row.amount }}</span>
</template>
</el-table-column>
<el-table-column prop="locationId" label="货架" width="300">
<el-table-column prop="locationId" label="货架" width="180">
<template #default="scope">
<LocationCascaderSelect
v-if="scope.row.isEditing"
......@@ -62,21 +62,27 @@
</template>
</el-table-column>
<!-- PLT标签 -->
<el-table-column prop="storageLocation" label="PTL标签" width="180">
<el-table-column prop="storageLocation" label="PTL标签" width="150">
<template #default="scope">
<div v-if="scope.row.isEditing">
<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-input v-model="scope.row.storageLocation" placeholder="请选择PTL标签" size="small" readonly>
<template #append>
<el-button slot="append" type="primary" size="small" @click="openPtlTagSelect(scope.$index)">选择</el-button>
</template>
</el-input>
</div>
<span v-else>{{ scope.row.storageLocation || '-' }}</span>
</template>
</el-table-column>
<!-- RFID标签 -->
<el-table-column prop="rfidTagIds" label="RFID标签" width="180">
<el-table-column prop="rfidTagIds" label="RFID标签" width="150">
<template #default="scope">
<div v-if="scope.row.isEditing">
<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-input v-model="scope.row.rfidTagCodes" placeholder="请选择RFID标签" size="small" readonly>
<template #append>
<el-button slot="append" type="primary" size="small" @click="openRfidTagSelect(scope.$index)">选择</el-button>
</template>
</el-input>
</div>
<span v-else>{{ scope.row.rfidTagCodes || '-' }}</span>
</template>
......
<template>
<el-dialog title="入库操作" v-model="dialogVisible" width="1200px" append-to-body>
<el-form :model="inventoryQueryParams" ref="inventoryQueryRef" :inline="true" label-width="120px">
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="inventoryQueryParams.batchNo" placeholder="请输入或扫描批次号" clearable
@keyup.enter="handleInventoryQuery" @input="handleBatchNoInput" />
<el-form-item label="物资编码/名称" prop="materialCodeOrName">
<el-input v-model="inventoryQueryParams.materialCodeOrName" placeholder="请输入物资编码/名称" clearable
@keyup.enter="handleInventoryQuery" @input="handleMaterialCodeOrNameInput" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleInventoryQuery">搜索</el-button>
......@@ -11,64 +11,41 @@
</el-form-item>
</el-form>
<el-table :data="inventoryDetailList" border style="width: 100%; margin-top: 20px;" height="600px">
<el-table-column prop="wmsMaterial.materialName" label="物资名称" width="200" />
<el-table-column prop="batchNo" label="批次号" width="150" />
<el-table-column prop="planQuantity" label="计划数量" width="100" />
<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" />
<el-table :data="inventoryDetailList" border height="600px">
<el-table-column type="expand">
<template #default="props">
<el-table :data="props.row.inBoundItem">
<el-table-column prop="batchNo" label="批次号" width="150" />
<el-table-column prop="actualQuantity" label="入库数量" width="100" />
<el-table-column prop="unitPrice" label="单价" width="100" />
<el-table-column label="金额" width="100" prop="amount" />
<el-table-column prop="locationId" label="货架" width="300" />
<el-table-column prop="storageLocation" label="PTL标签"/>
<el-table-column prop="rfidTagIds" label="RFID标签" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-button link v-if="scope.row.isCanceled === '0'" type="primary"
@click="handleCancelByRelation(scope.row)">撤销</el-button>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column prop="wmsMaterial.materialName" label="物资名称" width="200" />
<el-table-column prop="wmsMaterial.materialCode" label="物资编码" width="150" />
<el-table-column prop="wmsMaterial.specification" label="规格型号" width="150" />
<el-table-column prop="planQuantity" label="计划数量" width="100" />
<el-table-column prop="unitPrice" label="单价" width="100" />
<el-table-column label="金额" width="100">
<template #default="scope">
<span>{{ scope.row.amount }}</span>
</template>
</el-table-column>
<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">
<template #default="scope">
<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>
</template>
</el-table-column>
<el-table-column prop="rfidTagIds" label="RFID标签" width="180">
<template #default="scope">
<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>
</template>
</el-table-column>
<el-table-column prop="unit" label="计量单位" />
<el-table-column label="金额" width="100" prop="amount" />
<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 label="操作" width="100" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleExportQrcode(scope.row)">打印二维码</el-button>
<el-button type="primary" size="small" @click="handleAddInBoundItem(scope.row)">添加入库</el-button>
</template>
</el-table-column>
</el-table>
......@@ -76,7 +53,6 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirmInventory">确认入库</el-button>
</div>
</template>
......@@ -91,17 +67,21 @@
<!-- 二维码打印对话框组件 -->
<QrcodePrintDialog ref="qrcodePrintDialogRef" v-model:visible="qrcodePrintDialogVisible"
:qrcode-value="currentQrcodeValue" @print="handleQrcodePrint" />
<!-- 添加入库明细对话框组件 -->
<AddInBoundItemDialog ref="addInBoundItemDialogRef" @submit="handleAddInBoundItemSubmit" />
</el-dialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { listWmsInboundOrderItem } from "@/api/ware/wmsInboundOrderItem"
import { listWmsInboundOrderItemDetail } from "@/api/ware/wmsInboundOrderItemDetail"
import { addToInventory } from "@/api/ware/wmsInboundOrder"
import RfidTagSelectDialog from "@/components/RfidTagSelectDialog.vue"
import PtlTagSelectDialog from "@/components/PtlTagSelectDialog.vue"
import QrcodePrintDialog from "@/components/QrcodePrintDialog.vue"
import LocationCascaderSelect from "@/components/LocationCascaderSelect.vue"
import AddInBoundItemDialog from "@/views/ware/wmsInboundOrder/components/AddInBoundItemDialog.vue"
const props = defineProps({
visible: {
......@@ -150,6 +130,8 @@ const qrcodePrintDialogRef = ref(null)
const currentEditingRowIndex = ref(-1)
// 当前要打印的二维码值
const currentQrcodeValue = ref('')
// 添加入库明细对话框状态
const addInBoundItemDialogRef = ref(null)
// 处理批次号输入(支持扫描枪)
function handleBatchNoInput() {
......@@ -172,7 +154,7 @@ function calculateAmount(row) {
// 入库明细查询
function handleInventoryQuery() {
listWmsInboundOrderItem({ batchNo: inventoryQueryParams.value.batchNo, orderId: inboundOrder.value.orderId, putawayStatus: '0' }).then(response => {
listWmsInboundOrderItemDetail({ orderId: inboundOrder.value.orderId, putawayStatus: '0' }).then(response => {
if (response.rows.length) {
inventoryDetailList.value = response.rows;
} else {
......@@ -294,9 +276,17 @@ function handleQrcodePrint() {
function openDialog(currentInboundOrder) {
inboundOrder.value = currentInboundOrder
inventoryDetailList.value = []
handleInventoryQuery()
}
function handleAddInBoundItem(row) {
addInBoundItemDialogRef.value?.handleAdd(row)
}
function handleAddInBoundItemSubmit(inBoundOrderItem) {
}
defineExpose({
openDialog
})
......
......@@ -4,9 +4,19 @@
<el-form-item label="入库单号" prop="orderNo">
<el-input v-model="queryParams.orderNo" placeholder="请输入入库单号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="入库类型" prop="orderType">
<el-select v-model="queryParams.orderType" placeholder="请选择入库类型">
<el-option v-for="item in inbound_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="关联单号" prop="relatedOrderNo">
<el-input v-model="queryParams.relatedOrderNo" placeholder="请输入关联单号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="orderStatus">
<el-select v-model="queryParams.orderStatus" placeholder="请选择状态">
<el-option v-for="item in in_order_status" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
......@@ -31,7 +41,7 @@
</el-table-column>
<el-table-column label="仓库" align="center" prop="warehouseName" />
<el-table-column label="供应商" align="center" prop="supplierName" />
<el-table-column label="关联单号" align="center" prop="relatedOrderNo" />
<el-table-column label="关联单号" align="center" prop="relatedOrderNo" width="180" />
<el-table-column label="总数量" align="center" prop="totalQuantity" />
<el-table-column label="总金额" align="center" prop="totalAmount" />
<el-table-column label="状态" align="center" prop="orderStatus">
......@@ -39,37 +49,18 @@
<dict-tag :options="in_order_status" :value="scope.row.orderStatus" />
</template>
</el-table-column>
<el-table-column label="入库人" align="center" prop="applicantUserName" />
<el-table-column label="入库时间" align="center" prop="applyTime" width="180">
<el-table-column label="操作人" align="center" prop="applicantUserName" />
<el-table-column label="操作时间" align="center" prop="applyTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.applyTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="预计到货日期" align="center" prop="expectedArrivalDate" width="180">
<el-table-column label="预计到货日期" align="center" prop="expectedArrivalDate" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.expectedArrivalDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column> -->
<el-table-column label="实际到货日期" align="center" prop="actualArrivalDate" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.actualArrivalDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="质检员" align="center" prop="qualityCheckerId" />
<el-table-column label="质检时间" align="center" prop="qualityCheckTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.qualityCheckTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="质检状态" align="center" prop="qualityStatus" />
<el-table-column label="上架员" align="center" prop="putawayPersonId" />
<el-table-column label="上架时间" align="center" prop="putawayTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.putawayTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="审批状态" align="center" prop="approvalStatus" /> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="备注" align="center" prop="remark" width="200" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="400" fixed="right">
<template #default="scope">
<el-button v-if="scope.row.orderStatus === '0'" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
......@@ -81,7 +72,7 @@
<el-button link type="primary" icon="Check" @click="handleInventory(scope.row)"
v-hasPermi="['ware:wmsInboundOrder:inventory']">入库</el-button>
<el-button v-if="showAdjustBtn(scope.row)" link type="primary" icon="SetUp" @click="handleAdjustInventory(scope.row)"
v-hasPermi="['ware:wmsInboundOrder:adjust']">调整入库明细信息</el-button>
v-hasPermi="['ware:wmsInboundOrder:adjust']">入库后信息调整</el-button>
</template>
</el-table-column>
</el-table>
......@@ -89,7 +80,7 @@
<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="1500px" append-to-body>
<el-form ref="wmsInboundOrderRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
......@@ -137,78 +128,29 @@
<el-input v-model="form.relatedOrderNo" placeholder="请输入关联单号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总数量" prop="totalQuantity">
<el-input v-model="form.totalQuantity" placeholder="请输入总数量" disabled />
<el-col :span="8">
<el-form-item label="操作人" prop="applicantId">
<el-input v-model="form.applicantName" placeholder="请输入操作人" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="总金额" prop="totalAmount">
<el-input v-model="form.totalAmount" placeholder="请输入总金额" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="入库人" prop="applicantId">
<el-input v-model="form.applicantName" placeholder="请输入申请人" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="入库时间" prop="applyTime">
<el-form-item label="操作时间" prop="applyTime">
<el-date-picker clearable v-model="form.applyTime" type="date" value-format="YYYY-MM-DD"
placeholder="请选择申请时间">
placeholder="请选择操作时间" disabled>
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="实际到货日期" prop="actualArrivalDate">
<el-date-picker clearable v-model="form.actualArrivalDate" type="date" value-format="YYYY-MM-DD"
placeholder="请选择实际到货日期">
</el-date-picker>
</el-form-item>
</el-col>
<!-- <el-col :span="12">
<el-col :span="12">
<el-form-item label="预计到货日期" prop="expectedArrivalDate">
<el-date-picker clearable v-model="form.expectedArrivalDate" type="date" value-format="YYYY-MM-DD"
placeholder="请选择预计到货日期">
</el-date-picker>
</el-form-item>
</el-col> -->
</el-row>
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="质检员" prop="qualityCheckerId">
<el-input v-model="form.qualityCheckerId" placeholder="请输入质检员" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="质检时间" prop="qualityCheckTime">
<el-date-picker clearable v-model="form.qualityCheckTime" type="date" value-format="YYYY-MM-DD"
placeholder="请选择质检时间">
</el-date-picker>
</el-form-item>
</el-col>
</el-row> -->
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="上架员" prop="putawayPersonId">
<el-input v-model="form.putawayPersonId" placeholder="请输入上架员" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="上架时间" prop="putawayTime">
<el-date-picker clearable v-model="form.putawayTime" type="date" value-format="YYYY-MM-DD"
placeholder="请选择上架时间">
</el-date-picker>
</el-form-item>
</el-col>
</el-row> -->
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
......@@ -216,7 +158,6 @@
</el-form-item>
</el-col>
</el-row>
<!-- 入库单明细 -->
<el-divider>入库单明细</el-divider>
<el-row :gutter="20" style="margin-bottom: 10px;">
......@@ -224,36 +165,41 @@
<el-button type="primary" size="small" icon="Plus" @click="addDetail">添加明细</el-button>
</el-col>
</el-row>
<el-table :data="form.items" border style="width: 100%">
<el-table :data="form.itemDetails" border style="width: 100%" height="400">
<el-table-column prop="materialName" label="物资名称" width="150">
<template #default="scope">
<el-input v-model="scope.row.materialName" placeholder="请输入物资名称" size="small" disabled />
</template>
</el-table-column>
<el-table-column prop="planQuantity" label="计划数量" width="100">
<el-table-column prop="materialCode" label="物资编号" width="150">
<template #default="scope">
<el-input v-model="scope.row.materialCode" placeholder="请输入物资编号" size="small" disabled />
</template>
</el-table-column>
<el-table-column prop="specification" label="规格型号">
<template #default="scope">
<el-input v-model="scope.row.specification" placeholder="请输入规格型号" size="small" disabled />
</template>
</el-table-column>
<el-table-column prop="planQuantity" label="计划数量" width="150">
<template #default="scope">
<el-input
v-model.number="scope.row.planQuantity"
placeholder="请输入计划数量"
size="small"
type="number"
@change="calculateAmount(scope.row)"
:precision="3"
:step="0.001"
/>
</template>
</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">
<el-input v-model="scope.row.unit" placeholder="请输入单位" size="small" disabled />
<el-input v-model="scope.row.unit" placeholder="请输入计量单位" size="small" disabled />
</template>
</el-table-column>
<el-table-column prop="unitPrice" label="单价" width="100">
<el-table-column prop="unitPrice" label="单价" width="180">
<template #default="scope">
<el-input
v-model.number="scope.row.unitPrice"
......@@ -266,7 +212,7 @@
/>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="100">
<el-table-column prop="amount" label="金额" width="180">
<template #default="scope">
<el-input
v-model.number="scope.row.amount"
......@@ -278,55 +224,15 @@
/>
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="180">
<template #default="scope">
<el-input v-model="scope.row.batchNo" placeholder="请输入批次号" size="small" />
</template>
</el-table-column>
<el-table-column prop="productionDate" label="生产日期" width="180">
<template #default="scope">
<el-date-picker clearable v-model="scope.row.productionDate" type="date" value-format="YYYY-MM-DD"
placeholder="请选择生产日期" size="small" format="YYYY-MM-DD" class="!w-full">
</el-date-picker>
</template>
</el-table-column>
<el-table-column prop="expirationDate" label="失效日期" width="180">
<template #default="scope">
<el-date-picker clearable v-model="scope.row.expirationDate" type="date" value-format="YYYY-MM-DD"
placeholder="请选择失效日期" size="small" format="YYYY-MM-DD" class="!w-full">
</el-date-picker>
</template>
</el-table-column>
<!-- <el-table-column prop="qualityStatus" label="质检状态" width="100">
<template #default="scope">
<el-input v-model="scope.row.qualityStatus" placeholder="请输入质检状态" size="small" />
</template>
</el-table-column>
<el-table-column prop="checkQuantity" label="质检数量" width="100">
<template #default="scope">
<el-input v-model.number="scope.row.checkQuantity" placeholder="请输入质检数量" size="small" />
</template>
</el-table-column> -->
<!-- <el-table-column prop="unqualifiedQuantity" label="不合格数量" width="120">
<template #default="scope">
<el-input v-model.number="scope.row.unqualifiedQuantity" placeholder="请输入不合格数量" size="small" />
</template>
</el-table-column>
<el-table-column prop="unqualifiedReason" label="不合格原因" width="150">
<template #default="scope">
<el-input v-model="scope.row.unqualifiedReason" placeholder="请输入不合格原因" size="small" />
</template>
</el-table-column> -->
<el-table-column prop="remark" label="备注">
<template #default="scope">
<el-input type="textarea" v-model="scope.row.remark" placeholder="请输入备注" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button type="danger" size="small" icon="Delete" @click="deleteDetail(scope.$index)">
删除
{{ scope.row.delFlag === '1' ? '撤销删除' : '标记删除' }}
</el-button>
</template>
</el-table-column>
......@@ -371,12 +277,11 @@
</template>
<script setup name="WmsInboundOrder">
import { listWmsInboundOrder, getWmsInboundOrder, delWmsInboundOrder, addWmsInboundOrder, updateWmsInboundOrder, delWmsInboundOrderItem, updateWmsInboundOrderItem, saveWmsInboundOrder, addToInventory } from "@/api/ware/wmsInboundOrder"
import { generateBatchNo, generateInboundOrderNo } from "@/api/ware/codeGenerator"
import { listWmsInboundOrder, getWmsInboundOrder, delWmsInboundOrder, saveWmsInboundOrder } from "@/api/ware/wmsInboundOrder"
import { generateInboundOrderNo } from "@/api/ware/codeGenerator"
import MaterialSelectDialog from "@/components/MaterialSelectDialog.vue"
import WarehouseSelectDialog from "@/components/WarehouseSelectDialog.vue"
import SupplierSelectDialog from "@/components/SupplierSelectDialog.vue"
import LocationCascaderSelect from "@/components/LocationCascaderSelect.vue"
import InventoryDialog from "./components/InventoryDialog.vue"
import AdjustInventoryDialog from "./components/AdjustInventoryDialog.vue"
import useUserStore from '@/store/modules/user'
......@@ -502,7 +407,7 @@ function handleAdjustInventory(row) {
/** 查询入库单列表 */
/** 查询入库单列表 */
function getList() {
loading.value = true
listWmsInboundOrder(queryParams.value).then(response => {
......@@ -545,7 +450,7 @@ function reset() {
createTime: null,
updateBy: null,
updateTime: null,
items: []
itemDetails: []
}
proxy.resetForm("wmsInboundOrderRef")
}
......@@ -573,7 +478,7 @@ function handleSelectionChange(selection) {
function handleAdd() {
reset()
open.value = true
title.value = "添加入库单"
title.value = "添加入库单"
// 获取当前用户信息
const userStore = useUserStore()
......@@ -645,6 +550,13 @@ function handleUpdate(row) {
const _orderId = row.orderId || ids.value
getWmsInboundOrder(_orderId).then(response => {
form.value = response.data
form.value.itemDetails?.forEach(item => {
item.materialName = item.wmsMaterial?.materialName || ''
item.materialCode = item.wmsMaterial?.materialCode || ''
item.specification = item.wmsMaterial?.specification || ''
})
open.value = true
title.value = "修改入库单"
})
......@@ -656,14 +568,14 @@ function submitForm() {
proxy.$refs["wmsInboundOrderRef"].validate(valid => {
if (valid) {
// 验证入库单明细
if (!form.value.items || form.value.items.length === 0) {
if (!form.value.itemDetails || form.value.itemDetails.length === 0) {
proxy.$modal.msgError("请至少添加一条入库单明细")
return
}
// 验证每条明细的必填字段
for (let i = 0; i < form.value.items.length; i++) {
const detail = form.value.items[i]
for (let i = 0; i < form.value.itemDetails.length; i++) {
const detail = form.value.itemDetails[i]
if (!detail.materialId) {
proxy.$modal.msgError(`第${i + 1}条明细的物资ID不能为空`)
return
......@@ -691,8 +603,8 @@ function submitForm() {
/** 添加明细 */
function addDetail() {
if (!form.value.items) {
form.value.items = []
if (!form.value.itemDetails) {
form.value.itemDetails = []
}
// 打开物资选择对话框
materialDialogVisible.value = true
......@@ -712,16 +624,11 @@ function handleMaterialConfirm(selectedMaterials) {
// 将选中的物资添加到明细列表
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 = {
materialId: material.materialId,
materialName: material.materialName,
materialCode: material.materialCode,
specification: material.specification,
planQuantity: null,
actualQuantity: null,
unit: material.unit,
......@@ -739,47 +646,39 @@ function handleMaterialConfirm(selectedMaterials) {
areaId: null, // 库区ID
locationId: null, // 货架ID
putawayStatus: '0', // 默认待上架
remark: null
remark: null,
delFlag: '0'
}
// 调用接口生成批次号
generateBatchNo().then(response => {
newItem.batchNo = response.msg;
form.value.items.push(newItem)
}).catch(error => {
console.error('生成批次号失败:', error)
})
form.value.itemDetails.push(newItem)
})
}
/** 计算明细金额 */
function calculateAmount(row) {
// 实际数量 * 单价 = 金额
if (row.actualQuantity && row.unitPrice) {
// 计划数量 * 单价 = 金额
if (row.planQuantity && row.unitPrice) {
// 计算金额并保留2位小数
row.amount = Number((row.actualQuantity * row.unitPrice).toFixed(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) {
if (!form.value.itemDetails || form.value.itemDetails.length === 0) {
form.value.totalQuantity = 0
form.value.totalAmount = 0
return
}
// 计算总数量(实际数量之和)
const totalQuantity = form.value.items.reduce((sum, item) => {
return sum + (item.actualQuantity || 0)
const totalQuantity = form.value.itemDetails.reduce((sum, item) => {
return sum + (item.planQuantity || 0)
}, 0)
// 计算总金额(金额之和)
const totalAmount = form.value.items.reduce((sum, item) => {
const totalAmount = form.value.itemDetails.reduce((sum, item) => {
return sum + (item.amount || 0)
}, 0)
......@@ -789,18 +688,14 @@ function updateTotal() {
/** 删除明细 */
function deleteDetail(index) {
const item = form.value.items[index]
if (item.itemId) {
const item = form.value.itemDetails[index]
if (item.itemDetailId) {
// 已有ID的明细,标记为删除
item.delFlag = 1
// 从显示列表中移除
form.value.items.splice(index, 1)
item.delFlag = item.delFlag === '1' ? '0' : '1'
} else {
// 新增的明细,直接删除
form.value.items.splice(index, 1)
form.value.itemDetails.splice(index, 1)
}
// 更新主表总数量和总金额
updateTotal()
}
// 移除了编辑和保存明细的功能
......@@ -817,18 +712,20 @@ function handleDelete(row) {
}).catch(() => { })
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('ware/wmsInboundOrder/export', {
...queryParams.value
}, `wmsInboundOrder_${new Date().getTime()}.xlsx`)
}
/** 打印按钮操作 */
function handlePrint(row) {
// 获取完整的入库单数据,包括明细
getWmsInboundOrder(row.orderId).then(response => {
const orderData = response.data
orderData.totalQuantity = orderData.itemDetails?.reduce((sum, item) => {
return sum + (item.planQuantity || 0)
}, 0) || 0
orderData.totalAmount = orderData.itemDetails?.reduce((sum, item) => {
return sum + (item.amount || 0)
}, 0) || 0
// 创建打印窗口
const printWindow = window.open('', '_blank')
......@@ -903,22 +800,22 @@ function handlePrint(row) {
</div>
<div class="info-row">
<span class="info-label">供应商:</span>
<span>${orderData.supplierName}</span>
<span>${orderData.supplierName || ''}</span>
</div>
<div class="info-row">
<span class="info-label">数量:</span>
<span class="info-label">计划入库数量:</span>
<span>${orderData.totalQuantity}</span>
</div>
<div class="info-row">
<span class="info-label">金额:</span>
<span class="info-label">计划入库金额:</span>
<span>${orderData.totalAmount}</span>
</div>
<div class="info-row">
<span class="info-label">申请人:</span>
<span>${orderData.applicantName}</span>
<span class="info-label">操作人:</span>
<span>${orderData.applicantUserName}</span>
</div>
<div class="info-row">
<span class="info-label">申请时间:</span>
<span class="info-label">操作时间:</span>
<span>${orderData.applyTime ? new Date(orderData.applyTime).toLocaleDateString() : ''}</span>
</div>
<div class="info-row">
......@@ -933,14 +830,12 @@ function handlePrint(row) {
<thead>
<tr>
<th>物资名称</th>
<th>物资编号</th>
<th>规格型号</th>
<th>计划数量</th>
<th>实际数量</th>
<th>单位</th>
<th>计量单位</th>
<th>单价</th>
<th>金额</th>
<th>批次号</th>
<th>生产日期</th>
<th>失效日期</th>
<th>备注</th>
</tr>
</thead>
......@@ -948,19 +843,17 @@ function handlePrint(row) {
`
// 添加明细数据
if (orderData.items && orderData.items.length > 0) {
orderData.items.forEach(detail => {
if (orderData.itemDetails && orderData.itemDetails.length > 0) {
orderData.itemDetails.forEach(detail => {
printContent += `
<tr>
<td>${detail.wmsMaterial.materialName}</td>
<td>${detail.wmsMaterial?.materialName}</td>
<td>${detail.wmsMaterial?.materialCode}</td>
<td>${detail.wmsMaterial?.specification}</td>
<td>${detail.planQuantity}</td>
<td>${detail.actualQuantity}</td>
<td>${detail.unit}</td>
<td>${detail.unitPrice}</td>
<td>${detail.amount}</td>
<td>${detail.batchNo || ''}</td>
<td>${detail.productionDate ? new Date(detail.productionDate).toLocaleDateString() : ''}</td>
<td>${detail.expirationDate ? new Date(detail.expirationDate).toLocaleDateString() : ''}</td>
<td>${detail.remark || ''}</td>
</tr>
`
......@@ -1007,7 +900,7 @@ function handleExportQrcode(row) {
}
function showAdjustBtn(row) {
// 当订单状态不是'0'且入库时间(applyTime)小于等于7天时显示调整按钮
// 当订单状态不是'0'且操作时间(applyTime)小于等于7天时显示调整按钮
return row.orderStatus !== '0' && dayjs(row.applyTime).isSameOrAfter(dayjs().subtract(7, 'day'))
}
......
......@@ -34,10 +34,13 @@
<el-table v-loading="loading" :data="wmsInventoryList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" fixed="left" />
<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="areaId" /> -->
<el-table-column label="货架ID" align="center" prop="locationId" fixed="left" />
<el-table-column label="物资名称" align="center" prop="material.materialName" fixed="left" />
<el-table-column label="物资编号" align="center" prop="material.materialCode" fixed="left" />
<el-table-column label="规格型号" align="center" prop="material.specification" fixed="left" />
<el-table-column label="计量单位" align="center" prop="material.unit" fixed="left" />
<el-table-column label="仓库" align="center" prop="warehouse.warehouseName" />
<el-table-column label="库区" align="center" prop="area.areaName" />
<el-table-column label="货架" align="center" prop="location.locationName"/>
<el-table-column label="批次号" align="center" prop="batchNo" width="180" />
<el-table-column label="生产日期" align="center" prop="productionDate" width="180">
<template #default="scope">
......@@ -52,7 +55,7 @@
<el-table-column label="库存数量" align="center" prop="quantity" />
<el-table-column label="可用数量" align="center" prop="availableQuantity" />
<el-table-column label="锁定数量" align="center" prop="lockedQuantity" />
<el-table-column label="单位成本" align="center" prop="unitCost" />
<el-table-column label="入库单价" align="center" prop="unitCost" />
<el-table-column label="总成本" align="center" prop="totalCost" />
<!-- <el-table-column label="质量状态" align="center" prop="qualityStatus" /> -->
<el-table-column label="库存状态" align="center" prop="inventoryStatus">
......@@ -75,7 +78,6 @@
<span>{{ scope.row.rfidTag || '-' }}</span>
</template>
</el-table-column>
<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="rfidCoverageRate" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right">
......@@ -190,7 +192,7 @@
</el-col>
</el-row>
<!-- 第七行:锁定数量、单位成本 -->
<!-- 第七行:锁定数量、入库单价 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="锁定数量" prop="lockedQuantity">
......@@ -198,8 +200,8 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位成本" prop="unitCost">
<el-input v-model="form.unitCost" placeholder="请输入单位成本" />
<el-form-item label="入库单价" prop="unitCost">
<el-input v-model="form.unitCost" placeholder="请输入入库单价" />
</el-form-item>
</el-col>
</el-row>
......@@ -317,17 +319,50 @@
<div class="location-visual-container">
<!-- 第一列:货架信息 -->
<div class="visual-column location-info">
<h3>当前货架信息</h3>
<el-form :model="selectedLocationInfo" label-width="80px" class="three-col-form">
<h3>库存基础信息</h3>
<el-form :model="selectedInventoryInfo" label-width="80px" class="three-col-form">
<div class="form-row">
<el-form-item label="货架编码">
<span>{{ selectedLocationInfo.locationCode || '-' }}</span>
<el-form-item label="物资名称">
<span>{{ selectedInventoryInfo.materialName || '-' }}</span>
</el-form-item>
<el-form-item label="货架名称">
<span>{{ selectedLocationInfo.locationName || '-' }}</span>
<el-form-item label="物资编号">
<span>{{ selectedInventoryInfo.materialCode || '-' }}</span>
</el-form-item>
<el-form-item label="库区名称">
<span>{{ selectedLocationInfo.areaId || '-' }}</span>
<el-form-item label="规格型号">
<span>{{ selectedInventoryInfo.specification || '-' }}</span>
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="计量单位">
<span>{{ selectedInventoryInfo.unit || '-' }}</span>
</el-form-item>
<el-form-item label="批次号">
<span>{{ selectedInventoryInfo.batchNo || '-' }}</span>
</el-form-item>
<el-form-item label="生产日期">
<span>{{ parseTime(selectedInventoryInfo.productionDate, '{y}-{m}-{d}') || '-' }}</span>
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="有效日期">
<span>{{ parseTime(selectedInventoryInfo.expirationDate, '{y}-{m}-{d}') || '-' }}</span>
</el-form-item>
<el-form-item label="库存数量">
<span>{{ selectedInventoryInfo.quantity || 0 }}</span>
</el-form-item>
<el-form-item label="可用数量">
<span>{{ selectedInventoryInfo.availableQuantity || 0 }}</span>
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="锁定数量">
<span>{{ selectedInventoryInfo.lockedQuantity || 0 }}</span>
</el-form-item>
<el-form-item label="入库单价">
<span>{{ selectedInventoryInfo.unitCost || 0 }}</span>
</el-form-item>
<el-form-item label="总成本">
<span>{{ selectedInventoryInfo.totalCost || 0 }}</span>
</el-form-item>
</div>
</el-form>
......@@ -402,6 +437,22 @@ const selectedLocationInfo = reactive({
materialCount: '0'
})
// 当前选中的库存信息
const selectedInventoryInfo = reactive({
materialName: '',
materialCode: '',
specification: '',
unit: '',
batchNo: '',
productionDate: '',
expirationDate: '',
quantity: 0,
availableQuantity: 0,
lockedQuantity: 0,
unitCost: 0,
totalCost: 0
})
// 存储所有货架的列表
const locations = ref([])
// 存储所有货架的单元格数据,结构:{ locationId: string, cells: Cell[] }
......@@ -702,6 +753,21 @@ function handleLocationVisualization(row) {
listWmsLocation({ pageNum: 1, pageSize: 1000 }).then(response => {
locations.value = response.rows;
})
// 保存当前库存信息
Object.assign(selectedInventoryInfo, {
materialName: row.material?.materialName || '',
materialCode: row.material?.materialCode || '',
specification: row.material?.specification || '',
unit: row.material?.unit || '',
batchNo: row.batchNo || '',
productionDate: row.productionDate || '',
expirationDate: row.expirationDate || '',
quantity: row.quantity || 0,
availableQuantity: row.availableQuantity || 0,
lockedQuantity: row.lockedQuantity || 0,
unitCost: row.unitCost || 0,
totalCost: row.totalCost || 0
})
locationVisualDialogVisible.value = true
}
......
<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"
>
<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 }}
{{ detail.material?.materialCode }}
</el-descriptions-item>
<el-descriptions-item label="物资名称">
{{ detail.materialName }}
{{ detail.material?.materialName }}
</el-descriptions-item>
<el-descriptions-item label="规格型号">
{{ detail.specification }}
{{ detail.material?.specification }}
</el-descriptions-item>
<el-descriptions-item label="计划数量">
<span class="planned-quantity">{{ detail.plannedQuantity }}</span>
<el-descriptions-item label="计划出库数量">
<span class="planned-quantity">{{ detail.planQuantity }}</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 label="实际出库数量">
<span class="actual-quantity">{{ detail.actualQuantity }}</span>
</el-descriptions-item>
<el-descriptions-item label="剩余需求">
<span class="remaining-need">
{{ Math.max(0, detail.plannedQuantity - detail.actualQuantity) }}
<el-descriptions-item label="拣货状态">
<span class="picked-status">
<dict-tag :options="out_order_status" :value="detail.pickingStatus" />
</span>
</el-descriptions-item>
</el-descriptions>
<el-progress
:percentage="completionPercentage"
:status="completionPercentage >= 100 ? 'success' : 'primary'"
:stroke-width="12"
style="margin-top: 15px;"
/>
</div>
<!-- 批次出库记录 -->
......@@ -51,48 +34,36 @@
{{ 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 :data="transactions" stripe border size="small" height="300px">
<el-table-column type="index" label="序号" width="60" align="center" fixed="left" />
<el-table-column label="RFID标签" align="center" prop="rfidCode" />
<el-table-column label="批次号" align="center" prop="batchNo" width="120" />
<el-table-column label="仓库" align="center" prop="warehouse.warehouseName" />
<el-table-column label="库区" align="center" prop="area.areaName" />
<el-table-column label="货架" align="center" prop="location.locationName" />
<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="110">
<template #default="scope">
<span>{{ parseTime(scope.row.outboundTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="是否撤销" align="center" prop="isCanceled">
<template #default="scope">
<dict-tag :options="is_cancel" :value="scope.row.isCanceled" />
</template>
</el-table-column>
<el-table-column label="撤销时间" align="center" prop="cancelTime" width="110">
<template #default="scope">
<span>{{ parseTime(scope.row.cancelTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-button link v-if="scope.row.isCanceled === '0'" type="primary" @click="handleCancelByRelation(scope.row)">撤销</el-button>
</template>
</el-table-column>
</el-table>
</div>
......@@ -123,11 +94,8 @@
<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 v-if="detail?.actualQuantity < detail?.plannedQuantity" type="primary"
@click="handleContinueOutbound">
继续出库
</el-button>
</span>
......@@ -139,6 +107,12 @@
import { ref, computed, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { listWmsOutboundItemInventory } from "@/api/ware/wmsOutboundItemInventory"
import useUserStore from '@/store/modules/user'
import { cancelByRelationId } from '@/api/ware/wmsOutboundOrder'
const { proxy } = getCurrentInstance()
const { outbound_type, out_order_status, is_cancel } = proxy.useDict('outbound_type', 'out_order_status', 'is_cancel')
const props = defineProps({
modelValue: {
type: Boolean,
......@@ -146,6 +120,7 @@ const props = defineProps({
}
})
const userStore = useUserStore()
const detail = ref({})
const transactions = ref([])
......@@ -160,7 +135,7 @@ const visible = computed({
// 计算属性
const dialogTitle = computed(() => {
if (!detail.value) return '批次详情'
return `${detail.value.materialName} - 批次出库详情`
return `${detail.value.material?.materialName || ''} - 批次出库详情`
})
const completionPercentage = computed(() => {
......@@ -178,7 +153,7 @@ const normalTransactionCount = computed(() => {
})
const reversedTransactionCount = computed(() => {
return transactions.value.filter(t => t.status === 'REVERSED').length
return transactions.value.filter(t => t.isCanceled === '1').length
})
const totalAmount = computed(() => {
......@@ -209,16 +184,37 @@ const handleContinueOutbound = () => {
visible.value = false
}
const openDialog = (row) => {
detail.value = row
const openDialog = (outboundOrderItem) => {
detail.value = outboundOrderItem
listWmsOutboundItemInventory({
orderId: row.orderId,
itemId: outboundOrderItem.itemId,
pageNum: 1,
pageSize: 1000
}).then(res => {
transactions.value = res.rows || []
})
}
/**
* 撤销关联流水
* @param row 关联流水行数据
*/
const handleCancelByRelation = async (row) => {
try {
const result = await cancelByRelationId(row, userStore.id)
if (result.code !== 200) {
ElMessage.error(result.msg || '撤销失败')
return
}
row.isCanceled = '1'
ElMessage.success('已撤销成功')
} catch (error) {
console.error('撤销失败:', error)
ElMessage.error('撤销失败')
}
}
defineExpose({
openDialog
})
......@@ -326,7 +322,7 @@ defineExpose({
}
:deep(.el-descriptions__label) {
width: 80px;
width: 120px;
font-weight: 500;
}
......
<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">
<!-- 订单信息头 -->
<el-dialog v-model="dialogVisible" width="90%" :before-close="handleClose" destroy-on-close>
<template #header>
<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 class="order-info flex gap-2 items-center">
<div class="order-no font-bold text-lg">
出库单编号:{{ order.orderNo }}
</div>
<dict-tag :options="out_order_status" :value="order.orderStatus" />
</div>
<div class="order-actions"></div>
</div>
</template>
<div class="outbound-execution-container">
<!-- 双栏布局 -->
<div class="execution-layout">
<!-- 左栏:出库单明细 -->
......@@ -30,57 +23,54 @@
</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 ref="tableRef" :data="orderDetails" stripe highlight-current-row row-key="itemId"
height="calc(100vh - 320px)" v-loading="loadingDetails" @expand-change="handleExpandChange" preserve-expanded-content>
<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 :data="props.row.inventoryRecords">
<el-table-column label="批次号" align="center" prop="batchNo" width="120" />
<el-table-column label="仓库" align="center" prop="warehouse.warehouseName" />
<el-table-column label="库区" align="center" prop="area.areaName" />
<el-table-column label="货架" align="center" prop="location.locationName" />
<el-table-column label="RFID标签" align="center" prop="rfidCode" />
<el-table-column label="数量" align="center" prop="quantity" />
<el-table-column label="单位成本" align="center" prop="unitCost" />
<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="isCanceled">
<template #default="scope">
<dict-tag :options="is_cancel" :value="scope.row.isCanceled" />
</template>
</el-table-column>
<el-table-column label="撤销时间" align="center" prop="cancelTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.cancelTime, '{y}-{m}-{d}') }}</span>
<span>{{ parseTime(scope.row.cancelTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</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">
<!-- <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="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>
<el-button link v-if="scope.row.isCanceled === '0'" type="primary" @click="handleCancelByRelation(scope.row)">撤销</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="material.materialCode" label="物资编码" width="120" />
<el-table-column prop="material.materialName" label="物资名称" width="120" />
<el-table-column prop="material.specification" 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 prop="planQuantity" label="计划出库" align="right" />
<el-table-column prop="actualQuantity" label="已出库" align="right" />
<el-table-column prop="amount" label="金额" align="right" />
<el-table-column prop="pickingStatus" label="拣货状态">
<template #default="scope">
<dict-tag :options="out_order_status" :value="scope.row.pickingStatus" />
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleCancel(scope.row)">
......@@ -109,7 +99,7 @@
<!-- 扫码输入区域 -->
<div class="scan-input-area">
<div class="input-wrapper">
<el-input v-model="scanInput" placeholder="请扫描二维码或输入批次号" size="large" clearable @keyup.enter="handleScan"
<el-input v-model="scanInput" placeholder="请扫描二维码" size="large" clearable @keyup.enter="handleScan"
@clear="clearScanResult" ref="scanInputRef">
<template #prepend>
<el-icon>
......@@ -173,17 +163,13 @@
</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"
<el-input-number v-model="outboundQuantity" :min="0" :precision="3" :step="1" size="large"
controls-position="right" style="width: 200px;" />
<span class="unit">{{ currentBatch.unit }}</span>
</div>
......@@ -233,23 +219,26 @@
<el-table :data="recentTransactions" stripe height="200px" v-loading="loadingTransactions">
<el-table-column label="时间" width="140">
<template #default="{ row }">
{{ formatTime(row.scannedAt) }}
{{ formatTime(row.relation?.outboundTime) }}
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="150" />
<el-table-column prop="materialName" label="物资">
<template #default="{ row }">
{{ row.materialName }}
</template>
</el-table-column>
<el-table-column label="出库数量" width="100" align="right">
<template #default="{ row }">
{{ row.quantity }} {{ row.unit }}
{{ row.thisPickQuantity }}
</template>
</el-table-column>
<el-table-column prop="location" label="库位" width="100" />
<el-table-column label="操作" width="80">
<el-table-column label="状态" width="100" align="right">
<template #default="{ row }">
<el-button type="danger" size="small" link @click="undoTransaction(row.id)">
撤销
</el-button>
{{ row.message }}
</template>
</el-table-column>
<el-table-column prop="locationId" label="货架" width="100" />
</el-table>
</div>
</div>
......@@ -291,9 +280,11 @@ import { updateWmsOutboundOrder, confirmOutbound } from '@/api/ware/wmsOutboundO
import useUserStore from '@/store/modules/user'
import { cancelByDetailId, cancelByRelationId } from '@/api/ware/wmsOutboundOrder'
import { listWmsInventory } from "@/api/ware/wmsInventory"
import {listWmsOutboundItemInventory} from "@/api/ware/wmsOutboundItemInventory"
const { proxy } = getCurrentInstance()
const { outbound_type, out_order_status } = proxy.useDict('outbound_type', 'out_order_status')
const { outbound_type, out_order_status, is_cancel } = proxy.useDict('outbound_type', 'out_order_status', 'is_cancel')
const props = defineProps({
visible: Boolean,
......@@ -305,7 +296,8 @@ const userStore = useUserStore()
// 响应式数据
const orderDetails = ref([])
const currentDetailId = ref('')
const currentBatch = ref(null)
const tableRef = ref(null)
const expandedRows = ref(new Set())
const scanInput = ref('')
const scanMessage = ref(null)
const outboundQuantity = ref(0)
......@@ -313,6 +305,7 @@ const recentTransactions = ref([])
const selectedDetail = ref(null)
const detailTransactions = ref([])
const lastTransaction = ref(null)
const currentBatch = ref(null)
// 对话框控制
const dialogVisible = computed({
......@@ -389,23 +382,20 @@ const loadOrderDetails = async () => {
}
}
const loadRecentTransactions = async () => {
loadingTransactions.value = true
const loadOutboundItemInventory = async (row) => {
try {
const response = await listWmsOperationRecord({
orderId: props.order.orderId,
operationType: 'OUTBOUND',
pageSize: 50, // 限制只显示最近的50条记录
sortBy: 'createTime',
sortDesc: true
const response = await listWmsOutboundItemInventory({
itemId: row.itemId,
pageNum: 1,
pageSize: 1000
})
recentTransactions.value = response.data || []
lastTransaction.value = response.data[0] || null
// 直接更新传入的row对象,保持对象引用不变
row.inventoryRecords = response.rows || []
} catch (error) {
console.error('加载交易记录失败:', error)
ElMessage.error('加载交易记录失败')
} finally {
loadingTransactions.value = false
console.error('加载出库记录失败:', error)
ElMessage.error('加载出库记录失败')
}
}
......@@ -493,19 +483,29 @@ const handleOutbound = async () => {
return
}
// 更新出库单明细的实际数量
await updateWmsOutboundOrderItem({
itemId: currentDetail.itemId,
actualQuantity: confirmResponse.data.pickingQuantity + (currentDetail.actualQuantity || 0),
})
recentTransactions.value.push(confirmResponse.data)
showScanMessage(`出库成功: ${outboundQuantity.value} ${currentBatch.value.unit}`, 'success')
lastTransaction.value = confirmResponse.data?.relation;
// 重新加载数据
await Promise.all([
loadOrderDetails(),
])
tableRef.value?.toggleRowExpansion(currentDetail, true)
// 检查当前行是否已展开
if (expandedRows.value.has(currentDetail.itemId)) {
// 先收缩行
tableRef.value?.toggleRowExpansion(currentDetail, false)
// 然后重新展开并加载最新数据
nextTick(() => {
tableRef.value?.toggleRowExpansion(currentDetail, true)
})
}
// 更新明细的已出库数量
currentDetail.actualQuantity = (currentDetail.actualQuantity || 0) + outboundQuantity.value;
currentDetail.pickedStatus = '1'
showScanMessage(`出库成功: ${outboundQuantity.value} ${currentBatch.value.unit}`, 'success')
// 清空当前批次
clearScanResult()
......@@ -519,6 +519,19 @@ const handleOutbound = async () => {
}
}
const handleExpandChange = (row, tableExpandedRows) => {
// 维护expandedRows集合
if (tableExpandedRows.includes(row)) {
expandedRows.value.add(row.itemId)
// 展开时加载交易记录
loadOutboundItemInventory(row)
} else {
expandedRows.value.delete(row.itemId)
// 收起时清空交易记录
row.inventoryRecords = []
}
}
const handleUndoLast = async () => {
if (!lastTransaction.value) return
......@@ -528,46 +541,35 @@ const handleUndoLast = async () => {
const lastRecord = lastTransaction.value
// 找到对应的出库单明细
const currentDetail = orderDetails.value.find(d => d.id === lastRecord.itemId)
const currentDetail = orderDetails.value.find(d => d.itemId === 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
})
const cancelResponse = await cancelByRelationId(lastRecord.relationId)
// 记录撤销操作日志
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
})
if (cancelResponse.code !== 200) {
ElMessage.error(cancelResponse.msg || '撤销失败')
return
}
ElMessage.success('已撤销最后操作')
// 更新明细的已出库数量
currentDetail.actualQuantity = (currentDetail.actualQuantity || 0) - (lastRecord.quantity || 0);
currentDetail.pickedStatus = currentDetail.actualQuantity === 0 ? '0' : '1'
// 检查当前行是否已展开
if (expandedRows.value.has(currentDetail.itemId)) {
// 先收缩行
tableRef.value?.toggleRowExpansion(currentDetail, false)
// 然后重新展开并加载最新数据
nextTick(() => {
tableRef.value?.toggleRowExpansion(currentDetail, true)
})
}
// 重新加载数据
await Promise.all([
loadOrderDetails(),
// loadRecentTransactions()
])
ElMessage.success('已撤销批次 ' + lastRecord.batchNo)
emit('operation-log', {
type: 'UNDO',
message: `撤销批次 ${lastRecord.batchNo} 的出库`,
timestamp: new Date()
})
} catch (error) {
console.error('撤销失败:', error)
......@@ -633,22 +635,69 @@ const isNearExpiry = (expiryDate) => {
const diffDays = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diffDays <= 30 && diffDays > 0
}
/**
* 撤销明细
* @param row 明细行数据
*/
const handleCancel = async (row) => {
try {
await cancelByDetailId({ itemId: row.itemId }, userStore.id)
const result = await cancelByDetailId(row, userStore.id)
if (result.code !== 200) {
ElMessage.error(result.msg || '撤销失败')
return
}
// 更新出库明细
const updatedDetail = orderDetails.value.find(d => d.itemId === row.itemId)
if (updatedDetail) {
updatedDetail.actualQuantity = 0
updatedDetail.pickedStatus = '0'
}
ElMessage.success('已撤销成功')
// 检查当前行是否已展开
if (expandedRows.value.has(row.itemId)) {
// 先收缩行
tableRef.value?.toggleRowExpansion(row, false)
// 然后重新展开并加载最新数据
nextTick(() => {
tableRef.value?.toggleRowExpansion(row, true)
})
}
} catch (error) {
console.error('撤销失败:', error)
ElMessage.error('撤销失败')
}
}
/**
* 监听键盘回车键
* 撤销关联流水
* @param row 关联流水行数据
*/
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleScan()
const handleCancelByRelation = async (row) => {
try {
const result = await cancelByRelationId(row, userStore.id)
if (result.code !== 200) {
ElMessage.error(result.msg || '撤销失败')
return
}
ElMessage.success('已撤销成功')
// 检查当前行是否已展开
if (expandedRows.value.has(row.itemId)) {
// 先收缩行
tableRef.value?.toggleRowExpansion(row, false)
// 然后重新展开并加载最新数据
nextTick(() => {
tableRef.value?.toggleRowExpansion(row, true)
})
}
} catch (error) {
console.error('撤销失败:', error)
ElMessage.error('撤销失败')
}
}
......@@ -665,20 +714,17 @@ const handleSpaceKeyDown = (e) => {
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 })
......@@ -698,7 +744,7 @@ watch(() => props.order, async (newOrder) => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
padding: 0 16px;
}
.order-info {
......@@ -751,7 +797,7 @@ watch(() => props.order, async (newOrder) => {
}
.scan-section {
max-width: 500px;
max-width: 700px;
background: #f8fafc;
}
......
<template>
<el-dialog
v-model="dialogVisible"
title="出库单详情"
width="1200px"
append-to-body
>
<div class="detail-header flex !justify-end">
<el-button type="success" icon="Document" @click="handlePrint">打印出库单</el-button>
</div>
<!-- 出库单基本信息 -->
<el-descriptions :column="3" border size="small" class="order-info-descriptions">
<el-descriptions-item label="出库单号">
{{ orderDetail.orderNo }}
</el-descriptions-item>
<el-descriptions-item label="出库类型">
<dict-tag :options="outbound_type" :value="orderDetail.orderType" />
</el-descriptions-item>
<el-descriptions-item label="仓库">
{{ orderDetail.warehouseName }}
</el-descriptions-item>
<el-descriptions-item label="客户">
{{ orderDetail.customerName }}
</el-descriptions-item>
<el-descriptions-item label="总数量">
{{ orderDetail.totalQuantity }}
</el-descriptions-item>
<el-descriptions-item label="总金额">
{{ orderDetail.totalAmount }}
</el-descriptions-item>
<el-descriptions-item label="出库人">
{{ orderDetail.nickName || orderDetail.applicantName }}
</el-descriptions-item>
<el-descriptions-item label="出库时间">
{{ orderDetail.applyTime }}
</el-descriptions-item>
<el-descriptions-item label="预计发货日期">
{{ orderDetail.expectedDeliveryDate }}
</el-descriptions-item>
<el-descriptions-item label="出库原因" :span="3">
{{ orderDetail.remark || '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 明细列表 -->
<el-divider>出库单明细</el-divider>
<el-table :data="orderDetail.items" border style="width: 100%" height="400px">
<el-table-column prop="material.materialCode" label="物资编码" width="150" fixed="left" />
<el-table-column prop="material.materialName" label="物资名称" width="150" fixed="left" />
<el-table-column prop="material.specification" label="规格型号" width="150" fixed="left" />
<el-table-column prop="planQuantity" label="计划数量" width="120" />
<el-table-column prop="actualQuantity" label="实际数量" width="120" />
<el-table-column prop="unit" label="计量单位" width="80" />
<el-table-column prop="unitPrice" label="单价" />
<el-table-column prop="amount" label="金额" />
<el-table-column prop="pickingStatus" label="拣货状态">
<template #default="scope">
<dict-tag :options="out_order_status" :value="scope.row.pickingStatus" />
</template>
</el-table-column>
<el-table-column prop="productionDate" label="生产日期" width="120" />
<el-table-column prop="expirationDate" label="失效日期" width="120" />
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="100">
<template #default="scope">
<el-button link type="primary" @click="handleInventoryFlow(scope.row)" >出库流水</el-button>
</template>
</el-table-column>
</el-table>
<!-- 出库流水详情对话框 -->
<BatchDetailDialog v-model="showBatchDialog" ref="batchDetailDialogRef" />
</el-dialog>
</template>
<script setup name="WmsOutboundOrderDetail">
import { ref, onMounted, getCurrentInstance, watch, computed } from 'vue'
import { getWmsOutboundOrder } from "@/api/ware/wmsOutboundOrder"
import BatchDetailDialog from './BatchDetailDialog.vue'
const { proxy } = getCurrentInstance()
// 字典
const { out_order_status, outbound_type } = proxy.useDict('out_order_status', 'outbound_type')
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
order: {
type: Object,
default: () => ({})
}
})
// Emits
const emit = defineEmits(['update:visible'])
// 响应式数据
const orderDetail = ref({
orderNo: '',
orderType: '',
warehouseId: '',
warehouseName: '',
areaId: '',
areaName: '',
locationId: '',
locationName: '',
customerId: '',
customerName: '',
totalQuantity: '',
totalAmount: '',
applicantName: '',
applyTime: '',
expectedDeliveryDate: '',
actualDeliveryDate: '',
remark: '',
items: [],
})
// 出库流水详情对话框状态
const showBatchDialog = ref(false)
// 出库流水详情对话框实例引用
const batchDetailDialogRef = ref(null)
// 计算属性:控制弹框显示
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
// 获取出库单详情
function getOrderDetail() {
if (props.order?.orderId) {
getWmsOutboundOrder(props.order.orderId).then(response => {
const data = props.order || {}
orderDetail.value = {
...props.order,
warehouseName: data.warehouse?.warehouseName || '',
areaName: data.area?.areaName || '',
locationName: data.location?.locationName || '',
customerName: data.customerUser?.customerName || '',
nickName: data.applicantUser?.nickName || '',
items: response.data?.items || []
}
})
}
}
// 打印功能
function handlePrint() {
const orderData = orderDetail.value
// 创建打印窗口
const printWindow = window.open('', '_blank')
// 构建打印内容
let printContent = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>出库单打印</title>
<style>
@media print {
@page {
margin: 0;
padding: 0;
size: landscape; /* 设置为横向 */
}
}
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
.order-info {
margin-bottom: 20px;
padding: 10px;
border: 1px solid #eee;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.info-row {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.info-label {
width: 120px;
font-weight: bold;
margin-right: 10px;
}
.table-container {
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.footer {
margin-top: 30px;
text-align: right;
}
</style>
</head>
<body>
<h1>出库单</h1>
<div class="order-info">
<div class="info-row">
<span class="info-label">出库单号:</span>
<span>${orderData.orderNo}</span>
</div>
<div class="info-row">
<span class="info-label">出库类型:</span>
<span>${proxy.selectDictLabel(outbound_type.value, orderData.orderType)}</span>
</div>
<div class="info-row">
<span class="info-label">状态:</span>
<span>${proxy.selectDictLabel(out_order_status.value, orderData.orderStatus)}</span>
</div>
<div class="info-row">
<span class="info-label">仓库:</span>
<span>${orderData.warehouse?.warehouseName || orderData.warehouseName || ''}</span>
</div>
<div class="info-row">
<span class="info-label">客户名称:</span>
<span>${orderData.customerUser?.customerName || orderData.customerName || ''}</span>
</div>
<div class="info-row">
<span class="info-label">联系人:</span>
<span>${orderData.customerUser?.contactPerson || ''}</span>
</div>
<div class="info-row">
<span class="info-label">联系电话:</span>
<span>${orderData.customerUser?.contactPhone || ''}</span>
</div>
<div class="info-row">
<span class="info-label">总数量:</span>
<span>${orderData.totalQuantity}</span>
</div>
<div class="info-row">
<span class="info-label">总金额:</span>
<span>${orderData.totalAmount}</span>
</div>
<div class="info-row">
<span class="info-label">出库人:</span>
<span>${orderData.applicantUser?.nickName || orderData.nickName || orderData.applicantName || ''}</span>
</div>
<div class="info-row">
<span class="info-label">出库时间:</span>
<span>${orderData.applyTime ? new Date(orderData.applyTime).toLocaleDateString() : ''}</span>
</div>
<div class="info-row">
<span class="info-label">预计发货日期:</span>
<span>${orderData.expectedDeliveryDate ? new Date(orderData.expectedDeliveryDate).toLocaleDateString() : ''}</span>
</div>
<div class="info-row">
<span class="info-label">备注:</span>
<span>${orderData.remark || ''}</span>
</div>
</div>
<div class="table-container">
<h3>出库单明细</h3>
<table>
<thead>
<tr>
<th>物资编码</th>
<th>物资名称</th>
<th>规格型号</th>
<th>计划数量</th>
<th>单位</th>
<th>单价</th>
<th>金额</th>
<th>生产日期</th>
<th>失效日期</th>
<th>备注</th>
</tr>
</thead>
<tbody>
`
// 添加明细数据
if (orderData.items && orderData.items.length > 0) {
orderData.items.forEach(detail => {
printContent += `
<tr>
<td>${detail.material?.materialCode || detail.materialCode || ''}</td>
<td>${detail.material?.materialName || detail.materialName || ''}</td>
<td>${detail.material?.specification || detail.specification || ''}</td>
<td>${detail.planQuantity}</td>
<td>${detail.unit}</td>
<td>${detail.unitPrice}</td>
<td>${detail.amount}</td>
<td>${detail.productionDate ? new Date(detail.productionDate).toLocaleDateString() : ''}</td>
<td>${detail.expirationDate ? new Date(detail.expirationDate).toLocaleDateString() : ''}</td>
<td>${detail.remark || ''}</td>
</tr>
`
})
} else {
printContent += `
<tr>
<td colspan="10">无明细数据</td>
</tr>
`
}
// 完成打印内容
printContent += `
</tbody>
</table>
</div>
<div class="footer">
<p>打印时间:${new Date().toLocaleString()}</p>
</div>
</body>
</html>
`
// 写入打印窗口
printWindow.document.write(printContent)
printWindow.document.close()
// 执行打印
printWindow.print()
// 关闭打印窗口
printWindow.close()
}
function handleInventoryFlow(row) {
showBatchDialog.value = true
batchDetailDialogRef.value.openDialog(row)
}
// 监听弹框显示状态变化
watch(() => props.order, (newVal) => {
if (newVal) {
getOrderDetail()
}
}, { immediate: true, deep: true })
</script>
<style scoped>
.detail-header {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-card {
padding: 20px;
}
.order-info-form {
margin-bottom: 20px;
}
</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
......@@ -50,8 +50,18 @@
<dict-tag :options="outbound_type" :value="scope.row.orderType" />
</template>
</el-table-column>
<el-table-column label="仓库ID" align="center" prop="warehouseId" />
<el-table-column label="客户ID" align="center" prop="customerId" />
<el-table-column label="仓库" align="center" prop="warehouse.warehouseName" />
<el-table-column label="客户" align="center" prop="customerUser.customerName" width="180">
<template #default="scope">
<div v-if="scope.row.customerUser">
<div>{{ scope.row.customerUser.customerName || '无' }}</div>
<div class="contact-info">
{{ scope.row.customerUser.contactPerson || '无' }} / {{ scope.row.customerUser.contactPhone || '无' }}
</div>
</div>
<div v-else></div>
</template>
</el-table-column>
<el-table-column label="总数量" align="center" prop="totalQuantity" />
<el-table-column label="总金额" align="center" prop="totalAmount" />
<el-table-column label="状态" align="center" prop="orderStatus">
......@@ -59,8 +69,8 @@
<dict-tag :options="out_order_status" :value="scope.row.orderStatus" />
</template>
</el-table-column>
<el-table-column label="出库人" align="center" prop="applicantId" />
<el-table-column label="出库时间" align="center" prop="applyTime" width="180">
<el-table-column label="出库人" align="center" prop="applicantUser.nickName" />
<el-table-column label="操作时间" align="center" prop="applyTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.applyTime, '{y}-{m}-{d}') }}</span>
</template>
......@@ -70,20 +80,14 @@
<span>{{ parseTime(scope.row.expectedDeliveryDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="实际发货日期" align="center" prop="actualDeliveryDate" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.actualDeliveryDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="出库策略" align="center" prop="outboundStrategy" /> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="250">
<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="Delete" @click="handleDelete(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:remove']">删除</el-button>
<el-button link type="primary" icon="Eye" @click="handleDetail(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:view']">详情</el-button>
<el-button v-if="scope.row.orderStatus === '0'" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['ware:wmsOutboundOrder:edit']">修改</el-button>
<el-button v-if="scope.row.orderStatus === '0'" 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="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>
</el-table-column>
</el-table>
......@@ -355,6 +359,9 @@
<!-- 出库流水详情对话框 -->
<BatchDetailDialog v-model="showBatchDialog" ref="batchDetailDialogRef" />
<!-- 出库单详情对话框 -->
<WmsOutboundOrderDetail v-model:visible="detailVisible" :order="currentOrder" />
</div>
</template>
......@@ -368,6 +375,7 @@ import OutboundExecution from "./components/OutboundExecution.vue"
import useUserStore from '@/store/modules/user'
import {dayjs} from 'element-plus'
import BatchDetailDialog from './components/BatchDetailDialog.vue'
import WmsOutboundOrderDetail from './components/detail.vue'
const { proxy } = getCurrentInstance()
......@@ -393,6 +401,10 @@ const inventoryDialogVisible = ref(false)
// 库存选择对话框实例引用
const inventorySelectDialogRef = ref(null)
// 详情弹框控制
const detailVisible = ref(false)
const currentOrderId = ref('')
// 仓库选择对话框状态
const warehouseDialogVisible = ref(false)
// 仓库选择对话框实例引用
......@@ -413,8 +425,6 @@ const showBatchDialog = ref(false)
// 出库流水详情对话框实例引用
const batchDetailDialogRef = ref(null)
// 处理出库按钮点击事件
const handleOutbound = (row) => {
currentOrder.value = row
......@@ -758,7 +768,11 @@ function handleExport() {
function handlePrint(row) {
// 获取完整的出库单数据,包括明细
getWmsOutboundOrder(row.orderId).then(response => {
const orderData = response.data
const orderData = {
...response.data,
...row,
items: response.data?.items || []
}
// 创建打印窗口
const printWindow = window.open('', '_blank')
......@@ -771,16 +785,22 @@ function handlePrint(row) {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>出库单打印</title>
<style>
@media print {
@page {
margin: 0;
padding: 0;
size: landscape; /* 设置为横向 */
}
}
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
border: 1px solid #ccc;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
margin-bottom: 10px;
}
.order-info {
margin-bottom: 20px;
......@@ -830,12 +850,28 @@ function handlePrint(row) {
<span>${orderData.orderNo}</span>
</div>
<div class="info-row">
<span class="info-label">出库类型:</span>
<span>${proxy.selectDictLabel(outbound_type.value, orderData.orderType)}</span>
</div>
<div class="info-row">
<span class="info-label">状态:</span>
<span>${proxy.selectDictLabel(out_order_status.value, orderData.orderStatus)}</span>
</div>
<div class="info-row">
<span class="info-label">仓库:</span>
<span>${orderData.warehouseId}</span>
<span>${orderData.warehouse?.warehouseName || orderData.warehouse || ''}</span>
</div>
<div class="info-row">
<span class="info-label">客户名称:</span>
<span>${orderData.customerUser?.customerName || orderData.customerName || ''}</span>
</div>
<div class="info-row">
<span class="info-label">联系人:</span>
<span>${orderData.customerUser?.contactPerson || ''}</span>
</div>
<div class="info-row">
<span class="info-label">客户:</span>
<span>${orderData.customerId}</span>
<span class="info-label">联系电话:</span>
<span>${orderData.customerUser?.contactPhone || ''}</span>
</div>
<div class="info-row">
<span class="info-label">总数量:</span>
......@@ -847,7 +883,7 @@ function handlePrint(row) {
</div>
<div class="info-row">
<span class="info-label">出库人:</span>
<span>${orderData.applicantId}</span>
<span>${orderData.applicantUser?.nickName || orderData.applicantName || orderData.applicantId || ''}</span>
</div>
<div class="info-row">
<span class="info-label">出库时间:</span>
......@@ -855,7 +891,7 @@ function handlePrint(row) {
</div>
<div class="info-row">
<span class="info-label">预计发货日期:</span>
<span>${orderData.expectedDeliveryDate}</span>
<span>${orderData.expectedDeliveryDate ? new Date(orderData.expectedDeliveryDate).toLocaleDateString() : ''}</span>
</div>
<div class="info-row">
<span class="info-label">备注:</span>
......@@ -868,12 +904,13 @@ function handlePrint(row) {
<table>
<thead>
<tr>
<th>物资编码</th>
<th>物资名称</th>
<th>规格型号</th>
<th>计划数量</th>
<th>单位</th>
<th>单价</th>
<th>金额</th>
<th>批次号</th>
<th>生产日期</th>
<th>失效日期</th>
<th>备注</th>
......@@ -885,14 +922,18 @@ function handlePrint(row) {
// 添加明细数据
if (orderData.items && orderData.items.length > 0) {
orderData.items.forEach(detail => {
// 处理拣货状态显示
const pickingStatusText = detail.pickingStatus === '0' ? '待拣货' : detail.pickingStatus === '1' ? '拣货中' : detail.pickingStatus === '2' ? '已完成' : '已取消';
printContent += `
<tr>
<td>${detail.materialName}</td>
<td>${detail.material?.materialCode || detail.materialCode || ''}</td>
<td>${detail.material?.materialName || detail.materialName || ''}</td>
<td>${detail.material?.specification || detail.specification || ''}</td>
<td>${detail.planQuantity}</td>
<td>${detail.unit}</td>
<td>${detail.unitPrice}</td>
<td>${detail.amount}</td>
<td>${detail.batchNo || ''}</td>
<td>${detail.productionDate ? new Date(detail.productionDate).toLocaleDateString() : ''}</td>
<td>${detail.expirationDate ? new Date(detail.expirationDate).toLocaleDateString() : ''}</td>
<td>${detail.remark || ''}</td>
......@@ -902,7 +943,7 @@ function handlePrint(row) {
} else {
printContent += `
<tr>
<td colspan="10">无明细数据</td>
<td colspan="12">无明细数据</td>
</tr>
`
}
......@@ -943,5 +984,10 @@ function handleInventoryFlow(row) {
batchDetailDialogRef.value.openDialog(row)
}
function handleDetail(row) {
currentOrder.value = row;
detailVisible.value = true
}
getList()
</script>
<template>
<div class="app-container">
<el-tabs v-model="activeTab" tab-position="top" @tab-click="handleTabClick">
<el-tab-pane v-for="item in panes" :key="item.key" :label="item.label" :name="item.key">
<component :is="item.component" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup name="WmsSpecial">
import { ref } from 'vue'
const activeTab = ref('first')
const panes = ref([
{
key: 'first',
label: '入库',
component: () => import('@/views/ware/wmsSpecial/InBoundOrder.vue')
},
{
key: 'second',
label: '出库',
component: () => import('@/views/ware/wmsSpecial/OutBoundOrder.vue')
}
])
</script>
\ No newline at end of file
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