feat: 同步本地未提交的前后端更新(plan/quality/material/inspection/production 等模块)

This commit is contained in:
2026-06-20 18:19:06 +08:00
parent 970afe10b4
commit db3945c263
19 changed files with 1681 additions and 961 deletions

View File

@@ -4,25 +4,33 @@
<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>
<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:90px;">
<span class="kv-label"></span>
<select v-model="query.shift" class="kv-input" style="width:80px;">
<option value="">全部</option>
<option v-for="s in ['','','','']" :key="s" :value="s">{{ s }}</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>
<input v-model="query.start_date" type="date" class="kv-input" style="width:140px;" />
<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:140px;" />
<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>
<button class="btn btn-outline" @click="openDialog()"> 新增实绩</button>
</div>
</div>
</div>
@@ -34,99 +42,158 @@
<span class="ch-badge"> {{ total }} </span>
</div>
<div class="table-scroll" v-loading="loading">
<table class="data-table">
<table class="data-table compact">
<thead>
<tr>
<th>卷号</th><th></th><th>开始时间</th><th>结束时间</th>
<th>处理重量(kg)</th><th>平均速度</th><th>最大速度</th>
<th>酸耗(L)</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>成品长度</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.coil_no }}</td>
<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 class="td-muted">{{ fmtTime(row.start_time) }}</td>
<td class="td-muted">{{ fmtTime(row.end_time) }}</td>
<td class="td-num">{{ row.process_weight || '—' }}</td>
<td class="td-num">{{ row.avg_speed ? row.avg_speed + ' m/min' : '—' }}</td>
<td class="td-num">{{ row.max_speed ? row.max_speed + ' m/min' : '—' }}</td>
<td class="td-num">{{ row.acid_consumption || '—' }}</td>
<td class="td-num">{{ row.inlet_thickness || '—' }}</td>
<td class="td-num">{{ row.outlet_thickness || '—' }}</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 v-if="row.quality_grade" :class="['badge', gradeClass(row.quality_grade)]">{{ row.quality_grade }}</span>
<span v-else class="td-muted"></span>
<span class="action-link" @click="openDialog(row)">编辑</span>
<span class="action-link" style="color:#1d8eff" @click="viewCert(row)">质保书</span>
</td>
<td><span class="action-link" @click="openDialog(row)">编辑</span></td>
</tr>
<tr v-if="!tableData.length && !loading">
<td colspan="12" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
<td colspan="22" class="td-muted" style="text-align:center;padding:24px;">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="card-body" style="padding:8px 14px;" v-if="total > query.page_size">
<div class="flex-row">
<button class="btn btn-outline" :disabled="query.page<=1" @click="query.page--;fetchData()">上一页</button>
<span class="kv-label"> {{ query.page }} / {{ Math.ceil(total/query.page_size) }} </span>
<button class="btn btn-outline" :disabled="query.page>=Math.ceil(total/query.page_size)" @click="query.page++;fetchData()">下一页</button>
</div>
</div>
</div>
<!-- Modal -->
<!-- 新增/编辑弹窗 -->
<div v-if="dialogVisible" class="modal-mask" @click.self="dialogVisible=false">
<div class="modal-box" style="width:680px;">
<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-2" style="gap:12px;">
<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" />
<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>
<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 v-for="s in ['甲','乙','丙','丁']" :key="s" :value="s">{{ s }}</option>
<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.start_time" type="datetime-local" class="kv-input" />
<div class="kv-label"></div>
<input v-model="form.team" class="kv-input" />
</div>
<div class="form-field">
<div class="kv-label">结束时间</div>
<input v-model="form.end_time" type="datetime-local" class="kv-input" />
<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">处理重量 (kg)</div>
<input v-model.number="form.process_weight" type="number" class="kv-input" />
<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">平均速度 (m/min)</div>
<input v-model.number="form.avg_speed" type="number" class="kv-input" />
<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">酸耗 (L)</div>
<input v-model.number="form.acid_consumption" type="number" class="kv-input" />
<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">质量等级</div>
<select v-model="form.quality_grade" class="kv-input">
<option value="A1">A1</option>
<option value="A2">A2</option>
<option value="B1">B1</option>
<option value="B2">B2</option>
<option value="C">C</option>
<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.operator" class="kv-input" />
<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>
@@ -136,65 +203,132 @@
</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: 20, coil_no: '', shift: '', start_date: '', end_date: '' },
dialogVisible: false, editRow: null, form: {},
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 = { ...this.query }
if (params.start_date) params.start_date = params.start_date + 'T00:00:00'
if (params.end_date) params.end_date = params.end_date + 'T23:59:59'
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)
this.tableData = res.data.items
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 }
},
fmtTime(t) { return t ? t.replace('T', ' ').slice(0, 16) : '—' },
gradeClass(g) {
if (g?.startsWith('A')) return 'badge-green'
if (g?.startsWith('B')) return 'badge-blue'
return 'badge-yellow'
},
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; this.form = row ? { ...row } : {}; this.dialogVisible = true
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 }
if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return }
this.saving = true
try {
if (this.editRow) await updateProductionRecord(this.editRow.id, this.form)
else await createProductionRecord(this.form)
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: 12px; &:hover { text-decoration: underline; } }
.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; width: 640px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
.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>