Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
AiQIDashboard
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
AiQIDashboard
Commits
5febff30
Commit
5febff30
authored
Jul 14, 2026
by
崔婷婷
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
接口修改 v2
parent
472c0975
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
212 additions
and
238 deletions
+212
-238
QcDashboard.vue
src/components/QcDashboard.vue
+211
-237
index.js
src/ws/index.js
+1
-1
No files found.
src/components/QcDashboard.vue
View file @
5febff30
<
template
>
<div
class=
"dashboard"
>
<!-- ======== 顶部信息栏
(5.1)
======== -->
<!-- ======== 顶部信息栏 ======== -->
<header
class=
"header"
>
<div
class=
"header-left"
>
<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>
{{
workstation
.
workstation_id
}}
</em></span>
<span
class=
"meta-item"
>
员工ID:
<em>
{{
operator
.
operator_id
}}
</em></span>
<span
class=
"meta-item"
>
开始时间:
<em>
{{
fmt
Cycle
Time
}}
</em></span>
<span
class=
"meta-item"
>
开始时间:
<em>
{{
fmt
Product
Time
}}
</em></span>
</div>
<h1
class=
"title"
>
飞天兔AI行为引导系统
</h1>
<div
class=
"header-right"
>
<span
class=
"meta-item"
>
当前工序:
<em>
{{
currentStepName
}}
</em></span>
<span
class=
"meta-item"
>
{{
currentTime
}}
</span>
<span
class=
"meta-item"
>
系统编号:
<em>
{{
cycle
Id
}}
</em></span>
<span
class=
"meta-item"
>
产品序号:
<em>
{{
product
Id
}}
</em></span>
</div>
</header>
<!-- ======== 系统状态栏
(5.5)
======== -->
<!-- ======== 系统状态栏 ======== -->
<div
class=
"system-bar"
>
<span
class=
"sys-item"
>
FPS:
<em>
{{
system
.
fps
}}
</em></span>
<span
class=
"sys-item"
>
推理耗时:
<em>
{{
system
.
inference_ms
}}
ms
</em></span>
...
...
@@ -28,50 +28,45 @@
<em>
{{
system
.
camera_connected
?
'已连接'
:
'断开'
}}
</em>
</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>
<main
class=
"main"
>
<!-- ======== 左侧:视频
+ 工序进度指示(5.2)
======== -->
<!-- ======== 左侧:视频
画面
======== -->
<section
class=
"panel video-panel"
>
<div
class=
"video-wrap"
>
<img
:src=
"videoUrl"
class=
"video-stream"
/>
<span
class=
"video-label"
>
MJPEG 实时画面
</span>
</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>
<!-- ======== 右侧:SOP 工序列表
(5.3)+ 状态环 + 统计(5.4)
======== -->
<!-- ======== 右侧:SOP 工序列表
+ 状态环 + 统计
======== -->
<section
class=
"info-panel"
>
<div
class=
"panel steps-card"
>
<div
class=
"card-header"
>
<span>
工序明细
</span>
<span
class=
"progress-count"
>
{{
pass
edCount
}}
/
{{
displaySteps
.
length
}}
</span>
<span
class=
"progress-count"
>
{{
complet
edCount
}}
/
{{
displaySteps
.
length
}}
</span>
</div>
<div
class=
"table-wrap"
>
<div
class=
"steps-header"
>
<span>
NO
</span>
<span>
工序
</span>
<span>
时长(s)
</span>
<span>
帧进度
</span>
<span>
状态
</span>
</div>
<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_name
}}
</span>
<span
class=
"duration-cell"
>
{{
s
.
display_duration
.
toFixed
(
1
)
}}
/
{{
s
.
target_duration
.
toFixed
(
1
)
}}
</span>
<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>
</div>
</div>
...
...
@@ -88,7 +83,7 @@
<ul
class=
"status-detail"
>
<li><span>
整双结果
</span><em
:class=
"resultClass"
>
{{
resultLabel
}}
</em></li>
<li><span>
结束类型
</span><em
:class=
"endTypeClass"
>
{{
endTypeLabel
}}
</em></li>
<li><span>
周期耗时
</span><em>
{{
cycle
Duration
.
toFixed
(
1
)
}}
s
</em></li>
<li><span>
产品耗时
</span><em>
{{
product
Duration
.
toFixed
(
1
)
}}
s
</em></li>
</ul>
</div>
...
...
@@ -98,22 +93,22 @@
<span
class=
"stat-value accent"
>
{{
stats
.
pass_rate_pct
}}
%
</span>
</div>
<div
class=
"panel stat-card"
>
<span
class=
"stat-label"
>
平均耗时
</span>
<span
class=
"stat-value accent"
>
{{
stats
.
avg_duration
}}
s
</span>
<span
class=
"stat-label"
>
总数
</span>
<span
class=
"stat-value accent"
>
{{
stats
.
total_products
}}
</span>
</div>
<div
class=
"panel stat-card"
>
<span
class=
"stat-label"
>
不通过双数
</span>
<span
class=
"stat-value
warn"
>
{{
stats
.
failed_cycles
}}
</span>
<span
class=
"stat-label"
>
平均耗时
</span>
<span
class=
"stat-value
"
>
{{
stats
.
avg_duration
}}
s
</span>
</div>
<div
class=
"panel stat-card"
>
<span
class=
"stat-label"
>
总双
数
</span>
<span
class=
"stat-value
"
>
{{
stats
.
total_cycles
}}
</span>
<span
class=
"stat-label"
>
跳步次
数
</span>
<span
class=
"stat-value
warn"
>
{{
stats
.
skip_missing_count
}}
</span>
</div>
</div>
</section>
</main>
<!-- 跳步/错误
弹窗
-->
<!-- 跳步/错误
Toast
-->
<Teleport
to=
"body"
>
<div
v-if=
"alertMsg"
class=
"alert-toast"
:class=
"alertMsg.level"
@
click=
"alertMsg = null"
>
<span>
{{
alertMsg
.
text
}}
</span>
...
...
@@ -126,37 +121,38 @@
import
{
ref
,
reactive
,
computed
,
onMounted
,
onBeforeUnmount
}
from
'vue'
import
{
connect
}
from
'../ws/index.js'
// ==================== 数据模型(
按文档 2.1~2.9
) ====================
// ==================== 数据模型(
v2
) ====================
//
2.1 工位
//
工位 / 操作员 / 批次
const
workstation
=
reactive
({
workstation_id
:
'-'
,
name
:
'-'
,
area
:
'-'
,
line
:
'-'
})
// 2.2 操作员
const
operator
=
reactive
({
operator_id
:
'-'
,
name
:
'-'
})
// 2.3 批次
const
batch
=
reactive
({
batch_id
:
'-'
,
product_type
:
'-'
,
series
:
'-'
,
started_at
:
'-'
})
// 2.4 工序配置
// 2.1 工序配置
const
stepConfigs
=
ref
([])
// 2.7 周期
const
cycleId
=
ref
(
'-'
)
const
cycleResult
=
ref
(
null
)
// 'pass' | 'fail' | null
const
cycleEndType
=
ref
(
null
)
// 'flow_complete' | 'timeout' | 'material_change' | 'manual_reset
' | null
const
cycleDuration
=
ref
(
0
)
const
cycleStartedAt
=
ref
(
''
)
const
p
assedCount
=
ref
(
0
)
// 2.4/2.5 产品
const
productId
=
ref
(
'-'
)
const
productResult
=
ref
(
null
)
// 'pass' | 'fail
' | null
const
productEndType
=
ref
(
null
)
// 'flow_complete' | 'timeout' | 'material_change' | 'manual_reset' | null
const
productDuration
=
ref
(
0
)
const
p
roductStartedAt
=
ref
(
''
)
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
({})
// 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
({
total_
cycles
:
0
,
passed_cycles
:
0
,
failed_cycle
s
:
0
,
total_
products
:
0
,
passed_products
:
0
,
failed_product
s
:
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
({
fps
:
0
,
inference_ms
:
0
,
model_loaded
:
false
,
model_version
:
'-'
,
camera_connected
:
false
,
...
...
@@ -179,16 +175,27 @@ let clockTimer = null
// 合并 step_configs + stepStates + activeProgress → 展示行
const
displaySteps
=
computed
(()
=>
stepConfigs
.
value
.
map
(
cfg
=>
{
const
state
=
stepStates
[
cfg
.
step_index
]
||
{
status
:
'pending'
,
a
ctual_duration
:
0
}
const
state
=
stepStates
[
cfg
.
step_index
]
||
{
status
:
'pending'
,
a
nomaly_type
:
'none'
}
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
{
step_index
:
cfg
.
step_index
,
step_code
:
cfg
.
step_code
,
step_name
:
cfg
.
step_name
,
target_duration
:
cfg
.
pass_duration
,
window_timeout
:
cfg
.
window_timeout
,
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(() => {
return
cs
?
cs
.
step_name
:
'-'
})
// 格式化 cycleStartedAt(ISO 8601 → HH:MM:SS)
const
fmtCycleTime
=
computed
(()
=>
{
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})
/
)
const
completedCount
=
computed
(()
=>
displaySteps
.
value
.
filter
(
s
=>
s
.
status
===
'completed'
||
s
.
status
===
'incomplete'
).
length
)
// 格式化 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
]
:
'-'
})
// 状态环
const
ringMainText
=
computed
(()
=>
{
if
(
cycleEndType
.
value
)
return
cycle
Result
.
value
===
'pass'
?
'通过'
:
'不通过'
if
(
productEndType
.
value
)
return
product
Result
.
value
===
'pass'
?
'通过'
:
'不通过'
if
(
activeProgress
.
step_index
>
0
)
return
'检测中'
return
'待机'
})
const
ringSubText
=
computed
(()
=>
{
if
(
cycleEndType
.
value
)
return
endTypeLabel
.
value
const
pct
=
activeProgress
.
progress
||
0
return
`
${
Math
.
round
(
pct
*
100
)}
%`
if
(
productEndType
.
value
)
return
endTypeLabel
.
value
if
(
activeProgress
.
target_frames
>
0
)
{
return
`
${
Math
.
round
((
activeProgress
.
consecutive_frames
/
activeProgress
.
target_frames
)
*
100
)}
%`
}
return
''
})
const
ringMainClass
=
computed
(()
=>
{
if
(
!
cycle
EndType
.
value
)
return
''
return
cycle
Result
.
value
===
'pass'
?
'ring-pass'
:
'ring-fail'
if
(
!
product
EndType
.
value
)
return
''
return
product
Result
.
value
===
'pass'
?
'ring-pass'
:
'ring-fail'
})
const
ringStyle
=
computed
(()
=>
{
if
(
cycle
EndType
.
value
)
{
const
color
=
cycle
Result
.
value
===
'pass'
?
'#34d399'
:
'#f87171'
if
(
product
EndType
.
value
)
{
const
color
=
product
Result
.
value
===
'pass'
?
'#34d399'
:
'#f87171'
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
:
'rgba(255,255,255,0.08)'
}
})
// 结果 / 结束类型中文
const
resultLabel
=
computed
(()
=>
{
if
(
cycle
Result
.
value
===
'pass'
)
return
'通过'
if
(
cycle
Result
.
value
===
'fail'
)
return
'不通过'
if
(
product
Result
.
value
===
'pass'
)
return
'通过'
if
(
product
Result
.
value
===
'fail'
)
return
'不通过'
return
'-'
})
const
resultClass
=
computed
(()
=>
{
if
(
cycle
Result
.
value
===
'pass'
)
return
'text-pass'
if
(
cycle
Result
.
value
===
'fail'
)
return
'text-fail'
if
(
product
Result
.
value
===
'pass'
)
return
'text-pass'
if
(
product
Result
.
value
===
'fail'
)
return
'text-fail'
return
''
})
const
endTypeLabel
=
computed
(()
=>
{
const
map
=
{
flow_complete
:
'流程结束'
,
timeout
:
'超时结束'
,
material_change
:
'物料切换'
,
manual_reset
:
'手动复位'
}
return
map
[
cycle
EndType
.
value
]
||
'-'
return
map
[
product
EndType
.
value
]
||
'-'
})
const
endTypeClass
=
computed
(()
=>
cycle
EndType
.
value
===
'timeout'
?
'text-fail'
:
''
)
const
endTypeClass
=
computed
(()
=>
product
EndType
.
value
===
'timeout'
?
'text-fail'
:
''
)
//
---- UI 帮助函数 ----
//
UI 帮助函数
function
statusLabel
(
s
)
{
const
map
=
{
pending
:
'待检'
,
active
:
'进行中'
,
pass
:
'通过'
,
fail
:
'未达标'
,
missing
:
'缺失
'
}
const
map
=
{
pending
:
'待检'
,
active
:
'进行中'
,
completed
:
'已完成'
,
incomplete
:
'未完成
'
}
return
map
[
s
]
||
s
}
function
statusBadgeClass
(
s
)
{
if
(
s
===
'active'
)
return
'badge-progress'
if
(
s
===
'
pass
'
)
return
'badge-pass'
if
(
s
===
'
fail'
||
s
===
'missing
'
)
return
'badge-fail'
if
(
s
===
'
completed
'
)
return
'badge-pass'
if
(
s
===
'
incomplete
'
)
return
'badge-fail'
return
'badge-wait'
}
function
miniItemClass
(
s
)
{
return
{
active
:
s
.
status
===
'active'
,
pass
:
s
.
status
===
'pass'
,
fail
:
s
.
status
===
'fail'
||
s
.
status
===
'missing'
,
}
}
// ==================== 时钟 ====================
function
updateClock
()
{
...
...
@@ -280,133 +288,146 @@ function updateClock() {
function
handleSnapshot
(
msg
)
{
const
d
=
msg
.
data
// 2.1
if
(
d
.
workstation
)
Object
.
assign
(
workstation
,
d
.
workstation
)
// 2.2
if
(
d
.
operator
)
Object
.
assign
(
operator
,
d
.
operator
)
// 2.3
if
(
d
.
batch
)
Object
.
assign
(
batch
,
d
.
batch
)
// 2.4
if
(
d
.
step_configs
)
stepConfigs
.
value
=
d
.
step_configs
// 2.8
if
(
d
.
stats
)
{
Object
.
assign
(
stats
,
d
.
stats
)
stats
.
pass_rate_pct
=
Math
.
round
(
stats
.
pass_rate
*
100
)
}
// 2.9
if
(
d
.
system
)
Object
.
assign
(
system
,
d
.
system
)
// 2.7 当前周期
if
(
d
.
current_cycle
)
{
applyCycle
(
d
.
current_cycle
)
cycleEndType
.
value
=
d
.
current_cycle
.
end_type
||
null
cycleResult
.
value
=
d
.
current_cycle
.
result
||
null
cycleDuration
.
value
=
d
.
current_cycle
.
duration
||
0
if
(
d
.
current_product
)
{
applyProduct
(
d
.
current_product
)
productEndType
.
value
=
d
.
current_product
.
end_type
||
null
productResult
.
value
=
d
.
current_product
.
result
||
null
productDuration
.
value
=
d
.
current_product
.
duration
||
0
}
}
function
applyCycle
(
cycle
)
{
cycleId
.
value
=
cycle
.
cycle_id
||
'-'
cycleStartedAt
.
value
=
cycle
.
started_at
||
''
passedCount
.
value
=
cycle
.
step_count_passed
||
0
currentStepIndex
.
value
=
cycle
.
current_step_index
||
1
function
applyProduct
(
product
)
{
productId
.
value
=
product
.
product_id
||
'-'
productStartedAt
.
value
=
product
.
started_at
||
''
currentStepIndex
.
value
=
product
.
current_step_index
||
1
// 初始化 stepStates
Object
.
keys
(
stepStates
).
forEach
(
k
=>
delete
stepStates
[
k
])
if
(
cycle
.
steps
)
{
cycle
.
steps
.
forEach
(
s
=>
{
if
(
product
.
steps
)
{
product
.
steps
.
forEach
(
s
=>
{
stepStates
[
s
.
step_index
]
=
{
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
cycleId
.
value
=
d
.
cycle
_id
cycle
StartedAt
.
value
=
d
.
started_at
cycle
EndType
.
value
=
null
cycle
Result
.
value
=
null
cycle
Duration
.
value
=
0
productId
.
value
=
d
.
product
_id
product
StartedAt
.
value
=
d
.
started_at
product
EndType
.
value
=
null
product
Result
.
value
=
null
product
Duration
.
value
=
0
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
])
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
// 当前工序 → completed/incomplete
stepStates
[
d
.
step_index
]
=
{
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
.
status
===
'active'
)
{
activeProgress
.
step_index
=
d
.
step_index
activeProgress
.
current_duration
=
0
activeProgress
.
target_duration
=
d
.
target_duration
activeProgress
.
progress
=
0
if
(
d
.
next_step_index
)
{
currentStepIndex
.
value
=
d
.
next_step_index
// 下一道工序 → active
stepStates
[
d
.
next_step_index
]
=
{
status
:
'active'
,
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
)
{
const
d
=
msg
.
data
activeProgress
.
step_index
=
d
.
step_index
activeProgress
.
current_duration
=
d
.
current_duration
activeProgress
.
target_duration
=
d
.
target_duration
activeProgress
.
progress
=
d
.
progress
// 确保工序状态为 active(防御:step_confirmed 可能尚未到达)
if
(
stepStates
[
d
.
step_index
]
&&
stepStates
[
d
.
step_index
].
status
!==
'active'
)
{
stepStates
[
d
.
step_index
]
=
{
status
:
'active'
,
anomaly_type
:
'none'
,
best_confidence
:
0
}
}
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
cycleEndType
.
value
=
d
.
end_type
cycleResult
.
value
=
d
.
result
cycleDuration
.
value
=
d
.
duration
||
0
passedCount
.
value
=
d
.
step_count_passed
||
0
productEndType
.
value
=
d
.
end_type
productResult
.
value
=
d
.
result
productDuration
.
value
=
d
.
duration
||
0
// 更新最终步骤状态
if
(
d
.
steps
&&
d
.
steps
.
length
)
{
d
.
steps
.
forEach
(
s
=>
{
stepStates
[
s
.
step_index
]
=
{
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
activeProgress
.
progress
=
0
activeProgress
.
current_duration
=
0
Object
.
assign
(
activeProgress
,
{
step_index
:
0
,
consecutive_frames
:
0
,
target_frames
:
0
,
best_confidence
:
0
})
}
// 3.3 stats_update
function
handleStatsUpdate
(
msg
)
{
const
d
=
msg
.
data
Object
.
assign
(
stats
,
d
)
stats
.
pass_rate_pct
=
Math
.
round
(
d
.
pass_rate
*
100
)
}
// 3.3 system_status
function
handleSystemStatus
(
msg
)
{
Object
.
assign
(
system
,
msg
.
data
)
}
// 3.3 skip_detected
function
handleSkipDetected
(
msg
)
{
alertMsg
.
value
=
{
text
:
msg
.
data
.
message
,
level
:
'warn'
}
setTimeout
(()
=>
{
if
(
alertMsg
.
value
&&
alertMsg
.
value
.
text
===
msg
.
data
.
message
)
alertMsg
.
value
=
null
},
5000
)
}
// 3.3 error
function
handleError
(
msg
)
{
// 区分后端业务错误(有 data.code)与 WebSocket 传输层错误
if
(
msg
.
data
&&
msg
.
data
.
code
)
{
alertMsg
.
value
=
{
text
:
`[
${
msg
.
data
.
code
}
]
${
msg
.
data
.
message
}
`
,
level
:
'error'
}
}
else
{
...
...
@@ -422,13 +443,13 @@ onMounted(() => {
const
ws
=
connect
()
.
on
(
'snapshot'
,
handleSnapshot
)
.
on
(
'
cycle_start'
,
handleCycle
Start
)
.
on
(
'step_
update'
,
handleStepUpdate
)
.
on
(
'
product_start'
,
handleProduct
Start
)
.
on
(
'step_
confirmed'
,
handleStepConfirmed
)
.
on
(
'step_progress'
,
handleStepProgress
)
.
on
(
'cycle_end'
,
handleCycleEnd
)
.
on
(
'skip_detected'
,
handleSkipDetected
)
.
on
(
'product_end'
,
handleProductEnd
)
.
on
(
'stats_update'
,
handleStatsUpdate
)
.
on
(
'system_status'
,
handleSystemStatus
)
.
on
(
'skip_detected'
,
handleSkipDetected
)
.
on
(
'error'
,
handleError
)
.
on
(
'reconnecting'
,
(
data
)
=>
{
wsReconnecting
.
value
=
true
...
...
@@ -440,7 +461,6 @@ onMounted(() => {
wsReconnectAttempt
.
value
=
0
})
// 挂载到组件实例,便于卸载时清理
window
.
__qcWs
=
ws
})
...
...
@@ -471,10 +491,10 @@ onBeforeUnmount(() => {
overflow
:
hidden
;
background
:
radial-gradient
(
circle
at
20%
0%
,
#10224a
0%
,
var
(
--bg
)
55%
);
color
:
var
(
--text
);
padding
:
clamp
(
8px
,
1.2vw
,
24
px
);
padding
:
clamp
(
6px
,
1vw
,
12
px
);
display
:
flex
;
flex-direction
:
column
;
gap
:
8
px
;
gap
:
6
px
;
}
/* ======== 顶部信息栏 ======== */
...
...
@@ -484,18 +504,18 @@ onBeforeUnmount(() => {
align-items
:
center
;
justify-content
:
space-between
;
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
));
border
:
1px
solid
var
(
--border
);
border-radius
:
10px
;
padding
:
clamp
(
8px
,
1vw
,
20px
)
clamp
(
16px
,
1.6vw
,
32
px
);
padding
:
clamp
(
8px
,
1vw
,
14px
)
clamp
(
14px
,
1.4vw
,
24
px
);
}
.title
{
position
:
absolute
;
left
:
50%
;
transform
:
translateX
(
-50%
);
font-size
:
clamp
(
18px
,
1.6vw
,
3
4
px
);
font-size
:
clamp
(
18px
,
1.6vw
,
3
0
px
);
font-weight
:
700
;
letter-spacing
:
2px
;
color
:
#fff
;
...
...
@@ -507,9 +527,9 @@ onBeforeUnmount(() => {
.header-left
,
.header-right
{
display
:
flex
;
gap
:
clamp
(
1
2px
,
1.5vw
,
24
px
);
gap
:
clamp
(
1
0px
,
1.2vw
,
18
px
);
flex-wrap
:
nowrap
;
font-size
:
clamp
(
11px
,
0.85vw
,
1
8
px
);
font-size
:
clamp
(
11px
,
0.85vw
,
1
6
px
);
color
:
var
(
--text-dim
);
align-items
:
center
;
}
...
...
@@ -529,8 +549,8 @@ onBeforeUnmount(() => {
display
:
flex
;
align-items
:
center
;
gap
:
clamp
(
14px
,
1.5vw
,
24px
);
padding
:
clamp
(
3px
,
0.4vw
,
8px
)
clamp
(
16px
,
1.6vw
,
28px
)
;
font-size
:
clamp
(
11px
,
0.85vw
,
1
7
px
);
padding
:
3px
18px
;
font-size
:
clamp
(
11px
,
0.85vw
,
1
5
px
);
color
:
var
(
--text-dim
);
flex-shrink
:
0
;
}
...
...
@@ -544,6 +564,9 @@ onBeforeUnmount(() => {
.sys-item.warn
{
color
:
var
(
--warn
);
}
.sys-item.warn
em
{
color
:
var
(
--warn
);
}
.cam-dot
{
display
:
inline-block
;
...
...
@@ -563,7 +586,7 @@ onBeforeUnmount(() => {
.main
{
flex
:
1
;
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
2.
1
fr
)
minmax
(
30
0px
,
1
fr
);
grid-template-columns
:
minmax
(
0
,
2.
4
fr
)
minmax
(
29
0px
,
1
fr
);
gap
:
8px
;
min-height
:
0
;
}
...
...
@@ -583,7 +606,6 @@ onBeforeUnmount(() => {
display
:
flex
;
flex-direction
:
column
;
padding
:
8px
;
gap
:
8px
;
}
.video-wrap
{
...
...
@@ -606,71 +628,12 @@ onBeforeUnmount(() => {
position
:
absolute
;
bottom
:
8px
;
right
:
12px
;
font-size
:
clamp
(
10px
,
0.7vw
,
14px
)
;
font-size
:
11px
;
color
:
rgba
(
255
,
255
,
255
,
0.45
);
letter-spacing
:
1px
;
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
{
display
:
flex
;
...
...
@@ -680,8 +643,8 @@ onBeforeUnmount(() => {
}
.steps-card
{
flex
:
1
0
;
padding
:
clamp
(
8px
,
0.9vw
,
1
2
px
);
flex
:
1
2
;
padding
:
clamp
(
8px
,
0.9vw
,
1
4
px
);
display
:
flex
;
flex-direction
:
column
;
}
...
...
@@ -690,7 +653,7 @@ onBeforeUnmount(() => {
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
font-size
:
clamp
(
13px
,
1vw
,
20
px
);
font-size
:
clamp
(
13px
,
1vw
,
19
px
);
font-weight
:
600
;
margin-bottom
:
4px
;
padding-bottom
:
6px
;
...
...
@@ -723,7 +686,7 @@ onBeforeUnmount(() => {
height
:
clamp
(
28px
,
3vh
,
44px
);
border-bottom
:
1px
solid
var
(
--border
);
flex-shrink
:
0
;
font-size
:
clamp
(
1
1
px
,
0.9vw
,
17px
);
font-size
:
clamp
(
1
2
px
,
0.9vw
,
17px
);
line-height
:
1
;
}
...
...
@@ -740,6 +703,17 @@ onBeforeUnmount(() => {
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
{
font-variant-numeric
:
tabular-nums
;
white-space
:
nowrap
;
...
...
@@ -763,7 +737,7 @@ onBeforeUnmount(() => {
/* ======== 状态卡片 ======== */
.status-card
{
flex
:
2
;
flex
:
1
;
display
:
flex
;
align-items
:
center
;
gap
:
clamp
(
14px
,
1.8vw
,
24px
);
...
...
@@ -771,8 +745,8 @@ onBeforeUnmount(() => {
}
.ring
{
width
:
clamp
(
70px
,
5vw
,
12
0px
);
height
:
clamp
(
70px
,
5vw
,
12
0px
);
width
:
clamp
(
70px
,
8vw
,
11
0px
);
height
:
clamp
(
70px
,
8vw
,
11
0px
);
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
...
...
@@ -782,28 +756,28 @@ onBeforeUnmount(() => {
}
.ring-inner
{
width
:
clamp
(
54px
,
4vw
,
94
px
);
height
:
clamp
(
54px
,
4vw
,
94
px
);
width
:
clamp
(
54px
,
6.2vw
,
86
px
);
height
:
clamp
(
54px
,
6.2vw
,
86
px
);
border-radius
:
50%
;
background
:
#0b1730
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
font-size
:
clamp
(
1
1px
,
0.85vw
,
16
px
);
font-size
:
clamp
(
1
2px
,
0.9vw
,
17
px
);
}
.ring-inner
span
:first-child
{
font-weight
:
700
;
}
.ring-pass
{
color
:
var
(
--success
);
}
.ring-fail
{
color
:
var
(
--danger
);
}
.ring-sub
{
color
:
var
(
--text-dim
);
font-size
:
clamp
(
8px
,
0.6vw
,
11
px
);
}
.ring-sub
{
color
:
var
(
--text-dim
);
font-size
:
clamp
(
9px
,
0.65vw
,
12
px
);
}
.status-detail
{
list-style
:
none
;
display
:
flex
;
flex-direction
:
column
;
gap
:
clamp
(
6px
,
0.8vw
,
12px
);
font-size
:
clamp
(
1
3px
,
0.95vw
,
19
px
);
font-size
:
clamp
(
1
4px
,
1vw
,
20
px
);
}
.status-detail
li
{
...
...
@@ -832,8 +806,8 @@ onBeforeUnmount(() => {
gap
:
clamp
(
3px
,
0.4vw
,
6px
);
}
.stat-label
{
font-size
:
clamp
(
1
2px
,
0.85vw
,
17
px
);
color
:
var
(
--text-dim
);
}
.stat-value
{
font-size
:
clamp
(
20px
,
1.5vw
,
32
px
);
font-weight
:
700
;
}
.stat-label
{
font-size
:
clamp
(
1
1px
,
1.2vw
,
14
px
);
color
:
var
(
--text-dim
);
}
.stat-value
{
font-size
:
clamp
(
20px
,
2.2vw
,
28
px
);
font-weight
:
700
;
}
.stat-value.accent
{
color
:
var
(
--accent-strong
);
}
.stat-value.warn
{
color
:
var
(
--warn
);
}
...
...
src/ws/index.js
View file @
5febff30
...
...
@@ -9,7 +9,7 @@
* ws.on('snapshot', data => { ... })
* ws.on('step_progress', data => { ... })
* 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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment