Commit d7a073ab authored by luwei's avatar luwei

实时检测、历史记录完善

parent 3faede64
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())
......@@ -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
......
"""温度控制系统模型预测控制器。
本模块实现论文中描述的 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)
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',
]
......@@ -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,
......
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')
)
"""实时监控服务 — 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] 程控电源指令 → %.4f 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,
}
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:
......
......@@ -7,4 +7,5 @@ PyMySQL
openpyxl
xlrd
numpy
scipy
torch
\ No newline at end of file
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
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
.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}
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
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
......@@ -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>
......
import request from '@/utils/request'
export function getHistoryList() {
return request.get('/monitor/history')
}
export function getHistoryDetail(expId) {
return request.get(`/monitor/experiments/${expId}/report`)
}
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`)
}
......@@ -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',
......
<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, 1fr);
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>
<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>
<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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment