Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
M
mini-wms
概览
Overview
Details
Activity
Cycle Analytics
版本库
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
问题
0
Issues
0
列表
Board
标记
里程碑
合并请求
0
Merge Requests
0
CI / CD
CI / CD
流水线
作业
日程表
图表
维基
Wiki
代码片段
Snippets
成员
Members
Collapse sidebar
Close sidebar
活动
图像
聊天
创建新问题
作业
提交
Issue Boards
Open sidebar
周海峰
mini-wms
Commits
7103082c
Commit
7103082c
authored
Dec 04, 2025
by
yubin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
inventory
parent
c5a04317
隐藏空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
1213 行增加
和
0 行删除
+1213
-0
ruoyi-admin-vue/src/api/inventory/inventory.js
+61
-0
ruoyi-admin-vue/src/components/materialsSeletor.vue
+456
-0
ruoyi-admin-vue/src/views/inventory/orders/OutboundOrderFormWithItems.vue
+696
-0
没有找到文件。
ruoyi-admin-vue/src/api/inventory/inventory.js
0 → 100644
View file @
7103082c
import
request
from
'@/utils/request'
// 查询库存列表
export
function
listInventory
(
query
)
{
return
request
({
url
:
'/inventory/inventory/list'
,
method
:
'get'
,
params
:
query
})
}
// 查询库存详细
export
function
getInventory
(
id
)
{
return
request
({
url
:
'/inventory/inventory/'
+
id
,
method
:
'get'
})
}
// 新增库存
export
function
addInventory
(
data
)
{
return
request
({
url
:
'/inventory/inventory'
,
method
:
'post'
,
data
:
data
})
}
// 修改库存
export
function
updateInventory
(
data
)
{
return
request
({
url
:
'/inventory/inventory'
,
method
:
'put'
,
data
:
data
})
}
// 删除库存
export
function
delInventory
(
id
)
{
return
request
({
url
:
'/inventory/inventory/'
+
id
,
method
:
'delete'
})
}
// 批量删除库存
export
function
delInventoryByIds
(
ids
)
{
return
request
({
url
:
'/inventory/inventory/'
+
ids
,
method
:
'delete'
})
}
// 根据物料ID查询库存
export
function
listInventoryByMaterialId
(
materialId
)
{
return
request
({
url
:
'/inventory/inventory/listByMaterialId'
,
method
:
'get'
,
params
:
{
materialId
:
materialId
}
})
}
ruoyi-admin-vue/src/components/materialsSeletor.vue
0 → 100644
View file @
7103082c
<
template
>
<div
class=
"material-selector-container"
style=
"overflow: hidden;"
>
<splitpanes
class=
"default-theme"
>
<pane
size=
"16"
style=
"overflow: auto;"
>
<TreeComponent
ref=
"treeComponent"
:tree-data=
"categoryTreeData"
:tree-props=
"treeProps"
:node-key=
"nodeKey"
:show-search=
"true"
search-placeholder=
"请输入分类名称"
:default-expand-all=
"true"
:highlight-current=
"true"
:loading=
"loadingTree"
@
node-click=
"handleTreeClick"
>
<template
#
node-content=
"
{ node, data }">
<span
class=
"custom-tree-node"
>
{{
node
.
label
}}
(
{{
data
.
categoryCode
||
'-'
}}
)
</span>
</
template
>
</TreeComponent>
</pane>
<pane
size=
"84"
style=
"overflow: auto;"
>
<div
style=
"padding: 10px; display: flex; flex-direction: column;"
>
<el-form
:model=
"queryParams"
ref=
"queryForm"
size=
"small"
:inline=
"true"
label-width=
"88px"
>
<el-form-item
label=
"物料编码"
prop=
"materialCode"
>
<el-input
v-model=
"queryParams.materialCode"
placeholder=
"请输入物料编码"
clearable
@
keyup
.
enter
.
native=
"handleQuery"
/>
</el-form-item>
<el-form-item
label=
"物料名称"
prop=
"materialName"
>
<el-input
v-model=
"queryParams.materialName"
placeholder=
"请输入物料名称"
clearable
@
keyup
.
enter
.
native=
"handleQuery"
/>
</el-form-item>
<el-form-item
label=
"SAP物料号"
prop=
"sapNo"
>
<el-input
v-model=
"queryParams.sapNo"
placeholder=
"请输入SAP物料号"
clearable
@
keyup
.
enter
.
native=
"handleQuery"
/>
</el-form-item>
<el-form-item
label=
"物料分类"
prop=
"categoryNameInput"
>
<el-input
v-model=
"queryParams.categoryNameInput"
placeholder=
"请输入物料分类"
clearable
@
keyup
.
enter
.
native=
"handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button
type=
"primary"
icon=
"el-icon-search"
size=
"mini"
@
click=
"handleQuery"
>
搜索
</el-button>
<el-button
icon=
"el-icon-refresh"
size=
"mini"
@
click=
"resetQuery"
>
重置
</el-button>
</el-form-item>
</el-form>
<el-table
ref=
"table"
v-loading=
"loading"
:data=
"materialsList"
@
selection-change=
"handleSelectionChange"
:scroll-x=
"true"
:row-key=
"row => row.materialCode"
@
row-click=
"handleRowClick"
:select-on-indeterminate=
"false"
>
<el-table-column
type=
"selection"
width=
"55"
align=
"center"
/>
<el-table-column
type=
"index"
label=
"序号"
align=
"center"
/>
<el-table-column
label=
"物料编码"
align=
"center"
prop=
"materialCode"
width=
"120"
/>
<el-table-column
label=
"物料名称"
align=
"center"
prop=
"materialName"
width=
"150"
/>
<el-table-column
label=
"SAP物料号"
align=
"center"
prop=
"sapNo"
/>
<el-table-column
label=
"TS Code"
align=
"center"
prop=
"tsCode"
/>
<el-table-column
label=
"物料分类编码(原始)"
align=
"center"
prop=
"categoryCode"
width=
"150"
/>
<el-table-column
label=
"物料分类编码(格式化)"
align=
"center"
width=
"180"
>
<
template
slot-scope=
"scope"
>
{{
categoryCodeFormatMap
[
scope
.
row
.
categoryCode
]
||
scope
.
row
.
categoryCode
||
'-'
}}
</
template
>
</el-table-column>
<el-table-column
label=
"物料分类名称"
align=
"center"
width=
"150"
>
<
template
slot-scope=
"scope"
>
{{
scope
.
row
.
displayCategory
||
categoryMap
[
scope
.
row
.
categoryCode
]
||
'未匹配'
}}
</
template
>
</el-table-column>
<el-table-column
label=
"规格型号"
align=
"center"
prop=
"specification"
/>
<el-table-column
label=
"计量单位"
align=
"center"
prop=
"materialUnit"
/>
<el-table-column
label=
"是否批次管理"
align=
"center"
prop=
"isBatchManaged"
>
<
template
slot-scope=
"scope"
>
<el-tag
:type=
"scope.row.isBatchManaged === 1 ? 'success' : 'info'"
size=
"mini"
>
{{
scope
.
row
.
isBatchManaged
===
1
?
'是'
:
'否'
}}
</el-tag>
</
template
>
</el-table-column>
</el-table>
<pagination
v-show=
"total>0"
:total=
"total"
:page
.
sync=
"queryParams.pageNum"
:limit
.
sync=
"queryParams.pageSize"
@
pagination=
"getList"
/>
</div>
</pane>
</splitpanes>
</div>
</template>
<
script
>
import
{
listMaterials
}
from
"@/api/inventory/materials"
import
{
listMaterials_category
}
from
"@/api/inventory/materials_category"
import
TreeComponent
from
'@/views/inventory/materials_category/treeComponent.vue'
import
{
Splitpanes
,
Pane
}
from
'splitpanes'
import
'splitpanes/dist/splitpanes.css'
export
default
{
name
:
"MaterialSelector"
,
components
:
{
TreeComponent
,
Splitpanes
,
Pane
},
props
:
{
selectedMaterialCodes
:
{
type
:
Array
,
default
:
()
=>
[]
},
defaultCategoryCodes
:
{
type
:
Array
,
default
:
()
=>
[]
},
multiple
:
{
type
:
Boolean
,
default
:
true
}
},
data
()
{
return
{
categoryTreeData
:
[],
treeProps
:
{
children
:
'children'
,
label
:
'label'
,
value
:
'sid'
},
nodeKey
:
'sid'
,
loadingTree
:
false
,
categoryMap
:
{},
categoryNameToCodeMap
:
{},
categoryCodeToSidMap
:
{},
categoryCodeFormatMap
:
{},
currentNodeId
:
null
,
queryParams
:
{
pageNum
:
1
,
pageSize
:
10
,
materialCode
:
null
,
materialName
:
null
,
sapNo
:
null
,
tsCode
:
null
,
categoryCode
:
null
,
categoryNameInput
:
null
,
specification
:
null
,
},
materialsList
:
[],
total
:
0
,
loading
:
true
,
selectedRows
:
[]
}
},
watch
:
{
/**
* 监听选中的物料编码变化,强制同步表格选中状态
*/
selectedMaterialCodes
:
{
immediate
:
true
,
deep
:
true
,
handler
(
val
)
{
console
.
log
(
'选择器接收的选中编码:'
,
val
)
if
(
!
val
||
!
val
.
length
||
!
this
.
materialsList
.
length
)
return
this
.
$nextTick
(()
=>
{
if
(
this
.
$refs
.
table
)
{
// 先清空所有选中状态
this
.
$refs
.
table
.
clearSelection
()
// 统一编码格式后再匹配
const
formattedCodes
=
val
.
map
(
code
=>
code
.
trim
().
toUpperCase
())
// 重新选中指定编码的物料
this
.
materialsList
.
forEach
(
row
=>
{
const
rowCode
=
row
.
materialCode
.
trim
().
toUpperCase
()
if
(
formattedCodes
.
includes
(
rowCode
))
{
this
.
$refs
.
table
.
toggleRowSelection
(
row
,
true
)
}
})
}
})
}
},
defaultCategoryCodes
:
{
immediate
:
true
,
handler
(
val
)
{
if
(
val
&&
val
.
length
&&
this
.
categoryTreeData
.
length
)
{
this
.
$nextTick
(()
=>
{
this
.
selectCategoryNodes
(
val
)
})
}
}
}
},
async
created
()
{
await
Promise
.
all
([
this
.
getCategoryTreeData
(),
this
.
getCategoryList
()
])
this
.
getList
()
},
methods
:
{
async
getCategoryList
()
{
try
{
const
response
=
await
listMaterials_category
({
pageNum
:
1
,
pageSize
:
1000
})
if
(
response
.
rows
&&
response
.
rows
.
length
>
0
)
{
this
.
categoryMap
=
{}
this
.
categoryNameToCodeMap
=
{}
this
.
categoryCodeFormatMap
=
{}
response
.
rows
.
forEach
(
item
=>
{
if
(
item
.
isUsed
!==
0
&&
item
.
isUsed
!==
'0'
)
{
const
code
=
item
.
categoryCode
this
.
categoryMap
[
code
]
=
item
.
categoryName
// 格式化分类编码
let
formattedCode
=
code
if
(
code
&&
code
.
length
===
6
)
{
formattedCode
=
`
${
code
.
substring
(
0
,
4
)}
-
${
code
.
substring
(
4
)}
`
}
else
if
(
code
&&
code
.
length
===
8
)
{
formattedCode
=
`
${
code
.
substring
(
0
,
4
)}
-
${
code
.
substring
(
4
,
6
)}
-
${
code
.
substring
(
6
)}
`
}
this
.
categoryCodeFormatMap
[
code
]
=
formattedCode
if
(
!
this
.
categoryNameToCodeMap
[
item
.
categoryName
])
{
this
.
categoryNameToCodeMap
[
item
.
categoryName
]
=
code
}
else
if
(
!
Array
.
isArray
(
this
.
categoryNameToCodeMap
[
item
.
categoryName
]))
{
this
.
categoryNameToCodeMap
[
item
.
categoryName
]
=
[
this
.
categoryNameToCodeMap
[
item
.
categoryName
],
code
]
}
else
{
this
.
categoryNameToCodeMap
[
item
.
categoryName
].
push
(
code
)
}
}
})
}
}
catch
(
error
)
{
console
.
error
(
'获取分类列表失败:'
,
error
)
}
},
async
getCategoryTreeData
()
{
this
.
loadingTree
=
true
try
{
const
response
=
await
listMaterials_category
({
pageNum
:
1
,
pageSize
:
1000
})
if
(
response
.
rows
&&
response
.
rows
.
length
>
0
)
{
const
activeCategories
=
response
.
rows
.
filter
(
item
=>
item
.
isUsed
!==
0
&&
item
.
isUsed
!==
'0'
)
this
.
categoryTreeData
=
this
.
buildTreeData
(
activeCategories
)
this
.
buildCategoryCodeToSidMap
(
this
.
categoryTreeData
)
}
}
catch
(
error
)
{
console
.
error
(
'获取分类树数据失败:'
,
error
)
}
finally
{
this
.
loadingTree
=
false
}
},
buildTreeData
(
list
,
parentId
=
null
)
{
return
list
.
filter
(
item
=>
parentId
===
null
?
(
!
item
.
parentId
||
item
.
parentId
===
0
||
item
.
parentId
===
'0'
)
:
item
.
parentId
==
parentId
)
.
map
(
item
=>
({
...
item
,
sid
:
String
(
item
.
id
),
label
:
item
.
categoryName
,
children
:
this
.
buildTreeData
(
list
,
item
.
id
).
length
?
this
.
buildTreeData
(
list
,
item
.
id
)
:
undefined
}))
},
buildCategoryCodeToSidMap
(
treeData
)
{
treeData
.
forEach
(
node
=>
{
if
(
node
.
categoryCode
)
{
this
.
categoryCodeToSidMap
[
node
.
categoryCode
]
=
node
.
sid
const
formattedCode
=
this
.
categoryCodeFormatMap
[
node
.
categoryCode
]
if
(
formattedCode
)
{
this
.
categoryCodeToSidMap
[
formattedCode
]
=
node
.
sid
}
}
if
(
node
.
children
&&
node
.
children
.
length
)
{
this
.
buildCategoryCodeToSidMap
(
node
.
children
)
}
})
},
selectCategoryNodes
(
categoryCodes
)
{
if
(
!
this
.
$refs
.
treeComponent
||
!
this
.
$refs
.
treeComponent
.
$refs
.
tree
)
return
const
tree
=
this
.
$refs
.
treeComponent
.
$refs
.
tree
categoryCodes
.
forEach
(
code
=>
{
const
rawCode
=
code
.
replace
(
/-/g
,
''
)
const
sid
=
this
.
categoryCodeToSidMap
[
rawCode
]
||
this
.
categoryCodeToSidMap
[
code
]
if
(
sid
)
{
tree
.
setCurrentKey
(
sid
)
this
.
currentNodeId
=
sid
const
node
=
tree
.
getNode
(
sid
)
if
(
node
)
{
tree
.
expandNode
(
node
)
}
}
})
},
handleTreeClick
(
data
)
{
this
.
currentNodeId
=
data
.
sid
this
.
queryParams
.
categoryCode
=
data
.
categoryCode
this
.
queryParams
.
categoryNameInput
=
null
this
.
queryParams
.
pageNum
=
1
this
.
getList
()
},
getList
()
{
this
.
loading
=
true
listMaterials
(
this
.
queryParams
).
then
(
response
=>
{
this
.
materialsList
=
(
response
.
rows
||
[]).
filter
(
item
=>
item
.
isUsed
!==
0
&&
item
.
isUsed
!==
'0'
).
map
(
item
=>
({
...
item
,
displayCategory
:
this
.
categoryMap
[
item
.
categoryCode
]
||
`
${
this
.
categoryCodeFormatMap
[
item
.
categoryCode
]
||
item
.
categoryCode
}
(未匹配分类)`
}))
this
.
total
=
response
.
total
console
.
log
(
'物料选择器加载列表:'
,
this
.
materialsList
)
// 强制同步选中状态
this
.
$nextTick
(()
=>
{
if
(
this
.
selectedMaterialCodes
.
length
&&
this
.
$refs
.
table
)
{
this
.
$refs
.
table
.
clearSelection
()
const
formattedCodes
=
this
.
selectedMaterialCodes
.
map
(
code
=>
code
.
trim
().
toUpperCase
())
this
.
materialsList
.
forEach
(
row
=>
{
const
rowCode
=
row
.
materialCode
.
trim
().
toUpperCase
()
if
(
formattedCodes
.
includes
(
rowCode
))
{
this
.
$refs
.
table
.
toggleRowSelection
(
row
,
true
)
}
})
}
})
}).
finally
(()
=>
{
this
.
loading
=
false
})
},
handleQuery
()
{
const
inputName
=
this
.
queryParams
.
categoryNameInput
if
(
inputName
)
{
const
matchedCode
=
this
.
categoryNameToCodeMap
[
inputName
]
if
(
matchedCode
)
{
this
.
queryParams
.
categoryCode
=
Array
.
isArray
(
matchedCode
)
?
matchedCode
[
0
]
:
matchedCode
}
else
{
const
matchedCodes
=
Object
.
entries
(
this
.
categoryMap
)
.
filter
(([
code
,
name
])
=>
name
.
includes
(
inputName
))
.
map
(([
code
])
=>
code
)
if
(
matchedCodes
.
length
>
0
)
{
this
.
queryParams
.
categoryCode
=
matchedCodes
[
0
]
}
}
}
this
.
queryParams
.
pageNum
=
1
this
.
getList
()
},
resetQuery
()
{
this
.
queryParams
=
{
pageNum
:
1
,
pageSize
:
10
,
materialCode
:
null
,
materialName
:
null
,
sapNo
:
null
,
tsCode
:
null
,
categoryCode
:
null
,
categoryNameInput
:
null
,
specification
:
null
,
}
this
.
currentNodeId
=
null
if
(
this
.
$refs
.
treeComponent
&&
this
.
$refs
.
treeComponent
.
$refs
.
tree
)
{
this
.
$refs
.
treeComponent
.
$refs
.
tree
.
setCurrentKey
(
null
)
}
this
.
getList
()
},
handleSelectionChange
(
selection
)
{
this
.
selectedRows
=
selection
const
categoryCodes
=
[...
new
Set
(
selection
.
map
(
item
=>
item
.
categoryCode
).
filter
(
c
=>
c
))]
const
formattedCategoryIds
=
categoryCodes
.
map
(
code
=>
this
.
categoryCodeFormatMap
[
code
]
||
code
)
const
selectionData
=
{
materialCodes
:
selection
.
map
(
item
=>
item
.
materialCode
||
''
),
names
:
selection
.
map
(
item
=>
item
.
materialName
||
''
),
formattedCategoryIds
:
formattedCategoryIds
}
console
.
log
(
'选择器选中数据:'
,
selectionData
)
this
.
$emit
(
'selection-change'
,
selectionData
)
this
.
$emit
(
'change'
,
selectionData
.
materialCodes
)
this
.
$emit
(
'input'
,
selectionData
.
materialCodes
)
},
handleRowClick
(
row
)
{
if
(
!
this
.
multiple
)
{
this
.
$refs
.
table
.
clearSelection
()
this
.
$refs
.
table
.
toggleRowSelection
(
row
,
true
)
const
categoryCode
=
row
.
categoryCode
||
''
const
formattedCategoryId
=
this
.
categoryCodeFormatMap
[
categoryCode
]
||
categoryCode
const
selectionData
=
{
materialCodes
:
[
row
.
materialCode
||
''
],
names
:
[
row
.
materialName
||
''
],
formattedCategoryIds
:
[
formattedCategoryId
]
}
this
.
$emit
(
'selection-change'
,
selectionData
)
this
.
$emit
(
'input'
,
selectionData
.
materialCodes
)
this
.
$emit
(
'select'
,
row
.
materialCode
)
}
},
getSelection
()
{
const
categoryCodes
=
[...
new
Set
(
this
.
selectedRows
.
map
(
item
=>
item
.
categoryCode
).
filter
(
c
=>
c
))]
const
formattedCategoryIds
=
categoryCodes
.
map
(
code
=>
this
.
categoryCodeFormatMap
[
code
]
||
code
)
return
{
materialCodes
:
this
.
selectedRows
.
map
(
item
=>
item
.
materialCode
||
''
),
names
:
this
.
selectedRows
.
map
(
item
=>
item
.
materialName
||
''
),
formattedCategoryIds
:
formattedCategoryIds
}
},
getSelectedMaterials
()
{
return
this
.
selectedRows
.
map
(
item
=>
item
.
materialCode
||
''
)
}
}
}
</
script
>
<
style
scoped
>
.material-selector-container
{
height
:
100%
;
min-height
:
500px
;
}
.custom-tree-node
{
font-size
:
14px
;
}
</
style
>
\ No newline at end of file
ruoyi-admin-vue/src/views/inventory/orders/OutboundOrderFormWithItems.vue
0 → 100644
View file @
7103082c
<
template
>
<el-dialog
:title=
"title"
:visible
.
sync=
"open"
width=
"950px"
append-to-body
@
close=
"handleClose"
:close-on-click-modal=
"false"
:close-on-press-escape=
"false"
>
<el-form
ref=
"detailForm"
:model=
"form"
label-width=
"100px"
:rules=
"rules"
>
<!-- 通用信息:保留货物ID、计划总数量(全局字段) -->
<el-row
:gutter=
"20"
>
<el-col
:span=
"12"
>
<el-form-item
label=
"货物ID"
prop=
"materialId"
>
<el-input
v-model=
"form.materialId"
placeholder=
"请选择或输入货物ID"
readonly
@
click
.
native=
"openMaterialSelector = true"
style=
"cursor: pointer"
>
<template
slot=
"suffix"
>
<el-button
icon=
"el-icon-search"
size=
"mini"
@
click
.
native=
"openMaterialSelector = true"
></el-button>
</
template
>
</el-input>
<!-- 物料选择器弹窗 -->
<el-dialog
title=
"选择物料"
:visible
.
sync=
"openMaterialSelector"
width=
"90%"
append-to-body
:close-on-click-modal=
"false"
:close-on-press-escape=
"false"
:modal=
"true"
:modal-append-to-body=
"true"
>
<div
style=
"height: 70vh; overflow: auto; padding: 0 10px;"
>
<MaterialSelector
ref=
"materialsSeletor"
@
selection-change=
"handleMaterialSelectionChange"
:selected-material-codes=
"form.materialUuids ? [form.materialUuids] : []"
:multiple=
"true"
/>
</div>
<div
slot=
"footer"
class=
"dialog-footer"
>
<el-button
@
click
.
native=
"openMaterialSelector = false"
>
取消
</el-button>
<el-button
type=
"primary"
@
click
.
native=
"confirmMaterialSelect"
>
确认选择
</el-button>
</div>
</el-dialog>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"计划总数量"
prop=
"totalPlannedQuantity"
>
<el-input
v-model
.
number=
"form.totalPlannedQuantity"
placeholder=
"请输入计划总数量"
type=
"number"
min=
"1"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 库存信息列表 -->
<el-row
v-if=
"form.materialId && form.materialId.trim()"
style=
"margin: 10px 0;"
>
<el-col
:span=
"24"
>
<div
style=
"margin-bottom: 8px; font-weight: 600; color: #1989fa;"
>
库存信息列表
</div>
<el-table
ref=
"inventoryTable"
:data=
"inventoryList"
border
size=
"small"
max-height=
"400"
highlight-current-row
stripe
empty-text=
"暂无库存数据"
@
selection-change=
"handleSelectionChange"
@
row-click=
"handleRowClick"
>
<el-table-column
type=
"selection"
width=
"55"
/>
<el-table-column
prop=
"id"
label=
"库存ID"
width=
"100"
/>
<el-table-column
prop=
"materialId"
label=
"货物ID"
width=
"100"
/>
<el-table-column
prop=
"batchId"
label=
"批次ID"
width=
"100"
/>
<el-table-column
prop=
"orderId"
label=
"订单ID"
width=
"120"
/>
<el-table-column
prop=
"warehouseId"
label=
"仓库ID"
width=
"100"
/>
<el-table-column
prop=
"locationId"
label=
"库位ID"
width=
"100"
/>
<el-table-column
prop=
"inventoryType"
label=
"库存类型"
width=
"100"
>
<
template
slot-scope=
"scope"
>
<el-tag
size=
"mini"
:type=
"scope.row.inventoryType === 1 ? 'primary' : 'success'"
>
{{
scope
.
row
.
inventoryType
===
1
?
'正品'
:
'次品'
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
label=
"可用数量"
width=
"100"
>
<
template
slot-scope=
"scope"
>
{{
(
scope
.
row
.
quantity
||
0
)
-
(
scope
.
row
.
lockedQuantity
||
0
)
}}
</
template
>
</el-table-column>
<el-table-column
label=
"选择数量"
width=
"120"
>
<
template
slot-scope=
"scope"
>
<el-input
v-model
.
number=
"scope.row.selectedQty"
type=
"number"
size=
"mini"
min=
"1"
:max=
"(scope.row.quantity || 0) - (scope.row.lockedQuantity || 0)"
@
input=
"handleRowQtyInput(scope.row)"
placeholder=
"输入数量"
/>
</
template
>
</el-table-column>
<el-table-column
label=
"计划件数"
width=
"120"
>
<
template
slot-scope=
"scope"
>
<el-input
v-model
.
number=
"scope.row.plannedPackages"
type=
"number"
size=
"mini"
min=
"0"
@
input=
"handleRowPackagesInput(scope.row)"
placeholder=
"输入件数"
/>
</
template
>
</el-table-column>
</el-table>
<!-- 选中行的扩展字段填写区域 -->
<div
v-if=
"currentSelectedRow"
style=
"margin-top: 10px; padding: 10px; border: 1px solid #e6e6e6; border-radius: 4px;"
>
<div
style=
"margin-bottom: 8px; font-weight: 600; color: #1989fa;"
>
库存【ID: {{ currentSelectedRow.id }}】明细信息
</div>
<el-row
:gutter=
"20"
>
<el-col
:span=
"8"
>
<el-form-item
label=
"约数"
prop=
"divisor"
>
<el-input
v-model
.
number=
"currentSelectedRow.divisor"
placeholder=
"请输入约数"
type=
"number"
min=
"0"
/>
</el-form-item>
</el-col>
<el-col
:span=
"8"
>
<el-form-item
label=
"标签颜色"
prop=
"labelColor"
>
<el-select
v-model=
"currentSelectedRow.labelColor"
placeholder=
"请选择标签颜色"
>
<el-option
label=
"红色"
value=
"1"
></el-option>
<el-option
label=
"蓝色"
value=
"2"
></el-option>
<el-option
label=
"绿色"
value=
"3"
></el-option>
<el-option
label=
"黄色"
value=
"4"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col
:span=
"8"
>
<el-form-item
label=
"单价"
prop=
"unitPrice"
>
<el-input
v-model
.
number=
"currentSelectedRow.unitPrice"
placeholder=
"请输入单价"
type=
"number"
min=
"0"
step=
"0.01"
/>
</el-form-item>
</el-col>
</el-row>
<el-row
:gutter=
"20"
style=
"margin-top: 10px;"
>
<el-col
:span=
"12"
>
<el-form-item
label=
"收货时间"
prop=
"receivedAt"
>
<el-date-picker
v-model=
"currentSelectedRow.receivedAt"
type=
"datetime"
value-format=
"yyyy-MM-dd HH:mm:ss"
placeholder=
"请选择收货时间"
style=
"width: 100%"
/>
</el-form-item>
</el-col>
<el-col
:span=
"12"
>
<el-form-item
label=
"收货人"
prop=
"receivedBy"
>
<el-input
v-model=
"currentSelectedRow.receivedBy"
placeholder=
"请输入收货人"
/>
</el-form-item>
</el-col>
</el-row>
<el-row
:gutter=
"20"
style=
"margin-top: 10px;"
>
<el-col
:span=
"24"
>
<el-form-item
label=
"凭证号"
prop=
"voucherNumber"
>
<el-input
v-model=
"currentSelectedRow.voucherNumber"
placeholder=
"请输入凭证号"
/>
</el-form-item>
</el-col>
</el-row>
<el-row
:gutter=
"20"
style=
"margin-top: 10px;"
>
<el-col
:span=
"24"
>
<el-form-item
label=
"备注"
prop=
"remark"
>
<el-input
v-model=
"currentSelectedRow.remark"
placeholder=
"请输入备注"
type=
"textarea"
rows=
"2"
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
<!-- 已生成的明细预览 -->
<el-row
v-if=
"details.length > 0"
style=
"margin: 10px 0;"
>
<el-col
:span=
"24"
>
<div
style=
"margin-bottom: 8px; font-weight: 600; color: #1989fa;"
>
已生成明细项
</div>
<el-table
:data=
"details"
border
size=
"small"
max-height=
"200"
>
<el-table-column
prop=
"batchId"
label=
"批次ID"
/>
<el-table-column
prop=
"warehouseId"
label=
"仓库ID"
/>
<el-table-column
prop=
"locationId"
label=
"库位ID"
/>
<el-table-column
prop=
"plannedQuantity"
label=
"计划数量"
/>
<el-table-column
prop=
"actualQuantity"
label=
"实际数量"
/>
<el-table-column
prop=
"plannedPackages"
label=
"计划件数"
/>
<el-table-column
prop=
"actualPackages"
label=
"实际件数"
/>
<el-table-column
prop=
"divisor"
label=
"约数"
/>
<el-table-column
prop=
"unitPrice"
label=
"单价"
/>
<el-table-column
prop=
"labelColor"
label=
"标签颜色"
>
<
template
slot-scope=
"scope"
>
{{
getLabelColorText
(
scope
.
row
.
labelColor
)
}}
</
template
>
</el-table-column>
<el-table-column
prop=
"receivedAt"
label=
"收货时间"
/>
<el-table-column
prop=
"receivedBy"
label=
"收货人"
/>
<el-table-column
prop=
"voucherNumber"
label=
"凭证号"
/>
<el-table-column
prop=
"remark"
label=
"备注"
/>
<el-table-column
label=
"操作"
width=
"80"
>
<
template
slot-scope=
"scope"
>
<el-button
type=
"text"
size=
"mini"
@
click
.
native=
"removeDetail(scope.row)"
>
删除
</el-button>
</
template
>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-form>
<div
slot=
"footer"
class=
"dialog-footer"
>
<el-button
@
click
.
native=
"handleClose"
>
取消
</el-button>
<el-button
type=
"primary"
@
click
.
native=
"handleSubmit"
>
确定
</el-button>
</div>
</el-dialog>
</template>
<
script
>
import
{
listInventoryByMaterialId
}
from
"@/api/inventory/inventory"
import
MaterialSelector
from
'../../../components/materialsSeletor.vue'
// 防抖函数(适配Vue2 this上下文)
function
debounce
(
fn
,
delay
=
500
)
{
let
timer
=
null
return
function
(...
args
)
{
if
(
timer
)
clearTimeout
(
timer
)
timer
=
setTimeout
(()
=>
{
fn
.
apply
(
this
,
args
)
},
delay
)
}
}
export
default
{
name
:
'OutboundOrderFormWithItems'
,
components
:
{
MaterialSelector
},
props
:
{
title
:
{
type
:
String
,
default
:
'新增明细项'
},
open
:
{
type
:
Boolean
,
default
:
false
},
initForm
:
{
type
:
Object
,
default
:
()
=>
({})
}
},
data
()
{
return
{
// 仅保留全局字段:货物ID、计划总数量
form
:
{
materialUuids
:
''
,
materialId
:
''
,
totalPlannedQuantity
:
null
,
itemStatus
:
'1'
// 对应数据库状态:1-待收货
},
details
:
[],
inventoryList
:
[],
loadingInstance
:
null
,
rules
:
{
materialId
:
[{
required
:
true
,
message
:
'请选择货物ID'
,
trigger
:
[
'change'
,
'blur'
]
}]
// totalPlannedQuantity: [{
// required: true,
// message: '请输入计划总数量',
// trigger: 'blur',
// type: 'number'
// }, {
// validator: (rule, value, callback) => {
// if (value && value
<
1
)
{
// callback(new Error('计划总数量不能小于1'))
// } else {
// callback()
// }
// },
// trigger: 'blur'
// }]
},
openMaterialSelector
:
false
,
selectedMaterialId
:
''
,
selectedMaterialInfo
:
null
,
currentSelectedRow
:
null
// 存储当前点击的库存行
}
},
created
()
{
this
.
debounceMaterialIdChange
=
debounce
(
this
.
handleMaterialIdChange
.
bind
(
this
),
500
)
},
watch
:
{
open
:
{
handler
(
newVal
)
{
if
(
newVal
)
{
this
.
form
=
{
...
this
.
$options
.
data
().
form
,
...
this
.
initForm
}
this
.
selectedMaterialId
=
this
.
form
.
materialId
||
''
this
.
selectedMaterialInfo
=
null
this
.
currentSelectedRow
=
null
// 重置选中行
if
(
this
.
form
.
materialId
&&
this
.
form
.
materialId
.
trim
())
{
this
.
handleMaterialIdChange
()
}
}
else
{
this
.
$nextTick
(()
=>
{
this
.
closeLoading
()
if
(
this
.
$refs
.
detailForm
)
{
this
.
$refs
.
detailForm
.
resetFields
()
}
})
}
},
immediate
:
true
},
initForm
:
{
handler
(
val
)
{
if
(
this
.
open
)
{
this
.
form
=
{
...
this
.
$options
.
data
().
form
,
...
val
}
this
.
selectedMaterialId
=
this
.
form
.
materialId
||
''
this
.
selectedMaterialInfo
=
null
this
.
currentSelectedRow
=
null
// 重置选中行
if
(
this
.
form
.
materialId
&&
this
.
form
.
materialId
.
trim
())
{
this
.
handleMaterialIdChange
()
}
}
},
immediate
:
true
,
deep
:
true
}
},
methods
:
{
// 标签颜色值转文本
getLabelColorText
(
value
)
{
const
colorMap
=
{
'1'
:
'红色'
,
'2'
:
'蓝色'
,
'3'
:
'绿色'
,
'4'
:
'黄色'
}
return
colorMap
[
value
]
||
'-'
},
closeLoading
()
{
if
(
this
.
loadingInstance
)
{
this
.
loadingInstance
.
close
()
this
.
loadingInstance
=
null
}
},
async
handleMaterialIdChange
()
{
const
materialId
=
this
.
form
.
materialId
?.
trim
()
||
''
if
(
!
materialId
)
{
this
.
inventoryList
=
[]
this
.
closeLoading
()
return
}
try
{
this
.
loadingInstance
=
this
.
$loading
({
text
:
'查询中...'
,
target
:
this
.
$el
,
lock
:
true
,
background
:
'rgba(0, 0, 0, 0.7)'
})
const
res
=
await
listInventoryByMaterialId
(
materialId
)
if
(
res
.
code
===
200
)
{
// 为每个库存行初始化扩展字段
this
.
inventoryList
=
(
res
.
rows
||
[]).
map
(
item
=>
({
...
item
,
selectedQty
:
null
,
plannedPackages
:
null
,
divisor
:
null
,
// 约数
labelColor
:
''
,
// 标签颜色
unitPrice
:
null
,
// 单价
receivedAt
:
''
,
// 收货时间
receivedBy
:
''
,
// 收货人
voucherNumber
:
''
,
// 凭证号
remark
:
''
,
// 备注
quantity
:
item
.
quantity
||
0
,
lockedQuantity
:
item
.
lockedQuantity
||
0
}))
if
(
this
.
inventoryList
.
length
===
0
)
{
this
.
$message
.
warning
(
'未查询到该物料的库存信息'
)
}
}
else
{
this
.
inventoryList
=
[]
this
.
$message
.
error
(
res
.
msg
||
'查询库存失败'
)
}
}
catch
(
error
)
{
this
.
$message
.
error
(
'查询库存失败:'
+
(
error
.
message
||
'网络异常'
))
this
.
inventoryList
=
[]
}
finally
{
this
.
closeLoading
()
}
},
// 点击库存行触发 - 显示扩展字段填写区域
handleRowClick
(
row
)
{
this
.
currentSelectedRow
=
row
// 自动选中当前行
this
.
$refs
.
inventoryTable
.
toggleRowSelection
(
row
,
true
)
},
handleRowQtyInput
(
row
)
{
if
(
isNaN
(
row
.
selectedQty
)
||
row
.
selectedQty
===
''
)
{
row
.
selectedQty
=
null
return
}
const
availableQty
=
(
row
.
quantity
||
0
)
-
(
row
.
lockedQuantity
||
0
)
if
(
row
.
selectedQty
>
availableQty
)
{
this
.
$message
.
warning
(
`选择数量不能超过可用数量
${
availableQty
}
`
)
row
.
selectedQty
=
availableQty
}
else
if
(
row
.
selectedQty
<
1
)
{
this
.
$message
.
warning
(
'选择数量不能小于1'
)
row
.
selectedQty
=
1
}
},
handleRowPackagesInput
(
row
)
{
if
(
isNaN
(
row
.
plannedPackages
)
||
row
.
plannedPackages
===
''
)
{
row
.
plannedPackages
=
null
return
}
if
(
row
.
plannedPackages
<
0
)
{
this
.
$message
.
warning
(
'计划件数不能小于0'
)
row
.
plannedPackages
=
0
}
},
handleSelectionChange
(
rows
)
{
// 过滤已取消选择的行
this
.
details
=
this
.
details
.
filter
(
detail
=>
rows
.
some
(
row
=>
row
.
id
===
detail
.
inventoryId
)
)
// 处理选中行,携带行级扩展字段
rows
.
forEach
(
row
=>
{
// 选择数量为空则跳过
if
(
row
.
selectedQty
===
null
||
row
.
selectedQty
===
''
)
return
const
availableQty
=
(
row
.
quantity
||
0
)
-
(
row
.
lockedQuantity
||
0
)
if
(
row
.
selectedQty
>
availableQty
||
row
.
selectedQty
<
1
)
{
this
.
$message
.
warning
(
`行
${
row
.
id
}
的选择数量不合法,请重新输入`
)
row
.
selectedQty
=
Math
.
min
(
Math
.
max
(
row
.
selectedQty
,
1
),
availableQty
)
}
// 校验行级必填字段(可根据实际需求调整)
if
(
!
row
.
divisor
&&
row
.
divisor
!==
0
)
{
this
.
$message
.
warning
(
`库存ID:
${
row
.
id
}
请填写约数`
)
return
}
if
(
!
row
.
labelColor
)
{
this
.
$message
.
warning
(
`库存ID:
${
row
.
id
}
请选择标签颜色`
)
return
}
if
(
!
row
.
unitPrice
&&
row
.
unitPrice
!==
0
)
{
this
.
$message
.
warning
(
`库存ID:
${
row
.
id
}
请填写单价`
)
return
}
if
(
!
row
.
receivedAt
)
{
this
.
$message
.
warning
(
`库存ID:
${
row
.
id
}
请选择收货时间`
)
return
}
if
(
!
row
.
receivedBy
)
{
this
.
$message
.
warning
(
`库存ID:
${
row
.
id
}
请填写收货人`
)
return
}
const
existingIndex
=
this
.
details
.
findIndex
(
d
=>
d
.
inventoryId
===
row
.
id
)
const
newDetail
=
{
inventoryId
:
row
.
id
,
materialId
:
this
.
form
.
materialId
,
batchId
:
row
.
batchId
||
''
,
warehouseId
:
row
.
warehouseId
||
''
,
locationId
:
row
.
locationId
||
''
,
plannedQuantity
:
row
.
selectedQty
,
actualQuantity
:
row
.
selectedQty
,
plannedPackages
:
row
.
plannedPackages
||
0
,
actualPackages
:
row
.
plannedPackages
||
0
,
// 实际件数默认等于计划件数
divisor
:
row
.
divisor
,
// 行级约数
labelColor
:
row
.
labelColor
,
// 行级标签颜色
unitPrice
:
row
.
unitPrice
,
// 行级单价
itemStatus
:
this
.
form
.
itemStatus
||
'1'
,
receivedAt
:
row
.
receivedAt
,
// 行级收货时间
receivedBy
:
row
.
receivedBy
,
// 行级收货人
voucherNumber
:
row
.
voucherNumber
||
''
,
// 行级凭证号
remark
:
row
.
remark
||
''
// 行级备注
}
if
(
existingIndex
>
-
1
)
{
this
.
details
.
splice
(
existingIndex
,
1
,
newDetail
)
}
else
{
this
.
details
.
push
(
newDetail
)
}
})
},
removeDetail
(
row
)
{
this
.
details
=
this
.
details
.
filter
(
d
=>
d
.
inventoryId
!==
row
.
inventoryId
)
if
(
this
.
$refs
.
inventoryTable
&&
row
.
id
)
{
const
inventoryRow
=
this
.
inventoryList
.
find
(
item
=>
item
.
id
===
row
.
inventoryId
)
if
(
inventoryRow
)
{
this
.
$refs
.
inventoryTable
.
toggleRowSelection
(
inventoryRow
,
false
)
// 清空该行的填写内容
inventoryRow
.
selectedQty
=
null
inventoryRow
.
plannedPackages
=
null
inventoryRow
.
divisor
=
null
inventoryRow
.
labelColor
=
''
inventoryRow
.
unitPrice
=
null
inventoryRow
.
receivedAt
=
''
inventoryRow
.
receivedBy
=
''
inventoryRow
.
voucherNumber
=
''
inventoryRow
.
remark
=
''
// 如果删除的是当前选中行,重置currentSelectedRow
if
(
this
.
currentSelectedRow
&&
this
.
currentSelectedRow
.
id
===
row
.
inventoryId
)
{
this
.
currentSelectedRow
=
null
}
}
}
},
handleSubmit
()
{
if
(
!
this
.
form
.
materialId
?.
trim
())
{
this
.
$message
.
error
(
'请先选择物料ID'
)
return
}
this
.
$refs
.
detailForm
.
validate
(
async
(
valid
)
=>
{
if
(
!
valid
)
{
this
.
$message
.
error
(
'表单校验失败,请检查必填项'
)
return
}
if
(
this
.
details
.
length
===
0
)
{
this
.
$message
.
error
(
'请选择库存并填写数量生成明细'
)
return
}
// 计算实际总数量
const
totalActual
=
this
.
details
.
reduce
((
sum
,
d
)
=>
sum
+
(
d
.
actualQuantity
||
0
),
0
)
if
(
this
.
form
.
totalPlannedQuantity
&&
this
.
form
.
totalPlannedQuantity
!==
totalActual
)
{
try
{
await
this
.
$confirm
(
`实际总数量(
${
totalActual
}
)与计划总数量(
${
this
.
form
.
totalPlannedQuantity
}
)不一致,是否继续提交?`
,
'提示'
,
{
confirmButtonText
:
'继续'
,
cancelButtonText
:
'取消'
,
type
:
'warning'
}
)
}
catch
(
e
)
{
return
}
}
this
.
$emit
(
'submit'
,
this
.
details
)
this
.
$emit
(
'update:open'
,
false
)
})
},
handleClose
()
{
this
.
closeLoading
()
this
.
$nextTick
(()
=>
{
this
.
form
=
this
.
$options
.
data
().
form
this
.
details
=
[]
this
.
inventoryList
=
[]
this
.
selectedMaterialId
=
''
this
.
selectedMaterialInfo
=
null
this
.
openMaterialSelector
=
false
this
.
currentSelectedRow
=
null
// 重置选中行
if
(
this
.
$refs
.
detailForm
)
{
this
.
$refs
.
detailForm
.
resetFields
()
}
})
this
.
$emit
(
'update:open'
,
false
)
},
handleMaterialSelectionChange
()
{
const
selectedData
=
this
.
$refs
.
materialsSeletor
?.
getSelectedMaterials
?.()
||
this
.
$refs
.
materialsSeletor
?.
selectedList
||
[]
if
(
selectedData
&&
selectedData
.
length
>
0
)
{
this
.
selectedMaterialInfo
=
selectedData
[
0
]
this
.
selectedMaterialId
=
this
.
selectedMaterialInfo
||
''
}
else
{
this
.
selectedMaterialInfo
=
null
this
.
selectedMaterialId
=
''
}
},
confirmMaterialSelect
()
{
this
.
handleMaterialSelectionChange
()
if
(
!
this
.
selectedMaterialId
)
{
this
.
$message
.
warning
(
'请选择物料后再确认'
)
return
}
this
.
$set
(
this
.
form
,
'materialId'
,
this
.
selectedMaterialId
)
this
.
$set
(
this
.
form
,
'materialUuids'
,
this
.
selectedMaterialInfo
.
materialCode
||
this
.
selectedMaterialId
)
this
.
openMaterialSelector
=
false
this
.
handleMaterialIdChange
()
this
.
$nextTick
(()
=>
{
if
(
this
.
$refs
.
detailForm
)
{
this
.
$refs
.
detailForm
.
validateField
(
'materialId'
)
}
})
}
},
beforeDestroy
()
{
this
.
closeLoading
()
}
}
</
script
>
<
style
scoped
>
.dialog-footer
{
text-align
:
center
;
}
/
deep
/
.el-table
{
--el-table-header-text-color
:
#303133
;
--el-table-row-hover-bg-color
:
#f8f9fa
;
}
/
deep
/
.el-table
th
{
background-color
:
#f8f9fa
;
font-weight
:
600
;
}
/
deep
/
.el-table
.el-tag
{
margin
:
0
;
}
/
deep
/
.el-table
.el-input
{
width
:
100px
;
}
/
deep
/
.material-selector-container
{
height
:
100%
;
min-height
:
500px
;
}
/* 修复模态框遮罩层点击穿透问题 */
/
deep
/
.el-dialog__wrapper
{
.el-modal__mask
{
pointer-events
:
auto
!important
;
}
.el-dialog
{
pointer-events
:
auto
!important
;
}
}
</
style
>
\ No newline at end of file
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论