Commit 3faede64 authored by luwei's avatar luwei

功能修改

parent 478c4876
......@@ -100,6 +100,20 @@ def delete_file(file_id: str):
raise HTTPException(status_code=400, detail=str(error)) from error
@router.get('/quality-config')
def get_quality_config():
return success_response(data=service.get_quality_config())
@router.get('/files/{file_id}/quality')
def get_file_quality(file_id: str):
try:
result = service.assess_file_quality(file_id=file_id)
return success_response(data=result)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
@router.get('/files/{file_id}/records')
def get_file_records(file_id: str, limit: int = Query(default=500, ge=1, le=5000)):
try:
......
......@@ -73,18 +73,30 @@ class PackageCreateRequest(BaseModel):
remark: str | None = Field(default=None)
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
row_start: int | None = Field(default=None, ge=1)
row_end: int | None = Field(default=None, ge=1)
class PreviewRequest(BaseModel):
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
row_start: int | None = Field(default=None, ge=1)
row_end: int | None = Field(default=None, ge=1)
@router.post('/preview')
def preview_package(request: PreviewRequest, limit: int = Query(default=300, ge=1, le=2000)):
try:
if request.row_start and request.row_end and request.row_start > request.row_end:
raise ValueError('起始行不能大于结束行')
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)
result = service.preview_records(
file_ids=request.file_ids,
limit=limit,
clean_rules=clean_rules,
row_start=request.row_start,
row_end=request.row_end,
)
return success_response(data=result)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
......@@ -101,6 +113,8 @@ def list_packages(
@router.post('')
def create_package(request: PackageCreateRequest):
try:
if request.row_start and request.row_end and request.row_start > request.row_end:
raise ValueError('起始行不能大于结束行')
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(
......@@ -109,6 +123,8 @@ def create_package(request: PackageCreateRequest):
remark=request.remark,
file_ids=request.file_ids,
clean_rules=clean_rules,
row_start=request.row_start,
row_end=request.row_end,
)
return success_response(data=pkg, message='数据包创建成功')
except ValueError as error:
......
from app.models.data_management import Category, DataFile, DataPackage, DataPackageFile
from app.models.data_management import Category, DataFile, DataPackage, DataPackageFile, DataQualityConfig
from app.models.eval_management import EvalRecord
from app.models.train_management import SavedModel, TrainTask
__all__ = ['Category', 'DataFile', 'DataPackage', 'DataPackageFile', 'TrainTask', 'SavedModel', 'EvalRecord']
__all__ = ['Category', 'DataFile', 'DataPackage', 'DataPackageFile', 'DataQualityConfig', 'TrainTask', 'SavedModel', 'EvalRecord']
from sqlalchemy import BIGINT, TIMESTAMP, Enum, Index, Integer, JSON, String, Text, text
from sqlalchemy import BIGINT, TIMESTAMP, Double, Enum, Index, Integer, JSON, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
......@@ -59,6 +59,8 @@ class DataPackage(Base):
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='野值清洗规则')
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, 含)')
created_at: Mapped[str] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
updated_at: Mapped[str] = mapped_column(
TIMESTAMP,
......@@ -79,3 +81,24 @@ class DataPackageFile(Base):
package_id: Mapped[int] = mapped_column(BIGINT, nullable=False, comment='数据包ID')
file_id: Mapped[int] = mapped_column(BIGINT, nullable=False, comment='数据文件ID')
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text('0'), comment='排序')
class DataQualityConfig(Base):
__tablename__ = 'data_quality_config'
__table_args__ = (
{'mysql_comment': '数据质量准确性配置表'},
)
id: Mapped[int] = mapped_column(BIGINT, primary_key=True, autoincrement=True)
field_name: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, comment='字段名')
label: Mapped[str] = mapped_column(String(100), nullable=False, comment='显示名称')
unit: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("''"), comment='单位')
min_value: Mapped[float | None] = mapped_column(Double, nullable=True, comment='合规最小值')
max_value: Mapped[float | None] = mapped_column(Double, nullable=True, comment='合规最大值')
created_at: Mapped[str] = mapped_column(TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP'))
updated_at: Mapped[str] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
)
......@@ -9,7 +9,7 @@ from openpyxl import Workbook, load_workbook
from sqlalchemy import func
from app.database import db_session
from app.models import Category, DataFile
from app.models import Category, DataFile, DataQualityConfig
class DataManagementService:
......@@ -436,3 +436,186 @@ class DataManagementService:
workbook.save(output)
workbook.close()
return output.getvalue()
# ── quality ───────────────────────────────────────────────────────────────
def get_quality_config(self) -> list[dict[str, Any]]:
with db_session() as session:
rows = session.query(DataQualityConfig).order_by(DataQualityConfig.id.asc()).all()
return [
{
'id': item.id,
'field_name': item.field_name,
'label': item.label,
'unit': item.unit,
'min_value': item.min_value,
'max_value': item.max_value,
}
for item in rows
]
def assess_file_quality(self, file_id: str) -> dict[str, Any]:
file_db_id = self._parse_int_id(file_id, '文件ID')
with db_session() as session:
file_meta = session.query(DataFile).filter(DataFile.id == file_db_id).first()
if not file_meta:
raise ValueError('文件不存在')
target_path = self._resolve_local_file_path(file_meta.file_path, file_meta.stored_name)
if not target_path.exists():
raise ValueError('文件已丢失,请重新上传')
quality_config = self._get_quality_config_map()
all_rows = self._read_all_excel_rows(target_path)
if not all_rows:
return self._empty_quality_result()
first_row = [item.strip() for item in all_rows[0]]
has_header = any(
self._normalize_header(item) in {'time', 'current', 'voltage', 'set_temperature', 'actual_temperature'}
for item in first_row
)
if has_header:
header = first_row
data_rows = all_rows[1:]
else:
header = ['time', 'current', 'voltage', 'set_temperature', 'actual_temperature']
data_rows = all_rows
index_map = self._build_index_map(header)
total = len(data_rows)
if total == 0:
return self._empty_quality_result()
measurement_fields = ['current', 'voltage', 'set_temperature', 'actual_temperature']
complete_count = 0
continuous_count = 0
accurate_count = 0
has_measurement_count = 0
for row in data_rows:
field_values: dict[str, float | None] = {}
for field in measurement_fields:
val_str = self._read_value(row, index_map[field])
field_values[field] = self._to_float(val_str)
any_present = any(v is not None for v in field_values.values())
all_present = all(v is not None for v in field_values.values())
if any_present:
continuous_count += 1
has_measurement_count += 1
in_range = True
for field, val in field_values.items():
if val is None:
continue
cfg = quality_config.get(field, {})
min_val = cfg.get('min_value')
max_val = cfg.get('max_value')
if min_val is not None and val < min_val:
in_range = False
break
if max_val is not None and val > max_val:
in_range = False
break
if in_range:
accurate_count += 1
if all_present:
complete_count += 1
completeness = round(complete_count / total * 100, 2) if total > 0 else 0.0
continuity = round(continuous_count / total * 100, 2) if total > 0 else 0.0
accuracy = round(accurate_count / has_measurement_count * 100, 2) if has_measurement_count > 0 else 0.0
return {
'total': total,
'complete_count': complete_count,
'continuous_count': continuous_count,
'accurate_count': accurate_count,
'has_measurement_count': has_measurement_count,
'completeness': completeness,
'continuity': continuity,
'accuracy': accuracy,
'quality_config': list(quality_config.values()),
}
def _get_quality_config_map(self) -> dict[str, dict[str, Any]]:
with db_session() as session:
rows = session.query(DataQualityConfig).all()
return {
item.field_name: {
'field_name': item.field_name,
'label': item.label,
'unit': item.unit,
'min_value': item.min_value,
'max_value': item.max_value,
}
for item in rows
}
@staticmethod
def _empty_quality_result() -> dict[str, Any]:
return {
'total': 0,
'complete_count': 0,
'continuous_count': 0,
'accurate_count': 0,
'has_measurement_count': 0,
'completeness': 0.0,
'continuity': 0.0,
'accuracy': 0.0,
'quality_config': [],
}
def _read_all_excel_rows(self, file_path: Path) -> list[list[str]]:
"""Read all rows including fully-empty rows (for continuity check)."""
suffix = file_path.suffix.lower()
if suffix == '.xlsx':
return self._read_xlsx_rows_all(file_path)
if suffix == '.xls':
return self._read_xls_rows_all(file_path)
if suffix == '.csv':
return self._read_csv_rows_all(file_path)
return []
def _read_xlsx_rows_all(self, file_path: Path) -> list[list[str]]:
workbook = load_workbook(file_path, data_only=True, read_only=True)
try:
sheet = workbook.active
rows: list[list[str]] = []
for row in sheet.iter_rows(values_only=True):
cells = [self._cell_to_text(cell) for cell in row]
rows.append(cells)
return rows
finally:
workbook.close()
def _read_xls_rows_all(self, file_path: Path) -> list[list[str]]:
workbook = xlrd.open_workbook(file_path)
sheet = workbook.sheet_by_index(0)
rows: list[list[str]] = []
for row_index in range(sheet.nrows):
cells = [self._cell_to_text(sheet.cell_value(row_index, col_index)) for col_index in range(sheet.ncols)]
rows.append(cells)
return rows
def _read_csv_rows_all(self, file_path: Path) -> list[list[str]]:
encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312']
for encoding in encodings:
try:
with file_path.open('r', encoding=encoding, newline='') as file:
reader = csv.reader(file)
rows: list[list[str]] = []
for row in reader:
cells = [self._cell_to_text(cell) for cell in row]
rows.append(cells)
return rows
except UnicodeDecodeError:
continue
raise ValueError('CSV 文件编码无法识别,请保存为 UTF-8 或 GBK 后重试')
......@@ -105,6 +105,8 @@ class EvalService:
def _load_package_records(self, package_id: int) -> list[dict[str, Any]]:
with db_session() as session:
pkg = session.query(DataPackage).filter(DataPackage.id == package_id).first()
clean_rules = pkg.clean_rules if pkg else None
pf_rows = (
session.query(DataPackageFile)
.filter(DataPackageFile.package_id == package_id)
......@@ -126,8 +128,45 @@ class EvalService:
recs, _ = self._dm._read_records(path, limit=None)
all_records.extend(recs)
if clean_rules and clean_rules.get('enabled'):
all_records = self._apply_clean_rules(all_records, clean_rules)
return all_records
@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: list[dict[str, Any]] = []
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
@staticmethod
def _record_to_dict(row: EvalRecord, include_chart: bool = True) -> dict[str, Any]:
d: dict[str, Any] = {
......
......@@ -154,11 +154,15 @@ class PackageManagementService:
remark: str | None,
file_ids: list[int],
clean_rules: dict | None = None,
row_start: int | None = None,
row_end: int | None = None,
) -> dict[str, Any]:
if not name:
raise ValueError('数据包名称不能为空')
if not file_ids:
raise ValueError('请至少选择一个数据文件')
if row_start is not None and row_end is not None and row_start > row_end:
raise ValueError('起始行不能大于结束行')
cat_db_id: int | None = None
if category_id and str(category_id).strip().lower() not in {'', 'all', 'none', 'null'}:
......@@ -180,8 +184,13 @@ class PackageManagementService:
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)
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:
......@@ -194,6 +203,8 @@ class PackageManagementService:
remark=remark,
data_count=total_count,
clean_rules=stored_rules,
row_start=row_start,
row_end=row_end,
)
session.add(pkg)
session.flush()
......@@ -225,6 +236,8 @@ class PackageManagementService:
raise ValueError('数据包不存在')
stored_clean_rules = pkg.clean_rules
stored_row_start = pkg.row_start
stored_row_end = pkg.row_end
pf_rows = (
session.query(DataPackageFile)
......@@ -236,13 +249,19 @@ 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, clean_rules=stored_clean_rules)
return self._merge_records(
file_ids, file_map, limit,
clean_rules=stored_clean_rules,
row_start=stored_row_start, row_end=stored_row_end,
)
def preview_records(
self,
file_ids: list[int],
limit: int = 300,
clean_rules: dict | None = None,
row_start: int | None = None,
row_end: int | None = None,
) -> dict[str, Any]:
if not file_ids:
return {'records': [], 'count': 0}
......@@ -251,7 +270,11 @@ 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, clean_rules=clean_rules)
return self._merge_records(
file_ids, file_map, limit,
clean_rules=clean_rules,
row_start=row_start, row_end=row_end,
)
# ── helpers ──────────────────────────────────────────────────────────────
......@@ -261,8 +284,12 @@ class PackageManagementService:
file_map: dict[int, Any],
limit: int | None,
clean_rules: dict | None = None,
row_start: int | None = None,
row_end: int | None = None,
) -> dict[str, Any]:
use_filter = bool(clean_rules and clean_rules.get('enabled'))
use_range = row_start is not None or row_end is not None
need_full_read = use_filter or use_range
all_records: list[dict[str, Any]] = []
total_count = 0
remaining = limit
......@@ -272,22 +299,28 @@ class PackageManagementService:
continue
file_meta = file_map[fid]
path = self._dm._resolve_local_file_path(file_meta.file_path, file_meta.stored_name)
if use_filter:
# Read all rows so filter can be applied correctly
if need_full_read:
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)
if not use_filter and remaining is not None:
if not need_full_read 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 use_range:
# row_start/row_end are 1-based and inclusive
start_idx = (row_start - 1) if row_start and row_start >= 1 else 0
end_idx = row_end if row_end else len(all_records)
all_records = all_records[start_idx:end_idx]
total_count = len(all_records)
if limit is not None and limit > 0:
return {'records': all_records[:limit], 'count': total_count}
......@@ -334,6 +367,8 @@ class PackageManagementService:
'category_id': pkg.category_id,
'remark': pkg.remark,
'data_count': pkg.data_count,
'row_start': pkg.row_start,
'row_end': pkg.row_end,
'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 '',
}
......
.data-page[data-v-7089f2ac]{background:var(--bg-page);height:100%}.data-page.dragging[data-v-7089f2ac]{-webkit-user-select:none;user-select:none;cursor:col-resize}.data-layout[data-v-7089f2ac]{background:var(--bg-white);border:1px solid var(--border-color);height:100%;box-shadow:var(--shadow-card);align-items:stretch;gap:0;display:flex}.pane-divider[data-v-7089f2ac]{background:var(--border-color);cursor:col-resize;flex-shrink:0;width:4px;transition:background .15s;position:relative}.pane-divider[data-v-7089f2ac]:after{content:"";position:absolute;inset:0 -2px}.pane-divider[data-v-7089f2ac]:hover,.dragging .pane-divider[data-v-7089f2ac]{background:var(--primary-border)}.pane-card[data-v-7089f2ac]{border:none;border-right:1px solid var(--border-color);background:var(--bg-white);border-radius:0;min-width:0;height:100%;box-shadow:none!important}.pane-card[data-v-7089f2ac] .el-card__header{border-bottom:1px solid var(--border-color);background:#f7f8fa;align-items:center;height:44px;padding:0 16px;display:flex}.pane-card[data-v-7089f2ac] .el-card__body{flex-direction:column;gap:10px;height:calc(100% - 44px);padding:12px 16px;display:flex}.pane-card--tree[data-v-7089f2ac] .el-card__body{padding:8px 0;overflow-y:auto}.pane-header[data-v-7089f2ac]{width:100%;color:var(--text-primary);justify-content:space-between;align-items:center;font-size:14px;font-weight:600;display:flex}.file-title[data-v-7089f2ac]{color:var(--text-secondary);margin-left:4px;font-size:13px;font-weight:400}.tree-node[data-v-7089f2ac]{justify-content:space-between;align-items:center;gap:8px;width:100%;padding-right:4px;display:flex}.tree-main[data-v-7089f2ac]{align-items:center;gap:6px;min-width:0;display:inline-flex}.tree-icon[data-v-7089f2ac]{color:var(--primary);flex:none;font-size:14px}.tree-name[data-v-7089f2ac]{color:var(--text-primary);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.tree-actions[data-v-7089f2ac]{flex-shrink:0;display:none}.tree-actions .el-button[data-v-7089f2ac]{color:var(--text-tertiary);padding:0 3px;font-size:13px}.tree-actions .el-button[data-v-7089f2ac]:hover{color:var(--primary)}.tree-node:hover .tree-actions[data-v-7089f2ac]{display:inline-flex}.upload-panel[data-v-7089f2ac]{width:100%}.upload-panel__icon[data-v-7089f2ac]{color:var(--primary);margin-bottom:8px;font-size:28px}.search-form[data-v-7089f2ac]{flex-shrink:0;margin-bottom:4px}.content-wrap[data-v-7089f2ac]{flex:1;min-height:0}@media (width<=980px){.data-layout[data-v-7089f2ac]{display:block;overflow-y:auto}.pane-divider[data-v-7089f2ac]{display:none}.pane-card[data-v-7089f2ac]{border-right:none;border-bottom:1px solid var(--border-color);min-height:420px;margin-bottom:12px;width:100%!important}}
import{n as e,r as t,u as n}from"./es-DCOtnflc.js";import{$ as r,A as i,B as a,D as o,Ht as s,I as c,N as l,Q as u,R as d,V as f,Vt as p,X as m,_ as h,a as g,at as _,d as v,f as y,ht as b,m as x,p as S,st as C,t as w,u as T,v as E}from"./_plugin-vue_export-helper-D1RKUtCV.js";import{t as D}from"./request-D8DwihSV.js";import{t as O}from"./echarts-B4btcaVd.js";function k(){return D.get(`/eval/packages`)}function A(){return D.get(`/eval/models`)}function j(e){return D.post(`/eval/run`,e)}function M(){return D.get(`/eval/records`)}function N(e){return D.get(`/eval/records/${e}`)}function P(e){return D.delete(`/eval/records/${e}`)}var F={__name:`EvalChart`,props:{chartData:{type:Array,default:()=>[]},height:{type:String,default:`360px`}},setup(e){let t=e,n=C(null),r=null,a=T(()=>Array.isArray(t.chartData)?t.chartData.filter(e=>e!=null):[]),s=T(()=>a.value.map(e=>e.time||String(e.index??``))),u=T(()=>a.value.map(e=>e.actual??null)),d=T(()=>a.value.map(e=>e.predicted??null)),f=()=>{if(!n.value)return;r||=O(n.value);let e=a.value.length>0;r.setOption({animation:!1,color:[`#409EFF`,`#F56C6C`],tooltip:{trigger:`axis`,backgroundColor:`rgba(255,255,255,0.96)`,borderColor:`#e2e8f0`,borderWidth:1,textStyle:{color:`#334155`},extraCssText:`box-shadow: 0 8px 24px rgba(15,23,42,0.14); border-radius: 10px;`,formatter(e){if(!e?.length)return``;let t=[`<div style="margin-bottom:6px;font-weight:600;font-size:12px;">${e[0].axisValue}</div>`];return e.forEach(e=>{let n=e.data==null?`--`:Number(e.data).toFixed(4);t.push(`<div style="display:flex;align-items:center;gap:8px;min-width:180px;justify-content:space-between;">
<span>${e.marker}${e.seriesName}</span>
<strong>${n} ℃</strong>
</div>`)}),t.join(``)}},legend:{bottom:4,itemWidth:20,itemHeight:10,textStyle:{color:`#475569`,fontSize:12},data:[`实际温度 (℃)`,`预测温度 (℃)`]},grid:{top:16,left:16,right:20,bottom:52,containLabel:!0},xAxis:{type:`category`,boundaryGap:!1,data:s.value,axisLabel:{color:`#64748b`,fontSize:11,rotate:35,interval:Math.max(0,Math.floor(s.value.length/12)-1),hideOverlap:!0},axisLine:{lineStyle:{color:`#cbd5e1`}}},yAxis:{type:`value`,name:`温度 (℃)`,nameTextStyle:{color:`#64748b`,fontSize:11},axisLabel:{color:`#64748b`,fontSize:11},splitLine:{lineStyle:{type:`dashed`,color:`rgba(148,163,184,0.4)`}}},series:[{name:`实际温度 (℃)`,type:`line`,smooth:!1,symbol:`none`,lineStyle:{width:2,type:`solid`},data:u.value},{name:`预测温度 (℃)`,type:`line`,smooth:!1,symbol:`none`,lineStyle:{width:2,type:`dashed`},data:d.value}],graphic:e?[]:[{type:`text`,left:`center`,top:`middle`,style:{text:`暂无评估数据`,fill:`#94a3b8`,fontSize:14}}],dataZoom:e?[{type:`inside`,start:0,end:100},{type:`slider`,start:0,end:100,height:20,bottom:28}]:[]},!0)},h=()=>r?.resize();return m(()=>t.chartData,async()=>{await o(),f()},{deep:!0}),l(async()=>{await o(),f(),window.addEventListener(`resize`,h)}),i(()=>{window.removeEventListener(`resize`,h),r?.dispose(),r=null}),(e,r)=>(c(),x(`div`,{ref_key:`chartRef`,ref:n,style:p({width:`100%`,height:t.height})},null,4))}},ee={class:`eval-page`},te={key:0,class:`eval-loading`},ne={class:`metrics-row`},re={class:`metric-chip`},ie={class:`metric-value`},ae={class:`metric-chip`},oe={class:`metric-value`},I={class:`metric-chip`},L={class:`metric-value`},R={class:`metric-chip`},z={class:`metric-value`},B={class:`metric-chip`},V={class:`metric-value`},H={class:`card-header-row`},U={class:`metric-cell`},W={class:`metric-cell`},G={style:{"min-height":`120px`}},K={class:`metrics-row`,style:{"margin-bottom":`12px`}},se={class:`metric-chip`},ce={class:`metric-value`},le={class:`metric-chip`},ue={class:`metric-value`},de={class:`metric-chip`},fe={class:`metric-value`},q={class:`metric-chip`},pe={class:`metric-value`},J=w({__name:`index`,setup(i){let o=C([]),p=C([]),m=_({model_id:``,package_id:``}),w=C(!1),T=C(null),D=C([]),O=C(!1),J=C(!1),Y=C(null),X=C(!1),Z=async()=>{let[e,t]=await Promise.all([k(),A()]);o.value=e,p.value=t},Q=async()=>{O.value=!0;try{D.value=await M()}finally{O.value=!1}},me=async()=>{if(!m.model_id){t.warning(`请选择模型`);return}if(!m.package_id){t.warning(`请选择数据包`);return}w.value=!0,T.value=null;try{T.value=await j({model_id:m.model_id,package_id:m.package_id}),t.success(`评估完成`),await Q()}catch(e){t.error(e?.message||`评估失败`)}finally{w.value=!1}},he=async e=>{X.value=!0,J.value=!0,Y.value=null;try{Y.value=await N(e.id)}finally{X.value=!1}},ge=async n=>{try{await e.confirm(`确定删除模型"${n.model_name}"在"${n.package_name}"上的评估记录吗?`,`提示`,{type:`warning`}),await P(n.id),t.success(`已删除`),T.value?.id===n.id&&(T.value=null),await Q()}catch{}},$=e=>e==null?`-`:Number(e).toFixed(5);return l(async()=>{await Promise.all([Z(),Q()])}),(e,t)=>{let i=a(`el-option`),l=a(`el-select`),_=a(`el-form-item`),C=a(`el-button`),k=a(`el-form`),A=a(`el-icon`),j=a(`el-card`),M=a(`el-table-column`),N=a(`el-table`),P=a(`el-dialog`),Z=f(`loading`);return c(),x(`div`,ee,[E(j,{class:`config-card`,shadow:`hover`},{header:u(()=>[...t[4]||=[v(`span`,{class:`card-title`},`评估配置`,-1)]]),default:u(()=>[E(k,{model:m,inline:``,class:`eval-form`},{default:u(()=>[E(_,{label:`选择模型`,required:``},{default:u(()=>[E(l,{modelValue:m.model_id,"onUpdate:modelValue":t[0]||=e=>m.model_id=e,placeholder:`请选择已保存模型`,filterable:``,style:{width:`260px`}},{default:u(()=>[(c(!0),x(g,null,d(p.value,e=>(c(),y(i,{key:e.id,label:`${e.model_name}(训练包:${e.package_name})`,value:e.id},null,8,[`label`,`value`]))),128))]),_:1},8,[`modelValue`])]),_:1}),E(_,{label:`选择数据包`,required:``},{default:u(()=>[E(l,{modelValue:m.package_id,"onUpdate:modelValue":t[1]||=e=>m.package_id=e,placeholder:`请选择评估数据包`,filterable:``,style:{width:`240px`}},{default:u(()=>[(c(!0),x(g,null,d(o.value,e=>(c(),y(i,{key:e.id,label:`${e.name}${e.data_count} 条)`,value:e.id},null,8,[`label`,`value`]))),128))]),_:1},8,[`modelValue`])]),_:1}),E(_,null,{default:u(()=>[E(C,{type:`primary`,loading:w.value,onClick:me},{default:u(()=>[...t[5]||=[h(` 开始评估 `,-1)]]),_:1},8,[`loading`])]),_:1})]),_:1},8,[`model`]),w.value?(c(),x(`div`,te,[E(A,{class:`is-loading`,style:{"font-size":`24px`,color:`#409EFF`}},{default:u(()=>[E(b(n))]),_:1}),t[6]||=v(`span`,null,`正在推理,请稍候…`,-1)])):T.value?(c(),x(g,{key:1},[v(`div`,ne,[v(`div`,re,[t[7]||=v(`span`,{class:`metric-label`},`评估样本`,-1),v(`span`,ie,s(T.value.total_count)+` 条`,1)]),v(`div`,ae,[t[8]||=v(`span`,{class:`metric-label`},`MAE`,-1),v(`span`,oe,s($(T.value.mae))+` ℃`,1)]),v(`div`,I,[t[9]||=v(`span`,{class:`metric-label`},`RMSE`,-1),v(`span`,L,s($(T.value.rmse))+` ℃`,1)]),v(`div`,R,[t[10]||=v(`span`,{class:`metric-label`},`模型`,-1),v(`span`,z,s(T.value.model_name),1)]),v(`div`,B,[t[11]||=v(`span`,{class:`metric-label`},`数据包`,-1),v(`span`,V,s(T.value.package_name),1)])]),E(F,{"chart-data":T.value.chart_data,height:`340px`},null,8,[`chart-data`])],64)):S(``,!0)]),_:1}),E(j,{class:`records-card`,shadow:`hover`},{header:u(()=>[v(`div`,H,[t[13]||=v(`span`,{class:`card-title`},`评估记录`,-1),E(C,{icon:b(n),size:`small`,plain:``,loading:O.value,onClick:Q},{default:u(()=>[...t[12]||=[h(` 刷新 `,-1)]]),_:1},8,[`icon`,`loading`])])]),default:u(()=>[r((c(),y(N,{data:D.value,border:``,stripe:``,height:`calc(100vh - 580px)`},{default:u(()=>[E(M,{prop:`model_name`,label:`模型名称`,"min-width":`140`,"show-overflow-tooltip":``}),E(M,{prop:`package_name`,label:`评估数据包`,"min-width":`130`,"show-overflow-tooltip":``}),E(M,{label:`样本数`,width:`90`,align:`center`},{default:u(({row:e})=>[h(s(e.total_count),1)]),_:1}),E(M,{label:`MAE (℃)`,width:`110`,align:`center`},{default:u(({row:e})=>[v(`span`,U,s($(e.mae)),1)]),_:1}),E(M,{label:`RMSE (℃)`,width:`110`,align:`center`},{default:u(({row:e})=>[v(`span`,W,s($(e.rmse)),1)]),_:1}),E(M,{prop:`created_at`,label:`评估时间`,width:`165`}),E(M,{label:`操作`,width:`120`,fixed:`right`,align:`center`},{default:u(({row:e})=>[E(C,{link:``,type:`primary`,onClick:t=>he(e)},{default:u(()=>[...t[14]||=[h(`查看`,-1)]]),_:1},8,[`onClick`]),E(C,{link:``,type:`danger`,onClick:t=>ge(e)},{default:u(()=>[...t[15]||=[h(`删除`,-1)]]),_:1},8,[`onClick`])]),_:1})]),_:1},8,[`data`])),[[Z,O.value]])]),_:1}),E(P,{modelValue:J.value,"onUpdate:modelValue":t[3]||=e=>J.value=e,title:Y.value?`${Y.value.model_name}${Y.value.package_name}`:`评估详情`,width:`860px`,"destroy-on-close":``},{footer:u(()=>[E(C,{onClick:t[2]||=e=>J.value=!1},{default:u(()=>[...t[20]||=[h(`关闭`,-1)]]),_:1})]),default:u(()=>[r((c(),x(`div`,G,[Y.value?(c(),x(g,{key:0},[v(`div`,K,[v(`div`,se,[t[16]||=v(`span`,{class:`metric-label`},`样本数`,-1),v(`span`,ce,s(Y.value.total_count),1)]),v(`div`,le,[t[17]||=v(`span`,{class:`metric-label`},`MAE`,-1),v(`span`,ue,s($(Y.value.mae))+` ℃`,1)]),v(`div`,de,[t[18]||=v(`span`,{class:`metric-label`},`RMSE`,-1),v(`span`,fe,s($(Y.value.rmse))+` ℃`,1)]),v(`div`,q,[t[19]||=v(`span`,{class:`metric-label`},`评估时间`,-1),v(`span`,pe,s(Y.value.created_at),1)])]),E(F,{"chart-data":Y.value.chart_data||[],height:`380px`},null,8,[`chart-data`])],64)):S(``,!0)])),[[Z,X.value]])]),_:1},8,[`modelValue`,`title`])])}}},[[`__scopeId`,`data-v-ec2828d6`]]);export{J as default};
\ No newline at end of file
import{n as e,r as t,u as n}from"./es-DCOtnflc.js";import{$ as r,B as i,Ht as a,I as o,N as s,Q as c,V as l,_ as u,d,f,ht as p,m,p as h,st as g,t as _,v}from"./_plugin-vue_export-helper-D1RKUtCV.js";import{a as y,r as b}from"./trainManagement-TFX-G3Jl.js";var x={class:`model-list-page`},S={class:`card-header-row`},C={class:`params-text`},w={class:`loss-text`},T=_({__name:`index`,setup(_){let T=g([]),E=g(!1),D=async()=>{E.value=!0;try{T.value=await y()}finally{E.value=!1}},O=async n=>{try{await e.confirm(`确定删除模型"${n.model_name}"吗?删除后无法恢复。`,`提示`,{type:`warning`}),await b(n.id),t.success(`模型已删除`),await D()}catch{}},k=e=>e?[`seq=${e.seq_len}`,`hidden=${e.hidden_size}`,`layers=${e.num_layers}`,`epochs=${e.epochs}`,`lr=${e.learning_rate}`].join(` / `):`-`,A=e=>e.train_loss==null?`-`:`训练 ${Number(e.train_loss).toFixed(5)}`+(e.val_loss==null?``:` / 验证 ${Number(e.val_loss).toFixed(5)}`);return s(D),(e,t)=>{let s=i(`el-button`),g=i(`el-table-column`),_=i(`el-tooltip`),y=i(`el-table`),b=i(`el-empty`),j=i(`el-card`),M=l(`loading`);return o(),m(`div`,x,[v(j,{shadow:`hover`,class:`list-card`},{header:c(()=>[d(`div`,S,[t[1]||=d(`span`,{class:`card-title`},`已保存模型`,-1),v(s,{icon:p(n),size:`small`,plain:``,loading:E.value,onClick:D},{default:c(()=>[...t[0]||=[u(` 刷新 `,-1)]]),_:1},8,[`icon`,`loading`])])]),default:c(()=>[r((o(),f(y,{data:T.value,border:``,stripe:``,height:`calc(100vh - 160px)`},{default:c(()=>[v(g,{type:`index`,width:`55`,label:`#`,align:`center`}),v(g,{prop:`model_name`,label:`模型名称`,"min-width":`150`,"show-overflow-tooltip":``}),v(g,{prop:`package_name`,label:`训练数据包`,"min-width":`140`,"show-overflow-tooltip":``}),v(g,{label:`LSTM 参数`,"min-width":`280`,"show-overflow-tooltip":``},{default:c(({row:e})=>[v(_,{content:k(e.params),placement:`top`},{default:c(()=>[d(`span`,C,a(k(e.params)),1)]),_:2},1032,[`content`])]),_:1}),v(g,{label:`训练损失`,"min-width":`190`,"show-overflow-tooltip":``},{default:c(({row:e})=>[d(`span`,w,a(A(e)),1)]),_:1}),v(g,{prop:`created_at`,label:`保存时间`,width:`170`}),v(g,{label:`操作`,width:`90`,fixed:`right`,align:`center`},{default:c(({row:e})=>[v(s,{link:``,type:`danger`,onClick:t=>O(e)},{default:c(()=>[...t[2]||=[u(`删除`,-1)]]),_:1},8,[`onClick`])]),_:1})]),_:1},8,[`data`])),[[M,E.value]]),!E.value&&T.value.length===0?(o(),f(b,{key:0,description:`暂无已保存的模型,请先在模型训练页面完成训练并保存`,style:{padding:`40px 0`}})):h(``,!0)]),_:1})])}}},[[`__scopeId`,`data-v-c204d828`]]);export{T as default};
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
import{g as e,r as t}from"./es-DCOtnflc.js";function n(e,t){return function(){return e.apply(t,arguments)}}var{toString:r}=Object.prototype,{getPrototypeOf:i}=Object,{iterator:a,toStringTag:o}=Symbol,s=(e=>t=>{let n=r.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),c=e=>(e=e.toLowerCase(),t=>s(t)===e),l=e=>t=>typeof t===e,{isArray:u}=Array,d=l(`undefined`);function f(e){return e!==null&&!d(e)&&e.constructor!==null&&!d(e.constructor)&&g(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}var p=c(`ArrayBuffer`);function m(e){let t;return t=typeof ArrayBuffer<`u`&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&p(e.buffer),t}var h=l(`string`),g=l(`function`),_=l(`number`),v=e=>typeof e==`object`&&!!e,y=e=>e===!0||e===!1,b=e=>{if(s(e)!==`object`)return!1;let t=i(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(o in e)&&!(a in e)},ee=e=>{if(!v(e)||f(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},x=c(`Date`),S=c(`File`),C=e=>!!(e&&e.uri!==void 0),te=e=>e&&e.getParts!==void 0,ne=c(`Blob`),re=c(`FileList`),ie=e=>v(e)&&g(e.pipe);function ae(){return typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:typeof global<`u`?global:{}}var oe=ae(),se=oe.FormData===void 0?void 0:oe.FormData,ce=e=>{let t;return e&&(se&&e instanceof se||g(e.append)&&((t=s(e))===`formdata`||t===`object`&&g(e.toString)&&e.toString()===`[object FormData]`))},le=c(`URLSearchParams`),[ue,de,fe,pe]=[`ReadableStream`,`Request`,`Response`,`Headers`].map(c),me=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,``);function w(e,t,{allOwnKeys:n=!1}={}){if(e==null)return;let r,i;if(typeof e!=`object`&&(e=[e]),u(e))for(r=0,i=e.length;r<i;r++)t.call(null,e[r],r,e);else{if(f(e))return;let i=n?Object.getOwnPropertyNames(e):Object.keys(e),a=i.length,o;for(r=0;r<a;r++)o=i[r],t.call(null,e[o],o,e)}}function he(e,t){if(f(e))return null;t=t.toLowerCase();let n=Object.keys(e),r=n.length,i;for(;r-- >0;)if(i=n[r],t===i.toLowerCase())return i;return null}var T=typeof globalThis<`u`?globalThis:typeof self<`u`?self:typeof window<`u`?window:global,ge=e=>!d(e)&&e!==T;function E(){let{caseless:e,skipUndefined:t}=ge(this)&&this||{},n={},r=(r,i)=>{if(i===`__proto__`||i===`constructor`||i===`prototype`)return;let a=e&&he(n,i)||i;b(n[a])&&b(r)?n[a]=E(n[a],r):b(r)?n[a]=E({},r):u(r)?n[a]=r.slice():(!t||!d(r))&&(n[a]=r)};for(let e=0,t=arguments.length;e<t;e++)arguments[e]&&w(arguments[e],r);return n}var _e=(e,t,r,{allOwnKeys:i}={})=>(w(t,(t,i)=>{r&&g(t)?Object.defineProperty(e,i,{value:n(t,r),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,i,{value:t,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:i}),e),ve=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),ye=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,`constructor`,{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,`super`,{value:t.prototype}),n&&Object.assign(e.prototype,n)},be=(e,t,n,r)=>{let a,o,s,c={};if(t||={},e==null)return t;do{for(a=Object.getOwnPropertyNames(e),o=a.length;o-- >0;)s=a[o],(!r||r(s,e,t))&&!c[s]&&(t[s]=e[s],c[s]=!0);e=n!==!1&&i(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},xe=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;let r=e.indexOf(t,n);return r!==-1&&r===n},Se=e=>{if(!e)return null;if(u(e))return e;let t=e.length;if(!_(t))return null;let n=Array(t);for(;t-- >0;)n[t]=e[t];return n},Ce=(e=>t=>e&&t instanceof e)(typeof Uint8Array<`u`&&i(Uint8Array)),we=(e,t)=>{let n=(e&&e[a]).call(e),r;for(;(r=n.next())&&!r.done;){let n=r.value;t.call(e,n[0],n[1])}},Te=(e,t)=>{let n,r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},Ee=c(`HTMLFormElement`),De=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(e,t,n){return t.toUpperCase()+n}),Oe=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),ke=c(`RegExp`),Ae=(e,t)=>{let n=Object.getOwnPropertyDescriptors(e),r={};w(n,(n,i)=>{let a;(a=t(n,i,e))!==!1&&(r[i]=a||n)}),Object.defineProperties(e,r)},je=e=>{Ae(e,(t,n)=>{if(g(e)&&[`arguments`,`caller`,`callee`].indexOf(n)!==-1)return!1;let r=e[n];if(g(r)){if(t.enumerable=!1,`writable`in t){t.writable=!1;return}t.set||=()=>{throw Error(`Can not rewrite read-only method '`+n+`'`)}}})},Me=(e,t)=>{let n={},r=e=>{e.forEach(e=>{n[e]=!0})};return u(e)?r(e):r(String(e).split(t)),n},Ne=()=>{},Pe=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function Fe(e){return!!(e&&g(e.append)&&e[o]===`FormData`&&e[a])}var Ie=e=>{let t=Array(10),n=(e,r)=>{if(v(e)){if(t.indexOf(e)>=0)return;if(f(e))return e;if(!(`toJSON`in e)){t[r]=e;let i=u(e)?[]:{};return w(e,(e,t)=>{let a=n(e,r+1);!d(a)&&(i[t]=a)}),t[r]=void 0,i}}return e};return n(e,0)},Le=c(`AsyncFunction`),Re=e=>e&&(v(e)||g(e))&&g(e.then)&&g(e.catch),ze=((e,t)=>e?setImmediate:t?((e,t)=>(T.addEventListener(`message`,({source:n,data:r})=>{n===T&&r===e&&t.length&&t.shift()()},!1),n=>{t.push(n),T.postMessage(e,`*`)}))(`axios@${Math.random()}`,[]):e=>setTimeout(e))(typeof setImmediate==`function`,g(T.postMessage)),D={isArray:u,isArrayBuffer:p,isBuffer:f,isFormData:ce,isArrayBufferView:m,isString:h,isNumber:_,isBoolean:y,isObject:v,isPlainObject:b,isEmptyObject:ee,isReadableStream:ue,isRequest:de,isResponse:fe,isHeaders:pe,isUndefined:d,isDate:x,isFile:S,isReactNativeBlob:C,isReactNative:te,isBlob:ne,isRegExp:ke,isFunction:g,isStream:ie,isURLSearchParams:le,isTypedArray:Ce,isFileList:re,forEach:w,merge:E,extend:_e,trim:me,stripBOM:ve,inherits:ye,toFlatObject:be,kindOf:s,kindOfTest:c,endsWith:xe,toArray:Se,forEachEntry:we,matchAll:Te,isHTMLForm:Ee,hasOwnProperty:Oe,hasOwnProp:Oe,reduceDescriptors:Ae,freezeMethods:je,toObjectSet:Me,toCamelCase:De,noop:Ne,toFiniteNumber:Pe,findKey:he,global:T,isContextDefined:ge,isSpecCompliantForm:Fe,toJSONObject:Ie,isAsyncFn:Le,isThenable:Re,setImmediate:ze,asap:typeof queueMicrotask<`u`?queueMicrotask.bind(T):typeof process<`u`&&process.nextTick||ze,isIterable:e=>e!=null&&g(e[a])},O=class e extends Error{static from(t,n,r,i,a,o){let s=new e(t.message,n||t.code,r,i,a);return s.cause=t,s.name=t.name,t.status!=null&&s.status==null&&(s.status=t.status),o&&Object.assign(s,o),s}constructor(e,t,n,r,i){super(e),Object.defineProperty(this,`message`,{value:e,enumerable:!0,writable:!0,configurable:!0}),this.name=`AxiosError`,this.isAxiosError=!0,t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),i&&(this.response=i,this.status=i.status)}toJSON(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:D.toJSONObject(this.config),code:this.code,status:this.status}}};O.ERR_BAD_OPTION_VALUE=`ERR_BAD_OPTION_VALUE`,O.ERR_BAD_OPTION=`ERR_BAD_OPTION`,O.ECONNABORTED=`ECONNABORTED`,O.ETIMEDOUT=`ETIMEDOUT`,O.ERR_NETWORK=`ERR_NETWORK`,O.ERR_FR_TOO_MANY_REDIRECTS=`ERR_FR_TOO_MANY_REDIRECTS`,O.ERR_DEPRECATED=`ERR_DEPRECATED`,O.ERR_BAD_RESPONSE=`ERR_BAD_RESPONSE`,O.ERR_BAD_REQUEST=`ERR_BAD_REQUEST`,O.ERR_CANCELED=`ERR_CANCELED`,O.ERR_NOT_SUPPORT=`ERR_NOT_SUPPORT`,O.ERR_INVALID_URL=`ERR_INVALID_URL`;function k(e){return D.isPlainObject(e)||D.isArray(e)}function Be(e){return D.endsWith(e,`[]`)?e.slice(0,-2):e}function A(e,t,n){return e?e.concat(t).map(function(e,t){return e=Be(e),!n&&t?`[`+e+`]`:e}).join(n?`.`:``):t}function Ve(e){return D.isArray(e)&&!e.some(k)}var He=D.toFlatObject(D,{},null,function(e){return/^is[A-Z]/.test(e)});function j(e,t,n){if(!D.isObject(e))throw TypeError(`target must be an object`);t||=new FormData,n=D.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(e,t){return!D.isUndefined(t[e])});let r=n.metaTokens,i=n.visitor||l,a=n.dots,o=n.indexes,s=(n.Blob||typeof Blob<`u`&&Blob)&&D.isSpecCompliantForm(t);if(!D.isFunction(i))throw TypeError(`visitor must be a function`);function c(e){if(e===null)return``;if(D.isDate(e))return e.toISOString();if(D.isBoolean(e))return e.toString();if(!s&&D.isBlob(e))throw new O(`Blob is not supported. Use a Buffer instead.`);return D.isArrayBuffer(e)||D.isTypedArray(e)?s&&typeof Blob==`function`?new Blob([e]):Buffer.from(e):e}function l(e,n,i){let s=e;if(D.isReactNative(t)&&D.isReactNativeBlob(e))return t.append(A(i,n,a),c(e)),!1;if(e&&!i&&typeof e==`object`){if(D.endsWith(n,`{}`))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(D.isArray(e)&&Ve(e)||(D.isFileList(e)||D.endsWith(n,`[]`))&&(s=D.toArray(e)))return n=Be(n),s.forEach(function(e,r){!(D.isUndefined(e)||e===null)&&t.append(o===!0?A([n],r,a):o===null?n:n+`[]`,c(e))}),!1}return k(e)?!0:(t.append(A(i,n,a),c(e)),!1)}let u=[],d=Object.assign(He,{defaultVisitor:l,convertValue:c,isVisitable:k});function f(e,n){if(!D.isUndefined(e)){if(u.indexOf(e)!==-1)throw Error(`Circular reference detected in `+n.join(`.`));u.push(e),D.forEach(e,function(e,r){(!(D.isUndefined(e)||e===null)&&i.call(t,e,D.isString(r)?r.trim():r,n,d))===!0&&f(e,n?n.concat(r):[r])}),u.pop()}}if(!D.isObject(e))throw TypeError(`data must be an object`);return f(e),t}function Ue(e){let t={"!":`%21`,"'":`%27`,"(":`%28`,")":`%29`,"~":`%7E`,"%20":`+`,"%00":`\0`};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(e){return t[e]})}function M(e,t){this._pairs=[],e&&j(e,this,t)}var We=M.prototype;We.append=function(e,t){this._pairs.push([e,t])},We.toString=function(e){let t=e?function(t){return e.call(this,t,Ue)}:Ue;return this._pairs.map(function(e){return t(e[0])+`=`+t(e[1])},``).join(`&`)};function Ge(e){return encodeURIComponent(e).replace(/%3A/gi,`:`).replace(/%24/g,`$`).replace(/%2C/gi,`,`).replace(/%20/g,`+`)}function Ke(e,t,n){if(!t)return e;let r=n&&n.encode||Ge,i=D.isFunction(n)?{serialize:n}:n,a=i&&i.serialize,o;if(o=a?a(t,i):D.isURLSearchParams(t)?t.toString():new M(t,i).toString(r),o){let t=e.indexOf(`#`);t!==-1&&(e=e.slice(0,t)),e+=(e.indexOf(`?`)===-1?`?`:`&`)+o}return e}var qe=class{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:n?n.synchronous:!1,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&=[]}forEach(e){D.forEach(this.handlers,function(t){t!==null&&e(t)})}},N={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0},Je={isBrowser:!0,classes:{URLSearchParams:typeof URLSearchParams<`u`?URLSearchParams:M,FormData:typeof FormData<`u`?FormData:null,Blob:typeof Blob<`u`?Blob:null},protocols:[`http`,`https`,`file`,`blob`,`url`,`data`]},Ye=e({hasBrowserEnv:()=>P,hasStandardBrowserEnv:()=>Xe,hasStandardBrowserWebWorkerEnv:()=>Ze,navigator:()=>F,origin:()=>Qe}),P=typeof window<`u`&&typeof document<`u`,F=typeof navigator==`object`&&navigator||void 0,Xe=P&&(!F||[`ReactNative`,`NativeScript`,`NS`].indexOf(F.product)<0),Ze=typeof WorkerGlobalScope<`u`&&self instanceof WorkerGlobalScope&&typeof self.importScripts==`function`,Qe=P&&window.location.href||`http://localhost`,I={...Ye,...Je};function $e(e,t){return j(e,new I.classes.URLSearchParams,{visitor:function(e,t,n,r){return I.isNode&&D.isBuffer(e)?(this.append(t,e.toString(`base64`)),!1):r.defaultVisitor.apply(this,arguments)},...t})}function et(e){return D.matchAll(/\w+|\[(\w*)]/g,e).map(e=>e[0]===`[]`?``:e[1]||e[0])}function tt(e){let t={},n=Object.keys(e),r,i=n.length,a;for(r=0;r<i;r++)a=n[r],t[a]=e[a];return t}function nt(e){function t(e,n,r,i){let a=e[i++];if(a===`__proto__`)return!0;let o=Number.isFinite(+a),s=i>=e.length;return a=!a&&D.isArray(r)?r.length:a,s?(D.hasOwnProp(r,a)?r[a]=[r[a],n]:r[a]=n,!o):((!r[a]||!D.isObject(r[a]))&&(r[a]=[]),t(e,n,r[a],i)&&D.isArray(r[a])&&(r[a]=tt(r[a])),!o)}if(D.isFormData(e)&&D.isFunction(e.entries)){let n={};return D.forEachEntry(e,(e,r)=>{t(et(e),r,n,0)}),n}return null}function rt(e,t,n){if(D.isString(e))try{return(t||JSON.parse)(e),D.trim(e)}catch(e){if(e.name!==`SyntaxError`)throw e}return(n||JSON.stringify)(e)}var L={transitional:N,adapter:[`xhr`,`http`,`fetch`],transformRequest:[function(e,t){let n=t.getContentType()||``,r=n.indexOf(`application/json`)>-1,i=D.isObject(e);if(i&&D.isHTMLForm(e)&&(e=new FormData(e)),D.isFormData(e))return r?JSON.stringify(nt(e)):e;if(D.isArrayBuffer(e)||D.isBuffer(e)||D.isStream(e)||D.isFile(e)||D.isBlob(e)||D.isReadableStream(e))return e;if(D.isArrayBufferView(e))return e.buffer;if(D.isURLSearchParams(e))return t.setContentType(`application/x-www-form-urlencoded;charset=utf-8`,!1),e.toString();let a;if(i){if(n.indexOf(`application/x-www-form-urlencoded`)>-1)return $e(e,this.formSerializer).toString();if((a=D.isFileList(e))||n.indexOf(`multipart/form-data`)>-1){let t=this.env&&this.env.FormData;return j(a?{"files[]":e}:e,t&&new t,this.formSerializer)}}return i||r?(t.setContentType(`application/json`,!1),rt(e)):e}],transformResponse:[function(e){let t=this.transitional||L.transitional,n=t&&t.forcedJSONParsing,r=this.responseType===`json`;if(D.isResponse(e)||D.isReadableStream(e))return e;if(e&&D.isString(e)&&(n&&!this.responseType||r)){let n=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e,this.parseReviver)}catch(e){if(n)throw e.name===`SyntaxError`?O.from(e,O.ERR_BAD_RESPONSE,this,null,this.response):e}}return e}],timeout:0,xsrfCookieName:`XSRF-TOKEN`,xsrfHeaderName:`X-XSRF-TOKEN`,maxContentLength:-1,maxBodyLength:-1,env:{FormData:I.classes.FormData,Blob:I.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:`application/json, text/plain, */*`,"Content-Type":void 0}}};D.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`],e=>{L.headers[e]={}});var it=D.toObjectSet([`age`,`authorization`,`content-length`,`content-type`,`etag`,`expires`,`from`,`host`,`if-modified-since`,`if-unmodified-since`,`last-modified`,`location`,`max-forwards`,`proxy-authorization`,`referer`,`retry-after`,`user-agent`]),at=e=>{let t={},n,r,i;return e&&e.split(`
`).forEach(function(e){i=e.indexOf(`:`),n=e.substring(0,i).trim().toLowerCase(),r=e.substring(i+1).trim(),!(!n||t[n]&&it[n])&&(n===`set-cookie`?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+`, `+r:r)}),t},ot=Symbol(`internals`),st=e=>!/[\r\n]/.test(e);function ct(e,t){if(!(e===!1||e==null)){if(D.isArray(e)){e.forEach(e=>ct(e,t));return}if(!st(String(e)))throw Error(`Invalid character in header content ["${t}"]`)}}function R(e){return e&&String(e).trim().toLowerCase()}function lt(e){let t=e.length;for(;t>0;){let n=e.charCodeAt(t-1);if(n!==10&&n!==13)break;--t}return t===e.length?e:e.slice(0,t)}function z(e){return e===!1||e==null?e:D.isArray(e)?e.map(z):lt(String(e))}function ut(e){let t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g,r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}var dt=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function B(e,t,n,r,i){if(D.isFunction(r))return r.call(this,t,n);if(i&&(t=n),D.isString(t)){if(D.isString(r))return t.indexOf(r)!==-1;if(D.isRegExp(r))return r.test(t)}}function ft(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,t,n)=>t.toUpperCase()+n)}function pt(e,t){let n=D.toCamelCase(` `+t);[`get`,`set`,`has`].forEach(r=>{Object.defineProperty(e,r+n,{value:function(e,n,i){return this[r].call(this,t,e,n,i)},configurable:!0})})}var V=class{constructor(e){e&&this.set(e)}set(e,t,n){let r=this;function i(e,t,n){let i=R(t);if(!i)throw Error(`header name must be a non-empty string`);let a=D.findKey(r,i);(!a||r[a]===void 0||n===!0||n===void 0&&r[a]!==!1)&&(ct(e,t),r[a||t]=z(e))}let a=(e,t)=>D.forEach(e,(e,n)=>i(e,n,t));if(D.isPlainObject(e)||e instanceof this.constructor)a(e,t);else if(D.isString(e)&&(e=e.trim())&&!dt(e))a(at(e),t);else if(D.isObject(e)&&D.isIterable(e)){let n={},r,i;for(let t of e){if(!D.isArray(t))throw TypeError(`Object iterator must return a key-value pair`);n[i=t[0]]=(r=n[i])?D.isArray(r)?[...r,t[1]]:[r,t[1]]:t[1]}a(n,t)}else e!=null&&i(t,e,n);return this}get(e,t){if(e=R(e),e){let n=D.findKey(this,e);if(n){let e=this[n];if(!t)return e;if(t===!0)return ut(e);if(D.isFunction(t))return t.call(this,e,n);if(D.isRegExp(t))return t.exec(e);throw TypeError(`parser must be boolean|regexp|function`)}}}has(e,t){if(e=R(e),e){let n=D.findKey(this,e);return!!(n&&this[n]!==void 0&&(!t||B(this,this[n],n,t)))}return!1}delete(e,t){let n=this,r=!1;function i(e){if(e=R(e),e){let i=D.findKey(n,e);i&&(!t||B(n,n[i],i,t))&&(delete n[i],r=!0)}}return D.isArray(e)?e.forEach(i):i(e),r}clear(e){let t=Object.keys(this),n=t.length,r=!1;for(;n--;){let i=t[n];(!e||B(this,this[i],i,e,!0))&&(delete this[i],r=!0)}return r}normalize(e){let t=this,n={};return D.forEach(this,(r,i)=>{let a=D.findKey(n,i);if(a){t[a]=z(r),delete t[i];return}let o=e?ft(i):String(i).trim();o!==i&&delete t[i],t[o]=z(r),n[o]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){let t=Object.create(null);return D.forEach(this,(n,r)=>{n!=null&&n!==!1&&(t[r]=e&&D.isArray(n)?n.join(`, `):n)}),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([e,t])=>e+`: `+t).join(`
`)}getSetCookie(){return this.get(`set-cookie`)||[]}get[Symbol.toStringTag](){return`AxiosHeaders`}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){let n=new this(e);return t.forEach(e=>n.set(e)),n}static accessor(e){let t=(this[ot]=this[ot]={accessors:{}}).accessors,n=this.prototype;function r(e){let r=R(e);t[r]||(pt(n,e),t[r]=!0)}return D.isArray(e)?e.forEach(r):r(e),this}};V.accessor([`Content-Type`,`Content-Length`,`Accept`,`Accept-Encoding`,`User-Agent`,`Authorization`]),D.reduceDescriptors(V.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}}),D.freezeMethods(V);function H(e,t){let n=this||L,r=t||n,i=V.from(r.headers),a=r.data;return D.forEach(e,function(e){a=e.call(n,a,i.normalize(),t?t.status:void 0)}),i.normalize(),a}function mt(e){return!!(e&&e.__CANCEL__)}var U=class extends O{constructor(e,t,n){super(e??`canceled`,O.ERR_CANCELED,t,n),this.name=`CanceledError`,this.__CANCEL__=!0}};function ht(e,t,n){let r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new O(`Request failed with status code `+n.status,[O.ERR_BAD_REQUEST,O.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function gt(e){let t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||``}function _t(e,t){e||=10;let n=Array(e),r=Array(e),i=0,a=0,o;return t=t===void 0?1e3:t,function(s){let c=Date.now(),l=r[a];o||=c,n[i]=s,r[i]=c;let u=a,d=0;for(;u!==i;)d+=n[u++],u%=e;if(i=(i+1)%e,i===a&&(a=(a+1)%e),c-o<t)return;let f=l&&c-l;return f?Math.round(d*1e3/f):void 0}}function vt(e,t){let n=0,r=1e3/t,i,a,o=(t,r=Date.now())=>{n=r,i=null,a&&=(clearTimeout(a),null),e(...t)};return[(...e)=>{let t=Date.now(),s=t-n;s>=r?o(e,t):(i=e,a||=setTimeout(()=>{a=null,o(i)},r-s))},()=>i&&o(i)]}var W=(e,t,n=3)=>{let r=0,i=_t(50,250);return vt(n=>{let a=n.loaded,o=n.lengthComputable?n.total:void 0,s=a-r,c=i(s),l=a<=o;r=a,e({loaded:a,total:o,progress:o?a/o:void 0,bytes:s,rate:c||void 0,estimated:c&&o&&l?(o-a)/c:void 0,event:n,lengthComputable:o!=null,[t?`download`:`upload`]:!0})},n)},yt=(e,t)=>{let n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},bt=e=>(...t)=>D.asap(()=>e(...t)),xt=I.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,I.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(I.origin),I.navigator&&/(msie|trident)/i.test(I.navigator.userAgent)):()=>!0,St=I.hasStandardBrowserEnv?{write(e,t,n,r,i,a,o){if(typeof document>`u`)return;let s=[`${e}=${encodeURIComponent(t)}`];D.isNumber(n)&&s.push(`expires=${new Date(n).toUTCString()}`),D.isString(r)&&s.push(`path=${r}`),D.isString(i)&&s.push(`domain=${i}`),a===!0&&s.push(`secure`),D.isString(o)&&s.push(`SameSite=${o}`),document.cookie=s.join(`; `)},read(e){if(typeof document>`u`)return null;let t=document.cookie.match(RegExp(`(?:^|; )`+e+`=([^;]*)`));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,``,Date.now()-864e5,`/`)}}:{write(){},read(){return null},remove(){}};function Ct(e){return typeof e==`string`?/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e):!1}function wt(e,t){return t?e.replace(/\/?\/$/,``)+`/`+t.replace(/^\/+/,``):e}function Tt(e,t,n){let r=!Ct(t);return e&&(r||n==0)?wt(e,t):t}var Et=e=>e instanceof V?{...e}:e;function G(e,t){t||={};let n={};function r(e,t,n,r){return D.isPlainObject(e)&&D.isPlainObject(t)?D.merge.call({caseless:r},e,t):D.isPlainObject(t)?D.merge({},t):D.isArray(t)?t.slice():t}function i(e,t,n,i){if(!D.isUndefined(t))return r(e,t,n,i);if(!D.isUndefined(e))return r(void 0,e,n,i)}function a(e,t){if(!D.isUndefined(t))return r(void 0,t)}function o(e,t){if(!D.isUndefined(t))return r(void 0,t);if(!D.isUndefined(e))return r(void 0,e)}function s(n,i,a){if(a in t)return r(n,i);if(a in e)return r(void 0,n)}let c={url:a,method:a,data:a,baseURL:o,transformRequest:o,transformResponse:o,paramsSerializer:o,timeout:o,timeoutMessage:o,withCredentials:o,withXSRFToken:o,adapter:o,responseType:o,xsrfCookieName:o,xsrfHeaderName:o,onUploadProgress:o,onDownloadProgress:o,decompress:o,maxContentLength:o,maxBodyLength:o,beforeRedirect:o,transport:o,httpAgent:o,httpsAgent:o,cancelToken:o,socketPath:o,responseEncoding:o,validateStatus:s,headers:(e,t,n)=>i(Et(e),Et(t),n,!0)};return D.forEach(Object.keys({...e,...t}),function(r){if(r===`__proto__`||r===`constructor`||r===`prototype`)return;let a=D.hasOwnProp(c,r)?c[r]:i,o=a(e[r],t[r],r);D.isUndefined(o)&&a!==s||(n[r]=o)}),n}var Dt=e=>{let t=G({},e),{data:n,withXSRFToken:r,xsrfHeaderName:i,xsrfCookieName:a,headers:o,auth:s}=t;if(t.headers=o=V.from(o),t.url=Ke(Tt(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),s&&o.set(`Authorization`,`Basic `+btoa((s.username||``)+`:`+(s.password?unescape(encodeURIComponent(s.password)):``))),D.isFormData(n)){if(I.hasStandardBrowserEnv||I.hasStandardBrowserWebWorkerEnv)o.setContentType(void 0);else if(D.isFunction(n.getHeaders)){let e=n.getHeaders(),t=[`content-type`,`content-length`];Object.entries(e).forEach(([e,n])=>{t.includes(e.toLowerCase())&&o.set(e,n)})}}if(I.hasStandardBrowserEnv&&(r&&D.isFunction(r)&&(r=r(t)),r||r!==!1&&xt(t.url))){let e=i&&a&&St.read(a);e&&o.set(i,e)}return t},Ot=typeof XMLHttpRequest<`u`&&function(e){return new Promise(function(t,n){let r=Dt(e),i=r.data,a=V.from(r.headers).normalize(),{responseType:o,onUploadProgress:s,onDownloadProgress:c}=r,l,u,d,f,p;function m(){f&&f(),p&&p(),r.cancelToken&&r.cancelToken.unsubscribe(l),r.signal&&r.signal.removeEventListener(`abort`,l)}let h=new XMLHttpRequest;h.open(r.method.toUpperCase(),r.url,!0),h.timeout=r.timeout;function g(){if(!h)return;let r=V.from(`getAllResponseHeaders`in h&&h.getAllResponseHeaders());ht(function(e){t(e),m()},function(e){n(e),m()},{data:!o||o===`text`||o===`json`?h.responseText:h.response,status:h.status,statusText:h.statusText,headers:r,config:e,request:h}),h=null}`onloadend`in h?h.onloadend=g:h.onreadystatechange=function(){!h||h.readyState!==4||h.status===0&&!(h.responseURL&&h.responseURL.indexOf(`file:`)===0)||setTimeout(g)},h.onabort=function(){h&&=(n(new O(`Request aborted`,O.ECONNABORTED,e,h)),null)},h.onerror=function(t){let r=new O(t&&t.message?t.message:`Network Error`,O.ERR_NETWORK,e,h);r.event=t||null,n(r),h=null},h.ontimeout=function(){let t=r.timeout?`timeout of `+r.timeout+`ms exceeded`:`timeout exceeded`,i=r.transitional||N;r.timeoutErrorMessage&&(t=r.timeoutErrorMessage),n(new O(t,i.clarifyTimeoutError?O.ETIMEDOUT:O.ECONNABORTED,e,h)),h=null},i===void 0&&a.setContentType(null),`setRequestHeader`in h&&D.forEach(a.toJSON(),function(e,t){h.setRequestHeader(t,e)}),D.isUndefined(r.withCredentials)||(h.withCredentials=!!r.withCredentials),o&&o!==`json`&&(h.responseType=r.responseType),c&&([d,p]=W(c,!0),h.addEventListener(`progress`,d)),s&&h.upload&&([u,f]=W(s),h.upload.addEventListener(`progress`,u),h.upload.addEventListener(`loadend`,f)),(r.cancelToken||r.signal)&&(l=t=>{h&&=(n(!t||t.type?new U(null,e,h):t),h.abort(),null)},r.cancelToken&&r.cancelToken.subscribe(l),r.signal&&(r.signal.aborted?l():r.signal.addEventListener(`abort`,l)));let _=gt(r.url);if(_&&I.protocols.indexOf(_)===-1){n(new O(`Unsupported protocol `+_+`:`,O.ERR_BAD_REQUEST,e));return}h.send(i||null)})},kt=(e,t)=>{let{length:n}=e=e?e.filter(Boolean):[];if(t||n){let n=new AbortController,r,i=function(e){if(!r){r=!0,o();let t=e instanceof Error?e:this.reason;n.abort(t instanceof O?t:new U(t instanceof Error?t.message:t))}},a=t&&setTimeout(()=>{a=null,i(new O(`timeout of ${t}ms exceeded`,O.ETIMEDOUT))},t),o=()=>{e&&=(a&&clearTimeout(a),a=null,e.forEach(e=>{e.unsubscribe?e.unsubscribe(i):e.removeEventListener(`abort`,i)}),null)};e.forEach(e=>e.addEventListener(`abort`,i));let{signal:s}=n;return s.unsubscribe=()=>D.asap(o),s}},At=function*(e,t){let n=e.byteLength;if(!t||n<t){yield e;return}let r=0,i;for(;r<n;)i=r+t,yield e.slice(r,i),r=i},jt=async function*(e,t){for await(let n of Mt(e))yield*At(n,t)},Mt=async function*(e){if(e[Symbol.asyncIterator]){yield*e;return}let t=e.getReader();try{for(;;){let{done:e,value:n}=await t.read();if(e)break;yield n}}finally{await t.cancel()}},Nt=(e,t,n,r)=>{let i=jt(e,t),a=0,o,s=e=>{o||(o=!0,r&&r(e))};return new ReadableStream({async pull(e){try{let{done:t,value:r}=await i.next();if(t){s(),e.close();return}let o=r.byteLength;n&&n(a+=o),e.enqueue(new Uint8Array(r))}catch(e){throw s(e),e}},cancel(e){return s(e),i.return()}},{highWaterMark:2})},Pt=64*1024,{isFunction:K}=D,Ft=(({Request:e,Response:t})=>({Request:e,Response:t}))(D.global),{ReadableStream:It,TextEncoder:Lt}=D.global,Rt=(e,...t)=>{try{return!!e(...t)}catch{return!1}},zt=e=>{e=D.merge.call({skipUndefined:!0},Ft,e);let{fetch:t,Request:n,Response:r}=e,i=t?K(t):typeof fetch==`function`,a=K(n),o=K(r);if(!i)return!1;let s=i&&K(It),c=i&&(typeof Lt==`function`?(e=>t=>e.encode(t))(new Lt):async e=>new Uint8Array(await new n(e).arrayBuffer())),l=a&&s&&Rt(()=>{let e=!1,t=new It,r=new n(I.origin,{body:t,method:`POST`,get duplex(){return e=!0,`half`}}).headers.has(`Content-Type`);return t.cancel(),e&&!r}),u=o&&s&&Rt(()=>D.isReadableStream(new r(``).body)),d={stream:u&&(e=>e.body)};i&&[`text`,`arrayBuffer`,`blob`,`formData`,`stream`].forEach(e=>{!d[e]&&(d[e]=(t,n)=>{let r=t&&t[e];if(r)return r.call(t);throw new O(`Response type '${e}' is not supported`,O.ERR_NOT_SUPPORT,n)})});let f=async e=>{if(e==null)return 0;if(D.isBlob(e))return e.size;if(D.isSpecCompliantForm(e))return(await new n(I.origin,{method:`POST`,body:e}).arrayBuffer()).byteLength;if(D.isArrayBufferView(e)||D.isArrayBuffer(e))return e.byteLength;if(D.isURLSearchParams(e)&&(e+=``),D.isString(e))return(await c(e)).byteLength},p=async(e,t)=>D.toFiniteNumber(e.getContentLength())??f(t);return async e=>{let{url:i,method:o,data:s,signal:c,cancelToken:f,timeout:m,onDownloadProgress:h,onUploadProgress:g,responseType:_,headers:v,withCredentials:y=`same-origin`,fetchOptions:b}=Dt(e),ee=t||fetch;_=_?(_+``).toLowerCase():`text`;let x=kt([c,f&&f.toAbortSignal()],m),S=null,C=x&&x.unsubscribe&&(()=>{x.unsubscribe()}),te;try{if(g&&l&&o!==`get`&&o!==`head`&&(te=await p(v,s))!==0){let e=new n(i,{method:`POST`,body:s,duplex:`half`}),t;if(D.isFormData(s)&&(t=e.headers.get(`content-type`))&&v.setContentType(t),e.body){let[t,n]=yt(te,W(bt(g)));s=Nt(e.body,Pt,t,n)}}D.isString(y)||(y=y?`include`:`omit`);let t=a&&`credentials`in n.prototype,c={...b,signal:x,method:o.toUpperCase(),headers:v.normalize().toJSON(),body:s,duplex:`half`,credentials:t?y:void 0};S=a&&new n(i,c);let f=await(a?ee(S,b):ee(i,c)),m=u&&(_===`stream`||_===`response`);if(u&&(h||m&&C)){let e={};[`status`,`statusText`,`headers`].forEach(t=>{e[t]=f[t]});let t=D.toFiniteNumber(f.headers.get(`content-length`)),[n,i]=h&&yt(t,W(bt(h),!0))||[];f=new r(Nt(f.body,Pt,n,()=>{i&&i(),C&&C()}),e)}_||=`text`;let ne=await d[D.findKey(d,_)||`text`](f,e);return!m&&C&&C(),await new Promise((t,n)=>{ht(t,n,{data:ne,headers:V.from(f.headers),status:f.status,statusText:f.statusText,config:e,request:S})})}catch(t){throw C&&C(),t&&t.name===`TypeError`&&/Load failed|fetch/i.test(t.message)?Object.assign(new O(`Network Error`,O.ERR_NETWORK,e,S,t&&t.response),{cause:t.cause||t}):O.from(t,t&&t.code,e,S,t&&t.response)}}},Bt=new Map,Vt=e=>{let t=e&&e.env||{},{fetch:n,Request:r,Response:i}=t,a=[r,i,n],o=a.length,s,c,l=Bt;for(;o--;)s=a[o],c=l.get(s),c===void 0&&l.set(s,c=o?new Map:zt(t)),l=c;return c};Vt();var q={http:null,xhr:Ot,fetch:{get:Vt}};D.forEach(q,(e,t)=>{if(e){try{Object.defineProperty(e,`name`,{value:t})}catch{}Object.defineProperty(e,`adapterName`,{value:t})}});var Ht=e=>`- ${e}`,Ut=e=>D.isFunction(e)||e===null||e===!1;function Wt(e,t){e=D.isArray(e)?e:[e];let{length:n}=e,r,i,a={};for(let o=0;o<n;o++){r=e[o];let n;if(i=r,!Ut(r)&&(i=q[(n=String(r)).toLowerCase()],i===void 0))throw new O(`Unknown adapter '${n}'`);if(i&&(D.isFunction(i)||(i=i.get(t))))break;a[n||`#`+o]=i}if(!i){let e=Object.entries(a).map(([e,t])=>`adapter ${e} `+(t===!1?`is not supported by the environment`:`is not available in the build`));throw new O(`There is no suitable adapter to dispatch the request `+(n?e.length>1?`since :
`+e.map(Ht).join(`
`):` `+Ht(e[0]):`as no adapter specified`),`ERR_NOT_SUPPORT`)}return i}var Gt={getAdapter:Wt,adapters:q};function J(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new U(null,e)}function Kt(e){return J(e),e.headers=V.from(e.headers),e.data=H.call(e,e.transformRequest),[`post`,`put`,`patch`].indexOf(e.method)!==-1&&e.headers.setContentType(`application/x-www-form-urlencoded`,!1),Gt.getAdapter(e.adapter||L.adapter,e)(e).then(function(t){return J(e),t.data=H.call(e,e.transformResponse,t),t.headers=V.from(t.headers),t},function(t){return mt(t)||(J(e),t&&t.response&&(t.response.data=H.call(e,e.transformResponse,t.response),t.response.headers=V.from(t.response.headers))),Promise.reject(t)})}var qt=`1.15.0`,Y={};[`object`,`boolean`,`number`,`function`,`string`,`symbol`].forEach((e,t)=>{Y[e]=function(n){return typeof n===e||`a`+(t<1?`n `:` `)+e}});var Jt={};Y.transitional=function(e,t,n){function r(e,t){return`[Axios v`+qt+`] Transitional option '`+e+`'`+t+(n?`. `+n:``)}return(n,i,a)=>{if(e===!1)throw new O(r(i,` has been removed`+(t?` in `+t:``)),O.ERR_DEPRECATED);return t&&!Jt[i]&&(Jt[i]=!0,console.warn(r(i,` has been deprecated since v`+t+` and will be removed in the near future`))),e?e(n,i,a):!0}},Y.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};function Yt(e,t,n){if(typeof e!=`object`)throw new O(`options must be an object`,O.ERR_BAD_OPTION_VALUE);let r=Object.keys(e),i=r.length;for(;i-- >0;){let a=r[i],o=t[a];if(o){let t=e[a],n=t===void 0||o(t,a,e);if(n!==!0)throw new O(`option `+a+` must be `+n,O.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new O(`Unknown option `+a,O.ERR_BAD_OPTION)}}var X={assertOptions:Yt,validators:Y},Z=X.validators,Q=class{constructor(e){this.defaults=e||{},this.interceptors={request:new qe,response:new qe}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=Error();let n=(()=>{if(!t.stack)return``;let e=t.stack.indexOf(`
`);return e===-1?``:t.stack.slice(e+1)})();try{if(!e.stack)e.stack=n;else if(n){let t=n.indexOf(`
`),r=t===-1?-1:n.indexOf(`
`,t+1),i=r===-1?``:n.slice(r+1);String(e.stack).endsWith(i)||(e.stack+=`
`+n)}}catch{}}throw e}}_request(e,t){typeof e==`string`?(t||={},t.url=e):t=e||{},t=G(this.defaults,t);let{transitional:n,paramsSerializer:r,headers:i}=t;n!==void 0&&X.assertOptions(n,{silentJSONParsing:Z.transitional(Z.boolean),forcedJSONParsing:Z.transitional(Z.boolean),clarifyTimeoutError:Z.transitional(Z.boolean),legacyInterceptorReqResOrdering:Z.transitional(Z.boolean)},!1),r!=null&&(D.isFunction(r)?t.paramsSerializer={serialize:r}:X.assertOptions(r,{encode:Z.function,serialize:Z.function},!0)),t.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls===void 0?t.allowAbsoluteUrls=!0:t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls),X.assertOptions(t,{baseUrl:Z.spelling(`baseURL`),withXsrfToken:Z.spelling(`withXSRFToken`)},!0),t.method=(t.method||this.defaults.method||`get`).toLowerCase();let a=i&&D.merge(i.common,i[t.method]);i&&D.forEach([`delete`,`get`,`head`,`post`,`put`,`patch`,`common`],e=>{delete i[e]}),t.headers=V.concat(a,i);let o=[],s=!0;this.interceptors.request.forEach(function(e){if(typeof e.runWhen==`function`&&e.runWhen(t)===!1)return;s&&=e.synchronous;let n=t.transitional||N;n&&n.legacyInterceptorReqResOrdering?o.unshift(e.fulfilled,e.rejected):o.push(e.fulfilled,e.rejected)});let c=[];this.interceptors.response.forEach(function(e){c.push(e.fulfilled,e.rejected)});let l,u=0,d;if(!s){let e=[Kt.bind(this),void 0];for(e.unshift(...o),e.push(...c),d=e.length,l=Promise.resolve(t);u<d;)l=l.then(e[u++],e[u++]);return l}d=o.length;let f=t;for(;u<d;){let e=o[u++],t=o[u++];try{f=e(f)}catch(e){t.call(this,e);break}}try{l=Kt.call(this,f)}catch(e){return Promise.reject(e)}for(u=0,d=c.length;u<d;)l=l.then(c[u++],c[u++]);return l}getUri(e){return e=G(this.defaults,e),Ke(Tt(e.baseURL,e.url,e.allowAbsoluteUrls),e.params,e.paramsSerializer)}};D.forEach([`delete`,`get`,`head`,`options`],function(e){Q.prototype[e]=function(t,n){return this.request(G(n||{},{method:e,url:t,data:(n||{}).data}))}}),D.forEach([`post`,`put`,`patch`],function(e){function t(t){return function(n,r,i){return this.request(G(i||{},{method:e,headers:t?{"Content-Type":`multipart/form-data`}:{},url:n,data:r}))}}Q.prototype[e]=t(),Q.prototype[e+`Form`]=t(!0)});var Xt=class e{constructor(e){if(typeof e!=`function`)throw TypeError(`executor must be a function.`);let t;this.promise=new Promise(function(e){t=e});let n=this;this.promise.then(e=>{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null}),this.promise.then=e=>{let t,r=new Promise(e=>{n.subscribe(e),t=e}).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e(function(e,r,i){n.reason||(n.reason=new U(e,r,i),t(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;let t=this._listeners.indexOf(e);t!==-1&&this._listeners.splice(t,1)}toAbortSignal(){let e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let t;return{token:new e(function(e){t=e}),cancel:t}}};function Zt(e){return function(t){return e.apply(null,t)}}function Qt(e){return D.isObject(e)&&e.isAxiosError===!0}var $t={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries($t).forEach(([e,t])=>{$t[t]=e});function en(e){let t=new Q(e),r=n(Q.prototype.request,t);return D.extend(r,Q.prototype,t,{allOwnKeys:!0}),D.extend(r,t,null,{allOwnKeys:!0}),r.create=function(t){return en(G(e,t))},r}var $=en(L);$.Axios=Q,$.CanceledError=U,$.CancelToken=Xt,$.isCancel=mt,$.VERSION=qt,$.toFormData=j,$.AxiosError=O,$.Cancel=$.CanceledError,$.all=function(e){return Promise.all(e)},$.spread=Zt,$.isAxiosError=Qt,$.mergeConfig=G,$.AxiosHeaders=V,$.formToJSON=e=>nt(D.isHTMLForm(e)?new FormData(e):e),$.getAdapter=Gt.getAdapter,$.HttpStatusCode=$t,$.default=$;var tn=$.create({baseURL:`/api`,timeout:2e4});tn.interceptors.response.use(e=>{if(e.config.responseType===`blob`)return e.data;let n=e.data;return n?.code===void 0?n:n.code===0?n.data:(t.error(n.message||`请求失败`),Promise.reject(Error(n.message||`请求失败`)))},e=>{let n=e?.response?.data?.message||e.message||`网络异常`;return t.error(n),Promise.reject(e)});export{tn as t};
\ No newline at end of file
import{t as e}from"./request-D8DwihSV.js";function t(){return e.get(`/train/packages`)}function n(){return e.get(`/train/tasks`)}function r(t){return e.post(`/train/tasks`,t)}function i(t){return e.post(`/train/tasks/${t}/cancel`)}function a(t){return e.post(`/train/tasks/${t}/restart`)}function o(t){return e.delete(`/train/tasks/${t}`)}function s(t){return e.post(`/train/tasks/${t}/save`)}function c(){return e.get(`/train/models`)}function l(t){return e.delete(`/train/models/${t}`)}export{c as a,a as c,o as i,s as l,r as n,t as o,l as r,n as s,i as t};
\ 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-CvdqK_rU.js"></script>
<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-DCOtnflc.js">
<link rel="modulepreload" crossorigin href="/assets/es-XoFJL7_H.js">
<link rel="stylesheet" crossorigin href="/assets/index-pfQxyObg.css">
</head>
<body>
......
......@@ -41,3 +41,11 @@ export function downloadDataTemplate() {
responseType: 'blob',
})
}
export function getFileQuality(fileId) {
return request.get(`/data/files/${fileId}/quality`)
}
export function getQualityConfig() {
return request.get('/data/quality-config')
}
<script setup>
import { Delete, Edit, Folder, FolderOpened, Upload } from '@element-plus/icons-vue'
import { Delete, Document, Edit, Folder, FolderOpened, Upload } from '@element-plus/icons-vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
......@@ -9,6 +9,7 @@ import {
downloadDataTemplate,
getCategoryTree,
getDataFiles,
getFileQuality,
getFileRecords,
updateCategory,
uploadDataFile,
......@@ -42,6 +43,32 @@ const uploadForm = reactive({
categoryId: '',
})
const qualityDialogVisible = ref(false)
const qualityLoading = ref(false)
const qualityFile = ref(null)
const qualityResult = ref(null)
const qualityColorClass = (percent) => {
if (percent >= 90) return 'quality-good'
if (percent >= 70) return 'quality-warn'
return 'quality-bad'
}
const handleQualityCheck = async (row) => {
qualityFile.value = row
qualityResult.value = null
qualityDialogVisible.value = true
qualityLoading.value = true
try {
qualityResult.value = await getFileQuality(row.id)
} catch {
ElMessage.error('质量评估失败')
qualityDialogVisible.value = false
} finally {
qualityLoading.value = false
}
}
const layoutRef = ref(null)
const leftPaneWidth = ref(16)
const middlePaneWidth = ref(38)
......@@ -483,9 +510,10 @@ onBeforeUnmount(() => {
<el-table-column prop="filename" label="文件名" min-width="160" />
<el-table-column prop="uploaded_at" label="上传时间" min-width="170" />
<el-table-column prop="data_count" label="数据量" width="90" />
<el-table-column label="操作" width="150" fixed="right">
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleViewFile(row)">查看</el-button>
<el-button link type="success" @click="handleQualityCheck(row)">质量判定</el-button>
<el-button link type="danger" @click="handleDeleteFile(row)">删除</el-button>
</template>
</el-table-column>
......@@ -578,6 +606,99 @@ onBeforeUnmount(() => {
<el-button type="primary" @click="submitCategory">保存</el-button>
</template>
</el-dialog>
<!-- 质量判定对话框 -->
<el-dialog
v-model="qualityDialogVisible"
title="数据质量判定"
width="500px"
destroy-on-close
>
<div v-if="qualityFile" class="quality-filename">
<el-icon><Document /></el-icon>
{{ qualityFile.filename }}
</div>
<div v-loading="qualityLoading" class="quality-body">
<template v-if="qualityResult && !qualityLoading">
<div class="quality-total">{{ qualityResult.total }} 条数据行</div>
<!-- 完整性 -->
<div class="quality-item">
<div class="quality-item__header">
<span class="quality-item__label">完整性</span>
<span class="quality-item__count">{{ qualityResult.complete_count }} / {{ qualityResult.total }} 条字段完整</span>
<span class="quality-item__pct" :class="qualityColorClass(qualityResult.completeness)">
{{ qualityResult.completeness }}%
</span>
</div>
<el-progress
:percentage="qualityResult.completeness"
:color="qualityResult.completeness >= 90 ? '#67c23a' : qualityResult.completeness >= 70 ? '#e6a23c' : '#f56c6c'"
:stroke-width="10"
:show-text="false"
/>
<div class="quality-item__desc">每条数据行的电流、电压、设定温度、实际温度均有值</div>
</div>
<!-- 连续性 -->
<div class="quality-item">
<div class="quality-item__header">
<span class="quality-item__label">连续性</span>
<span class="quality-item__count">{{ qualityResult.continuous_count }} / {{ qualityResult.total }} 条有效行</span>
<span class="quality-item__pct" :class="qualityColorClass(qualityResult.continuity)">
{{ qualityResult.continuity }}%
</span>
</div>
<el-progress
:percentage="qualityResult.continuity"
:color="qualityResult.continuity >= 90 ? '#67c23a' : qualityResult.continuity >= 70 ? '#e6a23c' : '#f56c6c'"
:stroke-width="10"
:show-text="false"
/>
<div class="quality-item__desc">非断点行(至少有一个测量值的行)占比,断点为所有测量字段均为空的行</div>
</div>
<!-- 准确性 -->
<div class="quality-item">
<div class="quality-item__header">
<span class="quality-item__label">准确性</span>
<span class="quality-item__count">{{ qualityResult.accurate_count }} / {{ qualityResult.has_measurement_count }} 条在范围内</span>
<span class="quality-item__pct" :class="qualityColorClass(qualityResult.accuracy)">
{{ qualityResult.accuracy }}%
</span>
</div>
<el-progress
:percentage="qualityResult.accuracy"
:color="qualityResult.accuracy >= 90 ? '#67c23a' : qualityResult.accuracy >= 70 ? '#e6a23c' : '#f56c6c'"
:stroke-width="10"
:show-text="false"
/>
<div class="quality-item__desc">
所有非空测量值均在配置范围内的行占比
<span v-if="qualityResult.quality_config && qualityResult.quality_config.length">
(当前配置:
<span v-for="(cfg, idx) in qualityResult.quality_config" :key="cfg.field_name">
<template v-if="cfg.min_value !== null || cfg.max_value !== null">
{{ cfg.label }}
<template v-if="cfg.min_value !== null">{{ cfg.min_value }}{{ cfg.unit }}</template>
<template v-if="cfg.max_value !== null">{{ cfg.max_value }}{{ cfg.unit }}</template>
<template v-if="idx < qualityResult.quality_config.length - 1"></template>
</template>
</span>
</span>
<span v-else class="quality-no-config">(未配置准确性范围,请在 data_quality_config 表中设置)</span>
</div>
</div>
</template>
<el-empty v-else-if="!qualityLoading" description="暂无质量数据" :image-size="60" />
</div>
<template #footer>
<el-button @click="qualityDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</section>
</template>
......@@ -760,4 +881,71 @@ onBeforeUnmount(() => {
border-bottom: 1px solid var(--border-color);
}
}
// quality dialog
.quality-filename {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #475569;
margin-bottom: 16px;
word-break: break-all;
}
.quality-total {
font-size: 13px;
color: #64748b;
margin-bottom: 12px;
}
.quality-body {
min-height: 120px;
}
.quality-item {
margin-bottom: 20px;
&__header {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 6px;
}
&__label {
font-size: 14px;
font-weight: 600;
color: #0f172a;
min-width: 48px;
}
&__count {
font-size: 12px;
color: #64748b;
flex: 1;
}
&__pct {
font-size: 18px;
font-weight: 700;
min-width: 56px;
text-align: right;
&.quality-good { color: #67c23a; }
&.quality-warn { color: #e6a23c; }
&.quality-bad { color: #f56c6c; }
}
&__desc {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
line-height: 1.5;
}
}
.quality-no-config {
color: #e6a23c;
}
</style>
......@@ -3,6 +3,7 @@ import { ArrowLeft } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { createPackage, getAllDataFiles, getPkgCategoryTree, previewPackage } from '@/api/packageManagement'
import { getQualityConfig } from '@/api/dataManagement'
import DataCurve from '@/views/DataManagement/components/DataCurve.vue'
const emit = defineEmits(['cancel', 'saved'])
......@@ -56,6 +57,56 @@ const cleanRules = reactive({
temperature_max: null,
})
const applyQualityConfigToCleanRules = (configs) => {
const map = {}
for (const cfg of configs) {
map[cfg.field_name] = cfg
}
if (map.current) {
if (cleanRules.current_min === null) cleanRules.current_min = map.current.min_value ?? null
if (cleanRules.current_max === null) cleanRules.current_max = map.current.max_value ?? null
}
if (map.voltage) {
if (cleanRules.voltage_min === null) cleanRules.voltage_min = map.voltage.min_value ?? null
if (cleanRules.voltage_max === null) cleanRules.voltage_max = map.voltage.max_value ?? null
}
// temperature: take broader range of set_temperature and actual_temperature
const setTMin = map.set_temperature?.min_value ?? null
const setTMax = map.set_temperature?.max_value ?? null
const actTMin = map.actual_temperature?.min_value ?? null
const actTMax = map.actual_temperature?.max_value ?? null
if (cleanRules.temperature_min === null) {
const candidates = [setTMin, actTMin].filter((v) => v !== null)
cleanRules.temperature_min = candidates.length ? Math.min(...candidates) : null
}
if (cleanRules.temperature_max === null) {
const candidates = [setTMax, actTMax].filter((v) => v !== null)
cleanRules.temperature_max = candidates.length ? Math.max(...candidates) : null
}
}
watch(
() => cleanRules.enabled,
async (enabled) => {
if (!enabled) return
// Only auto-fill if all fields are still null (user hasn't typed yet)
const allNull =
cleanRules.current_min === null &&
cleanRules.current_max === null &&
cleanRules.voltage_min === null &&
cleanRules.voltage_max === null &&
cleanRules.temperature_min === null &&
cleanRules.temperature_max === null
if (!allNull) return
try {
const configs = await getQualityConfig()
applyQualityConfigToCleanRules(configs)
} catch {
// ignore: quality config may not be configured yet
}
},
)
const cleanRulesPayload = computed(() => {
if (!cleanRules.enabled) return null
return {
......@@ -69,7 +120,31 @@ const cleanRulesPayload = computed(() => {
}
})
// ── preview ───────────────────────────────────────────────────────────────────
// ── row range ─────────────────────────────────────────────────────────────────
const rowRange = reactive({
enabled: false,
start: null,
end: null,
})
const rowRangeError = computed(() => {
if (!rowRange.enabled) return ''
const s = rowRange.start
const e = rowRange.end
if (s !== null && s < 1) return '起始行不能小于 1'
if (e !== null && e < 1) return '结束行不能小于 1'
if (s !== null && e !== null && s > e) return '起始行不能大于结束行'
return ''
})
const rowRangePayload = computed(() => {
if (!rowRange.enabled) return { row_start: null, row_end: null }
return {
row_start: rowRange.start ?? null,
row_end: rowRange.end ?? null,
}
})
const previewLoading = ref(false)
const previewRecords = ref([])
const previewTotal = ref(0)
......@@ -87,7 +162,11 @@ const triggerPreview = () => {
previewLoading.value = true
try {
const result = await previewPackage(
{ file_ids: selectedFileIds.value, clean_rules: cleanRulesPayload.value },
{
file_ids: selectedFileIds.value,
clean_rules: cleanRulesPayload.value,
...rowRangePayload.value,
},
{ limit: 300 },
)
previewRecords.value = result.records
......@@ -100,6 +179,7 @@ const triggerPreview = () => {
watch(selectedFileIds, triggerPreview, { deep: true })
watch(cleanRules, triggerPreview, { deep: true })
watch(rowRange, triggerPreview, { deep: true })
// ── save ──────────────────────────────────────────────────────────────────────
const saving = ref(false)
......@@ -113,6 +193,10 @@ const handleGenerate = async () => {
ElMessage.warning('请至少选择一个数据文件')
return
}
if (rowRangeError.value) {
ElMessage.warning(rowRangeError.value)
return
}
saving.value = true
try {
......@@ -122,6 +206,7 @@ const handleGenerate = async () => {
remark: form.remark.trim() || null,
file_ids: selectedFileIds.value,
clean_rules: cleanRulesPayload.value,
...rowRangePayload.value,
})
ElMessage.success('数据包创建成功')
emit('saved')
......@@ -255,6 +340,34 @@ onMounted(async () => {
</div>
</div>
</el-form-item>
<el-form-item label="数据行范围">
<div class="row-range-wrap">
<el-checkbox v-model="rowRange.enabled">启用行范围截取</el-checkbox>
<div v-if="rowRange.enabled" class="row-range-inputs">
<el-input-number
v-model="rowRange.start"
:controls="false"
:value-on-clear="null"
:min="1"
:precision="0"
placeholder="起始行"
class="range-input"
/>
<span class="range-sep">~</span>
<el-input-number
v-model="rowRange.end"
:controls="false"
:value-on-clear="null"
:min="1"
:precision="0"
placeholder="结束行"
class="range-input"
/>
<span class="range-unit">行(含两端)</span>
<div v-if="rowRangeError" class="row-range-error">{{ rowRangeError }}</div>
</div>
</div>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
......@@ -450,4 +563,28 @@ onMounted(async () => {
font-size: 13px;
color: #94a3b8;
}
.row-range-wrap {
width: 100%;
}
.row-range-inputs {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.range-unit {
font-size: 12px;
color: #94a3b8;
}
.row-range-error {
width: 100%;
font-size: 12px;
color: #f56c6c;
margin-top: 2px;
}
</style>
\ No newline at end of file
......@@ -45,6 +45,12 @@ CREATE TABLE IF NOT EXISTS data_packages (
ALTER TABLE data_packages
ADD COLUMN IF NOT EXISTS clean_rules JSON NULL COMMENT '野值清洗规则';
ALTER TABLE data_packages
ADD COLUMN IF NOT EXISTS row_start INT NULL COMMENT '数据行起始行号(1-based, 含)';
ALTER TABLE data_packages
ADD COLUMN IF NOT EXISTS row_end INT NULL COMMENT '数据行结束行号(1-based, 含)';
CREATE TABLE IF NOT EXISTS data_package_files (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
package_id BIGINT NOT NULL COMMENT '数据包ID',
......@@ -59,3 +65,21 @@ ALTER TABLE train_tasks
ALTER TABLE saved_models
ADD COLUMN IF NOT EXISTS test_loss FLOAT NULL COMMENT '测试集损失';
CREATE TABLE IF NOT EXISTS data_quality_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
field_name VARCHAR(50) NOT NULL COMMENT '字段名: current/voltage/set_temperature/actual_temperature',
label VARCHAR(100) NOT NULL COMMENT '显示名称',
unit VARCHAR(20) NOT NULL DEFAULT '' COMMENT '单位',
min_value DOUBLE DEFAULT NULL COMMENT '合规最小值',
max_value DOUBLE DEFAULT NULL COMMENT '合规最大值',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_field_name (field_name)
) COMMENT='数据质量准确性配置表';
INSERT IGNORE INTO data_quality_config (field_name, label, unit) VALUES
('current', '电流', 'A'),
('voltage', '电压', 'V'),
('set_temperature', '设定温度', '℃'),
('actual_temperature', '实际温度', '℃');
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