You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1106 lines
28 KiB

<template>
<!-- 原材料清单组件 -->
<div class="raw-material-container">
<!-- 工具栏 -->
<div class="toolbar-row">
<div class="toolbar-left">
<el-button
type="primary"
size="small"
class="add-btn"
v-if="canEdit"
@click="openAddDialog">
新增物料
</el-button>
<el-button
type="primary"
size="small"
class="reset-btn"
v-if="canEdit"
:disabled="selectedRows.length === 0"
@click="batchDeleteRawMaterial">
批量删除
</el-button>
<span
v-if="!disabled && !canEdit"
style="margin-left: 8px; color: #909399; font-size: 12px;">
仅试验负责人可维护原材料清单
</span>
<span
v-if="canEdit && shouldRecordChangeLog"
style="margin-left: 8px; color: #E6A23C; font-size: 12px;">
当前为非草稿状态:原材料增删改将自动记录详细修改日志,可点击右上角“修改记录”查看
</span>
</div>
<div class="toolbar-right" v-if="applyNo">
<el-tooltip content="查看原材料修改记录" placement="top">
<el-badge
:value="changeLogList.length"
:max="99"
:hidden="changeLogList.length === 0"
class="change-log-badge">
<el-button
type="warning"
plain
size="small"
icon="el-icon-tickets"
@click="openChangeLogDrawer">
修改记录
</el-button>
</el-badge>
</el-tooltip>
</div>
</div>
<!-- 数据表格 -->
<el-table
ref="rawMaterialTable"
:key="`raw-material-${applyNo || 'empty'}-${canEdit ? 'edit' : 'view'}`"
:data="rawMaterialList"
v-loading="tableLoading"
border
:height="260"
@selection-change="handleSelectionChange"
style="width: 100%">
<!-- 多选框 -->
<el-table-column
type="selection"
width="55"
align="center"
v-if="canEdit">
</el-table-column>
<!-- 序号 -->
<el-table-column
type="index"
label="序号"
width="60"
align="center">
</el-table-column>
<!-- 物料编码 -->
<el-table-column
prop="partNo"
label="物料编码"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.partNo || '-' }}
</template>
</el-table-column>
<!-- 物料描述 -->
<el-table-column
prop="partDesc"
label="物料描述"
min-width="200"
align="center"
header-align="center"
show-overflow-tooltip>
</el-table-column>
<!-- 工序 -->
<el-table-column
prop="processStep"
label="工序"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.processStep || '-' }}
</template>
</el-table-column>
<!-- 数量 -->
<el-table-column
prop="quantity"
label="数量"
width="120"
align="center"
header-align="center">
</el-table-column>
<!-- 单位 -->
<el-table-column
prop="umid"
label="单位"
width="80"
align="center"
header-align="center">
<template slot-scope="scope">
{{ scope.row.umid || '-' }}
</template>
</el-table-column>
<!-- 备注 -->
<el-table-column
prop="remark"
label="备注"
min-width="150"
align="left"
header-align="center"
show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.remark || '-' }}
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
label="操作"
width="150"
align="center"
header-align="center"
v-if="canEdit">
<template slot-scope="scope">
<el-link
style="cursor:pointer; margin-right: 10px;"
@click="openEditDialog(scope.row)">
修改
</el-link>
<el-link
style="cursor:pointer; color: #F56C6C;"
@click="deleteRawMaterial(scope.row)">
删除
</el-link>
</template>
</el-table-column>
</el-table>
<!-- 原材料修改记录抽屉 -->
<el-drawer
title="原材料修改记录"
:visible.sync="changeLogDrawerVisible"
:append-to-body="true"
size="55%">
<div class="change-log-drawer-body">
<div class="change-log-drawer-toolbar">
<span class="change-log-count">共 {{ changeLogList.length }} 条</span>
<el-button
type="text"
size="small"
:loading="changeLogLoading"
@click="loadChangeLogList">
刷新
</el-button>
</div>
<el-table
:data="changeLogList"
v-loading="changeLogLoading"
border
size="small"
class="change-log-table"
height="68vh"
style="width: 100%">
<el-table-column type="expand" width="50">
<template slot-scope="scope">
<div class="log-detail-wrapper">
<div class="log-detail-item">
<span class="log-detail-label">详细说明:</span>
<pre class="log-detail-pre">{{ scope.row.detailContent || '-' }}</pre>
</div>
<!-- <div class="log-detail-item" v-if="scope.row.beforeContent">
<span class="log-detail-label">修改前快照:</span>
<pre class="log-detail-pre">{{ scope.row.beforeContent }}</pre>
</div>
<div class="log-detail-item" v-if="scope.row.afterContent">
<span class="log-detail-label">修改后快照:</span>
<pre class="log-detail-pre">{{ scope.row.afterContent }}</pre>
</div>-->
</div>
</template>
</el-table-column>
<el-table-column
prop="createdDate"
label="时间"
width="160"
align="center"
header-align="center">
<template slot-scope="scope">
{{ formatDateTime(scope.row.createdDate) }}
</template>
</el-table-column>
<el-table-column
prop="operationType"
label="操作类型"
width="90"
align="center"
class-name="operation-type-cell"
header-align="center">
<template slot-scope="scope">
<span
:class="['operation-type-pill', getOperationTypeClass(scope.row.operationType)]">
{{ formatOperationType(scope.row.operationType) || '-' }}
</span>
</template>
</el-table-column>
<el-table-column
prop="operatorDisplayName"
label="操作人"
width="120"
align="center"
header-align="center">
<template slot-scope="scope">
{{ scope.row.operatorDisplayName || scope.row.operatorUserName || '-' }}
</template>
</el-table-column>
<el-table-column
prop="applyStatus"
label="单据状态"
width="90"
align="center"
header-align="center">
</el-table-column>
<el-table-column
prop="operationDescDisplay"
label="操作摘要"
min-width="240"
show-overflow-tooltip>
</el-table-column>
</el-table>
</div>
</el-drawer>
<!-- 新增/编辑弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="420px"
append-to-body
:close-on-click-modal="false">
<el-form :model="formData" label-width="80px" size="small">
<el-form-item label="工序" required>
<el-select
v-model="formData.processStep"
placeholder="请选择工序"
filterable
clearable
:loading="processOptionsLoading"
style="width: 100%">
<el-option
v-for="item in processOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="物料编码">
<el-input
v-model="formData.partNo"
placeholder="请输入物料编码(可为空)"
clearable
@blur="handlePartNoBlur"
@keyup.enter.native="handlePartNoBlur">
</el-input>
<div style="color: #909399; font-size: 12px; margin-top: -10px;">
输入物料编码后按回车或失去焦点,自动查询物料描述
</div>
</el-form-item>
<el-form-item label="物料描述" required>
<el-input
v-model="formData.partDesc"
placeholder="请输入物料描述(必填)"
clearable>
</el-input>
</el-form-item>
<el-form-item label="数量" required>
<el-input
v-model="formData.quantity"
:precision="2"
:min="0.01"
:controls="true"
style="width: 100%">
</el-input>
</el-form-item>
<el-form-item label="单位">
<el-input
v-model="formData.umid"
placeholder="输入物料编码后自动带出"
clearable>
</el-input>
</el-form-item>
<el-form-item label="备注">
<el-input
type="textarea"
v-model="formData.remark"
placeholder="请输入备注"
:autosize="{minRows: 2, maxRows: 3}"
clearable>
</el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer" style="margin-top: 10px">
<el-button type="primary" @click="saveRawMaterial" :loading="saveLoading">
{{ saveLoading ? '保存中...' : '保存' }}
</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getRawMaterialList, getRawMaterialChangeLogList, saveRawMaterial, deleteRawMaterial, batchDeleteRawMaterial, getPartDescByPartNo } from '@/api/erf/erf'
import { searchStandardRoutingOperationList } from '@/api/part/standardRoutingOperation'
export default {
name: 'ExpRawMaterialList',
props: {
// 试验单号
applyNo: {
type: String,
required: true
},
// 工厂编码
site: {
type: String,
default: ''
},
// buNo
buNo: {
type: String,
default: ''
},
// 当前申请单状态
applyStatus: {
type: String,
default: ''
},
// 试验负责人(显示名)
projectLeader: {
type: String,
default: ''
},
// 试验负责人(用户名)
projectLeaderName: {
type: String,
default: ''
},
// 是否禁用编辑
disabled: {
type: Boolean,
default: false
},
},
data() {
return {
// 原材料清单数据
rawMaterialList: [],
// 原材料修改记录
changeLogList: [],
// 表格加载状态
tableLoading: false,
// 修改记录加载状态
changeLogLoading: false,
// 修改记录抽屉
changeLogDrawerVisible: false,
// 已选中的行
selectedRows: [],
// 弹窗显示状态
dialogVisible: false,
// 弹窗标题
dialogTitle: '新增物料',
// 表单数据
formData: {
id: null,
applyNo: '',
site: '',
buNo: '',
processStep: '',
partNo: '',
partDesc: '',
quantity: '',
umid: '',
remark: ''
},
// 保存加载状态
saveLoading: false,
// 标准工序下拉选项
processOptions: [],
// 标准工序加载状态
processOptionsLoading: false
}
},
mounted() {
this.loadRawMaterialList()
this.loadChangeLogList()
},
watch: {
applyNo(newVal) {
if (newVal) {
this.loadRawMaterialList()
this.loadChangeLogList()
} else {
this.rawMaterialList = []
this.selectedRows = []
this.changeLogList = []
this.changeLogDrawerVisible = false
}
},
buNo(newVal, oldVal) {
if (newVal !== oldVal) {
if (this.canEdit) {
this.loadProcessOptions()
} else {
this.processOptions = []
}
}
},
canEdit(newVal) {
if (newVal) {
this.loadProcessOptions()
} else {
this.processOptions = []
}
this.selectedRows = []
this.refreshRawMaterialTableLayout()
}
},
computed: {
/**
* 是否可编辑原材料清单(仅试验负责人)
*/
canEdit() {
if (this.disabled) {
return false
}
const currentUserName = (this.$store.state.user.name || '').trim()
const currentUserDisplay = (this.$store.state.user.userDisplay || '').trim()
const leaderList = [this.projectLeaderName, this.projectLeader]
.filter(item => item && item.trim())
.map(item => item.trim())
if (leaderList.length === 0) {
return false
}
return leaderList.some(item => item === currentUserName || item === currentUserDisplay)
},
/**
* 非草稿状态下需记录详细修改日志
*/
shouldRecordChangeLog() {
return !!this.applyStatus && this.applyStatus !== '草稿'
}
},
methods: {
/**
* 加载原材料清单列表
*/
loadRawMaterialList() {
if (!this.applyNo) {
return
}
this.tableLoading = true
getRawMaterialList({ applyNo: this.applyNo }).then(({data}) => {
this.tableLoading = false
if (data && data.code === 0) {
this.rawMaterialList = data.list || []
this.refreshRawMaterialTableLayout()
} else {
this.rawMaterialList = []
this.$message.error(data.msg || '查询原材料清单失败')
}
}).catch(error => {
this.tableLoading = false
this.$message.error('查询原材料清单异常')
})
},
/**
* 刷新原材料表格布局,避免列结构切换后错位
*/
refreshRawMaterialTableLayout() {
this.$nextTick(() => {
if (this.$refs.rawMaterialTable && this.$refs.rawMaterialTable.doLayout) {
this.$refs.rawMaterialTable.doLayout()
}
})
},
/**
* 加载原材料修改记录
*/
loadChangeLogList() {
if (!this.applyNo) {
this.changeLogList = []
return
}
this.changeLogLoading = true
getRawMaterialChangeLogList({ applyNo: this.applyNo }).then(({data}) => {
this.changeLogLoading = false
if (data && data.code === 0) {
const logList = data.list || []
this.changeLogList = logList.map(item => {
const rowData = item || {}
return Object.assign({}, rowData, {
operationDescDisplay: this.buildOperationDescDisplay(rowData)
})
})
} else {
this.changeLogList = []
this.$message.error(data.msg || '查询原材料修改记录失败')
}
}).catch(() => {
this.changeLogLoading = false
this.changeLogList = []
this.$message.error('查询原材料修改记录异常')
})
},
/**
* 打开修改记录抽屉
*/
openChangeLogDrawer() {
this.changeLogDrawerVisible = true
this.loadChangeLogList()
},
/**
* 按BU加载标准工序下拉
*/
loadProcessOptions() {
if (!this.buNo) {
this.processOptions = []
return
}
this.processOptionsLoading = true
const queryData = {
userName: this.$store.state.user.name,
site: this.site || this.$store.state.user.site,
buNo: this.buNo,
page: 1,
limit: 500
}
searchStandardRoutingOperationList(queryData).then(({data}) => {
this.processOptionsLoading = false
if (data && data.code === 0) {
const list = (data.page && data.page.list) ? data.page.list : []
const optionMap = {}
list.forEach(item => {
const processName = item.operationName ? item.operationName.trim() : ''
if (!processName) {
return
}
if (!optionMap[processName]) {
const hasOperationNo = item.operationNo !== null && item.operationNo !== undefined && item.operationNo !== ''
optionMap[processName] = {
value: processName,
label: hasOperationNo ? `${item.operationNo} - ${processName}` : processName
}
}
})
this.processOptions = Object.values(optionMap)
} else {
this.processOptions = []
this.$message.error(data.msg || '加载标准工序失败')
}
}).catch(() => {
this.processOptionsLoading = false
this.processOptions = []
this.$message.error('加载标准工序异常')
})
},
/**
* 编辑场景兜底:已选工序不在下拉时补充显示
*/
ensureProcessOption(processStep) {
if (!processStep) {
return
}
const exists = this.processOptions.some(item => item.value === processStep)
if (!exists) {
this.processOptions.push({
value: processStep,
label: processStep
})
}
},
/**
* 打开新增弹窗
*/
openAddDialog() {
this.loadProcessOptions()
this.dialogTitle = '新增物料'
this.formData = {
id: null,
applyNo: this.applyNo,
site: this.site || this.$store.state.user.site,
buNo: this.buNo,
processStep: '',
partNo: '',
partDesc: '',
quantity: '',
umid: '',
remark: ''
}
this.dialogVisible = true
},
/**
* 打开编辑弹窗
*/
openEditDialog(row) {
this.dialogTitle = '修改物料'
this.formData = {
id: row.id,
applyNo: row.applyNo,
site: row.site,
buNo: this.buNo,
processStep: row.processStep || '',
partNo: row.partNo,
partDesc: row.partDesc,
quantity: row.quantity,
umid: row.umid || '',
remark: row.remark
}
this.ensureProcessOption(this.formData.processStep)
this.dialogVisible = true
},
/**
* 保存物料
*/
saveRawMaterial() {
// 数据验证
if (!this.formData.partDesc) {
this.$message.warning('请输入物料描述')
return
}
if (!this.formData.processStep) {
this.$message.warning('请选择工序')
return
}
const qty = Number(this.formData.quantity)
if (isNaN(qty) || qty <= 0) {
this.$message.warning('请输入有效数字(必须大于0)')
return
}
this.saveLoading = true
// 保存数据
const saveData = {
id: this.formData.id,
applyNo: this.formData.applyNo,
site: this.formData.site,
processStep: this.formData.processStep,
partNo: this.formData.partNo || null,
partDesc: this.formData.partDesc,
quantity: this.formData.quantity,
umid: this.formData.umid || null,
remark: this.formData.remark
}
saveRawMaterial(saveData).then(({data}) => {
this.saveLoading = false
if (data && data.code === 0) {
this.$message.success('保存成功')
this.dialogVisible = false
this.loadRawMaterialList()
this.loadChangeLogList()
} else {
this.$message.error(data.msg || '保存失败')
}
}).catch(error => {
this.saveLoading = false
this.$message.error('保存异常')
})
},
/**
* 删除物料
*/
deleteRawMaterial(row) {
this.$confirm('确定删除该物料记录?', '操作提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteRawMaterial({ id: row.id }).then(({data}) => {
if (data && data.code === 0) {
this.$message.success('删除成功')
this.loadRawMaterialList()
this.loadChangeLogList()
} else {
this.$message.error(data.msg || '删除失败')
}
}).catch(error => {
this.$message.error('删除异常')
})
})
},
/**
* 批量删除物料
*/
batchDeleteRawMaterial() {
if (this.selectedRows.length === 0) {
this.$message.warning('请先选择要删除的物料')
return
}
this.$confirm(`确定删除选中的 ${this.selectedRows.length} 条物料记录?`, '操作提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const ids = this.selectedRows.map(row => row.id)
batchDeleteRawMaterial({ ids: ids }).then(({data}) => {
if (data && data.code === 0) {
this.$message.success('删除成功')
this.loadRawMaterialList()
this.loadChangeLogList()
this.selectedRows = []
} else {
this.$message.error(data.msg || '删除失败')
}
}).catch(error => {
this.$message.error('删除异常')
})
})
},
/**
* 物料编码失去焦点或回车时查询part表
*/
handlePartNoBlur() {
const partNo = this.formData.partNo
if (!partNo || !partNo.trim()) {
return
}
// 查询part表获取物料描述
getPartDescByPartNo({
partNo: partNo.trim(),
site: this.formData.site,
buNo: ''
}).then(({data}) => {
if (data && data.code === 0 && data.partDesc) {
this.formData.partDesc = data.partDesc
this.formData.umid = data.umid || ''
} else {
this.$message.warning('未找到该物料编码,请手动填写物料描述')
}
}).catch(error => {
console.error('查询物料描述异常:', error)
})
},
/**
* 表格多选变化
*/
handleSelectionChange(selection) {
this.selectedRows = selection
},
/**
* 格式化时间
*/
formatDateTime(dateValue) {
if (!dateValue) {
return '-'
}
if (typeof dateValue === 'string') {
return dateValue
}
const date = new Date(dateValue)
if (isNaN(date.getTime())) {
return '-'
}
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const hh = String(date.getHours()).padStart(2, '0')
const mm = String(date.getMinutes()).padStart(2, '0')
const ss = String(date.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
},
/**
* 操作类型对应样式
*/
getOperationTypeClass(operationType) {
const operation = this.formatOperationType(operationType)
const classMap = {
'新增': 'operation-type-add',
'删除': 'operation-type-delete',
'修改': 'operation-type-edit'
}
return classMap[operation] || 'operation-type-default'
},
/**
* 标准化操作类型
*/
formatOperationType(operationType) {
return operationType ? String(operationType).trim() : ''
},
/**
* 生成摘要展示文本(兼容历史“仅字段名”日志)
*/
buildOperationDescDisplay(logItem) {
const rowData = logItem || {}
const operationDesc = rowData.operationDesc ? String(rowData.operationDesc).trim() : ''
const operationType = this.formatOperationType(rowData.operationType)
const detailDesc = this.buildOperationDescFromDetail(rowData.detailContent)
if (operationType === '修改' && detailDesc) {
return detailDesc
}
return operationDesc || detailDesc || '-'
},
/**
* 从详细说明提取“旧值 -> 新值”明细并拼成摘要
*/
buildOperationDescFromDetail(detailContent) {
if (!detailContent) {
return ''
}
const diffList = String(detailContent)
.split('\n')
.map(item => item.trim())
.filter(item => /^\d+\.\s+.+\s+->\s+.+$/.test(item))
.map(item => item.replace(/^\d+\.\s*/, ''))
if (diffList.length === 0) {
return ''
}
return `非草稿状态修改原材料,变更明细:${diffList.join(';')}`
}
}
}
</script>
<style scoped>
.raw-material-container {
padding: 10px;
background-color: #ffffff;
}
.toolbar-row {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.toolbar-left {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.toolbar-right {
display: flex;
align-items: center;
flex-shrink: 0;
}
.change-log-badge {
margin-right: 2px;
}
.change-log-drawer-body {
padding: 0 16px 12px 16px;
}
.change-log-drawer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.change-log-count {
color: #606266;
font-size: 13px;
font-weight: 600;
}
/* 覆盖全局 .el-table .cell 固定14px,避免操作类型被上下裁切 */
.change-log-table >>> td.operation-type-cell .cell {
height: auto !important;
line-height: 20px !important;
overflow: visible !important;
padding-top: 2px;
padding-bottom: 2px;
}
.operation-type-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 20px;
line-height: 16px;
padding: 0 8px;
box-sizing: border-box;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
.operation-type-add {
background: #f0f9eb;
color: #67c23a;
border: 1px solid #c2e7b0;
}
.operation-type-delete {
background: #fef0f0;
color: #f56c6c;
border: 1px solid #f5bcbc;
}
.operation-type-edit {
background: #fdf6ec;
color: #e6a23c;
border: 1px solid #f5dab1;
}
.operation-type-default {
background: #f4f4f5;
color: #909399;
border: 1px solid #e9e9eb;
}
.log-detail-wrapper {
padding: 8px 12px;
background: #fafafa;
}
.log-detail-item {
margin-bottom: 8px;
}
.log-detail-item:last-child {
margin-bottom: 0;
}
.log-detail-label {
display: inline-block;
margin-bottom: 4px;
color: #606266;
font-size: 12px;
font-weight: 600;
}
.log-detail-pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 2px;
padding: 6px 8px;
color: #606266;
font-size: 12px;
line-height: 1.5;
}
/* 按钮样式 - 与附件上传保持一致 */
.add-btn {
background-color: #F0F9FF;
border-color: #C0E6C7;
color: #67C23A;
}
.add-btn:hover:not(:disabled) {
background-color: #67C23A;
border-color: #67C23A;
color: #FFFFFF;
}
.reset-btn {
background-color: #FEF0F0;
border-color: #FAB6B6;
color: #F56C6C;
}
.reset-btn:hover:not(:disabled) {
background-color: #F56C6C;
border-color: #F56C6C;
color: #FFFFFF;
}
.reset-btn:hover:disabled {
background-color: #FEF0F0;
border-color: #FAB6B6;
color: #F56C6C;
}
.reset-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 表格样式 */
.el-table >>> .el-table__header-wrapper th {
background-color: #F5F7FA;
color: #606266;
font-weight: 600;
font-size: 13px;
}
.el-table >>> .el-table__body-wrapper td {
font-size: 13px;
color: #606266;
}
/* 弹窗表单样式 */
.dialog-footer {
text-align: center;
}
</style>