Commit 2923b581 by linjinhong

Merge branch 'release' of http://47.122.114.111:9999/qinjianhui/factory_front into release

parents 01a0520f 5dac4fdb
......@@ -10,6 +10,9 @@ import {
AnyObject,
InterProductList,
ExportInWarehouseInfo,
stockingPlanSearchForm,
InterStackingPlanDetail,
InterWarehouseBase
} from '@/types/api/warehouse'
export interface LogListData {
createTime: string
......@@ -596,3 +599,121 @@ export function deleteWarehouseOutRecordApi(ids: string) {
},
)
}
// 备货计划 树状图
export function getStackingPlanStatusTree() {
return axios.get<never, BaseRespData<InterWarehouseTree[]>>(
'factoryStockingPlanRecord/status_tree',
)
}
// 备货计划 列表
export function getStackingPlanListPage(data: stockingPlanSearchForm, currentPage: number,
pageSize: number,) {
return axios.post<never, BasePaginationData<InterWarehousePage>>(
'factoryStockingPlanRecord/list_page',
{
...data,
currentPage,
pageSize,
},
)
}
// 备货计划 新增
export function addStackingPlanApi(data: InterWarehouseDetail) {
return axios.post<never, BaseRespData<never>>(
'factoryStockingPlanRecord/add',
data,
)
}
// 备货计划 编辑
export function updateStackingPlanApi(data: InterStackingPlanDetail) {
return axios.post<never, BaseRespData<never>>(
'factoryStockingPlanRecord/update',
data,
)
}
// 备货计划 详情
export function getStackingPlanDetailApi(id: number | undefined) {
return axios.get<never, BaseRespData<InterStackingPlanDetail>>(
'factoryStockingPlanRecord/get',
{
params: {
id,
},
},
)
}
// 备货计划 日志
export function getStackingPlanLogApi(stockingPlanRecordId?: number) {
return axios.get<never, BaseRespData<LogListData[]>>(
`factoryStockingPlanRecord/log/${stockingPlanRecordId}`,
)
}
// 备货计划 驳回
export function rejectStackingPlanApi({
list,
rejectReason,
status,
}: {
list: WarehouseParams[]
rejectReason: string
status: string
}) {
return axios.post<never, BaseRespData<never>>(
'factoryStockingPlanRecord/reject',
{
list,
rejectReason,
status,
},
)
}
// 备货计划 删除
export function deleteStackingPlanApi(ids: string) {
return axios.get<never, BaseRespData<never>>(
'factoryStockingPlanRecord/delete',
{
params: { ids },
},
)
}
// 备货计划 更新物流单号
export function updateShipmentNumberApi(data: InterWarehouseBase[]) {
return axios.post<never, BaseRespData<never>>(
'factoryStockingPlanRecord/update_shipmentNumber',
{ stockingPlanRecordParamList: data },
)
}
// 备货计划 导出
export function stackingPlanRecordExport(data: ExportInWarehouseInfo) {
return axios.post<never, BasePaginationData<never>>(
'factoryStockingPlanRecord/export',
data,
{
responseType: 'blob',
},
)
}
// 备货计划 盘点
export function scanBoxCodeApi(boxCode: string) {
return axios.get<never, BaseRespData<never>>(
'factoryStockingPlanRecord/scan_box_code',
{
params: { boxCode },
},
)
}
// 备货计划 打印箱贴
export function printBarcodeApi(data: { id: number, dataVersion: number }[]) {
return axios.post<never, BaseRespData<never[]>>(
'factoryStockingPlanRecord/print_barcode',
data,
)
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import {
RouteLocationNormalized,
NavigationGuardNext,
} from 'vue-router'
import { cleanupImagePreview } from '@/utils/hooks/useImagePreview.'
import Login from '@/views/Login.vue'
import Reset from '@/views/Reset.vue'
......@@ -31,6 +32,7 @@ import receiptDoc from '@/views/warehouse/receiptDoc.vue'
import issueDoc from '@/views/warehouse/issueDoc.vue'
import ExternalAuthorisationPage from '@/views/system/externalAuthorisationPage.vue'
import CustomersPage from '@/views/system/CustomersPage.vue'
import stockingPlan from '@/views/warehouse/stockingPlan.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
......@@ -129,7 +131,7 @@ const router = createRouter({
meta: {
title: '下载生产客户端',
},
component: () => {},
component: () => { },
beforeEnter() {
// 假设你的下载链接是这个
const downloadLink = '/exeFiles/JomallProductionAssistantSetup.exe'
......@@ -280,6 +282,13 @@ const router = createRouter({
component: issueDoc,
},
{
path: '/warehouse/stocking-plan',
meta: {
title: '备货计划',
},
component: stockingPlan,
},
{
path: '/warehouse/warning',
meta: {
title: '仓库预警',
......@@ -333,6 +342,8 @@ router.beforeEach(
_from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
// 路由切换时清理图片预览,防止预览元素残留
cleanupImagePreview()
const token = getToken()
if (to.query.factoryCode) {
localStorage.setItem('factory_code', String(to.query.factoryCode))
......
......@@ -88,7 +88,6 @@ const menu: MenuItem[] = [
id: 124,
label: '出库单',
},
{
index: '/warehouse/manage',
id: 121,
......@@ -99,6 +98,11 @@ const menu: MenuItem[] = [
id: 122,
label: '库位管理',
},
{
index: '/warehouse/stocking-plan',
id: 124,
label: '备货计划',
},
],
},
......@@ -216,7 +220,7 @@ const menu: MenuItem[] = [
label: '工厂设置',
},
],
},
},
// {
// index: '',
// id: 3,
......
......@@ -22,6 +22,7 @@ export interface InterProductList {
productNo?: string | null //custom的货号
buyStored?: number | null //入库数量
outCount?: number | null //出库数量
stockUpStored?: number | null //备货数量
remark?: string | null
skuImage?: string
skuName?: string
......@@ -54,9 +55,10 @@ export interface InterWarehouseBase {
rejectReason?: string | null
number?: number | string | null
supplierItemNo?: string | null
sourceOn?: string | null //备货单号or入库单号
}
// 主表列表ts
export interface InterWarehousePage extends InterWarehouseBase {}
export interface InterWarehousePage extends InterWarehouseBase { }
// 子表列表ts
export interface InterWarehouseDetail extends InterWarehouseBase {
productList: InterProductList[]
......@@ -81,6 +83,7 @@ export interface InterProductList {
productNo?: string | null //货号
buyStored?: number | null //入库数量
outCount?: number | null //出库数量
stockUpStored?: number | null //备货数量
usableInventory?: number | null //可用库存数量
remark?: string | null
skuImage?: string
......@@ -118,3 +121,21 @@ export interface ILocation {
locationId?: number | null
locationCode?: string | null
}
// 备货计划 查询
export interface stockingPlanSearchForm extends InterWarehouseBase {
timeType?: string
startDate?: string
endDate?: string
dateStr?: string
warehouseSku?: string
}
// 备货计划
export interface InterStackingPlanDetail extends InterWarehouseDetail {
boxSum?: number
checkBoxSum?: number
box?: number | string
}
import { ref } from 'vue'
import { ref, onUnmounted } from 'vue'
export default function useImagePreview() {
const show = ref(false)
const div = document.createElement('div')
div.style.position = 'fixed'
div.style.zIndex = '9999999999999'
div.style.display = 'none'
const img = document.createElement('img')
img.style.width = '300px'
div.appendChild(img)
document.body.appendChild(div)
// 单例模式:所有实例共享同一个预览元素
let previewDiv: HTMLDivElement | null = null
let previewImg: HTMLImageElement | null = null
const show = ref(false)
let currentTarget: HTMLElement | null = null
let mousemoveHandler: ((ev: MouseEvent) => void) | null = null
// 初始化预览元素(只创建一次)
function initPreviewElement() {
if (previewDiv) return
let currentTarget: HTMLElement | null = null
previewDiv = document.createElement('div')
previewDiv.style.position = 'fixed'
previewDiv.style.zIndex = '9999999999999'
previewDiv.style.display = 'none'
previewImg = document.createElement('img')
previewImg.style.width = '300px'
previewDiv.appendChild(previewImg)
document.body.appendChild(previewDiv)
}
const mousemoveHandler = (ev: MouseEvent) => {
if (!currentTarget) return
const rect = currentTarget.getBoundingClientRect()
const inOrigin =
ev.clientX >= rect.left &&
ev.clientX <= rect.right &&
ev.clientY >= rect.top &&
ev.clientY <= rect.bottom
// 清理函数:移除事件监听器和重置状态
function cleanup() {
if (mousemoveHandler) {
window.removeEventListener('mousemove', mousemoveHandler)
mousemoveHandler = null
}
if (previewDiv) {
previewDiv.style.display = 'none'
}
show.value = false
currentTarget = null
}
const divRect = div.getBoundingClientRect()
const inPreview =
ev.clientX >= divRect.left &&
ev.clientX <= divRect.right &&
ev.clientY >= divRect.top &&
ev.clientY <= divRect.bottom
// 导出清理函数,供路由切换时使用
export function cleanupImagePreview() {
cleanup()
}
if (!inOrigin && !inPreview) {
div.style.display = 'none'
show.value = false
window.removeEventListener('mousemove', mousemoveHandler)
currentTarget = null
export default function useImagePreview() {
// 确保预览元素已初始化
initPreviewElement()
// 创建 mousemove 处理器(如果还没有)
if (!mousemoveHandler) {
mousemoveHandler = (ev: MouseEvent) => {
if (!currentTarget || !previewDiv) {
cleanup()
return
}
const rect = currentTarget.getBoundingClientRect()
const inOrigin =
ev.clientX >= rect.left &&
ev.clientX <= rect.right &&
ev.clientY >= rect.top &&
ev.clientY <= rect.bottom
const divRect = previewDiv.getBoundingClientRect()
const inPreview =
ev.clientX >= divRect.left &&
ev.clientX <= divRect.right &&
ev.clientY >= divRect.top &&
ev.clientY <= divRect.bottom
if (!inOrigin && !inPreview) {
cleanup()
}
}
}
......@@ -44,12 +77,13 @@ export default function useImagePreview() {
newBorder?: boolean,
positionBOOTTOM?: boolean,
) => {
if (!previewDiv || !previewImg) return
ev.preventDefault()
if (show.value === true) return
img.src = url
img.style.backgroundColor = '#eee'
if (newBorder) img.style.border = '1px solid #eee'
if (newWitdh) img.style.width = newWitdh
previewImg.src = url
previewImg.style.backgroundColor = '#eee'
if (newBorder) previewImg.style.border = '1px solid #eee'
if (newWitdh) previewImg.style.width = newWitdh
const cW = document.body.clientWidth
const cH = document.body.clientHeight
const cX = ev.clientX
......@@ -59,14 +93,15 @@ export default function useImagePreview() {
else y = cY - 150
if (cX + 300 >= cW) x = cX - 300
else x = cX + 60
img.onload = () => {
div.style.left = x + 'px'
div.style.top = y + 'px'
div.style.display = 'block'
previewImg.onload = () => {
if (!previewDiv || !previewImg) return
previewDiv.style.left = x + 'px'
previewDiv.style.top = y + 'px'
previewDiv.style.display = 'block'
if (positionBOOTTOM) {
// 获取图片实际尺寸
const imgWidth = img.clientWidth
const imgHeight = img.clientHeight
const imgWidth = previewImg.clientWidth
const imgHeight = previewImg.clientHeight
// 计算新位置(鼠标正下方)
let x = cX / 2
......@@ -85,20 +120,26 @@ export default function useImagePreview() {
if (y < 0) {
y = 10
}
div.style.left = x + 'px'
div.style.top = y + 'px'
previewDiv.style.left = x + 'px'
previewDiv.style.top = y + 'px'
}
show.value = true
currentTarget = ev.currentTarget as HTMLElement
window.addEventListener('mousemove', mousemoveHandler)
if (mousemoveHandler) {
window.addEventListener('mousemove', mousemoveHandler)
}
}
}
const mouseleaveImg = () => {
if (show.value === false) return
div.style.display = 'none'
show.value = false
// 在 mouseleave 时也要清理事件监听器
cleanup()
}
// 组件卸载时清理(防止内存泄漏)
onUnmounted(() => {
cleanup()
})
return { mouseoverImg, mouseleaveImg }
}
......@@ -184,6 +184,14 @@
align="center"
></ElTableColumn>
<ElTableColumn
label="备货单号"
show-overflow-tooltip
prop="sourceOn"
width="130"
header-align="center"
align="center"
></ElTableColumn>
<ElTableColumn
label="单据状态"
width="130"
prop="billStatusTxt"
......
<template>
<div class="page card h-100 flex-gap-10 overflow-hidden flex">
<div class="left">
<ElTree
ref="treeRef"
default-expand-all
:expand-on-click-node="false"
:default-expanded-keys="[]"
:highlight-current="true"
node-key="code"
:data="treeData"
:props="{ children: 'children', label: 'name' }"
@node-click="nodeClick"
>
<template #default="{ data }">
<div class="tree-node">
<div class="tree-node-label">{{ data.name }}</div>
<div
v-if="data.countQuantity || data.countQuantity === 0"
class="tree-node-count"
>
{{ `(${data.countQuantity})` }}
</div>
</div>
</template>
</ElTree>
</div>
<div class="right">
<div class="delivery-note-page flex-column card h-100 overflow-hidden">
<splitDiv size="50">
<template #top>
<div class="header-filter-form">
<ElForm ref="searchFormRef" :model="searchForm" inline>
<ElFormItem>
<el-select
v-model="searchForm.dateStr"
clearable
:teleported="false"
placeholder="时间类型"
style="width: 100px; margin-right: 5px"
>
<el-option value="createTime" label="创建时间" />
<el-option value="auditTime" label="审核时间" />
<el-option value="deliveryTime" label="发货时间" />
<el-option value="warehouseTime" label="入库时间" />
</el-select>
<el-date-picker
v-model="tradingTime"
:shortcuts="pickerOptions.shortcuts"
:default-time="[
new Date(0, 0, 0, 0, 0, 0),
new Date(0, 0, 0, 23, 59, 59),
]"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结束时间"
clearable
style="width: 260px"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</ElFormItem>
<ElFormItem label="备货单号">
<ElInput
v-model.trim="searchForm.inNo"
clearable
placeholder="请输入备货单号"
style="width: 160px"
/>
</ElFormItem>
<ElFormItem label="备货仓库">
<ElSelect
v-model="searchForm.warehouseId"
clearable
placeholder="请选择仓库"
style="width: 160px"
>
<ElOption
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
></ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem style="margin-right: 10px" label="库存SKU">
<ElInput
v-model="searchForm.warehouseSku"
clearable
placeholder="请输入库存SKU"
style="width: 160px"
/>
</ElFormItem>
<ElFormItem style="margin-right: 10px" label="物流单号">
<ElInput
v-model.trim="searchForm.shipmentNumber"
clearable
placeholder="请输入物流单号"
style="width: 160px"
/>
</ElFormItem>
<ElFormItem>
<ElButton ref="searchBtnRef" type="primary" @click="search">
查询
</ElButton>
</ElFormItem>
<ElFormItem>
<ElButton @click="resetSearchForm(), (tradingTime = [])">
重置
</ElButton>
</ElFormItem>
<ElFormItem>
<el-button type="success" @click="handleExport">
导出
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'PENDING_SUBMIT'">
<el-button type="primary" @click="addDialog(1, null)">
新增
</el-button>
</ElFormItem>
<ElFormItem
v-if="
nodeCode === 'PENDING_AUDIT' || nodeCode === 'WAIT_SHIPMENT'
"
>
<el-button type="danger" @click="rejectedInRecord('record')">
驳回
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'PENDING_SUBMIT'">
<el-button type="danger" @click="handleBatchDelete">
删除
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'PENDING_SUBMIT'">
<el-button type="success" @click="auditOrder('submitAudit')">
提交审核
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'WAIT_SHIPMENT'">
<el-button type="success" @click="printProductTag">
打印箱贴
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'IN_TRANSIT'">
<el-button
type="primary"
@click="openUpdateShipmentNumberDialog"
>
更新物流单号
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'IN_TRANSIT'">
<el-button
type="warning"
@click="auditOrder('toWaitLoadedCabinets')"
>
转至待卸柜
</el-button>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'WAIT_UNLOADED_CABINETS'">
<ElButton type="warning" @click="openScanDialog">
盘点
</ElButton>
</ElFormItem>
<ElFormItem v-if="nodeCode === 'WAIT_INBOUND'">
<el-button
type="success"
@click="auditOrder('toWaitInbound')"
>
转至待上架
</el-button>
</ElFormItem>
</ElForm>
</div>
<div
class="delivery-note-content flex-1 flex-column overflow-hidden"
>
<div class="delivery-note-list flex-1 overflow-hidden">
<ElTable
ref="singleTableRef"
highlight-current-row
:data="tableData"
default-expand-all
size="small"
style="width: 100%; height: 100%"
border
@current-change="rowClick"
@selection-change="handleSelectionChange"
>
<ElTableColumn
type="selection"
width="70"
header-align="center"
align="center"
/>
<ElTableColumn
type="index"
label="序号"
width="60"
fixed="left"
header-align="center"
align="center"
/>
<ElTableColumn
label="备货单号"
show-overflow-tooltip
prop="inNo"
width="130"
header-align="center"
align="center"
/>
<ElTableColumn
label="入库单号"
show-overflow-tooltip
prop="sourceOn"
width="130"
header-align="center"
align="center"
/>
<ElTableColumn
label="单据状态"
width="100"
prop="billStatusTxt"
header-align="center"
align="center"
/>
<ElTableColumn
label="备货仓库"
show-overflow-tooltip
prop="warehouseName"
width="130"
header-align="center"
align="center"
/>
<ElTableColumn
label="工厂编号"
show-overflow-tooltip
prop="factoryCode"
width="90"
header-align="center"
align="center"
/>
<ElTableColumn
label="物流单号"
show-overflow-tooltip
prop="shipmentNumber"
width="200"
header-align="center"
align="center"
/>
<ElTableColumn
label="币种"
show-overflow-tooltip
width="80"
prop="currencyName"
header-align="center"
align="center"
/>
<ElTableColumn
label="总金额"
show-overflow-tooltip
width="100"
prop="totalPrice"
header-align="center"
align="center"
/>
<ElTableColumn
label="箱数"
header-align="center"
prop="boxSum"
width="60"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="盘点箱数"
header-align="center"
prop="checkBoxSum"
width="80"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="SKU数量"
header-align="center"
prop="skuAmount"
width="90"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="备货总数量"
header-align="center"
prop="total"
width="90"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="创建时间"
header-align="center"
prop="makeTime"
width="130"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="审核时间"
header-align="center"
prop="auditTime"
width="130"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="发货时间"
header-align="center"
prop="deliveryTime"
width="130"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="入库时间"
header-align="center"
prop="warehouseTime"
width="130"
align="center"
show-overflow-tooltip
/>
<ElTableColumn
label="驳回原因"
show-overflow-tooltip
prop="rejectCause"
header-align="center"
width="130"
align="center"
/>
<ElTableColumn
label="备注"
show-overflow-tooltip
prop="remark"
header-align="center"
align="center"
/>
<ElTableColumn
width="100"
align="center"
header-align="center"
label="操作"
fixed="right"
>
<template #default="{ row }">
<ElButton
v-if="nodeCode === 'PENDING_SUBMIT'"
type="primary"
link
@click="addDialog(2, row)"
>
编辑
</ElButton>
<ElButton
v-if="nodeCode === 'PENDING_AUDIT'"
type="warning"
link
@click="addDialog(3, row)"
>
审核
</ElButton>
<ElButton
v-if="nodeCode === 'WAIT_SHIPMENT'"
type="success"
link
@click="addDialog(4, row)"
>
完成发货
</ElButton>
</template>
</ElTableColumn>
</ElTable>
</div>
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[100, 200, 300, 400, 500]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
style="margin: 10px auto 0; text-align: right"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></ElPagination>
</div>
</template>
<template #bottom>
<el-tabs v-model="tabsValue" @tab-click="tabsClick">
<el-tab-pane name="0" label="入库商品">
<div class="table-wrap">
<ElTable size="small" :data="detailList" height="100%" border>
<ElTableColumn
show-overflow-tooltip
width="60"
align="center"
label="序号"
type="index"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
width="100"
label="SKU图片"
prop="factory_order_number"
>
<template #default="{ row }">
<ImageView
:src="row.skuImage"
width="40px"
height="40px"
/>
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
label="库存SKU"
width="180"
prop="warehouseSku"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="商品名称"
prop="skuName"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="款号"
prop="productNo"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
width="90"
label="备货数量"
prop="stockUpStored"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="库位"
prop="locationCode"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="币种"
width="80"
prop="currencyName"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="成本价"
prop="costPrice"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="总成本"
prop="totalPrice"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="创建时间"
width="130"
prop="createTime"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
width="240"
label="备注"
prop="remark"
/>
</ElTable>
</div>
</el-tab-pane>
<el-tab-pane name="1" label="操作日志">
<ul
style="
color: #333;
font-size: 12px;
height: 100%;
overflow: auto;
"
>
<li
v-for="(item, index) in logList"
:key="index"
style="display: flex"
>
<span style="display: inline-block">
{{ item.createdTime }}&emsp;&emsp;
</span>
<span style="display: inline-block">{{
item.description
}}</span>
</li>
</ul>
</el-tab-pane>
</el-tabs>
</template>
</splitDiv>
</div>
</div>
</div>
<ElDialog
v-model="importDialogVisible"
title="导入备货单"
width="500px"
:close-on-click-modal="false"
>
<div class="import-dialog">
<div class="import-content">
<UploadExcel
v-model="importedFileUrl"
:import-type="'localAndXlsx'"
:import-name="'备货单'"
:import-url="'/files/stockingPlan.xlsx'"
@imported="handleLocalImport"
/>
</div>
</div>
</ElDialog>
<ElDialog
v-model="newDialogVisible"
:title="actionMap[currentAction - 1].label"
width="80%"
:close-on-click-modal="false"
>
<div class="dialog-form">
<ElForm
ref="editFormRef"
:model="editForm"
:rules="rules"
inline
label-width="90px"
>
<ElFormItem label="备货单号" prop="account">
<ElInput v-model.trim="editForm.inNo" clearable disabled />
</ElFormItem>
<ElFormItem label="工厂编号:" prop="factoryCode">
<span>{{ editForm.factoryCode }}</span>
</ElFormItem>
<ElFormItem label="备货仓库" prop="warehouseId" required>
<ElSelect
v-model="editForm.warehouseId"
clearable
:disabled="formId"
placeholder="请选择备货仓库"
style="width: 160px"
@change="handleWarehouseChange(editForm.warehouseId)"
>
<ElOption
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
></ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="箱数" prop="boxSum">
<ElInputNumber
v-model.trim="editForm.boxSum"
:precision="0"
:min="1"
placeholder="请输入箱数"
:disabled="currentAction === 3 || currentAction === 4"
style="width: 160px"
clearable
/>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput
v-model.trim="editForm.remark"
:disabled="currentAction === 3 || currentAction === 4"
placeholder="请输入备注"
style="width: 160px"
clearable
/>
</ElFormItem>
</ElForm>
<ElTable
size="small"
:data="otherPurchaseData"
height="500px"
border
@selection-change="productSelectionChange"
>
<ElTableColumn
type="selection"
width="70"
header-align="center"
align="center"
/>
<ElTableColumn
show-overflow-tooltip
width="60"
align="center"
label="序号"
type="index"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
width="100"
label="SKU图片"
prop="skuImage"
>
<template #default="{ row }">
<ImageView :src="row.skuImage" width="40px" height="40px" />
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
label="库存SKU"
prop="warehouseSku"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="商品名称"
prop="skuName"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="款号"
prop="productNo"
/>
<ElTableColumn align="center" label="备货数量" prop="stockUpStored">
<template #default="{ row }">
<el-input
v-model.number="row.stockUpStored"
:disabled="currentAction === 3 || currentAction === 4"
placeholder="备货数量"
style="width: 120px"
clearable
size="small"
@input="setCostPrice(row)"
></el-input>
</template>
</ElTableColumn>
<ElTableColumn
align="center"
width="80"
label="币种"
prop="currencyName"
/>
<ElTableColumn
width="100"
align="center"
label="成本价"
prop="costPrice"
/>
<ElTableColumn
align="center"
width="100"
label="总成本"
prop="totalPrice"
/>
<ElTableColumn align="center" label="库位" prop="locationCode">
<template #default="{ row }">
<!-- <span v-if="formId&&row.locationCode">{{ row.locationCode }}</span> -->
<ElSelect
v-model="row.locationId"
clearable
placeholder="请输入库位"
:disabled="currentAction === 3 || currentAction === 4"
style="width: 120px"
filterable
@change="handleLocationChange(row.locationId, row)"
>
<ElOption
v-for="item in locationList"
:key="item.locationId"
:label="item.locationCode"
:value="item.locationId"
></ElOption>
</ElSelect>
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
width="240"
label="备注"
prop="remark"
>
<template #default="{ row }">
<ElInput
v-model.trim="row.remark"
clearable
size="small"
:disabled="currentAction === 3 || currentAction === 4"
/>
</template>
</ElTableColumn>
</ElTable>
</div>
<template #footer>
<div class="product-dialog-footer">
<div v-if="currentAction !== 3 && currentAction !== 4">
<el-input
v-model.trim="selectSku"
placeholder="库存SKU"
style="width: 200px; margin: 0 10px"
clearable
size="small"
/>
<el-popover placement="top-start" width="1000" trigger="click">
<div v-if="skuData.length > 0" style="height: 50vh">
<ElTable size="small" :data="filterSkuData" height="100%" border>
<ElTableColumn
show-overflow-tooltip
width="60"
align="center"
label="序号"
type="index"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="SKU图片"
width="100"
prop="image"
>
<template #default="{ row }">
<ImageView :src="row.image" width="40px" height="40px" />
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
label="库存SKU"
width="200"
prop="skuName"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
width="200"
label="库存SKU"
prop="sku"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="款号"
prop="productNo"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="币种"
width="80"
prop="currencyName"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="成本价"
width="80"
prop="factoryPrice"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="库位"
prop="locationCode"
/>
<ElTableColumn
width="80"
align="center"
header-align="center"
label="操作"
>
<template #default="{ row }">
<el-icon :size="32" color="#67C23A" class="cursor-pointer">
<CirclePlusFilled @click="skudblclick(row)" />
</el-icon>
</template>
</ElTableColumn>
</ElTable>
</div>
<template #reference>
<el-button
type="primary"
size="small"
style="width: 90px"
@click="selectbySku()"
>
查询
</el-button>
</template>
</el-popover>
<el-button
style="margin-left: 6px"
type="success"
size="small"
@click="addPurchase"
>
批量新增
</el-button>
<el-button
type="danger"
style="margin-left: 10px"
size="small"
@click="deleteOtherWarehousing()"
>
删除
</el-button>
<el-button
type="primary"
style="margin-left: 10px"
size="small"
@click="importData"
>
导入
</el-button>
</div>
<div
:style="{
margin: currentAction === 3 || currentAction === 4 ? '0 auto' : '',
}"
>
<el-button size="small" @click="newDialogVisible = false">
取消
</el-button>
<el-button
v-if="currentAction === 3 || currentAction === 4"
type="danger"
size="small"
@click="rejectedInRecord('audit')"
>
驳回
</el-button>
<el-button type="primary" size="small" @click="addOtherCurrency">
确认
</el-button>
</div>
</div>
</template>
</ElDialog>
<ElDialog
v-model="exportVisible"
title="导出选项"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="exportForm" label-width="80px">
<el-form-item label="" prop="resource">
<el-radio-group v-model="exportForm.resource" @change="isAllExport">
<el-radio :label="0">导出本页</el-radio>
<el-radio :label="1">导出选中</el-radio>
<el-radio :label="2">全部</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="">
<el-checkbox v-model="exportForm.delivery"> 包含详情</el-checkbox>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportVisible = false">取消</el-button>
<el-button type="primary" @click="submitExportForm">确认</el-button>
</span>
</template>
</ElDialog>
<el-dialog v-model="showPrintDialog" title="打印参数设置">
<el-table height="400px" :data="printData" border>
<el-table-column width="60" align="center" type="index" label="序号" />
<el-table-column align="center" prop="skuName" label="商品名称" />
<el-table-column align="center" prop="warehouseSku" label="库存SKU" />
<el-table-column align="center" prop="locationName" label="库位编码" />
<el-table-column align="center" prop="supplierItemNo" label="款号" />
<el-table-column align="center" prop="number" label="打印数量">
<template #default="{ row }">
<el-input
v-model.number="row.number"
oninput="value=value.replace(/[^\-?\d.]/g,'')"
placeholder="打印数量"
clearable
></el-input>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="showPrintDialog = false">取消</el-button>
<el-button type="primary" @click="handlePrintProductTag">打印</el-button>
</template>
</el-dialog>
<ElDialog
v-model="addPurchaseVisible"
title="批量添加"
width="500px"
:close-on-click-modal="false"
>
<div>
<el-input
v-model.trim="purchaseTextarea"
type="textarea"
placeholder="请输入库存 SKU"
:rows="5"
minlength="1"
maxlength="1000"
show-word-limit
/>
<div style="margin-top: 12px; color: #777">
{{ '多个字段使用_##_,_##_隔开' }}
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="addPurchaseVisible = false">取消</el-button>
<el-button type="primary" @click="submitPurchase">确认</el-button>
</span>
</template>
</ElDialog>
<ElDialog
v-model="updateShipmentNumberVisible"
title="更新物流单号"
width="500px"
>
<ElTable :data="updateShipmentNumberForm" border>
<ElTableColumn prop="inNo" label="备货单号" />
<ElTableColumn prop="shipmentNumber" label="物流单号">
<template #default="{ row }">
<el-input
v-model="row.shipmentNumber"
clearable
placeholder="请输入物流单号"
/>
</template>
</ElTableColumn>
</ElTable>
<template #footer>
<el-button @click="updateShipmentNumberVisible = false">取消</el-button>
<el-button type="primary" @click="updateShipmentNumber"> 确认 </el-button>
</template>
</ElDialog>
<ElDialog
v-model="scanDialogVisible"
width="800px"
title="盘点"
@close="getTreeNum()"
>
<el-input
ref="scanInputRef"
v-model="scanInput"
placeholder="请输入备货单号"
style="width: 90%"
clearable
@keyup.enter="scan"
/>
<el-button type="primary" style="margin-left: 10px" @click="scan">
查询
</el-button>
<el-descriptions style="margin-top: 50px">
<el-descriptions-item label="备货仓库">
{{ scanData.warehouseName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="备货单号">
{{ scanData.inNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="物流单号">
{{ scanData.shipmentNumber || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前箱子">
{{ scanData.box || '-' }}
</el-descriptions-item>
<el-descriptions-item label="箱数">
{{ scanData.boxSum || '-' }}
</el-descriptions-item>
<el-descriptions-item label="盘点箱数">
{{ scanData.checkBoxSum || '-' }}
</el-descriptions-item>
</el-descriptions>
</ElDialog>
</template>
<script setup lang="ts">
import { ElMessage, ElRadioGroup, ElTree } from 'element-plus'
import { CirclePlusFilled } from '@element-plus/icons-vue'
import splitDiv from '@/components/splitDiv/splitDiv.vue'
import { ElTable } from 'element-plus'
import usePageList from '@/utils/hooks/usePageList'
import { checkUpdateParams, AnyObject } from '@/utils/hooks/commonUtil'
import { useValue } from '@/utils/hooks/useValue'
import {
getStackingPlanStatusTree,
getStackingPlanListPage,
getStackingPlanDetailApi,
getBySkuApi,
warehouseInfoGetAll,
getByWareHouseIdAndCodeApi,
addStackingPlanApi,
updateStackingPlanApi,
deleteStackingPlanApi,
auditOrderApi,
getStackingPlanLogApi,
rejectStackingPlanApi,
LogListData,
warehouseInfo,
printBarcodeApi,
factoryWarehouseInventoryPrint,
stackingPlanRecordExport,
updateShipmentNumberApi,
scanBoxCodeApi,
} from '@/api/warehouse'
import { filePath } from '@/api/axios.ts'
import BigNumber from 'bignumber.js'
import { ref, onMounted, watch, nextTick } from 'vue'
import 'element-plus/dist/index.css'
import {
stockingPlanSearchForm,
InterWarehousePage,
InterWarehouseTree,
InterProductList,
InterskuList,
ILocation,
InterStackingPlanDetail,
InterWarehouseBase,
} from '@/types/api/warehouse'
import ImageView from '@/components/ImageView.vue'
import UploadExcel from '@/components/UploadExcel.vue'
// import { debounce } from 'lodash-es'
import { useEnterKeyTrigger } from '@/utils/hooks/useEnterKeyTrigger.ts'
const warehouseList = ref<warehouseInfo[]>([])
const pickerOptions = {
shortcuts: [
{
text: '今日',
value: () => {
const start = new Date(new Date(getStartTime()).getTime())
const end = new Date()
return [start, end]
},
},
{
text: '昨天',
value: () => {
const start = new Date()
const end = new Date(new Date(getStartTime()).getTime() - 1)
start.setTime(end.getTime() - 3600 * 1000 * 24 * 1 + 1)
return [start, end]
},
},
{
text: '最近7天',
value: () => {
const end = new Date()
const start = new Date(getStartTime())
start.setTime(start.getTime() - 3600 * 1000 * 24 * 6)
return [start, end]
},
},
{
text: '最近14天',
value: () => {
const end = new Date()
const start = new Date(getStartTime())
start.setTime(start.getTime() - 3600 * 1000 * 24 * 13)
return [start, end]
},
},
{
text: '最近30天',
value: () => {
const end = new Date()
const start = new Date(getStartTime())
start.setTime(start.getTime() - 3600 * 1000 * 24 * 29)
return [start, end]
},
},
{
text: '本星期',
value: () => {
const end = new Date()
const start = new Date()
const nowDay = new Date().getDay() - 1
start.setTime(
new Date(getStartTime()).getTime() - 3600 * 1000 * 24 * nowDay,
)
return [start, end]
},
},
{
text: '上星期',
value: () => {
const end = new Date()
const start = new Date()
const nowDay = new Date().getDay() - 1
end.setTime(
new Date(getStartTime()).getTime() - 3600 * 1000 * 24 * nowDay - 1,
)
start.setTime(end.getTime() - 3600 * 1000 * 24 * 7 + 1)
return [start, end]
},
},
{
text: '这个月',
value: () => {
const end = new Date()
const start = new Date()
const nowDate = new Date().getDate() - 1
start.setTime(
new Date(getStartTime()).getTime() - 3600 * 1000 * 24 * nowDate,
)
return [start, end]
},
},
{
text: '上个月',
value: () => {
const date = new Date()
let year = date.getFullYear()
let month = date.getMonth()
const end = new Date(
new Date(`${year}-${month + 1}-1 00:00:00`).getTime() - 1,
)
if (month === 0) {
month = 12
year = year - 1
}
const start = new Date(
new Date(`${year}-${month}-1 00:00:00`).getTime(),
)
return [start, end]
},
},
{
text: '历史',
value: () => {
return ['', '']
},
},
],
}
function getStartTime() {
const date = new Date()
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `${year}-${month}-${day} 00:00:00`
}
const selectSku = ref('')
const treeData = ref<InterWarehouseTree[]>()
const [searchForm, resetSearchForm] = useValue<stockingPlanSearchForm>({
dateStr: 'createTime',
})
const tradingTime = ref<string[]>([])
const selections = ref<InterWarehousePage[]>([])
const detailList = ref<InterProductList[]>([])
const tabsValue = ref<string>('0')
const singleTableRef = ref<InstanceType<typeof ElTable>>()
const currentRow = ref<InterWarehousePage | null>(null)
const logList = ref<LogListData[]>([])
const rules = {
warehouseId: [{ required: true, message: '请选择仓库', trigger: 'change' }],
boxSum: [{ required: true, message: '请输入箱数', trigger: 'change' }],
}
const isAllExport = () => {
if (exportForm.value.resource == '2') {
ElMessage.warning('最多支持5000条!')
}
}
const nodeCode = ref<string>(
sessionStorage.getItem('InRecord_NodeCode') || 'all',
)
const treeRef = ref<InstanceType<typeof ElTree>>()
const {
currentPage,
pageSize,
total,
data: tableData,
refresh: search,
onCurrentPageChange: handleCurrentChange,
onPageSizeChange: handleSizeChange,
} = usePageList({
query: (page, pageSize) =>
getStackingPlanListPage(
{
...searchForm.value,
billStatus: nodeCode.value == 'all' ? '' : nodeCode.value,
startDate: tradingTime.value && tradingTime.value[0],
endDate: tradingTime.value && tradingTime.value[1],
},
page,
pageSize,
).then((res) => res.data),
})
const setCostPrice = (item: InterProductList) => {
if (!item.costPrice) {
ElMessage.warning('商品成本价为空,请完善商品成本价')
return
}
if (item) {
const stockUpStored = item.stockUpStored ?? 0
const costPrice = item.costPrice ?? 0
const amount = new BigNumber(stockUpStored)
.multipliedBy(costPrice)
.toFixed(2)
item.totalPrice = Number(amount)
}
}
const getTreeNum = async () => {
try {
const res = await getStackingPlanStatusTree()
res.data = [{ code: 'all', name: '全部', children: res.data }]
treeData.value = res.data
await nextTick(() => {
treeRef.value!.setCurrentKey(nodeCode.value, true)
})
} catch (e) {
console.error(e)
}
}
const showPrintDialog = ref(false)
const printData = ref([])
// const printProductTag = async () => {
// if (!selections.value.length) {
// return ElMessage.warning('请选择要操作的数据')
// }
// const str = selections.value.map((el: InterWarehousePage) => el.id).join(',')
// try {
// const res = await printBarcodeApi(str)
// if (res.code === 200) {
// printData.value = res.data
// showPrintDialog.value = true
// } else {
// ElMessage.error(res.message)
// }
// } catch (e) {
// console.error(e)
// }
// }
const printProductTag = async () => {
if (!selections.value.length) {
return ElMessage.warning('请选择要操作的数据')
}
const str: { id: number; dataVersion: number }[] = selections.value.map(
(el: InterWarehousePage) => ({
id: el.id ?? 0,
dataVersion: el.dataVersion ?? 0,
}),
)
const res = await printBarcodeApi(str)
showPrintDialog.value = false
window.open(filePath + res.message, '_blank')
}
async function handlePrintProductTag() {
const flag = printData.value.every(
(el: InterWarehousePage) => el.number && el.number != '0',
)
if (!flag) {
return ElMessage.warning('打印数量需大于0')
}
const list = printData.value.map(
({
skuName = '',
warehouseSku = '',
supplierItemNo = '',
number = '',
locationName = '',
}) => ({
skuName,
warehouseSku,
supplierItemNo,
number,
locationName,
}),
)
const res = await factoryWarehouseInventoryPrint({
list,
code: 'PT002',
})
showPrintDialog.value = false
window.open(filePath + res.message, '_blank')
}
const batchAddCommodity = async (sku: string): Promise<InterskuList[]> => {
if (!editForm.value.warehouseId) {
ElMessage.error('请选择仓库')
return []
}
try {
const res = await getBySkuApi(editForm.value.warehouseId, sku)
const arr: InterskuList[] = res.data || []
const ids: Record<string, boolean> = {}
// 过滤掉商品列表已经加了的
for (const item of otherPurchaseData.value) {
if (item.warehouseSku !== undefined) {
ids[item.warehouseSku] = true
}
}
// 使用 filter 方法过滤掉已经存在的 SKU
const filteredArr = arr.filter((currentItem: InterskuList) => {
return currentItem.sku === undefined || !ids[currentItem.sku]
})
return filteredArr
} catch (e) {
console.error(e)
return []
}
}
interface InterImportData {
warehouseSku: string
remark?: string | null
[key: string]: unknown
}
// 前端导入Excel
const excelFieldMap: Record<string, keyof InterProductList> = {
// SKU图片: 'skuImage',
库存SKU: 'warehouseSku',
// SKU名称: 'skuName',
备货数量: 'stockUpStored',
// '成本价(¥)': 'costPrice',
// '总成本(¥)': 'totalPrice',
库位编码: 'locationCode',
备注: 'remark',
}
const handleLocalImport = async ({
path,
data,
}: {
path: string
data: InterImportData[]
}) => {
// 1. 将原始导入数据映射到 InterImportData[]
const importedData: InterImportData[] = data
.map((item) => {
const obj: InterImportData = { warehouseSku: '' }
Object.keys(excelFieldMap).forEach((excelKey) => {
const field = excelFieldMap[excelKey]
const value = item[excelKey]
if (field === 'warehouseSku') {
obj[field] = typeof value === 'string' ? value : String(value ?? '')
} else if (field === 'remark') {
obj[field] =
typeof value === 'string'
? value
: value === null || value === undefined
? null
: String(value)
} else if (field === 'buyStored') {
// 确保 value 是一个有效的数字或数字字符串,否则设置为 null
if (
typeof value === 'number' ||
(typeof value === 'string' && !isNaN(Number(value)))
) {
obj[field] = String(value) // 转换为字符串
} else {
obj[field] = null // 无效值设置为 null
}
} else {
obj[field] = value
}
})
return obj
})
.filter((item) => item.warehouseSku)
// console.log('@', importedData, data)
if (importedData.length === 0) {
ElMessage.warning('导入数据中没有有效的商品SKU')
importDialogVisible.value = false
return
}
// 2. 提取导入的 SKU 列表
const importedSkus = importedData.map((item) => item.warehouseSku).join(',')
// 3. 调用 batchAddCommodity 获取商品的完整信息并过滤掉已有的 SKU
const filteredSkusList = await batchAddCommodity(importedSkus) // 使用 await 等待结果
if (filteredSkusList.length === 0) {
ElMessage.warning('导入的商品SKU已存在或无效')
importedFileUrl.value = path
importDialogVisible.value = false
return
}
// 4. 将备注信息合并到获取到的商品列表中
const mergedProductList = filteredSkusList.map((skuItem) => {
// 在导入数据中找到匹配的备注信息
const importedItem = importedData.find(
(item) => item.warehouseSku === skuItem.sku,
)
const target = locationList.value.find((item: InterProductList) => {
return item.locationCode == importedItem?.locationCode
})
return {
skuImage: skuItem.image,
warehouseSku: skuItem.sku,
skuName: skuItem.skuName,
productNo: skuItem.productNo,
locationCode: importedItem?.locationCode ?? null,
locationId: target?.locationId,
costPrice: skuItem.factoryPrice,
buyStored: importedItem?.buyStored ?? null,
totalPrice: new BigNumber(
(importedItem?.stockUpStored ?? 0) as number | string,
)
.multipliedBy(skuItem.factoryPrice ?? 0)
.toNumber(),
usableInventory: skuItem.usableInventory,
stockUpStored: importedItem?.stockUpStored ?? null,
remark: importedItem?.remark ?? null,
currencyName: skuItem.currencyName,
} as InterProductList // 明确类型
})
// 5. 更新 otherPurchaseData
otherPurchaseData.value = [...otherPurchaseData.value, ...mergedProductList]
importedFileUrl.value = path
importDialogVisible.value = false
}
const exportVisible = ref(false)
const exportForm = ref({
delivery: false,
resource: '',
})
const handleExport = () => {
exportVisible.value = true
}
const submitExportForm = async () => {
if (exportForm.value.resource === '') {
return ElMessage.error('请选择导出类型')
}
let purchaseIds = ''
let exportTotal: number | undefined = undefined
const params: AnyObject = {}
const resourceType = Number(exportForm.value.resource)
if (resourceType === 0) {
purchaseIds = tableData.value
.map((el: InterWarehousePage) => el.id)
.join(',')
} else if (resourceType === 1) {
purchaseIds = selections.value
.map((el: InterWarehousePage) => el.id)
.join(',')
} else if (resourceType === 2) {
purchaseIds = ''
exportTotal = total.value
params.billStatus = nodeCode.value == 'all' ? '' : nodeCode.value
}
params.idList = purchaseIds
if (exportTotal !== undefined) {
params.total = exportTotal
}
stackingPlanRecordExport({
showDetail: exportForm.value.delivery,
...params,
...searchForm.value,
})
.then((res) => {
const blob = new Blob([res as unknown as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const filename = `备货计划_${new Date().getTime()}.xlsx`
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = filename
link.click()
window.URL.revokeObjectURL(link.href)
})
.finally(() => {
exportVisible.value = false
})
}
const getWarehouseList = async () => {
try {
const res = await warehouseInfoGetAll()
warehouseList.value = res.data
await nextTick(() => {
treeRef.value!.setCurrentKey(nodeCode.value, true)
})
} catch (e) {
console.error(e)
}
}
const rowClick = (row: InterWarehousePage) => {
if (!row) {
currentRow.value = null
}
currentRow.value = row
tabsClick()
}
const skuData = ref<InterskuList[]>([])
const selectbySku = async () => {
if (!editForm.value.warehouseId) return ElMessage.error('请选择仓库')
try {
const res = await getBySkuApi(editForm.value.warehouseId, selectSku.value)
skuData.value = res.data || []
} catch (e) {
console.error(e)
}
}
const skudblclick = (val: InterskuList) => {
// 使用可选链和空值合并运算符处理可能的null值
const {
locationCode = '',
factoryPrice = null,
productNo = '',
sku = '',
skuName = '',
image = '',
locationId = null,
currencyName = '',
currencyCode = null,
} = val || {}
// 币种一致性校验
const lastItem =
otherPurchaseData.value[otherPurchaseData.value.length - 1] || null
if (lastItem && lastItem.currencyName) {
if (!currencyName || currencyName !== lastItem.currencyName) {
ElMessage.error(`添加的商品币种需一致`)
return
}
}
otherPurchaseData.value = [
...otherPurchaseData.value,
{
skuImage: image,
warehouseSku: sku,
skuName,
productNo,
locationCode: locationCode ?? '', // 确保空值处理
locationId: locationId ?? null, // 确保空值处理
costPrice: factoryPrice,
buyStored: null,
totalPrice: null,
currencyName,
currencyCode,
},
]
// 使用filter代替forEach+splice,时间复杂度从O(n^2)降到O(n)
const skuSet = new Set(
otherPurchaseData.value.map((item: InterProductList) => item.warehouseSku),
)
skuData.value = skuData.value.filter(
(item: InterskuList) => !skuSet.has(item.sku),
)
}
const tabsClick = async () => {
if (!currentRow.value) {
detailList.value = []
logList.value = []
return
}
await nextTick()
if (tabsValue.value === '0') {
searchDetail()
} else if (tabsValue.value === '1') {
getLogList()
}
}
const [editForm, resetEditForm] = useValue<InterStackingPlanDetail>({
inNo: '',
warehouseId: '',
warehouseName: '',
remark: '',
factoryCode: '',
factoryId: 0,
productList: [],
})
const newDialogVisible = ref(false)
const editFormRef = ref()
const editForm2 = ref({})
const formId = ref<number | undefined>(undefined)
const actionMap = [
{ label: '新增备货计划', value: 1 },
{ label: '编辑备货计划', value: 2 },
{ label: '备货计划审核', value: 3 },
{ label: '完成发货', value: 4 },
]
const currentAction = ref<number>(1)
const otherPurchaseData = ref<InterProductList[]>([])
const addDialog = async (i: number, v: InterWarehousePage | null) => {
currentAction.value = i
if (i !== 1) {
if (v) formId.value = v.id
if (v) getProduct(v.id)
if (!formId.value) return ElMessage('请勾选至少一条记录')
} else {
await nextTick()
editForm.value = JSON.parse(JSON.stringify(editForm2.value))
resetEditForm()
const userJson = localStorage.getItem('user')
if (userJson) {
try {
const userData = JSON.parse(userJson)
editForm.value.factoryCode = userData.factoryCode || ''
editForm.value.factoryId = userData.factoryId || 0
const defaultWarehouse = warehouseList.value.find(
(item: warehouseInfo) => item.defaulted === 1,
)
if (defaultWarehouse) {
editForm.value.warehouseId = defaultWarehouse.id
editForm.value.warehouseName = defaultWarehouse.name
}
} catch {
// ignore
}
}
otherPurchaseData.value = []
formId.value = undefined
}
fetchLocationList('')
selectSku.value = ''
newDialogVisible.value = true
}
const getProduct = async (id: number | undefined) => {
try {
const res = await getStackingPlanDetailApi(id)
editForm.value = JSON.parse(JSON.stringify(res.data))
otherPurchaseData.value = res.data?.productList || []
} catch (e) {
console.error(e)
}
}
const handleSelectionChange = (v: InterWarehousePage[]) => {
selections.value = v
}
const otherWarehouseSelection = ref<InterProductList[]>([])
const productSelectionChange = (v: InterProductList[]) => {
otherWarehouseSelection.value = v
}
const auditOrder = async (key: string) => {
if (selections.value.length === 0) {
return ElMessage.warning('请选择要操作的数据')
}
let url = ''
let text = ''
switch (key) {
case 'submitAudit':
url = 'factoryStockingPlanRecord/submit_audit'
text = '提交审核'
break
case 'toWaitLoadedCabinets':
url = 'factoryStockingPlanRecord/to_wait_unloaded_cabinets'
text = '转至待卸柜'
break
case 'toWaitInbound':
url = 'factoryStockingPlanRecord/to_wait_inbound'
text = '转至待上架'
break
}
const confimText = `确定对选中的信息进行${text}?`
await ElMessageBox.confirm(confimText, '重要提示', {
confirmButtonText: '确定',
type: 'warning',
})
try {
const data = selections.value.map(
({ id, dataVersion }: InterWarehousePage) => ({
id,
dataVersion,
}),
)
await auditOrderApi(url, data)
ElMessage.success('操作成功')
search()
await getTreeNum()
} catch (e) {
console.error(e)
}
}
const rejectedInRecord = (type: 'audit' | 'record') => {
if (type === 'record') {
if (selections.value.length === 0) {
return ElMessage.warning('请选择要操作的数据')
}
}
ElMessageBox.prompt('请输入驳回原因', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
inputPattern: /.+/,
customClass: 'reject',
inputErrorMessage: '请输入驳回原因',
inputPlaceholder: '驳回原因',
}).then(async ({ value }: { value: string }) => {
const data = selections.value.map(
({ id, dataVersion }: InterWarehousePage) => ({
id,
dataVersion,
}),
)
try {
await rejectStackingPlanApi({
list: data,
rejectReason: value,
status: nodeCode.value,
})
ElMessage.success('操作成功')
search()
await getTreeNum()
} catch (e) {
console.error(e)
}
})
}
watch(
() => tableData.value,
() => {
if (tableData.value && tableData.value.length > 0) {
if (singleTableRef.value) {
singleTableRef.value!.setCurrentRow(tableData.value[0])
currentRow.value = (tableData.value as never)[0]
}
} else {
detailList.value = []
}
},
{ immediate: true },
)
watch(
() => editForm.value.warehouseId,
(newVal: number | string | undefined) => {
if (newVal) {
fetchLocationList('')
}
},
)
const addOtherCurrency = async () => {
try {
await editFormRef.value?.validate()
} catch {
return
}
const arr = otherPurchaseData.value
if (arr.length === 0) {
ElMessage.error('请至少选择一条数据')
return
}
for (let i = 0; i < arr.length; i++) {
if (!arr[i].stockUpStored) {
ElMessage.error('请输入备货数量')
return
}
if (!arr[i].locationId) {
ElMessage.error('请选择库位')
return
}
const found = locationList.value.find(
(item: InterProductList) => item.locationId === arr[i].locationId,
)
if (!arr[i].locationCode) {
arr[i].locationCode = found ? found?.locationCode : ''
}
}
let url = ''
switch (nodeCode.value) {
// 新增
case 'PENDING_SUBMIT':
if (!formId.value) {
addSection()
} else {
upSection()
}
return
// 审核
case 'PENDING_AUDIT':
url = 'factoryStockingPlanRecord/audit'
break
// 完成发货
case 'WAIT_SHIPMENT':
url = 'factoryStockingPlanRecord/complete_shipment'
break
}
try {
console.log(currentRow.value)
const data = [
{ id: currentRow.value?.id, dataVersion: currentRow.value?.dataVersion },
] as InterWarehousePage[]
await auditOrderApi(url, data)
newDialogVisible.value = false
ElMessage.success('操作成功')
search()
await getTreeNum()
} catch (e) {
console.error(e)
}
}
const filterSkuData = computed(() => {
const skuList = otherPurchaseData.value.map((el) => el.warehouseSku)
// console.log(skuList, skuData.value)
return skuData.value.filter((el) => !skuList.includes(el.sku))
})
const addSection = async () => {
const params = { ...editForm.value }
try {
await addStackingPlanApi({
...params,
productList: otherPurchaseData.value,
})
ElMessage.success('保存成功')
newDialogVisible.value = false
search()
await getTreeNum()
} catch (e) {
console.error(e)
}
}
const upSection = async () => {
const params = { ...editForm.value }
// params.productList = otherPurchaseData.value
const result = checkUpdateParams(
{ ...params, productList: otherPurchaseData.value },
editForm.value as unknown as AnyObject,
'id',
{
productList: 'warehouseSku',
},
)
try {
await updateStackingPlanApi(result as unknown as InterStackingPlanDetail)
newDialogVisible.value = false
ElMessage.success('修改成功')
search()
await getTreeNum()
} catch (e) {
console.error(e)
}
}
const addPurchaseVisible = ref(false)
const purchaseTextarea = ref('')
const addPurchase = async () => {
addPurchaseVisible.value = true
purchaseTextarea.value = ''
}
const submitPurchase = async () => {
if (!purchaseTextarea.value) {
ElMessage.warning('请输入库存 SKU')
return
}
const filteredSkusList = await batchAddCommodity(purchaseTextarea.value)
const mergedProductList = filteredSkusList.map((skuItem) => {
return {
skuImage: skuItem.image,
warehouseSku: skuItem.sku,
skuName: skuItem.skuName,
productNo: skuItem.productNo,
locationCode: skuItem.locationCode ?? '',
locationId: skuItem.locationId ?? null,
costPrice: skuItem.factoryPrice,
stockUpStored: null,
totalPrice: null,
usableInventory: skuItem.usableInventory,
remark: null,
} as InterProductList
})
otherPurchaseData.value = [...otherPurchaseData.value, ...mergedProductList]
addPurchaseVisible.value = false
}
const deleteOtherWarehousing = () => {
const arr = otherWarehouseSelection.value
if (arr.length === 0) return
const idList = arr.map((v: InterProductList) => v.warehouseSku)
otherPurchaseData.value = otherPurchaseData.value.filter(
(item: InterProductList) => !idList.includes(item.warehouseSku),
)
}
const importDialogVisible = ref(false)
const importedFileUrl = ref('')
const importData = async () => {
importDialogVisible.value = true
importedFileUrl.value = ''
}
const handleBatchDelete = async () => {
if (!selections.value.length) {
return ElMessage.warning('请选择要删除的数据')
}
await ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
const str = selections.value.map((el: InterWarehousePage) => el.id).join(',')
await deleteStackingPlanApi(str)
ElMessage.success('删除成功')
search()
await getTreeNum()
}
const nodeClick = async (data: InterWarehouseTree) => {
nodeCode.value = data.code ?? ''
sessionStorage.setItem('InRecord_NodeCode', data.code ?? '')
Promise.all([search(), getTreeNum()])
}
const searchDetail = async () => {
try {
const res = await getStackingPlanDetailApi(currentRow.value?.id)
detailList.value = res.data?.productList || []
} catch (e) {
console.error(e)
}
}
const getLogList = async () => {
try {
const res = await getStackingPlanLogApi(currentRow.value?.id)
logList.value = res.data
} catch (e) {
console.error(e)
}
}
const locationList = ref<ILocation[]>([])
const locationLoading = ref(false)
const fetchLocationList = async (query: string) => {
// if (!query) {
// locationList.value = []
// return
// }
if (!editForm.value.warehouseId) return
locationLoading.value = true
try {
const res = await getByWareHouseIdAndCodeApi(
editForm.value.warehouseId,
query,
)
const result = res.data || []
locationList.value = result.map((item: ILocation) => {
return {
locationId: item.id,
locationCode: item.locationCode,
}
})
if (otherPurchaseData.value?.length > 0) {
//const importedSkus = otherPurchaseData.value.map((item) => item.warehouseSku).join(',')
// // 3. 调用 batchAddCommodity 获取商品的完整信息并过滤掉已有的 SKU
// const filteredSkusList = await batchAddCommodity(importedSkus)
// console.log('filteredSkusList', otherPurchaseData.value)
// 新增时切换仓库将重新匹配表格库位,确保该商品的库位和仓库一一对应
otherPurchaseData.value = otherPurchaseData.value.map(
(item: InterProductList) => {
const foundItem = locationList.value.find(
(locationItem: ILocation) =>
locationItem.locationCode === item.locationCode,
)
// 创建新对象而不是修改原对象
return {
...item,
locationCode: foundItem ? foundItem.locationCode : '',
locationId: foundItem ? foundItem.locationId : null,
}
},
)
}
} catch (e) {
locationList.value = []
} finally {
locationLoading.value = false
}
}
// 输入2秒后再调用接口(节流)
// const handleLocationSearch = debounce(fetchLocationList, 2000)
const handleLocationChange = (val: number, row: InterProductList) => {
const found = locationList.value.find(
(item: InterProductList) => item.locationId === val,
)
row.locationCode = found ? found.locationCode : ''
}
const handleWarehouseChange = (val: number | string | undefined) => {
const found = warehouseList.value.find(
(item: warehouseInfo) => item.id === val,
)
editForm.value.warehouseName = found ? found.name : ''
}
// 更新物流单号
const updateShipmentNumberVisible = ref(false)
const updateShipmentNumberForm = ref<InterWarehouseBase[]>([
{
inNo: '',
shipmentNumber: '',
warehouseId: '',
id: 0,
},
])
// 打开更新物流单号弹窗
const openUpdateShipmentNumberDialog = () => {
if (selections.value.length === 0) {
return ElMessage.warning('请选择要操作的数据')
}
updateShipmentNumberForm.value = selections.value.map(
(item: InterWarehousePage) => {
return {
inNo: item.inNo,
shipmentNumber: item.shipmentNumber,
warehouseId: item.warehouseId,
id: item.id,
}
},
)
updateShipmentNumberVisible.value = true
}
// 更新物流单号
const updateShipmentNumber = async () => {
try {
await updateShipmentNumberApi(
updateShipmentNumberForm.value as InterWarehouseBase[],
)
updateShipmentNumberVisible.value = false
ElMessage.success('更新成功')
search()
await getTreeNum()
} catch (e) {
console.error(e)
}
}
// 盘点
const scanDialogVisible = ref(false)
const scanInput = ref('')
const scanData = ref<InterStackingPlanDetail>({} as InterStackingPlanDetail)
const openScanDialog = () => {
scanInput.value = ''
scanData.value = {} as InterStackingPlanDetail
scanDialogVisible.value = true
}
// 扫码盘点
const scan = async () => {
console.log(scanInput.value, 'scanInput.value')
if (!scanInput.value) {
return ElMessage.warning('请输入备货单号')
}
scanInput.value = scanInput.value.trim()
// 将中文破折号转换为下划线
scanInput.value = scanInput.value.replace(/——/g, '_')
const formatRegex = /^BHD\d{9}_\d+$/
if (!formatRegex.test(scanInput.value)) {
return ElMessage.warning('请输入正确箱码')
}
const loading = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const res = await scanBoxCodeApi(scanInput.value)
if (res.code === 200) {
scanData.value = res.data
} else {
ElMessage.error(res.message)
}
} catch (e) {
console.error(e)
} finally {
loading.close()
scanInput.value = ''
scanInputRef.value?.focus()
}
}
/**
* @description: 页面添加回车监听
*/
const searchFormRef = ref()
const searchBtnRef = ref()
const scanInputRef = ref<InstanceType<typeof ElInput>>()
useEnterKeyTrigger({
formRef: searchFormRef,
btnRef: searchBtnRef,
callback: () => {
search()
},
})
onMounted(() => {
getTreeNum()
getWarehouseList()
})
</script>
<style lang="scss" scoped>
.dialog-footer {
display: inline-block;
width: 500px;
text-align: center;
}
.cursor-pointer {
cursor: pointer;
}
.header-filter-form {
:deep(.el-form-item) {
margin-right: 14px;
margin-bottom: 10px;
}
}
.product-dialog-footer {
display: flex;
justify-content: space-between;
margin: 8px 0;
}
$border: solid 1px #ddd;
.send-order-list {
display: grid;
grid-template-columns: 2fr 1fr;
border-left: $border;
border-top: $border;
}
.send-order-column {
padding: 10px 16px;
line-height: 1.5;
}
.send-order-header {
font-weight: bold;
text-align: center;
background-color: #f8f8f9;
}
.send-order-product-item {
display: flex;
justify-content: space-between;
gap: 20px;
&:not(:first-child) {
border-top: $border;
padding: 10px 0;
}
.send-order-prop-list {
flex: 1;
}
}
.send-order-product-image {
width: 100px;
}
.delivery-note-list {
:deep(.vertical-align-top) {
vertical-align: top;
}
}
.left {
width: 160px;
:deep(.el-tree-node__content) {
height: 30px;
line-height: 30px;
}
:deep(.el-tree-node__label) {
font-size: 13px;
cursor: pointer;
display: inline-block;
width: 100%;
color: black !important;
padding: 3px 7px;
}
:deep(.el-tree-node__expand-icon) {
display: none;
}
:deep(.is-current) {
.tree-node-label,
.tree-node-count {
background-color: #ecf5ff;
color: #409eff !important;
}
.el-tree-node__children {
.tree-node-label,
.tree-node-count {
background-color: transparent !important;
color: black !important;
}
}
}
}
.tree-node {
display: flex;
color: #333;
font-weight: 500;
}
.right {
flex: 1;
flex-shrink: 0;
background: white;
overflow: hidden;
}
::v-deep(.el-tree-node__label) {
font-size: 13px;
color: rgb(96, 98, 102);
cursor: pointer;
}
::v-deep(.el-tree-node__expand-icon) {
display: none;
}
::v-deep(.el-tree-node__label) {
display: inline-block;
width: 100%;
color: black !important;
padding: 3px 7px;
}
::v-deep(.is-current) {
.el-tree-node__label {
background-color: #ecf5ff;
color: #409eff !important;
}
.el-tree-node__children {
.el-tree-node__label {
background-color: transparent !important;
color: black !important;
}
}
}
::v-deep(.splitpanes__pane) {
display: flex;
flex-direction: column;
}
::v-deep(.splitpanes--horizontal > .splitpanes__splitter) {
min-height: 5px;
margin-top: 10px;
}
.draw-line {
width: 100%;
height: 5px;
background: #eff3f6;
}
.btn-list {
margin-bottom: 10px;
}
::v-deep(.el-tree-node) {
cursor: pointer;
margin-bottom: 5px;
}
::v-deep(.el-tree-node__label) {
font-size: 14px;
}
::v-deep(.el-tabs) {
display: flex;
// flex-direction: column;
height: 100%;
.el-tabs__content {
flex: 1;
flex-shrink: 0;
overflow: hidden;
.el-tab-pane {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
}
.delivery-note-page {
:deep(#top) {
height: 100%;
}
}
.import-dialog {
.import-content {
padding-bottom: 20px;
}
}
.import-success {
text-align: center;
padding: 20px 0;
.success-icon {
font-size: 48px;
margin-bottom: 16px;
}
.success-text {
font-size: 16px;
color: #67c23a;
margin-bottom: 16px;
}
.file-link {
a {
color: #409eff;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
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