|
|
<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>
|