Commit 36e2cc19 authored by luwei's avatar luwei

提交修改

parent 3fd809ab
......@@ -18,6 +18,16 @@ class CategoryUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
class CleanRules(BaseModel):
enabled: bool = False
current_min: float | None = None
current_max: float | None = None
voltage_min: float | None = None
voltage_max: float | None = None
temperature_min: float | None = None
temperature_max: float | None = None
@router.get('/categories')
def get_categories():
return success_response(data=service.get_category_tree())
......@@ -62,16 +72,19 @@ class PackageCreateRequest(BaseModel):
category_id: str | int | None = Field(default=None)
remark: str | None = Field(default=None)
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
class PreviewRequest(BaseModel):
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
@router.post('/preview')
def preview_package(request: PreviewRequest, limit: int = Query(default=300, ge=1, le=2000)):
try:
result = service.preview_records(file_ids=request.file_ids, limit=limit)
clean_rules = request.clean_rules.model_dump() if request.clean_rules else None
result = service.preview_records(file_ids=request.file_ids, limit=limit, clean_rules=clean_rules)
return success_response(data=result)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
......@@ -89,11 +102,13 @@ def list_packages(
def create_package(request: PackageCreateRequest):
try:
category_id = None if request.category_id in (None, '', 'all') else str(request.category_id)
clean_rules = request.clean_rules.model_dump() if request.clean_rules else None
pkg = service.create_package(
name=request.name.strip(),
category_id=category_id,
remark=request.remark,
file_ids=request.file_ids,
clean_rules=clean_rules,
)
return success_response(data=pkg, message='数据包创建成功')
except ValueError as error:
......
......@@ -17,7 +17,6 @@ class LSTMParams(BaseModel):
epochs: int = Field(default=50, ge=1, le=2000)
batch_size: int = Field(default=32, ge=1, le=512)
learning_rate: float = Field(default=0.001, gt=0, le=1)
train_ratio: float = Field(default=0.8, ge=0.5, le=0.99)
class CreateTaskRequest(BaseModel):
......
"""LSTM temperature forecasting trainer.
Uses PyTorch if available. If not installed, raises a descriptive RuntimeError
so the train service can mark the task as failed with a helpful message.
"""
from __future__ import annotations
......@@ -46,6 +44,11 @@ FEATURE_COLS = ['current', 'voltage', 'set_temperature', 'actual_temperature']
TARGET_COL = 'actual_temperature'
TARGET_IDX = FEATURE_COLS.index(TARGET_COL)
# Fixed dataset split ratios (train / val / test)
_TRAIN_RATIO = 0.70
_VAL_RATIO = 0.15
# test = 1 - _TRAIN_RATIO - _VAL_RATIO (≈ 0.15)
def _check_torch() -> None:
if not _TORCH_AVAILABLE:
......@@ -97,13 +100,14 @@ def train_lstm(
Args:
records: list of dicts with keys in FEATURE_COLS.
params: hyper-parameter dict (seq_len, hidden_size, num_layers,
epochs, batch_size, learning_rate, train_ratio).
epochs, batch_size, learning_rate).
train_ratio is ignored – fixed 70/15/15 split is used.
save_path: destination .pt file.
on_progress: callback(pct, train_loss, val_loss) called after each epoch.
cancel_event: when set, training stops with InterruptedError.
Returns:
{'train_loss': float, 'val_loss': float|None}
{'train_loss': float, 'val_loss': float|None, 'test_loss': float|None}
"""
_check_torch()
......@@ -113,7 +117,6 @@ def train_lstm(
epochs = max(1, int(params.get('epochs', 50)))
batch_size = max(1, int(params.get('batch_size', 32)))
lr = float(params.get('learning_rate', 0.001))
train_ratio = min(0.99, max(0.5, float(params.get('train_ratio', 0.8))))
# ── data preparation ────────────────────────────────────────────────────
data = _extract_features(records)
......@@ -132,17 +135,27 @@ def train_lstm(
data_norm = (data - data_min) / data_range
X, y = _make_sequences(data_norm, seq_len)
n_train = max(1, int(len(X) * train_ratio))
# Fixed 70 / 15 / 15 split
n_total = len(X)
n_train = max(1, int(n_total * _TRAIN_RATIO))
n_val = max(1, int(n_total * _VAL_RATIO))
# test gets the remainder so the three parts always sum to n_total
X_train, y_train = X[:n_train], y[:n_train]
X_val, y_val = X[n_train:], y[n_train:]
X_val, y_val = X[n_train : n_train + n_val], y[n_train : n_train + n_val]
X_test, y_test = X[n_train + n_val :], y[n_train + n_val :]
device = torch.device('cpu')
X_train_t = torch.tensor(X_train).to(device)
y_train_t = torch.tensor(y_train).to(device)
has_val = len(X_val) > 0
has_val = len(X_val) > 0
has_test = len(X_test) > 0
if has_val:
X_val_t = torch.tensor(X_val).to(device)
y_val_t = torch.tensor(y_val).to(device)
if has_test:
X_test_t = torch.tensor(X_test).to(device)
y_test_t = torch.tensor(y_test).to(device)
train_loader = DataLoader(
TensorDataset(X_train_t, y_train_t),
......@@ -186,6 +199,14 @@ def train_lstm(
pct = int((epoch + 1) / epochs * 100)
on_progress(pct, train_loss, val_loss)
# ── test-set evaluation (completely hidden during training) ──────────────
test_loss: float | None = None
if has_test:
model.eval()
with torch.no_grad():
test_pred = model(X_test_t)
test_loss = criterion(test_pred, y_test_t).item()
# ── persist ──────────────────────────────────────────────────────────────
save_path.parent.mkdir(parents=True, exist_ok=True)
torch.save(
......@@ -206,5 +227,6 @@ def train_lstm(
return {
'train_loss': round(float(train_loss), 6),
'val_loss': round(float(val_loss), 6) if val_loss is not None else None,
'val_loss': round(float(val_loss), 6) if val_loss is not None else None,
'test_loss': round(float(test_loss), 6) if test_loss is not None else None,
}
from sqlalchemy import BIGINT, TIMESTAMP, Enum, Index, Integer, String, Text, text
from sqlalchemy import BIGINT, TIMESTAMP, Enum, Index, Integer, JSON, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
......@@ -58,6 +58,7 @@ class DataPackage(Base):
category_id: Mapped[int | None] = mapped_column(BIGINT, nullable=True, comment='分类ID(data_package类型)')
remark: Mapped[str | None] = mapped_column(Text, nullable=True, comment='备注')
data_count: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text('0'), comment='数据条数')
clean_rules: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment='野值清洗规则')
created_at: Mapped[str] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
updated_at: Mapped[str] = mapped_column(
TIMESTAMP,
......
......@@ -25,6 +25,7 @@ class TrainTask(Base):
progress: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text('0'), comment='进度 0-100')
train_loss: Mapped[float | None] = mapped_column(FLOAT, nullable=True, comment='训练损失')
val_loss: Mapped[float | None] = mapped_column(FLOAT, nullable=True, comment='验证损失')
test_loss: Mapped[float | None] = mapped_column(FLOAT, nullable=True, comment='测试损失')
error_msg: Mapped[str | None] = mapped_column(Text, nullable=True, comment='错误信息')
is_saved: 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'))
......@@ -51,4 +52,5 @@ class SavedModel(Base):
file_path: Mapped[str] = mapped_column(String(500), nullable=False, comment='模型文件路径')
train_loss: Mapped[float | None] = mapped_column(FLOAT, nullable=True)
val_loss: Mapped[float | None] = mapped_column(FLOAT, nullable=True)
test_loss: Mapped[float | None] = mapped_column(FLOAT, nullable=True)
created_at: Mapped[str] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
......@@ -153,6 +153,7 @@ class PackageManagementService:
category_id: str | None,
remark: str | None,
file_ids: list[int],
clean_rules: dict | None = None,
) -> dict[str, Any]:
if not name:
raise ValueError('数据包名称不能为空')
......@@ -177,13 +178,22 @@ class PackageManagementService:
if len(files) != len(file_ids):
raise ValueError('部分数据文件不存在')
total_count = sum(f.data_count for f in files)
file_map = {f.id: f for f in files}
if clean_rules and clean_rules.get('enabled'):
result = self._merge_records(file_ids, file_map, limit=None, clean_rules=clean_rules)
total_count = result['count']
stored_rules: dict | None = clean_rules
else:
total_count = sum(f.data_count for f in files)
stored_rules = None
pkg = DataPackage(
name=name,
category_id=cat_db_id,
remark=remark,
data_count=total_count,
clean_rules=stored_rules,
)
session.add(pkg)
session.flush()
......@@ -214,6 +224,8 @@ class PackageManagementService:
if not pkg:
raise ValueError('数据包不存在')
stored_clean_rules = pkg.clean_rules
pf_rows = (
session.query(DataPackageFile)
.filter(DataPackageFile.package_id == db_id)
......@@ -224,9 +236,14 @@ class PackageManagementService:
files = session.query(DataFile).filter(DataFile.id.in_(file_ids)).all()
file_map = {f.id: f for f in files}
return self._merge_records(file_ids, file_map, limit)
return self._merge_records(file_ids, file_map, limit, clean_rules=stored_clean_rules)
def preview_records(self, file_ids: list[int], limit: int = 300) -> dict[str, Any]:
def preview_records(
self,
file_ids: list[int],
limit: int = 300,
clean_rules: dict | None = None,
) -> dict[str, Any]:
if not file_ids:
return {'records': [], 'count': 0}
......@@ -234,7 +251,7 @@ class PackageManagementService:
files = session.query(DataFile).filter(DataFile.id.in_(file_ids)).all()
file_map = {f.id: f for f in files}
return self._merge_records(file_ids, file_map, limit)
return self._merge_records(file_ids, file_map, limit, clean_rules=clean_rules)
# ── helpers ──────────────────────────────────────────────────────────────
......@@ -242,8 +259,10 @@ class PackageManagementService:
self,
file_ids: list[int],
file_map: dict[int, Any],
limit: int,
limit: int | None,
clean_rules: dict | None = None,
) -> dict[str, Any]:
use_filter = bool(clean_rules and clean_rules.get('enabled'))
all_records: list[dict[str, Any]] = []
total_count = 0
remaining = limit
......@@ -253,15 +272,61 @@ class PackageManagementService:
continue
file_meta = file_map[fid]
path = self._dm._resolve_local_file_path(file_meta.file_path, file_meta.stored_name)
records, count = self._dm._read_records(path, limit=remaining if remaining > 0 else 0)
if use_filter:
# Read all rows so filter can be applied correctly
records, count = self._dm._read_records(path, limit=None)
else:
read_limit = (remaining if remaining is not None and remaining > 0 else 0) if remaining is not None else None
records, count = self._dm._read_records(path, limit=read_limit)
total_count += count
all_records.extend(records)
remaining -= len(records)
if remaining <= 0:
break
if not use_filter and remaining is not None:
remaining -= len(records)
if remaining <= 0:
break
if use_filter:
all_records = self._apply_clean_rules(all_records, clean_rules)
total_count = len(all_records)
if limit is not None and limit > 0:
return {'records': all_records[:limit], 'count': total_count}
return {'records': all_records, 'count': total_count}
@staticmethod
def _apply_clean_rules(records: list[dict[str, Any]], clean_rules: dict) -> list[dict[str, Any]]:
c_min = clean_rules.get('current_min')
c_max = clean_rules.get('current_max')
v_min = clean_rules.get('voltage_min')
v_max = clean_rules.get('voltage_max')
t_min = clean_rules.get('temperature_min')
t_max = clean_rules.get('temperature_max')
result = []
for r in records:
current = r.get('current')
voltage = r.get('voltage')
temp = r.get('actual_temperature')
if current is not None:
if c_min is not None and current < c_min:
continue
if c_max is not None and current > c_max:
continue
if voltage is not None:
if v_min is not None and voltage < v_min:
continue
if v_max is not None and voltage > v_max:
continue
if temp is not None:
if t_min is not None and temp < t_min:
continue
if t_max is not None and temp > t_max:
continue
result.append(r)
return result
def _pkg_to_dict(self, pkg: DataPackage) -> dict[str, Any]:
return {
'id': pkg.id,
......
......@@ -150,6 +150,7 @@ class TrainService:
file_path=str(model_path),
train_loss=task.train_loss,
val_loss=task.val_loss,
test_loss=task.test_loss,
)
session.add(saved)
task.is_saved = 1
......@@ -242,6 +243,7 @@ class TrainService:
progress=100,
train_loss=result['train_loss'],
val_loss=result.get('val_loss'),
test_loss=result.get('test_loss'),
)
except InterruptedError:
......@@ -300,6 +302,7 @@ class TrainService:
'progress': task.progress,
'train_loss': task.train_loss,
'val_loss': task.val_loss,
'test_loss': task.test_loss,
'error_msg': task.error_msg,
'is_saved': bool(task.is_saved),
'created_at': task.created_at.strftime('%Y-%m-%d %H:%M:%S') if task.created_at else '',
......@@ -316,5 +319,6 @@ class TrainService:
'params': model.params,
'train_loss': model.train_loss,
'val_loss': model.val_loss,
'test_loss': model.test_loss,
'created_at': model.created_at.strftime('%Y-%m-%d %H:%M:%S') if model.created_at else '',
}
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/DataManagement-CA4nqNQp.js","assets/_plugin-vue_export-helper-D1RKUtCV.js","assets/es-DCOtnflc.js","assets/request-D8DwihSV.js","assets/DataCurve--Vg_Op5b.js","assets/echarts-B4btcaVd.js","assets/DataCurve-DL-fCLWu.css","assets/DataManagement-D7Mq9-H4.css","assets/UnderConstruction-n0JcUgMW.js","assets/UnderConstruction-BwhS9Ul2.css","assets/ModelTraining-DPxp6Y6Z.js","assets/trainManagement-TFX-G3Jl.js","assets/ModelTraining-D7HksGk_.css","assets/ModelList-DLDTMRtX.js","assets/ModelList-CHRYisJa.css","assets/ModelEvaluation-CVr4AavX.js","assets/ModelEvaluation-D27Aus9X.css","assets/PackageManagement-DRdx9gQd.js","assets/PackageManagement-DHmLXqM4.css"])))=>i.map(i=>d[i]);
import{p as e,t}from"./es-DCOtnflc.js";import{B as n,C as r,D as i,I as a,L as o,Q as s,R as c,X as l,a as u,at as d,ct as f,d as p,et as ee,ht as m,lt as te,m as ne,rt as re,st as ie,t as ae,u as h,v as g,w as _,y as v}from"./_plugin-vue_export-helper-D1RKUtCV.js";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var oe=typeof window<`u`,se=Symbol(),y;(function(e){e.direct=`direct`,e.patchObject=`patch object`,e.patchFunction=`patch function`})(y||={});var ce=typeof window==`object`&&window.window===window?window:typeof self==`object`&&self.self===self?self:typeof global==`object`&&global.global===global?global:typeof globalThis==`object`?globalThis:{HTMLElement:null};function b(e,{autoBom:t=!1}={}){return t&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)?new Blob([``,e],{type:e.type}):e}function x(e,t,n){let r=new XMLHttpRequest;r.open(`GET`,e),r.responseType=`blob`,r.onload=function(){T(r.response,t,n)},r.onerror=function(){console.error(`could not download file`)},r.send()}function S(e){let t=new XMLHttpRequest;t.open(`HEAD`,e,!1);try{t.send()}catch{}return t.status>=200&&t.status<=299}function C(e){try{e.dispatchEvent(new MouseEvent(`click`))}catch{let t=new MouseEvent(`click`,{bubbles:!0,cancelable:!0,view:window,detail:0,screenX:80,screenY:20,clientX:80,clientY:20,ctrlKey:!1,altKey:!1,shiftKey:!1,metaKey:!1,button:0,relatedTarget:null});e.dispatchEvent(t)}}var w=typeof navigator==`object`?navigator:{userAgent:``},le=/Macintosh/.test(w.userAgent)&&/AppleWebKit/.test(w.userAgent)&&!/Safari/.test(w.userAgent),T=oe?typeof HTMLAnchorElement<`u`&&`download`in HTMLAnchorElement.prototype&&!le?ue:`msSaveOrOpenBlob`in w?E:D:()=>{};function ue(e,t=`download`,n){let r=document.createElement(`a`);r.download=t,r.rel=`noopener`,typeof e==`string`?(r.href=e,r.origin===location.origin?C(r):S(r.href)?x(e,t,n):(r.target=`_blank`,C(r))):(r.href=URL.createObjectURL(e),setTimeout(function(){URL.revokeObjectURL(r.href)},4e4),setTimeout(function(){C(r)},0))}function E(e,t=`download`,n){if(typeof e==`string`)if(S(e))x(e,t,n);else{let t=document.createElement(`a`);t.href=e,t.target=`_blank`,setTimeout(function(){C(t)})}else navigator.msSaveOrOpenBlob(b(e,n),t)}function D(e,t,n,r){if(r||=open(``,`_blank`),r&&(r.document.title=r.document.body.innerText=`downloading...`),typeof e==`string`)return x(e,t,n);let i=e.type===`application/octet-stream`,a=/constructor/i.test(String(ce.HTMLElement))||`safari`in ce,o=/CriOS\/[\d]+/.test(navigator.userAgent);if((o||i&&a||le)&&typeof FileReader<`u`){let t=new FileReader;t.onloadend=function(){let e=t.result;if(typeof e!=`string`)throw r=null,Error(`Wrong reader.result type`);e=o?e:e.replace(/^data:[^;]*;/,`data:attachment/file;`),r?r.location.href=e:location.assign(e),r=null},t.readAsDataURL(e)}else{let t=URL.createObjectURL(e);r?r.location.assign(t):location.href=t,r=null,setTimeout(function(){URL.revokeObjectURL(t)},4e4)}}var{assign:de}=Object;function O(){let e=ee(!0),t=e.run(()=>ie({})),n=[],r=[],i=re({install(e){i._a=e,e.provide(se,i),e.config.globalProperties.$pinia=i,r.forEach(e=>n.push(e)),r=[]},use(e){return this._a?n.push(e):r.push(e),this},_p:n,_a:null,_e:e,_s:new Map,state:t});return i}var{assign:fe}=Object,k=typeof document<`u`;function A(e){return typeof e==`object`||`displayName`in e||`props`in e||`__vccOpts`in e}function j(e){return e.__esModule||e[Symbol.toStringTag]===`Module`||e.default&&A(e.default)}var M=Object.assign;function pe(e,t){let n={};for(let r in t){let i=t[r];n[r]=P(i)?i.map(e):e(i)}return n}var N=()=>{},P=Array.isArray;function F(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var I=function(e){return e[e.MATCHER_NOT_FOUND=1]=`MATCHER_NOT_FOUND`,e[e.NAVIGATION_GUARD_REDIRECT=2]=`NAVIGATION_GUARD_REDIRECT`,e[e.NAVIGATION_ABORTED=4]=`NAVIGATION_ABORTED`,e[e.NAVIGATION_CANCELLED=8]=`NAVIGATION_CANCELLED`,e[e.NAVIGATION_DUPLICATED=16]=`NAVIGATION_DUPLICATED`,e}({}),L=Symbol(``);I.MATCHER_NOT_FOUND,I.NAVIGATION_GUARD_REDIRECT,I.NAVIGATION_ABORTED,I.NAVIGATION_CANCELLED,I.NAVIGATION_DUPLICATED;function R(e,t){return M(Error(),{type:e,[L]:!0},t)}function z(e,t){return e instanceof Error&&L in e&&(t==null||!!(e.type&t))}var B=Symbol(``),me=Symbol(``),V=Symbol(``),he=Symbol(``),ge=Symbol(``);function _e(){return _(V)}function ve(e){return _(he)}var ye=/#/g,be=/&/g,xe=/\//g,Se=/=/g,Ce=/\?/g,we=/\+/g,Te=/%5B/g,Ee=/%5D/g,De=/%5E/g,Oe=/%60/g,ke=/%7B/g,Ae=/%7C/g,je=/%7D/g,Me=/%20/g;function Ne(e){return e==null?``:encodeURI(``+e).replace(Ae,`|`).replace(Te,`[`).replace(Ee,`]`)}function Pe(e){return Ne(e).replace(ke,`{`).replace(je,`}`).replace(De,`^`)}function Fe(e){return Ne(e).replace(we,`%2B`).replace(Me,`+`).replace(ye,`%23`).replace(be,`%26`).replace(Oe,"`").replace(ke,`{`).replace(je,`}`).replace(De,`^`)}function Ie(e){return Fe(e).replace(Se,`%3D`)}function Le(e){return Ne(e).replace(ye,`%23`).replace(Ce,`%3F`)}function Re(e){return Le(e).replace(xe,`%2F`)}function H(e){if(e==null)return null;try{return decodeURIComponent(``+e)}catch{}return``+e}var ze=/\/$/,Be=e=>e.replace(ze,``);function Ve(e,t,n=`/`){let r,i={},a=``,o=``,s=t.indexOf(`#`),c=t.indexOf(`?`);return c=s>=0&&c>s?-1:c,c>=0&&(r=t.slice(0,c),a=t.slice(c,s>0?s:t.length),i=e(a.slice(1))),s>=0&&(r||=t.slice(0,s),o=t.slice(s,t.length)),r=Je(r??t,n),{fullPath:r+a+o,path:r,query:i,hash:H(o)}}function He(e,t){let n=t.query?e(t.query):``;return t.path+(n&&`?`)+n+(t.hash||``)}function Ue(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||`/`}function We(e,t,n){let r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&U(t.matched[r],n.matched[i])&&Ge(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function U(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Ge(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Ke(e[n],t[n]))return!1;return!0}function Ke(e,t){return P(e)?qe(e,t):P(t)?qe(t,e):(e&&e.valueOf())===(t&&t.valueOf())}function qe(e,t){return P(t)?e.length===t.length&&e.every((e,n)=>e===t[n]):e.length===1&&e[0]===t}function Je(e,t){if(e.startsWith(`/`))return e;if(!e)return t;let n=t.split(`/`),r=e.split(`/`),i=r[r.length-1];(i===`..`||i===`.`)&&r.push(``);let a=n.length-1,o,s;for(o=0;o<r.length;o++)if(s=r[o],s!==`.`)if(s===`..`)a>1&&a--;else break;return n.slice(0,a).join(`/`)+`/`+r.slice(o).join(`/`)}var W={path:`/`,name:void 0,params:{},query:{},hash:``,fullPath:`/`,matched:[],meta:{},redirectedFrom:void 0},G=function(e){return e.pop=`pop`,e.push=`push`,e}({}),Ye=function(e){return e.back=`back`,e.forward=`forward`,e.unknown=``,e}({});function Xe(e){if(!e)if(k){let t=document.querySelector(`base`);e=t&&t.getAttribute(`href`)||`/`,e=e.replace(/^\w+:\/\/[^\/]+/,``)}else e=`/`;return e[0]!==`/`&&e[0]!==`#`&&(e=`/`+e),Be(e)}var Ze=/^[^#]+#/;function Qe(e,t){return e.replace(Ze,`#`)+t}function $e(e,t){let n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}var K=()=>({left:window.scrollX,top:window.scrollY});function et(e){let t;if(`el`in e){let n=e.el,r=typeof n==`string`&&n.startsWith(`#`),i=typeof n==`string`?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=$e(i,e)}else t=e;`scrollBehavior`in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left==null?window.scrollX:t.left,t.top==null?window.scrollY:t.top)}function tt(e,t){return(history.state?history.state.position-t:-1)+e}var nt=new Map;function rt(e,t){nt.set(e,t)}function it(e){let t=nt.get(e);return nt.delete(e),t}function at(e){return typeof e==`string`||e&&typeof e==`object`}function ot(e){return typeof e==`string`||typeof e==`symbol`}function st(e){let t={};if(e===``||e===`?`)return t;let n=(e[0]===`?`?e.slice(1):e).split(`&`);for(let e=0;e<n.length;++e){let r=n[e].replace(we,` `),i=r.indexOf(`=`),a=H(i<0?r:r.slice(0,i)),o=i<0?null:H(r.slice(i+1));if(a in t){let e=t[a];P(e)||(e=t[a]=[e]),e.push(o)}else t[a]=o}return t}function ct(e){let t=``;for(let n in e){let r=e[n];if(n=Ie(n),r==null){r!==void 0&&(t+=(t.length?`&`:``)+n);continue}(P(r)?r.map(e=>e&&Fe(e)):[r&&Fe(r)]).forEach(e=>{e!==void 0&&(t+=(t.length?`&`:``)+n,e!=null&&(t+=`=`+e))})}return t}function lt(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=P(r)?r.map(e=>e==null?null:``+e):r==null?r:``+r)}return t}function q(){let e=[];function t(t){return e.push(t),()=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function J(e,t,n,r,i,a=e=>e()){let o=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((s,c)=>{let l=e=>{e===!1?c(R(I.NAVIGATION_ABORTED,{from:n,to:t})):e instanceof Error?c(e):at(e)?c(R(I.NAVIGATION_GUARD_REDIRECT,{from:t,to:e})):(o&&r.enterCallbacks[i]===o&&typeof e==`function`&&o.push(e),s())},u=a(()=>e.call(r&&r.instances[i],t,n,l)),d=Promise.resolve(u);e.length<3&&(d=d.then(l)),d.catch(e=>c(e))})}function ut(e,t,n,r,i=e=>e()){let a=[];for(let o of e)for(let e in o.components){let s=o.components[e];if(!(t!==`beforeRouteEnter`&&!o.instances[e]))if(A(s)){let c=(s.__vccOpts||s)[t];c&&a.push(J(c,n,r,o,e,i))}else{let c=s();a.push(()=>c.then(a=>{if(!a)throw Error(`Couldn't resolve component "${e}" at "${o.path}"`);let s=j(a)?a.default:a;o.mods[e]=a,o.components[e]=s;let c=(s.__vccOpts||s)[t];return c&&J(c,n,r,o,e,i)()}))}}return a}function dt(e,t){let n=[],r=[],i=[],a=Math.max(t.matched.length,e.matched.length);for(let o=0;o<a;o++){let a=t.matched[o];a&&(e.matched.find(e=>U(e,a))?r.push(a):n.push(a));let s=e.matched[o];s&&(t.matched.find(e=>U(e,s))||i.push(s))}return[n,r,i]}var ft=()=>location.protocol+`//`+location.host;function pt(e,t){let{pathname:n,search:r,hash:i}=t,a=e.indexOf(`#`);if(a>-1){let t=i.includes(e.slice(a))?e.slice(a).length:1,n=i.slice(t);return n[0]!==`/`&&(n=`/`+n),Ue(n,``)}return Ue(n,e)+r+i}function mt(e,t,n,r){let i=[],a=[],o=null,s=({state:a})=>{let s=pt(e,location),c=n.value,l=t.value,u=0;if(a){if(n.value=s,t.value=a,o&&o===c){o=null;return}u=l?a.position-l.position:0}else r(s);i.forEach(e=>{e(n.value,c,{delta:u,type:G.pop,direction:u?u>0?Ye.forward:Ye.back:Ye.unknown})})};function c(){o=n.value}function l(e){i.push(e);let t=()=>{let t=i.indexOf(e);t>-1&&i.splice(t,1)};return a.push(t),t}function u(){if(document.visibilityState===`hidden`){let{history:e}=window;if(!e.state)return;e.replaceState(M({},e.state,{scroll:K()}),``)}}function d(){for(let e of a)e();a=[],window.removeEventListener(`popstate`,s),window.removeEventListener(`pagehide`,u),document.removeEventListener(`visibilitychange`,u)}return window.addEventListener(`popstate`,s),window.addEventListener(`pagehide`,u),document.addEventListener(`visibilitychange`,u),{pauseListeners:c,listen:l,destroy:d}}function ht(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?K():null}}function gt(e){let{history:t,location:n}=window,r={value:pt(e,n)},i={value:t.state};i.value||a(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function a(r,a,o){let s=e.indexOf(`#`),c=s>-1?(n.host&&document.querySelector(`base`)?e:e.slice(s))+r:ft()+e+r;try{t[o?`replaceState`:`pushState`](a,``,c),i.value=a}catch(e){console.error(e),n[o?`replace`:`assign`](c)}}function o(e,n){a(e,M({},t.state,ht(i.value.back,e,i.value.forward,!0),n,{position:i.value.position}),!0),r.value=e}function s(e,n){let o=M({},i.value,t.state,{forward:e,scroll:K()});a(o.current,o,!0),a(e,M({},ht(r.value,e,null),{position:o.position+1},n),!1),r.value=e}return{location:r,state:i,push:s,replace:o}}function _t(e){e=Xe(e);let t=gt(e),n=mt(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}let i=M({location:``,base:e,go:r,createHref:Qe.bind(null,e)},t,n);return Object.defineProperty(i,`location`,{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,`state`,{enumerable:!0,get:()=>t.state.value}),i}var Y=function(e){return e[e.Static=0]=`Static`,e[e.Param=1]=`Param`,e[e.Group=2]=`Group`,e}({}),X=function(e){return e[e.Static=0]=`Static`,e[e.Param=1]=`Param`,e[e.ParamRegExp=2]=`ParamRegExp`,e[e.ParamRegExpEnd=3]=`ParamRegExpEnd`,e[e.EscapeNext=4]=`EscapeNext`,e}(X||{}),vt={type:Y.Static,value:``},yt=/[a-zA-Z0-9_]/;function bt(e){if(!e)return[[]];if(e===`/`)return[[vt]];if(!e.startsWith(`/`))throw Error(`Invalid path "${e}"`);function t(e){throw Error(`ERR (${n})/"${l}": ${e}`)}let n=X.Static,r=n,i=[],a;function o(){a&&i.push(a),a=[]}let s=0,c,l=``,u=``;function d(){l&&=(n===X.Static?a.push({type:Y.Static,value:l}):n===X.Param||n===X.ParamRegExp||n===X.ParamRegExpEnd?(a.length>1&&(c===`*`||c===`+`)&&t(`A repeatable param (${l}) must be alone in its segment. eg: '/:ids+.`),a.push({type:Y.Param,value:l,regexp:u,repeatable:c===`*`||c===`+`,optional:c===`*`||c===`?`})):t(`Invalid state to consume buffer`),``)}function f(){l+=c}for(;s<e.length;){if(c=e[s++],c===`\\`&&n!==X.ParamRegExp){r=n,n=X.EscapeNext;continue}switch(n){case X.Static:c===`/`?(l&&d(),o()):c===`:`?(d(),n=X.Param):f();break;case X.EscapeNext:f(),n=r;break;case X.Param:c===`(`?n=X.ParamRegExp:yt.test(c)?f():(d(),n=X.Static,c!==`*`&&c!==`?`&&c!==`+`&&s--);break;case X.ParamRegExp:c===`)`?u[u.length-1]==`\\`?u=u.slice(0,-1)+c:n=X.ParamRegExpEnd:u+=c;break;case X.ParamRegExpEnd:d(),n=X.Static,c!==`*`&&c!==`?`&&c!==`+`&&s--,u=``;break;default:t(`Unknown state`);break}}return n===X.ParamRegExp&&t(`Unfinished custom RegExp for param "${l}"`),d(),o(),i}var xt=`[^/]+?`,St={sensitive:!1,strict:!1,start:!0,end:!0},Z=function(e){return e[e._multiplier=10]=`_multiplier`,e[e.Root=90]=`Root`,e[e.Segment=40]=`Segment`,e[e.SubSegment=30]=`SubSegment`,e[e.Static=40]=`Static`,e[e.Dynamic=20]=`Dynamic`,e[e.BonusCustomRegExp=10]=`BonusCustomRegExp`,e[e.BonusWildcard=-50]=`BonusWildcard`,e[e.BonusRepeatable=-20]=`BonusRepeatable`,e[e.BonusOptional=-8]=`BonusOptional`,e[e.BonusStrict=.7000000000000001]=`BonusStrict`,e[e.BonusCaseSensitive=.25]=`BonusCaseSensitive`,e}(Z||{}),Ct=/[.+*?^${}()[\]/\\]/g;function wt(e,t){let n=M({},St,t),r=[],i=n.start?`^`:``,a=[];for(let t of e){let e=t.length?[]:[Z.Root];n.strict&&!t.length&&(i+=`/`);for(let r=0;r<t.length;r++){let o=t[r],s=Z.Segment+(n.sensitive?Z.BonusCaseSensitive:0);if(o.type===Y.Static)r||(i+=`/`),i+=o.value.replace(Ct,`\\$&`),s+=Z.Static;else if(o.type===Y.Param){let{value:e,repeatable:n,optional:c,regexp:l}=o;a.push({name:e,repeatable:n,optional:c});let u=l||xt;if(u!==xt){s+=Z.BonusCustomRegExp;try{RegExp(`(${u})`)}catch(t){throw Error(`Invalid custom RegExp for param "${e}" (${u}): `+t.message)}}let d=n?`((?:${u})(?:/(?:${u}))*)`:`(${u})`;r||(d=c&&t.length<2?`(?:/${d})`:`/`+d),c&&(d+=`?`),i+=d,s+=Z.Dynamic,c&&(s+=Z.BonusOptional),n&&(s+=Z.BonusRepeatable),u===`.*`&&(s+=Z.BonusWildcard)}e.push(s)}r.push(e)}if(n.strict&&n.end){let e=r.length-1;r[e][r[e].length-1]+=Z.BonusStrict}n.strict||(i+=`/?`),n.end?i+=`$`:n.strict&&!i.endsWith(`/`)&&(i+=`(?:/|$)`);let o=new RegExp(i,n.sensitive?``:`i`);function s(e){let t=e.match(o),n={};if(!t)return null;for(let e=1;e<t.length;e++){let r=t[e]||``,i=a[e-1];n[i.name]=r&&i.repeatable?r.split(`/`):r}return n}function c(t){let n=``,r=!1;for(let i of e){(!r||!n.endsWith(`/`))&&(n+=`/`),r=!1;for(let e of i)if(e.type===Y.Static)n+=e.value;else if(e.type===Y.Param){let{value:a,repeatable:o,optional:s}=e,c=a in t?t[a]:``;if(P(c)&&!o)throw Error(`Provided param "${a}" is an array but it is not repeatable (* or + modifiers)`);let l=P(c)?c.join(`/`):c;if(!l)if(s)i.length<2&&(n.endsWith(`/`)?n=n.slice(0,-1):r=!0);else throw Error(`Missing required param "${a}"`);n+=l}}return n||`/`}return{re:o,score:r,keys:a,parse:s,stringify:c}}function Tt(e,t){let n=0;for(;n<e.length&&n<t.length;){let r=t[n]-e[n];if(r)return r;n++}return e.length<t.length?e.length===1&&e[0]===Z.Static+Z.Segment?-1:1:e.length>t.length?t.length===1&&t[0]===Z.Static+Z.Segment?1:-1:0}function Et(e,t){let n=0,r=e.score,i=t.score;for(;n<r.length&&n<i.length;){let e=Tt(r[n],i[n]);if(e)return e;n++}if(Math.abs(i.length-r.length)===1){if(Dt(r))return 1;if(Dt(i))return-1}return i.length-r.length}function Dt(e){let t=e[e.length-1];return e.length>0&&t[t.length-1]<0}var Ot={strict:!1,end:!0,sensitive:!1};function kt(e,t,n){let r=M(wt(bt(e.path),n),{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function At(e,t){let n=[],r=new Map;t=F(Ot,t);function i(e){return r.get(e)}function a(e,n,r){let i=!r,s=Mt(e);s.aliasOf=r&&r.record;let l=F(t,e),u=[s];if(`alias`in e){let t=typeof e.alias==`string`?[e.alias]:e.alias;for(let e of t)u.push(Mt(M({},s,{components:r?r.record.components:s.components,path:e,aliasOf:r?r.record:s})))}let d,f;for(let t of u){let{path:u}=t;if(n&&u[0]!==`/`){let e=n.record.path,r=e[e.length-1]===`/`?``:`/`;t.path=n.record.path+(u&&r+u)}if(d=kt(t,n,l),r?r.alias.push(d):(f||=d,f!==d&&f.alias.push(d),i&&e.name&&!Pt(d)&&o(e.name)),Rt(d)&&c(d),s.children){let e=s.children;for(let t=0;t<e.length;t++)a(e[t],d,r&&r.children[t])}r||=d}return f?()=>{o(f)}:N}function o(e){if(ot(e)){let t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(o),t.alias.forEach(o))}else{let t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(o),e.alias.forEach(o))}}function s(){return n}function c(e){let t=It(e,n);n.splice(t,0,e),e.record.name&&!Pt(e)&&r.set(e.record.name,e)}function l(e,t){let i,a={},o,s;if(`name`in e&&e.name){if(i=r.get(e.name),!i)throw R(I.MATCHER_NOT_FOUND,{location:e});s=i.record.name,a=M(jt(t.params,i.keys.filter(e=>!e.optional).concat(i.parent?i.parent.keys.filter(e=>e.optional):[]).map(e=>e.name)),e.params&&jt(e.params,i.keys.map(e=>e.name))),o=i.stringify(a)}else if(e.path!=null)o=e.path,i=n.find(e=>e.re.test(o)),i&&(a=i.parse(o),s=i.record.name);else{if(i=t.name?r.get(t.name):n.find(e=>e.re.test(t.path)),!i)throw R(I.MATCHER_NOT_FOUND,{location:e,currentLocation:t});s=i.record.name,a=M({},t.params,e.params),o=i.stringify(a)}let c=[],l=i;for(;l;)c.unshift(l.record),l=l.parent;return{name:s,path:o,params:a,matched:c,meta:Ft(c)}}e.forEach(e=>a(e));function u(){n.length=0,r.clear()}return{addRoute:a,resolve:l,removeRoute:o,clearRoutes:u,getRoutes:s,getRecordMatcher:i}}function jt(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Mt(e){let t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Nt(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:`components`in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,`mods`,{value:{}}),t}function Nt(e){let t={},n=e.props||!1;if(`component`in e)t.default=n;else for(let r in e.components)t[r]=typeof n==`object`?n[r]:n;return t}function Pt(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Ft(e){return e.reduce((e,t)=>M(e,t.meta),{})}function It(e,t){let n=0,r=t.length;for(;n!==r;){let i=n+r>>1;Et(e,t[i])<0?r=i:n=i+1}let i=Lt(e);return i&&(r=t.lastIndexOf(i,r-1)),r}function Lt(e){let t=e;for(;t=t.parent;)if(Rt(t)&&Et(e,t)===0)return t}function Rt({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function zt(e){let t=_(V),n=_(he),r=h(()=>{let n=m(e.to);return t.resolve(n)}),i=h(()=>{let{matched:e}=r.value,{length:t}=e,i=e[t-1],a=n.matched;if(!i||!a.length)return-1;let o=a.findIndex(U.bind(null,i));if(o>-1)return o;let s=Wt(e[t-2]);return t>1&&Wt(i)===s&&a[a.length-1].path!==s?a.findIndex(U.bind(null,e[t-2])):o}),a=h(()=>i.value>-1&&Ut(n.params,r.value.params)),o=h(()=>i.value>-1&&i.value===n.matched.length-1&&Ge(n.params,r.value.params));function s(n={}){if(Ht(n)){let n=t[m(e.replace)?`replace`:`push`](m(e.to)).catch(N);return e.viewTransition&&typeof document<`u`&&`startViewTransition`in document&&document.startViewTransition(()=>n),n}return Promise.resolve()}return{route:r,href:h(()=>r.value.href),isActive:a,isExactActive:o,navigate:s}}function Bt(e){return e.length===1?e[0]:e}var Vt=v({name:`RouterLink`,compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:`page`},viewTransition:Boolean},useLink:zt,setup(e,{slots:t}){let n=d(zt(e)),{options:i}=_(V),a=h(()=>({[Gt(e.activeClass,i.linkActiveClass,`router-link-active`)]:n.isActive,[Gt(e.exactActiveClass,i.linkExactActiveClass,`router-link-exact-active`)]:n.isExactActive}));return()=>{let i=t.default&&Bt(t.default(n));return e.custom?i:r(`a`,{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:a.value},i)}}});function Ht(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){let t=e.currentTarget.getAttribute(`target`);if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Ut(e,t){for(let n in t){let r=t[n],i=e[n];if(typeof r==`string`){if(r!==i)return!1}else if(!P(i)||i.length!==r.length||r.some((e,t)=>e.valueOf()!==i[t].valueOf()))return!1}return!0}function Wt(e){return e?e.aliasOf?e.aliasOf.path:e.path:``}var Gt=(e,t,n)=>e??t??n,Kt=v({name:`RouterView`,inheritAttrs:!1,props:{name:{type:String,default:`default`},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){let i=_(ge),a=h(()=>e.route||i.value),s=_(me,0),c=h(()=>{let e=m(s),{matched:t}=a.value,n;for(;(n=t[e])&&!n.components;)e++;return e}),u=h(()=>a.value.matched[c.value]);o(me,h(()=>c.value+1)),o(B,u),o(ge,a);let d=ie();return l(()=>[d.value,u.value,e.name],([e,t,n],[r,i,a])=>{t&&(t.instances[n]=e,i&&i!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=i.leaveGuards),t.updateGuards.size||(t.updateGuards=i.updateGuards))),e&&t&&(!i||!U(t,i)||!r)&&(t.enterCallbacks[n]||[]).forEach(t=>t(e))},{flush:`post`}),()=>{let i=a.value,o=e.name,s=u.value,c=s&&s.components[o];if(!c)return qt(n.default,{Component:c,route:i});let l=s.props[o],f=r(c,M({},l?l===!0?i.params:typeof l==`function`?l(i):l:null,t,{onVnodeUnmounted:e=>{e.component.isUnmounted&&(s.instances[o]=null)},ref:d}));return qt(n.default,{Component:f,route:i})||f}}});function qt(e,t){if(!e)return null;let n=e(t);return n.length===1?n[0]:n}var Jt=Kt;function Yt(e){let t=At(e.routes,e),n=e.parseQuery||st,r=e.stringifyQuery||ct,a=e.history,o=q(),s=q(),c=q(),l=te(W),u=W;k&&e.scrollBehavior&&`scrollRestoration`in history&&(history.scrollRestoration=`manual`);let d=pe.bind(null,e=>``+e),p=pe.bind(null,Re),ee=pe.bind(null,H);function ne(e,n){let r,i;return ot(e)?(r=t.getRecordMatcher(e),i=n):i=e,t.addRoute(i,r)}function re(e){let n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function ie(){return t.getRoutes().map(e=>e.record)}function ae(e){return!!t.getRecordMatcher(e)}function h(e,i){if(i=M({},i||l.value),typeof e==`string`){let r=Ve(n,e,i.path),o=t.resolve({path:r.path},i),s=a.createHref(r.fullPath);return M(r,o,{params:ee(o.params),hash:H(r.hash),redirectedFrom:void 0,href:s})}let o;if(e.path!=null)o=M({},e,{path:Ve(n,e.path,i.path).path});else{let t=M({},e.params);for(let e in t)t[e]??delete t[e];o=M({},e,{params:p(t)}),i.params=p(i.params)}let s=t.resolve(o,i),c=e.hash||``;s.params=d(ee(s.params));let u=He(r,M({},e,{hash:Pe(c),path:s.path})),f=a.createHref(u);return M({fullPath:u,hash:c,query:r===ct?lt(e.query):e.query||{}},s,{redirectedFrom:void 0,href:f})}function g(e){return typeof e==`string`?Ve(n,e,l.value.path):M({},e)}function _(e,t){if(u!==e)return R(I.NAVIGATION_CANCELLED,{from:t,to:e})}function v(e){return y(e)}function oe(e){return v(M(g(e),{replace:!0}))}function se(e,t){let n=e.matched[e.matched.length-1];if(n&&n.redirect){let{redirect:r}=n,i=typeof r==`function`?r(e,t):r;return typeof i==`string`&&(i=i.includes(`?`)||i.includes(`#`)?i=g(i):{path:i},i.params={}),M({query:e.query,hash:e.hash,params:i.path==null?e.params:{}},i)}}function y(e,t){let n=u=h(e),i=l.value,a=e.state,o=e.force,s=e.replace===!0,c=se(n,i);if(c)return y(M(g(c),{state:typeof c==`object`?M({},a,c.state):a,force:o,replace:s}),t||n);let d=n;d.redirectedFrom=t;let f;return!o&&We(r,i,n)&&(f=R(I.NAVIGATION_DUPLICATED,{to:d,from:i}),fe(i,i,!0,!1)),(f?Promise.resolve(f):x(d,i)).catch(e=>z(e)?z(e,I.NAVIGATION_GUARD_REDIRECT)?e:O(e):D(e,d,i)).then(e=>{if(e){if(z(e,I.NAVIGATION_GUARD_REDIRECT))return y(M({replace:s},g(e.to),{state:typeof e.to==`object`?M({},a,e.to.state):a,force:o}),t||d)}else e=C(d,i,!0,s,a);return S(d,i,e),e})}function ce(e,t){let n=_(e,t);return n?Promise.reject(n):Promise.resolve()}function b(e){let t=F.values().next().value;return t&&typeof t.runWithContext==`function`?t.runWithContext(e):e()}function x(e,t){let n,[r,i,a]=dt(e,t);n=ut(r.reverse(),`beforeRouteLeave`,e,t);for(let i of r)i.leaveGuards.forEach(r=>{n.push(J(r,e,t))});let c=ce.bind(null,e,t);return n.push(c),B(n).then(()=>{n=[];for(let r of o.list())n.push(J(r,e,t));return n.push(c),B(n)}).then(()=>{n=ut(i,`beforeRouteUpdate`,e,t);for(let r of i)r.updateGuards.forEach(r=>{n.push(J(r,e,t))});return n.push(c),B(n)}).then(()=>{n=[];for(let r of a)if(r.beforeEnter)if(P(r.beforeEnter))for(let i of r.beforeEnter)n.push(J(i,e,t));else n.push(J(r.beforeEnter,e,t));return n.push(c),B(n)}).then(()=>(e.matched.forEach(e=>e.enterCallbacks={}),n=ut(a,`beforeRouteEnter`,e,t,b),n.push(c),B(n))).then(()=>{n=[];for(let r of s.list())n.push(J(r,e,t));return n.push(c),B(n)}).catch(e=>z(e,I.NAVIGATION_CANCELLED)?e:Promise.reject(e))}function S(e,t,n){c.list().forEach(r=>b(()=>r(e,t,n)))}function C(e,t,n,r,i){let o=_(e,t);if(o)return o;let s=t===W,c=k?history.state:{};n&&(r||s?a.replace(e.fullPath,M({scroll:s&&c&&c.scroll},i)):a.push(e.fullPath,i)),l.value=e,fe(e,t,n,s),O()}let w;function le(){w||=a.listen((e,t,n)=>{if(!L.listening)return;let r=h(e),i=se(r,L.currentRoute.value);if(i){y(M(i,{replace:!0,force:!0}),r).catch(N);return}u=r;let o=l.value;k&&rt(tt(o.fullPath,n.delta),K()),x(r,o).catch(e=>z(e,I.NAVIGATION_ABORTED|I.NAVIGATION_CANCELLED)?e:z(e,I.NAVIGATION_GUARD_REDIRECT)?(y(M(g(e.to),{force:!0}),r).then(e=>{z(e,I.NAVIGATION_ABORTED|I.NAVIGATION_DUPLICATED)&&!n.delta&&n.type===G.pop&&a.go(-1,!1)}).catch(N),Promise.reject()):(n.delta&&a.go(-n.delta,!1),D(e,r,o))).then(e=>{e||=C(r,o,!1),e&&(n.delta&&!z(e,I.NAVIGATION_CANCELLED)?a.go(-n.delta,!1):n.type===G.pop&&z(e,I.NAVIGATION_ABORTED|I.NAVIGATION_DUPLICATED)&&a.go(-1,!1)),S(r,o,e)}).catch(N)})}let T=q(),ue=q(),E;function D(e,t,n){O(e);let r=ue.list();return r.length?r.forEach(r=>r(e,t,n)):console.error(e),Promise.reject(e)}function de(){return E&&l.value!==W?Promise.resolve():new Promise((e,t)=>{T.add([e,t])})}function O(e){return E||(E=!e,le(),T.list().forEach(([t,n])=>e?n(e):t()),T.reset()),e}function fe(t,n,r,a){let{scrollBehavior:o}=e;if(!k||!o)return Promise.resolve();let s=!r&&it(tt(t.fullPath,0))||(a||!r)&&history.state&&history.state.scroll||null;return i().then(()=>o(t,n,s)).then(e=>e&&et(e)).catch(e=>D(e,t,n))}let A=e=>a.go(e),j,F=new Set,L={currentRoute:l,listening:!0,addRoute:ne,removeRoute:re,clearRoutes:t.clearRoutes,hasRoute:ae,getRoutes:ie,resolve:h,options:e,push:v,replace:oe,go:A,back:()=>A(-1),forward:()=>A(1),beforeEach:o.add,beforeResolve:s.add,afterEach:c.add,onError:ue.add,isReady:de,install(e){e.component(`RouterLink`,Vt),e.component(`RouterView`,Jt),e.config.globalProperties.$router=L,Object.defineProperty(e.config.globalProperties,`$route`,{enumerable:!0,get:()=>m(l)}),k&&!j&&l.value===W&&(j=!0,v(a.location).catch(e=>{}));let t={};for(let e in W)Object.defineProperty(t,e,{get:()=>l.value[e],enumerable:!0});e.provide(V,L),e.provide(he,f(t)),e.provide(ge,l);let n=e.unmount;F.add(e),e.unmount=function(){F.delete(e),F.size<1&&(u=W,w&&w(),w=null,l.value=W,j=!1,E=!1),n()}}};function B(e){return e.reduce((e,t)=>e.then(()=>b(t)),Promise.resolve())}return L}var Xt={class:`app-shell`},Zt={class:`top-header`},Qt={class:`app-main`},$t=ae({__name:`App`,setup(e){let t=ve(),r=_e(),i=[{label:`数据管理`,path:`/data-management`},{label:`数据包管理`,path:`/package-management`},{label:`模型训练`,path:`/model-training`},{label:`模型列表`,path:`/model-list`},{label:`模型评估`,path:`/model-evaluation`},{label:`实时监控`,path:`/realtime-monitor`},{label:`历史数据`,path:`/history-data`}],o=h(()=>i.find(e=>t.path.startsWith(e.path))?.path??`/data-management`),l=e=>{r.push(e)};return(e,t)=>{let r=n(`el-tab-pane`),d=n(`el-tabs`),f=n(`router-view`);return a(),ne(`div`,Xt,[p(`header`,Zt,[t[0]||=p(`div`,{class:`project-title`},`热实验温度控制系统`,-1),t[1]||=p(`div`,{class:`header-divider`},null,-1),g(d,{class:`top-tabs`,"model-value":o.value,onTabChange:l},{default:s(()=>[(a(),ne(u,null,c(i,e=>g(r,{key:e.path,label:e.label,name:e.path},null,8,[`label`,`name`])),64))]),_:1},8,[`model-value`])]),p(`main`,Qt,[g(f)])])}}},[[`__scopeId`,`data-v-ecec5b3d`]]),en=`modulepreload`,tn=function(e){return`/`+e},nn={},Q=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=tn(t,n),t in nn)return;nn[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:en,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},rn=Yt({history:_t(`/`),routes:[{path:`/`,redirect:`/data-management`},{path:`/data-management`,name:`data-management`,component:()=>Q(()=>import(`./DataManagement-CA4nqNQp.js`),__vite__mapDeps([0,1,2,3,4,5,6,7]))},{path:`/realtime-monitor`,name:`realtime-monitor`,component:()=>Q(()=>import(`./UnderConstruction-n0JcUgMW.js`),__vite__mapDeps([8,1,9])),props:{title:`实时监控`}},{path:`/history-data`,name:`history-data`,component:()=>Q(()=>import(`./UnderConstruction-n0JcUgMW.js`),__vite__mapDeps([8,1,9])),props:{title:`历史数据`}},{path:`/model-training`,name:`model-training`,component:()=>Q(()=>import(`./ModelTraining-DPxp6Y6Z.js`),__vite__mapDeps([10,1,2,11,3,12]))},{path:`/model-list`,name:`model-list`,component:()=>Q(()=>import(`./ModelList-DLDTMRtX.js`),__vite__mapDeps([13,1,2,11,3,14]))},{path:`/model-evaluation`,name:`model-evaluation`,component:()=>Q(()=>import(`./ModelEvaluation-CVr4AavX.js`),__vite__mapDeps([15,1,2,3,5,16]))},{path:`/package-management`,name:`package-management`,component:()=>Q(()=>import(`./PackageManagement-DRdx9gQd.js`),__vite__mapDeps([17,1,2,3,4,5,6,18]))}]}),$=e($t);$.use(O()),$.use(rn),$.use(t),$.mount(`#app`);
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -5,10 +5,10 @@
<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-B6mx_Oi_.js"></script>
<script type="module" crossorigin src="/assets/index-D5S_ELZm.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-D1RKUtCV.js">
<link rel="modulepreload" crossorigin href="/assets/es-DCOtnflc.js">
<link rel="stylesheet" crossorigin href="/assets/index-Bdu9N6pH.css">
<link rel="stylesheet" crossorigin href="/assets/index-BrwBxauG.css">
</head>
<body>
<div id="app"></div>
......
......@@ -6,10 +6,10 @@ const route = useRoute()
const router = useRouter()
const tabs = [
{ label: '数据管理', path: '/data-management' },
{ label: '数据包管理', path: '/package-management' },
{ label: '数据文件', path: '/data-management' },
{ label: '数据包', path: '/package-management' },
{ label: '模型训练', path: '/model-training' },
{ label: '模型列表', path: '/model-list' },
{ label: '模型', path: '/model-list' },
{ label: '模型评估', path: '/model-evaluation' },
{ label: '实时监控', path: '/realtime-monitor' },
{ label: '历史数据', path: '/history-data' },
......@@ -30,7 +30,7 @@ const handleTabChange = (path) => {
<template>
<div class="app-shell">
<header class="top-header">
<div class="project-title">验温度控制系统</div>
<div class="project-title">验温度控制系统</div>
<div class="header-divider"></div>
<el-tabs class="top-tabs" :model-value="activeTab" @tab-change="handleTabChange">
<el-tab-pane
......
......@@ -81,12 +81,15 @@ const renderChart = () => {
return ''
}
const unitMap = { '电流(A)': ' A', '电压(V)': ' V', '温度(℃)': ' ℃' }
const lines = [`<div style="margin-bottom:6px;font-weight:600;">${params[0].axisValue}</div>`]
params.forEach((item) => {
const val = Number.isFinite(item.data) ? Number(item.data).toFixed(2) : '--'
const unit = unitMap[item.seriesName] ?? ''
lines.push(
`<div style="display:flex;align-items:center;gap:6px;min-width:160px;justify-content:space-between;">
<span>${item.marker}${item.seriesName}</span>
<strong>${formatValue(item.data)}</strong>
<strong>${val}${unit}</strong>
</div>`,
)
})
......@@ -103,9 +106,9 @@ const renderChart = () => {
data: ['电流(A)', '电压(V)', '温度(℃)'],
},
grid: {
top: 24,
top: 48,
left: 24,
right: 24,
right: 60,
bottom: 56,
containLabel: true,
},
......@@ -123,18 +126,26 @@ const renderChart = () => {
},
},
},
yAxis: {
type: 'value',
axisLabel: {
color: '#64748b',
},
splitLine: {
lineStyle: {
type: 'dashed',
color: 'rgba(148, 163, 184, 0.45)',
yAxis: [
{
type: 'value',
name: '温度 (℃)',
position: 'left',
nameTextStyle: { color: '#64748b', fontSize: 11 },
axisLabel: { color: '#64748b', formatter: '{value} ℃' },
splitLine: {
lineStyle: { type: 'dashed', color: 'rgba(148, 163, 184, 0.45)' },
},
},
},
{
type: 'value',
name: '电流 / 电压',
position: 'right',
nameTextStyle: { color: '#64748b', fontSize: 11 },
axisLabel: { color: '#64748b' },
splitLine: { show: false },
},
],
series: [
{
name: '电流(A)',
......@@ -142,6 +153,7 @@ const renderChart = () => {
smooth: true,
symbol: 'circle',
symbolSize: 7,
yAxisIndex: 1,
data: getSeriesData('current'),
},
{
......@@ -150,6 +162,7 @@ const renderChart = () => {
smooth: true,
symbol: 'circle',
symbolSize: 7,
yAxisIndex: 1,
data: getSeriesData('voltage'),
},
{
......@@ -158,6 +171,7 @@ const renderChart = () => {
smooth: true,
symbol: 'circle',
symbolSize: 7,
yAxisIndex: 0,
data: getSeriesData('actual_temperature'),
},
],
......
......@@ -23,7 +23,6 @@ const form = reactive({
epochs: 50,
batch_size: 32,
learning_rate: 0.001,
train_ratio: 0.8,
},
})
......@@ -152,15 +151,15 @@ const formatParams = (params) => {
`轮数 ${params.epochs}`,
`批次 ${params.batch_size}`,
`学习率 ${params.learning_rate}`,
`训练比 ${params.train_ratio}`,
].join(' / ')
}
const formatLoss = (task) => {
if (task.train_loss == null) return '-'
const train = `训练: ${Number(task.train_loss).toFixed(5)}`
const val = task.val_loss != null ? ` / 验证: ${Number(task.val_loss).toFixed(5)}` : ''
return train + val
const parts = [`训练: ${Number(task.train_loss).toFixed(5)}`]
if (task.val_loss != null) parts.push(`验证: ${Number(task.val_loss).toFixed(5)}`)
if (task.test_loss != null) parts.push(`测试: ${Number(task.test_loss).toFixed(5)}`)
return parts.join(' / ')
}
onMounted(async () => {
......@@ -279,17 +278,6 @@ onBeforeUnmount(stopPolling)
style="width: 100%"
/>
</el-form-item>
<el-form-item label="训练集比例">
<el-input-number
v-model="form.params.train_ratio"
:min="0.5"
:max="0.99"
:step="0.05"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</div>
</div>
......
......@@ -45,6 +45,30 @@ const form = reactive({
remark: '',
})
// ── clean rules ───────────────────────────────────────────────────────────────
const cleanRules = reactive({
enabled: false,
current_min: null,
current_max: null,
voltage_min: null,
voltage_max: null,
temperature_min: null,
temperature_max: null,
})
const cleanRulesPayload = computed(() => {
if (!cleanRules.enabled) return null
return {
enabled: true,
current_min: cleanRules.current_min ?? null,
current_max: cleanRules.current_max ?? null,
voltage_min: cleanRules.voltage_min ?? null,
voltage_max: cleanRules.voltage_max ?? null,
temperature_min: cleanRules.temperature_min ?? null,
temperature_max: cleanRules.temperature_max ?? null,
}
})
// ── preview ───────────────────────────────────────────────────────────────────
const previewLoading = ref(false)
const previewRecords = ref([])
......@@ -62,7 +86,10 @@ const triggerPreview = () => {
previewDebounceTimer = setTimeout(async () => {
previewLoading.value = true
try {
const result = await previewPackage({ file_ids: selectedFileIds.value }, { limit: 300 })
const result = await previewPackage(
{ file_ids: selectedFileIds.value, clean_rules: cleanRulesPayload.value },
{ limit: 300 },
)
previewRecords.value = result.records
previewTotal.value = result.count
} finally {
......@@ -72,6 +99,7 @@ const triggerPreview = () => {
}
watch(selectedFileIds, triggerPreview, { deep: true })
watch(cleanRules, triggerPreview, { deep: true })
// ── save ──────────────────────────────────────────────────────────────────────
const saving = ref(false)
......@@ -93,6 +121,7 @@ const handleGenerate = async () => {
category_id: form.categoryId || null,
remark: form.remark.trim() || null,
file_ids: selectedFileIds.value,
clean_rules: cleanRulesPayload.value,
})
ElMessage.success('数据包创建成功')
emit('saved')
......@@ -165,6 +194,67 @@ onMounted(async () => {
<el-form-item label="数据包名称" required>
<el-input v-model="form.name" maxlength="100" show-word-limit placeholder="请输入数据包名称" />
</el-form-item>
<el-form-item label="清洗规则">
<div class="clean-rules-wrap">
<el-checkbox v-model="cleanRules.enabled">野值清理</el-checkbox>
<div v-if="cleanRules.enabled" class="clean-range-grid">
<div class="clean-range-row">
<span class="clean-range-label">电流范围 (A)</span>
<el-input-number
v-model="cleanRules.current_min"
:controls="false"
:value-on-clear="null"
placeholder="最小值"
class="range-input"
/>
<span class="range-sep">~</span>
<el-input-number
v-model="cleanRules.current_max"
:controls="false"
:value-on-clear="null"
placeholder="最大值"
class="range-input"
/>
</div>
<div class="clean-range-row">
<span class="clean-range-label">电压范围 (V)</span>
<el-input-number
v-model="cleanRules.voltage_min"
:controls="false"
:value-on-clear="null"
placeholder="最小值"
class="range-input"
/>
<span class="range-sep">~</span>
<el-input-number
v-model="cleanRules.voltage_max"
:controls="false"
:value-on-clear="null"
placeholder="最大值"
class="range-input"
/>
</div>
<div class="clean-range-row">
<span class="clean-range-label">温度范围 (℃)</span>
<el-input-number
v-model="cleanRules.temperature_min"
:controls="false"
:value-on-clear="null"
placeholder="最小值"
class="range-input"
/>
<span class="range-sep">~</span>
<el-input-number
v-model="cleanRules.temperature_max"
:controls="false"
:value-on-clear="null"
placeholder="最大值"
class="range-input"
/>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
......@@ -323,4 +413,41 @@ onMounted(async () => {
margin-bottom: 16px;
}
}
</style>
.clean-rules-wrap {
width: 100%;
}
.clean-range-grid {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.clean-range-row {
display: flex;
align-items: center;
gap: 6px;
}
.clean-range-label {
font-size: 12px;
color: #475569;
width: 86px;
flex-shrink: 0;
}
.range-input {
width: 88px;
:deep(.el-input__inner) {
text-align: center;
}
}
.range-sep {
font-size: 13px;
color: #94a3b8;
}
</style>
\ No newline at end of file
......@@ -8,7 +8,6 @@ import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
server: {
proxy: {
......
......@@ -42,6 +42,9 @@ CREATE TABLE IF NOT EXISTS data_packages (
INDEX idx_pkg_category (category_id)
) COMMENT='数据包表';
ALTER TABLE data_packages
ADD COLUMN IF NOT EXISTS clean_rules JSON NULL COMMENT '野值清洗规则';
CREATE TABLE IF NOT EXISTS data_package_files (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
package_id BIGINT NOT NULL COMMENT '数据包ID',
......@@ -50,3 +53,9 @@ CREATE TABLE IF NOT EXISTS data_package_files (
INDEX idx_dpf_package (package_id),
INDEX idx_dpf_file (file_id)
) COMMENT='数据包文件关联表';
ALTER TABLE train_tasks
ADD COLUMN IF NOT EXISTS test_loss FLOAT NULL COMMENT '测试集损失';
ALTER TABLE saved_models
ADD COLUMN IF NOT EXISTS test_loss FLOAT NULL COMMENT '测试集损失';
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