快速排查新增字段与前端保存逻辑

This commit is contained in:
2026-03-23 15:33:12 +08:00
parent 3f503eab0c
commit 31f85e7cd6
9 changed files with 763 additions and 335 deletions

View File

@@ -6,6 +6,7 @@
<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-button size="small" icon="el-icon-setting" @click="columnSettingVisible = true">列设置</el-button>
<el-date-picker
v-model="filter.range"
type="daterange"
@@ -34,39 +35,31 @@
<div class="sheet-body">
<el-table
ref="quickSheetTable"
v-loading="loading"
:data="displayRows"
border
size="mini"
class="excel-table"
>
<el-table-column label="序号" width="60" align="center">
<el-table-column label="序号" width="60" align="center" fixed="left">
<template slot-scope="scope">
{{ ((pager.pageNum - 1) * pager.pageSize) + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
v-for="col in flatColumns"
v-for="(col, colIndex) in displayColumns"
:key="col.prop || col.label"
:label="col.label"
:prop="col.prop"
:width="col.width"
:min-width="col.minWidth"
:align="col.align || 'center'"
:fixed="fixedProps.includes(col.prop) ? 'left' : false"
: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)">
<template v-if="isTimeField(col.prop)">
<el-date-picker
v-model="scope.row[col.prop]"
type="datetime"
@@ -75,6 +68,8 @@
style="width: 100%"
@change="onCellChange(scope.row)"
@input="onCellChange(scope.row)"
@keydown.native.enter.prevent="focusNextCell(scope.$index, colIndex, 'down')"
@keydown.native.tab.prevent="focusNextCell(scope.$index, colIndex, 'right')"
/>
</template>
<template v-else>
@@ -86,6 +81,8 @@
:fetch-suggestions="queryLineHistory"
@select="onLineHistorySelect(scope.row)"
@input="onLineHistoryInput(scope.row)"
@keydown.native.enter.prevent="focusNextCell(scope.$index, colIndex, 'down')"
@keydown.native.tab.prevent="focusNextCell(scope.$index, colIndex, 'right')"
/>
<el-input
v-else
@@ -93,6 +90,8 @@
size="mini"
clearable
@input="onCellChange(scope.row)"
@keydown.native.enter.prevent="focusNextCell(scope.$index, colIndex, 'down')"
@keydown.native.tab.prevent="focusNextCell(scope.$index, colIndex, 'right')"
/>
</template>
</template>
@@ -118,102 +117,57 @@
<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>
<el-drawer
title="列设置"
:visible.sync="columnSettingVisible"
direction="rtl"
size="55%"
append-to-body
@open="resetColumnDraft">
<div class="col-setting-body">
<div class="col-setting-toolbar">
<el-button size="mini" @click="restoreDefaultColumns">恢复默认</el-button>
<el-button size="mini" @click="resetColumnDraft">重置</el-button>
<el-button size="mini" type="primary" @click="saveColumnSettings">保存</el-button>
</div>
<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 class="col-section">
<div class="col-section-title">固定列最多5列</div>
<draggable v-model="fixedColumnList" handle=".drag-handle" animation="200" class="col-setting-grid">
<div v-for="item in fixedColumnList" :key="`fixed-${item.prop}`" class="col-setting-item">
<i class="el-icon-rank drag-handle" />
<span class="col-label">{{ item.label }}</span>
<el-button type="text" size="mini" @click="removeFromFixed(item.prop)">移除</el-button>
</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>
</draggable>
</div>
<div class="col-section">
<div class="col-section-title">其他列</div>
<draggable v-model="normalColumnList" handle=".drag-handle" animation="200" class="col-setting-grid">
<div v-for="item in normalColumnList" :key="`normal-${item.prop}`" class="col-setting-item">
<i class="el-icon-rank drag-handle" />
<span class="col-label">{{ item.label }}</span>
<el-button type="text" size="mini" @click="addToFixed(item.prop)">加入固定</el-button>
</div>
</div>
</draggable>
</div>
</div>
<div class="coil-picker-foot"> {{ coilPicker.total || 0 }} 条钢卷按库位分布展示</div>
</el-dialog>
</el-drawer>
</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'
import draggable from 'vuedraggable'
export default {
name: 'ApsQuickSheet',
components: { draggable },
data() {
return {
loading: false,
@@ -231,18 +185,10 @@ export default {
pageNum: 1,
pageSize: 25
},
coilPicker: {
visible: false,
loading: false,
currentRow: null,
selectedWarehouseId: undefined,
enterCoilNo: '',
currentCoilNo: '',
manufacturer: '',
warehouseOptions: [],
coilGrid: {},
total: 0
}
columnSettingVisible: false,
columnSettingList: [],
fixedColumnList: [],
normalColumnList: []
}
},
computed: {
@@ -258,7 +204,23 @@ export default {
})
}
loop(this.currentTemplate.columns || [])
return res
return res.filter(c => c && c.prop)
},
displayColumns() {
const setting = this.columnSettingList || []
const hasSetting = setting.length > 0
const base = this.flatColumns
const selected = hasSetting
? setting.filter(i => i.visible).map(i => base.find(c => c.prop === i.prop)).filter(Boolean)
: base
const selectedSet = new Set(selected.map(c => c.prop))
const rest = base.filter(c => !selectedSet.has(c.prop))
return [...selected, ...rest]
},
fixedProps() {
return (this.columnSettingList || []).filter(i => i.visible && i.fixed).map(i => i.prop).slice(0, 5)
},
displayRows() {
const [startAfter, endBefore] = this.filter.range || []
@@ -288,35 +250,53 @@ export default {
},
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.initColumnSettings()
this.loadRows()
this.loadLines()
window.addEventListener('keydown', this.handleGlobalKeydown)
},
beforeDestroy() {
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
this.persistColumnSettings()
window.removeEventListener('keydown', this.handleGlobalKeydown)
},
methods: {
handleGlobalKeydown(e) {
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.userAgent)
const ctrlOrCmd = isMac ? e.metaKey : e.ctrlKey
if (ctrlOrCmd && (e.key === 's' || e.key === 'S')) {
e.preventDefault()
this.saveAll(false)
}
},
focusNextCell(visibleRowIndex, colIndex, direction) {
const pageStart = (this.pager.pageNum - 1) * this.pager.pageSize
const targetRow = direction === 'down' ? visibleRowIndex + 1 : visibleRowIndex
const targetCol = direction === 'right' ? colIndex + 1 : colIndex
if (targetCol >= this.displayColumns.length) return
const globalRowIndex = pageStart + targetRow
if (globalRowIndex >= this.rows.length) {
this.rows.push(this.buildEmptyRow())
}
this.$nextTick(() => {
const wrappers = this.$el.querySelectorAll('.excel-table .el-table__body-wrapper tbody tr')
const tr = wrappers[targetRow]
if (!tr) return
const tds = tr.querySelectorAll('td')
const td = tds[targetCol + 1]
if (!td) return
const input = td.querySelector('input, textarea, .el-input__inner')
if (input) input.focus()
})
},
async loadLines() {
const res = await listProductionLine({ pageNum: 1, pageSize: 1000 })
this.lineOptions = res.rows || []
@@ -359,133 +339,6 @@ export default {
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)
@@ -540,6 +393,91 @@ export default {
isTimeField(prop) {
return prop === 'startTime' || prop === 'endTime'
},
isLeadingColumn(col) {
if (!col || !col.prop) return false
return this.leadingPropsByRole.includes(col.prop)
},
initColumnSettings() {
const key = this.getColumnSettingKey()
const stored = localStorage.getItem(key)
const base = this.flatColumns.map((col, idx) => ({ prop: col.prop, label: col.label, visible: true, fixed: idx < 5 }))
if (!stored) {
this.columnSettingList = base
this.resetColumnDraft()
return
}
try {
const parsed = JSON.parse(stored)
if (!Array.isArray(parsed)) {
this.columnSettingList = base
this.resetColumnDraft()
return
}
const savedProps = parsed.map(i => i.prop)
const extras = base.filter(i => !savedProps.includes(i.prop))
this.columnSettingList = [...parsed.filter(i => base.some(b => b.prop === i.prop)).map(i => {
const match = base.find(b => b.prop === i.prop)
return { ...match, visible: i.visible !== false, fixed: i.fixed === true }
}), ...extras]
this.resetColumnDraft()
} catch (e) {
this.columnSettingList = base
this.resetColumnDraft()
}
},
resetColumnDraft() {
const fixed = (this.columnSettingList || []).filter(i => i.visible && i.fixed).slice(0, 5)
const normal = (this.columnSettingList || []).filter(i => !i.fixed || !i.visible)
this.fixedColumnList = fixed.map(i => ({ ...i, visible: true, fixed: true }))
this.normalColumnList = normal.map(i => ({ ...i, fixed: false }))
},
saveColumnSettings() {
const fixed = (this.fixedColumnList || []).map(i => ({ ...i, visible: true, fixed: true }))
const normal = (this.normalColumnList || []).map(i => ({ ...i, fixed: false }))
this.columnSettingList = [...fixed, ...normal]
this.persistColumnSettings()
this.columnSettingVisible = false
this.pager.pageNum = 1
this.loadRows().finally(() => {
this.$nextTick(() => {
this.$refs.quickSheetTable && this.$refs.quickSheetTable.doLayout && this.$refs.quickSheetTable.doLayout()
})
})
},
addToFixed(prop) {
if ((this.fixedColumnList || []).length >= 5) {
this.$message.warning('固定列最多5列')
return
}
const idx = this.normalColumnList.findIndex(i => i.prop === prop)
if (idx < 0) return
const item = this.normalColumnList.splice(idx, 1)[0]
this.fixedColumnList.push({ ...item, visible: true, fixed: true })
},
removeFromFixed(prop) {
const idx = this.fixedColumnList.findIndex(i => i.prop === prop)
if (idx < 0) return
const item = this.fixedColumnList.splice(idx, 1)[0]
this.normalColumnList.unshift({ ...item, fixed: false })
},
persistColumnSettings() {
const key = this.getColumnSettingKey()
const payload = (this.columnSettingList || []).map(item => ({
prop: item.prop,
label: item.label,
visible: item.visible !== false,
fixed: item.fixed === true
}))
localStorage.setItem(key, JSON.stringify(payload))
},
restoreDefaultColumns() {
const base = this.flatColumns.map((col, idx) => ({ prop: col.prop, label: col.label, visible: true, fixed: idx < 5 }))
this.columnSettingList = base
this.resetColumnDraft()
},
getColumnSettingKey() {
return `apsQuickSheetColumns_${this.templateKey}`
},
markDirty(row) {
if (!row) return
if (!row._tmpId) row._tmpId = Math.random().toString(36).slice(2)
@@ -596,15 +534,6 @@ export default {
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()
@@ -716,33 +645,55 @@ export default {
line-height: 1.6;
}
.coil-picker-btn {
padding: 0;
.col-setting-body {
padding: 12px;
height: calc(100% - 12px);
overflow: hidden;
}
.col-setting-toolbar {
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
.col-section {
margin-bottom: 12px;
}
.col-section-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.col-setting-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.col-setting-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
margin-bottom: 0;
min-height: 38px;
}
.col-setting-item:hover {
border-color: #d9ecff;
background: #f5faff;
}
.drag-handle {
cursor: move;
color: #909399;
}
.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>

View File

@@ -18,31 +18,34 @@
</el-select>
<el-input v-model="filter.customer" size="small" clearable placeholder="客户" style="width: 160px" @input="onFilterChange" />
<el-button size="small" icon="el-icon-refresh" @click="loadRows">刷新</el-button>
<el-button size="small" icon="el-icon-setting" @click="columnSettingVisible = true">列设置</el-button>
<el-button size="small" icon="el-icon-download" @click="exportExcel">导出</el-button>
</div>
</div>
<div class="sheet-body">
<el-table
ref="quickSheetPreviewTable"
v-loading="loading"
:data="displayRows"
border
size="mini"
class="excel-table"
>
<el-table-column label="序号" width="60" align="center">
<el-table-column label="序号" width="60" align="center" fixed="left">
<template slot-scope="scope">
{{ ((pager.pageNum - 1) * pager.pageSize) + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
v-for="col in flatColumns"
v-for="col in displayColumns"
:key="col.prop || col.label"
:label="col.label"
:prop="col.prop"
:width="col.width"
:min-width="col.minWidth"
:align="col.align || 'center'"
:fixed="fixedProps.includes(col.prop) ? 'left' : false"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
/>
</el-table>
@@ -57,6 +60,25 @@
/>
</div>
</div>
<el-drawer
title="列设置"
:visible.sync="columnSettingVisible"
direction="rtl"
size="55%"
append-to-body>
<div class="col-setting-body">
<div class="col-setting-toolbar">
<el-button size="mini" @click="restoreDefaultColumns">恢复默认</el-button>
</div>
<draggable v-model="columnSettingList" handle=".drag-handle" animation="200" @change="persistColumnSettings" class="col-setting-grid">
<div v-for="item in columnSettingList" :key="item.prop" class="col-setting-item">
<i class="el-icon-rank drag-handle" />
<el-checkbox v-model="item.visible" @change="persistColumnSettings">{{ item.label }}</el-checkbox>
</div>
</draggable>
</div>
</el-drawer>
</div>
</template>
@@ -65,9 +87,11 @@ import { fetchQuickSheetList, exportQuickSheet } from '@/api/aps/quickSheet'
import { listProductionLine } from '@/api/wms/productionLine'
import { getTemplateByKey } from './sheets/templates'
import { saveAs } from 'file-saver'
import draggable from 'vuedraggable'
export default {
name: 'ApsQuickSheetPreview',
components: { draggable },
data() {
return {
loading: false,
@@ -81,7 +105,9 @@ export default {
pager: {
pageNum: 1,
pageSize: 25
}
},
columnSettingVisible: false,
columnSettingList: []
}
},
computed: {
@@ -97,7 +123,20 @@ export default {
})
}
loop(this.currentTemplate.columns || [])
return res
return res.filter(c => c && c.prop)
},
displayColumns() {
const setting = this.columnSettingList || []
const hasSetting = setting.length > 0
const base = this.flatColumns
const selected = hasSetting
? setting.filter(i => i.visible).map(i => base.find(c => c.prop === i.prop)).filter(Boolean)
: base
const selectedSet = new Set(selected.map(c => c.prop))
const rest = base.filter(c => !selectedSet.has(c.prop))
return [...selected, ...rest]
},
filteredRows() {
const [startAfter, endBefore] = this.filter.range || []
@@ -129,10 +168,60 @@ export default {
const today = new Date()
const start = `${today.getFullYear()}-${`${today.getMonth() + 1}`.padStart(2, '0')}-${`${today.getDate()}`.padStart(2, '0')}`
this.filter.range = [start, '']
this.initColumnSettings()
this.loadRows()
this.loadLines()
},
beforeDestroy() {
this.persistColumnSettings()
},
methods: {
initColumnSettings() {
const key = this.getColumnSettingKey()
const stored = localStorage.getItem(key)
const base = this.flatColumns.map(col => ({ prop: col.prop, label: col.label, visible: true }))
if (!stored) {
this.columnSettingList = base
return
}
try {
const parsed = JSON.parse(stored)
if (!Array.isArray(parsed)) {
this.columnSettingList = base
return
}
const map = new Map(parsed.map(item => [item.prop, item]))
const merged = base.map(item => {
const saved = map.get(item.prop)
return saved ? { ...item, visible: saved.visible !== false } : item
})
const savedProps = parsed.map(i => i.prop)
const extras = base.filter(i => !savedProps.includes(i.prop))
this.columnSettingList = [...parsed.filter(i => base.some(b => b.prop === i.prop)).map(i => {
const match = base.find(b => b.prop === i.prop)
return { ...match, visible: i.visible !== false }
}), ...extras]
} catch (e) {
this.columnSettingList = base
}
},
persistColumnSettings() {
const key = this.getColumnSettingKey()
const payload = (this.columnSettingList || []).map(item => ({
prop: item.prop,
label: item.label,
visible: item.visible !== false
}))
localStorage.setItem(key, JSON.stringify(payload))
},
restoreDefaultColumns() {
const base = this.flatColumns.map(col => ({ prop: col.prop, label: col.label, visible: true }))
this.columnSettingList = base
this.persistColumnSettings()
},
getColumnSettingKey() {
return 'apsQuickSheetPreviewColumns_unified'
},
async loadLines() {
const res = await listProductionLine({ pageNum: 1, pageSize: 1000 })
this.lineOptions = res.rows || []
@@ -222,4 +311,44 @@ export default {
display: flex;
justify-content: flex-end;
}
.col-setting-body {
padding: 12px;
height: calc(100% - 12px);
overflow: hidden;
}
.col-setting-toolbar {
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
.col-setting-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.col-setting-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
margin-bottom: 0;
min-height: 38px;
}
.col-setting-item:hover {
border-color: #d9ecff;
background: #f5faff;
}
.drag-handle {
cursor: move;
color: #909399;
}
</style>

View File

@@ -16,21 +16,47 @@ export const APS_SHEET_TEMPLATES = [
name: '统一排产表',
columns: [
{ label: '产线', prop: 'lineName', minWidth: 140 },
{ label: '计划号', prop: 'planCode', minWidth: 140 },
{ label: '订单号', prop: 'orderCode', minWidth: 140 },
{ label: '排产单号', prop: 'planCode', minWidth: 140 },
{ label: '排产类型', prop: 'planType', width: 100 },
{ label: '排产人', prop: 'scheduler', width: 100 },
{ label: '修改人', prop: 'updateBy', width: 100 },
{ label: '备注', prop: 'remark', minWidth: 160 },
{ label: '内容序号', prop: 'bizSeqNo', width: 110 },
{ label: '销售内容', prop: 'orderCode', minWidth: 140 },
{ label: '订单合同号', prop: 'contractCode', minWidth: 140 },
{ label: '客户', prop: 'customerName', minWidth: 140 },
{ label: '业务员', prop: 'salesman', width: 100 },
{ label: '产品', prop: 'productName', minWidth: 140 },
{ label: '成品名称', prop: 'productName', minWidth: 140 },
{ label: '材质', prop: 'productMaterial', width: 100 },
{ label: '镀层g', prop: 'coatingG', width: 90, align: 'right' },
{ label: '成品宽度', prop: 'productWidth', width: 100, align: 'right' },
{ label: '扎制厚度', prop: 'rollingThick', width: 100, align: 'right' },
{ label: '标丝厚度', prop: 'markCoatThick', width: 100, align: 'right' },
{ label: '吨钢长度区间m', prop: 'tonSteelLengthRange', minWidth: 130 },
{ label: '数量', prop: 'planQty', width: 90, align: 'right' },
{ label: '重量', prop: 'planWeight', width: 100, align: 'right' },
{ label: '表面处理', prop: 'surfaceTreatment', width: 110 },
{ label: '切边要求', prop: 'widthReq', width: 110 },
{ label: '宽度要求', prop: 'rawEdgeReq', width: 110 },
{ label: '用途', prop: 'usageReq', width: 100 },
{ label: '后处理', prop: 'postProcess', width: 100 },
{ label: '下工序', prop: 'nextProcess', width: 100 },
{ label: '取样', prop: 'sampleReq', width: 90 },
{ label: '开始时间', prop: 'startTime', width: 170 },
{ label: '结束时间', prop: 'endTime', width: 170 },
{ label: '厂家', prop: 'rawManufacturer', width: 100 },
{ label: '原料信息', prop: 'rawMaterial', minWidth: 140 },
{ label: '原料厚度mm', prop: 'rawThick', width: 110, align: 'right' },
{ label: '宽度mm', prop: 'rawWidth', width: 100, align: 'right' },
{ label: '原料钢卷', prop: 'rawMaterialId', minWidth: 220 },
{ label: '原料卷号', prop: 'rawCoilNos', minWidth: 220 },
{ label: '钢卷位置', prop: 'rawLocation', minWidth: 140 },
{ label: '包装要求', prop: 'rawPackaging', minWidth: 140 },
{ label: '切边要求', prop: 'rawEdgeReq', minWidth: 120 },
{ label: '镀层种类', prop: 'rawCoatingType', width: 110 },
{ label: '原料净重', prop: 'rawNetWeight', width: 110, align: 'right' },
{ label: '计划数量', prop: 'planQty', width: 100, align: 'right' },
{ label: '开始时间', prop: 'startTime', width: 170 },
{ label: '结束时间', prop: 'endTime', width: 170 }
{ label: '原料净重', prop: 'rawNetWeight', width: 110, align: 'right' }
],
summary: { sumFields: ['planQty', 'rawNetWeight'] }
}