Commit 5febff30 authored by 崔婷婷's avatar 崔婷婷

接口修改 v2

parent 472c0975
<template> <template>
<div class="dashboard"> <div class="dashboard">
<!-- ======== 顶部信息栏(5.1) ======== --> <!-- ======== 顶部信息栏 ======== -->
<header class="header"> <header class="header">
<div class="header-left"> <div class="header-left">
<span class="meta-item">批次号:<em>{{ batch.batch_id }}</em></span> <span class="meta-item">批次号:<em>{{ batch.batch_id }}</em></span>
<span class="meta-item">产品类型:<em>{{ batch.product_type }}</em></span> <span class="meta-item">产品型号:<em>{{ batch.product_type }}</em></span>
<span class="meta-item">系列:<em>{{ batch.series }}</em></span> <span class="meta-item">系列:<em>{{ batch.series }}</em></span>
<span class="meta-item">工位:<em>{{ workstation.workstation_id }}</em></span> <span class="meta-item">工位:<em>{{ workstation.workstation_id }}</em></span>
<span class="meta-item">员工ID:<em>{{ operator.operator_id }}</em></span> <span class="meta-item">员工ID:<em>{{ operator.operator_id }}</em></span>
<span class="meta-item">开始时间:<em>{{ fmtCycleTime }}</em></span> <span class="meta-item">开始时间:<em>{{ fmtProductTime }}</em></span>
</div> </div>
<h1 class="title">飞天兔AI行为引导系统</h1> <h1 class="title">飞天兔AI行为引导系统</h1>
<div class="header-right"> <div class="header-right">
<span class="meta-item">当前工序:<em>{{ currentStepName }}</em></span> <span class="meta-item">当前工序:<em>{{ currentStepName }}</em></span>
<span class="meta-item">{{ currentTime }}</span> <span class="meta-item">{{ currentTime }}</span>
<span class="meta-item">系统编号:<em>{{ cycleId }}</em></span> <span class="meta-item">产品序号:<em>{{ productId }}</em></span>
</div> </div>
</header> </header>
<!-- ======== 系统状态栏(5.5) ======== --> <!-- ======== 系统状态栏 ======== -->
<div class="system-bar"> <div class="system-bar">
<span class="sys-item">FPS:<em>{{ system.fps }}</em></span> <span class="sys-item">FPS:<em>{{ system.fps }}</em></span>
<span class="sys-item">推理耗时:<em>{{ system.inference_ms }}ms</em></span> <span class="sys-item">推理耗时:<em>{{ system.inference_ms }}ms</em></span>
...@@ -28,50 +28,45 @@ ...@@ -28,50 +28,45 @@
<em>{{ system.camera_connected ? '已连接' : '断开' }}</em> <em>{{ system.camera_connected ? '已连接' : '断开' }}</em>
</span> </span>
<span v-if="wsReconnecting" class="sys-item warn">重连中 {{ wsReconnectAttempt }}/{{ isFinite(wsReconnectMax) ? wsReconnectMax : '∞' }}…</span> <span v-if="wsReconnecting" class="sys-item warn">重连中 {{ wsReconnectAttempt }}/{{ isFinite(wsReconnectMax) ? wsReconnectMax : '∞' }}…</span>
<span v-if="stats.skip_missing_count > 0" class="sys-item warn">跳步违规:<em>{{ stats.skip_missing_count }}</em> 次</span>
</div> </div>
<main class="main"> <main class="main">
<!-- ======== 左侧:视频 + 工序进度指示(5.2) ======== --> <!-- ======== 左侧:视频画面 ======== -->
<section class="panel video-panel"> <section class="panel video-panel">
<div class="video-wrap"> <div class="video-wrap">
<img :src="videoUrl" class="video-stream" /> <img :src="videoUrl" class="video-stream" />
<span class="video-label">MJPEG 实时画面</span> <span class="video-label">MJPEG 实时画面</span>
</div> </div>
<!--
<div class="step-mini-grid" :style="{ gridTemplateColumns: `repeat(${displaySteps.length}, 1fr)` }">
<div
v-for="s in displaySteps"
:key="s.step_index"
class="step-mini-item"
:class="miniItemClass(s)"
>
<span class="step-mini-no">{{ s.step_index }}</span>
<span class="step-mini-name">{{ s.step_name }}</span>
<span class="badge step-mini-badge" :class="statusBadgeClass(s.status)">{{ statusLabel(s.status) }}</span>
</div>
</div>
-->
</section> </section>
<!-- ======== 右侧:SOP 工序列表(5.3)+ 状态环 + 统计(5.4) ======== --> <!-- ======== 右侧:SOP 工序列表 + 状态环 + 统计 ======== -->
<section class="info-panel"> <section class="info-panel">
<div class="panel steps-card"> <div class="panel steps-card">
<div class="card-header"> <div class="card-header">
<span>工序明细</span> <span>工序明细</span>
<span class="progress-count">{{ passedCount }}/{{ displaySteps.length }}</span> <span class="progress-count">{{ completedCount }}/{{ displaySteps.length }}</span>
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<div class="steps-header"> <div class="steps-header">
<span>NO</span> <span>NO</span>
<span>工序</span> <span>工序</span>
<span>时长(s)</span> <span>帧进度</span>
<span>状态</span> <span>状态</span>
</div> </div>
<div class="steps-body"> <div class="steps-body">
<div v-for="s in displaySteps" :key="s.step_index" class="step-row"> <div
v-for="s in displaySteps"
:key="s.step_index"
class="step-row"
:class="{ 'row-skip': s.anomaly_type === 'skip_missing' }"
>
<span>{{ s.step_index }}</span> <span>{{ s.step_index }}</span>
<span>{{ s.step_name }}</span> <span>
<span class="duration-cell">{{ s.display_duration.toFixed(1) }}/{{ s.target_duration.toFixed(1) }}</span> {{ s.step_name }}
<i v-if="s.anomaly_type === 'skip_missing'" class="skip-icon" title="跳步缺失">⚠</i>
</span>
<span class="duration-cell">{{ s.framesText }}</span>
<span class="badge" :class="statusBadgeClass(s.status)">{{ statusLabel(s.status) }}</span> <span class="badge" :class="statusBadgeClass(s.status)">{{ statusLabel(s.status) }}</span>
</div> </div>
</div> </div>
...@@ -88,7 +83,7 @@ ...@@ -88,7 +83,7 @@
<ul class="status-detail"> <ul class="status-detail">
<li><span>整双结果</span><em :class="resultClass">{{ resultLabel }}</em></li> <li><span>整双结果</span><em :class="resultClass">{{ resultLabel }}</em></li>
<li><span>结束类型</span><em :class="endTypeClass">{{ endTypeLabel }}</em></li> <li><span>结束类型</span><em :class="endTypeClass">{{ endTypeLabel }}</em></li>
<li><span>周期耗时</span><em>{{ cycleDuration.toFixed(1) }}s</em></li> <li><span>产品耗时</span><em>{{ productDuration.toFixed(1) }}s</em></li>
</ul> </ul>
</div> </div>
...@@ -98,22 +93,22 @@ ...@@ -98,22 +93,22 @@
<span class="stat-value accent">{{ stats.pass_rate_pct }}%</span> <span class="stat-value accent">{{ stats.pass_rate_pct }}%</span>
</div> </div>
<div class="panel stat-card"> <div class="panel stat-card">
<span class="stat-label">平均耗时</span> <span class="stat-label">总数</span>
<span class="stat-value accent">{{ stats.avg_duration }}s</span> <span class="stat-value accent">{{ stats.total_products }}</span>
</div> </div>
<div class="panel stat-card"> <div class="panel stat-card">
<span class="stat-label">不通过双数</span> <span class="stat-label">平均耗时</span>
<span class="stat-value warn">{{ stats.failed_cycles }}</span> <span class="stat-value">{{ stats.avg_duration }}s</span>
</div> </div>
<div class="panel stat-card"> <div class="panel stat-card">
<span class="stat-label">总双数</span> <span class="stat-label">跳步次数</span>
<span class="stat-value">{{ stats.total_cycles }}</span> <span class="stat-value warn">{{ stats.skip_missing_count }}</span>
</div> </div>
</div> </div>
</section> </section>
</main> </main>
<!-- 跳步/错误弹窗 --> <!-- 跳步/错误 Toast -->
<Teleport to="body"> <Teleport to="body">
<div v-if="alertMsg" class="alert-toast" :class="alertMsg.level" @click="alertMsg = null"> <div v-if="alertMsg" class="alert-toast" :class="alertMsg.level" @click="alertMsg = null">
<span>{{ alertMsg.text }}</span> <span>{{ alertMsg.text }}</span>
...@@ -126,37 +121,38 @@ ...@@ -126,37 +121,38 @@
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue' import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { connect } from '../ws/index.js' import { connect } from '../ws/index.js'
// ==================== 数据模型(按文档 2.1~2.9) ==================== // ==================== 数据模型(v2) ====================
// 2.1 工位 // 工位 / 操作员 / 批次
const workstation = reactive({ workstation_id: '-', name: '-', area: '-', line: '-' }) const workstation = reactive({ workstation_id: '-', name: '-', area: '-', line: '-' })
// 2.2 操作员
const operator = reactive({ operator_id: '-', name: '-' }) const operator = reactive({ operator_id: '-', name: '-' })
// 2.3 批次
const batch = reactive({ batch_id: '-', product_type: '-', series: '-', started_at: '-' }) const batch = reactive({ batch_id: '-', product_type: '-', series: '-', started_at: '-' })
// 2.4 工序配置
// 2.1 工序配置
const stepConfigs = ref([]) const stepConfigs = ref([])
// 2.7 周期
const cycleId = ref('-') // 2.4/2.5 产品
const cycleResult = ref(null) // 'pass' | 'fail' | null const productId = ref('-')
const cycleEndType = ref(null) // 'flow_complete' | 'timeout' | 'material_change' | 'manual_reset' | null const productResult = ref(null) // 'pass' | 'fail' | null
const cycleDuration = ref(0) const productEndType = ref(null) // 'flow_complete' | 'timeout' | 'material_change' | 'manual_reset' | null
const cycleStartedAt = ref('') const productDuration = ref(0)
const passedCount = ref(0) const productStartedAt = ref('')
const currentStepIndex = ref(1) const currentStepIndex = ref(1)
// 2.5 + 2.6 工序状态 → stepStates[step_index] = { status, actual_duration } // 工序状态 → stepStates[step_index] = { status, anomaly_type, best_confidence }
const stepStates = reactive({}) const stepStates = reactive({})
// 3.3.4 step_progress → 当前 active 工序实时进度
const activeProgress = reactive({ step_index: 0, current_duration: 0, target_duration: 0, progress: 0 })
// 2.8 统计 // 3.3 step_progress → 当前 active 工序实时的帧进度
const activeProgress = reactive({ step_index: 0, consecutive_frames: 0, target_frames: 0, best_confidence: 0 })
// 2.6 统计
const stats = reactive({ const stats = reactive({
total_cycles: 0, passed_cycles: 0, failed_cycles: 0, total_products: 0, passed_products: 0, failed_products: 0,
pass_rate: 0, pass_rate_pct: 0, avg_duration: 0, pass_rate: 0, pass_rate_pct: 0, avg_duration: 0,
skip_missing_count: 0, ai_misrecognition_count: 0,
}) })
// 2.9 系统状态 // 2.7 系统状态
const system = reactive({ const system = reactive({
fps: 0, inference_ms: 0, model_loaded: false, fps: 0, inference_ms: 0, model_loaded: false,
model_version: '-', camera_connected: false, model_version: '-', camera_connected: false,
...@@ -179,16 +175,27 @@ let clockTimer = null ...@@ -179,16 +175,27 @@ let clockTimer = null
// 合并 step_configs + stepStates + activeProgress → 展示行 // 合并 step_configs + stepStates + activeProgress → 展示行
const displaySteps = computed(() => stepConfigs.value.map(cfg => { const displaySteps = computed(() => stepConfigs.value.map(cfg => {
const state = stepStates[cfg.step_index] || { status: 'pending', actual_duration: 0 } const state = stepStates[cfg.step_index] || { status: 'pending', anomaly_type: 'none' }
const isActive = state.status === 'active' const isActive = state.status === 'active'
const isCompleted = state.status === 'completed' || state.status === 'incomplete'
// 帧进度文本
let framesText = '-'
if (isActive && activeProgress.target_frames > 0) {
framesText = `${activeProgress.consecutive_frames}/${activeProgress.target_frames}`
} else if (isActive) {
framesText = '...'
} else if (isCompleted) {
framesText = '已完成'
}
return { return {
step_index: cfg.step_index, step_index: cfg.step_index,
step_code: cfg.step_code, step_code: cfg.step_code,
step_name: cfg.step_name, step_name: cfg.step_name,
target_duration: cfg.pass_duration,
window_timeout: cfg.window_timeout,
status: state.status, status: state.status,
display_duration: isActive ? activeProgress.current_duration : (state.actual_duration || 0), anomaly_type: state.anomaly_type || 'none',
framesText,
} }
})) }))
...@@ -197,77 +204,78 @@ const currentStepName = computed(() => { ...@@ -197,77 +204,78 @@ const currentStepName = computed(() => {
return cs ? cs.step_name : '-' return cs ? cs.step_name : '-'
}) })
// 格式化 cycleStartedAt(ISO 8601 → HH:MM:SS) const completedCount = computed(() =>
const fmtCycleTime = computed(() => { displaySteps.value.filter(s => s.status === 'completed' || s.status === 'incomplete').length
if (!cycleStartedAt.value) return '-' )
// "2026-07-09T08:42:05+08:00" → 取 T 后面的 HH:MM:SS
const m = cycleStartedAt.value.match(/T(\d{2}:\d{2}:\d{2})/) // 格式化 started_at → HH:MM:SS
const fmtProductTime = computed(() => {
if (!productStartedAt.value) return '-'
const m = productStartedAt.value.match(/T(\d{2}:\d{2}:\d{2})/)
return m ? m[1] : '-' return m ? m[1] : '-'
}) })
// 状态环 // 状态环
const ringMainText = computed(() => { const ringMainText = computed(() => {
if (cycleEndType.value) return cycleResult.value === 'pass' ? '通过' : '不通过' if (productEndType.value) return productResult.value === 'pass' ? '通过' : '不通过'
if (activeProgress.step_index > 0) return '检测中' if (activeProgress.step_index > 0) return '检测中'
return '待机' return '待机'
}) })
const ringSubText = computed(() => { const ringSubText = computed(() => {
if (cycleEndType.value) return endTypeLabel.value if (productEndType.value) return endTypeLabel.value
const pct = activeProgress.progress || 0 if (activeProgress.target_frames > 0) {
return `${Math.round(pct * 100)}%` return `${Math.round((activeProgress.consecutive_frames / activeProgress.target_frames) * 100)}%`
}
return ''
}) })
const ringMainClass = computed(() => { const ringMainClass = computed(() => {
if (!cycleEndType.value) return '' if (!productEndType.value) return ''
return cycleResult.value === 'pass' ? 'ring-pass' : 'ring-fail' return productResult.value === 'pass' ? 'ring-pass' : 'ring-fail'
}) })
const ringStyle = computed(() => { const ringStyle = computed(() => {
if (cycleEndType.value) { if (productEndType.value) {
const color = cycleResult.value === 'pass' ? '#34d399' : '#f87171' const color = productResult.value === 'pass' ? '#34d399' : '#f87171'
return { background: color } return { background: color }
} }
const pct = Math.min(100, (activeProgress.progress || 0) * 100) if (activeProgress.target_frames > 0) {
const pct = Math.min(100, (activeProgress.consecutive_frames / activeProgress.target_frames) * 100)
return { background: `conic-gradient(#3ddc84 ${pct}%, rgba(255,255,255,0.08) ${pct}%)` } return { background: `conic-gradient(#3ddc84 ${pct}%, rgba(255,255,255,0.08) ${pct}%)` }
}
return { background: 'rgba(255,255,255,0.08)' }
}) })
// 结果 / 结束类型中文 // 结果 / 结束类型中文
const resultLabel = computed(() => { const resultLabel = computed(() => {
if (cycleResult.value === 'pass') return '通过' if (productResult.value === 'pass') return '通过'
if (cycleResult.value === 'fail') return '不通过' if (productResult.value === 'fail') return '不通过'
return '-' return '-'
}) })
const resultClass = computed(() => { const resultClass = computed(() => {
if (cycleResult.value === 'pass') return 'text-pass' if (productResult.value === 'pass') return 'text-pass'
if (cycleResult.value === 'fail') return 'text-fail' if (productResult.value === 'fail') return 'text-fail'
return '' return ''
}) })
const endTypeLabel = computed(() => { const endTypeLabel = computed(() => {
const map = { flow_complete: '流程结束', timeout: '超时结束', material_change: '物料切换', manual_reset: '手动复位' } const map = { flow_complete: '流程结束', timeout: '超时结束', material_change: '物料切换', manual_reset: '手动复位' }
return map[cycleEndType.value] || '-' return map[productEndType.value] || '-'
}) })
const endTypeClass = computed(() => cycleEndType.value === 'timeout' ? 'text-fail' : '') const endTypeClass = computed(() => productEndType.value === 'timeout' ? 'text-fail' : '')
// ---- UI 帮助函数 ---- // UI 帮助函数
function statusLabel(s) { function statusLabel(s) {
const map = { pending: '待检', active: '进行中', pass: '通过', fail: '未达标', missing: '缺失' } const map = { pending: '待检', active: '进行中', completed: '已完成', incomplete: '未完成' }
return map[s] || s return map[s] || s
} }
function statusBadgeClass(s) { function statusBadgeClass(s) {
if (s === 'active') return 'badge-progress' if (s === 'active') return 'badge-progress'
if (s === 'pass') return 'badge-pass' if (s === 'completed') return 'badge-pass'
if (s === 'fail' || s === 'missing') return 'badge-fail' if (s === 'incomplete') return 'badge-fail'
return 'badge-wait' return 'badge-wait'
} }
function miniItemClass(s) {
return {
active: s.status === 'active',
pass: s.status === 'pass',
fail: s.status === 'fail' || s.status === 'missing',
}
}
// ==================== 时钟 ==================== // ==================== 时钟 ====================
function updateClock() { function updateClock() {
...@@ -280,133 +288,146 @@ function updateClock() { ...@@ -280,133 +288,146 @@ function updateClock() {
function handleSnapshot(msg) { function handleSnapshot(msg) {
const d = msg.data const d = msg.data
// 2.1
if (d.workstation) Object.assign(workstation, d.workstation) if (d.workstation) Object.assign(workstation, d.workstation)
// 2.2
if (d.operator) Object.assign(operator, d.operator) if (d.operator) Object.assign(operator, d.operator)
// 2.3
if (d.batch) Object.assign(batch, d.batch) if (d.batch) Object.assign(batch, d.batch)
// 2.4
if (d.step_configs) stepConfigs.value = d.step_configs if (d.step_configs) stepConfigs.value = d.step_configs
// 2.8
if (d.stats) { if (d.stats) {
Object.assign(stats, d.stats) Object.assign(stats, d.stats)
stats.pass_rate_pct = Math.round(stats.pass_rate * 100) stats.pass_rate_pct = Math.round(stats.pass_rate * 100)
} }
// 2.9
if (d.system) Object.assign(system, d.system) if (d.system) Object.assign(system, d.system)
// 2.7 当前周期 if (d.current_product) {
if (d.current_cycle) { applyProduct(d.current_product)
applyCycle(d.current_cycle) productEndType.value = d.current_product.end_type || null
cycleEndType.value = d.current_cycle.end_type || null productResult.value = d.current_product.result || null
cycleResult.value = d.current_cycle.result || null productDuration.value = d.current_product.duration || 0
cycleDuration.value = d.current_cycle.duration || 0
} }
} }
function applyCycle(cycle) { function applyProduct(product) {
cycleId.value = cycle.cycle_id || '-' productId.value = product.product_id || '-'
cycleStartedAt.value = cycle.started_at || '' productStartedAt.value = product.started_at || ''
passedCount.value = cycle.step_count_passed || 0 currentStepIndex.value = product.current_step_index || 1
currentStepIndex.value = cycle.current_step_index || 1
// 初始化 stepStates
Object.keys(stepStates).forEach(k => delete stepStates[k]) Object.keys(stepStates).forEach(k => delete stepStates[k])
if (cycle.steps) { if (product.steps) {
cycle.steps.forEach(s => { product.steps.forEach(s => {
stepStates[s.step_index] = { stepStates[s.step_index] = {
status: s.status, status: s.status,
actual_duration: s.actual_duration || 0, anomaly_type: s.anomaly_type || 'none',
best_confidence: s.best_confidence || 0,
} }
}) })
} }
} }
function handleCycleStart(msg) { // 3.3 product_start
function handleProductStart(msg) {
const d = msg.data const d = msg.data
cycleId.value = d.cycle_id productId.value = d.product_id
cycleStartedAt.value = d.started_at productStartedAt.value = d.started_at
cycleEndType.value = null productEndType.value = null
cycleResult.value = null productResult.value = null
cycleDuration.value = 0 productDuration.value = 0
currentStepIndex.value = d.step_index || 1 currentStepIndex.value = d.step_index || 1
passedCount.value = 0
activeProgress.step_index = 0
activeProgress.current_duration = 0
activeProgress.progress = 0
// 重置所有步骤 Object.assign(activeProgress, { step_index: 0, consecutive_frames: 0, target_frames: 0, best_confidence: 0 })
Object.keys(stepStates).forEach(k => delete stepStates[k]) Object.keys(stepStates).forEach(k => delete stepStates[k])
stepConfigs.value.forEach(sc => { stepConfigs.value.forEach(sc => {
stepStates[sc.step_index] = { status: 'pending', actual_duration: 0 } const isInitial = sc.step_index === (d.step_index || 1)
stepStates[sc.step_index] = {
status: isInitial ? 'active' : 'pending',
anomaly_type: 'none',
}
}) })
} }
function handleStepUpdate(msg) { // 3.3 step_confirmed
function handleStepConfirmed(msg) {
const d = msg.data const d = msg.data
// 当前工序 → completed/incomplete
stepStates[d.step_index] = { stepStates[d.step_index] = {
status: d.status, status: d.status,
actual_duration: d.actual_duration || 0, anomaly_type: d.anomaly_type || 'none',
best_confidence: d.best_confidence || 0,
} }
if (d.next_step_index) currentStepIndex.value = d.next_step_index
// 如果新工序 active,重置进度 if (d.next_step_index) {
if (d.status === 'active') { currentStepIndex.value = d.next_step_index
activeProgress.step_index = d.step_index // 下一道工序 → active
activeProgress.current_duration = 0 stepStates[d.next_step_index] = {
activeProgress.target_duration = d.target_duration status: 'active',
activeProgress.progress = 0 anomaly_type: 'none',
best_confidence: 0,
}
Object.assign(activeProgress, {
step_index: d.next_step_index,
consecutive_frames: 0,
target_frames: 0,
best_confidence: 0,
})
} else {
// 最后一道,清进度
Object.assign(activeProgress, { step_index: 0, consecutive_frames: 0, target_frames: 0, best_confidence: 0 })
} }
// 更新通过计数
passedCount.value = Object.values(stepStates).filter(s => s.status === 'pass').length
} }
// 3.3 step_progress
function handleStepProgress(msg) { function handleStepProgress(msg) {
const d = msg.data const d = msg.data
activeProgress.step_index = d.step_index // 确保工序状态为 active(防御:step_confirmed 可能尚未到达)
activeProgress.current_duration = d.current_duration if (stepStates[d.step_index] && stepStates[d.step_index].status !== 'active') {
activeProgress.target_duration = d.target_duration stepStates[d.step_index] = { status: 'active', anomaly_type: 'none', best_confidence: 0 }
activeProgress.progress = d.progress }
Object.assign(activeProgress, {
step_index: d.step_index,
consecutive_frames: d.consecutive_frames,
target_frames: d.target_frames,
best_confidence: d.best_confidence,
})
} }
function handleCycleEnd(msg) { // 3.3 product_end
function handleProductEnd(msg) {
const d = msg.data const d = msg.data
cycleEndType.value = d.end_type productEndType.value = d.end_type
cycleResult.value = d.result productResult.value = d.result
cycleDuration.value = d.duration || 0 productDuration.value = d.duration || 0
passedCount.value = d.step_count_passed || 0
// 更新最终步骤状态
if (d.steps && d.steps.length) { if (d.steps && d.steps.length) {
d.steps.forEach(s => { d.steps.forEach(s => {
stepStates[s.step_index] = { stepStates[s.step_index] = {
status: s.status, status: s.status,
actual_duration: s.actual_duration || 0, anomaly_type: s.anomaly_type || 'none',
best_confidence: s.best_confidence || 0,
} }
}) })
} }
activeProgress.step_index = 0 Object.assign(activeProgress, { step_index: 0, consecutive_frames: 0, target_frames: 0, best_confidence: 0 })
activeProgress.progress = 0
activeProgress.current_duration = 0
} }
// 3.3 stats_update
function handleStatsUpdate(msg) { function handleStatsUpdate(msg) {
const d = msg.data const d = msg.data
Object.assign(stats, d) Object.assign(stats, d)
stats.pass_rate_pct = Math.round(d.pass_rate * 100) stats.pass_rate_pct = Math.round(d.pass_rate * 100)
} }
// 3.3 system_status
function handleSystemStatus(msg) { function handleSystemStatus(msg) {
Object.assign(system, msg.data) Object.assign(system, msg.data)
} }
// 3.3 skip_detected
function handleSkipDetected(msg) { function handleSkipDetected(msg) {
alertMsg.value = { text: msg.data.message, level: 'warn' } alertMsg.value = { text: msg.data.message, level: 'warn' }
setTimeout(() => { if (alertMsg.value && alertMsg.value.text === msg.data.message) alertMsg.value = null }, 5000) setTimeout(() => { if (alertMsg.value && alertMsg.value.text === msg.data.message) alertMsg.value = null }, 5000)
} }
// 3.3 error
function handleError(msg) { function handleError(msg) {
// 区分后端业务错误(有 data.code)与 WebSocket 传输层错误
if (msg.data && msg.data.code) { if (msg.data && msg.data.code) {
alertMsg.value = { text: `[${msg.data.code}] ${msg.data.message}`, level: 'error' } alertMsg.value = { text: `[${msg.data.code}] ${msg.data.message}`, level: 'error' }
} else { } else {
...@@ -422,13 +443,13 @@ onMounted(() => { ...@@ -422,13 +443,13 @@ onMounted(() => {
const ws = connect() const ws = connect()
.on('snapshot', handleSnapshot) .on('snapshot', handleSnapshot)
.on('cycle_start', handleCycleStart) .on('product_start', handleProductStart)
.on('step_update', handleStepUpdate) .on('step_confirmed', handleStepConfirmed)
.on('step_progress', handleStepProgress) .on('step_progress', handleStepProgress)
.on('cycle_end', handleCycleEnd) .on('skip_detected', handleSkipDetected)
.on('product_end', handleProductEnd)
.on('stats_update', handleStatsUpdate) .on('stats_update', handleStatsUpdate)
.on('system_status', handleSystemStatus) .on('system_status', handleSystemStatus)
.on('skip_detected', handleSkipDetected)
.on('error', handleError) .on('error', handleError)
.on('reconnecting', (data) => { .on('reconnecting', (data) => {
wsReconnecting.value = true wsReconnecting.value = true
...@@ -440,7 +461,6 @@ onMounted(() => { ...@@ -440,7 +461,6 @@ onMounted(() => {
wsReconnectAttempt.value = 0 wsReconnectAttempt.value = 0
}) })
// 挂载到组件实例,便于卸载时清理
window.__qcWs = ws window.__qcWs = ws
}) })
...@@ -471,10 +491,10 @@ onBeforeUnmount(() => { ...@@ -471,10 +491,10 @@ onBeforeUnmount(() => {
overflow: hidden; overflow: hidden;
background: radial-gradient(circle at 20% 0%, #10224a 0%, var(--bg) 55%); background: radial-gradient(circle at 20% 0%, #10224a 0%, var(--bg) 55%);
color: var(--text); color: var(--text);
padding: clamp(8px, 1.2vw, 24px); padding: clamp(6px, 1vw, 12px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 6px;
} }
/* ======== 顶部信息栏 ======== */ /* ======== 顶部信息栏 ======== */
...@@ -484,18 +504,18 @@ onBeforeUnmount(() => { ...@@ -484,18 +504,18 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: clamp(8px, 1vw, 14px); gap: 8px;
background: linear-gradient(180deg, rgba(21, 42, 84, 0.9), rgba(13, 27, 58, 0.6)); background: linear-gradient(180deg, rgba(21, 42, 84, 0.9), rgba(13, 27, 58, 0.6));
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
padding: clamp(8px, 1vw, 20px) clamp(16px, 1.6vw, 32px); padding: clamp(8px, 1vw, 14px) clamp(14px, 1.4vw, 24px);
} }
.title { .title {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
font-size: clamp(18px, 1.6vw, 34px); font-size: clamp(18px, 1.6vw, 30px);
font-weight: 700; font-weight: 700;
letter-spacing: 2px; letter-spacing: 2px;
color: #fff; color: #fff;
...@@ -507,9 +527,9 @@ onBeforeUnmount(() => { ...@@ -507,9 +527,9 @@ onBeforeUnmount(() => {
.header-left, .header-left,
.header-right { .header-right {
display: flex; display: flex;
gap: clamp(12px, 1.5vw, 24px); gap: clamp(10px, 1.2vw, 18px);
flex-wrap: nowrap; flex-wrap: nowrap;
font-size: clamp(11px, 0.85vw, 18px); font-size: clamp(11px, 0.85vw, 16px);
color: var(--text-dim); color: var(--text-dim);
align-items: center; align-items: center;
} }
...@@ -529,8 +549,8 @@ onBeforeUnmount(() => { ...@@ -529,8 +549,8 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: clamp(14px, 1.5vw, 24px); gap: clamp(14px, 1.5vw, 24px);
padding: clamp(3px, 0.4vw, 8px) clamp(16px, 1.6vw, 28px); padding: 3px 18px;
font-size: clamp(11px, 0.85vw, 17px); font-size: clamp(11px, 0.85vw, 15px);
color: var(--text-dim); color: var(--text-dim);
flex-shrink: 0; flex-shrink: 0;
} }
...@@ -544,6 +564,9 @@ onBeforeUnmount(() => { ...@@ -544,6 +564,9 @@ onBeforeUnmount(() => {
.sys-item.warn { .sys-item.warn {
color: var(--warn); color: var(--warn);
} }
.sys-item.warn em {
color: var(--warn);
}
.cam-dot { .cam-dot {
display: inline-block; display: inline-block;
...@@ -563,7 +586,7 @@ onBeforeUnmount(() => { ...@@ -563,7 +586,7 @@ onBeforeUnmount(() => {
.main { .main {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: minmax(0, 2.1fr) minmax(300px, 1fr); grid-template-columns: minmax(0, 2.4fr) minmax(290px, 1fr);
gap: 8px; gap: 8px;
min-height: 0; min-height: 0;
} }
...@@ -583,7 +606,6 @@ onBeforeUnmount(() => { ...@@ -583,7 +606,6 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 8px; padding: 8px;
gap: 8px;
} }
.video-wrap { .video-wrap {
...@@ -606,71 +628,12 @@ onBeforeUnmount(() => { ...@@ -606,71 +628,12 @@ onBeforeUnmount(() => {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 12px; right: 12px;
font-size: clamp(10px, 0.7vw, 14px); font-size: 11px;
color: rgba(255, 255, 255, 0.45); color: rgba(255, 255, 255, 0.45);
letter-spacing: 1px; letter-spacing: 1px;
pointer-events: none; pointer-events: none;
} }
/* ======== 工序进度指示 ======== */
.step-mini-grid {
display: grid;
gap: clamp(3px, 0.4vw, 6px);
flex-shrink: 0;
}
.step-mini-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
border-radius: 6px;
padding: clamp(6px, 0.7vh, 10px) clamp(3px, 0.4vw, 6px);
transition: border-color 0.3s, box-shadow 0.3s;
}
.step-mini-item.active {
border-color: rgba(245, 158, 11, 0.6);
box-shadow: 0 0 6px rgba(245, 158, 11, 0.2);
}
.step-mini-item.pass {
border-color: rgba(52, 211, 153, 0.4);
}
.step-mini-item.fail {
border-color: rgba(248, 113, 113, 0.4);
}
.step-mini-no {
display: inline-flex;
align-items: center;
justify-content: center;
width: clamp(15px, 1.4vw, 20px);
height: clamp(15px, 1.4vw, 20px);
border-radius: 4px;
background: rgba(56, 189, 248, 0.15);
color: var(--accent);
font-size: clamp(9px, 0.9vw, 12px);
font-weight: 700;
flex-shrink: 0;
}
.step-mini-name {
font-size: clamp(10px, 1vw, 16px);
color: var(--text);
white-space: nowrap;
text-align: center;
}
.step-mini-badge {
font-size: clamp(8px, 0.8vw, 12px) !important;
padding: 1px 4px !important;
}
/* ======== 工序明细表 ======== */ /* ======== 工序明细表 ======== */
.info-panel { .info-panel {
display: flex; display: flex;
...@@ -680,8 +643,8 @@ onBeforeUnmount(() => { ...@@ -680,8 +643,8 @@ onBeforeUnmount(() => {
} }
.steps-card { .steps-card {
flex: 10; flex: 12;
padding: clamp(8px, 0.9vw, 12px); padding: clamp(8px, 0.9vw, 14px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
...@@ -690,7 +653,7 @@ onBeforeUnmount(() => { ...@@ -690,7 +653,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: clamp(13px, 1vw, 20px); font-size: clamp(13px, 1vw, 19px);
font-weight: 600; font-weight: 600;
margin-bottom: 4px; margin-bottom: 4px;
padding-bottom: 6px; padding-bottom: 6px;
...@@ -723,7 +686,7 @@ onBeforeUnmount(() => { ...@@ -723,7 +686,7 @@ onBeforeUnmount(() => {
height: clamp(28px, 3vh, 44px); height: clamp(28px, 3vh, 44px);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
font-size: clamp(11px, 0.9vw, 17px); font-size: clamp(12px, 0.9vw, 17px);
line-height: 1; line-height: 1;
} }
...@@ -740,6 +703,17 @@ onBeforeUnmount(() => { ...@@ -740,6 +703,17 @@ onBeforeUnmount(() => {
min-height: 0; min-height: 0;
} }
.step-row.row-skip {
background: rgba(245, 158, 11, 0.06);
}
.skip-icon {
font-style: normal;
color: var(--warn);
margin-left: 3px;
font-size: 12px;
}
.duration-cell { .duration-cell {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
white-space: nowrap; white-space: nowrap;
...@@ -763,7 +737,7 @@ onBeforeUnmount(() => { ...@@ -763,7 +737,7 @@ onBeforeUnmount(() => {
/* ======== 状态卡片 ======== */ /* ======== 状态卡片 ======== */
.status-card { .status-card {
flex: 2; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: clamp(14px, 1.8vw, 24px); gap: clamp(14px, 1.8vw, 24px);
...@@ -771,8 +745,8 @@ onBeforeUnmount(() => { ...@@ -771,8 +745,8 @@ onBeforeUnmount(() => {
} }
.ring { .ring {
width: clamp(70px, 5vw, 120px); width: clamp(70px, 8vw, 110px);
height: clamp(70px, 5vw, 120px); height: clamp(70px, 8vw, 110px);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -782,28 +756,28 @@ onBeforeUnmount(() => { ...@@ -782,28 +756,28 @@ onBeforeUnmount(() => {
} }
.ring-inner { .ring-inner {
width: clamp(54px, 4vw, 94px); width: clamp(54px, 6.2vw, 86px);
height: clamp(54px, 4vw, 94px); height: clamp(54px, 6.2vw, 86px);
border-radius: 50%; border-radius: 50%;
background: #0b1730; background: #0b1730;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: clamp(11px, 0.85vw, 16px); font-size: clamp(12px, 0.9vw, 17px);
} }
.ring-inner span:first-child { font-weight: 700; } .ring-inner span:first-child { font-weight: 700; }
.ring-pass { color: var(--success); } .ring-pass { color: var(--success); }
.ring-fail { color: var(--danger); } .ring-fail { color: var(--danger); }
.ring-sub { color: var(--text-dim); font-size: clamp(8px, 0.6vw, 11px); } .ring-sub { color: var(--text-dim); font-size: clamp(9px, 0.65vw, 12px); }
.status-detail { .status-detail {
list-style: none; list-style: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: clamp(6px, 0.8vw, 12px); gap: clamp(6px, 0.8vw, 12px);
font-size: clamp(13px, 0.95vw, 19px); font-size: clamp(14px, 1vw, 20px);
} }
.status-detail li { .status-detail li {
...@@ -832,8 +806,8 @@ onBeforeUnmount(() => { ...@@ -832,8 +806,8 @@ onBeforeUnmount(() => {
gap: clamp(3px, 0.4vw, 6px); gap: clamp(3px, 0.4vw, 6px);
} }
.stat-label { font-size: clamp(12px, 0.85vw, 17px); color: var(--text-dim); } .stat-label { font-size: clamp(11px, 1.2vw, 14px); color: var(--text-dim); }
.stat-value { font-size: clamp(20px, 1.5vw, 32px); font-weight: 700; } .stat-value { font-size: clamp(20px, 2.2vw, 28px); font-weight: 700; }
.stat-value.accent { color: var(--accent-strong); } .stat-value.accent { color: var(--accent-strong); }
.stat-value.warn { color: var(--warn); } .stat-value.warn { color: var(--warn); }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
* ws.on('snapshot', data => { ... }) * ws.on('snapshot', data => { ... })
* ws.on('step_progress', data => { ... }) * ws.on('step_progress', data => { ... })
* ws.send({ type: 'ping' }) * ws.send({ type: 'ping' })
* ws.send({ type: 'reset_cycle', reason: 'operator_request' }) * ws.send({ type: 'reset_product', reason: 'operator_request' })
* *
* 环境变量: * 环境变量:
* VITE_WS_URL — WebSocket 地址,默认 ws://127.0.0.1:8080/ws * VITE_WS_URL — WebSocket 地址,默认 ws://127.0.0.1:8080/ws
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment