2026-05-27 16:38:40 +08:00
|
|
|
|
<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>
|
2026-05-27 17:04:48 +08:00
|
|
|
|
<input v-model="query.coil_no" class="kv-input" style="width:150px;" @keyup.enter="fetchData" />
|
2026-05-27 16:38:40 +08:00
|
|
|
|
</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 v-for="s in statusOptions" :key="s.value" :value="s.value">{{ s.label }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex-row">
|
|
|
|
|
|
<button class="btn btn-primary" @click="fetchData">查询</button>
|
|
|
|
|
|
<button class="btn btn-outline" @click="openDialog()">+ 新增钢卷</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-left:auto;" class="flex-row">
|
|
|
|
|
|
<span class="kv-label">共 <span class="kv-value">{{ total }}</span> 条</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 数据表 -->
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
📦 钢卷台账
|
|
|
|
|
|
<span class="ch-badge">{{ tableData.length }} / {{ total }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="table-scroll" v-loading="loading">
|
|
|
|
|
|
<table class="data-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>卷号</th><th>钢种</th><th>厚度(mm)</th><th>宽度(mm)</th>
|
|
|
|
|
|
<th>毛重(kg)</th><th>净重(kg)</th><th>状态</th><th>创建时间</th><th>操作</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-for="row in tableData" :key="row.coil_no">
|
|
|
|
|
|
<td class="td-num">{{ row.coil_no }}</td>
|
|
|
|
|
|
<td>{{ row.steel_grade || '—' }}</td>
|
|
|
|
|
|
<td class="td-num">{{ row.spec_thickness || '—' }}</td>
|
|
|
|
|
|
<td class="td-num">{{ row.spec_width || '—' }}</td>
|
|
|
|
|
|
<td class="td-num">{{ row.gross_weight || '—' }}</td>
|
|
|
|
|
|
<td class="td-num">{{ row.net_weight || '—' }}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<span :class="['badge', statusBadge(row.status)]">{{ statusLabel(row.status) }}</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="td-muted">{{ fmtTime(row.created_at) }}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<span class="action-link" @click="viewTracking(row)">跟踪</span>
|
|
|
|
|
|
<span class="action-link" @click="openDialog(row)">编辑</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr v-if="!tableData.length && !loading">
|
|
|
|
|
|
<td colspan="9" 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">
|
|
|
|
|
|
<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="form-field">
|
|
|
|
|
|
<div class="kv-label">卷号 <span style="color:var(--accent-red)">*</span></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.steel_grade" class="kv-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
|
<div class="kv-label">规格厚度 (mm)</div>
|
|
|
|
|
|
<input v-model.number="form.spec_thickness" type="number" class="kv-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
|
<div class="kv-label">规格宽度 (mm)</div>
|
|
|
|
|
|
<input v-model.number="form.spec_width" type="number" class="kv-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
|
<div class="kv-label">毛重 (kg)</div>
|
|
|
|
|
|
<input v-model.number="form.gross_weight" type="number" class="kv-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
|
<div class="kv-label">净重 (kg)</div>
|
|
|
|
|
|
<input v-model.number="form.net_weight" type="number" class="kv-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-field">
|
|
|
|
|
|
<div class="kv-label">内径 (mm)</div>
|
|
|
|
|
|
<input v-model.number="form.inner_diameter" type="number" class="kv-input" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn btn-outline" @click="dialogVisible=false">取消</button>
|
|
|
|
|
|
<button class="btn btn-primary" :disabled="saving" @click="saveCoil">{{ saving ? '保存中...' : '保存' }}</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 跟踪详情 Modal -->
|
|
|
|
|
|
<div v-if="trackingVisible" class="modal-mask" @click.self="trackingVisible=false">
|
|
|
|
|
|
<div class="modal-box" style="width:860px;max-width:95vw;">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
物料跟踪记录 — <span style="color:var(--sms-highlight)">{{ trackingCoil }}</span>
|
|
|
|
|
|
<span class="modal-close" @click="trackingVisible=false">✕</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body" style="max-height:400px;overflow-y:auto;">
|
|
|
|
|
|
<table class="data-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr><th>时间</th><th>位置</th><th>事件类型</th><th>描述</th><th>实测厚度</th><th>速度</th><th>操作员</th></tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-for="t in trackingData" :key="t.id">
|
|
|
|
|
|
<td class="td-muted">{{ fmtTime(t.event_time) }}</td>
|
|
|
|
|
|
<td>{{ t.position || '—' }}</td>
|
|
|
|
|
|
<td><span class="badge badge-blue">{{ t.event_type }}</span></td>
|
|
|
|
|
|
<td>{{ t.event_desc || '—' }}</td>
|
|
|
|
|
|
<td class="td-num">{{ t.actual_thickness || '—' }}</td>
|
|
|
|
|
|
<td class="td-num">{{ t.speed || '—' }}</td>
|
|
|
|
|
|
<td class="td-muted">{{ t.operator || '—' }}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr v-if="!trackingData.length">
|
|
|
|
|
|
<td colspan="7" class="td-muted" style="text-align:center;padding:20px;">暂无跟踪记录</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn btn-outline" @click="trackingVisible=false">关闭</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import { getCoils, createCoil, updateCoil, getTracking } from '@/api'
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_MAP = {
|
|
|
|
|
|
waiting: { label: '等待入线', badge: 'badge-gray' },
|
|
|
|
|
|
on_line: { label: '在线处理', badge: 'badge-green' },
|
|
|
|
|
|
finished: { label: '处理完成', badge: 'badge-blue' },
|
|
|
|
|
|
abnormal: { label: '异常', badge: 'badge-red' },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'Material',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
loading: false, saving: false,
|
|
|
|
|
|
tableData: [], total: 0,
|
|
|
|
|
|
query: { page: 1, page_size: 20, coil_no: '', status: '' },
|
|
|
|
|
|
statusOptions: Object.entries(STATUS_MAP).map(([value, { label }]) => ({ value, label })),
|
|
|
|
|
|
dialogVisible: false, editRow: null, form: {},
|
|
|
|
|
|
trackingVisible: false, trackingCoil: '', trackingData: [],
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created() { this.fetchData() },
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
async fetchData() {
|
|
|
|
|
|
this.loading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getCoils(this.query)
|
|
|
|
|
|
this.tableData = res.data.items
|
|
|
|
|
|
this.total = res.data.total
|
|
|
|
|
|
} finally { this.loading = false }
|
|
|
|
|
|
},
|
|
|
|
|
|
statusLabel(s) { return STATUS_MAP[s]?.label || s },
|
|
|
|
|
|
statusBadge(s) { return STATUS_MAP[s]?.badge || 'badge-gray' },
|
|
|
|
|
|
fmtTime(t) { return t ? t.replace('T', ' ').slice(0, 16) : '—' },
|
|
|
|
|
|
openDialog(row = null) {
|
|
|
|
|
|
this.editRow = row
|
|
|
|
|
|
this.form = row ? { ...row } : {}
|
|
|
|
|
|
this.dialogVisible = true
|
|
|
|
|
|
},
|
|
|
|
|
|
async saveCoil() {
|
|
|
|
|
|
if (!this.form.coil_no) { this.$message.error('卷号不能为空'); return }
|
|
|
|
|
|
this.saving = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.editRow) await updateCoil(this.form.coil_no, this.form)
|
|
|
|
|
|
else await createCoil(this.form)
|
|
|
|
|
|
this.$message.success('保存成功')
|
|
|
|
|
|
this.dialogVisible = false
|
|
|
|
|
|
this.fetchData()
|
|
|
|
|
|
} finally { this.saving = false }
|
|
|
|
|
|
},
|
|
|
|
|
|
async viewTracking(row) {
|
|
|
|
|
|
this.trackingCoil = row.coil_no
|
|
|
|
|
|
const res = await getTracking({ coil_no: row.coil_no, page_size: 100 })
|
|
|
|
|
|
this.trackingData = res.data.items
|
|
|
|
|
|
this.trackingVisible = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
@import '@/assets/styles/variables';
|
|
|
|
|
|
|
|
|
|
|
|
.action-link {
|
|
|
|
|
|
color: $sms-highlight;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
|
font-family: $font-main;
|
|
|
|
|
|
&:hover { text-decoration: underline; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-field { display: flex; flex-direction: column; gap: 5px; }
|
|
|
|
|
|
|
|
|
|
|
|
// Modal
|
|
|
|
|
|
.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-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;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|