feat(material): 去Tab分段展示 + 总图段位标签带 + 跟踪表段列 / 移除看板与产能分析

- 移除 Tab 切换,5 段(入口/酸洗/清洗/烘干/出口)顺序堆叠
- 总图 SVG 顶部加段位色带(5 段不同色),同时跟踪表新增「段」列
- 修复横向溢出(minmax(0,1fr) + auto-fit 槽卡片 + overflow-x:hidden)
- 删除菜单与路由中的「生产看板」「产能分析」,首页重定向到 /plan

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:04:33 +08:00
parent 45abd3586b
commit 7e18bc5a7f
8 changed files with 126 additions and 280 deletions

View File

@@ -14,14 +14,8 @@ const routes = [
path: '/',
component: () => import('@/views/Layout.vue'),
meta: { requiresAuth: true },
redirect: '/dashboard',
redirect: '/plan',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '生产看板', icon: 'el-icon-monitor', requiresAuth: true }
},
{
path: 'material',
name: 'Material',
@@ -70,12 +64,6 @@ const routes = [
component: () => import('@/views/Quality.vue'),
meta: { title: '质量管理', icon: 'el-icon-medal', requiresAuth: true }
},
{
path: 'capacity',
name: 'Capacity',
component: () => import('@/views/Capacity.vue'),
meta: { title: '产能分析', icon: 'el-icon-s-data', requiresAuth: true }
},
]
},
{ path: '*', redirect: '/' }

View File

@@ -70,7 +70,6 @@ const IC = {
}
const MENU = [
{ path: '/dashboard', title: '生产看板', icon: IC.dashboard },
{ path: '/plan', title: '计划管理', icon: IC.plan },
{ path: '/material', title: '物料跟踪', icon: IC.material },
{ path: '/production', title: '实绩管理', icon: IC.production },
@@ -79,7 +78,6 @@ const MENU = [
{ path: '/downtime', title: '停机管理', icon: IC.downtime },
{ path: '/inspection', title: '设备巡检', icon: IC.inspection },
{ path: '/quality', title: '质量管理', icon: IC.quality },
{ path: '/capacity', title: '产能分析', icon: IC.capacity },
]
export default {

View File

@@ -36,8 +36,16 @@
<div class="line-wrap card">
<div class="card-header">推拉酸洗线 - 物料跟踪总图</div>
<div class="line-body">
<svg viewBox="0 0 1900 280" preserveAspectRatio="xMidYMid meet" class="line-svg">
<rect x="0" y="0" width="1900" height="280" fill="#0a1218" />
<svg viewBox="0 -32 1900 312" preserveAspectRatio="xMidYMid meet" class="line-svg">
<rect x="0" y="-32" width="1900" height="312" fill="#0a1218" />
<!-- 段位标签带 -->
<g v-for="s in sections" :key="'sec-'+s.name">
<rect :x="s.bandX" y="-28" :width="s.bandW" height="22" :fill="s.color" opacity="0.18" rx="3"/>
<rect :x="s.bandX" y="-28" :width="s.bandW" height="22" fill="none" :stroke="s.color" stroke-width="1" opacity="0.7" rx="3"/>
<text :x="s.labelX" y="-13" text-anchor="middle" font-size="12" font-weight="bold" :fill="s.color"
font-family="Arial,sans-serif">{{ s.name }}</text>
</g>
<!-- 顶部标签 -->
<g v-for="eq in equipments" :key="'lab-'+eq.k" font-family="Arial,sans-serif">
@@ -232,19 +240,12 @@
</div>
</div>
<!-- 下半: 分段 Tab -->
<div class="card sec-card">
<div class="tab-bar">
<div v-for="t in tabs" :key="t.k" :class="['tab', { active: tab === t.k }]" @click="tab = t.k">
{{ t.label }}
<span class="tab-cnt">{{ t.count }}</span>
</div>
<div class="tab-spacer"></div>
<span class="hd-cnt">{{ rtItems.length }} 项实时数据</span>
<!-- 入口段 -->
<section class="sec">
<div class="sec-title-bar">入口段
<span class="ch-badge">在线 {{ onlinePlans.length }} / 生产中 {{ producingPlan ? 1 : 0 }}</span>
</div>
<!-- 入口段 -->
<div v-if="tab === 'entry'" class="pane">
<div class="pane">
<div class="pane-grid entry-grid">
<!-- 在线计划 + 移动按钮 -->
<div class="sub-card">
@@ -291,8 +292,13 @@
</div>
</div>
<!-- 酸洗段 -->
<div v-if="tab === 'acid'" class="pane">
</div>
</section>
<!-- 酸洗段 -->
<section class="sec">
<div class="sec-title-bar">酸洗段5 </div>
<div class="pane">
<div class="tank-grid">
<div v-for="(a, i) in acid" :key="'a'+i" class="sub-card">
<div class="sub-header acid">{{ i+1 }}# 酸洗槽</div>
@@ -323,8 +329,13 @@
</div>
</div>
<!-- 漂洗段 -->
<div v-if="tab === 'rinse'" class="pane">
</div>
</section>
<!-- 漂洗段 -->
<section class="sec">
<div class="sec-title-bar">漂洗段5 + 烘干</div>
<div class="pane">
<div class="tank-grid">
<div v-for="(r, i) in rinse" :key="'r'+i" class="sub-card">
<div class="sub-header rinse">{{ i+1 }}# 漂洗</div>
@@ -358,8 +369,13 @@
</div>
</div>
<!-- 出口段 -->
<div v-if="tab === 'exit'" class="pane">
</div>
</section>
<!-- 出口段 -->
<section class="sec">
<div class="sec-title-bar">出口段</div>
<div class="pane">
<div class="pane-grid two">
<div class="sub-card">
<div class="sub-header">三辊张力装置</div>
@@ -393,12 +409,18 @@
</div>
</div>
<!-- 跟踪表 -->
<div v-if="tab === 'track'" class="pane">
</div>
</section>
<!-- 跟踪表 -->
<section class="sec">
<div class="sec-title-bar">物料跟踪表 <span class="hd-cnt">{{ equipments.length }} 台设备</span></div>
<div class="pane">
<table class="data-table compact tracking-table">
<thead>
<tr>
<th style="width:32px;">#</th>
<th style="width:72px;"></th>
<th>设备</th>
<th style="width:64px;">状态</th>
<th>当前钢卷</th>
@@ -411,6 +433,11 @@
<tr v-for="(eq, i) in equipments" :key="eq.k"
:class="{ 'row-active': eq.k === currentEquipment.k, 'row-passed': i < currentEquipment.idx, 'row-pending': i > currentEquipment.idx }">
<td class="td-num">{{ i + 1 }}</td>
<td>
<span class="sec-tag" :style="{ color: sectionColor(eq.section), borderColor: sectionColor(eq.section) }">
{{ eq.section }}
</span>
</td>
<td>{{ eq.label }}</td>
<td>
<span v-if="eq.k === currentEquipment.k" class="badge badge-yellow">加工中</span>
@@ -425,7 +452,7 @@
</tbody>
</table>
</div>
</div>
</section>
</div>
</template>
@@ -435,27 +462,35 @@ function rnd(base, amp) { return base + (Math.random() - 0.5) * amp }
function fix(v, n = 1) { return Number(v).toFixed(n) }
const EQUIPMENTS = [
{ k:'uncoiler', label:'开卷机', type:'coiler', code:'DC-1' },
{ k:'straightener', label:'九辊矫直机', type:'rolls9', code:'STR-9' },
{ k:'crop_shear', label:'切头剪', type:'shear', code:'CRP' },
{ k:'acid1', label:'酸洗槽1', type:'acid', idx:0 },
{ k:'acid2', label:'酸洗槽2', type:'acid', idx:1 },
{ k:'acid3', label:'酸洗槽3', type:'acid', idx:2 },
{ k:'acid4', label:'酸洗槽4', type:'acid', idx:3 },
{ k:'acid5', label:'酸洗槽5', type:'acid', idx:4 },
{ k:'rinse', label:'漂洗段', type:'rinse' },
{ k:'dryer', label:'热风烘干段', type:'dryer' },
{ k:'br1', label:'1号夹送辊', type:'pinch', code:'BR-1' },
{ k:'loop', label:'活套坑', type:'loop' },
{ k:'br2', label:'2号夹送辊', type:'pinch', code:'BR-2' },
{ k:'br3', label:'3号夹送辊', type:'pinch', code:'BR-3' },
{ k:'tension', label:'三辊张力装置', type:'tension3', code:'TEN-3' },
{ k:'leveler', label:'平整机', type:'leveler', code:'SPM' },
{ k:'tail_shear', label:'切尾剪', type:'shear', code:'TLS' },
{ k:'oiler', label:'静电涂油机', type:'oiler', code:'EOL' },
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1' },
{ k:'uncoiler', label:'开卷机', type:'coiler', code:'DC-1', section:'入口段' },
{ k:'straightener', label:'九辊矫直机', type:'rolls9', code:'STR-9', section:'入口段' },
{ k:'crop_shear', label:'切头剪', type:'shear', code:'CRP', section:'入口段' },
{ k:'acid1', label:'酸洗槽1', type:'acid', idx:0, section:'酸洗段' },
{ k:'acid2', label:'酸洗槽2', type:'acid', idx:1, section:'酸洗段' },
{ k:'acid3', label:'酸洗槽3', type:'acid', idx:2, section:'酸洗段' },
{ k:'acid4', label:'酸洗槽4', type:'acid', idx:3, section:'酸洗段' },
{ k:'acid5', label:'酸洗槽5', type:'acid', idx:4, section:'酸洗段' },
{ k:'rinse', label:'漂洗段', type:'rinse', section:'清洗段' },
{ k:'dryer', label:'热风烘干段', type:'dryer', section:'烘干段' },
{ k:'br1', label:'1号夹送辊', type:'pinch', code:'BR-1', section:'出口段' },
{ k:'loop', label:'活套坑', type:'loop', section:'出口段' },
{ k:'br2', label:'2号夹送辊', type:'pinch', code:'BR-2', section:'出口段' },
{ k:'br3', label:'3号夹送辊', type:'pinch', code:'BR-3', section:'出口段' },
{ k:'tension', label:'三辊张力装置', type:'tension3', code:'TEN-3', section:'出口段' },
{ k:'leveler', label:'平整机', type:'leveler', code:'SPM', section:'出口段' },
{ k:'tail_shear', label:'切尾剪', type:'shear', code:'TLS', section:'出口段' },
{ k:'oiler', label:'静电涂油机', type:'oiler', code:'EOL', section:'出口段' },
{ k:'recoiler', label:'卷取机', type:'recoiler', code:'REC-1', section:'出口段' },
]
const SECTION_COLORS = {
'入口段': '#5a8fc8',
'酸洗段': '#ffaa44',
'清洗段': '#3aa0c8',
'烘干段': '#e87a3a',
'出口段': '#88c070',
}
// 默认辊缝值 (mm)
const DEFAULT_GAP = {
straightener: 4.20,
@@ -514,19 +549,9 @@ export default {
_plansTimer: null,
plans: [],
moving: false,
tab: 'entry',
}
},
computed: {
tabs() {
return [
{ k: 'entry', label: '入口段', count: this.onlinePlans.length },
{ k: 'acid', label: '酸洗段', count: 5 },
{ k: 'rinse', label: '漂洗段', count: 5 },
{ k: 'exit', label: '出口段', count: 3 },
{ k: 'track', label: '物料跟踪表', count: this.equipments.length },
]
},
entryItems() {
const f = (v, n=1) => Number(v).toFixed(n)
return [
@@ -554,6 +579,29 @@ export default {
const step = (xEnd - xStart) / (n - 1)
return EQUIPMENTS.map((e, i) => ({ ...e, x: xStart + step * i }))
},
sections() {
const eqs = this.equipments
const groups = []
let cur = null
eqs.forEach((e, i) => {
if (!cur || cur.name !== e.section) {
if (cur) groups.push(cur)
cur = { name: e.section, color: SECTION_COLORS[e.section] || '#9aa8b6',
startIdx: i, endIdx: i, x0: e.x, x1: e.x }
} else {
cur.endIdx = i
cur.x1 = e.x
}
})
if (cur) groups.push(cur)
const half = (eqs[1].x - eqs[0].x) / 2
return groups.map(g => ({
...g,
bandX: g.x0 - half + 4,
bandW: (g.x1 - g.x0) + half * 2 - 8,
labelX: (g.x0 + g.x1) / 2,
}))
},
weldX() {
const p = Math.max(0, Math.min(1, this.weld.position))
return 50 + (1850 - 50) * p
@@ -635,6 +683,7 @@ export default {
methods: {
fmt(v, n = 2) { return v != null && v !== '' ? Number(v).toFixed(n) : '—' },
fix(v, n = 1) { return Number(v).toFixed(n) },
sectionColor(s) { return SECTION_COLORS[s] || '#9aa8b6' },
fmtTime(t) { return t ? t.slice(0, 16).replace('T', ' ') : '—' },
async loadPlans() {
try {
@@ -777,7 +826,12 @@ export default {
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.mat-page { display: flex; flex-direction: column; gap: 10px; }
.mat-page { display: flex; flex-direction: column; gap: 10px; min-width: 0; overflow-x: hidden; }
.sec-tag {
display: inline-block; font-size: 10.5px; padding: 1px 6px;
border: 1px solid; border-radius: 3px; background: rgba(0,0,0,.25);
font-weight: 600; letter-spacing: 0.5px;
}
.status-bar {
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
@@ -791,34 +845,28 @@ export default {
.line-wrap { padding: 0; }
.line-body { padding: 6px 10px 10px; background: #0a1218; }
.line-svg { width: 100%; height: 280px; display: block; }
.line-svg { width: 100%; height: 312px; display: block; }
.sec-card { padding: 0; }
.tab-bar {
display: flex; align-items: center; gap: 2px;
border-bottom: 1px solid $border; padding: 0 10px;
background: #161d24;
.sec {
background: $bg-card; border: 1px solid $border; border-radius: 6px;
overflow: hidden;
}
.tab {
padding: 9px 16px; font-size: 12.5px; cursor: pointer;
color: $text-muted; border-bottom: 2px solid transparent;
user-select: none;
.sec-title-bar {
display: flex; align-items: center; gap: 8px;
padding: 7px 12px; font-size: 13px; color: #c8d4e0; font-weight: 600;
background: #161d24; border-bottom: 1px solid $border;
}
.tab:hover { color: #c8d4e0; }
.tab.active { color: $sms-highlight; border-bottom-color: $sms-highlight; font-weight: 600; }
.tab-cnt {
display: inline-block; margin-left: 5px;
background: rgba(255,255,255,.06); border-radius: 9px;
padding: 0 6px; font-size: 10px; color: #9aa8b6;
}
.tab.active .tab-cnt { background: rgba(0,200,255,.15); color: #00c8ff; }
.tab-spacer { flex: 1; }
.pane { padding: 10px 12px; }
.pane-grid { display: grid; grid-template-columns: 1fr 1.4fr; gap: 10px; }
.pane-grid.two { grid-template-columns: 1fr 1fr; }
.entry-grid { grid-template-columns: 1fr 1.3fr; }
.tank-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.pane-grid {
display: grid; gap: 10px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
}
.pane-grid.two { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.entry-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); }
.tank-grid {
display: grid; gap: 8px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.sub-card { background: #0f161c; border: 1px solid $border; border-radius: 4px; display: flex; flex-direction: column; }
.sub-header {