Files
klp-oa/klp-ui/src/views/mes/qc/inspectionSample/index.vue
砂糖 212b79be56 feat(qc): 新增检化验申请单相关API,优化检验项模板接口并清理冗余页面
1. 将检验项模板查询接口从POST改为GET请求
2. 新增检化验申请单的增删改查、批量保存检验数据API
3. 删除多个冗余的质检历史、检验项、检验任务页面
2026-07-08 16:49:22 +08:00

488 lines
16 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" :disabled="mainLoading" @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" :disabled="mainLoading" @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="header">
<span class="required-mark">日期</span>
</template>
<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="请先选择所属单位和检验方案" />
</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,
templateList: [],
cascadeData: {},
companyOptions: [],
schemeOptions: [],
selectedCompany: '',
selectedScheme: '',
currentTemplate: null,
checkItems: [],
mainList: [],
mainTotal: 0,
mainQueryParams: {
pageNum: 1,
pageSize: 1000
},
dateRange: []
}
},
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() {
if (this.mainLoading) return
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.mainLoading) return
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() {
const itemMap = {}
if (this.checkItems.length > 0) {
this.checkItems.forEach(item => {
itemMap[item.itemName] = {
detailId: undefined,
mainId: undefined,
itemName: item.itemName,
itemValue: '',
itemUnit: item.unit,
upperLimit: item.targetUpper,
lowerLimit: item.targetLower,
rangeDesc: ''
}
})
}
const newRow = {
_isNew: true,
_items: itemMap,
_originalItems: JSON.parse(JSON.stringify(itemMap)),
inspectionDate: '',
sampleName: '',
sampleNo: '',
batchNo: '',
remark: ''
}
this.mainList.unshift(newRow)
},
handleSaveMain(row) {
if (!row.inspectionDate) {
this.$message.warning('请选择检验日期')
return
}
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)
const requestData = {
sampleName: row.sampleName,
sampleNo: row.sampleNo,
batchNo: row.batchNo,
inspectionDate: row.inspectionDate,
remark: row.remark,
templateId: this.currentTemplate.templateId,
detailList: detailList
}
const isNew = row._isNew || !row.mainId
const apiCall = isNew ? addInspectionMain(requestData) : updateInspectionMain({ ...requestData, mainId: row.mainId })
apiCall.then(() => {
this.$message.success('保存成功')
this.getMainList()
}).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;
}
.required-mark::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
</style>