Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
T
thermal-control-system
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
luwei
thermal-control-system
Commits
d7a073ab
Commit
d7a073ab
authored
May 07, 2026
by
luwei
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
实时检测、历史记录完善
parent
3faede64
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
2998 additions
and
24 deletions
+2998
-24
monitor.py
backend/app/api/monitor.py
+137
-0
main.py
backend/app/main.py
+3
-0
mpc_controller.py
backend/app/ml/mpc_controller.py
+458
-0
__init__.py
backend/app/models/__init__.py
+6
-1
data_management.py
backend/app/models/data_management.py
+2
-0
monitor.py
backend/app/models/monitor.py
+60
-0
monitor_service.py
backend/app/services/monitor_service.py
+590
-0
package_management_service.py
backend/app/services/package_management_service.py
+47
-13
requirements.txt
backend/requirements.txt
+1
-0
pkg_6_20260507172252193173.csv
backend/uploads/packages/pkg_6_20260507172252193173.csv
+11
-0
pkg_7_20260507172430554650.csv
backend/uploads/packages/pkg_7_20260507172430554650.csv
+102
-0
UnderConstruction-BwhS9Ul2.css
frontend/dist/assets/UnderConstruction-BwhS9Ul2.css
+0
-1
UnderConstruction-n0JcUgMW.js
frontend/dist/assets/UnderConstruction-n0JcUgMW.js
+0
-1
_plugin-vue_export-helper-D1RKUtCV.js
frontend/dist/assets/_plugin-vue_export-helper-D1RKUtCV.js
+0
-1
index.html
frontend/dist/index.html
+3
-3
historyData.js
frontend/src/api/historyData.js
+9
-0
realtimeMonitor.js
frontend/src/api/realtimeMonitor.js
+49
-0
index.js
frontend/src/router/index.js
+7
-4
index.vue
frontend/src/views/HistoryData/index.vue
+478
-0
ExperimentDetail.vue
...src/views/RealtimeMonitor/components/ExperimentDetail.vue
+737
-0
index.vue
frontend/src/views/RealtimeMonitor/index.vue
+298
-0
No files found.
backend/app/api/monitor.py
0 → 100644
View file @
d7a073ab
from
fastapi
import
APIRouter
,
HTTPException
,
Query
from
pydantic
import
BaseModel
,
Field
from
app.services.monitor_service
import
MonitorService
from
app.utils.response
import
success_response
router
=
APIRouter
()
service
=
MonitorService
()
# ── 请求 Schema ───────────────────────────────────────────────────────────────
class
MPCParamsSchema
(
BaseModel
):
P
:
int
=
Field
(
default
=
20
,
ge
=
5
,
le
=
100
,
description
=
'预测时域'
)
M
:
int
=
Field
(
default
=
5
,
ge
=
1
,
le
=
20
,
description
=
'控制时域'
)
Q
:
float
=
Field
(
default
=
10.0
,
gt
=
0
,
description
=
'跟踪误差权重'
)
R
:
float
=
Field
(
default
=
0.1
,
gt
=
0
,
description
=
'控制增量权重'
)
alpha
:
float
=
Field
(
default
=
0.8
,
gt
=
0
,
lt
=
1
,
description
=
'参考轨迹柔化系数'
)
u_min
:
float
=
Field
(
default
=
0.0
,
description
=
'电流下限(A)'
)
u_max
:
float
=
Field
(
default
=
10.0
,
description
=
'电流上限(A)'
)
du_min
:
float
=
Field
(
default
=-
2.0
,
description
=
'电流增量下限(A/步)'
)
du_max
:
float
=
Field
(
default
=
2.0
,
description
=
'电流增量上限(A/步)'
)
y_min
:
float
=
Field
(
default
=-
50.0
,
description
=
'温度约束下限(°C)'
)
y_max
:
float
=
Field
(
default
=
200.0
,
description
=
'温度约束上限(°C)'
)
correction_gain
:
float
=
Field
(
default
=
0.5
,
ge
=
0
,
le
=
1
,
description
=
'反馈校正增益'
)
class
CreateExperimentRequest
(
BaseModel
):
name
:
str
=
Field
(
min_length
=
1
,
max_length
=
255
)
model_id
:
int
package_id
:
int
target_temp
:
float
=
Field
(
description
=
'目标温度(°C)'
)
sampling_interval
:
float
=
Field
(
default
=
1.0
,
gt
=
0
,
le
=
3600
,
description
=
'采样周期(秒)'
)
mpc_params
:
MPCParamsSchema
=
Field
(
default_factory
=
MPCParamsSchema
)
# ── 元数据接口 ────────────────────────────────────────────────────────────────
@
router
.
get
(
'/models'
)
def
list_models
():
return
success_response
(
data
=
service
.
list_models
())
@
router
.
get
(
'/packages'
)
def
list_packages
():
return
success_response
(
data
=
service
.
list_packages
())
# ── 试验 CRUD ─────────────────────────────────────────────────────────────────
@
router
.
get
(
'/experiments'
)
def
list_experiments
():
return
success_response
(
data
=
service
.
list_experiments
())
@
router
.
post
(
'/experiments'
)
def
create_experiment
(
req
:
CreateExperimentRequest
):
try
:
exp
=
service
.
create_experiment
(
name
=
req
.
name
.
strip
(),
model_id
=
req
.
model_id
,
package_id
=
req
.
package_id
,
target_temp
=
req
.
target_temp
,
sampling_interval
=
req
.
sampling_interval
,
mpc_params
=
req
.
mpc_params
.
model_dump
(),
)
return
success_response
(
data
=
exp
,
message
=
'试验创建成功'
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
str
(
exc
))
from
exc
@
router
.
get
(
'/experiments/{exp_id}'
)
def
get_experiment
(
exp_id
:
int
):
try
:
return
success_response
(
data
=
service
.
get_experiment
(
exp_id
))
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
404
,
detail
=
str
(
exc
))
from
exc
@
router
.
delete
(
'/experiments/{exp_id}'
)
def
delete_experiment
(
exp_id
:
int
):
try
:
service
.
delete_experiment
(
exp_id
)
return
success_response
(
message
=
'试验已删除'
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
404
,
detail
=
str
(
exc
))
from
exc
# ── 控制操作 ──────────────────────────────────────────────────────────────────
@
router
.
post
(
'/experiments/{exp_id}/start'
)
def
start_experiment
(
exp_id
:
int
):
try
:
service
.
start_experiment
(
exp_id
)
return
success_response
(
message
=
'试验已启动'
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
str
(
exc
))
from
exc
@
router
.
post
(
'/experiments/{exp_id}/stop'
)
def
stop_experiment
(
exp_id
:
int
):
try
:
service
.
stop_experiment
(
exp_id
)
return
success_response
(
message
=
'试验已停止'
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
str
(
exc
))
from
exc
# ── 数据 / 报告 / 导出 ────────────────────────────────────────────────────────
@
router
.
get
(
'/experiments/{exp_id}/data'
)
def
get_data_points
(
exp_id
:
int
,
from_step
:
int
=
Query
(
default
=
0
,
ge
=
0
)):
return
success_response
(
data
=
service
.
get_data_points
(
exp_id
,
from_step
))
@
router
.
get
(
'/experiments/{exp_id}/report'
)
def
get_report
(
exp_id
:
int
):
try
:
return
success_response
(
data
=
service
.
get_report
(
exp_id
))
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
404
,
detail
=
str
(
exc
))
from
exc
@
router
.
post
(
'/experiments/{exp_id}/export'
)
def
export_to_history
(
exp_id
:
int
):
try
:
result
=
service
.
export_to_history
(
exp_id
)
return
success_response
(
data
=
result
,
message
=
'已导出到历史数据'
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
400
,
detail
=
str
(
exc
))
from
exc
# ── 历史数据 ──────────────────────────────────────────────────────────────────
@
router
.
get
(
'/history'
)
def
list_history
():
return
success_response
(
data
=
service
.
list_history_experiments
())
backend/app/main.py
View file @
d7a073ab
...
...
@@ -5,11 +5,13 @@ from fastapi.responses import JSONResponse
from
app.api.data_management
import
router
as
data_management_router
from
app.api.eval_management
import
router
as
eval_management_router
from
app.api.monitor
import
router
as
monitor_router
from
app.api.package_management
import
router
as
package_management_router
from
app.api.train_management
import
router
as
train_management_router
from
app.database
import
Base
,
engine
from
app.models
import
Category
,
DataFile
,
DataPackage
,
DataPackageFile
# noqa: F401
from
app.models.eval_management
import
EvalRecord
# noqa: F401
from
app.models.monitor
import
MonitorDataPoint
,
MonitorExperiment
# noqa: F401
from
app.models.train_management
import
SavedModel
,
TrainTask
# noqa: F401
from
app.utils.response
import
error_response
,
success_response
...
...
@@ -60,6 +62,7 @@ def create_app() -> FastAPI:
app
.
include_router
(
package_management_router
,
prefix
=
'/api/packages'
,
tags
=
[
'数据包管理'
])
app
.
include_router
(
train_management_router
,
prefix
=
'/api/train'
,
tags
=
[
'模型训练'
])
app
.
include_router
(
eval_management_router
,
prefix
=
'/api/eval'
,
tags
=
[
'模型评估'
])
app
.
include_router
(
monitor_router
,
prefix
=
'/api/monitor'
,
tags
=
[
'实时监控'
])
return
app
...
...
backend/app/ml/mpc_controller.py
0 → 100644
View file @
d7a073ab
"""温度控制系统模型预测控制器。
本模块实现论文中描述的 MPC 算法,包含以下组件:
- 参考轨迹生成
- 状态估计器
- 基于 LSTM 的预测模型封装
- 滚动优化器(最小化二次代价函数 J)
- 反馈校正
使用方法:
controller = MPCController.from_checkpoint('model.pt', mpc_params)
u = controller.step(history_records, yr=target_temp)
"""
from
__future__
import
annotations
from
dataclasses
import
dataclass
from
pathlib
import
Path
from
typing
import
Callable
import
numpy
as
np
# ── 可选依赖:PyTorch ────────────────────────────────────────────────────────
_TORCH_AVAILABLE
=
False
try
:
import
torch
import
torch.nn
as
nn
_TORCH_AVAILABLE
=
True
class
_LSTMModel
(
nn
.
Module
):
def
__init__
(
self
,
input_size
:
int
,
hidden_size
:
int
,
num_layers
:
int
)
->
None
:
super
()
.
__init__
()
dropout
=
0.2
if
num_layers
>
1
else
0.0
self
.
lstm
=
nn
.
LSTM
(
input_size
,
hidden_size
,
num_layers
,
batch_first
=
True
,
dropout
=
dropout
,
)
self
.
fc
=
nn
.
Linear
(
hidden_size
,
1
)
def
forward
(
self
,
x
:
'torch.Tensor'
)
->
'torch.Tensor'
:
out
,
_
=
self
.
lstm
(
x
)
return
self
.
fc
(
out
[:,
-
1
,
:])
.
squeeze
(
-
1
)
except
ImportError
:
pass
# ── 特征列定义(须与训练器保持一致)────────────────────────────────────────────
FEATURE_COLS
=
[
'current'
,
'voltage'
,
'set_temperature'
,
'actual_temperature'
]
TARGET_COL
=
'actual_temperature'
TARGET_IDX
=
FEATURE_COLS
.
index
(
TARGET_COL
)
CURRENT_IDX
=
FEATURE_COLS
.
index
(
'current'
)
# ─────────────────────────────────────────────────────────────────────────────
# MPC 参数数据类
# ─────────────────────────────────────────────────────────────────────────────
@
dataclass
class
MPCParams
:
"""MPC 控制器的全部可调参数。
属性说明:
P : 预测时域长度(步数)。应覆盖温度系统的主要动态响应时间。
M : 控制时域长度(步数),M <= P。M 越小越稳定、计算量越小。
Q : 跟踪误差权重(标量或长度为 P 的列表)。
Q 越大跟踪越快,但可能超调。
R : 控制增量权重(标量或长度为 M 的列表)。
R 越大电流变化越平滑,响应越慢。
alpha : 参考轨迹柔化系数(0 < α < 1)。
越接近1,趋近设定值越缓慢平滑。
u_min : 控制量下限(电流,A),须 ≥ 0。
u_max : 控制量上限(电流,A)。
du_min : 每步控制增量下限(A/步)。
du_max : 每步控制增量上限(A/步)。
y_min : 预测温度下限(°C),安全下界。
y_max : 预测温度上限(°C),安全上界。
correction_gain : 反馈校正增益(0~1)。控制预测误差对后续预测的修正强度。
optimizer : 'scipy'(默认,无梯度 SLSQP)或 'gradient'
(PyTorch 自动微分,torch 可用时更快)。
"""
P
:
int
=
20
M
:
int
=
5
Q
:
float
=
10.0
R
:
float
=
0.1
alpha
:
float
=
0.8
u_min
:
float
=
0.0
u_max
:
float
=
10.0
du_min
:
float
=
-
2.0
du_max
:
float
=
2.0
y_min
:
float
=
-
50.0
y_max
:
float
=
200.0
correction_gain
:
float
=
0.5
optimizer
:
str
=
'scipy'
# ─────────────────────────────────────────────────────────────────────────────
# LSTMPredictor – 封装已保存的 .pt 检查点
# ─────────────────────────────────────────────────────────────────────────────
class
LSTMPredictor
:
"""加载已训练的 LSTM 检查点,并提供 predict() 预测接口。"""
def
__init__
(
self
,
checkpoint
:
dict
)
->
None
:
if
not
_TORCH_AVAILABLE
:
raise
RuntimeError
(
'PyTorch 未安装,无法加载 LSTM 模型。'
)
self
.
seq_len
:
int
=
checkpoint
[
'seq_len'
]
self
.
feature_cols
:
list
[
str
]
=
checkpoint
[
'feature_cols'
]
self
.
data_min
=
np
.
array
(
checkpoint
[
'data_min'
],
dtype
=
np
.
float32
)
self
.
data_max
=
np
.
array
(
checkpoint
[
'data_max'
],
dtype
=
np
.
float32
)
self
.
data_range
=
self
.
data_max
-
self
.
data_min
self
.
data_range
[
self
.
data_range
==
0
]
=
1.0
self
.
_model
=
_LSTMModel
(
checkpoint
[
'input_size'
],
checkpoint
[
'hidden_size'
],
checkpoint
[
'num_layers'
],
)
self
.
_model
.
load_state_dict
(
checkpoint
[
'model_state'
])
self
.
_model
.
eval
()
@
classmethod
def
from_file
(
cls
,
path
:
'Path | str'
)
->
'LSTMPredictor'
:
ckpt
=
torch
.
load
(
str
(
path
),
map_location
=
'cpu'
,
weights_only
=
False
)
return
cls
(
ckpt
)
# ── 归一化辅助方法 ──────────────────────────────────────────────────
def
_norm
(
self
,
arr
:
np
.
ndarray
)
->
np
.
ndarray
:
return
(
arr
-
self
.
data_min
)
/
self
.
data_range
def
_denorm_target
(
self
,
val
:
float
)
->
float
:
idx
=
TARGET_IDX
return
val
*
self
.
data_range
[
idx
]
+
self
.
data_min
[
idx
]
# ── 单步预测 ──────────────────────────────────────────────────────────
def
predict_one
(
self
,
seq_norm
:
np
.
ndarray
)
->
float
:
"""基于归一化后的 (seq_len, features) 序列预测下一时刻温度。
返回原始单位(°C)的预测温度。
"""
x
=
torch
.
tensor
(
seq_norm
[
np
.
newaxis
],
dtype
=
torch
.
float32
)
# 形状 (1, T, F)
with
torch
.
no_grad
():
y_norm
=
self
.
_model
(
x
)
.
item
()
return
self
.
_denorm_target
(
y_norm
)
# ── 多步滚动预测 ──────────────────────────────────────────────────────────
def
rollout
(
self
,
history
:
np
.
ndarray
,
u_sequence
:
np
.
ndarray
,
correction_offset
:
float
=
0.0
,
)
->
np
.
ndarray
:
"""给定控制序列,滚动仳真未来 P 步温度。
参数:
history : (seq_len, n_features) 近期测量值,原始单位。
u_sequence : (P,) 未来电流序列,原始单位。
correction_offset : 每步预测叠加的加法校正量(反馈校正,原始单位)。
返回:
y_pred : (P,) 预测温度,原始单位。
"""
P
=
len
(
u_sequence
)
seq
=
history
.
copy
()
.
astype
(
np
.
float32
)
# (seq_len, F),原始单位
y_pred
=
np
.
empty
(
P
,
dtype
=
np
.
float32
)
for
i
in
range
(
P
):
seq_norm
=
self
.
_norm
(
seq
)
y_next
=
self
.
predict_one
(
seq_norm
)
+
correction_offset
y_pred
[
i
]
=
y_next
# 滑动窗口向前推进一步
new_row
=
seq
[
-
1
]
.
copy
()
new_row
[
CURRENT_IDX
]
=
u_sequence
[
i
]
new_row
[
TARGET_IDX
]
=
y_next
seq
=
np
.
vstack
([
seq
[
1
:],
new_row
])
return
y_pred
# ─────────────────────────────────────────────────────────────────────────────
# 状态估计器
# ─────────────────────────────────────────────────────────────────────────────
class
StateEstimator
:
"""以近期测量值的滑动窗口作为“扩展状态”。
由于加热器热状态、航天器接收热量等内部状态无法直接测量,
使用最近 seq_len 步的温度与电流采样值作为充分代理状态。
"""
def
__init__
(
self
,
seq_len
:
int
,
n_features
:
int
)
->
None
:
self
.
seq_len
=
seq_len
self
.
n_features
=
n_features
self
.
_buffer
:
list
[
np
.
ndarray
]
=
[]
def
update
(
self
,
measurement
:
np
.
ndarray
)
->
None
:
"""追加一条新测量记录(长度为 n_features 的一维数组)。"""
self
.
_buffer
.
append
(
measurement
.
astype
(
np
.
float32
))
if
len
(
self
.
_buffer
)
>
self
.
seq_len
:
self
.
_buffer
.
pop
(
0
)
def
ready
(
self
)
->
bool
:
return
len
(
self
.
_buffer
)
>=
self
.
seq_len
def
get_state
(
self
)
->
np
.
ndarray
:
"""返回 (seq_len, n_features) 历史状态数组。"""
if
not
self
.
ready
():
raise
RuntimeError
(
f
'状态估计器尚未就绪:已有 {len(self._buffer)} 条记录,'
f
'需要至少 {self.seq_len} 条。'
)
return
np
.
array
(
self
.
_buffer
[
-
self
.
seq_len
:],
dtype
=
np
.
float32
)
# ─────────────────────────────────────────────────────────────────────────────
# MPC 控制器
# ─────────────────────────────────────────────────────────────────────────────
class
MPCController
:
"""温度控制完整 MPC 控制回路。
实时循环典型用法::
ctrl = MPCController.from_checkpoint('model.pt', MPCParams())
for measurement in sensor_stream:
ctrl.update_measurement(measurement)
if ctrl.ready():
u = ctrl.step(yr=target_temperature)
send_to_power_supply(u)
批量仳真模式::
u = ctrl.step_from_records(records, yr=35.0)
"""
def
__init__
(
self
,
predictor
:
LSTMPredictor
,
params
:
MPCParams
)
->
None
:
self
.
predictor
=
predictor
self
.
p
=
params
self
.
_estimator
=
StateEstimator
(
predictor
.
seq_len
,
len
(
FEATURE_COLS
))
self
.
_u_prev
:
float
=
0.0
# 上一时刻控制量
self
.
_correction_offset
:
float
=
0.0
# 反馈校正偏置
# ── factory ───────────────────────────────────────────────────────────────
@
classmethod
def
from_checkpoint
(
cls
,
path
:
'Path | str'
,
params
:
'MPCParams | None'
=
None
,
)
->
'MPCController'
:
predictor
=
LSTMPredictor
.
from_file
(
path
)
return
cls
(
predictor
,
params
or
MPCParams
())
# ── 测量值输入 ────────────────────────────────────────────────────────────────
def
update_measurement
(
self
,
record
:
'dict | np.ndarray'
)
->
None
:
"""将一条新传感器测量值送入状态估计器。
参数:
record : 键与 FEATURE_COLS 匹配的字典,或长度为 len(FEATURE_COLS) 的一维 numpy 数组。
"""
if
isinstance
(
record
,
dict
):
row
=
np
.
array
(
[
float
(
record
.
get
(
c
,
0
)
or
0
)
for
c
in
FEATURE_COLS
],
dtype
=
np
.
float32
,
)
else
:
row
=
np
.
asarray
(
record
,
dtype
=
np
.
float32
)
self
.
_estimator
.
update
(
row
)
def
ready
(
self
)
->
bool
:
"""历史数据积累足够后返回 True,此时可执行 MPC 控制步。"""
return
self
.
_estimator
.
ready
()
# ── 参考轨迹 ───────────────────────────────────────────────────────────────────
def
_reference_trajectory
(
self
,
y_k
:
float
,
yr
:
float
)
->
np
.
ndarray
:
"""生成柔化参考轨迹:w(k+j) = α^j · y(k) + (1 - α^j) · yr,j = 1..P"""
j
=
np
.
arange
(
1
,
self
.
p
.
P
+
1
,
dtype
=
np
.
float64
)
alpha_j
=
self
.
p
.
alpha
**
j
return
alpha_j
*
y_k
+
(
1.0
-
alpha_j
)
*
yr
# ── 代价函数 ───────────────────────────────────────────────────────────────────
def
_cost
(
self
,
du_sequence
:
np
.
ndarray
,
# (M,) 待优化的控制增量序列
history
:
np
.
ndarray
,
# (seq_len, F) 当前状态
w
:
np
.
ndarray
,
# (P,) 参考轨迹
)
->
float
:
"""二次型 MPC 代价函数 J = Σ Q·(y−w)² + Σ R·(Δu)²"""
u_seq
=
self
.
_build_u_sequence
(
du_sequence
)
y_pred
=
self
.
predictor
.
rollout
(
history
,
u_seq
,
self
.
_correction_offset
)
tracking
=
float
(
np
.
sum
(
self
.
p
.
Q
*
(
y_pred
-
w
)
**
2
))
effort
=
float
(
np
.
sum
(
self
.
p
.
R
*
du_sequence
**
2
))
return
tracking
+
effort
def
_build_u_sequence
(
self
,
du_sequence
:
np
.
ndarray
)
->
np
.
ndarray
:
"""将 M 个控制增量展开为 P 步控制量序列,并执行限幅。"""
u_seq
=
np
.
empty
(
self
.
p
.
P
,
dtype
=
np
.
float64
)
u
=
self
.
_u_prev
for
i
in
range
(
self
.
p
.
P
):
du
=
du_sequence
[
i
]
if
i
<
self
.
p
.
M
else
0.0
u
=
float
(
np
.
clip
(
u
+
du
,
self
.
p
.
u_min
,
self
.
p
.
u_max
))
u_seq
[
i
]
=
u
return
u_seq
# ── 滚动优化器 ─────────────────────────────────────────────────────────────────────
def
_optimize
(
self
,
history
:
np
.
ndarray
,
w
:
np
.
ndarray
)
->
np
.
ndarray
:
"""求解最优控制增量序列,返回形状 (M,) 的数组。"""
from
scipy.optimize
import
minimize
M
=
self
.
p
.
M
x0
=
np
.
zeros
(
M
)
bounds
=
[(
self
.
p
.
du_min
,
self
.
p
.
du_max
)]
*
M
# 不等式约束:u_min ≤ u(k+i) ≤ u_max(通过累加 Δu 链构造)
constraints
=
[]
for
i
in
range
(
M
):
def
u_lower
(
x
,
idx
=
i
):
return
self
.
_u_prev
+
float
(
np
.
sum
(
x
[:
idx
+
1
]))
-
self
.
p
.
u_min
def
u_upper
(
x
,
idx
=
i
):
return
self
.
p
.
u_max
-
(
self
.
_u_prev
+
float
(
np
.
sum
(
x
[:
idx
+
1
])))
constraints
+=
[
{
'type'
:
'ineq'
,
'fun'
:
u_lower
},
{
'type'
:
'ineq'
,
'fun'
:
u_upper
},
]
result
=
minimize
(
self
.
_cost
,
x0
,
args
=
(
history
,
w
),
method
=
'SLSQP'
,
bounds
=
bounds
,
constraints
=
constraints
,
options
=
{
'maxiter'
:
200
,
'ftol'
:
1e-6
},
)
return
result
.
x
# ── 反馈校正 ─────────────────────────────────────────────────────────────────────
def
_update_correction
(
self
,
y_actual
:
float
,
y_predicted
:
float
)
->
None
:
"""利用 k 时刻预测误差更新加法校正偏置。"""
error
=
y_actual
-
y_predicted
self
.
_correction_offset
+=
self
.
p
.
correction_gain
*
error
# ── 主控制步 ───────────────────────────────────────────────────────────────────────
def
step
(
self
,
yr
:
float
,
y_actual
:
'float | None'
=
None
)
->
float
:
"""执行一次 MPC 控制步,返回控制量 u(k)。
参数:
yr : 温度设定值(°C)。
y_actual : 本步实际温度测量值,用于反馈校正。
为 None 时使用状态缓冲区最后一个值。
返回:
u : 最优电流指令(A),输出至程控电源。
"""
if
not
self
.
ready
():
raise
RuntimeError
(
'控制器尚未就绪,请先调用 update_measurement() 积累足够历史数据。'
)
history
=
self
.
_estimator
.
get_state
()
# (seq_len, F)
y_k
=
float
(
history
[
-
1
,
TARGET_IDX
])
# 当前温度
# ── 反馈校正 ──────────────────────────────────────────────────────────────
if
y_actual
is
not
None
:
y_pred_k
=
self
.
predictor
.
predict_one
(
self
.
predictor
.
_norm
(
history
))
self
.
_update_correction
(
y_actual
,
y_pred_k
)
# ── 参考轨迹 ──────────────────────────────────────────────────────────────
w
=
self
.
_reference_trajectory
(
y_k
,
yr
)
# (P,)
# ── 滚动优化 ──────────────────────────────────────────────────────────────
du_opt
=
self
.
_optimize
(
history
,
w
)
# 仅执行第一步增量(滚动时域原则)
u_opt
=
float
(
np
.
clip
(
self
.
_u_prev
+
du_opt
[
0
],
self
.
p
.
u_min
,
self
.
p
.
u_max
))
self
.
_u_prev
=
u_opt
return
u_opt
# ── 批量仿真 ─────────────────────────────────────────────────────────────────────
def
simulate
(
self
,
records
:
list
[
dict
],
yr
:
float
,
on_step
:
'Callable[[int, float, float, float], None] | None'
=
None
,
)
->
dict
:
"""对一组历史记录执行闭环 MPC 仿真。
参数:
records : 按时序排列的测量字典列表(格式与训练数据相同)。
yr : 温度设定值。
on_step : 可选回调函数 callback(step_idx, y_actual, u_cmd, w_ref)。
返回:
包含键 'u_history'、'y_history'、'w_history'、'error_history' 的字典。
"""
u_hist
,
y_hist
,
w_hist
,
err_hist
=
[],
[],
[],
[]
for
i
,
rec
in
enumerate
(
records
):
self
.
update_measurement
(
rec
)
y_actual
=
float
(
rec
.
get
(
'actual_temperature'
)
or
0
)
if
not
self
.
ready
():
u_hist
.
append
(
0.0
)
y_hist
.
append
(
y_actual
)
w_hist
.
append
(
yr
)
err_hist
.
append
(
yr
-
y_actual
)
continue
u
=
self
.
step
(
yr
=
yr
,
y_actual
=
y_actual
)
w_k
=
self
.
_reference_trajectory
(
y_actual
,
yr
)[
0
]
u_hist
.
append
(
u
)
y_hist
.
append
(
y_actual
)
w_hist
.
append
(
w_k
)
err_hist
.
append
(
yr
-
y_actual
)
if
on_step
:
on_step
(
i
,
y_actual
,
u
,
w_k
)
return
{
'u_history'
:
u_hist
,
'y_history'
:
y_hist
,
'w_history'
:
w_hist
,
'error_history'
:
err_hist
,
}
# ── 便捷接口:从原始记录执行单步控制 ────────────────────────────────────────
def
step_from_records
(
self
,
records
:
list
[
dict
],
yr
:
float
)
->
float
:
"""将最近 seq_len 条记录送入状态估计器后执行一次控制步,返回控制量。
适用于无需在外部维护状态的一次性调用。
内部状态估计器缓冲区在多次调用之间不会被清空,
因此也可用于滚动方式调用。
"""
for
rec
in
records
:
self
.
update_measurement
(
rec
)
return
self
.
step
(
yr
=
yr
)
backend/app/models/__init__.py
View file @
d7a073ab
from
app.models.data_management
import
Category
,
DataFile
,
DataPackage
,
DataPackageFile
,
DataQualityConfig
from
app.models.eval_management
import
EvalRecord
from
app.models.monitor
import
MonitorDataPoint
,
MonitorExperiment
from
app.models.train_management
import
SavedModel
,
TrainTask
__all__
=
[
'Category'
,
'DataFile'
,
'DataPackage'
,
'DataPackageFile'
,
'DataQualityConfig'
,
'TrainTask'
,
'SavedModel'
,
'EvalRecord'
]
__all__
=
[
'Category'
,
'DataFile'
,
'DataPackage'
,
'DataPackageFile'
,
'DataQualityConfig'
,
'TrainTask'
,
'SavedModel'
,
'EvalRecord'
,
'MonitorExperiment'
,
'MonitorDataPoint'
,
]
backend/app/models/data_management.py
View file @
d7a073ab
...
...
@@ -61,6 +61,8 @@ class DataPackage(Base):
clean_rules
:
Mapped
[
dict
|
None
]
=
mapped_column
(
JSON
,
nullable
=
True
,
comment
=
'野值清洗规则'
)
row_start
:
Mapped
[
int
|
None
]
=
mapped_column
(
Integer
,
nullable
=
True
,
comment
=
'数据行起始行号(1-based, 含)'
)
row_end
:
Mapped
[
int
|
None
]
=
mapped_column
(
Integer
,
nullable
=
True
,
comment
=
'数据行结束行号(1-based, 含)'
)
stored_name
:
Mapped
[
str
|
None
]
=
mapped_column
(
String
(
255
),
nullable
=
True
,
unique
=
True
,
comment
=
'合并后CSV存储文件名'
)
file_path
:
Mapped
[
str
|
None
]
=
mapped_column
(
String
(
255
),
nullable
=
True
,
comment
=
'合并后CSV路径'
)
created_at
:
Mapped
[
str
]
=
mapped_column
(
TIMESTAMP
,
nullable
=
False
,
server_default
=
text
(
'CURRENT_TIMESTAMP'
))
updated_at
:
Mapped
[
str
]
=
mapped_column
(
TIMESTAMP
,
...
...
backend/app/models/monitor.py
0 → 100644
View file @
d7a073ab
from
sqlalchemy
import
BIGINT
,
FLOAT
,
TIMESTAMP
,
Enum
,
Index
,
Integer
,
JSON
,
String
,
Text
,
text
from
sqlalchemy.orm
import
Mapped
,
mapped_column
from
app.database
import
Base
class
MonitorExperiment
(
Base
):
__tablename__
=
'monitor_experiments'
__table_args__
=
(
Index
(
'idx_exp_status'
,
'status'
),
{
'mysql_comment'
:
'实时控制试验表'
},
)
id
:
Mapped
[
int
]
=
mapped_column
(
BIGINT
,
primary_key
=
True
,
autoincrement
=
True
)
name
:
Mapped
[
str
]
=
mapped_column
(
String
(
255
),
nullable
=
False
,
comment
=
'试验名称'
)
model_id
:
Mapped
[
int
]
=
mapped_column
(
BIGINT
,
nullable
=
False
,
comment
=
'模型ID'
)
model_name
:
Mapped
[
str
]
=
mapped_column
(
String
(
255
),
nullable
=
False
,
comment
=
'模型名称'
)
package_id
:
Mapped
[
int
]
=
mapped_column
(
BIGINT
,
nullable
=
False
,
comment
=
'初始数据包ID'
)
package_name
:
Mapped
[
str
]
=
mapped_column
(
String
(
255
),
nullable
=
False
,
comment
=
'数据包名称'
)
target_temp
:
Mapped
[
float
]
=
mapped_column
(
FLOAT
,
nullable
=
False
,
comment
=
'目标温度(°C)'
)
mpc_params
:
Mapped
[
dict
]
=
mapped_column
(
JSON
,
nullable
=
False
,
comment
=
'MPC参数'
)
status
:
Mapped
[
str
]
=
mapped_column
(
Enum
(
'idle'
,
'running'
,
'stopped'
,
name
=
'exp_status_enum'
),
nullable
=
False
,
server_default
=
text
(
"'idle'"
),
comment
=
'试验状态: idle/running/stopped'
,
)
total_steps
:
Mapped
[
int
]
=
mapped_column
(
Integer
,
nullable
=
False
,
server_default
=
text
(
'0'
),
comment
=
'已采集步数'
)
start_time
:
Mapped
[
str
|
None
]
=
mapped_column
(
TIMESTAMP
,
nullable
=
True
,
comment
=
'开始时间'
)
stop_time
:
Mapped
[
str
|
None
]
=
mapped_column
(
TIMESTAMP
,
nullable
=
True
,
comment
=
'停止时间'
)
error_msg
:
Mapped
[
str
|
None
]
=
mapped_column
(
Text
,
nullable
=
True
,
comment
=
'错误信息'
)
sampling_interval
:
Mapped
[
float
]
=
mapped_column
(
FLOAT
,
nullable
=
False
,
server_default
=
text
(
'1.0'
),
comment
=
'采样周期(秒)'
)
exported
:
Mapped
[
int
]
=
mapped_column
(
Integer
,
nullable
=
False
,
server_default
=
text
(
'0'
),
comment
=
'是否已导出到历史数据'
)
created_at
:
Mapped
[
str
]
=
mapped_column
(
TIMESTAMP
,
nullable
=
False
,
server_default
=
text
(
'CURRENT_TIMESTAMP'
)
)
class
MonitorDataPoint
(
Base
):
__tablename__
=
'monitor_data_points'
__table_args__
=
(
Index
(
'idx_dp_exp_step'
,
'experiment_id'
,
'step_idx'
),
{
'mysql_comment'
:
'试验数据点表'
},
)
id
:
Mapped
[
int
]
=
mapped_column
(
BIGINT
,
primary_key
=
True
,
autoincrement
=
True
)
experiment_id
:
Mapped
[
int
]
=
mapped_column
(
BIGINT
,
nullable
=
False
,
comment
=
'试验ID'
)
step_idx
:
Mapped
[
int
]
=
mapped_column
(
Integer
,
nullable
=
False
,
comment
=
'步骤序号'
)
actual_temp
:
Mapped
[
float
]
=
mapped_column
(
FLOAT
,
nullable
=
False
,
comment
=
'实际温度(°C)'
)
reference_temp
:
Mapped
[
float
]
=
mapped_column
(
FLOAT
,
nullable
=
False
,
comment
=
'参考轨迹温度(°C)'
)
current_output
:
Mapped
[
float
]
=
mapped_column
(
FLOAT
,
nullable
=
False
,
comment
=
'电流输出(A)'
)
created_at
:
Mapped
[
str
]
=
mapped_column
(
TIMESTAMP
,
nullable
=
False
,
server_default
=
text
(
'CURRENT_TIMESTAMP'
)
)
backend/app/services/monitor_service.py
0 → 100644
View file @
d7a073ab
"""实时监控服务 — MPC 控制循环在后台线程中运行。
控制策略(真实硬件模式):
- 每步从数据包文件重新读取最新 seq_len 条记录作为真实传感器历史
- 用历史末尾温度 y(k) 与上步预测 y(k|k-1) 计算反馈校正量
- MPC + LSTM 多步滚动预测,最小化二次代价函数,输出最优电流 u(k)
- 将 u(k) 发送给程控电源(_send_to_power_supply,待硬件实现)
- 记录 y(k+1|k) 供下步反馈校正
- 等待采样周期后重复
"""
from
__future__
import
annotations
import
threading
from
datetime
import
datetime
from
pathlib
import
Path
from
typing
import
Any
import
numpy
as
np
from
app.database
import
db_session
from
app.ml.mpc_controller
import
CURRENT_IDX
,
FEATURE_COLS
,
MPCController
,
MPCParams
,
TARGET_IDX
from
app.models
import
DataFile
,
DataPackage
,
DataPackageFile
from
app.models.monitor
import
MonitorDataPoint
,
MonitorExperiment
from
app.models.train_management
import
SavedModel
# ── 取消事件注册表 ────────────────────────────────────────────────────────────
_cancel_events
:
dict
[
int
,
threading
.
Event
]
=
{}
_registry_lock
=
threading
.
Lock
()
# 最大控制步数(安全上限,防止无限运行)
_MAX_STEPS
=
600
class
MonitorService
:
def
__init__
(
self
)
->
None
:
self
.
_base_dir
=
Path
(
__file__
)
.
resolve
()
.
parents
[
2
]
self
.
_models_dir
=
self
.
_base_dir
/
'saved_models'
# ── 模型 / 数据包下拉列表 ─────────────────────────────────────────────────
def
list_models
(
self
)
->
list
[
dict
[
str
,
Any
]]:
with
db_session
()
as
session
:
rows
=
session
.
query
(
SavedModel
)
.
order_by
(
SavedModel
.
created_at
.
desc
())
.
all
()
return
[
{
'id'
:
m
.
id
,
'model_name'
:
m
.
model_name
,
'package_name'
:
m
.
package_name
,
'seq_len'
:
m
.
params
.
get
(
'seq_len'
,
20
)
if
m
.
params
else
20
,
}
for
m
in
rows
]
def
list_packages
(
self
)
->
list
[
dict
[
str
,
Any
]]:
with
db_session
()
as
session
:
rows
=
(
session
.
query
(
DataPackage
)
.
order_by
(
DataPackage
.
created_at
.
desc
())
.
all
()
)
return
[{
'id'
:
p
.
id
,
'name'
:
p
.
name
,
'data_count'
:
p
.
data_count
}
for
p
in
rows
]
# ── 试验 CRUD ─────────────────────────────────────────────────────────────
def
list_experiments
(
self
)
->
list
[
dict
[
str
,
Any
]]:
with
db_session
()
as
session
:
rows
=
(
session
.
query
(
MonitorExperiment
)
.
order_by
(
MonitorExperiment
.
created_at
.
desc
())
.
all
()
)
return
[
self
.
_exp_to_dict
(
e
)
for
e
in
rows
]
def
get_experiment
(
self
,
exp_id
:
int
)
->
dict
[
str
,
Any
]:
with
db_session
()
as
session
:
exp
=
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
if
not
exp
:
raise
ValueError
(
'试验不存在'
)
return
self
.
_exp_to_dict
(
exp
)
def
create_experiment
(
self
,
name
:
str
,
model_id
:
int
,
package_id
:
int
,
target_temp
:
float
,
sampling_interval
:
float
,
mpc_params
:
dict
,
)
->
dict
[
str
,
Any
]:
with
db_session
()
as
session
:
model
=
session
.
query
(
SavedModel
)
.
filter
(
SavedModel
.
id
==
model_id
)
.
first
()
if
not
model
:
raise
ValueError
(
'模型不存在'
)
pkg
=
session
.
query
(
DataPackage
)
.
filter
(
DataPackage
.
id
==
package_id
)
.
first
()
if
not
pkg
:
raise
ValueError
(
'数据包不存在'
)
exp
=
MonitorExperiment
(
name
=
name
,
model_id
=
model_id
,
model_name
=
model
.
model_name
,
package_id
=
package_id
,
package_name
=
pkg
.
name
,
target_temp
=
target_temp
,
sampling_interval
=
sampling_interval
,
mpc_params
=
mpc_params
,
status
=
'idle'
,
total_steps
=
0
,
)
session
.
add
(
exp
)
session
.
commit
()
session
.
refresh
(
exp
)
return
self
.
_exp_to_dict
(
exp
)
def
delete_experiment
(
self
,
exp_id
:
int
)
->
None
:
# 先停止正在运行的仿真
self
.
_cancel_thread
(
exp_id
)
with
db_session
()
as
session
:
exp
=
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
if
not
exp
:
raise
ValueError
(
'试验不存在'
)
session
.
query
(
MonitorDataPoint
)
.
filter
(
MonitorDataPoint
.
experiment_id
==
exp_id
)
.
delete
()
session
.
delete
(
exp
)
session
.
commit
()
# ── 控制操作 ──────────────────────────────────────────────────────────────
def
start_experiment
(
self
,
exp_id
:
int
)
->
None
:
with
db_session
()
as
session
:
exp
=
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
if
not
exp
:
raise
ValueError
(
'试验不存在'
)
if
exp
.
status
==
'running'
:
raise
ValueError
(
'试验正在运行中'
)
exp
.
status
=
'running'
exp
.
start_time
=
datetime
.
now
()
exp
.
error_msg
=
None
session
.
commit
()
# 若存在旧线程先取消
self
.
_cancel_thread
(
exp_id
)
cancel_event
=
threading
.
Event
()
with
_registry_lock
:
_cancel_events
[
exp_id
]
=
cancel_event
t
=
threading
.
Thread
(
target
=
self
.
_simulation_worker
,
args
=
(
exp_id
,
cancel_event
),
daemon
=
True
,
name
=
f
'mpc-sim-{exp_id}'
,
)
t
.
start
()
def
stop_experiment
(
self
,
exp_id
:
int
)
->
None
:
self
.
_cancel_thread
(
exp_id
)
with
db_session
()
as
session
:
exp
=
(
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
)
if
exp
and
exp
.
status
==
'running'
:
exp
.
status
=
'stopped'
exp
.
stop_time
=
datetime
.
now
()
session
.
commit
()
# ── 数据查询 ──────────────────────────────────────────────────────────────
def
get_data_points
(
self
,
exp_id
:
int
,
from_step
:
int
=
0
)
->
list
[
dict
[
str
,
Any
]]:
with
db_session
()
as
session
:
rows
=
(
session
.
query
(
MonitorDataPoint
)
.
filter
(
MonitorDataPoint
.
experiment_id
==
exp_id
,
MonitorDataPoint
.
step_idx
>=
from_step
,
)
.
order_by
(
MonitorDataPoint
.
step_idx
)
.
all
()
)
return
[
self
.
_point_to_dict
(
p
)
for
p
in
rows
]
# ── 报告 ──────────────────────────────────────────────────────────────────
def
get_report
(
self
,
exp_id
:
int
)
->
dict
[
str
,
Any
]:
with
db_session
()
as
session
:
exp
=
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
if
not
exp
:
raise
ValueError
(
'试验不存在'
)
rows
=
(
session
.
query
(
MonitorDataPoint
)
.
filter
(
MonitorDataPoint
.
experiment_id
==
exp_id
)
.
order_by
(
MonitorDataPoint
.
step_idx
)
.
all
()
)
if
not
rows
:
return
{
'experiment'
:
self
.
_exp_to_dict
(
exp
),
'summary'
:
None
,
'points'
:
[]}
actuals
=
np
.
array
([
p
.
actual_temp
for
p
in
rows
],
dtype
=
np
.
float64
)
references
=
np
.
array
([
p
.
reference_temp
for
p
in
rows
],
dtype
=
np
.
float64
)
currents
=
np
.
array
([
p
.
current_output
for
p
in
rows
],
dtype
=
np
.
float64
)
target
=
float
(
exp
.
target_temp
)
errors
=
actuals
-
target
mae
=
float
(
np
.
mean
(
np
.
abs
(
errors
)))
rmse
=
float
(
np
.
sqrt
(
np
.
mean
(
errors
**
2
)))
# 超调量(仅升温场景)
if
target
>
actuals
[
0
]:
overshoot
=
max
(
0.0
,
float
(
np
.
max
(
actuals
))
-
target
)
else
:
overshoot
=
max
(
0.0
,
target
-
float
(
np
.
min
(
actuals
)))
# 调节时间:首次进入 ±2°C 稳定带并持续 10 步
settling_step
=
None
band
=
2.0
for
i
in
range
(
len
(
actuals
)):
if
abs
(
actuals
[
i
]
-
target
)
<=
band
:
end
=
min
(
i
+
10
,
len
(
actuals
))
if
all
(
abs
(
actuals
[
j
]
-
target
)
<=
band
*
1.5
for
j
in
range
(
i
,
end
)):
settling_step
=
int
(
i
)
break
# 图表数据(最多 600 点)
n
=
len
(
rows
)
step
=
max
(
1
,
n
//
600
)
chart
=
[
self
.
_point_to_dict
(
rows
[
i
])
for
i
in
range
(
0
,
n
,
step
)]
return
{
'experiment'
:
self
.
_exp_to_dict
(
exp
),
'summary'
:
{
'total_steps'
:
n
,
'duration_s'
:
round
(
n
*
float
(
exp
.
sampling_interval
or
1.0
),
1
),
'target_temp'
:
target
,
'initial_temp'
:
round
(
float
(
actuals
[
0
]),
3
),
'final_temp'
:
round
(
float
(
actuals
[
-
1
]),
3
),
'mae'
:
round
(
mae
,
4
),
'rmse'
:
round
(
rmse
,
4
),
'overshoot'
:
round
(
overshoot
,
4
),
'settling_step'
:
settling_step
,
'avg_current'
:
round
(
float
(
np
.
mean
(
currents
)),
4
),
'max_current'
:
round
(
float
(
np
.
max
(
currents
)),
4
),
},
'points'
:
chart
,
}
# ── 历史数据列表 ───────────────────────────────────────────────────────────
def
list_history_experiments
(
self
)
->
list
[
dict
[
str
,
Any
]]:
with
db_session
()
as
session
:
rows
=
(
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
exported
==
1
)
.
order_by
(
MonitorExperiment
.
stop_time
.
desc
())
.
all
()
)
return
[
self
.
_exp_to_dict
(
e
)
for
e
in
rows
]
# ── 导出到历史数据 ─────────────────────────────────────────────────────────
def
export_to_history
(
self
,
exp_id
:
int
)
->
dict
[
str
,
Any
]:
with
db_session
()
as
session
:
exp
=
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
if
not
exp
:
raise
ValueError
(
'试验不存在'
)
if
exp
.
status
==
'running'
:
raise
ValueError
(
'请先停止试验再导出'
)
exp
.
exported
=
1
session
.
commit
()
return
{
'exported'
:
True
}
# ── 控制线程 ───────────────────────────────────────────────────────────────
def
_simulation_worker
(
self
,
exp_id
:
int
,
cancel_event
:
threading
.
Event
)
->
None
:
"""真实 MPC 控制循环。
每步从数据包文件重新读取最新 seq_len 条记录作为真实传感器历史,
运行 MPC 优化后将电流指令下发给程控电源,并进行单步预测误差反馈校正。
"""
try
:
# ── 读取试验配置 ──────────────────────────────────────────────────
with
db_session
()
as
session
:
exp
=
(
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
first
()
)
if
not
exp
:
return
model_id
=
exp
.
model_id
package_id
=
exp
.
package_id
target_temp
=
float
(
exp
.
target_temp
)
sampling_interval
=
float
(
exp
.
sampling_interval
or
1.0
)
mpc_params_dict
=
exp
.
mpc_params
or
{}
start_step
=
int
(
exp
.
total_steps
)
# ── 加载模型文件 ──────────────────────────────────────────────────
with
db_session
()
as
session
:
model_row
=
(
session
.
query
(
SavedModel
)
.
filter
(
SavedModel
.
id
==
model_id
)
.
first
()
)
if
not
model_row
:
self
.
_set_error
(
exp_id
,
'模型不存在'
)
return
model_path
=
self
.
_base_dir
/
model_row
.
file_path
if
not
model_path
.
exists
():
self
.
_set_error
(
exp_id
,
f
'模型文件不存在: {model_path}'
)
return
# ── 构造 MPC 参数 ─────────────────────────────────────────────────
params
=
MPCParams
(
P
=
int
(
mpc_params_dict
.
get
(
'P'
,
20
)),
M
=
int
(
mpc_params_dict
.
get
(
'M'
,
5
)),
Q
=
float
(
mpc_params_dict
.
get
(
'Q'
,
10.0
)),
R
=
float
(
mpc_params_dict
.
get
(
'R'
,
0.1
)),
alpha
=
float
(
mpc_params_dict
.
get
(
'alpha'
,
0.8
)),
u_min
=
float
(
mpc_params_dict
.
get
(
'u_min'
,
0.0
)),
u_max
=
float
(
mpc_params_dict
.
get
(
'u_max'
,
10.0
)),
du_min
=
float
(
mpc_params_dict
.
get
(
'du_min'
,
-
2.0
)),
du_max
=
float
(
mpc_params_dict
.
get
(
'du_max'
,
2.0
)),
y_min
=
float
(
mpc_params_dict
.
get
(
'y_min'
,
-
50.0
)),
y_max
=
float
(
mpc_params_dict
.
get
(
'y_max'
,
200.0
)),
correction_gain
=
float
(
mpc_params_dict
.
get
(
'correction_gain'
,
0.5
)),
)
ctrl
=
MPCController
.
from_checkpoint
(
model_path
,
params
)
seq_len
=
ctrl
.
predictor
.
seq_len
# ── 初始热身:从数据包读取最新记录填满缓冲区 ─────────────────────
init_records
=
self
.
_load_package_records_tail
(
package_id
,
seq_len
+
10
)
if
len
(
init_records
)
<
seq_len
:
self
.
_set_error
(
exp_id
,
f
'数据包记录不足 {seq_len} 条(当前 {len(init_records)} 条),无法初始化'
,
)
return
for
rec
in
init_records
:
ctrl
.
update_measurement
(
rec
)
if
not
ctrl
.
ready
():
self
.
_set_error
(
exp_id
,
'状态估计器初始化失败'
)
return
# y(k+1|k):上一步对下一时刻温度的预测,初始设为 None
y_pred_next
:
float
|
None
=
None
# ── 主控制循环 ────────────────────────────────────────────────────
for
step
in
range
(
start_step
,
_MAX_STEPS
):
if
cancel_event
.
is_set
():
break
# 1. 从数据包文件重新读取最新 seq_len 条真实传感器记录
fresh_records
=
self
.
_load_package_records_tail
(
package_id
,
seq_len
)
if
len
(
fresh_records
)
<
seq_len
:
# 数据还未更新到足够条数,跳过本步等待
cancel_event
.
wait
(
sampling_interval
)
continue
# 2. 用真实数据刷新状态缓冲区(保留 correction_offset 和 u_prev)
ctrl
.
_estimator
.
_buffer
=
[]
for
rec
in
fresh_records
:
ctrl
.
update_measurement
(
rec
)
# 3. 获取当前真实温度 y(k)
state
=
ctrl
.
_estimator
.
get_state
()
# (seq_len, F)
y_k
=
float
(
state
[
-
1
,
TARGET_IDX
])
# 4. 反馈校正:误差 = y(k) - y(k|k-1)
if
y_pred_next
is
not
None
:
error
=
y_k
-
y_pred_next
ctrl
.
_correction_offset
+=
ctrl
.
p
.
correction_gain
*
error
# 5. MPC 优化求 u(k)
# y_actual=None:校正已在步骤 4 手动完成,避免 ctrl.step() 内部重复
try
:
u_cmd
=
ctrl
.
step
(
yr
=
target_temp
,
y_actual
=
None
)
except
Exception
:
u_cmd
=
ctrl
.
_u_prev
# 6. 预测 y(k+1|k),供下一步反馈校正使用
# 将 u(k) 放入序列末尾,向前滚动一步预测
new_row
=
state
[
-
1
]
.
copy
()
new_row
[
CURRENT_IDX
]
=
u_cmd
seq_with_u
=
np
.
vstack
([
state
[
1
:],
new_row
])
# (seq_len, F)
y_pred_next
=
(
ctrl
.
predictor
.
predict_one
(
ctrl
.
predictor
.
_norm
(
seq_with_u
))
+
ctrl
.
_correction_offset
)
# 7. 参考轨迹第一步(用于前端展示)
w_k
=
float
(
ctrl
.
_reference_trajectory
(
y_k
,
target_temp
)[
0
])
# 8. 将电流指令发送给程控电源
self
.
_send_to_power_supply
(
u_cmd
,
exp_id
)
# 9. 持久化本步数据
with
db_session
()
as
session
:
session
.
add
(
MonitorDataPoint
(
experiment_id
=
exp_id
,
step_idx
=
step
,
actual_temp
=
round
(
y_k
,
4
),
reference_temp
=
round
(
w_k
,
4
),
current_output
=
round
(
u_cmd
,
4
),
)
)
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
update
({
'total_steps'
:
step
+
1
})
session
.
commit
()
# 10. 等待采样周期(可被 stop 信号提前中断)
cancel_event
.
wait
(
sampling_interval
)
# ── 正常结束(达到最大步数) ──────────────────────────────────────
with
db_session
()
as
session
:
exp_row
=
(
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
,
MonitorExperiment
.
status
==
'running'
,
)
.
first
()
)
if
exp_row
:
exp_row
.
status
=
'stopped'
exp_row
.
stop_time
=
datetime
.
now
()
session
.
commit
()
except
Exception
as
exc
:
self
.
_set_error
(
exp_id
,
str
(
exc
))
finally
:
with
_registry_lock
:
_cancel_events
.
pop
(
exp_id
,
None
)
# ── 工具方法 ──────────────────────────────────────────────────────────────
def
_cancel_thread
(
self
,
exp_id
:
int
)
->
None
:
with
_registry_lock
:
event
=
_cancel_events
.
get
(
exp_id
)
if
event
:
event
.
set
()
def
_set_error
(
self
,
exp_id
:
int
,
msg
:
str
)
->
None
:
try
:
with
db_session
()
as
session
:
session
.
query
(
MonitorExperiment
)
.
filter
(
MonitorExperiment
.
id
==
exp_id
)
.
update
(
{
'status'
:
'stopped'
,
'error_msg'
:
msg
,
'stop_time'
:
datetime
.
now
(),
}
)
session
.
commit
()
finally
:
with
_registry_lock
:
_cancel_events
.
pop
(
exp_id
,
None
)
def
_load_package_records
(
self
,
package_id
:
int
)
->
list
[
dict
[
str
,
Any
]]:
"""从数据包加载所有记录。
若数据包已保存合并 CSV(stored_name 非空),则直接读取该文件;
否则回退为逐源文件读取合并。
"""
from
app.services.data_management_service
import
DataManagementService
dm
=
DataManagementService
()
with
db_session
()
as
session
:
pkg
=
session
.
query
(
DataPackage
)
.
filter
(
DataPackage
.
id
==
package_id
)
.
first
()
if
not
pkg
:
return
[]
pkg_stored_name
:
str
|
None
=
pkg
.
stored_name
# 同时准备源文件信息(fallback 用)
pf_rows
=
(
session
.
query
(
DataPackageFile
)
.
filter
(
DataPackageFile
.
package_id
==
package_id
)
.
order_by
(
DataPackageFile
.
sort_order
.
asc
())
.
all
()
)
file_ids
=
[
pf
.
file_id
for
pf
in
pf_rows
]
files
=
session
.
query
(
DataFile
)
.
filter
(
DataFile
.
id
.
in_
(
file_ids
))
.
all
()
if
file_ids
else
[]
file_map
=
{
f
.
id
:
f
for
f
in
files
}
def
_filter_valid
(
records
:
list
[
dict
[
str
,
Any
]])
->
list
[
dict
[
str
,
Any
]]:
return
[
r
for
r
in
records
if
r
.
get
(
'actual_temperature'
)
is
not
None
and
float
(
r
[
'actual_temperature'
])
<
9000
]
# 优先读取合并 CSV 文件
if
pkg_stored_name
:
pkg_file
=
dm
.
_upload_dir
/
'packages'
/
pkg_stored_name
if
pkg_file
.
exists
():
records
,
_
=
dm
.
_read_records
(
pkg_file
,
limit
=
None
)
return
_filter_valid
(
records
)
# Fallback:逐源文件读取
if
not
file_ids
:
return
[]
all_records
:
list
[
dict
[
str
,
Any
]]
=
[]
for
fid
in
file_ids
:
if
fid
not
in
file_map
:
continue
fmeta
=
file_map
[
fid
]
try
:
path
=
dm
.
_resolve_local_file_path
(
fmeta
.
file_path
,
fmeta
.
stored_name
)
records
,
_
=
dm
.
_read_records
(
path
,
limit
=
None
)
all_records
.
extend
(
records
)
except
Exception
:
continue
return
_filter_valid
(
all_records
)
def
_load_package_records_tail
(
self
,
package_id
:
int
,
n
:
int
)
->
list
[
dict
[
str
,
Any
]]:
"""从数据包加载最后 n 条有效记录(按文件顺序拼接后取尾部)。
每次控制步调用,始终读取文件最新内容,模拟传感器实时写入场景。
无效行(actual_temperature 为 None 或 ≥ 9000)已在 _load_package_records 过滤。
"""
all_records
=
self
.
_load_package_records
(
package_id
)
return
all_records
[
-
n
:]
if
len
(
all_records
)
>=
n
else
all_records
def
_send_to_power_supply
(
self
,
u_cmd
:
float
,
exp_id
:
int
)
->
None
:
"""将电流指令发送到程控电源。
TODO: 根据实际硬件接口实现,例如:
- VISA / GPIB(仪器总线)
- Modbus TCP/RTU
- RS-232/RS-485 串口协议
当前为占位实现,仅写入日志。
"""
import
logging
logging
.
getLogger
(
__name__
)
.
info
(
'[exp=
%
d] 程控电源指令 →
%.4
f A'
,
exp_id
,
u_cmd
)
@
staticmethod
def
_exp_to_dict
(
exp
:
MonitorExperiment
)
->
dict
[
str
,
Any
]:
return
{
'id'
:
exp
.
id
,
'name'
:
exp
.
name
,
'model_id'
:
exp
.
model_id
,
'model_name'
:
exp
.
model_name
,
'package_id'
:
exp
.
package_id
,
'package_name'
:
exp
.
package_name
,
'target_temp'
:
exp
.
target_temp
,
'sampling_interval'
:
exp
.
sampling_interval
,
'mpc_params'
:
exp
.
mpc_params
,
'status'
:
exp
.
status
,
'total_steps'
:
exp
.
total_steps
,
'start_time'
:
(
exp
.
start_time
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
exp
.
start_time
and
not
isinstance
(
exp
.
start_time
,
str
)
else
exp
.
start_time
),
'stop_time'
:
(
exp
.
stop_time
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
exp
.
stop_time
and
not
isinstance
(
exp
.
stop_time
,
str
)
else
exp
.
stop_time
),
'error_msg'
:
exp
.
error_msg
,
'exported'
:
bool
(
exp
.
exported
),
'created_at'
:
(
exp
.
created_at
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
exp
.
created_at
and
not
isinstance
(
exp
.
created_at
,
str
)
else
exp
.
created_at
),
}
@
staticmethod
def
_point_to_dict
(
p
:
MonitorDataPoint
)
->
dict
[
str
,
Any
]:
return
{
'step_idx'
:
p
.
step_idx
,
'actual_temp'
:
p
.
actual_temp
,
'reference_temp'
:
p
.
reference_temp
,
'current_output'
:
p
.
current_output
,
}
backend/app/services/package_management_service.py
View file @
d7a073ab
import
csv
import
io
from
datetime
import
datetime
from
pathlib
import
Path
from
typing
import
Any
from
sqlalchemy
import
func
...
...
@@ -8,6 +12,8 @@ from app.services.data_management_service import DataManagementService
class
PackageManagementService
:
# CSV 存储目录与数据文件相同
_PACKAGES_SUBDIR
=
'packages'
def
__init__
(
self
)
->
None
:
self
.
_dm
=
DataManagementService
()
...
...
@@ -184,18 +190,14 @@ class PackageManagementService:
file_map
=
{
f
.
id
:
f
for
f
in
files
}
needs_full_merge
=
bool
((
clean_rules
and
clean_rules
.
get
(
'enabled'
))
or
row_start
is
not
None
or
row_end
is
not
None
)
if
needs_full_merge
:
result
=
self
.
_merge_records
(
file_ids
,
file_map
,
limit
=
None
,
clean_rules
=
clean_rules
,
row_start
=
row_start
,
row_end
=
row_end
,
)
total_count
=
result
[
'count'
]
stored_rules
:
dict
|
None
=
clean_rules
else
:
total_count
=
sum
(
f
.
data_count
for
f
in
files
)
stored_rules
=
None
# 始终执行完整合并,以便写入磁盘 CSV
merged
=
self
.
_merge_records
(
file_ids
,
file_map
,
limit
=
None
,
clean_rules
=
clean_rules
,
row_start
=
row_start
,
row_end
=
row_end
,
)
total_count
=
merged
[
'count'
]
stored_rules
:
dict
|
None
=
clean_rules
if
(
clean_rules
and
clean_rules
.
get
(
'enabled'
))
else
None
pkg
=
DataPackage
(
name
=
name
,
...
...
@@ -207,7 +209,16 @@ class PackageManagementService:
row_end
=
row_end
,
)
session
.
add
(
pkg
)
session
.
flush
()
session
.
flush
()
# 获取 pkg.id
# 写入合并 CSV 文件
pkg_dir
=
self
.
_dm
.
_upload_dir
/
'packages'
pkg_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
ts
=
datetime
.
now
()
.
strftime
(
'
%
Y
%
m
%
d
%
H
%
M
%
S
%
f'
)
stored_name
=
f
'pkg_{pkg.id}_{ts}.csv'
self
.
_write_records_csv
(
merged
[
'records'
],
pkg_dir
/
stored_name
)
pkg
.
stored_name
=
stored_name
pkg
.
file_path
=
f
'/app/uploads/packages/{stored_name}'
for
idx
,
fid
in
enumerate
(
file_ids
):
pf
=
DataPackageFile
(
package_id
=
pkg
.
id
,
file_id
=
fid
,
sort_order
=
idx
)
...
...
@@ -223,9 +234,15 @@ class PackageManagementService:
pkg
=
session
.
query
(
DataPackage
)
.
filter
(
DataPackage
.
id
==
db_id
)
.
first
()
if
not
pkg
:
raise
ValueError
(
'数据包不存在'
)
stored_name
=
pkg
.
stored_name
session
.
query
(
DataPackageFile
)
.
filter
(
DataPackageFile
.
package_id
==
db_id
)
.
delete
()
session
.
delete
(
pkg
)
session
.
commit
()
# 删除磁盘上的合并 CSV 文件
if
stored_name
:
pkg_file
=
self
.
_dm
.
_upload_dir
/
'packages'
/
stored_name
if
pkg_file
.
exists
():
pkg_file
.
unlink
()
def
get_package_records
(
self
,
package_id
:
str
,
limit
:
int
=
500
)
->
dict
[
str
,
Any
]:
db_id
=
self
.
_parse_int_id
(
package_id
,
'数据包ID'
)
...
...
@@ -369,10 +386,27 @@ class PackageManagementService:
'data_count'
:
pkg
.
data_count
,
'row_start'
:
pkg
.
row_start
,
'row_end'
:
pkg
.
row_end
,
'stored_name'
:
pkg
.
stored_name
,
'file_path'
:
pkg
.
file_path
,
'created_at'
:
pkg
.
created_at
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
pkg
.
created_at
else
''
,
'updated_at'
:
pkg
.
updated_at
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
if
pkg
.
updated_at
else
''
,
}
@
staticmethod
def
_write_records_csv
(
records
:
list
[
dict
[
str
,
Any
]],
dest
:
Path
)
->
None
:
"""将合并后的记录写入 CSV 文件(UTF-8 BOM,带英文表头)。"""
with
dest
.
open
(
'w'
,
newline
=
''
,
encoding
=
'utf-8-sig'
)
as
f
:
writer
=
csv
.
writer
(
f
)
writer
.
writerow
([
'time'
,
'current'
,
'voltage'
,
'set_temperature'
,
'actual_temperature'
])
for
r
in
records
:
writer
.
writerow
([
r
.
get
(
'time'
,
''
),
r
.
get
(
'current'
,
''
),
r
.
get
(
'voltage'
,
''
),
r
.
get
(
'set_temperature'
,
''
),
r
.
get
(
'actual_temperature'
,
''
),
])
@
staticmethod
def
_parse_int_id
(
value
:
str
,
label
:
str
=
'ID'
)
->
int
:
try
:
...
...
backend/requirements.txt
View file @
d7a073ab
...
...
@@ -7,4 +7,5 @@ PyMySQL
openpyxl
xlrd
numpy
scipy
torch
\ No newline at end of file
backend/uploads/packages/pkg_6_20260507172252193173.csv
0 → 100644
View file @
d7a073ab
time,current,voltage,set_temperature,actual_temperature
2026/1/22 9:04,0.0,0.09,19.06,19.06
2026/1/22 9:05,0.0,0.09,19.06,19.053
2026/1/22 9:05,0.0,0.09,19.06,19.047
2026/1/22 9:05,0.0,0.09,19.06,19.072
2026/1/22 9:05,0.0,0.09,19.06,19.061
2026/1/22 9:05,0.0,0.09,19.06,19.082
2026/1/22 9:05,0.0,0.09,19.06,19.064
2026/1/22 9:05,0.0,0.09,19.06,19.062
2026/1/22 9:05,0.0,0.09,19.06,19.059
2026/1/22 9:05,0.0,0.09,19.06,19.067
backend/uploads/packages/pkg_7_20260507172430554650.csv
0 → 100644
View file @
d7a073ab
time,current,voltage,set_temperature,actual_temperature
2026/1/22 9:15,0.0,0.09,19.346,19.359
2026/1/22 9:16,0.0,0.09,19.346,19.328
2026/1/22 9:16,0.0,0.09,19.346,19.351
2026/1/22 9:16,0.0,0.09,19.346,19.331
2026/1/22 9:16,0.0,0.09,19.346,19.33
2026/1/22 9:16,0.0,0.09,19.346,19.334
2026/1/22 9:16,0.0,0.09,19.322,19.322
2026/1/22 9:16,0.0,0.09,19.322,19.335
2026/1/22 9:16,0.0,0.09,19.322,19.352
2026/1/22 9:16,0.0,0.09,19.322,19.321
2026/1/22 14:16,0.0,0.09,-10.327,-10.857
2026/1/22 14:16,0.0,0.09,-9.797,-11.068
2026/1/22 14:16,0.018,0.09,-9.267,-11.31
2026/1/22 14:16,0.035,0.15,-8.737,-11.534
2026/1/22 14:16,0.059,0.25,-8.207,-11.771
2026/1/22 14:17,0.089,0.37,-7.677,-12.012
2026/1/22 14:17,0.125,0.52,-7.147,-12.23
2026/1/22 14:17,0.17,0.7,-6.617,-12.481
2026/1/22 14:17,0.221,0.92,-6.087,-12.709
2026/1/22 14:17,0.282,1.17,-5.557,-12.958
2026/1/22 14:17,0.35,1.46,-5.027,-13.189
2026/1/22 14:17,0.427,1.79,-4.497,-13.42
2026/1/22 14:17,0.516,2.2,-3.967,-13.672
2026/1/22 14:17,0.612,2.66,-3.437,-13.893
2026/1/22 14:17,0.72,3.23,-2.907,-14.132
2026/1/22 14:18,0.838,3.92,-2.377,-14.364
2026/1/22 14:18,0.967,4.82,-1.847,-14.596
2026/1/22 14:18,1.109,6.02,-1.317,-14.836
2026/1/22 14:18,1.263,7.74,-0.787,-15.075
2026/1/22 14:18,1.427,10.4,-0.257,-15.295
2026/1/22 14:18,1.603,14.75,0.273,-15.513
2026/1/22 14:18,1.786,21.29,0.803,-15.686
2026/1/22 14:18,1.97,29.05,1.333,-15.777
2026/1/22 14:18,2.127,35.57,1.863,-15.627
2026/1/22 14:18,2.249,40.6,2.393,-15.208
2026/1/22 14:19,2.327,44.06,2.923,-14.487
2026/1/22 14:19,2.376,46.15,3.453,-13.601
2026/1/22 14:19,2.395,47.21,3.983,-12.548
2026/1/22 14:19,2.4,47.59,4.513,-11.441
2026/1/22 14:19,2.402,47.79,5.043,-10.355
2026/1/22 14:19,2.399,47.79,5.562253173,-9.273
2026/1/22 14:19,2.397,47.78,6.063479747,-8.235
2026/1/22 14:19,2.391,47.68,6.547305539,-7.215
2026/1/22 14:19,2.387,47.59,7.014334642,-6.24
2026/1/22 14:19,2.371,47.21,7.465150176,-5.229
2026/1/22 14:20,2.358,46.81,7.900315018,-4.272
2026/1/22 14:20,2.345,46.4,8.320372505,-3.348
2026/1/22 14:20,2.321,45.74,8.725847108,-2.397
2026/1/22 14:20,2.3,45.07,9.117245094,-1.496
2026/1/22 14:20,2.278,44.38,9.495055151,-0.625
2026/1/22 14:20,2.255,43.64,9.859749003,0.224
2026/1/22 14:20,2.236,43.02,10.211782,1.006
2026/1/22 14:20,2.223,42.57,10.55159368,1.729
2026/1/22 14:20,2.212,42.22,10.87960832,2.405
2026/1/22 14:20,2.197,41.79,11.19623547,3.09
2026/1/22 14:21,2.183,41.34,11.50187047,3.742
2026/1/22 14:21,2.17,40.95,11.79689492,4.357
2026/1/22 14:21,2.153,40.45,12.08167718,4.983
2026/1/22 14:21,2.144,40.11,12.35657284,5.536
2026/1/22 14:21,2.127,39.64,12.6219251,6.114
2026/1/22 14:21,2.112,39.17,12.87806529,6.661
2026/1/22 14:21,2.104,38.86,13.12531321,7.149
2026/1/22 14:21,2.097,38.64,13.36397758,7.608
2026/1/22 14:21,2.089,38.42,13.59435638,8.055
2026/1/22 14:21,2.076,38.05,13.81673726,8.524
2026/1/22 14:22,2.068,37.77,14.03139787,8.944
2026/1/22 14:22,2.056,37.51,14.23860624,9.373
2026/1/22 14:22,2.053,37.34,14.43862108,9.734
2026/1/22 14:22,2.045,37.14,14.63169213,10.113
2026/1/22 14:22,2.034,36.82,14.81806044,10.499
2026/1/22 14:22,2.029,36.63,14.99795872,10.828
2026/1/22 14:22,2.024,36.47,15.17161158,11.154
2026/1/22 14:22,2.02,36.34,15.33923584,11.457
2026/1/22 14:22,2.015,36.21,15.50104079,11.758
2026/1/22 14:22,2.01,36.05,15.65722845,12.053
2026/1/22 14:23,2.002,35.83,15.80799383,12.353
2026/1/22 14:23,1.995,35.61,15.95352519,12.639
2026/1/22 14:23,1.993,35.51,16.09400422,12.883
2026/1/22 14:23,1.989,35.4,16.22960633,13.133
2026/1/22 14:23,1.984,35.26,16.36050082,13.38
2026/1/22 14:23,1.98,35.12,16.48685112,13.616
2026/1/22 14:23,1.977,35.02,16.608815,13.834
2026/1/22 14:23,1.973,34.9,16.72654473,14.056
2026/1/22 14:23,1.97,34.8,16.84018731,14.261
2026/1/22 14:23,1.966,34.69,16.94988463,14.464
2026/1/22 14:24,1.962,34.56,17.05577365,14.667
2026/1/22 14:24,1.961,34.52,17.15798658,14.835
2026/1/22 14:24,1.956,34.38,17.25665105,15.035
2026/1/22 14:24,1.954,34.3,17.35189025,15.199
2026/1/22 14:24,1.949,34.17,17.44382308,15.383
2026/1/22 14:24,1.948,34.12,17.53256434,15.529
2026/1/22 14:24,1.947,34.09,17.61822482,15.676
2026/1/22 14:24,1.947,34.08,17.70091147,15.81
2026/1/22 14:24,1.944,34.01,17.78072755,15.963
2026/1/22 14:24,1.943,33.96,17.8577727,16.096
2026/1/22 14:25,1.945,33.99,17.93214311,16.206
2026/1/22 14:25,1.942,33.94,18.00393166,16.345
2026/1/22 14:25,1.94,33.88,18.07322796,16.47
2026/1/22 14:25,1.941,33.88,18.14011855,16.577
2026/1/22 14:25,1.938,33.81,18.20468693,16.709
2026/1/22 14:25,1.937,33.77,18.26701374,16.816
frontend/dist/assets/UnderConstruction-BwhS9Ul2.css
deleted
100644 → 0
View file @
3faede64
.uc-page
[
data-v-d5068297
]
{
background
:
var
(
--bg-page
);
place-items
:
center
;
height
:
100%
;
display
:
grid
}
.uc-card
[
data-v-d5068297
]
{
background
:
var
(
--bg-white
);
border
:
1px
solid
var
(
--border-color
);
width
:
min
(
480px
,
100%
);
box-shadow
:
var
(
--shadow-card
);
text-align
:
center
;
border-radius
:
4px
;
padding
:
40px
32px
}
.uc-card
h2
[
data-v-d5068297
]
{
color
:
var
(
--text-primary
);
margin
:
0
0
12px
;
font-size
:
16px
;
font-weight
:
600
}
.uc-card
p
[
data-v-d5068297
]
{
color
:
var
(
--text-secondary
);
margin
:
0
;
font-size
:
13px
;
line-height
:
1.6
}
frontend/dist/assets/UnderConstruction-n0JcUgMW.js
deleted
100644 → 0
View file @
3faede64
import
{
Ht
as
e
,
I
as
t
,
d
as
n
,
m
as
r
,
t
as
i
}
from
"./_plugin-vue_export-helper-D1RKUtCV.js"
;
var
a
=
{
class
:
`uc-page`
},
o
=
{
class
:
`uc-card`
},
s
=
i
({
__name
:
`UnderConstruction`
,
props
:{
title
:{
type
:
String
,
default
:
`页面建设中`
}},
setup
(
i
){
return
(
s
,
c
)
=>
(
t
(),
r
(
`section`
,
a
,[
n
(
`div`
,
o
,[
n
(
`h2`
,
null
,
e
(
i
.
title
),
1
),
c
[
0
]
||=
n
(
`p`
,
null
,
`该模块正在建设中,可先使用“数据管理”完成数据上传与查看。`
,
-
1
)])]))}},[[
`__scopeId`
,
`data-v-d5068297`
]]);
export
{
s
as
default
};
\ No newline at end of file
frontend/dist/assets/_plugin-vue_export-helper-D1RKUtCV.js
deleted
100644 → 0
View file @
3faede64
function
e
(
e
){
let
t
=
Object
.
create
(
null
);
for
(
let
n
of
e
.
split
(
`,`
))
t
[
n
]
=
1
;
return
e
=>
e
in
t
}
var
t
=
{},
n
=
[],
r
=
()
=>
{},
i
=
()
=>!
1
,
a
=
e
=>
e
.
charCodeAt
(
0
)
===
111
&&
e
.
charCodeAt
(
1
)
===
110
&&
(
e
.
charCodeAt
(
2
)
>
122
||
e
.
charCodeAt
(
2
)
<
97
),
o
=
e
=>
e
.
startsWith
(
`onUpdate:`
),
s
=
Object
.
assign
,
c
=
(
e
,
t
)
=>
{
let
n
=
e
.
indexOf
(
t
);
n
>-
1
&&
e
.
splice
(
n
,
1
)},
l
=
Object
.
prototype
.
hasOwnProperty
,
u
=
(
e
,
t
)
=>
l
.
call
(
e
,
t
),
d
=
Array
.
isArray
,
f
=
e
=>
x
(
e
)
===
`[object Map]`
,
p
=
e
=>
x
(
e
)
===
`[object Set]`
,
m
=
e
=>
x
(
e
)
===
`[object Date]`
,
h
=
e
=>
typeof
e
==
`function`
,
g
=
e
=>
typeof
e
==
`string`
,
_
=
e
=>
typeof
e
==
`symbol`
,
v
=
e
=>
typeof
e
==
`object`
&&!!
e
,
y
=
e
=>
(
v
(
e
)
||
h
(
e
))
&&
h
(
e
.
then
)
&&
h
(
e
.
catch
),
b
=
Object
.
prototype
.
toString
,
x
=
e
=>
b
.
call
(
e
),
S
=
e
=>
x
(
e
).
slice
(
8
,
-
1
),
C
=
e
=>
x
(
e
)
===
`[object Object]`
,
w
=
e
=>
g
(
e
)
&&
e
!==
`NaN`
&&
e
[
0
]
!==
`-`
&&
``
+
parseInt
(
e
,
10
)
===
e
,
T
=
e
(
`,key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted`
),
ee
=
e
=>
{
let
t
=
Object
.
create
(
null
);
return
(
n
=>
t
[
n
]
||
(
t
[
n
]
=
e
(
n
)))},
te
=
/-
\w
/g
,
E
=
ee
(
e
=>
e
.
replace
(
te
,
e
=>
e
.
slice
(
1
).
toUpperCase
())),
ne
=
/
\B([
A-Z
])
/g
,
D
=
ee
(
e
=>
e
.
replace
(
ne
,
`-$1`
).
toLowerCase
()),
re
=
ee
(
e
=>
e
.
charAt
(
0
).
toUpperCase
()
+
e
.
slice
(
1
)),
ie
=
ee
(
e
=>
e
?
`on
${
re
(
e
)}
`
:
``
),
O
=
(
e
,
t
)
=>!
Object
.
is
(
e
,
t
),
ae
=
(
e
,...
t
)
=>
{
for
(
let
n
=
0
;
n
<
e
.
length
;
n
++
)
e
[
n
](...
t
)},
k
=
(
e
,
t
,
n
,
r
=!
1
)
=>
{
Object
.
defineProperty
(
e
,
t
,{
configurable
:
!
0
,
enumerable
:
!
1
,
writable
:
r
,
value
:
n
})},
oe
=
e
=>
{
let
t
=
parseFloat
(
e
);
return
isNaN
(
t
)?
e
:
t
},
se
=
e
=>
{
let
t
=
g
(
e
)?
Number
(
e
):
NaN
;
return
isNaN
(
t
)?
e
:
t
},
ce
,
le
=
()
=>
ce
||=
typeof
globalThis
<
`u`
?
globalThis
:
typeof
self
<
`u`
?
self
:
typeof
window
<
`u`
?
window
:
typeof
global
<
`u`
?
global
:{};
function
ue
(
e
){
if
(
d
(
e
)){
let
t
=
{};
for
(
let
n
=
0
;
n
<
e
.
length
;
n
++
){
let
r
=
e
[
n
],
i
=
g
(
r
)?
me
(
r
):
ue
(
r
);
if
(
i
)
for
(
let
e
in
i
)
t
[
e
]
=
i
[
e
]}
return
t
}
else
if
(
g
(
e
)
||
v
(
e
))
return
e
}
var
de
=
/;
(?![^
(
]
*
\))
/g
,
fe
=
/:
([^]
+
)
/
,
pe
=
/
\/\*[^]
*
?\*\/
/g
;
function
me
(
e
){
let
t
=
{};
return
e
.
replace
(
pe
,
``
).
split
(
de
).
forEach
(
e
=>
{
if
(
e
){
let
n
=
e
.
split
(
fe
);
n
.
length
>
1
&&
(
t
[
n
[
0
].
trim
()]
=
n
[
1
].
trim
())}}),
t
}
function
he
(
e
){
let
t
=
``
;
if
(
g
(
e
))
t
=
e
;
else
if
(
d
(
e
))
for
(
let
n
=
0
;
n
<
e
.
length
;
n
++
){
let
r
=
he
(
e
[
n
]);
r
&&
(
t
+=
r
+
` `
)}
else
if
(
v
(
e
))
for
(
let
n
in
e
)
e
[
n
]
&&
(
t
+=
n
+
` `
);
return
t
.
trim
()}
function
ge
(
e
){
if
(
!
e
)
return
null
;
let
{
class
:
t
,
style
:
n
}
=
e
;
return
t
&&!
g
(
t
)
&&
(
e
.
class
=
he
(
t
)),
n
&&
(
e
.
style
=
ue
(
n
)),
e
}
var
_e
=
`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`
,
ve
=
e
(
_e
);
_e
+
``
;
function
ye
(
e
){
return
!!
e
||
e
===
``
}
function
be
(
e
,
t
){
if
(
e
.
length
!==
t
.
length
)
return
!
1
;
let
n
=!
0
;
for
(
let
r
=
0
;
n
&&
r
<
e
.
length
;
r
++
)
n
=
xe
(
e
[
r
],
t
[
r
]);
return
n
}
function
xe
(
e
,
t
){
if
(
e
===
t
)
return
!
0
;
let
n
=
m
(
e
),
r
=
m
(
t
);
if
(
n
||
r
)
return
n
&&
r
?
e
.
getTime
()
===
t
.
getTime
():
!
1
;
if
(
n
=
_
(
e
),
r
=
_
(
t
),
n
||
r
)
return
e
===
t
;
if
(
n
=
d
(
e
),
r
=
d
(
t
),
n
||
r
)
return
n
&&
r
?
be
(
e
,
t
):
!
1
;
if
(
n
=
v
(
e
),
r
=
v
(
t
),
n
||
r
){
if
(
!
n
||!
r
||
Object
.
keys
(
e
).
length
!==
Object
.
keys
(
t
).
length
)
return
!
1
;
for
(
let
n
in
e
){
let
r
=
e
.
hasOwnProperty
(
n
),
i
=
t
.
hasOwnProperty
(
n
);
if
(
r
&&!
i
||!
r
&&
i
||!
xe
(
e
[
n
],
t
[
n
]))
return
!
1
}}
return
String
(
e
)
===
String
(
t
)}
function
Se
(
e
,
t
){
return
e
.
findIndex
(
e
=>
xe
(
e
,
t
))}
var
Ce
=
e
=>!!
(
e
&&
e
.
__v_isRef
===!
0
),
we
=
e
=>
g
(
e
)?
e
:
e
==
null
?
``
:
d
(
e
)
||
v
(
e
)
&&
(
e
.
toString
===
b
||!
h
(
e
.
toString
))?
Ce
(
e
)?
we
(
e
.
value
):
JSON
.
stringify
(
e
,
Te
,
2
):
String
(
e
),
Te
=
(
e
,
t
)
=>
Ce
(
t
)?
Te
(
e
,
t
.
value
):
f
(
t
)?{[
`Map(
${
t
.
size
}
)`
]:[...
t
.
entries
()].
reduce
((
e
,[
t
,
n
],
r
)
=>
(
e
[
Ee
(
t
,
r
)
+
` =>`
]
=
n
,
e
),{})}:
p
(
t
)?{[
`Set(
${
t
.
size
}
)`
]:[...
t
.
values
()].
map
(
e
=>
Ee
(
e
))}:
_
(
t
)?
Ee
(
t
):
v
(
t
)
&&!
d
(
t
)
&&!
C
(
t
)?
String
(
t
):
t
,
Ee
=
(
e
,
t
=
``
)
=>
_
(
e
)?
`Symbol(
${
e
.
description
??
t
})
`:e,A,De=class{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.__v_skip=!0,this.parent=A,!e&&A&&(this.index=(A.scopes||=[]).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].pause();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let e,t;if(this.scopes)for(e=0,t=this.scopes.length;e<t;e++)this.scopes[e].resume();for(e=0,t=this.effects.length;e<t;e++)this.effects[e].resume()}}run(e){if(this._active){let t=A;try{return A=this,e()}finally{A=t}}}on(){++this._on===1&&(this.prevScope=A,A=this)}off(){this._on>0&&--this._on===0&&(A=this.prevScope,this.prevScope=void 0)}stop(e){if(this._active){this._active=!1;let t,n;for(t=0,n=this.effects.length;t<n;t++)this.effects[t].stop();for(this.effects.length=0,t=0,n=this.cleanups.length;t<n;t++)this.cleanups[t]();if(this.cleanups.length=0,this.scopes){for(t=0,n=this.scopes.length;t<n;t++)this.scopes[t].stop(!0);this.scopes.length=0}if(!this.detached&&this.parent&&!e){let e=this.parent.scopes.pop();e&&e!==this&&(this.parent.scopes[this.index]=e,e.index=this.index)}this.parent=void 0}}};function Oe(e){return new De(e)}function ke(){return A}function Ae(e,t=!1){A&&A.cleanups.push(e)}var j,je=new WeakSet,Me=class{constructor(e){this.fn=e,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,A&&A.active&&A.effects.push(this)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,je.has(this)&&(je.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||Ie(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,Je(this),ze(this);let e=j,t=M;j=this,M=!0;try{return this.fn()}finally{Be(this),j=e,M=t,this.flags&=-3}}stop(){if(this.flags&1){for(let e=this.deps;e;e=e.nextDep)Ue(e);this.deps=this.depsTail=void 0,Je(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?je.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){Ve(this)&&this.run()}get dirty(){return Ve(this)}},Ne=0,Pe,Fe;function Ie(e,t=!1){if(e.flags|=8,t){e.next=Fe,Fe=e;return}e.next=Pe,Pe=e}function Le(){Ne++}function Re(){if(--Ne>0)return;if(Fe){let e=Fe;for(Fe=void 0;e;){let t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;Pe;){let t=Pe;for(Pe=void 0;t;){let n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(t){e||=t}t=n}}if(e)throw e}function ze(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Be(e){let t,n=e.depsTail,r=n;for(;r;){let e=r.prevDep;r.version===-1?(r===n&&(n=e),Ue(r),We(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function Ve(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(He(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function He(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ye)||(e.globalVersion=Ye,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Ve(e))))return;e.flags|=2;let t=e.dep,n=j,r=M;j=e,M=!0;try{ze(e);let n=e.fn(e._value);(t.version===0||O(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(e){throw t.version++,e}finally{j=n,M=r,Be(e),e.flags&=-3}}function Ue(e,t=!1){let{dep:n,prevSub:r,nextSub:i}=e;if(r&&(r.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)Ue(e,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function We(e){let{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}var M=!0,Ge=[];function Ke(){Ge.push(M),M=!1}function qe(){let e=Ge.pop();M=e===void 0?!0:e}function Je(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let e=j;j=void 0;try{t()}finally{j=e}}}var Ye=0,Xe=class{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},Ze=class{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!j||!M||j===this.computed)return;let t=this.activeLink;if(t===void 0||t.sub!==j)t=this.activeLink=new Xe(j,this),j.deps?(t.prevDep=j.depsTail,j.depsTail.nextDep=t,j.depsTail=t):j.deps=j.depsTail=t,Qe(t);else if(t.version===-1&&(t.version=this.version,t.nextDep)){let e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=j.depsTail,t.nextDep=void 0,j.depsTail.nextDep=t,j.depsTail=t,j.deps===t&&(j.deps=e)}return t}trigger(e){this.version++,Ye++,this.notify(e)}notify(e){Le();try{for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{Re()}}};function Qe(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Qe(e)}let n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}var $e=new WeakMap,et=Symbol(``),tt=Symbol(``),nt=Symbol(``);function N(e,t,n){if(M&&j){let t=$e.get(e);t||$e.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new Ze),r.map=t,r.key=n),r.track()}}function rt(e,t,n,r,i,a){let o=$e.get(e);if(!o){Ye++;return}let s=e=>{e&&e.trigger()};if(Le(),t===`
clear
`)o.forEach(s);else{let i=d(e),a=i&&w(n);if(i&&n===`
length
`){let e=Number(r);o.forEach((t,n)=>{(n===`
length
`||n===nt||!_(n)&&n>=e)&&s(t)})}else switch((n!==void 0||o.has(void 0))&&s(o.get(n)),a&&s(o.get(nt)),t){case`
add
`:i?a&&s(o.get(`
length
`)):(s(o.get(et)),f(e)&&s(o.get(tt)));break;case`
delete
`:i||(s(o.get(et)),f(e)&&s(o.get(tt)));break;case`
set
`:f(e)&&s(o.get(et));break}}Re()}function it(e,t){let n=$e.get(e);return n&&n.get(t)}function at(e){let t=I(e);return t===e?t:(N(t,`
iterate
`,nt),F(e)?t:t.map(L))}function ot(e){return N(e=I(e),`
iterate
`,nt),e}function P(e,t){return Ut(e)?Kt(Ht(e)?L(t):t):L(t)}var st={__proto__:null,[Symbol.iterator](){return ct(this,Symbol.iterator,e=>P(this,e))},concat(...e){return at(this).concat(...e.map(e=>d(e)?at(e):e))},entries(){return ct(this,`
entries
`,e=>(e[1]=P(this,e[1]),e))},every(e,t){return ut(this,`
every
`,e,t,void 0,arguments)},filter(e,t){return ut(this,`
filter
`,e,t,e=>e.map(e=>P(this,e)),arguments)},find(e,t){return ut(this,`
find
`,e,t,e=>P(this,e),arguments)},findIndex(e,t){return ut(this,`
findIndex
`,e,t,void 0,arguments)},findLast(e,t){return ut(this,`
findLast
`,e,t,e=>P(this,e),arguments)},findLastIndex(e,t){return ut(this,`
findLastIndex
`,e,t,void 0,arguments)},forEach(e,t){return ut(this,`
forEach
`,e,t,void 0,arguments)},includes(...e){return ft(this,`
includes
`,e)},indexOf(...e){return ft(this,`
indexOf
`,e)},join(e){return at(this).join(e)},lastIndexOf(...e){return ft(this,`
lastIndexOf
`,e)},map(e,t){return ut(this,`
map
`,e,t,void 0,arguments)},pop(){return pt(this,`
pop
`)},push(...e){return pt(this,`
push
`,e)},reduce(e,...t){return dt(this,`
reduce
`,e,t)},reduceRight(e,...t){return dt(this,`
reduceRight
`,e,t)},shift(){return pt(this,`
shift
`)},some(e,t){return ut(this,`
some
`,e,t,void 0,arguments)},splice(...e){return pt(this,`
splice
`,e)},toReversed(){return at(this).toReversed()},toSorted(e){return at(this).toSorted(e)},toSpliced(...e){return at(this).toSpliced(...e)},unshift(...e){return pt(this,`
unshift
`,e)},values(){return ct(this,`
values
`,e=>P(this,e))}};function ct(e,t,n){let r=ot(e),i=r[t]();return r!==e&&!F(e)&&(i._next=i.next,i.next=()=>{let e=i._next();return e.done||(e.value=n(e.value)),e}),i}var lt=Array.prototype;function ut(e,t,n,r,i,a){let o=ot(e),s=o!==e&&!F(e),c=o[t];if(c!==lt[t]){let t=c.apply(e,a);return s?L(t):t}let l=n;o!==e&&(s?l=function(t,r){return n.call(this,P(e,t),r,e)}:n.length>2&&(l=function(t,r){return n.call(this,t,r,e)}));let u=c.call(o,l,r);return s&&i?i(u):u}function dt(e,t,n,r){let i=ot(e),a=i!==e&&!F(e),o=n,s=!1;i!==e&&(a?(s=r.length===0,o=function(t,r,i){return s&&(s=!1,t=P(e,t)),n.call(this,t,P(e,r),i,e)}):n.length>3&&(o=function(t,r,i){return n.call(this,t,r,i,e)}));let c=i[t](o,...r);return s?P(e,c):c}function ft(e,t,n){let r=I(e);N(r,`
iterate
`,nt);let i=r[t](...n);return(i===-1||i===!1)&&Wt(n[0])?(n[0]=I(n[0]),r[t](...n)):i}function pt(e,t,n=[]){Ke(),Le();let r=I(e)[t].apply(e,n);return Re(),qe(),r}var mt=e(`
__proto__
,
__v_isRef
,
__isVue
`),ht=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!==`
arguments
`&&e!==`
caller
`).map(e=>Symbol[e]).filter(_));function gt(e){_(e)||(e=String(e));let t=I(this);return N(t,`
has
`,e),t.hasOwnProperty(e)}var _t=class{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if(t===`
__v_skip
`)return e.__v_skip;let r=this._isReadonly,i=this._isShallow;if(t===`
__v_isReactive
`)return!r;if(t===`
__v_isReadonly
`)return r;if(t===`
__v_isShallow
`)return i;if(t===`
__v_raw
`)return n===(r?i?Ft:Pt:i?Nt:Mt).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;let a=d(e);if(!r){let e;if(a&&(e=st[t]))return e;if(t===`
hasOwnProperty
`)return gt}let o=Reflect.get(e,t,R(e)?e:n);if((_(t)?ht.has(t):mt(t))||(r||N(e,`
get
`,t),i))return o;if(R(o)){let e=a&&w(t)?o:o.value;return r&&v(e)?Bt(e):e}return v(o)?r?Bt(o):Rt(o):o}},vt=class extends _t{constructor(e=!1){super(!1,e)}set(e,t,n,r){let i=e[t],a=d(e)&&w(t);if(!this._isShallow){let e=Ut(i);if(!F(n)&&!Ut(n)&&(i=I(i),n=I(n)),!a&&R(i)&&!R(n))return e||(i.value=n),!0}let o=a?Number(t)<e.length:u(e,t),s=Reflect.set(e,t,n,R(e)?e:r);return e===I(r)&&(o?O(n,i)&&rt(e,`
set
`,t,n,i):rt(e,`
add
`,t,n)),s}deleteProperty(e,t){let n=u(e,t),r=e[t],i=Reflect.deleteProperty(e,t);return i&&n&&rt(e,`
delete
`,t,void 0,r),i}has(e,t){let n=Reflect.has(e,t);return(!_(t)||!ht.has(t))&&N(e,`
has
`,t),n}ownKeys(e){return N(e,`
iterate
`,d(e)?`
length
`:et),Reflect.ownKeys(e)}},yt=class extends _t{constructor(e=!1){super(!0,e)}set(e,t){return!0}deleteProperty(e,t){return!0}},bt=new vt,xt=new yt,St=new vt(!0),Ct=e=>e,wt=e=>Reflect.getPrototypeOf(e);function Tt(e,t,n){return function(...r){let i=this.__v_raw,a=I(i),o=f(a),c=e===`
entries
`||e===Symbol.iterator&&o,l=e===`
keys
`&&o,u=i[e](...r),d=n?Ct:t?Kt:L;return!t&&N(a,`
iterate
`,l?tt:et),s(Object.create(u),{next(){let{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[d(e[0]),d(e[1])]:d(e),done:t}}})}}function Et(e){return function(...t){return e===`
delete
`?!1:e===`
clear
`?void 0:this}}function Dt(e,t){let n={get(n){let r=this.__v_raw,i=I(r),a=I(n);e||(O(n,a)&&N(i,`
get
`,n),N(i,`
get
`,a));let{has:o}=wt(i),s=t?Ct:e?Kt:L;if(o.call(i,n))return s(r.get(n));if(o.call(i,a))return s(r.get(a));r!==i&&r.get(n)},get size(){let t=this.__v_raw;return!e&&N(I(t),`
iterate
`,et),t.size},has(t){let n=this.__v_raw,r=I(n),i=I(t);return e||(O(t,i)&&N(r,`
has
`,t),N(r,`
has
`,i)),t===i?n.has(t):n.has(t)||n.has(i)},forEach(n,r){let i=this,a=i.__v_raw,o=I(a),s=t?Ct:e?Kt:L;return!e&&N(o,`
iterate
`,et),a.forEach((e,t)=>n.call(r,s(e),s(t),i))}};return s(n,e?{add:Et(`
add
`),set:Et(`
set
`),delete:Et(`
delete
`),clear:Et(`
clear
`)}:{add(e){let n=I(this),r=wt(n),i=I(e),a=!t&&!F(e)&&!Ut(e)?i:e;return r.has.call(n,a)||O(e,a)&&r.has.call(n,e)||O(i,a)&&r.has.call(n,i)||(n.add(a),rt(n,`
add
`,a,a)),this},set(e,n){!t&&!F(n)&&!Ut(n)&&(n=I(n));let r=I(this),{has:i,get:a}=wt(r),o=i.call(r,e);o||=(e=I(e),i.call(r,e));let s=a.call(r,e);return r.set(e,n),o?O(n,s)&&rt(r,`
set
`,e,n,s):rt(r,`
add
`,e,n),this},delete(e){let t=I(this),{has:n,get:r}=wt(t),i=n.call(t,e);i||=(e=I(e),n.call(t,e));let a=r?r.call(t,e):void 0,o=t.delete(e);return i&&rt(t,`
delete
`,e,void 0,a),o},clear(){let e=I(this),t=e.size!==0,n=e.clear();return t&&rt(e,`
clear
`,void 0,void 0,void 0),n}}),[`
keys
`,`
values
`,`
entries
`,Symbol.iterator].forEach(r=>{n[r]=Tt(r,e,t)}),n}function Ot(e,t){let n=Dt(e,t);return(t,r,i)=>r===`
__v_isReactive
`?!e:r===`
__v_isReadonly
`?e:r===`
__v_raw
`?t:Reflect.get(u(n,r)&&r in t?n:t,r,i)}var kt={get:Ot(!1,!1)},At={get:Ot(!1,!0)},jt={get:Ot(!0,!1)},Mt=new WeakMap,Nt=new WeakMap,Pt=new WeakMap,Ft=new WeakMap;function It(e){switch(e){case`
Object
`:case`
Array
`:return 1;case`
Map
`:case`
Set
`:case`
WeakMap
`:case`
WeakSet
`:return 2;default:return 0}}function Lt(e){return e.__v_skip||!Object.isExtensible(e)?0:It(S(e))}function Rt(e){return Ut(e)?e:Vt(e,!1,bt,kt,Mt)}function zt(e){return Vt(e,!1,St,At,Nt)}function Bt(e){return Vt(e,!0,xt,jt,Pt)}function Vt(e,t,n,r,i){if(!v(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;let a=Lt(e);if(a===0)return e;let o=i.get(e);if(o)return o;let s=new Proxy(e,a===2?r:n);return i.set(e,s),s}function Ht(e){return Ut(e)?Ht(e.__v_raw):!!(e&&e.__v_isReactive)}function Ut(e){return!!(e&&e.__v_isReadonly)}function F(e){return!!(e&&e.__v_isShallow)}function Wt(e){return e?!!e.__v_raw:!1}function I(e){let t=e&&e.__v_raw;return t?I(t):e}function Gt(e){return!u(e,`
__v_skip
`)&&Object.isExtensible(e)&&k(e,`
__v_skip
`,!0),e}var L=e=>v(e)?Rt(e):e,Kt=e=>v(e)?Bt(e):e;function R(e){return e?e.__v_isRef===!0:!1}function qt(e){return Yt(e,!1)}function Jt(e){return Yt(e,!0)}function Yt(e,t){return R(e)?e:new Xt(e,t)}var Xt=class{constructor(e,t){this.dep=new Ze,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:I(e),this._value=t?e:L(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){let t=this._rawValue,n=this.__v_isShallow||F(e)||Ut(e);e=n?e:I(e),O(e,t)&&(this._rawValue=e,this._value=n?e:L(e),this.dep.trigger())}};function Zt(e){e.dep&&e.dep.trigger()}function Qt(e){return R(e)?e.value:e}function $t(e){return h(e)?e():Qt(e)}var en={get:(e,t,n)=>t===`
__v_raw
`?e:Qt(Reflect.get(e,t,n)),set:(e,t,n,r)=>{let i=e[t];return R(i)&&!R(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function tn(e){return Ht(e)?e:new Proxy(e,en)}function nn(e){let t=d(e)?Array(e.length):{};for(let n in e)t[n]=sn(e,n);return t}var rn=class{constructor(e,t,n){this._object=e,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0,this._key=_(t)?t:String(t),this._raw=I(e);let r=!0,i=e;if(!d(e)||_(this._key)||!w(this._key))do r=!Wt(i)||F(i);while(r&&(i=i.__v_raw));this._shallow=r}get value(){let e=this._object[this._key];return this._shallow&&(e=Qt(e)),this._value=e===void 0?this._defaultValue:e}set value(e){if(this._shallow&&R(this._raw[this._key])){let t=this._object[this._key];if(R(t)){t.value=e;return}}this._object[this._key]=e}get dep(){return it(this._raw,this._key)}},an=class{constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}};function on(e,t,n){return R(e)?e:h(e)?new an(e):v(e)&&arguments.length>1?sn(e,t,n):qt(e)}function sn(e,t,n){return new rn(e,t,n)}var cn=class{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Ze(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ye-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&j!==this)return Ie(this,!0),!0}get value(){let e=this.dep.track();return He(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}};function ln(e,t,n=!1){let r,i;return h(e)?r=e:(r=e.get,i=e.set),new cn(r,i,n)}var un={},dn=new WeakMap,fn=void 0;function pn(e,t=!1,n=fn){if(n){let t=dn.get(n);t||dn.set(n,t=[]),t.push(e)}}function mn(e,n,i=t){let{immediate:a,deep:o,once:s,scheduler:l,augmentJob:u,call:f}=i,p=e=>o?e:F(e)||o===!1||o===0?hn(e,1):hn(e),m,g,_,v,y=!1,b=!1;if(R(e)?(g=()=>e.value,y=F(e)):Ht(e)?(g=()=>p(e),y=!0):d(e)?(b=!0,y=e.some(e=>Ht(e)||F(e)),g=()=>e.map(e=>{if(R(e))return e.value;if(Ht(e))return p(e);if(h(e))return f?f(e,2):e()})):g=h(e)?n?f?()=>f(e,2):e:()=>{if(_){Ke();try{_()}finally{qe()}}let t=fn;fn=m;try{return f?f(e,3,[v]):e(v)}finally{fn=t}}:r,n&&o){let e=g,t=o===!0?1/0:o;g=()=>hn(e(),t)}let x=ke(),S=()=>{m.stop(),x&&x.active&&c(x.effects,m)};if(s&&n){let e=n;n=(...t)=>{e(...t),S()}}let C=b?Array(e.length).fill(un):un,w=e=>{if(!(!(m.flags&1)||!m.dirty&&!e))if(n){let e=m.run();if(o||y||(b?e.some((e,t)=>O(e,C[t])):O(e,C))){_&&_();let t=fn;fn=m;try{let t=[e,C===un?void 0:b&&C[0]===un?[]:C,v];C=e,f?f(n,3,t):n(...t)}finally{fn=t}}}else m.run()};return u&&u(w),m=new Me(g),m.scheduler=l?()=>l(w,!1):w,v=e=>pn(e,!1,m),_=m.onStop=()=>{let e=dn.get(m);if(e){if(f)f(e,4);else for(let t of e)t();dn.delete(m)}},n?a?w(!0):C=m.run():l?l(w.bind(null,!0),!0):m.run(),S.pause=m.pause.bind(m),S.resume=m.resume.bind(m),S.stop=S,S}function hn(e,t=1/0,n){if(t<=0||!v(e)||e.__v_skip||(n||=new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,R(e))hn(e.value,t,n);else if(d(e))for(let r=0;r<e.length;r++)hn(e[r],t,n);else if(p(e)||f(e))e.forEach(e=>{hn(e,t,n)});else if(C(e)){for(let r in e)hn(e[r],t,n);for(let r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&hn(e[r],t,n)}return e}function gn(e,t,n,r){try{return r?e(...r):e()}catch(e){_n(e,t,n)}}function z(e,t,n,r){if(h(e)){let i=gn(e,t,n,r);return i&&y(i)&&i.catch(e=>{_n(e,t,n)}),i}if(d(e)){let i=[];for(let a=0;a<e.length;a++)i.push(z(e[a],t,n,r));return i}}function _n(e,n,r,i=!0){let a=n?n.vnode:null,{errorHandler:o,throwUnhandledErrorInProduction:s}=n&&n.appContext.config||t;if(n){let t=n.parent,i=n.proxy,a=`
https
:
//vuejs.org/error-reference/#runtime-${r}`;for(;t;){let n=t.ec;if(n){for(let t=0;t<n.length;t++)if(n[t](e,i,a)===!1)return}t=t.parent}if(o){Ke(),gn(o,null,10,[e,i,a]),qe();return}}vn(e,r,a,i,s)}function vn(e,t,n,r=!0,i=!1){if(i)throw e;console.error(e)}var B=[],V=-1,yn=[],bn=null,xn=0,Sn=Promise.resolve(),Cn=null;function wn(e){let t=Cn||Sn;return e?t.then(this?e.bind(this):e):t}function Tn(e){let t=V+1,n=B.length;for(;t<n;){let r=t+n>>>1,i=B[r],a=jn(i);a<e||a===e&&i.flags&2?t=r+1:n=r}return t}function En(e){if(!(e.flags&1)){let t=jn(e),n=B[B.length-1];!n||!(e.flags&2)&&t>=jn(n)?B.push(e):B.splice(Tn(t),0,e),e.flags|=1,Dn()}}function Dn(){Cn||=Sn.then(Mn)}function On(e){d(e)?yn.push(...e):bn&&e.id===-1?bn.splice(xn+1,0,e):e.flags&1||(yn.push(e),e.flags|=1),Dn()}function kn(e,t,n=V+1){for(;n<B.length;n++){let t=B[n];if(t&&t.flags&2){if(e&&t.id!==e.uid)continue;B.splice(n,1),n--,t.flags&4&&(t.flags&=-2),t(),t.flags&4||(t.flags&=-2)}}}function An(e){if(yn.length){let e=[...new Set(yn)].sort((e,t)=>jn(e)-jn(t));if(yn.length=0,bn){bn.push(...e);return}for(bn=e,xn=0;xn<bn.length;xn++){let e=bn[xn];e.flags&4&&(e.flags&=-2),e.flags&8||e(),e.flags&=-2}bn=null,xn=0}}var jn=e=>e.id==null?e.flags&2?-1:1/0:e.id;function Mn(e){try{for(V=0;V<B.length;V++){let e=B[V];e&&!(e.flags&8)&&(e.flags&4&&(e.flags&=-2),gn(e,e.i,e.i?15:14),e.flags&4||(e.flags&=-2))}}finally{for(;V<B.length;V++){let e=B[V];e&&(e.flags&=-2)}V=-1,B.length=0,An(e),Cn=null,(B.length||yn.length)&&Mn(e)}}var H=null,Nn=null;function Pn(e){let t=H;return H=e,Nn=e&&e.type.__scopeId||null,t}function Fn(e,t=H,n){if(!t||e._n)return e;let r=(...n)=>{r._d&&Ca(-1);let i=Pn(t),a;try{a=e(...n)}finally{Pn(i),r._d&&Ca(1)}return a};return r._n=!0,r._c=!0,r._d=!0,r}function In(e,n){if(H===null)return e;let r=io(H),i=e.dirs||=[];for(let e=0;e<n.length;e++){let[a,o,s,c=t]=n[e];a&&(h(a)&&(a={mounted:a,updated:a}),a.deep&&hn(o),i.push({dir:a,instance:r,value:o,oldValue:void 0,arg:s,modifiers:c}))}return e}function Ln(e,t,n,r){let i=e.dirs,a=t&&t.dirs;for(let o=0;o<i.length;o++){let s=i[o];a&&(s.oldValue=a[o].value);let c=s.dir[r];c&&(Ke(),z(c,n,8,[e.el,s,e,t]),qe())}}function Rn(e,t){if($){let n=$.provides,r=$.parent&&$.parent.provides;r===n&&(n=$.provides=Object.create(r)),n[e]=t}}function zn(e,t,n=!1){let r=Ua();if(r||Oi){let i=Oi?Oi._context.provides:r?r.parent==null||r.ce?r.vnode.appContext&&r.vnode.appContext.provides:r.parent.provides:void 0;if(i&&e in i)return i[e];if(arguments.length>1)return n&&h(t)?t.call(r&&r.proxy):t}}var Bn=Symbol.for(`v-scx`),Vn=()=>zn(Bn);function Hn(e,t){return Wn(e,null,t)}function Un(e,t,n){return Wn(e,t,n)}function Wn(e,n,i=t){let{immediate:a,deep:o,flush:c,once:l}=i,u=s({},i),d=n&&a||!n&&c!==`post`,f;if(Ya){if(c===`sync`){let e=Vn();f=e.__watcherHandles||=[]}else if(!d){let e=()=>{};return e.stop=r,e.resume=r,e.pause=r,e}}let p=$;u.call=(e,t,n)=>z(e,p,t,n);let m=!1;c===`post`?u.scheduler=e=>{K(e,p&&p.suspense)}:c!==`sync`&&(m=!0,u.scheduler=(e,t)=>{t?e():En(e)}),u.augmentJob=e=>{n&&(e.flags|=4),m&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};let h=mn(e,n,u);return Ya&&(f?f.push(h):d&&h()),h}function Gn(e,t,n){let r=this.proxy,i=g(e)?e.includes(`.`)?Kn(r,e):()=>r[e]:e.bind(r,r),a;h(t)?a=t:(a=t.handler,n=t);let o=Ka(this),s=Wn(i,a.bind(r),n);return o(),s}function Kn(e,t){let n=t.split(`.`);return()=>{let t=e;for(let e=0;e<n.length&&t;e++)t=t[n[e]];return t}}var qn=new WeakMap,Jn=Symbol(`_vte`),Yn=e=>e.__isTeleport,Xn=e=>e&&(e.disabled||e.disabled===``),Zn=e=>e&&(e.defer||e.defer===``),Qn=e=>typeof SVGElement<`u`&&e instanceof SVGElement,$n=e=>typeof MathMLElement==`function`&&e instanceof MathMLElement,er=(e,t)=>{let n=e&&e.to;return g(n)?t?t(n):null:n},tr={name:`Teleport`,__isTeleport:!0,process(e,t,n,r,i,a,o,s,c,l){let{mc:u,pc:d,pbc:f,o:{insert:p,querySelector:m,createText:h,createComment:g}}=l,_=Xn(t.props),{dynamicChildren:v}=t,y=(e,t,n)=>{e.shapeFlag&16&&u(e.children,t,n,i,a,o,s,c)},b=(e=t)=>{let n=Xn(e.props),r=e.target=er(e.props,m),a=or(r,e,h,p);r&&(o!==`svg`&&Qn(r)?o=`svg`:o!==`mathml`&&$n(r)&&(o=`mathml`),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(r),n||(y(e,r,a),ar(e,!1)))},x=e=>{let t=()=>{qn.get(e)===t&&(qn.delete(e),Xn(e.props)&&(y(e,n,e.anchor),ar(e,!0)),b(e))};qn.set(e,t),K(t,a)};if(e==null){let e=t.el=h(``),i=t.anchor=h(``);if(p(e,n,r),p(i,n,r),Zn(t.props)||a&&a.pendingBranch){x(t);return}_&&(y(t,n,i),ar(t,!0)),b()}else{t.el=e.el;let r=t.anchor=e.anchor,u=qn.get(e);if(u){u.flags|=8,qn.delete(e),x(t);return}t.targetStart=e.targetStart;let p=t.target=e.target,h=t.targetAnchor=e.targetAnchor,g=Xn(e.props),y=g?n:p,b=g?r:h;if(o===`svg`||Qn(p)?o=`svg`:(o===`mathml`||$n(p))&&(o=`mathml`),v?(f(e.dynamicChildren,v,y,i,a,o,s),ua(e,t,!0)):c||d(e,t,y,b,i,a,o,s,!1),_)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):nr(t,n,r,l,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){let e=t.target=er(t.props,m);e&&nr(t,e,null,l,0)}else g&&nr(t,p,h,l,1);ar(t,_)}},remove(e,t,n,{um:r,o:{remove:i}},a){let{shapeFlag:o,children:s,anchor:c,targetStart:l,targetAnchor:u,target:d,props:f}=e,p=a||!Xn(f),m=qn.get(e);if(m&&(m.flags|=8,qn.delete(e),p=!1),d&&(i(l),i(u)),a&&i(c),o&16)for(let e=0;e<s.length;e++){let i=s[e];r(i,t,n,p,!!i.dynamicChildren)}},move:nr,hydrate:rr};function nr(e,t,n,{o:{insert:r},m:i},a=2){a===0&&r(e.targetAnchor,t,n);let{el:o,anchor:s,shapeFlag:c,children:l,props:u}=e,d=a===2;if(d&&r(o,t,n),(!d||Xn(u))&&c&16)for(let e=0;e<l.length;e++)i(l[e],t,n,2);d&&r(s,t,n)}function rr(e,t,n,r,i,a,{o:{nextSibling:o,parentNode:s,querySelector:c,insert:l,createText:u}},d){function f(e,n){let r=n;for(;r;){if(r&&r.nodeType===8){if(r.data===`teleport start anchor`)t.targetStart=r;else if(r.data===`teleport anchor`){t.targetAnchor=r,e._lpa=t.targetAnchor&&o(t.targetAnchor);break}}r=o(r)}}function p(e,t){t.anchor=d(o(e),t,s(e),n,r,i,a)}let m=t.target=er(t.props,c),h=Xn(t.props);if(m){let c=m._lpa||m.firstChild;t.shapeFlag&16&&(h?(p(e,t),f(m,c),t.targetAnchor||or(m,t,u,l,s(e)===m?e:null)):(t.anchor=o(e),f(m,c),t.targetAnchor||or(m,t,u,l),d(c&&o(c),t,m,n,r,i,a))),ar(t,h)}else h&&t.shapeFlag&16&&(p(e,t),t.targetStart=e,t.targetAnchor=o(e));return t.anchor&&o(t.anchor)}var ir=tr;function ar(e,t){let n=e.ctx;if(n&&n.ut){let r,i;for(t?(r=e.el,i=e.anchor):(r=e.targetStart,i=e.targetAnchor);r&&r!==i;)r.nodeType===1&&r.setAttribute(`data-v-owner`,n.uid),r=r.nextSibling;n.ut()}}function or(e,t,n,r,i=null){let a=t.targetStart=n(``),o=t.targetAnchor=n(``);return a[Jn]=o,e&&(r(a,e,i),r(o,e,i)),o}var U=Symbol(`_leaveCb`),sr=Symbol(`_enterCb`);function cr(){let e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Fr(()=>{e.isMounted=!0}),Rr(()=>{e.isUnmounting=!0}),e}var W=[Function,Array],lr={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:W,onEnter:W,onAfterEnter:W,onEnterCancelled:W,onBeforeLeave:W,onLeave:W,onAfterLeave:W,onLeaveCancelled:W,onBeforeAppear:W,onAppear:W,onAfterAppear:W,onAppearCancelled:W},ur=e=>{let t=e.subTree;return t.component?ur(t.component):t},dr={name:`BaseTransition`,props:lr,setup(e,{slots:t}){let n=Ua(),r=cr();return()=>{let i=t.default&&yr(t.default(),!0);if(!i||!i.length)return;let a=fr(i),o=I(e),{mode:s}=o;if(r.isLeaving)return gr(a);let c=_r(a);if(!c)return gr(a);let l=hr(c,o,r,n,e=>l=e);c.type!==J&&vr(c,l);let u=n.subTree&&_r(n.subTree);if(u&&u.type!==J&&!Oa(u,c)&&ur(n).type!==J){let e=hr(u,o,r,n);if(vr(u,e),s===`out-in`&&c.type!==J)return r.isLeaving=!0,e.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete e.afterLeave,u=void 0},gr(a);s===`in-out`&&c.type!==J?e.delayLeave=(e,t,n)=>{let i=mr(r,u);i[String(u.key)]=u,e[U]=()=>{t(),e[U]=void 0,delete l.delayedLeave,u=void 0},l.delayedLeave=()=>{n(),delete l.delayedLeave,u=void 0}}:u=void 0}else u&&=void 0;return a}}};function fr(e){let t=e[0];if(e.length>1){for(let n of e)if(n.type!==J){t=n;break}}return t}var pr=dr;function mr(e,t){let{leavingVNodes:n}=e,r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function hr(e,t,n,r,i){let{appear:a,mode:o,persisted:s=!1,onBeforeEnter:c,onEnter:l,onAfterEnter:u,onEnterCancelled:f,onBeforeLeave:p,onLeave:m,onAfterLeave:h,onLeaveCancelled:g,onBeforeAppear:_,onAppear:v,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=mr(n,e),C=(e,t)=>{e&&z(e,r,9,t)},w=(e,t)=>{let n=t[1];C(e,t),d(e)?e.every(e=>e.length<=1)&&n():e.length<=1&&n()},T={mode:o,persisted:s,beforeEnter(t){let r=c;if(!n.isMounted)if(a)r=_||c;else return;t[U]&&t[U](!0);let i=S[x];i&&Oa(e,i)&&i.el[U]&&i.el[U](),C(r,[t])},enter(t){if(S[x]===e)return;let r=l,i=u,o=f;if(!n.isMounted)if(a)r=v||l,i=y||u,o=b||f;else return;let s=!1;t[sr]=e=>{s||(s=!0,C(e?o:i,[t]),T.delayedLeave&&T.delayedLeave(),t[sr]=void 0)};let c=t[sr].bind(null,!1);r?w(r,[t,c]):c()},leave(t,r){let i=String(e.key);if(t[sr]&&t[sr](!0),n.isUnmounting)return r();C(p,[t]);let a=!1;t[U]=n=>{a||(a=!0,r(),C(n?g:h,[t]),t[U]=void 0,S[i]===e&&delete S[i])};let o=t[U].bind(null,!1);S[i]=e,m?w(m,[t,o]):o()},clone(e){let a=hr(e,t,n,r,i);return i&&i(a),a}};return T}function gr(e){if(Dr(e))return e=Pa(e),e.children=null,e}function _r(e){if(!Dr(e))return Yn(e.type)&&e.children?fr(e.children):e;if(e.component)return e.component.subTree;let{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&h(n.default))return n.default()}}function vr(e,t){e.shapeFlag&6&&e.component?(e.transition=t,vr(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function yr(e,t=!1,n){let r=[],i=0;for(let a=0;a<e.length;a++){let o=e[a],s=n==null?o.key:String(n)+String(o.key==null?a:o.key);o.type===q?(o.patchFlag&128&&i++,r=r.concat(yr(o.children,t,s))):(t||o.type!==J)&&r.push(s==null?o:Pa(o,{key:s}))}if(i>1)for(let e=0;e<r.length;e++)r[e].patchFlag=-2;return r}function br(e,t){return h(e)?s({name:e.name},t,{setup:e}):e}function xr(e){e.ids=[e.ids[0]+ e.ids[2]+++`-`,0,0]}function Sr(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))&&!n.configurable)}var Cr=new WeakMap;function wr(e,n,r,a,o=!1){if(d(e)){e.forEach((e,t)=>wr(e,n&&(d(n)?n[t]:n),r,a,o));return}if(Er(a)&&!o){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&wr(e,n,r,a.component.subTree);return}let s=a.shapeFlag&4?io(a.component):a.el,l=o?null:s,{i:f,r:p}=e,m=n&&n.r,_=f.refs===t?f.refs={}:f.refs,v=f.setupState,y=I(v),b=v===t?i:e=>Sr(_,e)?!1:u(y,e),x=(e,t)=>!(t&&Sr(_,t));if(m!=null&&m!==p){if(Tr(n),g(m))_[m]=null,b(m)&&(v[m]=null);else if(R(m)){let e=n;x(m,e.k)&&(m.value=null),e.k&&(_[e.k]=null)}}if(h(p))gn(p,f,12,[l,_]);else{let t=g(p),n=R(p);if(t||n){let i=()=>{if(e.f){let n=t?b(p)?v[p]:_[p]:x(p)||!e.k?p.value:_[e.k];if(o)d(n)&&c(n,s);else if(d(n))n.includes(s)||n.push(s);else if(t)_[p]=[s],b(p)&&(v[p]=_[p]);else{let t=[s];x(p,e.k)&&(p.value=t),e.k&&(_[e.k]=t)}}else t?(_[p]=l,b(p)&&(v[p]=l)):n&&(x(p,e.k)&&(p.value=l),e.k&&(_[e.k]=l))};if(l){let t=()=>{i(),Cr.delete(e)};t.id=-1,Cr.set(e,t),K(t,r)}else Tr(e),i()}}}function Tr(e){let t=Cr.get(e);t&&(t.flags|=8,Cr.delete(e))}le().requestIdleCallback,le().cancelIdleCallback;var Er=e=>!!e.type.__asyncLoader,Dr=e=>e.type.__isKeepAlive;function Or(e,t){Ar(e,`a`,t)}function kr(e,t){Ar(e,`da`,t)}function Ar(e,t,n=$){let r=e.__wdc||=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()};if(Mr(t,r,n),n){let e=n.parent;for(;e&&e.parent;)Dr(e.parent.vnode)&&jr(r,t,n,e),e=e.parent}}function jr(e,t,n,r){let i=Mr(t,e,r,!0);zr(()=>{c(r[t],i)},n)}function Mr(e,t,n=$,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||=(...r)=>{Ke();let i=Ka(n),a=z(t,n,e,r);return i(),qe(),a};return r?i.unshift(a):i.push(a),a}}var Nr=e=>(t,n=$)=>{(!Ya||e===`sp`)&&Mr(e,(...e)=>t(...e),n)},Pr=Nr(`bm`),Fr=Nr(`m`),Ir=Nr(`bu`),Lr=Nr(`u`),Rr=Nr(`bum`),zr=Nr(`um`),Br=Nr(`sp`),Vr=Nr(`rtg`),Hr=Nr(`rtc`);function Ur(e,t=$){Mr(`ec`,e,t)}var Wr=`components`,Gr=`directives`;function Kr(e,t){return Xr(Wr,e,!0,t)||e}var qr=Symbol.for(`v-ndc`);function Jr(e){return g(e)?Xr(Wr,e,!1)||e:e||qr}function Yr(e){return Xr(Gr,e)}function Xr(e,t,n=!0,r=!1){let i=H||$;if(i){let n=i.type;if(e===Wr){let e=ao(n,!1);if(e&&(e===t||e===E(t)||e===re(E(t))))return n}let a=Zr(i[e]||n[e],t)||Zr(i.appContext[e],t);return!a&&r?n:a}}function Zr(e,t){return e&&(e[t]||e[E(t)]||e[re(E(t))])}function Qr(e,t,n,r){let i,a=n&&n[r],o=d(e);if(o||g(e)){let n=o&&Ht(e),r=!1,s=!1;n&&(r=!F(e),s=Ut(e),e=ot(e)),i=Array(e.length);for(let n=0,o=e.length;n<o;n++)i[n]=t(r?s?Kt(L(e[n])):L(e[n]):e[n],n,void 0,a&&a[n])}else if(typeof e==`number`){i=Array(e);for(let n=0;n<e;n++)i[n]=t(n+1,n,void 0,a&&a[n])}else if(v(e))if(e[Symbol.iterator])i=Array.from(e,(e,n)=>t(e,n,void 0,a&&a[n]));else{let n=Object.keys(e);i=Array(n.length);for(let r=0,o=n.length;r<o;r++){let o=n[r];i[r]=t(e[o],o,r,a&&a[r])}}else i=[];return n&&(n[r]=i),i}function $r(e,t){for(let n=0;n<t.length;n++){let r=t[n];if(d(r))for(let t=0;t<r.length;t++)e[r[t].name]=r[t].fn;else r&&(e[r.name]=r.key?(...e)=>{let t=r.fn(...e);return t&&(t.key=r.key),t}:r.fn)}return e}function ei(e,t,n={},r,i){if(H.ce||H.parent&&Er(H.parent)&&H.parent.ce){let e=Object.keys(n).length>0;return t!==`default`&&(n.name=t),ba(),Ea(q,null,[X(`slot`,n,r&&r())],e?-2:64)}let a=e[t];a&&a._c&&(a._d=!1),ba();let o=a&&ti(a(n)),s=n.key||o&&o.key,c=Ea(q,{key:(s&&!_(s)?s:`_${t}`)+(!o&&r?`_fb`:``)},o||(r?r():[]),o&&e._===1?64:-2);return!i&&c.scopeId&&(c.slotScopeIds=[c.scopeId+`-s`]),a&&a._c&&(a._d=!0),c}function ti(e){return e.some(e=>Da(e)?!(e.type===J||e.type===q&&!ti(e.children)):!0)?e:null}function ni(e,t){let n={};for(let r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:ie(r)]=e[r];return n}var ri=e=>e?Ja(e)?io(e):ri(e.parent):null,ii=s(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ri(e.parent),$root:e=>ri(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>gi(e),$forceUpdate:e=>e.f||=()=>{En(e.update)},$nextTick:e=>e.n||=wn.bind(e.proxy),$watch:e=>Gn.bind(e)}),ai=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),oi={get({_:e},n){if(n===`__v_skip`)return!0;let{ctx:r,setupState:i,data:a,props:o,accessCache:s,type:c,appContext:l}=e;if(n[0]!==`$`){let e=s[n];if(e!==void 0)switch(e){case 1:return i[n];case 2:return a[n];case 4:return r[n];case 3:return o[n]}else if(ai(i,n))return s[n]=1,i[n];else if(a!==t&&u(a,n))return s[n]=2,a[n];else if(u(o,n))return s[n]=3,o[n];else if(r!==t&&u(r,n))return s[n]=4,r[n];else di&&(s[n]=0)}let d=ii[n],f,p;if(d)return n===`$attrs`&&N(e.attrs,`get`,``),d(e);if((f=c.__cssModules)&&(f=f[n]))return f;if(r!==t&&u(r,n))return s[n]=4,r[n];if(p=l.config.globalProperties,u(p,n))return p[n]},set({_:e},n,r){let{data:i,setupState:a,ctx:o}=e;return ai(a,n)?(a[n]=r,!0):i!==t&&u(i,n)?(i[n]=r,!0):u(e.props,n)||n[0]===`$`&&n.slice(1)in e?!1:(o[n]=r,!0)},has({_:{data:e,setupState:n,accessCache:r,ctx:i,appContext:a,props:o,type:s}},c){let l;return!!(r[c]||e!==t&&c[0]!==`$`&&u(e,c)||ai(n,c)||u(o,c)||u(i,c)||u(ii,c)||u(a.config.globalProperties,c)||(l=s.__cssModules)&&l[c])},defineProperty(e,t,n){return n.get==null?u(n,`value`)&&this.set(e,t,n.value,null):e._.accessCache[t]=0,Reflect.defineProperty(e,t,n)}};function si(){return li(`useSlots`).slots}function ci(){return li(`useAttrs`).attrs}function li(e){let t=Ua();return t.setupContext||=ro(t)}function ui(e){return d(e)?e.reduce((e,t)=>(e[t]=null,e),{}):e}var di=!0;function fi(e){let t=gi(e),n=e.proxy,i=e.ctx;di=!1,t.beforeCreate&&mi(t.beforeCreate,e,`bc`);let{data:a,computed:o,methods:s,watch:c,provide:l,inject:u,created:f,beforeMount:p,mounted:m,beforeUpdate:g,updated:_,activated:y,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:T,renderTracked:ee,renderTriggered:te,errorCaptured:E,serverPrefetch:ne,expose:D,inheritAttrs:re,components:ie,directives:O,filters:ae}=t;if(u&&pi(u,i,null),s)for(let e in s){let t=s[e];h(t)&&(i[e]=t.bind(n))}if(a){let t=a.call(n,n);v(t)&&(e.data=Rt(t))}if(di=!0,o)for(let e in o){let t=o[e],a=so({get:h(t)?t.bind(n,n):h(t.get)?t.get.bind(n,n):r,set:!h(t)&&h(t.set)?t.set.bind(n):r});Object.defineProperty(i,e,{enumerable:!0,configurable:!0,get:()=>a.value,set:e=>a.value=e})}if(c)for(let e in c)hi(c[e],i,n,e);if(l){let e=h(l)?l.call(n):l;Reflect.ownKeys(e).forEach(t=>{Rn(t,e[t])})}f&&mi(f,e,`c`);function k(e,t){d(t)?t.forEach(t=>e(t.bind(n))):t&&e(t.bind(n))}if(k(Pr,p),k(Fr,m),k(Ir,g),k(Lr,_),k(Or,y),k(kr,b),k(Ur,E),k(Hr,ee),k(Vr,te),k(Rr,S),k(zr,w),k(Br,ne),d(D))if(D.length){let t=e.exposed||={};D.forEach(e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})})}else e.exposed||={};T&&e.render===r&&(e.render=T),re!=null&&(e.inheritAttrs=re),ie&&(e.components=ie),O&&(e.directives=O),ne&&xr(e)}function pi(e,t,n=r){d(e)&&(e=xi(e));for(let n in e){let r=e[n],i;i=v(r)?`default`in r?zn(r.from||n,r.default,!0):zn(r.from||n):zn(r),R(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[n]=i}}function mi(e,t,n){z(d(e)?e.map(e=>e.bind(t.proxy)):e.bind(t.proxy),t,n)}function hi(e,t,n,r){let i=r.includes(`.`)?Kn(n,r):()=>n[r];if(g(e)){let n=t[e];h(n)&&Un(i,n)}else if(h(e))Un(i,e.bind(n));else if(v(e))if(d(e))e.forEach(e=>hi(e,t,n,r));else{let r=h(e.handler)?e.handler.bind(n):t[e.handler];h(r)&&Un(i,r,e)}}function gi(e){let t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:a,config:{optionMergeStrategies:o}}=e.appContext,s=a.get(t),c;return s?c=s:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(e=>_i(c,e,o,!0)),_i(c,t,o)),v(t)&&a.set(t,c),c}function _i(e,t,n,r=!1){let{mixins:i,extends:a}=t;a&&_i(e,a,n,!0),i&&i.forEach(t=>_i(e,t,n,!0));for(let i in t)if(!(r&&i===`expose`)){let r=vi[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}var vi={data:yi,props:Ci,emits:Ci,methods:Si,computed:Si,beforeCreate:G,created:G,beforeMount:G,mounted:G,beforeUpdate:G,updated:G,beforeDestroy:G,beforeUnmount:G,destroyed:G,unmounted:G,activated:G,deactivated:G,errorCaptured:G,serverPrefetch:G,components:Si,directives:Si,watch:wi,provide:yi,inject:bi};function yi(e,t){return t?e?function(){return s(h(e)?e.call(this,this):e,h(t)?t.call(this,this):t)}:t:e}function bi(e,t){return Si(xi(e),xi(t))}function xi(e){if(d(e)){let t={};for(let n=0;n<e.length;n++)t[e[n]]=e[n];return t}return e}function G(e,t){return e?[...new Set([].concat(e,t))]:t}function Si(e,t){return e?s(Object.create(null),e,t):t}function Ci(e,t){return e?d(e)&&d(t)?[...new Set([...e,...t])]:s(Object.create(null),ui(e),ui(t??{})):t}function wi(e,t){if(!e)return t;if(!t)return e;let n=s(Object.create(null),e);for(let r in t)n[r]=G(e[r],t[r]);return n}function Ti(){return{app:null,config:{isNativeTag:i,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}var Ei=0;function Di(e,t){return function(n,r=null){h(n)||(n=s({},n)),r!=null&&!v(r)&&(r=null);let i=Ti(),a=new WeakSet,o=[],c=!1,l=i.app={_uid:Ei++,_component:n,_props:r,_container:null,_context:i,_instance:null,version:lo,get config(){return i.config},set config(e){},use(e,...t){return a.has(e)||(e&&h(e.install)?(a.add(e),e.install(l,...t)):h(e)&&(a.add(e),e(l,...t))),l},mixin(e){return i.mixins.includes(e)||i.mixins.push(e),l},component(e,t){return t?(i.components[e]=t,l):i.components[e]},directive(e,t){return t?(i.directives[e]=t,l):i.directives[e]},mount(a,o,s){if(!c){let u=l._ceVNode||X(n,r);return u.appContext=i,s===!0?s=`svg`:s===!1&&(s=void 0),o&&t?t(u,a):e(u,a,s),c=!0,l._container=a,a.__vue_app__=l,io(u.component)}},onUnmount(e){o.push(e)},unmount(){c&&(z(o,l._instance,16),e(null,l._container),delete l._container.__vue_app__)},provide(e,t){return i.provides[e]=t,l},runWithContext(e){let t=Oi;Oi=l;try{return e()}finally{Oi=t}}};return l}}var Oi=null,ki=(e,t)=>t===`modelValue`||t===`model-value`?e.modelModifiers:e[`${t}Modifiers`]||e[`${E(t)}Modifiers`]||e[`${D(t)}Modifiers`];function Ai(e,n,...r){if(e.isUnmounted)return;let i=e.vnode.props||t,a=r,o=n.startsWith(`update:`),s=o&&ki(i,n.slice(7));s&&(s.trim&&(a=r.map(e=>g(e)?e.trim():e)),s.number&&(a=r.map(oe)));let c,l=i[c=ie(n)]||i[c=ie(E(n))];!l&&o&&(l=i[c=ie(D(n))]),l&&z(l,e,6,a);let u=i[c+`Once`];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,z(u,e,6,a)}}var ji=new WeakMap;function Mi(e,t,n=!1){let r=n?ji:t.emitsCache,i=r.get(e);if(i!==void 0)return i;let a=e.emits,o={},c=!1;if(!h(e)){let r=e=>{let n=Mi(e,t,!0);n&&(c=!0,s(o,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return!a&&!c?(v(e)&&r.set(e,null),null):(d(a)?a.forEach(e=>o[e]=null):s(o,a),v(e)&&r.set(e,o),o)}function Ni(e,t){return!e||!a(t)?!1:(t=t.slice(2).replace(/Once$/,``),u(e,t[0].toLowerCase()+t.slice(1))||u(e,D(t))||u(e,t))}function Pi(e){let{type:t,vnode:n,proxy:r,withProxy:i,propsOptions:[a],slots:s,attrs:c,emit:l,render:u,renderCache:d,props:f,data:p,setupState:m,ctx:h,inheritAttrs:g}=e,_=Pn(e),v,y;try{if(n.shapeFlag&4){let e=i||r,t=e;v=Z(u.call(t,e,d,f,m,p,h)),y=c}else{let e=t;v=Z(e.length>1?e(f,{attrs:c,slots:s,emit:l}):e(f,null)),y=t.props?c:Fi(c)}}catch(t){ya.length=0,_n(t,e,1),v=X(J)}let b=v;if(y&&g!==!1){let e=Object.keys(y),{shapeFlag:t}=b;e.length&&t&7&&(a&&e.some(o)&&(y=Ii(y,a)),b=Pa(b,y,!1,!0))}return n.dirs&&(b=Pa(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&vr(b,n.transition),v=b,Pn(_),v}var Fi=e=>{let t;for(let n in e)(n===`class`||n===`style`||a(n))&&((t||={})[n]=e[n]);return t},Ii=(e,t)=>{let n={};for(let r in e)(!o(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Li(e,t,n){let{props:r,children:i,component:a}=e,{props:o,children:s,patchFlag:c}=t,l=a.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Ri(r,o,l):!!o;if(c&8){let e=t.dynamicProps;for(let t=0;t<e.length;t++){let n=e[t];if(zi(o,r,n)&&!Ni(l,n))return!0}}}else return(i||s)&&(!s||!s.$stable)?!0:r===o?!1:r?o?Ri(r,o,l):!0:!!o;return!1}function Ri(e,t,n){let r=Object.keys(t);if(r.length!==Object.keys(e).length)return!0;for(let i=0;i<r.length;i++){let a=r[i];if(zi(t,e,a)&&!Ni(n,a))return!0}return!1}function zi(e,t,n){let r=e[n],i=t[n];return n===`style`&&v(r)&&v(i)?!xe(r,i):r!==i}function Bi({vnode:e,parent:t,suspense:n},r){for(;t;){let n=t.subTree;if(n.suspense&&n.suspense.activeBranch===e&&(n.suspense.vnode.el=n.el=r,e=n),n===e)(e=t.vnode).el=r,t=t.parent;else break}n&&n.activeBranch===e&&(n.vnode.el=r)}var Vi={},Hi=()=>Object.create(Vi),Ui=e=>Object.getPrototypeOf(e)===Vi;function Wi(e,t,n,r=!1){let i={},a=Hi();e.propsDefaults=Object.create(null),Ki(e,t,i,a);for(let t in e.propsOptions[0])t in i||(i[t]=void 0);n?e.props=r?i:zt(i):e.type.props?e.props=i:e.props=a,e.attrs=a}function Gi(e,t,n,r){let{props:i,attrs:a,vnode:{patchFlag:o}}=e,s=I(i),[c]=e.propsOptions,l=!1;if((r||o>0)&&!(o&16)){if(o&8){let n=e.vnode.dynamicProps;for(let r=0;r<n.length;r++){let o=n[r];if(Ni(e.emitsOptions,o))continue;let d=t[o];if(c)if(u(a,o))d!==a[o]&&(a[o]=d,l=!0);else{let t=E(o);i[t]=qi(c,s,t,d,e,!1)}else d!==a[o]&&(a[o]=d,l=!0)}}}else{Ki(e,t,i,a)&&(l=!0);let r;for(let a in s)(!t||!u(t,a)&&((r=D(a))===a||!u(t,r)))&&(c?n&&(n[a]!==void 0||n[r]!==void 0)&&(i[a]=qi(c,s,a,void 0,e,!0)):delete i[a]);if(a!==s)for(let e in a)(!t||!u(t,e))&&(delete a[e],l=!0)}l&&rt(e.attrs,`set`,``)}function Ki(e,n,r,i){let[a,o]=e.propsOptions,s=!1,c;if(n)for(let t in n){if(T(t))continue;let l=n[t],d;a&&u(a,d=E(t))?!o||!o.includes(d)?r[d]=l:(c||={})[d]=l:Ni(e.emitsOptions,t)||(!(t in i)||l!==i[t])&&(i[t]=l,s=!0)}if(o){let n=I(r),i=c||t;for(let t=0;t<o.length;t++){let s=o[t];r[s]=qi(a,n,s,i[s],e,!u(i,s))}}return s}function qi(e,t,n,r,i,a){let o=e[n];if(o!=null){let e=u(o,`default`);if(e&&r===void 0){let e=o.default;if(o.type!==Function&&!o.skipFactory&&h(e)){let{propsDefaults:a}=i;if(n in a)r=a[n];else{let o=Ka(i);r=a[n]=e.call(null,t),o()}}else r=e;i.ce&&i.ce._setProp(n,r)}o[0]&&(a&&!e?r=!1:o[1]&&(r===``||r===D(n))&&(r=!0))}return r}var Ji=new WeakMap;function Yi(e,r,i=!1){let a=i?Ji:r.propsCache,o=a.get(e);if(o)return o;let c=e.props,l={},f=[],p=!1;if(!h(e)){let t=e=>{p=!0;let[t,n]=Yi(e,r,!0);s(l,t),n&&f.push(...n)};!i&&r.mixins.length&&r.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!p)return v(e)&&a.set(e,n),n;if(d(c))for(let e=0;e<c.length;e++){let n=E(c[e]);Xi(n)&&(l[n]=t)}else if(c)for(let e in c){let t=E(e);if(Xi(t)){let n=c[e],r=l[t]=d(n)||h(n)?{type:n}:s({},n),i=r.type,a=!1,o=!0;if(d(i))for(let e=0;e<i.length;++e){let t=i[e],n=h(t)&&t.name;if(n===`Boolean`){a=!0;break}else n===`String`&&(o=!1)}else a=h(i)&&i.name===`Boolean`;r[0]=a,r[1]=o,(a||u(r,`default`))&&f.push(t)}}let m=[l,f];return v(e)&&a.set(e,m),m}function Xi(e){return e[0]!==`$`&&!T(e)}var Zi=e=>e===`_`||e===`_ctx`||e===`$stable`,Qi=e=>d(e)?e.map(Z):[Z(e)],$i=(e,t,n)=>{if(t._n)return t;let r=Fn((...e)=>Qi(t(...e)),n);return r._c=!1,r},ea=(e,t,n)=>{let r=e._ctx;for(let n in e){if(Zi(n))continue;let i=e[n];if(h(i))t[n]=$i(n,i,r);else if(i!=null){let e=Qi(i);t[n]=()=>e}}},ta=(e,t)=>{let n=Qi(t);e.slots.default=()=>n},na=(e,t,n)=>{for(let r in t)(n||!Zi(r))&&(e[r]=t[r])},ra=(e,t,n)=>{let r=e.slots=Hi();if(e.vnode.shapeFlag&32){let e=t._;e?(na(r,t,n),n&&k(r,`_`,e,!0)):ea(t,r)}else t&&ta(e,t)},ia=(e,n,r)=>{let{vnode:i,slots:a}=e,o=!0,s=t;if(i.shapeFlag&32){let e=n._;e?r&&e===1?o=!1:na(a,n,r):(o=!n.$stable,ea(n,a)),s=n}else n&&(ta(e,n),s={default:1});if(o)for(let e in a)!Zi(e)&&s[e]==null&&delete a[e]},K=ga;function aa(e){return oa(e)}function oa(e,i){let a=le();a.__VUE__=!0;let{insert:o,remove:s,patchProp:c,createElement:l,createText:u,createComment:d,setText:f,setElementText:p,parentNode:m,nextSibling:h,setScopeId:g=r,insertStaticContent:_}=e,v=(e,t,n,r=null,i=null,a=null,o=void 0,s=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!Oa(e,t)&&(r=ye(e),me(e,i,a,!0),e=null),t.patchFlag===-2&&(c=!1,t.dynamicChildren=null);let{type:l,ref:u,shapeFlag:d}=t;switch(l){case _a:y(e,t,n,r);break;case J:b(e,t,n,r);break;case va:e??x(t,n,r,o);break;case q:ie(e,t,n,r,i,a,o,s,c);break;default:d&1?w(e,t,n,r,i,a,o,s,c):d&6?O(e,t,n,r,i,a,o,s,c):(d&64||d&128)&&l.process(e,t,n,r,i,a,o,s,c,Se)}u!=null&&i?wr(u,e&&e.ref,a,t||e,!t):u==null&&e&&e.ref!=null&&wr(e.ref,null,a,e,!0)},y=(e,t,n,r)=>{if(e==null)o(t.el=u(t.children),n,r);else{let n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},b=(e,t,n,r)=>{e==null?o(t.el=d(t.children||``),n,r):t.el=e.el},x=(e,t,n,r)=>{[e.el,e.anchor]=_(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let i;for(;e&&e!==t;)i=h(e),o(e,n,r),e=i;o(t,n,r)},C=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=h(e),s(e),e=n;s(t)},w=(e,t,n,r,i,a,o,s,c)=>{if(t.type===`svg`?o=`svg`:t.type===`math`&&(o=`mathml`),e==null)ee(t,n,r,i,a,o,s,c);else{let n=e.el&&e.el._isVueCE?e.el:null;try{n&&n._beginPatch(),ne(e,t,i,a,o,s,c)}finally{n&&n._endPatch()}}},ee=(e,t,n,r,i,a,s,u)=>{let d,f,{props:m,shapeFlag:h,transition:g,dirs:_}=e;if(d=e.el=l(e.type,a,m&&m.is,m),h&8?p(d,e.children):h&16&&E(e.children,d,null,r,i,sa(e,a),s,u),_&&Ln(e,null,r,`created`),te(d,e,e.scopeId,s,r),m){for(let e in m)e!==`value`&&!T(e)&&c(d,e,null,m[e],a,r);`value`in m&&c(d,`value`,null,m.value,a),(f=m.onVnodeBeforeMount)&&Q(f,r,e)}_&&Ln(e,null,r,`beforeMount`);let v=la(i,g);v&&g.beforeEnter(d),o(d,t,n),((f=m&&m.onVnodeMounted)||v||_)&&K(()=>{try{f&&Q(f,r,e),v&&g.enter(d),_&&Ln(e,null,r,`mounted`)}finally{}},i)},te=(e,t,n,r,i)=>{if(n&&g(e,n),r)for(let t=0;t<r.length;t++)g(e,r[t]);if(i){let n=i.subTree;if(t===n||ha(n.type)&&(n.ssContent===t||n.ssFallback===t)){let t=i.vnode;te(e,t,t.scopeId,t.slotScopeIds,i.parent)}}},E=(e,t,n,r,i,a,o,s,c=0)=>{for(let l=c;l<e.length;l++)v(null,e[l]=s?La(e[l]):Z(e[l]),t,n,r,i,a,o,s)},ne=(e,n,r,i,a,o,s)=>{let l=n.el=e.el,{patchFlag:u,dynamicChildren:d,dirs:f}=n;u|=e.patchFlag&16;let m=e.props||t,h=n.props||t,g;if(r&&ca(r,!1),(g=h.onVnodeBeforeUpdate)&&Q(g,r,n,e),f&&Ln(n,e,r,`beforeUpdate`),r&&ca(r,!0),(m.innerHTML&&h.innerHTML==null||m.textContent&&h.textContent==null)&&p(l,``),d?D(e.dynamicChildren,d,l,r,i,sa(n,a),o):s||ue(e,n,l,null,r,i,sa(n,a),o,!1),u>0){if(u&16)re(l,m,h,r,a);else if(u&2&&m.class!==h.class&&c(l,`class`,null,h.class,a),u&4&&c(l,`style`,m.style,h.style,a),u&8){let e=n.dynamicProps;for(let t=0;t<e.length;t++){let n=e[t],i=m[n],o=h[n];(o!==i||n===`value`)&&c(l,n,i,o,a,r)}}u&1&&e.children!==n.children&&p(l,n.children)}else !s&&d==null&&re(l,m,h,r,a);((g=h.onVnodeUpdated)||f)&&K(()=>{g&&Q(g,r,n,e),f&&Ln(n,e,r,`updated`)},i)},D=(e,t,n,r,i,a,o)=>{for(let s=0;s<t.length;s++){let c=e[s],l=t[s];v(c,l,c.el&&(c.type===q||!Oa(c,l)||c.shapeFlag&198)?m(c.el):n,null,r,i,a,o,!0)}},re=(e,n,r,i,a)=>{if(n!==r){if(n!==t)for(let t in n)!T(t)&&!(t in r)&&c(e,t,n[t],null,a,i);for(let t in r){if(T(t))continue;let o=r[t],s=n[t];o!==s&&t!==`value`&&c(e,t,s,o,a,i)}`value`in r&&c(e,`value`,n.value,r.value,a)}},ie=(e,t,n,r,i,a,s,c,l)=>{let d=t.el=e?e.el:u(``),f=t.anchor=e?e.anchor:u(``),{patchFlag:p,dynamicChildren:m,slotScopeIds:h}=t;h&&(c=c?c.concat(h):h),e==null?(o(d,n,r),o(f,n,r),E(t.children||[],n,f,i,a,s,c,l)):p>0&&p&64&&m&&e.dynamicChildren&&e.dynamicChildren.length===m.length?(D(e.dynamicChildren,m,n,i,a,s,c),(t.key!=null||i&&t===i.subTree)&&ua(e,t,!0)):ue(e,t,n,f,i,a,s,c,l)},O=(e,t,n,r,i,a,o,s,c)=>{t.slotScopeIds=s,e==null?t.shapeFlag&512?i.ctx.activate(t,n,r,o,c):k(t,n,r,i,a,o,c):oe(e,t,c)},k=(e,t,n,r,i,a,o)=>{let s=e.component=Ha(e,r,i);if(Dr(e)&&(s.ctx.renderer=Se),Xa(s,!1,o),s.asyncDep){if(i&&i.registerDep(s,se,o),!e.el){let r=s.subTree=X(J);b(null,r,t,n),e.placeholder=r.el}}else se(s,e,t,n,i,a,o)},oe=(e,t,n)=>{let r=t.component=e.component;if(Li(e,t,n))if(r.asyncDep&&!r.asyncResolved){ce(r,t,n);return}else r.next=t,r.update();else t.el=e.el,r.vnode=t},se=(e,t,n,r,i,a,o)=>{let s=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:c}=e;{let n=fa(e);if(n){t&&(t.el=c.el,ce(e,t,o)),n.asyncDep.then(()=>{K(()=>{e.isUnmounted||l()},i)});return}}let u=t,d;ca(e,!1),t?(t.el=c.el,ce(e,t,o)):t=c,n&&ae(n),(d=t.props&&t.props.onVnodeBeforeUpdate)&&Q(d,s,t,c),ca(e,!0);let f=Pi(e),p=e.subTree;e.subTree=f,v(p,f,m(p.el),ye(p),e,i,a),t.el=f.el,u===null&&Bi(e,f.el),r&&K(r,i),(d=t.props&&t.props.onVnodeUpdated)&&K(()=>Q(d,s,t,c),i)}else{let o,{el:s,props:c}=t,{bm:l,m:u,parent:d,root:f,type:p}=e,m=Er(t);if(ca(e,!1),l&&ae(l),!m&&(o=c&&c.onVnodeBeforeMount)&&Q(o,d,t),ca(e,!0),s&&we){let t=()=>{e.subTree=Pi(e),we(s,e.subTree,e,i,null)};m&&p.__asyncHydrate?p.__asyncHydrate(s,e,t):t()}else{f.ce&&f.ce._hasShadowRoot()&&f.ce._injectChildStyle(p,e.parent?e.parent.type:void 0);let o=e.subTree=Pi(e);v(null,o,n,r,e,i,a),t.el=o.el}if(u&&K(u,i),!m&&(o=c&&c.onVnodeMounted)){let e=t;K(()=>Q(o,d,e),i)}(t.shapeFlag&256||d&&Er(d.vnode)&&d.vnode.shapeFlag&256)&&e.a&&K(e.a,i),e.isMounted=!0,t=n=r=null}};e.scope.on();let c=e.effect=new Me(s);e.scope.off();let l=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>En(u),ca(e,!0),l()},ce=(e,t,n)=>{t.component=e;let r=e.vnode.props;e.vnode=t,e.next=null,Gi(e,t.props,r,n),ia(e,t.children,n),Ke(),kn(e),qe()},ue=(e,t,n,r,i,a,o,s,c=!1)=>{let l=e&&e.children,u=e?e.shapeFlag:0,d=t.children,{patchFlag:f,shapeFlag:m}=t;if(f>0){if(f&128){fe(l,d,n,r,i,a,o,s,c);return}else if(f&256){de(l,d,n,r,i,a,o,s,c);return}}m&8?(u&16&&ve(l,i,a),d!==l&&p(n,d)):u&16?m&16?fe(l,d,n,r,i,a,o,s,c):ve(l,i,a,!0):(u&8&&p(n,``),m&16&&E(d,n,r,i,a,o,s,c))},de=(e,t,r,i,a,o,s,c,l)=>{e||=n,t||=n;let u=e.length,d=t.length,f=Math.min(u,d),p;for(p=0;p<f;p++){let n=t[p]=l?La(t[p]):Z(t[p]);v(e[p],n,r,null,a,o,s,c,l)}u>d?ve(e,a,o,!0,!1,f):E(t,r,i,a,o,s,c,l,f)},fe=(e,t,r,i,a,o,s,c,l)=>{let u=0,d=t.length,f=e.length-1,p=d-1;for(;u<=f&&u<=p;){let n=e[u],i=t[u]=l?La(t[u]):Z(t[u]);if(Oa(n,i))v(n,i,r,null,a,o,s,c,l);else break;u++}for(;u<=f&&u<=p;){let n=e[f],i=t[p]=l?La(t[p]):Z(t[p]);if(Oa(n,i))v(n,i,r,null,a,o,s,c,l);else break;f--,p--}if(u>f){if(u<=p){let e=p+1,n=e<d?t[e].el:i;for(;u<=p;)v(null,t[u]=l?La(t[u]):Z(t[u]),r,n,a,o,s,c,l),u++}}else if(u>p)for(;u<=f;)me(e[u],a,o,!0),u++;else{let m=u,h=u,g=new Map;for(u=h;u<=p;u++){let e=t[u]=l?La(t[u]):Z(t[u]);e.key!=null&&g.set(e.key,u)}let _,y=0,b=p-h+1,x=!1,S=0,C=Array(b);for(u=0;u<b;u++)C[u]=0;for(u=m;u<=f;u++){let n=e[u];if(y>=b){me(n,a,o,!0);continue}let i;if(n.key!=null)i=g.get(n.key);else for(_=h;_<=p;_++)if(C[_-h]===0&&Oa(n,t[_])){i=_;break}i===void 0?me(n,a,o,!0):(C[i-h]=u+1,i>=S?S=i:x=!0,v(n,t[i],r,null,a,o,s,c,l),y++)}let w=x?da(C):n;for(_=w.length-1,u=b-1;u>=0;u--){let e=h+u,n=t[e],f=t[e+1],p=e+1<d?f.el||ma(f):i;C[u]===0?v(null,n,r,p,a,o,s,c,l):x&&(_<0||u!==w[_]?pe(n,r,p,2):_--)}}},pe=(e,t,n,r,i=null)=>{let{el:a,type:c,transition:l,children:u,shapeFlag:d}=e;if(d&6){pe(e.component.subTree,t,n,r);return}if(d&128){e.suspense.move(t,n,r);return}if(d&64){c.move(e,t,n,Se);return}if(c===q){o(a,t,n);for(let e=0;e<u.length;e++)pe(u[e],t,n,r);o(e.anchor,t,n);return}if(c===va){S(e,t,n);return}if(r!==2&&d&1&&l)if(r===0)l.beforeEnter(a),o(a,t,n),K(()=>l.enter(a),i);else{let{leave:r,delayLeave:i,afterLeave:c}=l,u=()=>{e.ctx.isUnmounted?s(a):o(a,t,n)},d=()=>{a._isLeaving&&a[U](!0),r(a,()=>{u(),c&&c()})};i?i(a,u,d):d()}else o(a,t,n)},me=(e,t,n,r=!1,i=!1)=>{let{type:a,props:o,ref:s,children:c,dynamicChildren:l,shapeFlag:u,patchFlag:d,dirs:f,cacheIndex:p,memo:m}=e;if(d===-2&&(i=!1),s!=null&&(Ke(),wr(s,null,n,e,!0),qe()),p!=null&&(t.renderCache[p]=void 0),u&256){t.ctx.deactivate(e);return}let h=u&1&&f,g=!Er(e),_;if(g&&(_=o&&o.onVnodeBeforeUnmount)&&Q(_,t,e),u&6)_e(e.component,n,r);else{if(u&128){e.suspense.unmount(n,r);return}h&&Ln(e,null,t,`beforeUnmount`),u&64?e.type.remove(e,t,n,Se,r):l&&!l.hasOnce&&(a!==q||d>0&&d&64)?ve(l,t,n,!1,!0):(a===q&&d&384||!i&&u&16)&&ve(c,t,n),r&&he(e)}let v=m!=null&&p==null;(g&&(_=o&&o.onVnodeUnmounted)||h||v)&&K(()=>{_&&Q(_,t,e),h&&Ln(e,null,t,`unmounted`),v&&(e.el=null)},n)},he=e=>{let{type:t,el:n,anchor:r,transition:i}=e;if(t===q){ge(n,r);return}if(t===va){C(e);return}let a=()=>{s(n),i&&!i.persisted&&i.afterLeave&&i.afterLeave()};if(e.shapeFlag&1&&i&&!i.persisted){let{leave:t,delayLeave:r}=i,o=()=>t(n,a);r?r(e.el,a,o):o()}else a()},ge=(e,t)=>{let n;for(;e!==t;)n=h(e),s(e),e=n;s(t)},_e=(e,t,n)=>{let{bum:r,scope:i,job:a,subTree:o,um:s,m:c,a:l}=e;pa(c),pa(l),r&&ae(r),i.stop(),a&&(a.flags|=8,me(o,e,t,n)),s&&K(s,t),K(()=>{e.isUnmounted=!0},t)},ve=(e,t,n,r=!1,i=!1,a=0)=>{for(let o=a;o<e.length;o++)me(e[o],t,n,r,i)},ye=e=>{if(e.shapeFlag&6)return ye(e.component.subTree);if(e.shapeFlag&128)return e.suspense.next();let t=h(e.anchor||e.el),n=t&&t[Jn];return n?h(n):t},be=!1,xe=(e,t,n)=>{let r;e==null?t._vnode&&(me(t._vnode,null,null,!0),r=t._vnode.component):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,be||=(be=!0,kn(r),An(),!1)},Se={p:v,um:me,m:pe,r:he,mt:k,mc:E,pc:ue,pbc:D,n:ye,o:e},Ce,we;return i&&([Ce,we]=i(Se)),{render:xe,hydrate:Ce,createApp:Di(xe,Ce)}}function sa({type:e,props:t},n){return n===`svg`&&e===`foreignObject`||n===`mathml`&&e===`annotation-xml`&&t&&t.encoding&&t.encoding.includes(`html`)?void 0:n}function ca({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function la(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function ua(e,t,n=!1){let r=e.children,i=t.children;if(d(r)&&d(i))for(let e=0;e<r.length;e++){let t=r[e],a=i[e];a.shapeFlag&1&&!a.dynamicChildren&&((a.patchFlag<=0||a.patchFlag===32)&&(a=i[e]=La(i[e]),a.el=t.el),!n&&a.patchFlag!==-2&&ua(t,a)),a.type===_a&&(a.patchFlag===-1&&(a=i[e]=La(a)),a.el=t.el),a.type===J&&!a.el&&(a.el=t.el)}}function da(e){let t=e.slice(),n=[0],r,i,a,o,s,c=e.length;for(r=0;r<c;r++){let c=e[r];if(c!==0){if(i=n[n.length-1],e[i]<c){t[r]=i,n.push(r);continue}for(a=0,o=n.length-1;a<o;)s=a+o>>1,e[n[s]]<c?a=s+1:o=s;c<e[n[a]]&&(a>0&&(t[r]=n[a-1]),n[a]=r)}}for(a=n.length,o=n[a-1];a-- >0;)n[a]=o,o=t[o];return n}function fa(e){let t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:fa(t)}function pa(e){if(e)for(let t=0;t<e.length;t++)e[t].flags|=8}function ma(e){if(e.placeholder)return e.placeholder;let t=e.component;return t?ma(t.subTree):null}var ha=e=>e.__isSuspense;function ga(e,t){t&&t.pendingBranch?d(e)?t.effects.push(...e):t.effects.push(e):On(e)}var q=Symbol.for(`v-fgt`),_a=Symbol.for(`v-txt`),J=Symbol.for(`v-cmt`),va=Symbol.for(`v-stc`),ya=[],Y=null;function ba(e=!1){ya.push(Y=e?null:[])}function xa(){ya.pop(),Y=ya[ya.length-1]||null}var Sa=1;function Ca(e,t=!1){Sa+=e,e<0&&Y&&t&&(Y.hasOnce=!0)}function wa(e){return e.dynamicChildren=Sa>0?Y||n:null,xa(),Sa>0&&Y&&Y.push(e),e}function Ta(e,t,n,r,i,a){return wa(ja(e,t,n,r,i,a,!0))}function Ea(e,t,n,r,i){return wa(X(e,t,n,r,i,!0))}function Da(e){return e?e.__v_isVNode===!0:!1}function Oa(e,t){return e.type===t.type&&e.key===t.key}var ka=({key:e})=>e??null,Aa=({ref:e,ref_key:t,ref_for:n})=>(typeof e==`number`&&(e=``+e),e==null?null:g(e)||R(e)||h(e)?{i:H,r:e,k:t,f:!!n}:e);function ja(e,t=null,n=null,r=0,i=null,a=e===q?0:1,o=!1,s=!1){let c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ka(t),ref:t&&Aa(t),scopeId:Nn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:a,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:H};return s?(Ra(c,n),a&128&&e.normalize(c)):n&&(c.shapeFlag|=g(n)?8:16),Sa>0&&!o&&Y&&(c.patchFlag>0||a&6)&&c.patchFlag!==32&&Y.push(c),c}var X=Ma;function Ma(e,t=null,n=null,r=0,i=null,a=!1){if((!e||e===qr)&&(e=J),Da(e)){let r=Pa(e,t,!0);return n&&Ra(r,n),Sa>0&&!a&&Y&&(r.shapeFlag&6?Y[Y.indexOf(e)]=r:Y.push(r)),r.patchFlag=-2,r}if(oo(e)&&(e=e.__vccOpts),t){t=Na(t);let{class:e,style:n}=t;e&&!g(e)&&(t.class=he(e)),v(n)&&(Wt(n)&&!d(n)&&(n=s({},n)),t.style=ue(n))}let o=g(e)?1:ha(e)?128:Yn(e)?64:v(e)?4:h(e)?2:0;return ja(e,t,n,r,i,o,a,!0)}function Na(e){return e?Wt(e)||Ui(e)?s({},e):e:null}function Pa(e,t,n=!1,r=!1){let{props:i,ref:a,patchFlag:o,children:s,transition:c}=e,l=t?za(i||{},t):i,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&ka(l),ref:t&&t.ref?n&&a?d(a)?a.concat(Aa(t)):[a,Aa(t)]:Aa(t):a,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:s,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==q?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Pa(e.ssContent),ssFallback:e.ssFallback&&Pa(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&r&&vr(u,c.clone(u)),u}function Fa(e=` `,t=0){return X(_a,null,e,t)}function Ia(e=``,t=!1){return t?(ba(),Ea(J,null,e)):X(J,null,e)}function Z(e){return e==null||typeof e==`boolean`?X(J):d(e)?X(q,null,e.slice()):Da(e)?La(e):X(_a,null,String(e))}function La(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Pa(e)}function Ra(e,t){let n=0,{shapeFlag:r}=e;if(t==null)t=null;else if(d(t))n=16;else if(typeof t==`object`)if(r&65){let n=t.default;n&&(n._c&&(n._d=!1),Ra(e,n()),n._c&&(n._d=!0));return}else{n=32;let r=t._;!r&&!Ui(t)?t._ctx=H:r===3&&H&&(H.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else h(t)?(t={default:t,_ctx:H},n=32):(t=String(t),r&64?(n=16,t=[Fa(t)]):n=8);e.children=t,e.shapeFlag|=n}function za(...e){let t={};for(let n=0;n<e.length;n++){let r=e[n];for(let e in r)if(e===`class`)t.class!==r.class&&(t.class=he([t.class,r.class]));else if(e===`style`)t.style=ue([t.style,r.style]);else if(a(e)){let n=t[e],i=r[e];i&&n!==i&&!(d(n)&&n.includes(i))?t[e]=n?[].concat(n,i):i:i==null&&n==null&&!o(e)&&(t[e]=i)}else e!==``&&(t[e]=r[e])}return t}function Q(e,t,n,r=null){z(e,t,7,[n,r])}var Ba=Ti(),Va=0;function Ha(e,n,r){let i=e.type,a=(n?n.appContext:e.appContext)||Ba,o={uid:Va++,vnode:e,type:i,parent:n,appContext:a,root:null,next:null,subTree:null,effect:null,update:null,job:null,scope:new De(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:n?n.provides:Object.create(a.provides),ids:n?n.ids:[``,0,0],accessCache:null,renderCache:[],components:null,directives:null,propsOptions:Yi(i,a),emitsOptions:Mi(i,a),emit:null,emitted:null,propsDefaults:t,inheritAttrs:i.inheritAttrs,ctx:t,data:t,props:t,attrs:t,slots:t,refs:t,setupState:t,setupContext:null,suspense:r,suspenseId:r?r.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};return o.ctx={_:o},o.root=n?n.root:o,o.emit=Ai.bind(null,o),e.ce&&e.ce(o),o}var $=null,Ua=()=>$||H,Wa,Ga;{let e=le(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach(t=>t(e)):r[0](e)}};Wa=t(`__VUE_INSTANCE_SETTERS__`,e=>$=e),Ga=t(`__VUE_SSR_SETTERS__`,e=>Ya=e)}var Ka=e=>{let t=$;return Wa(e),e.scope.on(),()=>{e.scope.off(),Wa(t)}},qa=()=>{$&&$.scope.off(),Wa(null)};function Ja(e){return e.vnode.shapeFlag&4}var Ya=!1;function Xa(e,t=!1,n=!1){t&&Ga(t);let{props:r,children:i}=e.vnode,a=Ja(e);Wi(e,r,a,t),ra(e,i,n||t);let o=a?Za(e,t):void 0;return t&&Ga(!1),o}function Za(e,t){let n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,oi);let{setup:r}=n;if(r){Ke();let n=e.setupContext=r.length>1?ro(e):null,i=Ka(e),a=gn(r,e,0,[e.props,n]),o=y(a);if(qe(),i(),(o||e.sp)&&!Er(e)&&xr(e),o){if(a.then(qa,qa),t)return a.then(n=>{Qa(e,n,t)}).catch(t=>{_n(t,e,0)});e.asyncDep=a}else Qa(e,a,t)}else to(e,t)}function Qa(e,t,n){h(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:v(t)&&(e.setupState=tn(t)),to(e,n)}var $a,eo;function to(e,t,n){let i=e.type;if(!e.render){if(!t&&$a&&!i.render){let t=i.template||gi(e).template;if(t){let{isCustomElement:n,compilerOptions:r}=e.appContext.config,{delimiters:a,compilerOptions:o}=i;i.render=$a(t,s(s({isCustomElement:n,delimiters:a},r),o))}}e.render=i.render||r,eo&&eo(e)}{let t=Ka(e);Ke();try{fi(e)}finally{qe(),t()}}}var no={get(e,t){return N(e,`get`,``),e[t]}};function ro(e){return{attrs:new Proxy(e.attrs,no),slots:e.slots,emit:e.emit,expose:t=>{e.exposed=t||{}}}}function io(e){return e.exposed?e.exposeProxy||=new Proxy(tn(Gt(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in ii)return ii[n](e)},has(e,t){return t in e||t in ii}}):e.proxy}function ao(e,t=!0){return h(e)?e.displayName||e.name:e.name||t&&e.__name}function oo(e){return h(e)&&`__vccOpts`in e}var so=(e,t)=>ln(e,t,Ya);function co(e,t,n){try{Ca(-1);let r=arguments.length;return r===2?v(t)&&!d(t)?Da(t)?X(e,null,[t]):X(e,t):X(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&Da(n)&&(n=[n]),X(e,t,n))}finally{Ca(1)}}var lo=`3.5.32`,uo=r,fo=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};export{In as $,Rr as A,C as At,Kr as B,ge as Bt,co as C,ae as Ct,wn as D,o as Dt,za as E,h as Et,Lr as F,_ as Ft,ni as G,Jr as H,we as Ht,ba as I,xe as It,cr as J,ci as K,Rn as L,Se as Lt,kr as M,p as Mt,Fr as N,ve as Nt,Or as O,v as Ot,zr as P,g as Pt,Fn as Q,Qr as R,oe as Rt,Na as S,ye as St,Da as T,m as Tt,hr as U,ie as Ut,Yr as V,ue as Vt,vr as W,se as Wt,Un as X,uo as Y,Hn as Z,Fa as _,E as _t,q as a,Rt as at,Ua as b,u as bt,z as c,zt as ct,ja as d,on as dt,Oe as et,Ea as f,nn as ft,$r as g,r as gt,aa as h,Qt as ht,J as i,Ae as it,Ir as j,y as jt,Pr as k,a as kt,Pa as l,Jt as lt,Ta as m,Zt as mt,pr as n,R as nt,ir as o,Bt as ot,Ia as p,$t as pt,si as q,lr as r,Gt as rt,_a as s,qt as st,fo as t,ke as tt,so as u,I as ut,X as v,re as vt,zn as w,d as wt,yr as x,D as xt,br as y,s as yt,ei as z,he as zt};
\ No newline at end of file
frontend/dist/index.html
View file @
d7a073ab
...
...
@@ -5,9 +5,9 @@
<link
rel=
"icon"
href=
"/favicon.ico"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<title>
热实验温度控制系统
</title>
<script
type=
"module"
crossorigin
src=
"/assets/index-
8e_Vyf7m
.js"
></script>
<link
rel=
"modulepreload"
crossorigin
href=
"/assets/_plugin-vue_export-helper-
D1RKUtCV
.js"
>
<link
rel=
"modulepreload"
crossorigin
href=
"/assets/
es-XoFJL7_H
.js"
>
<script
type=
"module"
crossorigin
src=
"/assets/index-
BuprtPJP
.js"
></script>
<link
rel=
"modulepreload"
crossorigin
href=
"/assets/_plugin-vue_export-helper-
BeNqMkde
.js"
>
<link
rel=
"modulepreload"
crossorigin
href=
"/assets/
vue-router-DixiVDJx
.js"
>
<link
rel=
"stylesheet"
crossorigin
href=
"/assets/index-pfQxyObg.css"
>
</head>
<body>
...
...
frontend/src/api/historyData.js
0 → 100644
View file @
d7a073ab
import
request
from
'@/utils/request'
export
function
getHistoryList
()
{
return
request
.
get
(
'/monitor/history'
)
}
export
function
getHistoryDetail
(
expId
)
{
return
request
.
get
(
`/monitor/experiments/
${
expId
}
/report`
)
}
frontend/src/api/realtimeMonitor.js
0 → 100644
View file @
d7a073ab
import
request
from
'@/utils/request'
// ── 元数据 ────────────────────────────────────────────────────────────────────
export
function
getMonitorModels
()
{
return
request
.
get
(
'/monitor/models'
)
}
export
function
getMonitorPackages
()
{
return
request
.
get
(
'/monitor/packages'
)
}
// ── 试验 CRUD ─────────────────────────────────────────────────────────────────
export
function
getExperiments
()
{
return
request
.
get
(
'/monitor/experiments'
)
}
export
function
createExperiment
(
payload
)
{
return
request
.
post
(
'/monitor/experiments'
,
payload
)
}
export
function
getExperiment
(
expId
)
{
return
request
.
get
(
`/monitor/experiments/
${
expId
}
`
)
}
export
function
deleteExperiment
(
expId
)
{
return
request
.
delete
(
`/monitor/experiments/
${
expId
}
`
)
}
// ── 控制操作 ──────────────────────────────────────────────────────────────────
export
function
startExperiment
(
expId
)
{
return
request
.
post
(
`/monitor/experiments/
${
expId
}
/start`
)
}
export
function
stopExperiment
(
expId
)
{
return
request
.
post
(
`/monitor/experiments/
${
expId
}
/stop`
)
}
// ── 数据 / 报告 / 导出 ────────────────────────────────────────────────────────
export
function
getDataPoints
(
expId
,
fromStep
=
0
)
{
return
request
.
get
(
`/monitor/experiments/
${
expId
}
/data`
,
{
params
:
{
from_step
:
fromStep
}
})
}
export
function
getReport
(
expId
)
{
return
request
.
get
(
`/monitor/experiments/
${
expId
}
/report`
)
}
export
function
exportToHistory
(
expId
)
{
return
request
.
post
(
`/monitor/experiments/
${
expId
}
/export`
)
}
frontend/src/router/index.js
View file @
d7a073ab
...
...
@@ -15,14 +15,17 @@ const router = createRouter({
{
path
:
'/realtime-monitor'
,
name
:
'realtime-monitor'
,
component
:
()
=>
import
(
'@/components/common/UnderConstruction.vue'
),
props
:
{
title
:
'实时监控'
},
component
:
()
=>
import
(
'@/views/RealtimeMonitor/index.vue'
),
},
{
path
:
'/realtime-monitor/:id'
,
name
:
'realtime-monitor-detail'
,
component
:
()
=>
import
(
'@/views/RealtimeMonitor/components/ExperimentDetail.vue'
),
},
{
path
:
'/history-data'
,
name
:
'history-data'
,
component
:
()
=>
import
(
'@/components/common/UnderConstruction.vue'
),
props
:
{
title
:
'历史数据'
},
component
:
()
=>
import
(
'@/views/HistoryData/index.vue'
),
},
{
path
:
'/model-training'
,
...
...
frontend/src/views/HistoryData/index.vue
View file @
d7a073ab
<
script
setup
>
import
*
as
echarts
from
'echarts'
import
{
Refresh
}
from
'@element-plus/icons-vue'
import
{
ElMessage
}
from
'element-plus'
import
{
onMounted
,
onBeforeUnmount
,
ref
,
nextTick
}
from
'vue'
import
{
getHistoryList
,
getHistoryDetail
}
from
'@/api/historyData'
// ── 列表 ───────────────────────────────────────────────────────────────────────
const
records
=
ref
([])
const
loadingList
=
ref
(
false
)
const
selectedId
=
ref
(
null
)
const
loadList
=
async
()
=>
{
loadingList
.
value
=
true
try
{
records
.
value
=
await
getHistoryList
()
}
catch
{
ElMessage
.
error
(
'加载历史数据失败'
)
}
finally
{
loadingList
.
value
=
false
}
}
// ── 详情 ───────────────────────────────────────────────────────────────────────
const
detail
=
ref
(
null
)
const
loadingDetail
=
ref
(
false
)
let
tempChart
=
null
let
currChart
=
null
const
tempChartRef
=
ref
(
null
)
const
currChartRef
=
ref
(
null
)
const
disposeCharts
=
()
=>
{
tempChart
?.
dispose
()
currChart
?.
dispose
()
tempChart
=
null
currChart
=
null
}
const
initCharts
=
(
points
,
targetTemp
)
=>
{
disposeCharts
()
if
(
!
tempChartRef
.
value
||
!
currChartRef
.
value
)
return
tempChart
=
echarts
.
init
(
tempChartRef
.
value
)
currChart
=
echarts
.
init
(
currChartRef
.
value
)
const
xData
=
points
.
map
((
d
)
=>
`步
${
d
.
step_idx
}
`
)
const
actuals
=
points
.
map
((
d
)
=>
d
.
actual_temp
)
const
refs
=
points
.
map
((
d
)
=>
d
.
reference_temp
)
const
currents
=
points
.
map
((
d
)
=>
d
.
current_output
)
const
targetLine
=
points
.
map
(()
=>
targetTemp
)
const
intervalVal
=
Math
.
max
(
0
,
Math
.
floor
(
xData
.
length
/
10
)
-
1
)
tempChart
.
setOption
({
animation
:
false
,
color
:
[
'#409EFF'
,
'#67C23A'
,
'#F56C6C'
],
tooltip
:
{
trigger
:
'axis'
,
backgroundColor
:
'rgba(255,255,255,0.96)'
,
borderColor
:
'#e2e8f0'
,
borderWidth
:
1
,
formatter
(
params
)
{
if
(
!
params
?.
length
)
return
''
const
lines
=
[
`<div style="margin-bottom:4px;font-weight:600">
${
params
[
0
].
axisValue
}
</div>`
]
params
.
forEach
((
p
)
=>
{
const
v
=
p
.
data
!=
null
?
Number
(
p
.
data
).
toFixed
(
3
)
+
' °C'
:
'--'
lines
.
push
(
`<div style="display:flex;justify-content:space-between;gap:16px">
<span>
${
p
.
marker
}${
p
.
seriesName
}
</span><strong>
${
v
}
</strong>
</div>`
,
)
})
return
lines
.
join
(
''
)
},
},
legend
:
{
bottom
:
4
,
itemWidth
:
18
,
itemHeight
:
8
,
textStyle
:
{
color
:
'#475569'
,
fontSize
:
12
}
},
grid
:
{
top
:
16
,
left
:
16
,
right
:
16
,
bottom
:
52
,
containLabel
:
true
},
xAxis
:
{
type
:
'category'
,
boundaryGap
:
false
,
data
:
xData
,
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
,
interval
:
intervalVal
},
axisLine
:
{
lineStyle
:
{
color
:
'#cbd5e1'
}
},
},
yAxis
:
{
type
:
'value'
,
name
:
'温度 (°C)'
,
nameTextStyle
:
{
color
:
'#64748b'
,
fontSize
:
11
},
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
},
splitLine
:
{
lineStyle
:
{
color
:
'#e2e8f0'
}
},
},
series
:
[
{
name
:
'实际温度'
,
type
:
'line'
,
data
:
actuals
,
smooth
:
true
,
symbol
:
'none'
,
lineStyle
:
{
width
:
2
}
},
{
name
:
'参考轨迹'
,
type
:
'line'
,
data
:
refs
,
smooth
:
true
,
symbol
:
'none'
,
lineStyle
:
{
width
:
1.5
,
type
:
'dashed'
}
},
{
name
:
'目标温度'
,
type
:
'line'
,
data
:
targetLine
,
symbol
:
'none'
,
lineStyle
:
{
width
:
1.5
,
type
:
'dotted'
,
color
:
'#F56C6C'
}
},
],
})
currChart
.
setOption
({
animation
:
false
,
color
:
[
'#E6A23C'
],
tooltip
:
{
trigger
:
'axis'
,
backgroundColor
:
'rgba(255,255,255,0.96)'
,
borderColor
:
'#e2e8f0'
,
borderWidth
:
1
,
formatter
(
params
)
{
if
(
!
params
?.
length
)
return
''
const
v
=
params
[
0
].
data
!=
null
?
Number
(
params
[
0
].
data
).
toFixed
(
3
)
+
' A'
:
'--'
return
`<div style="font-weight:600">
${
params
[
0
].
axisValue
}
</div>
<div>
${
params
[
0
].
marker
}
电流输出:<strong>
${
v
}
</strong></div>`
},
},
legend
:
{
bottom
:
4
,
itemWidth
:
18
,
itemHeight
:
8
,
textStyle
:
{
color
:
'#475569'
,
fontSize
:
12
}
},
grid
:
{
top
:
16
,
left
:
16
,
right
:
16
,
bottom
:
52
,
containLabel
:
true
},
xAxis
:
{
type
:
'category'
,
boundaryGap
:
false
,
data
:
xData
,
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
,
interval
:
intervalVal
},
axisLine
:
{
lineStyle
:
{
color
:
'#cbd5e1'
}
},
},
yAxis
:
{
type
:
'value'
,
name
:
'电流 (A)'
,
nameTextStyle
:
{
color
:
'#64748b'
,
fontSize
:
11
},
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
},
splitLine
:
{
lineStyle
:
{
color
:
'#e2e8f0'
}
},
min
:
0
,
},
series
:
[
{
name
:
'电流输出'
,
type
:
'line'
,
data
:
currents
,
smooth
:
false
,
symbol
:
'none'
,
lineStyle
:
{
width
:
2
},
areaStyle
:
{
opacity
:
0.08
}
},
],
})
}
const
selectRecord
=
async
(
row
)
=>
{
if
(
selectedId
.
value
===
row
.
id
)
return
selectedId
.
value
=
row
.
id
detail
.
value
=
null
loadingDetail
.
value
=
true
try
{
detail
.
value
=
await
getHistoryDetail
(
row
.
id
)
await
nextTick
()
if
(
detail
.
value
?.
points
?.
length
)
{
initCharts
(
detail
.
value
.
points
,
detail
.
value
.
experiment
?.
target_temp
)
}
}
catch
{
ElMessage
.
error
(
'加载详情失败'
)
}
finally
{
loadingDetail
.
value
=
false
}
}
const
onResize
=
()
=>
{
tempChart
?.
resize
()
currChart
?.
resize
()
}
onMounted
(
async
()
=>
{
await
loadList
()
if
(
records
.
value
.
length
)
{
await
selectRecord
(
records
.
value
[
0
])
}
window
.
addEventListener
(
'resize'
,
onResize
)
})
onBeforeUnmount
(()
=>
{
window
.
removeEventListener
(
'resize'
,
onResize
)
disposeCharts
()
})
const
fmtNum
=
(
v
,
digits
=
4
)
=>
(
v
!=
null
?
Number
(
v
).
toFixed
(
digits
)
:
'--'
)
</
script
>
<
template
>
<div
class=
"history-page"
>
<!-- ── 左侧:记录列表 ────────────────────────────────────────────────── -->
<div
class=
"left-panel"
>
<div
class=
"panel-header"
>
<span
class=
"panel-title"
>
历史数据
</span>
<el-button
:icon=
"Refresh"
size=
"small"
plain
:loading=
"loadingList"
@
click=
"loadList"
>
刷新
</el-button>
</div>
<div
v-loading=
"loadingList"
class=
"record-list"
>
<el-empty
v-if=
"!loadingList && !records.length"
description=
"暂无已导出的历史数据"
:image-size=
"80"
/>
<div
v-for=
"row in records"
:key=
"row.id"
class=
"record-item"
:class=
"
{ active: selectedId === row.id }"
@click="selectRecord(row)"
>
<div
class=
"record-name"
>
{{
row
.
name
}}
</div>
<div
class=
"record-meta"
>
<span>
{{
row
.
model_name
}}
</span>
<el-tag
size=
"small"
type=
"info"
class=
"steps-tag"
>
{{
row
.
total_steps
}}
步
</el-tag>
</div>
<div
class=
"record-time"
>
{{
row
.
stop_time
??
row
.
created_at
}}
</div>
</div>
</div>
</div>
<!-- ── 右侧:详情 ────────────────────────────────────────────────────── -->
<div
class=
"right-panel"
v-loading=
"loadingDetail"
>
<el-empty
v-if=
"!detail && !loadingDetail"
description=
"请在左侧选择一条历史记录"
:image-size=
"100"
/>
<template
v-if=
"detail"
>
<!-- 基本信息 -->
<el-card
shadow=
"hover"
class=
"info-card"
>
<div
class=
"info-grid"
>
<div
class=
"info-item"
>
<span
class=
"label"
>
试验名称
</span>
<span
class=
"value"
>
{{
detail
.
experiment
?.
name
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
预测模型
</span>
<span
class=
"value"
>
{{
detail
.
experiment
?.
model_name
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
初始数据包
</span>
<span
class=
"value"
>
{{
detail
.
experiment
?.
package_name
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
目标温度
</span>
<span
class=
"value highlight"
>
{{
detail
.
experiment
?.
target_temp
?.
toFixed
(
1
)
}}
°C
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
开始时间
</span>
<span
class=
"value"
>
{{
detail
.
experiment
?.
start_time
??
'--'
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
停止时间
</span>
<span
class=
"value"
>
{{
detail
.
experiment
?.
stop_time
??
'--'
}}
</span>
</div>
</div>
</el-card>
<!-- 指标摘要 -->
<el-card
v-if=
"detail.summary"
shadow=
"hover"
class=
"summary-card"
>
<template
#
header
><span
class=
"card-title"
>
控制性能指标
</span></
template
>
<div
class=
"metrics-row"
>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
采集步数
</span>
<span
class=
"metric-value"
>
{{ detail.summary.total_steps }}
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
仿真时长
</span>
<span
class=
"metric-value"
>
{{ detail.summary.duration_s }} s
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
初始温度
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.initial_temp, 2) }} °C
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
最终温度
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.final_temp, 2) }} °C
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
MAE
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.mae) }} °C
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
RMSE
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.rmse) }} °C
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
最大超调
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.overshoot) }} °C
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
调节时间
</span>
<span
class=
"metric-value"
>
{{ detail.summary.settling_step ?? '未稳定' }} 步
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
平均电流
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.avg_current) }} A
</span>
</div>
<div
class=
"metric-chip"
>
<span
class=
"metric-label"
>
最大电流
</span>
<span
class=
"metric-value"
>
{{ fmtNum(detail.summary.max_current) }} A
</span>
</div>
</div>
</el-card>
<!-- 温度曲线 -->
<el-card
shadow=
"hover"
class=
"chart-card"
>
<
template
#
header
><span
class=
"card-title"
>
温度曲线
</span></
template
>
<el-empty
v-if=
"!detail.points?.length"
description=
"暂无数据点"
:image-size=
"60"
/>
<div
v-else
ref=
"tempChartRef"
class=
"chart-body"
/>
</el-card>
<!-- 电流曲线 -->
<el-card
shadow=
"hover"
class=
"chart-card"
>
<
template
#
header
><span
class=
"card-title"
>
电流输出曲线
</span></
template
>
<el-empty
v-if=
"!detail.points?.length"
description=
"暂无数据点"
:image-size=
"60"
/>
<div
v-else
ref=
"currChartRef"
class=
"chart-body"
/>
</el-card>
</template>
</div>
</div>
</template>
<
style
scoped
>
.history-page
{
display
:
flex
;
gap
:
16px
;
height
:
calc
(
100vh
-
60px
);
padding
:
16px
;
box-sizing
:
border-box
;
overflow
:
hidden
;
}
/* ── 左侧 ── */
.left-panel
{
width
:
280px
;
flex-shrink
:
0
;
display
:
flex
;
flex-direction
:
column
;
border
:
1px
solid
#e2e8f0
;
border-radius
:
8px
;
background
:
#fff
;
overflow
:
hidden
;
}
.panel-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
padding
:
12px
16px
;
border-bottom
:
1px
solid
#e2e8f0
;
flex-shrink
:
0
;
}
.panel-title
{
font-size
:
14px
;
font-weight
:
600
;
color
:
#1e293b
;
}
.record-list
{
flex
:
1
;
overflow-y
:
auto
;
padding
:
8px
;
}
.record-item
{
padding
:
10px
12px
;
border-radius
:
6px
;
cursor
:
pointer
;
transition
:
background
0.15s
;
margin-bottom
:
4px
;
border
:
1px
solid
transparent
;
}
.record-item
:hover
{
background
:
#f1f5f9
;
}
.record-item.active
{
background
:
#eff6ff
;
border-color
:
#bfdbfe
;
}
.record-name
{
font-size
:
13px
;
font-weight
:
600
;
color
:
#1e293b
;
margin-bottom
:
4px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
.record-meta
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
font-size
:
12px
;
color
:
#64748b
;
margin-bottom
:
2px
;
}
.steps-tag
{
flex-shrink
:
0
;
}
.record-time
{
font-size
:
11px
;
color
:
#94a3b8
;
}
/* ── 右侧 ── */
.right-panel
{
flex
:
1
;
min-width
:
0
;
overflow-y
:
auto
;
display
:
flex
;
flex-direction
:
column
;
gap
:
14px
;
}
.info-card
,
.summary-card
,
.chart-card
{
flex-shrink
:
0
;
}
.info-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
gap
:
12px
16px
;
}
.info-item
{
display
:
flex
;
flex-direction
:
column
;
gap
:
2px
;
}
.label
{
font-size
:
11px
;
color
:
#94a3b8
;
}
.value
{
font-size
:
13px
;
color
:
#1e293b
;
font-weight
:
500
;
}
.value.highlight
{
color
:
#2563eb
;
font-weight
:
700
;
}
.metrics-row
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
10px
;
}
.metric-chip
{
background
:
#f8fafc
;
border
:
1px
solid
#e2e8f0
;
border-radius
:
8px
;
padding
:
8px
14px
;
display
:
flex
;
flex-direction
:
column
;
gap
:
2px
;
min-width
:
100px
;
}
.metric-label
{
font-size
:
11px
;
color
:
#94a3b8
;
}
.metric-value
{
font-size
:
14px
;
font-weight
:
600
;
color
:
#1e293b
;
}
.card-title
{
font-size
:
14px
;
font-weight
:
600
;
color
:
#1e293b
;
}
.chart-body
{
height
:
260px
;
width
:
100%
;
}
</
style
>
frontend/src/views/RealtimeMonitor/components/ExperimentDetail.vue
0 → 100644
View file @
d7a073ab
<
script
setup
>
import
*
as
echarts
from
'echarts'
import
{
ArrowLeft
,
VideoPlay
,
VideoPause
,
Download
,
DataAnalysis
}
from
'@element-plus/icons-vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
onMounted
,
onBeforeUnmount
,
ref
,
computed
,
nextTick
,
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
getExperiment
,
startExperiment
,
stopExperiment
,
getDataPoints
,
getReport
,
exportToHistory
,
}
from
'@/api/realtimeMonitor'
const
router
=
useRouter
()
const
route
=
useRoute
()
const
expId
=
Number
(
route
.
params
.
id
)
// ── 试验状态 ───────────────────────────────────────────────────────────────────
const
experiment
=
ref
(
null
)
const
loadingExp
=
ref
(
false
)
const
loadExperiment
=
async
()
=>
{
loadingExp
.
value
=
true
try
{
experiment
.
value
=
await
getExperiment
(
expId
)
}
finally
{
loadingExp
.
value
=
false
}
}
const
isRunning
=
computed
(()
=>
experiment
.
value
?.
status
===
'running'
)
const
isStopped
=
computed
(()
=>
experiment
.
value
?.
status
===
'stopped'
)
// ── 实时数据 ───────────────────────────────────────────────────────────────────
const
dataPoints
=
ref
([])
// 全部数据点
let
nextFromStep
=
0
// 下次轮询起始步
let
pollTimer
=
null
const
fetchNewPoints
=
async
()
=>
{
try
{
const
newPts
=
await
getDataPoints
(
expId
,
nextFromStep
)
if
(
newPts
.
length
)
{
dataPoints
.
value
=
[...
dataPoints
.
value
,
...
newPts
]
nextFromStep
=
dataPoints
.
value
[
dataPoints
.
value
.
length
-
1
].
step_idx
+
1
updateCharts
()
}
// 更新试验状态
const
latest
=
await
getExperiment
(
expId
)
experiment
.
value
=
latest
if
(
latest
.
status
!==
'running'
)
{
stopPolling
()
}
}
catch
{
// 静默处理轮询错误
}
}
const
startPolling
=
()
=>
{
if
(
pollTimer
)
return
pollTimer
=
setInterval
(
fetchNewPoints
,
2000
)
}
const
stopPolling
=
()
=>
{
if
(
pollTimer
)
{
clearInterval
(
pollTimer
)
pollTimer
=
null
}
}
// ── 控制操作 ───────────────────────────────────────────────────────────────────
const
controlling
=
ref
(
false
)
const
handleStart
=
async
()
=>
{
controlling
.
value
=
true
try
{
await
startExperiment
(
expId
)
experiment
.
value
=
await
getExperiment
(
expId
)
ElMessage
.
success
(
'MPC 控制已启动'
)
startPolling
()
}
finally
{
controlling
.
value
=
false
}
}
const
handleStop
=
async
()
=>
{
controlling
.
value
=
true
try
{
await
stopExperiment
(
expId
)
stopPolling
()
experiment
.
value
=
await
getExperiment
(
expId
)
// 加载全部数据点
const
all
=
await
getDataPoints
(
expId
,
0
)
dataPoints
.
value
=
all
updateCharts
()
ElMessage
.
success
(
'控制已停止'
)
}
finally
{
controlling
.
value
=
false
}
}
// ── 报告 ───────────────────────────────────────────────────────────────────────
const
reportVisible
=
ref
(
false
)
const
reportData
=
ref
(
null
)
const
loadingReport
=
ref
(
false
)
const
handleReport
=
async
()
=>
{
loadingReport
.
value
=
true
try
{
reportData
.
value
=
await
getReport
(
expId
)
reportVisible
.
value
=
true
}
finally
{
loadingReport
.
value
=
false
}
}
// ── 导出 ───────────────────────────────────────────────────────────────────────
const
exporting
=
ref
(
false
)
const
handleExport
=
async
()
=>
{
try
{
await
ElMessageBox
.
confirm
(
'确定将本次试验数据导出到历史数据吗?'
,
'导出确认'
,
{
type
:
'info'
})
exporting
.
value
=
true
await
exportToHistory
(
expId
)
experiment
.
value
=
await
getExperiment
(
expId
)
ElMessage
.
success
(
'已导出到历史数据'
)
}
catch
{
// 取消
}
finally
{
exporting
.
value
=
false
}
}
// ── 图表 ───────────────────────────────────────────────────────────────────────
const
tempChartRef
=
ref
(
null
)
const
currChartRef
=
ref
(
null
)
let
tempChart
=
null
let
currChart
=
null
const
MAX_CHART_POINTS
=
300
// 超过时抽样显示
const
buildDisplayData
=
()
=>
{
const
pts
=
dataPoints
.
value
if
(
!
pts
.
length
)
return
[]
const
step
=
pts
.
length
>
MAX_CHART_POINTS
?
Math
.
ceil
(
pts
.
length
/
MAX_CHART_POINTS
)
:
1
return
pts
.
filter
((
_
,
i
)
=>
i
%
step
===
0
)
}
const
initCharts
=
()
=>
{
if
(
tempChartRef
.
value
&&
!
tempChart
)
{
tempChart
=
echarts
.
init
(
tempChartRef
.
value
)
}
if
(
currChartRef
.
value
&&
!
currChart
)
{
currChart
=
echarts
.
init
(
currChartRef
.
value
)
}
updateCharts
()
}
const
updateCharts
=
()
=>
{
const
display
=
buildDisplayData
()
const
xData
=
display
.
map
((
d
)
=>
`步
${
d
.
step_idx
}
`
)
const
actuals
=
display
.
map
((
d
)
=>
d
.
actual_temp
)
const
refs
=
display
.
map
((
d
)
=>
d
.
reference_temp
)
const
currents
=
display
.
map
((
d
)
=>
d
.
current_output
)
const
targetLine
=
display
.
map
(()
=>
experiment
.
value
?.
target_temp
??
null
)
// ── 温度图 ────────────────────────────────────────────────────────────────
if
(
tempChart
)
{
tempChart
.
setOption
(
{
animation
:
false
,
color
:
[
'#409EFF'
,
'#67C23A'
,
'#F56C6C'
],
tooltip
:
{
trigger
:
'axis'
,
backgroundColor
:
'rgba(255,255,255,0.96)'
,
borderColor
:
'#e2e8f0'
,
borderWidth
:
1
,
formatter
(
params
)
{
if
(
!
params
?.
length
)
return
''
const
lines
=
[
`<div style="margin-bottom:4px;font-weight:600">
${
params
[
0
].
axisValue
}
</div>`
]
params
.
forEach
((
p
)
=>
{
const
v
=
p
.
data
!=
null
?
Number
(
p
.
data
).
toFixed
(
3
)
+
' °C'
:
'--'
lines
.
push
(
`<div style="display:flex;justify-content:space-between;gap:16px">
<span>
${
p
.
marker
}${
p
.
seriesName
}
</span><strong>
${
v
}
</strong>
</div>`
,
)
})
return
lines
.
join
(
''
)
},
},
legend
:
{
bottom
:
4
,
itemWidth
:
18
,
itemHeight
:
8
,
textStyle
:
{
color
:
'#475569'
,
fontSize
:
12
},
},
grid
:
{
top
:
16
,
left
:
16
,
right
:
16
,
bottom
:
52
,
containLabel
:
true
},
xAxis
:
{
type
:
'category'
,
boundaryGap
:
false
,
data
:
xData
,
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
,
interval
:
Math
.
max
(
0
,
Math
.
floor
(
xData
.
length
/
10
)
-
1
)
},
axisLine
:
{
lineStyle
:
{
color
:
'#cbd5e1'
}
},
},
yAxis
:
{
type
:
'value'
,
name
:
'温度 (°C)'
,
nameTextStyle
:
{
color
:
'#64748b'
,
fontSize
:
11
},
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
},
splitLine
:
{
lineStyle
:
{
color
:
'#e2e8f0'
}
},
},
series
:
[
{
name
:
'实际温度'
,
type
:
'line'
,
data
:
actuals
,
smooth
:
true
,
symbol
:
'none'
,
lineStyle
:
{
width
:
2
},
},
{
name
:
'参考轨迹'
,
type
:
'line'
,
data
:
refs
,
smooth
:
true
,
symbol
:
'none'
,
lineStyle
:
{
width
:
1.5
,
type
:
'dashed'
},
},
{
name
:
'目标温度'
,
type
:
'line'
,
data
:
targetLine
,
symbol
:
'none'
,
lineStyle
:
{
width
:
1.5
,
type
:
'dotted'
,
color
:
'#F56C6C'
},
},
],
},
true
,
)
}
// ── 电流图 ────────────────────────────────────────────────────────────────
if
(
currChart
)
{
currChart
.
setOption
(
{
animation
:
false
,
color
:
[
'#E6A23C'
],
tooltip
:
{
trigger
:
'axis'
,
backgroundColor
:
'rgba(255,255,255,0.96)'
,
borderColor
:
'#e2e8f0'
,
borderWidth
:
1
,
formatter
(
params
)
{
if
(
!
params
?.
length
)
return
''
const
v
=
params
[
0
].
data
!=
null
?
Number
(
params
[
0
].
data
).
toFixed
(
3
)
+
' A'
:
'--'
return
`<div style="font-weight:600">
${
params
[
0
].
axisValue
}
</div>
<div>
${
params
[
0
].
marker
}
电流输出:<strong>
${
v
}
</strong></div>`
},
},
legend
:
{
bottom
:
4
,
itemWidth
:
18
,
itemHeight
:
8
,
textStyle
:
{
color
:
'#475569'
,
fontSize
:
12
},
},
grid
:
{
top
:
16
,
left
:
16
,
right
:
16
,
bottom
:
52
,
containLabel
:
true
},
xAxis
:
{
type
:
'category'
,
boundaryGap
:
false
,
data
:
xData
,
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
,
interval
:
Math
.
max
(
0
,
Math
.
floor
(
xData
.
length
/
10
)
-
1
)
},
axisLine
:
{
lineStyle
:
{
color
:
'#cbd5e1'
}
},
},
yAxis
:
{
type
:
'value'
,
name
:
'电流 (A)'
,
nameTextStyle
:
{
color
:
'#64748b'
,
fontSize
:
11
},
axisLabel
:
{
color
:
'#64748b'
,
fontSize
:
11
},
splitLine
:
{
lineStyle
:
{
color
:
'#e2e8f0'
}
},
min
:
0
,
},
series
:
[
{
name
:
'电流输出'
,
type
:
'line'
,
data
:
currents
,
smooth
:
false
,
symbol
:
'none'
,
lineStyle
:
{
width
:
2
},
areaStyle
:
{
opacity
:
0.08
},
},
],
},
true
,
)
}
}
// ── 窗口 resize ────────────────────────────────────────────────────────────────
const
onResize
=
()
=>
{
tempChart
?.
resize
()
currChart
?.
resize
()
}
// ── 生命周期 ───────────────────────────────────────────────────────────────────
onMounted
(
async
()
=>
{
await
loadExperiment
()
// 加载已有数据点
const
pts
=
await
getDataPoints
(
expId
,
0
)
dataPoints
.
value
=
pts
if
(
pts
.
length
)
nextFromStep
=
pts
[
pts
.
length
-
1
].
step_idx
+
1
await
nextTick
()
initCharts
()
if
(
experiment
.
value
?.
status
===
'running'
)
{
startPolling
()
}
window
.
addEventListener
(
'resize'
,
onResize
)
})
onBeforeUnmount
(()
=>
{
stopPolling
()
window
.
removeEventListener
(
'resize'
,
onResize
)
tempChart
?.
dispose
()
currChart
?.
dispose
()
})
// ── 状态辅助 ───────────────────────────────────────────────────────────────────
const
statusType
=
(
s
)
=>
({
idle
:
'info'
,
running
:
'success'
,
stopped
:
''
}[
s
]
??
'info'
)
const
statusLabel
=
(
s
)
=>
({
idle
:
'未开始'
,
running
:
'运行中'
,
stopped
:
'已停止'
}[
s
]
??
s
)
const
latestPoint
=
computed
(()
=>
{
const
pts
=
dataPoints
.
value
return
pts
.
length
?
pts
[
pts
.
length
-
1
]
:
null
})
</
script
>
<
template
>
<div
class=
"detail-page"
v-loading=
"loadingExp"
>
<!-- 顶部导航 -->
<div
class=
"page-header"
>
<el-button
:icon=
"ArrowLeft"
link
@
click=
"router.back()"
>
返回列表
</el-button>
<span
class=
"header-title"
>
{{
experiment
?.
name
??
'试验详情'
}}
</span>
<el-tag
v-if=
"experiment"
:type=
"statusType(experiment.status)"
size=
"small"
>
{{
statusLabel
(
experiment
.
status
)
}}
</el-tag>
</div>
<template
v-if=
"experiment"
>
<!-- 信息卡 + 控制面板 -->
<el-row
:gutter=
"16"
class=
"info-row"
>
<!-- 试验信息 -->
<el-col
:span=
"16"
>
<el-card
shadow=
"hover"
class=
"info-card"
>
<div
class=
"info-grid"
>
<div
class=
"info-item"
>
<span
class=
"label"
>
预测模型
</span>
<span
class=
"value"
>
{{
experiment
.
model_name
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
初始数据包
</span>
<span
class=
"value"
>
{{
experiment
.
package_name
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
目标温度
</span>
<span
class=
"value highlight"
>
{{
experiment
.
target_temp
?.
toFixed
(
1
)
}}
°C
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
已采集步数
</span>
<span
class=
"value"
>
{{
experiment
.
total_steps
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
采样周期
</span>
<span
class=
"value"
>
{{
experiment
.
sampling_interval
??
1.0
}}
s
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
开始时间
</span>
<span
class=
"value"
>
{{
experiment
.
start_time
??
'--'
}}
</span>
</div>
<div
class=
"info-item"
>
<span
class=
"label"
>
停止时间
</span>
<span
class=
"value"
>
{{
experiment
.
stop_time
??
'--'
}}
</span>
</div>
</div>
<div
v-if=
"experiment.error_msg"
class=
"error-msg"
>
⚠
{{
experiment
.
error_msg
}}
</div>
</el-card>
</el-col>
<!-- 实时读数 + 操作 -->
<el-col
:span=
"8"
>
<el-card
shadow=
"hover"
class=
"control-card"
>
<div
class=
"realtime-stats"
>
<div
class=
"stat-item"
>
<div
class=
"stat-label"
>
当前温度
</div>
<div
class=
"stat-value temp"
>
{{
latestPoint
?
latestPoint
.
actual_temp
.
toFixed
(
3
)
:
'--'
}}
°C
</div>
</div>
<div
class=
"stat-item"
>
<div
class=
"stat-label"
>
参考轨迹
</div>
<div
class=
"stat-value ref"
>
{{
latestPoint
?
latestPoint
.
reference_temp
.
toFixed
(
3
)
:
'--'
}}
°C
</div>
</div>
<div
class=
"stat-item"
>
<div
class=
"stat-label"
>
当前电流
</div>
<div
class=
"stat-value curr"
>
{{
latestPoint
?
latestPoint
.
current_output
.
toFixed
(
3
)
:
'--'
}}
A
</div>
</div>
<div
class=
"stat-item"
>
<div
class=
"stat-label"
>
目标温度
</div>
<div
class=
"stat-value target"
>
{{
experiment
.
target_temp
?.
toFixed
(
1
)
}}
°C
</div>
</div>
</div>
<div
class=
"control-btns"
>
<el-button
v-if=
"!isRunning"
type=
"success"
:icon=
"VideoPlay"
:loading=
"controlling"
:disabled=
"isStopped && experiment.total_steps >= 600"
style=
"width:100%;margin-bottom:10px"
@
click=
"handleStart"
>
开始 MPC 控制
</el-button>
<el-button
v-else
type=
"danger"
:icon=
"VideoPause"
:loading=
"controlling"
style=
"width:100%;margin-bottom:10px"
@
click=
"handleStop"
>
停止控制
</el-button>
<el-button
type=
"primary"
:icon=
"DataAnalysis"
:loading=
"loadingReport"
:disabled=
"!experiment.total_steps"
style=
"width:100%;margin-bottom:10px"
plain
@
click=
"handleReport"
>
查看报告
</el-button>
<el-button
:icon=
"Download"
:loading=
"exporting"
:disabled=
"isRunning || !experiment.total_steps || experiment.exported"
style=
"width:100%"
plain
@
click=
"handleExport"
>
{{
experiment
.
exported
?
'已导出'
:
'导出到历史数据'
}}
</el-button>
</div>
</el-card>
</el-col>
</el-row>
<!-- MPC 参数展示 -->
<el-card
shadow=
"hover"
class=
"params-card"
>
<template
#
header
>
<span
class=
"card-title"
>
MPC 控制参数
</span>
</
template
>
<div
class=
"params-row"
v-if=
"experiment.mpc_params"
>
<span
v-for=
"(v, k) in experiment.mpc_params"
:key=
"k"
class=
"param-chip"
>
<span
class=
"param-key"
>
{{ k }}
</span>
<span
class=
"param-val"
>
{{ v }}
</span>
</span>
</div>
</el-card>
<!-- 温度曲线图 -->
<el-card
shadow=
"hover"
class=
"chart-card"
>
<
template
#
header
>
<div
class=
"chart-header"
>
<span
class=
"card-title"
>
温度曲线
</span>
<span
v-if=
"isRunning"
class=
"live-badge"
>
● 实时更新中
</span>
</div>
</
template
>
<div
v-if=
"!dataPoints.length"
class=
"empty-chart"
>
<el-empty
description=
"暂无数据,请启动 MPC 控制"
:image-size=
"80"
/>
</div>
<div
v-else
ref=
"tempChartRef"
class=
"chart-body"
/>
</el-card>
<!-- 电流曲线图 -->
<el-card
shadow=
"hover"
class=
"chart-card"
>
<
template
#
header
>
<span
class=
"card-title"
>
电流输出曲线
</span>
</
template
>
<div
v-if=
"!dataPoints.length"
class=
"empty-chart"
>
<el-empty
description=
"暂无数据"
:image-size=
"80"
/>
</div>
<div
v-else
ref=
"currChartRef"
class=
"chart-body"
/>
</el-card>
</template>
<!-- 报告对话框 -->
<el-dialog
v-model=
"reportVisible"
title=
"试验报告"
width=
"620px"
>
<
template
v-if=
"reportData"
>
<el-descriptions
:column=
"2"
border
size=
"small"
class=
"report-desc"
>
<el-descriptions-item
label=
"试验名称"
:span=
"2"
>
{{
reportData
.
experiment
?.
name
}}
</el-descriptions-item>
<el-descriptions-item
label=
"目标温度"
>
{{
reportData
.
summary
?.
target_temp
?.
toFixed
(
1
)
}}
°C
</el-descriptions-item>
<el-descriptions-item
label=
"最终温度"
>
{{
reportData
.
summary
?.
final_temp
?.
toFixed
(
3
)
}}
°C
</el-descriptions-item>
<el-descriptions-item
label=
"初始温度"
>
{{
reportData
.
summary
?.
initial_temp
?.
toFixed
(
3
)
}}
°C
</el-descriptions-item>
<el-descriptions-item
label=
"采集步数"
>
{{
reportData
.
summary
?.
total_steps
}}
</el-descriptions-item>
<el-descriptions-item
label=
"仿真时长"
>
{{
reportData
.
summary
?.
duration_s
}}
s
</el-descriptions-item>
<el-descriptions-item
label=
"调节时间(步)"
>
{{
reportData
.
summary
?.
settling_step
??
'未稳定'
}}
</el-descriptions-item>
<el-descriptions-item
label=
"MAE (°C)"
>
{{
reportData
.
summary
?.
mae
}}
</el-descriptions-item>
<el-descriptions-item
label=
"RMSE (°C)"
>
{{
reportData
.
summary
?.
rmse
}}
</el-descriptions-item>
<el-descriptions-item
label=
"最大超调 (°C)"
>
{{
reportData
.
summary
?.
overshoot
}}
</el-descriptions-item>
<el-descriptions-item
label=
"平均电流 (A)"
>
{{
reportData
.
summary
?.
avg_current
}}
</el-descriptions-item>
<el-descriptions-item
label=
"最大电流 (A)"
>
{{
reportData
.
summary
?.
max_current
}}
</el-descriptions-item>
</el-descriptions>
</
template
>
<
template
#
footer
>
<el-button
@
click=
"reportVisible = false"
>
关闭
</el-button>
</
template
>
</el-dialog>
</div>
</template>
<
style
lang=
"scss"
scoped
>
.detail-page
{
padding
:
20px
;
display
:
flex
;
flex-direction
:
column
;
gap
:
16px
;
}
.page-header
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
.header-title
{
font-size
:
16px
;
font-weight
:
600
;
color
:
var
(
--
text-primary
);
flex
:
1
;
}
}
.info-row
{
margin
:
0
!
important
;
}
.info-card
{
height
:
100%
;
.info-grid
{
display
:
grid
;
grid-template-columns
:
1fr
1fr
;
gap
:
12px
24px
;
}
.info-item
{
display
:
flex
;
flex-direction
:
column
;
gap
:
2px
;
}
.label
{
font-size
:
12px
;
color
:
var
(
--
text-tertiary
);
}
.value
{
font-size
:
14px
;
color
:
var
(
--
text-primary
);
font-weight
:
500
;
&
.highlight
{
color
:
#165dff
;
font-size
:
18px
;
font-weight
:
700
;
}
}
.error-msg
{
margin-top
:
12px
;
color
:
#f56c6c
;
font-size
:
13px
;
background
:
#fff0f0
;
padding
:
8px
12px
;
border-radius
:
4px
;
}
}
.control-card
{
.realtime-stats
{
display
:
grid
;
grid-template-columns
:
1fr
1fr
;
gap
:
12px
;
margin-bottom
:
16px
;
}
.stat-item
{
text-align
:
center
;
padding
:
10px
8px
;
border-radius
:
6px
;
background
:
var
(
--
bg-page
);
}
.stat-label
{
font-size
:
11px
;
color
:
var
(
--
text-tertiary
);
margin-bottom
:
4px
;
}
.stat-value
{
font-size
:
16px
;
font-weight
:
700
;
&
.temp
{
color
:
#409eff
;
}
&
.ref
{
color
:
#67c23a
;
}
&
.curr
{
color
:
#e6a23c
;
}
&
.target
{
color
:
#f56c6c
;
}
}
}
.params-card
{
.params-row
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
8px
;
}
.param-chip
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
4px
;
padding
:
3px
10px
;
border-radius
:
12px
;
background
:
var
(
--
primary-light
);
font-size
:
12px
;
.param-key
{
color
:
var
(
--
text-secondary
);
}
.param-val
{
color
:
var
(
--
primary
);
font-weight
:
600
;
}
}
}
.chart-card
{
.chart-header
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
}
.live-badge
{
font-size
:
12px
;
color
:
#67c23a
;
animation
:
blink
1
.2s
ease-in-out
infinite
;
}
.empty-chart
{
height
:
240px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
}
.chart-body
{
height
:
280px
;
width
:
100%
;
}
}
.card-title
{
font-size
:
14px
;
font-weight
:
600
;
color
:
var
(
--
text-primary
);
}
.report-desc
{
:deep
(
.el-descriptions__label
)
{
width
:
140px
;
}
}
@keyframes
blink
{
0
%
,
100
%
{
opacity
:
1
;
}
50
%
{
opacity
:
0
.3
;
}
}
</
style
>
frontend/src/views/RealtimeMonitor/index.vue
View file @
d7a073ab
<
script
setup
>
import
{
Plus
,
Refresh
,
Delete
,
View
}
from
'@element-plus/icons-vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
onMounted
,
ref
,
reactive
}
from
'vue'
import
{
useRouter
}
from
'vue-router'
import
{
getExperiments
,
createExperiment
,
deleteExperiment
,
getMonitorModels
,
getMonitorPackages
,
}
from
'@/api/realtimeMonitor'
const
router
=
useRouter
()
// ── 试验列表 ───────────────────────────────────────────────────────────────────
const
experiments
=
ref
([])
const
loading
=
ref
(
false
)
const
loadExperiments
=
async
()
=>
{
loading
.
value
=
true
try
{
experiments
.
value
=
await
getExperiments
()
}
finally
{
loading
.
value
=
false
}
}
// ── 创建对话框 ─────────────────────────────────────────────────────────────────
const
dialogVisible
=
ref
(
false
)
const
submitting
=
ref
(
false
)
const
models
=
ref
([])
const
packages
=
ref
([])
const
defaultMpcParams
=
()
=>
({
P
:
20
,
M
:
5
,
Q
:
10.0
,
R
:
0.1
,
alpha
:
0.8
,
u_min
:
0.0
,
u_max
:
10.0
,
du_min
:
-
2.0
,
du_max
:
2.0
,
y_min
:
-
50.0
,
y_max
:
200.0
,
correction_gain
:
0.5
,
})
const
form
=
reactive
({
name
:
''
,
model_id
:
''
,
package_id
:
''
,
target_temp
:
35.0
,
sampling_interval
:
1.0
,
mpc_params
:
defaultMpcParams
(),
})
const
showAdvanced
=
ref
(
false
)
const
openDialog
=
async
()
=>
{
Object
.
assign
(
form
,
{
name
:
''
,
model_id
:
''
,
package_id
:
''
,
target_temp
:
35.0
,
sampling_interval
:
1.0
,
mpc_params
:
defaultMpcParams
(),
})
showAdvanced
.
value
=
false
dialogVisible
.
value
=
true
if
(
!
models
.
value
.
length
)
{
const
[
m
,
p
]
=
await
Promise
.
all
([
getMonitorModels
(),
getMonitorPackages
()])
models
.
value
=
m
packages
.
value
=
p
}
}
const
handleCreate
=
async
()
=>
{
if
(
!
form
.
name
.
trim
())
{
ElMessage
.
warning
(
'请输入试验名称'
);
return
}
if
(
!
form
.
model_id
)
{
ElMessage
.
warning
(
'请选择预测模型'
);
return
}
if
(
!
form
.
package_id
)
{
ElMessage
.
warning
(
'请选择初始数据包'
);
return
}
submitting
.
value
=
true
try
{
await
createExperiment
({
name
:
form
.
name
.
trim
(),
model_id
:
form
.
model_id
,
package_id
:
form
.
package_id
,
target_temp
:
form
.
target_temp
,
sampling_interval
:
form
.
sampling_interval
,
mpc_params
:
{
...
form
.
mpc_params
},
})
ElMessage
.
success
(
'试验创建成功'
)
dialogVisible
.
value
=
false
await
loadExperiments
()
}
finally
{
submitting
.
value
=
false
}
}
// ── 删除试验 ───────────────────────────────────────────────────────────────────
const
handleDelete
=
async
(
row
)
=>
{
try
{
await
ElMessageBox
.
confirm
(
`确定删除试验"
${
row
.
name
}
"吗?相关数据将一并删除。`
,
'删除确认'
,
{
type
:
'warning'
},
)
await
deleteExperiment
(
row
.
id
)
ElMessage
.
success
(
'试验已删除'
)
await
loadExperiments
()
}
catch
{
// 用户取消
}
}
// ── 进入详情 ───────────────────────────────────────────────────────────────────
const
handleView
=
(
row
)
=>
{
router
.
push
({
name
:
'realtime-monitor-detail'
,
params
:
{
id
:
row
.
id
}
})
}
// ── 状态标签 ───────────────────────────────────────────────────────────────────
const
statusType
=
(
s
)
=>
({
idle
:
'info'
,
running
:
'success'
,
stopped
:
''
}[
s
]
??
'info'
)
const
statusLabel
=
(
s
)
=>
({
idle
:
'未开始'
,
running
:
'运行中'
,
stopped
:
'已停止'
}[
s
]
??
s
)
onMounted
(
loadExperiments
)
</
script
>
<
template
>
<div
class=
"monitor-page"
>
<!-- 页头 -->
<el-card
shadow=
"hover"
class=
"page-card"
>
<template
#
header
>
<div
class=
"card-header-row"
>
<span
class=
"card-title"
>
实时监控试验
</span>
<div
class=
"header-actions"
>
<el-button
:icon=
"Refresh"
size=
"small"
plain
:loading=
"loading"
@
click=
"loadExperiments"
>
刷新
</el-button>
<el-button
type=
"primary"
:icon=
"Plus"
size=
"small"
@
click=
"openDialog"
>
创建试验
</el-button>
</div>
</div>
</
template
>
<el-table
:data=
"experiments"
v-loading=
"loading"
stripe
>
<el-table-column
prop=
"name"
label=
"试验名称"
min-width=
"160"
/>
<el-table-column
prop=
"model_name"
label=
"预测模型"
min-width=
"140"
/>
<el-table-column
prop=
"package_name"
label=
"数据包"
min-width=
"140"
/>
<el-table-column
prop=
"target_temp"
label=
"目标温度(°C)"
width=
"120"
align=
"right"
>
<
template
#
default=
"{ row }"
>
{{
row
.
target_temp
?.
toFixed
(
1
)
}}
</
template
>
</el-table-column>
<el-table-column
prop=
"status"
label=
"状态"
width=
"100"
align=
"center"
>
<
template
#
default=
"{ row }"
>
<el-tag
:type=
"statusType(row.status)"
size=
"small"
>
{{
statusLabel
(
row
.
status
)
}}
</el-tag>
</
template
>
</el-table-column>
<el-table-column
prop=
"total_steps"
label=
"已采集步数"
width=
"110"
align=
"right"
/>
<el-table-column
prop=
"created_at"
label=
"创建时间"
width=
"160"
/>
<el-table-column
label=
"操作"
width=
"140"
align=
"center"
fixed=
"right"
>
<
template
#
default=
"{ row }"
>
<el-button
size=
"small"
type=
"primary"
:icon=
"View"
link
@
click=
"handleView(row)"
>
进入
</el-button>
<el-button
size=
"small"
type=
"danger"
:icon=
"Delete"
link
:disabled=
"row.status === 'running'"
@
click=
"handleDelete(row)"
>
删除
</el-button>
</
template
>
</el-table-column>
</el-table>
</el-card>
<!-- 创建试验对话框 -->
<el-dialog
v-model=
"dialogVisible"
title=
"创建试验"
width=
"600px"
:close-on-click-modal=
"false"
>
<el-form
label-width=
"130px"
@
submit
.
prevent
>
<el-form-item
label=
"试验名称"
required
>
<el-input
v-model=
"form.name"
placeholder=
"请输入试验名称"
/>
</el-form-item>
<el-form-item
label=
"预测模型"
required
>
<el-select
v-model=
"form.model_id"
placeholder=
"选择模型"
style=
"width:100%"
>
<el-option
v-for=
"m in models"
:key=
"m.id"
:label=
"m.model_name"
:value=
"m.id"
>
<span>
{{ m.model_name }}
</span>
<span
style=
"float:right;color:#86909c;font-size:12px"
>
seq={{ m.seq_len }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item
label=
"初始数据包"
required
>
<el-select
v-model=
"form.package_id"
placeholder=
"选择数据包"
style=
"width:100%"
>
<el-option
v-for=
"p in packages"
:key=
"p.id"
:label=
"p.name"
:value=
"p.id"
/>
</el-select>
</el-form-item>
<el-form-item
label=
"目标温度(°C)"
required
>
<el-input-number
v-model=
"form.target_temp"
:step=
"0.5"
:precision=
"1"
style=
"width:160px"
/>
</el-form-item>
<el-form-item
label=
"采样周期(s)"
required
>
<el-input-number
v-model=
"form.sampling_interval"
:min=
"0.1"
:max=
"3600"
:step=
"0.5"
:precision=
"1"
style=
"width:160px"
/>
<span
style=
"margin-left:8px;color:#94a3b8;font-size:12px"
>
每步等待时长,需与传感器采集频率一致
</span>
</el-form-item>
<!-- MPC 参数(高级) -->
<el-divider
content-position=
"left"
>
<el-button
link
type=
"primary"
size=
"small"
@
click=
"showAdvanced = !showAdvanced"
>
{{ showAdvanced ? '收起 MPC 参数' : '展开 MPC 参数' }}
</el-button>
</el-divider>
<
template
v-if=
"showAdvanced"
>
<div
class=
"params-grid"
>
<el-form-item
label=
"预测时域 P"
>
<el-input-number
v-model=
"form.mpc_params.P"
:min=
"5"
:max=
"100"
:step=
"5"
/>
</el-form-item>
<el-form-item
label=
"控制时域 M"
>
<el-input-number
v-model=
"form.mpc_params.M"
:min=
"1"
:max=
"20"
/>
</el-form-item>
<el-form-item
label=
"跟踪权重 Q"
>
<el-input-number
v-model=
"form.mpc_params.Q"
:min=
"0.1"
:step=
"1"
:precision=
"1"
/>
</el-form-item>
<el-form-item
label=
"增量权重 R"
>
<el-input-number
v-model=
"form.mpc_params.R"
:min=
"0.01"
:step=
"0.05"
:precision=
"3"
/>
</el-form-item>
<el-form-item
label=
"柔化系数 α"
>
<el-input-number
v-model=
"form.mpc_params.alpha"
:min=
"0.1"
:max=
"0.99"
:step=
"0.05"
:precision=
"2"
/>
</el-form-item>
<el-form-item
label=
"反馈增益"
>
<el-input-number
v-model=
"form.mpc_params.correction_gain"
:min=
"0"
:max=
"1"
:step=
"0.1"
:precision=
"2"
/>
</el-form-item>
<el-form-item
label=
"电流下限(A)"
>
<el-input-number
v-model=
"form.mpc_params.u_min"
:min=
"0"
:precision=
"1"
/>
</el-form-item>
<el-form-item
label=
"电流上限(A)"
>
<el-input-number
v-model=
"form.mpc_params.u_max"
:min=
"0"
:precision=
"1"
/>
</el-form-item>
<el-form-item
label=
"Δu 下限"
>
<el-input-number
v-model=
"form.mpc_params.du_min"
:step=
"0.5"
:precision=
"1"
/>
</el-form-item>
<el-form-item
label=
"Δu 上限"
>
<el-input-number
v-model=
"form.mpc_params.du_max"
:step=
"0.5"
:precision=
"1"
/>
</el-form-item>
</div>
</
template
>
</el-form>
<
template
#
footer
>
<el-button
@
click=
"dialogVisible = false"
>
取消
</el-button>
<el-button
type=
"primary"
:loading=
"submitting"
@
click=
"handleCreate"
>
创建
</el-button>
</
template
>
</el-dialog>
</div>
</template>
<
style
lang=
"scss"
scoped
>
.monitor-page
{
padding
:
20px
;
}
.page-card
{
border-radius
:
8px
;
}
.card-header-row
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
}
.card-title
{
font-size
:
15px
;
font-weight
:
600
;
color
:
var
(
--
text-primary
);
}
.header-actions
{
display
:
flex
;
gap
:
8px
;
}
.params-grid
{
display
:
grid
;
grid-template-columns
:
1fr
1fr
;
gap
:
0
16px
;
}
</
style
>
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