749 lines
27 KiB
Vue
749 lines
27 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="aps-quick-sheet excel-theme">
|
|||
|
|
<div class="sheet-toolbar">
|
|||
|
|
<div class="sheet-actions">
|
|||
|
|
<el-button size="small" icon="el-icon-refresh" @click="loadRows">刷新</el-button>
|
|||
|
|
<el-button size="small" type="primary" icon="el-icon-finished" :loading="saving" @click="saveAll">保存</el-button>
|
|||
|
|
<el-button size="small" icon="el-icon-plus" @click="addRow">新增行</el-button>
|
|||
|
|
<el-button size="small" icon="el-icon-download" @click="exportCsv">导出</el-button>
|
|||
|
|
<el-date-picker
|
|||
|
|
v-model="filter.range"
|
|||
|
|
type="daterange"
|
|||
|
|
range-separator="至"
|
|||
|
|
start-placeholder="开始日期"
|
|||
|
|
end-placeholder="结束日期"
|
|||
|
|
value-format="yyyy-MM-dd"
|
|||
|
|
size="small"
|
|||
|
|
style="width: 240px"
|
|||
|
|
@change="onFilterChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="preset-bar">
|
|||
|
|
<span class="preset-title">预设方案:</span>
|
|||
|
|
<el-button
|
|||
|
|
v-for="line in lineOptions"
|
|||
|
|
:key="line.lineId"
|
|||
|
|
size="mini"
|
|||
|
|
plain
|
|||
|
|
@click="applyPreset(line)"
|
|||
|
|
>
|
|||
|
|
{{ line.lineName || line.lineCode || ('产线' + line.lineId) }}
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="sheet-body">
|
|||
|
|
<el-table
|
|||
|
|
v-loading="loading"
|
|||
|
|
:data="displayRows"
|
|||
|
|
border
|
|||
|
|
size="mini"
|
|||
|
|
class="excel-table"
|
|||
|
|
>
|
|||
|
|
<el-table-column label="序号" width="60" align="center">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
{{ ((pager.pageNum - 1) * pager.pageSize) + scope.$index + 1 }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column
|
|||
|
|
v-for="col in flatColumns"
|
|||
|
|
: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="isTimeField(col.prop)">
|
|||
|
|
<el-date-picker
|
|||
|
|
v-model="scope.row[col.prop]"
|
|||
|
|
type="datetime"
|
|||
|
|
value-format="yyyy-MM-dd HH:mm:ss"
|
|||
|
|
size="mini"
|
|||
|
|
style="width: 100%"
|
|||
|
|
@change="onCellChange(scope.row)"
|
|||
|
|
@input="onCellChange(scope.row)"
|
|||
|
|
/>
|
|||
|
|
</template>
|
|||
|
|
<template v-else>
|
|||
|
|
<el-autocomplete
|
|||
|
|
v-if="col.prop === 'lineName'"
|
|||
|
|
v-model="scope.row[col.prop]"
|
|||
|
|
size="mini"
|
|||
|
|
clearable
|
|||
|
|
:fetch-suggestions="queryLineHistory"
|
|||
|
|
@select="onLineHistorySelect(scope.row)"
|
|||
|
|
@input="onLineHistoryInput(scope.row)"
|
|||
|
|
/>
|
|||
|
|
<el-input
|
|||
|
|
v-else
|
|||
|
|
v-model="scope.row[col.prop]"
|
|||
|
|
size="mini"
|
|||
|
|
clearable
|
|||
|
|
@input="onCellChange(scope.row)"
|
|||
|
|
/>
|
|||
|
|
</template>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="操作" width="70" fixed="right" align="center">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<el-button type="text" size="mini" @click="clearRow(scope.row)">清空</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<div class="pager-wrap">
|
|||
|
|
<el-pagination
|
|||
|
|
small
|
|||
|
|
background
|
|||
|
|
layout="prev, pager, next, total"
|
|||
|
|
:current-page.sync="pager.pageNum"
|
|||
|
|
:page-size="pager.pageSize"
|
|||
|
|
:total="totalRows"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="missingRows.length" class="missing-tip">
|
|||
|
|
<div v-for="msg in missingRows" :key="msg" class="missing-item">{{ msg }}</div>
|
|||
|
|
</div>
|
|||
|
|
<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 { fetchQuickSheetList, fetchQuickSheetPreset, saveQuickSheet, exportQuickSheet, deleteQuickSheetRow } from '@/api/aps/quickSheet'
|
|||
|
|
import { listProductionLine } from '@/api/wms/productionLine'
|
|||
|
|
import { getMaterialCoilLocationGrid } from '@/api/wms/coil'
|
|||
|
|
import { treeActualWarehouseTwoLevel } from '@/api/wms/actualWarehouse'
|
|||
|
|
import { getTemplateByKey } from './sheets/templates'
|
|||
|
|
import { saveAs } from 'file-saver'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'ApsQuickSheet',
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
loading: false,
|
|||
|
|
saving: false,
|
|||
|
|
rows: [],
|
|||
|
|
lineOptions: [],
|
|||
|
|
lineHistory: [],
|
|||
|
|
dirtyIds: new Set(),
|
|||
|
|
autoSaveTimer: null,
|
|||
|
|
templateKey: 'unified',
|
|||
|
|
filter: {
|
|||
|
|
range: []
|
|||
|
|
},
|
|||
|
|
pager: {
|
|||
|
|
pageNum: 1,
|
|||
|
|
pageSize: 25
|
|||
|
|
},
|
|||
|
|
coilPicker: {
|
|||
|
|
visible: false,
|
|||
|
|
loading: false,
|
|||
|
|
currentRow: null,
|
|||
|
|
selectedWarehouseId: undefined,
|
|||
|
|
enterCoilNo: '',
|
|||
|
|
currentCoilNo: '',
|
|||
|
|
manufacturer: '',
|
|||
|
|
warehouseOptions: [],
|
|||
|
|
coilGrid: {},
|
|||
|
|
total: 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
currentTemplate() {
|
|||
|
|
return getTemplateByKey(this.templateKey)
|
|||
|
|
},
|
|||
|
|
flatColumns() {
|
|||
|
|
const res = []
|
|||
|
|
const loop = (cols) => {
|
|||
|
|
;(cols || []).forEach(c => {
|
|||
|
|
if (c.children && c.children.length) loop(c.children)
|
|||
|
|
else res.push(c)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
loop(this.currentTemplate.columns || [])
|
|||
|
|
return res
|
|||
|
|
},
|
|||
|
|
displayRows() {
|
|||
|
|
const [startAfter, endBefore] = this.filter.range || []
|
|||
|
|
const filtered = this.rows.filter(r => {
|
|||
|
|
if (!r.startTime) return true
|
|||
|
|
const d = String(r.startTime).slice(0, 10)
|
|||
|
|
if (startAfter && d < startAfter) return false
|
|||
|
|
if (endBefore && d > endBefore) return false
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
const start = (this.pager.pageNum - 1) * this.pager.pageSize
|
|||
|
|
const page = filtered.slice(start, start + this.pager.pageSize)
|
|||
|
|
if (page.length < this.pager.pageSize) {
|
|||
|
|
return page.concat(this.buildEmptyRows(this.pager.pageSize - page.length))
|
|||
|
|
}
|
|||
|
|
return page
|
|||
|
|
},
|
|||
|
|
totalRows() {
|
|||
|
|
const [startAfter, endBefore] = this.filter.range || []
|
|||
|
|
return this.rows.filter(r => {
|
|||
|
|
if (!r.startTime) return true
|
|||
|
|
const d = String(r.startTime).slice(0, 10)
|
|||
|
|
if (startAfter && d < startAfter) return false
|
|||
|
|
if (endBefore && d > endBefore) return false
|
|||
|
|
return true
|
|||
|
|
}).length
|
|||
|
|
},
|
|||
|
|
missingRows() {
|
|||
|
|
return this.collectMissing(this.displayRows)
|
|||
|
|
},
|
|||
|
|
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() {
|
|||
|
|
const today = new Date()
|
|||
|
|
const start = `${today.getFullYear()}-${`${today.getMonth() + 1}`.padStart(2, '0')}-${`${today.getDate()}`.padStart(2, '0')}`
|
|||
|
|
this.filter.range = [start, '']
|
|||
|
|
this.loadRows()
|
|||
|
|
this.loadLines()
|
|||
|
|
},
|
|||
|
|
beforeDestroy() {
|
|||
|
|
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
async loadLines() {
|
|||
|
|
const res = await listProductionLine({ pageNum: 1, pageSize: 1000 })
|
|||
|
|
this.lineOptions = res.rows || []
|
|||
|
|
},
|
|||
|
|
async loadRows() {
|
|||
|
|
this.loading = true
|
|||
|
|
try {
|
|||
|
|
const res = await fetchQuickSheetList()
|
|||
|
|
this.rows = (res.data || []).map(r => this.normalizeRow(r))
|
|||
|
|
this.lineHistory = this.rows
|
|||
|
|
.map(r => r.lineName)
|
|||
|
|
.filter(v => v && v.trim())
|
|||
|
|
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|||
|
|
} finally {
|
|||
|
|
this.loading = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
buildEmptyRows(count) {
|
|||
|
|
return Array.from({ length: count }).map(() => this.buildEmptyRow())
|
|||
|
|
},
|
|||
|
|
async applyPreset(line) {
|
|||
|
|
if (!line || !line.lineId) return
|
|||
|
|
const res = await fetchQuickSheetPreset({ lineId: line.lineId })
|
|||
|
|
const preset = res.data || []
|
|||
|
|
if (!preset.length) return
|
|||
|
|
const targetRow = this.rows.find(r => !r.lineId && !r.lineName) || this.buildEmptyRow()
|
|||
|
|
const base = this.normalizeRow(preset[0])
|
|||
|
|
Object.keys(base).forEach(k => {
|
|||
|
|
if (k === 'quickSheetId') return
|
|||
|
|
targetRow[k] = base[k]
|
|||
|
|
})
|
|||
|
|
targetRow.lineId = line.lineId
|
|||
|
|
targetRow.lineName = line.lineName || line.lineCode || ''
|
|||
|
|
if (!this.rows.includes(targetRow)) {
|
|||
|
|
this.rows.push(targetRow)
|
|||
|
|
}
|
|||
|
|
if (this.rows.length < 25) {
|
|||
|
|
this.rows = this.rows.concat(this.buildEmptyRows(25 - this.rows.length))
|
|||
|
|
}
|
|||
|
|
this.markDirty(targetRow)
|
|||
|
|
this.scheduleAutoSave()
|
|||
|
|
},
|
|||
|
|
async loadWarehouseOptions() {
|
|||
|
|
const res = await treeActualWarehouseTwoLevel()
|
|||
|
|
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
|
|||
|
|
},
|
|||
|
|
async loadCoilsByWarehouse() {
|
|||
|
|
if (!this.coilPicker.selectedWarehouseId) {
|
|||
|
|
this.coilPicker.coilGrid = {}
|
|||
|
|
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 || '',
|
|||
|
|
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.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()
|
|||
|
|
},
|
|||
|
|
openCoilPicker(row) {
|
|||
|
|
this.coilPicker.currentRow = row
|
|||
|
|
this.coilPicker.visible = true
|
|||
|
|
this.loadWarehouseOptions().then(() => {
|
|||
|
|
if (!this.coilPicker.selectedWarehouseId && this.coilPicker.warehouseOptions.length) {
|
|||
|
|
this.coilPicker.selectedWarehouseId = this.coilPicker.warehouseOptions[0].actualWarehouseId
|
|||
|
|
}
|
|||
|
|
this.loadCoilsByWarehouse()
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
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)
|
|||
|
|
},
|
|||
|
|
pickRawMaterial(item) {
|
|||
|
|
const row = this.coilPicker.currentRow
|
|||
|
|
if (!row) return
|
|||
|
|
this.$set(row, 'rawMaterialId', item.rawMaterialId)
|
|||
|
|
this.$set(row, 'rawCoilNos', item.rawMaterialCode || '')
|
|||
|
|
this.$set(row, 'rawNetWeight', item.coilWeight != null ? item.coilWeight : '')
|
|||
|
|
this.$set(row, 'rawPackaging', item.packageType || '')
|
|||
|
|
this.$set(row, 'rawEdgeReq', item.edgeType || '')
|
|||
|
|
this.$set(row, 'rawCoatingType', item.zincLayer || item.surfaceTreatmentDesc || '')
|
|||
|
|
this.$set(row, 'rawLocation', item.rawLocation || '')
|
|||
|
|
this.coilPicker.visible = false
|
|||
|
|
this.onCellChange(row)
|
|||
|
|
},
|
|||
|
|
onCellChange(row) {
|
|||
|
|
if (row && !this.rows.includes(row)) {
|
|||
|
|
this.rows.push(row)
|
|||
|
|
}
|
|||
|
|
this.markDirty(row)
|
|||
|
|
this.scheduleAutoSave()
|
|||
|
|
},
|
|||
|
|
queryLineHistory(query, cb) {
|
|||
|
|
const list = this.lineHistory || []
|
|||
|
|
const q = (query || '').trim().toLowerCase()
|
|||
|
|
const res = list
|
|||
|
|
.filter(item => !q || String(item).toLowerCase().includes(q))
|
|||
|
|
.map(item => ({ value: item }))
|
|||
|
|
cb(res)
|
|||
|
|
},
|
|||
|
|
onLineHistoryInput(row) {
|
|||
|
|
if (!row) return
|
|||
|
|
if (row.lineId && row.lineName && row.lineName.trim() !== String(row.lineName).trim()) {
|
|||
|
|
row.lineId = null
|
|||
|
|
}
|
|||
|
|
this.onCellChange(row)
|
|||
|
|
},
|
|||
|
|
onLineHistorySelect(row) {
|
|||
|
|
return () => {
|
|||
|
|
this.onCellChange(row)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onFilterChange() {
|
|||
|
|
this.pager.pageNum = 1
|
|||
|
|
},
|
|||
|
|
normalizeRow(row) {
|
|||
|
|
const base = this.buildEmptyRow()
|
|||
|
|
Object.keys(base).forEach(k => {
|
|||
|
|
if (row && row[k] !== undefined && row[k] !== null) base[k] = row[k]
|
|||
|
|
})
|
|||
|
|
const idVal = row ? (row.quickSheetId !== undefined && row.quickSheetId !== null ? row.quickSheetId : row.id) : null
|
|||
|
|
base.quickSheetId = idVal != null ? idVal : null
|
|||
|
|
return base
|
|||
|
|
},
|
|||
|
|
buildEmptyRow() {
|
|||
|
|
const row = { quickSheetId: null }
|
|||
|
|
this.flatColumns.forEach(col => {
|
|||
|
|
if (!col.prop) return
|
|||
|
|
row[col.prop] = ''
|
|||
|
|
})
|
|||
|
|
return row
|
|||
|
|
},
|
|||
|
|
isRowEmpty(row) {
|
|||
|
|
if (!row) return true
|
|||
|
|
return !this.flatColumns.some(col => col.prop && row[col.prop])
|
|||
|
|
},
|
|||
|
|
isTimeField(prop) {
|
|||
|
|
return prop === 'startTime' || prop === 'endTime'
|
|||
|
|
},
|
|||
|
|
markDirty(row) {
|
|||
|
|
if (!row) return
|
|||
|
|
if (!row._tmpId) row._tmpId = Math.random().toString(36).slice(2)
|
|||
|
|
this.dirtyIds.add(row._tmpId)
|
|||
|
|
},
|
|||
|
|
async clearRow(row) {
|
|||
|
|
if (!row) return
|
|||
|
|
if (!row.quickSheetId) {
|
|||
|
|
this.flatColumns.forEach(col => {
|
|||
|
|
if (!col.prop) return
|
|||
|
|
row[col.prop] = ''
|
|||
|
|
})
|
|||
|
|
row.quickSheetId = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await deleteQuickSheetRow(row.quickSheetId)
|
|||
|
|
this.flatColumns.forEach(col => {
|
|||
|
|
if (!col.prop) return
|
|||
|
|
row[col.prop] = ''
|
|||
|
|
})
|
|||
|
|
row.quickSheetId = null
|
|||
|
|
},
|
|||
|
|
scheduleAutoSave() {
|
|||
|
|
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
|||
|
|
this.autoSaveTimer = setTimeout(() => {
|
|||
|
|
this.saveAll(true)
|
|||
|
|
}, 800)
|
|||
|
|
},
|
|||
|
|
async saveAll(isAuto = false, payload) {
|
|||
|
|
const rows = payload && payload.rows
|
|||
|
|
? payload.rows
|
|||
|
|
: this.rows.filter(r => r && r._tmpId && this.dirtyIds.has(r._tmpId))
|
|||
|
|
.map(r => ({
|
|||
|
|
...r,
|
|||
|
|
quickSheetId: r.quickSheetId || null
|
|||
|
|
}))
|
|||
|
|
if (!rows.length) {
|
|||
|
|
if (!isAuto) this.$message.warning('暂无可保存数据')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const missing = this.collectMissing(rows)
|
|||
|
|
if (missing.length && !isAuto) {
|
|||
|
|
this.$message.warning(`必填项未填写:${missing.join(';')}`)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.saving = true
|
|||
|
|
try {
|
|||
|
|
await saveQuickSheet({ rows })
|
|||
|
|
if (!isAuto) this.$message.success('保存成功')
|
|||
|
|
rows.forEach(r => {
|
|||
|
|
if (r._tmpId) this.dirtyIds.delete(r._tmpId)
|
|||
|
|
})
|
|||
|
|
} finally {
|
|||
|
|
this.saving = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
addRow() {
|
|||
|
|
this.rows.push(this.buildEmptyRow())
|
|||
|
|
if (this.rows.length < this.pager.pageSize) {
|
|||
|
|
this.rows = this.rows.concat(this.buildEmptyRows(this.pager.pageSize - this.rows.length))
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onFilterChange() {
|
|||
|
|
this.pager.pageNum = 1
|
|||
|
|
},
|
|||
|
|
exportCsv() {
|
|||
|
|
this.loading = true
|
|||
|
|
exportQuickSheet()
|
|||
|
|
.then(blob => {
|
|||
|
|
const filename = `quick_sheet_${new Date().getTime()}.xlsx`
|
|||
|
|
saveAs(new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), filename)
|
|||
|
|
})
|
|||
|
|
.finally(() => {
|
|||
|
|
this.loading = false
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
addRow() {
|
|||
|
|
this.rows.unshift(this.buildEmptyRow())
|
|||
|
|
this.pager.pageNum = 1
|
|||
|
|
},
|
|||
|
|
collectMissing(rows) {
|
|||
|
|
const msgs = []
|
|||
|
|
rows.forEach((row, idx) => {
|
|||
|
|
if (this.isRowEmpty(row)) return
|
|||
|
|
const line = idx + 1
|
|||
|
|
const missing = []
|
|||
|
|
if (!row.lineName && !row.lineId) missing.push('产线')
|
|||
|
|
if (!row.planCode) missing.push('计划号')
|
|||
|
|
if (!row.startTime) missing.push('开始时间')
|
|||
|
|
if (!row.endTime) missing.push('结束时间')
|
|||
|
|
if (missing.length) {
|
|||
|
|
msgs.push(`第${line}行缺少${missing.join('、')}`)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
return msgs
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.aps-quick-sheet {
|
|||
|
|
padding: 8px;
|
|||
|
|
background: #f7f9fc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sheet-toolbar,
|
|||
|
|
.sheet-body {
|
|||
|
|
border: 1px solid #eef2f7;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sheet-toolbar {
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sheet-actions { display: flex; gap: 8px; }
|
|||
|
|
|
|||
|
|
.preset-bar { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #667085; }
|
|||
|
|
.preset-title { font-weight: 600; color: #475467; }
|
|||
|
|
|
|||
|
|
::v-deep .excel-table {
|
|||
|
|
border: 1px solid #edf1f7;
|
|||
|
|
}
|
|||
|
|
::v-deep .excel-table th.el-table__cell {
|
|||
|
|
background: #fafbfe;
|
|||
|
|
color: #5d6b82;
|
|||
|
|
font-weight: 600;
|
|||
|
|
border-right: 1px solid #edf1f7;
|
|||
|
|
border-bottom: 1px solid #edf1f7;
|
|||
|
|
padding: 4px 0;
|
|||
|
|
}
|
|||
|
|
::v-deep .excel-table td.el-table__cell {
|
|||
|
|
border-right: 1px solid #f0f2f6;
|
|||
|
|
border-bottom: 1px solid #f0f2f6;
|
|||
|
|
padding: 1px 2px;
|
|||
|
|
}
|
|||
|
|
::v-deep .excel-table .el-table__row:hover > td {
|
|||
|
|
background-color: #f7faff !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .el-input__inner {
|
|||
|
|
border-radius: 0;
|
|||
|
|
height: 26px;
|
|||
|
|
line-height: 26px;
|
|||
|
|
border: none;
|
|||
|
|
box-shadow: none;
|
|||
|
|
background: transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .el-input__inner:focus {
|
|||
|
|
border: none;
|
|||
|
|
box-shadow: inset 0 0 0 1px #4a90e2;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.missing-tip {
|
|||
|
|
margin-top: 8px;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
background: #fff7e6;
|
|||
|
|
border: 1px solid #ffd591;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #d46b08;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.missing-item {
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.coil-picker-btn {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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>
|