Files
pickling-mes/frontend/src/views/Production.vue

335 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<div class="card">
<div class="card-body" style="padding:10px 14px;">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
<div class="flex-row">
<span class="kv-label">子卷号</span>
<input v-model="query.coil_no" class="kv-input" style="width:140px;" @keyup.enter="fetchData" />
</div>
<div class="flex-row">
<span class="kv-label"></span>
<select v-model="query.shift" class="kv-input" style="width:80px;">
<option value="">全部</option>
<option v-for="s in ['A','B','C','D']" :key="s" :value="s">{{ s }}</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">状态</span>
<select v-model="query.status" class="kv-input" style="width:110px;">
<option value="">全部</option>
<option value="UNWEIGH">未称重</option>
<option value="PRODUCT">已产出</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">日期</span>
<input v-model="query.start_date" type="date" class="kv-input" style="width:130px;" />
<span class="kv-label">~</span>
<input v-model="query.end_date" type="date" class="kv-input" style="width:130px;" />
</div>
<div class="flex-row">
<button class="btn btn-primary" @click="fetchData">查询</button>
<button class="btn btn-outline" @click="openDialog()"> 新增实绩</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
生产实绩
<span class="ch-badge"> {{ total }} </span>
</div>
<div class="table-scroll" v-loading="loading">
<table class="data-table compact">
<thead>
<tr>
<th>子卷号</th><th>热卷号</th><th></th><th></th><th>钢种</th>
<th>来料厚度</th><th>出口厚度</th><th>偏差上限</th><th>偏差下限</th>
<th>来料宽度</th><th>出口宽度</th>
<th>来料重量</th><th>称重重量</th>
<th>包装要求</th>
<th>表面质量</th><th>表面版型</th>
<th>成品质量</th><th>成品长度</th><th>吨钢长度</th>
<th>下线时间</th><th>状态</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td class="td-num">{{ row.sub_coil_no || row.coil_no }}</td>
<td class="td-num">{{ row.hot_coil_no || '—' }}</td>
<td>{{ row.shift || '—' }}</td>
<td>{{ row.team != null ? row.team : '—' }}</td>
<td>{{ row.steel_grade || '—' }}</td>
<td class="td-num">{{ fmt(row.incoming_thickness) }}</td>
<td class="td-num">{{ fmt(row.outlet_thickness) }}</td>
<td class="td-num">{{ fmt(row.deviation_upper, 3) }}</td>
<td class="td-num">{{ fmt(row.deviation_lower, 3) }}</td>
<td class="td-num">{{ fmt(row.incoming_width, 0) }}</td>
<td class="td-num">{{ fmt(row.outlet_width, 0) }}</td>
<td class="td-num">{{ fmt(row.incoming_weight, 3) }}</td>
<td class="td-num">{{ fmt(row.weighed_weight, 3) }}</td>
<td>{{ row.packaging_req || '—' }}</td>
<td>{{ row.surface_quality || '—' }}</td>
<td>--</td>
<td class="td-num">{{ fmt(row.product_quality) }}</td>
<td class="td-num">{{ fmt(row.product_length, 0) }}</td>
<td class="td-num">{{ fmt(row.length_per_ton) }}</td>
<td class="td-muted">{{ fmtTime(row.offline_time) }}</td>
<td><span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span></td>
<td>
<span class="action-link" @click="openDialog(row)">编辑</span>
<span class="action-link" style="color:#1d8eff" @click="viewCert(row)">质保书</span>
</td>
</tr>
<tr v-if="!tableData.length && !loading">
<td colspan="22" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 新增/编辑弹窗 -->
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
<div class="modal-box" style="width:820px;">
<div class="modal-header">
{{ editRow ? '编辑实绩' : '新增实绩' }}
<span class="modal-close" @click="dialogVisible=false"></span>
</div>
<div class="modal-body">
<div class="grid-3" style="gap:12px;">
<div class="form-field">
<div class="kv-label">子卷号 *</div>
<input v-model="form.coil_no" class="kv-input" :disabled="!!editRow" />
</div>
<div class="form-field">
<div class="kv-label">热卷号</div>
<input v-model="form.hot_coil_no" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">钢种</div>
<input v-model="form.steel_grade" class="kv-input" placeholder="QTGLG-2019" />
</div>
<div class="form-field">
<div class="kv-label"></div>
<select v-model="form.shift" class="kv-input">
<option value=""></option>
<option v-for="s in ['A','B','C','D']" :key="s" :value="s">{{ s }}</option>
</select>
</div>
<div class="form-field">
<div class="kv-label"></div>
<input v-model="form.team" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">来料厚度 (mm)</div>
<input v-model.number="form.incoming_thickness" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">出口厚度 (mm)</div>
<input v-model.number="form.outlet_thickness" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">偏差上限</div>
<input v-model.number="form.deviation_upper" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">偏差下限</div>
<input v-model.number="form.deviation_lower" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">来料宽度 (mm)</div>
<input v-model.number="form.incoming_width" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">出口宽度 (mm)</div>
<input v-model.number="form.outlet_width" type="number" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">来料重量 (t)</div>
<input v-model.number="form.incoming_weight" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">称重重量 (t)</div>
<input v-model.number="form.weighed_weight" type="number" step="0.001" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">包装要求</div>
<select v-model="form.packaging_req" class="kv-input">
<option value=""></option>
<option value="简包">简包</option>
<option value="筒包">筒包</option>
<option value="裸包">裸包</option>
</select>
</div>
<div class="form-field">
<div class="kv-label">表面质量</div>
<input v-model="form.surface_quality" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">表面版型</div>
<input class="kv-input" value="--" disabled />
</div>
<div class="form-field">
<div class="kv-label">成品质量 (%)</div>
<input v-model.number="form.product_quality" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">成品长度 (m)</div>
<input v-model.number="form.product_length" type="number" step="1" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">吨钢长度 (m/t)</div>
<input v-model.number="form.length_per_ton" type="number" step="0.01" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">下线时间</div>
<input v-model="form.offline_time" type="datetime-local" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">状态</div>
<select v-model="form.status" class="kv-input">
<option value="UNWEIGH">未称重</option>
<option value="PRODUCT">已产出</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="dialogVisible=false">取消</button>
<button class="btn btn-primary" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
</div>
<!-- 质保书弹窗 -->
<div v-if="certVisible" class="modal-mask" @click.self="certVisible=false">
<div class="modal-box" style="width:680px;">
<div class="modal-header">
质保书 {{ certRow && (certRow.sub_coil_no || certRow.coil_no) }}
<span class="modal-close" @click="certVisible=false"></span>
</div>
<div class="modal-body" style="background:#fff;color:#000;padding:24px;">
<div style="text-align:center;font-size:18px;font-weight:600;margin-bottom:8px;">钢卷质量保证书</div>
<div style="text-align:center;font-size:11px;color:#666;margin-bottom:18px;">Quality Certificate</div>
<table class="cert-table">
<tr><th>子卷号</th><td>{{ certRow.sub_coil_no || certRow.coil_no }}</td><th>热卷号</th><td>{{ certRow.hot_coil_no || '—' }}</td></tr>
<tr><th>钢种</th><td>{{ certRow.steel_grade || '—' }}</td><th>/</th><td>{{ (certRow.shift || '—') + ' / ' + (certRow.team != null ? certRow.team : '—') }}</td></tr>
<tr><th>来料厚度</th><td>{{ fmt(certRow.incoming_thickness) }} mm</td><th>出口厚度</th><td>{{ fmt(certRow.outlet_thickness) }} mm</td></tr>
<tr><th>偏差上限</th><td>{{ fmt(certRow.deviation_upper, 3) }}</td><th>偏差下限</th><td>{{ fmt(certRow.deviation_lower, 3) }}</td></tr>
<tr><th>来料宽度</th><td>{{ fmt(certRow.incoming_width, 0) }} mm</td><th>出口宽度</th><td>{{ fmt(certRow.outlet_width, 0) }} mm</td></tr>
<tr><th>来料重量</th><td>{{ fmt(certRow.incoming_weight, 3) }} t</td><th>称重重量</th><td>{{ fmt(certRow.weighed_weight, 3) }} t</td></tr>
<tr><th>包装要求</th><td>{{ certRow.packaging_req || '—' }}</td><th>表面质量</th><td>{{ certRow.surface_quality || '—' }}</td></tr>
<tr><th>表面版型</th><td colspan="3">--</td></tr>
<tr><th>成品质量</th><td>{{ fmt(certRow.product_quality) }} %</td><th>成品长度</th><td>{{ fmt(certRow.product_length, 0) }} m</td></tr>
<tr><th>吨钢长度</th><td>{{ fmt(certRow.length_per_ton) }} m/t</td><th>下线时间</th><td>{{ fmtTime(certRow.offline_time) }}</td></tr>
<tr><th>备注</th><td colspan="3">{{ certRow.remark || '—' }}</td></tr>
</table>
<div style="margin-top:18px;display:flex;justify-content:space-between;font-size:12px;color:#666;">
<div>检验员________________</div>
<div>签发日期{{ today }}</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="certVisible=false">关闭</button>
<button class="btn btn-primary" @click="printCert">打印</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getProductionRecords, createProductionRecord, updateProductionRecord } from '@/api'
const STATUS_MAP = {
UNWEIGH: { label: '未称重', badge: 'badge-yellow' },
PRODUCT: { label: '已产出', badge: 'badge-blue' },
}
export default {
name: 'Production',
data() {
return {
loading: false, saving: false,
tableData: [], total: 0,
query: { page: 1, page_size: 50, coil_no: '', shift: '', status: '', start_date: '', end_date: '' },
dialogVisible: false, editRow: null, form: { status: 'UNWEIGH' },
certVisible: false, certRow: {},
}
},
computed: {
today() {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
},
},
created() { this.fetchData() },
methods: {
async fetchData() {
this.loading = true
const params = { page: this.query.page, page_size: this.query.page_size }
if (this.query.coil_no) params.coil_no = this.query.coil_no
if (this.query.shift) params.shift = this.query.shift
if (this.query.start_date) params.start_date = this.query.start_date
if (this.query.end_date) params.end_date = this.query.end_date
try {
const res = await getProductionRecords(params)
let items = res.data.items || []
if (this.query.status) items = items.filter(x => x.status === this.query.status)
this.tableData = items
this.total = res.data.total
} finally { this.loading = false }
},
fmt(v, n = 2) { return v != null ? Number(v).toFixed(n) : '—' },
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
statusLabel(s) { return STATUS_MAP[s]?.label || s || '—' },
statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' },
openDialog(row = null) {
this.editRow = row
if (row) {
const r = { ...row }
if (r.offline_time) r.offline_time = r.offline_time.slice(0, 16)
this.form = r
} else {
this.form = { coil_no: '', status: 'UNWEIGH' }
}
this.dialogVisible = true
},
async save() {
if (!this.form.coil_no) { this.$message.error('子卷号不能为空'); return }
this.saving = true
try {
const d = { ...this.form }
if (d.offline_time && d.offline_time.length === 16) d.offline_time += ':00'
if (this.editRow) await updateProductionRecord(this.editRow.id, d)
else await createProductionRecord(d)
this.$message.success('保存成功')
this.dialogVisible = false; this.fetchData()
} finally { this.saving = false }
},
viewCert(row) { this.certRow = row; this.certVisible = true },
printCert() { window.print() },
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; margin-right: 10px; &:hover { text-decoration: underline; } }
.form-field { display: flex; flex-direction: column; gap: 5px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; }
.data-table.compact th, .data-table.compact td { padding: 4px 6px; font-size: 11.5px; }
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 96vw; max-height: 92vh; display: flex; flex-direction: column; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
.modal-body { padding: 16px; overflow-y: auto; }
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
.cert-table { width: 100%; border-collapse: collapse; font-size: 12px;
th, td { border: 1px solid #888; padding: 6px 10px; }
th { background: #eee; width: 110px; text-align: left; }
}
</style>