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
>
<
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>
{{
fmt
Cycle
Time
}}
</em></span>
<span
class=
"meta-item"
>
开始时间:
<em>
{{
fmt
Product
Time
}}
</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>
{{
cycle
Id
}}
</em></span>
<span
class=
"meta-item"
>
产品序号:
<em>
{{
product
Id
}}
</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"
>
{{
pass
edCount
}}
/
{{
displaySteps
.
length
}}
</span>
<span
class=
"progress-count"
>
{{
complet
edCount
}}
/
{{
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>
{{
cycle
Duration
.
toFixed
(
1
)
}}
s
</em></li>
<li><span>
产品耗时
</span><em>
{{
product
Duration
.
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
p
assedCount
=
ref
(
0
)
const
p
roductStartedAt
=
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_cycle
s
:
0
,
total_
products
:
0
,
passed_products
:
0
,
failed_product
s
:
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'
,
a
ctual_duration
:
0
}
const
state
=
stepStates
[
cfg
.
step_index
]
||
{
status
:
'pending'
,
a
nomaly_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
cycle
Result
.
value
===
'pass'
?
'通过'
:
'不通过'
if
(
productEndType
.
value
)
return
product
Result
.
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
(
!
cycle
EndType
.
value
)
return
''
if
(
!
product
EndType
.
value
)
return
''
return
cycle
Result
.
value
===
'pass'
?
'ring-pass'
:
'ring-fail'
return
product
Result
.
value
===
'pass'
?
'ring-pass'
:
'ring-fail'
})
})
const
ringStyle
=
computed
(()
=>
{
const
ringStyle
=
computed
(()
=>
{
if
(
cycle
EndType
.
value
)
{
if
(
product
EndType
.
value
)
{
const
color
=
cycle
Result
.
value
===
'pass'
?
'#34d399'
:
'#f87171'
const
color
=
product
Result
.
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
(
cycle
Result
.
value
===
'pass'
)
return
'通过'
if
(
product
Result
.
value
===
'pass'
)
return
'通过'
if
(
cycle
Result
.
value
===
'fail'
)
return
'不通过'
if
(
product
Result
.
value
===
'fail'
)
return
'不通过'
return
'-'
return
'-'
})
})
const
resultClass
=
computed
(()
=>
{
const
resultClass
=
computed
(()
=>
{
if
(
cycle
Result
.
value
===
'pass'
)
return
'text-pass'
if
(
product
Result
.
value
===
'pass'
)
return
'text-pass'
if
(
cycle
Result
.
value
===
'fail'
)
return
'text-fail'
if
(
product
Result
.
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
[
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
)
{
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
cycle
StartedAt
.
value
=
d
.
started_at
product
StartedAt
.
value
=
d
.
started_at
cycle
EndType
.
value
=
null
product
EndType
.
value
=
null
cycle
Result
.
value
=
null
product
Result
.
value
=
null
cycle
Duration
.
value
=
0
product
Duration
.
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'
,
handleCycle
Start
)
.
on
(
'
product_start'
,
handleProduct
Start
)
.
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
,
24
px
);
padding
:
clamp
(
6px
,
1vw
,
12
px
);
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
8
px
;
gap
:
6
px
;
}
}
/* ======== 顶部信息栏 ======== */
/* ======== 顶部信息栏 ======== */
...
@@ -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
,
32
px
);
padding
:
clamp
(
8px
,
1vw
,
14px
)
clamp
(
14px
,
1.4vw
,
24
px
);
}
}
.title
{
.title
{
position
:
absolute
;
position
:
absolute
;
left
:
50%
;
left
:
50%
;
transform
:
translateX
(
-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
;
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
(
1
2px
,
1.5vw
,
24
px
);
gap
:
clamp
(
1
0px
,
1.2vw
,
18
px
);
flex-wrap
:
nowrap
;
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
);
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
,
1
7
px
);
font-size
:
clamp
(
11px
,
0.85vw
,
1
5
px
);
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.
1
fr
)
minmax
(
30
0px
,
1
fr
);
grid-template-columns
:
minmax
(
0
,
2.
4
fr
)
minmax
(
29
0px
,
1
fr
);
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
:
1
0
;
flex
:
1
2
;
padding
:
clamp
(
8px
,
0.9vw
,
1
2
px
);
padding
:
clamp
(
8px
,
0.9vw
,
1
4
px
);
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
,
20
px
);
font-size
:
clamp
(
13px
,
1vw
,
19
px
);
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
(
1
1
px
,
0.9vw
,
17px
);
font-size
:
clamp
(
1
2
px
,
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
,
12
0px
);
width
:
clamp
(
70px
,
8vw
,
11
0px
);
height
:
clamp
(
70px
,
5vw
,
12
0px
);
height
:
clamp
(
70px
,
8vw
,
11
0px
);
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
,
94
px
);
width
:
clamp
(
54px
,
6.2vw
,
86
px
);
height
:
clamp
(
54px
,
4vw
,
94
px
);
height
:
clamp
(
54px
,
6.2vw
,
86
px
);
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
(
1
1px
,
0.85vw
,
16
px
);
font-size
:
clamp
(
1
2px
,
0.9vw
,
17
px
);
}
}
.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
,
11
px
);
}
.ring-sub
{
color
:
var
(
--text-dim
);
font-size
:
clamp
(
9px
,
0.65vw
,
12
px
);
}
.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
(
1
3px
,
0.95vw
,
19
px
);
font-size
:
clamp
(
1
4px
,
1vw
,
20
px
);
}
}
.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
(
1
2px
,
0.85vw
,
17
px
);
color
:
var
(
--text-dim
);
}
.stat-label
{
font-size
:
clamp
(
1
1px
,
1.2vw
,
14
px
);
color
:
var
(
--text-dim
);
}
.stat-value
{
font-size
:
clamp
(
20px
,
1.5vw
,
32
px
);
font-weight
:
700
;
}
.stat-value
{
font-size
:
clamp
(
20px
,
2.2vw
,
28
px
);
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
);
}
...
...
src/ws/index.js
View file @
5febff30
...
@@ -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
...
...
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