@@ -66,40 +66,25 @@
<!-- 新增提示组件 -- >
< el-alert title = "提示:列表存在分页,部分信息需翻页查看" type = "info" closable show -icon style = "margin-bottom: 10px;" / >
< el-table v-loading = "loading" :data="requirementsList" @selection-change="handleSelectionChange"
@ expand -change = " onExpandChange " >
< el-table-column type = "expand" width = "36" >
< template slot -scope = " props " >
< div style = "padding: 8px 24px; background:#fafafa;" >
< div style = "font-weight:600; margin-bottom:6px;" >
已入库批次 ( { { ( batchMap [ props . row . requirementId ] || [ ] ) . length } } 批 )
< / div >
< el-table v-loading = "batchLoading[props.row.requirementId]"
: data = "batchMap[props.row.requirementId] || []" size = "mini" stripe >
< el-table-column label = "入库时间" prop = "signTime" width = "160" >
< template slot -scope = " s " > { { parseTime ( s . row . signTime , '{y}-{m}-{d} {h}:{i}' ) } } < / template >
< / el-table-column >
< el-table-column label = "入库单" prop = "masterNum" width = "160" / >
< el-table-column label = "入库人" prop = "signUser" width = "100" / >
< el-table-column label = "物料概览" prop = "summary" min -width = " 240 " show -overflow -tooltip / >
< el-table-column label = "总数量" prop = "totalQty" width = "80" align = "right" / >
< el-table-column label = "总金额" width = "110" align = "right" >
< template slot -scope = " s " > ¥ { { Number ( s . row . totalAmount || 0 ) . toFixed ( 2 ) } } < / template >
< / el-table-column >
< / el-table >
< div v-if ="(batchMap[props.row.requirementId] || []).length === 0
&& !batchLoading[props.row.requirementId]"
style="text-align:center; color:#909399; padding:12px 0;">
暂无入库批次
</div>
</div>
</template>
</el-table-column>
< el-table v-loading = "loading" :data="requirementsList" @selection-change="handleSelectionChange" >
< el -table -column type = "selection" width = "55" align = "center" / >
< el-table-column label = "需求标题" align = "center" prop = "title" min -width = " 160 " show -overflow -tooltip / >
< el-table-column label = "需求方" align = "center" prop = "requesterNickName" width = "100" show -overflow -tooltip / >
< el-table-column label = "负责人" align = "center" prop = "ownerNickName" width = "100" show -overflow -tooltip / >
< el-table-column label = "关联项目" align = "center" prop = "projectName" min -width = " 160 " show -overflow -tooltip / >
< el-table-column label = "采购物料" align = "left" min -width = " 240 " >
< template slot -scope = " { row } " >
< template v-if = "row.materials && row.materials.length" >
< div v-for = "m in row.materials" :key="m.id" class="mat-row" >
< span class = "mat-name" > { { m . name } } < span v-if = "m.model" class="mat-model" > / {{ m.model }} < / span > < / span >
< span class = "mat-stock" : class = "{ low: (m.inventory||0) <= 0 }" >
库存 { { m . inventory == null ? 0 : m . inventory } } { { m . unit || '' } }
< / span >
< / div >
< / template >
< span v-else style = "color:#c0c4cc;" > 未关联 < / span >
< / template >
< / el-table-column >
< el-table-column label = "需求描述" align = "center" prop = "description" min -width = " 200 " show -overflow -tooltip >
< template slot -scope = " { row } " >
< span v-if = "row.description" class="copyable-text" @click="copyText(row.description)"
@@ -153,9 +138,6 @@
</el-table-column>
<el-table-column label=" 操作 " align=" center " class-name=" small - padding fixed - width ">
<template slot-scope=" scope ">
<el-button size="mini" type="text" icon="el-icon-truck" style="color:#409eff"
v-if="scope.row.status !== 2 && scope.row.status !== 3"
@click="handleGoToInbound(scope.row)">执行入库</el-button>
<el-button size=" mini " type=" text " icon=" el - icon - check " @click=" handleComplete ( scope . row ) "
v-if=" scope . row . status === 1 ">完成</el-button>
<el-button size=" mini " type=" text " icon=" el - icon - edit " @click=" handleUpdate ( scope . row ) "
@@ -184,6 +166,25 @@
<el-form-item label=" 关联项目 " prop=" projectId ">
<project-select v-model=" form . projectId " style=" width : 100 % " />
</el-form-item>
<el-form-item label=" 采购物料 " prop=" materialIdArr ">
<div class=" mat - trigger ">
<el-button size=" mini " icon=" el - icon - search " @click=" openMaterialPicker ">
选择物料{{ selectedMaterials.length ? `(已选 ${selectedMaterials.length}) ` : '' }}
</el-button>
<span v-if=" ! selectedMaterials . length " class=" mat - trigger - hint ">可多选;留空也可保存,后续再关联</span>
</div>
<div v-if=" selectedMaterials . length " class=" mat - stock - panel ">
<div v-for=" m in selectedMaterials " :key=" m . id " class=" mat - stock - row ">
<span class=" mat - stock - name ">{{ m.name }}<span v-if=" m . model " style=" color : # 909399 ; "> / {{ m.model }}</span></span>
<span class=" mat - stock - tag " :class=" { low : ( m . inventory || 0 ) <= 0 } ">
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
</span>
<span v-if=" m . brand " class=" mat - stock - meta ">品牌:{{ m.brand }}</span>
<span v-if=" m . specifications " class=" mat - stock - meta ">规格:{{ m.specifications }}</span>
<i class=" el - icon - close mat - remove " title=" 移除 " @click=" removeSelectedMaterial ( m . id ) "></i>
</div>
</div>
</el-form-item>
<el-form-item label=" 需求描述 " prop=" description ">
<el-input v-model=" form . description " type=" textarea " placeholder=" 请输入需求描述 " />
</el-form-item>
@@ -205,6 +206,84 @@
</div>
</el-dialog>
<!-- 物料选择器 -->
<el-dialog title=" 选择采购物料 " :visible.sync=" materialPickerOpen " width=" 780 px "
append-to-body :close-on-click-modal=" false " @open=" onPickerOpen ">
<div class=" picker - toolbar ">
<el-input v-model=" picker . kw " placeholder=" 搜索 名称 / 型号 / 品牌 / 规格 "
size=" mini " clearable prefix-icon=" el - icon - search "
style=" width : 320 px ; " @input=" onPickerSearch " @clear=" onPickerSearch " />
<el-button size=" mini " type=" primary " plain icon=" el - icon - plus "
style=" margin - left : 8 px ; " @click=" openInlineNew ">未找到?新增物料</el-button>
<span class=" picker - tip " v-if=" picker . tempSelected . length ">
已勾选 <b>{{ picker.tempSelected.length }}</b> 项
</span>
</div>
<!-- 内嵌新增物料表单 -->
<el-card v-if=" newMat . show " shadow=" never " class=" new - mat - card ">
<div slot=" header " class=" new - mat - header ">
<span><i class=" el - icon - plus " /> 新增物料到库存</span>
<el-button type=" text " size=" mini " icon=" el - icon - close " @click=" newMat . show = false ">收起</el-button>
</div>
<el-form :model=" newMat " :rules=" newMatRules " ref=" newMatForm " size=" mini "
label-width=" 70 px " :inline=" true ">
<el-form-item label=" 名称 " prop=" name " required>
<el-input v-model=" newMat . name " placeholder=" 物料名称 " style=" width : 180 px ; " />
</el-form-item>
<el-form-item label=" 型号 " prop=" model ">
<el-input v-model=" newMat . model " placeholder=" 可选 " style=" width : 140 px ; " />
</el-form-item>
<el-form-item label=" 规格 " prop=" specifications ">
<el-input v-model=" newMat . specifications " placeholder=" 可选 " style=" width : 140 px ; " />
</el-form-item>
<el-form-item label=" 品牌 " prop=" brand ">
<el-input v-model=" newMat . brand " placeholder=" 可选 " style=" width : 120 px ; " />
</el-form-item>
<el-form-item label=" 单位 " prop=" unit ">
<el-input v-model=" newMat . unit " placeholder=" 个 / 箱 / kg " style=" width : 100 px ; " />
</el-form-item>
<el-form-item label=" 初始库存 " prop=" inventory " required>
<el-input-number v-model=" newMat . inventory " :min=" 0 " :step=" 1 " size=" mini " />
</el-form-item>
<el-form-item>
<el-button size=" mini " type=" primary " :loading=" newMat . saving " @click=" submitNewMat ">保存并选中</el-button>
<el-button size=" mini " @click=" newMat . show = false ">取消</el-button>
</el-form-item>
</el-form>
</el-card>
<el-table :data=" picker . list " v-loading=" picker . loading " size=" mini "
ref=" pickerTable " border highlight-current-row max-height=" 380 "
row-key=" id " :row-class-name=" pickerRowClass "
@row-click=" onPickerRowClick "
@selection-change=" onPickerSelectionChange ">
<el-table-column type=" selection " width=" 42 " reserve-selection />
<el-table-column label=" 名称 " prop=" name " min-width=" 140 " show-overflow-tooltip />
<el-table-column label=" 型号 " prop=" model " width=" 120 " show-overflow-tooltip />
<el-table-column label=" 规格 " prop=" specifications " width=" 110 " show-overflow-tooltip />
<el-table-column label=" 品牌 " prop=" brand " width=" 100 " show-overflow-tooltip />
<el-table-column label=" 单位 " prop=" unit " width=" 60 " align=" center " />
<el-table-column label=" 库存 " width=" 90 " align=" right ">
<template slot-scope=" s ">
<span :style=" { color : ( s . row . inventory || 0 ) <= 0 ? '#f56c6c' : '#67c23a' , fontWeight : 600 } ">
{{ s.row.inventory == null ? 0 : s.row.inventory }}
</span>
</template>
</el-table-column>
</el-table>
<div class=" picker - pager " v-if=" picker . total > picker . pageSize ">
<el-pagination small background layout=" prev , pager , next , total "
:total=" picker . total " :page-size=" picker . pageSize " :current-page.sync=" picker . pageNum "
@current-change=" loadPickerList " />
</div>
<div slot=" footer ">
<el-button @click=" materialPickerOpen = false ">取 消</el-button>
<el-button type=" primary " @click=" confirmPicker ">确定({{ picker.tempSelected.length }}) </el-button>
</div>
</el-dialog>
<!-- 需求详情对话框 -->
<el-dialog title=" 需求详情 " :visible.sync=" detailDialog " width=" 600 px " append-to-body>
<el-descriptions :column=" 1 " border>
@@ -233,8 +312,9 @@
</template>
<script>
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/ requirement";
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@ / api / oa / requirement ";
import { listUser } from " @ / api / system / user ";
import { listOaWarehouse, getOaWarehouse, addOaWarehouse } from " @ / api / oa / warehouse / oaWarehouse ";
import FilePreview from '@/components/FilePreview';
import FileUpload from '@/components/FileUpload';
import ProjectSelect from " @ / components / fad - service / ProjectSelect ";
@@ -244,9 +324,35 @@ export default {
components: { FileUpload, FilePreview, ProjectSelect },
data () {
return {
// 入库批次(按 requirementId 缓存 )
batchMap: {} ,
batchLoading: {},
// 当前已选物料明细(用于库存展示 )
selectedMaterials: [] ,
// 物料选择器
materialPickerOpen: false,
picker: {
kw: '',
loading: false,
list: [],
total: 0,
pageNum: 1,
pageSize: 10,
tempSelected: [], // 选择器内部勾选项(含完整物料对象)
searchTimer: null,
},
// 新增物料内嵌表单
newMat: {
show: false,
saving: false,
name: '',
model: '',
specifications: '',
brand: '',
unit: '',
inventory: 0,
},
newMatRules: {
name: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
inventory: [{ required: true, message: '初始库存不能为空', trigger: 'change' }],
},
// 按钮loading
buttonLoading: false,
// 遮罩层
@@ -324,11 +430,120 @@ export default {
this.refreshStat();
},
methods: {
// 跳到入库明细页面,并预填该采购需求
handleGoToInbound (row ) {
this.$router.push({
path: '/step/in',
query: { requirementId: String(row.requirementId), requirementTitle: row.title }
// ====== 物料选择器 ======
openMaterialPicker ( ) {
this.picker.kw = ''
this.picker.pageNum = 1
// 当前已选项作为初始勾选
this.picker.tempSelected = this.selectedMaterials.slice()
this.newMat.show = false
this.materialPickerOpen = true
},
onPickerOpen () {
this.loadPickerList().then(() => this.syncPickerSelectionUI())
},
loadPickerList () {
this.picker.loading = true
const params = {
pageNum: this.picker.pageNum,
pageSize: this.picker.pageSize,
}
if (this.picker.kw && this.picker.kw.trim()) params.name = this.picker.kw.trim()
return listOaWarehouse(params).then(res => {
this.picker.list = res.rows || []
this.picker.total = res.total || 0
this.$nextTick(() => this.syncPickerSelectionUI())
}).finally(() => { this.picker.loading = false })
},
onPickerSearch () {
clearTimeout(this.picker.searchTimer)
this.picker.searchTimer = setTimeout(() => {
this.picker.pageNum = 1
this.loadPickerList()
}, 250)
},
// 表格分页/刷新后把已勾选项重新打勾
syncPickerSelectionUI () {
const tbl = this.$refs.pickerTable
if (!tbl) return
tbl.clearSelection()
const ids = new Set(this.picker.tempSelected.map(m => m.id))
for (const row of this.picker.list) {
if (ids.has(row.id)) tbl.toggleRowSelection(row, true)
}
},
onPickerSelectionChange (rows) {
// 当前页勾选 + 之前其它页保留下来的(不在 list 中的)
const curIds = new Set(this.picker.list.map(m => m.id))
const keepOther = this.picker.tempSelected.filter(m => !curIds.has(m.id))
this.picker.tempSelected = keepOther.concat(rows)
},
onPickerRowClick (row) {
const tbl = this.$refs.pickerTable
if (!tbl) return
const idx = this.picker.tempSelected.findIndex(m => m.id === row.id)
tbl.toggleRowSelection(row, idx === -1)
},
pickerRowClass ({ row }) {
return this.picker.tempSelected.find(m => m.id === row.id) ? 'picker-row-checked' : ''
},
confirmPicker () {
this.selectedMaterials = this.picker.tempSelected.slice()
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
this.materialPickerOpen = false
},
removeSelectedMaterial (id) {
this.selectedMaterials = this.selectedMaterials.filter(m => m.id !== id)
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
},
// ====== 新增物料 ======
openInlineNew () {
this.newMat.show = true
this.newMat.name = this.picker.kw || ''
this.newMat.model = ''
this.newMat.specifications = ''
this.newMat.brand = ''
this.newMat.unit = ''
this.newMat.inventory = 0
this.$nextTick(() => { this.$refs.newMatForm && this.$refs.newMatForm.clearValidate() })
},
submitNewMat () {
this.$refs.newMatForm.validate(valid => {
if (!valid) return
this.newMat.saving = true
const payload = {
name: this.newMat.name.trim(),
model: this.newMat.model || undefined,
specifications: this.newMat.specifications || undefined,
brand: this.newMat.brand || undefined,
unit: this.newMat.unit || undefined,
inventory: Number(this.newMat.inventory) || 0,
}
addOaWarehouse(payload).then(res => {
// 后端返回的 data 可能是 id 或对象,统一兜底再 get 一次
const newId = (res && (res.data && (res.data.id || typeof res.data === 'number'))) ? (res.data.id || res.data) : null
const finish = (full) => {
if (!full) return
// 直接放进当前页顶端 + 勾选
if (!this.picker.list.find(m => m.id === full.id)) {
this.picker.list.unshift(full)
}
if (!this.picker.tempSelected.find(m => m.id === full.id)) {
this.picker.tempSelected.push(full)
}
this.syncPickerSelectionUI()
this.newMat.show = false
this.$modal.msgSuccess('已新增并自动选中')
}
if (newId) {
getOaWarehouse(newId).then(r => finish(r.data))
} else {
// 兜底:用刚提交的字段拼一个临时对象
finish({ ...payload, id: Date.now() })
// 重新查一下保证 id 真实
this.loadPickerList()
}
}).finally(() => { this.newMat.saving = false })
})
},
// 后端已联查 sys_oss 拼好字符串 " ossId | name | url , , ossId | name | url "
@@ -368,18 +583,6 @@ export default {
if (file && file.url) window.open(file.url, '_blank')
}
},
// 展开行:加载该需求的入库批次
onExpandChange (row, expanded) {
if (!expanded || !expanded.length) return
const id = row.requirementId
if (this.batchMap[id]) return // 已有缓存
this.$set(this.batchLoading, id, true)
getRequirementBatches(id).then(res => {
this.$set(this.batchMap, id, res.data || [])
}).finally(() => {
this.$set(this.batchLoading, id, false)
})
},
async onStatusChange (row, newVal) {
row._updating = true
// 如果后端需要字符串,可改为 String(newVal)
@@ -453,6 +656,8 @@ export default {
requesterId: undefined,
ownerId: undefined,
projectId: undefined,
materialIds: undefined,
materialIdArr: [],
description: undefined,
deadline: undefined,
status: 0,
@@ -488,6 +693,7 @@ export default {
handleAdd () {
this.reset();
this.form.requesterId = this.$store.state.user.id;
this.selectedMaterials = [];
this.open = true;
this.title = " 添加OA 需求 ";
},
@@ -504,10 +710,21 @@ export default {
handleUpdate (row) {
this.loading = true;
this.reset();
this.selectedMaterials = [];
const requirementId = row.requirementId || this.ids
getRequirements(requirementId).then(response => {
this.loading = false;
this.form = response.data;
const data = response.data || {} ;
const ids = (data.materialIds || '').split(',').map(s => Number(s)).filter(n => !isNaN(n) && n)
data.materialIdArr = ids
this.form = data;
if (data.materials && data.materials.length) {
this.selectedMaterials = data.materials.slice()
} else if (ids.length) {
// 兜底:逐个拉
Promise.all(ids.map(id => getOaWarehouse(id).then(r => r.data).catch(() => null)))
.then(list => { this.selectedMaterials = list.filter(Boolean) })
}
this.open = true;
this.title = " 修改OA 需求 ";
});
@@ -517,6 +734,8 @@ export default {
this.$refs[" form "].validate(valid => {
if (valid) {
this.buttonLoading = true;
const arr = Array.isArray(this.form.materialIdArr) ? this.form.materialIdArr : []
this.form.materialIds = arr.length ? arr.join(',') : null
if (this.form.requirementId != null) {
updateRequirements(this.form).then(response => {
this.$modal.msgSuccess(" 修改成功 ");
@@ -652,6 +871,67 @@ export default {
cursor : pointer ;
& : hover { color : # 409 eff ; text - decoration : underline ; }
}
. mat - trigger {
display : flex ; align - items : center ; gap : 8 px ;
. mat - trigger - hint { font - size : 12 px ; color : # 909399 ; }
}
. mat - remove {
margin - left : auto ;
cursor : pointer ;
color : # c0c4cc ;
& : hover { color : # f56c6c ; }
}
. picker - toolbar {
display : flex ; align - items : center ; margin - bottom : 10 px ;
. picker - tip { margin - left : auto ; font - size : 12 px ; color : # 606266 ; }
}
. picker - pager { text - align : right ; margin - top : 10 px ; }
. new - mat - card {
margin - bottom : 10 px ;
: : v - deep . el - card _ _header { padding : 6 px 12 px ; background : # f5f7fa ; }
. new - mat - header {
display : flex ; align - items : center ; justify - content : space - between ;
font - size : 13 px ; color : # 303133 ;
}
}
: : v - deep . picker - row - checked > td { background : # ecf5ff ! important ; }
. mat - row {
display : flex ;
align - items : center ;
gap : 8 px ;
font - size : 12 px ;
line - height : 1.6 ;
& + . mat - row { border - top : 1 px dashed # ebeef5 ; }
. mat - name { flex : 1 ; color : # 303133 ; }
. mat - model { color : # 909399 ; margin - left : 2 px ; }
. mat - stock {
flex - shrink : 0 ;
padding : 0 6 px ;
border - radius : 3 px ;
background : # f0f9eb ; color : # 67 c23a ;
& . low { background : # fef0f0 ; color : # f56c6c ; }
}
}
. mat - stock - panel {
margin - top : 4 px ;
padding : 6 px 8 px ;
background : # fafafa ;
border - radius : 3 px ;
. mat - stock - row {
display : flex ; flex - wrap : wrap ; gap : 6 px ;
align - items : center ;
font - size : 12 px ;
line - height : 1.8 ;
& + . mat - stock - row { border - top : 1 px dashed # ebeef5 ; }
}
. mat - stock - name { flex : 1 ; min - width : 120 px ; color : # 303133 ; }
. mat - stock - tag {
padding : 0 6 px ; border - radius : 3 px ;
background : # f0f9eb ; color : # 67 c23a ;
& . low { background : # fef0f0 ; color : # f56c6c ; }
}
. mat - stock - meta { color : # 909399 ; }
}
. accessory - link {
display : inline - block ;
max - width : 160 px ;