Files
klp-oa/klp-ui/src/views/mes/qc/inspectionSample/index.vue
Joshi 7cf9f4eca4 feat(qc): 新增批量检验功能并优化数据处理流程
- 添加 BatchInspectionMainBo 和 BatchInspectionTensileBo 数据传输对象
- 集成检验明细列表到 WmsInspectionMainBo 和 WmsInspectionTensileDetailBo
- 在 inspectionMain 控制器中实现批量插入检验主数据接口
- 在 inspectionTensileDetail 控制器中实现批量插入拉伸试验明细接口
- 重构前端检验样品页面,合并主表和明细表的保存操作
- 优化前端拉伸试验页面的批量新增逻辑,统一使用批量插入接口
- 更新服务层实现以支持批量插入并更新申请单关联ID列表
- 移除前端页面中单独的明细表增删改操作接口调用
2026-07-08 13:43:31 +08:00

519 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<div v-loading="filterLoading" style="margin-bottom: 16px; min-height: 50px;">
<div style="display: flex; align-items: center; margin-bottom: 6px;">
<span style="font-size: 13px; color: #909399; white-space: nowrap; margin-right: 12px; width: 68px; text-align: right;">所属单位</span>
<el-radio-group v-model="selectedCompany" size="small" @change="handleCompanyChange">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button v-for="company in companyOptions" :key="company" :label="company">{{ company }}</el-radio-button>
</el-radio-group>
</div>
<div style="display: flex; align-items: center; margin-bottom: 6px;">
<span style="font-size: 13px; color: #909399; white-space: nowrap; margin-right: 12px; width: 68px; text-align: right;">方案名称</span>
<el-radio-group v-model="selectedScheme" size="small" @change="handleSchemeChange">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button v-for="scheme in schemeOptions" :key="scheme" :label="scheme">{{ scheme }}</el-radio-button>
</el-radio-group>
</div>
<div style="display: flex; align-items: center;">
<span style="font-size: 13px; color: #909399; white-space: nowrap; margin-right: 12px; width: 68px; text-align: right;">检验日期</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd HH:mm:ss"
size="small"
style="width: 340px;"
@change="handleDateRangeChange"
/>
</div>
</div>
<div v-if="currentTemplate">
<div class="card-header">
<h3>{{ currentTemplate.templateName }}</h3>
<div>
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddMain">新增</el-button>
<el-button type="info" plain icon="el-icon-refresh" size="mini" @click="getMainList">刷新</el-button>
</div>
</div>
<el-table v-loading="mainLoading" :data="mainList" border stripe class="dynamic-table" fit max-height="calc(100vh - 260px)">
<el-table-column label="日期" align="center" width="150" fixed="left">
<template slot-scope="scope">
<el-date-picker
v-model="scope.row.inspectionDate"
type="date"
value-format="yyyy-MM-dd HH:mm:ss"
size="mini"
class="inspect-input"
style="width: 100%;"
/>
</template>
</el-table-column>
<el-table-column label="样品名称" align="center" width="130" min-width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.sampleName" size="mini" class="inspect-input" />
</template>
</el-table-column>
<el-table-column label="样品编号" align="center" width="130" min-width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.sampleNo" size="mini" class="inspect-input" />
</template>
</el-table-column>
<el-table-column label="来样编号" align="center" width="130" min-width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.batchNo" size="mini" class="inspect-input" />
</template>
</el-table-column>
<el-table-column label="分析项目" align="center">
<el-table-column
v-for="checkItem in checkItems"
:key="checkItem.itemId"
:label="checkItem.itemName"
align="center"
min-width="130"
>
<el-table-column :label="checkItem.unit || ''" align="center" min-width="120">
<template slot-scope="scope">
<el-input
v-if="scope.row._items[checkItem.itemName]"
v-model="scope.row._items[checkItem.itemName].itemValue"
size="mini"
class="inspect-input"
/>
<span v-else>-</span>
</template>
</el-table-column>
</el-table-column>
</el-table-column>
<el-table-column label="备注" align="center" width="150">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" size="mini" class="inspect-input" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" :loading="scope.row._saving" @click="handleSaveMain(scope.row)">保存</el-button>
<el-button type="text" size="mini" style="color: #f56c6c;" @click="handleDeleteMain(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="mainTotal > 0"
:total="mainTotal"
:page.sync="mainQueryParams.pageNum"
:limit.sync="mainQueryParams.pageSize"
@pagination="getMainList"
/>
</div>
<el-empty v-else description="请先选择所属单位和检验方案" />
<el-dialog :title="'新增检验记录'" :visible.sync="addMainDialogVisible" width="600px" append-to-body>
<el-form ref="addMainForm" :model="addMainFormData" label-width="100px">
<el-form-item label="样品名称" prop="sampleName">
<el-input v-model="addMainFormData.sampleName" placeholder="请输入样品名称" />
</el-form-item>
<el-form-item label="样品编号" prop="sampleNo">
<el-input v-model="addMainFormData.sampleNo" placeholder="请输入样品编号" />
</el-form-item>
<el-form-item label="来样编号" prop="batchNo">
<el-input v-model="addMainFormData.batchNo" placeholder="请输入来样编号" />
</el-form-item>
<el-form-item label="检验日期" prop="inspectionDate">
<el-date-picker v-model="addMainFormData.inspectionDate" type="date" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择检验日期" style="width:100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="addMainFormData.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addMainDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAddMain">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listInspectionMain, addInspectionMain, updateInspectionMain, delInspectionMain } from '@/api/mes/qc/inspectionMain'
import { listInspectionDetail } from '@/api/mes/qc/inspectionDetail'
import { listInspectionItemTemplate, getInfoByInspectionItem } from '@/api/mes/qc/inspectionItemTemplate'
export default {
name: 'InspectionSample',
data() {
return {
filterLoading: false,
mainLoading: false,
addMainDialogVisible: false,
templateList: [],
cascadeData: {},
companyOptions: [],
schemeOptions: [],
selectedCompany: '',
selectedScheme: '',
currentTemplate: null,
checkItems: [],
mainList: [],
mainTotal: 0,
mainQueryParams: {
pageNum: 1,
pageSize: 1000
},
dateRange: [],
addMainFormData: {
sampleName: '',
sampleNo: '',
batchNo: '',
inspectionDate: '',
remark: ''
}
}
},
created() {
this.initDateRange()
this.loadCascadeData()
},
methods: {
initDateRange() {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth()
const start = new Date(year, month, 1)
const end = new Date(year, month + 1, 0, 23, 59, 59)
const fmt = d => {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day} 00:00:00`
}
const fmtEnd = d => {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day} 23:59:59`
}
this.dateRange = [fmt(start), fmtEnd(end)]
},
parseTime(time, pattern) {
if (!time) return ''
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
const date = new Date(time)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return format.replace('{y}', year)
.replace('{m}', month.toString().padStart(2, '0'))
.replace('{d}', day.toString().padStart(2, '0'))
.replace('{h}', hour.toString().padStart(2, '0'))
.replace('{i}', minute.toString().padStart(2, '0'))
.replace('{s}', second.toString().padStart(2, '0'))
},
loadCascadeData() {
this.filterLoading = true
listInspectionItemTemplate({ pageNum: 1, pageSize: 9999 }).then(response => {
const templates = response.rows || response.data || []
this.templateList = templates
const cascade = {}
templates.forEach(t => {
const company = t.templateUnit || '未分类'
if (!cascade[company]) {
cascade[company] = []
}
if (t.templateName && !cascade[company].includes(t.templateName)) {
cascade[company].push(t.templateName)
}
})
this.cascadeData = cascade
this.companyOptions = Object.keys(cascade).sort()
const allSchemes = new Set()
Object.values(cascade).forEach(schemes => {
schemes.forEach(s => allSchemes.add(s))
})
this.schemeOptions = [...allSchemes].sort()
}).finally(() => {
this.filterLoading = false
})
},
handleCompanyChange() {
this.selectedScheme = ''
if (this.selectedCompany) {
this.schemeOptions = this.cascadeData[this.selectedCompany] || []
} else {
const allSchemes = new Set()
Object.values(this.cascadeData).forEach(schemes => {
schemes.forEach(s => allSchemes.add(s))
})
this.schemeOptions = [...allSchemes].sort()
}
this.currentTemplate = null
this.mainList = []
},
handleSchemeChange() {
if (!this.selectedScheme) {
this.currentTemplate = null
this.mainList = []
return
}
let template
if (this.selectedCompany) {
template = this.templateList.find(t =>
t.templateName === this.selectedScheme && (t.templateUnit || '未分类') === this.selectedCompany
)
} else {
template = this.templateList.find(t => t.templateName === this.selectedScheme)
}
if (template) {
this.currentTemplate = template
this.mainQueryParams.pageNum = 1
this.loadCheckItems(template)
this.getMainList()
}
},
handleDateRangeChange() {
this.mainQueryParams.pageNum = 1
this.getMainList()
},
loadCheckItems(template) {
if (!template.inspectionItem) {
this.checkItems = []
return
}
getInfoByInspectionItem(template.inspectionItem).then(response => {
this.checkItems = response.data || []
}).catch(() => {
this.checkItems = []
})
},
getMainList() {
if (!this.currentTemplate) return
this.mainLoading = true
const params = {
...this.mainQueryParams,
templateId: this.currentTemplate.templateId
}
if (this.dateRange && this.dateRange.length === 2) {
params.inspectionDateStart = this.dateRange[0]
params.inspectionDateEnd = this.dateRange[1]
}
listInspectionMain(params).then(response => {
this.mainList = (response.rows || []).map(main => {
const itemMap = {}
if (this.checkItems.length > 0) {
this.checkItems.forEach(item => {
itemMap[item.itemName] = {
detailId: undefined,
mainId: main.mainId,
itemName: item.itemName,
itemValue: '',
itemUnit: item.unit,
upperLimit: item.targetUpper,
lowerLimit: item.targetLower,
rangeDesc: ''
}
})
}
return {
...main,
_items: itemMap,
_originalItems: JSON.parse(JSON.stringify(itemMap))
}
})
this.mainTotal = response.total || 0
this.mainLoading = false
this.loadMainDetails()
}).catch(() => {
this.mainLoading = false
})
},
loadMainDetails() {
if (this.mainList.length === 0) return
this.mainList.forEach(main => {
const details = main.detailList || []
details.forEach(detail => {
if (main._items[detail.itemName]) {
main._items[detail.itemName] = {
detailId: detail.detailId,
mainId: detail.mainId,
itemName: detail.itemName,
itemValue: detail.itemValue || '',
itemUnit: detail.itemUnit || main._items[detail.itemName].itemUnit,
upperLimit: detail.upperLimit,
lowerLimit: detail.lowerLimit,
rangeDesc: detail.rangeDesc
}
}
})
main._originalItems = JSON.parse(JSON.stringify(main._items))
})
},
handleAddMain() {
this.addMainFormData = {
sampleName: '',
sampleNo: '',
batchNo: '',
inspectionDate: '',
remark: ''
}
this.addMainDialogVisible = true
},
submitAddMain() {
const data = {
...this.addMainFormData,
templateId: this.currentTemplate.templateId
}
addInspectionMain(data).then(() => {
this.$message.success('新增成功')
this.addMainDialogVisible = false
this.getMainList()
}).catch(() => {
this.$message.error('新增失败')
})
},
handleSaveMain(row) {
row._saving = true
const detailList = this.checkItems.map(checkItem => {
const itemData = row._items[checkItem.itemName]
if (!itemData || (!itemData.itemValue && !itemData.detailId)) return null
return {
detailId: itemData.detailId,
mainId: row.mainId,
itemName: itemData.itemName,
itemValue: itemData.itemValue || '',
itemUnit: itemData.itemUnit || '',
upperLimit: itemData.upperLimit,
lowerLimit: itemData.lowerLimit,
rangeDesc: itemData.rangeDesc || ''
}
}).filter(Boolean)
updateInspectionMain({
mainId: row.mainId,
sampleName: row.sampleName,
sampleNo: row.sampleNo,
batchNo: row.batchNo,
inspectionDate: row.inspectionDate,
remark: row.remark,
detailList: detailList
}).then(() => {
this.$message.success('保存成功')
return listInspectionDetail({ mainId: row.mainId, pageNum: 1, pageSize: 9999 })
}).then(response => {
const details = response.rows || []
if (this.checkItems.length > 0) {
this.checkItems.forEach(item => {
const found = details.find(d => d.itemName === item.itemName)
if (found) {
row._items[item.itemName] = {
detailId: found.detailId,
mainId: found.mainId,
itemName: found.itemName,
itemValue: found.itemValue || '',
itemUnit: found.itemUnit || item.unit,
upperLimit: found.upperLimit,
lowerLimit: found.lowerLimit,
rangeDesc: found.rangeDesc
}
}
})
}
row._originalItems = JSON.parse(JSON.stringify(row._items))
row._saving = false
}).catch(() => {
this.$message.error('保存失败')
row._saving = false
})
},
handleDeleteMain(row) {
this.$confirm('是否确认删除该检验记录?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delInspectionMain(row.mainId).then(() => {
this.$message.success('删除成功')
this.getMainList()
}).catch(() => {
this.$message.error('删除失败')
})
})
}
}
}
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.dynamic-table {
width: 100%;
overflow-x: auto;
}
::v-deep .dynamic-table .el-table__header-wrapper {
background-color: #f0f5ff;
}
::v-deep .dynamic-table .el-table__header-wrapper th {
background-color: #f0f5ff;
}
::v-deep .inspect-input .el-input__inner {
border: 1px solid #e4e7ed;
border-radius: 2px;
text-align: center;
background-color: #fff;
padding: 0 4px;
height: 24px;
line-height: 24px;
font-size: 13px;
}
::v-deep .inspect-input.is-focus .el-input__inner {
border-color: #409eff;
}
::v-deep .dynamic-table .el-table__cell {
padding: 2px 0;
}
::v-deep .dynamic-table .el-table__header-wrapper th {
padding: 4px 0;
font-size: 13px;
}
::v-deep .dynamic-table .el-table__body-wrapper {
overflow-x: auto;
}
</style>