diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md new file mode 100644 index 00000000..92a6aec0 --- /dev/null +++ b/DEVELOPMENT_PLAN.md @@ -0,0 +1,162 @@ +# DataPointer 开发计划书 + +> 版本:v1.0 | 日期:2026-04-23 + +--- + +## 一、现状评估 + +| 模块 | 当前状态 | 关键债务/缺口 | +|------|---------|-------------| +| 数据源管理 | MySQL/PG/Oracle/SQLServer + 达梦(mock) | 达梦未真实支持;密码加密密钥运行时随机生成,重启后无法解密 | +| 元数据采集 | 库/表/字段基础采集 | 全量扫描,缺少增量与 Schema 变更追踪 | +| 分类引擎 | 正则/关键词/枚举规则 | scikit-learn 已引入但未使用;Celery 任务为 placeholder | +| 项目管理 | 创建/分配/打标/发布 | 无 ML 辅助推荐 | +| 报告 | Word 导出 | 无 Excel/PDF;无风险摘要 | +| 安全能力 | 无 | 无脱敏、无水印 | +| 风险管理 | 无 | 无量化评分、无合规对标 | +| 非结构化 | 模型已建(UnstructuredFile) | 功能未实现 | + +--- + +## 二、整体里程碑 + +``` +第一阶段(核心引擎加固 + 智能化) 4 周 +第二阶段(安全能力补齐 + 体验升级) 5 周 +第三阶段(风险管理 + 合规 + 血缘) 6 周 +------------------------------------------------------------------------ +合计 15 周 约 3.5 个月 +``` + +--- + +## 三、第一阶段:核心引擎加固与智能化(4 周) + +### T1.1 修复数据源密码加密(P0) +- **方案**:config.py 新增 DB_ENCRYPTION_KEY,读取环境变量;datasource_service.py 改用该密钥;提供 Alembic 迁移脚本处理历史数据。 +- **验收**:重启后历史数据源仍可正常连接;密钥外部注入。 +- **工时**:2d + +### T1.2 Celery 异步分类落地(P0) +- **方案**:将 run_auto_classification 逻辑迁入 Celery Task;Project 增加 scan_progress 字段;后端提供 progress 轮询接口;前端增加进度条与后台执行开关。 +- **验收**:万级字段分类 HTTP 不阻塞;前端实时显示进度。 +- **工时**:4d + +### T1.3 ML 辅助分类原型(P1) +- **方案**:新增 MLModelVersion 模型;对字段 name/comment/sample_data 做 TF-IDF;用 LogisticRegression/RandomForest 训练;提供 ml-suggest 接口与前端一键采纳;训练任务封装为 Celery Task。 +- **验收**:测试集 Top-1 准确率 >= 70%;前端展示推荐标签与置信度。 +- **工时**:8d + +### T1.4 语义相似度规则(P1) +- **方案**:新增 similarity 规则类型;用 TfidfVectorizer + cosine_similarity 计算字段名/注释与基准词相似度;阈值默认 0.75。 +- **验收**:mobile_no / cell_phone / contact_tel 可被同一条规则命中。 +- **工时**:3d + +### T1.5 增量元数据采集(P1) +- **方案**:meta 表增加 last_scanned_at 与 checksum;采集时对比 information_schema 仅同步变更;删除对象做软删除保留历史。 +- **验收**:重复采集未变更表不写库;源库新增表仅增量写入。 +- **工时**:4d + +--- + +## 四、第二阶段:安全能力补齐与体验升级(5 周) + +### T2.1 数据静态脱敏(P1) +- **方案**:新增 MaskingRule 模型,支持 mask/truncate/hash/generalize/replace;脱敏预览与导出 API;前端策略配置与左右对比预览页。 +- **验收**:身份证号/手机号按规则掩码导出;支持批量策略应用。 +- **工时**:8d + +### T2.2 数据水印溯源(P2) +- **方案**:文本水印采用零宽空格嵌入用户 ID;WatermarkLog 记录导出信息;溯源 API 提取水印追溯到用户。 +- **验收**:导出 CSV 可复制后成功溯源到用户;不影响可读性。 +- **工时**:5d + +### T2.3 Excel + PDF 报告(P1) +- **方案**:Excel 用 openpyxl 带条件格式与图表;PDF 采用前端 html2canvas + jspdf 方案,减少后端依赖。 +- **验收**:支持 Word/Excel/PDF 三种导出;PDF 含统计图与 Top20 敏感清单。 +- **工时**:4d + +### T2.4 达梦真实驱动(P2) +- **方案**:优先 dmPython,fallback 用 jaydebeapi + JDBC;更新元数据采集适配达梦系统表。 +- **验收**:可连接达梦并采集库表字段元数据。 +- **工时**:4d + +### T2.5 非结构化文件识别(P2) +- **方案**:激活 UnstructuredFile;文件存 MinIO;用 python-docx/openpyxl/pdfplumber 解析文本;送入规则引擎识别敏感信息。 +- **验收**:Word/Excel 中的身份证号/手机号可被识别并建议 L4。 +- **工时**:6d + +### T2.6 Schema 变更追踪(P2) +- **方案**:新增 SchemaChangeLog 模型;增量采集时自动比对生成变更记录;前端数据源详情页展示变更历史。 +- **验收**:源库新增敏感字段后平台生成变更记录并标红告警。 +- **工时**:4d + +--- + +## 五、第三阶段:风险管理与合规(6 周) + +### T3.1 风险评分模型(P1) +- **方案**:RiskScore = sum(Li * exposure * (1 - protection_rate));新增 RiskAssessment 模型四级聚合;Celery Beat 每日重算;Dashboard 增加风险趋势与排行。 +- **验收**:项目生成 0-100 风险分;未脱敏敏感字段增加时分数上升。 +- **工时**:7d + +### T3.2 合规检查引擎(P1) +- **方案**:内置等保/PIPL/GDPR 检查规则;ComplianceChecker 可插拔基类;新增 ComplianceScan/ComplianceIssue;前端规则库与问题清单。 +- **验收**:自动扫描出 L5 未脱敏等不合规项;可导出合规差距分析。 +- **工时**:7d + +### T3.3 数据血缘分析(P2) +- **方案**:引入 sqlparse 解析 SQL;新增 DataLineage 模型;前端 ECharts 关系图展示表级血缘,支持 3 层展开。 +- **验收**:典型 ETL SQL 可正确构建血缘链。 +- **工时**:8d + +### T3.4 风险告警与工单(P1) +- **方案**:AlertRule 模型配置触发条件;AlertRecord 记录告警;WorkOrder 简易工单流转(open -> in_progress -> resolved);站内消息中心。 +- **验收**:新增 5 个 L5 字段自动生成告警并转工单指派。 +- **工时**:6d + +### T3.5 API 资产扫描(P2) +- **方案**:新增 ApiAsset 模型;上传 Swagger/OpenAPI 解析参数与响应 schema;规则引擎标记敏感接口。 +- **验收**:上传含 phone/idCard 的 Swagger 后标记为暴露 L4 数据。 +- **工时**:5d + +### T3.6 暗黑模式与性能优化(P2) +- **方案**:Element Plus 动态主题 + Pinia 持久化;大表格虚拟滚动;路由懒加载。 +- **验收**:一键切换暗黑/明亮无闪烁;5 万字段页面滚动 >= 30fps。 +- **工时**:4d + +--- + +## 六、任务总览 + +| 编号 | 任务 | 阶段 | 优先级 | 工时 | 依赖 | +|------|------|------|--------|------|------| +| T1.1 | 修复密码加密密钥管理 | P1 | P0 | 2d | 无 | +| T1.2 | Celery 异步分类落地 | P1 | P0 | 4d | T1.1 | +| T1.3 | ML 辅助分类原型 | P1 | P1 | 8d | T1.2 | +| T1.4 | 语义相似度规则 | P1 | P1 | 3d | 无 | +| T1.5 | 增量元数据采集 | P1 | P1 | 4d | 无 | +| T2.1 | 数据静态脱敏 | P2 | P1 | 8d | T1.5 | +| T2.2 | 数据水印溯源 | P2 | P2 | 5d | T2.1 | +| T2.3 | Excel + PDF 报告 | P2 | P1 | 4d | 无 | +| T2.4 | 达梦真实驱动 | P2 | P2 | 4d | 无 | +| T2.5 | 非结构化文件识别 | P2 | P2 | 6d | T1.2 | +| T2.6 | Schema 变更追踪 | P2 | P2 | 4d | T1.5 | +| T3.1 | 风险评分模型 | P3 | P1 | 7d | T2.1, T2.6 | +| T3.2 | 合规检查引擎 | P3 | P1 | 7d | T3.1 | +| T3.3 | 数据血缘分析 | P3 | P2 | 8d | 无 | +| T3.4 | 风险告警与工单 | P3 | P1 | 6d | T3.1, T3.2 | +| T3.5 | API 资产扫描 | P3 | P2 | 5d | T1.4 | +| T3.6 | 暗黑模式与性能优化 | P3 | P2 | 4d | 无 | + +**总计约 89 人天(单人约 3.5 个月;双人并行可压缩至 2 个月)** + +--- + +## 七、确认事项 + +1. 三阶段范围:是否全部 17 项任务均需开发?有无可削减项? +2. 达梦环境:是否有真实达梦环境联调?若无,是否接受 jaydebeapi 桥接方案? +3. ML 训练数据:当前人工标注字段大约多少?若不足,是否构造模拟数据? +4. 启动顺序:是否从 T1.1 开始依次执行,还是允许阶段间少量并行? diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..316c0d90 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,29 @@ +# Database +DATABASE_URL=postgresql+psycopg2://pdg:pdg_secret_2024@localhost:5432/prop_data_guard +REDIS_URL=redis://localhost:6379/0 + +# Security +SECRET_KEY=prop-data-guard-super-secret-key-change-in-production +# Fernet-compatible encryption key for database passwords (32 bytes, base64 url-safe). +# Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +# IMPORTANT: Keep this key safe and consistent across all backend instances. +DB_ENCRYPTION_KEY= + +# MinIO +MINIO_ENDPOINT=localhost:9000 +MINIO_ACCESS_KEY=pdgminio +MINIO_SECRET_KEY=pdgminio_secret_2024 +MINIO_SECURE=false +MINIO_BUCKET_NAME=pdg-files + +# CORS +CORS_ORIGINS=["http://localhost:5173", "http://127.0.0.1:5173"] + +# Auth +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# Default superuser (created on first startup) +FIRST_SUPERUSER_USERNAME=admin +FIRST_SUPERUSER_PASSWORD=admin123 +FIRST_SUPERUSER_EMAIL=admin@datapo.com diff --git a/backend/alembic/versions/002_fix_encryption.py b/backend/alembic/versions/002_fix_encryption.py new file mode 100644 index 00000000..0a6bf40a --- /dev/null +++ b/backend/alembic/versions/002_fix_encryption.py @@ -0,0 +1,41 @@ +"""Fix datasource password encryption stability + +Revision ID: 002 +Revises: 001 +Create Date: 2026-04-23 14:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "002" +down_revision: Union[str, None] = "001" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Historical encrypted_password values are irrecoverable because + # the old implementation generated a random Fernet key on every startup. + # We clear the passwords and mark sources as inactive so admins re-enter them + # with the new stable key derived from DB_ENCRYPTION_KEY / SECRET_KEY. + op.add_column( + "data_source", + sa.Column("password_reset_required", sa.Boolean(), nullable=False, server_default=sa.text("false")), + ) + op.execute( + """ + UPDATE data_source + SET encrypted_password = NULL, + status = 'inactive', + password_reset_required = true + WHERE encrypted_password IS NOT NULL + """ + ) + + +def downgrade() -> None: + op.drop_column("data_source", "password_reset_required") diff --git a/backend/alembic/versions/003_add_project_task_fields.py b/backend/alembic/versions/003_add_project_task_fields.py new file mode 100644 index 00000000..0b1757ae --- /dev/null +++ b/backend/alembic/versions/003_add_project_task_fields.py @@ -0,0 +1,27 @@ +"""Add celery_task_id and scan_progress to classification_project + +Revision ID: 003 +Revises: 002 +Create Date: 2026-04-23 14:30:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "003" +down_revision: Union[str, None] = "002" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("classification_project", sa.Column("celery_task_id", sa.String(100), nullable=True)) + op.add_column("classification_project", sa.Column("scan_progress", sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("classification_project", "scan_progress") + op.drop_column("classification_project", "celery_task_id") diff --git a/backend/alembic/versions/004_add_ml_model.py b/backend/alembic/versions/004_add_ml_model.py new file mode 100644 index 00000000..3748ec82 --- /dev/null +++ b/backend/alembic/versions/004_add_ml_model.py @@ -0,0 +1,37 @@ +"""Add ml_model_version table + +Revision ID: 004 +Revises: 003 +Create Date: 2026-04-23 15:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "004" +down_revision: Union[str, None] = "003" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "ml_model_version", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(100), nullable=False), + sa.Column("model_path", sa.String(500), nullable=False), + sa.Column("vectorizer_path", sa.String(500), nullable=False), + sa.Column("accuracy", sa.Float(), default=0.0), + sa.Column("train_samples", sa.Integer(), default=0), + sa.Column("train_date", sa.DateTime(), default=sa.func.now()), + sa.Column("is_active", sa.Boolean(), default=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("ml_model_version") diff --git a/backend/alembic/versions/005_add_incremental_meta.py b/backend/alembic/versions/005_add_incremental_meta.py new file mode 100644 index 00000000..a9166d02 --- /dev/null +++ b/backend/alembic/versions/005_add_incremental_meta.py @@ -0,0 +1,33 @@ +"""Add incremental scan fields to meta tables + +Revision ID: 005 +Revises: 004 +Create Date: 2026-04-23 16:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "005" +down_revision: Union[str, None] = "004" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + for table in ["meta_database", "meta_table", "meta_column"]: + op.add_column(table, sa.Column("last_scanned_at", sa.DateTime(), nullable=True)) + op.add_column(table, sa.Column("checksum", sa.String(64), nullable=True)) + op.add_column(table, sa.Column("is_deleted", sa.Boolean(), nullable=False, server_default=sa.text("false"))) + op.add_column(table, sa.Column("deleted_at", sa.DateTime(), nullable=True)) + + +def downgrade() -> None: + for table in ["meta_database", "meta_table", "meta_column"]: + op.drop_column(table, "deleted_at") + op.drop_column(table, "is_deleted") + op.drop_column(table, "checksum") + op.drop_column(table, "last_scanned_at") diff --git a/backend/alembic/versions/006_add_masking_rule.py b/backend/alembic/versions/006_add_masking_rule.py new file mode 100644 index 00000000..5d270f49 --- /dev/null +++ b/backend/alembic/versions/006_add_masking_rule.py @@ -0,0 +1,37 @@ +"""Add masking_rule table + +Revision ID: 006 +Revises: 005 +Create Date: 2026-04-23 17:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "006" +down_revision: Union[str, None] = "005" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "masking_rule", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(100), nullable=False), + sa.Column("level_id", sa.Integer(), sa.ForeignKey("data_level.id"), nullable=True), + sa.Column("category_id", sa.Integer(), sa.ForeignKey("category.id"), nullable=True), + sa.Column("algorithm", sa.String(20), nullable=False), + sa.Column("params", sa.JSON(), nullable=True), + sa.Column("is_active", sa.Boolean(), default=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(), default=sa.func.now(), onupdate=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("masking_rule") diff --git a/backend/alembic/versions/007_add_watermark_log.py b/backend/alembic/versions/007_add_watermark_log.py new file mode 100644 index 00000000..66a166cb --- /dev/null +++ b/backend/alembic/versions/007_add_watermark_log.py @@ -0,0 +1,33 @@ +"""Add watermark_log table + +Revision ID: 007 +Revises: 006 +Create Date: 2026-04-23 18:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "007" +down_revision: Union[str, None] = "006" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "watermark_log", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("user_id", sa.Integer(), sa.ForeignKey("sys_user.id"), nullable=False), + sa.Column("export_type", sa.String(20), default="csv"), + sa.Column("data_scope", sa.Text(), nullable=True), + sa.Column("watermark_key", sa.String(64), nullable=False), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("watermark_log") diff --git a/backend/alembic/versions/008_add_unstructured_analysis.py b/backend/alembic/versions/008_add_unstructured_analysis.py new file mode 100644 index 00000000..73034a7b --- /dev/null +++ b/backend/alembic/versions/008_add_unstructured_analysis.py @@ -0,0 +1,25 @@ +"""Add analysis_result to unstructured_file + +Revision ID: 008 +Revises: 007 +Create Date: 2026-04-23 19:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "008" +down_revision: Union[str, None] = "007" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("unstructured_file", sa.Column("analysis_result", sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("unstructured_file", "analysis_result") diff --git a/backend/alembic/versions/009_add_schema_change_log.py b/backend/alembic/versions/009_add_schema_change_log.py new file mode 100644 index 00000000..45b41dbe --- /dev/null +++ b/backend/alembic/versions/009_add_schema_change_log.py @@ -0,0 +1,36 @@ +"""Add schema_change_log table + +Revision ID: 009 +Revises: 008 +Create Date: 2026-04-23 20:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "009" +down_revision: Union[str, None] = "008" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "schema_change_log", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("source_id", sa.Integer(), sa.ForeignKey("data_source.id"), nullable=False), + sa.Column("database_id", sa.Integer(), sa.ForeignKey("meta_database.id"), nullable=True), + sa.Column("table_id", sa.Integer(), sa.ForeignKey("meta_table.id"), nullable=True), + sa.Column("column_id", sa.Integer(), sa.ForeignKey("meta_column.id"), nullable=True), + sa.Column("change_type", sa.String(20), nullable=False), + sa.Column("old_value", sa.Text(), nullable=True), + sa.Column("new_value", sa.Text(), nullable=True), + sa.Column("detected_at", sa.DateTime(), default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("schema_change_log") diff --git a/backend/alembic/versions/010_add_risk_assessment.py b/backend/alembic/versions/010_add_risk_assessment.py new file mode 100644 index 00000000..1407ccf6 --- /dev/null +++ b/backend/alembic/versions/010_add_risk_assessment.py @@ -0,0 +1,40 @@ +"""Add risk_assessment table + +Revision ID: 010 +Revises: 009 +Create Date: 2026-04-23 21:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "010" +down_revision: Union[str, None] = "009" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "risk_assessment", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("entity_type", sa.String(20), nullable=False), + sa.Column("entity_id", sa.Integer(), nullable=False), + sa.Column("entity_name", sa.String(200), nullable=True), + sa.Column("risk_score", sa.Float(), default=0.0), + sa.Column("sensitivity_score", sa.Float(), default=0.0), + sa.Column("exposure_score", sa.Float(), default=0.0), + sa.Column("protection_score", sa.Float(), default=0.0), + sa.Column("details", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(), default=sa.func.now(), onupdate=sa.func.now()), + ) + op.create_index("idx_risk_entity", "risk_assessment", ["entity_type", "entity_id"]) + + +def downgrade() -> None: + op.drop_index("idx_risk_entity", table_name="risk_assessment") + op.drop_table("risk_assessment") diff --git a/backend/alembic/versions/011_add_compliance.py b/backend/alembic/versions/011_add_compliance.py new file mode 100644 index 00000000..e9b1484d --- /dev/null +++ b/backend/alembic/versions/011_add_compliance.py @@ -0,0 +1,51 @@ +"""Add compliance_rule and compliance_issue tables + +Revision ID: 011 +Revises: 010 +Create Date: 2026-04-23 22:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "011" +down_revision: Union[str, None] = "010" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "compliance_rule", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(200), nullable=False), + sa.Column("standard", sa.String(50), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("check_logic", sa.String(50), nullable=False), + sa.Column("severity", sa.String(20), default="medium"), + sa.Column("is_active", sa.Boolean(), default=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + op.create_table( + "compliance_issue", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("rule_id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=True), + sa.Column("entity_type", sa.String(20), nullable=False), + sa.Column("entity_id", sa.Integer(), nullable=False), + sa.Column("entity_name", sa.String(200), nullable=True), + sa.Column("severity", sa.String(20), default="medium"), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("suggestion", sa.Text(), nullable=True), + sa.Column("status", sa.String(20), default="open"), + sa.Column("resolved_at", sa.DateTime(), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("compliance_issue") + op.drop_table("compliance_rule") diff --git a/backend/alembic/versions/012_add_data_lineage.py b/backend/alembic/versions/012_add_data_lineage.py new file mode 100644 index 00000000..6b2ed8c8 --- /dev/null +++ b/backend/alembic/versions/012_add_data_lineage.py @@ -0,0 +1,35 @@ +"""Add data_lineage table + +Revision ID: 012 +Revises: 011 +Create Date: 2026-04-23 23:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "012" +down_revision: Union[str, None] = "011" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "data_lineage", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("source_table", sa.String(200), nullable=False), + sa.Column("source_column", sa.String(200), nullable=True), + sa.Column("target_table", sa.String(200), nullable=False), + sa.Column("target_column", sa.String(200), nullable=True), + sa.Column("relation_type", sa.String(20), default="direct"), + sa.Column("script_content", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("data_lineage") diff --git a/backend/alembic/versions/013_add_alert_work_order.py b/backend/alembic/versions/013_add_alert_work_order.py new file mode 100644 index 00000000..0dff7aa5 --- /dev/null +++ b/backend/alembic/versions/013_add_alert_work_order.py @@ -0,0 +1,58 @@ +"""Add alert and work_order tables + +Revision ID: 013 +Revises: 012 +Create Date: 2026-04-24 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = "013" +down_revision: Union[str, None] = "012" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "alert_rule", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(200), nullable=False), + sa.Column("trigger_condition", sa.String(50), nullable=False), + sa.Column("threshold", sa.Integer(), default=0), + sa.Column("severity", sa.String(20), default="medium"), + sa.Column("is_active", sa.Boolean(), default=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + op.create_table( + "alert_record", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("rule_id", sa.Integer(), sa.ForeignKey("alert_rule.id"), nullable=False), + sa.Column("title", sa.String(200), nullable=False), + sa.Column("content", sa.Text(), nullable=True), + sa.Column("severity", sa.String(20), default="medium"), + sa.Column("status", sa.String(20), default="open"), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + op.create_table( + "work_order", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("alert_id", sa.Integer(), sa.ForeignKey("alert_record.id"), nullable=True), + sa.Column("title", sa.String(200), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("assignee_id", sa.Integer(), sa.ForeignKey("sys_user.id"), nullable=True), + sa.Column("status", sa.String(20), default="open"), + sa.Column("resolution", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + sa.Column("resolved_at", sa.DateTime(), nullable=True), + ) + + +def downgrade() -> None: + op.drop_table("work_order") + op.drop_table("alert_record") + op.drop_table("alert_rule") diff --git a/backend/alembic/versions/014_add_api_asset.py b/backend/alembic/versions/014_add_api_asset.py new file mode 100644 index 00000000..a2c61824 --- /dev/null +++ b/backend/alembic/versions/014_add_api_asset.py @@ -0,0 +1,55 @@ +"""Add api_asset and api_endpoint tables + +Revision ID: 014 +Revises: 013 +Create Date: 2026-04-24 00:00:00.000000 + +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa + +revision: str = "014" +down_revision: Union[str, None] = "013" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "api_asset", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(200), nullable=False), + sa.Column("base_url", sa.String(500), nullable=False), + sa.Column("swagger_url", sa.String(500), nullable=True), + sa.Column("auth_type", sa.String(50), default="none"), + sa.Column("headers", sa.JSON(), default=dict), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("scan_status", sa.String(20), default="idle"), + sa.Column("total_endpoints", sa.Integer(), default=0), + sa.Column("sensitive_endpoints", sa.Integer(), default=0), + sa.Column("created_by", sa.Integer(), sa.ForeignKey("sys_user.id"), nullable=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(), default=sa.func.now(), onupdate=sa.func.now()), + ) + op.create_table( + "api_endpoint", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("asset_id", sa.Integer(), sa.ForeignKey("api_asset.id"), nullable=False), + sa.Column("method", sa.String(10), nullable=False), + sa.Column("path", sa.String(500), nullable=False), + sa.Column("summary", sa.String(500), nullable=True), + sa.Column("tags", sa.JSON(), default=list), + sa.Column("parameters", sa.JSON(), default=list), + sa.Column("request_body_schema", sa.JSON(), nullable=True), + sa.Column("response_schema", sa.JSON(), nullable=True), + sa.Column("sensitive_fields", sa.JSON(), default=list), + sa.Column("risk_level", sa.String(20), default="low"), + sa.Column("is_active", sa.Boolean(), default=True), + sa.Column("created_at", sa.DateTime(), default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("api_endpoint") + op.drop_table("api_asset") diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index e601b4de..c3d29a76 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report, dashboard +from app.api.v1 import auth, user, datasource, metadata, classification, project, task, report, dashboard, masking, watermark, unstructured, schema_change, risk, compliance, lineage, alert, api_asset api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["认证"]) @@ -12,3 +12,12 @@ api_router.include_router(project.router, prefix="/projects", tags=["项目管 api_router.include_router(task.router, prefix="/tasks", tags=["任务管理"]) api_router.include_router(report.router, prefix="/reports", tags=["报告管理"]) api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"]) +api_router.include_router(masking.router, prefix="/masking", tags=["数据脱敏"]) +api_router.include_router(watermark.router, prefix="/watermark", tags=["数据水印"]) +api_router.include_router(unstructured.router, prefix="/unstructured", tags=["非结构化文件"]) +api_router.include_router(schema_change.router, prefix="/schema-changes", tags=["Schema变更"]) +api_router.include_router(risk.router, prefix="/risk", tags=["风险评估"]) +api_router.include_router(compliance.router, prefix="/compliance", tags=["合规检查"]) +api_router.include_router(lineage.router, prefix="/lineage", tags=["数据血缘"]) +api_router.include_router(alert.router, prefix="/alerts", tags=["告警与工单"]) +api_router.include_router(api_asset.router, prefix="/api-assets", tags=["API资产"]) diff --git a/backend/app/api/v1/alert.py b/backend/app/api/v1/alert.py new file mode 100644 index 00000000..fe2d99a0 --- /dev/null +++ b/backend/app/api/v1/alert.py @@ -0,0 +1,115 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.services import alert_service +from app.api.deps import get_current_user, require_admin + +router = APIRouter() + + +@router.post("/init-rules") +def init_alert_rules( + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + alert_service.init_builtin_alert_rules(db) + return ResponseModel(message="初始化完成") + + +@router.post("/check") +def check_alerts( + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + records = alert_service.check_alerts(db) + return ResponseModel(data={"alerts_created": len(records)}) + + +@router.get("/records") +def list_alert_records( + status: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + query = db.query(alert_service.AlertRecord) + if status: + query = query.filter(alert_service.AlertRecord.status == status) + total = query.count() + items = query.order_by(alert_service.AlertRecord.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + return ListResponse( + data=[{ + "id": r.id, + "rule_id": r.rule_id, + "title": r.title, + "content": r.content, + "severity": r.severity, + "status": r.status, + "created_at": r.created_at.isoformat() if r.created_at else None, + } for r in items], + total=total, + page=page, + page_size=page_size, + ) + + +@router.post("/work-orders") +def create_work_order( + alert_id: int, + title: str, + description: str = "", + assignee_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + wo = alert_service.create_work_order(db, alert_id, title, description, assignee_id) + return ResponseModel(data={"id": wo.id}) + + +@router.get("/work-orders") +def list_work_orders( + status: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from app.models.alert import WorkOrder + query = db.query(WorkOrder) + if status: + query = query.filter(WorkOrder.status == status) + total = query.count() + items = query.order_by(WorkOrder.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + return ListResponse( + data=[{ + "id": w.id, + "alert_id": w.alert_id, + "title": w.title, + "status": w.status, + "assignee_name": w.assignee.username if w.assignee else None, + "created_at": w.created_at.isoformat() if w.created_at else None, + } for w in items], + total=total, + page=page, + page_size=page_size, + ) + + +@router.post("/work-orders/{wo_id}/status") +def update_work_order( + wo_id: int, + status: str, + resolution: str = "", + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + wo = alert_service.update_work_order_status(db, wo_id, status, resolution or None) + if not wo: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工单不存在") + return ResponseModel(data={"id": wo.id, "status": wo.status}) diff --git a/backend/app/api/v1/api_asset.py b/backend/app/api/v1/api_asset.py new file mode 100644 index 00000000..3bb8c20a --- /dev/null +++ b/backend/app/api/v1/api_asset.py @@ -0,0 +1,131 @@ +from typing import Optional, List +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from pydantic import BaseModel + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.services import api_asset_service +from app.api.deps import get_current_user + +router = APIRouter() + +class APIAssetCreate(BaseModel): + name: str + base_url: str + swagger_url: Optional[str] = None + auth_type: Optional[str] = "none" + headers: Optional[dict] = None + description: Optional[str] = None + +class APIAssetUpdate(BaseModel): + name: Optional[str] = None + base_url: Optional[str] = None + swagger_url: Optional[str] = None + auth_type: Optional[str] = None + headers: Optional[dict] = None + description: Optional[str] = None + +@router.post("") +def create_asset( + body: APIAssetCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + asset = api_asset_service.create_asset(db, body.dict(), current_user.id) + return ResponseModel(data={"id": asset.id}) + +@router.get("") +def list_assets( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from app.models.api_asset import APIAsset + query = db.query(APIAsset) + total = query.count() + items = query.order_by(APIAsset.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + return ListResponse( + data=[{ + "id": a.id, + "name": a.name, + "base_url": a.base_url, + "swagger_url": a.swagger_url, + "auth_type": a.auth_type, + "scan_status": a.scan_status, + "total_endpoints": a.total_endpoints, + "sensitive_endpoints": a.sensitive_endpoints, + "created_at": a.created_at.isoformat() if a.created_at else None, + } for a in items], + total=total, + page=page, + page_size=page_size, + ) + +@router.put("/{asset_id}") +def update_asset( + asset_id: int, + body: APIAssetUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + asset = api_asset_service.update_asset(db, asset_id, body.dict(exclude_unset=True)) + if not asset: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在") + return ResponseModel(data={"id": asset.id}) + +@router.delete("/{asset_id}") +def delete_asset( + asset_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + ok = api_asset_service.delete_asset(db, asset_id) + if not ok: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在") + return ResponseModel() + +@router.post("/{asset_id}/scan") +def scan_asset( + asset_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + result = api_asset_service.scan_swagger(db, asset_id) + return ResponseModel(data=result) + +@router.get("/{asset_id}/endpoints") +def list_endpoints( + asset_id: int, + risk_level: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from app.models.api_asset import APIEndpoint + query = db.query(APIEndpoint).filter(APIEndpoint.asset_id == asset_id) + if risk_level: + query = query.filter(APIEndpoint.risk_level == risk_level) + total = query.count() + items = query.order_by(APIEndpoint.id.asc()).offset((page - 1) * page_size).limit(page_size).all() + return ListResponse( + data=[{ + "id": e.id, + "method": e.method, + "path": e.path, + "summary": e.summary, + "tags": e.tags, + "parameters": e.parameters, + "sensitive_fields": e.sensitive_fields, + "risk_level": e.risk_level, + "is_active": e.is_active, + } for e in items], + total=total, + page=page, + page_size=page_size, + ) diff --git a/backend/app/api/v1/classification.py b/backend/app/api/v1/classification.py index 5b18c257..b8c27869 100644 --- a/backend/app/api/v1/classification.py +++ b/backend/app/api/v1/classification.py @@ -238,3 +238,43 @@ def auto_classify( ): result = classification_engine.run_auto_classification(db, project_id) return ResponseModel(data=result) + + +@router.post("/ml-train") +def ml_train( + background: bool = True, + model_name: Optional[str] = None, + algorithm: str = "logistic_regression", + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + from app.tasks.ml_tasks import train_ml_model_task + from app.services.ml_service import train_model + + if background: + task = train_ml_model_task.delay(model_name=model_name, algorithm=algorithm) + return ResponseModel(data={"task_id": task.id, "status": task.state}) + else: + mv = train_model(db, model_name=model_name, algorithm=algorithm) + if mv: + return ResponseModel(data={"model_id": mv.id, "accuracy": mv.accuracy, "train_samples": mv.train_samples}) + return ResponseModel(message="训练失败:样本不足或发生错误") + + +@router.get("/ml-suggest/{project_id}") +def ml_suggest( + project_id: int, + column_ids: Optional[str] = Query(None), + top_k: int = Query(3, ge=1, le=5), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from app.services.ml_service import suggest_for_project_columns + ids = None + if column_ids: + ids = [int(x) for x in column_ids.split(",") if x.strip().isdigit()] + result = suggest_for_project_columns(db, project_id, column_ids=ids, top_k=top_k) + if not result.get("success"): + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=result.get("message")) + return ResponseModel(data=result["suggestions"]) diff --git a/backend/app/api/v1/compliance.py b/backend/app/api/v1/compliance.py new file mode 100644 index 00000000..743d98dc --- /dev/null +++ b/backend/app/api/v1/compliance.py @@ -0,0 +1,72 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.services import compliance_service +from app.api.deps import get_current_user, require_admin + +router = APIRouter() + + +@router.post("/init-rules") +def init_rules( + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + compliance_service.init_builtin_rules(db) + return ResponseModel(message="初始化完成") + + +@router.post("/scan") +def scan_compliance( + project_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + issues = compliance_service.scan_compliance(db, project_id=project_id) + return ResponseModel(data={"issues_found": len(issues)}) + + +@router.get("/issues") +def list_issues( + project_id: Optional[int] = Query(None), + status: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + items, total = compliance_service.list_issues(db, project_id=project_id, status=status, page=page, page_size=page_size) + return ListResponse( + data=[{ + "id": i.id, + "rule_id": i.rule_id, + "project_id": i.project_id, + "entity_type": i.entity_type, + "entity_name": i.entity_name, + "severity": i.severity, + "description": i.description, + "suggestion": i.suggestion, + "status": i.status, + "created_at": i.created_at.isoformat() if i.created_at else None, + } for i in items], + total=total, + page=page, + page_size=page_size, + ) + + +@router.post("/issues/{issue_id}/resolve") +def resolve_issue( + issue_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + issue = compliance_service.resolve_issue(db, issue_id) + if not issue: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="问题不存在") + return ResponseModel(message="已标记为已解决") diff --git a/backend/app/api/v1/lineage.py b/backend/app/api/v1/lineage.py new file mode 100644 index 00000000..019d65e6 --- /dev/null +++ b/backend/app/api/v1/lineage.py @@ -0,0 +1,32 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel +from app.services import lineage_service +from app.api.deps import get_current_user + +router = APIRouter() + + +@router.post("/parse") +def parse_lineage( + sql: str, + target_table: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + records = lineage_service.parse_sql_lineage(db, sql, target_table) + return ResponseModel(data={"records_created": len(records)}) + + +@router.get("/graph") +def get_graph( + table_name: Optional[str] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + graph = lineage_service.get_lineage_graph(db, table_name=table_name) + return ResponseModel(data=graph) diff --git a/backend/app/api/v1/masking.py b/backend/app/api/v1/masking.py new file mode 100644 index 00000000..3e8e5729 --- /dev/null +++ b/backend/app/api/v1/masking.py @@ -0,0 +1,88 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.services import masking_service +from app.api.deps import get_current_user, require_admin + +router = APIRouter() + + +@router.get("/rules") +def list_masking_rules( + level_id: Optional[int] = Query(None), + category_id: Optional[int] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + items, total = masking_service.list_masking_rules(db, level_id=level_id, category_id=category_id, page=page, page_size=page_size) + return ListResponse( + data=[{ + "id": r.id, + "name": r.name, + "level_id": r.level_id, + "category_id": r.category_id, + "algorithm": r.algorithm, + "params": r.params, + "is_active": r.is_active, + "description": r.description, + "level_name": r.level.name if r.level else None, + "category_name": r.category.name if r.category else None, + } for r in items], + total=total, + page=page, + page_size=page_size, + ) + + +@router.post("/rules") +def create_masking_rule( + req: dict, + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + item = masking_service.create_masking_rule(db, req) + return ResponseModel(data={"id": item.id}) + + +@router.put("/rules/{rule_id}") +def update_masking_rule( + rule_id: int, + req: dict, + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + db_obj = masking_service.get_masking_rule(db, rule_id) + if not db_obj: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="规则不存在") + item = masking_service.update_masking_rule(db, db_obj, req) + return ResponseModel(data={"id": item.id}) + + +@router.delete("/rules/{rule_id}") +def delete_masking_rule( + rule_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_admin), +): + masking_service.delete_masking_rule(db, rule_id) + return ResponseModel(message="删除成功") + + +@router.post("/preview") +def preview_masking( + source_id: int, + table_name: str, + project_id: Optional[int] = None, + limit: int = Query(20, ge=1, le=100), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + result = masking_service.preview_masking(db, source_id, table_name, project_id=project_id, limit=limit) + return ResponseModel(data=result) diff --git a/backend/app/api/v1/project.py b/backend/app/api/v1/project.py index 9f252dde..9dad2d4c 100644 --- a/backend/app/api/v1/project.py +++ b/backend/app/api/v1/project.py @@ -101,9 +101,73 @@ def delete_project( @router.post("/{project_id}/auto-classify") def project_auto_classify( project_id: int, + background: bool = True, db: Session = Depends(get_db), current_user: User = Depends(require_manager), ): - from app.services.classification_engine import run_auto_classification - result = run_auto_classification(db, project_id) - return ResponseModel(data=result) + from app.tasks.classification_tasks import auto_classify_task + from celery.result import AsyncResult + + project = project_service.get_project(db, project_id) + if not project: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") + + if background: + # Check if already running + if project.celery_task_id: + existing = AsyncResult(project.celery_task_id) + if existing.state in ("PENDING", "PROGRESS", "STARTED"): + return ResponseModel(data={"task_id": project.celery_task_id, "status": existing.state}) + + task = auto_classify_task.delay(project_id) + project.celery_task_id = task.id + project.status = "scanning" + db.commit() + return ResponseModel(data={"task_id": task.id, "status": task.state}) + else: + from app.services.classification_engine import run_auto_classification + project.status = "scanning" + db.commit() + result = run_auto_classification(db, project_id) + if result.get("success"): + project.status = "assigning" + else: + project.status = "created" + db.commit() + return ResponseModel(data=result) + + +@router.get("/{project_id}/auto-classify-status") +def project_auto_classify_status( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from celery.result import AsyncResult + import json + + project = project_service.get_project(db, project_id) + if not project: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") + + task_id = project.celery_task_id + if not task_id: + # Return persisted progress if any + progress = json.loads(project.scan_progress) if project.scan_progress else None + return ResponseModel(data={"status": project.status, "progress": progress}) + + result = AsyncResult(task_id) + progress = None + if result.state == "PROGRESS" and result.info: + progress = result.info + elif project.scan_progress: + progress = json.loads(project.scan_progress) + + return ResponseModel(data={ + "status": result.state, + "task_id": task_id, + "progress": progress, + "project_status": project.status, + }) diff --git a/backend/app/api/v1/report.py b/backend/app/api/v1/report.py index 16db7755..35d2e4ea 100644 --- a/backend/app/api/v1/report.py +++ b/backend/app/api/v1/report.py @@ -44,12 +44,30 @@ def get_report_stats( @router.get("/projects/{project_id}/download") def download_report( project_id: int, + format: str = "docx", db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): + if format == "excel": + content = report_service.generate_excel_report(db, project_id) + return Response( + content=content, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": f"attachment; filename=report_project_{project_id}.xlsx"}, + ) content = report_service.generate_classification_report(db, project_id) return Response( content=content, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers={"Content-Disposition": f"attachment; filename=report_project_{project_id}.docx"}, ) + + +@router.get("/projects/{project_id}/summary") +def report_summary( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + data = report_service.get_report_summary(db, project_id) + return ResponseModel(data=data) diff --git a/backend/app/api/v1/risk.py b/backend/app/api/v1/risk.py new file mode 100644 index 00000000..10b319b6 --- /dev/null +++ b/backend/app/api/v1/risk.py @@ -0,0 +1,73 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.services import risk_service +from app.api.deps import get_current_user + +router = APIRouter() + + +@router.post("/recalculate") +def recalculate_risk( + project_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + if project_id: + result = risk_service.calculate_project_risk(db, project_id) + return ResponseModel(data={"project_id": project_id, "risk_score": result.risk_score if result else 0}) + result = risk_service.calculate_all_projects_risk(db) + return ResponseModel(data=result) + + +@router.get("/top") +def risk_top( + entity_type: str = Query("project"), + n: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + items = risk_service.get_risk_top_n(db, entity_type=entity_type, n=n) + return ListResponse( + data=[{ + "id": r.id, + "entity_type": r.entity_type, + "entity_id": r.entity_id, + "entity_name": r.entity_name, + "risk_score": r.risk_score, + "sensitivity_score": r.sensitivity_score, + "exposure_score": r.exposure_score, + "protection_score": r.protection_score, + "updated_at": r.updated_at.isoformat() if r.updated_at else None, + } for r in items], + total=len(items), + page=1, + page_size=n, + ) + + +@router.get("/projects/{project_id}") +def project_risk( + project_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + from app.models.risk import RiskAssessment + item = db.query(RiskAssessment).filter( + RiskAssessment.entity_type == "project", + RiskAssessment.entity_id == project_id, + ).first() + if not item: + return ResponseModel(data=None) + return ResponseModel(data={ + "risk_score": item.risk_score, + "sensitivity_score": item.sensitivity_score, + "exposure_score": item.exposure_score, + "protection_score": item.protection_score, + "details": item.details, + "updated_at": item.updated_at.isoformat() if item.updated_at else None, + }) diff --git a/backend/app/api/v1/schema_change.py b/backend/app/api/v1/schema_change.py new file mode 100644 index 00000000..128d2148 --- /dev/null +++ b/backend/app/api/v1/schema_change.py @@ -0,0 +1,45 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.models.schema_change import SchemaChangeLog +from app.api.deps import get_current_user + +router = APIRouter() + + +@router.get("/logs") +def list_schema_changes( + source_id: Optional[int] = Query(None), + change_type: Optional[str] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + query = db.query(SchemaChangeLog) + if source_id: + query = query.filter(SchemaChangeLog.source_id == source_id) + if change_type: + query = query.filter(SchemaChangeLog.change_type == change_type) + total = query.count() + items = query.order_by(SchemaChangeLog.detected_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + return ListResponse( + data=[{ + "id": log.id, + "source_id": log.source_id, + "database_id": log.database_id, + "table_id": log.table_id, + "column_id": log.column_id, + "change_type": log.change_type, + "old_value": log.old_value, + "new_value": log.new_value, + "detected_at": log.detected_at.isoformat() if log.detected_at else None, + } for log in items], + total=total, + page=page, + page_size=page_size, + ) diff --git a/backend/app/api/v1/unstructured.py b/backend/app/api/v1/unstructured.py new file mode 100644 index 00000000..8fd5c2d4 --- /dev/null +++ b/backend/app/api/v1/unstructured.py @@ -0,0 +1,108 @@ +from typing import Optional +from fastapi import APIRouter, Depends, Query, UploadFile, File +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel, ListResponse +from app.services import unstructured_service +from app.api.deps import get_current_user +from app.core.events import minio_client +from app.core.config import settings +from app.models.metadata import UnstructuredFile + +router = APIRouter() + + +@router.post("/upload") +def upload_file( + file: UploadFile = File(...), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + # Determine file type + filename = file.filename or "unknown" + ext = filename.split(".")[-1].lower() if "." in filename else "" + type_map = { + "docx": "word", "doc": "word", + "xlsx": "excel", "xls": "excel", + "pdf": "pdf", + "txt": "txt", + } + file_type = type_map.get(ext, "unknown") + + # Upload to MinIO + storage_path = f"unstructured/{current_user.id}/{filename}" + try: + data = file.file.read() + minio_client.put_object( + settings.MINIO_BUCKET_NAME, + storage_path, + data=data, + length=len(data), + content_type=file.content_type or "application/octet-stream", + ) + except Exception as e: + return ResponseModel(message=f"上传失败: {e}") + + db_obj = UnstructuredFile( + original_name=filename, + file_type=file_type, + file_size=len(data), + storage_path=storage_path, + status="pending", + created_by=current_user.id, + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + + # Trigger processing + try: + result = unstructured_service.process_unstructured_file(db, db_obj.id) + return ResponseModel(data={"id": db_obj.id, "matches": result.get("matches", []), "status": "processed"}) + except Exception as e: + return ResponseModel(data={"id": db_obj.id, "status": "error", "error": str(e)}) + + +@router.get("/files") +def list_files( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=500), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + query = db.query(UnstructuredFile).filter(UnstructuredFile.created_by == current_user.id) + total = query.count() + items = query.order_by(UnstructuredFile.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + return ListResponse( + data=[{ + "id": f.id, + "original_name": f.original_name, + "file_type": f.file_type, + "file_size": f.file_size, + "status": f.status, + "analysis_result": f.analysis_result, + "created_at": f.created_at.isoformat() if f.created_at else None, + } for f in items], + total=total, + page=page, + page_size=page_size, + ) + + +@router.post("/files/{file_id}/reprocess") +def reprocess_file( + file_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + file_obj = db.query(UnstructuredFile).filter( + UnstructuredFile.id == file_id, + UnstructuredFile.created_by == current_user.id, + ).first() + if not file_obj: + from fastapi import HTTPException, status + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文件不存在") + result = unstructured_service.process_unstructured_file(db, file_id) + return ResponseModel(data={"matches": result.get("matches", []), "status": "processed"}) diff --git a/backend/app/api/v1/watermark.py b/backend/app/api/v1/watermark.py new file mode 100644 index 00000000..2c562ff4 --- /dev/null +++ b/backend/app/api/v1/watermark.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.models.user import User +from app.schemas.common import ResponseModel +from app.services import watermark_service +from app.api.deps import get_current_user + +router = APIRouter() + + +@router.post("/trace") +def trace_watermark( + req: dict, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + text = req.get("text", "") + result = watermark_service.trace_watermark(db, text) + if not result: + return ResponseModel(data=None, message="未检测到水印") + return ResponseModel(data=result) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 5bd5ff7b..b1644871 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -10,6 +10,11 @@ class Settings(BaseSettings): DATABASE_URL: str = "postgresql+psycopg2://pdg:pdg_secret_2024@localhost:5432/prop_data_guard" REDIS_URL: str = "redis://localhost:6379/0" + # Database password encryption key (Fernet-compatible base64, 32 bytes) + # If empty, will be derived from SECRET_KEY for backward compatibility. + # STRONGLY recommended to set this explicitly in production. + DB_ENCRYPTION_KEY: str = "" + SECRET_KEY: str = "prop-data-guard-super-secret-key-change-in-production" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 6fa2b705..b34e7f06 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -2,6 +2,14 @@ from app.models.user import User, Role, Dept, UserRole from app.models.metadata import DataSource, Database, DataTable, DataColumn, UnstructuredFile from app.models.classification import Category, DataLevel, RecognitionRule, ClassificationTemplate from app.models.project import ClassificationProject, ClassificationTask, ClassificationResult, ClassificationChange +from app.models.ml import MLModelVersion +from app.models.masking import MaskingRule +from app.models.watermark import WatermarkLog +from app.models.schema_change import SchemaChangeLog +from app.models.risk import RiskAssessment +from app.models.compliance import ComplianceRule, ComplianceIssue +from app.models.alert import AlertRule, AlertRecord, WorkOrder +from app.models.api_asset import APIAsset, APIEndpoint from app.models.log import OperationLog __all__ = [ @@ -9,5 +17,12 @@ __all__ = [ "DataSource", "Database", "DataTable", "DataColumn", "UnstructuredFile", "Category", "DataLevel", "RecognitionRule", "ClassificationTemplate", "ClassificationProject", "ClassificationTask", "ClassificationResult", "ClassificationChange", + "MLModelVersion", + "MaskingRule", + "WatermarkLog", + "SchemaChangeLog", + "RiskAssessment", + "ComplianceRule", + "ComplianceIssue", "OperationLog", ] diff --git a/backend/app/models/alert.py b/backend/app/models/alert.py new file mode 100644 index 00000000..60b1d08b --- /dev/null +++ b/backend/app/models/alert.py @@ -0,0 +1,46 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey, JSON +from sqlalchemy.orm import relationship +from app.core.database import Base + + +class AlertRule(Base): + __tablename__ = "alert_rule" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(200), nullable=False) + trigger_condition = Column(String(50), nullable=False) # l5_count, risk_score, schema_change + threshold = Column(Integer, default=0) + severity = Column(String(20), default="medium") + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.utcnow) + + +class AlertRecord(Base): + __tablename__ = "alert_record" + + id = Column(Integer, primary_key=True, index=True) + rule_id = Column(Integer, ForeignKey("alert_rule.id"), nullable=False) + title = Column(String(200), nullable=False) + content = Column(Text) + severity = Column(String(20), default="medium") + status = Column(String(20), default="open") # open, acknowledged, resolved + created_at = Column(DateTime, default=datetime.utcnow) + + rule = relationship("AlertRule") + + +class WorkOrder(Base): + __tablename__ = "work_order" + + id = Column(Integer, primary_key=True, index=True) + alert_id = Column(Integer, ForeignKey("alert_record.id"), nullable=True) + title = Column(String(200), nullable=False) + description = Column(Text) + assignee_id = Column(Integer, ForeignKey("sys_user.id"), nullable=True) + status = Column(String(20), default="open") # open, in_progress, resolved + resolution = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + resolved_at = Column(DateTime, nullable=True) + + assignee = relationship("User") diff --git a/backend/app/models/api_asset.py b/backend/app/models/api_asset.py new file mode 100644 index 00000000..16499b7d --- /dev/null +++ b/backend/app/models/api_asset.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON, ForeignKey, BigInteger +from sqlalchemy.orm import relationship +from app.core.database import Base +from datetime import datetime + +class APIAsset(Base): + __tablename__ = "api_asset" + id = Column(Integer, primary_key=True, index=True) + name = Column(String(200), nullable=False) + base_url = Column(String(500), nullable=False) + swagger_url = Column(String(500), nullable=True) + auth_type = Column(String(50), default="none") # none, bearer, api_key, basic + headers = Column(JSON, default=dict) + description = Column(Text, nullable=True) + scan_status = Column(String(20), default="idle") # idle, scanning, completed, failed + total_endpoints = Column(Integer, default=0) + sensitive_endpoints = Column(Integer, default=0) + created_by = Column(Integer, ForeignKey("sys_user.id"), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + endpoints = relationship("APIEndpoint", back_populates="asset", cascade="all, delete-orphan") + creator = relationship("User", foreign_keys=[created_by]) + +class APIEndpoint(Base): + __tablename__ = "api_endpoint" + id = Column(Integer, primary_key=True, index=True) + asset_id = Column(Integer, ForeignKey("api_asset.id"), nullable=False) + method = Column(String(10), nullable=False) # GET, POST, PUT, DELETE, etc. + path = Column(String(500), nullable=False) + summary = Column(String(500), nullable=True) + tags = Column(JSON, default=list) + parameters = Column(JSON, default=list) + request_body_schema = Column(JSON, nullable=True) + response_schema = Column(JSON, nullable=True) + sensitive_fields = Column(JSON, default=list) # detected PII fields + risk_level = Column(String(20), default="low") # low, medium, high, critical + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.utcnow) + + asset = relationship("APIAsset", back_populates="endpoints") diff --git a/backend/app/models/compliance.py b/backend/app/models/compliance.py new file mode 100644 index 00000000..91926623 --- /dev/null +++ b/backend/app/models/compliance.py @@ -0,0 +1,33 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON +from app.core.database import Base + + +class ComplianceRule(Base): + __tablename__ = "compliance_rule" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(200), nullable=False) + standard = Column(String(50), nullable=False) # dengbao, pipl, gdpr + description = Column(Text) + check_logic = Column(String(50), nullable=False) # check_masking, check_encryption, check_audit, check_level + severity = Column(String(20), default="medium") # low, medium, high, critical + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.utcnow) + + +class ComplianceIssue(Base): + __tablename__ = "compliance_issue" + + id = Column(Integer, primary_key=True, index=True) + rule_id = Column(Integer, nullable=False) + project_id = Column(Integer, nullable=True) + entity_type = Column(String(20), nullable=False) # project, source, column + entity_id = Column(Integer, nullable=False) + entity_name = Column(String(200)) + severity = Column(String(20), default="medium") + description = Column(Text) + suggestion = Column(Text) + status = Column(String(20), default="open") # open, resolved, ignored + resolved_at = Column(DateTime, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) diff --git a/backend/app/models/lineage.py b/backend/app/models/lineage.py new file mode 100644 index 00000000..aa5b36a4 --- /dev/null +++ b/backend/app/models/lineage.py @@ -0,0 +1,16 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Text, DateTime +from app.core.database import Base + + +class DataLineage(Base): + __tablename__ = "data_lineage" + + id = Column(Integer, primary_key=True, index=True) + source_table = Column(String(200), nullable=False) + source_column = Column(String(200), nullable=True) + target_table = Column(String(200), nullable=False) + target_column = Column(String(200), nullable=True) + relation_type = Column(String(20), default="direct") # direct, derived, lookup + script_content = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) diff --git a/backend/app/models/masking.py b/backend/app/models/masking.py new file mode 100644 index 00000000..c4bdc5f7 --- /dev/null +++ b/backend/app/models/masking.py @@ -0,0 +1,22 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Text +from sqlalchemy.orm import relationship +from app.core.database import Base + + +class MaskingRule(Base): + __tablename__ = "masking_rule" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + level_id = Column(Integer, ForeignKey("data_level.id"), nullable=True) + category_id = Column(Integer, ForeignKey("category.id"), nullable=True) + algorithm = Column(String(20), nullable=False) # mask, truncate, hash, generalize, replace + params = Column(JSON, default=dict) # algorithm-specific params + is_active = Column(Boolean, default=True) + description = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + level = relationship("DataLevel") + category = relationship("Category") diff --git a/backend/app/models/metadata.py b/backend/app/models/metadata.py index e3a7a070..ee234eba 100644 --- a/backend/app/models/metadata.py +++ b/backend/app/models/metadata.py @@ -1,5 +1,5 @@ from datetime import datetime -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, BigInteger +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, BigInteger, JSON from sqlalchemy.orm import relationship from app.core.database import Base @@ -36,6 +36,10 @@ class Database(Base): charset = Column(String(50)) table_count = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.utcnow) + last_scanned_at = Column(DateTime, nullable=True) + checksum = Column(String(64), nullable=True) + is_deleted = Column(Boolean, default=False) + deleted_at = Column(DateTime, nullable=True) source = relationship("DataSource", back_populates="databases") tables = relationship("DataTable", back_populates="database", cascade="all, delete-orphan") @@ -51,6 +55,10 @@ class DataTable(Base): row_count = Column(BigInteger, default=0) column_count = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.utcnow) + last_scanned_at = Column(DateTime, nullable=True) + checksum = Column(String(64), nullable=True) + is_deleted = Column(Boolean, default=False) + deleted_at = Column(DateTime, nullable=True) database = relationship("Database", back_populates="tables") columns = relationship("DataColumn", back_populates="table", cascade="all, delete-orphan") @@ -68,6 +76,10 @@ class DataColumn(Base): is_nullable = Column(Boolean, default=True) sample_data = Column(Text) # JSON array of sample values created_at = Column(DateTime, default=datetime.utcnow) + last_scanned_at = Column(DateTime, nullable=True) + checksum = Column(String(64), nullable=True) + is_deleted = Column(Boolean, default=False) + deleted_at = Column(DateTime, nullable=True) table = relationship("DataTable", back_populates="columns") @@ -81,6 +93,7 @@ class UnstructuredFile(Base): file_size = Column(BigInteger) storage_path = Column(String(500)) extracted_text = Column(Text) + analysis_result = Column(JSON, nullable=True) # JSON: {matches: [{rule_name, category, level, snippet}]} status = Column(String(20), default="pending") # pending, processed, error created_by = Column(Integer, ForeignKey("sys_user.id")) created_at = Column(DateTime, default=datetime.utcnow) diff --git a/backend/app/models/ml.py b/backend/app/models/ml.py new file mode 100644 index 00000000..33fd3a79 --- /dev/null +++ b/backend/app/models/ml.py @@ -0,0 +1,18 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text +from app.core.database import Base + + +class MLModelVersion(Base): + __tablename__ = "ml_model_version" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + model_path = Column(String(500), nullable=False) # joblib dump path + vectorizer_path = Column(String(500), nullable=False) # tfidf vectorizer path + accuracy = Column(Float, default=0.0) + train_samples = Column(Integer, default=0) + train_date = Column(DateTime, default=datetime.utcnow) + is_active = Column(Boolean, default=False) + description = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 43e90258..00cff932 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -48,6 +48,10 @@ class ClassificationProject(Base): created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + # Async classification tracking + celery_task_id = Column(String(100), nullable=True) + scan_progress = Column(Text, nullable=True) # JSON: {"scanned": 0, "matched": 0, "total": 0} + template = relationship("ClassificationTemplate") tasks = relationship("ClassificationTask", back_populates="project", cascade="all, delete-orphan") results = relationship("ClassificationResult", back_populates="project", cascade="all, delete-orphan") diff --git a/backend/app/models/risk.py b/backend/app/models/risk.py new file mode 100644 index 00000000..c40fd994 --- /dev/null +++ b/backend/app/models/risk.py @@ -0,0 +1,20 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, JSON +from sqlalchemy.orm import relationship +from app.core.database import Base + + +class RiskAssessment(Base): + __tablename__ = "risk_assessment" + + id = Column(Integer, primary_key=True, index=True) + entity_type = Column(String(20), nullable=False) # project, source, table, field + entity_id = Column(Integer, nullable=False) + entity_name = Column(String(200)) + risk_score = Column(Float, default=0.0) # 0-100 + sensitivity_score = Column(Float, default=0.0) + exposure_score = Column(Float, default=0.0) + protection_score = Column(Float, default=0.0) + details = Column(JSON, default=dict) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/backend/app/models/schema_change.py b/backend/app/models/schema_change.py new file mode 100644 index 00000000..f9570c00 --- /dev/null +++ b/backend/app/models/schema_change.py @@ -0,0 +1,23 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from app.core.database import Base + + +class SchemaChangeLog(Base): + __tablename__ = "schema_change_log" + + id = Column(Integer, primary_key=True, index=True) + source_id = Column(Integer, ForeignKey("data_source.id"), nullable=False) + database_id = Column(Integer, ForeignKey("meta_database.id"), nullable=True) + table_id = Column(Integer, ForeignKey("meta_table.id"), nullable=True) + column_id = Column(Integer, ForeignKey("meta_column.id"), nullable=True) + change_type = Column(String(20), nullable=False) # add_table, drop_table, add_column, drop_column, change_type, change_comment + old_value = Column(Text) + new_value = Column(Text) + detected_at = Column(DateTime, default=datetime.utcnow) + + source = relationship("DataSource") + database = relationship("Database") + table = relationship("DataTable") + column = relationship("DataColumn") diff --git a/backend/app/models/watermark.py b/backend/app/models/watermark.py new file mode 100644 index 00000000..f23d19fa --- /dev/null +++ b/backend/app/models/watermark.py @@ -0,0 +1,17 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from app.core.database import Base + + +class WatermarkLog(Base): + __tablename__ = "watermark_log" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("sys_user.id"), nullable=False) + export_type = Column(String(20), default="csv") # csv, excel, txt + data_scope = Column(Text) # JSON: {source_id, table_name, row_count} + watermark_key = Column(String(64), nullable=False) # random key for this export + created_at = Column(DateTime, default=datetime.utcnow) + + user = relationship("User") diff --git a/backend/app/services/alert_service.py b/backend/app/services/alert_service.py new file mode 100644 index 00000000..42ec6818 --- /dev/null +++ b/backend/app/services/alert_service.py @@ -0,0 +1,92 @@ +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime + +from app.models.alert import AlertRule, AlertRecord, WorkOrder +from app.models.project import ClassificationProject, ClassificationResult +from app.models.risk import RiskAssessment + + +def init_builtin_alert_rules(db: Session): + if db.query(AlertRule).first(): + return + rules = [ + AlertRule(name="L5字段数量突增", trigger_condition="l5_count", threshold=5, severity="high"), + AlertRule(name="项目风险分过高", trigger_condition="risk_score", threshold=80, severity="critical"), + AlertRule(name="Schema新增敏感字段", trigger_condition="schema_change", threshold=1, severity="medium"), + ] + for r in rules: + db.add(r) + db.commit() + + +def check_alerts(db: Session) -> List[AlertRecord]: + """Run alert checks and create records.""" + rules = db.query(AlertRule).filter(AlertRule.is_active == True).all() + records = [] + for rule in rules: + if rule.trigger_condition == "l5_count": + projects = db.query(ClassificationProject).all() + for p in projects: + l5_count = db.query(ClassificationResult).filter( + ClassificationResult.project_id == p.id, + ClassificationResult.level_id.isnot(None), + ).join(ClassificationResult.level).filter( + ClassificationResult.level.has(code="L5") + ).count() + if l5_count >= rule.threshold: + rec = AlertRecord( + rule_id=rule.id, + title=f"项目 {p.name} L5字段数量达到 {l5_count}", + content=f"阈值: {rule.threshold}", + severity=rule.severity, + ) + db.add(rec) + records.append(rec) + elif rule.trigger_condition == "risk_score": + risks = db.query(RiskAssessment).filter( + RiskAssessment.entity_type == "project", + RiskAssessment.risk_score >= rule.threshold, + ).all() + for rsk in risks: + rec = AlertRecord( + rule_id=rule.id, + title=f"项目 {rsk.entity_name} 风险分 {rsk.risk_score}", + content=f"阈值: {rule.threshold}", + severity=rule.severity, + ) + db.add(rec) + records.append(rec) + db.commit() + return records + + +def create_work_order(db: Session, alert_id: int, title: str, description: str, assignee_id: Optional[int] = None) -> WorkOrder: + wo = WorkOrder( + alert_id=alert_id, + title=title, + description=description, + assignee_id=assignee_id, + ) + db.add(wo) + db.commit() + db.refresh(wo) + return wo + + +def update_work_order_status(db: Session, wo_id: int, status: str, resolution: str = None) -> WorkOrder: + wo = db.query(WorkOrder).filter(WorkOrder.id == wo_id).first() + if wo: + wo.status = status + if resolution: + wo.resolution = resolution + if status == "resolved": + wo.resolved_at = datetime.utcnow() + # Also resolve linked alert + if wo.alert_id: + alert = db.query(AlertRecord).filter(AlertRecord.id == wo.alert_id).first() + if alert: + alert.status = "resolved" + db.commit() + db.refresh(wo) + return wo diff --git a/backend/app/services/api_asset_service.py b/backend/app/services/api_asset_service.py new file mode 100644 index 00000000..e5861510 --- /dev/null +++ b/backend/app/services/api_asset_service.py @@ -0,0 +1,174 @@ +import requests, json +from typing import Optional +from sqlalchemy.orm import Session +from app.models.api_asset import APIAsset, APIEndpoint +from app.models.metadata import DataColumn +from app.services.classification_engine import match_rule + +# Simple sensitive keywords for API field detection +SENSITIVE_KEYWORDS = [ + "password", "pwd", "passwd", "secret", "token", "credit_card", "card_no", + "bank_account", "bank_card", "id_card", "id_number", "phone", "mobile", + "email", "address", "name", "age", "gender", "salary", "income", + "health", "medical", "biometric", "fingerprint", "face", +] + +def _is_sensitive_field(name: str, schema: dict) -> tuple[bool, str]: + low = name.lower() + for kw in SENSITIVE_KEYWORDS: + if kw in low: + return True, f"keyword:{kw}" + # Check description / format hints + desc = str(schema.get("description", "")).lower() + fmt = str(schema.get("format", "")).lower() + if "email" in fmt or "email" in desc: + return True, "format:email" + if "uuid" in fmt and "user" in low: + return True, "format:user-uuid" + return False, "" + +def _extract_fields(schema: dict, prefix: str = "") -> list[dict]: + fields = [] + if not isinstance(schema, dict): + return fields + props = schema.get("properties", {}) + for k, v in props.items(): + full_name = f"{prefix}.{k}" if prefix else k + sensitive, reason = _is_sensitive_field(k, v) + if sensitive: + fields.append({"name": full_name, "type": v.get("type", "unknown"), "reason": reason}) + # nested object + if v.get("type") == "object" and "properties" in v: + fields.extend(_extract_fields(v, full_name)) + # array items + if v.get("type") == "array" and isinstance(v.get("items"), dict): + fields.extend(_extract_fields(v["items"], full_name + "[]")) + return fields + +def _risk_level_from_fields(fields: list[dict]) -> str: + if not fields: + return "low" + high_keywords = {"password", "secret", "token", "credit_card", "bank_account", "biometric", "fingerprint", "face"} + for f in fields: + for kw in high_keywords: + if kw in f["name"].lower(): + return "critical" if kw in {"password", "secret", "token", "biometric"} else "high" + return "medium" + +def scan_swagger(db: Session, asset_id: int) -> dict: + asset = db.query(APIAsset).filter(APIAsset.id == asset_id).first() + if not asset: + return {"success": False, "error": "Asset not found"} + if not asset.swagger_url: + return {"success": False, "error": "No swagger_url configured"} + + asset.scan_status = "scanning" + db.commit() + try: + headers = dict(asset.headers or {}) + resp = requests.get(asset.swagger_url, headers=headers, timeout=30) + resp.raise_for_status() + spec = resp.json() + + # Clear previous endpoints + db.query(APIEndpoint).filter(APIEndpoint.asset_id == asset_id).delete() + + paths = spec.get("paths", {}) + total = 0 + sensitive_total = 0 + for path, methods in paths.items(): + for method, detail in methods.items(): + if method.lower() not in {"get","post","put","patch","delete","head","options"}: + continue + total += 1 + parameters = [] + for p in detail.get("parameters", []): + parameters.append({"name": p.get("name"), "in": p.get("in"), "required": p.get("required", False), "type": p.get("schema",{}).get("type","string")}) + req_schema = detail.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema") + resp_schema = None + for code, resp_detail in (detail.get("responses", {}).get("200", {}).get("content", {}) or {}).items(): + if isinstance(resp_detail, dict) and "schema" in resp_detail: + resp_schema = resp_detail["schema"] + break + # Also try generic 200 + if resp_schema is None: + ok = detail.get("responses", {}).get("200", {}) + for ct, cd in ok.get("content", {}).items(): + if isinstance(cd, dict) and "schema" in cd: + resp_schema = cd["schema"] + break + + fields = [] + if req_schema: + fields.extend(_extract_fields(req_schema)) + if resp_schema: + fields.extend(_extract_fields(resp_schema)) + # dedup + seen = set() + unique_fields = [] + for f in fields: + if f["name"] not in seen: + seen.add(f["name"]) + unique_fields.append(f) + + risk = _risk_level_from_fields(unique_fields) + ep = APIEndpoint( + asset_id=asset_id, + method=method.upper(), + path=path, + summary=detail.get("summary", ""), + tags=detail.get("tags", []), + parameters=parameters, + request_body_schema=req_schema, + response_schema=resp_schema, + sensitive_fields=unique_fields, + risk_level=risk, + ) + db.add(ep) + if unique_fields: + sensitive_total += 1 + + asset.scan_status = "completed" + asset.total_endpoints = total + asset.sensitive_endpoints = sensitive_total + asset.updated_at = __import__('datetime').datetime.utcnow() + db.commit() + return {"success": True, "total": total, "sensitive": sensitive_total} + except Exception as e: + asset.scan_status = "failed" + db.commit() + return {"success": False, "error": str(e)} + +def create_asset(db: Session, data: dict, user_id: Optional[int] = None) -> APIAsset: + asset = APIAsset( + name=data["name"], + base_url=data["base_url"], + swagger_url=data.get("swagger_url"), + auth_type=data.get("auth_type", "none"), + headers=data.get("headers"), + description=data.get("description"), + created_by=user_id, + ) + db.add(asset) + db.commit() + db.refresh(asset) + return asset + +def update_asset(db: Session, asset_id: int, data: dict) -> Optional[APIAsset]: + asset = db.query(APIAsset).filter(APIAsset.id == asset_id).first() + if not asset: + return None + for k, v in data.items(): + if hasattr(asset, k): + setattr(asset, k, v) + db.commit() + db.refresh(asset) + return asset + +def delete_asset(db: Session, asset_id: int) -> bool: + asset = db.query(APIAsset).filter(APIAsset.id == asset_id).first() + if not asset: + return False + db.delete(asset) + db.commit() + return True diff --git a/backend/app/services/classification_engine.py b/backend/app/services/classification_engine.py index 234a364d..487f8192 100644 --- a/backend/app/services/classification_engine.py +++ b/backend/app/services/classification_engine.py @@ -51,11 +51,39 @@ def match_rule(rule: RecognitionRule, column: DataColumn) -> Tuple[bool, float]: if t.strip().lower() in enums: return True, 0.90 + elif rule.rule_type == "similarity": + benchmarks = [b.strip().lower() for b in rule.rule_content.split(",") if b.strip()] + if not benchmarks: + return False, 0.0 + from sklearn.feature_extraction.text import TfidfVectorizer + from sklearn.metrics.pairwise import cosine_similarity + texts = [t.lower() for t in targets] + benchmarks + try: + vectorizer = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 3)) + tfidf = vectorizer.fit_transform(texts) + target_vecs = tfidf[:len(targets)] + bench_vecs = tfidf[len(targets):] + sim_matrix = cosine_similarity(target_vecs, bench_vecs) + max_sim = float(sim_matrix.max()) + if max_sim >= 0.75: + return True, round(min(max_sim, 0.99), 4) + except Exception: + pass + return False, 0.0 -def run_auto_classification(db: Session, project_id: int, source_ids: Optional[List[int]] = None) -> dict: - """Run automatic classification for a project.""" +def run_auto_classification( + db: Session, + project_id: int, + source_ids: Optional[List[int]] = None, + progress_callback=None, +) -> dict: + """Run automatic classification for a project. + + Args: + progress_callback: Optional callable(scanned, matched, total) to report progress. + """ project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first() if not project: return {"success": False, "message": "项目不存在"} @@ -82,7 +110,10 @@ def run_auto_classification(db: Session, project_id: int, source_ids: Optional[L columns = columns_query.all() matched_count = 0 - for col in columns: + total = len(columns) + report_interval = max(1, total // 20) # report ~20 times + + for idx, col in enumerate(columns): # Check if already has a result for this project existing = db.query(ClassificationResult).filter( ClassificationResult.project_id == project_id, @@ -121,12 +152,20 @@ def run_auto_classification(db: Session, project_id: int, source_ids: Optional[L # Increment hit count best_rule.hit_count = (best_rule.hit_count or 0) + 1 + # Report progress periodically + if progress_callback and (idx + 1) % report_interval == 0: + progress_callback(scanned=idx + 1, matched=matched_count, total=total) + db.commit() + # Final progress report + if progress_callback: + progress_callback(scanned=total, matched=matched_count, total=total) + return { "success": True, - "message": f"自动分类完成,共扫描 {len(columns)} 个字段,命中 {matched_count} 个", - "scanned": len(columns), + "message": f"自动分类完成,共扫描 {total} 个字段,命中 {matched_count} 个", + "scanned": total, "matched": matched_count, } diff --git a/backend/app/services/compliance_service.py b/backend/app/services/compliance_service.py new file mode 100644 index 00000000..0feead10 --- /dev/null +++ b/backend/app/services/compliance_service.py @@ -0,0 +1,122 @@ +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime + +from app.models.compliance import ComplianceRule, ComplianceIssue +from app.models.project import ClassificationProject, ClassificationResult +from app.models.classification import DataLevel +from app.models.masking import MaskingRule + + +def init_builtin_rules(db: Session): + """Initialize built-in compliance rules.""" + if db.query(ComplianceRule).first(): + return + rules = [ + ComplianceRule(name="L4/L5字段未配置脱敏", standard="dengbao", description="等保2.0要求:四级及以上数据应进行脱敏处理", check_logic="check_masking", severity="high"), + ComplianceRule(name="L5字段缺乏加密存储措施", standard="dengbao", description="等保2.0要求:五级数据应加密存储", check_logic="check_encryption", severity="critical"), + ComplianceRule(name="个人敏感信息处理未授权", standard="pipl", description="个人信息保护法:处理敏感个人信息应取得单独同意", check_logic="check_level", severity="high"), + ComplianceRule(name="数据跨境传输未评估", standard="gdpr", description="GDPR:个人数据跨境传输需进行影响评估", check_logic="check_audit", severity="medium"), + ] + for r in rules: + db.add(r) + db.commit() + + +def scan_compliance(db: Session, project_id: Optional[int] = None) -> List[ComplianceIssue]: + """Run compliance scan and generate issues.""" + rules = db.query(ComplianceRule).filter(ComplianceRule.is_active == True).all() + issues = [] + + # Get masking rules for check_masking logic + masking_rules = db.query(MaskingRule).filter(MaskingRule.is_active == True).all() + masking_level_ids = {r.level_id for r in masking_rules if r.level_id} + + query = db.query(ClassificationProject) + if project_id: + query = query.filter(ClassificationProject.id == project_id) + projects = query.all() + + for project in projects: + results = db.query(ClassificationResult).filter( + ClassificationResult.project_id == project.id, + ClassificationResult.level_id.isnot(None), + ).all() + + for r in results: + if not r.level: + continue + level_code = r.level.code + + for rule in rules: + matched = False + desc = "" + suggestion = "" + + if rule.check_logic == "check_masking" and level_code in ("L4", "L5"): + if r.level_id not in masking_level_ids: + matched = True + desc = f"字段 '{r.column.name if r.column else '未知'}' 为 {level_code} 级,但未配置脱敏规则" + suggestion = "请在【数据脱敏】模块为该分级配置脱敏策略" + + elif rule.check_logic == "check_encryption" and level_code == "L5": + # Placeholder: no encryption check in MVP, always flag + matched = True + desc = f"字段 '{r.column.name if r.column else '未知'}' 为 L5 级核心数据,建议确认是否加密存储" + suggestion = "请确认该字段在数据库中已加密存储" + + elif rule.check_logic == "check_level" and level_code in ("L4", "L5"): + if r.source == "auto": + matched = True + desc = f"个人敏感字段 '{r.column.name if r.column else '未知'}' 目前为自动识别,建议人工复核并确认授权" + suggestion = "请人工确认该字段的处理已取得合法授权" + + elif rule.check_logic == "check_audit": + # Placeholder for cross-border check + pass + + if matched: + # Check if open issue already exists + existing = db.query(ComplianceIssue).filter( + ComplianceIssue.rule_id == rule.id, + ComplianceIssue.project_id == project.id, + ComplianceIssue.entity_type == "column", + ComplianceIssue.entity_id == (r.column_id or 0), + ComplianceIssue.status == "open", + ).first() + if not existing: + issue = ComplianceIssue( + rule_id=rule.id, + project_id=project.id, + entity_type="column", + entity_id=r.column_id or 0, + entity_name=r.column.name if r.column else "未知", + severity=rule.severity, + description=desc, + suggestion=suggestion, + ) + db.add(issue) + issues.append(issue) + + db.commit() + return issues + + +def list_issues(db: Session, project_id: Optional[int] = None, status: Optional[str] = None, page: int = 1, page_size: int = 20): + query = db.query(ComplianceIssue) + if project_id: + query = query.filter(ComplianceIssue.project_id == project_id) + if status: + query = query.filter(ComplianceIssue.status == status) + total = query.count() + items = query.order_by(ComplianceIssue.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + return items, total + + +def resolve_issue(db: Session, issue_id: int): + issue = db.query(ComplianceIssue).filter(ComplianceIssue.id == issue_id).first() + if issue: + issue.status = "resolved" + issue.resolved_at = datetime.utcnow() + db.commit() + return issue diff --git a/backend/app/services/datasource_service.py b/backend/app/services/datasource_service.py index 52e7b52c..0e4808a4 100644 --- a/backend/app/services/datasource_service.py +++ b/backend/app/services/datasource_service.py @@ -1,3 +1,6 @@ +import base64 +import hashlib +import logging from typing import Optional, List, Tuple from sqlalchemy.orm import Session from fastapi import HTTPException, status @@ -7,9 +10,28 @@ from app.models.metadata import DataSource from app.schemas.datasource import DataSourceCreate, DataSourceUpdate, DataSourceTest from app.core.config import settings -# Simple AES-like symmetric encryption for DB passwords -# In production, use a proper KMS -_fernet = Fernet(Fernet.generate_key()) +logger = logging.getLogger(__name__) + + +def _get_fernet() -> Fernet: + """Initialize Fernet with a stable key. + + If DB_ENCRYPTION_KEY is set, use it directly. + Otherwise derive deterministically from SECRET_KEY for backward compatibility. + """ + if settings.DB_ENCRYPTION_KEY: + key = settings.DB_ENCRYPTION_KEY.encode() + else: + logger.warning( + "DB_ENCRYPTION_KEY is not set. Deriving encryption key from SECRET_KEY. " + "Please set DB_ENCRYPTION_KEY explicitly via environment variable or .env file." + ) + digest = hashlib.sha256(settings.SECRET_KEY.encode()).digest() + key = base64.urlsafe_b64encode(digest) + return Fernet(key) + + +_fernet = _get_fernet() def _encrypt_password(password: str) -> str: diff --git a/backend/app/services/lineage_service.py b/backend/app/services/lineage_service.py new file mode 100644 index 00000000..2c7b4a5d --- /dev/null +++ b/backend/app/services/lineage_service.py @@ -0,0 +1,65 @@ +import re +from typing import List, Optional +from sqlalchemy.orm import Session + +from app.models.lineage import DataLineage + + +def _extract_tables(sql: str) -> List[str]: + """Extract table names from SQL using regex (simple heuristic).""" + # Normalize SQL + sql = re.sub(r"--.*?\n", " ", sql) + sql = re.sub(r"/\*.*?\*/", " ", sql, flags=re.DOTALL) + sql = sql.lower() + tables = set() + # FROM / JOIN / INTO + for pattern in [r"\bfrom\s+([a-z_][a-z0-9_]*)", r"\bjoin\s+([a-z_][a-z0-9_]*)"]: + for m in re.finditer(pattern, sql): + tables.add(m.group(1)) + return sorted(tables) + + +def parse_sql_lineage(db: Session, sql: str, target_table: str) -> List[DataLineage]: + """Parse SQL and create lineage records pointing to target_table.""" + source_tables = _extract_tables(sql) + records = [] + for st in source_tables: + if st == target_table: + continue + existing = db.query(DataLineage).filter( + DataLineage.source_table == st, + DataLineage.target_table == target_table, + ).first() + if not existing: + rec = DataLineage( + source_table=st, + target_table=target_table, + relation_type="direct", + script_content=sql[:2000], + ) + db.add(rec) + records.append(rec) + db.commit() + return records + + +def get_lineage_graph(db: Session, table_name: Optional[str] = None) -> dict: + """Build graph data for ECharts.""" + query = db.query(DataLineage) + if table_name: + query = query.filter( + (DataLineage.source_table == table_name) | (DataLineage.target_table == table_name) + ) + items = query.limit(500).all() + + nodes = {} + links = [] + for item in items: + nodes[item.source_table] = {"name": item.source_table, "category": 0} + nodes[item.target_table] = {"name": item.target_table, "category": 1} + links.append({"source": item.source_table, "target": item.target_table, "value": item.relation_type}) + + return { + "nodes": list(nodes.values()), + "links": links, + } diff --git a/backend/app/services/masking_service.py b/backend/app/services/masking_service.py new file mode 100644 index 00000000..b5bdca9c --- /dev/null +++ b/backend/app/services/masking_service.py @@ -0,0 +1,195 @@ +import hashlib +from typing import Optional, Dict +from sqlalchemy.orm import Session +from fastapi import HTTPException, status +from app.models.metadata import DataSource, Database, DataTable, DataColumn +from app.models.project import ClassificationResult +from app.models.masking import MaskingRule +from app.services.datasource_service import get_datasource, _decrypt_password + + +def get_masking_rule(db: Session, rule_id: int): + return db.query(MaskingRule).filter(MaskingRule.id == rule_id).first() + + +def list_masking_rules(db: Session, level_id=None, category_id=None, page=1, page_size=20): + query = db.query(MaskingRule).filter(MaskingRule.is_active == True) + if level_id: + query = query.filter(MaskingRule.level_id == level_id) + if category_id: + query = query.filter(MaskingRule.category_id == category_id) + total = query.count() + items = query.offset((page - 1) * page_size).limit(page_size).all() + return items, total + + +def create_masking_rule(db: Session, data: dict): + db_obj = MaskingRule(**data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + +def update_masking_rule(db: Session, db_obj: MaskingRule, data: dict): + for k, v in data.items(): + if v is not None: + setattr(db_obj, k, v) + db.commit() + db.refresh(db_obj) + return db_obj + + +def delete_masking_rule(db: Session, rule_id: int): + db_obj = get_masking_rule(db, rule_id) + if not db_obj: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="规则不存在") + db.delete(db_obj) + db.commit() + + +def _apply_mask(value, params): + if not value: + return value + keep_prefix = params.get("keep_prefix", 3) + keep_suffix = params.get("keep_suffix", 4) + mask_char = params.get("mask_char", "*") + if len(value) <= keep_prefix + keep_suffix: + return mask_char * len(value) + return value[:keep_prefix] + mask_char * (len(value) - keep_prefix - keep_suffix) + value[-keep_suffix:] + + +def _apply_truncate(value, params): + length = params.get("length", 3) + suffix = params.get("suffix", "...") + if not value or len(value) <= length: + return value + return value[:length] + suffix + + +def _apply_hash(value, params): + algorithm = params.get("algorithm", "sha256") + if algorithm == "md5": + return hashlib.md5(str(value).encode()).hexdigest()[:16] + return hashlib.sha256(str(value).encode()).hexdigest()[:32] + + +def _apply_generalize(value, params): + try: + step = params.get("step", 10) + num = float(value) + lower = int(num // step * step) + upper = lower + step + return f"{lower}-{upper}" + except Exception: + return value + + +def _apply_replace(value, params): + return params.get("replacement", "[REDACTED]") + + +def apply_masking(value, algorithm, params): + if value is None: + return None + handlers = { + "mask": _apply_mask, + "truncate": _apply_truncate, + "hash": _apply_hash, + "generalize": _apply_generalize, + "replace": _apply_replace, + } + handler = handlers.get(algorithm) + if not handler: + return value + return handler(str(value), params or {}) + + +def _get_column_rules(db: Session, table_id: int, project_id=None): + columns = db.query(DataColumn).filter(DataColumn.table_id == table_id).all() + col_rules = {} + results = {} + if project_id: + res_list = db.query(ClassificationResult).filter( + ClassificationResult.project_id == project_id, + ClassificationResult.column_id.in_([c.id for c in columns]), + ).all() + results = {r.column_id: r for r in res_list} + rules = db.query(MaskingRule).filter(MaskingRule.is_active == True).all() + rule_map = {} + for r in rules: + key = (r.level_id, r.category_id) + if key not in rule_map: + rule_map[key] = r + for col in columns: + matched_rule = None + if col.id in results: + r = results[col.id] + matched_rule = rule_map.get((r.level_id, r.category_id)) + if not matched_rule: + matched_rule = rule_map.get((r.level_id, None)) + if not matched_rule: + matched_rule = rule_map.get((None, r.category_id)) + col_rules[col.id] = matched_rule + return col_rules + + +def preview_masking(db: Session, source_id: int, table_name: str, project_id=None, limit=20): + source = get_datasource(db, source_id) + if not source: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="数据源不存在") + table = ( + db.query(DataTable) + .join(Database) + .filter(Database.source_id == source_id, DataTable.name == table_name) + .first() + ) + if not table: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="表不存在") + col_rules = _get_column_rules(db, table.id, project_id) + from sqlalchemy import create_engine, text + password = "" + if source.encrypted_password: + try: + password = _decrypt_password(source.encrypted_password) + except Exception: + pass + driver_map = { + "mysql": "mysql+pymysql", + "postgresql": "postgresql+psycopg2", + "oracle": "oracle+cx_oracle", + "sqlserver": "mssql+pymssql", + } + driver = driver_map.get(source.source_type, source.source_type) + url = f"{driver}://{source.username}:{password}@{source.host}:{source.port}/{source.database_name}" + engine = create_engine(url, pool_pre_ping=True) + columns = db.query(DataColumn).filter(DataColumn.table_id == table.id).all() + rows_raw = [] + try: + with engine.connect() as conn: + result = conn.execute(text(f'SELECT * FROM "{table_name}" LIMIT {limit}')) + rows_raw = [dict(row._mapping) for row in result] + except Exception: + try: + with engine.connect() as conn: + result = conn.execute(text(f"SELECT * FROM {table_name} LIMIT {limit}")) + rows_raw = [dict(row._mapping) for row in result] + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"查询失败: {e}") + masked_rows = [] + for raw in rows_raw: + masked = {} + for col in columns: + val = raw.get(col.name) + rule = col_rules.get(col.id) + if rule: + masked[col.name] = apply_masking(val, rule.algorithm, rule.params or {}) + else: + masked[col.name] = val + masked_rows.append(masked) + return { + "success": True, + "columns": [{"name": c.name, "data_type": c.data_type, "has_rule": col_rules.get(c.id) is not None} for c in columns], + "rows": masked_rows, + "total_rows": len(masked_rows), + } diff --git a/backend/app/services/metadata_service.py b/backend/app/services/metadata_service.py index 99d1fe35..551ecc8d 100644 --- a/backend/app/services/metadata_service.py +++ b/backend/app/services/metadata_service.py @@ -3,9 +3,23 @@ from sqlalchemy.orm import Session from fastapi import HTTPException, status from app.models.metadata import DataSource, Database, DataTable, DataColumn +from app.models.schema_change import SchemaChangeLog from app.services.datasource_service import get_datasource, _decrypt_password +def _log_schema_change(db: Session, source_id: int, change_type: str, database_id: int = None, table_id: int = None, column_id: int = None, old_value: str = None, new_value: str = None): + log = SchemaChangeLog( + source_id=source_id, + database_id=database_id, + table_id=table_id, + column_id=column_id, + change_type=change_type, + old_value=old_value, + new_value=new_value, + ) + db.add(log) + + def get_database(db: Session, db_id: int) -> Optional[Database]: return db.query(Database).filter(Database.id == db_id).first() @@ -19,14 +33,14 @@ def get_column(db: Session, column_id: int) -> Optional[DataColumn]: def list_databases(db: Session, source_id: Optional[int] = None) -> List[Database]: - query = db.query(Database) + query = db.query(Database).filter(Database.is_deleted == False) if source_id: query = query.filter(Database.source_id == source_id) return query.all() def list_tables(db: Session, database_id: Optional[int] = None, keyword: Optional[str] = None) -> Tuple[List[DataTable], int]: - query = db.query(DataTable) + query = db.query(DataTable).filter(DataTable.is_deleted == False) if database_id: query = query.filter(DataTable.database_id == database_id) if keyword: @@ -37,7 +51,7 @@ def list_tables(db: Session, database_id: Optional[int] = None, keyword: Optiona def list_columns(db: Session, table_id: Optional[int] = None, keyword: Optional[str] = None, page: int = 1, page_size: int = 50) -> Tuple[List[DataColumn], int]: - query = db.query(DataColumn) + query = db.query(DataColumn).filter(DataColumn.is_deleted == False) if table_id: query = query.filter(DataColumn.table_id == table_id) if keyword: @@ -49,7 +63,7 @@ def list_columns(db: Session, table_id: Optional[int] = None, keyword: Optional[ return items, total -def build_tree(db: Session, source_id: Optional[int] = None) -> List[dict]: +def build_tree(db: Session, source_id: Optional[int] = None, include_deleted: bool = False) -> List[dict]: sources = db.query(DataSource) if source_id: sources = sources.filter(DataSource.id == source_id) @@ -65,20 +79,24 @@ def build_tree(db: Session, source_id: Optional[int] = None) -> List[dict]: "meta": {"source_type": s.source_type, "status": s.status}, } for d in s.databases: + if not include_deleted and d.is_deleted: + continue db_node = { "id": d.id, "name": d.name, "type": "database", "children": [], - "meta": {"charset": d.charset, "table_count": d.table_count}, + "meta": {"charset": d.charset, "table_count": d.table_count, "is_deleted": d.is_deleted}, } for t in d.tables: + if not include_deleted and t.is_deleted: + continue table_node = { "id": t.id, "name": t.name, "type": "table", "children": [], - "meta": {"comment": t.comment, "row_count": t.row_count, "column_count": t.column_count}, + "meta": {"comment": t.comment, "row_count": t.row_count, "column_count": t.column_count, "is_deleted": t.is_deleted}, } db_node["children"].append(table_node) source_node["children"].append(db_node) @@ -86,9 +104,16 @@ def build_tree(db: Session, source_id: Optional[int] = None) -> List[dict]: return result +def _compute_checksum(data: dict) -> str: + import hashlib, json + payload = json.dumps(data, sort_keys=True, ensure_ascii=False, default=str) + return hashlib.sha256(payload.encode()).hexdigest()[:32] + + def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: from sqlalchemy import create_engine, inspect, text import json + from datetime import datetime source = get_datasource(db, source_id) if not source: @@ -118,29 +143,56 @@ def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: inspector = inspect(engine) db_names = inspector.get_schema_names() or [source.database_name] + scan_time = datetime.utcnow() total_tables = 0 total_columns = 0 + updated_tables = 0 + updated_columns = 0 for db_name in db_names: - db_obj = db.query(Database).filter(Database.source_id == source.id, Database.name == db_name).first() + db_checksum = _compute_checksum({"name": db_name}) + db_obj = db.query(Database).filter( + Database.source_id == source.id, Database.name == db_name + ).first() if not db_obj: - db_obj = Database(source_id=source.id, name=db_name) + db_obj = Database(source_id=source.id, name=db_name, checksum=db_checksum, last_scanned_at=scan_time) db.add(db_obj) - db.commit() - db.refresh(db_obj) + else: + db_obj.checksum = db_checksum + db_obj.last_scanned_at = scan_time + db_obj.is_deleted = False + db_obj.deleted_at = None table_names = inspector.get_table_names(schema=db_name) for tname in table_names: - table_obj = db.query(DataTable).filter(DataTable.database_id == db_obj.id, DataTable.name == tname).first() + t_checksum = _compute_checksum({"name": tname}) + table_obj = db.query(DataTable).filter( + DataTable.database_id == db_obj.id, DataTable.name == tname + ).first() if not table_obj: - table_obj = DataTable(database_id=db_obj.id, name=tname) + table_obj = DataTable(database_id=db_obj.id, name=tname, checksum=t_checksum, last_scanned_at=scan_time) db.add(table_obj) - db.commit() - db.refresh(table_obj) + _log_schema_change(db, source.id, "add_table", database_id=db_obj.id, table_id=table_obj.id, new_value=tname) + else: + if table_obj.checksum != t_checksum: + table_obj.checksum = t_checksum + updated_tables += 1 + table_obj.last_scanned_at = scan_time + table_obj.is_deleted = False + table_obj.deleted_at = None columns = inspector.get_columns(tname, schema=db_name) for col in columns: - col_obj = db.query(DataColumn).filter(DataColumn.table_id == table_obj.id, DataColumn.name == col["name"]).first() + col_checksum = _compute_checksum({ + "name": col["name"], + "type": str(col.get("type", "")), + "max_length": col.get("max_length"), + "comment": col.get("comment"), + "nullable": col.get("nullable", True), + }) + col_obj = db.query(DataColumn).filter( + DataColumn.table_id == table_obj.id, DataColumn.name == col["name"] + ).first() if not col_obj: sample = None try: @@ -150,7 +202,6 @@ def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: sample = json.dumps(samples, ensure_ascii=False) except Exception: pass - col_obj = DataColumn( table_id=table_obj.id, name=col["name"], @@ -159,13 +210,58 @@ def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: comment=col.get("comment"), is_nullable=col.get("nullable", True), sample_data=sample, + checksum=col_checksum, + last_scanned_at=scan_time, ) db.add(col_obj) total_columns += 1 + _log_schema_change(db, source.id, "add_column", database_id=db_obj.id, table_id=table_obj.id, column_id=col_obj.id, new_value=col["name"]) + else: + if col_obj.checksum != col_checksum: + old_val = f"type={col_obj.data_type}, len={col_obj.length}, comment={col_obj.comment}" + new_val = f"type={str(col.get('type', ''))}, len={col.get('max_length')}, comment={col.get('comment')}" + _log_schema_change(db, source.id, "change_type", database_id=db_obj.id, table_id=table_obj.id, column_id=col_obj.id, old_value=old_val, new_value=new_val) + col_obj.checksum = col_checksum + col_obj.data_type = str(col.get("type", "")) + col_obj.length = col.get("max_length") + col_obj.comment = col.get("comment") + col_obj.is_nullable = col.get("nullable", True) + updated_columns += 1 + col_obj.last_scanned_at = scan_time + col_obj.is_deleted = False + col_obj.deleted_at = None total_tables += 1 - db.commit() + # Soft-delete objects not seen in this scan and log changes + deleted_dbs = db.query(Database).filter( + Database.source_id == source.id, + Database.last_scanned_at < scan_time, + ).all() + for d in deleted_dbs: + _log_schema_change(db, source.id, "drop_database", database_id=d.id, old_value=d.name) + d.is_deleted = True + d.deleted_at = scan_time + + for db_obj in db.query(Database).filter(Database.source_id == source.id).all(): + deleted_tables = db.query(DataTable).filter( + DataTable.database_id == db_obj.id, + DataTable.last_scanned_at < scan_time, + ).all() + for t in deleted_tables: + _log_schema_change(db, source.id, "drop_table", database_id=db_obj.id, table_id=t.id, old_value=t.name) + t.is_deleted = True + t.deleted_at = scan_time + + for table_obj in db.query(DataTable).filter(DataTable.database_id == db_obj.id).all(): + deleted_cols = db.query(DataColumn).filter( + DataColumn.table_id == table_obj.id, + DataColumn.last_scanned_at < scan_time, + ).all() + for c in deleted_cols: + _log_schema_change(db, source.id, "drop_column", database_id=db_obj.id, table_id=table_obj.id, column_id=c.id, old_value=c.name) + c.is_deleted = True + c.deleted_at = scan_time source.status = "active" db.commit() @@ -176,6 +272,8 @@ def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: "databases": len(db_names), "tables": total_tables, "columns": total_columns, + "updated_tables": updated_tables, + "updated_columns": updated_columns, } except Exception as e: source.status = "error" diff --git a/backend/app/services/metadata_service.py.bak b/backend/app/services/metadata_service.py.bak new file mode 100644 index 00000000..99d1fe35 --- /dev/null +++ b/backend/app/services/metadata_service.py.bak @@ -0,0 +1,183 @@ +from typing import Optional, List, Tuple +from sqlalchemy.orm import Session +from fastapi import HTTPException, status + +from app.models.metadata import DataSource, Database, DataTable, DataColumn +from app.services.datasource_service import get_datasource, _decrypt_password + + +def get_database(db: Session, db_id: int) -> Optional[Database]: + return db.query(Database).filter(Database.id == db_id).first() + + +def get_table(db: Session, table_id: int) -> Optional[DataTable]: + return db.query(DataTable).filter(DataTable.id == table_id).first() + + +def get_column(db: Session, column_id: int) -> Optional[DataColumn]: + return db.query(DataColumn).filter(DataColumn.id == column_id).first() + + +def list_databases(db: Session, source_id: Optional[int] = None) -> List[Database]: + query = db.query(Database) + if source_id: + query = query.filter(Database.source_id == source_id) + return query.all() + + +def list_tables(db: Session, database_id: Optional[int] = None, keyword: Optional[str] = None) -> Tuple[List[DataTable], int]: + query = db.query(DataTable) + if database_id: + query = query.filter(DataTable.database_id == database_id) + if keyword: + query = query.filter( + (DataTable.name.contains(keyword)) | (DataTable.comment.contains(keyword)) + ) + return query.all(), query.count() + + +def list_columns(db: Session, table_id: Optional[int] = None, keyword: Optional[str] = None, page: int = 1, page_size: int = 50) -> Tuple[List[DataColumn], int]: + query = db.query(DataColumn) + if table_id: + query = query.filter(DataColumn.table_id == table_id) + if keyword: + query = query.filter( + (DataColumn.name.contains(keyword)) | (DataColumn.comment.contains(keyword)) + ) + total = query.count() + items = query.offset((page - 1) * page_size).limit(page_size).all() + return items, total + + +def build_tree(db: Session, source_id: Optional[int] = None) -> List[dict]: + sources = db.query(DataSource) + if source_id: + sources = sources.filter(DataSource.id == source_id) + sources = sources.all() + + result = [] + for s in sources: + source_node = { + "id": s.id, + "name": s.name, + "type": "source", + "children": [], + "meta": {"source_type": s.source_type, "status": s.status}, + } + for d in s.databases: + db_node = { + "id": d.id, + "name": d.name, + "type": "database", + "children": [], + "meta": {"charset": d.charset, "table_count": d.table_count}, + } + for t in d.tables: + table_node = { + "id": t.id, + "name": t.name, + "type": "table", + "children": [], + "meta": {"comment": t.comment, "row_count": t.row_count, "column_count": t.column_count}, + } + db_node["children"].append(table_node) + source_node["children"].append(db_node) + result.append(source_node) + return result + + +def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: + from sqlalchemy import create_engine, inspect, text + import json + + source = get_datasource(db, source_id) + if not source: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="数据源不存在") + + driver_map = { + "mysql": "mysql+pymysql", + "postgresql": "postgresql+psycopg2", + "oracle": "oracle+cx_oracle", + "sqlserver": "mssql+pymssql", + } + driver = driver_map.get(source.source_type, source.source_type) + + if source.source_type == "dm": + return {"success": True, "message": "达梦数据库同步成功(模拟)", "databases": 0, "tables": 0, "columns": 0} + + password = "" + if source.encrypted_password: + try: + password = _decrypt_password(source.encrypted_password) + except Exception: + pass + + try: + url = f"{driver}://{source.username}:{password}@{source.host}:{source.port}/{source.database_name}" + engine = create_engine(url, pool_pre_ping=True) + inspector = inspect(engine) + + db_names = inspector.get_schema_names() or [source.database_name] + total_tables = 0 + total_columns = 0 + + for db_name in db_names: + db_obj = db.query(Database).filter(Database.source_id == source.id, Database.name == db_name).first() + if not db_obj: + db_obj = Database(source_id=source.id, name=db_name) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + + table_names = inspector.get_table_names(schema=db_name) + for tname in table_names: + table_obj = db.query(DataTable).filter(DataTable.database_id == db_obj.id, DataTable.name == tname).first() + if not table_obj: + table_obj = DataTable(database_id=db_obj.id, name=tname) + db.add(table_obj) + db.commit() + db.refresh(table_obj) + + columns = inspector.get_columns(tname, schema=db_name) + for col in columns: + col_obj = db.query(DataColumn).filter(DataColumn.table_id == table_obj.id, DataColumn.name == col["name"]).first() + if not col_obj: + sample = None + try: + with engine.connect() as conn: + result = conn.execute(text(f'SELECT "{col["name"]}" FROM "{db_name}"."{tname}" LIMIT 5')) + samples = [str(r[0]) for r in result if r[0] is not None] + sample = json.dumps(samples, ensure_ascii=False) + except Exception: + pass + + col_obj = DataColumn( + table_id=table_obj.id, + name=col["name"], + data_type=str(col.get("type", "")), + length=col.get("max_length"), + comment=col.get("comment"), + is_nullable=col.get("nullable", True), + sample_data=sample, + ) + db.add(col_obj) + total_columns += 1 + + total_tables += 1 + + db.commit() + + source.status = "active" + db.commit() + + return { + "success": True, + "message": "元数据同步成功", + "databases": len(db_names), + "tables": total_tables, + "columns": total_columns, + } + except Exception as e: + source.status = "error" + db.commit() + return {"success": False, "message": f"同步失败: {str(e)}", "databases": 0, "tables": 0, "columns": 0} diff --git a/backend/app/services/ml_service.py b/backend/app/services/ml_service.py new file mode 100644 index 00000000..321b309d --- /dev/null +++ b/backend/app/services/ml_service.py @@ -0,0 +1,195 @@ +import os +import json +import logging +from typing import List, Optional, Tuple +from datetime import datetime + +import joblib +import numpy as np +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score +from sqlalchemy.orm import Session + +from app.models.project import ClassificationResult +from app.models.classification import Category +from app.models.ml import MLModelVersion + +logger = logging.getLogger(__name__) + +MODELS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ml_models") +os.makedirs(MODELS_DIR, exist_ok=True) + + +def _build_text_features(column_name: str, comment: Optional[str], sample_data: Optional[str]) -> str: + parts = [column_name] + if comment: + parts.append(comment) + if sample_data: + try: + samples = json.loads(sample_data) + if isinstance(samples, list): + parts.extend([str(s) for s in samples[:5]]) + except Exception: + parts.append(sample_data) + return " ".join(parts) + + +def _fetch_training_data(db: Session, min_samples_per_class: int = 5): + results = ( + db.query(ClassificationResult) + .filter(ClassificationResult.source == "manual") + .filter(ClassificationResult.category_id.isnot(None)) + .all() + ) + texts = [] + labels = [] + for r in results: + if r.column: + text = _build_text_features(r.column.name, r.column.comment, r.column.sample_data) + texts.append(text) + labels.append(r.category_id) + from collections import Counter + counts = Counter(labels) + valid_classes = {c for c, n in counts.items() if n >= min_samples_per_class} + filtered_texts = [] + filtered_labels = [] + for t, l in zip(texts, labels): + if l in valid_classes: + filtered_texts.append(t) + filtered_labels.append(l) + return filtered_texts, filtered_labels, len(filtered_labels) + + +def train_model(db: Session, model_name: Optional[str] = None, algorithm: str = "logistic_regression", test_size: float = 0.2): + texts, labels, total = _fetch_training_data(db) + if total < 20: + logger.warning("Not enough training data (need >= 20, got %d)", total) + return None + X_train, X_test, y_train, y_test = train_test_split( + texts, labels, test_size=test_size, random_state=42, stratify=labels + ) + vectorizer = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 4), max_features=5000) + X_train_vec = vectorizer.fit_transform(X_train) + X_test_vec = vectorizer.transform(X_test) + if algorithm == "logistic_regression": + clf = LogisticRegression(max_iter=1000, multi_class="multinomial", solver="lbfgs") + elif algorithm == "random_forest": + clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) + else: + clf = LogisticRegression(max_iter=1000, multi_class="multinomial", solver="lbfgs") + clf.fit(X_train_vec, y_train) + y_pred = clf.predict(X_test_vec) + acc = accuracy_score(y_test, y_pred) + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + name = model_name or f"model_{timestamp}" + model_path = os.path.join(MODELS_DIR, f"{name}_clf.joblib") + vec_path = os.path.join(MODELS_DIR, f"{name}_tfidf.joblib") + joblib.dump(clf, model_path) + joblib.dump(vectorizer, vec_path) + db.query(MLModelVersion).filter(MLModelVersion.is_active == True).update({"is_active": False}) + mv = MLModelVersion( + name=name, + model_path=model_path, + vectorizer_path=vec_path, + accuracy=acc, + train_samples=total, + is_active=True, + description=f"Algorithm: {algorithm}, test_accuracy: {acc:.4f}", + ) + db.add(mv) + db.commit() + db.refresh(mv) + logger.info("Trained model %s with accuracy %.4f on %d samples", name, acc, total) + return mv + + +def _get_active_model(db: Session): + mv = db.query(MLModelVersion).filter(MLModelVersion.is_active == True).first() + if not mv or not os.path.exists(mv.model_path) or not os.path.exists(mv.vectorizer_path): + return None + clf = joblib.load(mv.model_path) + vectorizer = joblib.load(mv.vectorizer_path) + return clf, vectorizer, mv + + +def predict_categories(db: Session, texts: List[str], top_k: int = 3): + model_tuple = _get_active_model(db) + if not model_tuple: + return [[] for _ in texts] + clf, vectorizer, mv = model_tuple + X = vectorizer.transform(texts) + if hasattr(clf, "predict_proba"): + probs = clf.predict_proba(X) + else: + preds = clf.predict(X) + return [[{"category_id": int(p), "confidence": 1.0}] for p in preds] + classes = [int(c) for c in clf.classes_] + results = [] + for prob in probs: + top_idx = np.argsort(prob)[::-1][:top_k] + suggestions = [] + for idx in top_idx: + cat_id = classes[idx] + confidence = float(prob[idx]) + if confidence > 0.01: + suggestions.append({"category_id": cat_id, "confidence": round(confidence, 4)}) + results.append(suggestions) + return results + + +def suggest_for_project_columns(db: Session, project_id: int, column_ids: Optional[List[int]] = None, top_k: int = 3): + from app.models.project import ClassificationProject + from app.models.metadata import DataColumn + + project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first() + if not project: + return {"success": False, "message": "项目不存在"} + + query = db.query(DataColumn).join( + ClassificationResult, + (ClassificationResult.column_id == DataColumn.id) & (ClassificationResult.project_id == project_id), + isouter=True, + ) + if column_ids: + query = query.filter(DataColumn.id.in_(column_ids)) + + columns = query.all() + texts = [] + col_map = [] + for col in columns: + texts.append(_build_text_features(col.name, col.comment, col.sample_data)) + col_map.append(col) + + if not texts: + return {"success": True, "suggestions": [], "message": "没有可预测的字段"} + + predictions = predict_categories(db, texts, top_k=top_k) + suggestions = [] + all_category_ids = set() + for col, preds in zip(col_map, predictions): + for p in preds: + all_category_ids.add(p["category_id"]) + + categories = {c.id: c for c in db.query(Category).filter(Category.id.in_(list(all_category_ids))).all()} + + for col, preds in zip(col_map, predictions): + item = { + "column_id": col.id, + "column_name": col.name, + "table_name": col.table.name if col.table else None, + "suggestions": [], + } + for p in preds: + cat = categories.get(p["category_id"]) + item["suggestions"].append({ + "category_id": p["category_id"], + "category_name": cat.name if cat else None, + "category_code": cat.code if cat else None, + "confidence": p["confidence"], + }) + suggestions.append(item) + + return {"success": True, "suggestions": suggestions} diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index b7fc3c4b..c0311f9a 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -94,3 +94,155 @@ def generate_classification_report(db: Session, project_id: int) -> bytes: doc.save(buffer) buffer.seek(0) return buffer.read() + + +def generate_excel_report(db: Session, project_id: int) -> bytes: + """Generate an Excel report for a classification project.""" + from openpyxl import Workbook + from openpyxl.styles import Font, PatternFill, Alignment, Border, Side + from openpyxl.chart import PieChart, Reference + from sqlalchemy import func + + project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first() + if not project: + raise ValueError("项目不存在") + + wb = Workbook() + ws = wb.active + ws.title = "报告概览" + + # Title + ws.merge_cells('A1:D1') + ws['A1'] = '数据分类分级项目报告' + ws['A1'].font = Font(size=18, bold=True) + ws['A1'].alignment = Alignment(horizontal='center') + + # Basic info + ws['A3'] = '项目名称' + ws['B3'] = project.name + ws['A4'] = '报告生成时间' + ws['B4'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ws['A5'] = '项目状态' + ws['B5'] = project.status + ws['A6'] = '模板版本' + ws['B6'] = project.template.version if project.template else 'N/A' + + # Statistics + results = db.query(ClassificationResult).filter(ClassificationResult.project_id == project_id).all() + total = len(results) + auto_count = sum(1 for r in results if r.source == 'auto') + manual_count = sum(1 for r in results if r.source == 'manual') + + ws['A8'] = '总字段数' + ws['B8'] = total + ws['A9'] = '自动识别' + ws['B9'] = auto_count + ws['A10'] = '人工打标' + ws['B10'] = manual_count + + # Level distribution + ws['A12'] = '分级' + ws['B12'] = '数量' + ws['C12'] = '占比' + ws['A12'].font = Font(bold=True) + ws['B12'].font = Font(bold=True) + ws['C12'].font = Font(bold=True) + + level_stats = {} + for r in results: + if r.level: + level_stats[r.level.name] = level_stats.get(r.level.name, 0) + 1 + + red_fill = PatternFill(start_color='FFCCCC', end_color='FFCCCC', fill_type='solid') + row = 13 + for level_name, count in sorted(level_stats.items(), key=lambda x: -x[1]): + ws.cell(row=row, column=1, value=level_name) + ws.cell(row=row, column=2, value=count) + pct = f'{count / total * 100:.1f}%' if total > 0 else '0%' + ws.cell(row=row, column=3, value=pct) + if 'L4' in level_name or 'L5' in level_name: + for c in range(1, 4): + ws.cell(row=row, column=c).fill = red_fill + row += 1 + + # High risk sheet + ws2 = wb.create_sheet("高敏感数据清单") + ws2.append(['字段名', '所属表', '分类', '分级', '来源', '置信度']) + for cell in ws2[1]: + cell.font = Font(bold=True) + cell.fill = PatternFill(start_color='DDEBF7', end_color='DDEBF7', fill_type='solid') + + high_risk = [r for r in results if r.level and r.level.code in ('L4', 'L5')] + for r in high_risk[:500]: + ws2.append([ + r.column.name if r.column else 'N/A', + r.column.table.name if r.column and r.column.table else 'N/A', + r.category.name if r.category else 'N/A', + r.level.name if r.level else 'N/A', + '自动' if r.source == 'auto' else '人工', + r.confidence, + ]) + + # Auto-fit column widths roughly + for ws_sheet in [ws, ws2]: + for column in ws_sheet.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 50) + ws_sheet.column_dimensions[column_letter].width = adjusted_width + + buffer = BytesIO() + wb.save(buffer) + buffer.seek(0) + return buffer.read() + + +def get_report_summary(db: Session, project_id: int) -> dict: + """Get aggregated report data for PDF generation (frontend).""" + from sqlalchemy import func + project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first() + if not project: + raise ValueError("项目不存在") + + results = db.query(ClassificationResult).filter(ClassificationResult.project_id == project_id).all() + total = len(results) + auto_count = sum(1 for r in results if r.source == 'auto') + manual_count = sum(1 for r in results if r.source == 'manual') + + level_stats = {} + for r in results: + if r.level: + level_stats[r.level.name] = level_stats.get(r.level.name, 0) + 1 + + high_risk = [] + for r in results: + if r.level and r.level.code in ('L4', 'L5'): + high_risk.append({ + "column_name": r.column.name if r.column else 'N/A', + "table_name": r.column.table.name if r.column and r.column.table else 'N/A', + "category_name": r.category.name if r.category else 'N/A', + "level_name": r.level.name if r.level else 'N/A', + "source": '自动' if r.source == 'auto' else '人工', + "confidence": r.confidence, + }) + + return { + "project_name": project.name, + "status": project.status, + "template_version": project.template.version if project.template else 'N/A', + "generated_at": datetime.now().isoformat(), + "total": total, + "auto": auto_count, + "manual": manual_count, + "level_distribution": [ + {"name": name, "count": count} + for name, count in sorted(level_stats.items(), key=lambda x: -x[1]) + ], + "high_risk": high_risk[:100], + } diff --git a/backend/app/services/risk_service.py b/backend/app/services/risk_service.py new file mode 100644 index 00000000..f883a839 --- /dev/null +++ b/backend/app/services/risk_service.py @@ -0,0 +1,125 @@ +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime + +from app.models.project import ClassificationProject, ClassificationResult +from app.models.classification import DataLevel +from app.models.metadata import DataSource, Database, DataTable, DataColumn +from app.models.masking import MaskingRule +from app.models.risk import RiskAssessment + + +def _get_level_weight(level_code: str) -> int: + weights = {"L1": 1, "L2": 2, "L3": 3, "L4": 4, "L5": 5} + return weights.get(level_code, 1) + + +def calculate_project_risk(db: Session, project_id: int) -> RiskAssessment: + """Calculate risk score for a project.""" + project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first() + if not project: + return None + + results = db.query(ClassificationResult).filter( + ClassificationResult.project_id == project_id, + ClassificationResult.level_id.isnot(None), + ).all() + + total_risk = 0.0 + total_sensitivity = 0.0 + total_exposure = 0.0 + total_protection = 0.0 + detail_items = [] + + # Get all active masking rules for quick lookup + rules = db.query(MaskingRule).filter(MaskingRule.is_active == True).all() + rule_level_ids = {r.level_id for r in rules if r.level_id} + rule_cat_ids = {r.category_id for r in rules if r.category_id} + + for r in results: + if not r.level: + continue + level_weight = _get_level_weight(r.level.code) + # Exposure: count source connections for the column's table + source_count = 1 + if r.column and r.column.table and r.column.table.database: + # Simple: if table exists in multiple dbs (rare), count them + source_count = max(1, len(r.column.table.database.source.databases or [])) + exposure_factor = 1 + source_count * 0.2 + + # Protection: check if masking rule exists for this level/category + has_masking = (r.level_id in rule_level_ids) or (r.category_id in rule_cat_ids) + protection_rate = 0.3 if has_masking else 0.0 + + item_risk = level_weight * exposure_factor * (1 - protection_rate) + total_risk += item_risk + total_sensitivity += level_weight + total_exposure += exposure_factor + total_protection += protection_rate + + detail_items.append({ + "column_id": r.column_id, + "column_name": r.column.name if r.column else None, + "level": r.level.code if r.level else None, + "level_weight": level_weight, + "exposure_factor": round(exposure_factor, 2), + "protection_rate": protection_rate, + "item_risk": round(item_risk, 2), + }) + + # Normalize to 0-100 (heuristic: assume max reasonable raw score is 15 per field) + count = len(detail_items) or 1 + max_raw = count * 15 + risk_score = min(100, (total_risk / max_raw) * 100) if max_raw > 0 else 0 + + # Upsert risk assessment + existing = db.query(RiskAssessment).filter( + RiskAssessment.entity_type == "project", + RiskAssessment.entity_id == project_id, + ).first() + + if existing: + existing.risk_score = round(risk_score, 2) + existing.sensitivity_score = round(total_sensitivity / count, 2) + existing.exposure_score = round(total_exposure / count, 2) + existing.protection_score = round(total_protection / count, 2) + existing.details = {"items": detail_items[:100], "total_items": len(detail_items)} + existing.updated_at = datetime.utcnow() + else: + existing = RiskAssessment( + entity_type="project", + entity_id=project_id, + entity_name=project.name, + risk_score=round(risk_score, 2), + sensitivity_score=round(total_sensitivity / count, 2), + exposure_score=round(total_exposure / count, 2), + protection_score=round(total_protection / count, 2), + details={"items": detail_items[:100], "total_items": len(detail_items)}, + ) + db.add(existing) + + db.commit() + return existing + + +def calculate_all_projects_risk(db: Session) -> dict: + """Batch calculate risk for all projects.""" + projects = db.query(ClassificationProject).all() + updated = 0 + for p in projects: + try: + calculate_project_risk(db, p.id) + updated += 1 + except Exception: + pass + return {"updated": updated} + + +def get_risk_top_n(db: Session, entity_type: str = "project", n: int = 10) -> List[RiskAssessment]: + return ( + db.query(RiskAssessment) + .filter(RiskAssessment.entity_type == entity_type) + .order_by(RiskAssessment.risk_score.desc()) + .limit(n) + .all() + ) diff --git a/backend/app/services/unstructured_service.py b/backend/app/services/unstructured_service.py new file mode 100644 index 00000000..2d248369 --- /dev/null +++ b/backend/app/services/unstructured_service.py @@ -0,0 +1,99 @@ +import os +import re +import json +from typing import Optional, List +from sqlalchemy.orm import Session +from fastapi import HTTPException, status + +from app.models.metadata import UnstructuredFile +from app.core.events import minio_client +from app.core.config import settings + + +def extract_text_from_file(file_path: str, file_type: str) -> str: + text = "" + ft = file_type.lower() + if ft in ("word", "docx"): + try: + from docx import Document + doc = Document(file_path) + text = "\n".join([p.text for p in doc.paragraphs if p.text]) + except Exception as e: + raise ValueError(f"解析Word失败: {e}") + elif ft in ("excel", "xlsx", "xls"): + try: + from openpyxl import load_workbook + wb = load_workbook(file_path, data_only=True) + parts = [] + for sheet in wb.worksheets: + for row in sheet.iter_rows(values_only=True): + parts.append(" ".join([str(c) for c in row if c is not None])) + text = "\n".join(parts) + except Exception as e: + raise ValueError(f"解析Excel失败: {e}") + elif ft == "pdf": + try: + import pdfplumber + with pdfplumber.open(file_path) as pdf: + text = "\n".join([page.extract_text() or "" for page in pdf.pages]) + except Exception as e: + raise ValueError(f"解析PDF失败: {e}") + elif ft == "txt": + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + text = f.read() + else: + raise ValueError(f"不支持的文件类型: {ft}") + return text + + +def scan_text_for_sensitive(text: str) -> List[dict]: + """Scan extracted text for sensitive patterns using built-in rules.""" + matches = [] + # ID card + id_pattern = re.compile(r"(? dict: + file_obj = db.query(UnstructuredFile).filter(UnstructuredFile.id == file_id).first() + if not file_obj: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文件不存在") + if not file_obj.storage_path: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="文件未上传") + + # Download from MinIO to temp + tmp_path = f"/tmp/unstructured_{file_id}_{file_obj.original_name}" + try: + minio_client.fget_object(settings.MINIO_BUCKET_NAME, file_obj.storage_path, tmp_path) + text = extract_text_from_file(tmp_path, file_obj.file_type or "") + file_obj.extracted_text = text[:50000] # limit storage + matches = scan_text_for_sensitive(text) + file_obj.analysis_result = {"matches": matches, "total_chars": len(text)} + file_obj.status = "processed" + db.commit() + return {"success": True, "matches": matches, "total_chars": len(text)} + except Exception as e: + file_obj.status = "error" + db.commit() + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) + finally: + if os.path.exists(tmp_path): + os.remove(tmp_path) diff --git a/backend/app/services/watermark_service.py b/backend/app/services/watermark_service.py new file mode 100644 index 00000000..c7c8f831 --- /dev/null +++ b/backend/app/services/watermark_service.py @@ -0,0 +1,97 @@ +import secrets +from typing import Optional, Tuple +from sqlalchemy.orm import Session +from app.models.watermark import WatermarkLog + +# Zero-width characters for binary encoding +ZW_SPACE = "\u200b" # zero-width space -> 0 +ZW_NOJOIN = "\u200c" # zero-width non-joiner -> 1 +MARKER = "\u200d" # zero-width joiner -> start marker + + +def _int_to_binary_bits(n: int, bits: int = 32) -> str: + return format(n, f"0{bits}b") + + +def _binary_bits_to_int(bits: str) -> int: + return int(bits, 2) + + +def embed_watermark(text: str, user_id: int, key: str) -> str: + """Embed invisible watermark into text using zero-width characters.""" + # Encode user_id as 32-bit binary + bits = _int_to_binary_bits(user_id) + # Encode key hash as 16-bit for verification + key_bits = _int_to_binary_bits(hash(key) & 0xFFFF, 16) + payload = key_bits + bits + watermark_chars = MARKER + "".join(ZW_NOJOIN if b == "1" else ZW_SPACE for b in payload) + # Append watermark at the end of the text (before trailing newlines if any) + text = text.rstrip("\n") + return text + watermark_chars + "\n" + + +def extract_watermark(text: str) -> Tuple[Optional[int], Optional[str]]: + """Extract watermark from text. Returns (user_id, key_hash_bits) or (None, None).""" + if MARKER not in text: + return None, None + idx = text.index(MARKER) + payload = text[idx + len(MARKER):] + bits = "" + for ch in payload: + if ch == ZW_SPACE: + bits += "0" + elif ch == ZW_NOJOIN: + bits += "1" + else: + # Stop at first non-watermark character + break + if len(bits) < 16: + return None, None + key_bits = bits[:16] + user_bits = bits[16:48] + try: + user_id = _binary_bits_to_int(user_bits) + return user_id, key_bits + except Exception: + return None, None + + +def apply_watermark_to_lines(lines: list, user_id: int, key: str) -> list: + """Apply watermark to each line of CSV/TXT.""" + return [embed_watermark(line, user_id, key) for line in lines] + + +def create_watermark_log(db: Session, user_id: int, export_type: str, data_scope: dict) -> WatermarkLog: + key = secrets.token_hex(16) + log = WatermarkLog( + user_id=user_id, + export_type=export_type, + data_scope=str(data_scope), + watermark_key=key, + ) + db.add(log) + db.commit() + db.refresh(log) + return log + + +def trace_watermark(db: Session, text: str) -> Optional[dict]: + """Trace leaked text back to user.""" + user_id, _ = extract_watermark(text) + if user_id is None: + return None + log = ( + db.query(WatermarkLog) + .filter(WatermarkLog.user_id == user_id) + .order_by(WatermarkLog.created_at.desc()) + .first() + ) + if not log: + return None + return { + "user_id": log.user_id, + "username": log.user.username if log.user else None, + "export_type": log.export_type, + "data_scope": log.data_scope, + "created_at": log.created_at.isoformat() if log.created_at else None, + } diff --git a/backend/app/tasks/classification_tasks.py b/backend/app/tasks/classification_tasks.py index 82d11131..98c1e7ea 100644 --- a/backend/app/tasks/classification_tasks.py +++ b/backend/app/tasks/classification_tasks.py @@ -1,3 +1,4 @@ +import json from app.tasks.worker import celery_app @@ -5,12 +6,10 @@ from app.tasks.worker import celery_app def auto_classify_task(self, project_id: int, source_ids: list = None): """ Async task to run automatic classification on metadata. - Phase 1 placeholder. """ from app.core.database import SessionLocal - from app.models.project import ClassificationProject, ClassificationResult, ResultStatus - from app.models.classification import RecognitionRule - from app.models.metadata import DataColumn + from app.models.project import ClassificationProject + from app.services.classification_engine import run_auto_classification db = SessionLocal() try: @@ -18,15 +17,46 @@ def auto_classify_task(self, project_id: int, source_ids: list = None): if not project: return {"status": "failed", "reason": "project not found"} - # Update project status + def progress_callback(scanned, matched, total): + percent = int(scanned / total * 100) if total else 0 + meta = { + "scanned": scanned, + "matched": matched, + "total": total, + "percent": percent, + } + self.update_state(state="PROGRESS", meta=meta) + # Persist lightweight progress to DB for UI polling + project.scan_progress = json.dumps(meta) + db.commit() + + # Initialize project.status = "scanning" + project.scan_progress = json.dumps({"scanned": 0, "matched": 0, "total": 0, "percent": 0}) db.commit() - rules = db.query(RecognitionRule).filter(RecognitionRule.is_active == True).all() - # TODO: implement rule matching logic in Phase 2 + result = run_auto_classification( + db, + project_id, + source_ids=source_ids, + progress_callback=progress_callback, + ) - project.status = "assigning" + if result.get("success"): + project.status = "assigning" + else: + project.status = "created" + + project.celery_task_id = None db.commit() - return {"status": "completed", "project_id": project_id, "matched": 0} + return {"status": "completed", "project_id": project_id, "result": result} + except Exception as e: + db.rollback() + project = db.query(ClassificationProject).filter(ClassificationProject.id == project_id).first() + if project: + project.status = "created" + project.celery_task_id = None + db.commit() + return {"status": "failed", "reason": str(e)} finally: db.close() diff --git a/backend/app/tasks/ml_tasks.py b/backend/app/tasks/ml_tasks.py new file mode 100644 index 00000000..1cd73349 --- /dev/null +++ b/backend/app/tasks/ml_tasks.py @@ -0,0 +1,26 @@ +from app.tasks.worker import celery_app + + +@celery_app.task(bind=True) +def train_ml_model_task(self, model_name: str = None, algorithm: str = "logistic_regression"): + from app.core.database import SessionLocal + from app.services.ml_service import train_model + + db = SessionLocal() + try: + self.update_state(state="PROGRESS", meta={"message": "Fetching training data"}) + mv = train_model(db, model_name=model_name, algorithm=algorithm) + if mv: + return { + "status": "completed", + "model_id": mv.id, + "name": mv.name, + "accuracy": mv.accuracy, + "train_samples": mv.train_samples, + } + else: + return {"status": "failed", "reason": "Not enough training data (need >= 20 samples)"} + except Exception as e: + return {"status": "failed", "reason": str(e)} + finally: + db.close() diff --git a/backend/app/tasks/worker.py b/backend/app/tasks/worker.py index d70b8678..194173b1 100644 --- a/backend/app/tasks/worker.py +++ b/backend/app/tasks/worker.py @@ -5,7 +5,7 @@ celery_app = Celery( "data_pointer", broker=settings.REDIS_URL, backend=settings.REDIS_URL, - include=["app.tasks.classification_tasks"], + include=["app.tasks.classification_tasks", "app.tasks.ml_tasks"], ) celery_app.conf.update( diff --git a/backend/scripts/api_integration_test.py b/backend/scripts/api_integration_test.py new file mode 100644 index 00000000..50b24a00 --- /dev/null +++ b/backend/scripts/api_integration_test.py @@ -0,0 +1,124 @@ +import sys, requests +BASE = "http://localhost:8000" +API = f"{BASE}/api/v1" +errors, passed = [], [] + +def check(name, ok, detail=""): + if ok: + passed.append(name); print(f" ✅ {name}") + else: + errors.append((name, detail)); print(f" ❌ {name}: {detail}") + +def get_items(resp): + d = resp.json().get("data", []) + if isinstance(d, list): + return d + if isinstance(d, dict): + return d.get("items", []) + return [] + +def get_total(resp): + return resp.json().get("total", 0) + +print("\n[1/15] Health") +r = requests.get(f"{BASE}/health") +check("health", r.status_code == 200 and r.json().get("status") == "ok") + +print("\n[2/15] Auth") +r = requests.post(f"{API}/auth/login", json={"username": "admin", "password": "admin123"}) +check("login", r.status_code == 200) +token = r.json().get("data", {}).get("access_token", "") +check("token", bool(token)) +headers = {"Authorization": f"Bearer {token}"} + +print("\n[3/15] User") +r = requests.get(f"{API}/users/me", headers=headers) +check("me", r.status_code == 200 and r.json()["data"]["username"] == "admin") +r = requests.get(f"{API}/users?page_size=100", headers=headers) +check("users", r.status_code == 200 and len(get_items(r)) >= 80, f"got {len(get_items(r))}") + +print("\n[4/15] Depts") +r = requests.get(f"{API}/users/depts", headers=headers) +check("depts", r.status_code == 200 and len(r.json().get("data", [])) >= 12, f"got {len(r.json().get('data', []))}") + +print("\n[5/15] DataSources") +r = requests.get(f"{API}/datasources", headers=headers) +check("datasources", r.status_code == 200 and len(get_items(r)) >= 12, f"got {len(get_items(r))}") + +print("\n[6/15] Metadata") +r = requests.get(f"{API}/metadata/databases", headers=headers) +check("databases", r.status_code == 200 and len(get_items(r)) >= 31, f"got {len(get_items(r))}") +r = requests.get(f"{API}/metadata/tables", headers=headers) +check("tables", r.status_code == 200 and len(get_items(r)) >= 800, f"got {len(get_items(r))}") +r = requests.get(f"{API}/metadata/columns", headers=headers) +check("columns", r.status_code == 200 and get_total(r) >= 10000, f"total={get_total(r)}") + +print("\n[7/15] Classification") +r = requests.get(f"{API}/classifications/levels", headers=headers) +check("levels", r.status_code == 200 and len(r.json().get("data", [])) == 5) +r = requests.get(f"{API}/classifications/categories", headers=headers) +check("categories", r.status_code == 200 and len(r.json().get("data", [])) >= 20, f"got {len(r.json().get('data', []))}") +r = requests.get(f"{API}/classifications/results", headers=headers) +check("results", r.status_code == 200 and get_total(r) >= 1000, f"total={get_total(r)}") + +print("\n[8/15] Projects") +r = requests.get(f"{API}/projects", headers=headers) +check("projects", r.status_code == 200 and len(get_items(r)) >= 8, f"got {len(get_items(r))}") + +print("\n[9/15] Tasks") +r = requests.get(f"{API}/tasks/my-tasks", headers=headers) +check("tasks", r.status_code == 200 and len(get_items(r)) >= 20, f"got {len(get_items(r))}") + +print("\n[10/15] Dashboard") +r = requests.get(f"{API}/dashboard/stats", headers=headers) +check("stats", r.status_code == 200) +stats = r.json().get("data", {}) +check("stats.data_sources", stats.get("data_sources", 0) >= 12, f"got {stats.get('data_sources')}") +check("stats.tables", stats.get("tables", 0) >= 800, f"got {stats.get('tables')}") +check("stats.columns", stats.get("columns", 0) >= 10000, f"got {stats.get('columns')}") +check("stats.labeled", stats.get("labeled", 0) >= 10000, f"got {stats.get('labeled')}") +r = requests.get(f"{API}/dashboard/distribution", headers=headers) +check("distribution", r.status_code == 200 and "level_distribution" in r.json().get("data", {})) + +print("\n[11/15] Reports") +r = requests.get(f"{API}/reports/stats", headers=headers) +check("report stats", r.status_code == 200) + +print("\n[12/15] Masking") +r = requests.get(f"{API}/masking/rules", headers=headers) +check("masking rules", r.status_code == 200) + +print("\n[13/15] Watermark") +r = requests.post(f"{API}/watermark/trace", headers={**headers, "Content-Type": "application/json"}, json={"content": "test watermark"}) +check("watermark trace", r.status_code == 200) + +print("\n[14/15] Risk") +r = requests.get(f"{API}/risk/top", headers=headers) +check("risk top", r.status_code == 200) + +print("\n[15/15] Compliance") +r = requests.get(f"{API}/compliance/issues", headers=headers) +check("compliance issues", r.status_code == 200) + +# Additional modules +print("\n[Bonus] Additional modules") +r = requests.get(f"{API}/lineage/graph", headers=headers) +check("lineage graph", r.status_code == 200 and "nodes" in r.json().get("data", {})) +r = requests.get(f"{API}/alerts/records", headers=headers) +check("alert records", r.status_code == 200) +r = requests.get(f"{API}/schema-changes/logs", headers=headers) +check("schema changes logs", r.status_code == 200) +r = requests.get(f"{API}/unstructured/files", headers=headers) +check("unstructured files", r.status_code == 200) +r = requests.get(f"{API}/api-assets", headers=headers) +check("api assets", r.status_code == 200) + +print("\n" + "="*60) +print(f"Results: {len(passed)} passed, {len(errors)} failed") +print("="*60) +if errors: + for n, d in errors: print(f" ❌ {n}: {d}") + sys.exit(1) +else: + print("🎉 All integration tests passed!") + sys.exit(0) diff --git a/backend/scripts/generate_ml_training_data.py b/backend/scripts/generate_ml_training_data.py new file mode 100644 index 00000000..c9975ca0 --- /dev/null +++ b/backend/scripts/generate_ml_training_data.py @@ -0,0 +1,59 @@ +""" +Generate synthetic manual-labeled data for ML model training/demo. +Run this script after metadata has been scanned so there are columns to label. +""" +import random +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from app.core.database import SessionLocal +from app.models.metadata import DataColumn +from app.models.classification import Category +from app.models.project import ClassificationResult + + +def main(): + db = SessionLocal() + try: + columns = db.query(DataColumn).limit(300).all() + if not columns: + print("No columns found in database. Please scan a data source first.") + return + + categories = db.query(Category).filter(Category.level == 2).all() + if not categories: + print("No sub-categories found.") + return + + # Clear old manual labels to avoid duplicates + db.query(ClassificationResult).filter(ClassificationResult.source == "manual").delete() + db.commit() + + count = 0 + for col in columns: + # Deterministic pseudo-random based on column name for reproducibility + rng = random.Random(col.name) + cat = rng.choice(categories) + # Create a fake manual result (project_id=1 assumed to exist or None) + result = ClassificationResult( + project_id=None, + column_id=col.id, + category_id=cat.id, + level_id=cat.parent.level if cat.parent else 3, # fallback + source="manual", + confidence=1.0, + status="manual", + ) + db.add(result) + count += 1 + + db.commit() + print(f"Generated {count} manual labels across {len(categories)} categories.") + finally: + db.close() + + +if __name__ == "__main__": + main() diff --git a/backend/scripts/patch_metadata.py b/backend/scripts/patch_metadata.py new file mode 100644 index 00000000..4fe8c214 --- /dev/null +++ b/backend/scripts/patch_metadata.py @@ -0,0 +1,176 @@ +import sys + +with open(sys.argv[1], 'r') as f: + content = f.read() + +marker = 'def sync_metadata(db: Session, source_id: int, user_id: int) -> dict:' +idx = content.find(marker) +if idx == -1: + print('Marker not found') + sys.exit(1) + +new_func = '''def _compute_checksum(data: dict) -> str: + import hashlib, json + payload = json.dumps(data, sort_keys=True, ensure_ascii=False, default=str) + return hashlib.sha256(payload.encode()).hexdigest()[:32] + + +def sync_metadata(db: Session, source_id: int, user_id: int) -> dict: + from sqlalchemy import create_engine, inspect, text + import json + from datetime import datetime + + source = get_datasource(db, source_id) + if not source: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="数据源不存在") + + driver_map = { + "mysql": "mysql+pymysql", + "postgresql": "postgresql+psycopg2", + "oracle": "oracle+cx_oracle", + "sqlserver": "mssql+pymssql", + } + driver = driver_map.get(source.source_type, source.source_type) + + if source.source_type == "dm": + return {"success": True, "message": "达梦数据库同步成功(模拟)", "databases": 0, "tables": 0, "columns": 0} + + password = "" + if source.encrypted_password: + try: + password = _decrypt_password(source.encrypted_password) + except Exception: + pass + + try: + url = f"{driver}://{source.username}:{password}@{source.host}:{source.port}/{source.database_name}" + engine = create_engine(url, pool_pre_ping=True) + inspector = inspect(engine) + + db_names = inspector.get_schema_names() or [source.database_name] + scan_time = datetime.utcnow() + total_tables = 0 + total_columns = 0 + updated_tables = 0 + updated_columns = 0 + + for db_name in db_names: + db_checksum = _compute_checksum({"name": db_name}) + db_obj = db.query(Database).filter( + Database.source_id == source.id, Database.name == db_name + ).first() + if not db_obj: + db_obj = Database(source_id=source.id, name=db_name, checksum=db_checksum, last_scanned_at=scan_time) + db.add(db_obj) + else: + db_obj.checksum = db_checksum + db_obj.last_scanned_at = scan_time + db_obj.is_deleted = False + db_obj.deleted_at = None + + table_names = inspector.get_table_names(schema=db_name) + for tname in table_names: + t_checksum = _compute_checksum({"name": tname}) + table_obj = db.query(DataTable).filter( + DataTable.database_id == db_obj.id, DataTable.name == tname + ).first() + if not table_obj: + table_obj = DataTable(database_id=db_obj.id, name=tname, checksum=t_checksum, last_scanned_at=scan_time) + db.add(table_obj) + else: + if table_obj.checksum != t_checksum: + table_obj.checksum = t_checksum + updated_tables += 1 + table_obj.last_scanned_at = scan_time + table_obj.is_deleted = False + table_obj.deleted_at = None + + columns = inspector.get_columns(tname, schema=db_name) + for col in columns: + col_checksum = _compute_checksum({ + "name": col["name"], + "type": str(col.get("type", "")), + "max_length": col.get("max_length"), + "comment": col.get("comment"), + "nullable": col.get("nullable", True), + }) + col_obj = db.query(DataColumn).filter( + DataColumn.table_id == table_obj.id, DataColumn.name == col["name"] + ).first() + if not col_obj: + sample = None + try: + with engine.connect() as conn: + result = conn.execute(text(f'SELECT "{col["name"]}" FROM "{db_name}"."{tname}" LIMIT 5')) + samples = [str(r[0]) for r in result if r[0] is not None] + sample = json.dumps(samples, ensure_ascii=False) + except Exception: + pass + col_obj = DataColumn( + table_id=table_obj.id, + name=col["name"], + data_type=str(col.get("type", "")), + length=col.get("max_length"), + comment=col.get("comment"), + is_nullable=col.get("nullable", True), + sample_data=sample, + checksum=col_checksum, + last_scanned_at=scan_time, + ) + db.add(col_obj) + total_columns += 1 + else: + if col_obj.checksum != col_checksum: + col_obj.checksum = col_checksum + col_obj.data_type = str(col.get("type", "")) + col_obj.length = col.get("max_length") + col_obj.comment = col.get("comment") + col_obj.is_nullable = col.get("nullable", True) + updated_columns += 1 + col_obj.last_scanned_at = scan_time + col_obj.is_deleted = False + col_obj.deleted_at = None + + total_tables += 1 + + # Soft-delete objects not seen in this scan + db.query(Database).filter( + Database.source_id == source.id, + Database.last_scanned_at < scan_time, + ).update({"is_deleted": True, "deleted_at": scan_time}, synchronize_session=False) + + for db_obj in db.query(Database).filter(Database.source_id == source.id).all(): + db.query(DataTable).filter( + DataTable.database_id == db_obj.id, + DataTable.last_scanned_at < scan_time, + ).update({"is_deleted": True, "deleted_at": scan_time}, synchronize_session=False) + for table_obj in db.query(DataTable).filter(DataTable.database_id == db_obj.id).all(): + db.query(DataColumn).filter( + DataColumn.table_id == table_obj.id, + DataColumn.last_scanned_at < scan_time, + ).update({"is_deleted": True, "deleted_at": scan_time}, synchronize_session=False) + + source.status = "active" + db.commit() + + return { + "success": True, + "message": "元数据同步成功", + "databases": len(db_names), + "tables": total_tables, + "columns": total_columns, + "updated_tables": updated_tables, + "updated_columns": updated_columns, + } + except Exception as e: + source.status = "error" + db.commit() + return {"success": False, "message": f"同步失败: {str(e)}", "databases": 0, "tables": 0, "columns": 0} +''' + +new_content = content[:idx] + new_func + +with open(sys.argv[1], 'w') as f: + f.write(new_content) + +print('Patched successfully') diff --git a/docker-compose.yml b/docker-compose.yml index 7213c91b..30e2f0dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,6 +54,7 @@ services: - MINIO_ACCESS_KEY=pdgminio - MINIO_SECRET_KEY=pdgminio_secret_2024 - SECRET_KEY=prop-data-guard-super-secret-key-change-in-production + - DB_ENCRYPTION_KEY=${DB_ENCRYPTION_KEY:-} - ACCESS_TOKEN_EXPIRE_MINUTES=30 - REFRESH_TOKEN_EXPIRE_DAYS=7 volumes: @@ -88,6 +89,7 @@ services: - DATABASE_URL=postgresql+psycopg2://pdg:pdg_secret_2024@db:5432/prop_data_guard - REDIS_URL=redis://redis:6379/0 - SECRET_KEY=prop-data-guard-super-secret-key-change-in-production + - DB_ENCRYPTION_KEY=${DB_ENCRYPTION_KEY:-} volumes: - ./backend:/app depends_on: @@ -103,6 +105,7 @@ services: - DATABASE_URL=postgresql+psycopg2://pdg:pdg_secret_2024@db:5432/prop_data_guard - REDIS_URL=redis://redis:6379/0 - SECRET_KEY=prop-data-guard-super-secret-key-change-in-production + - DB_ENCRYPTION_KEY=${DB_ENCRYPTION_KEY:-} volumes: - ./backend:/app depends_on: diff --git a/frontend/dist/assets/Category-B9_ZSQhL.css b/frontend/dist/assets/Category-B9_ZSQhL.css deleted file mode 100644 index 14a22391..00000000 --- a/frontend/dist/assets/Category-B9_ZSQhL.css +++ /dev/null @@ -1 +0,0 @@ -.page-title[data-v-0ac8aaa8]{font-size:20px;font-weight:600;margin-bottom:20px;color:#303133}.section[data-v-0ac8aaa8]{margin-bottom:24px}.section .section-header[data-v-0ac8aaa8]{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.section .section-title[data-v-0ac8aaa8]{font-size:16px;font-weight:600;color:#303133}.level-card[data-v-0ac8aaa8]{padding:16px;margin-bottom:12px;background:#fff;border-radius:8px}.level-card .level-header[data-v-0ac8aaa8]{display:flex;align-items:center;gap:10px;margin-bottom:8px}.level-card .level-name[data-v-0ac8aaa8]{font-size:15px;font-weight:600;color:#303133}.level-card .level-desc[data-v-0ac8aaa8]{font-size:13px;color:#606266;line-height:1.5;margin-bottom:8px}.level-card .level-ctrl .ctrl-item[data-v-0ac8aaa8]{font-size:12px;color:#909399;margin-bottom:2px}.level-card .level-ctrl .ctrl-item .ctrl-key[data-v-0ac8aaa8]{font-weight:500;color:#606266}.category-tree[data-v-0ac8aaa8]{padding:16px;background:#fff;border-radius:8px}.category-tree .custom-tree-node[data-v-0ac8aaa8]{display:flex;align-items:center;justify-content:space-between;flex:1;overflow:hidden}.category-tree .custom-tree-node .node-label[data-v-0ac8aaa8]{display:flex;align-items:center;gap:8px;overflow:hidden}.category-tree .custom-tree-node .node-label .code-tag[data-v-0ac8aaa8]{font-size:11px;flex-shrink:0}.category-tree .custom-tree-node .node-actions[data-v-0ac8aaa8]{flex-shrink:0}.table-card[data-v-0ac8aaa8]{padding:16px;background:#fff;border-radius:8px} diff --git a/frontend/dist/assets/DataSource-DBPYeC9V.css b/frontend/dist/assets/DataSource-DBPYeC9V.css deleted file mode 100644 index 7ded0676..00000000 --- a/frontend/dist/assets/DataSource-DBPYeC9V.css +++ /dev/null @@ -1 +0,0 @@ -.page-header[data-v-e577ddaa]{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.page-header .page-title[data-v-e577ddaa]{font-size:20px;font-weight:600;color:#303133}.search-bar[data-v-e577ddaa]{padding:16px;margin-bottom:16px}.table-card[data-v-e577ddaa]{padding:16px}.pagination-bar[data-v-e577ddaa]{display:flex;justify-content:flex-end;margin-top:16px} diff --git a/frontend/dist/assets/Layout-7AUm9Wj-.css b/frontend/dist/assets/Layout-7AUm9Wj-.css deleted file mode 100644 index bb54b9c6..00000000 --- a/frontend/dist/assets/Layout-7AUm9Wj-.css +++ /dev/null @@ -1 +0,0 @@ -.layout-container[data-v-6b05a74f]{height:100vh}.layout-aside[data-v-6b05a74f]{background-color:#1a2b4a;display:flex;flex-direction:column}.logo[data-v-6b05a74f]{height:56px;display:flex;align-items:center;justify-content:center;gap:10px;background-color:#13203a;flex-shrink:0}.logo .logo-text[data-v-6b05a74f]{color:#fff;font-size:16px;font-weight:600;letter-spacing:1px}.layout-menu[data-v-6b05a74f]{border-right:none;flex:1}.layout-header[data-v-6b05a74f]{background-color:#fff;display:flex;align-items:center;justify-content:space-between;box-shadow:0 1px 4px #0000000d;padding:0 16px}.layout-header .header-left[data-v-6b05a74f]{display:flex;align-items:center;gap:12px}.layout-header .header-title[data-v-6b05a74f]{font-size:16px;font-weight:600;color:#303133}.layout-header .header-right .user-info[data-v-6b05a74f]{display:flex;align-items:center;gap:8px;cursor:pointer;color:#606266}.layout-header .header-right .user-info .username[data-v-6b05a74f]{font-size:14px;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.layout-main[data-v-6b05a74f]{background-color:#f5f7fa;padding:0;overflow-y:auto}[data-v-6b05a74f] .mobile-drawer .el-drawer__body{padding:0;background-color:#1a2b4a;display:flex;flex-direction:column} diff --git a/frontend/dist/assets/Login-BDSQKn9t.css b/frontend/dist/assets/Login-BDSQKn9t.css deleted file mode 100644 index e8fbaa33..00000000 --- a/frontend/dist/assets/Login-BDSQKn9t.css +++ /dev/null @@ -1 +0,0 @@ -.login-page[data-v-8c2e034d]{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#1a2b4a,#2d4a7c);padding:16px}.login-box[data-v-8c2e034d]{width:100%;max-width:420px;padding:40px 32px;background:#fff;border-radius:12px}.login-header[data-v-8c2e034d]{text-align:center;margin-bottom:32px}.login-header .login-title[data-v-8c2e034d]{font-size:24px;font-weight:700;color:#1a2b4a;margin-top:12px}.login-header .login-subtitle[data-v-8c2e034d]{font-size:14px;color:#909399;margin-top:8px}.login-btn[data-v-8c2e034d]{width:100%;height:44px;font-size:16px;border-radius:6px}.login-footer[data-v-8c2e034d]{margin-top:24px;text-align:center;font-size:12px;color:#c0c4cc} diff --git a/frontend/dist/assets/Metadata-BXIwI_Qd.css b/frontend/dist/assets/Metadata-BXIwI_Qd.css deleted file mode 100644 index 385c77c3..00000000 --- a/frontend/dist/assets/Metadata-BXIwI_Qd.css +++ /dev/null @@ -1 +0,0 @@ -.metadata-page .page-title[data-v-866ea4fc]{font-size:20px;font-weight:600;margin-bottom:16px;color:#303133}.content-row .el-col[data-v-866ea4fc]{margin-bottom:16px}.tree-card[data-v-866ea4fc]{padding:16px;height:calc(100vh - 140px);overflow-y:auto}.tree-card .tree-header[data-v-866ea4fc]{font-size:16px;font-weight:600;margin-bottom:12px;color:#303133}.tree-card .tree-search[data-v-866ea4fc]{margin-bottom:12px}.custom-tree-node[data-v-866ea4fc]{display:flex;align-items:center;gap:6px;flex:1;overflow:hidden}.custom-tree-node .node-label[data-v-866ea4fc]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.custom-tree-node .node-badge[data-v-866ea4fc]{font-size:11px;color:#909399;background:#f2f6fc;padding:0 6px;border-radius:10px}.detail-card[data-v-866ea4fc]{padding:16px;height:calc(100vh - 140px);display:flex;flex-direction:column}.detail-card .detail-header[data-v-866ea4fc]{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:12px}.detail-card .detail-header .detail-title[data-v-866ea4fc]{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:600;color:#303133}.detail-card .detail-header .detail-title .placeholder[data-v-866ea4fc]{color:#909399;font-weight:400}.sample-text[data-v-866ea4fc]{color:#909399;font-size:12px} diff --git a/frontend/dist/assets/Project-C3Fnk4wf.css b/frontend/dist/assets/Project-C3Fnk4wf.css deleted file mode 100644 index 31e046cc..00000000 --- a/frontend/dist/assets/Project-C3Fnk4wf.css +++ /dev/null @@ -1 +0,0 @@ -.page-header[data-v-5fcafe59]{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.page-header .page-title[data-v-5fcafe59]{font-size:20px;font-weight:600;color:#303133}.search-bar[data-v-5fcafe59]{padding:16px;margin-bottom:16px}.project-list .el-col[data-v-5fcafe59]{margin-bottom:16px}.project-card[data-v-5fcafe59]{padding:20px;background:#fff;border-radius:8px;transition:box-shadow .2s}.project-card[data-v-5fcafe59]:hover{box-shadow:0 4px 16px #00000014}.project-card .project-header[data-v-5fcafe59]{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.project-card .project-header .project-name[data-v-5fcafe59]{font-size:16px;font-weight:600;color:#303133}.project-card .project-desc[data-v-5fcafe59]{font-size:13px;color:#909399;margin-bottom:16px;min-height:20px}.project-card .project-stats[data-v-5fcafe59]{display:flex;gap:16px;margin-bottom:16px;padding:12px 0;border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0}.project-card .project-stats .stat-item[data-v-5fcafe59]{text-align:center;flex:1}.project-card .project-stats .stat-item .stat-num[data-v-5fcafe59]{font-size:18px;font-weight:700;color:#303133}.project-card .project-stats .stat-item .stat-label[data-v-5fcafe59]{font-size:12px;color:#909399;margin-top:2px}.project-card .project-actions[data-v-5fcafe59]{display:flex;justify-content:flex-end;gap:8px} diff --git a/frontend/dist/assets/System-zrM6wI5r.css b/frontend/dist/assets/System-zrM6wI5r.css deleted file mode 100644 index d99fa80e..00000000 --- a/frontend/dist/assets/System-zrM6wI5r.css +++ /dev/null @@ -1 +0,0 @@ -.page-title[data-v-d05c8e34]{font-size:20px;font-weight:600;margin-bottom:16px;color:#303133}.system-tabs[data-v-d05c8e34]{background:#fff;padding:16px;border-radius:8px}.table-card[data-v-d05c8e34]{padding:16px}.table-card .table-header[data-v-d05c8e34]{display:flex;align-items:center;gap:12px;margin-bottom:16px} diff --git a/frontend/dist/assets/index-s_XEM0GP.css b/frontend/dist/assets/index-s_XEM0GP.css deleted file mode 100644 index 1d787554..00000000 --- a/frontend/dist/assets/index-s_XEM0GP.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";:root{--el-color-white:#fff;--el-color-black:#000;--el-color-primary-rgb:64, 158, 255;--el-color-success-rgb:103, 194, 58;--el-color-warning-rgb:230, 162, 60;--el-color-danger-rgb:245, 108, 108;--el-color-error-rgb:245, 108, 108;--el-color-info-rgb:144, 147, 153;--el-font-size-extra-large:20px;--el-font-size-large:18px;--el-font-size-medium:16px;--el-font-size-base:14px;--el-font-size-small:13px;--el-font-size-extra-small:12px;--el-font-family:"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;--el-font-weight-primary:500;--el-font-line-height-primary:24px;--el-index-normal:1;--el-index-top:1000;--el-index-popper:2000;--el-border-radius-base:4px;--el-border-radius-small:2px;--el-border-radius-round:20px;--el-border-radius-circle:100%;--el-transition-duration:.3s;--el-transition-duration-fast:.2s;--el-transition-function-ease-in-out-bezier:cubic-bezier(.645, .045, .355, 1);--el-transition-function-fast-bezier:cubic-bezier(.23, 1, .32, 1);--el-transition-all:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);--el-transition-fade:opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-md-fade:transform var(--el-transition-duration) var(--el-transition-function-fast-bezier), opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-fade-linear:opacity var(--el-transition-duration-fast) linear;--el-transition-border:border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-box-shadow:box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-color:color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-component-size-large:40px;--el-component-size:32px;--el-component-size-small:24px;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--el-color-primary:#409eff;--el-color-primary-light-3:#79bbff;--el-color-primary-light-5:#a0cfff;--el-color-primary-light-7:#c6e2ff;--el-color-primary-light-8:#d9ecff;--el-color-primary-light-9:#ecf5ff;--el-color-primary-dark-2:#337ecc;--el-color-success:#67c23a;--el-color-success-light-3:#95d475;--el-color-success-light-5:#b3e19d;--el-color-success-light-7:#d1edc4;--el-color-success-light-8:#e1f3d8;--el-color-success-light-9:#f0f9eb;--el-color-success-dark-2:#529b2e;--el-color-warning:#e6a23c;--el-color-warning-light-3:#eebe77;--el-color-warning-light-5:#f3d19e;--el-color-warning-light-7:#f8e3c5;--el-color-warning-light-8:#faecd8;--el-color-warning-light-9:#fdf6ec;--el-color-warning-dark-2:#b88230;--el-color-danger:#f56c6c;--el-color-danger-light-3:#f89898;--el-color-danger-light-5:#fab6b6;--el-color-danger-light-7:#fcd3d3;--el-color-danger-light-8:#fde2e2;--el-color-danger-light-9:#fef0f0;--el-color-danger-dark-2:#c45656;--el-color-error:#f56c6c;--el-color-error-light-3:#f89898;--el-color-error-light-5:#fab6b6;--el-color-error-light-7:#fcd3d3;--el-color-error-light-8:#fde2e2;--el-color-error-light-9:#fef0f0;--el-color-error-dark-2:#c45656;--el-color-info:#909399;--el-color-info-light-3:#b1b3b8;--el-color-info-light-5:#c8c9cc;--el-color-info-light-7:#dedfe0;--el-color-info-light-8:#e9e9eb;--el-color-info-light-9:#f4f4f5;--el-color-info-dark-2:#73767a;--el-bg-color:#fff;--el-bg-color-page:#f2f3f5;--el-bg-color-overlay:#fff;--el-text-color-primary:#303133;--el-text-color-regular:#606266;--el-text-color-secondary:#909399;--el-text-color-placeholder:#a8abb2;--el-text-color-disabled:#c0c4cc;--el-border-color:#dcdfe6;--el-border-color-light:#e4e7ed;--el-border-color-lighter:#ebeef5;--el-border-color-extra-light:#f2f6fc;--el-border-color-dark:#d4d7de;--el-border-color-darker:#cdd0d6;--el-fill-color:#f0f2f5;--el-fill-color-light:#f5f7fa;--el-fill-color-lighter:#fafafa;--el-fill-color-extra-light:#fafcff;--el-fill-color-dark:#ebedf0;--el-fill-color-darker:#e6e8eb;--el-fill-color-blank:#fff;--el-box-shadow:0px 12px 32px 4px #0000000a, 0px 8px 20px #00000014;--el-box-shadow-light:0px 0px 12px #0000001f;--el-box-shadow-lighter:0px 0px 6px #0000001f;--el-box-shadow-dark:0px 16px 48px 16px #00000014, 0px 12px 32px #0000001f, 0px 8px 16px -8px #00000029;--el-disabled-bg-color:var(--el-fill-color-light);--el-disabled-text-color:var(--el-text-color-placeholder);--el-disabled-border-color:var(--el-border-color-light);--el-overlay-color:#000c;--el-overlay-color-light:#000000b3;--el-overlay-color-lighter:#00000080;--el-mask-color:#ffffffe6;--el-mask-color-extra-light:#ffffff4d;--el-border-width:1px;--el-border-style:solid;--el-border-color-hover:var(--el-text-color-disabled);--el-border:var(--el-border-width) var(--el-border-style) var(--el-border-color);--el-svg-monochrome-grey:var(--el-border-color)}.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.fade-in-linear-enter-from,.fade-in-linear-leave-to{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.el-fade-in-linear-enter-from,.el-fade-in-linear-leave-to{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-fade-in-enter-from,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter-from,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transition:var(--el-transition-md-fade);transform-origin:top;transform:scaleY(1)}.el-zoom-in-top-enter-active[data-popper-placement^=top],.el-zoom-in-top-leave-active[data-popper-placement^=top]{transform-origin:bottom}.el-zoom-in-top-enter-from,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transition:var(--el-transition-md-fade);transform-origin:bottom;transform:scaleY(1)}.el-zoom-in-bottom-enter-from,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transition:var(--el-transition-md-fade);transform-origin:0 0;transform:scale(1)}.el-zoom-in-left-enter-from,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45)}.collapse-transition{transition:var(--el-transition-duration) height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.el-collapse-transition-leave-active,.el-collapse-transition-enter-active{transition:var(--el-transition-duration) max-height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.horizontal-collapse-transition{transition:var(--el-transition-duration) width ease-in-out,var(--el-transition-duration) padding-left ease-in-out,var(--el-transition-duration) padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter-from,.el-list-leave-to{opacity:0;transform:translateY(-30px)}.el-list-leave-active{position:absolute!important}.el-opacity-transition{transition:opacity var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@keyframes rotating{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.el-icon{--color:inherit;fill:currentColor;width:1em;height:1em;color:var(--color);line-height:1em;font-size:inherit;justify-content:center;align-items:center;display:inline-flex;position:relative}.el-icon.is-loading{animation:2s linear infinite rotating}.el-icon svg{width:1em;height:1em}.el-affix--fixed{position:fixed}.el-alert{--el-alert-padding:8px 16px;--el-alert-border-radius-base:var(--el-border-radius-base);--el-alert-title-font-size:14px;--el-alert-title-with-description-font-size:16px;--el-alert-description-font-size:14px;--el-alert-close-font-size:16px;--el-alert-close-customed-font-size:14px;--el-alert-icon-size:16px;--el-alert-icon-large-size:28px;width:100%;padding:var(--el-alert-padding);box-sizing:border-box;border-radius:var(--el-alert-border-radius-base);background-color:var(--el-color-white);opacity:1;transition:opacity var(--el-transition-duration-fast);align-items:center;margin:0;display:flex;position:relative;overflow:hidden}.el-alert.is-light .el-alert__close-btn{color:var(--el-text-color-placeholder)}.el-alert.is-dark .el-alert__close-btn,.el-alert.is-dark .el-alert__description{color:var(--el-color-white)}.el-alert.is-center{justify-content:center}.el-alert--primary{--el-alert-bg-color:var(--el-color-primary-light-9)}.el-alert--primary.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-primary)}.el-alert--primary.is-light .el-alert__description{color:var(--el-color-primary)}.el-alert--primary.is-dark{background-color:var(--el-color-primary);color:var(--el-color-white)}.el-alert--success{--el-alert-bg-color:var(--el-color-success-light-9)}.el-alert--success.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-success)}.el-alert--success.is-light .el-alert__description{color:var(--el-color-success)}.el-alert--success.is-dark{background-color:var(--el-color-success);color:var(--el-color-white)}.el-alert--info{--el-alert-bg-color:var(--el-color-info-light-9)}.el-alert--info.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-info)}.el-alert--info.is-light .el-alert__description{color:var(--el-color-info)}.el-alert--info.is-dark{background-color:var(--el-color-info);color:var(--el-color-white)}.el-alert--warning{--el-alert-bg-color:var(--el-color-warning-light-9)}.el-alert--warning.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-warning)}.el-alert--warning.is-light .el-alert__description{color:var(--el-color-warning)}.el-alert--warning.is-dark{background-color:var(--el-color-warning);color:var(--el-color-white)}.el-alert--error{--el-alert-bg-color:var(--el-color-error-light-9)}.el-alert--error.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-error)}.el-alert--error.is-light .el-alert__description{color:var(--el-color-error)}.el-alert--error.is-dark{background-color:var(--el-color-error);color:var(--el-color-white)}.el-alert__content{flex-direction:column;gap:4px;display:flex}.el-alert .el-alert__icon{font-size:var(--el-alert-icon-size);width:var(--el-alert-icon-size);margin-right:8px}.el-alert .el-alert__icon.is-big{font-size:var(--el-alert-icon-large-size);width:var(--el-alert-icon-large-size);margin-right:12px}.el-alert__title{font-size:var(--el-alert-title-font-size);line-height:24px}.el-alert__title.with-description{font-size:var(--el-alert-title-with-description-font-size)}.el-alert .el-alert__description{font-size:var(--el-alert-description-font-size);margin:0}.el-alert .el-alert__close-btn{font-size:var(--el-alert-close-font-size);opacity:1;cursor:pointer;position:absolute;top:12px;right:16px}.el-alert .el-alert__close-btn.is-customed{font-style:normal;font-size:var(--el-alert-close-customed-font-size);line-height:24px;top:8px}.el-alert-fade-enter-from,.el-alert-fade-leave-active{opacity:0}.el-aside{box-sizing:border-box;width:var(--el-aside-width,300px);flex-shrink:0;overflow:auto}.el-autocomplete{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;width:var(--el-input-width);display:inline-block;position:relative}.el-autocomplete__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-box-shadow-light)}.el-autocomplete__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-autocomplete__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-autocomplete__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-autocomplete__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-autocomplete__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-autocomplete-suggestion{border-radius:var(--el-border-radius-base);box-sizing:border-box}.el-autocomplete-suggestion__header{border-bottom:1px solid var(--el-border-color-lighter);padding:10px}.el-autocomplete-suggestion__footer{border-top:1px solid var(--el-border-color-lighter);padding:10px}.el-autocomplete-suggestion__wrap{box-sizing:border-box;max-height:280px;padding:10px 0}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{cursor:pointer;color:var(--el-text-color-regular);line-height:34px;font-size:var(--el-font-size-base);text-align:left;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0 20px;list-style:none;overflow:hidden}.el-autocomplete-suggestion li:hover,.el-autocomplete-suggestion li.highlighted{background-color:var(--el-fill-color-light)}.el-autocomplete-suggestion li.divider{border-top:1px solid var(--el-color-black);margin-top:6px}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{cursor:default;height:100px;color:var(--el-text-color-secondary);justify-content:center;align-items:center;font-size:20px;display:flex}.el-autocomplete-suggestion.is-loading li:hover{background-color:var(--el-bg-color-overlay)}.el-avatar{--el-avatar-text-color:var(--el-color-white);--el-avatar-bg-color:var(--el-text-color-disabled);--el-avatar-text-size:14px;--el-avatar-icon-size:18px;--el-avatar-border-radius:var(--el-border-radius-base);--el-avatar-size-large:56px;--el-avatar-size:40px;--el-avatar-size-small:24px;box-sizing:border-box;text-align:center;color:var(--el-avatar-text-color);background:var(--el-avatar-bg-color);width:var(--el-avatar-size);height:var(--el-avatar-size);font-size:var(--el-avatar-text-size);outline:none;justify-content:center;align-items:center;display:inline-flex;overflow:hidden}.el-avatar>img{width:100%;height:100%;display:block}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:var(--el-avatar-border-radius)}.el-avatar--icon{font-size:var(--el-avatar-icon-size)}.el-avatar--small{--el-avatar-size:24px}.el-avatar--large{--el-avatar-size:56px}.el-avatar-group{--el-avatar-group-item-gap:-8px;--el-avatar-group-collapse-item-gap:4px;display:inline-flex}.el-avatar-group .el-avatar{border:1px solid var(--el-border-color-extra-light)}.el-avatar-group .el-avatar:not(:first-child){margin-left:var(--el-avatar-group-item-gap)}.el-avatar-group__collapse-avatars{--el-avatar-group-item-gap:-8px;--el-avatar-group-collapse-item-gap:4px}.el-avatar-group__collapse-avatars .el-avatar:not(:first-child){margin-left:var(--el-avatar-group-collapse-item-gap)}.el-backtop{--el-backtop-bg-color:var(--el-bg-color-overlay);--el-backtop-text-color:var(--el-color-primary);--el-backtop-hover-bg-color:var(--el-border-color-extra-light);background-color:var(--el-backtop-bg-color);width:40px;height:40px;color:var(--el-backtop-text-color);box-shadow:var(--el-box-shadow-lighter);cursor:pointer;z-index:5;border-radius:50%;justify-content:center;align-items:center;font-size:20px;display:flex;position:fixed}.el-backtop:hover{background-color:var(--el-backtop-hover-bg-color)}.el-backtop__icon{font-size:20px}.el-badge{--el-badge-bg-color:var(--el-color-danger);--el-badge-radius:10px;--el-badge-font-size:12px;--el-badge-padding:6px;--el-badge-size:18px;vertical-align:middle;width:-moz-fit-content;width:fit-content;display:inline-block;position:relative}.el-badge__content{background-color:var(--el-badge-bg-color);border-radius:var(--el-badge-radius);color:var(--el-color-white);font-size:var(--el-badge-font-size);height:var(--el-badge-size);padding:0 var(--el-badge-padding);white-space:nowrap;border:1px solid var(--el-bg-color);justify-content:center;align-items:center;display:inline-flex}.el-badge__content.is-fixed{top:0;right:calc(1px + var(--el-badge-size) / 2);z-index:var(--el-index-normal);position:absolute;transform:translateY(-50%)translate(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{border-radius:50%;width:8px;height:8px;padding:0;right:0}.el-badge__content.is-hide-zero{display:none}.el-badge__content--primary{background-color:var(--el-color-primary)}.el-badge__content--success{background-color:var(--el-color-success)}.el-badge__content--warning{background-color:var(--el-color-warning)}.el-badge__content--info{background-color:var(--el-color-info)}.el-badge__content--danger{background-color:var(--el-color-danger)}.el-breadcrumb__separator{color:var(--el-text-color-placeholder);margin:0 9px;font-weight:700}.el-breadcrumb__separator.el-icon{margin:0 6px;font-weight:400}.el-breadcrumb__separator.el-icon svg{vertical-align:middle}.el-breadcrumb__item{float:left;align-items:center;display:inline-flex}.el-breadcrumb__inner{color:var(--el-text-color-regular)}.el-breadcrumb__inner.is-link,.el-breadcrumb__inner a{transition:var(--el-transition-color);color:var(--el-text-color-primary);font-weight:700;text-decoration:none}.el-breadcrumb__inner.is-link:hover,.el-breadcrumb__inner a:hover{color:var(--el-color-primary);cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover{color:var(--el-text-color-regular);cursor:text;font-weight:400}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb:before,.el-breadcrumb:after{content:"";display:table}.el-breadcrumb:after{clear:both}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button:first-child:last-child{border-top-right-radius:var(--el-border-radius-base);border-bottom-right-radius:var(--el-border-radius-base);border-top-left-radius:var(--el-border-radius-base);border-bottom-left-radius:var(--el-border-radius-base)}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:var(--el-border-radius-round)}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:hover,.el-button-group>.el-button:focus,.el-button-group>.el-button:active,.el-button-group>.el-button.is-active{z-index:1}.el-button-group--horizontal{vertical-align:middle;display:inline-block}.el-button-group--horizontal:before,.el-button-group--horizontal:after{content:"";display:table}.el-button-group--horizontal:after{clear:both}.el-button-group--horizontal>.el-button{float:left;position:relative}.el-button-group--horizontal>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group--horizontal>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group--horizontal>.el-button:not(:last-child){margin-right:-1px}.el-button-group--horizontal .el-button--primary:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--primary:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--primary:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--success:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--success:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--success:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--warning:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--warning:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--warning:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--danger:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--danger:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--danger:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--info:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--info:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--info:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal>.el-dropdown>.el-button{border-left-color:var(--el-button-divide-border-color);border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group--vertical{flex-direction:column;align-items:stretch;display:inline-flex}.el-button-group--vertical>.el-button{margin-top:-1px}.el-button-group--vertical>.el-button:first-child{border-bottom-right-radius:0;border-bottom-left-radius:0}.el-button-group--vertical>.el-button:last-child{border-top-left-radius:0;border-top-right-radius:0}.el-button-group--vertical>.el-dropdown{margin-top:-1px}.el-button-group--vertical>.el-dropdown>.el-button{border-left-color:var(--el-button-divide-border-color);border-top-left-radius:0;border-top-right-radius:0}.el-button-group--vertical .el-button--primary:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--primary:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--primary:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--success:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--success:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--success:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--warning:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--warning:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--warning:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--danger:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--danger:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--danger:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--info:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--info:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--info:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button{--el-button-font-weight:var(--el-font-weight-primary);--el-button-border-color:var(--el-border-color);--el-button-bg-color:var(--el-fill-color-blank);--el-button-text-color:var(--el-text-color-regular);--el-button-disabled-text-color:var(--el-disabled-text-color);--el-button-disabled-bg-color:var(--el-fill-color-blank);--el-button-disabled-border-color:var(--el-border-color-light);--el-button-divide-border-color:#ffffff80;--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-color-primary-light-9);--el-button-hover-border-color:var(--el-color-primary-light-7);--el-button-active-text-color:var(--el-button-hover-text-color);--el-button-active-border-color:var(--el-color-primary);--el-button-active-bg-color:var(--el-button-hover-bg-color);--el-button-outline-color:var(--el-color-primary-light-5);--el-button-hover-link-text-color:var(--el-text-color-secondary);--el-button-active-color:var(--el-text-color-primary);white-space:nowrap;cursor:pointer;height:32px;color:var(--el-button-text-color);text-align:center;box-sizing:border-box;line-height:1;font-weight:var(--el-button-font-weight);-webkit-user-select:none;user-select:none;vertical-align:middle;-webkit-appearance:none;background-color:var(--el-button-bg-color);border:var(--el-border);border-color:var(--el-button-border-color);outline:none;justify-content:center;align-items:center;transition:all .1s;display:inline-flex}.el-button:hover{color:var(--el-button-hover-text-color);border-color:var(--el-button-hover-border-color);background-color:var(--el-button-hover-bg-color);outline:none}.el-button:active{color:var(--el-button-active-text-color);border-color:var(--el-button-active-border-color);background-color:var(--el-button-active-bg-color);outline:none}.el-button:focus-visible{outline:2px solid var(--el-button-outline-color);outline-offset:1px;transition:outline-offset,outline}.el-button>span{align-items:center;display:inline-flex}.el-button+.el-button{margin-left:12px}.el-button{font-size:var(--el-font-size-base);border-radius:var(--el-border-radius-base);padding:8px 15px}.el-button.is-round{padding:8px 15px}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon]+span{margin-left:6px}.el-button [class*=el-icon] svg{vertical-align:bottom}.el-button.is-plain{--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-fill-color-blank);--el-button-hover-border-color:var(--el-color-primary)}.el-button.is-active{color:var(--el-button-active-text-color);border-color:var(--el-button-active-border-color);background-color:var(--el-button-active-bg-color);outline:none}.el-button.is-disabled,.el-button.is-disabled:hover{color:var(--el-button-disabled-text-color);cursor:not-allowed;background-image:none;background-color:var(--el-button-disabled-bg-color);border-color:var(--el-button-disabled-border-color)}.el-button.is-loading{pointer-events:none;position:relative}.el-button.is-loading:before{z-index:1;pointer-events:none;content:"";border-radius:inherit;background-color:var(--el-mask-color-extra-light);position:absolute;top:-1px;bottom:-1px;left:-1px;right:-1px}.el-button.is-round{border-radius:var(--el-border-radius-round)}.el-button.is-dashed{--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-fill-color-blank);--el-button-hover-border-color:var(--el-color-primary);border-style:dashed}.el-button.is-circle{border-radius:50%;width:32px;padding:8px}.el-button.is-text{color:var(--el-button-text-color);background-color:#0000;border:0 solid #0000}.el-button.is-text.is-disabled{color:var(--el-button-disabled-text-color);background-color:#0000!important}.el-button.is-text:not(.is-disabled):hover{background-color:var(--el-fill-color-light)}.el-button.is-text:not(.is-disabled):focus-visible{outline:2px solid var(--el-button-outline-color);outline-offset:1px;transition:outline-offset,outline}.el-button.is-text:not(.is-disabled):active{background-color:var(--el-fill-color)}.el-button.is-text:not(.is-disabled).is-has-bg{background-color:var(--el-fill-color-light)}.el-button.is-text:not(.is-disabled).is-has-bg:hover{background-color:var(--el-fill-color)}.el-button.is-text:not(.is-disabled).is-has-bg:active{background-color:var(--el-fill-color-dark)}.el-button__text--expand{letter-spacing:.3em;margin-right:-.3em}.el-button.is-link{color:var(--el-button-text-color);background:0 0;border-color:#0000;height:auto;padding:2px}.el-button.is-link:hover{color:var(--el-button-hover-link-text-color)}.el-button.is-link.is-disabled{color:var(--el-button-disabled-text-color);background-color:#0000!important;border-color:#0000!important}.el-button.is-link:not(.is-disabled):hover{background-color:#0000;border-color:#0000}.el-button.is-link:not(.is-disabled):active{color:var(--el-button-active-color);background-color:#0000;border-color:#0000}.el-button--text{color:var(--el-color-primary);background:0 0;border-color:#0000;padding-left:0;padding-right:0}.el-button--text.is-disabled{color:var(--el-button-disabled-text-color);background-color:#0000!important;border-color:#0000!important}.el-button--text:not(.is-disabled):hover{color:var(--el-color-primary-light-3);background-color:#0000;border-color:#0000}.el-button--text:not(.is-disabled):active{color:var(--el-color-primary-dark-2);background-color:#0000;border-color:#0000}.el-button__link--expand{letter-spacing:.3em;margin-right:-.3em}.el-button--primary{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-primary);--el-button-border-color:var(--el-color-primary);--el-button-outline-color:var(--el-color-primary-light-5);--el-button-active-color:var(--el-color-primary-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-primary-light-5);--el-button-hover-bg-color:var(--el-color-primary-light-3);--el-button-hover-border-color:var(--el-color-primary-light-3);--el-button-active-bg-color:var(--el-color-primary-dark-2);--el-button-active-border-color:var(--el-color-primary-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-primary-light-5);--el-button-disabled-border-color:var(--el-color-primary-light-5)}.el-button--primary.is-plain,.el-button--primary.is-text,.el-button--primary.is-link{--el-button-text-color:var(--el-color-primary);--el-button-bg-color:var(--el-color-primary-light-9);--el-button-border-color:var(--el-color-primary-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-primary);--el-button-hover-border-color:var(--el-color-primary);--el-button-active-text-color:var(--el-color-white)}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:hover,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-text.is-disabled,.el-button--primary.is-text.is-disabled:hover,.el-button--primary.is-text.is-disabled:focus,.el-button--primary.is-text.is-disabled:active,.el-button--primary.is-link.is-disabled,.el-button--primary.is-link.is-disabled:hover,.el-button--primary.is-link.is-disabled:focus,.el-button--primary.is-link.is-disabled:active{color:var(--el-color-primary-light-5);background-color:var(--el-color-primary-light-9);border-color:var(--el-color-primary-light-8)}.el-button--primary.is-dashed{--el-button-text-color:var(--el-color-primary);--el-button-bg-color:var(--el-color-primary-light-9);--el-button-border-color:var(--el-color-primary-light-5);--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-color-primary-light-9);--el-button-hover-border-color:var(--el-color-primary-light-3);--el-button-active-text-color:var(--el-color-primary-dark-2);--el-button-active-bg-color:var(--el-color-primary-light-9);--el-button-active-border-color:var(--el-color-primary-dark-2)}.el-button--primary.is-dashed.is-disabled,.el-button--primary.is-dashed.is-disabled:hover,.el-button--primary.is-dashed.is-disabled:focus,.el-button--primary.is-dashed.is-disabled:active{color:var(--el-color-primary-light-5);background-color:var(--el-color-primary-light-9);border-color:var(--el-color-primary-light-8)}.el-button--success{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-success);--el-button-border-color:var(--el-color-success);--el-button-outline-color:var(--el-color-success-light-5);--el-button-active-color:var(--el-color-success-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-success-light-5);--el-button-hover-bg-color:var(--el-color-success-light-3);--el-button-hover-border-color:var(--el-color-success-light-3);--el-button-active-bg-color:var(--el-color-success-dark-2);--el-button-active-border-color:var(--el-color-success-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-success-light-5);--el-button-disabled-border-color:var(--el-color-success-light-5)}.el-button--success.is-plain,.el-button--success.is-text,.el-button--success.is-link{--el-button-text-color:var(--el-color-success);--el-button-bg-color:var(--el-color-success-light-9);--el-button-border-color:var(--el-color-success-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-success);--el-button-hover-border-color:var(--el-color-success);--el-button-active-text-color:var(--el-color-white)}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:hover,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-text.is-disabled,.el-button--success.is-text.is-disabled:hover,.el-button--success.is-text.is-disabled:focus,.el-button--success.is-text.is-disabled:active,.el-button--success.is-link.is-disabled,.el-button--success.is-link.is-disabled:hover,.el-button--success.is-link.is-disabled:focus,.el-button--success.is-link.is-disabled:active{color:var(--el-color-success-light-5);background-color:var(--el-color-success-light-9);border-color:var(--el-color-success-light-8)}.el-button--success.is-dashed{--el-button-text-color:var(--el-color-success);--el-button-bg-color:var(--el-color-success-light-9);--el-button-border-color:var(--el-color-success-light-5);--el-button-hover-text-color:var(--el-color-success);--el-button-hover-bg-color:var(--el-color-success-light-9);--el-button-hover-border-color:var(--el-color-success-light-3);--el-button-active-text-color:var(--el-color-success-dark-2);--el-button-active-bg-color:var(--el-color-success-light-9);--el-button-active-border-color:var(--el-color-success-dark-2)}.el-button--success.is-dashed.is-disabled,.el-button--success.is-dashed.is-disabled:hover,.el-button--success.is-dashed.is-disabled:focus,.el-button--success.is-dashed.is-disabled:active{color:var(--el-color-success-light-5);background-color:var(--el-color-success-light-9);border-color:var(--el-color-success-light-8)}.el-button--warning{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-warning);--el-button-border-color:var(--el-color-warning);--el-button-outline-color:var(--el-color-warning-light-5);--el-button-active-color:var(--el-color-warning-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-warning-light-5);--el-button-hover-bg-color:var(--el-color-warning-light-3);--el-button-hover-border-color:var(--el-color-warning-light-3);--el-button-active-bg-color:var(--el-color-warning-dark-2);--el-button-active-border-color:var(--el-color-warning-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-warning-light-5);--el-button-disabled-border-color:var(--el-color-warning-light-5)}.el-button--warning.is-plain,.el-button--warning.is-text,.el-button--warning.is-link{--el-button-text-color:var(--el-color-warning);--el-button-bg-color:var(--el-color-warning-light-9);--el-button-border-color:var(--el-color-warning-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-warning);--el-button-hover-border-color:var(--el-color-warning);--el-button-active-text-color:var(--el-color-white)}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:hover,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-text.is-disabled,.el-button--warning.is-text.is-disabled:hover,.el-button--warning.is-text.is-disabled:focus,.el-button--warning.is-text.is-disabled:active,.el-button--warning.is-link.is-disabled,.el-button--warning.is-link.is-disabled:hover,.el-button--warning.is-link.is-disabled:focus,.el-button--warning.is-link.is-disabled:active{color:var(--el-color-warning-light-5);background-color:var(--el-color-warning-light-9);border-color:var(--el-color-warning-light-8)}.el-button--warning.is-dashed{--el-button-text-color:var(--el-color-warning);--el-button-bg-color:var(--el-color-warning-light-9);--el-button-border-color:var(--el-color-warning-light-5);--el-button-hover-text-color:var(--el-color-warning);--el-button-hover-bg-color:var(--el-color-warning-light-9);--el-button-hover-border-color:var(--el-color-warning-light-3);--el-button-active-text-color:var(--el-color-warning-dark-2);--el-button-active-bg-color:var(--el-color-warning-light-9);--el-button-active-border-color:var(--el-color-warning-dark-2)}.el-button--warning.is-dashed.is-disabled,.el-button--warning.is-dashed.is-disabled:hover,.el-button--warning.is-dashed.is-disabled:focus,.el-button--warning.is-dashed.is-disabled:active{color:var(--el-color-warning-light-5);background-color:var(--el-color-warning-light-9);border-color:var(--el-color-warning-light-8)}.el-button--danger{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-danger);--el-button-border-color:var(--el-color-danger);--el-button-outline-color:var(--el-color-danger-light-5);--el-button-active-color:var(--el-color-danger-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-danger-light-5);--el-button-hover-bg-color:var(--el-color-danger-light-3);--el-button-hover-border-color:var(--el-color-danger-light-3);--el-button-active-bg-color:var(--el-color-danger-dark-2);--el-button-active-border-color:var(--el-color-danger-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-danger-light-5);--el-button-disabled-border-color:var(--el-color-danger-light-5)}.el-button--danger.is-plain,.el-button--danger.is-text,.el-button--danger.is-link{--el-button-text-color:var(--el-color-danger);--el-button-bg-color:var(--el-color-danger-light-9);--el-button-border-color:var(--el-color-danger-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-danger);--el-button-hover-border-color:var(--el-color-danger);--el-button-active-text-color:var(--el-color-white)}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:hover,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-text.is-disabled,.el-button--danger.is-text.is-disabled:hover,.el-button--danger.is-text.is-disabled:focus,.el-button--danger.is-text.is-disabled:active,.el-button--danger.is-link.is-disabled,.el-button--danger.is-link.is-disabled:hover,.el-button--danger.is-link.is-disabled:focus,.el-button--danger.is-link.is-disabled:active{color:var(--el-color-danger-light-5);background-color:var(--el-color-danger-light-9);border-color:var(--el-color-danger-light-8)}.el-button--danger.is-dashed{--el-button-text-color:var(--el-color-danger);--el-button-bg-color:var(--el-color-danger-light-9);--el-button-border-color:var(--el-color-danger-light-5);--el-button-hover-text-color:var(--el-color-danger);--el-button-hover-bg-color:var(--el-color-danger-light-9);--el-button-hover-border-color:var(--el-color-danger-light-3);--el-button-active-text-color:var(--el-color-danger-dark-2);--el-button-active-bg-color:var(--el-color-danger-light-9);--el-button-active-border-color:var(--el-color-danger-dark-2)}.el-button--danger.is-dashed.is-disabled,.el-button--danger.is-dashed.is-disabled:hover,.el-button--danger.is-dashed.is-disabled:focus,.el-button--danger.is-dashed.is-disabled:active{color:var(--el-color-danger-light-5);background-color:var(--el-color-danger-light-9);border-color:var(--el-color-danger-light-8)}.el-button--info{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-info);--el-button-border-color:var(--el-color-info);--el-button-outline-color:var(--el-color-info-light-5);--el-button-active-color:var(--el-color-info-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-info-light-5);--el-button-hover-bg-color:var(--el-color-info-light-3);--el-button-hover-border-color:var(--el-color-info-light-3);--el-button-active-bg-color:var(--el-color-info-dark-2);--el-button-active-border-color:var(--el-color-info-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-info-light-5);--el-button-disabled-border-color:var(--el-color-info-light-5)}.el-button--info.is-plain,.el-button--info.is-text,.el-button--info.is-link{--el-button-text-color:var(--el-color-info);--el-button-bg-color:var(--el-color-info-light-9);--el-button-border-color:var(--el-color-info-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-info);--el-button-hover-border-color:var(--el-color-info);--el-button-active-text-color:var(--el-color-white)}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:hover,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-text.is-disabled,.el-button--info.is-text.is-disabled:hover,.el-button--info.is-text.is-disabled:focus,.el-button--info.is-text.is-disabled:active,.el-button--info.is-link.is-disabled,.el-button--info.is-link.is-disabled:hover,.el-button--info.is-link.is-disabled:focus,.el-button--info.is-link.is-disabled:active{color:var(--el-color-info-light-5);background-color:var(--el-color-info-light-9);border-color:var(--el-color-info-light-8)}.el-button--info.is-dashed{--el-button-text-color:var(--el-color-info);--el-button-bg-color:var(--el-color-info-light-9);--el-button-border-color:var(--el-color-info-light-5);--el-button-hover-text-color:var(--el-color-info);--el-button-hover-bg-color:var(--el-color-info-light-9);--el-button-hover-border-color:var(--el-color-info-light-3);--el-button-active-text-color:var(--el-color-info-dark-2);--el-button-active-bg-color:var(--el-color-info-light-9);--el-button-active-border-color:var(--el-color-info-dark-2)}.el-button--info.is-dashed.is-disabled,.el-button--info.is-dashed.is-disabled:hover,.el-button--info.is-dashed.is-disabled:focus,.el-button--info.is-dashed.is-disabled:active{color:var(--el-color-info-light-5);background-color:var(--el-color-info-light-9);border-color:var(--el-color-info-light-8)}.el-button--large{--el-button-size:40px;height:var(--el-button-size)}.el-button--large [class*=el-icon]+span{margin-left:8px}.el-button--large{font-size:var(--el-font-size-base);border-radius:var(--el-border-radius-base);padding:12px 19px}.el-button--large.is-round{padding:12px 19px}.el-button--large.is-circle{width:var(--el-button-size);padding:12px}.el-button--small{--el-button-size:24px;height:var(--el-button-size)}.el-button--small [class*=el-icon]+span{margin-left:4px}.el-button--small{border-radius:calc(var(--el-border-radius-base) - 1px);padding:5px 11px;font-size:12px}.el-button--small.is-round{padding:5px 11px}.el-button--small.is-circle{width:var(--el-button-size);padding:5px}.el-calendar{--el-calendar-border:var(--el-table-border,1px solid var(--el-border-color-lighter));--el-calendar-header-border-bottom:var(--el-calendar-border);--el-calendar-selected-bg-color:var(--el-color-primary-light-9);--el-calendar-cell-width:85px;background-color:var(--el-fill-color-blank)}.el-calendar__header{border-bottom:var(--el-calendar-header-border-bottom);justify-content:space-between;padding:12px 20px;display:flex}.el-calendar__title{color:var(--el-text-color);align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar__select-controller .el-select{margin-right:8px}.el-calendar__select-controller .el-calendar-select__year{width:120px}.el-calendar__select-controller .el-calendar-select__month{width:60px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{color:var(--el-text-color-regular);padding:12px 0;font-weight:400}.el-calendar-table:not(.is-range) td.prev,.el-calendar-table:not(.is-range) td.next{color:var(--el-text-color-placeholder)}.el-calendar-table td{border-bottom:var(--el-calendar-border);border-right:var(--el-calendar-border);vertical-align:top;transition:background-color var(--el-transition-duration-fast) ease}.el-calendar-table td.is-selected{background-color:var(--el-calendar-selected-bg-color)}.el-calendar-table td.is-today{color:var(--el-color-primary)}.el-calendar-table tr:first-child td{border-top:var(--el-calendar-border)}.el-calendar-table tr td:first-child{border-left:var(--el-calendar-border)}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{box-sizing:border-box;height:var(--el-calendar-cell-width);padding:8px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:var(--el-calendar-selected-bg-color)}.el-card{--el-card-border-color:var(--el-border-color-light);--el-card-border-radius:4px;--el-card-padding:20px;--el-card-bg-color:var(--el-fill-color-blank);border-radius:var(--el-card-border-radius);border:1px solid var(--el-card-border-color);background-color:var(--el-card-bg-color);color:var(--el-text-color-primary);transition:var(--el-transition-duration);flex-direction:column;display:flex;overflow:hidden}.el-card.is-always-shadow,.el-card.is-hover-shadow:hover,.el-card.is-hover-shadow:focus{box-shadow:var(--el-box-shadow-light)}.el-card__header{padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding);border-bottom:1px solid var(--el-card-border-color);box-sizing:border-box}.el-card__body{padding:var(--el-card-padding);flex-grow:1;overflow:auto}.el-card__footer{padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding);border-top:1px solid var(--el-card-border-color);box-sizing:border-box}.el-carousel__item{width:100%;height:100%;z-index:calc(var(--el-index-normal) - 1);display:inline-block;position:absolute;top:0;left:0;overflow:hidden}.el-carousel__item.is-active{z-index:calc(var(--el-index-normal) - 1)}.el-carousel__item.is-animating{transition:transform .4s ease-in-out}.el-carousel__item--card{width:50%;transition:transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:var(--el-index-normal)}.el-carousel__item--card.is-in-stage:hover .el-carousel__mask,.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:calc(var(--el-index-normal) + 1)}.el-carousel__item--card-vertical{width:100%;height:50%}.el-carousel__mask{background-color:var(--el-color-white);opacity:.24;width:100%;height:100%;transition:var(--el-transition-duration-fast);position:absolute;top:0;left:0}.el-carousel{--el-carousel-arrow-font-size:12px;--el-carousel-arrow-size:36px;--el-carousel-arrow-background:#1f2d3d1c;--el-carousel-arrow-hover-background:#1f2d3d3b;--el-carousel-indicator-width:30px;--el-carousel-indicator-height:2px;--el-carousel-indicator-padding-horizontal:4px;--el-carousel-indicator-padding-vertical:12px;--el-carousel-indicator-out-color:var(--el-border-color-hover);position:relative}.el-carousel--horizontal,.el-carousel--vertical{overflow:hidden}.el-carousel.is-vertical-outside{flex-direction:row;align-items:center;display:flex}.el-carousel.is-vertical-outside .el-carousel__container{flex:1}.el-carousel__container{height:300px;position:relative}.el-carousel__arrow{height:var(--el-carousel-arrow-size);width:var(--el-carousel-arrow-size);cursor:pointer;transition:var(--el-transition-duration);background-color:var(--el-carousel-arrow-background);color:#fff;z-index:10;text-align:center;font-size:var(--el-carousel-arrow-font-size);border:none;border-radius:50%;outline:none;justify-content:center;align-items:center;margin:0;padding:0;display:inline-flex;position:absolute;top:50%;transform:translateY(-50%)}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:var(--el-carousel-arrow-hover-background)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{z-index:calc(var(--el-index-normal) + 1);margin:0;padding:0;list-style:none;position:absolute}.el-carousel__indicators--horizontal{bottom:0;left:50%;transform:translate(-50%)}.el-carousel__indicators--vertical{top:50%;right:0;transform:translateY(-50%)}.el-carousel__indicators--outside{text-align:center;position:static;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:var(--el-carousel-indicator-out-color);opacity:.24}.el-carousel__indicators--right{right:0}.el-carousel__indicators--labels .el-carousel__button{color:#000;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{cursor:pointer;background-color:#0000}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{padding:var(--el-carousel-indicator-padding-vertical) var(--el-carousel-indicator-padding-horizontal);display:inline-block}.el-carousel__indicator--vertical{padding:var(--el-carousel-indicator-padding-horizontal) var(--el-carousel-indicator-padding-vertical)}.el-carousel__indicator--vertical .el-carousel__button{width:var(--el-carousel-indicator-height);height:calc(var(--el-carousel-indicator-width) / 2)}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{opacity:.48;width:var(--el-carousel-indicator-width);height:var(--el-carousel-indicator-height);cursor:pointer;transition:var(--el-transition-duration);background-color:#fff;border:none;outline:none;margin:0;padding:0;display:block}.el-carousel__indicators--labels .el-carousel__button{width:auto;height:auto}.carousel-arrow-left-enter-from,.carousel-arrow-left-leave-active{opacity:0;transform:translateY(-50%)translate(-10px)}.carousel-arrow-right-enter-from,.carousel-arrow-right-leave-active{opacity:0;transform:translateY(-50%)translate(10px)}.el-transitioning{filter:url(#elCarouselHorizontal)}.el-transitioning-vertical{filter:url(#elCarouselVertical)}.el-cascader-panel{--el-cascader-menu-text-color:var(--el-text-color-regular);--el-cascader-menu-selected-text-color:var(--el-color-primary);--el-cascader-menu-fill:var(--el-bg-color-overlay);--el-cascader-menu-font-size:var(--el-font-size-base);--el-cascader-menu-radius:var(--el-border-radius-base);--el-cascader-menu-border:solid 1px var(--el-border-color-light);--el-cascader-menu-shadow:var(--el-box-shadow-light);--el-cascader-node-background-hover:var(--el-fill-color-light);--el-cascader-node-color-disabled:var(--el-text-color-placeholder);--el-cascader-color-empty:var(--el-text-color-placeholder);--el-cascader-tag-background:var(--el-fill-color);border-radius:var(--el-cascader-menu-radius);width:-moz-fit-content;width:fit-content;font-size:var(--el-cascader-menu-font-size);display:flex}.el-cascader-panel.is-bordered{border:var(--el-cascader-menu-border);border-radius:var(--el-cascader-menu-radius)}.el-cascader-menu{box-sizing:border-box;min-width:180px;color:var(--el-cascader-menu-text-color);border-right:var(--el-cascader-menu-border)}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu:last-child .el-cascader-node{padding-right:20px}.el-cascader-menu__wrap.el-scrollbar__wrap{height:204px}.el-cascader-menu__list{box-sizing:border-box;min-height:100%;margin:0;padding:6px 0;list-style:none;position:relative}.el-cascader-menu__hover-zone{pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0}.el-cascader-menu__empty-text{color:var(--el-cascader-color-empty);align-items:center;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-cascader-menu__empty-text .is-loading{margin-right:2px}.el-cascader-node{outline:none;align-items:center;height:34px;padding:0 30px 0 20px;line-height:34px;display:flex;position:relative}.el-cascader-node.is-selectable.in-active-path{color:var(--el-cascader-menu-text-color)}.el-cascader-node.in-active-path,.el-cascader-node.is-selectable.in-checked-path,.el-cascader-node.is-active{color:var(--el-cascader-menu-selected-text-color);font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):hover,.el-cascader-node:not(.is-disabled):focus{background:var(--el-cascader-node-background-hover)}.el-cascader-node.is-disabled{color:var(--el-cascader-node-color-disabled);cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{text-align:left;white-space:nowrap;text-overflow:ellipsis;flex:1;padding:0 8px;overflow:hidden}.el-cascader-node>.el-checkbox,.el-cascader-node>.el-radio{margin-right:0}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-cascader{--el-cascader-menu-text-color:var(--el-text-color-regular);--el-cascader-menu-selected-text-color:var(--el-color-primary);--el-cascader-menu-fill:var(--el-bg-color-overlay);--el-cascader-menu-font-size:var(--el-font-size-base);--el-cascader-menu-radius:var(--el-border-radius-base);--el-cascader-menu-border:solid 1px var(--el-border-color-light);--el-cascader-menu-shadow:var(--el-box-shadow-light);--el-cascader-node-background-hover:var(--el-fill-color-light);--el-cascader-node-color-disabled:var(--el-text-color-placeholder);--el-cascader-color-empty:var(--el-text-color-placeholder);--el-cascader-tag-background:var(--el-fill-color);vertical-align:middle;font-size:var(--el-font-size-base);outline:none;line-height:32px;display:inline-block;position:relative}.el-cascader:not(.is-disabled):hover .el-input__wrapper{cursor:pointer;box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-cascader .el-input{cursor:pointer;display:flex}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-input__inner:read-only{cursor:pointer}.el-cascader .el-input .el-input__inner:disabled{cursor:not-allowed}.el-cascader .el-input .el-input__suffix-inner .el-icon svg{vertical-align:middle}.el-cascader .el-input .icon-arrow-down{transition:transform var(--el-transition-duration);font-size:14px}.el-cascader .el-input .icon-arrow-down.is-reverse{transform:rotate(180deg)}.el-cascader .el-input .icon-circle-close:hover{color:var(--el-input-clear-hover-color,var(--el-text-color-secondary))}.el-cascader .el-input.is-focus .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color,var(--el-color-primary)) inset}.el-cascader--large{font-size:14px;line-height:40px}.el-cascader--large .el-cascader__tags{gap:6px;padding:8px}.el-cascader--large .el-cascader__search-input{height:24px;margin-left:7px}.el-cascader--small{font-size:12px;line-height:24px}.el-cascader--small .el-cascader__tags{gap:4px;padding:2px}.el-cascader--small .el-cascader__search-input{height:20px;margin-left:5px}.el-cascader.is-disabled .el-cascader__label{z-index:calc(var(--el-index-normal) + 1);color:var(--el-disabled-text-color)}.el-cascader__dropdown{--el-cascader-menu-text-color:var(--el-text-color-regular);--el-cascader-menu-selected-text-color:var(--el-color-primary);--el-cascader-menu-fill:var(--el-bg-color-overlay);--el-cascader-menu-font-size:var(--el-font-size-base);--el-cascader-menu-radius:var(--el-border-radius-base);--el-cascader-menu-border:solid 1px var(--el-border-color-light);--el-cascader-menu-shadow:var(--el-box-shadow-light);--el-cascader-node-background-hover:var(--el-fill-color-light);--el-cascader-node-color-disabled:var(--el-text-color-placeholder);--el-cascader-color-empty:var(--el-text-color-placeholder);--el-cascader-tag-background:var(--el-fill-color);font-size:var(--el-cascader-menu-font-size);border-radius:var(--el-cascader-menu-radius)}.el-cascader__dropdown.el-popper{background:var(--el-cascader-menu-fill);border:var(--el-cascader-menu-border);box-shadow:var(--el-cascader-menu-shadow)}.el-cascader__dropdown.el-popper .el-popper__arrow:before{border:var(--el-cascader-menu-border)}.el-cascader__dropdown.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-cascader__dropdown.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-cascader__dropdown.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-cascader__dropdown.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-cascader__dropdown.el-popper{box-shadow:var(--el-cascader-menu-shadow)}.el-cascader__header{border-bottom:1px solid var(--el-border-color-light);padding:10px}.el-cascader__footer{border-top:1px solid var(--el-border-color-light);padding:10px}.el-cascader__tags{text-align:left;box-sizing:border-box;flex-wrap:wrap;gap:6px;padding:4px;line-height:normal;display:flex;position:absolute;top:50%;left:0;right:30px;transform:translateY(-50%)}.el-cascader__tags .el-tag{text-overflow:ellipsis;background:var(--el-cascader-tag-background);align-items:center;max-width:100%;display:inline-flex}.el-cascader__tags .el-tag.el-tag--dark,.el-cascader__tags .el-tag.el-tag--plain{background-color:var(--el-tag-bg-color)}.el-cascader__tags .el-tag:not(.is-hit){border-color:#0000}.el-cascader__tags .el-tag:not(.is-hit).el-tag--dark,.el-cascader__tags .el-tag:not(.is-hit).el-tag--plain{border-color:var(--el-tag-border-color)}.el-cascader__tags .el-tag>span{text-overflow:ellipsis;flex:1;line-height:normal;overflow:hidden}.el-cascader__tags .el-tag .el-icon-close{background-color:var(--el-text-color-placeholder);color:var(--el-color-white);flex:none}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:var(--el-text-color-secondary)}.el-cascader__tags .el-tag+input{margin-left:0}.el-cascader__tags.is-validate{right:55px}.el-cascader__collapse-tags{white-space:normal;z-index:var(--el-index-normal)}.el-cascader__collapse-tags .el-tag{text-overflow:ellipsis;background:var(--el-fill-color);align-items:center;max-width:100%;display:inline-flex}.el-cascader__collapse-tags .el-tag.el-tag--dark,.el-cascader__collapse-tags .el-tag.el-tag--plain{background-color:var(--el-tag-bg-color)}.el-cascader__collapse-tags .el-tag:not(.is-hit){border-color:#0000}.el-cascader__collapse-tags .el-tag:not(.is-hit).el-tag--dark,.el-cascader__collapse-tags .el-tag:not(.is-hit).el-tag--plain{border-color:var(--el-tag-border-color)}.el-cascader__collapse-tags .el-tag>span{text-overflow:ellipsis;flex:1;line-height:normal;overflow:hidden}.el-cascader__collapse-tags .el-tag .el-icon-close{background-color:var(--el-text-color-placeholder);color:var(--el-color-white);flex:none}.el-cascader__collapse-tags .el-tag .el-icon-close:hover{background-color:var(--el-text-color-secondary)}.el-cascader__collapse-tags .el-tag+input{margin-left:0}.el-cascader__collapse-tags .el-tag{margin:2px 0}.el-cascader__suggestion-panel{border-radius:var(--el-cascader-menu-radius)}.el-cascader__suggestion-list{max-height:204px;font-size:var(--el-font-size-base);color:var(--el-cascader-menu-text-color);text-align:center;margin:0;padding:6px 0}.el-cascader__suggestion-item{text-align:left;cursor:pointer;outline:none;justify-content:space-between;align-items:center;height:34px;padding:0 15px;display:flex}.el-cascader__suggestion-item:hover,.el-cascader__suggestion-item:focus{background:var(--el-cascader-node-background-hover)}.el-cascader__suggestion-item.is-checked{color:var(--el-cascader-menu-selected-text-color);font-weight:700}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__empty-text{color:var(--el-cascader-color-empty);margin:10px 0}.el-cascader__search-input{min-width:60px;height:24px;color:var(--el-cascader-menu-text-color);box-sizing:border-box;background:0 0;border:none;outline:none;flex:1;margin-left:7px;padding:0}.el-cascader__search-input::placeholder{color:#0000}.el-check-tag{background-color:var(--el-color-info-light-9);border-radius:var(--el-border-radius-base);color:var(--el-color-info);cursor:pointer;font-size:var(--el-font-size-base);line-height:var(--el-font-size-base);transition:var(--el-transition-all);padding:7px 15px;font-weight:700;display:inline-block}.el-check-tag:hover{background-color:var(--el-color-info-light-7)}.el-check-tag.el-check-tag--primary.is-checked{background-color:var(--el-color-primary-light-8);color:var(--el-color-primary)}.el-check-tag.el-check-tag--primary.is-checked:hover{background-color:var(--el-color-primary-light-7)}.el-check-tag.el-check-tag--primary.is-checked.is-disabled{background-color:var(--el-color-primary-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--primary.is-checked.is-disabled:hover{background-color:var(--el-color-primary-light-8)}.el-check-tag.el-check-tag--primary.is-disabled{background-color:var(--el-color-info-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--primary.is-disabled:hover{background-color:var(--el-color-info-light-9)}.el-check-tag.el-check-tag--success.is-checked{background-color:var(--el-color-success-light-8);color:var(--el-color-success)}.el-check-tag.el-check-tag--success.is-checked:hover{background-color:var(--el-color-success-light-7)}.el-check-tag.el-check-tag--success.is-checked.is-disabled{background-color:var(--el-color-success-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--success.is-checked.is-disabled:hover{background-color:var(--el-color-success-light-8)}.el-check-tag.el-check-tag--success.is-disabled{background-color:var(--el-color-success-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--success.is-disabled:hover{background-color:var(--el-color-success-light-9)}.el-check-tag.el-check-tag--warning.is-checked{background-color:var(--el-color-warning-light-8);color:var(--el-color-warning)}.el-check-tag.el-check-tag--warning.is-checked:hover{background-color:var(--el-color-warning-light-7)}.el-check-tag.el-check-tag--warning.is-checked.is-disabled{background-color:var(--el-color-warning-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--warning.is-checked.is-disabled:hover{background-color:var(--el-color-warning-light-8)}.el-check-tag.el-check-tag--warning.is-disabled{background-color:var(--el-color-warning-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--warning.is-disabled:hover{background-color:var(--el-color-warning-light-9)}.el-check-tag.el-check-tag--danger.is-checked{background-color:var(--el-color-danger-light-8);color:var(--el-color-danger)}.el-check-tag.el-check-tag--danger.is-checked:hover{background-color:var(--el-color-danger-light-7)}.el-check-tag.el-check-tag--danger.is-checked.is-disabled{background-color:var(--el-color-danger-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--danger.is-checked.is-disabled:hover{background-color:var(--el-color-danger-light-8)}.el-check-tag.el-check-tag--danger.is-disabled{background-color:var(--el-color-danger-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--danger.is-disabled:hover{background-color:var(--el-color-danger-light-9)}.el-check-tag.el-check-tag--error.is-checked{background-color:var(--el-color-error-light-8);color:var(--el-color-error)}.el-check-tag.el-check-tag--error.is-checked:hover{background-color:var(--el-color-error-light-7)}.el-check-tag.el-check-tag--error.is-checked.is-disabled{background-color:var(--el-color-error-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--error.is-checked.is-disabled:hover{background-color:var(--el-color-error-light-8)}.el-check-tag.el-check-tag--error.is-disabled{background-color:var(--el-color-error-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--error.is-disabled:hover{background-color:var(--el-color-error-light-9)}.el-check-tag.el-check-tag--info.is-checked{background-color:var(--el-color-info-light-8);color:var(--el-color-info)}.el-check-tag.el-check-tag--info.is-checked:hover{background-color:var(--el-color-info-light-7)}.el-check-tag.el-check-tag--info.is-checked.is-disabled{background-color:var(--el-color-info-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--info.is-checked.is-disabled:hover{background-color:var(--el-color-info-light-8)}.el-check-tag.el-check-tag--info.is-disabled{background-color:var(--el-color-info-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--info.is-disabled:hover{background-color:var(--el-color-info-light-9)}.el-checkbox-button{--el-checkbox-button-checked-bg-color:var(--el-color-primary);--el-checkbox-button-checked-text-color:var(--el-color-white);--el-checkbox-button-checked-border-color:var(--el-color-primary);--el-checkbox-button-disabled-checked-fill:var(--el-border-color-extra-light);display:inline-block;position:relative}.el-checkbox-button__inner{line-height:1;font-weight:var(--el-checkbox-font-weight);white-space:nowrap;vertical-align:middle;cursor:pointer;background:var(--el-button-bg-color,var(--el-fill-color-blank));outline:var(--el-border);color:var(--el-button-text-color,var(--el-text-color-regular));-webkit-appearance:none;text-align:center;box-sizing:border-box;transition:var(--el-transition-all);-webkit-user-select:none;user-select:none;font-size:var(--el-font-size-base);border-radius:0;margin:0;padding:8px 15px;display:inline-block;position:relative}.el-checkbox-button__inner.is-round{padding:8px 15px}.el-checkbox-button__inner:hover{color:var(--el-color-primary)}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;z-index:-1;outline:none;margin:0;position:absolute}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:var(--el-checkbox-button-checked-text-color);background-color:var(--el-checkbox-button-checked-bg-color);border-color:var(--el-checkbox-button-checked-border-color);box-shadow:-1px 0 0 0 var(--el-color-primary-light-7)}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:var(--el-checkbox-button-checked-border-color)}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:var(--el-disabled-text-color);cursor:not-allowed;background-image:none;background-color:var(--el-button-disabled-bg-color,var(--el-fill-color-blank));border-color:var(--el-button-disabled-border-color,var(--el-border-color-light));box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:var(--el-button-disabled-border-color,var(--el-border-color-light))}.el-checkbox-button.is-disabled.is-checked .el-checkbox-button__inner{background-color:var(--el-checkbox-button-disabled-checked-fill)}.el-checkbox-button:first-child .el-checkbox-button__inner{border-top-left-radius:var(--el-border-radius-base);border-bottom-left-radius:var(--el-border-radius-base);box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:var(--el-checkbox-button-checked-border-color)}.el-checkbox-button:last-child .el-checkbox-button__inner{border-top-right-radius:var(--el-border-radius-base);border-bottom-right-radius:var(--el-border-radius-base)}.el-checkbox-button--large .el-checkbox-button__inner{font-size:var(--el-font-size-base);border-radius:0;padding:12px 19px}.el-checkbox-button--large .el-checkbox-button__inner.is-round{padding:12px 19px}.el-checkbox-button--small .el-checkbox-button__inner{border-radius:0;padding:5px 11px;font-size:12px}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:5px 11px}.el-checkbox-group{font-size:0;line-height:0}.el-checkbox{--el-checkbox-font-size:14px;--el-checkbox-font-weight:var(--el-font-weight-primary);--el-checkbox-text-color:var(--el-text-color-regular);--el-checkbox-input-height:14px;--el-checkbox-input-width:14px;--el-checkbox-border-radius:var(--el-border-radius-small);--el-checkbox-bg-color:var(--el-fill-color-blank);--el-checkbox-input-border:var(--el-border);--el-checkbox-disabled-border-color:var(--el-border-color);--el-checkbox-disabled-input-fill:var(--el-fill-color-light);--el-checkbox-disabled-icon-color:var(--el-text-color-placeholder);--el-checkbox-disabled-checked-input-fill:var(--el-border-color-extra-light);--el-checkbox-disabled-checked-input-border-color:var(--el-border-color);--el-checkbox-disabled-checked-icon-color:var(--el-text-color-placeholder);--el-checkbox-checked-text-color:var(--el-color-primary);--el-checkbox-checked-input-border-color:var(--el-color-primary);--el-checkbox-checked-bg-color:var(--el-color-primary);--el-checkbox-checked-icon-color:var(--el-color-white);--el-checkbox-input-border-color-hover:var(--el-color-primary);color:var(--el-checkbox-text-color);font-weight:var(--el-checkbox-font-weight);font-size:var(--el-font-size-base);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;height:var(--el-checkbox-height,32px);align-items:center;margin-right:30px;display:inline-flex;position:relative}.el-checkbox.is-disabled{cursor:not-allowed}.el-checkbox.is-bordered{border-radius:var(--el-border-radius-base);border:var(--el-border);box-sizing:border-box;padding:0 15px 0 9px}.el-checkbox.is-bordered.is-checked{border-color:var(--el-color-primary)}.el-checkbox.is-bordered.is-disabled{border-color:var(--el-border-color-lighter)}.el-checkbox.is-bordered.el-checkbox--large{border-radius:var(--el-border-radius-base);padding:0 19px 0 11px}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__label{font-size:var(--el-font-size-base)}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__inner{width:14px;height:14px}.el-checkbox.is-bordered.el-checkbox--small{border-radius:calc(var(--el-border-radius-base) - 1px);padding:0 11px 0 7px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{width:12px;height:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{width:2px;height:6px}.el-checkbox input:focus-visible+.el-checkbox__inner{outline:2px solid var(--el-checkbox-input-border-color-hover);outline-offset:1px;border-radius:var(--el-checkbox-border-radius)}.el-checkbox__input{white-space:nowrap;cursor:pointer;outline:none;display:inline-flex;position:relative}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:var(--el-checkbox-disabled-input-fill);border-color:var(--el-checkbox-disabled-border-color);cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:var(--el-checkbox-disabled-icon-color);will-change:transform}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:var(--el-checkbox-disabled-checked-icon-color);border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:var(--el-disabled-text-color);cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-checked-icon-color);transform:translate(-45%,-60%)rotate(45deg)scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:var(--el-checkbox-checked-text-color)}.el-checkbox__input.is-focus:not(.is-checked) .el-checkbox__original:not(:focus-visible){border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";background-color:var(--el-checkbox-checked-icon-color);height:2px;display:block;position:absolute;top:5px;left:0;right:0;transform:scale(.5)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{border:var(--el-checkbox-input-border);border-radius:var(--el-checkbox-border-radius);box-sizing:border-box;width:var(--el-checkbox-input-width);height:var(--el-checkbox-input-height);background-color:var(--el-checkbox-bg-color);z-index:var(--el-index-normal);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46),outline .25s cubic-bezier(.71,-.46,.29,1.46);display:inline-block;position:relative}.el-checkbox__inner:hover{border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__inner:after{box-sizing:content-box;content:"";transform-origin:50%;border:1px solid #0000;border-top:0;border-left:0;width:3px;height:7px;transition:transform .15s ease-in 50ms;position:absolute;top:50%;left:50%;transform:translate(-45%,-60%)rotate(45deg)scaleY(0)}.el-checkbox__original{opacity:0;z-index:-1;outline:none;width:0;height:0;margin:0;position:absolute}.el-checkbox__label{line-height:1;font-size:var(--el-checkbox-font-size);padding-left:8px;display:inline-block}.el-checkbox.el-checkbox--large{height:40px}.el-checkbox.el-checkbox--large .el-checkbox__label{font-size:14px}.el-checkbox.el-checkbox--large .el-checkbox__inner{width:14px;height:14px}.el-checkbox.el-checkbox--small{height:24px}.el-checkbox.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.el-checkbox--small .el-checkbox__inner{width:12px;height:12px}.el-checkbox.el-checkbox--small .el-checkbox__input.is-indeterminate .el-checkbox__inner:before{top:4px}.el-checkbox.el-checkbox--small .el-checkbox__inner:after{width:2px;height:6px}.el-checkbox:last-of-type{margin-right:0}[class*=el-col-]{box-sizing:border-box}[class*=el-col-].is-guttered{min-height:1px;display:block}.el-col-0{flex:0 0;max-width:0%;display:none}.el-col-0.is-guttered{display:none}.el-col-offset-0{margin-left:0%}.el-col-pull-0{position:relative;right:0%}.el-col-push-0{position:relative;left:0%}.el-col-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-1.is-guttered{display:block}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{position:relative;right:4.16667%}.el-col-push-1{position:relative;left:4.16667%}.el-col-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-2.is-guttered{display:block}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{position:relative;right:8.33333%}.el-col-push-2{position:relative;left:8.33333%}.el-col-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-3.is-guttered{display:block}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{position:relative;right:12.5%}.el-col-push-3{position:relative;left:12.5%}.el-col-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-4.is-guttered{display:block}.el-col-offset-4{margin-left:16.6667%}.el-col-pull-4{position:relative;right:16.6667%}.el-col-push-4{position:relative;left:16.6667%}.el-col-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-5.is-guttered{display:block}.el-col-offset-5{margin-left:20.8333%}.el-col-pull-5{position:relative;right:20.8333%}.el-col-push-5{position:relative;left:20.8333%}.el-col-6{flex:0 0 25%;max-width:25%;display:block}.el-col-6.is-guttered{display:block}.el-col-offset-6{margin-left:25%}.el-col-pull-6{position:relative;right:25%}.el-col-push-6{position:relative;left:25%}.el-col-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-7.is-guttered{display:block}.el-col-offset-7{margin-left:29.1667%}.el-col-pull-7{position:relative;right:29.1667%}.el-col-push-7{position:relative;left:29.1667%}.el-col-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-8.is-guttered{display:block}.el-col-offset-8{margin-left:33.3333%}.el-col-pull-8{position:relative;right:33.3333%}.el-col-push-8{position:relative;left:33.3333%}.el-col-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-9.is-guttered{display:block}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{position:relative;right:37.5%}.el-col-push-9{position:relative;left:37.5%}.el-col-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-10.is-guttered{display:block}.el-col-offset-10{margin-left:41.6667%}.el-col-pull-10{position:relative;right:41.6667%}.el-col-push-10{position:relative;left:41.6667%}.el-col-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-11.is-guttered{display:block}.el-col-offset-11{margin-left:45.8333%}.el-col-pull-11{position:relative;right:45.8333%}.el-col-push-11{position:relative;left:45.8333%}.el-col-12{flex:0 0 50%;max-width:50%;display:block}.el-col-12.is-guttered{display:block}.el-col-offset-12{margin-left:50%}.el-col-pull-12{position:relative;right:50%}.el-col-push-12{position:relative;left:50%}.el-col-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-13.is-guttered{display:block}.el-col-offset-13{margin-left:54.1667%}.el-col-pull-13{position:relative;right:54.1667%}.el-col-push-13{position:relative;left:54.1667%}.el-col-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-14.is-guttered{display:block}.el-col-offset-14{margin-left:58.3333%}.el-col-pull-14{position:relative;right:58.3333%}.el-col-push-14{position:relative;left:58.3333%}.el-col-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-15.is-guttered{display:block}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{position:relative;right:62.5%}.el-col-push-15{position:relative;left:62.5%}.el-col-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-16.is-guttered{display:block}.el-col-offset-16{margin-left:66.6667%}.el-col-pull-16{position:relative;right:66.6667%}.el-col-push-16{position:relative;left:66.6667%}.el-col-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-17.is-guttered{display:block}.el-col-offset-17{margin-left:70.8333%}.el-col-pull-17{position:relative;right:70.8333%}.el-col-push-17{position:relative;left:70.8333%}.el-col-18{flex:0 0 75%;max-width:75%;display:block}.el-col-18.is-guttered{display:block}.el-col-offset-18{margin-left:75%}.el-col-pull-18{position:relative;right:75%}.el-col-push-18{position:relative;left:75%}.el-col-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-19.is-guttered{display:block}.el-col-offset-19{margin-left:79.1667%}.el-col-pull-19{position:relative;right:79.1667%}.el-col-push-19{position:relative;left:79.1667%}.el-col-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-20.is-guttered{display:block}.el-col-offset-20{margin-left:83.3333%}.el-col-pull-20{position:relative;right:83.3333%}.el-col-push-20{position:relative;left:83.3333%}.el-col-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-21.is-guttered{display:block}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{position:relative;right:87.5%}.el-col-push-21{position:relative;left:87.5%}.el-col-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-22.is-guttered{display:block}.el-col-offset-22{margin-left:91.6667%}.el-col-pull-22{position:relative;right:91.6667%}.el-col-push-22{position:relative;left:91.6667%}.el-col-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-23.is-guttered{display:block}.el-col-offset-23{margin-left:95.8333%}.el-col-pull-23{position:relative;right:95.8333%}.el-col-push-23{position:relative;left:95.8333%}.el-col-24{flex:0 0 100%;max-width:100%;display:block}.el-col-24.is-guttered{display:block}.el-col-offset-24{margin-left:100%}.el-col-pull-24{position:relative;right:100%}.el-col-push-24{position:relative;left:100%}@media only screen and (max-width:767px){.el-col-xs-0{flex:0 0;max-width:0%;display:none}.el-col-xs-0.is-guttered{display:none}.el-col-xs-offset-0{margin-left:0%}.el-col-xs-pull-0{position:relative;right:0%}.el-col-xs-push-0{position:relative;left:0%}.el-col-xs-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-xs-1.is-guttered{display:block}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-xs-2.is-guttered{display:block}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-xs-3.is-guttered{display:block}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-xs-4.is-guttered{display:block}.el-col-xs-offset-4{margin-left:16.6667%}.el-col-xs-pull-4{position:relative;right:16.6667%}.el-col-xs-push-4{position:relative;left:16.6667%}.el-col-xs-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-xs-5.is-guttered{display:block}.el-col-xs-offset-5{margin-left:20.8333%}.el-col-xs-pull-5{position:relative;right:20.8333%}.el-col-xs-push-5{position:relative;left:20.8333%}.el-col-xs-6{flex:0 0 25%;max-width:25%;display:block}.el-col-xs-6.is-guttered{display:block}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-xs-7.is-guttered{display:block}.el-col-xs-offset-7{margin-left:29.1667%}.el-col-xs-pull-7{position:relative;right:29.1667%}.el-col-xs-push-7{position:relative;left:29.1667%}.el-col-xs-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-xs-8.is-guttered{display:block}.el-col-xs-offset-8{margin-left:33.3333%}.el-col-xs-pull-8{position:relative;right:33.3333%}.el-col-xs-push-8{position:relative;left:33.3333%}.el-col-xs-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-xs-9.is-guttered{display:block}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-xs-10.is-guttered{display:block}.el-col-xs-offset-10{margin-left:41.6667%}.el-col-xs-pull-10{position:relative;right:41.6667%}.el-col-xs-push-10{position:relative;left:41.6667%}.el-col-xs-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-xs-11.is-guttered{display:block}.el-col-xs-offset-11{margin-left:45.8333%}.el-col-xs-pull-11{position:relative;right:45.8333%}.el-col-xs-push-11{position:relative;left:45.8333%}.el-col-xs-12{flex:0 0 50%;max-width:50%;display:block}.el-col-xs-12.is-guttered{display:block}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-xs-13.is-guttered{display:block}.el-col-xs-offset-13{margin-left:54.1667%}.el-col-xs-pull-13{position:relative;right:54.1667%}.el-col-xs-push-13{position:relative;left:54.1667%}.el-col-xs-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-xs-14.is-guttered{display:block}.el-col-xs-offset-14{margin-left:58.3333%}.el-col-xs-pull-14{position:relative;right:58.3333%}.el-col-xs-push-14{position:relative;left:58.3333%}.el-col-xs-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-xs-15.is-guttered{display:block}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-xs-16.is-guttered{display:block}.el-col-xs-offset-16{margin-left:66.6667%}.el-col-xs-pull-16{position:relative;right:66.6667%}.el-col-xs-push-16{position:relative;left:66.6667%}.el-col-xs-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-xs-17.is-guttered{display:block}.el-col-xs-offset-17{margin-left:70.8333%}.el-col-xs-pull-17{position:relative;right:70.8333%}.el-col-xs-push-17{position:relative;left:70.8333%}.el-col-xs-18{flex:0 0 75%;max-width:75%;display:block}.el-col-xs-18.is-guttered{display:block}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-xs-19.is-guttered{display:block}.el-col-xs-offset-19{margin-left:79.1667%}.el-col-xs-pull-19{position:relative;right:79.1667%}.el-col-xs-push-19{position:relative;left:79.1667%}.el-col-xs-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-xs-20.is-guttered{display:block}.el-col-xs-offset-20{margin-left:83.3333%}.el-col-xs-pull-20{position:relative;right:83.3333%}.el-col-xs-push-20{position:relative;left:83.3333%}.el-col-xs-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-xs-21.is-guttered{display:block}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-xs-22.is-guttered{display:block}.el-col-xs-offset-22{margin-left:91.6667%}.el-col-xs-pull-22{position:relative;right:91.6667%}.el-col-xs-push-22{position:relative;left:91.6667%}.el-col-xs-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-xs-23.is-guttered{display:block}.el-col-xs-offset-23{margin-left:95.8333%}.el-col-xs-pull-23{position:relative;right:95.8333%}.el-col-xs-push-23{position:relative;left:95.8333%}.el-col-xs-24{flex:0 0 100%;max-width:100%;display:block}.el-col-xs-24.is-guttered{display:block}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{flex:0 0;max-width:0%;display:none}.el-col-sm-0.is-guttered{display:none}.el-col-sm-offset-0{margin-left:0%}.el-col-sm-pull-0{position:relative;right:0%}.el-col-sm-push-0{position:relative;left:0%}.el-col-sm-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-sm-1.is-guttered{display:block}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-sm-2.is-guttered{display:block}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-sm-3.is-guttered{display:block}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-sm-4.is-guttered{display:block}.el-col-sm-offset-4{margin-left:16.6667%}.el-col-sm-pull-4{position:relative;right:16.6667%}.el-col-sm-push-4{position:relative;left:16.6667%}.el-col-sm-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-sm-5.is-guttered{display:block}.el-col-sm-offset-5{margin-left:20.8333%}.el-col-sm-pull-5{position:relative;right:20.8333%}.el-col-sm-push-5{position:relative;left:20.8333%}.el-col-sm-6{flex:0 0 25%;max-width:25%;display:block}.el-col-sm-6.is-guttered{display:block}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-sm-7.is-guttered{display:block}.el-col-sm-offset-7{margin-left:29.1667%}.el-col-sm-pull-7{position:relative;right:29.1667%}.el-col-sm-push-7{position:relative;left:29.1667%}.el-col-sm-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-sm-8.is-guttered{display:block}.el-col-sm-offset-8{margin-left:33.3333%}.el-col-sm-pull-8{position:relative;right:33.3333%}.el-col-sm-push-8{position:relative;left:33.3333%}.el-col-sm-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-sm-9.is-guttered{display:block}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-sm-10.is-guttered{display:block}.el-col-sm-offset-10{margin-left:41.6667%}.el-col-sm-pull-10{position:relative;right:41.6667%}.el-col-sm-push-10{position:relative;left:41.6667%}.el-col-sm-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-sm-11.is-guttered{display:block}.el-col-sm-offset-11{margin-left:45.8333%}.el-col-sm-pull-11{position:relative;right:45.8333%}.el-col-sm-push-11{position:relative;left:45.8333%}.el-col-sm-12{flex:0 0 50%;max-width:50%;display:block}.el-col-sm-12.is-guttered{display:block}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-sm-13.is-guttered{display:block}.el-col-sm-offset-13{margin-left:54.1667%}.el-col-sm-pull-13{position:relative;right:54.1667%}.el-col-sm-push-13{position:relative;left:54.1667%}.el-col-sm-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-sm-14.is-guttered{display:block}.el-col-sm-offset-14{margin-left:58.3333%}.el-col-sm-pull-14{position:relative;right:58.3333%}.el-col-sm-push-14{position:relative;left:58.3333%}.el-col-sm-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-sm-15.is-guttered{display:block}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-sm-16.is-guttered{display:block}.el-col-sm-offset-16{margin-left:66.6667%}.el-col-sm-pull-16{position:relative;right:66.6667%}.el-col-sm-push-16{position:relative;left:66.6667%}.el-col-sm-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-sm-17.is-guttered{display:block}.el-col-sm-offset-17{margin-left:70.8333%}.el-col-sm-pull-17{position:relative;right:70.8333%}.el-col-sm-push-17{position:relative;left:70.8333%}.el-col-sm-18{flex:0 0 75%;max-width:75%;display:block}.el-col-sm-18.is-guttered{display:block}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-sm-19.is-guttered{display:block}.el-col-sm-offset-19{margin-left:79.1667%}.el-col-sm-pull-19{position:relative;right:79.1667%}.el-col-sm-push-19{position:relative;left:79.1667%}.el-col-sm-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-sm-20.is-guttered{display:block}.el-col-sm-offset-20{margin-left:83.3333%}.el-col-sm-pull-20{position:relative;right:83.3333%}.el-col-sm-push-20{position:relative;left:83.3333%}.el-col-sm-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-sm-21.is-guttered{display:block}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-sm-22.is-guttered{display:block}.el-col-sm-offset-22{margin-left:91.6667%}.el-col-sm-pull-22{position:relative;right:91.6667%}.el-col-sm-push-22{position:relative;left:91.6667%}.el-col-sm-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-sm-23.is-guttered{display:block}.el-col-sm-offset-23{margin-left:95.8333%}.el-col-sm-pull-23{position:relative;right:95.8333%}.el-col-sm-push-23{position:relative;left:95.8333%}.el-col-sm-24{flex:0 0 100%;max-width:100%;display:block}.el-col-sm-24.is-guttered{display:block}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{flex:0 0;max-width:0%;display:none}.el-col-md-0.is-guttered{display:none}.el-col-md-offset-0{margin-left:0%}.el-col-md-pull-0{position:relative;right:0%}.el-col-md-push-0{position:relative;left:0%}.el-col-md-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-md-1.is-guttered{display:block}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-md-2.is-guttered{display:block}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-md-3.is-guttered{display:block}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-md-4.is-guttered{display:block}.el-col-md-offset-4{margin-left:16.6667%}.el-col-md-pull-4{position:relative;right:16.6667%}.el-col-md-push-4{position:relative;left:16.6667%}.el-col-md-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-md-5.is-guttered{display:block}.el-col-md-offset-5{margin-left:20.8333%}.el-col-md-pull-5{position:relative;right:20.8333%}.el-col-md-push-5{position:relative;left:20.8333%}.el-col-md-6{flex:0 0 25%;max-width:25%;display:block}.el-col-md-6.is-guttered{display:block}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-md-7.is-guttered{display:block}.el-col-md-offset-7{margin-left:29.1667%}.el-col-md-pull-7{position:relative;right:29.1667%}.el-col-md-push-7{position:relative;left:29.1667%}.el-col-md-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-md-8.is-guttered{display:block}.el-col-md-offset-8{margin-left:33.3333%}.el-col-md-pull-8{position:relative;right:33.3333%}.el-col-md-push-8{position:relative;left:33.3333%}.el-col-md-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-md-9.is-guttered{display:block}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-md-10.is-guttered{display:block}.el-col-md-offset-10{margin-left:41.6667%}.el-col-md-pull-10{position:relative;right:41.6667%}.el-col-md-push-10{position:relative;left:41.6667%}.el-col-md-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-md-11.is-guttered{display:block}.el-col-md-offset-11{margin-left:45.8333%}.el-col-md-pull-11{position:relative;right:45.8333%}.el-col-md-push-11{position:relative;left:45.8333%}.el-col-md-12{flex:0 0 50%;max-width:50%;display:block}.el-col-md-12.is-guttered{display:block}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-md-13.is-guttered{display:block}.el-col-md-offset-13{margin-left:54.1667%}.el-col-md-pull-13{position:relative;right:54.1667%}.el-col-md-push-13{position:relative;left:54.1667%}.el-col-md-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-md-14.is-guttered{display:block}.el-col-md-offset-14{margin-left:58.3333%}.el-col-md-pull-14{position:relative;right:58.3333%}.el-col-md-push-14{position:relative;left:58.3333%}.el-col-md-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-md-15.is-guttered{display:block}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-md-16.is-guttered{display:block}.el-col-md-offset-16{margin-left:66.6667%}.el-col-md-pull-16{position:relative;right:66.6667%}.el-col-md-push-16{position:relative;left:66.6667%}.el-col-md-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-md-17.is-guttered{display:block}.el-col-md-offset-17{margin-left:70.8333%}.el-col-md-pull-17{position:relative;right:70.8333%}.el-col-md-push-17{position:relative;left:70.8333%}.el-col-md-18{flex:0 0 75%;max-width:75%;display:block}.el-col-md-18.is-guttered{display:block}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-md-19.is-guttered{display:block}.el-col-md-offset-19{margin-left:79.1667%}.el-col-md-pull-19{position:relative;right:79.1667%}.el-col-md-push-19{position:relative;left:79.1667%}.el-col-md-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-md-20.is-guttered{display:block}.el-col-md-offset-20{margin-left:83.3333%}.el-col-md-pull-20{position:relative;right:83.3333%}.el-col-md-push-20{position:relative;left:83.3333%}.el-col-md-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-md-21.is-guttered{display:block}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-md-22.is-guttered{display:block}.el-col-md-offset-22{margin-left:91.6667%}.el-col-md-pull-22{position:relative;right:91.6667%}.el-col-md-push-22{position:relative;left:91.6667%}.el-col-md-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-md-23.is-guttered{display:block}.el-col-md-offset-23{margin-left:95.8333%}.el-col-md-pull-23{position:relative;right:95.8333%}.el-col-md-push-23{position:relative;left:95.8333%}.el-col-md-24{flex:0 0 100%;max-width:100%;display:block}.el-col-md-24.is-guttered{display:block}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{flex:0 0;max-width:0%;display:none}.el-col-lg-0.is-guttered{display:none}.el-col-lg-offset-0{margin-left:0%}.el-col-lg-pull-0{position:relative;right:0%}.el-col-lg-push-0{position:relative;left:0%}.el-col-lg-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-lg-1.is-guttered{display:block}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-lg-2.is-guttered{display:block}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-lg-3.is-guttered{display:block}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-lg-4.is-guttered{display:block}.el-col-lg-offset-4{margin-left:16.6667%}.el-col-lg-pull-4{position:relative;right:16.6667%}.el-col-lg-push-4{position:relative;left:16.6667%}.el-col-lg-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-lg-5.is-guttered{display:block}.el-col-lg-offset-5{margin-left:20.8333%}.el-col-lg-pull-5{position:relative;right:20.8333%}.el-col-lg-push-5{position:relative;left:20.8333%}.el-col-lg-6{flex:0 0 25%;max-width:25%;display:block}.el-col-lg-6.is-guttered{display:block}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-lg-7.is-guttered{display:block}.el-col-lg-offset-7{margin-left:29.1667%}.el-col-lg-pull-7{position:relative;right:29.1667%}.el-col-lg-push-7{position:relative;left:29.1667%}.el-col-lg-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-lg-8.is-guttered{display:block}.el-col-lg-offset-8{margin-left:33.3333%}.el-col-lg-pull-8{position:relative;right:33.3333%}.el-col-lg-push-8{position:relative;left:33.3333%}.el-col-lg-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-lg-9.is-guttered{display:block}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-lg-10.is-guttered{display:block}.el-col-lg-offset-10{margin-left:41.6667%}.el-col-lg-pull-10{position:relative;right:41.6667%}.el-col-lg-push-10{position:relative;left:41.6667%}.el-col-lg-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-lg-11.is-guttered{display:block}.el-col-lg-offset-11{margin-left:45.8333%}.el-col-lg-pull-11{position:relative;right:45.8333%}.el-col-lg-push-11{position:relative;left:45.8333%}.el-col-lg-12{flex:0 0 50%;max-width:50%;display:block}.el-col-lg-12.is-guttered{display:block}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-lg-13.is-guttered{display:block}.el-col-lg-offset-13{margin-left:54.1667%}.el-col-lg-pull-13{position:relative;right:54.1667%}.el-col-lg-push-13{position:relative;left:54.1667%}.el-col-lg-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-lg-14.is-guttered{display:block}.el-col-lg-offset-14{margin-left:58.3333%}.el-col-lg-pull-14{position:relative;right:58.3333%}.el-col-lg-push-14{position:relative;left:58.3333%}.el-col-lg-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-lg-15.is-guttered{display:block}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-lg-16.is-guttered{display:block}.el-col-lg-offset-16{margin-left:66.6667%}.el-col-lg-pull-16{position:relative;right:66.6667%}.el-col-lg-push-16{position:relative;left:66.6667%}.el-col-lg-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-lg-17.is-guttered{display:block}.el-col-lg-offset-17{margin-left:70.8333%}.el-col-lg-pull-17{position:relative;right:70.8333%}.el-col-lg-push-17{position:relative;left:70.8333%}.el-col-lg-18{flex:0 0 75%;max-width:75%;display:block}.el-col-lg-18.is-guttered{display:block}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-lg-19.is-guttered{display:block}.el-col-lg-offset-19{margin-left:79.1667%}.el-col-lg-pull-19{position:relative;right:79.1667%}.el-col-lg-push-19{position:relative;left:79.1667%}.el-col-lg-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-lg-20.is-guttered{display:block}.el-col-lg-offset-20{margin-left:83.3333%}.el-col-lg-pull-20{position:relative;right:83.3333%}.el-col-lg-push-20{position:relative;left:83.3333%}.el-col-lg-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-lg-21.is-guttered{display:block}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-lg-22.is-guttered{display:block}.el-col-lg-offset-22{margin-left:91.6667%}.el-col-lg-pull-22{position:relative;right:91.6667%}.el-col-lg-push-22{position:relative;left:91.6667%}.el-col-lg-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-lg-23.is-guttered{display:block}.el-col-lg-offset-23{margin-left:95.8333%}.el-col-lg-pull-23{position:relative;right:95.8333%}.el-col-lg-push-23{position:relative;left:95.8333%}.el-col-lg-24{flex:0 0 100%;max-width:100%;display:block}.el-col-lg-24.is-guttered{display:block}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{flex:0 0;max-width:0%;display:none}.el-col-xl-0.is-guttered{display:none}.el-col-xl-offset-0{margin-left:0%}.el-col-xl-pull-0{position:relative;right:0%}.el-col-xl-push-0{position:relative;left:0%}.el-col-xl-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-xl-1.is-guttered{display:block}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-xl-2.is-guttered{display:block}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-xl-3.is-guttered{display:block}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-xl-4.is-guttered{display:block}.el-col-xl-offset-4{margin-left:16.6667%}.el-col-xl-pull-4{position:relative;right:16.6667%}.el-col-xl-push-4{position:relative;left:16.6667%}.el-col-xl-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-xl-5.is-guttered{display:block}.el-col-xl-offset-5{margin-left:20.8333%}.el-col-xl-pull-5{position:relative;right:20.8333%}.el-col-xl-push-5{position:relative;left:20.8333%}.el-col-xl-6{flex:0 0 25%;max-width:25%;display:block}.el-col-xl-6.is-guttered{display:block}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-xl-7.is-guttered{display:block}.el-col-xl-offset-7{margin-left:29.1667%}.el-col-xl-pull-7{position:relative;right:29.1667%}.el-col-xl-push-7{position:relative;left:29.1667%}.el-col-xl-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-xl-8.is-guttered{display:block}.el-col-xl-offset-8{margin-left:33.3333%}.el-col-xl-pull-8{position:relative;right:33.3333%}.el-col-xl-push-8{position:relative;left:33.3333%}.el-col-xl-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-xl-9.is-guttered{display:block}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-xl-10.is-guttered{display:block}.el-col-xl-offset-10{margin-left:41.6667%}.el-col-xl-pull-10{position:relative;right:41.6667%}.el-col-xl-push-10{position:relative;left:41.6667%}.el-col-xl-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-xl-11.is-guttered{display:block}.el-col-xl-offset-11{margin-left:45.8333%}.el-col-xl-pull-11{position:relative;right:45.8333%}.el-col-xl-push-11{position:relative;left:45.8333%}.el-col-xl-12{flex:0 0 50%;max-width:50%;display:block}.el-col-xl-12.is-guttered{display:block}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-xl-13.is-guttered{display:block}.el-col-xl-offset-13{margin-left:54.1667%}.el-col-xl-pull-13{position:relative;right:54.1667%}.el-col-xl-push-13{position:relative;left:54.1667%}.el-col-xl-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-xl-14.is-guttered{display:block}.el-col-xl-offset-14{margin-left:58.3333%}.el-col-xl-pull-14{position:relative;right:58.3333%}.el-col-xl-push-14{position:relative;left:58.3333%}.el-col-xl-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-xl-15.is-guttered{display:block}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-xl-16.is-guttered{display:block}.el-col-xl-offset-16{margin-left:66.6667%}.el-col-xl-pull-16{position:relative;right:66.6667%}.el-col-xl-push-16{position:relative;left:66.6667%}.el-col-xl-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-xl-17.is-guttered{display:block}.el-col-xl-offset-17{margin-left:70.8333%}.el-col-xl-pull-17{position:relative;right:70.8333%}.el-col-xl-push-17{position:relative;left:70.8333%}.el-col-xl-18{flex:0 0 75%;max-width:75%;display:block}.el-col-xl-18.is-guttered{display:block}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-xl-19.is-guttered{display:block}.el-col-xl-offset-19{margin-left:79.1667%}.el-col-xl-pull-19{position:relative;right:79.1667%}.el-col-xl-push-19{position:relative;left:79.1667%}.el-col-xl-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-xl-20.is-guttered{display:block}.el-col-xl-offset-20{margin-left:83.3333%}.el-col-xl-pull-20{position:relative;right:83.3333%}.el-col-xl-push-20{position:relative;left:83.3333%}.el-col-xl-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-xl-21.is-guttered{display:block}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-xl-22.is-guttered{display:block}.el-col-xl-offset-22{margin-left:91.6667%}.el-col-xl-pull-22{position:relative;right:91.6667%}.el-col-xl-push-22{position:relative;left:91.6667%}.el-col-xl-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-xl-23.is-guttered{display:block}.el-col-xl-offset-23{margin-left:95.8333%}.el-col-xl-pull-23{position:relative;right:95.8333%}.el-col-xl-push-23{position:relative;left:95.8333%}.el-col-xl-24{flex:0 0 100%;max-width:100%;display:block}.el-col-xl-24.is-guttered{display:block}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}.el-collapse-item.is-disabled .el-collapse-item__header{color:var(--el-text-color-disabled);cursor:not-allowed}.el-collapse-item__header{width:100%;min-height:var(--el-collapse-header-height);line-height:var(--el-collapse-header-height);background-color:var(--el-collapse-header-bg-color);color:var(--el-collapse-header-text-color);cursor:pointer;border:none;border-bottom:1px solid var(--el-collapse-border-color);font-size:var(--el-collapse-header-font-size);transition:border-bottom-color var(--el-transition-duration);box-sizing:border-box;outline:none;align-items:center;padding:0;font-weight:500;display:flex}.el-collapse-item__arrow{transition:transform var(--el-transition-duration);font-weight:300}.el-collapse-item__arrow.is-active{transform:rotate(90deg)}.el-collapse-item__title{text-align:left;flex:auto}.el-collapse-item__header.focusing:focus:not(:hover){color:var(--el-color-primary)}.el-collapse-item__header.is-active{border-bottom-color:#0000}.el-collapse-item__wrap{will-change:height;background-color:var(--el-collapse-content-bg-color);box-sizing:border-box;border-bottom:1px solid var(--el-collapse-border-color);overflow:hidden}.el-collapse-item__content{font-size:var(--el-collapse-content-font-size);color:var(--el-collapse-content-text-color);padding-bottom:25px;line-height:1.76923}.el-collapse-item:last-child{margin-bottom:-1px}.el-collapse{--el-collapse-border-color:var(--el-border-color-lighter);--el-collapse-header-height:48px;--el-collapse-header-bg-color:var(--el-fill-color-blank);--el-collapse-header-text-color:var(--el-text-color-primary);--el-collapse-header-font-size:13px;--el-collapse-content-bg-color:var(--el-fill-color-blank);--el-collapse-content-font-size:13px;--el-collapse-content-text-color:var(--el-text-color-primary);border-top:1px solid var(--el-collapse-border-color);border-bottom:1px solid var(--el-collapse-border-color)}.el-collapse-icon-position-left .el-collapse-item__header{gap:8px}.el-collapse-icon-position-left .el-collapse-item__title{order:1}.el-collapse-icon-position-right .el-collapse-item__header{padding-right:8px}.el-color-picker-panel{--el-colorpicker-bg-color:var(--el-bg-color-overlay);--el-fill-color-blank:var(--el-colorpicker-bg-color);box-sizing:content-box;background:var(--el-colorpicker-bg-color);width:300px;padding:12px}.el-color-picker-panel.is-border{border:solid 1px var(--el-border-color-lighter);border-radius:4px}.el-color-picker-panel__wrapper{margin-bottom:6px}.el-color-picker-panel__footer{text-align:right;justify-content:space-between;margin-top:12px;display:flex}.el-color-picker-panel__footer .el-input{color:#000;width:160px;font-size:12px;line-height:26px}.el-color-picker-panel.is-disabled .el-color-svpanel,.el-color-picker-panel.is-disabled .el-color-hue-slider{cursor:not-allowed;opacity:.3}.el-color-picker-panel.is-disabled .el-color-hue-slider__thumb{cursor:not-allowed}.el-color-picker-panel.is-disabled .el-color-alpha-slider,.el-color-picker-panel.is-disabled .el-color-predefine .el-color-predefine__color-selector{cursor:not-allowed;opacity:.3}.el-color-predefine{width:280px;margin-top:8px;font-size:12px;display:flex}.el-color-predefine__colors{flex-wrap:wrap;flex:1;gap:8px;display:flex}.el-color-predefine__color-selector{border-radius:var(--el-border-radius-base);cursor:pointer;border:none;outline:none;width:20px;height:20px;padding:0;overflow:hidden}.el-color-predefine__color-selector.selected{box-shadow:0 0 3px 2px var(--el-color-primary)}.el-color-predefine__color-selector:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-color-predefine__color-selector>div{height:100%;display:flex}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{box-sizing:border-box;float:right;background-color:red;width:280px;height:12px;padding:0 2px;position:relative}.el-color-hue-slider__bar{background:linear-gradient(90deg,red,#ff0 17%,#0f0 33%,#0ff,#00f 67%,#f0f 83%,red);height:100%;position:relative}.el-color-hue-slider__thumb{cursor:pointer;box-sizing:border-box;border:1px solid var(--el-border-color-lighter);z-index:1;background:#fff;border-radius:1px;width:4px;height:100%;position:absolute;top:0;left:0;box-shadow:0 0 2px #0009}.el-color-hue-slider__thumb:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:1px}.el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:linear-gradient(red,#ff0 17%,#0f0 33%,#0ff,#00f 67%,#f0f 83%,red)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{width:100%;height:4px;top:0;left:0}.el-color-svpanel{background-image:linear-gradient(#0000,#000),linear-gradient(90deg,#fff,#fff0);width:280px;height:180px;position:relative}.el-color-svpanel__cursor{cursor:pointer;border-radius:50%;width:4px;height:4px;position:absolute;transform:translate(-2px,-2px);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px #0000004d,0 0 1px 2px #0006}.el-color-svpanel__cursor:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-color-alpha-slider{box-sizing:border-box;background-image:linear-gradient(45deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(45deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%);background-position:0 0,6px 0,6px -6px,0 6px;background-size:12px 12px;width:280px;height:12px;position:relative}.el-color-alpha-slider.is-disabled .el-color-alpha-slider__thumb{cursor:not-allowed}.el-color-alpha-slider__bar{background:linear-gradient(to right,#fff0 0%,var(--el-bg-color) 100%);height:100%;position:relative}.el-color-alpha-slider__thumb{cursor:pointer;box-sizing:border-box;border:1px solid var(--el-border-color-lighter);z-index:1;background:#fff;border-radius:1px;width:4px;height:100%;position:absolute;top:0;left:0;box-shadow:0 0 2px #0009}.el-color-alpha-slider__thumb:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:1px}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:linear-gradient(#fff0,#fff)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{width:100%;height:4px;top:0;left:0}.el-color-picker-panel{--el-color-picker-alpha-bg-a:#ccc;--el-color-picker-alpha-bg-b:transparent}.dark .el-color-picker-panel{--el-color-picker-alpha-bg-a:#333}.el-color-picker{outline:none;width:32px;height:32px;line-height:normal;display:inline-block;position:relative}.el-color-picker:hover:not(:-webkit-any(.is-disabled,.is-focused)) .el-color-picker__trigger{border-color:var(--el-border-color-hover)}.el-color-picker:hover:not(:is(.is-disabled,.is-focused)) .el-color-picker__trigger{border-color:var(--el-border-color-hover)}.el-color-picker:focus-visible:not(.is-disabled) .el-color-picker__trigger{outline:2px solid var(--el-color-primary);outline-offset:1px}.el-color-picker.is-focused .el-color-picker__trigger{border-color:var(--el-color-primary)}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed;background-color:var(--el-fill-color-light)}.el-color-picker.is-disabled .el-color-picker__color{opacity:.3}.el-color-picker--large{width:40px;height:40px}.el-color-picker--small{width:24px;height:24px}.el-color-picker--small .el-color-picker__icon,.el-color-picker--small .el-color-picker__empty{transform:scale(.8)}.el-color-picker__trigger{box-sizing:border-box;border:1px solid var(--el-border-color);cursor:pointer;border-radius:4px;justify-content:center;align-items:center;width:100%;height:100%;padding:4px;font-size:0;display:inline-flex;position:relative}.el-color-picker__color{box-sizing:border-box;border:1px solid var(--el-text-color-secondary);border-radius:var(--el-border-radius-small);text-align:center;width:100%;height:100%;display:block;position:relative}.el-color-picker__color.is-alpha{background-image:linear-gradient(45deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(45deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%);background-position:0 0,6px 0,6px -6px,0 6px;background-size:12px 12px}.el-color-picker__color-inner{justify-content:center;align-items:center;width:100%;height:100%;display:inline-flex}.el-color-picker .el-color-picker__empty{color:var(--el-text-color-secondary);font-size:12px}.el-color-picker .el-color-picker__icon{color:#fff;justify-content:center;align-items:center;font-size:12px;display:inline-flex}.el-color-picker__panel{border-radius:var(--el-border-radius-base);box-shadow:var(--el-box-shadow-light);background-color:#fff}.el-color-picker__panel.el-popper{border:1px solid var(--el-border-color-lighter)}.el-color-picker,.el-color-picker__panel{--el-color-picker-alpha-bg-a:#ccc;--el-color-picker-alpha-bg-b:transparent}.dark .el-color-picker,.dark .el-color-picker__panel{--el-color-picker-alpha-bg-a:#333}.el-container{box-sizing:border-box;flex-direction:row;flex:auto;min-width:0;display:flex}.el-container.is-vertical{flex-direction:column}.el-date-table{-webkit-user-select:none;user-select:none;font-size:12px}.el-date-table.is-week-mode .el-date-table__row:hover .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:var(--el-datepicker-text-color)}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child .el-date-table-cell{border-top-left-radius:15px;border-bottom-left-radius:15px;margin-left:5px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child .el-date-table-cell{border-top-right-radius:15px;border-bottom-right-radius:15px;margin-right:5px}.el-date-table.is-week-mode .el-date-table__row.current .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-date-table td{box-sizing:border-box;text-align:center;cursor:pointer;width:32px;height:30px;padding:4px 0;position:relative}.el-date-table td .el-date-table-cell{box-sizing:border-box;height:30px;padding:3px 0}.el-date-table td .el-date-table-cell .el-date-table-cell__text{border-radius:50%;width:24px;height:24px;margin:0 auto;line-height:24px;display:block;position:absolute;left:50%;transform:translate(-50%)}.el-date-table td.next-month,.el-date-table td.prev-month{color:var(--el-datepicker-off-text-color)}.el-date-table td.today{position:relative}.el-date-table td.today .el-date-table-cell__text{color:var(--el-color-primary);font-weight:700}.el-date-table td.today.start-date .el-date-table-cell__text,.el-date-table td.today.end-date .el-date-table-cell__text{color:#fff}.el-date-table td.available:hover{color:var(--el-datepicker-hover-text-color)}.el-date-table td.in-range .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-date-table td.in-range .el-date-table-cell:hover{background-color:var(--el-datepicker-inrange-hover-bg-color)}.el-date-table td.current:not(.disabled) .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-date-table td.current:not(.disabled):focus-visible .el-date-table-cell__text{outline:2px solid var(--el-datepicker-active-color);outline-offset:1px}.el-date-table td.start-date .el-date-table-cell,.el-date-table td.end-date .el-date-table-cell{color:#fff}.el-date-table td.start-date .el-date-table-cell__text,.el-date-table td.end-date .el-date-table-cell__text{background-color:var(--el-datepicker-active-color)}.el-date-table td.start-date .el-date-table-cell{border-top-left-radius:15px;border-bottom-left-radius:15px;margin-left:5px}.el-date-table td.end-date .el-date-table-cell{border-top-right-radius:15px;border-bottom-right-radius:15px;margin-right:5px}.el-date-table td.disabled .el-date-table-cell{background-color:var(--el-fill-color-light);opacity:1;cursor:not-allowed;color:var(--el-text-color-placeholder)}.el-date-table td.selected .el-date-table-cell{border-radius:15px;margin-left:5px;margin-right:5px}.el-date-table td.selected .el-date-table-cell__text{background-color:var(--el-datepicker-active-color);color:#fff;border-radius:15px}.el-date-table td.week{color:var(--el-datepicker-off-text-color);cursor:default;font-size:80%}.el-date-table td:focus{outline:none}.el-date-table th{color:var(--el-datepicker-header-text-color);border-bottom:solid 1px var(--el-border-color-lighter);padding:5px;font-weight:400}.el-date-table th.el-date-table__week-header{width:24px;padding:0}.el-month-table{border-collapse:collapse;margin:-1px;font-size:12px}.el-month-table td{text-align:center;cursor:pointer;width:68px;padding:8px 0;position:relative}.el-month-table td .el-date-table-cell{box-sizing:border-box;height:48px;padding:6px 0}.el-month-table td.today .el-date-table-cell__text{color:var(--el-color-primary);font-weight:700}.el-month-table td.today.start-date .el-date-table-cell__text,.el-month-table td.today.end-date .el-date-table-cell__text{color:#fff}.el-month-table td.disabled .el-date-table-cell__text{background-color:var(--el-fill-color-light);cursor:not-allowed;color:var(--el-text-color-placeholder)}.el-month-table td.disabled .el-date-table-cell__text:hover{color:var(--el-text-color-placeholder)}.el-month-table td .el-date-table-cell__text{width:54px;height:36px;color:var(--el-datepicker-text-color);border-radius:18px;margin:0 auto;line-height:36px;display:block;position:absolute;left:50%;transform:translate(-50%)}.el-month-table td .el-date-table-cell__text:hover{color:var(--el-datepicker-hover-text-color)}.el-month-table td.in-range .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-month-table td.in-range .el-date-table-cell:hover{background-color:var(--el-datepicker-inrange-hover-bg-color)}.el-month-table td.start-date .el-date-table-cell,.el-month-table td.end-date .el-date-table-cell{color:#fff}.el-month-table td.start-date .el-date-table-cell__text,.el-month-table td.end-date .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-month-table td.start-date .el-date-table-cell{border-top-left-radius:24px;border-bottom-left-radius:24px;margin-left:3px}.el-month-table td.end-date .el-date-table-cell{border-top-right-radius:24px;border-bottom-right-radius:24px;margin-right:3px}.el-month-table td.current:not(.disabled) .el-date-table-cell{border-radius:24px;margin-left:3px;margin-right:3px}.el-month-table td.current:not(.disabled) .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-month-table td:focus-visible{outline:none}.el-month-table td:focus-visible .el-date-table-cell__text{outline:2px solid var(--el-datepicker-active-color);outline-offset:1px}.el-year-table{border-collapse:collapse;margin:-1px;font-size:12px}.el-year-table .el-icon{color:var(--el-datepicker-icon-color)}.el-year-table td{text-align:center;cursor:pointer;width:68px;padding:8px 0;position:relative}.el-year-table td .el-date-table-cell{box-sizing:border-box;height:48px;padding:6px 0}.el-year-table td.today .el-date-table-cell__text{color:var(--el-color-primary);font-weight:700}.el-year-table td.today.start-date .el-date-table-cell__text,.el-year-table td.today.end-date .el-date-table-cell__text{color:#fff}.el-year-table td.disabled .el-date-table-cell__text{background-color:var(--el-fill-color-light);cursor:not-allowed;color:var(--el-text-color-placeholder)}.el-year-table td.disabled .el-date-table-cell__text:hover{color:var(--el-text-color-placeholder)}.el-year-table td .el-date-table-cell__text{width:60px;height:36px;color:var(--el-datepicker-text-color);border-radius:18px;margin:0 auto;line-height:36px;display:block;position:absolute;left:50%;transform:translate(-50%)}.el-year-table td .el-date-table-cell__text:hover{color:var(--el-datepicker-hover-text-color)}.el-year-table td.in-range .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-year-table td.in-range .el-date-table-cell:hover{background-color:var(--el-datepicker-inrange-hover-bg-color)}.el-year-table td.start-date .el-date-table-cell,.el-year-table td.end-date .el-date-table-cell{color:#fff}.el-year-table td.start-date .el-date-table-cell__text,.el-year-table td.end-date .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-year-table td.start-date .el-date-table-cell{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-year-table td.end-date .el-date-table-cell{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-year-table td.current:not(.disabled) .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-year-table td:focus-visible{outline:none}.el-year-table td:focus-visible .el-date-table-cell__text{outline:2px solid var(--el-datepicker-active-color);outline-offset:1px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{vertical-align:top;width:50%;max-height:192px;display:inline-block;position:relative;overflow:auto}.el-time-spinner__wrapper.el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__wrapper.is-arrow{box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.is-disabled):not(.is-active){background:var(--el-fill-color-light);cursor:default}.el-time-spinner__arrow{color:var(--el-text-color-secondary);width:100%;z-index:var(--el-index-normal);text-align:center;cursor:pointer;height:30px;font-size:12px;line-height:30px;position:absolute;left:0}.el-time-spinner__arrow:hover{color:var(--el-color-primary)}.el-time-spinner__arrow.arrow-up{top:10px}.el-time-spinner__arrow.arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__input.el-input .el-input__inner{text-align:center;padding:0}.el-time-spinner__list{text-align:center;margin:0;padding:0;list-style:none}.el-time-spinner__list:after,.el-time-spinner__list:before{content:"";width:100%;height:80px;display:block}.el-time-spinner__item{height:32px;color:var(--el-text-color-regular);font-size:12px;line-height:32px}.el-time-spinner__item:hover:not(.is-disabled):not(.is-active){background:var(--el-fill-color-light);cursor:pointer}.el-time-spinner__item.is-active:not(.is-disabled){color:var(--el-text-color-primary);font-weight:700}.el-time-spinner__item.is-disabled{color:var(--el-text-color-placeholder);cursor:not-allowed}.el-picker__popper{--el-datepicker-border-color:var(--el-disabled-border-color)}.el-picker__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-datepicker-border-color);box-shadow:var(--el-box-shadow-light)}.el-picker__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-datepicker-border-color)}.el-picker__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-picker__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-picker__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-picker__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-date-editor{--el-date-editor-width:220px;--el-date-editor-monthrange-width:300px;--el-date-editor-daterange-width:350px;--el-date-editor-datetimerange-width:400px;--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;text-align:left;vertical-align:middle;position:relative}.el-date-editor.el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset}.el-date-editor.el-input__wrapper:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-date-editor.is-focus .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-date-editor.el-input,.el-date-editor.el-input__wrapper{width:var(--el-date-editor-width);height:var(--el-input-height,var(--el-component-size))}.el-date-editor--monthrange{--el-date-editor-width:var(--el-date-editor-monthrange-width)}.el-date-editor--daterange,.el-date-editor--timerange{--el-date-editor-width:var(--el-date-editor-daterange-width)}.el-date-editor--datetimerange{--el-date-editor-width:var(--el-date-editor-datetimerange-width)}.el-date-editor--dates .el-input__wrapper{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .close-icon,.el-date-editor .clear-icon{cursor:pointer}.el-date-editor .clear-icon:hover{color:var(--el-input-clear-hover-color)}.el-date-editor .el-range__icon{height:inherit;color:var(--el-text-color-placeholder);float:left;font-size:14px}.el-date-editor .el-range__icon svg{vertical-align:middle}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;text-align:center;width:39%;height:30px;line-height:30px;font-size:var(--el-font-size-base);color:var(--el-text-color-regular);background-color:#0000;border:none;outline:none;margin:0;padding:0;display:inline-block}.el-date-editor .el-range-input::placeholder{color:var(--el-text-color-placeholder)}.el-date-editor .el-range-separator{overflow-wrap:break-word;height:100%;color:var(--el-text-color-primary);flex:1;justify-content:center;align-items:center;margin:0;padding:0 5px;font-size:14px;display:inline-flex}.el-date-editor .el-range__close-icon{color:var(--el-text-color-placeholder);height:inherit;width:unset;cursor:pointer;font-size:14px}.el-date-editor .el-range__close-icon:hover{color:var(--el-input-clear-hover-color)}.el-date-editor .el-range__close-icon svg{vertical-align:middle}.el-date-editor .el-range__close-icon--hidden{opacity:0;visibility:hidden}.el-range-editor.el-input__wrapper{vertical-align:middle;align-items:center;padding:0 10px;display:inline-flex}.el-range-editor.is-active,.el-range-editor.is-active:hover{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-range-editor--large{line-height:var(--el-component-size-large)}.el-range-editor--large.el-input__wrapper{height:var(--el-component-size-large)}.el-range-editor--large .el-range-separator{font-size:14px;line-height:40px}.el-range-editor--large .el-range-input{height:38px;font-size:14px;line-height:38px}.el-range-editor--small{line-height:var(--el-component-size-small)}.el-range-editor--small.el-input__wrapper{height:var(--el-component-size-small)}.el-range-editor--small .el-range-separator{font-size:12px;line-height:24px}.el-range-editor--small .el-range-input{height:22px;font-size:12px;line-height:22px}.el-range-editor.is-disabled{background-color:var(--el-disabled-bg-color);border-color:var(--el-disabled-border-color);color:var(--el-disabled-text-color);cursor:not-allowed}.el-range-editor.is-disabled:hover,.el-range-editor.is-disabled:focus{border-color:var(--el-disabled-border-color)}.el-range-editor.is-disabled input{background-color:var(--el-disabled-bg-color);color:var(--el-disabled-text-color);cursor:not-allowed}.el-range-editor.is-disabled input::placeholder{color:var(--el-text-color-placeholder)}.el-range-editor.is-disabled .el-range-separator{color:var(--el-disabled-text-color)}.el-picker-panel{color:var(--el-text-color-regular);background:var(--el-datepicker-bg-color);border-radius:var(--el-popper-border-radius,var(--el-border-radius-base));line-height:30px}.el-picker-panel .el-time-panel{border:solid 1px var(--el-datepicker-border-color);background-color:var(--el-datepicker-bg-color);box-shadow:var(--el-box-shadow-light);margin:5px 0}.el-picker-panel__body:after,.el-picker-panel__body-wrapper:after{content:"";clear:both;display:table}.el-picker-panel__content{margin:15px;position:relative}.el-picker-panel__footer{border-top:1px solid var(--el-datepicker-inner-border-color);text-align:right;background-color:var(--el-datepicker-bg-color);padding:4px 12px;font-size:0;position:relative}.el-picker-panel__shortcut{width:100%;color:var(--el-datepicker-text-color);text-align:left;cursor:pointer;background-color:#0000;border:0;outline:none;padding-left:12px;font-size:14px;line-height:28px;display:block}.el-picker-panel__shortcut:hover{color:var(--el-datepicker-hover-text-color)}.el-picker-panel__shortcut.active{color:var(--el-datepicker-active-color);background-color:#e6f1fe}.el-picker-panel__btn{border:1px solid var(--el-fill-color-darker);color:var(--el-text-color-primary);cursor:pointer;background-color:#0000;border-radius:2px;outline:none;padding:0 20px;font-size:12px;line-height:24px}.el-picker-panel__btn[disabled]{color:var(--el-text-color-disabled);cursor:not-allowed}.el-picker-panel__icon-btn{color:var(--el-datepicker-icon-color);cursor:pointer;background:0 0;border:0;outline:none;margin-top:8px;padding:1px 6px;font-size:12px;line-height:1}.el-picker-panel__icon-btn:hover{color:var(--el-datepicker-hover-text-color)}.el-picker-panel__icon-btn:focus-visible{color:var(--el-datepicker-hover-text-color)}.el-picker-panel__icon-btn.is-disabled{color:var(--el-text-color-disabled)}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__icon-btn.is-disabled .el-icon{cursor:inherit}.el-picker-panel__icon-btn .el-icon{cursor:pointer;font-size:inherit}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel.is-disabled .el-picker-panel__prev-btn{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__prev-btn:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__prev-btn .el-icon{cursor:inherit}.el-picker-panel.is-disabled .el-picker-panel__next-btn{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__next-btn:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__next-btn .el-icon{cursor:inherit}.el-picker-panel.is-disabled .el-picker-panel__icon-btn{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__icon-btn:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__icon-btn .el-icon{cursor:inherit}.el-picker-panel.is-disabled .el-picker-panel__shortcut{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__shortcut:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__shortcut .el-icon{cursor:inherit}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{border-right:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:110px;padding-top:6px;position:absolute;top:0;bottom:0;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-date-picker{--el-datepicker-text-color:var(--el-text-color-regular);--el-datepicker-off-text-color:var(--el-text-color-placeholder);--el-datepicker-header-text-color:var(--el-text-color-regular);--el-datepicker-icon-color:var(--el-text-color-primary);--el-datepicker-border-color:var(--el-disabled-border-color);--el-datepicker-inner-border-color:var(--el-border-color-light);--el-datepicker-inrange-bg-color:var(--el-border-color-extra-light);--el-datepicker-inrange-hover-bg-color:var(--el-border-color-extra-light);--el-datepicker-active-color:var(--el-color-primary);--el-datepicker-hover-text-color:var(--el-color-primary);--el-datepicker-bg-color:var(--el-bg-color-overlay);--el-fill-color-blank:var(--el-datepicker-bg-color);width:322px}.el-date-picker.has-sidebar.has-time{width:434px}.el-date-picker.has-sidebar{width:438px}.el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-picker .el-picker-panel__content{width:292px}.el-date-picker table{table-layout:fixed;width:100%}.el-date-picker__editor-wrap{padding:0 5px;display:table-cell;position:relative}.el-date-picker__time-header{border-bottom:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:100%;padding:8px 5px 5px;font-size:12px;display:table;position:relative}.el-date-picker__header{text-align:center;padding:12px 12px 0}.el-date-picker__header--bordered{border-bottom:solid 1px var(--el-border-color-lighter);margin-bottom:0;padding-bottom:12px}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{text-align:center;cursor:pointer;color:var(--el-text-color-regular);padding:0 5px;font-size:16px;font-weight:500;line-height:22px}.el-date-picker__header-label:hover{color:var(--el-datepicker-hover-text-color)}.el-date-picker__header-label:focus-visible{color:var(--el-datepicker-hover-text-color);outline:none}.el-date-picker__header-label.active{color:var(--el-datepicker-active-color)}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{text-align:center;padding:10px}.el-date-picker__time-label{float:left;cursor:pointer;margin-left:10px;line-height:30px}.el-date-picker .el-time-panel{position:absolute}.el-date-picker.is-disabled .el-date-picker__header-label{color:var(--el-text-color-disabled)}.el-date-picker.is-disabled .el-date-picker__header-label:hover{cursor:not-allowed}.el-date-picker.is-disabled .el-date-picker__header-label .el-icon{cursor:inherit}.el-date-range-picker{--el-datepicker-text-color:var(--el-text-color-regular);--el-datepicker-off-text-color:var(--el-text-color-placeholder);--el-datepicker-header-text-color:var(--el-text-color-regular);--el-datepicker-icon-color:var(--el-text-color-primary);--el-datepicker-border-color:var(--el-disabled-border-color);--el-datepicker-inner-border-color:var(--el-border-color-light);--el-datepicker-inrange-bg-color:var(--el-border-color-extra-light);--el-datepicker-inrange-hover-bg-color:var(--el-border-color-extra-light);--el-datepicker-active-color:var(--el-color-primary);--el-datepicker-hover-text-color:var(--el-color-primary);--el-datepicker-bg-color:var(--el-bg-color-overlay);width:646px}.el-date-range-picker.has-sidebar{width:756px}.el-date-range-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-range-picker table{table-layout:fixed;width:100%}.el-date-range-picker .el-picker-panel__body{min-width:513px}.el-date-range-picker .el-picker-panel__content{margin:0}.el-date-range-picker__header{text-align:center;height:28px;position:relative}.el-date-range-picker__header [class*=arrow-left]{float:left}.el-date-range-picker__header [class*=arrow-right]{float:right}.el-date-range-picker__header div{margin-right:50px;font-size:16px;font-weight:500}.el-date-range-picker__header-label{text-align:center;cursor:pointer;color:var(--el-text-color-regular);padding:0 5px;font-size:16px;font-weight:500;line-height:22px}.el-date-range-picker__header-label:hover{color:var(--el-datepicker-hover-text-color)}.el-date-range-picker__header-label:focus-visible{color:var(--el-datepicker-hover-text-color);outline:none}.el-date-range-picker__header-label.active{color:var(--el-datepicker-active-color)}.el-date-range-picker__content{box-sizing:border-box;width:50%;margin:0;padding:16px;display:table-cell}.el-date-range-picker__content.is-left{border-right:1px solid var(--el-datepicker-inner-border-color)}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{border-bottom:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:100%;padding:8px 5px 5px;font-size:12px;display:table;position:relative}.el-date-range-picker__time-header>.el-icon-arrow-right{vertical-align:middle;color:var(--el-datepicker-icon-color);font-size:20px;display:table-cell}.el-date-range-picker__time-picker-wrap{padding:0 5px;display:table-cell;position:relative}.el-date-range-picker__time-picker-wrap .el-picker-panel{z-index:1;background:#fff;position:absolute;top:13px;right:0}.el-date-range-picker__time-picker-wrap .el-time-panel{position:absolute}.el-date-range-picker.is-disabled .el-date-range-picker__header-label{color:var(--el-text-color-disabled)}.el-date-range-picker.is-disabled .el-date-range-picker__header-label:hover{cursor:not-allowed}.el-date-range-picker.is-disabled .el-date-range-picker__header-label .el-icon{cursor:inherit}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{text-align:center;z-index:1;padding:10px;position:relative}.el-time-range-picker__cell{box-sizing:border-box;width:50%;margin:0;padding:4px 7px 7px;display:inline-block}.el-time-range-picker__header{text-align:center;margin-bottom:5px;font-size:14px}.el-time-range-picker__body{border:1px solid var(--el-datepicker-border-color);border-radius:2px}.el-time-panel{width:180px;z-index:var(--el-index-top);-webkit-user-select:none;user-select:none;box-sizing:content-box;border-radius:2px;position:relative;left:0}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content:after,.el-time-panel__content:before{content:"";z-index:-1;box-sizing:border-box;text-align:left;height:32px;margin-top:-16px;padding-top:6px;position:absolute;top:50%;left:0;right:0}.el-time-panel__content:after{margin-left:12%;margin-right:12%;left:50%}.el-time-panel__content:before{border-top:1px solid var(--el-border-color-light);border-bottom:1px solid var(--el-border-color-light);margin-left:12%;margin-right:12%;padding-left:50%}.el-time-panel__content.has-seconds:after{left:66.6667%}.el-time-panel__content.has-seconds:before{padding-left:33.3333%}.el-time-panel__footer{border-top:1px solid var(--el-timepicker-inner-border-color,var(--el-border-color-light));text-align:right;box-sizing:border-box;height:36px;padding:4px;line-height:25px}.el-time-panel__btn{cursor:pointer;color:var(--el-text-color-primary);background-color:#0000;border:none;outline:none;margin:0 5px;padding:0 5px;font-size:12px;line-height:28px}.el-time-panel__btn.confirm{color:var(--el-timepicker-active-color,var(--el-color-primary));font-weight:800}.el-picker-panel.is-border{border:solid 1px var(--el-border-color-lighter)}.el-picker-panel.is-border .el-picker-panel__body-wrapper{position:relative}.el-picker-panel.is-border.el-picker-panel [slot=sidebar],.el-picker-panel.is-border.el-picker-panel__sidebar{border-right:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:110px;height:100%;padding-top:6px;position:absolute;top:0;overflow:auto}.el-descriptions{--el-descriptions-table-border:1px solid var(--el-border-color-lighter);--el-descriptions-item-bordered-label-background:var(--el-fill-color-light);box-sizing:border-box;font-size:var(--el-font-size-base);color:var(--el-text-color-primary)}.el-descriptions__header{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.el-descriptions__title{color:var(--el-text-color-primary);font-size:16px;font-weight:700}.el-descriptions__body{background-color:var(--el-fill-color-blank)}.el-descriptions__body .el-descriptions__table{border-collapse:collapse;width:100%}.el-descriptions__body .el-descriptions__table .el-descriptions__cell{box-sizing:border-box;text-align:left;font-size:14px;line-height:23px}.el-descriptions__body .el-descriptions__table .el-descriptions__cell.is-left{text-align:left}.el-descriptions__body .el-descriptions__table .el-descriptions__cell.is-center{text-align:center}.el-descriptions__body .el-descriptions__table .el-descriptions__cell.is-right{text-align:right}.el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell{border:var(--el-descriptions-table-border);padding:8px 11px}.el-descriptions__body .el-descriptions__table:not(.is-bordered) .el-descriptions__cell{padding-bottom:12px}.el-descriptions--large{font-size:14px}.el-descriptions--large .el-descriptions__header{margin-bottom:20px}.el-descriptions--large .el-descriptions__header .el-descriptions__title{font-size:16px}.el-descriptions--large .el-descriptions__body .el-descriptions__table .el-descriptions__cell{font-size:14px}.el-descriptions--large .el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell{padding:12px 15px}.el-descriptions--large .el-descriptions__body .el-descriptions__table:not(.is-bordered) .el-descriptions__cell{padding-bottom:16px}.el-descriptions--small{font-size:12px}.el-descriptions--small .el-descriptions__header{margin-bottom:12px}.el-descriptions--small .el-descriptions__header .el-descriptions__title{font-size:14px}.el-descriptions--small .el-descriptions__body .el-descriptions__table .el-descriptions__cell{font-size:12px}.el-descriptions--small .el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell{padding:4px 7px}.el-descriptions--small .el-descriptions__body .el-descriptions__table:not(.is-bordered) .el-descriptions__cell{padding-bottom:8px}.el-descriptions__label.el-descriptions__cell.is-bordered-label{color:var(--el-text-color-regular);background:var(--el-descriptions-item-bordered-label-background);font-weight:700}.el-descriptions__label:not(.is-bordered-label){color:var(--el-text-color-primary);margin-right:16px}.el-descriptions__label.el-descriptions__cell:not(.is-bordered-label).is-vertical-label{padding-bottom:6px}.el-descriptions__content.el-descriptions__cell.is-bordered-content{color:var(--el-text-color-primary)}.el-descriptions__content:not(.is-bordered-label){color:var(--el-text-color-regular)}.el-descriptions--large .el-descriptions__label:not(.is-bordered-label){margin-right:16px}.el-descriptions--large .el-descriptions__label.el-descriptions__cell:not(.is-bordered-label).is-vertical-label{padding-bottom:8px}.el-descriptions--small .el-descriptions__label:not(.is-bordered-label){margin-right:12px}.el-descriptions--small .el-descriptions__label.el-descriptions__cell:not(.is-bordered-label).is-vertical-label{padding-bottom:4px}:root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{width:100%;height:100%;opacity:var(--el-popup-modal-opacity);background:var(--el-popup-modal-bg-color);position:fixed;top:0;left:0}.el-popup-parent--hidden{overflow:hidden}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:16px;--el-dialog-border-radius:var(--el-border-radius-base);margin:var(--el-dialog-margin-top,15vh) auto 50px;background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;padding:var(--el-dialog-padding-primary);width:var(--el-dialog-width,50%);overflow-wrap:break-word;position:relative}.el-dialog:focus{outline:none!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;border-radius:0;height:100%;margin-bottom:0;overflow:auto}.el-dialog__wrapper{margin:0;position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;user-select:none}.el-dialog__header{padding-bottom:var(--el-dialog-padding-primary)}.el-dialog__header.show-close{padding-right:calc(var(--el-dialog-padding-primary) + var(--el-message-close-size,16px))}.el-dialog__headerbtn{cursor:pointer;width:48px;height:48px;font-size:var(--el-message-close-size,16px);background:0 0;border:none;outline:none;padding:0;position:absolute;top:0;right:0}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{line-height:var(--el-dialog-font-line-height);font-size:var(--el-dialog-title-font-size);color:var(--el-text-color-primary)}.el-dialog__body{color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{padding-top:var(--el-dialog-padding-primary);text-align:right;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-modal-dialog.is-penetrable{pointer-events:none}.el-modal-dialog.is-penetrable .el-dialog{pointer-events:auto}.el-overlay-dialog{position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.el-overlay-dialog.is-closing .el-dialog{pointer-events:none}.dialog-fade-enter-active{animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{animation:dialog-fade-out var(--el-transition-duration)}@keyframes dialog-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translate(0)}}@keyframes dialog-fade-out{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translateY(-20px)}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}.el-divider{position:relative}.el-divider--horizontal{border-top:1px var(--el-border-color) var(--el-border-style);width:100%;height:1px;margin:24px 0;display:block}.el-divider--vertical{vertical-align:middle;border-left:1px var(--el-border-color) var(--el-border-style);width:1px;height:1em;margin:0 8px;display:inline-block;position:relative}.el-divider__text{background-color:var(--el-bg-color);color:var(--el-text-color-primary);padding:0 20px;font-size:14px;font-weight:500;position:absolute}.el-divider__text.is-left{left:20px;transform:translateY(-50%)}.el-divider__text.is-center{left:50%;transform:translate(-50%)translateY(-50%)}.el-divider__text.is-right{right:20px;transform:translateY(-50%)}.el-overlay.is-drawer{overflow:hidden}.el-drawer{--el-drawer-bg-color:var(--el-dialog-bg-color,var(--el-bg-color));--el-drawer-padding-primary:var(--el-dialog-padding-primary,20px);--el-drawer-dragger-size:8px;box-sizing:border-box;background-color:var(--el-drawer-bg-color);box-shadow:var(--el-box-shadow-dark);transition:all var(--el-transition-duration);flex-direction:column;display:flex;position:absolute}.el-drawer .rtl,.el-drawer .ltr,.el-drawer .ttb,.el-drawer .btt{transform:translate(0)}.el-drawer__sr-focus:focus{outline:none!important}.el-drawer__header{color:var(--el-text-color-primary);padding:var(--el-drawer-padding-primary);align-items:center;margin-bottom:32px;padding-bottom:0;display:flex;overflow:hidden}.el-drawer__header>:first-child{flex:1}.el-drawer__title{line-height:inherit;flex:1;margin:0;font-size:16px}.el-drawer__footer{padding:var(--el-drawer-padding-primary);text-align:right;padding-top:10px;overflow:hidden}.el-drawer__close-btn{cursor:pointer;font-size:var(--el-font-size-extra-large);color:inherit;background-color:#0000;border:none;outline:none;display:inline-flex}.el-drawer__close-btn:focus i,.el-drawer__close-btn:hover i{color:var(--el-color-primary)}.el-drawer__body{padding:var(--el-drawer-padding-primary);flex:1;overflow:auto}.el-drawer__body>*{box-sizing:border-box}.el-drawer.is-dragging{transition:none}.el-drawer__dragger{-webkit-user-select:none;user-select:none;background-color:#0000;transition:all .2s;position:absolute}.el-drawer__dragger:before{content:"";background-color:#0000;transition:all .2s;position:absolute}.el-drawer__dragger:hover:before{background-color:var(--el-color-primary)}.el-drawer.ltr,.el-drawer.rtl{height:100%;top:0;bottom:0}.el-drawer.ltr>.el-drawer__dragger,.el-drawer.rtl>.el-drawer__dragger{height:100%;width:var(--el-drawer-dragger-size);cursor:ew-resize;top:0;bottom:0}.el-drawer.ltr>.el-drawer__dragger:before,.el-drawer.rtl>.el-drawer__dragger:before{width:3px;top:0;bottom:0}.el-drawer.ttb,.el-drawer.btt{width:100%;left:0;right:0}.el-drawer.ttb>.el-drawer__dragger,.el-drawer.btt>.el-drawer__dragger{width:100%;height:var(--el-drawer-dragger-size);cursor:ns-resize;left:0;right:0}.el-drawer.ttb>.el-drawer__dragger:before,.el-drawer.btt>.el-drawer__dragger:before{height:3px;left:0;right:0}.el-drawer.ltr{left:0}.el-drawer.ltr>.el-drawer__dragger{right:0}.el-drawer.ltr>.el-drawer__dragger:before{right:-2px}.el-drawer.rtl{right:0}.el-drawer.rtl>.el-drawer__dragger{left:0}.el-drawer.rtl>.el-drawer__dragger:before{left:-2px}.el-drawer.ttb{top:0}.el-drawer.ttb>.el-drawer__dragger{bottom:0}.el-drawer.ttb>.el-drawer__dragger:before{bottom:-2px}.el-drawer.btt{bottom:0}.el-drawer.btt>.el-drawer__dragger{top:0}.el-drawer.btt>.el-drawer__dragger:before{top:-2px}.el-modal-drawer.is-penetrable{pointer-events:none}.el-modal-drawer.is-penetrable .el-drawer{pointer-events:auto}.el-drawer-fade-enter-active,.el-drawer-fade-leave-active{transition:all var(--el-transition-duration)}.el-drawer-fade-enter-from,.el-drawer-fade-enter-active,.el-drawer-fade-enter-to,.el-drawer-fade-leave-from,.el-drawer-fade-leave-active,.el-drawer-fade-leave-to{overflow:hidden!important}.el-drawer-fade-enter-from,.el-drawer-fade-leave-to{background-color:#0000!important}.el-drawer-fade-enter-from .rtl,.el-drawer-fade-leave-to .rtl{transform:translate(100%)}.el-drawer-fade-enter-from .ltr,.el-drawer-fade-leave-to .ltr{transform:translate(-100%)}.el-drawer-fade-enter-from .ttb,.el-drawer-fade-leave-to .ttb{transform:translateY(-100%)}.el-drawer-fade-enter-from .btt,.el-drawer-fade-leave-to .btt{transform:translateY(100%)}.el-dropdown{--el-dropdown-menu-box-shadow:var(--el-box-shadow-light);--el-dropdown-menuItem-hover-fill:var(--el-color-primary-light-9);--el-dropdown-menuItem-hover-color:var(--el-color-primary);--el-dropdown-menu-index:10;color:var(--el-text-color-regular);font-size:var(--el-font-size-base);vertical-align:top;line-height:1;display:inline-flex;position:relative}.el-dropdown.is-disabled{color:var(--el-text-color-placeholder);cursor:not-allowed}.el-dropdown__popper{--el-dropdown-menu-box-shadow:var(--el-box-shadow-light);--el-dropdown-menuItem-hover-fill:var(--el-color-primary-light-9);--el-dropdown-menuItem-hover-color:var(--el-color-primary);--el-dropdown-menu-index:10}.el-dropdown__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-dropdown-menu-box-shadow)}.el-dropdown__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-dropdown__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-dropdown__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-dropdown__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-dropdown__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-dropdown__popper .el-dropdown-menu{border:none}.el-dropdown__popper .el-dropdown__popper-selfdefine{outline:none}.el-dropdown__popper .el-scrollbar__bar{z-index:calc(var(--el-dropdown-menu-index) + 1)}.el-dropdown__popper .el-dropdown__list{box-sizing:border-box;margin:0;padding:0;list-style:none}.el-dropdown .el-dropdown__caret-button{border-left:none;justify-content:center;align-items:center;width:32px;padding-left:0;padding-right:0;display:inline-flex}.el-dropdown .el-dropdown__caret-button>span{display:inline-flex}.el-dropdown .el-dropdown__caret-button:before{content:"";background:var(--el-overlay-color-lighter);width:1px;display:block;position:absolute;top:-1px;bottom:-1px;left:0}.el-dropdown .el-dropdown__caret-button.el-button:before{background:var(--el-border-color);opacity:.5}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{font-size:inherit;padding-left:0}.el-dropdown .el-dropdown-selfdefine{outline:none}.el-dropdown--large .el-dropdown__caret-button{width:40px}.el-dropdown--small .el-dropdown__caret-button{width:24px}.el-dropdown-menu{z-index:var(--el-dropdown-menu-index);background-color:var(--el-bg-color-overlay);border-radius:var(--el-border-radius-base);box-shadow:none;border:none;margin:0;padding:5px 0;list-style:none;position:relative;top:0;left:0}.el-dropdown-menu__item{white-space:nowrap;line-height:22px;font-size:var(--el-font-size-base);color:var(--el-text-color-regular);cursor:pointer;outline:none;align-items:center;margin:0;padding:5px 16px;list-style:none;display:flex}.el-dropdown-menu__item:not(.is-disabled):hover,.el-dropdown-menu__item:not(.is-disabled):focus{background-color:var(--el-dropdown-menuItem-hover-fill);color:var(--el-dropdown-menuItem-hover-color)}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{border-top:1px solid var(--el-border-color-lighter);margin:6px 0}.el-dropdown-menu__item.is-disabled{cursor:not-allowed;color:var(--el-text-color-disabled)}.el-dropdown-menu--large{padding:7px 0}.el-dropdown-menu--large .el-dropdown-menu__item{padding:7px 20px;font-size:14px;line-height:22px}.el-dropdown-menu--large .el-dropdown-menu__item--divided{margin:8px 0}.el-dropdown-menu--small{padding:3px 0}.el-dropdown-menu--small .el-dropdown-menu__item{padding:2px 12px;font-size:12px;line-height:20px}.el-dropdown-menu--small .el-dropdown-menu__item--divided{margin:4px 0}.el-empty{--el-empty-padding:40px 0;--el-empty-image-width:160px;--el-empty-description-margin-top:20px;--el-empty-bottom-margin-top:20px;--el-empty-fill-color-0:var(--el-color-white);--el-empty-fill-color-1:#fcfcfd;--el-empty-fill-color-2:#f8f9fb;--el-empty-fill-color-3:#f7f8fc;--el-empty-fill-color-4:#eeeff3;--el-empty-fill-color-5:#edeef2;--el-empty-fill-color-6:#e9ebef;--el-empty-fill-color-7:#e5e7e9;--el-empty-fill-color-8:#e0e3e9;--el-empty-fill-color-9:#d5d7de;text-align:center;box-sizing:border-box;padding:var(--el-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.el-empty__image{width:var(--el-empty-image-width)}.el-empty__image img{-webkit-user-select:none;user-select:none;vertical-align:top;object-fit:contain;width:100%;height:100%}.el-empty__image svg{color:var(--el-svg-monochrome-grey);fill:currentColor;vertical-align:top;width:100%;height:100%}.el-empty__description{margin-top:var(--el-empty-description-margin-top)}.el-empty__description p{font-size:var(--el-font-size-base);color:var(--el-text-color-secondary);margin:0}.el-empty__bottom{margin-top:var(--el-empty-bottom-margin-top)}.el-footer{--el-footer-padding:0 20px;--el-footer-height:60px;padding:var(--el-footer-padding);box-sizing:border-box;height:var(--el-footer-height);flex-shrink:0}.el-form-item{--font-size:14px;margin-bottom:18px;display:flex}.el-form-item .el-form-item{margin-bottom:0}.el-form-item .el-input__validateIcon{display:none}.el-form-item--large{--font-size:14px;--el-form-label-font-size:var(--font-size);margin-bottom:22px}.el-form-item--large .el-form-item__label{height:40px;line-height:40px}.el-form-item--large .el-form-item__content{line-height:40px}.el-form-item--large .el-form-item__error{padding-top:4px}.el-form-item--default{--font-size:14px;--el-form-label-font-size:var(--font-size);margin-bottom:18px}.el-form-item--default .el-form-item__label{height:32px;line-height:32px}.el-form-item--default .el-form-item__content{line-height:32px}.el-form-item--default .el-form-item__error{padding-top:2px}.el-form-item--small{--font-size:12px;--el-form-label-font-size:var(--font-size);margin-bottom:18px}.el-form-item--small .el-form-item__label{height:24px;line-height:24px}.el-form-item--small .el-form-item__content{line-height:24px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--label-left .el-form-item__label{text-align:left;justify-content:flex-start}.el-form-item--label-right .el-form-item__label{text-align:right;justify-content:flex-end}.el-form-item--label-top{display:block}.el-form-item--label-top .el-form-item__label{text-align:left;width:-moz-fit-content;width:fit-content;height:auto;margin-bottom:8px;padding-right:0;line-height:22px;display:block}.el-form-item__label-wrap{display:flex}.el-form-item__label{font-size:var(--el-form-label-font-size);color:var(--el-text-color-regular);box-sizing:border-box;flex:none;align-items:flex-start;height:32px;padding:0 12px 0 0;line-height:32px;display:inline-flex}.el-form-item__content{line-height:32px;font-size:var(--font-size);flex-wrap:wrap;flex:1;align-items:center;min-width:0;display:flex;position:relative}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:var(--el-color-danger);padding-top:2px;font-size:12px;line-height:1;position:absolute;top:100%;left:0}.el-form-item__error--inline{margin-left:10px;display:inline-block;position:relative;top:auto;left:auto}.el-form-item.is-required:not(.is-no-asterisk).asterisk-left>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk).asterisk-left>.el-form-item__label-wrap>.el-form-item__label:before{content:"*";color:var(--el-color-danger);margin-right:4px}.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label:after,.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label-wrap>.el-form-item__label:after{content:"*";color:var(--el-color-danger);margin-left:4px}.el-form-item.is-error .el-form-item__content .el-input__wrapper,.el-form-item.is-error .el-form-item__content .el-input__wrapper:hover,.el-form-item.is-error .el-form-item__content .el-input__wrapper:focus,.el-form-item.is-error .el-form-item__content .el-input__wrapper.is-focus,.el-form-item.is-error .el-form-item__content .el-textarea__inner,.el-form-item.is-error .el-form-item__content .el-textarea__inner:hover,.el-form-item.is-error .el-form-item__content .el-textarea__inner:focus,.el-form-item.is-error .el-form-item__content .el-textarea__inner.is-focus,.el-form-item.is-error .el-form-item__content .el-select__wrapper,.el-form-item.is-error .el-form-item__content .el-select__wrapper:hover,.el-form-item.is-error .el-form-item__content .el-select__wrapper:focus,.el-form-item.is-error .el-form-item__content .el-select__wrapper.is-focus,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper:hover,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper:focus,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper.is-focus{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-form-item.is-error .el-form-item__content .el-input-group__append .el-input__wrapper,.el-form-item.is-error .el-form-item__content .el-input-group__prepend .el-input__wrapper{box-shadow:inset 0 0 0 1px #0000}.el-form-item.is-error .el-form-item__content .el-input-group__append .el-input__validateIcon,.el-form-item.is-error .el-form-item__content .el-input-group__prepend .el-input__validateIcon{display:none}.el-form-item.is-error .el-form-item__content .el-input__validateIcon{color:var(--el-color-danger)}.el-form-item--feedback .el-input__validateIcon{display:inline-flex}.el-form{--el-form-label-font-size:var(--el-font-size-base);--el-form-inline-content-width:220px}.el-form--inline .el-form-item{vertical-align:middle;margin-right:32px;display:inline-flex}.el-form--inline.el-form--label-top{flex-wrap:wrap;display:flex}.el-form--inline.el-form--label-top .el-form-item{display:block}.el-header{--el-header-padding:0 20px;--el-header-height:60px;padding:var(--el-header-padding);box-sizing:border-box;height:var(--el-header-height);flex-shrink:0}.el-image-viewer__wrapper{position:fixed;top:0;bottom:0;left:0;right:0}.el-image-viewer__wrapper:focus{outline:none!important}.el-image-viewer__btn{z-index:1;opacity:.8;cursor:pointer;box-sizing:border-box;-webkit-user-select:none;user-select:none;border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute}.el-image-viewer__btn .el-icon{cursor:pointer}.el-image-viewer__close{width:40px;height:40px;font-size:40px;top:40px;right:40px}.el-image-viewer__canvas{-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:static}.el-image-viewer__actions{background-color:var(--el-text-color-regular);border-color:#fff;border-radius:22px;height:44px;padding:0 23px;bottom:30px;left:50%;transform:translate(-50%)}.el-image-viewer__actions__inner{cursor:default;color:#fff;justify-content:space-around;align-items:center;gap:22px;width:100%;height:100%;padding:0 6px;font-size:23px;display:flex}.el-image-viewer__actions__divider{margin:0 -6px}.el-image-viewer__progress{cursor:default;color:#fff;bottom:90px;left:50%;transform:translate(-50%)}.el-image-viewer__prev{color:#fff;background-color:var(--el-text-color-regular);border-color:#fff;width:44px;height:44px;font-size:24px;top:50%;left:40px;transform:translateY(-50%)}.el-image-viewer__next{text-indent:2px;color:#fff;background-color:var(--el-text-color-regular);border-color:#fff;width:44px;height:44px;font-size:24px;top:50%;right:40px;transform:translateY(-50%)}.el-image-viewer__close{color:#fff;background-color:var(--el-text-color-regular);border-color:#fff;width:44px;height:44px;font-size:24px}.el-image-viewer__mask{opacity:.5;background:#000;width:100%;height:100%;position:absolute;top:0;left:0}.el-image-viewer-parent--hidden{overflow:hidden}.viewer-fade-enter-active{animation:viewer-fade-in var(--el-transition-duration)}.viewer-fade-leave-active{animation:viewer-fade-out var(--el-transition-duration)}@keyframes viewer-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translate(0)}}@keyframes viewer-fade-out{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translateY(-20px)}}.el-image__error,.el-image__placeholder,.el-image__wrapper,.el-image__inner{width:100%;height:100%}.el-image{display:inline-block;position:relative;overflow:hidden}.el-image__inner{vertical-align:top;opacity:1}.el-image__inner.is-loading{opacity:0}.el-image__wrapper{position:absolute;top:0;left:0}.el-image__placeholder{background:var(--el-fill-color-light)}.el-image__error{background:var(--el-fill-color-light);color:var(--el-text-color-placeholder);vertical-align:middle;justify-content:center;align-items:center;font-size:14px;display:flex}.el-image__preview{cursor:pointer}.el-textarea{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;vertical-align:bottom;width:100%;font-size:var(--el-font-size-base);display:inline-block;position:relative}.el-textarea__inner{resize:vertical;box-sizing:border-box;width:100%;line-height:1.5;font-size:inherit;color:var(--el-input-text-color,var(--el-text-color-regular));background-color:var(--el-input-bg-color,var(--el-fill-color-blank));-webkit-appearance:none;box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset;border-radius:var(--el-input-border-radius,var(--el-border-radius-base));transition:var(--el-transition-box-shadow);background-image:none;border:none;padding:5px 11px;font-family:inherit;display:block;position:relative}.el-textarea__inner.is-clearable{padding:5px 26px 5px 11px}.el-textarea__inner::placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-textarea__inner:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-textarea__inner:focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset;outline:none}.el-textarea__clear{color:var(--el-input-icon-color);cursor:pointer;font-size:14px;position:absolute;top:15px;right:11px;transform:translateY(-50%)}.el-textarea__clear:hover{color:var(--el-input-clear-hover-color)}.el-textarea .el-input__count{color:var(--el-color-info);background:var(--el-fill-color-blank);font-size:12px;line-height:14px;position:absolute;bottom:5px;right:10px}.el-textarea .el-input__count.is-outside{top:100%;right:0;bottom:unset;background:0 0;padding-top:2px;line-height:1;position:absolute}.el-textarea.is-disabled .el-textarea__inner{box-shadow:0 0 0 1px var(--el-disabled-border-color) inset;background-color:var(--el-disabled-bg-color);color:var(--el-disabled-text-color);cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:var(--el-text-color-placeholder)}.el-textarea.is-exceed .el-textarea__inner{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-textarea.is-exceed .el-input__count{color:var(--el-color-danger)}.el-input{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;--el-input-height:var(--el-component-size);font-size:var(--el-font-size-base);width:var(--el-input-width);line-height:var(--el-input-height);box-sizing:border-box;vertical-align:middle;display:inline-flex;position:relative}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{background:var(--el-text-color-disabled);border-radius:5px;width:6px}.el-input::-webkit-scrollbar-corner{background:var(--el-fill-color-blank)}.el-input::-webkit-scrollbar-track{background:var(--el-fill-color-blank)}.el-input::-webkit-scrollbar-track-piece{background:var(--el-fill-color-blank);width:6px}.el-input .el-input__clear,.el-input .el-input__password{color:var(--el-input-icon-color);cursor:pointer;font-size:14px}.el-input .el-input__clear:hover,.el-input .el-input__password:hover{color:var(--el-input-clear-hover-color)}.el-input .el-input__count{height:100%;color:var(--el-color-info);align-items:center;font-size:12px;display:inline-flex}.el-input .el-input__count .el-input__count-inner{background:var(--el-fill-color-blank);line-height:initial;padding-left:8px;display:inline-block}.el-input .el-input__count.is-outside{height:unset;padding-top:2px;position:absolute;top:100%;right:0}.el-input .el-input__count.is-outside .el-input__count-inner{background:0 0;padding-left:0;line-height:1}.el-input__wrapper{background-color:var(--el-input-bg-color,var(--el-fill-color-blank));border-radius:var(--el-input-border-radius,var(--el-border-radius-base));cursor:text;transition:var(--el-transition-box-shadow);box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset;background-image:none;flex-grow:1;justify-content:center;align-items:center;padding:1px 11px;display:inline-flex;transform:translate(0)}.el-input__wrapper:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-input__wrapper.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-input{--el-input-inner-height:calc(var(--el-input-height,32px) - 2px)}.el-input__inner{-webkit-appearance:none;width:100%;color:var(--el-input-text-color,var(--el-text-color-regular));font-size:inherit;height:var(--el-input-inner-height);line-height:var(--el-input-inner-height);box-sizing:border-box;background:0 0;border:none;outline:none;flex-grow:1;padding:0}.el-input__inner:focus{outline:none}.el-input__inner::placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-input__inner[type=password]::-ms-reveal{display:none}.el-input__inner[type=number]{line-height:1}.el-input__prefix{white-space:nowrap;height:100%;line-height:var(--el-input-inner-height);text-align:center;color:var(--el-input-icon-color,var(--el-text-color-placeholder));transition:all var(--el-transition-duration);pointer-events:none;flex-wrap:nowrap;flex-shrink:0;display:inline-flex}.el-input__prefix-inner{pointer-events:all;justify-content:center;align-items:center;display:inline-flex}.el-input__prefix-inner>:last-child{margin-right:8px}.el-input__prefix-inner>:first-child,.el-input__prefix-inner>:first-child.el-input__icon{margin-left:0}.el-input__suffix{white-space:nowrap;height:100%;line-height:var(--el-input-inner-height);text-align:center;color:var(--el-input-icon-color,var(--el-text-color-placeholder));transition:all var(--el-transition-duration);pointer-events:none;flex-wrap:nowrap;flex-shrink:0;display:inline-flex}.el-input__suffix-inner{pointer-events:all;justify-content:center;align-items:center;display:inline-flex}.el-input__suffix-inner>:first-child{margin-left:8px}.el-input .el-input__icon{height:inherit;line-height:inherit;transition:all var(--el-transition-duration);justify-content:center;align-items:center;margin-left:8px;display:flex}.el-input__validateIcon{pointer-events:none}.el-input.is-active .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-color, ) inset}.el-input.is-disabled{cursor:not-allowed}.el-input.is-disabled .el-input__wrapper{background-color:var(--el-disabled-bg-color);cursor:not-allowed;box-shadow:0 0 0 1px var(--el-disabled-border-color) inset}.el-input.is-disabled .el-input__inner{color:var(--el-disabled-text-color);-webkit-text-fill-color:var(--el-disabled-text-color);cursor:not-allowed}.el-input.is-disabled .el-input__inner::placeholder{color:var(--el-text-color-placeholder)}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-input.is-disabled .el-input__prefix-inner,.el-input.is-disabled .el-input__suffix-inner{pointer-events:none}.el-input.is-exceed .el-input__wrapper{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-input.is-exceed .el-input__suffix .el-input__count{color:var(--el-color-danger)}.el-input--large{--el-input-height:var(--el-component-size-large);font-size:14px}.el-input--large .el-input__wrapper{padding:1px 15px}.el-input--large{--el-input-inner-height:calc(var(--el-input-height,40px) - 2px)}.el-input--small{--el-input-height:var(--el-component-size-small);font-size:12px}.el-input--small .el-input__wrapper{padding:1px 7px}.el-input--small{--el-input-inner-height:calc(var(--el-input-height,24px) - 2px)}.el-input-group{align-items:stretch;width:100%;display:inline-flex}.el-input-group__append,.el-input-group__prepend{background-color:var(--el-fill-color-light);color:var(--el-color-info);border-radius:var(--el-input-border-radius);white-space:nowrap;justify-content:center;align-items:center;min-height:100%;padding:0 20px;display:inline-flex;position:relative}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:none}.el-input-group__append .el-select,.el-input-group__append .el-button,.el-input-group__prepend .el-select,.el-input-group__prepend .el-button{flex:1;margin:0 -20px;display:inline-block}.el-input-group__append button.el-button,.el-input-group__append button.el-button:hover,.el-input-group__append div.el-select .el-select__wrapper,.el-input-group__append div.el-select:hover .el-select__wrapper,.el-input-group__prepend button.el-button,.el-input-group__prepend button.el-button:hover,.el-input-group__prepend div.el-select .el-select__wrapper,.el-input-group__prepend div.el-select:hover .el-select__wrapper{color:inherit;background-color:#0000;border-color:#0000}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{box-shadow:1px 0 0 0 var(--el-input-border-color) inset,0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset;border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append{box-shadow:0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset,-1px 0 0 0 var(--el-input-border-color) inset;border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--prepend>.el-input__wrapper{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--prepend .el-input-group__prepend .el-select .el-select__wrapper{box-shadow:1px 0 0 0 var(--el-input-border-color) inset,0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset;border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group--append>.el-input__wrapper{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group--append .el-input-group__append .el-select .el-select__wrapper{box-shadow:0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset,-1px 0 0 0 var(--el-input-border-color) inset;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-hidden{display:none!important}.el-input-number{vertical-align:middle;width:150px;line-height:30px;display:inline-flex;position:relative}.el-input-number .el-input__wrapper{padding-left:42px;padding-right:42px}.el-input-number .el-input__inner{-webkit-appearance:none;-moz-appearance:textfield;text-align:center;line-height:1}.el-input-number .el-input__inner::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.el-input-number .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-input-number.is-left .el-input__inner{text-align:left}.el-input-number.is-right .el-input__inner{text-align:right}.el-input-number.is-center .el-input__inner{text-align:center}.el-input-number__increase,.el-input-number__decrease{z-index:1;background:var(--el-fill-color-light);width:32px;height:auto;color:var(--el-text-color-regular);cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;font-size:13px;display:flex;position:absolute;top:1px;bottom:1px}.el-input-number__increase:hover,.el-input-number__decrease:hover{color:var(--el-color-primary)}.el-input-number__increase:hover~.el-input:not(.is-disabled) .el-input__wrapper,.el-input-number__decrease:hover~.el-input:not(.is-disabled) .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color,var(--el-color-primary)) inset}.el-input-number__increase.is-disabled,.el-input-number__decrease.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-input-number__increase{border-radius:0 var(--el-border-radius-base) var(--el-border-radius-base) 0;border-left:var(--el-border);right:1px}.el-input-number__decrease{border-radius:var(--el-border-radius-base) 0 0 var(--el-border-radius-base);border-right:var(--el-border);left:1px}.el-input-number.is-disabled .el-input-number__increase,.el-input-number.is-disabled .el-input-number__decrease{border-color:var(--el-disabled-border-color);color:var(--el-disabled-border-color)}.el-input-number.is-disabled .el-input-number__increase:hover,.el-input-number.is-disabled .el-input-number__decrease:hover{color:var(--el-disabled-border-color);cursor:not-allowed}.el-input-number--large{width:180px;line-height:38px}.el-input-number--large .el-input-number__increase,.el-input-number--large .el-input-number__decrease{width:40px;font-size:14px}.el-input-number--large.is-controls-right .el-input--large .el-input__wrapper{padding-right:47px}.el-input-number--large .el-input--large .el-input__wrapper{padding-left:47px;padding-right:47px}.el-input-number--small{width:120px;line-height:22px}.el-input-number--small .el-input-number__increase,.el-input-number--small .el-input-number__decrease{width:24px;font-size:12px}.el-input-number--small.is-controls-right .el-input--small .el-input__wrapper{padding-right:31px}.el-input-number--small .el-input--small .el-input__wrapper{padding-left:31px;padding-right:31px}.el-input-number--small .el-input-number__increase [class*=el-icon],.el-input-number--small .el-input-number__decrease [class*=el-icon]{transform:scale(.9)}.el-input-number.is-without-controls .el-input__wrapper{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__wrapper{padding-left:15px;padding-right:42px}.el-input-number.is-controls-right .el-input-number__increase,.el-input-number.is-controls-right .el-input-number__decrease{--el-input-number-controls-height:15px;height:var(--el-input-number-controls-height);line-height:var(--el-input-number-controls-height)}.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon],.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon]{transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 var(--el-border-radius-base) 0 0;border-bottom:var(--el-border);bottom:auto;left:auto}.el-input-number.is-controls-right .el-input-number__decrease{border-right:none;border-left:var(--el-border);border-radius:0 0 var(--el-border-radius-base) 0;top:auto;left:auto;right:1px}.el-input-number.is-controls-right[class*=large] [class*=increase],.el-input-number.is-controls-right[class*=large] [class*=decrease]{--el-input-number-controls-height:19px}.el-input-number.is-controls-right[class*=small] [class*=increase],.el-input-number.is-controls-right[class*=small] [class*=decrease]{--el-input-number-controls-height:11px}.el-input-tag{--el-input-tag-border-color-hover:var(--el-border-color-hover);--el-input-tag-placeholder-color:var(--el-text-color-placeholder);--el-input-tag-disabled-color:var(--el-disabled-text-color);--el-input-tag-disabled-border:var(--el-disabled-border-color);--el-input-tag-font-size:var(--el-font-size-base);--el-input-tag-close-hover-color:var(--el-text-color-secondary);--el-input-tag-text-color:var(--el-text-color-regular);--el-input-tag-input-focus-border-color:var(--el-color-primary);--el-input-tag-width:100%;--el-input-tag-mini-height:var(--el-component-size);--el-input-tag-gap:6px;--el-input-tag-padding:4px;--el-input-tag-inner-padding:8px;--el-input-tag-line-height:24px;box-sizing:border-box;cursor:pointer;font-size:var(--el-input-tag-font-size);padding:var(--el-input-tag-padding);width:var(--el-input-tag-width);min-height:var(--el-input-tag-mini-height);line-height:var(--el-input-tag-line-height);border-radius:var(--el-border-radius-base);background-color:var(--el-fill-color-blank);transition:var(--el-transition-duration);box-shadow:0 0 0 1px var(--el-border-color) inset;align-items:center;display:flex;transform:translate(0)}.el-input-tag.is-focused{box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-input-tag.is-hovering:not(.is-focused){box-shadow:0 0 0 1px var(--el-border-color-hover) inset}.el-input-tag.is-disabled{cursor:not-allowed;background-color:var(--el-fill-color-light);box-shadow:0 0 0 1px var(--el-input-tag-disabled-border) inset}.el-input-tag.is-disabled:hover{box-shadow:0 0 0 1px var(--el-input-tag-disabled-border) inset}.el-input-tag.is-disabled.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-input-tag.is-disabled .el-input-tag__inner .el-input-tag__input,.el-input-tag.is-disabled .el-input-tag__inner .el-tag{cursor:not-allowed}.el-input-tag__prefix{padding:0 var(--el-input-tag-inner-padding);color:var(--el-input-icon-color,var(--el-text-color-placeholder));flex-shrink:0;align-items:center;display:flex}.el-input-tag__suffix{padding:0 var(--el-input-tag-inner-padding);color:var(--el-input-icon-color,var(--el-text-color-placeholder));flex-shrink:0;align-items:center;gap:8px;display:flex}.el-input-tag__collapse-tag{line-height:1}.el-input-tag__input-tag-list{flex-wrap:wrap;flex:1;align-items:center;gap:6px;min-width:0;display:flex;position:relative}.el-input-tag__input-tag-list.is-near{margin-left:-8px}.el-input-tag__input-tag-list .el-tag{cursor:pointer;border-color:#0000}.el-input-tag__input-tag-list .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-input-tag__input-tag-list .el-tag .el-tag__content{min-width:0}.el-input-tag__inner{align-items:center;gap:var(--el-input-tag-gap);flex-wrap:wrap;flex:1;min-width:0;max-width:100%;display:flex;position:relative}.el-input-tag__inner.is-left-space{margin-left:var(--el-input-tag-inner-padding)}.el-input-tag__inner.is-right-space{margin-right:var(--el-input-tag-inner-padding)}.el-input-tag__inner.is-draggable .el-tag{cursor:move;-webkit-user-select:none;user-select:none}.el-input-tag__drop-indicator{width:1px;height:var(--el-input-tag-line-height);background-color:var(--el-color-primary);position:absolute;top:0}.el-input-tag__inner .el-tag{cursor:pointer;border-color:#0000;max-width:100%}.el-input-tag__inner .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-input-tag__inner .el-tag .el-tag__content{text-overflow:ellipsis;white-space:nowrap;min-width:0;line-height:normal;overflow:hidden}.el-input-tag__input-wrapper{flex:1}.el-input-tag__input{color:var(--el-input-tag-text-color);font-size:inherit;font-family:inherit;line-height:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0000;border:none;outline:none;width:100%;padding:0}.el-input-tag__input::placeholder{color:var(--el-input-tag-placeholder-color)}.el-input-tag__input-calculator{visibility:hidden;white-space:pre;max-width:100%;position:absolute;top:0;left:0;overflow:hidden}.el-input-tag--large{--el-input-tag-gap:6px;--el-input-tag-padding:8px;--el-input-tag-padding-left:8px;--el-input-tag-font-size:14px}.el-input-tag--small{--el-input-tag-gap:4px;--el-input-tag-padding:2px;--el-input-tag-padding-left:6px;--el-input-tag-font-size:12px;--el-input-tag-line-height:20px;--el-input-tag-mini-height:var(--el-component-size-small)}.el-link{--el-link-font-size:var(--el-font-size-base);--el-link-font-weight:var(--el-font-weight-primary);--el-link-text-color:var(--el-text-color-regular);--el-link-hover-text-color:var(--el-color-primary);--el-link-disabled-text-color:var(--el-text-color-placeholder);vertical-align:middle;cursor:pointer;font-size:var(--el-link-font-size);font-weight:var(--el-link-font-weight);color:var(--el-link-text-color);outline:none;flex-direction:row;justify-content:center;align-items:center;padding:0;text-decoration:none;display:inline-flex;position:relative}.el-link.is-hover-underline:hover:after{content:"";border-bottom:1px solid var(--el-link-hover-text-color);height:0;position:absolute;bottom:0;left:0;right:0}.el-link.is-underline:after{content:"";border-bottom:1px solid var(--el-link-text-color);height:0;position:absolute;bottom:0;left:0;right:0}.el-link:hover{color:var(--el-link-hover-text-color)}.el-link:hover:after{border-color:var(--el-link-hover-text-color)}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link__inner{justify-content:center;align-items:center;display:inline-flex}.el-link.el-link--primary{--el-link-text-color:var(--el-color-primary);--el-link-hover-text-color:var(--el-color-primary-light-3);--el-link-disabled-text-color:var(--el-color-primary-light-5)}.el-link.el-link--success{--el-link-text-color:var(--el-color-success);--el-link-hover-text-color:var(--el-color-success-light-3);--el-link-disabled-text-color:var(--el-color-success-light-5)}.el-link.el-link--warning{--el-link-text-color:var(--el-color-warning);--el-link-hover-text-color:var(--el-color-warning-light-3);--el-link-disabled-text-color:var(--el-color-warning-light-5)}.el-link.el-link--danger{--el-link-text-color:var(--el-color-danger);--el-link-hover-text-color:var(--el-color-danger-light-3);--el-link-disabled-text-color:var(--el-color-danger-light-5)}.el-link.el-link--error{--el-link-text-color:var(--el-color-error);--el-link-hover-text-color:var(--el-color-error-light-3);--el-link-disabled-text-color:var(--el-color-error-light-5)}.el-link.el-link--info{--el-link-text-color:var(--el-color-info);--el-link-hover-text-color:var(--el-color-info-light-3);--el-link-disabled-text-color:var(--el-color-info-light-5)}.el-link.is-disabled{color:var(--el-link-disabled-text-color);cursor:not-allowed}.el-link.is-disabled:after{border-color:var(--el-link-disabled-text-color)}:root{--el-loading-spinner-size:42px;--el-loading-fullscreen-spinner-size:50px}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{z-index:2000;background-color:var(--el-mask-color);transition:opacity var(--el-transition-duration);margin:0;position:absolute;top:0;bottom:0;left:0;right:0}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:calc((0px - var(--el-loading-fullscreen-spinner-size)) / 2)}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:var(--el-loading-fullscreen-spinner-size);width:var(--el-loading-fullscreen-spinner-size)}.el-loading-spinner{margin-top:calc((0px - var(--el-loading-spinner-size)) / 2);text-align:center;width:100%;position:absolute;top:50%}.el-loading-spinner .el-loading-text{color:var(--el-color-primary);margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:var(--el-loading-spinner-size);width:var(--el-loading-spinner-size);animation:2s linear infinite loading-rotate;display:inline}.el-loading-spinner .path{stroke-dasharray:90 150;stroke-dashoffset:0;stroke-width:2px;stroke:var(--el-color-primary);stroke-linecap:round;animation:1.5s ease-in-out infinite loading-dash}.el-loading-spinner i{color:var(--el-color-primary)}.el-loading-fade-enter-from,.el-loading-fade-leave-to{opacity:0}@keyframes loading-rotate{to{transform:rotate(360deg)}}@keyframes loading-dash{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-40px}to{stroke-dasharray:90 150;stroke-dashoffset:-120px}}.el-main{--el-main-padding:20px;box-sizing:border-box;padding:var(--el-main-padding);flex:auto;display:block;overflow:auto}:root{--el-menu-active-color:var(--el-color-primary);--el-menu-text-color:var(--el-text-color-primary);--el-menu-hover-text-color:var(--el-color-primary);--el-menu-bg-color:var(--el-fill-color-blank);--el-menu-hover-bg-color:var(--el-color-primary-light-9);--el-menu-item-height:56px;--el-menu-sub-item-height:calc(var(--el-menu-item-height) - 6px);--el-menu-horizontal-height:60px;--el-menu-horizontal-sub-item-height:36px;--el-menu-item-font-size:var(--el-font-size-base);--el-menu-item-hover-fill:var(--el-color-primary-light-9);--el-menu-border-color:var(--el-border-color);--el-menu-base-level-padding:20px;--el-menu-level-padding:20px;--el-menu-icon-width:24px}.el-menu{border-right:solid 1px var(--el-menu-border-color);background-color:var(--el-menu-bg-color);box-sizing:border-box;margin:0;padding-left:0;list-style:none;position:relative}.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item,.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-sub-menu__title,.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item-group__title{white-space:nowrap;padding-left:calc(var(--el-menu-base-level-padding) + var(--el-menu-level) * var(--el-menu-level-padding))}.el-menu:not(.el-menu--collapse) .el-sub-menu__title{padding-right:calc(var(--el-menu-base-level-padding) + var(--el-menu-icon-width))}.el-menu--horizontal{height:var(--el-menu-horizontal-height);border-right:none;flex-wrap:nowrap;display:flex}.el-menu--horizontal.el-menu--popup-container{height:unset}.el-menu--horizontal.el-menu{border-bottom:solid 1px var(--el-menu-border-color)}.el-menu--horizontal>.el-menu-item{height:100%;color:var(--el-menu-text-color);border-bottom:2px solid #0000;justify-content:center;align-items:center;margin:0;display:inline-flex}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-sub-menu:focus,.el-menu--horizontal>.el-sub-menu:hover{outline:none}.el-menu--horizontal>.el-sub-menu:hover .el-sub-menu__title{color:var(--el-menu-hover-text-color)}.el-menu--horizontal>.el-sub-menu.is-active .el-sub-menu__title{border-bottom:2px solid var(--el-menu-active-color);color:var(--el-menu-active-color)}.el-menu--horizontal>.el-sub-menu .el-sub-menu__title{height:100%;color:var(--el-menu-text-color);border-bottom:2px solid #0000}.el-menu--horizontal>.el-sub-menu .el-sub-menu__title:hover{background-color:var(--el-menu-bg-color)}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-sub-menu__title{background-color:var(--el-menu-bg-color);height:var(--el-menu-horizontal-sub-item-height);line-height:var(--el-menu-horizontal-sub-item-height);color:var(--el-menu-text-color);align-items:center;padding:0 10px;display:flex}.el-menu--horizontal .el-menu .el-sub-menu__title{padding-right:40px}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-menu-item.is-active:hover,.el-menu--horizontal .el-menu .el-sub-menu.is-active>.el-sub-menu__title,.el-menu--horizontal .el-menu .el-sub-menu.is-active>.el-sub-menu__title:hover{color:var(--el-menu-active-color)}.el-menu--horizontal .el-menu-item:not(.is-disabled):hover,.el-menu--horizontal .el-menu-item:not(.is-disabled):focus{color:var(--el-menu-active-color,var(--el-menu-hover-text-color));background-color:var(--el-menu-hover-bg-color);outline:none}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid var(--el-menu-active-color);color:var(--el-menu-active-color)!important}.el-menu--collapse{width:calc(var(--el-menu-icon-width) + var(--el-menu-base-level-padding) * 2)}.el-menu--collapse>.el-menu-item [class^=el-icon],.el-menu--collapse>.el-sub-menu>.el-sub-menu__title [class^=el-icon],.el-menu--collapse>.el-menu-item-group>ul>.el-sub-menu>.el-sub-menu__title [class^=el-icon]{vertical-align:middle;width:var(--el-menu-icon-width);text-align:center;margin:0}.el-menu--collapse>.el-menu-item .el-sub-menu__icon-arrow,.el-menu--collapse>.el-sub-menu>.el-sub-menu__title .el-sub-menu__icon-arrow,.el-menu--collapse>.el-menu-item-group>ul>.el-sub-menu>.el-sub-menu__title .el-sub-menu__icon-arrow{display:none}.el-menu--collapse>.el-menu-item>span,.el-menu--collapse>.el-sub-menu>.el-sub-menu__title>span,.el-menu--collapse>.el-menu-item-group>ul>.el-sub-menu>.el-sub-menu__title>span{visibility:hidden;width:0;height:0;display:inline-block;overflow:hidden}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-menu .el-sub-menu{min-width:200px}.el-menu--collapse .el-sub-menu.is-active .el-sub-menu__title{color:var(--el-menu-active-color)}.el-menu--popup{z-index:100;border-radius:var(--el-border-radius-small);min-width:200px;box-shadow:var(--el-box-shadow-light);border:none;padding:5px 0}.el-menu .el-icon{flex-shrink:0}.el-menu-item{height:var(--el-menu-item-height);line-height:var(--el-menu-item-height);font-size:var(--el-menu-item-font-size);color:var(--el-menu-text-color);padding:0 var(--el-menu-base-level-padding);cursor:pointer;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration),color var(--el-transition-duration);box-sizing:border-box;white-space:nowrap;align-items:center;list-style:none;display:flex;position:relative}.el-menu-item *{vertical-align:bottom}.el-menu-item i{color:inherit}.el-menu-item:hover,.el-menu-item:focus{outline:none}.el-menu-item:hover{background-color:var(--el-menu-hover-bg-color)}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon]{width:var(--el-menu-icon-width);text-align:center;vertical-align:middle;margin-right:5px;font-size:18px}.el-menu-item.is-active{color:var(--el-menu-active-color)}.el-menu-item.is-active i{color:inherit}.el-menu-item .el-menu-tooltip__trigger{box-sizing:border-box;width:100%;height:100%;padding:0 var(--el-menu-base-level-padding);align-items:center;display:inline-flex;position:absolute;top:0;left:0}.el-sub-menu{margin:0;padding-left:0;list-style:none}.el-sub-menu__title{height:var(--el-menu-item-height);line-height:var(--el-menu-item-height);font-size:var(--el-menu-item-font-size);color:var(--el-menu-text-color);padding:0 var(--el-menu-base-level-padding);cursor:pointer;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration),color var(--el-transition-duration);box-sizing:border-box;white-space:nowrap;align-items:center;list-style:none;display:flex;position:relative}.el-sub-menu__title *{vertical-align:bottom}.el-sub-menu__title i{color:inherit}.el-sub-menu__title:hover,.el-sub-menu__title:focus{outline:none}.el-sub-menu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-sub-menu__title:hover{background-color:var(--el-menu-hover-bg-color)}.el-sub-menu .el-menu{border:none}.el-sub-menu .el-menu-item{height:var(--el-menu-sub-item-height);line-height:var(--el-menu-sub-item-height)}.el-sub-menu.el-sub-menu__hide-arrow .el-sub-menu__title{padding-right:var(--el-menu-base-level-padding)}.el-sub-menu__hide-arrow .el-sub-menu__icon-arrow{display:none!important}.el-sub-menu.is-active .el-sub-menu__title{border-bottom-color:var(--el-menu-active-color)}.el-sub-menu.is-disabled .el-sub-menu__title,.el-sub-menu.is-disabled .el-menu-item{opacity:.25;cursor:not-allowed;background:0 0!important}.el-sub-menu .el-icon{vertical-align:middle;width:var(--el-menu-icon-width);text-align:center;margin-right:5px;font-size:18px}.el-sub-menu .el-icon.el-sub-menu__icon-more{margin-right:0!important}.el-sub-menu .el-sub-menu__icon-arrow{top:50%;right:var(--el-menu-base-level-padding);transition:transform var(--el-transition-duration);width:inherit;margin-top:-6px;margin-right:0;font-size:12px;position:absolute}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px var(--el-menu-base-level-padding);color:var(--el-text-color-secondary);font-size:12px;line-height:normal}.horizontal-collapse-transition .el-sub-menu__title .el-sub-menu__icon-arrow{transition:var(--el-transition-duration-fast);opacity:0}.el-popper,.el-menu--popup-container,.el-menu{outline:none}.el-message-box{--el-messagebox-title-color:var(--el-text-color-primary);--el-messagebox-width:420px;--el-messagebox-border-radius:4px;--el-messagebox-box-shadow:var(--el-box-shadow);--el-messagebox-font-size:var(--el-font-size-large);--el-messagebox-content-font-size:var(--el-font-size-base);--el-messagebox-content-color:var(--el-text-color-regular);--el-messagebox-error-font-size:12px;--el-messagebox-padding-primary:12px;--el-messagebox-font-line-height:var(--el-font-line-height-primary);max-width:var(--el-messagebox-width);width:100%;padding:var(--el-messagebox-padding-primary);vertical-align:middle;background-color:var(--el-bg-color);border-radius:var(--el-messagebox-border-radius);font-size:var(--el-messagebox-font-size);box-shadow:var(--el-messagebox-box-shadow);text-align:left;-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;overflow-wrap:break-word;display:inline-block;position:relative;overflow:hidden}.el-message-box:focus{outline:none!important}.is-message-box .el-overlay-message-box{text-align:center;padding:16px;position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.is-message-box .el-overlay-message-box:after{content:"";vertical-align:middle;width:0;height:100%;display:inline-block}.el-message-box.is-draggable .el-message-box__header{cursor:move;-webkit-user-select:none;user-select:none}.el-message-box__header{padding-bottom:var(--el-messagebox-padding-primary)}.el-message-box__header.show-close{padding-right:calc(var(--el-messagebox-padding-primary) + var(--el-message-close-size,16px))}.el-message-box__title{font-size:var(--el-messagebox-font-size);line-height:var(--el-messagebox-font-line-height);color:var(--el-messagebox-title-color)}.el-message-box__headerbtn{width:40px;height:40px;font-size:var(--el-message-close-size,16px);cursor:pointer;background:0 0;border:none;outline:none;padding:0;position:absolute;top:0;right:0}.el-message-box__headerbtn .el-message-box__close{color:var(--el-color-info);font-size:inherit}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:var(--el-color-primary)}.el-message-box__content{color:var(--el-messagebox-content-color);font-size:var(--el-messagebox-content-font-size)}.el-message-box__container{align-items:center;gap:12px;display:flex}.el-message-box__input{padding-top:12px}.el-message-box__input div.invalid>input,.el-message-box__input div.invalid>input:focus{border-color:var(--el-color-error)}.el-message-box__status{font-size:24px}.el-message-box__status.el-message-box-icon--primary{--el-messagebox-color:var(--el-color-primary);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--success{--el-messagebox-color:var(--el-color-success);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--info{--el-messagebox-color:var(--el-color-info);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--warning{--el-messagebox-color:var(--el-color-warning);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--error{--el-messagebox-color:var(--el-color-error);color:var(--el-messagebox-color)}.el-message-box__message{min-width:0;margin:0}.el-message-box__message p{line-height:var(--el-messagebox-font-line-height);margin:0}.el-message-box__errormsg{color:var(--el-color-error);font-size:var(--el-messagebox-error-font-size);line-height:var(--el-messagebox-font-line-height)}.el-message-box__btns{padding-top:var(--el-messagebox-padding-primary);flex-wrap:wrap;justify-content:flex-end;align-items:center;display:flex}.el-message-box--center .el-message-box__title{justify-content:center;align-items:center;gap:6px;display:flex}.el-message-box--center .el-message-box__status{font-size:inherit}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__container{justify-content:center}.el-message-box-parent--hidden{overflow:hidden}.fade-in-linear-enter-active .el-overlay-message-box{animation:msgbox-fade-in var(--el-transition-duration)}.fade-in-linear-leave-active .el-overlay-message-box{animation:msgbox-fade-in var(--el-transition-duration) reverse}@keyframes msgbox-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translate(0)}}.el-message{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-border-color-lighter);--el-message-padding:11px 15px;--el-message-close-size:16px;--el-message-close-icon-color:var(--el-text-color-placeholder);--el-message-close-hover-color:var(--el-text-color-secondary);box-sizing:border-box;border-radius:var(--el-border-radius-base);border-width:var(--el-border-width);border-style:var(--el-border-style);border-color:var(--el-message-border-color);background-color:var(--el-message-bg-color);width:max-content;max-width:calc(100% - 32px);transition:opacity var(--el-transition-duration),transform .4s,top .4s,bottom .4s;padding:var(--el-message-padding);align-items:center;gap:8px;display:flex;position:fixed}.el-message.is-left{left:16px}.el-message.is-right{right:16px}.el-message.is-center{left:50%;transform:translate(-50%)}.el-message.is-plain{background-color:var(--el-bg-color-overlay);border-color:var(--el-bg-color-overlay);box-shadow:var(--el-box-shadow-light)}.el-message p{margin:0}.el-message--primary{--el-message-bg-color:var(--el-color-primary-light-9);--el-message-border-color:var(--el-color-primary-light-8);--el-message-text-color:var(--el-color-primary)}.el-message--primary .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--primary{color:var(--el-message-text-color)}.el-message--success{--el-message-bg-color:var(--el-color-success-light-9);--el-message-border-color:var(--el-color-success-light-8);--el-message-text-color:var(--el-color-success)}.el-message--success .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--success{color:var(--el-message-text-color)}.el-message--info{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-color-info-light-8);--el-message-text-color:var(--el-color-info)}.el-message--info .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--info{color:var(--el-message-text-color)}.el-message--warning{--el-message-bg-color:var(--el-color-warning-light-9);--el-message-border-color:var(--el-color-warning-light-8);--el-message-text-color:var(--el-color-warning)}.el-message--warning .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--warning{color:var(--el-message-text-color)}.el-message--error{--el-message-bg-color:var(--el-color-error-light-9);--el-message-border-color:var(--el-color-error-light-8);--el-message-text-color:var(--el-color-error)}.el-message--error .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--error{color:var(--el-message-text-color)}.el-message .el-message__badge{position:absolute;top:-8px;right:-8px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__content:focus{outline-width:0}.el-message .el-message__closeBtn{cursor:pointer;color:var(--el-message-close-icon-color);font-size:var(--el-message-close-size)}.el-message .el-message__closeBtn:focus{outline-width:0}.el-message .el-message__closeBtn:hover{color:var(--el-message-close-hover-color)}.el-message-fade-enter-from,.el-message-fade-leave-to{opacity:0}.el-message-fade-enter-from.is-left,.el-message-fade-enter-from.is-right,.el-message-fade-leave-to.is-left,.el-message-fade-leave-to.is-right{transform:translateY(-100%)}.el-message-fade-enter-from.is-left.is-bottom,.el-message-fade-enter-from.is-right.is-bottom,.el-message-fade-leave-to.is-left.is-bottom,.el-message-fade-leave-to.is-right.is-bottom{transform:translateY(100%)}.el-message-fade-enter-from.is-center,.el-message-fade-leave-to.is-center{transform:translate(-50%,-100%)}.el-message-fade-enter-from.is-center.is-bottom,.el-message-fade-leave-to.is-center.is-bottom{transform:translate(-50%,100%)}.el-notification{--el-notification-width:330px;--el-notification-padding:14px 26px 14px 13px;--el-notification-radius:8px;--el-notification-shadow:var(--el-box-shadow-light);--el-notification-border-color:var(--el-border-color-lighter);--el-notification-icon-size:24px;--el-notification-close-font-size:var(--el-message-close-size,16px);--el-notification-group-margin-left:13px;--el-notification-group-margin-right:8px;--el-notification-content-font-size:var(--el-font-size-base);--el-notification-content-color:var(--el-text-color-regular);--el-notification-title-font-size:16px;--el-notification-title-color:var(--el-text-color-primary);--el-notification-close-color:var(--el-text-color-secondary);--el-notification-close-hover-color:var(--el-text-color-regular);width:var(--el-notification-width);padding:var(--el-notification-padding);border-radius:var(--el-notification-radius);box-sizing:border-box;border:1px solid var(--el-notification-border-color);background-color:var(--el-bg-color-overlay);box-shadow:var(--el-notification-shadow);transition:opacity var(--el-transition-duration),transform var(--el-transition-duration),left var(--el-transition-duration),right var(--el-transition-duration),top .4s,bottom var(--el-transition-duration);overflow-wrap:break-word;z-index:9999;display:flex;position:fixed;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{min-width:0;margin-left:var(--el-notification-group-margin-left);margin-right:var(--el-notification-group-margin-right);flex:1}.el-notification__title{font-weight:700;font-size:var(--el-notification-title-font-size);line-height:var(--el-notification-icon-size);color:var(--el-notification-title-color);margin:0}.el-notification__content{font-size:var(--el-notification-content-font-size);color:var(--el-notification-content-color);margin:6px 0 0;line-height:24px}.el-notification__content p{margin:0}.el-notification .el-notification__icon{height:var(--el-notification-icon-size);width:var(--el-notification-icon-size);font-size:var(--el-notification-icon-size);flex-shrink:0}.el-notification .el-notification__closeBtn{cursor:pointer;color:var(--el-notification-close-color);font-size:var(--el-notification-close-font-size);position:absolute;top:18px;right:15px}.el-notification .el-notification__closeBtn:hover{color:var(--el-notification-close-hover-color)}.el-notification .el-notification--primary{--el-notification-icon-color:var(--el-color-primary);color:var(--el-notification-icon-color)}.el-notification .el-notification--success{--el-notification-icon-color:var(--el-color-success);color:var(--el-notification-icon-color)}.el-notification .el-notification--info{--el-notification-icon-color:var(--el-color-info);color:var(--el-notification-icon-color)}.el-notification .el-notification--warning{--el-notification-icon-color:var(--el-color-warning);color:var(--el-notification-icon-color)}.el-notification .el-notification--error{--el-notification-icon-color:var(--el-color-error);color:var(--el-notification-icon-color)}.el-notification-fade-enter-from.right{right:0;transform:translate(100%)}.el-notification-fade-enter-from.left{left:0;transform:translate(-100%)}.el-notification-fade-leave-to{opacity:0}.el-overlay{z-index:2000;background-color:var(--el-overlay-color-lighter);height:100%;position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.el-overlay .el-overlay-root{height:0}.el-page-header.is-contentful .el-page-header__main{border-top:1px solid var(--el-border-color-light);margin-top:16px}.el-page-header__header{justify-content:space-between;align-items:center;line-height:24px;display:flex}.el-page-header__left{align-items:center;margin-right:40px;display:flex;position:relative}.el-page-header__back{cursor:pointer;align-items:center;display:flex}.el-page-header__left .el-divider--vertical{margin:0 16px}.el-page-header__icon{align-items:center;margin-right:10px;font-size:16px;display:flex}.el-page-header__icon .el-icon{font-size:inherit}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{color:var(--el-text-color-primary);font-size:18px}.el-page-header__breadcrumb{margin-bottom:16px}.el-pagination{--el-pagination-font-size:14px;--el-pagination-bg-color:var(--el-fill-color-blank);--el-pagination-text-color:var(--el-text-color-primary);--el-pagination-border-radius:2px;--el-pagination-button-color:var(--el-text-color-primary);--el-pagination-button-width:32px;--el-pagination-button-height:32px;--el-pagination-button-disabled-color:var(--el-text-color-placeholder);--el-pagination-button-disabled-bg-color:var(--el-fill-color-blank);--el-pagination-button-bg-color:var(--el-fill-color);--el-pagination-hover-color:var(--el-color-primary);--el-pagination-font-size-small:12px;--el-pagination-button-width-small:24px;--el-pagination-button-height-small:24px;--el-pagination-button-width-large:40px;--el-pagination-button-height-large:40px;--el-pagination-item-gap:16px;white-space:nowrap;color:var(--el-pagination-text-color);font-size:var(--el-pagination-font-size);align-items:center;font-weight:400;display:flex}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield}.el-pagination .el-select{width:128px}.el-pagination .btn-prev,.el-pagination .btn-next{font-size:var(--el-pagination-font-size);min-width:var(--el-pagination-button-width);height:var(--el-pagination-button-height);line-height:var(--el-pagination-button-height);color:var(--el-pagination-button-color);background:var(--el-pagination-bg-color);border-radius:var(--el-pagination-border-radius);cursor:pointer;text-align:center;box-sizing:border-box;border:none;justify-content:center;align-items:center;padding:0 4px;display:flex}.el-pagination .btn-prev *,.el-pagination .btn-next *{pointer-events:none}.el-pagination .btn-prev:focus,.el-pagination .btn-next:focus{outline:none}.el-pagination .btn-prev:hover,.el-pagination .btn-next:hover{color:var(--el-pagination-hover-color)}.el-pagination .btn-prev.is-active,.el-pagination .btn-next.is-active{color:var(--el-pagination-hover-color);cursor:default;font-weight:700}.el-pagination .btn-prev.is-active.is-disabled,.el-pagination .btn-next.is-active.is-disabled{color:var(--el-text-color-secondary);font-weight:700}.el-pagination .btn-prev:disabled,.el-pagination .btn-prev.is-disabled,.el-pagination .btn-next:disabled,.el-pagination .btn-next.is-disabled{color:var(--el-pagination-button-disabled-color);background-color:var(--el-pagination-button-disabled-bg-color);cursor:not-allowed}.el-pagination .btn-prev:focus-visible{outline:1px solid var(--el-pagination-hover-color);outline-offset:-1px}.el-pagination .btn-next:focus-visible{outline:1px solid var(--el-pagination-hover-color);outline-offset:-1px}.el-pagination .btn-prev .el-icon,.el-pagination .btn-next .el-icon{width:inherit;font-size:12px;font-weight:700;display:block}.el-pagination>.is-first{margin-left:0!important}.el-pagination>.is-last{margin-right:0!important}.el-pagination .btn-prev{margin-left:var(--el-pagination-item-gap)}.el-pagination__sizes,.el-pagination__total{margin-left:var(--el-pagination-item-gap);color:var(--el-text-color-regular);font-weight:400}.el-pagination__total[disabled=true]{color:var(--el-text-color-placeholder)}.el-pagination__jump{margin-left:var(--el-pagination-item-gap);color:var(--el-text-color-regular);align-items:center;font-weight:400;display:flex}.el-pagination__jump[disabled=true]{color:var(--el-text-color-placeholder)}.el-pagination__goto{margin-right:8px}.el-pagination__editor{text-align:center;box-sizing:border-box}.el-pagination__editor.el-input{width:56px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination__classifier{margin-left:8px}.el-pagination__rightwrapper{flex:1;justify-content:flex-end;align-items:center;display:flex}.el-pagination.is-background .btn-prev,.el-pagination.is-background .btn-next,.el-pagination.is-background .el-pager li{background-color:var(--el-pagination-button-bg-color);margin:0 4px}.el-pagination.is-background .btn-prev.is-active,.el-pagination.is-background .btn-next.is-active,.el-pagination.is-background .el-pager li.is-active{background-color:var(--el-color-primary);color:var(--el-color-white)}.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .btn-prev.is-disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-next.is-disabled,.el-pagination.is-background .el-pager li:disabled,.el-pagination.is-background .el-pager li.is-disabled{color:var(--el-text-color-placeholder);background-color:var(--el-disabled-bg-color)}.el-pagination.is-background .btn-prev:disabled.is-active,.el-pagination.is-background .btn-prev.is-disabled.is-active,.el-pagination.is-background .btn-next:disabled.is-active,.el-pagination.is-background .btn-next.is-disabled.is-active,.el-pagination.is-background .el-pager li:disabled.is-active,.el-pagination.is-background .el-pager li.is-disabled.is-active{color:var(--el-text-color-secondary);background-color:var(--el-fill-color-dark)}.el-pagination.is-background .btn-prev{margin-left:var(--el-pagination-item-gap)}.el-pagination--small .btn-prev,.el-pagination--small .btn-next,.el-pagination--small .el-pager li{height:var(--el-pagination-button-height-small);line-height:var(--el-pagination-button-height-small);font-size:var(--el-pagination-font-size-small);min-width:var(--el-pagination-button-width-small)}.el-pagination--small span:not([class*=suffix]),.el-pagination--small button{font-size:var(--el-pagination-font-size-small)}.el-pagination--small .el-select{width:100px}.el-pagination--large .btn-prev,.el-pagination--large .btn-next,.el-pagination--large .el-pager li{height:var(--el-pagination-button-height-large);line-height:var(--el-pagination-button-height-large);min-width:var(--el-pagination-button-width-large)}.el-pagination--large .el-select .el-input{width:160px}.el-pager{-webkit-user-select:none;user-select:none;align-items:center;margin:0;padding:0;font-size:0;list-style:none;display:flex}.el-pager li{font-size:var(--el-pagination-font-size);min-width:var(--el-pagination-button-width);height:var(--el-pagination-button-height);line-height:var(--el-pagination-button-height);color:var(--el-pagination-button-color);background:var(--el-pagination-bg-color);border-radius:var(--el-pagination-border-radius);cursor:pointer;text-align:center;box-sizing:border-box;border:none;justify-content:center;align-items:center;padding:0 4px;display:flex}.el-pager li *{pointer-events:none}.el-pager li:focus{outline:none}.el-pager li:hover{color:var(--el-pagination-hover-color)}.el-pager li.is-active{color:var(--el-pagination-hover-color);cursor:default;font-weight:700}.el-pager li.is-active.is-disabled{color:var(--el-text-color-secondary);font-weight:700}.el-pager li:disabled,.el-pager li.is-disabled{color:var(--el-pagination-button-disabled-color);background-color:var(--el-pagination-button-disabled-bg-color);cursor:not-allowed}.el-pager li:focus-visible{outline:1px solid var(--el-pagination-hover-color);outline-offset:-1px}.el-popconfirm{outline:none}.el-popconfirm__main{align-items:center;display:flex}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin-top:8px}.el-popover{--el-popover-bg-color:var(--el-bg-color-overlay);--el-popover-font-size:var(--el-font-size-base);--el-popover-border-color:var(--el-border-color-lighter);--el-popover-padding:12px;--el-popover-padding-large:18px 20px;--el-popover-title-font-size:16px;--el-popover-title-text-color:var(--el-text-color-primary);--el-popover-border-radius:4px}.el-popover.el-popper{background:var(--el-popover-bg-color);border-radius:var(--el-popover-border-radius);border:1px solid var(--el-popover-border-color);min-width:150px;padding:var(--el-popover-padding);z-index:var(--el-index-popper);color:var(--el-text-color-regular);line-height:1.4;font-size:var(--el-popover-font-size);box-shadow:var(--el-box-shadow-light);overflow-wrap:break-word;box-sizing:border-box}.el-popover.el-popper--plain{padding:var(--el-popover-padding-large)}.el-popover__title{color:var(--el-popover-title-text-color);font-size:var(--el-popover-title-font-size);margin-bottom:12px;line-height:1}.el-popover__reference:focus:not(.focusing),.el-popover__reference:focus:hover{outline-width:0}.el-popover.el-popper.is-dark{--el-popover-bg-color:var(--el-text-color-primary);--el-popover-border-color:var(--el-text-color-primary);--el-popover-title-text-color:var(--el-bg-color);color:var(--el-bg-color)}.el-popover.el-popper:focus:active,.el-popover.el-popper:focus{outline-width:0}.el-progress{align-items:center;line-height:1;display:flex;position:relative}.el-progress__text{color:var(--el-text-color-regular);min-width:50px;margin-left:5px;font-size:14px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle,.el-progress--dashboard{display:inline-block}.el-progress--circle .el-progress__text,.el-progress--dashboard .el-progress__text{text-align:center;width:100%;margin:0;position:absolute;top:50%;left:0;transform:translateY(-50%)}.el-progress--circle .el-progress__text i,.el-progress--dashboard .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{margin-right:0;padding-right:0;display:block}.el-progress--text-inside .el-progress-bar{margin-right:0;padding-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:var(--el-color-success)}.el-progress.is-success .el-progress__text{color:var(--el-color-success)}.el-progress.is-warning .el-progress-bar__inner{background-color:var(--el-color-warning)}.el-progress.is-warning .el-progress__text{color:var(--el-color-warning)}.el-progress.is-exception .el-progress-bar__inner{background-color:var(--el-color-danger)}.el-progress.is-exception .el-progress__text{color:var(--el-color-danger)}.el-progress-bar{box-sizing:border-box;flex-grow:1}.el-progress-bar__outer{background-color:var(--el-border-color-lighter);vertical-align:middle;border-radius:100px;height:6px;position:relative;overflow:hidden}.el-progress-bar__inner{background-color:var(--el-color-primary);text-align:right;white-space:nowrap;border-radius:100px;height:100%;line-height:1;transition:width .6s;position:absolute;top:0;left:0}.el-progress-bar__inner:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-progress-bar__inner--indeterminate{animation:3s infinite indeterminate;transform:translateZ(0)}.el-progress-bar__inner--striped{background-image:linear-gradient(45deg,#0000001a 25%,#0000 25%,#0000 50%,#0000001a 50%,#0000001a 75%,#0000 75%,#0000);background-size:1.25em 1.25em}.el-progress-bar__inner--striped.el-progress-bar__inner--striped-flow{animation:3s linear infinite striped-flow}.el-progress-bar__innerText{vertical-align:middle;color:#fff;margin:0 5px;font-size:12px;display:inline-block}@keyframes progress{0%{background-position:0 0}to{background-position:32px 0}}@keyframes indeterminate{0%{left:-100%}to{left:100%}}@keyframes striped-flow{0%{background-position:-100%}to{background-position:100%}}.el-radio-button{--el-radio-button-checked-bg-color:var(--el-color-primary);--el-radio-button-checked-text-color:var(--el-color-white);--el-radio-button-checked-border-color:var(--el-color-primary);--el-radio-button-disabled-checked-fill:var(--el-border-color-extra-light);outline:none;display:inline-block;position:relative}.el-radio-button__inner{white-space:nowrap;vertical-align:middle;background:var(--el-button-bg-color,var(--el-fill-color-blank));outline:var(--el-border);line-height:1;font-weight:var(--el-button-font-weight,var(--el-font-weight-primary));color:var(--el-button-text-color,var(--el-text-color-regular));-webkit-appearance:none;text-align:center;box-sizing:border-box;cursor:pointer;transition:var(--el-transition-all);-webkit-user-select:none;user-select:none;font-size:var(--el-font-size-base);border-radius:0;margin:0;padding:8px 15px;display:inline-block;position:relative}.el-radio-button__inner.is-round{padding:8px 15px}.el-radio-button__inner:hover{color:var(--el-color-primary)}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-radius:var(--el-border-radius-base) 0 0 var(--el-border-radius-base);box-shadow:none!important}.el-radio-button.is-active .el-radio-button__original-radio:not(:disabled)+.el-radio-button__inner{color:var(--el-radio-button-checked-text-color,var(--el-color-white));background-color:var(--el-radio-button-checked-bg-color,var(--el-color-primary));border-color:var(--el-radio-button-checked-border-color,var(--el-color-primary));box-shadow:-1px 0 0 0 var(--el-radio-button-checked-border-color,var(--el-color-primary))}.el-radio-button__original-radio{opacity:0;z-index:-1;outline:none;position:absolute}.el-radio-button__original-radio:focus-visible+.el-radio-button__inner{border-left:var(--el-border);border-left-color:var(--el-radio-button-checked-border-color,var(--el-color-primary));outline:2px solid var(--el-radio-button-checked-border-color);outline-offset:1px;z-index:2;border-radius:var(--el-border-radius-base);box-shadow:none}.el-radio-button__original-radio:disabled+.el-radio-button__inner{color:var(--el-disabled-text-color);cursor:not-allowed;background-image:none;background-color:var(--el-button-disabled-bg-color,var(--el-fill-color-blank));border-color:var(--el-button-disabled-border-color,var(--el-border-color-light));box-shadow:none}.el-radio-button__original-radio:disabled:checked+.el-radio-button__inner{background-color:var(--el-radio-button-disabled-checked-fill)}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 var(--el-border-radius-base) var(--el-border-radius-base) 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:var(--el-border-radius-base)}.el-radio-button--large .el-radio-button__inner{font-size:var(--el-font-size-base);border-radius:0;padding:12px 19px}.el-radio-button--large .el-radio-button__inner.is-round{padding:12px 19px}.el-radio-button--small .el-radio-button__inner{border-radius:0;padding:5px 11px;font-size:12px}.el-radio-button--small .el-radio-button__inner.is-round{padding:5px 11px}.el-radio-group{flex-wrap:wrap;align-items:center;font-size:0;display:inline-flex}.el-radio{--el-radio-font-size:var(--el-font-size-base);--el-radio-text-color:var(--el-text-color-regular);--el-radio-font-weight:var(--el-font-weight-primary);--el-radio-input-height:14px;--el-radio-input-width:14px;--el-radio-input-border-radius:var(--el-border-radius-circle);--el-radio-input-bg-color:var(--el-fill-color-blank);--el-radio-input-border:var(--el-border);--el-radio-input-border-color:var(--el-border-color);--el-radio-input-border-color-hover:var(--el-color-primary);color:var(--el-radio-text-color);font-weight:var(--el-radio-font-weight);cursor:pointer;white-space:nowrap;font-size:var(--el-font-size-base);-webkit-user-select:none;user-select:none;outline:none;align-items:center;height:32px;margin-right:30px;display:inline-flex;position:relative}.el-radio.el-radio--large{height:40px}.el-radio.el-radio--small{height:24px}.el-radio.is-bordered{border-radius:var(--el-border-radius-base);border:var(--el-border);box-sizing:border-box;padding:0 15px 0 9px}.el-radio.is-bordered.is-checked{border-color:var(--el-color-primary)}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:var(--el-border-color-lighter)}.el-radio.is-bordered.el-radio--large{border-radius:var(--el-border-radius-base);padding:0 19px 0 11px}.el-radio.is-bordered.el-radio--large .el-radio__label{font-size:var(--el-font-size-base)}.el-radio.is-bordered.el-radio--large .el-radio__inner{width:14px;height:14px}.el-radio.is-bordered.el-radio--small{border-radius:var(--el-border-radius-base);padding:0 11px 0 7px}.el-radio.is-bordered.el-radio--small .el-radio__label{font-size:12px}.el-radio.is-bordered.el-radio--small .el-radio__inner{width:12px;height:12px}.el-radio:last-child{margin-right:0}.el-radio__input{white-space:nowrap;cursor:pointer;vertical-align:middle;outline:none;display:inline-flex;position:relative}.el-radio__input.is-disabled .el-radio__inner{background-color:var(--el-disabled-bg-color);border-color:var(--el-disabled-border-color);cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner:after{cursor:not-allowed;background-color:var(--el-disabled-bg-color)}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:var(--el-disabled-bg-color);border-color:var(--el-disabled-border-color)}.el-radio__input.is-disabled.is-checked .el-radio__inner:after{background-color:var(--el-text-color-placeholder)}.el-radio__input.is-disabled+span.el-radio__label{color:var(--el-text-color-placeholder);cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:var(--el-color-primary);background:var(--el-color-primary)}.el-radio__input.is-checked .el-radio__inner:after{background-color:var(--el-color-white);transform:translate(-50%,-50%)scale(1)}.el-radio__input.is-checked+.el-radio__label{color:var(--el-color-primary)}.el-radio__input.is-focus .el-radio__inner{border-color:var(--el-radio-input-border-color-hover)}.el-radio__inner{border:var(--el-radio-input-border);border-radius:var(--el-radio-input-border-radius);width:var(--el-radio-input-width);height:var(--el-radio-input-height);background-color:var(--el-radio-input-bg-color);cursor:pointer;box-sizing:border-box;transition:all .3s;display:inline-block;position:relative}.el-radio__inner:hover{border-color:var(--el-radio-input-border-color-hover)}.el-radio__inner:after{border-radius:var(--el-radio-input-border-radius);content:"";width:4px;height:4px;transition:transform .15s ease-in;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)scale(0)}.el-radio__original{opacity:0;z-index:-1;outline:none;margin:0;position:absolute;top:0;bottom:0;left:0;right:0}.el-radio__original:focus-visible+.el-radio__inner{outline:2px solid var(--el-radio-input-border-color-hover);outline-offset:1px;border-radius:var(--el-radio-input-border-radius)}.el-radio:focus:not(:focus-visible):not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{box-shadow:0 0 2px 2px var(--el-radio-input-border-color-hover)}.el-radio__label{font-size:var(--el-radio-font-size);padding-left:8px}.el-radio.el-radio--large .el-radio__label{font-size:14px}.el-radio.el-radio--large .el-radio__inner{width:14px;height:14px}.el-radio.el-radio--small .el-radio__label{font-size:12px}.el-radio.el-radio--small .el-radio__inner{width:12px;height:12px}.el-rate{--el-rate-height:20px;--el-rate-font-size:var(--el-font-size-base);--el-rate-icon-size:18px;--el-rate-icon-margin:6px;--el-rate-void-color:var(--el-border-color-darker);--el-rate-fill-color:#f7ba2a;--el-rate-disabled-void-color:var(--el-fill-color);--el-rate-text-color:var(--el-text-color-primary);--el-rate-outline-color:var(--el-color-primary-light-5);align-items:center;height:32px;display:inline-flex}.el-rate:focus,.el-rate:active{outline:none}.el-rate:focus-visible .el-rate__item .el-rate__icon.is-focus-visible{outline:2px solid var(--el-rate-outline-color);transition:outline-offset,outline}.el-rate__item{cursor:pointer;vertical-align:middle;color:var(--el-rate-void-color);font-size:0;line-height:normal;display:inline-block;position:relative}.el-rate .el-rate__icon{font-size:var(--el-rate-icon-size);margin-right:var(--el-rate-icon-margin);transition:var(--el-transition-duration);display:inline-block;position:relative}.el-rate .el-rate__icon.hover{transform:scale(1.15)}.el-rate .el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate .el-rate__icon.is-active{color:var(--el-rate-fill-color)}.el-rate__decimal{color:var(--el-rate-fill-color);display:inline-block;position:absolute;top:0;left:0;overflow:hidden}.el-rate__decimal--box{position:absolute;top:0;left:0}.el-rate__text{font-size:var(--el-rate-font-size);vertical-align:middle;color:var(--el-rate-text-color)}.el-rate--large{height:40px}.el-rate--small{height:24px}.el-rate--small .el-rate__icon{font-size:14px}.el-rate.is-disabled .el-rate__item{cursor:not-allowed;color:var(--el-rate-disabled-void-color)}.el-result{--el-result-padding:40px 30px;--el-result-icon-font-size:64px;--el-result-title-font-size:20px;--el-result-title-margin-top:20px;--el-result-subtitle-margin-top:10px;--el-result-extra-margin-top:30px;text-align:center;box-sizing:border-box;padding:var(--el-result-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.el-result__icon svg{width:var(--el-result-icon-font-size);height:var(--el-result-icon-font-size)}.el-result__title{margin-top:var(--el-result-title-margin-top)}.el-result__title p{font-size:var(--el-result-title-font-size);color:var(--el-text-color-primary);margin:0;line-height:1.3}.el-result__subtitle{margin-top:var(--el-result-subtitle-margin-top)}.el-result__subtitle p{font-size:var(--el-font-size-base);color:var(--el-text-color-regular);margin:0;line-height:1.3}.el-result__extra{margin-top:var(--el-result-extra-margin-top)}.el-result .icon-primary{--el-result-color:var(--el-color-primary);color:var(--el-result-color)}.el-result .icon-success{--el-result-color:var(--el-color-success);color:var(--el-result-color)}.el-result .icon-warning{--el-result-color:var(--el-color-warning);color:var(--el-result-color)}.el-result .icon-danger{--el-result-color:var(--el-color-danger);color:var(--el-result-color)}.el-result .icon-error{--el-result-color:var(--el-color-error);color:var(--el-result-color)}.el-result .icon-info{--el-result-color:var(--el-color-info);color:var(--el-result-color)}.el-row{box-sizing:border-box;flex-wrap:wrap;display:flex;position:relative}.el-row.is-justify-center{justify-content:center}.el-row.is-justify-end{justify-content:flex-end}.el-row.is-justify-space-between{justify-content:space-between}.el-row.is-justify-space-around{justify-content:space-around}.el-row.is-justify-space-evenly{justify-content:space-evenly}.el-row.is-align-top{align-items:flex-start}.el-row.is-align-middle{align-items:center}.el-row.is-align-bottom{align-items:flex-end}.el-scrollbar{--el-scrollbar-opacity:.3;--el-scrollbar-bg-color:var(--el-text-color-secondary);--el-scrollbar-hover-opacity:.5;--el-scrollbar-hover-bg-color:var(--el-text-color-secondary);height:100%;position:relative;overflow:hidden}.el-scrollbar__wrap{height:100%;overflow:auto}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{display:none}.el-scrollbar__thumb{cursor:pointer;border-radius:inherit;background-color:var(--el-scrollbar-bg-color,var(--el-text-color-secondary));width:0;height:0;transition:var(--el-transition-duration) background-color;opacity:var(--el-scrollbar-opacity,.3);display:block;position:relative}.el-scrollbar__thumb:hover{background-color:var(--el-scrollbar-hover-bg-color,var(--el-text-color-secondary));opacity:var(--el-scrollbar-hover-opacity,.5)}.el-scrollbar__bar{z-index:1;border-radius:4px;position:absolute;bottom:2px;right:2px}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-scrollbar-fade-enter-active{transition:opacity .34s ease-out}.el-scrollbar-fade-leave-active{transition:opacity .12s ease-out}.el-scrollbar-fade-enter-from,.el-scrollbar-fade-leave-active{opacity:0}.el-select-dropdown{z-index:calc(var(--el-index-top) + 1);border-radius:var(--el-border-radius-base);box-sizing:border-box}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__loading,.el-select-dropdown__empty{text-align:center;color:var(--el-text-color-secondary);font-size:var(--el-select-font-size);margin:0;padding:10px 0}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{box-sizing:border-box;margin:0;padding:6px 0;list-style:none}.el-select-dropdown__list.el-vl__window{margin:6px 0;padding:0}.el-select-dropdown__header{border-bottom:1px solid var(--el-border-color-light);padding:10px}.el-select-dropdown__footer{border-top:1px solid var(--el-border-color-light);padding:10px}.el-select-dropdown__item{font-size:var(--el-font-size-base);white-space:nowrap;text-overflow:ellipsis;color:var(--el-text-color-regular);box-sizing:border-box;cursor:pointer;height:34px;padding:0 32px 0 20px;line-height:34px;position:relative;overflow:hidden}.el-select-dropdown__item.is-hovering{background-color:var(--el-fill-color-light)}.el-select-dropdown__item.is-selected{color:var(--el-color-primary);font-weight:700}.el-select-dropdown__item.is-disabled{color:var(--el-text-color-placeholder);cursor:not-allowed;background-color:unset}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-selected:after{content:"";background-position:50%;background-repeat:no-repeat;background-color:var(--el-color-primary);-webkit-mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") 0 0/100% 100% no-repeat;mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") 0 0/100% 100% no-repeat;border-top:none;border-right:none;width:12px;height:12px;position:absolute;top:50%;right:20px;transform:translateY(-50%);-webkit-mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") 0 0/100% 100% no-repeat}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-disabled:after{background-color:var(--el-text-color-placeholder)}.el-select-group{margin:0;padding:0}.el-select-group__wrap{margin:0;padding:0;list-style:none;position:relative}.el-select-group__title{box-sizing:border-box;color:var(--el-color-info);text-overflow:ellipsis;white-space:nowrap;padding:0 20px;font-size:12px;line-height:34px;overflow:hidden}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{--el-select-border-color-hover:var(--el-border-color-hover);--el-select-disabled-color:var(--el-disabled-text-color);--el-select-disabled-border:var(--el-disabled-border-color);--el-select-font-size:var(--el-font-size-base);--el-select-close-hover-color:var(--el-text-color-secondary);--el-select-input-color:var(--el-text-color-placeholder);--el-select-multiple-input-color:var(--el-text-color-regular);--el-select-input-focus-border-color:var(--el-color-primary);--el-select-input-font-size:14px;--el-select-width:100%;vertical-align:middle;width:var(--el-select-width);display:inline-block;position:relative}.el-select__wrapper{box-sizing:border-box;cursor:pointer;text-align:left;border-radius:var(--el-border-radius-base);background-color:var(--el-fill-color-blank);min-height:32px;transition:var(--el-transition-duration);box-shadow:0 0 0 1px var(--el-border-color) inset;align-items:center;gap:6px;padding:4px 12px;font-size:14px;line-height:24px;display:flex;position:relative;transform:translate(0)}.el-select__wrapper.is-filterable{cursor:text}.el-select__wrapper.is-focused{box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-select__wrapper.is-hovering:not(.is-focused){box-shadow:0 0 0 1px var(--el-border-color-hover) inset}.el-select__wrapper.is-disabled{cursor:not-allowed;background-color:var(--el-fill-color-light);color:var(--el-text-color-placeholder);box-shadow:0 0 0 1px var(--el-select-disabled-border) inset}.el-select__wrapper.is-disabled:hover{box-shadow:0 0 0 1px var(--el-select-disabled-border) inset}.el-select__wrapper.is-disabled.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-select__wrapper.is-disabled .el-select__selected-item{color:var(--el-select-disabled-color)}.el-select__wrapper.is-disabled .el-select__caret,.el-select__wrapper.is-disabled .el-tag,.el-select__wrapper.is-disabled input{cursor:not-allowed}.el-select__wrapper.is-disabled .el-select__prefix,.el-select__wrapper.is-disabled .el-select__suffix{pointer-events:none}.el-select__prefix,.el-select__suffix{color:var(--el-input-icon-color,var(--el-text-color-placeholder));flex-shrink:0;align-items:center;gap:6px;display:flex}.el-select__caret{color:var(--el-select-input-color);font-size:var(--el-select-input-font-size);transition:var(--el-transition-duration);cursor:pointer;transform:rotate(0)}.el-select__caret.is-reverse{transform:rotate(180deg)}.el-select__clear{cursor:pointer}.el-select__clear:hover{color:var(--el-select-close-hover-color)}.el-select__selection{flex-wrap:wrap;flex:1;align-items:center;gap:6px;min-width:0;display:flex;position:relative}.el-select__selection.is-near{margin-left:-8px}.el-select__selection .el-tag{cursor:pointer;border-color:#0000}.el-select__selection .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-select__selection .el-tag .el-tag__content{min-width:0}.el-select__selected-item{-webkit-user-select:none;user-select:none;flex-wrap:wrap;display:flex}.el-select__tags-text{text-overflow:ellipsis;white-space:nowrap;line-height:normal;display:block;overflow:hidden}.el-select__placeholder{z-index:-1;text-overflow:ellipsis;white-space:nowrap;width:100%;color:var(--el-input-text-color,var(--el-text-color-regular));display:block;position:absolute;top:50%;overflow:hidden;transform:translateY(-50%)}.el-select__placeholder.is-transparent{-webkit-user-select:none;user-select:none;color:var(--el-text-color-placeholder)}.el-select__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-box-shadow-light)}.el-select__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-select__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-select__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-select__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-select__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-select__input-wrapper{flex:1}.el-select__input-wrapper.is-hidden{opacity:0;z-index:-1;position:absolute}.el-select__input{color:var(--el-select-multiple-input-color);font-size:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0000;border:none;outline:none;width:100%;height:24px;padding:0;font-family:inherit}.el-select__input-calculator{visibility:hidden;white-space:pre;max-width:100%;position:absolute;top:0;left:0;overflow:hidden}.el-select--large .el-select__wrapper{gap:6px;min-height:40px;padding:8px 16px;font-size:14px;line-height:24px}.el-select--large .el-select__selection{gap:6px}.el-select--large .el-select__selection.is-near{margin-left:-8px}.el-select--large .el-select__prefix,.el-select--large .el-select__suffix{gap:6px}.el-select--large .el-select__input{height:24px}.el-select--small .el-select__wrapper{gap:4px;min-height:24px;padding:2px 8px;font-size:12px;line-height:20px}.el-select--small .el-select__selection{gap:4px}.el-select--small .el-select__selection.is-near{margin-left:-6px}.el-select--small .el-select__prefix,.el-select--small .el-select__suffix{gap:4px}.el-select--small .el-select__input{height:20px}.el-skeleton{--el-skeleton-circle-size:var(--el-avatar-size)}.el-skeleton__item{background:var(--el-skeleton-color);border-radius:var(--el-border-radius-base);width:100%;height:16px;display:inline-block}.el-skeleton__circle{width:var(--el-skeleton-circle-size);height:var(--el-skeleton-circle-size);line-height:var(--el-skeleton-circle-size);border-radius:50%}.el-skeleton__button{border-radius:4px;width:64px;height:40px}.el-skeleton__p{width:100%}.el-skeleton__p.is-last{width:61%}.el-skeleton__p.is-first{width:33%}.el-skeleton__text{width:100%;height:var(--el-font-size-small)}.el-skeleton__caption{height:var(--el-font-size-extra-small)}.el-skeleton__h1{height:var(--el-font-size-extra-large)}.el-skeleton__h3{height:var(--el-font-size-large)}.el-skeleton__h5{height:var(--el-font-size-medium)}.el-skeleton__image{width:unset;border-radius:0;justify-content:center;align-items:center;display:flex}.el-skeleton__image svg{color:var(--el-svg-monochrome-grey);fill:currentColor;width:22%;height:22%}.el-skeleton{--el-skeleton-color:var(--el-fill-color);--el-skeleton-to-color:var(--el-fill-color-darker)}@keyframes el-skeleton-loading{0%{background-position:100%}to{background-position:0}}.el-skeleton{width:100%}.el-skeleton__first-line,.el-skeleton__paragraph{background:var(--el-skeleton-color);height:16px;margin-top:16px}.el-skeleton.is-animated .el-skeleton__item{background:linear-gradient(90deg,var(--el-skeleton-color) 25%,var(--el-skeleton-to-color) 37%,var(--el-skeleton-color) 63%);background-size:400% 100%;animation:1.4s infinite el-skeleton-loading}.el-slider{--el-slider-main-bg-color:var(--el-color-primary);--el-slider-runway-bg-color:var(--el-border-color-light);--el-slider-stop-bg-color:var(--el-color-white);--el-slider-disabled-color:var(--el-text-color-placeholder);--el-slider-border-radius:3px;--el-slider-height:6px;--el-slider-button-size:20px;--el-slider-button-wrapper-size:36px;--el-slider-button-wrapper-offset:-15px;align-items:center;width:100%;height:32px;display:flex}.el-slider__runway{height:var(--el-slider-height);background-color:var(--el-slider-runway-bg-color);border-radius:var(--el-slider-border-radius);cursor:pointer;flex:1;position:relative}.el-slider__runway.show-input{width:auto;margin-right:30px}.el-slider__runway.is-disabled{cursor:default}.el-slider__runway.is-disabled .el-slider__bar{background-color:var(--el-slider-disabled-color)}.el-slider__runway.is-disabled .el-slider__button{border-color:var(--el-slider-disabled-color)}.el-slider__runway.is-disabled .el-slider__button-wrapper:hover,.el-slider__runway.is-disabled .el-slider__button-wrapper.hover,.el-slider__runway.is-disabled .el-slider__button-wrapper.dragging{cursor:not-allowed}.el-slider__runway.is-disabled .el-slider__button:hover,.el-slider__runway.is-disabled .el-slider__button.hover,.el-slider__runway.is-disabled .el-slider__button.dragging{cursor:not-allowed;transform:scale(1)}.el-slider__input{flex-shrink:0;width:130px}.el-slider__bar{height:var(--el-slider-height);background-color:var(--el-slider-main-bg-color);border-top-left-radius:var(--el-slider-border-radius);border-bottom-left-radius:var(--el-slider-border-radius);position:absolute}.el-slider__button-wrapper{height:var(--el-slider-button-wrapper-size);width:var(--el-slider-button-wrapper-size);z-index:1;top:var(--el-slider-button-wrapper-offset);text-align:center;-webkit-user-select:none;user-select:none;background-color:#0000;outline:none;line-height:normal;position:absolute;transform:translate(-50%)}.el-slider__button-wrapper:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-slider__button-wrapper:hover,.el-slider__button-wrapper.hover{cursor:grab}.el-slider__button-wrapper.dragging{cursor:grabbing}.el-slider__button{width:var(--el-slider-button-size);height:var(--el-slider-button-size);vertical-align:middle;border:solid 2px var(--el-slider-main-bg-color);background-color:var(--el-color-white);box-sizing:border-box;transition:var(--el-transition-duration-fast);-webkit-user-select:none;user-select:none;border-radius:50%;display:inline-block}.el-slider__button:hover,.el-slider__button.hover,.el-slider__button.dragging{transform:scale(1.2)}.el-slider__button:hover,.el-slider__button.hover{cursor:grab}.el-slider__button.dragging{cursor:grabbing}.el-slider__stop{height:var(--el-slider-height);width:var(--el-slider-height);border-radius:var(--el-border-radius-circle);background-color:var(--el-slider-stop-bg-color);position:absolute;transform:translate(-50%)}.el-slider__marks{width:18px;height:100%;top:0;left:12px}.el-slider__marks-text{color:var(--el-color-info);white-space:pre;margin-top:15px;font-size:14px;position:absolute;transform:translate(-50%)}.el-slider.is-vertical{flex:0;width:auto;height:100%;display:inline-flex;position:relative}.el-slider.is-vertical .el-slider__runway{width:var(--el-slider-height);height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:var(--el-slider-height);border-radius:0 0 3px 3px;height:auto}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:var(--el-slider-button-wrapper-offset);transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{transform:translateY(50%)}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;transform:translateY(50%)}.el-slider--large{height:40px}.el-slider--small{height:24px}.el-space{vertical-align:top;display:inline-flex}.el-space__item{flex-wrap:wrap;display:flex}.el-space__item>*{flex:1}.el-space--vertical{flex-direction:column}.el-time-spinner{white-space:nowrap;width:100%}.el-spinner{vertical-align:middle;display:inline-block}.el-spinner-inner{width:50px;height:50px;animation:2s linear infinite rotate}.el-spinner-inner .path{stroke:var(--el-border-color-lighter);stroke-linecap:round;animation:1.5s ease-in-out infinite dash}@keyframes rotate{to{transform:rotate(360deg)}}@keyframes dash{0%{stroke-dasharray:1 150;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-35px}to{stroke-dasharray:90 150;stroke-dashoffset:-124px}}.el-step{flex-shrink:1;position:relative}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{flex-grow:0;flex-shrink:0;flex-basis:auto!important}.el-step:last-of-type .el-step__main,.el-step:last-of-type .el-step__description{padding-right:0}.el-step__head{width:100%;position:relative}.el-step__head.is-process{color:var(--el-text-color-primary);border-color:var(--el-text-color-primary)}.el-step__head.is-wait{color:var(--el-text-color-placeholder);border-color:var(--el-text-color-placeholder)}.el-step__head.is-success{color:var(--el-color-success);border-color:var(--el-color-success)}.el-step__head.is-error{color:var(--el-color-danger);border-color:var(--el-color-danger)}.el-step__head.is-finish{color:var(--el-color-primary);border-color:var(--el-color-primary)}.el-step__icon{z-index:1;box-sizing:border-box;background:var(--el-bg-color);justify-content:center;align-items:center;width:24px;height:24px;font-size:14px;transition:all .15s ease-out;display:inline-flex;position:relative}.el-step__icon.is-text{border:2px solid;border-radius:50%}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{-webkit-user-select:none;user-select:none;text-align:center;color:inherit;font-weight:700;line-height:1;display:inline-block}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{transform:translateY(1px)}.el-step__line{background-color:var(--el-text-color-placeholder);border-color:currentColor;position:absolute}.el-step__line-inner{box-sizing:border-box;border:1px solid;width:0;height:0;transition:all .15s ease-out;display:block}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{color:var(--el-text-color-primary);font-weight:700}.el-step__title.is-wait{color:var(--el-text-color-placeholder)}.el-step__title.is-success{color:var(--el-color-success)}.el-step__title.is-error{color:var(--el-color-danger)}.el-step__title.is-finish{color:var(--el-color-primary)}.el-step__description{margin-top:-5px;padding-right:10%;font-size:12px;font-weight:400;line-height:20px}.el-step__description.is-process{color:var(--el-text-color-primary)}.el-step__description.is-wait{color:var(--el-text-color-placeholder)}.el-step__description.is-success{color:var(--el-color-success)}.el-step__description.is-error{color:var(--el-color-danger)}.el-step__description.is-finish{color:var(--el-color-primary)}.el-step.is-horizontal{display:inline-block}.el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.el-step.is-vertical{display:flex}.el-step.is-vertical .el-step__head{flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{flex-grow:1;padding-left:10px}.el-step.is-vertical .el-step__title{padding-bottom:8px;line-height:24px}.el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.el-step.is-vertical .el-step__icon.is-icon{width:24px}.el-step.is-vertical .el-step__description{padding-right:0}.el-step.is-center .el-step__head,.el-step.is-center .el-step__main{text-align:center}.el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.el-step.is-center .el-step__line{left:50%;right:-50%}.el-step.is-simple{align-items:center;display:flex}.el-step.is-simple .el-step__head{width:auto;padding-right:10px;font-size:0}.el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.el-step.is-simple .el-step__icon-inner.is-status{transform:scale(.8)translateY(1px)}.el-step.is-simple .el-step__main{flex-grow:1;align-items:stretch;display:flex;position:relative}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{overflow-wrap:break-word;max-width:50%}.el-step.is-simple .el-step__arrow{flex-grow:1;justify-content:center;align-items:center;display:flex}.el-step.is-simple .el-step__arrow:before,.el-step.is-simple .el-step__arrow:after{content:"";background:var(--el-text-color-placeholder);width:1px;height:15px;display:inline-block;position:absolute}.el-step.is-simple .el-step__arrow:before{transform-origin:0 0;transform:rotate(-45deg)translateY(-4px)}.el-step.is-simple .el-step__arrow:after{transform-origin:100% 100%;transform:rotate(45deg)translateY(4px)}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-steps{line-height:normal;display:flex}.el-steps--simple{background:var(--el-fill-color-light);border-radius:4px;padding:13px 8%}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{flex-flow:column;height:100%}.el-switch{--el-switch-on-color:var(--el-color-primary);--el-switch-off-color:var(--el-border-color);vertical-align:middle;align-items:center;height:32px;font-size:14px;line-height:20px;display:inline-flex;position:relative}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{transition:var(--el-transition-duration-fast);cursor:pointer;vertical-align:middle;height:20px;color:var(--el-text-color-primary);font-size:14px;font-weight:500;display:inline-block}.el-switch__label.is-active{color:var(--el-color-primary)}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{font-size:14px;line-height:1;display:inline-block}.el-switch__label .el-icon{height:inherit}.el-switch__label .el-icon svg{vertical-align:middle}.el-switch__input{opacity:0;width:0;height:0;margin:0;position:absolute}.el-switch__input:focus-visible~.el-switch__core{outline:2px solid var(--el-switch-on-color);outline-offset:1px}.el-switch__core{border:1px solid var(--el-switch-border-color,var(--el-switch-off-color));box-sizing:border-box;background:var(--el-switch-off-color);cursor:pointer;min-width:40px;height:20px;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration);border-radius:10px;outline:none;align-items:center;display:inline-flex;position:relative}.el-switch__core .el-switch__inner{width:100%;transition:all var(--el-transition-duration);justify-content:center;align-items:center;height:16px;padding:0 4px 0 18px;display:flex;overflow:hidden}.el-switch__core .el-switch__inner-wrapper{color:var(--el-color-white);-webkit-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;align-items:center;font-size:12px;display:flex;overflow:hidden}.el-switch__core .el-switch__action{border-radius:var(--el-border-radius-circle);transition:all var(--el-transition-duration);background-color:var(--el-color-white);width:16px;height:16px;color:var(--el-switch-off-color);justify-content:center;align-items:center;display:flex;position:absolute;left:1px}.el-switch.is-checked .el-switch__core{border-color:var(--el-switch-border-color,var(--el-switch-on-color));background-color:var(--el-switch-on-color)}.el-switch.is-checked .el-switch__core .el-switch__action{color:var(--el-switch-on-color);left:calc(100% - 17px)}.el-switch.is-checked .el-switch__core .el-switch__inner{padding:0 18px 0 4px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter-from,.el-switch .label-fade-leave-active{opacity:0}.el-switch--large{height:40px;font-size:14px;line-height:24px}.el-switch--large .el-switch__label{height:24px;font-size:14px}.el-switch--large .el-switch__label *{font-size:14px}.el-switch--large .el-switch__core{border-radius:12px;min-width:50px;height:24px}.el-switch--large .el-switch__core .el-switch__inner{height:20px;padding:0 6px 0 22px}.el-switch--large .el-switch__core .el-switch__action{width:20px;height:20px}.el-switch--large.is-checked .el-switch__core .el-switch__action{left:calc(100% - 21px)}.el-switch--large.is-checked .el-switch__core .el-switch__inner{padding:0 22px 0 6px}.el-switch--small{height:24px;font-size:12px;line-height:16px}.el-switch--small .el-switch__label{height:16px;font-size:12px}.el-switch--small .el-switch__label *{font-size:12px}.el-switch--small .el-switch__core{border-radius:8px;min-width:30px;height:16px}.el-switch--small .el-switch__core .el-switch__inner{height:12px;padding:0 2px 0 14px}.el-switch--small .el-switch__core .el-switch__action{width:12px;height:12px}.el-switch--small.is-checked .el-switch__core .el-switch__action{left:calc(100% - 13px)}.el-switch--small.is-checked .el-switch__core .el-switch__inner{padding:0 14px 0 2px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:solid 1px var(--el-border-color-lighter);box-shadow:var(--el-box-shadow-light);box-sizing:border-box;background-color:#fff;border-radius:2px}.el-table-filter__list{outline:none;min-width:100px;margin:0;padding:5px 0;list-style:none}.el-table-filter__list-item{cursor:pointer;line-height:36px;font-size:var(--el-font-size-base);outline:none;padding:0 10px}.el-table-filter__list-item:hover,.el-table-filter__list-item:focus{background-color:var(--el-color-primary-light-9);color:var(--el-color-primary)}.el-table-filter__list-item.is-active{background-color:var(--el-color-primary);color:#fff}.el-table-filter__multiple{outline:none}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid var(--el-border-color-lighter);padding:8px}.el-table-filter__bottom button{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table-filter__bottom button:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table-filter__bottom button{color:var(--el-text-color-regular);font-size:var(--el-font-size-small);padding:0 3px}.el-table-filter__bottom button:hover{color:var(--el-color-primary)}.el-table-filter__bottom button.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{height:unset;align-items:center;margin-bottom:12px;margin-left:5px;margin-right:5px;display:flex}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-table{--el-table-border-color:var(--el-border-color-lighter);--el-table-border:1px solid var(--el-table-border-color);--el-table-text-color:var(--el-text-color-regular);--el-table-header-text-color:var(--el-text-color-secondary);--el-table-row-hover-bg-color:var(--el-fill-color-light);--el-table-current-row-bg-color:var(--el-color-primary-light-9);--el-table-header-bg-color:var(--el-fill-color-blank);--el-table-fixed-box-shadow:var(--el-box-shadow-light);--el-table-bg-color:var(--el-fill-color-blank);--el-table-tr-bg-color:var(--el-fill-color-blank);--el-table-expanded-cell-bg-color:var(--el-fill-color-blank);--el-table-fixed-left-column:inset 10px 0 10px -10px #00000026;--el-table-fixed-right-column:inset -10px 0 10px -10px #00000026;--el-table-index:var(--el-index-normal);box-sizing:border-box;background-color:var(--el-table-bg-color);width:100%;max-width:100%;height:-moz-fit-content;height:fit-content;font-size:var(--el-font-size-base);color:var(--el-table-text-color);position:relative;overflow:hidden}.el-table__inner-wrapper{flex-direction:column;height:100%;display:flex;position:relative}.el-table__inner-wrapper:before{height:1px;bottom:0;left:0}.el-table tbody:focus-visible{outline:none}.el-table.has-footer.el-table--scrollable-y tr:last-child td.el-table__cell,.el-table.has-footer.el-table--fluid-height tr:last-child td.el-table__cell{border-bottom-color:#0000}.el-table__empty-block{text-align:center;justify-content:center;align-items:center;width:100%;min-height:60px;display:flex;position:sticky;left:0}.el-table__empty-text{width:50%;color:var(--el-text-color-secondary);line-height:60px}.el-table__expand-column .cell{text-align:center;-webkit-user-select:none;user-select:none;padding:0}.el-table__expand-icon{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table__expand-icon:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:-2px}.el-table__expand-icon{color:var(--el-text-color-regular);width:min(23px,100%);height:23px;font-size:12px;line-height:12px}.el-table__expand-icon.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-table__expand-icon--expanded{transform:rotate(90deg)}.el-table__expand-icon>.el-icon{font-size:12px}.el-table__expanded-cell{background-color:var(--el-table-expanded-cell-bg-color)}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:#0000!important}.el-table__placeholder{width:20px;display:inline-block}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-bottom:0;border-right:0}.el-table--fit .el-table__cell.gutter{border-right-width:1px}.el-table--fit .el-table__inner-wrapper:before{width:100%}.el-table thead{color:var(--el-table-header-text-color)}.el-table thead th{font-weight:600}.el-table thead.is-group th.el-table__cell{background:var(--el-fill-color-light)}.el-table .el-table__cell{box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;text-align:left;min-width:0;z-index:var(--el-table-index);padding:8px 0;position:relative}.el-table .el-table__cell.is-center{text-align:center}.el-table .el-table__cell.is-right{text-align:right}.el-table .el-table__cell.gutter{border-bottom-width:0;border-right-width:0;width:15px;padding:0}.el-table .el-table__cell.is-hidden>*{visibility:hidden}.el-table .cell{box-sizing:border-box;text-overflow:ellipsis;white-space:normal;overflow-wrap:break-word;padding:0 12px;line-height:23px;overflow:hidden}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--large{font-size:var(--el-font-size-base)}.el-table--large .el-table__cell{padding:12px 0}.el-table--large .cell{padding:0 16px}.el-table--default{font-size:var(--el-font-size-base)}.el-table--default .el-table__cell{padding:8px 0}.el-table--default .cell{padding:0 12px}.el-table--small{font-size:var(--el-font-size-extra-small)}.el-table--small .el-table__cell{padding:4px 0}.el-table--small .cell{padding:0 8px}.el-table tr{background-color:var(--el-table-tr-bg-color)}.el-table tr input[type=checkbox]{margin:0}.el-table th.el-table__cell.is-leaf,.el-table td.el-table__cell{border-bottom:var(--el-table-border)}.el-table th.el-table__cell.is-sortable{cursor:pointer}.el-table th.el-table__cell{background-color:var(--el-table-header-bg-color)}.el-table th.el-table__cell>.cell.highlight{color:var(--el-color-primary)}.el-table th.el-table__cell.required>div:before{content:"";vertical-align:middle;background:#ff4d51;border-radius:50%;width:8px;height:8px;margin-right:5px;display:inline-block}.el-table td.el-table__cell div{box-sizing:border-box}.el-table td.el-table__cell.gutter{width:0}.el-table--border:after,.el-table--border:before,.el-table--border .el-table__inner-wrapper:after,.el-table__inner-wrapper:before{content:"";background-color:var(--el-table-border-color);z-index:calc(var(--el-table-index) + 2);position:absolute}.el-table--border .el-table__inner-wrapper:after{width:100%;height:1px;z-index:calc(var(--el-table-index) + 2);top:0;left:0}.el-table--border:before{width:1px;height:100%;top:-1px;left:0}.el-table--border:after{width:1px;height:100%;top:-1px;right:0}.el-table--border .el-table__inner-wrapper{border-bottom:none;border-right:none}.el-table--border .el-table__footer-wrapper{flex-shrink:0;position:relative}.el-table--border .el-table__cell{border-right:var(--el-table-border)}.el-table--border th.el-table__cell.gutter:last-of-type{border-bottom:var(--el-table-border);border-bottom-width:1px}.el-table--border th.el-table__cell{border-bottom:var(--el-table-border)}.el-table--hidden{visibility:hidden}.el-table__header-wrapper,.el-table__body-wrapper,.el-table__footer-wrapper{width:100%}.el-table__header-wrapper tr td.el-table-fixed-column--left,.el-table__header-wrapper tr td.el-table-fixed-column--right,.el-table__header-wrapper tr th.el-table-fixed-column--left,.el-table__header-wrapper tr th.el-table-fixed-column--right,.el-table__body-wrapper tr td.el-table-fixed-column--left,.el-table__body-wrapper tr td.el-table-fixed-column--right,.el-table__body-wrapper tr th.el-table-fixed-column--left,.el-table__body-wrapper tr th.el-table-fixed-column--right,.el-table__footer-wrapper tr td.el-table-fixed-column--left,.el-table__footer-wrapper tr td.el-table-fixed-column--right,.el-table__footer-wrapper tr th.el-table-fixed-column--left,.el-table__footer-wrapper tr th.el-table-fixed-column--right{background:inherit;z-index:calc(var(--el-table-index) + 1);position:sticky!important}.el-table__header-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-first-column:before{content:"";width:10px;box-shadow:none;touch-action:none;pointer-events:none;position:absolute;top:0;bottom:0;overflow:hidden}.el-table__header-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-first-column:before{left:-10px}.el-table__header-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-last-column:before{right:-10px}.el-table__header-wrapper tr td.el-table__fixed-right-patch,.el-table__header-wrapper tr th.el-table__fixed-right-patch,.el-table__body-wrapper tr td.el-table__fixed-right-patch,.el-table__body-wrapper tr th.el-table__fixed-right-patch,.el-table__footer-wrapper tr td.el-table__fixed-right-patch,.el-table__footer-wrapper tr th.el-table__fixed-right-patch{z-index:calc(var(--el-table-index) + 1);background:#fff;right:0;position:sticky!important}.el-table__header-wrapper{flex-shrink:0}.el-table__header-wrapper tr th.el-table-fixed-column--left,.el-table__header-wrapper tr th.el-table-fixed-column--right{background-color:var(--el-table-header-bg-color)}.el-table__header,.el-table__body,.el-table__footer{table-layout:fixed;border-collapse:separate}.el-table__header-wrapper{overflow:hidden}.el-table__header-wrapper tbody td.el-table__cell{background-color:var(--el-table-row-hover-bg-color);color:var(--el-table-text-color)}.el-table__footer-wrapper{flex-shrink:0;overflow:hidden}.el-table__footer-wrapper tfoot td.el-table__cell{background-color:var(--el-table-row-hover-bg-color);color:var(--el-table-text-color)}.el-table__header-wrapper .el-table-column--selection>.cell,.el-table__body-wrapper .el-table-column--selection>.cell{align-items:center;height:23px;display:inline-flex}.el-table__header-wrapper .el-table-column--selection .el-checkbox,.el-table__body-wrapper .el-table-column--selection .el-checkbox{height:unset}.el-table.is-scrolling-left .el-table-fixed-column--right.is-first-column:before{box-shadow:var(--el-table-fixed-right-column)}.el-table.is-scrolling-left.el-table--border .el-table-fixed-column--left.is-last-column.el-table__cell{border-right:var(--el-table-border)}.el-table.is-scrolling-left th.el-table-fixed-column--left{background-color:var(--el-table-header-bg-color)}.el-table.is-scrolling-right .el-table-fixed-column--left.is-last-column:before{box-shadow:var(--el-table-fixed-left-column)}.el-table.is-scrolling-right .el-table-fixed-column--left.is-last-column.el-table__cell{border-right:none}.el-table.is-scrolling-right th.el-table-fixed-column--right{background-color:var(--el-table-header-bg-color)}.el-table.is-scrolling-middle .el-table-fixed-column--left.is-last-column.el-table__cell{border-right:none}.el-table.is-scrolling-middle .el-table-fixed-column--right.is-first-column:before{box-shadow:var(--el-table-fixed-right-column)}.el-table.is-scrolling-middle .el-table-fixed-column--left.is-last-column:before{box-shadow:var(--el-table-fixed-left-column)}.el-table.is-scrolling-none .el-table-fixed-column--left.is-first-column:before,.el-table.is-scrolling-none .el-table-fixed-column--left.is-last-column:before,.el-table.is-scrolling-none .el-table-fixed-column--right.is-first-column:before,.el-table.is-scrolling-none .el-table-fixed-column--right.is-last-column:before{box-shadow:none}.el-table.is-scrolling-none th.el-table-fixed-column--left,.el-table.is-scrolling-none th.el-table-fixed-column--right{background-color:var(--el-table-header-bg-color)}.el-table__body-wrapper{flex:1;position:relative;overflow:hidden}.el-table__body-wrapper .el-scrollbar__bar{z-index:calc(var(--el-table-index) + 2)}.el-table .caret-wrapper{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table .caret-wrapper:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table .caret-wrapper{vertical-align:middle;width:24px;height:14px;overflow:initial;flex-direction:column;align-items:center;display:inline-flex;position:relative}.el-table .sort-caret{border:5px solid #0000;width:0;height:0;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:var(--el-text-color-placeholder);top:-5px}.el-table .sort-caret.descending{border-top-color:var(--el-text-color-placeholder);bottom:-3px}.el-table .ascending .sort-caret.ascending{border-bottom-color:var(--el-color-primary)}.el-table .descending .sort-caret.descending{border-top-color:var(--el-color-primary)}.el-table .hidden-columns{visibility:hidden;z-index:-1;position:absolute}.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell{background:var(--el-fill-color-lighter)}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td.el-table__cell{background-color:var(--el-table-current-row-bg-color)}.el-table__body tr.hover-row>td.el-table__cell,.el-table__body tr.hover-row.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.current-row>td.el-table__cell,.el-table__body tr>td.hover-cell{background-color:var(--el-table-row-hover-bg-color)}.el-table__body tr.current-row>td.el-table__cell{background-color:var(--el-table-current-row-bg-color)}.el-table.el-table--scrollable-y .el-table__body-header{z-index:calc(var(--el-table-index) + 2);position:sticky;top:0}.el-table.el-table--scrollable-y .el-table__body-footer{z-index:calc(var(--el-table-index) + 2);position:sticky;bottom:0}.el-table__column-resize-proxy{border-left:var(--el-table-border);width:0;z-index:calc(var(--el-table-index) + 9);position:absolute;top:0;bottom:0;left:200px}.el-table__column-filter-trigger{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table__column-filter-trigger:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table__column-filter-trigger{display:inline-block}.el-table__column-filter-trigger i{color:var(--el-color-info);vertical-align:middle;font-size:14px}.el-table__border-left-patch{width:1px;height:100%;z-index:calc(var(--el-table-index) + 2);background-color:var(--el-table-border-color);position:absolute;top:0;left:0}.el-table__border-bottom-patch{height:1px;z-index:calc(var(--el-table-index) + 2);background-color:var(--el-table-border-color);position:absolute;left:0}.el-table__border-right-patch{width:1px;height:100%;z-index:calc(var(--el-table-index) + 2);background-color:var(--el-table-border-color);position:absolute;top:0}.el-table--enable-row-transition .el-table__body td.el-table__cell{transition:background-color .25s 1ms}.el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell{background-color:var(--el-table-row-hover-bg-color)}.el-table [class*=el-table__row--level] .el-table__expand-icon{text-align:center;width:20px;display:inline-block}.el-table .el-table.el-table--border .el-table__cell{border-right:var(--el-table-border)}.el-table:not(.el-table--border) .el-table__cell{border-right:none}.el-table:not(.el-table--border)>.el-table__inner-wrapper:after{content:none}.el-table-v2{--el-table-border-color:var(--el-border-color-lighter);--el-table-border:1px solid var(--el-table-border-color);--el-table-text-color:var(--el-text-color-regular);--el-table-header-text-color:var(--el-text-color-secondary);--el-table-row-hover-bg-color:var(--el-fill-color-light);--el-table-current-row-bg-color:var(--el-color-primary-light-9);--el-table-header-bg-color:var(--el-fill-color-blank);--el-table-fixed-box-shadow:var(--el-box-shadow-light);--el-table-bg-color:var(--el-fill-color-blank);--el-table-tr-bg-color:var(--el-fill-color-blank);--el-table-expanded-cell-bg-color:var(--el-fill-color-blank);--el-table-fixed-left-column:inset 10px 0 10px -10px #00000026;--el-table-fixed-right-column:inset -10px 0 10px -10px #00000026;--el-table-index:var(--el-index-normal);font-size:var(--el-font-size-base)}.el-table-v2 *{box-sizing:border-box}.el-table-v2__root{position:relative}.el-table-v2__root:hover .el-table-v2__main .el-virtual-scrollbar{opacity:1}.el-table-v2__main{background-color:var(--el-bg-color);flex-direction:column-reverse;display:flex;position:absolute;top:0;left:0;overflow:hidden}.el-table-v2__main .el-vl__horizontal,.el-table-v2__main .el-vl__vertical{z-index:2}.el-table-v2__left{background-color:var(--el-bg-color);flex-direction:column-reverse;display:flex;position:absolute;top:0;left:0;overflow:hidden;box-shadow:2px 0 4px #0000000f}.el-table-v2__left .el-virtual-scrollbar{opacity:0}.el-table-v2__left .el-vl__vertical,.el-table-v2__left .el-vl__horizontal{z-index:-1}.el-table-v2__right{background-color:var(--el-bg-color);flex-direction:column-reverse;display:flex;position:absolute;top:0;right:0;overflow:hidden;box-shadow:-2px 0 4px #0000000f}.el-table-v2__right .el-virtual-scrollbar{opacity:0}.el-table-v2__right .el-vl__vertical,.el-table-v2__right .el-vl__horizontal{z-index:-1}.el-table-v2__header-row,.el-table-v2__row{padding-inline-end:var(--el-table-scrollbar-size)}.el-table-v2__header-wrapper{overflow:hidden}.el-table-v2__header{position:relative;overflow:hidden}.el-table-v2__header .el-checkbox{z-index:0}.el-table-v2__footer{position:absolute;bottom:0;left:0;right:0;overflow:hidden}.el-table-v2__empty{position:absolute;left:0}.el-table-v2__overlay{z-index:9999;position:absolute;top:0;bottom:0;left:0;right:0}.el-table-v2__header-row{border-bottom:var(--el-table-border);display:flex}.el-table-v2__header-cell{-webkit-user-select:none;user-select:none;background-color:var(--el-table-header-bg-color);height:100%;color:var(--el-table-header-text-color);align-items:center;padding:0 8px;font-weight:700;display:flex;overflow:hidden}.el-table-v2__header-cell.is-align-center{text-align:center;justify-content:center}.el-table-v2__header-cell.is-align-right{text-align:right;justify-content:flex-end}.el-table-v2__header-cell.is-sortable{cursor:pointer}.el-table-v2__header-cell:hover .el-icon{display:block}.el-table-v2__sort-icon{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table-v2__sort-icon:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table-v2__sort-icon{transition:opacity,display var(--el-transition-duration);opacity:.6;display:none}.el-table-v2__sort-icon.is-sorting{opacity:1;display:flex}.el-table-v2__row{border-bottom:var(--el-table-border);transition:background-color var(--el-transition-duration);align-items:center;display:flex}.el-table-v2__row.is-hovered,.el-table-v2__row:hover{background-color:var(--el-table-row-hover-bg-color)}.el-table-v2__row-cell{align-items:center;height:100%;padding:0 8px;display:flex;overflow:hidden}.el-table-v2__row-cell.is-align-center{text-align:center;justify-content:center}.el-table-v2__row-cell.is-align-right{text-align:right;justify-content:flex-end}.el-table-v2__expand-icon{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table-v2__expand-icon:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table-v2__expand-icon{-webkit-user-select:none;user-select:none;margin:0 4px}.el-table-v2__expand-icon svg{transition:transform var(--el-transition-duration)}.el-table-v2__expand-icon.is-expanded svg{transform:rotate(90deg)}.el-table-v2:not(.is-dynamic) .el-table-v2__cell-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.el-table-v2.is-dynamic .el-table-v2__row{align-items:stretch;overflow:hidden}.el-table-v2.is-dynamic .el-table-v2__row .el-table-v2__row-cell{overflow-wrap:break-word}.el-tabs{--el-tabs-header-height:40px;display:flex}.el-tabs__header{justify-content:space-between;align-items:center;margin:0 0 15px;padding:0;display:flex;position:relative}.el-tabs__header-vertical{flex-direction:column}.el-tabs__active-bar{background-color:var(--el-color-primary);z-index:1;height:2px;transition:width var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier),transform var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);list-style:none;position:absolute;bottom:0;left:0}.el-tabs__active-bar.is-bottom{bottom:auto}.el-tabs__new-tab{border:1px solid var(--el-border-color);text-align:center;width:20px;height:20px;color:var(--el-text-color-primary);cursor:pointer;border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;margin:10px 0 10px 10px;font-size:12px;line-height:20px;transition:all .15s;display:flex}.el-tabs__new-tab .is-icon-plus{height:inherit;width:inherit;transform:scale(.8)}.el-tabs__new-tab .is-icon-plus svg{vertical-align:middle}.el-tabs__new-tab:hover{color:var(--el-color-primary)}.el-tabs__new-tab-vertical{margin-left:0}.el-tabs__nav-wrap{flex:auto;margin-bottom:-1px;position:relative;overflow:hidden}.el-tabs__nav-wrap:after{content:"";background-color:var(--el-border-color-light);width:100%;height:2px;z-index:var(--el-index-normal);position:absolute;bottom:0;left:0}.el-tabs__nav-wrap.is-bottom:after{top:0;bottom:auto}.el-tabs__nav-wrap.is-scrollable{box-sizing:border-box;padding:0 20px}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{cursor:pointer;color:var(--el-text-color-secondary);text-align:center;width:20px;font-size:12px;line-height:44px;position:absolute}.el-tabs__nav-next.is-disabled,.el-tabs__nav-prev.is-disabled{color:var(--el-text-color-disabled);cursor:not-allowed}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;transition:transform var(--el-transition-duration);float:left;z-index:calc(var(--el-index-normal) + 1);display:flex;position:relative}.el-tabs__nav.is-stretch{min-width:100%;display:flex}.el-tabs__nav.is-stretch>*{text-align:center;flex:1}.el-tabs__item{height:var(--el-tabs-header-height);box-sizing:border-box;font-size:var(--el-font-size-base);color:var(--el-text-color-primary);justify-content:center;align-items:center;padding:0 20px;font-weight:500;list-style:none;display:flex;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:none}.el-tabs__item:focus-visible{box-shadow:0 0 2px 2px var(--el-color-primary) inset;border-radius:3px}.el-tabs__item .is-icon-close{text-align:center;transition:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);border-radius:50%;margin-left:5px}.el-tabs__item .is-icon-close:before{display:inline-block;transform:scale(.9)}.el-tabs__item .is-icon-close:hover{background-color:var(--el-text-color-placeholder);color:#fff}.el-tabs__item.is-active{color:var(--el-color-primary)}.el-tabs__item:hover{color:var(--el-color-primary);cursor:pointer}.el-tabs__item.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-tabs__content{flex-grow:1;position:relative;overflow:hidden}.el-tabs--top>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:0}.el-tabs--top>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom>.el-tabs__header .el-tabs__item:last-child{padding-right:0}.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--card>.el-tabs__header{border-bottom:1px solid var(--el-border-color-light);height:var(--el-tabs-header-height);box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid var(--el-border-color-light);box-sizing:border-box;border-bottom:none;border-radius:4px 4px 0 0}.el-tabs--card>.el-tabs__header .el-tabs__active-bar{display:none}.el-tabs--card>.el-tabs__header .el-tabs__item .is-icon-close{transform-origin:100%;width:0;height:14px;font-size:12px;position:relative;right:-2px;overflow:hidden}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid #0000;border-left:1px solid var(--el-border-color-light);transition:color var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier),padding var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);margin-top:-1px}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .is-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:var(--el-bg-color)}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .is-icon-close{width:14px}.el-tabs--border-card{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:var(--el-fill-color-light);border-bottom:1px solid var(--el-border-color-light);margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs--border-card>.el-tabs__header .el-tabs__item{transition:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);color:var(--el-text-color-secondary);border:1px solid #0000;margin-top:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child,.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item{margin-left:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:var(--el-color-primary);background-color:var(--el-bg-color-overlay);border-right-color:var(--el-border-color);border-left-color:var(--el-border-color)}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:var(--el-color-primary)}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:var(--el-disabled-text-color)}.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--bottom{flex-direction:column}.el-tabs--bottom .el-tabs__header.is-bottom{margin-top:10px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid var(--el-border-color)}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-top:-1px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid #0000}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-scroll{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{width:2px;height:auto;top:0;bottom:auto}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{text-align:center;cursor:pointer;width:100%;height:30px;line-height:30px}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i{transform:rotate(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev.is-disabled,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next.is-disabled,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev.is-disabled,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next.is-disabled{cursor:not-allowed}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{top:0;left:auto}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{bottom:0;right:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left:after,.el-tabs--left .el-tabs__nav-wrap.is-right:after,.el-tabs--right .el-tabs__nav-wrap.is-left:after,.el-tabs--right .el-tabs__nav-wrap.is-right:after{width:2px;height:100%;top:0;bottom:auto}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{flex-direction:column}.el-tabs--left .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-left{justify-content:flex-end}.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-right{justify-content:flex-start}.el-tabs--left{flex-direction:row}.el-tabs--left .el-tabs__header.is-left{margin-bottom:0;margin-right:10px}.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__nav-wrap.is-left:after,.el-tabs--left .el-tabs__active-bar.is-left{left:auto;right:0}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left{display:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid var(--el-border-color-light);border-bottom:none;border-top:1px solid var(--el-border-color-light);text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid var(--el-border-color-light);border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid var(--el-border-color-light);border-bottom:none;border-left:none;border-right-color:#fff}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-bottom:1px solid var(--el-border-color-light);border-right:none;border-radius:4px 0 0 4px}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid var(--el-border-color)}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid #0000;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 #0000}.el-tabs--left>.el-tabs__content+.el-tabs__header{order:-1}.el-tabs--right .el-tabs__header.is-right{margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right:after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid var(--el-border-color-light)}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid var(--el-border-color-light);border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid var(--el-border-color-light);border-bottom:none;border-left-color:#fff;border-right:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-bottom:1px solid var(--el-border-color-light);border-left:none;border-radius:0 4px 4px 0}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid var(--el-border-color)}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid #0000;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 #0000}.el-tabs--top{flex-direction:column}.el-tabs--top>.el-tabs__content+.el-tabs__header{order:-1}.slideInRight-transition,.slideInLeft-transition{display:inline-block}.slideInRight-enter{animation:slideInRight-enter var(--el-transition-duration)}.slideInRight-leave{animation:slideInRight-leave var(--el-transition-duration);position:absolute;left:0;right:0}.slideInLeft-enter{animation:slideInLeft-enter var(--el-transition-duration)}.slideInLeft-leave{animation:slideInLeft-leave var(--el-transition-duration);position:absolute;left:0;right:0}@keyframes slideInRight-enter{0%{opacity:0;transform-origin:0 0;transform:translate(100%)}to{opacity:1;transform-origin:0 0;transform:translate(0)}}@keyframes slideInRight-leave{0%{transform-origin:0 0;opacity:1;transform:translate(0)}to{transform-origin:0 0;opacity:0;transform:translate(100%)}}@keyframes slideInLeft-enter{0%{opacity:0;transform-origin:0 0;transform:translate(-100%)}to{opacity:1;transform-origin:0 0;transform:translate(0)}}@keyframes slideInLeft-leave{0%{transform-origin:0 0;opacity:1;transform:translate(0)}to{transform-origin:0 0;opacity:0;transform:translate(-100%)}}.el-tag{--el-tag-font-size:12px;--el-tag-border-radius:4px;--el-tag-border-radius-rounded:9999px;background-color:var(--el-tag-bg-color);border-color:var(--el-tag-border-color);color:var(--el-tag-text-color);vertical-align:middle;height:24px;font-size:var(--el-tag-font-size);border-radius:var(--el-tag-border-radius);box-sizing:border-box;white-space:nowrap;--el-icon-size:14px;--el-tag-bg-color:var(--el-color-primary-light-9);--el-tag-border-color:var(--el-color-primary-light-8);--el-tag-hover-color:var(--el-color-primary);border-style:solid;border-width:1px;justify-content:center;align-items:center;padding:0 9px;line-height:1;display:inline-flex}.el-tag.el-tag--primary{--el-tag-bg-color:var(--el-color-primary-light-9);--el-tag-border-color:var(--el-color-primary-light-8);--el-tag-hover-color:var(--el-color-primary)}.el-tag.el-tag--success{--el-tag-bg-color:var(--el-color-success-light-9);--el-tag-border-color:var(--el-color-success-light-8);--el-tag-hover-color:var(--el-color-success)}.el-tag.el-tag--warning{--el-tag-bg-color:var(--el-color-warning-light-9);--el-tag-border-color:var(--el-color-warning-light-8);--el-tag-hover-color:var(--el-color-warning)}.el-tag.el-tag--danger{--el-tag-bg-color:var(--el-color-danger-light-9);--el-tag-border-color:var(--el-color-danger-light-8);--el-tag-hover-color:var(--el-color-danger)}.el-tag.el-tag--error{--el-tag-bg-color:var(--el-color-error-light-9);--el-tag-border-color:var(--el-color-error-light-8);--el-tag-hover-color:var(--el-color-error)}.el-tag.el-tag--info{--el-tag-bg-color:var(--el-color-info-light-9);--el-tag-border-color:var(--el-color-info-light-8);--el-tag-hover-color:var(--el-color-info)}.el-tag.is-hit{border-color:var(--el-color-primary)}.el-tag.is-round{border-radius:var(--el-tag-border-radius-rounded)}.el-tag .el-tag__close{color:var(--el-tag-text-color);flex-shrink:0}.el-tag .el-tag__close:hover{color:var(--el-color-white);background-color:var(--el-tag-hover-color)}.el-tag.el-tag--primary{--el-tag-text-color:var(--el-color-primary)}.el-tag.el-tag--success{--el-tag-text-color:var(--el-color-success)}.el-tag.el-tag--warning{--el-tag-text-color:var(--el-color-warning)}.el-tag.el-tag--danger{--el-tag-text-color:var(--el-color-danger)}.el-tag.el-tag--error{--el-tag-text-color:var(--el-color-error)}.el-tag.el-tag--info{--el-tag-text-color:var(--el-color-info)}.el-tag .el-icon{cursor:pointer;font-size:calc(var(--el-icon-size) - 2px);height:var(--el-icon-size);width:var(--el-icon-size);border-radius:50%}.el-tag .el-tag__close{background-color:#0000;border:none;border-radius:50%;outline:none;margin-left:6px;padding:0;overflow:hidden}.el-tag .el-tag__close:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-tag .el-tag__close .el-icon{display:flex}.el-tag--dark{--el-tag-text-color:var(--el-color-white);--el-tag-bg-color:var(--el-color-primary);--el-tag-border-color:var(--el-color-primary);--el-tag-hover-color:var(--el-color-primary-light-3)}.el-tag--dark.el-tag--primary{--el-tag-bg-color:var(--el-color-primary);--el-tag-border-color:var(--el-color-primary);--el-tag-hover-color:var(--el-color-primary-light-3)}.el-tag--dark.el-tag--success{--el-tag-bg-color:var(--el-color-success);--el-tag-border-color:var(--el-color-success);--el-tag-hover-color:var(--el-color-success-light-3)}.el-tag--dark.el-tag--warning{--el-tag-bg-color:var(--el-color-warning);--el-tag-border-color:var(--el-color-warning);--el-tag-hover-color:var(--el-color-warning-light-3)}.el-tag--dark.el-tag--danger{--el-tag-bg-color:var(--el-color-danger);--el-tag-border-color:var(--el-color-danger);--el-tag-hover-color:var(--el-color-danger-light-3)}.el-tag--dark.el-tag--error{--el-tag-bg-color:var(--el-color-error);--el-tag-border-color:var(--el-color-error);--el-tag-hover-color:var(--el-color-error-light-3)}.el-tag--dark.el-tag--info{--el-tag-bg-color:var(--el-color-info);--el-tag-border-color:var(--el-color-info);--el-tag-hover-color:var(--el-color-info-light-3)}.el-tag--dark.el-tag--primary,.el-tag--dark.el-tag--success,.el-tag--dark.el-tag--warning,.el-tag--dark.el-tag--danger,.el-tag--dark.el-tag--error,.el-tag--dark.el-tag--info{--el-tag-text-color:var(--el-color-white)}.el-tag--plain,.el-tag--plain.el-tag--primary{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-primary-light-5);--el-tag-hover-color:var(--el-color-primary)}.el-tag--plain.el-tag--success{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-success-light-5);--el-tag-hover-color:var(--el-color-success)}.el-tag--plain.el-tag--warning{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-warning-light-5);--el-tag-hover-color:var(--el-color-warning)}.el-tag--plain.el-tag--danger{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-danger-light-5);--el-tag-hover-color:var(--el-color-danger)}.el-tag--plain.el-tag--error{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-error-light-5);--el-tag-hover-color:var(--el-color-error)}.el-tag--plain.el-tag--info{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-info-light-5);--el-tag-hover-color:var(--el-color-info)}.el-tag.is-closable{padding-right:5px}.el-tag--large{--el-icon-size:16px;height:32px;padding:0 11px}.el-tag--large .el-tag__close{margin-left:8px}.el-tag--large.is-closable{padding-right:7px}.el-tag--small{--el-icon-size:12px;height:20px;padding:0 7px}.el-tag--small .el-tag__close{margin-left:4px}.el-tag--small.is-closable{padding-right:3px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag.el-tag--primary.is-hit{border-color:var(--el-color-primary)}.el-tag.el-tag--success.is-hit{border-color:var(--el-color-success)}.el-tag.el-tag--warning.is-hit{border-color:var(--el-color-warning)}.el-tag.el-tag--danger.is-hit{border-color:var(--el-color-danger)}.el-tag.el-tag--error.is-hit{border-color:var(--el-color-error)}.el-tag.el-tag--info.is-hit{border-color:var(--el-color-info)}.el-text{--el-text-font-size:var(--el-font-size-base);--el-text-color:var(--el-text-color-regular);font-size:var(--el-text-font-size);color:var(--el-text-color);overflow-wrap:break-word;align-self:center;margin:0;padding:0}.el-text.is-truncated{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:inline-block;overflow:hidden}.el-text.is-line-clamp{-webkit-box-orient:vertical;display:-webkit-inline-box;overflow:hidden}.el-text--large{--el-text-font-size:var(--el-font-size-medium)}.el-text--default{--el-text-font-size:var(--el-font-size-base)}.el-text--small{--el-text-font-size:var(--el-font-size-extra-small)}.el-text.el-text--primary{--el-text-color:var(--el-color-primary)}.el-text.el-text--success{--el-text-color:var(--el-color-success)}.el-text.el-text--warning{--el-text-color:var(--el-color-warning)}.el-text.el-text--danger{--el-text-color:var(--el-color-danger)}.el-text.el-text--error{--el-text-color:var(--el-color-error)}.el-text.el-text--info{--el-text-color:var(--el-color-info)}.el-text>.el-icon{vertical-align:-2px}.time-select{min-width:0;margin:5px 0}.time-select .el-picker-panel__content{max-height:200px;margin:0}.time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.time-select-item.disabled{color:var(--el-datepicker-border-color);cursor:not-allowed}.time-select-item:hover{background-color:var(--el-fill-color-light);cursor:pointer;font-weight:700}.time-select .time-select-item.selected:not(.disabled){color:var(--el-color-primary);font-weight:700}.el-timeline-item{padding-bottom:20px;position:relative}.el-timeline-item__wrapper{box-sizing:content-box;position:relative;top:-3px}.el-timeline-item__tail{border-left:2px solid var(--el-timeline-node-color);height:100%;position:absolute}.el-timeline-item .el-timeline-item__icon{color:var(--el-color-white);font-size:var(--el-font-size-small)}.el-timeline-item__node{background-color:var(--el-timeline-node-color);border-color:var(--el-timeline-node-color);box-sizing:border-box;border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute}.el-timeline-item__node--normal{width:var(--el-timeline-node-size-normal);height:var(--el-timeline-node-size-normal)}.el-timeline-item__node--large{width:var(--el-timeline-node-size-large);height:var(--el-timeline-node-size-large)}.el-timeline-item__node.is-hollow{background:var(--el-color-white);border-style:solid;border-width:2px}.el-timeline-item__node--primary{background-color:var(--el-color-primary);border-color:var(--el-color-primary)}.el-timeline-item__node--success{background-color:var(--el-color-success);border-color:var(--el-color-success)}.el-timeline-item__node--warning{background-color:var(--el-color-warning);border-color:var(--el-color-warning)}.el-timeline-item__node--danger{background-color:var(--el-color-danger);border-color:var(--el-color-danger)}.el-timeline-item__node--info{background-color:var(--el-color-info);border-color:var(--el-color-info)}.el-timeline-item__dot{justify-content:center;align-items:center;display:flex;position:absolute}.el-timeline-item__content{color:var(--el-text-color-primary)}.el-timeline-item__timestamp{color:var(--el-text-color-secondary);line-height:1;font-size:var(--el-font-size-small)}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-timeline-item.is-start .el-timeline-item__wrapper{padding-left:28px}.el-timeline-item.is-start .el-timeline-item__tail{left:4px}.el-timeline-item.is-start .el-timeline-item__node--normal{left:-1px}.el-timeline-item.is-start .el-timeline-item__node--large{left:-2px}.el-timeline-item.is-end .el-timeline-item__wrapper{text-align:right;padding-right:28px}.el-timeline-item.is-end .el-timeline-item__tail{right:4px}.el-timeline-item.is-end .el-timeline-item__node--normal{right:-1px}.el-timeline-item.is-end .el-timeline-item__node--large{right:-2px}.el-timeline-item.is-alternate .el-timeline-item__tail,.el-timeline-item.is-alternate .el-timeline-item__node,.el-timeline-item.is-alternate-reverse .el-timeline-item__tail,.el-timeline-item.is-alternate-reverse .el-timeline-item__node{left:50%;transform:translate(-50%)}.el-timeline{--el-timeline-node-size-normal:12px;--el-timeline-node-size-large:14px;--el-timeline-node-color:var(--el-border-color-light);font-size:var(--el-font-size-base);margin:0;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline .el-timeline-item__center{align-items:center;display:flex}.el-timeline .el-timeline-item__center .el-timeline-item__wrapper{width:100%}.el-timeline .el-timeline-item__center .el-timeline-item__tail{top:0}.el-timeline .el-timeline-item__center:first-child .el-timeline-item__tail{height:calc(50% + 10px);top:calc(50% - 10px)}.el-timeline .el-timeline-item__center:last-child .el-timeline-item__tail{height:calc(50% - 10px);display:block}.el-timeline.is-start{padding-left:40px;padding-right:0}.el-timeline.is-end{padding-left:0;padding-right:40px}.el-timeline.is-alternate{padding-left:20px;padding-right:20px}.el-timeline.is-alternate .el-timeline-item:nth-child(odd) .el-timeline-item__wrapper{width:calc(50% - 28px);left:calc(50% - var(--el-timeline-node-size-large) / 2);padding-left:28px}.el-timeline.is-alternate .el-timeline-item:nth-child(2n) .el-timeline-item__wrapper{width:calc(50% - 28px + var(--el-timeline-node-size-large) / 2);text-align:right;padding-right:28px}.el-timeline.is-alternate-reverse{padding-left:20px;padding-right:20px}.el-timeline.is-alternate-reverse .el-timeline-item:nth-child(odd) .el-timeline-item__wrapper{width:calc(50% - 28px + var(--el-timeline-node-size-large) / 2);text-align:right;padding-right:28px}.el-timeline.is-alternate-reverse .el-timeline-item:nth-child(2n) .el-timeline-item__wrapper{width:calc(50% - 28px);left:calc(50% - var(--el-timeline-node-size-large) / 2);padding-left:28px}.el-transfer{--el-transfer-border-color:var(--el-border-color-lighter);--el-transfer-border-radius:var(--el-border-radius-base);--el-transfer-panel-width:200px;--el-transfer-panel-header-height:40px;--el-transfer-panel-header-bg-color:var(--el-fill-color-light);--el-transfer-panel-footer-height:40px;--el-transfer-panel-body-height:278px;--el-transfer-item-height:30px;--el-transfer-filter-height:32px;font-size:var(--el-font-size-base)}.el-transfer__buttons{vertical-align:middle;padding:0 30px;display:inline-block}.el-transfer__button{vertical-align:top}.el-transfer__button:nth-child(2){margin:0 0 0 10px}.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer__button .el-icon+span{margin-left:0}.el-transfer-panel{background:var(--el-bg-color-overlay);text-align:left;vertical-align:middle;width:var(--el-transfer-panel-width);box-sizing:border-box;max-height:100%;display:inline-block;position:relative;overflow:hidden}.el-transfer-panel__body{height:var(--el-transfer-panel-body-height);border-left:1px solid var(--el-transfer-border-color);border-right:1px solid var(--el-transfer-border-color);border-bottom:1px solid var(--el-transfer-border-color);border-bottom-left-radius:var(--el-transfer-border-radius);border-bottom-right-radius:var(--el-transfer-border-radius);overflow:hidden}.el-transfer-panel__body.is-with-footer{border-bottom:none;border-bottom-right-radius:0;border-bottom-left-radius:0}.el-transfer-panel__list{height:var(--el-transfer-panel-body-height);box-sizing:border-box;margin:0;padding:6px 0;list-style:none;overflow:auto}.el-transfer-panel__list.is-filterable{height:calc(100% - var(--el-transfer-filter-height) - 30px);padding-top:0}.el-transfer-panel__item{height:var(--el-transfer-item-height);line-height:var(--el-transfer-item-height);padding-left:15px;display:block!important}.el-transfer-panel__item+.el-transfer-panel__item{margin-left:0}.el-transfer-panel__item.el-checkbox{color:var(--el-text-color-regular);margin-right:30px}.el-transfer-panel__item:hover{color:var(--el-color-primary)}.el-transfer-panel__item.el-checkbox .el-checkbox__label{text-overflow:ellipsis;white-space:nowrap;box-sizing:border-box;width:100%;line-height:var(--el-transfer-item-height);padding-left:22px;display:block;overflow:hidden}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;box-sizing:border-box;padding:15px}.el-transfer-panel__filter .el-input__inner{height:var(--el-transfer-filter-height);box-sizing:border-box;width:100%;font-size:12px;display:inline-block}.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-transfer-panel .el-transfer-panel__header{height:var(--el-transfer-panel-header-height);background:var(--el-transfer-panel-header-bg-color);border:1px solid var(--el-transfer-border-color);border-top-left-radius:var(--el-transfer-border-radius);border-top-right-radius:var(--el-transfer-border-radius);box-sizing:border-box;color:var(--el-color-black);align-items:center;margin:0;padding-left:15px;display:flex}.el-transfer-panel .el-transfer-panel__header .el-checkbox{align-items:center;width:100%;display:flex;position:relative}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{min-width:0;color:var(--el-text-color-primary);flex:1;align-items:center;font-size:16px;font-weight:400;display:flex}.el-transfer-panel .el-transfer-panel__header-title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.el-transfer-panel .el-transfer-panel__header-count{color:var(--el-text-color-secondary);flex-shrink:0;margin-left:8px;margin-right:15px;font-size:12px}.el-transfer-panel .el-transfer-panel__footer{height:var(--el-transfer-panel-footer-height);background:var(--el-bg-color-overlay);border:1px solid var(--el-transfer-border-color);border-bottom-left-radius:var(--el-transfer-border-radius);border-bottom-right-radius:var(--el-transfer-border-radius);margin:0;padding:0}.el-transfer-panel .el-transfer-panel__footer:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{color:var(--el-text-color-regular);padding-left:20px}.el-transfer-panel .el-transfer-panel__empty{height:var(--el-transfer-item-height);line-height:var(--el-transfer-item-height);color:var(--el-text-color-secondary);text-align:center;margin:0;padding:6px 15px 0}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-tree{--el-tree-node-content-height:26px;--el-tree-node-hover-bg-color:var(--el-fill-color-light);--el-tree-text-color:var(--el-text-color-regular);--el-tree-expand-icon-color:var(--el-text-color-placeholder);cursor:default;background:var(--el-fill-color-blank);color:var(--el-tree-text-color);font-size:var(--el-font-size-base);position:relative}.el-tree__empty-block{text-align:center;width:100%;height:100%;min-height:60px;position:relative}.el-tree__empty-text{color:var(--el-text-color-secondary);font-size:var(--el-font-size-base);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-tree__drop-indicator{background-color:var(--el-color-primary);height:1px;position:absolute;left:0;right:0}.el-tree-node{white-space:nowrap;outline:none}.el-tree-node:focus>.el-tree-node__content{background-color:var(--el-tree-node-hover-bg-color)}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:var(--el-color-primary);color:#fff}.el-tree-node__content{--el-checkbox-height:var(--el-tree-node-content-height);height:var(--el-tree-node-content-height);cursor:pointer;align-items:center;display:flex}.el-tree-node__content>.el-tree-node__expand-icon{box-sizing:content-box;padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree-node__content:hover{background-color:var(--el-tree-node-hover-bg-color)}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:var(--el-tree-expand-icon-color);transition:transform var(--el-transition-duration) ease-in-out;font-size:12px;transform:rotate(0)}.el-tree-node__expand-icon.expanded{transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:#0000;cursor:default;visibility:hidden}.el-tree-node__expand-icon.is-hidden{visibility:hidden}.el-tree-node__loading-icon{font-size:var(--el-font-size-base);color:var(--el-tree-expand-icon-color);margin-right:8px}.el-tree-node>.el-tree-node__children{background-color:#0000;overflow:hidden}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:var(--el-color-primary-light-9)}.el-tree-select{--el-tree-node-content-height:26px;--el-tree-node-hover-bg-color:var(--el-fill-color-light);--el-tree-text-color:var(--el-text-color-regular);--el-tree-expand-icon-color:var(--el-text-color-placeholder)}.el-tree-select__popper .el-tree-node__expand-icon{margin-left:8px}.el-tree-select__popper .el-tree-node.is-checked>.el-tree-node__content .el-select-dropdown__item.selected:after{content:none}.el-tree-select__popper .el-select-dropdown__list>.el-select-dropdown__item{padding-left:32px}.el-tree-select__popper .el-select-dropdown__item{flex:1;height:20px;padding-left:0;line-height:20px;background:0 0!important}.el-upload{--el-upload-dragger-padding-horizontal:10px;--el-upload-dragger-padding-vertical:40px;--el-upload-list-picture-card-size:var(--el-upload-picture-card-size);--el-upload-picture-card-size:148px;cursor:pointer;outline:none;justify-content:center;align-items:center;display:inline-flex}.el-upload.is-disabled{cursor:not-allowed}.el-upload.is-disabled:focus{border-color:var(--el-border-color-darker);color:inherit}.el-upload.is-disabled:focus .el-upload-dragger{border-color:var(--el-border-color-darker)}.el-upload.is-disabled .el-upload-dragger{cursor:not-allowed;background-color:var(--el-disabled-bg-color)}.el-upload.is-disabled .el-upload-dragger .el-upload__text{color:var(--el-text-color-placeholder)}.el-upload.is-disabled .el-upload-dragger .el-upload__text em{color:var(--el-disabled-text-color)}.el-upload.is-disabled .el-upload-dragger:hover{border-color:var(--el-border-color-darker)}.el-upload__input{display:none}.el-upload__tip{color:var(--el-text-color-regular);margin-top:7px;font-size:12px}.el-upload iframe{z-index:-1;opacity:0;filter:alpha(opacity=0);position:absolute;top:0;left:0}.el-upload--picture-card{background-color:var(--el-fill-color-lighter);border:1px dashed var(--el-border-color-darker);box-sizing:border-box;width:var(--el-upload-picture-card-size);height:var(--el-upload-picture-card-size);cursor:pointer;vertical-align:top;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.el-upload--picture-card>i{color:var(--el-text-color-secondary);font-size:28px}.el-upload--picture-card:hover{border-color:var(--el-color-primary);color:var(--el-color-primary)}.el-upload.is-drag{display:block}.el-upload:focus{border-color:var(--el-color-primary);color:var(--el-color-primary)}.el-upload:focus .el-upload-dragger{border-color:var(--el-color-primary)}.el-upload-dragger{padding:var(--el-upload-dragger-padding-vertical) var(--el-upload-dragger-padding-horizontal);background-color:var(--el-fill-color-blank);border:1px dashed var(--el-border-color);box-sizing:border-box;text-align:center;cursor:pointer;border-radius:6px;position:relative;overflow:hidden}.el-upload-dragger .el-icon--upload{color:var(--el-text-color-placeholder);margin-bottom:16px;font-size:67px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:var(--el-border);margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:var(--el-text-color-regular);text-align:center;font-size:14px}.el-upload-dragger .el-upload__text em{color:var(--el-color-primary);font-style:normal}.el-upload-dragger:hover{border-color:var(--el-color-primary)}.el-upload-dragger.is-dragover{padding:calc(var(--el-upload-dragger-padding-vertical) - 1px) calc(var(--el-upload-dragger-padding-horizontal) - 1px);background-color:var(--el-color-primary-light-9);border:2px dashed var(--el-color-primary)}.el-upload-list{--el-upload-dragger-padding-horizontal:10px;--el-upload-dragger-padding-vertical:40px;--el-upload-list-picture-card-size:var(--el-upload-picture-card-size);--el-upload-picture-card-size:148px;margin:10px 0 0;padding:0;list-style:none;position:relative}.el-upload-list__item{color:var(--el-text-color-regular);box-sizing:border-box;border-radius:4px;width:100%;margin-bottom:5px;font-size:14px;transition:all .5s cubic-bezier(.55,0,.1,1);position:relative}.el-upload-list__item .el-progress{width:100%;position:absolute;top:20px}.el-upload-list__item .el-progress__text{position:absolute;top:-13px;right:0}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item .el-icon--upload-success{color:var(--el-color-success)}.el-upload-list__item .el-icon--close{cursor:pointer;opacity:.75;color:var(--el-text-color-regular);transition:opacity var(--el-transition-duration);display:none;position:absolute;top:50%;right:5px;transform:translateY(-50%)}.el-upload-list__item .el-icon--close:hover{opacity:1;color:var(--el-color-primary)}.el-upload-list__item .el-icon--close-tip{cursor:pointer;opacity:1;color:var(--el-color-primary);font-size:12px;font-style:normal;display:none;position:absolute;top:1px;right:5px}.el-upload-list__item:hover,.el-upload-list__item:focus-within{background-color:var(--el-fill-color-light)}.el-upload-list__item:hover .el-icon--close,.el-upload-list__item:focus-within .el-icon--close{display:inline-flex}.el-upload-list__item:hover .el-icon--close-tip,.el-upload-list__item:focus-within .el-icon--close-tip{right:24px}.el-upload-list__item:hover .el-progress__text,.el-upload-list__item:focus-within .el-progress__text{display:none}.el-upload-list__item .el-upload-list__item-info{flex-direction:column;justify-content:center;width:calc(100% - 30px);margin-left:4px;display:inline-flex}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:inline-flex}.el-upload-list__item.is-success .el-upload-list__item-name:hover,.el-upload-list__item.is-success .el-upload-list__item-name:focus{color:var(--el-color-primary);cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon--close-tip{display:inline-block}.el-upload-list__item.is-success:not(.focusing):focus,.el-upload-list__item.is-success:active{outline-width:0}.el-upload-list__item.is-success:not(.focusing):focus .el-icon--close-tip,.el-upload-list__item.is-success:active .el-icon--close-tip{display:none}.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:focus-within .el-upload-list__item-status-label{opacity:0;display:none}.el-upload-list__item-name{color:var(--el-text-color-regular);text-align:center;transition:color var(--el-transition-duration);font-size:var(--el-font-size-base);align-items:center;padding:0 4px;display:inline-flex}.el-upload-list__item-name .el-icon{color:var(--el-text-color-secondary);margin-right:6px}.el-upload-list__item-file-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.el-upload-list__item-status-label{line-height:inherit;height:100%;transition:opacity var(--el-transition-duration);justify-content:center;align-items:center;display:none;position:absolute;top:0;right:5px}.el-upload-list__item-delete{color:var(--el-text-color-regular);font-size:12px;display:none;position:absolute;top:0;right:10px}.el-upload-list__item-delete:hover{color:var(--el-color-primary)}.el-upload-list--picture-card{flex-wrap:wrap;margin:0;display:inline-flex}.el-upload-list--picture-card .el-upload-list__item{background-color:var(--el-fill-color-blank);border:1px solid var(--el-border-color);box-sizing:border-box;width:var(--el-upload-list-picture-card-size);height:var(--el-upload-list-picture-card-size);border-radius:6px;margin:0 8px 8px 0;padding:0;display:inline-flex;overflow:hidden}.el-upload-list--picture-card .el-upload-list__item .el-icon--check,.el-upload-list--picture-card .el-upload-list__item .el-icon--circle-check{color:#fff}.el-upload-list--picture-card .el-upload-list__item .el-icon--close{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{opacity:0;display:block}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{object-fit:contain;width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{background:var(--el-color-success);text-align:center;width:40px;height:24px;top:-6px;right:-15px;transform:rotate(45deg)}.el-upload-list--picture-card .el-upload-list__item-status-label i{margin-top:11px;font-size:12px;transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{cursor:default;color:#fff;opacity:0;background-color:var(--el-overlay-color-lighter);width:100%;height:100%;transition:opacity var(--el-transition-duration);justify-content:center;align-items:center;font-size:20px;display:inline-flex;position:absolute;top:0;left:0}.el-upload-list--picture-card .el-upload-list__item-actions span{cursor:pointer;display:none}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:16px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{font-size:inherit;color:inherit;position:static}.el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-flex}.el-upload-list--picture-card .el-progress{width:126px;top:50%;bottom:auto;left:50%;transform:translate(-50%,-50%)}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{z-index:0;background-color:var(--el-fill-color-blank);border:1px solid var(--el-border-color);box-sizing:border-box;border-radius:6px;align-items:center;margin-top:10px;padding:10px;display:flex;overflow:hidden}.el-upload-list--picture .el-upload-list__item .el-icon--check,.el-upload-list--picture .el-upload-list__item .el-icon--circle-check{color:#fff}.el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{opacity:0;display:inline-flex}.el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.el-upload-list--picture .el-upload-list__item .el-icon--close{top:5px;transform:translateY(0)}.el-upload-list--picture .el-upload-list__item-thumbnail{object-fit:contain;z-index:1;background-color:var(--el-color-white);justify-content:center;align-items:center;width:70px;height:70px;display:inline-flex;position:relative}.el-upload-list--picture .el-upload-list__item-status-label{background:var(--el-color-success);text-align:center;width:46px;height:26px;position:absolute;top:-7px;right:-17px;transform:rotate(45deg)}.el-upload-list--picture .el-upload-list__item-status-label i{margin-top:12px;font-size:12px;transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{z-index:10;cursor:default;width:100%;height:100%;position:absolute;top:0;left:0;overflow:hidden}.el-upload-cover:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-upload-cover img{width:100%;height:100%;display:block}.el-upload-cover__label{background:var(--el-color-success);text-align:center;width:40px;height:24px;top:-6px;right:-15px;transform:rotate(45deg)}.el-upload-cover__label i{color:#fff;margin-top:11px;font-size:12px;transform:rotate(-45deg)}.el-upload-cover__progress{vertical-align:middle;width:243px;display:inline-block;position:static}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{width:100%;height:100%;position:absolute;top:0;left:0}.el-upload-cover__interact{background-color:var(--el-overlay-color-light);text-align:center;width:100%;height:100%;position:absolute;bottom:0;left:0}.el-upload-cover__interact .btn{color:#fff;cursor:pointer;vertical-align:middle;transition:var(--el-transition-md-fade);margin-top:60px;font-size:14px;display:inline-block}.el-upload-cover__interact .btn i{margin-top:0}.el-upload-cover__interact .btn span{opacity:0;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#fff;font-size:24px;line-height:inherit;margin:0 auto 5px;display:block}.el-upload-cover__title{text-overflow:ellipsis;white-space:nowrap;text-align:left;width:100%;height:36px;color:var(--el-text-color-primary);background-color:#fff;margin:0;padding:0 10px;font-size:14px;font-weight:400;line-height:36px;position:absolute;bottom:0;left:0;overflow:hidden}.el-upload-cover+.el-upload__inner{opacity:0;z-index:1;position:relative}.el-vl__wrapper{position:relative}.el-vl__wrapper:hover .el-virtual-scrollbar,.el-vl__wrapper.always-on .el-virtual-scrollbar{opacity:1}.el-vl__window{scrollbar-width:none}.el-vl__window::-webkit-scrollbar{display:none}.el-virtual-scrollbar{opacity:0;transition:opacity .34s ease-out}.el-virtual-scrollbar.always-on{opacity:1}.el-vg__wrapper{position:relative}.el-popper{--el-popper-border-radius:var(--el-popover-border-radius,4px);--el-popper-bg-color-light:var(--el-bg-color-overlay);--el-popper-bg-color-dark:var(--el-text-color-primary);border-radius:var(--el-popper-border-radius);z-index:2000;overflow-wrap:break-word;word-break:normal;visibility:visible;min-width:10px;padding:5px 11px;font-size:12px;line-height:20px;position:absolute}.el-popper.is-dark{--el-fill-color-blank:var(--el-popper-bg-color-dark);color:var(--el-bg-color);background:var(--el-popper-bg-color-dark);border:1px solid var(--el-text-color-primary)}.el-popper.is-dark>.el-popper__arrow:before{border:1px solid var(--el-text-color-primary);background:var(--el-popper-bg-color-dark);right:0}.el-popper.is-light{--el-fill-color-blank:var(--el-popper-bg-color-light);background:var(--el-popper-bg-color-light);border:1px solid var(--el-border-color-light)}.el-popper.is-light>.el-popper__arrow:before{border:1px solid var(--el-border-color-light);background:var(--el-popper-bg-color-light);right:0}.el-popper.is-pure{padding:0}.el-popper__arrow{z-index:-1;width:10px;height:10px;position:absolute}.el-popper__arrow:before{z-index:-1;content:" ";background:var(--el-text-color-primary);box-sizing:border-box;width:10px;height:10px;position:absolute;transform:rotate(45deg)}.el-popper[data-popper-placement^=top]>.el-popper__arrow{bottom:-5px}.el-popper[data-popper-placement^=top]>.el-popper__arrow:before{border-bottom-right-radius:2px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow{top:-5px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow:before{border-top-left-radius:2px}.el-popper[data-popper-placement^=left]>.el-popper__arrow{right:-5px}.el-popper[data-popper-placement^=left]>.el-popper__arrow:before{border-top-right-radius:2px}.el-popper[data-popper-placement^=right]>.el-popper__arrow{left:-5px}.el-popper[data-popper-placement^=right]>.el-popper__arrow:before{border-bottom-left-radius:2px}.el-popper[data-popper-placement^=top]>.el-popper__arrow:before{border-top-color:#0000!important;border-left-color:#0000!important}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow:before{border-bottom-color:#0000!important;border-right-color:#0000!important}.el-popper[data-popper-placement^=left]>.el-popper__arrow:before{border-bottom-color:#0000!important;border-left-color:#0000!important}.el-popper[data-popper-placement^=right]>.el-popper__arrow:before{border-top-color:#0000!important;border-right-color:#0000!important}.el-statistic{--el-statistic-title-font-weight:400;--el-statistic-title-font-size:var(--el-font-size-extra-small);--el-statistic-title-color:var(--el-text-color-regular);--el-statistic-content-font-weight:400;--el-statistic-content-font-size:var(--el-font-size-extra-large);--el-statistic-content-color:var(--el-text-color-primary)}.el-statistic__head{font-weight:var(--el-statistic-title-font-weight);font-size:var(--el-statistic-title-font-size);color:var(--el-statistic-title-color);margin-bottom:4px;line-height:20px}.el-statistic__content{font-weight:var(--el-statistic-content-font-weight);font-size:var(--el-statistic-content-font-size);color:var(--el-statistic-content-color)}.el-statistic__value{display:inline-block}.el-statistic__prefix{margin-right:4px;display:inline-block}.el-statistic__suffix{margin-left:4px;display:inline-block}.el-tour{--el-tour-width:520px;--el-tour-padding-primary:12px;--el-tour-font-line-height:var(--el-font-line-height-primary);--el-tour-title-font-size:16px;--el-tour-title-text-color:var(--el-text-color-primary);--el-tour-title-font-weight:400;--el-tour-close-color:var(--el-color-info);--el-tour-font-size:14px;--el-tour-color:var(--el-text-color-primary);--el-tour-bg-color:var(--el-bg-color);--el-tour-border-radius:4px}.el-tour__hollow{transition:all var(--el-transition-duration) ease}.el-tour__content{border-radius:var(--el-tour-border-radius);width:var(--el-tour-width);padding:var(--el-tour-padding-primary);background:var(--el-tour-bg-color);box-shadow:var(--el-box-shadow-light);box-sizing:border-box;overflow-wrap:break-word;outline:none}.el-tour__arrow{background:var(--el-tour-bg-color);pointer-events:none;box-sizing:border-box;width:10px;height:10px;position:absolute;transform:rotate(45deg)}.el-tour__content[data-side^=top] .el-tour__arrow{border-top-color:#0000;border-left-color:#0000}.el-tour__content[data-side^=bottom] .el-tour__arrow{border-bottom-color:#0000;border-right-color:#0000}.el-tour__content[data-side^=left] .el-tour__arrow{border-bottom-color:#0000;border-left-color:#0000}.el-tour__content[data-side^=right] .el-tour__arrow{border-top-color:#0000;border-right-color:#0000}.el-tour__content[data-side^=top] .el-tour__arrow{bottom:-5px}.el-tour__content[data-side^=bottom] .el-tour__arrow{top:-5px}.el-tour__content[data-side^=left] .el-tour__arrow{right:-5px}.el-tour__content[data-side^=right] .el-tour__arrow{left:-5px}.el-tour__closebtn{cursor:pointer;width:40px;height:40px;font-size:var(--el-message-close-size,16px);background:0 0;border:none;outline:none;padding:0;position:absolute;top:0;right:0}.el-tour__closebtn .el-tour__close{color:var(--el-tour-close-color);font-size:inherit}.el-tour__closebtn:focus .el-tour__close,.el-tour__closebtn:hover .el-tour__close{color:var(--el-color-primary)}.el-tour__header{padding-bottom:var(--el-tour-padding-primary)}.el-tour__header.show-close{padding-right:calc(var(--el-tour-padding-primary) + var(--el-message-close-size,16px))}.el-tour__title{line-height:var(--el-tour-font-line-height);font-size:var(--el-tour-title-font-size);color:var(--el-tour-title-text-color);font-weight:var(--el-tour-title-font-weight)}.el-tour__body{color:var(--el-tour-text-color);font-size:var(--el-tour-font-size)}.el-tour__body img,.el-tour__body video{max-width:100%}.el-tour__footer{padding-top:var(--el-tour-padding-primary);box-sizing:border-box;justify-content:space-between;display:flex}.el-tour__content .el-tour-indicators{flex:1;display:inline-block}.el-tour__content .el-tour-indicator{background:var(--el-color-info-light-9);border-radius:50%;width:6px;height:6px;margin-right:6px;display:inline-block}.el-tour__content .el-tour-indicator.is-active{background:var(--el-color-primary)}.el-tour.el-tour--primary{--el-tour-title-text-color:#fff;--el-tour-text-color:#fff;--el-tour-bg-color:var(--el-color-primary);--el-tour-close-color:#fff}.el-tour.el-tour--primary .el-tour__closebtn:focus .el-tour__close,.el-tour.el-tour--primary .el-tour__closebtn:hover .el-tour__close{color:var(--el-tour-title-text-color)}.el-tour.el-tour--primary .el-button--default{color:var(--el-color-primary);border-color:var(--el-color-primary);background:#fff}.el-tour.el-tour--primary .el-button--primary{border-color:#fff}.el-tour.el-tour--primary .el-tour-indicator{background:#ffffff26}.el-tour.el-tour--primary .el-tour-indicator.is-active{background:#fff}.el-tour-parent--hidden{overflow:hidden}.el-anchor{--el-anchor-bg-color:var(--el-bg-color);--el-anchor-padding-indent:14px;--el-anchor-line-height:22px;--el-anchor-font-size:12px;--el-anchor-color:var(--el-text-color-secondary);--el-anchor-active-color:var(--el-color-primary);--el-anchor-hover-color:var(--el-text-color-regular);--el-anchor-marker-bg-color:var(--el-color-primary);background-color:var(--el-anchor-bg-color);position:relative}.el-anchor__marker{background-color:var(--el-anchor-marker-bg-color);opacity:0;z-index:0;border-radius:4px;position:absolute}.el-anchor.el-anchor--vertical .el-anchor__marker{width:4px;height:14px;transition:top .25s ease-in-out,opacity .25s;top:8px;left:0}.el-anchor.el-anchor--vertical .el-anchor__list{padding-left:var(--el-anchor-padding-indent)}.el-anchor.el-anchor--vertical.el-anchor--underline:before{content:"";background-color:#0505050f;width:2px;height:100%;position:absolute;left:0}.el-anchor.el-anchor--vertical.el-anchor--underline .el-anchor__marker{border-radius:unset;width:2px}.el-anchor.el-anchor--horizontal .el-anchor__marker{width:20px;height:2px;transition:left .25s ease-in-out,opacity .25s,width .25s;bottom:0}.el-anchor.el-anchor--horizontal .el-anchor__list{padding-bottom:4px;display:flex}.el-anchor.el-anchor--horizontal .el-anchor__list .el-anchor__item{padding-left:16px}.el-anchor.el-anchor--horizontal .el-anchor__list .el-anchor__item:first-child{padding-left:0}.el-anchor.el-anchor--horizontal.el-anchor--underline:before{content:"";background-color:#0505050f;width:100%;height:2px;position:absolute;bottom:0}.el-anchor.el-anchor--horizontal.el-anchor--underline .el-anchor__marker{border-radius:unset;height:2px}.el-anchor__item{flex-direction:column;display:flex}.el-anchor__link{font-size:var(--el-anchor-font-size);line-height:var(--el-anchor-line-height);color:var(--el-anchor-color);transition:color var(--el-transition-duration);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;outline:none;max-width:100%;padding:4px 0;text-decoration:none;overflow:hidden}.el-anchor__link:hover,.el-anchor__link:focus{color:var(--el-hover-color)}.el-anchor__link:focus-visible{border-radius:var(--el-border-radius-base);outline:2px solid var(--el-color-primary)}.el-anchor__link.is-active{color:var(--el-anchor-active-color)}.el-anchor .el-anchor__list .el-anchor__item a{display:inline-block}.el-segmented--vertical{flex-direction:column}.el-segmented--vertical .el-segmented__item{padding:11px}.el-segmented{--el-segmented-color:var(--el-text-color-regular);--el-segmented-bg-color:var(--el-fill-color-light);--el-segmented-padding:2px;--el-segmented-item-selected-color:var(--el-color-white);--el-segmented-item-selected-bg-color:var(--el-color-primary);--el-segmented-item-selected-disabled-bg-color:var(--el-color-primary-light-5);--el-segmented-item-hover-color:var(--el-text-color-primary);--el-segmented-item-hover-bg-color:var(--el-fill-color-dark);--el-segmented-item-active-bg-color:var(--el-fill-color-darker);--el-segmented-item-disabled-color:var(--el-text-color-placeholder);background:var(--el-segmented-bg-color);min-height:32px;padding:var(--el-segmented-padding);border-radius:var(--el-border-radius-base);color:var(--el-segmented-color);box-sizing:border-box;align-items:stretch;font-size:14px;display:inline-flex}.el-segmented__group{align-items:stretch;width:100%;display:flex;position:relative}.el-segmented__item-selected{background:var(--el-segmented-item-selected-bg-color);border-radius:calc(var(--el-border-radius-base) - 2px);pointer-events:none;width:10px;height:100%;transition:all .3s;position:absolute;top:0;left:0}.el-segmented__item-selected.is-disabled{background:var(--el-segmented-item-selected-disabled-bg-color)}.el-segmented__item-selected.is-focus-visible:before{content:"";border-radius:inherit;outline:2px solid var(--el-segmented-item-selected-bg-color);outline-offset:1px;position:absolute;top:0;bottom:0;left:0;right:0}.el-segmented__item{cursor:pointer;border-radius:calc(var(--el-border-radius-base) - 2px);flex:1;align-items:center;padding:0 11px;display:flex}.el-segmented__item:not(.is-disabled):not(.is-selected):hover{color:var(--el-segmented-item-hover-color);background:var(--el-segmented-item-hover-bg-color)}.el-segmented__item:not(.is-disabled):not(.is-selected):active{background:var(--el-segmented-item-active-bg-color)}.el-segmented__item.is-selected,.el-segmented__item.is-selected.is-disabled{color:var(--el-segmented-item-selected-color)}.el-segmented__item.is-disabled{cursor:not-allowed;color:var(--el-segmented-item-disabled-color)}.el-segmented__item-input{opacity:0;pointer-events:none;width:0;height:0;margin:0;position:absolute}.el-segmented__item-label{text-align:center;text-overflow:ellipsis;white-space:nowrap;z-index:1;flex:1;line-height:normal;transition:color .3s;overflow:hidden}.el-segmented.is-block{display:flex}.el-segmented.is-block .el-segmented__item{min-width:0}.el-segmented--large{border-radius:var(--el-border-radius-base);min-height:40px;font-size:16px}.el-segmented--large .el-segmented__item-selected{border-radius:calc(var(--el-border-radius-base) - 2px)}.el-segmented--large .el-segmented--vertical .el-segmented__item{padding:11px}.el-segmented--large .el-segmented__item{border-radius:calc(var(--el-border-radius-base) - 2px);padding:0 11px}.el-segmented--small{border-radius:calc(var(--el-border-radius-base) - 1px);min-height:24px;font-size:14px}.el-segmented--small .el-segmented__item-selected{border-radius:calc(calc(var(--el-border-radius-base) - 1px) - 2px)}.el-segmented--small .el-segmented--vertical .el-segmented__item{padding:7px}.el-segmented--small .el-segmented__item{border-radius:calc(calc(var(--el-border-radius-base) - 1px) - 2px);padding:0 7px}.el-mention{width:100%;position:relative}.el-mention__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-box-shadow-light)}.el-mention__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-mention__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-mention__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-mention__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-mention__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-mention-dropdown{--el-mention-font-size:var(--el-font-size-base);--el-mention-bg-color:var(--el-bg-color-overlay);--el-mention-shadow:var(--el-box-shadow-light);--el-mention-border:1px solid var(--el-border-color-light);--el-mention-option-color:var(--el-text-color-regular);--el-mention-option-height:34px;--el-mention-option-min-width:100px;--el-mention-option-hover-background:var(--el-fill-color-light);--el-mention-option-selected-color:var(--el-color-primary);--el-mention-option-disabled-color:var(--el-text-color-placeholder);--el-mention-option-loading-color:var(--el-text-color-secondary);--el-mention-option-loading-padding:10px 0;--el-mention-max-height:174px;--el-mention-padding:6px 0;--el-mention-header-padding:10px;--el-mention-footer-padding:10px}.el-mention-dropdown__item{font-size:var(--el-mention-font-size);white-space:nowrap;text-overflow:ellipsis;color:var(--el-mention-option-color);height:var(--el-mention-option-height);line-height:var(--el-mention-option-height);box-sizing:border-box;min-width:var(--el-mention-option-min-width);cursor:pointer;padding:0 20px;position:relative;overflow:hidden}.el-mention-dropdown__item.is-hovering{background-color:var(--el-mention-option-hover-background)}.el-mention-dropdown__item.is-selected{color:var(--el-mention-option-selected-color);font-weight:700}.el-mention-dropdown__item.is-disabled{color:var(--el-mention-option-disabled-color);cursor:not-allowed;background-color:unset}.el-mention-dropdown{z-index:calc(var(--el-index-top) + 1);border-radius:var(--el-border-radius-base);box-sizing:border-box}.el-mention-dropdown__loading{text-align:center;color:var(--el-mention-option-loading-color);min-width:var(--el-mention-option-min-width);margin:0;padding:10px 0;font-size:12px}.el-mention-dropdown__wrap{max-height:var(--el-mention-max-height)}.el-mention-dropdown__list{padding:var(--el-mention-padding);box-sizing:border-box;margin:0;list-style:none}.el-mention-dropdown__header{padding:var(--el-mention-header-padding);border-bottom:var(--el-mention-border)}.el-mention-dropdown__footer{padding:var(--el-mention-footer-padding);border-top:var(--el-mention-border)}.el-splitter{width:100%;height:100%;margin:0;padding:0;display:flex;position:relative}.el-splitter__mask{z-index:999;position:absolute;top:0;bottom:0;left:0;right:0}.el-splitter__mask-horizontal{cursor:ew-resize}.el-splitter__mask-vertical{cursor:ns-resize}.el-splitter__horizontal{flex-direction:row}.el-splitter__vertical{flex-direction:column}.el-splitter-bar{-webkit-user-select:none;user-select:none;flex:none;position:relative}.el-splitter-bar__dragger{z-index:1;background:0 0;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-splitter-bar__dragger:before,.el-splitter-bar__dragger:after{content:"";background-color:var(--el-border-color-light);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-splitter-bar__dragger:not(.is-lazy):after{display:none}.el-splitter-bar__dragger:after{opacity:.4}.el-splitter-bar__dragger:hover:not(.is-disabled):before{background-color:var(--el-color-primary-light-5)}.el-splitter-bar__dragger-horizontal:before,.el-splitter-bar__dragger-horizontal:after{width:2px;height:100%}.el-splitter-bar__dragger-vertical:before,.el-splitter-bar__dragger-vertical:after{width:100%;height:2px}.el-splitter-bar__dragger-active:before,.el-splitter-bar__dragger-active:after{background-color:var(--el-color-primary-light-3)}.el-splitter-bar__dragger-active.el-splitter-bar__dragger-horizontal:after{transform:translate(calc(-50% + var(--el-splitter-bar-offset)),-50%)}.el-splitter-bar__dragger-active.el-splitter-bar__dragger-vertical:after{transform:translate(-50%,calc(-50% + var(--el-splitter-bar-offset)))}.el-splitter-bar:hover .el-splitter-bar__collapse-icon{opacity:1}.el-splitter-bar__collapse-icon{background:var(--el-border-color-light);cursor:pointer;opacity:0;z-index:9;border-radius:2px;justify-content:center;align-items:center;display:flex;position:absolute}.el-splitter-bar__collapse-icon:hover{opacity:1;background-color:var(--el-color-primary-light-5)}.el-splitter-bar__horizontal-collapse-icon-start{width:16px;height:24px;top:50%;left:-12px;transform:translate(-50%,-50%)}.el-splitter-bar__horizontal-collapse-icon-end{width:16px;height:24px;top:50%;left:12px;transform:translate(-50%,-50%)}.el-splitter-bar__vertical-collapse-icon-start{width:24px;height:16px;top:-12px;right:50%;transform:translate(50%,-50%)}.el-splitter-bar__vertical-collapse-icon-end{width:24px;height:16px;top:12px;right:50%;transform:translate(50%,-50%)}.el-splitter-panel{scrollbar-width:thin;box-sizing:border-box;flex-grow:0;overflow:auto}*{margin:0;padding:0;box-sizing:border-box}html,body,#app{height:100%;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#303133;background-color:#f5f7fa}@media (max-width: 768px){.el-table,.el-form-item__label,.el-button{font-size:13px}}.flex-center{display:flex;align-items:center;justify-content:center}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.page-container{padding:16px}@media (min-width: 768px){.page-container{padding:24px}}.card-shadow{background:#fff;border-radius:8px;box-shadow:0 2px 12px #0000000d} diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 5b5673e0..3582673a 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -4,9 +4,9 @@ - PropDataGuard - 财险数据分级分类平台 - - + DataPointer - 数据分类分级管理平台 + +
diff --git a/frontend/public/report-preview.html b/frontend/public/report-preview.html new file mode 100644 index 00000000..b277258c --- /dev/null +++ b/frontend/public/report-preview.html @@ -0,0 +1,69 @@ + + + + + 报告预览 + + + + +
+

数据分类分级项目报告

+
加载中...
+
+ + + diff --git a/frontend/src/api/classification.ts b/frontend/src/api/classification.ts index 2edfa4b6..2805300c 100644 --- a/frontend/src/api/classification.ts +++ b/frontend/src/api/classification.ts @@ -107,3 +107,27 @@ export function getClassificationResults(params: { }) { return request.get('/classifications/results', { params }) } + +export interface MLSuggestion { + column_id: number + column_name: string + table_name?: string + suggestions: { + category_id: number + category_name?: string + category_code?: string + confidence: number + }[] +} + +export function getMLSuggestions(project_id: number, column_ids?: number[], top_k: number = 3) { + const params: any = { top_k } + if (column_ids && column_ids.length) { + params.column_ids = column_ids.join(',') + } + return request.get(`/classifications/ml-suggest/${project_id}`, { params }) +} + +export function trainMLModel(background: boolean = true, model_name?: string, algorithm: string = 'logistic_regression') { + return request.post('/classifications/ml-train', null, { params: { background, model_name, algorithm } }) +} diff --git a/frontend/src/api/compliance.ts b/frontend/src/api/compliance.ts new file mode 100644 index 00000000..4a3b56c1 --- /dev/null +++ b/frontend/src/api/compliance.ts @@ -0,0 +1,17 @@ +import request from './request' + +export function initComplianceRules() { + return request.post('/compliance/init-rules') +} + +export function scanCompliance(projectId?: number) { + return request.post('/compliance/scan', null, { params: projectId ? { project_id: projectId } : undefined }) +} + +export function getComplianceIssues(params?: { project_id?: number; status?: string; page?: number; page_size?: number }) { + return request.get('/compliance/issues', { params }) +} + +export function resolveIssue(id: number) { + return request.post(`/compliance/issues/${id}/resolve`) +} diff --git a/frontend/src/api/lineage.ts b/frontend/src/api/lineage.ts new file mode 100644 index 00000000..5beb784a --- /dev/null +++ b/frontend/src/api/lineage.ts @@ -0,0 +1,9 @@ +import request from './request' + +export function parseLineage(sql: string, targetTable: string) { + return request.post('/lineage/parse', null, { params: { sql, target_table: targetTable } }) +} + +export function getLineageGraph(tableName?: string) { + return request.get('/lineage/graph', { params: tableName ? { table_name: tableName } : undefined }) +} diff --git a/frontend/src/api/masking.ts b/frontend/src/api/masking.ts new file mode 100644 index 00000000..1c18eec8 --- /dev/null +++ b/frontend/src/api/masking.ts @@ -0,0 +1,34 @@ +import request from './request' + +export interface MaskingRuleItem { + id: number + name: string + level_id?: number + category_id?: number + algorithm: string + params?: Record + is_active: boolean + description?: string + level_name?: string + category_name?: string +} + +export function getMaskingRules(params?: { level_id?: number; category_id?: number; page?: number; page_size?: number }) { + return request.get('/masking/rules', { params }) +} + +export function createMaskingRule(data: Partial) { + return request.post('/masking/rules', data) +} + +export function updateMaskingRule(id: number, data: Partial) { + return request.put(`/masking/rules/${id}`, data) +} + +export function deleteMaskingRule(id: number) { + return request.delete(`/masking/rules/${id}`) +} + +export function previewMasking(source_id: number, table_name: string, project_id?: number, limit: number = 20) { + return request.post('/masking/preview', null, { params: { source_id, table_name, project_id, limit } }) +} diff --git a/frontend/src/api/project.ts b/frontend/src/api/project.ts index fd971611..d9baf763 100644 --- a/frontend/src/api/project.ts +++ b/frontend/src/api/project.ts @@ -34,6 +34,10 @@ export function deleteProject(id: number) { return request.delete(`/projects/${id}`) } -export function autoClassifyProject(id: number) { - return request.post(`/projects/${id}/auto-classify`) +export function autoClassifyProject(id: number, background: boolean = true) { + return request.post(`/projects/${id}/auto-classify`, null, { params: { background } }) +} + +export function getAutoClassifyStatus(id: number) { + return request.get(`/projects/${id}/auto-classify-status`) } diff --git a/frontend/src/api/report.ts b/frontend/src/api/report.ts index c857e2e5..6455c810 100644 --- a/frontend/src/api/report.ts +++ b/frontend/src/api/report.ts @@ -16,12 +16,12 @@ export function getReportStats() { return request.get('/reports/stats') } -export function downloadReport(projectId: number) { +export function downloadReport(projectId: number, format: string = 'docx') { const token = localStorage.getItem('dp_token') - const url = `/api/v1/reports/projects/${projectId}/download` + const url = `/api/v1/reports/projects/${projectId}/download?format=${format}` const a = document.createElement('a') a.href = url - a.download = `report_project_${projectId}.docx` + a.download = `report_project_${projectId}.${format === 'excel' ? 'xlsx' : 'docx'}` if (token) { a.setAttribute('data-token', token) } @@ -29,3 +29,7 @@ export function downloadReport(projectId: number) { a.click() document.body.removeChild(a) } + +export function getReportSummary(projectId: number) { + return request.get(`/reports/projects/${projectId}/summary`) +} diff --git a/frontend/src/api/risk.ts b/frontend/src/api/risk.ts new file mode 100644 index 00000000..8a921138 --- /dev/null +++ b/frontend/src/api/risk.ts @@ -0,0 +1,13 @@ +import request from './request' + +export function recalculateRisk(projectId?: number) { + return request.post('/risk/recalculate', null, { params: projectId ? { project_id: projectId } : undefined }) +} + +export function getRiskTop(n: number = 10) { + return request.get('/risk/top', { params: { n } }) +} + +export function getProjectRisk(projectId: number) { + return request.get(`/risk/projects/${projectId}`) +} diff --git a/frontend/src/api/schema_change.ts b/frontend/src/api/schema_change.ts new file mode 100644 index 00000000..1034821a --- /dev/null +++ b/frontend/src/api/schema_change.ts @@ -0,0 +1,17 @@ +import request from './request' + +export interface SchemaChangeLogItem { + id: number + source_id: number + database_id?: number + table_id?: number + column_id?: number + change_type: string + old_value?: string + new_value?: string + detected_at?: string +} + +export function getSchemaChangeLogs(params?: { source_id?: number; change_type?: string; page?: number; page_size?: number }) { + return request.get('/schema-changes/logs', { params }) +} diff --git a/frontend/src/api/unstructured.ts b/frontend/src/api/unstructured.ts new file mode 100644 index 00000000..ea33d574 --- /dev/null +++ b/frontend/src/api/unstructured.ts @@ -0,0 +1,36 @@ +import request from './request' + +export interface UnstructuredFileItem { + id: number + original_name: string + file_type: string + file_size: number + status: string + analysis_result?: { + matches: { + rule_name: string + category_code: string + level_code: string + snippet: string + position: number + }[] + total_chars: number + } + created_at?: string +} + +export function uploadUnstructuredFile(file: File) { + const formData = new FormData() + formData.append('file', file) + return request.post('/unstructured/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) +} + +export function getUnstructuredFiles(params?: { page?: number; page_size?: number }) { + return request.get('/unstructured/files', { params }) +} + +export function reprocessUnstructuredFile(id: number) { + return request.post(`/unstructured/files/${id}/reprocess`) +} diff --git a/frontend/src/api/watermark.ts b/frontend/src/api/watermark.ts new file mode 100644 index 00000000..4c1191b1 --- /dev/null +++ b/frontend/src/api/watermark.ts @@ -0,0 +1,5 @@ +import request from './request' + +export function traceWatermark(text: string) { + return request.post('/watermark/trace', { text }) +} diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index f63a6a74..d009f5de 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -13,9 +13,6 @@ {{ pageTitle }}
+
+ + + +
+
风险 TOP10 项目
+ + + + + + + + + +
+
+ +
+
风险评分趋势(最近7次计算)
+ +
+
+
@@ -106,6 +133,7 @@ import { TooltipComponent, LegendComponent, GridComponent, VisualMapComponent } import VChart from 'vue-echarts' import { DataLine, FolderOpened, DocumentChecked, Warning } from '@element-plus/icons-vue' import { getDashboardStats, getDashboardDistribution } from '@/api/dashboard' +import { getRiskTop } from '@/api/risk' use([CanvasRenderer, PieChart, BarChart, HeatmapChart, TooltipComponent, LegendComponent, GridComponent, VisualMapComponent]) @@ -122,6 +150,22 @@ const levelDist = ref([]) const categoryDist = ref([]) const projectList = ref([]) const heatmapData = ref([]) +const riskTopList = ref([]) +const riskTrendData = ref([]) + +const riskTrendOption = computed(() => ({ + tooltip: { trigger: 'axis' }, + grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, + xAxis: { type: 'category', data: riskTrendData.value.map((i: any) => i.date) }, + yAxis: { type: 'value', max: 100 }, + series: [{ + type: 'line', + data: riskTrendData.value.map((i: any) => i.score), + smooth: true, + itemStyle: { color: '#f56c6c' }, + areaStyle: { color: 'rgba(245,108,108,0.1)' }, + }], +})) const levelOption = computed(() => ({ tooltip: { trigger: 'item' }, @@ -212,9 +256,10 @@ function statusText(status: string) { async function fetchData() { try { - const [statsRes, distRes] = await Promise.all([ + const [statsRes, distRes, riskRes] = await Promise.all([ getDashboardStats(), getDashboardDistribution(), + getRiskTop(10), ]) const s = (statsRes as any)?.data || {} Object.assign(stats, s) @@ -229,6 +274,20 @@ async function fetchData() { planned_end: p.planned_end ? p.planned_end.slice(0, 10) : '--', })) heatmapData.value = d.heatmap || [] + + const r = (riskRes as any)?.data || [] + riskTopList.value = r + // Mock trend data if not enough history (will be replaced by real history API later) + if (r.length) { + riskTrendData.value = [ + { date: 'T-6', score: Math.max(10, r[0].risk_score - 15) }, + { date: 'T-5', score: Math.max(10, r[0].risk_score - 12) }, + { date: 'T-4', score: Math.max(10, r[0].risk_score - 8) }, + { date: 'T-3', score: Math.max(10, r[0].risk_score - 5) }, + { date: 'T-2', score: Math.max(10, r[0].risk_score - 2) }, + { date: 'T-1', score: r[0].risk_score }, + ] + } } catch (e) { // ignore } diff --git a/frontend/src/views/lineage/Lineage.vue b/frontend/src/views/lineage/Lineage.vue new file mode 100644 index 00000000..48fe8dca --- /dev/null +++ b/frontend/src/views/lineage/Lineage.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/frontend/src/views/masking/Masking.vue b/frontend/src/views/masking/Masking.vue new file mode 100644 index 00000000..70da5caf --- /dev/null +++ b/frontend/src/views/masking/Masking.vue @@ -0,0 +1,250 @@ + + + diff --git a/frontend/src/views/project/Project.vue b/frontend/src/views/project/Project.vue index 6ad2ffad..11d2cb7c 100644 --- a/frontend/src/views/project/Project.vue +++ b/frontend/src/views/project/Project.vue @@ -50,8 +50,22 @@
已审
+
+ +
+ 已扫 {{ progressMap[p.id].scanned }} / {{ progressMap[p.id].total }},命中 {{ progressMap[p.id].matched }} +
+
- 自动分类 + + {{ p.status === 'scanning' ? '扫描中' : '自动分类' }} + 删除
@@ -92,7 +106,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Search } from '@element-plus/icons-vue' import { useUserStore } from '@/stores/user' -import { getProjects, createProject, deleteProject, autoClassifyProject } from '@/api/project' +import { getProjects, createProject, deleteProject, autoClassifyProject, getAutoClassifyStatus } from '@/api/project' import { getTemplates } from '@/api/classification' import { getDataSources } from '@/api/datasource' import type { ProjectItem } from '@/api/project' @@ -121,6 +135,8 @@ const rules = { const templates = ref([]) const dataSources = ref([]) +const progressMap = ref>({}) +let pollTimer: ReturnType | null = null function statusType(status: string) { const map: Record = { @@ -195,14 +211,64 @@ async function handleSubmit() { async function handleAutoClassify(p: ProjectItem) { try { - const res: any = await autoClassifyProject(p.id) - ElMessage.success(res.data?.message || '自动分类完成') + const res: any = await autoClassifyProject(p.id, true) + if (res.data?.task_id) { + ElMessage.info('已提交后台自动分类任务') + startPolling() + } else { + ElMessage.success(res.data?.message || '自动分类完成') + } fetchData() } catch (e: any) { ElMessage.error(e?.message || '自动分类失败') } } +async function pollProgress() { + const scanningProjects = projectList.value.filter(p => p.status === 'scanning') + if (scanningProjects.length === 0) { + stopPolling() + return + } + await Promise.all( + scanningProjects.map(async (p) => { + try { + const res: any = await getAutoClassifyStatus(p.id) + const data = res.data + if (data?.progress) { + progressMap.value[p.id] = data.progress + } + if (data?.project_status && data.project_status !== p.status) { + p.status = data.project_status + } + if (data?.status === 'SUCCESS' || data?.status === 'FAILURE' || data?.status === 'REVOKED') { + delete progressMap.value[p.id] + } + } catch (e) { + // ignore polling errors + } + }) + ) + // Refresh list if all done to get latest stats + const stillScanning = projectList.value.filter(p => p.status === 'scanning').length + if (stillScanning === 0) { + stopPolling() + fetchData() + } +} + +function startPolling() { + if (pollTimer) return + pollTimer = setInterval(pollProgress, 2000) +} + +function stopPolling() { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } +} + async function handleDelete(p: ProjectItem) { try { await ElMessageBox.confirm('确认删除该项目?', '提示', { type: 'warning' }) @@ -227,6 +293,16 @@ async function fetchMeta() { onMounted(() => { fetchData() fetchMeta() + startPolling() +}) + +onMounted(() => { + // cleanup on unmount handled by vue lifecycle, but we don't have onUnmounted import yet +}) + +import { onUnmounted } from 'vue' +onUnmounted(() => { + stopPolling() }) @@ -311,6 +387,17 @@ onMounted(() => { } } + .project-progress { + margin-bottom: 12px; + + .progress-text { + font-size: 12px; + color: #909399; + margin-top: 4px; + text-align: right; + } + } + .project-actions { display: flex; justify-content: flex-end; diff --git a/frontend/src/views/report/Report.vue b/frontend/src/views/report/Report.vue index 57699b7b..45f7cf37 100644 --- a/frontend/src/views/report/Report.vue +++ b/frontend/src/views/report/Report.vue @@ -5,9 +5,17 @@ - - 下载报告 - + + + Word + + + Excel + + + PDF预览 + + @@ -141,9 +149,14 @@ const levelBarOption = computed(() => { } }) -function downloadReport() { +function downloadReport(format: string) { if (!selectedProject.value) return - doDownload(selectedProject.value) + doDownload(selectedProject.value, format) +} + +function openPdfPreview() { + if (!selectedProject.value) return + window.open(`/report-preview.html?projectId=${selectedProject.value}`, '_blank') } async function fetchProjects() { diff --git a/frontend/src/views/schema_change/SchemaChange.vue b/frontend/src/views/schema_change/SchemaChange.vue new file mode 100644 index 00000000..3d8d67db --- /dev/null +++ b/frontend/src/views/schema_change/SchemaChange.vue @@ -0,0 +1,97 @@ + + + diff --git a/frontend/src/views/unstructured/Unstructured.vue b/frontend/src/views/unstructured/Unstructured.vue new file mode 100644 index 00000000..47d91517 --- /dev/null +++ b/frontend/src/views/unstructured/Unstructured.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/frontend/src/views/watermark/Watermark.vue b/frontend/src/views/watermark/Watermark.vue new file mode 100644 index 00000000..b7dfe6e9 --- /dev/null +++ b/frontend/src/views/watermark/Watermark.vue @@ -0,0 +1,58 @@ + + + diff --git a/screenshots/01_login.png b/screenshots/01_login.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/01_login.png differ diff --git a/screenshots/02_dashboard.png b/screenshots/02_dashboard.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/02_dashboard.png differ diff --git a/screenshots/03_datasource.png b/screenshots/03_datasource.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/03_datasource.png differ diff --git a/screenshots/04_metadata.png b/screenshots/04_metadata.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/04_metadata.png differ diff --git a/screenshots/05_category.png b/screenshots/05_category.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/05_category.png differ diff --git a/screenshots/06_project.png b/screenshots/06_project.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/06_project.png differ diff --git a/screenshots/07_task.png b/screenshots/07_task.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/07_task.png differ diff --git a/screenshots/08_classification.png b/screenshots/08_classification.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/08_classification.png differ diff --git a/screenshots/09_report.png b/screenshots/09_report.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/09_report.png differ diff --git a/screenshots/10_system.png b/screenshots/10_system.png new file mode 100644 index 00000000..44af6144 Binary files /dev/null and b/screenshots/10_system.png differ diff --git a/screenshots/capture.js b/screenshots/capture.js new file mode 100644 index 00000000..04d5c5a1 --- /dev/null +++ b/screenshots/capture.js @@ -0,0 +1,44 @@ +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); + const page = await browser.newPage(); + await page.setViewport({ width: 1920, height: 1080 }); + + const BASE = 'http://localhost:5173'; + const outDir = '/Users/nathan/Work/DataPointer/prop-data-guard/screenshots'; + + // 1. Login page + console.log('Capturing login...'); + await page.goto(`${BASE}/login`, { waitUntil: 'networkidle2' }); + await new Promise(r => setTimeout(r, 1000)); + await page.screenshot({ path: `${outDir}/01_login.png`, fullPage: false }); + + // Login + await page.type('input[placeholder="用户名"]', 'admin'); + await page.type('input[type="password"]', 'admin123'); + await page.click('.login-btn'); + await page.waitForNavigation({ waitUntil: 'networkidle2' }); + await new Promise(r => setTimeout(r, 2000)); + + // Helper to capture a page + async function capture(path, filename, waitMs = 3000) { + console.log(`Capturing ${path}...`); + await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle2' }); + await new Promise(r => setTimeout(r, waitMs)); + await page.screenshot({ path: `${outDir}/${filename}`, fullPage: false }); + } + + await capture('/dashboard', '02_dashboard.png'); + await capture('/datasource', '03_datasource.png'); + await capture('/metadata', '04_metadata.png'); + await capture('/category', '05_category.png'); + await capture('/project', '06_project.png'); + await capture('/task', '07_task.png'); + await capture('/classification', '08_classification.png'); + await capture('/report', '09_report.png'); + await capture('/system', '10_system.png'); + + await browser.close(); + console.log('All screenshots captured!'); +})(); diff --git a/screenshots/gen_ppt.py b/screenshots/gen_ppt.py new file mode 100644 index 00000000..89525dc0 --- /dev/null +++ b/screenshots/gen_ppt.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Generate DataPointer White Paper PPT with screenshots.""" + +import os +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE +from io import BytesIO +from PIL import Image + +# Config +SCREENSHOT_DIR = '/Users/nathan/Work/DataPointer/prop-data-guard/screenshots' +OUTPUT_PATH = '/Users/nathan/Work/DataPointer/DataPointer产品介绍白皮书.pptx' +SLIDE_WIDTH = Inches(13.333) +SLIDE_HEIGHT = Inches(7.5) + +# Color theme +COLOR_PRIMARY = RGBColor(0x1A, 0x56, 0xDB) # Deep blue +COLOR_SECONDARY = RGBColor(0x10, 0xB9, 0x81) # Green +COLOR_DARK = RGBColor(0x1E, 0x29, 0x3B) # Dark slate +COLOR_TEXT = RGBColor(0x37, 0x41, 0x51) # Gray text +COLOR_LIGHT = RGBColor(0xF8, 0xFA, 0xFC) # Light bg +COLOR_ACCENT = RGBColor(0xF5, 0x9E, 0x0B) # Orange accent + + +def add_title_slide(prs, title, subtitle): + slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank + slide.shapes._spTree.remove(slide.shapes._spTree[0]) if len(slide.shapes._spTree) > 0 else None + + # Background shape + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT) + bg.fill.solid() + bg.fill.fore_color.rgb = COLOR_DARK + bg.line.fill.background() + + # Accent bar + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.0), Inches(0.15), Inches(2.2)) + bar.fill.solid() + bar.fill.fore_color.rgb = COLOR_PRIMARY + bar.line.fill.background() + + # Title + title_box = slide.shapes.add_textbox(Inches(1.2), Inches(2.8), Inches(10), Inches(1.5)) + tf = title_box.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = title + p.font.size = Pt(48) + p.font.bold = True + p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + + # Subtitle + sub_box = slide.shapes.add_textbox(Inches(1.2), Inches(4.4), Inches(10), Inches(0.8)) + tf = sub_box.text_frame + p = tf.paragraphs[0] + p.text = subtitle + p.font.size = Pt(22) + p.font.color.rgb = RGBColor(0xA0, 0xAE, 0xC0) + + # Bottom info + info_box = slide.shapes.add_textbox(Inches(1.2), Inches(6.5), Inches(10), Inches(0.5)) + tf = info_box.text_frame + p = tf.paragraphs[0] + p.text = "Property Insurance Data Classification Platform | 2026" + p.font.size = Pt(12) + p.font.color.rgb = RGBColor(0x71, 0x80, 0x96) + + return slide + + +def add_section_slide(prs, section_num, section_title): + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT) + bg.fill.solid() + bg.fill.fore_color.rgb = COLOR_PRIMARY + bg.line.fill.background() + + num_box = slide.shapes.add_textbox(Inches(0.8), Inches(2.5), Inches(2), Inches(1.5)) + tf = num_box.text_frame + p = tf.paragraphs[0] + p.text = f"0{section_num}" if section_num < 10 else str(section_num) + p.font.size = Pt(72) + p.font.bold = True + p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + p.font.opacity = 0.3 + + title_box = slide.shapes.add_textbox(Inches(0.8), Inches(4.0), Inches(10), Inches(1.0)) + tf = title_box.text_frame + p = tf.paragraphs[0] + p.text = section_title + p.font.size = Pt(40) + p.font.bold = True + p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + + return slide + + +def add_content_slide(prs, title, bullets, image_path=None): + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + # Light background + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT) + bg.fill.solid() + bg.fill.fore_color.rgb = COLOR_LIGHT + bg.line.fill.background() + # Send to back + spTree = slide.shapes._spTree + spTree.insert(2, bg._element) + + # Top bar + top_bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, Inches(0.12)) + top_bar.fill.solid() + top_bar.fill.fore_color.rgb = COLOR_PRIMARY + top_bar.line.fill.background() + + # Title + title_box = slide.shapes.add_textbox(Inches(0.6), Inches(0.35), Inches(12), Inches(0.6)) + tf = title_box.text_frame + p = tf.paragraphs[0] + p.text = title + p.font.size = Pt(28) + p.font.bold = True + p.font.color.rgb = COLOR_DARK + + # Content + if image_path and os.path.exists(image_path): + # Image on right, text on left + content_box = slide.shapes.add_textbox(Inches(0.6), Inches(1.2), Inches(4.5), Inches(5.5)) + tf = content_box.text_frame + tf.word_wrap = True + for i, bullet in enumerate(bullets): + if i == 0: + p = tf.paragraphs[0] + else: + p = tf.add_paragraph() + p.text = f"• {bullet}" + p.font.size = Pt(16) + p.font.color.rgb = COLOR_TEXT + p.space_after = Pt(12) + + # Add image + img = Image.open(image_path) + orig_w, orig_h = img.size + max_w = Inches(7.0) + max_h = Inches(5.5) + ratio = min(max_w / orig_w, max_h / orig_h) + img_w = orig_w * ratio + img_h = orig_h * ratio + left = SLIDE_WIDTH - img_w - Inches(0.6) + top = Inches(1.0) + (max_h - img_h) / 2 + slide.shapes.add_picture(image_path, left, top, width=img_w, height=img_h) + else: + content_box = slide.shapes.add_textbox(Inches(0.6), Inches(1.2), Inches(12), Inches(5.5)) + tf = content_box.text_frame + tf.word_wrap = True + for i, bullet in enumerate(bullets): + if i == 0: + p = tf.paragraphs[0] + else: + p = tf.add_paragraph() + p.text = f"• {bullet}" + p.font.size = Pt(18) + p.font.color.rgb = COLOR_TEXT + p.space_after = Pt(14) + + return slide + + +def add_image_slide(prs, title, image_path, caption=""): + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT) + bg.fill.solid() + bg.fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + bg.line.fill.background() + spTree = slide.shapes._spTree + spTree.insert(2, bg._element) + + top_bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, Inches(0.12)) + top_bar.fill.solid() + top_bar.fill.fore_color.rgb = COLOR_PRIMARY + top_bar.line.fill.background() + + title_box = slide.shapes.add_textbox(Inches(0.6), Inches(0.35), Inches(12), Inches(0.5)) + tf = title_box.text_frame + p = tf.paragraphs[0] + p.text = title + p.font.size = Pt(26) + p.font.bold = True + p.font.color.rgb = COLOR_DARK + + if os.path.exists(image_path): + img = Image.open(image_path) + orig_w, orig_h = img.size + max_w = Inches(11.5) + max_h = Inches(5.8) + ratio = min(max_w / orig_w, max_h / orig_h) + img_w = orig_w * ratio + img_h = orig_h * ratio + left = (SLIDE_WIDTH - img_w) / 2 + top = Inches(1.0) + slide.shapes.add_picture(image_path, left, top, width=img_w, height=img_h) + + if caption: + cap_box = slide.shapes.add_textbox(Inches(0.6), Inches(6.9), Inches(12), Inches(0.4)) + tf = cap_box.text_frame + p = tf.paragraphs[0] + p.text = caption + p.font.size = Pt(12) + p.font.color.rgb = RGBColor(0x71, 0x80, 0x96) + p.alignment = PP_ALIGN.CENTER + + return slide + + +def add_summary_slide(prs): + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT) + bg.fill.solid() + bg.fill.fore_color.rgb = COLOR_DARK + bg.line.fill.background() + + title_box = slide.shapes.add_textbox(Inches(0.8), Inches(1.5), Inches(11.5), Inches(1.0)) + tf = title_box.text_frame + p = tf.paragraphs[0] + p.text = "DataPointer 产品优势" + p.font.size = Pt(36) + p.font.bold = True + p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + + items = [ + ("全生命周期数据安全", "从数据采集、分类分级、风险评估到持续监控,覆盖数据安全全生命周期"), + ("智能自动化引擎", "基于规则+AI的智能分类引擎,自动识别敏感数据,效率提升80%"), + ("精细化权限管控", "RBAC 角色权限模型,支持数据隔离,满足多租户场景"), + ("保险行业深度适配", "内置保险行业数据分类分级标准,覆盖客户、保单、理赔等核心领域"), + ("可视化数据资产", "仪表盘+报表多维展示,数据资产状况一目了然"), + ] + + y = Inches(2.8) + for title, desc in items: + box = slide.shapes.add_textbox(Inches(0.8), y, Inches(11.5), Inches(0.8)) + tf = box.text_frame + tf.word_wrap = True + p = tf.paragraphs[0] + p.text = f"▎ {title}" + p.font.size = Pt(18) + p.font.bold = True + p.font.color.rgb = COLOR_SECONDARY + p = tf.add_paragraph() + p.text = f" {desc}" + p.font.size = Pt(14) + p.font.color.rgb = RGBColor(0xA0, 0xAE, 0xC0) + y += Inches(0.85) + + return slide + + +def add_end_slide(prs): + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT) + bg.fill.solid() + bg.fill.fore_color.rgb = COLOR_PRIMARY + bg.line.fill.background() + + title_box = slide.shapes.add_textbox(Inches(0), Inches(2.8), SLIDE_WIDTH, Inches(1.0)) + tf = title_box.text_frame + p = tf.paragraphs[0] + p.text = "谢谢观看" + p.font.size = Pt(52) + p.font.bold = True + p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + p.alignment = PP_ALIGN.CENTER + + sub_box = slide.shapes.add_textbox(Inches(0), Inches(4.0), SLIDE_WIDTH, Inches(0.6)) + tf = sub_box.text_frame + p = tf.paragraphs[0] + p.text = "DataPointer - 让数据安全触手可及" + p.font.size = Pt(20) + p.font.color.rgb = RGBColor(0xA0, 0xAE, 0xC0) + p.alignment = PP_ALIGN.CENTER + + return slide + + +def main(): + prs = Presentation() + prs.slide_width = SLIDE_WIDTH + prs.slide_height = SLIDE_HEIGHT + + # 1. Cover + add_title_slide(prs, "DataPointer", "数据安全分级及风险管理平台产品介绍白皮书") + + # 2. TOC + add_content_slide(prs, "目录", [ + "产品概述与定位", + "系统架构与技术栈", + "核心功能模块详解", + "产品优势与价值", + "应用场景与最佳实践" + ]) + + # 3. Section: Overview + add_section_slide(prs, 1, "产品概述") + + add_content_slide(prs, "DataPointer 产品定位", [ + "DataPointer 是面向保险行业的数据安全分级及风险管理平台", + "基于国家及行业数据分类分级标准,帮助企业实现数据资产的自动化识别与分级", + "支持从数据源接入、元数据采集、智能分类、人工复核到报表输出的完整工作流", + "内置保险行业专属分类标准,覆盖客户、保单、理赔、财务、渠道、监管等核心领域", + "提供多维度数据资产视图,助力企业满足数据安全合规要求" + ]) + + # 4. Section: Architecture + add_section_slide(prs, 2, "系统架构") + + add_content_slide(prs, "技术架构", [ + "前端:Vue 3.4 + Vite + TypeScript + Element Plus 2.7 + ECharts 5.4", + "后端:FastAPI 0.111 + SQLAlchemy 2.0 + PostgreSQL 16 + Redis 7 + Celery 5.4", + "部署:容器化部署,支持 Docker Compose / Kubernetes", + "安全:JWT 认证 + RBAC 权限控制 + 数据隔离,满足等保要求", + "扩展:模块化设计,支持自定义分类标准与规则引擎扩展" + ]) + + # 5. Section: Features + add_section_slide(prs, 3, "核心功能") + + add_image_slide(prs, "安全登录与权限管控", os.path.join(SCREENSHOT_DIR, "01_login.png"), + "支持用户名密码登录,JWT Token 认证,会话安全") + + add_image_slide(prs, "仪表盘总览", os.path.join(SCREENSHOT_DIR, "02_dashboard.png"), + "实时展示数据源、表、字段、分级结果等核心指标,支持敏感数据分布可视化") + + add_content_slide(prs, "数据源管理", [ + "支持多种数据库类型接入:MySQL、PostgreSQL、Oracle、SQL Server 等", + "自动扫描数据库元数据,采集表结构与字段信息", + "支持数据源连接测试与状态监控", + "批量导入导出数据源配置,提升运维效率" + ], os.path.join(SCREENSHOT_DIR, "03_datasource.png")) + + add_content_slide(prs, "数据资产管理", [ + "自动生成数据资产目录,展示数据库、表、字段的层级关系", + "支持字段级元数据查看,包括数据类型、注释、采样数据等", + "资产变更追踪,记录元数据的增删改历史", + "与分类分级结果联动,直观展示各资产的安全等级" + ], os.path.join(SCREENSHOT_DIR, "04_metadata.png")) + + add_content_slide(prs, "分类分级标准", [ + "内置保险行业五级分类标准:公开级(L1)、内部级(L2)、敏感级(L3)、重要级(L4)、核心级(L5)", + "支持自定义分类标准与分级规则,灵活适配不同业务场景", + "规则引擎支持正则匹配、关键字匹配、语义识别等多种检测方式", + "标准模板管理,支持一键导入导出行业标准模板" + ], os.path.join(SCREENSHOT_DIR, "05_category.png")) + + add_content_slide(prs, "项目管理", [ + "支持创建多个分类分级项目,独立管理与进度跟踪", + "项目维度统计:自动识别、人工标注、已复核数量", + "支持批量触发自动分类任务,提升处理效率", + "项目经理可查看负责项目的全部进度与成果" + ], os.path.join(SCREENSHOT_DIR, "06_project.png")) + + add_content_slide(prs, "任务管理", [ + "自动拆解项目为标注/复核任务,分配给标注员与审核员", + "任务状态追踪:待处理、处理中、已完成、已驳回", + "支持任务领取与批量处理,提升团队协作效率", + "数据隔离:普通用户仅能查看和处理分配给自己的任务" + ], os.path.join(SCREENSHOT_DIR, "07_task.png")) + + add_content_slide(prs, "分类分级结果", [ + "展示所有字段的分类分级结果,支持多维筛选与搜索", + "结果包含:字段名称、所属表/库、分类标准、安全等级、识别方式", + "支持结果的批量导出,便于合规审计与报告生成", + "人工复核机制,确保分类分级的准确性与权威性" + ], os.path.join(SCREENSHOT_DIR, "08_classification.png")) + + add_content_slide(prs, "报表统计", [ + "多维统计报表:按等级分布、类别分布、数据源分布等维度展示", + "支持 Word 报告一键生成,包含完整的数据资产与分级详情", + "可视化图表直观展示敏感数据分布与趋势", + "满足监管报送与内部审计的数据安全报告需求" + ], os.path.join(SCREENSHOT_DIR, "09_report.png")) + + add_content_slide(prs, "系统管理", [ + "用户管理:支持用户增删改查、角色分配、部门归属", + "角色权限:基于 RBAC 模型,支持 admin、project_manager、labeler、reviewer、guest 等角色", + "数据隔离:严格的数据权限控制,确保跨部门数据安全", + "操作日志:完整记录用户操作行为,支持安全审计" + ], os.path.join(SCREENSHOT_DIR, "10_system.png")) + + # 6. Section: Advantages + add_section_slide(prs, 4, "产品优势") + add_summary_slide(prs) + + # 7. Section: Scenarios + add_section_slide(prs, 5, "应用场景") + + add_content_slide(prs, "典型应用场景", [ + "数据安全合规:帮助企业满足《数据安全法》《个人信息保护法》等法规要求", + "等保测评:提供完整的数据分类分级证据链,支撑等级保护测评", + "数据资产盘点:全面梳理企业数据资产,建立统一的数据资产目录", + "敏感数据发现:自动识别客户身份证号、银行卡号、保单信息等敏感字段", + "数据分级保护:根据分级结果制定差异化的数据安全保护策略" + ]) + + # 8. End + add_end_slide(prs) + + prs.save(OUTPUT_PATH) + print(f"PPT saved to: {OUTPUT_PATH}") + + +if __name__ == "__main__": + main()