757 lines
29 KiB
Vue
757 lines
29 KiB
Vue
<template>
|
||
<div class="aps-sheet-page excel-theme">
|
||
<el-card class="sheet-header" shadow="never">
|
||
<div class="sheet-header-row">
|
||
<div class="sheet-actions">
|
||
<el-button size="small" icon="el-icon-back" @click="$router.back()">返回</el-button>
|
||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||
<el-button type="success" size="small" icon="el-icon-check" :loading="saving" @click="handleSaveSupplement">保存补录</el-button>
|
||
<el-button size="small" icon="el-icon-download" @click="handleExport">导出 Excel</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-form :inline="true" size="small" :model="query" class="sheet-form">
|
||
<el-form-item label="模板">
|
||
<el-input v-model="currentTemplate.name" disabled style="width: 220px" />
|
||
</el-form-item>
|
||
<el-form-item label="机组/产线">
|
||
<el-select v-model="query.lineId" clearable filterable placeholder="全部">
|
||
<el-option
|
||
v-for="line in lineOptions"
|
||
:key="line.lineId"
|
||
:label="line.lineName || ('产线' + line.lineId)"
|
||
:value="line.lineId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="计划编号">
|
||
<el-input v-model="query.planId" placeholder="计划号/计划编码(可选)" clearable style="width: 220px" />
|
||
</el-form-item>
|
||
<el-form-item label="订单编号">
|
||
<el-input v-model="query.orderId" placeholder="订单号/订单编码(可选)" clearable style="width: 220px" />
|
||
</el-form-item>
|
||
<el-form-item label="时间范围">
|
||
<el-date-picker
|
||
v-model="query.range"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
range-separator="至"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
:default-time="['00:00:00', '23:59:59']"
|
||
style="width: 380px"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-card class="sheet-body" shadow="never">
|
||
<div class="sheet-meta" v-if="sheet.header">
|
||
<div class="meta-item"><span class="k">计划号:</span><span class="v">{{ sheet.header.planCode || '-' }}</span></div>
|
||
<div class="meta-item"><span class="k">机组:</span><span class="v">{{ sheet.header.lineName || '-' }}</span></div>
|
||
<div class="meta-item"><span class="k">日期:</span><span class="v">{{ sheet.header.dateText || '-' }}</span></div>
|
||
<div class="meta-item"><span class="k">说明:</span><span class="v">点击“选择原料钢卷”弹窗按库区选卷</span></div>
|
||
<div class="meta-item meta-item-warn"><span class="k">补录提示:</span><span class="v">原料钢卷、原料卷号、钢卷位置、包装要求、切边要求、镀层种类为优先补录项,已前置展示;已有值也可直接修改,修改后自动保存。</span></div>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="sheet.rows"
|
||
border
|
||
size="mini"
|
||
:height="tableHeight"
|
||
class="excel-table"
|
||
>
|
||
<el-table-column
|
||
v-for="col in (currentTemplate.columns || [])"
|
||
:key="col.prop || col.label"
|
||
:label="col.label"
|
||
:prop="col.prop"
|
||
:width="col.width"
|
||
:min-width="col.minWidth"
|
||
:align="col.align || 'center'"
|
||
:show-overflow-tooltip="col.showOverflowTooltip !== false"
|
||
>
|
||
<template slot-scope="scope">
|
||
<template v-if="col.prop === 'rawMaterialId'">
|
||
<el-button
|
||
size="mini"
|
||
type="text"
|
||
class="coil-picker-btn"
|
||
@click="openCoilPicker(scope.row)"
|
||
>
|
||
{{ scope.row.rawCoilNos || '选择原料钢卷' }}
|
||
</el-button>
|
||
</template>
|
||
|
||
<template v-else-if="isEditableColumn(col.prop)">
|
||
<el-input
|
||
v-model="scope.row[col.prop]"
|
||
size="mini"
|
||
clearable
|
||
class="excel-cell-input"
|
||
:placeholder="`请输入${col.label}`"
|
||
@change="onCellEdit(scope.row, col.prop)"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else>
|
||
{{ scope.row[col.prop] == null || scope.row[col.prop] === '' ? '-' : scope.row[col.prop] }}
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="sheet-summary" v-if="summaryText">
|
||
<span class="k">合计:</span>
|
||
<span class="v">{{ summaryText }}</span>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-dialog title="选择原料钢卷" :visible.sync="coilPicker.visible" width="1280px" append-to-body>
|
||
<el-form :inline="true" size="small" class="coil-picker-form">
|
||
<el-form-item label="库区">
|
||
<el-select v-model="coilPicker.selectedWarehouseId" clearable filterable placeholder="全部库区" style="width: 220px" @change="onCoilFilterChange">
|
||
<el-option
|
||
v-for="w in coilPicker.warehouseOptions"
|
||
:key="w.actualWarehouseId"
|
||
:label="w.actualWarehouseName"
|
||
:value="w.actualWarehouseId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="入场钢卷号">
|
||
<el-input v-model="coilPicker.enterCoilNo" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
|
||
</el-form-item>
|
||
<el-form-item label="当前钢卷号">
|
||
<el-input v-model="coilPicker.currentCoilNo" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
|
||
</el-form-item>
|
||
<el-form-item label="厂家">
|
||
<el-input v-model="coilPicker.manufacturer" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button size="mini" type="primary" @click="searchCoils">查询</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div class="coil-stack-wrap" v-loading="coilPicker.loading">
|
||
<el-empty v-if="!coilColumnKeys.length" description="暂无可选原料钢卷" :image-size="60" />
|
||
<div v-else class="coil-bird-wrap">
|
||
<div class="coil-legend">
|
||
<span class="legend-item"><i class="dot layer1" />一层</span>
|
||
<span class="legend-item"><i class="dot layer2" />二层</span>
|
||
<span class="legend-item"><i class="dot occupied" />已占用</span>
|
||
</div>
|
||
<div class="coil-grid-scroll">
|
||
<div class="coil-col-ruler">
|
||
<div class="ruler-empty" />
|
||
<div v-for="col in coilColumnKeys" :key="`ruler-${col}`" class="ruler-col">{{ col }}</div>
|
||
</div>
|
||
<div class="coil-grid-main">
|
||
<div class="coil-row-ruler">
|
||
<div v-for="row in coilMaxRow" :key="`row-${row}`" class="ruler-row">{{ row }}</div>
|
||
</div>
|
||
<div class="coil-grid-columns">
|
||
<div v-for="col in coilColumnKeys" :key="`col-${col}`" class="coil-col-pair">
|
||
<div class="coil-layer">
|
||
<div
|
||
v-for="row in coilMaxRow"
|
||
:key="`l1-${col}-${row}`"
|
||
class="coil-cell layer1"
|
||
:class="{ occupied: !!(getCoilCell(col, row, 1) && getCoilCell(col, row, 1).rawMaterialId) }"
|
||
@click="selectCoilCell(col, row, 1)"
|
||
>
|
||
<template v-if="getCoilCell(col, row, 1)">
|
||
<div class="line1" :title="getCoilCell(col, row, 1).rawLocation">{{ getCoilCell(col, row, 1).rawLocation }}</div>
|
||
<div class="line2" :title="getCoilCell(col, row, 1).rawMaterialCode">{{ getCoilCell(col, row, 1).rawMaterialCode || '-' }}</div>
|
||
</template>
|
||
<template v-else>-</template>
|
||
</div>
|
||
</div>
|
||
<div class="coil-layer layer2-shift">
|
||
<div
|
||
v-for="row in (coilMaxRow - 1)"
|
||
:key="`l2-${col}-${row}`"
|
||
class="coil-cell layer2"
|
||
:class="{ occupied: !!(getCoilCell(col, row, 2) && getCoilCell(col, row, 2).rawMaterialId) }"
|
||
@click="selectCoilCell(col, row, 2)"
|
||
>
|
||
<template v-if="getCoilCell(col, row, 2)">
|
||
<div class="line1" :title="getCoilCell(col, row, 2).rawLocation">{{ getCoilCell(col, row, 2).rawLocation }}</div>
|
||
<div class="line2" :title="getCoilCell(col, row, 2).rawMaterialCode">{{ getCoilCell(col, row, 2).rawMaterialCode || '-' }}</div>
|
||
</template>
|
||
<template v-else>-</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="coil-picker-foot">共 {{ coilPicker.total || 0 }} 条钢卷(按库位分布展示)</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { APS_SHEET_TEMPLATES, getTemplateByKey } from './sheets/templates'
|
||
import { listProductionLine } from '@/api/wms/productionLine'
|
||
import { getMaterialCoilLocationGrid } from '@/api/wms/coil'
|
||
import { treeActualWarehouseTwoLevel } from '@/api/wms/actualWarehouse'
|
||
import { fetchScheduleSheet, exportScheduleSheet, saveScheduleSheetSupplement } from '@/api/aps/aps'
|
||
import { parseTime } from '@/utils/klp'
|
||
import { saveAs } from 'file-saver'
|
||
|
||
export default {
|
||
name: 'ApsScheduleSheet',
|
||
data() {
|
||
return {
|
||
templates: APS_SHEET_TEMPLATES,
|
||
lineOptions: [],
|
||
rawMaterialOptions: [],
|
||
loading: false,
|
||
saving: false,
|
||
autoSaveTimer: null,
|
||
pendingAutoSave: false,
|
||
dirtyOperationIds: [],
|
||
baselineByOperationId: {},
|
||
tableHeight: 520,
|
||
coilPicker: {
|
||
visible: false,
|
||
loading: false,
|
||
currentRow: null,
|
||
selectedWarehouseId: undefined,
|
||
enterCoilNo: '',
|
||
currentCoilNo: '',
|
||
manufacturer: '',
|
||
warehouseOptions: [],
|
||
groupedCoils: [],
|
||
coilGrid: {},
|
||
pageNum: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
},
|
||
query: {
|
||
templateKey: 'unified',
|
||
lineId: null,
|
||
planId: '',
|
||
orderId: '',
|
||
range: []
|
||
},
|
||
sheet: {
|
||
header: null,
|
||
rows: [],
|
||
summary: null
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
currentTemplate() {
|
||
return getTemplateByKey(this.query.templateKey)
|
||
},
|
||
summaryText() {
|
||
const s = this.sheet && this.sheet.summary
|
||
if (!s) return ''
|
||
const parts = []
|
||
if (s.totalPieceCount != null) parts.push(`件数 ${s.totalPieceCount}`)
|
||
if (s.totalWeightTon != null) parts.push(`重量 ${s.totalWeightTon}`)
|
||
return parts.join(',')
|
||
},
|
||
coilColumnKeys() {
|
||
const keys = Object.keys(this.coilPicker.coilGrid || {})
|
||
return keys.map(v => Number(v)).filter(v => !Number.isNaN(v)).sort((a, b) => a - b)
|
||
},
|
||
coilMaxRow() {
|
||
let max = 0
|
||
Object.values(this.coilPicker.coilGrid || {}).forEach(col => {
|
||
const l1 = col.layer1 || []
|
||
const l2 = col.layer2 || []
|
||
;[...l1, ...l2].forEach(item => {
|
||
const row = Number(item._row || 0)
|
||
if (row > max) max = row
|
||
})
|
||
})
|
||
return max || 1
|
||
}
|
||
},
|
||
created() {
|
||
this.tableHeight = document.documentElement.clientHeight - 280
|
||
this.initDefaultRange()
|
||
Promise.all([this.loadLines(), this.loadRawMaterials()]).finally(() => {
|
||
this.applyRouteQuery()
|
||
this.handleQuery()
|
||
})
|
||
},
|
||
beforeDestroy() {
|
||
if (this.autoSaveTimer) {
|
||
clearTimeout(this.autoSaveTimer)
|
||
this.autoSaveTimer = null
|
||
}
|
||
},
|
||
methods: {
|
||
initDefaultRange() {
|
||
const now = new Date()
|
||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
|
||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7, 23, 59, 59)
|
||
this.query.range = [parseTime(start), parseTime(end)]
|
||
},
|
||
loadLines() {
|
||
return listProductionLine({ pageNum: 1, pageSize: 999 }).then(res => {
|
||
this.lineOptions = (res.rows || res.data || []).map(r => ({
|
||
lineId: r.lineId,
|
||
lineName: r.lineName || r.lineCode
|
||
}))
|
||
}).catch(() => {})
|
||
},
|
||
loadRawMaterials() {
|
||
return this.loadCoilsByWarehouse()
|
||
},
|
||
loadWarehouseOptions() {
|
||
return treeActualWarehouseTwoLevel().then(res => {
|
||
const tree = res.data || []
|
||
const leaf = []
|
||
tree.forEach(p => {
|
||
;(p.children || []).forEach(c => {
|
||
leaf.push({
|
||
actualWarehouseId: c.actualWarehouseId,
|
||
actualWarehouseName: `${p.actualWarehouseName || ''}/${c.actualWarehouseName || ''}`
|
||
})
|
||
})
|
||
})
|
||
this.coilPicker.warehouseOptions = leaf
|
||
}).catch(() => {
|
||
this.coilPicker.warehouseOptions = []
|
||
})
|
||
},
|
||
async loadCoilsByWarehouse() {
|
||
if (!this.coilPicker.selectedWarehouseId) {
|
||
this.coilPicker.coilGrid = {}
|
||
this.rawMaterialOptions = []
|
||
this.coilPicker.total = 0
|
||
return
|
||
}
|
||
this.coilPicker.loading = true
|
||
try {
|
||
const gridRes = await getMaterialCoilLocationGrid({
|
||
itemType: 'raw_material',
|
||
actualWarehouseId: this.coilPicker.selectedWarehouseId,
|
||
enterCoilNo: this.coilPicker.enterCoilNo || undefined,
|
||
currentCoilNo: this.coilPicker.currentCoilNo || undefined,
|
||
manufacturer: this.coilPicker.manufacturer || undefined
|
||
})
|
||
|
||
const data = gridRes.data || {}
|
||
const warehouses = data.warehouses || []
|
||
const coils = (data.coils || []).map(item => ({
|
||
rawMaterialId: item.coilId,
|
||
rawMaterialCode: item.currentCoilNo || item.enterCoilNo || item.coilNo || '',
|
||
enterCoilNo: item.enterCoilNo || '',
|
||
manufacturer: item.manufacturer || '',
|
||
rawMaterialName: item.itemName || item.materialName || '',
|
||
specification: item.specification || '',
|
||
coilWeight: item.netWeight,
|
||
packageType: item.packagingRequirement || item.packageType,
|
||
edgeType: item.trimmingRequirement || item.edgeType,
|
||
zincLayer: item.coatingType || item.zincLayer,
|
||
surfaceTreatmentDesc: item.surfaceTreatmentDesc,
|
||
actualWarehouseId: item.actualWarehouseId,
|
||
actualWarehouseName: item.actualWarehouseName || '未分配库位',
|
||
rawLocation: item.actualWarehouseName || '未分配库位'
|
||
}))
|
||
this.rawMaterialOptions = coils
|
||
this.coilPicker.total = coils.length
|
||
|
||
const coilByWarehouseId = {}
|
||
coils.forEach(c => {
|
||
if (c.actualWarehouseId && !coilByWarehouseId[c.actualWarehouseId]) {
|
||
coilByWarehouseId[c.actualWarehouseId] = c
|
||
}
|
||
})
|
||
|
||
const grid = {}
|
||
warehouses.forEach(w => {
|
||
const code = String(w.actualWarehouseCode || '')
|
||
const m = code.match(/^([A-Za-z0-9]{3})([^-]+)-(\d{2})-(\d+)$/)
|
||
if (!m) return
|
||
const col = Number(m[2])
|
||
const row = Number(m[3])
|
||
const layer = Number(m[4])
|
||
if (!grid[col]) grid[col] = { layer1: [], layer2: [] }
|
||
const coil = coilByWarehouseId[w.actualWarehouseId] || null
|
||
const cell = {
|
||
_col: col,
|
||
_row: row,
|
||
_layer: layer,
|
||
actualWarehouseId: w.actualWarehouseId,
|
||
actualWarehouseCode: w.actualWarehouseCode,
|
||
rawLocation: w.actualWarehouseName || w.actualWarehouseCode,
|
||
...(coil || {})
|
||
}
|
||
if (layer === 1) grid[col].layer1.push(cell)
|
||
else if (layer === 2) grid[col].layer2.push(cell)
|
||
})
|
||
|
||
Object.keys(grid).forEach(k => {
|
||
grid[k].layer1.sort((a, b) => Number(a._row) - Number(b._row))
|
||
grid[k].layer2.sort((a, b) => Number(a._row) - Number(b._row))
|
||
})
|
||
this.coilPicker.coilGrid = grid
|
||
} finally {
|
||
this.coilPicker.loading = false
|
||
}
|
||
},
|
||
searchCoils() {
|
||
this.loadCoilsByWarehouse()
|
||
},
|
||
onCoilFilterChange() {
|
||
this.loadCoilsByWarehouse()
|
||
},
|
||
hasSelectedWarehouse() {
|
||
return !!this.coilPicker.selectedWarehouseId
|
||
},
|
||
getCoilCell(col, row, layer) {
|
||
const c = this.coilPicker.coilGrid[col]
|
||
if (!c) return null
|
||
const list = layer === 1 ? (c.layer1 || []) : (c.layer2 || [])
|
||
return list.find(i => Number(i._row) === Number(row)) || null
|
||
},
|
||
selectCoilCell(col, row, layer) {
|
||
const item = this.getCoilCell(col, row, layer)
|
||
if (!item) return
|
||
if (!item.rawMaterialId) {
|
||
this.$message.warning('该库位当前无钢卷,请选择有钢卷的库位')
|
||
return
|
||
}
|
||
this.pickRawMaterial(item)
|
||
},
|
||
rawMaterialLabel(item) {
|
||
return `${item.rawMaterialCode || '-'} / ${item.rawMaterialName || '-'} / ${item.specification || '-'}`
|
||
},
|
||
applyRouteQuery() {
|
||
const q = this.$route && this.$route.query ? this.$route.query : {}
|
||
if (q.templateKey) this.query.templateKey = String(q.templateKey)
|
||
if (q.planId) this.query.planId = String(q.planId)
|
||
if (q.orderId) this.query.orderId = String(q.orderId)
|
||
if (q.lineId) {
|
||
const lineIdStr = String(q.lineId)
|
||
const matched = (this.lineOptions || []).find(l => String(l.lineId) === lineIdStr)
|
||
this.query.lineId = matched ? matched.lineId : lineIdStr
|
||
}
|
||
if (q.queryStart && q.queryEnd) this.query.range = [q.queryStart, q.queryEnd]
|
||
},
|
||
isEditableColumn(prop) {
|
||
return ['rawCoilNos', 'rawNetWeight', 'rawPackaging', 'rawEdgeReq', 'rawCoatingType'].includes(prop)
|
||
},
|
||
normalizeSaveRow(r) {
|
||
return {
|
||
operationId: r.operationId,
|
||
rawMaterialId: r.rawMaterialId || null,
|
||
rawCoilNos: r.rawCoilNos || '',
|
||
rawNetWeight: r.rawNetWeight === '' || r.rawNetWeight == null ? null : r.rawNetWeight,
|
||
rawPackaging: r.rawPackaging || '',
|
||
rawEdgeReq: r.rawEdgeReq || '',
|
||
rawCoatingType: r.rawCoatingType || '',
|
||
rawLocation: r.rawLocation || ''
|
||
}
|
||
},
|
||
setBaselineFromRows(rows) {
|
||
const map = {}
|
||
;(rows || []).forEach(r => {
|
||
if (r && r.operationId) {
|
||
map[r.operationId] = this.normalizeSaveRow(r)
|
||
}
|
||
})
|
||
this.baselineByOperationId = map
|
||
this.dirtyOperationIds = []
|
||
},
|
||
markRowDirty(row) {
|
||
if (!row || !row.operationId) return
|
||
const id = row.operationId
|
||
const curr = this.normalizeSaveRow(row)
|
||
const baseline = this.baselineByOperationId[id] || {}
|
||
const changed = JSON.stringify(curr) !== JSON.stringify(baseline)
|
||
const ids = new Set(this.dirtyOperationIds || [])
|
||
if (changed) ids.add(id)
|
||
else ids.delete(id)
|
||
this.dirtyOperationIds = Array.from(ids)
|
||
},
|
||
onCellEdit(row) {
|
||
this.$forceUpdate()
|
||
this.markRowDirty(row)
|
||
this.scheduleAutoSave(row)
|
||
},
|
||
scheduleAutoSave(row) {
|
||
if (!row || !row.operationId) return
|
||
this.pendingAutoSave = true
|
||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
||
this.autoSaveTimer = setTimeout(() => {
|
||
this.handleSaveSupplement(true)
|
||
}, 900)
|
||
},
|
||
openCoilPicker(row) {
|
||
this.coilPicker.currentRow = row
|
||
this.coilPicker.visible = true
|
||
Promise.all([this.loadWarehouseOptions()]).then(() => {
|
||
if (!this.coilPicker.selectedWarehouseId && this.coilPicker.warehouseOptions.length) {
|
||
this.coilPicker.selectedWarehouseId = this.coilPicker.warehouseOptions[0].actualWarehouseId
|
||
this.$message.info('请先选择库区,再点击查询或直接选择库位钢卷')
|
||
}
|
||
this.loadCoilsByWarehouse()
|
||
})
|
||
},
|
||
pickRawMaterial(item) {
|
||
const row = this.coilPicker.currentRow
|
||
if (!row) return
|
||
if (!item || !item.rawMaterialId) {
|
||
this.$message.warning('请选择有效钢卷')
|
||
return
|
||
}
|
||
this.$set(row, 'rawMaterialId', item.rawMaterialId)
|
||
this.$set(row, 'rawCoilNos', item.rawMaterialCode || item.rawCoilNos || item.rawMaterialName || '')
|
||
this.$set(row, 'rawNetWeight', item.coilWeight != null ? item.coilWeight : (row.rawNetWeight || ''))
|
||
this.$set(row, 'rawPackaging', item.packageType || item.rawPackaging || row.rawPackaging || '')
|
||
this.$set(row, 'rawEdgeReq', item.edgeType || item.rawEdgeReq || row.rawEdgeReq || '')
|
||
this.$set(row, 'rawCoatingType', item.zincLayer || item.surfaceTreatmentDesc || row.rawCoatingType || '')
|
||
this.$set(row, 'rawLocation', item.rawLocation || '')
|
||
this.coilPicker.visible = false
|
||
this.$message.success('已选择钢卷并回填')
|
||
this.$forceUpdate()
|
||
this.markRowDirty(row)
|
||
this.scheduleAutoSave(row)
|
||
},
|
||
handleRawMaterialChange(row, rawMaterialId) {
|
||
const item = (this.rawMaterialOptions || []).find(r => r.rawMaterialId === rawMaterialId)
|
||
if (!item) return
|
||
row.rawMaterialId = item.rawMaterialId
|
||
row.rawCoilNos = item.rawMaterialCode || item.rawCoilNos || item.rawMaterialName || ''
|
||
row.rawNetWeight = item.coilWeight != null ? item.coilWeight : (row.rawNetWeight || '')
|
||
row.rawPackaging = item.packageType || item.rawPackaging || row.rawPackaging || ''
|
||
row.rawEdgeReq = item.edgeType || item.rawEdgeReq || row.rawEdgeReq || ''
|
||
row.rawCoatingType = item.zincLayer || item.surfaceTreatmentDesc || row.rawCoatingType || ''
|
||
row.rawLocation = item.rawLocation || row.rawLocation || ''
|
||
this.$forceUpdate()
|
||
this.markRowDirty(row)
|
||
this.scheduleAutoSave(row)
|
||
},
|
||
buildSavePayload() {
|
||
const dirtyIds = new Set(this.dirtyOperationIds || [])
|
||
const rows = (this.sheet.rows || [])
|
||
.filter(r => r && r.operationId && dirtyIds.has(r.operationId))
|
||
.map(r => this.normalizeSaveRow(r))
|
||
return { rows }
|
||
},
|
||
async handleSaveSupplement(isAuto = false) {
|
||
const payload = this.buildSavePayload()
|
||
if (!payload.rows.length) {
|
||
if (!isAuto) this.$message.warning('暂无可保存的数据')
|
||
return
|
||
}
|
||
if (this.saving) {
|
||
this.pendingAutoSave = true
|
||
return
|
||
}
|
||
const savedIds = payload.rows.map(r => r.operationId)
|
||
this.saving = true
|
||
try {
|
||
await saveScheduleSheetSupplement(payload)
|
||
payload.rows.forEach(r => {
|
||
this.baselineByOperationId[r.operationId] = { ...r }
|
||
})
|
||
const dirtySet = new Set(this.dirtyOperationIds || [])
|
||
savedIds.forEach(id => dirtySet.delete(id))
|
||
this.dirtyOperationIds = Array.from(dirtySet)
|
||
if (isAuto) {
|
||
this.pendingAutoSave = false
|
||
} else {
|
||
this.$message.success('补录保存成功')
|
||
}
|
||
} finally {
|
||
this.saving = false
|
||
if (this.pendingAutoSave) {
|
||
this.pendingAutoSave = false
|
||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
||
this.autoSaveTimer = setTimeout(() => {
|
||
this.handleSaveSupplement(true)
|
||
}, 300)
|
||
}
|
||
}
|
||
},
|
||
handleQuery() {
|
||
if (!this.query.range || this.query.range.length !== 2) {
|
||
this.$message.warning('请先选择时间范围')
|
||
return
|
||
}
|
||
const params = {
|
||
templateKey: this.query.templateKey,
|
||
lineId: this.query.lineId || undefined,
|
||
planId: this.query.planId || undefined,
|
||
orderId: this.query.orderId || undefined,
|
||
queryStart: this.query.range[0],
|
||
queryEnd: this.query.range[1]
|
||
}
|
||
this.loading = true
|
||
fetchScheduleSheet(params)
|
||
.then(res => {
|
||
const data = res.data || res
|
||
this.sheet = {
|
||
header: data.header || null,
|
||
rows: (data.rows || []).map(r => ({ ...r })),
|
||
summary: data.summary || null
|
||
}
|
||
this.setBaselineFromRows(this.sheet.rows)
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
handleExport() {
|
||
if (!this.query.range || this.query.range.length !== 2) {
|
||
this.$message.warning('请先选择时间范围')
|
||
return
|
||
}
|
||
const params = {
|
||
templateKey: this.query.templateKey,
|
||
lineId: this.query.lineId || undefined,
|
||
planId: this.query.planId || undefined,
|
||
orderId: this.query.orderId || undefined,
|
||
queryStart: this.query.range[0],
|
||
queryEnd: this.query.range[1]
|
||
}
|
||
this.loading = true
|
||
exportScheduleSheet(params)
|
||
.then(blob => {
|
||
const filename = `aps_${this.query.templateKey}_${new Date().getTime()}.xlsx`
|
||
saveAs(new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), filename)
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.aps-sheet-page {
|
||
padding: 8px;
|
||
background: #f3f5f7;
|
||
}
|
||
|
||
::v-deep .sheet-header.el-card,
|
||
::v-deep .sheet-body.el-card {
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.sheet-header,
|
||
.sheet-body {
|
||
border: 1px solid #d3d7dc;
|
||
border-radius: 2px;
|
||
background: #fff;
|
||
}
|
||
|
||
.sheet-header {
|
||
margin-bottom: 8px;
|
||
}
|
||
.sheet-header-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
.sheet-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14px;
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
color: #4d4f53;
|
||
}
|
||
.meta-item .k {
|
||
color: #8b9098;
|
||
}
|
||
.meta-item-warn {
|
||
width: 100%;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
background: #fff8e6;
|
||
border: 1px solid #f7df9a;
|
||
}
|
||
.meta-item-warn .k,
|
||
.meta-item-warn .v {
|
||
color: #8a6d1d;
|
||
}
|
||
.sheet-summary {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
}
|
||
.sheet-summary .k {
|
||
color: #8b9098;
|
||
}
|
||
|
||
::v-deep .excel-table {
|
||
border: 1px solid #bfc5cc;
|
||
}
|
||
::v-deep .excel-table th.el-table__cell {
|
||
background: #eef1f5;
|
||
color: #3c4045;
|
||
font-weight: 600;
|
||
border-right: 1px solid #bfc5cc;
|
||
border-bottom: 1px solid #bfc5cc;
|
||
padding: 4px 0;
|
||
}
|
||
::v-deep .excel-table td.el-table__cell {
|
||
border-right: 1px solid #d0d5db;
|
||
border-bottom: 1px solid #d0d5db;
|
||
padding: 1px 2px;
|
||
}
|
||
::v-deep .excel-table .el-table__row:hover > td {
|
||
background-color: #f5f9ff !important;
|
||
}
|
||
::v-deep .excel-table .current-row > td {
|
||
background-color: #eaf2ff !important;
|
||
}
|
||
|
||
::v-deep .excel-cell-input .el-input__inner,
|
||
::v-deep .excel-cell-select .el-input__inner {
|
||
border: 1px solid transparent;
|
||
border-radius: 0;
|
||
height: 24px;
|
||
line-height: 24px;
|
||
padding: 0 6px;
|
||
background: #fff;
|
||
}
|
||
::v-deep .excel-cell-input .el-input__inner:focus,
|
||
::v-deep .excel-cell-select .el-input__inner:focus {
|
||
border-color: #4a90e2;
|
||
box-shadow: inset 0 0 0 1px #4a90e2;
|
||
}
|
||
|
||
.coil-bird-wrap { border: 1px solid #ebeef5; border-radius: 6px; padding: 8px; }
|
||
.coil-legend { display: flex; gap: 14px; font-size: 12px; color: #606266; margin-bottom: 8px; }
|
||
.legend-item { display: inline-flex; align-items: center; gap: 4px; }
|
||
.dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
||
.dot.layer1 { background: #fff3e0; border: 1px solid #f5d7a1; }
|
||
.dot.layer2 { background: #e8f5e9; border: 1px solid #b7ddb9; }
|
||
.dot.occupied { background: #f6f8fb; border: 1px solid #d7dde5; }
|
||
.coil-grid-scroll { overflow: auto; max-height: 460px; }
|
||
.coil-col-ruler { display: flex; min-width: max-content; }
|
||
.ruler-empty { width: 28px; flex: none; }
|
||
.ruler-col { width: 138px; text-align: center; font-size: 12px; color: #606266; }
|
||
.coil-grid-main { display: flex; min-width: max-content; }
|
||
.coil-row-ruler { width: 28px; display: flex; flex-direction: column; }
|
||
.ruler-row { height: 56px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #909399; }
|
||
.coil-grid-columns { display: flex; gap: 8px; }
|
||
.coil-col-pair { display: grid; grid-template-columns: 1fr 1fr; width: 180px; border: 1px solid #eef2f7; border-radius: 4px; overflow: visible; }
|
||
.coil-layer { display: flex; flex-direction: column; }
|
||
.coil-layer.layer2-shift { margin-top: 28px; }
|
||
.coil-cell { height: 56px; border-bottom: 1px solid #eef2f7; border-right: 1px solid #eef2f7; padding: 4px; font-size: 11px; color: #909399; display: flex; align-items: center; justify-content: center; text-align: center; cursor: pointer; }
|
||
.coil-cell.layer1 { background: #fff3e0; color: #e67e22; }
|
||
.coil-cell.layer2 { background: #e8f5e9; color: #2e8b57; border-right: none; }
|
||
.coil-cell.occupied { font-weight: 600; }
|
||
.coil-cell .line1 { width: 100%; font-size: 11px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.coil-cell .line2 { width: 100%; font-size: 11px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.coil-picker-foot { margin-top: 8px; text-align: right; color: #909399; font-size: 12px; }
|
||
</style>
|