diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..acd946f36 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Git +.git +.gitignore +.gitattributes + +# IDE +.idea +.vscode +*.swp +*.swo + +# Documentation +*.md +docs/ +LICENSE + +# Development +.venv +__pycache__ +*.pyc +*.pyo +.pytest_cache +.coverage +htmlcov/ + +# macOS +.DS_Store +._* + +# Frontend source (will be built from GitHub) +fcb-fronted/ +themes/ + +# Test files +test_*.py +tests/ + +# Data (should be mounted as volume) +data/ + +# GitHub +.github/ + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Claude +.claude/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 90cc86369..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -custom: https://gitlab.com/Vastsa/lanpicbed/-/raw/master/zb_users/upload/2023/02/6402263fb2ff4fddad51cb64fa66f4fb.png diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 000000000..48d5f81fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/images/img.png b/.github/images/img.png new file mode 100644 index 000000000..b366ebe37 Binary files /dev/null and b/.github/images/img.png differ diff --git a/.github/images/img_1.png b/.github/images/img_1.png new file mode 100644 index 000000000..482704eb6 Binary files /dev/null and b/.github/images/img_1.png differ diff --git a/.github/images/img_10.png b/.github/images/img_10.png new file mode 100644 index 000000000..72bad696e Binary files /dev/null and b/.github/images/img_10.png differ diff --git a/.github/images/img_11.png b/.github/images/img_11.png new file mode 100644 index 000000000..5ee656561 Binary files /dev/null and b/.github/images/img_11.png differ diff --git a/.github/images/img_12.png b/.github/images/img_12.png new file mode 100644 index 000000000..0e3465984 Binary files /dev/null and b/.github/images/img_12.png differ diff --git a/.github/images/img_13.png b/.github/images/img_13.png new file mode 100644 index 000000000..9af20fdbb Binary files /dev/null and b/.github/images/img_13.png differ diff --git a/.github/images/img_14.png b/.github/images/img_14.png new file mode 100644 index 000000000..e6ef72c29 Binary files /dev/null and b/.github/images/img_14.png differ diff --git a/.github/images/img_2.png b/.github/images/img_2.png new file mode 100644 index 000000000..1f202c9c9 Binary files /dev/null and b/.github/images/img_2.png differ diff --git a/.github/images/img_3.png b/.github/images/img_3.png new file mode 100644 index 000000000..a47750d41 Binary files /dev/null and b/.github/images/img_3.png differ diff --git a/.github/images/img_4.png b/.github/images/img_4.png new file mode 100644 index 000000000..99a995699 Binary files /dev/null and b/.github/images/img_4.png differ diff --git a/.github/images/img_5.png b/.github/images/img_5.png new file mode 100644 index 000000000..d426d5a7e Binary files /dev/null and b/.github/images/img_5.png differ diff --git a/.github/images/img_6.png b/.github/images/img_6.png new file mode 100644 index 000000000..16a8e5673 Binary files /dev/null and b/.github/images/img_6.png differ diff --git a/.github/images/img_7.png b/.github/images/img_7.png new file mode 100644 index 000000000..dd338b976 Binary files /dev/null and b/.github/images/img_7.png differ diff --git a/.github/images/img_8.png b/.github/images/img_8.png new file mode 100644 index 000000000..99f52387b Binary files /dev/null and b/.github/images/img_8.png differ diff --git a/.github/images/img_9.png b/.github/images/img_9.png new file mode 100644 index 000000000..812f10ba4 Binary files /dev/null and b/.github/images/img_9.png differ diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 000000000..a695ab6e6 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,65 @@ +name: Build and push Docker image + +on: + workflow_dispatch: + push: + branches: + - master + - dev + tags: + - 'v*' + +env: + REGISTRY_IMAGE: ${{ secrets.DOCKER_USERNAME }}/filecodebox + +jobs: + buildx: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} + type=raw,value=beta,enable={{is_default_branch}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + image=moby/buildkit:latest + network=host + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..d047b2b87 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,69 @@ +# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程 +# +name: Deploy VitePress site to Pages + +on: + # 在针对 `main` 分支的推送上运行。如果你 + # 使用 `master` 分支作为默认分支,请将其更改为 `master` + push: + branches: [ master ] + + # 允许你从 Actions 选项卡手动运行此工作流程 + workflow_dispatch: + +# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列 +# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成 +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # 构建工作 + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # 如果未启用 lastUpdated,则不需要 + - uses: pnpm/action-setup@v3 + with: + version: 9 + # - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: 'docs/pnpm-lock.yaml' + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + working-directory: docs + run: pnpm install + - name: Build with VitePress + working-directory: docs + run: pnpm run docs:build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # 部署工作 + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index d01595631..0cebc4ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ database.db .Python build/ develop-eggs/ -dist/ downloads/ eggs/ .eggs/ @@ -31,7 +30,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST - +.vite/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -80,7 +79,9 @@ docs/_build/ # PyBuilder .pybuilder/ target/ - +*.db +./filecodebox.db-shm +./filecodebox.db-wal # Jupyter Notebook .ipynb_checkpoints @@ -153,3 +154,12 @@ for_test.py data/.env .backup/ /cloc-1.64.exe + +# Ignore node_modules +node_modules/ + +AGENTS.md + +dist/ +# Frontend themes (built from GitHub during Docker build) +themes/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index baecefdff..28453323e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,56 @@ -FROM python:3.9.5-slim-buster -LABEL author="Lan" -LABEL email="vast@tom.com" -LABEL version="6" +# 第一阶段:构建前端主题 +FROM node:20-alpine AS frontend-builder + +RUN apk add --no-cache git python3 make g++ + +WORKDIR /build + +# 克隆并构建 2024 主题 +RUN git clone --depth 1 https://github.com/vastsa/FileCodeBoxFronted.git /build/fronted-2024 && \ + cd /build/fronted-2024 && \ + npm install && \ + npm run build +# 克隆并构建 2023 主题 +RUN git clone --depth 1 https://github.com/vastsa/FileCodeBoxFronted2023.git /build/fronted-2023 && \ + cd /build/fronted-2023 && \ + npm install --legacy-peer-deps && \ + npm run build + +# 第二阶段:构建最终镜像 +FROM python:3.12-slim-bookworm +LABEL author="Lan" +LABEL email="xzu@live.com" -COPY . /app -RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime -RUN echo 'Asia/Shanghai' >/etc/timezone WORKDIR /app -RUN pip install -r requirements.txt + +# 复制项目文件(通过 .dockerignore 排除不必要的文件) +COPY . . + +# 设置时区 +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo 'Asia/Shanghai' > /etc/timezone + +# 从构建阶段复制编译好的前端主题 +COPY --from=frontend-builder /build/fronted-2024/dist ./themes/2024 +COPY --from=frontend-builder /build/fronted-2023/dist ./themes/2023 + +# 安装 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 环境变量配置 +ENV HOST="0.0.0.0" \ + PORT=12345 \ + WORKERS=1 \ + LOG_LEVEL="info" + EXPOSE 12345 -CMD ["python","main.py"] \ No newline at end of file + +# 生产环境启动命令 +CMD uvicorn main:app \ + --host $HOST \ + --port $PORT \ + --workers $WORKERS \ + --log-level $LOG_LEVEL \ + --proxy-headers \ + --forwarded-allow-ips "*" \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..034e84803 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 000000000..e4a73e5c2 --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/13 20:43 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/apps/admin/__init__.py b/apps/admin/__init__.py new file mode 100644 index 000000000..c131dcef4 --- /dev/null +++ b/apps/admin/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/14 14:38 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/apps/admin/dependencies.py b/apps/admin/dependencies.py new file mode 100644 index 000000000..205779bbb --- /dev/null +++ b/apps/admin/dependencies.py @@ -0,0 +1,132 @@ +# @Time : 2023/8/15 17:43 +# @Author : Lan +# @File : depends.py +# @Software: PyCharm +from fastapi import Header, HTTPException, Depends +from fastapi.requests import Request +import base64 +import hmac +import json +import time +from core.settings import settings +from apps.admin.services import FileService, ConfigService, LocalFileService + + +def create_token(data: dict, expires_in: int = 3600 * 24 * 30) -> str: + """ + 创建JWT token + :param data: 数据负载 + :param expires_in: 过期时间(秒) + """ + header = base64.b64encode( + json.dumps({"alg": "HS256", "typ": "JWT"}).encode() + ).decode() + payload = base64.b64encode( + json.dumps({**data, "exp": int(time.time()) + expires_in}).encode() + ).decode() + + signature = hmac.new( + settings.admin_token.encode(), f"{header}.{payload}".encode(), "sha256" + ).digest() + signature = base64.b64encode(signature).decode() + + return f"{header}.{payload}.{signature}" + + +def verify_token(token: str) -> dict: + """ + 验证JWT token + :param token: JWT token + :return: 解码后的数据 + """ + try: + header_b64, payload_b64, signature_b64 = token.split(".") + + # 验证签名 + expected_signature = hmac.new( + settings.admin_token.encode(), + f"{header_b64}.{payload_b64}".encode(), + "sha256", + ).digest() + expected_signature_b64 = base64.b64encode(expected_signature).decode() + + if not hmac.compare_digest(signature_b64, expected_signature_b64): + raise ValueError("无效的签名") + + # 解码payload + payload = json.loads(base64.b64decode(payload_b64)) + + # 检查是否过期 + if payload.get("exp", 0) < time.time(): + raise ValueError("token已过期") + + return payload + except Exception as e: + raise ValueError(f"token验证失败: {str(e)}") + + +def _extract_bearer_token(authorization: str) -> str: + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="未授权或授权校验失败") + token = authorization.split(" ", 1)[1].strip() + if not token: + raise HTTPException(status_code=401, detail="未授权或授权校验失败") + return token + + +def _require_admin_payload(authorization: str) -> dict: + token = _extract_bearer_token(authorization) + try: + payload = verify_token(token) + except ValueError as e: + raise HTTPException(status_code=401, detail=str(e)) + if not payload.get("is_admin", False): + raise HTTPException(status_code=401, detail="未授权或授权校验失败") + return payload + + +ADMIN_PUBLIC_ENDPOINTS = {("POST", "/admin/login")} + + +async def admin_required( + authorization: str = Header(default=None), request: Request = None +): + """ + 验证管理员权限 + """ + if request and (request.method, request.url.path) in ADMIN_PUBLIC_ENDPOINTS: + return None + return _require_admin_payload(authorization) + + +async def share_required_login(authorization: str = Header(default=None)): + """ + 验证分享上传权限 + + 当settings.openUpload为False时,要求用户必须登录并具有管理员权限 + 当settings.openUpload为True时,允许游客上传 + + :param authorization: 认证头信息 + :param request: 请求对象 + :return: 验证结果 + """ + if not settings.openUpload: + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException( + status_code=403, detail="本站未开启游客上传,如需上传请先登录后台" + ) + _require_admin_payload(authorization) + + return True + + +async def get_file_service(): + return FileService() + + +async def get_config_service(): + return ConfigService() + + +async def get_local_file_service(): + return LocalFileService() diff --git a/apps/admin/schemas.py b/apps/admin/schemas.py new file mode 100644 index 000000000..2567f7cf5 --- /dev/null +++ b/apps/admin/schemas.py @@ -0,0 +1,31 @@ +import datetime +from typing import Optional, Union + +from pydantic import BaseModel + + +class IDData(BaseModel): + id: int + + +class ShareItem(BaseModel): + expire_value: int + expire_style: str = "day" + filename: str + + +class DeleteItem(BaseModel): + filename: str + + +class LoginData(BaseModel): + password: str + + +class UpdateFileData(BaseModel): + id: int + code: Optional[str] = None + prefix: Optional[str] = None + suffix: Optional[str] = None + expired_at: Optional[Union[datetime.datetime, str]] = None + expired_count: Optional[int] = None diff --git a/apps/admin/services.py b/apps/admin/services.py new file mode 100644 index 000000000..ba927e32f --- /dev/null +++ b/apps/admin/services.py @@ -0,0 +1,158 @@ +import os +import time + +from core.response import APIResponse +from core.storage import FileStorageInterface, storages +from core.settings import settings +from apps.base.models import FileCodes, KeyValue, file_codes_pydantic +from apps.base.utils import get_expire_info, get_file_path_name +from fastapi import HTTPException +from core.settings import data_root +from core.utils import hash_password, is_password_hashed + + +class FileService: + def __init__(self): + self.file_storage: FileStorageInterface = storages[settings.file_storage]() + + async def delete_file(self, file_id: int): + file_code = await FileCodes.get(id=file_id) + await self.file_storage.delete_file(file_code) + await file_code.delete() + + async def list_files(self, page: int, size: int, keyword: str = ""): + offset = (page - 1) * size + files = ( + await FileCodes.filter(prefix__icontains=keyword).limit(size).offset(offset) + ) + total = await FileCodes.filter(prefix__icontains=keyword).count() + files_pydantic = [ + await file_codes_pydantic.from_tortoise_orm(f) for f in files + ] + return files_pydantic, total + + async def download_file(self, file_id: int): + file_code = await FileCodes.filter(id=file_id).first() + if not file_code: + raise HTTPException(status_code=404, detail="文件不存在") + if file_code.text: + return APIResponse(detail=file_code.text) + else: + return await self.file_storage.get_file_response(file_code) + + async def share_local_file(self, item): + local_file = LocalFileClass(item.filename) + if not await local_file.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + + text = await local_file.read() + expired_at, expired_count, used_count, code = await get_expire_info( + item.expire_value, item.expire_style + ) + path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(item) + + await self.file_storage.save_file(text, save_path) + + await FileCodes.create( + code=code, + prefix=prefix, + suffix=suffix, + uuid_file_name=uuid_file_name, + file_path=path, + size=local_file.size, + expired_at=expired_at, + expired_count=expired_count, + used_count=used_count, + ) + + return { + "code": code, + "name": local_file.file, + } + + +class ConfigService: + def get_config(self): + return dict(settings.items()) + + async def update_config(self, data: dict): + admin_token = data.get("admin_token") + if admin_token is None or admin_token == "": + raise HTTPException(status_code=400, detail="管理员密码不能为空") + + if not is_password_hashed(admin_token): + data["admin_token"] = hash_password(admin_token) + + for key, value in data.items(): + if key not in settings.default_config: + continue + if key in [ + "errorCount", + "errorMinute", + "max_save_seconds", + "onedrive_proxy", + "openUpload", + "port", + "s3_proxy", + "uploadCount", + "uploadMinute", + "uploadSize", + ]: + data[key] = int(value) + elif key in ["opacity"]: + data[key] = float(value) + else: + data[key] = value + + await KeyValue.filter(key="settings").update(value=data) + for k, v in data.items(): + settings.__setattr__(k, v) + + +class LocalFileService: + async def list_files(self): + files = [] + if not os.path.exists(data_root / "local"): + os.makedirs(data_root / "local") + for file in os.listdir(data_root / "local"): + local_file = LocalFileClass(file) + files.append({ + "file": local_file.file, + "ctime": local_file.ctime, + "size": local_file.size, + }) + return files + + async def delete_file(self, filename: str): + file = LocalFileClass(filename) + if await file.exists(): + await file.delete() + return "删除成功" + raise HTTPException(status_code=404, detail="文件不存在") + + +class LocalFileClass: + def __init__(self, file): + self.file = file + self.path = data_root / "local" / file + if os.path.exists(self.path): + self.ctime = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(self.path)) + ) + self.size = os.path.getsize(self.path) + else: + self.ctime = None + self.size = None + + async def read(self): + return open(self.path, "rb") + + async def write(self, data): + with open(self.path, "w") as f: + f.write(data) + + async def delete(self): + os.remove(self.path) + + async def exists(self): + return os.path.exists(self.path) diff --git a/apps/admin/views.py b/apps/admin/views.py new file mode 100644 index 000000000..9501c16f1 --- /dev/null +++ b/apps/admin/views.py @@ -0,0 +1,169 @@ +# @Time : 2023/8/14 14:38 +# @Author : Lan +# @File : views.py +# @Software: PyCharm +import datetime + +from fastapi import APIRouter, Depends, HTTPException +from apps.admin.services import FileService, ConfigService, LocalFileService +from apps.admin.dependencies import ( + admin_required, + get_file_service, + get_config_service, + get_local_file_service, +) +from apps.admin.schemas import IDData, ShareItem, DeleteItem, LoginData, UpdateFileData +from core.response import APIResponse +from apps.base.models import FileCodes, KeyValue +from apps.admin.dependencies import create_token +from core.settings import settings +from core.utils import get_now, verify_password + +admin_api = APIRouter( + prefix="/admin", tags=["管理"], dependencies=[Depends(admin_required)] +) + + +@admin_api.post("/login") +async def login(data: LoginData): + if not verify_password(data.password, settings.admin_token): + raise HTTPException(status_code=401, detail="密码错误") + + token = create_token({"is_admin": True}) + return APIResponse(detail={"token": token, "token_type": "Bearer"}) + + +@admin_api.get("/dashboard") +async def dashboard(): + all_codes = await FileCodes.all() + all_size = str(sum([code.size for code in all_codes])) + sys_start = await KeyValue.filter(key="sys_start").first() + now = await get_now() + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + yesterday_start = today_start - datetime.timedelta(days=1) + yesterday_end = today_start - datetime.timedelta(microseconds=1) + yesterday_codes = FileCodes.filter( + created_at__gte=yesterday_start, created_at__lte=yesterday_end + ) + today_codes = FileCodes.filter(created_at__gte=today_start) + return APIResponse( + detail={ + "totalFiles": len(all_codes), + "storageUsed": all_size, + "sysUptime": sys_start.value, + "yesterdayCount": await yesterday_codes.count(), + "yesterdaySize": str(sum([code.size for code in await yesterday_codes])), + "todayCount": await today_codes.count(), + "todaySize": str(sum([code.size for code in await today_codes])), + } + ) + + +@admin_api.delete("/file/delete") +async def file_delete( + data: IDData, + file_service: FileService = Depends(get_file_service), +): + await file_service.delete_file(data.id) + return APIResponse() + + +@admin_api.get("/file/list") +async def file_list( + page: int = 1, + size: int = 10, + keyword: str = "", + file_service: FileService = Depends(get_file_service), +): + files, total = await file_service.list_files(page, size, keyword) + return APIResponse( + detail={ + "page": page, + "size": size, + "data": files, + "total": total, + } + ) + + +@admin_api.get("/config/get") +async def get_config( + config_service: ConfigService = Depends(get_config_service), +): + return APIResponse(detail=config_service.get_config()) + + +@admin_api.patch("/config/update") +async def update_config( + data: dict, + config_service: ConfigService = Depends(get_config_service), +): + data.pop("themesChoices") + await config_service.update_config(data) + return APIResponse() + + +@admin_api.get("/file/download") +async def file_download( + id: int, + file_service: FileService = Depends(get_file_service), +): + file_content = await file_service.download_file(id) + return file_content + + +@admin_api.get("/local/lists") +async def get_local_lists( + local_file_service: LocalFileService = Depends(get_local_file_service), +): + files = await local_file_service.list_files() + return APIResponse(detail=files) + + +@admin_api.delete("/local/delete") +async def delete_local_file( + item: DeleteItem, + local_file_service: LocalFileService = Depends(get_local_file_service), +): + result = await local_file_service.delete_file(item.filename) + return APIResponse(detail=result) + + +@admin_api.post("/local/share") +async def share_local_file( + item: ShareItem, + file_service: FileService = Depends(get_file_service), +): + share_info = await file_service.share_local_file(item) + return APIResponse(detail=share_info) + + +@admin_api.patch("/file/update") +async def update_file( + data: UpdateFileData, +): + file_code = await FileCodes.filter(id=data.id).first() + if not file_code: + raise HTTPException(status_code=404, detail="文件不存在") + update_data = {} + + if data.code is not None and data.code != file_code.code: + # 判断code是否存在 + if await FileCodes.filter(code=data.code).first(): + raise HTTPException(status_code=400, detail="code已存在") + update_data["code"] = data.code + if data.prefix is not None and data.prefix != file_code.prefix: + update_data["prefix"] = data.prefix + if data.suffix is not None and data.suffix != file_code.suffix: + update_data["suffix"] = data.suffix + if ( + data.expired_at is not None + and data.expired_at != "" + and data.expired_at != file_code.expired_at + ): + update_data["expired_at"] = data.expired_at + if data.expired_count is not None and data.expired_count != file_code.expired_count: + update_data["expired_count"] = data.expired_count + + await file_code.update_from_dict(update_data).save() + return APIResponse(detail="更新成功") diff --git a/apps/base/__init__.py b/apps/base/__init__.py new file mode 100644 index 000000000..e4a73e5c2 --- /dev/null +++ b/apps/base/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/13 20:43 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/apps/base/dependencies.py b/apps/base/dependencies.py new file mode 100644 index 000000000..577a2dcc0 --- /dev/null +++ b/apps/base/dependencies.py @@ -0,0 +1,45 @@ +from typing import Dict, Union +from datetime import datetime, timedelta +from fastapi import HTTPException, Request + + +class IPRateLimit: + def __init__(self, count: int, minutes: int): + self.ips: Dict[str, Dict[str, Union[int, datetime]]] = {} + self.count = count + self.minutes = minutes + + def check_ip(self, ip: str) -> bool: + if ip in self.ips: + ip_info = self.ips[ip] + if ip_info["count"] >= self.count: + if ip_info["time"] + timedelta(minutes=self.minutes) > datetime.now(): + return False + self.ips.pop(ip) + return True + + def add_ip(self, ip: str) -> int: + ip_info = self.ips.get(ip, {"count": 0, "time": datetime.now()}) + ip_info["count"] += 1 + ip_info["time"] = datetime.now() + self.ips[ip] = ip_info + return ip_info["count"] + + async def remove_expired_ip(self) -> None: + now = datetime.now() + expiration = timedelta(minutes=self.minutes) + self.ips = { + ip: info + for ip, info in self.ips.items() + if info["time"] + expiration >= now + } + + def __call__(self, request: Request) -> str: + ip = ( + request.headers.get("X-Real-IP") + or request.headers.get("X-Forwarded-For") + or request.client.host + ) + if not self.check_ip(ip): + raise HTTPException(status_code=423, detail="请求次数过多,请稍后再试") + return ip diff --git a/apps/base/migrations/migrations_001.py b/apps/base/migrations/migrations_001.py new file mode 100644 index 000000000..2a1196033 --- /dev/null +++ b/apps/base/migrations/migrations_001.py @@ -0,0 +1,52 @@ +from tortoise import connections + + +async def create_file_codes_table(): + conn = connections.get("default") + await conn.execute_script( + """ + CREATE TABLE IF NOT EXISTS filecodes + ( + id INTEGER not null + primary key autoincrement, + code VARCHAR(255) not null + unique, + prefix VARCHAR(255) default '' not null, + suffix VARCHAR(255) default '' not null, + uuid_file_name VARCHAR(255), + file_path VARCHAR(255), + size INT default 0 not null, + text TEXT, + expired_at TIMESTAMP, + expired_count INT default 0 not null, + used_count INT default 0 not null, + created_at TIMESTAMP default CURRENT_TIMESTAMP not null + ); + CREATE INDEX IF NOT EXISTS idx_filecodes_code_1c7ee7 + on filecodes (code); + """ + ) + + +async def create_key_value_table(): + conn = connections.get("default") + await conn.execute_script( + """ + CREATE TABLE IF NOT EXISTS keyvalue + ( + id INTEGER not null + primary key autoincrement, + key VARCHAR(255) not null + unique, + value JSON, + created_at TIMESTAMP default CURRENT_TIMESTAMP not null + ); + CREATE INDEX IF NOT EXISTS idx_keyvalue_key_eab890 + on keyvalue (key); + """ + ) + + +async def migrate(): + await create_file_codes_table() + await create_key_value_table() diff --git a/apps/base/migrations/migrations_002.py b/apps/base/migrations/migrations_002.py new file mode 100644 index 000000000..a12069ceb --- /dev/null +++ b/apps/base/migrations/migrations_002.py @@ -0,0 +1,28 @@ +from tortoise import connections + + +async def create_upload_chunk_and_update_file_codes_table(): + conn = connections.get("default") + await conn.execute_script( + """ + ALTER TABLE "filecodes" ADD "file_hash" VARCHAR(128); + ALTER TABLE "filecodes" ADD "is_chunked" BOOL NOT NULL DEFAULT False; + ALTER TABLE "filecodes" ADD "upload_id" VARCHAR(128); + CREATE TABLE "uploadchunk" ( + id INTEGER not null primary key autoincrement, + "upload_id" VARCHAR(36) NOT NULL, + "chunk_index" INT NOT NULL, + "chunk_hash" VARCHAR(128) NOT NULL, + "total_chunks" INT NOT NULL, + "file_size" BIGINT NOT NULL, + "chunk_size" INT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL, + "file_name" VARCHAR(255) NOT NULL, + "completed" BOOL NOT NULL + ); + """ + ) + + +async def migrate(): + await create_upload_chunk_and_update_file_codes_table() diff --git a/apps/base/migrations/migrations_003.py b/apps/base/migrations/migrations_003.py new file mode 100644 index 000000000..ada2286b0 --- /dev/null +++ b/apps/base/migrations/migrations_003.py @@ -0,0 +1,26 @@ +from tortoise import connections + + +async def create_presign_upload_session_table(): + conn = connections.get("default") + await conn.execute_script( + """ + CREATE TABLE IF NOT EXISTS presignuploadsession ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + upload_id VARCHAR(36) NOT NULL UNIQUE, + file_name VARCHAR(255) NOT NULL, + file_size BIGINT NOT NULL, + save_path VARCHAR(512) NOT NULL, + mode VARCHAR(10) NOT NULL, + expire_value INT NOT NULL DEFAULT 1, + expire_style VARCHAR(20) NOT NULL DEFAULT 'day', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_presignuploadsession_upload_id ON presignuploadsession (upload_id); + """ + ) + + +async def migrate(): + await create_presign_upload_session_table() diff --git a/apps/base/migrations/migrations_004.py b/apps/base/migrations/migrations_004.py new file mode 100644 index 000000000..69f06fd77 --- /dev/null +++ b/apps/base/migrations/migrations_004.py @@ -0,0 +1,14 @@ +from tortoise import connections + + +async def add_save_path_to_uploadchunk(): + conn = connections.get("default") + await conn.execute_script( + """ + ALTER TABLE uploadchunk ADD COLUMN save_path VARCHAR(512) NULL; + """ + ) + + +async def migrate(): + await add_save_path_to_uploadchunk() diff --git a/apps/base/migrations/migrations_005.py b/apps/base/migrations/migrations_005.py new file mode 100644 index 000000000..72c253d7f --- /dev/null +++ b/apps/base/migrations/migrations_005.py @@ -0,0 +1,72 @@ +from tortoise import connections + + +def _need_upgrade(columns: list[tuple]) -> bool: + for column in columns: + # PRAGMA table_info 返回 (cid, name, type, notnull, dflt_value, pk) + if column[1] == "size": + column_type = (column[2] or "").upper() + return "BIGINT" not in column_type + return False + + +async def migrate(): + conn = connections.get("default") + result = await conn.execute_query("PRAGMA table_info(filecodes)") + columns = result[1] if result and len(result) > 1 else [] + + if not columns or not _need_upgrade(columns): + return + + await conn.execute_script( + """ + BEGIN; + CREATE TABLE IF NOT EXISTS filecodes_new + ( + id INTEGER not null + primary key autoincrement, + code VARCHAR(255) not null + unique, + prefix VARCHAR(255) default '' not null, + suffix VARCHAR(255) default '' not null, + uuid_file_name VARCHAR(255), + file_path VARCHAR(255), + size BIGINT default 0 not null, + text TEXT, + expired_at TIMESTAMP, + expired_count INT default 0 not null, + used_count INT default 0 not null, + created_at TIMESTAMP default CURRENT_TIMESTAMP not null, + file_hash VARCHAR(128), + is_chunked BOOL default False not null, + upload_id VARCHAR(128) + ); + + INSERT INTO filecodes_new (id, code, prefix, suffix, uuid_file_name, file_path, size, text, + expired_at, expired_count, used_count, created_at, file_hash, + is_chunked, upload_id) + SELECT id, + code, + prefix, + suffix, + uuid_file_name, + file_path, + size, + text, + expired_at, + expired_count, + used_count, + created_at, + file_hash, + is_chunked, + upload_id + FROM filecodes; + + DROP TABLE filecodes; + ALTER TABLE filecodes_new + RENAME TO filecodes; + CREATE INDEX IF NOT EXISTS idx_filecodes_code_1c7ee7 + on filecodes (code); + COMMIT; + """ + ) diff --git a/apps/base/models.py b/apps/base/models.py new file mode 100644 index 000000000..2d299c4e2 --- /dev/null +++ b/apps/base/models.py @@ -0,0 +1,92 @@ +# @Time : 2023/8/13 20:43 +# @Author : Lan +# @File : models.py +# @Software: PyCharm +from typing import Optional + +from tortoise.models import Model +from tortoise.contrib.pydantic import pydantic_model_creator + +from tortoise import fields, models +from datetime import datetime +from core.utils import get_now + + +class FileCodes(models.Model): + id = fields.IntField(pk=True) + code = fields.CharField(max_length=255, unique=True, index=True) + prefix = fields.CharField(max_length=255, default="") + suffix = fields.CharField(max_length=255, default="") + uuid_file_name = fields.CharField(max_length=255, null=True) + file_path = fields.CharField(max_length=255, null=True) + size = fields.BigIntField(default=0) + text = fields.TextField(null=True) + expired_at = fields.DatetimeField(null=True) + expired_count = fields.IntField(default=0) + used_count = fields.IntField(default=0) + created_at = fields.DatetimeField(auto_now_add=True) + file_hash = fields.CharField(max_length=64, null=True) + is_chunked = fields.BooleanField(default=False) + upload_id = fields.CharField(max_length=36, null=True) + + async def is_expired(self): + if self.expired_at is None: + return False + if self.expired_at and self.expired_count < 0: + return self.expired_at < await get_now() + return self.expired_count <= 0 + + async def get_file_path(self): + return f"{self.file_path}/{self.uuid_file_name}" + + +class UploadChunk(models.Model): + id = fields.IntField(pk=True) + upload_id = fields.CharField(max_length=36, index=True) + chunk_index = fields.IntField() + chunk_hash = fields.CharField(max_length=64) + total_chunks = fields.IntField() + file_size = fields.BigIntField() + chunk_size = fields.IntField() + file_name = fields.CharField(max_length=255) + save_path = fields.CharField(max_length=512, null=True) + created_at = fields.DatetimeField(auto_now_add=True) + completed = fields.BooleanField(default=False) + + +class KeyValue(Model): + id: Optional[int] = fields.IntField(pk=True) + key: Optional[str] = fields.CharField( + max_length=255, description="键", index=True, unique=True + ) + value: Optional[str] = fields.JSONField(description="值", null=True) + created_at: Optional[datetime] = fields.DatetimeField( + auto_now_add=True, description="创建时间" + ) + + +class PresignUploadSession(models.Model): + """预签名上传会话模型""" + + id = fields.IntField(pk=True) + upload_id = fields.CharField(max_length=36, unique=True, index=True) + file_name = fields.CharField(max_length=255) + file_size = fields.BigIntField() + save_path = fields.CharField(max_length=512) + mode = fields.CharField(max_length=10) # "direct" 或 "proxy" + expire_value = fields.IntField(default=1) + expire_style = fields.CharField(max_length=20, default="day") + created_at = fields.DatetimeField(auto_now_add=True) + expires_at = fields.DatetimeField() # 会话过期时间 + + async def is_expired(self): + """检查会话是否已过期""" + return self.expires_at < await get_now() + + +file_codes_pydantic = pydantic_model_creator(FileCodes, name="FileCodes") +upload_chunk_pydantic = pydantic_model_creator(UploadChunk, name="UploadChunk") +key_value_pydantic = pydantic_model_creator(KeyValue, name="KeyValue") +presign_upload_session_pydantic = pydantic_model_creator( + PresignUploadSession, name="PresignUploadSession" +) diff --git a/apps/base/schemas.py b/apps/base/schemas.py new file mode 100644 index 000000000..efe8025fd --- /dev/null +++ b/apps/base/schemas.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import Optional + + +class SelectFileModel(BaseModel): + code: str + + +class InitChunkUploadModel(BaseModel): + file_name: str + chunk_size: int = 5 * 1024 * 1024 + file_size: int + file_hash: str + + +class CompleteUploadModel(BaseModel): + expire_value: int + expire_style: str + + +# 预签名上传相关模型 +class PresignUploadInitRequest(BaseModel): + """预签名上传初始化请求""" + file_name: str + file_size: int + expire_value: int = 1 + expire_style: str = "day" + + +class PresignUploadInitResponse(BaseModel): + """预签名上传初始化响应""" + upload_id: str + upload_url: str + mode: str # "direct" 或 "proxy" + save_path: str + expires_in: int # URL过期时间(秒) diff --git a/apps/base/utils.py b/apps/base/utils.py new file mode 100644 index 000000000..70c76f2ea --- /dev/null +++ b/apps/base/utils.py @@ -0,0 +1,116 @@ +import datetime +import hashlib +import os +import uuid +from urllib.parse import unquote + +from fastapi import UploadFile, HTTPException +from typing import Optional, Tuple + +from apps.base.dependencies import IPRateLimit +from apps.base.models import FileCodes +from core.settings import settings +from core.utils import ( + get_random_num, + get_random_string, + max_save_times_desc, + sanitize_filename, + get_now, +) + + +async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]: + today = await get_now() + storage_path = settings.storage_path.strip("/") + file_uuid = uuid.uuid4().hex + filename = await sanitize_filename(unquote(file.filename or "")) + base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}" + path = f"{storage_path}/{base_path}" if storage_path else base_path + prefix, suffix = os.path.splitext(filename) + save_path = f"{path}/{filename}" + return path, suffix, prefix, filename, save_path + + +async def get_chunk_file_path_name( + file_name: str, upload_id: str +) -> Tuple[str, str, str, str, str]: + today = await get_now() + storage_path = settings.storage_path.strip("/") + base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{upload_id}" + path = f"{storage_path}/{base_path}" if storage_path else base_path + prefix, suffix = os.path.splitext(file_name) + save_path = f"{path}/{prefix}{suffix}" + return path, suffix, prefix, file_name, save_path + + +async def get_expire_info( + expire_value: int, expire_style: str +) -> Tuple[Optional[datetime.datetime], int, int, str]: + expired_count, used_count = -1, 0 + now = await get_now() + code = None + + max_timedelta = ( + datetime.timedelta(seconds=settings.max_save_seconds) + if settings.max_save_seconds > 0 + else datetime.timedelta(days=7) + ) + detail = ( + await max_save_times_desc(settings.max_save_seconds) + if settings.max_save_seconds > 0 + else "7天" + ) + detail = f"限制最长时间为 {detail[0]},可换用其他方式" + + expire_styles = { + "day": lambda: now + datetime.timedelta(days=expire_value), + "hour": lambda: now + datetime.timedelta(hours=expire_value), + "minute": lambda: now + datetime.timedelta(minutes=expire_value), + "count": lambda: (now + datetime.timedelta(days=1), expire_value), + "forever": lambda: (None, None), + } + + if expire_style in expire_styles: + result = expire_styles[expire_style]() + if isinstance(result, tuple): + expired_at, extra = result + if expire_style == "count": + expired_count = extra + elif expire_style == "forever": + code = await get_random_code(style="string") + else: + expired_at = result + if expired_at and expired_at - now > max_timedelta: + raise HTTPException(status_code=403, detail=detail) + else: + expired_at = now + datetime.timedelta(days=1) + + if not code: + code = await get_random_code() + + return expired_at, expired_count, used_count, code + + +async def get_random_code(style: str = "num") -> str: + while True: + code = await get_random_num() if style == "num" else await get_random_string() + if not await FileCodes.filter(code=code).exists(): + return str(code) + + +async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) -> str: + sha = hashlib.sha256() + await file.seek(0) + while True: + chunk = await file.read(chunk_size) + if not chunk: + break + sha.update(chunk) + await file.seek(0) + return sha.hexdigest() + + +ip_limit = { + "error": IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute), + "upload": IPRateLimit(count=settings.uploadCount, minutes=settings.uploadMinute), +} diff --git a/apps/base/views.py b/apps/base/views.py new file mode 100644 index 000000000..453d5204c --- /dev/null +++ b/apps/base/views.py @@ -0,0 +1,688 @@ +import datetime +import hashlib +import os +import uuid +from datetime import timedelta +from urllib.parse import unquote + +from typing import Optional, Tuple, Union + +from fastapi import APIRouter, Form, UploadFile, File, Depends, HTTPException +from starlette import status + +from apps.admin.dependencies import share_required_login +from apps.base.models import FileCodes, UploadChunk, PresignUploadSession +from apps.base.schemas import ( + SelectFileModel, + InitChunkUploadModel, + CompleteUploadModel, + PresignUploadInitRequest, +) +from apps.base.utils import ( + get_expire_info, + get_file_path_name, + ip_limit, + get_chunk_file_path_name, +) +from core.response import APIResponse +from core.settings import settings +from core.storage import storages, FileStorageInterface +from core.utils import get_select_token, get_now, sanitize_filename + +share_api = APIRouter(prefix="/share", tags=["分享"]) + + +# ============ 公共服务层 ============ +class FileUploadService: + """统一的文件上传服务""" + + @staticmethod + async def generate_file_path( + file_name: str, upload_id: Optional[str] = None + ) -> tuple[str, str, str, str, str]: + """统一的路径生成""" + today = datetime.datetime.now() + storage_path = settings.storage_path.strip("/") + file_uuid = upload_id or uuid.uuid4().hex + filename = await sanitize_filename(unquote(file_name)) + base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}" + path = f"{storage_path}/{base_path}" if storage_path else base_path + prefix, suffix = os.path.splitext(filename) + save_path = f"{path}/{filename}" + return path, suffix, prefix, filename, save_path + + @staticmethod + async def create_file_record( + file_name: str, + file_size: int, + file_path: str, + expire_value: int, + expire_style: str, + **extra_fields, + ) -> str: + """统一创建FileCodes记录,返回code""" + expired_at, expired_count, used_count, code = await get_expire_info( + expire_value, expire_style + ) + prefix, suffix = os.path.splitext(file_name) + + await FileCodes.create( + code=code, + prefix=prefix, + suffix=suffix, + uuid_file_name=file_name, + file_path=file_path, + size=file_size, + expired_at=expired_at, + expired_count=expired_count, + used_count=used_count, + **extra_fields, + ) + return code + + +async def validate_file_size(file: UploadFile, max_size: int) -> int: + size = file.size + if size is None: + await file.seek(0, 2) # type: ignore[arg-type] + size = file.file.tell() + await file.seek(0) + if size > max_size: + max_size_mb = max_size / (1024 * 1024) + raise HTTPException( + status_code=403, detail=f"大小超过限制,最大为{max_size_mb:.2f} MB" + ) + return size + + +async def create_file_code(code, **kwargs): + return await FileCodes.create(code=code, **kwargs) + + +@share_api.post("/text/", dependencies=[Depends(share_required_login)]) +async def share_text( + text: str = Form(...), + expire_value: int = Form(default=1, gt=0), + expire_style: str = Form(default="day"), + ip: str = Depends(ip_limit["upload"]), +): + text_size = len(text.encode("utf-8")) + max_txt_size = 222 * 1024 + if text_size > max_txt_size: + raise HTTPException(status_code=403, detail="内容过多,建议采用文件形式") + + expired_at, expired_count, used_count, code = await get_expire_info( + expire_value, expire_style + ) + await create_file_code( + code=code, + text=text, + expired_at=expired_at, + expired_count=expired_count, + used_count=used_count, + size=len(text), + prefix="Text", + ) + ip_limit["upload"].add_ip(ip) + return APIResponse(detail={"code": code}) + + +@share_api.post("/file/", dependencies=[Depends(share_required_login)]) +async def share_file( + expire_value: int = Form(default=1, gt=0), + expire_style: str = Form(default="day"), + file: UploadFile = File(...), + ip: str = Depends(ip_limit["upload"]), +): + file_size = await validate_file_size(file, settings.uploadSize) + if expire_style not in settings.expireStyle: + raise HTTPException(status_code=400, detail="过期时间类型错误") + expired_at, expired_count, used_count, code = await get_expire_info( + expire_value, expire_style + ) + path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(file) + file_storage: FileStorageInterface = storages[settings.file_storage]() + await file_storage.save_file(file, save_path) + await create_file_code( + code=code, + prefix=prefix, + suffix=suffix, + uuid_file_name=uuid_file_name, + file_path=path, + size=file_size, + expired_at=expired_at, + expired_count=expired_count, + used_count=used_count, + ) + ip_limit["upload"].add_ip(ip) + return APIResponse(detail={"code": code, "name": file.filename}) + + +async def get_code_file_by_code( + code: str, check: bool = True +) -> Tuple[bool, Union[FileCodes, str]]: + file_code = await FileCodes.filter(code=code).first() + if not file_code: + return False, "文件不存在" + if await file_code.is_expired() and check: + return False, "文件已过期" + return True, file_code + + +async def update_file_usage(file_code: FileCodes) -> None: + file_code.used_count += 1 + if file_code.expired_count > 0: + file_code.expired_count -= 1 + await file_code.save() + + +@share_api.get("/select/") +async def get_code_file(code: str, ip: str = Depends(ip_limit["error"])): + file_storage: FileStorageInterface = storages[settings.file_storage]() + has, file_code = await get_code_file_by_code(code) + if not has: + ip_limit["error"].add_ip(ip) + return APIResponse(code=404, detail=file_code) + + assert isinstance(file_code, FileCodes) + await update_file_usage(file_code) + return await file_storage.get_file_response(file_code) + + +@share_api.post("/select/") +async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit["error"])): + file_storage: FileStorageInterface = storages[settings.file_storage]() + has, file_code = await get_code_file_by_code(data.code) + if not has: + ip_limit["error"].add_ip(ip) + return APIResponse(code=404, detail=file_code) + + assert isinstance(file_code, FileCodes) + await update_file_usage(file_code) + return APIResponse( + detail={ + "code": file_code.code, + "name": file_code.prefix + file_code.suffix, + "size": file_code.size, + "text": ( + file_code.text + if file_code.text is not None + else await file_storage.get_file_url(file_code) + ), + } + ) + + +@share_api.get("/download") +async def download_file(key: str, code: str, ip: str = Depends(ip_limit["error"])): + file_storage: FileStorageInterface = storages[settings.file_storage]() + if await get_select_token(code) != key: + ip_limit["error"].add_ip(ip) + raise HTTPException(status_code=403, detail="下载鉴权失败") + has, file_code = await get_code_file_by_code(code, False) + if not has: + return APIResponse(code=404, detail="文件不存在") + assert isinstance(file_code, FileCodes) + return ( + APIResponse(detail=file_code.text) + if file_code.text + else await file_storage.get_file_response(file_code) + ) + + +chunk_api = APIRouter(prefix="/chunk", tags=["切片"]) + + +@chunk_api.post("/upload/init/", dependencies=[Depends(share_required_login)]) +async def init_chunk_upload(data: InitChunkUploadModel): + # 服务端校验:根据 total_chunks * chunk_size 计算理论最大上传量 + total_chunks = (data.file_size + data.chunk_size - 1) // data.chunk_size + max_possible_size = total_chunks * data.chunk_size + if max_possible_size > settings.uploadSize: + max_size_mb = settings.uploadSize / (1024 * 1024) + raise HTTPException( + status_code=403, detail=f"文件大小超过限制,最大为 {max_size_mb:.2f} MB" + ) + + # # 秒传检查 + # existing = await FileCodes.filter(file_hash=data.file_hash).first() + # if existing: + # if await existing.is_expired(): + # file_storage: FileStorageInterface = storages[settings.file_storage]( + # ) + # await file_storage.delete_file(existing) + # await existing.delete() + # else: + # return APIResponse(detail={ + # "code": existing.code, + # "existed": True, + # "name": f'{existing.prefix}{existing.suffix}' + # }) + + # 断点续传:检查是否存在相同文件的未完成上传会话 + existing_session = await UploadChunk.filter( + chunk_hash=data.file_hash, + chunk_index=-1, + file_size=data.file_size, + file_name=data.file_name, + ).first() + + if existing_session: + if not existing_session.save_path: + await UploadChunk.filter(upload_id=existing_session.upload_id).delete() + else: + uploaded_chunks = await UploadChunk.filter( + upload_id=existing_session.upload_id, completed=True + ).values_list("chunk_index", flat=True) + return APIResponse( + detail={ + "existed": False, + "upload_id": existing_session.upload_id, + "chunk_size": existing_session.chunk_size, + "total_chunks": existing_session.total_chunks, + "uploaded_chunks": list(uploaded_chunks), + } + ) + + # 创建新的上传会话 + upload_id = uuid.uuid4().hex + _, _, _, _, save_path = await get_chunk_file_path_name(data.file_name, upload_id) + await UploadChunk.create( + upload_id=upload_id, + chunk_index=-1, + total_chunks=total_chunks, + file_size=data.file_size, + chunk_size=data.chunk_size, + chunk_hash=data.file_hash, + file_name=data.file_name, + save_path=save_path, + ) + return APIResponse( + detail={ + "existed": False, + "upload_id": upload_id, + "chunk_size": data.chunk_size, + "total_chunks": total_chunks, + "uploaded_chunks": [], + } + ) + + +@chunk_api.post( + "/upload/chunk/{upload_id}/{chunk_index}", + dependencies=[Depends(share_required_login)], +) +async def upload_chunk( + upload_id: str, + chunk_index: int, + chunk: UploadFile = File(...), +): + # 获取上传会话信息 + chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first() + if not chunk_info: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在") + + # 检查分片索引有效性 + if chunk_index < 0 or chunk_index >= chunk_info.total_chunks: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="无效的分片索引") + + # 检查是否已上传(支持断点续传) + existing_chunk = await UploadChunk.filter( + upload_id=upload_id, chunk_index=chunk_index, completed=True + ).first() + if existing_chunk: + return APIResponse( + detail={"chunk_hash": existing_chunk.chunk_hash, "skipped": True} + ) + + # 读取分片数据并计算哈希 + chunk_data = await chunk.read() + chunk_size = len(chunk_data) + + # 校验分片大小不超过声明的 chunk_size + if chunk_size > chunk_info.chunk_size: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f"分片大小超过声明值: 最大 {chunk_info.chunk_size}, 实际 {chunk_size}", + ) + + # 计算已上传分片数,校验累计大小不超限(用分片数 * chunk_size 估算) + uploaded_count = await UploadChunk.filter( + upload_id=upload_id, completed=True + ).count() + # 已上传分片的最大可能大小 + 当前分片 + max_uploaded_size = uploaded_count * chunk_info.chunk_size + chunk_size + if max_uploaded_size > settings.uploadSize: + max_size_mb = settings.uploadSize / (1024 * 1024) + raise HTTPException( + status_code=403, detail=f"累计上传大小超过限制,最大为 {max_size_mb:.2f} MB" + ) + + chunk_hash = hashlib.sha256(chunk_data).hexdigest() + + save_path = chunk_info.save_path + + # 保存分片到存储 + storage = storages[settings.file_storage]() + try: + await storage.save_chunk( + upload_id, chunk_index, chunk_data, chunk_hash, save_path + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"分片保存失败: {str(e)}" + ) + + # 更新或创建分片记录(保存成功后再记录) + await UploadChunk.update_or_create( + upload_id=upload_id, + chunk_index=chunk_index, + defaults={ + "chunk_hash": chunk_hash, + "completed": True, + "file_size": chunk_info.file_size, + "total_chunks": chunk_info.total_chunks, + "chunk_size": chunk_info.chunk_size, + "file_name": chunk_info.file_name, + "save_path": chunk_info.save_path, + }, + ) + return APIResponse(detail={"chunk_hash": chunk_hash}) + + +@chunk_api.delete("/upload/{upload_id}", dependencies=[Depends(share_required_login)]) +async def cancel_upload(upload_id: str): + """取消上传并清理临时文件""" + chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first() + if not chunk_info: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在") + + save_path = chunk_info.save_path + + # 清理存储中的临时文件 + storage = storages[settings.file_storage]() + if save_path: + try: + await storage.clean_chunks(upload_id, save_path) + except Exception as e: + pass + + # 清理数据库记录 + await UploadChunk.filter(upload_id=upload_id).delete() + + return APIResponse(detail={"message": "上传已取消"}) + + +@chunk_api.get( + "/upload/status/{upload_id}", dependencies=[Depends(share_required_login)] +) +async def get_upload_status(upload_id: str): + """获取上传状态""" + chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first() + if not chunk_info: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在") + + # 获取已上传的分片列表 + uploaded_chunks = await UploadChunk.filter( + upload_id=upload_id, completed=True + ).values_list("chunk_index", flat=True) + + return APIResponse( + detail={ + "upload_id": upload_id, + "file_name": chunk_info.file_name, + "file_size": chunk_info.file_size, + "chunk_size": chunk_info.chunk_size, + "total_chunks": chunk_info.total_chunks, + "uploaded_chunks": list(uploaded_chunks), + "progress": len(uploaded_chunks) / chunk_info.total_chunks * 100, + } + ) + + +@chunk_api.post( + "/upload/complete/{upload_id}", dependencies=[Depends(share_required_login)] +) +async def complete_upload( + upload_id: str, data: CompleteUploadModel, ip: str = Depends(ip_limit["upload"]) +): + # 获取上传基本信息 + chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first() + if not chunk_info: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在") + + storage = storages[settings.file_storage]() + # 验证所有分片 + completed_chunks_list = await UploadChunk.filter( + upload_id=upload_id, completed=True + ).all() + if len(completed_chunks_list) != chunk_info.total_chunks: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="分片不完整") + + # 用分片数 * chunk_size 校验最大可能大小 + max_total_size = len(completed_chunks_list) * chunk_info.chunk_size + if max_total_size > settings.uploadSize: + save_path = chunk_info.save_path + if save_path: + try: + await storage.clean_chunks(upload_id, save_path) + except Exception: + pass + await UploadChunk.filter(upload_id=upload_id).delete() + max_size_mb = settings.uploadSize / (1024 * 1024) + raise HTTPException( + status_code=403, detail=f"实际上传大小超过限制,最大为 {max_size_mb:.2f} MB" + ) + + save_path = chunk_info.save_path + path = os.path.dirname(save_path) if save_path else "" + prefix, suffix = os.path.splitext(chunk_info.file_name) + + try: + # 合并文件并计算哈希 + _, file_hash = await storage.merge_chunks(upload_id, chunk_info, save_path) + # 创建文件记录 + expired_at, expired_count, used_count, code = await get_expire_info( + data.expire_value, data.expire_style + ) + await FileCodes.create( + code=code, + file_hash=file_hash, # 使用合并后计算的哈希 + is_chunked=True, + upload_id=upload_id, + size=chunk_info.file_size, + expired_at=expired_at, + expired_count=expired_count, + used_count=used_count, + file_path=path, + uuid_file_name=f"{prefix}{suffix}", + prefix=prefix, + suffix=suffix, + ) + # 清理临时文件 + await storage.clean_chunks(upload_id, save_path) + # 清理数据库中的分片记录 + await UploadChunk.filter(upload_id=upload_id).delete() + ip_limit["upload"].add_ip(ip) + return APIResponse(detail={"code": code, "name": chunk_info.file_name}) + except ValueError as e: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: + # 合并失败时清理临时文件 + try: + await storage.clean_chunks(upload_id, save_path) + except Exception: + pass + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件合并失败: {str(e)}" + ) + + +# ============ 预签名上传API ============ +presign_api = APIRouter(prefix="/presign", tags=["预签名上传"]) + +PRESIGN_SESSION_EXPIRES = 900 # 15分钟 + + +async def _get_valid_session( + upload_id: str, expected_mode: Optional[str] = None +) -> PresignUploadSession: + """获取并验证会话""" + session = await PresignUploadSession.filter(upload_id=upload_id).first() + if not session: + raise HTTPException(404, "上传会话不存在") + if await session.is_expired(): + await session.delete() + raise HTTPException(404, "上传会话已过期") + if expected_mode and session.mode != expected_mode: + raise HTTPException(400, f"此会话不支持{expected_mode}模式") + return session + + +@presign_api.post("/upload/init", dependencies=[Depends(share_required_login)]) +async def presign_upload_init( + data: PresignUploadInitRequest, ip: str = Depends(ip_limit["upload"]) +): + """初始化预签名上传,S3返回直传URL,其他存储返回代理URL""" + if data.file_size > settings.uploadSize: + raise HTTPException( + 403, + f"文件大小超过限制,最大为 {settings.uploadSize / (1024 * 1024):.2f} MB", + ) + if data.expire_style not in settings.expireStyle: + raise HTTPException(400, "过期时间类型错误") + + upload_id = uuid.uuid4().hex + path, _, _, filename, save_path = await FileUploadService.generate_file_path( + data.file_name, upload_id + ) + + storage: FileStorageInterface = storages[settings.file_storage]() + presigned_url = await storage.generate_presigned_upload_url( + save_path, PRESIGN_SESSION_EXPIRES + ) + + mode = "direct" if presigned_url else "proxy" + upload_url = presigned_url or f"/api/presign/upload/proxy/{upload_id}" + + await PresignUploadSession.create( + upload_id=upload_id, + file_name=filename, + file_size=data.file_size, + save_path=save_path, + mode=mode, + expire_value=data.expire_value, + expire_style=data.expire_style, + expires_at=await get_now() + timedelta(seconds=PRESIGN_SESSION_EXPIRES), + ) + + ip_limit["upload"].add_ip(ip) + return APIResponse( + detail={ + "upload_id": upload_id, + "upload_url": upload_url, + "mode": mode, + "expires_in": PRESIGN_SESSION_EXPIRES, + } + ) + + +@presign_api.put( + "/upload/proxy/{upload_id}", dependencies=[Depends(share_required_login)] +) +async def presign_upload_proxy( + upload_id: str, file: UploadFile = File(...), ip: str = Depends(ip_limit["upload"]) +): + """代理模式上传,服务器转存到存储后端""" + session = await _get_valid_session(upload_id, expected_mode="proxy") + + file_size = await validate_file_size(file, settings.uploadSize) + if abs(file_size - session.file_size) > 1024: + raise HTTPException(400, "文件大小与声明不符") + + storage: FileStorageInterface = storages[settings.file_storage]() + try: + await storage.save_file(file, session.save_path) + except Exception as e: + raise HTTPException(500, f"文件保存失败: {str(e)}") + + code = await FileUploadService.create_file_record( + session.file_name, + file_size, + os.path.dirname(session.save_path), + session.expire_value, + session.expire_style, + ) + + await session.delete() + ip_limit["upload"].add_ip(ip) + return APIResponse(detail={"code": code, "name": session.file_name}) + + +@presign_api.post( + "/upload/confirm/{upload_id}", dependencies=[Depends(share_required_login)] +) +async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upload"])): + """直传确认,客户端完成S3直传后调用获取分享码""" + session = await _get_valid_session(upload_id, expected_mode="direct") + + storage: FileStorageInterface = storages[settings.file_storage]() + if not await storage.file_exists(session.save_path): + raise HTTPException(404, "文件未上传或上传失败") + + code = await FileUploadService.create_file_record( + session.file_name, + session.file_size, + os.path.dirname(session.save_path), + session.expire_value, + session.expire_style, + ) + + await session.delete() + ip_limit["upload"].add_ip(ip) + return APIResponse(detail={"code": code, "name": session.file_name}) + + +@presign_api.get( + "/upload/status/{upload_id}", dependencies=[Depends(share_required_login)] +) +async def presign_upload_status(upload_id: str): + """查询上传会话状态""" + session = await PresignUploadSession.filter(upload_id=upload_id).first() + if not session: + raise HTTPException(404, "上传会话不存在") + + return APIResponse( + detail={ + "upload_id": session.upload_id, + "file_name": session.file_name, + "file_size": session.file_size, + "mode": session.mode, + "created_at": session.created_at.isoformat(), + "expires_at": session.expires_at.isoformat(), + "is_expired": await session.is_expired(), + } + ) + + +@presign_api.delete("/upload/{upload_id}", dependencies=[Depends(share_required_login)]) +async def presign_upload_cancel(upload_id: str): + """取消上传会话""" + session = await PresignUploadSession.filter(upload_id=upload_id).first() + if not session: + raise HTTPException(404, "上传会话不存在") + + if session.mode == "direct": + storage: FileStorageInterface = storages[settings.file_storage]() + try: + if await storage.file_exists(session.save_path): + temp_file_code = FileCodes( + file_path=os.path.dirname(session.save_path), + uuid_file_name=os.path.basename(session.save_path), + ) + await storage.delete_file(temp_file_code) + except Exception: + pass + + await session.delete() + return APIResponse(detail={"message": "上传会话已取消"}) diff --git a/core/__init__.py b/core/__init__.py index e69de29bb..bfdb9d41a 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -0,0 +1,4 @@ +# @Time : 2023/8/11 20:06 +# @Author : Lan +# @File : __init__.py.py +# @Software: PyCharm diff --git a/core/config.py b/core/config.py new file mode 100644 index 000000000..a2dd3491c --- /dev/null +++ b/core/config.py @@ -0,0 +1,21 @@ +from apps.base.models import KeyValue +from apps.base.utils import ip_limit +from core.settings import DEFAULT_CONFIG, settings + + +async def ensure_settings_row() -> None: + await KeyValue.get_or_create(key="settings", defaults={"value": DEFAULT_CONFIG}) + + +def _sync_ip_limits() -> None: + ip_limit["error"].minutes = settings.errorMinute + ip_limit["error"].count = settings.errorCount + ip_limit["upload"].minutes = settings.uploadMinute + ip_limit["upload"].count = settings.uploadCount + + +async def refresh_settings() -> None: + """从数据库读取最新配置并应用到运行时。""" + config_record = await KeyValue.filter(key="settings").first() + settings.user_config = config_record.value if config_record and config_record.value else {} + _sync_ip_limits() diff --git a/core/database.py b/core/database.py index af187863f..1b041193a 100644 --- a/core/database.py +++ b/core/database.py @@ -1,84 +1,146 @@ -import datetime - -from sqlalchemy import Boolean, Column, Integer, String, DateTime, JSON, Text, select, insert, delete -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.ext.asyncio import create_async_engine -from sqlalchemy.ext.asyncio.session import AsyncSession - -from settings import settings - -engine = create_async_engine(settings.DATABASE_URL) -Base = declarative_base() - - -class Options(Base): - __tablename__ = 'options' - id = Column(Integer, primary_key=True, index=True) - key = Column(String, unique=True, index=True) - value = Column(JSON) - - -class Codes(Base): - __tablename__ = "codes" - id = Column(Integer, primary_key=True, index=True) - code = Column(String(10), unique=True, index=True) - key = Column(String(30), unique=True) - name = Column(String(500)) - size = Column(Integer) - type = Column(String(20)) - text = Column(Text) - used = Column(Boolean, default=False) - count = Column(Integer, default=-1) - use_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now) - exp_time = Column(DateTime, nullable=True) - - -async def init_models(s): - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - if await conn.scalar(select(Options).filter(Options.key == 'INSTALL')) is None: - # 如果没有存在install,则清空表,并插入默认数据 - await conn.execute(delete(table=Options)) - await conn.execute(insert(table=Options, values=[ - {'key': 'INSTALL', 'value': settings.VERSION}, - {'key': 'DEBUG', 'value': settings.DEBUG}, - {'key': 'DATABASE_FILE', 'value': settings.DATABASE_FILE}, - {'key': 'PORT', 'value': settings.PORT}, - {'key': 'DATA_ROOT', 'value': settings.DATA_ROOT}, - {'key': 'STATIC_URL', 'value': settings.STATIC_URL}, - {'key': 'BANNERS', 'value': settings.BANNERS}, - {'key': 'ENABLE_UPLOAD', 'value': settings.ENABLE_UPLOAD}, - {'key': 'MAX_DAYS', 'value': settings.MAX_DAYS}, - {'key': 'ERROR_COUNT', 'value': settings.ERROR_COUNT}, - {'key': 'ERROR_MINUTE', 'value': settings.ERROR_COUNT}, - {'key': 'UPLOAD_COUNT', 'value': settings.UPLOAD_COUNT}, - {'key': 'UPLOAD_MINUTE', 'value': settings.UPLOAD_MINUTE}, - {'key': 'DELETE_EXPIRE_FILES_INTERVAL', 'value': settings.DELETE_EXPIRE_FILES_INTERVAL}, - {'key': 'ADMIN_ADDRESS', 'value': settings.ADMIN_ADDRESS}, - {'key': 'ADMIN_PASSWORD', 'value': settings.ADMIN_PASSWORD}, - {'key': 'FILE_SIZE_LIMIT', 'value': settings.FILE_SIZE_LIMIT}, - {'key': 'TITLE', 'value': settings.TITLE}, - {'key': 'DESCRIPTION', 'value': settings.DESCRIPTION}, - {'key': 'KEYWORDS', 'value': settings.KEYWORDS}, - {'key': 'STORAGE_ENGINE', 'value': settings.STORAGE_ENGINE}, - {'key': 'STORAGE_CONFIG', 'value': {}}, - ])) - print( - f'初始化数据库成功!\n' - f'如您未配置.env文件,将为您随机生成信息\n' - f'您的后台地址为:/{settings.ADMIN_ADDRESS}\n' - f'您的管理员密码为:{settings.ADMIN_PASSWORD}\n' - f'请尽快修改后台信息!\n' - f'FileCodeBox https://github.com/vastsa/FileCodeBox' - ) - await settings.updates(await conn.execute(select(Options).filter())) +import asyncio +import glob +import importlib +import os +from contextlib import asynccontextmanager +from typing import IO + +from tortoise import Tortoise + +from core.logger import logger +from core.settings import data_root + + +_DB_FILE = os.path.join(data_root, "filecodebox.db") +_STARTUP_LOCK_FILE = os.path.join(data_root, "filecodebox.startup.lock") + + +def get_db_config() -> dict: + return { + "connections": { + "default": { + "engine": "tortoise.backends.sqlite", + "credentials": { + "file_path": _DB_FILE, + "journal_mode": "WAL", + "busy_timeout": 10000, + "foreign_keys": "ON", + }, + } + }, + "apps": { + "models": { + "models": ["apps.base.models"], + "default_connection": "default", + } + }, + "use_tz": False, + "timezone": "Asia/Shanghai", + } + + +def _lock_file(file_obj: IO[str]) -> None: + if os.name == "nt": + import msvcrt + + # Windows 需要锁定至少 1 字节 + if os.fstat(file_obj.fileno()).st_size == 0: + file_obj.write("0") + file_obj.flush() + msvcrt.locking(file_obj.fileno(), msvcrt.LK_LOCK, 1) + else: + import fcntl + + fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX) + + +def _unlock_file(file_obj: IO[str]) -> None: + if os.name == "nt": + import msvcrt + + msvcrt.locking(file_obj.fileno(), msvcrt.LK_UNLCK, 1) + else: + import fcntl + fcntl.flock(file_obj.fileno(), fcntl.LOCK_UN) -async def get_config(key): - async with engine.begin() as conn: - return await conn.scalar(select(Options.value).filter(Options.key == key)) +@asynccontextmanager +async def db_startup_lock(): + os.makedirs(data_root, exist_ok=True) + lock_file = open(_STARTUP_LOCK_FILE, "a+", encoding="utf-8") + try: + await asyncio.to_thread(_lock_file, lock_file) + yield + finally: + await asyncio.to_thread(_unlock_file, lock_file) + lock_file.close() + + +async def init_db(): + try: + db_config = get_db_config() + + if not Tortoise._inited: + await Tortoise.init(config=db_config) + + async with db_startup_lock(): + # 创建migrations表 + await Tortoise.get_connection("default").execute_script(""" + CREATE TABLE IF NOT EXISTS migrates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + migration_file VARCHAR(255) NOT NULL UNIQUE, + executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # 执行迁移 + await execute_migrations() + + except Exception as e: + logger.error(f"数据库初始化失败: {str(e)}") + raise + + +async def execute_migrations(): + """执行数据库迁移""" + try: + # 收集迁移文件 + migration_files = [] + for root, dirs, files in os.walk("apps"): + if "migrations" in dirs: + migration_path = os.path.join(root, "migrations") + migration_files.extend(glob.glob(os.path.join(migration_path, "migrations_*.py"))) + + # 按文件名排序 + migration_files.sort() + + for migration_file in migration_files: + file_name = os.path.basename(migration_file) + + # 检查是否已执行 + executed = await Tortoise.get_connection("default").execute_query( + "SELECT id FROM migrates WHERE migration_file = ?", [file_name] + ) + + if not executed[1]: + logger.info(f"执行迁移: {file_name}") + # 导入并执行migration + module_path = migration_file.replace("/", ".").replace("\\", ".").replace(".py", "") + try: + migration_module = importlib.import_module(module_path) + if hasattr(migration_module, "migrate"): + await migration_module.migrate() + # 记录执行 + await Tortoise.get_connection("default").execute_query( + "INSERT INTO migrates (migration_file) VALUES (?)", + [file_name] + ) + logger.info(f"迁移完成: {file_name}") + except Exception as e: + logger.error(f"迁移 {file_name} 执行失败: {str(e)}") + raise -async def get_session(): - async with AsyncSession(engine, expire_on_commit=False) as s: - yield s + except Exception as e: + logger.error(f"迁移过程发生错误: {str(e)}") + raise diff --git a/core/depends.py b/core/depends.py deleted file mode 100644 index 8b79f9984..000000000 --- a/core/depends.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Union -from datetime import datetime, timedelta - -from fastapi import Header, HTTPException, Request - -from settings import settings - - -async def admin_required(pwd: Union[str, None] = Header(default=None), request: Request = None): - if 'share' in request.url.path: - if pwd != settings.ADMIN_PASSWORD and not settings.ENABLE_UPLOAD: - raise HTTPException(status_code=403, detail='本站上传功能已关闭,仅管理员可用') - else: - if settings.ADMIN_PASSWORD is None: - raise HTTPException(status_code=404, detail='您未设置管理员密码,无法使用此功能,请更新配置文件后,重启系统') - if not pwd or pwd != settings.ADMIN_PASSWORD: - raise HTTPException(status_code=401, detail="密码错误,请重新登录") - - -class IPRateLimit: - def __init__(self, count, minutes): - self.ips = {} - self.count = count - self.minutes = minutes - - def check_ip(self, ip): - # 检查ip是否被禁止 - if ip in self.ips: - if self.ips[ip]['count'] >= self.count: - if self.ips[ip]['time'] + timedelta(minutes=self.minutes) > datetime.now(): - return False - else: - self.ips.pop(ip) - return True - - def add_ip(self, ip): - ip_info = self.ips.get(ip, {'count': 0, 'time': datetime.now()}) - ip_info['count'] += 1 - ip_info['time'] = datetime.now() - self.ips[ip] = ip_info - return ip_info['count'] - - async def remove_expired_ip(self): - for ip in list(self.ips.keys()): - if self.ips[ip]['time'] + timedelta(minutes=self.minutes) < datetime.now(): - self.ips.pop(ip) - - def __call__(self, request: Request): - ip = request.headers.get('X-Real-IP', request.headers.get('X-Forwarded-For', request.client.host)) - if not self.check_ip(ip): - raise HTTPException(status_code=400, detail=f"请求次数过多,请稍后再试") - return ip diff --git a/core/logger.py b/core/logger.py new file mode 100644 index 000000000..101413b0d --- /dev/null +++ b/core/logger.py @@ -0,0 +1,28 @@ +import logging +import sys + + +def setup_logger(): + # 创建logger对象 + _logger = logging.getLogger('FileCodeBox') + _logger.setLevel(logging.INFO) + + # 创建控制台处理器 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.INFO) + + # 设置日志格式 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + console_handler.setFormatter(formatter) + + # 添加处理器到logger + _logger.addHandler(console_handler) + + return _logger + + +# 创建全局logger实例 +logger = setup_logger() diff --git a/core/response.py b/core/response.py new file mode 100644 index 000000000..8afd49438 --- /dev/null +++ b/core/response.py @@ -0,0 +1,15 @@ +# @Time : 2023/8/14 11:48 +# @Author : Lan +# @File : response.py +# @Software: PyCharm +from typing import Generic, Optional, TypeVar + +from pydantic import BaseModel + +T = TypeVar("T") + + +class APIResponse(BaseModel, Generic[T]): + code: int = 200 + message: str = "ok" + detail: Optional[T] = None diff --git a/core/settings.py b/core/settings.py new file mode 100644 index 000000000..c8f5e4b44 --- /dev/null +++ b/core/settings.py @@ -0,0 +1,101 @@ +# @Time : 2023/8/15 09:51 +# @Author : Lan +# @File : settings.py +# @Software: PyCharm +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +data_root = BASE_DIR / "data" + +if not data_root.exists(): + data_root.mkdir(parents=True, exist_ok=True) + +DEFAULT_CONFIG = { + "file_storage": "local", + "storage_path": "", + "name": "文件快递柜 - FileCodeBox", + "description": "开箱即用的文件快传系统", + "notify_title": "系统通知", + "notify_content": '欢迎使用 FileCodeBox,本程序开源于 Github ,欢迎Star和Fork。', + "page_explain": "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。", + "keywords": "FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件", + "s3_access_key_id": "", + "s3_secret_access_key": "", + "s3_bucket_name": "", + "s3_endpoint_url": "", + "s3_region_name": "auto", + "s3_signature_version": "s3v2", + "s3_hostname": "", + "s3_proxy": 0, + "max_save_seconds": 0, + "aws_session_token": "", + "onedrive_domain": "", + "onedrive_client_id": "", + "onedrive_username": "", + "onedrive_password": "", + "onedrive_root_path": "filebox_storage", + "onedrive_proxy": 0, + "webdav_root_path": "filebox_storage", + "webdav_proxy": 0, + "admin_token": "FileCodeBox2023", + "openUpload": 1, + "uploadSize": 1024 * 1024 * 10, + "expireStyle": ["day", "hour", "minute", "forever", "count"], + "uploadMinute": 1, + "enableChunk": 0, + "webdav_url": "", + "webdav_password": "", + "webdav_username": "", + "opacity": 0.9, + "background": "", + "uploadCount": 10, + "themesChoices": [ + { + "name": "2023", + "key": "themes/2023", + "author": "Lan", + "version": "1.0", + }, + { + "name": "2024", + "key": "themes/2024", + "author": "Lan", + "version": "1.0", + }, + ], + "themesSelect": "themes/2024", + "errorMinute": 1, + "errorCount": 10, + "serverWorkers": 1, + "serverHost": "0.0.0.0", + "serverPort": 12345, + "showAdminAddr": 0, + "robotsText": "User-agent: *\nDisallow: /", +} + + +class Settings: + def __init__(self, defaults=None): + self.default_config = defaults or {} + self.user_config = {} + + def __getattr__(self, attr): + if attr in self.user_config: + return self.user_config[attr] + if attr in self.default_config: + return self.default_config[attr] + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attr}'" + ) + + def __setattr__(self, key, value): + if key in ["default_config", "user_config"]: + super().__setattr__(key, value) + else: + self.user_config[key] = value + + def items(self): + return {**self.default_config, **self.user_config}.items() + + +settings = Settings(DEFAULT_CONFIG) diff --git a/core/storage.py b/core/storage.py index 58936b100..a40562676 100644 --- a/core/storage.py +++ b/core/storage.py @@ -1,174 +1,1349 @@ +# @Time : 2023/8/11 20:06 +# @Author : Lan +# @File : storage.py +# @Software: PyCharm +import base64 +import hashlib import os +import tempfile +from core.logger import logger +import shutil +from typing import Optional +from urllib.parse import quote, unquote + +import aiofiles +import aiohttp import asyncio -import time -import uuid -from datetime import datetime from pathlib import Path -from typing import BinaryIO -from fastapi import UploadFile +import datetime +import re +import aioboto3 +from botocore.config import Config +from fastapi import HTTPException, Response, UploadFile +from core.response import APIResponse +from core.settings import data_root, settings +from apps.base.models import FileCodes, UploadChunk +from core.utils import get_file_url, sanitize_filename +from fastapi.responses import FileResponse, StreamingResponse + + +class FileStorageInterface: + + async def save_file(self, file: UploadFile, save_path: str): + """ + 保存文件 + """ + raise NotImplementedError + + async def delete_file(self, file_code: FileCodes): + """ + 删除文件 + """ + raise NotImplementedError + + async def get_file_url(self, file_code: FileCodes): + """ + 获取文件分享的url + + 如果服务不支持直接访问文件,可以通过服务器中转下载。 + 此时,此方法可以调用 utils.py 中的 `get_file_url` 方法,获取服务器中转下载的url + """ + raise NotImplementedError + + async def get_file_response(self, file_code: FileCodes): + """ + 获取文件响应 -from core.database import Codes -from settings import settings + 如果服务不支持直接访问文件,则需要实现该方法,返回文件响应 + 其余情况,可以不实现该方法 + """ + raise NotImplementedError -if settings.STORAGE_ENGINE == 'aliyunsystem': - try: - import oss2 - except ImportError: - os.system('pip install oss2') - import oss2 + async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str): + """ + 保存分片文件 + :param upload_id: 上传会话ID + :param chunk_index: 分片索引 + :param chunk_data: 分片数据 + :param chunk_hash: 分片哈希值 + :param save_path: 文件保存路径 + """ + raise NotImplementedError + async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]: + """ + 合并分片文件并返回文件路径和完整哈希值 + :param upload_id: 上传会话ID + :param chunk_info: 分片信息 + :param save_path: 文件保存路径 + :return: (文件路径, 文件哈希值) + """ + raise NotImplementedError -class FileSystemStorage: + async def generate_presigned_upload_url(self, save_path: str, expires_in: int = 900) -> Optional[str]: + """ + 生成预签名上传URL + :param save_path: 文件保存路径 + :param expires_in: URL过期时间(秒),默认15分钟 + :return: 预签名URL,如果不支持直传则返回None + """ + return None # 默认不支持直传,使用代理模式 + + async def file_exists(self, save_path: str) -> bool: + """ + 检查文件是否存在 + :param save_path: 文件路径 + :return: 文件是否存在 + """ + raise NotImplementedError + + async def clean_chunks(self, upload_id: str, save_path: str): + """ + 清理临时分片文件 + :param upload_id: 上传会话ID + :param save_path: 文件保存路径 + """ + raise NotImplementedError + + +class SystemFileStorage(FileStorageInterface): def __init__(self): - self.DATA_ROOT = Path(settings.DATA_ROOT) - self.STATIC_URL = settings.STATIC_URL - self.NAME = "filesystem" - self.DOWN_PATH = '/select' - - async def get_filepath(self, text: str): - return self.DATA_ROOT / text.lstrip(self.STATIC_URL + '/') - - async def get_url(self, info: Codes): - return f'{self.DOWN_PATH}?code={info.code}' - - async def get_text(self, file: UploadFile, key: str): - ext = file.filename.split('.')[-1] - now = datetime.now() - path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/" - if not path.exists(): - path.mkdir(parents=True) - text = f"{self.STATIC_URL}/{(path / f'{key}.{ext}').relative_to(self.DATA_ROOT)}" - return text - - @staticmethod - async def get_size(file: UploadFile): - f = file.file - f.seek(0, os.SEEK_END) - size = f.tell() - f.seek(0, os.SEEK_SET) - return size - - @staticmethod - def _save(filepath, file: BinaryIO): - with open(filepath, 'wb') as f: - chunk_size = 256 * 1024 - chunk = file.read(chunk_size) + self.chunk_size = 256 * 1024 + self.root_path = data_root + + def _save(self, file, save_path): + with open(save_path, "wb") as f: + chunk = file.read(self.chunk_size) while chunk: f.write(chunk) - chunk = file.read(chunk_size) + chunk = file.read(self.chunk_size) + + async def save_file(self, file: UploadFile, save_path: str): + path_obj = Path(save_path) + directory = str(path_obj.parent) + # 提取原始文件名并进行清理 + filename = await sanitize_filename(path_obj.name) + # 构建安全的完整保存路径 + safe_save_path = self.root_path / directory / filename + # 确保目录存在 + if not safe_save_path.parent.exists(): + safe_save_path.parent.mkdir(parents=True) + await asyncio.to_thread(self._save, file.file, safe_save_path) - @staticmethod - def _save_chunk(filepath, file: bytes): - with open(filepath, 'wb') as f: - f.write(file) + async def delete_file(self, file_code: FileCodes): + save_path = self.root_path / await file_code.get_file_path() + if save_path.exists(): + save_path.unlink() - async def create_upload_file(self): - file_key = uuid.uuid4().hex - file_path = self.DATA_ROOT / f"temp/{file_key}" + async def get_file_url(self, file_code: FileCodes): + return await get_file_url(file_code.code) + + async def get_file_response(self, file_code: FileCodes): + file_path = self.root_path / await file_code.get_file_path() if not file_path.exists(): - file_path.mkdir(parents=True) - return file_key - - async def save_chunk_file(self, file_key, file_chunk, chunk_index, chunk_total): - file_path = self.DATA_ROOT / f"temp/{file_key}/" - await asyncio.to_thread(self._save_chunk, file_path / f"{chunk_total}-{chunk_index}.temp", file_chunk) - - async def merge_chunks(self, file_key, file_name, total_chunks: int): - ext = file_name.split('.')[-1] - now = datetime.now() - path = self.DATA_ROOT / f"upload/{now.year}/{now.month}/{now.day}/" - if not path.exists(): - path.mkdir(parents=True) - text = f"{self.STATIC_URL}/{(path / f'{file_key}.{ext}').relative_to(self.DATA_ROOT)}" - with open(path / f'{file_key}.{ext}', 'wb') as f: - for i in range(1, total_chunks + 1): - now_temp = self.DATA_ROOT / f'temp/{file_key}/{total_chunks}-{i}.temp' - with open(now_temp, 'rb') as r: - f.write(r.read()) - await asyncio.to_thread(os.remove, now_temp) - await asyncio.to_thread(os.rmdir, self.DATA_ROOT / f'temp/{file_key}/') - return text - - async def save_file(self, file: UploadFile, text: str): - filepath = await self.get_filepath(text) - await asyncio.to_thread(self._save, filepath, file.file) - - async def delete_file(self, text: str): - filepath = await self.get_filepath(text) - if filepath.exists(): - await asyncio.to_thread(os.remove, filepath) - await asyncio.to_thread(self.judge_delete_folder, filepath) - - async def delete_files(self, texts): - tasks = [self.delete_file(text) for text in texts] - await asyncio.gather(*tasks) - - def judge_delete_folder(self, filepath): - current = filepath.parent - while current != self.DATA_ROOT: - if not list(current.iterdir()): - os.rmdir(current) - current = current.parent + return APIResponse(code=404, detail="文件已过期删除") + filename = f"{file_code.prefix}{file_code.suffix}" + encoded_filename = quote(filename, safe='') + content_disposition = f"attachment; filename*=UTF-8''{encoded_filename}" + + # 尝试获取文件系统大小,如果成功则设置 Content-Length + headers = {"Content-Disposition": content_disposition} + try: + content_length = file_path.stat().st_size + headers["Content-Length"] = str(content_length) + except Exception: + # 如果获取文件大小失败,则不提供 Content-Length + pass + + return FileResponse( + file_path, + media_type="application/octet-stream", + headers=headers, + filename=filename # 保留原始文件名以备某些场景使用 + ) + + async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str): + """ + 保存分片文件到本地文件系统 + :param upload_id: 上传会话ID + :param chunk_index: 分片索引 + :param chunk_data: 分片数据 + :param chunk_hash: 分片哈希值 + :param save_path: 文件保存路径 + """ + chunk_dir = self.root_path / save_path + chunk_path = chunk_dir.parent / 'chunks' / upload_id / f"{chunk_index}.part" + if not chunk_path.parent.exists(): + chunk_path.parent.mkdir(parents=True, exist_ok=True) + # 使用临时文件写入,确保原子性 + temp_path = chunk_path.with_suffix('.tmp') + try: + async with aiofiles.open(temp_path, "wb") as f: + await f.write(chunk_data) + # 原子重命名 + temp_path.rename(chunk_path) + except Exception as e: + if temp_path.exists(): + temp_path.unlink() + raise e + + async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]: + """ + 合并本地文件系统的分片文件并返回文件路径和完整哈希值 + :param upload_id: 上传会话ID + :param chunk_info: 分片信息 + :param save_path: 文件保存路径 + :return: (文件路径, 文件哈希值) + """ + output_path = self.root_path / save_path + output_path.parent.mkdir(parents=True, exist_ok=True) + chunk_base_dir = output_path.parent / 'chunks' / upload_id + file_sha256 = hashlib.sha256() + + # 使用临时文件写入,确保原子性 + temp_output = output_path.with_suffix('.merging') + try: + async with aiofiles.open(temp_output, "wb") as out_file: + for i in range(chunk_info.total_chunks): + # 获取分片记录 + chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first() + if not chunk_record: + raise ValueError(f"分片{i}记录不存在") + chunk_path = chunk_base_dir / f"{i}.part" + if not chunk_path.exists(): + raise ValueError(f"分片{i}文件不存在") + async with aiofiles.open(chunk_path, "rb") as in_file: + chunk_data = await in_file.read() + current_hash = hashlib.sha256(chunk_data).hexdigest() + if current_hash != chunk_record.chunk_hash: + raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}") + file_sha256.update(chunk_data) + await out_file.write(chunk_data) + # 原子重命名 + temp_output.rename(output_path) + except Exception as e: + if temp_output.exists(): + temp_output.unlink() + raise e + return str(output_path), file_sha256.hexdigest() + + async def clean_chunks(self, upload_id: str, save_path: str): + """ + 清理本地文件系统的临时分片文件 + :param upload_id: 上传会话ID + :param save_path: 文件保存路径 + """ + chunk_dir = (self.root_path / save_path).parent / 'chunks' / upload_id + if chunk_dir.exists(): + try: + shutil.rmtree(chunk_dir) + except Exception as e: + logger.info(f"清理本地分片目录失败: {e}") + # 清理父级 chunks 目录(如果为空) + chunks_parent = chunk_dir.parent + if chunks_parent.exists() and not any(chunks_parent.iterdir()): + try: + chunks_parent.rmdir() + except Exception as e: + logger.info(f"清理 chunks 父目录失败: {e}") + + async def file_exists(self, save_path: str) -> bool: + """ + 检查文件是否存在于本地文件系统 + :param save_path: 文件路径 + :return: 文件是否存在 + """ + file_path = self.root_path / save_path + return file_path.exists() + + +class S3FileStorage(FileStorageInterface): + def __init__(self): + self.access_key_id = settings.s3_access_key_id + self.secret_access_key = settings.s3_secret_access_key + self.bucket_name = settings.s3_bucket_name + self.s3_hostname = settings.s3_hostname + self.region_name = settings.s3_region_name + self.signature_version = settings.s3_signature_version + self.endpoint_url = settings.s3_endpoint_url or f"https://{self.s3_hostname}" + self.aws_session_token = settings.aws_session_token + self.proxy = settings.s3_proxy + self.session = aioboto3.Session( + aws_access_key_id=self.access_key_id, + aws_secret_access_key=self.secret_access_key, + ) + if not settings.s3_endpoint_url: + self.endpoint_url = f"https://{self.s3_hostname}" + else: + # 如果提供了 s3_endpoint_url,则优先使用它 + self.endpoint_url = settings.s3_endpoint_url + + async def save_file(self, file: UploadFile, save_path: str): + async with self.session.client( + "s3", + endpoint_url=self.endpoint_url, + aws_session_token=self.aws_session_token, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + # 使用 upload_fileobj 流式上传,避免将整个文件加载到内存 + await s3.upload_fileobj( + file.file, + self.bucket_name, + save_path, + ExtraArgs={"ContentType": file.content_type or "application/octet-stream"}, + ) + + async def delete_file(self, file_code: FileCodes): + async with self.session.client( + "s3", + endpoint_url=self.endpoint_url, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + await s3.delete_object( + Bucket=self.bucket_name, Key=await file_code.get_file_path() + ) + + async def get_file_response(self, file_code: FileCodes): + try: + filename = file_code.prefix + file_code.suffix + content_length = None # 初始化为 None,表示未知大小 + + async with self.session.client( + "s3", + endpoint_url=self.endpoint_url, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + # 尝试获取文件大小(HEAD请求) + try: + head_response = await s3.head_object( + Bucket=self.bucket_name, + Key=await file_code.get_file_path() + ) + # 从HEAD响应中获取Content-Length + if 'ContentLength' in head_response: + content_length = head_response['ContentLength'] + elif 'Content-Length' in head_response['ResponseMetadata']['HTTPHeaders']: + content_length = int(head_response['ResponseMetadata']['HTTPHeaders']['Content-Length']) + except Exception: + # 如果HEAD请求失败,则不提供 Content-Length + pass + + link = await s3.generate_presigned_url( + "get_object", + Params={ + "Bucket": self.bucket_name, + "Key": await file_code.get_file_path(), + }, + ExpiresIn=3600, + ) + + # 创建ClientSession并传递给生成器复用 + session = aiohttp.ClientSession() + + async def stream_generator(): + try: + async with session.get(link) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, + detail=f"从S3获取文件失败: {resp.status}" + ) + # 设置块大小(例如64KB) + chunk_size = 65536 + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + yield chunk + finally: + await session.close() + + from fastapi.responses import StreamingResponse + encoded_filename = quote(filename, safe='') + headers = { + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" + } + if content_length is not None: + headers["Content-Length"] = str(content_length) + return StreamingResponse( + stream_generator(), + media_type="application/octet-stream", + headers=headers + ) + except HTTPException: + raise + except Exception: + raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试") + + async def get_file_url(self, file_code: FileCodes): + if file_code.prefix == "文本分享": + return file_code.text + if self.proxy: + return await get_file_url(file_code.code) + else: + async with self.session.client( + "s3", + endpoint_url=self.endpoint_url, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + result = await s3.generate_presigned_url( + "get_object", + Params={ + "Bucket": self.bucket_name, + "Key": await file_code.get_file_path(), + }, + ExpiresIn=3600, + ) + return result + + async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str): + """ + 保存分片到 S3(使用独立对象存储每个分片) + 注意:这里不使用 S3 原生的 multipart upload,而是将每个分片作为独立对象存储 + """ + chunk_key = str(Path(save_path).parent / "chunks" / upload_id / f"{chunk_index}.part") + async with self.session.client( + 's3', + endpoint_url=self.endpoint_url, + aws_session_token=self.aws_session_token, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + # 将分片作为独立对象上传 + await s3.put_object( + Bucket=self.bucket_name, + Key=chunk_key, + Body=chunk_data, + Metadata={ + 'chunk-hash': chunk_hash, + 'chunk-index': str(chunk_index) + } + ) + + async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]: + """ + 合并 S3 上的分片文件 + 使用 S3 的 multipart upload API 实现流式合并,避免内存问题 + """ + file_sha256 = hashlib.sha256() + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + + async with self.session.client( + 's3', + endpoint_url=self.endpoint_url, + aws_session_token=self.aws_session_token, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + # 创建 multipart upload + mpu = await s3.create_multipart_upload( + Bucket=self.bucket_name, + Key=save_path, + ContentType='application/octet-stream' + ) + mpu_id = mpu['UploadId'] + parts = [] + + try: + # 按顺序读取、验证并上传每个分片 + for i in range(chunk_info.total_chunks): + chunk_key = f"{chunk_dir}/{i}.part" + chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first() + if not chunk_record: + raise ValueError(f"分片{i}记录不存在") + + try: + response = await s3.get_object( + Bucket=self.bucket_name, + Key=chunk_key + ) + chunk_data = await response['Body'].read() + except Exception as e: + raise ValueError(f"分片{i}文件不存在: {e}") + + current_hash = hashlib.sha256(chunk_data).hexdigest() + if current_hash != chunk_record.chunk_hash: + raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}") + + file_sha256.update(chunk_data) + + # 上传分片到 multipart upload + part_response = await s3.upload_part( + Bucket=self.bucket_name, + Key=save_path, + UploadId=mpu_id, + PartNumber=i + 1, # S3 part numbers start at 1 + Body=chunk_data + ) + parts.append({ + 'PartNumber': i + 1, + 'ETag': part_response['ETag'] + }) + + # 释放内存 + del chunk_data + + # 完成 multipart upload + await s3.complete_multipart_upload( + Bucket=self.bucket_name, + Key=save_path, + UploadId=mpu_id, + MultipartUpload={'Parts': parts} + ) + except Exception as e: + # 出错时取消 multipart upload + await s3.abort_multipart_upload( + Bucket=self.bucket_name, + Key=save_path, + UploadId=mpu_id + ) + raise e + + return save_path, file_sha256.hexdigest() + + async def clean_chunks(self, upload_id: str, save_path: str): + """ + 清理 S3 上的临时分片文件 + :param upload_id: 上传会话ID + :param save_path: 文件保存路径 + """ + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + async with self.session.client( + 's3', + endpoint_url=self.endpoint_url, + aws_session_token=self.aws_session_token, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + try: + # 列出并删除所有分片对象 + paginator = s3.get_paginator('list_objects_v2') + async for page in paginator.paginate(Bucket=self.bucket_name, Prefix=chunk_dir): + objects = page.get('Contents', []) + if objects: + delete_objects = [{'Key': obj['Key']} for obj in objects] + await s3.delete_objects( + Bucket=self.bucket_name, + Delete={'Objects': delete_objects} + ) + except Exception as e: + logger.info(f"清理 S3 分片数据时出错: {e}") + + async def generate_presigned_upload_url(self, save_path: str, expires_in: int = 900) -> Optional[str]: + """ + 生成S3预签名上传URL + :param save_path: 文件保存路径 + :param expires_in: URL过期时间(秒),默认15分钟 + :return: 预签名PUT URL + """ + async with self.session.client( + "s3", + endpoint_url=self.endpoint_url, + aws_session_token=self.aws_session_token, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + return await s3.generate_presigned_url( + "put_object", + Params={ + "Bucket": self.bucket_name, + "Key": save_path, + }, + ExpiresIn=expires_in, + ) + + async def file_exists(self, save_path: str) -> bool: + """ + 检查文件是否存在于S3 + :param save_path: 文件路径 + :return: 文件是否存在 + """ + async with self.session.client( + "s3", + endpoint_url=self.endpoint_url, + aws_session_token=self.aws_session_token, + region_name=self.region_name, + config=Config(signature_version=self.signature_version), + ) as s3: + try: + await s3.head_object(Bucket=self.bucket_name, Key=save_path) + return True + except Exception: + return False + + +class OneDriveFileStorage(FileStorageInterface): + def __init__(self): + try: + import msal + from office365.graph_client import GraphClient + from office365.runtime.client_request_exception import ( + ClientRequestException, + ) + except ImportError: + raise ImportError("请先安装`msal`和`Office365-REST-Python-Client`") + self.msal = msal + self.domain = settings.onedrive_domain + self.client_id = settings.onedrive_client_id + self.username = settings.onedrive_username + self.password = settings.onedrive_password + self.proxy = settings.onedrive_proxy + self._ClientRequestException = ClientRequestException + + try: + client = GraphClient(self.acquire_token_pwd) + self.root_path = ( + client.me.drive.root.get_by_path(settings.onedrive_root_path) + .get() + .execute_query() + ) + except ClientRequestException as e: + if e.code == "itemNotFound": + client.me.drive.root.create_folder(settings.onedrive_root_path) + self.root_path = ( + client.me.drive.root.get_by_path( + settings.onedrive_root_path) + .get() + .execute_query() + ) else: - break + raise e + except Exception as e: + raise Exception("OneDrive验证失败,请检查配置是否正确\n" + str(e)) + + def acquire_token_pwd(self): + authority_url = f"https://login.microsoftonline.com/{self.domain}" + app = self.msal.PublicClientApplication( + authority=authority_url, client_id=self.client_id + ) + result = app.acquire_token_by_username_password( + username=self.username, + password=self.password, + scopes=["https://graph.microsoft.com/.default"], + ) + return result + + def _get_path_str(self, path): + if isinstance(path, str): + path = path.replace("\\", "/").replace("//", "/").split("/") + elif isinstance(path, Path): + path = str(path).replace("\\", "/").replace("//", "/").split("/") + else: + raise TypeError("path must be str or Path") + path[-1] = path[-1].split(".")[0] + return "/".join(path) + + def _save(self, file, save_path): + content = file.file.read() + name = save_path(file.filename) + path = self._get_path_str(save_path) + self.root_path.get_by_path(path).upload(name, content).execute_query() + + async def save_file(self, file: UploadFile, save_path: str): + await asyncio.to_thread(self._save, file, save_path) + + def _delete(self, save_path): + path = self._get_path_str(save_path) + try: + self.root_path.get_by_path(path).delete_object().execute_query() + except self._ClientRequestException as e: + if e.code == "itemNotFound": + pass + else: + raise e + + async def delete_file(self, file_code: FileCodes): + await asyncio.to_thread(self._delete, await file_code.get_file_path()) + + def _convert_link_to_download_link(self, link): + p1 = re.search(r"https://(.+)\.sharepoint\.com", link).group(1) + p2 = re.search(r"personal/(.+)/", link).group(1) + p3 = re.search(rf"{p2}/(.+)", link).group(1) + return f"https://{p1}.sharepoint.com/personal/{p2}/_layouts/52/download.aspx?share={p3}" + + def _get_file_url(self, save_path, name): + path = self._get_path_str(save_path) + remote_file = self.root_path.get_by_path(path + "/" + name) + expiration_datetime = datetime.datetime.now( + tz=datetime.timezone.utc + ) + datetime.timedelta(hours=1) + expiration_datetime = expiration_datetime.strftime( + "%Y-%m-%dT%H:%M:%SZ") + permission = remote_file.create_link( + "view", "anonymous", expiration_datetime=expiration_datetime + ).execute_query() + return self._convert_link_to_download_link(permission.link.webUrl) + + async def get_file_response(self, file_code: FileCodes): + try: + filename = file_code.prefix + file_code.suffix + link = await asyncio.to_thread( + self._get_file_url, await file_code.get_file_path(), filename + ) + + content_length = None # 初始化为 None,表示未知大小 + + # 创建ClientSession并复用 + session = aiohttp.ClientSession() + + # 尝试发送HEAD请求获取Content-Length + try: + async with session.head(link) as resp: + if resp.status == 200 and 'Content-Length' in resp.headers: + content_length = int(resp.headers['Content-Length']) + except Exception: + # 如果HEAD请求失败,则不提供 Content-Length + pass + + async def stream_generator(): + try: + async with session.get(link) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, + detail=f"从OneDrive获取文件失败: {resp.status}" + ) + chunk_size = 65536 + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + yield chunk + finally: + await session.close() + + encoded_filename = quote(filename, safe='') + headers = { + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" + } + if content_length is not None: + headers["Content-Length"] = str(content_length) + return StreamingResponse( + stream_generator(), + media_type="application/octet-stream", + headers=headers + ) + except HTTPException: + raise + except Exception: + raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试") + + async def get_file_url(self, file_code: FileCodes): + if self.proxy: + return await get_file_url(file_code.code) + else: + return await asyncio.to_thread( + self._get_file_url, + await file_code.get_file_path(), + f"{file_code.prefix}{file_code.suffix}", + ) + + def _save_chunk(self, chunk_path: str, chunk_data: bytes): + """同步保存分片到 OneDrive""" + path_parts = chunk_path.replace("\\", "/").split("/") + filename = path_parts[-1] + dir_path = "/".join(path_parts[:-1]) + + # 确保目录存在 + current_folder = self.root_path + for part in dir_path.split("/"): + if part: + try: + current_folder = current_folder.get_by_path(part).get().execute_query() + except self._ClientRequestException as e: + if e.code == "itemNotFound": + current_folder = current_folder.create_folder(part).execute_query() + else: + raise e + + # 上传分片 + current_folder.upload(filename, chunk_data).execute_query() + + async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str): + """保存分片到 OneDrive""" + chunk_path = str(Path(save_path).parent / "chunks" / upload_id / f"{chunk_index}.part") + await asyncio.to_thread(self._save_chunk, chunk_path, chunk_data) + + def _read_chunk(self, chunk_path: str) -> bytes: + """同步读取分片""" + path = self._get_path_str(chunk_path) + file_obj = self.root_path.get_by_path(path).get().execute_query() + return file_obj.get_content().execute_query().value + + def _upload_merged(self, save_path: str, data: bytes): + """同步上传合并后的文件""" + path_parts = save_path.replace("\\", "/").split("/") + filename = path_parts[-1] + dir_path = "/".join(path_parts[:-1]) + + # 确保目录存在 + current_folder = self.root_path + for part in dir_path.split("/"): + if part: + try: + current_folder = current_folder.get_by_path(part).get().execute_query() + except self._ClientRequestException as e: + if e.code == "itemNotFound": + current_folder = current_folder.create_folder(part).execute_query() + else: + raise e + + current_folder.upload(filename, data).execute_query() + + async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]: + """合并 OneDrive 上的分片文件,使用临时文件避免内存问题""" + file_sha256 = hashlib.sha256() + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + + # 使用临时文件存储合并数据 + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_path = temp_file.name + + try: + async with aiofiles.open(temp_path, 'wb') as out_file: + for i in range(chunk_info.total_chunks): + chunk_path = f"{chunk_dir}/{i}.part" + chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first() + if not chunk_record: + raise ValueError(f"分片{i}记录不存在") + try: + chunk_data = await asyncio.to_thread(self._read_chunk, chunk_path) + except Exception as e: + raise ValueError(f"分片{i}文件不存在: {e}") -class AliyunFileStorage: + current_hash = hashlib.sha256(chunk_data).hexdigest() + if current_hash != chunk_record.chunk_hash: + raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}") + + file_sha256.update(chunk_data) + await out_file.write(chunk_data) + del chunk_data # 释放内存 + + # 读取临时文件并上传 + async with aiofiles.open(temp_path, 'rb') as f: + merged_content = await f.read() + await asyncio.to_thread(self._upload_merged, save_path, merged_content) + finally: + # 清理临时文件 + if os.path.exists(temp_path): + os.unlink(temp_path) + + return save_path, file_sha256.hexdigest() + + def _delete_chunk_dir(self, chunk_dir: str): + """同步删除分片目录""" + try: + path = self._get_path_str(chunk_dir) + self.root_path.get_by_path(path).delete_object().execute_query() + except self._ClientRequestException as e: + if e.code != "itemNotFound": + raise e + + async def clean_chunks(self, upload_id: str, save_path: str): + """清理 OneDrive 上的临时分片文件""" + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + try: + await asyncio.to_thread(self._delete_chunk_dir, chunk_dir) + except Exception as e: + logger.info(f"清理 OneDrive 分片时出错: {e}") + + def _file_exists(self, save_path: str) -> bool: + """同步检查文件是否存在""" + try: + path = self._get_path_str(save_path) + self.root_path.get_by_path(path).get().execute_query() + return True + except self._ClientRequestException as e: + if e.code == "itemNotFound": + return False + raise e + + async def file_exists(self, save_path: str) -> bool: + """ + 检查文件是否存在于OneDrive + :param save_path: 文件路径 + :return: 文件是否存在 + """ + return await asyncio.to_thread(self._file_exists, save_path) + + +class OpenDALFileStorage(FileStorageInterface): def __init__(self): - auth = oss2.Auth(settings.KeyId, settings.KeySecret) - self.bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.BUCKET_NAME) - - def upload_file(self, upload_filepath, remote_filepath): - self.bucket.put_object_from_file(remote_filepath, upload_filepath) - - async def get_text(self, file: UploadFile, key: str): - ext = file.filename.split('.')[-1] - now = datetime.now() - path = f"FileCodeBox/upload/{now.year}/{now.month}/{now.day}" - text = f"{path}/{f'{key}.{ext}'}" - return f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/{text}" - - async def get_url(self, info: Codes): - text = info.text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/") - url = self.bucket.sign_url('GET', text, settings.ACCESSTIME, slash_safe=True) - return url - - @staticmethod - async def get_size(file: UploadFile): - f = file.file - f.seek(0, os.SEEK_END) - size = f.tell() - f.seek(0, os.SEEK_SET) - return size - - @staticmethod - def _save(filepath, file: BinaryIO): - with open(filepath, 'wb') as f: - chunk_size = 256 * 1024 - chunk = file.read(chunk_size) - while chunk: - f.write(chunk) - chunk = file.read(chunk_size) + try: + import opendal + except ImportError: + raise ImportError('请先安装 `opendal`, 例如: "pip install opendal"') + self.service = settings.opendal_scheme + service_settings = {} + for key, value in settings.items(): + if key.startswith("opendal_" + self.service): + setting_name = key.split("_", 2)[2] + service_settings[setting_name] = value + self.operator = opendal.AsyncOperator( + settings.opendal_scheme, **service_settings + ) + + async def save_file(self, file: UploadFile, save_path: str): + # 使用 asyncio.to_thread 避免阻塞事件循环 + content = await asyncio.to_thread(file.file.read) + await self.operator.write(save_path, content) + + async def delete_file(self, file_code: FileCodes): + await self.operator.delete(await file_code.get_file_path()) + + async def get_file_url(self, file_code: FileCodes): + return await get_file_url(file_code.code) + + async def get_file_response(self, file_code: FileCodes): + try: + filename = file_code.prefix + file_code.suffix + content_length = None # 初始化为 None,表示未知大小 + + # 尝试获取文件大小 + try: + stat_result = await self.operator.stat(await file_code.get_file_path()) + if hasattr(stat_result, 'content_length') and stat_result.content_length: + content_length = stat_result.content_length + elif hasattr(stat_result, 'size') and stat_result.size: + content_length = stat_result.size + except Exception: + # 如果获取大小失败,则不提供 Content-Length + pass + + # 尝试使用流式读取器 + try: + # OpenDAL 可能提供 reader 方法返回一个异步读取器 + reader = await self.operator.reader(await file_code.get_file_path()) + except AttributeError: + # 如果 reader 方法不存在,回退到全量读取(兼容旧版本) + content = await self.operator.read(await file_code.get_file_path()) + encoded_filename = quote(filename, safe='') + headers = { + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" + } + if content_length is not None: + headers["Content-Length"] = str(content_length) + return Response( + content, headers=headers, media_type="application/octet-stream" + ) + + async def stream_generator(): + chunk_size = 65536 + while True: + chunk = await reader.read(chunk_size) + if not chunk: + break + yield chunk + + encoded_filename = quote(filename, safe='') + headers = { + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" + } + if content_length is not None: + headers["Content-Length"] = str(content_length) + return StreamingResponse( + stream_generator(), + media_type="application/octet-stream", + headers=headers + ) + except Exception as e: + logger.info(e) + raise HTTPException(status_code=404, detail="文件已过期删除") + + async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str): + """保存分片到 OpenDAL 存储""" + chunk_path = str(Path(save_path).parent / "chunks" / upload_id / f"{chunk_index}.part") + await self.operator.write(chunk_path, chunk_data) + + async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]: + """合并 OpenDAL 存储上的分片文件,使用临时文件避免内存问题""" + file_sha256 = hashlib.sha256() + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + + # 使用临时文件存储合并数据 + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_path = temp_file.name + + try: + async with aiofiles.open(temp_path, 'wb') as out_file: + for i in range(chunk_info.total_chunks): + chunk_path = f"{chunk_dir}/{i}.part" + chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first() + if not chunk_record: + raise ValueError(f"分片{i}记录不存在") + + try: + chunk_data = await self.operator.read(chunk_path) + except Exception as e: + raise ValueError(f"分片{i}文件不存在: {e}") + + current_hash = hashlib.sha256(chunk_data).hexdigest() + if current_hash != chunk_record.chunk_hash: + raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}") + + file_sha256.update(chunk_data) + await out_file.write(chunk_data) + del chunk_data # 释放内存 + + # 读取临时文件并写入存储 + async with aiofiles.open(temp_path, 'rb') as f: + merged_content = await f.read() + await self.operator.write(save_path, merged_content) + finally: + # 清理临时文件 + if os.path.exists(temp_path): + os.unlink(temp_path) + + return save_path, file_sha256.hexdigest() + + async def clean_chunks(self, upload_id: str, save_path: str): + """清理 OpenDAL 存储上的临时分片文件""" + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + try: + # OpenDAL 支持递归删除 + await self.operator.remove_all(chunk_dir) + except Exception as e: + logger.info(f"清理 OpenDAL 分片时出错: {e}") + + async def file_exists(self, save_path: str) -> bool: + """ + 检查文件是否存在于OpenDAL存储 + :param save_path: 文件路径 + :return: 文件是否存在 + """ + try: + await self.operator.stat(save_path) + return True + except Exception: + return False + + +class WebDAVFileStorage(FileStorageInterface): + _instance: Optional["WebDAVFileStorage"] = None + + def __init__(self): + if not hasattr(self, "_initialized"): + self.base_url = settings.webdav_url.rstrip("/") + "/" + self.auth = aiohttp.BasicAuth( + login=settings.webdav_username, password=settings.webdav_password + ) + self._initialized = True + + def _build_url(self, path: str) -> str: + encoded_path = quote(str(path.replace("\\", "/").lstrip("/")).lstrip("/")) + return f"{self.base_url}{encoded_path}" + + async def _mkdir_p(self, directory_path: str): + """递归创建目录(类似mkdir -p)""" + path_obj = Path(unquote(directory_path)) + current_path = "" + + async with aiohttp.ClientSession(auth=self.auth) as session: + # 逐级检查目录是否存在 + for part in path_obj.parts: + current_path = str(Path(current_path) / part) + url = self._build_url(current_path) + + # 检查目录是否存在 + async with session.head(url) as resp: + if resp.status == 404: + # 创建目录 + async with session.request("MKCOL", url) as mkcol_resp: + if mkcol_resp.status not in (200, 201, 409): + content = await mkcol_resp.text() + raise HTTPException( + status_code=mkcol_resp.status, + detail=f"目录创建失败: {content[:200]}", + ) + + async def _is_dir_empty(self, dir_path: str) -> bool: + """检查目录是否为空""" + url = self._build_url(dir_path) + + async with aiohttp.ClientSession(auth=self.auth) as session: + async with session.request("PROPFIND", url, headers={"Depth": "1"}) as resp: + if resp.status != 207: # 207 是 Multi-Status 响应 + return False + content = await resp.text() + # 如果只有一个 response(当前目录),说明目录为空 + return content.count("") <= 1 + + async def _delete_empty_dirs(self, file_path: str, session: aiohttp.ClientSession): + """递归删除空目录""" + path_obj = Path(file_path) + current_path = path_obj.parent + + while str(current_path) != ".": + if not await self._is_dir_empty(str(current_path)): + break + + url = self._build_url(str(current_path)) + async with session.delete(url) as resp: + if resp.status not in (200, 204, 404): + break + + current_path = current_path.parent + + async def save_file(self, file: UploadFile, save_path: str): + """保存文件(自动创建目录,流式上传)""" + path_obj = Path(save_path) + directory_path = str(path_obj.parent) + # 提取原始文件名并进行清理 + filename = await sanitize_filename(path_obj.name) + # 构建安全的保存路径 + safe_save_path = str(Path(directory_path) / filename) + + try: + # 先创建目录结构 + await self._mkdir_p(directory_path) + # 上传文件(流式) + url = self._build_url(safe_save_path) + + async def file_sender(): + """流式读取文件内容""" + chunk_size = 256 * 1024 # 256KB chunks + while True: + chunk = await asyncio.to_thread(file.file.read, chunk_size) + if not chunk: + break + yield chunk + + async with aiohttp.ClientSession(auth=self.auth) as session: + async with session.put( + url, + data=file_sender(), + headers={"Content-Type": file.content_type or "application/octet-stream"} + ) as resp: + if resp.status not in (200, 201, 204): + content = await resp.text() + raise HTTPException( + status_code=resp.status, + detail=f"文件上传失败: {content[:200]}", + ) + except aiohttp.ClientError as e: + raise HTTPException( + status_code=503, detail=f"WebDAV连接异常: {str(e)}") + + async def delete_file(self, file_code: FileCodes): + """删除WebDAV文件及空目录""" + file_path = await file_code.get_file_path() + url = self._build_url(file_path) + try: + async with aiohttp.ClientSession(auth=self.auth) as session: + # 删除文件 + async with session.delete(url) as resp: + if resp.status not in (200, 204, 404): + content = await resp.text() + raise HTTPException( + status_code=resp.status, + detail=f"WebDAV删除失败: {content[:200]}", + ) + + # 使用同一个 session 删除空目录 + await self._delete_empty_dirs(file_path, session) + + except aiohttp.ClientError as e: + raise HTTPException( + status_code=503, detail=f"WebDAV连接异常: {str(e)}") + + async def get_file_url(self, file_code: FileCodes): + return await get_file_url(file_code.code) + + async def get_file_response(self, file_code: FileCodes): + """获取文件响应(代理模式)""" + try: + filename = file_code.prefix + file_code.suffix + url = self._build_url(await file_code.get_file_path()) + content_length = None # 初始化为 None,表示未知大小 + + # 创建ClientSession并复用(包含认证头) + session = aiohttp.ClientSession(headers={ + "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" + }) + + # 尝试发送HEAD请求获取Content-Length + try: + async with session.head(url) as resp: + if resp.status == 200 and 'Content-Length' in resp.headers: + content_length = int(resp.headers['Content-Length']) + except Exception: + # 如果HEAD请求失败,则不提供 Content-Length + pass + + async def stream_generator(): + try: + async with session.get(url) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, + detail=f"文件获取失败{resp.status}: {await resp.text()}", + ) + chunk_size = 65536 + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + yield chunk + finally: + await session.close() + + encoded_filename = quote(filename, safe='') + headers = { + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" + } + if content_length is not None: + headers["Content-Length"] = str(content_length) + return StreamingResponse( + stream_generator(), + media_type="application/octet-stream", + headers=headers + ) + except aiohttp.ClientError as e: + raise HTTPException( + status_code=503, detail=f"WebDAV连接异常: {str(e)}") + + async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str): + """保存分片到 WebDAV""" + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + chunk_path = f"{chunk_dir}/{chunk_index}.part" + + # 先创建目录结构 + await self._mkdir_p(chunk_dir) + + chunk_url = self._build_url(chunk_path) + async with aiohttp.ClientSession(auth=self.auth) as session: + async with session.put(chunk_url, data=chunk_data) as resp: + if resp.status not in (200, 201, 204): + content = await resp.text() + raise HTTPException( + status_code=resp.status, + detail=f"分片上传失败: {content[:200]}" + ) + + async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]: + """ + 合并 WebDAV 上的分片文件 + 使用临时文件避免内存问题 + """ + file_sha256 = hashlib.sha256() + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + + # 使用临时文件存储合并数据,避免内存问题 + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_path = temp_file.name + + try: + async with aiohttp.ClientSession(auth=self.auth) as session: + # 按顺序读取并验证每个分片,写入临时文件 + async with aiofiles.open(temp_path, 'wb') as out_file: + for i in range(chunk_info.total_chunks): + chunk_path = f"{chunk_dir}/{i}.part" + chunk_url = self._build_url(chunk_path) + + # 获取分片记录 + chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first() + if not chunk_record: + raise ValueError(f"分片{i}记录不存在") + + # 下载分片数据 + async with session.get(chunk_url) as resp: + if resp.status != 200: + raise ValueError(f"分片{i}文件不存在或无法访问") + chunk_data = await resp.read() + + # 验证哈希 + current_hash = hashlib.sha256(chunk_data).hexdigest() + if current_hash != chunk_record.chunk_hash: + raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}") + + file_sha256.update(chunk_data) + await out_file.write(chunk_data) + del chunk_data # 释放内存 + + # 确保目标目录存在 + output_dir = str(Path(save_path).parent) + await self._mkdir_p(output_dir) + + # 流式上传合并后的文件 + output_url = self._build_url(save_path) + + async def file_sender(): + async with aiofiles.open(temp_path, 'rb') as f: + while True: + chunk = await f.read(256 * 1024) + if not chunk: + break + yield chunk + + async with session.put(output_url, data=file_sender()) as resp: + if resp.status not in (200, 201, 204): + content = await resp.text() + raise HTTPException( + status_code=resp.status, + detail=f"合并文件上传失败: {content[:200]}" + ) + finally: + # 清理临时文件 + if os.path.exists(temp_path): + os.unlink(temp_path) + + return save_path, file_sha256.hexdigest() - async def save_file(self, file: UploadFile, remote_filepath: str): - now = int(datetime.now().timestamp()) - upload_filepath = settings.DATA_ROOT + str(now) - await asyncio.to_thread(self._save, upload_filepath, file.file) - self.upload_file(upload_filepath, remote_filepath) - remote_filepath = remote_filepath.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/") - self.upload_file(upload_filepath, remote_filepath) - await asyncio.to_thread(os.remove, upload_filepath) + async def clean_chunks(self, upload_id: str, save_path: str): + """ + 清理 WebDAV 上的临时分片文件 + :param upload_id: 上传会话ID + :param save_path: 文件保存路径 + """ + chunk_dir = str(Path(save_path).parent / "chunks" / upload_id) + chunk_dir_url = self._build_url(chunk_dir) + async with aiohttp.ClientSession(auth=self.auth) as session: + try: + # 检查分片目录是否存在 + async with session.request("PROPFIND", chunk_dir_url, headers={"Depth": "1"}) as resp: + if resp.status == 207: # 207 表示 Multi-Status + # 获取目录下的所有分片文件 + xml_data = await resp.text() + file_paths = re.findall( + r'(.*?)', xml_data) + for file_path in file_paths: + if file_path.endswith(".part"): + # 删除分片文件 + file_url = self._build_url(file_path) + async with session.delete(file_url) as delete_resp: + if delete_resp.status not in (200, 204, 404): + logger.info(f"删除分片文件失败: {file_path}") - async def delete_files(self, texts): - tasks = [self.delete_file(text) for text in texts] - await asyncio.gather(*tasks) + # 删除分片目录 + async with session.delete(chunk_dir_url) as delete_resp: + if delete_resp.status not in (200, 204, 404): + logger.info(f"删除分片目录失败: {chunk_dir_url}") + else: + logger.info(f"分片目录不存在: {chunk_dir_url}") + except Exception as e: + logger.info(f"清理 WebDAV 分片时出错: {e}") - async def delete_file(self, text: str): - text = text.strip(f"https://{settings.BUCKET_NAME}.{settings.OSS_ENDPOINT}/") - self.bucket.delete_object(text) + async def file_exists(self, save_path: str) -> bool: + """ + 检查文件是否存在于WebDAV + :param save_path: 文件路径 + :return: 文件是否存在 + """ + url = self._build_url(save_path) + async with aiohttp.ClientSession(auth=self.auth) as session: + async with session.head(url) as resp: + return resp.status == 200 -STORAGE_ENGINE = { - "filesystem": FileSystemStorage, - "aliyunsystem": AliyunFileStorage +storages = { + "local": SystemFileStorage, + "s3": S3FileStorage, + "onedrive": OneDriveFileStorage, + "opendal": OpenDALFileStorage, + "webdav": WebDAVFileStorage, } diff --git a/core/tasks.py b/core/tasks.py new file mode 100644 index 000000000..1347c0d33 --- /dev/null +++ b/core/tasks.py @@ -0,0 +1,86 @@ +# @Time : 2023/8/15 22:00 +# @Author : Lan +# @File : tasks.py +# @Software: PyCharm +import asyncio +import datetime +import logging +import os + +from tortoise.expressions import Q + +from apps.base.models import FileCodes, UploadChunk +from apps.base.utils import ip_limit, get_chunk_file_path_name +from core.config import refresh_settings +from core.settings import settings, data_root +from core.storage import FileStorageInterface, storages +from core.utils import get_now + + +async def delete_expire_files(): + while True: + try: + await refresh_settings() + file_storage: FileStorageInterface = storages[settings.file_storage]() + # 遍历 share目录下的所有文件夹,删除空的文件夹,并判断父目录是否为空,如果为空也删除 + if settings.file_storage == "local": + for root, dirs, files in os.walk(f"{data_root}/share/data"): + if not dirs and not files: + os.rmdir(root) + await ip_limit["error"].remove_expired_ip() + await ip_limit["upload"].remove_expired_ip() + expire_data = await FileCodes.filter( + Q(expired_at__lt=await get_now()) | Q(expired_count=0) + ).all() + for exp in expire_data: + try: + await file_storage.delete_file(exp) + except Exception as e: + logging.error(f"删除过期文件失败 code={exp.code}: {e}") + try: + await exp.delete() + except Exception as e: + logging.error(f"删除记录失败 code={exp.code}: {e}") + except Exception as e: + logging.error(e) + finally: + await asyncio.sleep(600) + + +async def clean_incomplete_uploads(): + while True: + try: + await refresh_settings() + file_storage: FileStorageInterface = storages[settings.file_storage]() + expire_hours = getattr(settings, "chunk_expire_hours", 24) + now = await get_now() + expire_time = now - datetime.timedelta(hours=expire_hours) + expired_sessions = await UploadChunk.filter( + chunk_index=-1, created_at__lt=expire_time + ).all() + + for session in expired_sessions: + try: + save_path = session.save_path + if not save_path: + _, _, _, _, save_path = await get_chunk_file_path_name( + session.file_name, session.upload_id + ) + await file_storage.clean_chunks(session.upload_id, save_path) + except Exception as e: + logging.error( + f"清理分片文件失败 upload_id={session.upload_id}: {e}" + ) + + try: + await UploadChunk.filter(upload_id=session.upload_id).delete() + logging.info(f"已清理过期上传会话 upload_id={session.upload_id}") + except Exception as e: + logging.error( + f"删除分片记录失败 upload_id={session.upload_id}: {e}" + ) + + except Exception as e: + logging.error(f"清理未完成上传任务异常: {e}") + finally: + await asyncio.sleep(3600) diff --git a/core/utils.py b/core/utils.py index 7999c7df5..0aca61953 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,83 +1,159 @@ +# @Time : 2023/8/13 19:54 +# @Author : Lan +# @File : utils.py +# @Software: PyCharm import datetime import hashlib +import os import random -import asyncio +import re import string import time -from sqlalchemy import or_, select, delete -from sqlalchemy.ext.asyncio.session import AsyncSession -from .database import Codes, engine -from .depends import IPRateLimit -from .storage import STORAGE_ENGINE -from settings import settings - -storage = STORAGE_ENGINE[settings.STORAGE_ENGINE]() - -# 错误IP限制器 -error_ip_limit = IPRateLimit(settings.ERROR_COUNT, settings.ERROR_MINUTE) -# 上传文件限制器 -upload_ip_limit = IPRateLimit(settings.UPLOAD_COUNT, settings.UPLOAD_MINUTE) - - -async def delete_expire_files(): - while True: - async with AsyncSession(engine, expire_on_commit=False) as s: - await error_ip_limit.remove_expired_ip() - await upload_ip_limit.remove_expired_ip() - query = select(Codes).where(or_(Codes.exp_time < datetime.datetime.now(), Codes.count == 0)) - exps = (await s.execute(query)).scalars().all() - files = [] - exps_ids = [] - for exp in exps: - if exp.type != "text": - files.append(exp.text) - exps_ids.append(exp.id) - await storage.delete_files(files) - query = delete(Codes).where(Codes.id.in_(exps_ids)) - await s.execute(query) - await s.commit() - await asyncio.sleep(settings.DELETE_EXPIRE_FILES_INTERVAL * 60) +from core.settings import settings async def get_random_num(): + """ + 获取随机数 + :return: + """ return random.randint(10000, 99999) +r_s = string.ascii_uppercase + string.digits + + async def get_random_string(): - r_s = string.ascii_letters + string.digits - return ''.join(random.choice(r_s) for _ in range(5)).upper() - - -async def get_code(s: AsyncSession, exp_style): - if exp_style == 'forever': - generate = get_random_string - else: - generate = get_random_num - code = await generate() - while (await s.execute(select(Codes.id).where(Codes.code == code))).scalar(): - code = await generate() - return str(code) - - -async def get_token(ip, code): - return hashlib.sha256(f"{ip}{code}{int(time.time() / 1000)}000{settings.ADMIN_PASSWORD}".encode()).hexdigest() - - -async def get_expire_info(expire_style, expire_value, s): - now = datetime.datetime.now() - if expire_value <= 0 or expire_value > 999: - return True, None, None - code = await get_code(s, expire_style) - if expire_style == 'day': - return False, now + datetime.timedelta(days=expire_value), -1, code - elif expire_style == 'hour': - return False, now + datetime.timedelta(hours=expire_value), -1, code - elif expire_style == 'minute': - return False, now + datetime.timedelta(minutes=expire_value), -1, code - elif expire_style == 'forever': - return False, None, -1, code - elif expire_style == 'count': - return False, None, expire_value, code - else: - return True, None, None + """ + 获取随机字符串 + :return: + """ + return "".join(random.choice(r_s) for _ in range(5)) + + +async def get_now(): + """ + 获取当前时间 + :return: + """ + return datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))) + + +async def get_select_token(code: str): + """ + 获取下载token + :param code: + :return: + """ + token = settings.admin_token + return hashlib.sha256( + f"{code}{int(time.time() / 1000)}000{token}".encode() + ).hexdigest() + + +async def get_file_url(code: str): + """ + 对于需要通过服务器中转下载的服务,获取文件下载地址 + :param code: + :return: + """ + return f"/share/download?key={await get_select_token(code)}&code={code}" + + +async def max_save_times_desc(max_save_seconds: int): + """ + 获取最大保存时间的描述 + :param max_save_seconds: + :return: + """ + + def gen_desc_zh(value: int, desc: str): + if value > 0: + return f"{value}{desc}" + else: + return "" + + def gen_desc_en(value: int, desc: str): + if value > 0: + ret = f"{value} {desc}" + if value > 1: + ret += "s" + ret += " " + return ret + else: + return "" + + max_timedelta = datetime.timedelta(seconds=max_save_seconds) + desc_zh, desc_en = "最长保存时间:", "Max save time: " + desc_zh += gen_desc_zh(max_timedelta.days, "天") + desc_en += gen_desc_en(max_timedelta.days, "day") + desc_zh += gen_desc_zh(max_timedelta.seconds // 3600, "小时") + desc_en += gen_desc_en(max_timedelta.seconds // 3600, "hour") + desc_zh += gen_desc_zh(max_timedelta.seconds % 3600 // 60, "分钟") + desc_en += gen_desc_en(max_timedelta.seconds % 3600 // 60, "minute") + desc_zh += gen_desc_zh(max_timedelta.seconds % 60, "秒") + desc_en += gen_desc_en(max_timedelta.seconds % 60, "second") + return desc_zh, desc_en + + +def hash_password(password: str) -> str: + """ + 使用 SHA256 + salt 哈希密码 + 返回格式: sha256$$ + """ + salt = os.urandom(16).hex() + password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest() + return f"sha256${salt}${password_hash}" + + +def verify_password(password: str, hashed: str) -> bool: + """ + 验证密码是否匹配 + 支持新格式 (sha256$salt$hash) 和旧格式 (明文) + """ + if not hashed: + return False + + # 新格式: sha256$salt$hash + if hashed.startswith("sha256$"): + parts = hashed.split("$") + if len(parts) != 3: + return False + _, salt, stored_hash = parts + password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest() + return password_hash == stored_hash + + # 旧格式: 明文比较 (兼容迁移前的数据) + return password == hashed + + +def is_password_hashed(password: str) -> bool: + """ + 检查密码是否已经是哈希格式 + """ + return password.startswith("sha256$") and len(password.split("$")) == 3 + + +async def sanitize_filename(filename: str) -> str: + """ + 安全处理文件名: + 1. 剥离路径只保留文件名 + 2. 替换非法字符 + 3. 处理空文件名情况 + """ + filename = os.path.basename(filename) + illegal_chars = r'[\\/*?:"<>|\x00-\x1F]' # 包含控制字符 + # 替换非法字符为下划线 + cleaned = re.sub(illegal_chars, "_", filename) + # 处理空格(可选替换为_) + cleaned = cleaned.replace(" ", "_") + # 处理连续下划线 + cleaned = re.sub(r"_+", "_", cleaned) + # 处理首尾特殊字符 + cleaned = cleaned.strip("._") + # 处理空文件名情况 + if not cleaned: + cleaned = "unnamed_file" + # 长度限制(按需调整) + return cleaned[:255] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..eff13d85d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" +services: + file-code-box: + image: lanol/filecodebox:latest + volumes: + - fcb-data:/app/data:rw + restart: unless-stopped + ports: + - "12345:12345" +volumes: + fcb-data: + external: false \ No newline at end of file diff --git a/docs/.vitepress/cache/deps/_metadata.json b/docs/.vitepress/cache/deps/_metadata.json new file mode 100644 index 000000000..02bdaba8f --- /dev/null +++ b/docs/.vitepress/cache/deps/_metadata.json @@ -0,0 +1,52 @@ +{ + "hash": "8f855eaf", + "configHash": "1b3ca22f", + "lockfileHash": "bd28b2c2", + "browserHash": "29e84937", + "optimized": { + "vue": { + "src": "../../../node_modules/.pnpm/vue@3.5.13/node_modules/vue/dist/vue.runtime.esm-bundler.js", + "file": "vue.js", + "fileHash": "3215885f", + "needsInterop": false + }, + "vitepress > @vue/devtools-api": { + "src": "../../../node_modules/.pnpm/@vue+devtools-api@7.7.1/node_modules/@vue/devtools-api/dist/index.js", + "file": "vitepress___@vue_devtools-api.js", + "fileHash": "5a5f95ef", + "needsInterop": false + }, + "vitepress > @vueuse/core": { + "src": "../../../node_modules/.pnpm/@vueuse+core@12.5.0/node_modules/@vueuse/core/index.mjs", + "file": "vitepress___@vueuse_core.js", + "fileHash": "0fbf66f0", + "needsInterop": false + }, + "vitepress > @vueuse/integrations/useFocusTrap": { + "src": "../../../node_modules/.pnpm/@vueuse+integrations@12.5.0_focus-trap@7.6.4/node_modules/@vueuse/integrations/useFocusTrap.mjs", + "file": "vitepress___@vueuse_integrations_useFocusTrap.js", + "fileHash": "91b03896", + "needsInterop": false + }, + "vitepress > mark.js/src/vanilla.js": { + "src": "../../../node_modules/.pnpm/mark.js@8.11.1/node_modules/mark.js/src/vanilla.js", + "file": "vitepress___mark__js_src_vanilla__js.js", + "fileHash": "99e4f81c", + "needsInterop": false + }, + "vitepress > minisearch": { + "src": "../../../node_modules/.pnpm/minisearch@7.1.1/node_modules/minisearch/dist/es/index.js", + "file": "vitepress___minisearch.js", + "fileHash": "cc176c9c", + "needsInterop": false + } + }, + "chunks": { + "chunk-KT7LHMJ2": { + "file": "chunk-KT7LHMJ2.js" + }, + "chunk-CQOUZRMK": { + "file": "chunk-CQOUZRMK.js" + } + } +} \ No newline at end of file diff --git a/docs/.vitepress/cache/deps/chunk-CQOUZRMK.js b/docs/.vitepress/cache/deps/chunk-CQOUZRMK.js new file mode 100644 index 000000000..bc476d6ff --- /dev/null +++ b/docs/.vitepress/cache/deps/chunk-CQOUZRMK.js @@ -0,0 +1,12542 @@ +// node_modules/.pnpm/@vue+shared@3.5.13/node_modules/@vue/shared/dist/shared.esm-bundler.js +function makeMap(str) { + const map2 = /* @__PURE__ */ Object.create(null); + for (const key of str.split(",")) map2[key] = 1; + return (val) => val in map2; +} +var EMPTY_OBJ = true ? Object.freeze({}) : {}; +var EMPTY_ARR = true ? Object.freeze([]) : []; +var NOOP = () => { +}; +var NO = () => false; +var isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter +(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); +var isModelListener = (key) => key.startsWith("onUpdate:"); +var extend = Object.assign; +var remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var hasOwn = (val, key) => hasOwnProperty.call(val, key); +var isArray = Array.isArray; +var isMap = (val) => toTypeString(val) === "[object Map]"; +var isSet = (val) => toTypeString(val) === "[object Set]"; +var isDate = (val) => toTypeString(val) === "[object Date]"; +var isRegExp = (val) => toTypeString(val) === "[object RegExp]"; +var isFunction = (val) => typeof val === "function"; +var isString = (val) => typeof val === "string"; +var isSymbol = (val) => typeof val === "symbol"; +var isObject = (val) => val !== null && typeof val === "object"; +var isPromise = (val) => { + return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); +}; +var objectToString = Object.prototype.toString; +var toTypeString = (value) => objectToString.call(value); +var toRawType = (value) => { + return toTypeString(value).slice(8, -1); +}; +var isPlainObject = (val) => toTypeString(val) === "[object Object]"; +var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; +var isReservedProp = makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" +); +var isBuiltInDirective = makeMap( + "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo" +); +var cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return (str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; +}; +var camelizeRE = /-(\w)/g; +var camelize = cacheStringFunction( + (str) => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : ""); + } +); +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() +); +var capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}); +var toHandlerKey = cacheStringFunction( + (str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; + } +); +var hasChanged = (value, oldValue) => !Object.is(value, oldValue); +var invokeArrayFns = (fns, ...arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](...arg); + } +}; +var def = (obj, key, value, writable = false) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + writable, + value + }); +}; +var looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; +var toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; +}; +var _globalThis; +var getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); +}; +var GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol"; +var isGloballyAllowed = makeMap(GLOBALS_ALLOWED); +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject(value)) { + return value; + } +} +var listDelimiterRE = /;(?![^(]*\))/g; +var propertyDelimiterRE = /:([^]+)/; +var styleCommentRE = /\/\*[^]*?\*\//g; +function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function stringifyStyle(styles) { + if (!styles) return ""; + if (isString(styles)) return styles; + let ret = ""; + for (const key in styles) { + const value = styles[key]; + if (isString(value) || typeof value === "number") { + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + ret += `${normalizedKey}:${value};`; + } + } + return ret; +} +function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); +} +function normalizeProps(props) { + if (!props) return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; +} +var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"; +var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"; +var MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"; +var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"; +var isHTMLTag = makeMap(HTML_TAGS); +var isSVGTag = makeMap(SVG_TAGS); +var isMathMLTag = makeMap(MATH_TAGS); +var isVoidTag = makeMap(VOID_TAGS); +var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; +var isSpecialBooleanAttr = makeMap(specialBooleanAttrs); +var isBooleanAttr = makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` +); +function includeBooleanAttr(value) { + return !!value || value === ""; +} +var isKnownHtmlAttr = makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` +); +var isKnownSvgAttr = makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` +); +var isKnownMathMLAttr = makeMap( + `accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns` +); +function isRenderableAttrValue(value) { + if (value == null) { + return false; + } + const type = typeof value; + return type === "string" || type === "number" || type === "boolean"; +} +var cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g; +function getEscapedCssVarName(key, doubleEscape) { + return key.replace( + cssVarNameEscapeSymbolsRE, + (s) => doubleEscape ? s === '"' ? '\\\\\\"' : `\\\\${s}` : `\\${s}` + ); +} +function looseCompareArrays(a, b) { + if (a.length !== b.length) return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; +} +function looseEqual(a, b) { + if (a === b) return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); +} +function looseIndexOf(arr, val) { + return arr.findIndex((item) => looseEqual(item, val)); +} +var isRef = (val) => { + return !!(val && val["__v_isRef"] === true); +}; +var toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val); +}; +var replacer = (_key, val) => { + if (isRef(val)) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; +}; +var stringifySymbol = (v, i = "") => { + var _a; + return ( + // Symbol.description in es2019+ so we need to cast here to pass + // the lib: es2016 check + isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v + ); +}; + +// node_modules/.pnpm/@vue+reactivity@3.5.13/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js +function warn(msg, ...args) { + console.warn(`[Vue warn] ${msg}`, ...args); +} +var activeEffectScope; +var EffectScope = class { + constructor(detached = false) { + this.detached = detached; + this._active = true; + this.effects = []; + this.cleanups = []; + this._isPaused = false; + this.parent = activeEffectScope; + if (!detached && activeEffectScope) { + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } + } + get active() { + return this._active; + } + pause() { + if (this._active) { + this._isPaused = true; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause(); + } + } + } + /** + * Resumes the effect scope, including all child scopes and effects. + */ + resume() { + if (this._active) { + if (this._isPaused) { + this._isPaused = false; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(); + } + } + } + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } else if (true) { + warn(`cannot run an inactive effect scope.`); + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + activeEffectScope = this; + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + activeEffectScope = this.parent; + } + stop(fromParent) { + if (this._active) { + this._active = false; + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + this.effects.length = 0; + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + this.cleanups.length = 0; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + this.scopes.length = 0; + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + } + } +}; +function effectScope(detached) { + return new EffectScope(detached); +} +function getCurrentScope() { + return activeEffectScope; +} +function onScopeDispose(fn, failSilently = false) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } else if (!failSilently) { + warn( + `onScopeDispose() is called when there is no active effect scope to be associated with.` + ); + } +} +var activeSub; +var pausedQueueEffects = /* @__PURE__ */ new WeakSet(); +var ReactiveEffect = class { + constructor(fn) { + this.fn = fn; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 1 | 4; + this.next = void 0; + this.cleanup = void 0; + this.scheduler = void 0; + if (activeEffectScope && activeEffectScope.active) { + activeEffectScope.effects.push(this); + } + } + pause() { + this.flags |= 64; + } + resume() { + if (this.flags & 64) { + this.flags &= ~64; + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this); + this.trigger(); + } + } + } + /** + * @internal + */ + notify() { + if (this.flags & 2 && !(this.flags & 32)) { + return; + } + if (!(this.flags & 8)) { + batch(this); + } + } + run() { + if (!(this.flags & 1)) { + return this.fn(); + } + this.flags |= 2; + cleanupEffect(this); + prepareDeps(this); + const prevEffect = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = this; + shouldTrack = true; + try { + return this.fn(); + } finally { + if (activeSub !== this) { + warn( + "Active effect was not restored correctly - this is likely a Vue internal bug." + ); + } + cleanupDeps(this); + activeSub = prevEffect; + shouldTrack = prevShouldTrack; + this.flags &= ~2; + } + } + stop() { + if (this.flags & 1) { + for (let link = this.deps; link; link = link.nextDep) { + removeSub(link); + } + this.deps = this.depsTail = void 0; + cleanupEffect(this); + this.onStop && this.onStop(); + this.flags &= ~1; + } + } + trigger() { + if (this.flags & 64) { + pausedQueueEffects.add(this); + } else if (this.scheduler) { + this.scheduler(); + } else { + this.runIfDirty(); + } + } + /** + * @internal + */ + runIfDirty() { + if (isDirty(this)) { + this.run(); + } + } + get dirty() { + return isDirty(this); + } +}; +var batchDepth = 0; +var batchedSub; +var batchedComputed; +function batch(sub, isComputed = false) { + sub.flags |= 8; + if (isComputed) { + sub.next = batchedComputed; + batchedComputed = sub; + return; + } + sub.next = batchedSub; + batchedSub = sub; +} +function startBatch() { + batchDepth++; +} +function endBatch() { + if (--batchDepth > 0) { + return; + } + if (batchedComputed) { + let e = batchedComputed; + batchedComputed = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= ~8; + e = next; + } + } + let error; + while (batchedSub) { + let e = batchedSub; + batchedSub = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= ~8; + if (e.flags & 1) { + try { + ; + e.trigger(); + } catch (err) { + if (!error) error = err; + } + } + e = next; + } + } + if (error) throw error; +} +function prepareDeps(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + link.version = -1; + link.prevActiveLink = link.dep.activeLink; + link.dep.activeLink = link; + } +} +function cleanupDeps(sub) { + let head; + let tail = sub.depsTail; + let link = tail; + while (link) { + const prev = link.prevDep; + if (link.version === -1) { + if (link === tail) tail = prev; + removeSub(link); + removeDep(link); + } else { + head = link; + } + link.dep.activeLink = link.prevActiveLink; + link.prevActiveLink = void 0; + link = prev; + } + sub.deps = head; + sub.depsTail = tail; +} +function isDirty(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) { + return true; + } + } + if (sub._dirty) { + return true; + } + return false; +} +function refreshComputed(computed3) { + if (computed3.flags & 4 && !(computed3.flags & 16)) { + return; + } + computed3.flags &= ~16; + if (computed3.globalVersion === globalVersion) { + return; + } + computed3.globalVersion = globalVersion; + const dep = computed3.dep; + computed3.flags |= 2; + if (dep.version > 0 && !computed3.isSSR && computed3.deps && !isDirty(computed3)) { + computed3.flags &= ~2; + return; + } + const prevSub = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = computed3; + shouldTrack = true; + try { + prepareDeps(computed3); + const value = computed3.fn(computed3._value); + if (dep.version === 0 || hasChanged(value, computed3._value)) { + computed3._value = value; + dep.version++; + } + } catch (err) { + dep.version++; + throw err; + } finally { + activeSub = prevSub; + shouldTrack = prevShouldTrack; + cleanupDeps(computed3); + computed3.flags &= ~2; + } +} +function removeSub(link, soft = false) { + const { dep, prevSub, nextSub } = link; + if (prevSub) { + prevSub.nextSub = nextSub; + link.prevSub = void 0; + } + if (nextSub) { + nextSub.prevSub = prevSub; + link.nextSub = void 0; + } + if (dep.subsHead === link) { + dep.subsHead = nextSub; + } + if (dep.subs === link) { + dep.subs = prevSub; + if (!prevSub && dep.computed) { + dep.computed.flags &= ~4; + for (let l = dep.computed.deps; l; l = l.nextDep) { + removeSub(l, true); + } + } + } + if (!soft && !--dep.sc && dep.map) { + dep.map.delete(dep.key); + } +} +function removeDep(link) { + const { prevDep, nextDep } = link; + if (prevDep) { + prevDep.nextDep = nextDep; + link.prevDep = void 0; + } + if (nextDep) { + nextDep.prevDep = prevDep; + link.nextDep = void 0; + } +} +function effect(fn, options) { + if (fn.effect instanceof ReactiveEffect) { + fn = fn.effect.fn; + } + const e = new ReactiveEffect(fn); + if (options) { + extend(e, options); + } + try { + e.run(); + } catch (err) { + e.stop(); + throw err; + } + const runner = e.run.bind(e); + runner.effect = e; + return runner; +} +function stop(runner) { + runner.effect.stop(); +} +var shouldTrack = true; +var trackStack = []; +function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; +} +function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; +} +function cleanupEffect(e) { + const { cleanup } = e; + e.cleanup = void 0; + if (cleanup) { + const prevSub = activeSub; + activeSub = void 0; + try { + cleanup(); + } finally { + activeSub = prevSub; + } + } +} +var globalVersion = 0; +var Link = class { + constructor(sub, dep) { + this.sub = sub; + this.dep = dep; + this.version = dep.version; + this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0; + } +}; +var Dep = class { + constructor(computed3) { + this.computed = computed3; + this.version = 0; + this.activeLink = void 0; + this.subs = void 0; + this.map = void 0; + this.key = void 0; + this.sc = 0; + if (true) { + this.subsHead = void 0; + } + } + track(debugInfo) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + let link = this.activeLink; + if (link === void 0 || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + } + addSub(link); + } else if (link.version === -1) { + link.version = this.version; + if (link.nextDep) { + const next = link.nextDep; + next.prevDep = link.prevDep; + if (link.prevDep) { + link.prevDep.nextDep = next; + } + link.prevDep = activeSub.depsTail; + link.nextDep = void 0; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + if (activeSub.deps === link) { + activeSub.deps = next; + } + } + } + if (activeSub.onTrack) { + activeSub.onTrack( + extend( + { + effect: activeSub + }, + debugInfo + ) + ); + } + return link; + } + trigger(debugInfo) { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + notify(debugInfo) { + startBatch(); + try { + if (true) { + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & 8)) { + head.sub.onTrigger( + extend( + { + effect: head.sub + }, + debugInfo + ) + ); + } + } + } + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + ; + link.sub.dep.notify(); + } + } + } finally { + endBatch(); + } + } +}; +function addSub(link) { + link.dep.sc++; + if (link.sub.flags & 4) { + const computed3 = link.dep.computed; + if (computed3 && !link.dep.subs) { + computed3.flags |= 4 | 16; + for (let l = computed3.deps; l; l = l.nextDep) { + addSub(l); + } + } + const currentTail = link.dep.subs; + if (currentTail !== link) { + link.prevSub = currentTail; + if (currentTail) currentTail.nextSub = link; + } + if (link.dep.subsHead === void 0) { + link.dep.subsHead = link; + } + link.dep.subs = link; + } +} +var targetMap = /* @__PURE__ */ new WeakMap(); +var ITERATE_KEY = Symbol( + true ? "Object iterate" : "" +); +var MAP_KEY_ITERATE_KEY = Symbol( + true ? "Map keys iterate" : "" +); +var ARRAY_ITERATE_KEY = Symbol( + true ? "Array iterate" : "" +); +function track(target, type, key) { + if (shouldTrack && activeSub) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = new Dep()); + dep.map = depsMap; + dep.key = key; + } + if (true) { + dep.track({ + target, + type, + key + }); + } else { + dep.track(); + } + } +} +function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + globalVersion++; + return; + } + const run = (dep) => { + if (dep) { + if (true) { + dep.trigger({ + target, + type, + key, + newValue, + oldValue, + oldTarget + }); + } else { + dep.trigger(); + } + } + }; + startBatch(); + if (type === "clear") { + depsMap.forEach(run); + } else { + const targetIsArray = isArray(target); + const isArrayIndex = targetIsArray && isIntegerKey(key); + if (targetIsArray && key === "length") { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) { + run(dep); + } + }); + } else { + if (key !== void 0 || depsMap.has(void 0)) { + run(depsMap.get(key)); + } + if (isArrayIndex) { + run(depsMap.get(ARRAY_ITERATE_KEY)); + } + switch (type) { + case "add": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isArrayIndex) { + run(depsMap.get("length")); + } + break; + case "delete": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + run(depsMap.get(ITERATE_KEY)); + } + break; + } + } + } + endBatch(); +} +function getDepFromReactive(object, key) { + const depMap = targetMap.get(object); + return depMap && depMap.get(key); +} +function reactiveReadArray(array) { + const raw = toRaw(array); + if (raw === array) return raw; + track(raw, "iterate", ARRAY_ITERATE_KEY); + return isShallow(array) ? raw : raw.map(toReactive); +} +function shallowReadArray(arr) { + track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY); + return arr; +} +var arrayInstrumentations = { + __proto__: null, + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, toReactive); + }, + concat(...args) { + return reactiveReadArray(this).concat( + ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x) + ); + }, + entries() { + return iterator(this, "entries", (value) => { + value[1] = toReactive(value[1]); + return value; + }); + }, + every(fn, thisArg) { + return apply(this, "every", fn, thisArg, void 0, arguments); + }, + filter(fn, thisArg) { + return apply(this, "filter", fn, thisArg, (v) => v.map(toReactive), arguments); + }, + find(fn, thisArg) { + return apply(this, "find", fn, thisArg, toReactive, arguments); + }, + findIndex(fn, thisArg) { + return apply(this, "findIndex", fn, thisArg, void 0, arguments); + }, + findLast(fn, thisArg) { + return apply(this, "findLast", fn, thisArg, toReactive, arguments); + }, + findLastIndex(fn, thisArg) { + return apply(this, "findLastIndex", fn, thisArg, void 0, arguments); + }, + // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement + forEach(fn, thisArg) { + return apply(this, "forEach", fn, thisArg, void 0, arguments); + }, + includes(...args) { + return searchProxy(this, "includes", args); + }, + indexOf(...args) { + return searchProxy(this, "indexOf", args); + }, + join(separator) { + return reactiveReadArray(this).join(separator); + }, + // keys() iterator only reads `length`, no optimisation required + lastIndexOf(...args) { + return searchProxy(this, "lastIndexOf", args); + }, + map(fn, thisArg) { + return apply(this, "map", fn, thisArg, void 0, arguments); + }, + pop() { + return noTracking(this, "pop"); + }, + push(...args) { + return noTracking(this, "push", args); + }, + reduce(fn, ...args) { + return reduce(this, "reduce", fn, args); + }, + reduceRight(fn, ...args) { + return reduce(this, "reduceRight", fn, args); + }, + shift() { + return noTracking(this, "shift"); + }, + // slice could use ARRAY_ITERATE but also seems to beg for range tracking + some(fn, thisArg) { + return apply(this, "some", fn, thisArg, void 0, arguments); + }, + splice(...args) { + return noTracking(this, "splice", args); + }, + toReversed() { + return reactiveReadArray(this).toReversed(); + }, + toSorted(comparer) { + return reactiveReadArray(this).toSorted(comparer); + }, + toSpliced(...args) { + return reactiveReadArray(this).toSpliced(...args); + }, + unshift(...args) { + return noTracking(this, "unshift", args); + }, + values() { + return iterator(this, "values", toReactive); + } +}; +function iterator(self2, method, wrapValue) { + const arr = shallowReadArray(self2); + const iter = arr[method](); + if (arr !== self2 && !isShallow(self2)) { + iter._next = iter.next; + iter.next = () => { + const result = iter._next(); + if (result.value) { + result.value = wrapValue(result.value); + } + return result; + }; + } + return iter; +} +var arrayProto = Array.prototype; +function apply(self2, method, fn, thisArg, wrappedRetFn, args) { + const arr = shallowReadArray(self2); + const needsWrap = arr !== self2 && !isShallow(self2); + const methodFn = arr[method]; + if (methodFn !== arrayProto[method]) { + const result2 = methodFn.apply(self2, args); + return needsWrap ? toReactive(result2) : result2; + } + let wrappedFn = fn; + if (arr !== self2) { + if (needsWrap) { + wrappedFn = function(item, index) { + return fn.call(this, toReactive(item), index, self2); + }; + } else if (fn.length > 2) { + wrappedFn = function(item, index) { + return fn.call(this, item, index, self2); + }; + } + } + const result = methodFn.call(arr, wrappedFn, thisArg); + return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result; +} +function reduce(self2, method, fn, args) { + const arr = shallowReadArray(self2); + let wrappedFn = fn; + if (arr !== self2) { + if (!isShallow(self2)) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, toReactive(item), index, self2); + }; + } else if (fn.length > 3) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, item, index, self2); + }; + } + } + return arr[method](wrappedFn, ...args); +} +function searchProxy(self2, method, args) { + const arr = toRaw(self2); + track(arr, "iterate", ARRAY_ITERATE_KEY); + const res = arr[method](...args); + if ((res === -1 || res === false) && isProxy(args[0])) { + args[0] = toRaw(args[0]); + return arr[method](...args); + } + return res; +} +function noTracking(self2, method, args = []) { + pauseTracking(); + startBatch(); + const res = toRaw(self2)[method].apply(self2, args); + endBatch(); + resetTracking(); + return res; +} +var isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); +var builtInSymbols = new Set( + Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) +); +function hasOwnProperty2(key) { + if (!isSymbol(key)) key = String(key); + const obj = toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); +} +var BaseReactiveHandler = class { + constructor(_isReadonly = false, _isShallow = false) { + this._isReadonly = _isReadonly; + this._isShallow = _isShallow; + } + get(target, key, receiver) { + if (key === "__v_skip") return target["__v_skip"]; + const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return isShallow2; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the receiver is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + let fn; + if (targetIsArray && (fn = arrayInstrumentations[key])) { + return fn; + } + if (key === "hasOwnProperty") { + return hasOwnProperty2; + } + } + const res = Reflect.get( + target, + key, + // if this is a proxy wrapping a ref, return methods using the raw ref + // as receiver so that we don't have to call `toRaw` on the ref in all + // its class methods + isRef2(target) ? target : receiver + ); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (isShallow2) { + return res; + } + if (isRef2(res)) { + return targetIsArray && isIntegerKey(key) ? res : res.value; + } + if (isObject(res)) { + return isReadonly2 ? readonly(res) : reactive(res); + } + return res; + } +}; +var MutableReactiveHandler = class extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(false, isShallow2); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + if (!this._isShallow) { + const isOldValueReadonly = isReadonly(oldValue); + if (!isShallow(value) && !isReadonly(value)) { + oldValue = toRaw(oldValue); + value = toRaw(value); + } + if (!isArray(target) && isRef2(oldValue) && !isRef2(value)) { + if (isOldValueReadonly) { + return false; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set( + target, + key, + value, + isRef2(target) ? target : receiver + ); + if (target === toRaw(receiver)) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + const oldValue = target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } +}; +var ReadonlyReactiveHandler = class extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(true, isShallow2); + } + set(target, key) { + if (true) { + warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + deleteProperty(target, key) { + if (true) { + warn( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } +}; +var mutableHandlers = new MutableReactiveHandler(); +var readonlyHandlers = new ReadonlyReactiveHandler(); +var shallowReactiveHandlers = new MutableReactiveHandler(true); +var shallowReadonlyHandlers = new ReadonlyReactiveHandler(true); +var toShallow = (value) => value; +var getProto = (v) => Reflect.getPrototypeOf(v); +function createIterableMethod(method, isReadonly2, isShallow2) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + }, + // iterable protocol + [Symbol.iterator]() { + return this; + } + }; + }; +} +function createReadonlyMethod(type) { + return function(...args) { + if (true) { + const key = args[0] ? `on key "${args[0]}" ` : ``; + warn( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ); + } + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; +} +function createInstrumentations(readonly2, shallow) { + const instrumentations = { + get(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has } = getProto(rawTarget); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + if (has.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } + }, + get size() { + const target = this["__v_raw"]; + !readonly2 && track(toRaw(target), "iterate", ITERATE_KEY); + return Reflect.get(target, "size", target); + }, + has(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); + }, + forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = toRaw(target); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + !readonly2 && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + } + }; + extend( + instrumentations, + readonly2 ? { + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear") + } : { + add(value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const proto = getProto(target); + const hadKey = proto.has.call(target, value); + if (!hadKey) { + target.add(value); + trigger(target, "add", value, value); + } + return this; + }, + set(key, value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else if (true) { + checkIdentityKeys(target, has, key); + } + const oldValue = get.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + return this; + }, + delete(key) { + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else if (true) { + checkIdentityKeys(target, has, key); + } + const oldValue = get ? get.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + }, + clear() { + const target = toRaw(this); + const hadItems = target.size !== 0; + const oldTarget = true ? isMap(target) ? new Map(target) : new Set(target) : void 0; + const result = target.clear(); + if (hadItems) { + trigger( + target, + "clear", + void 0, + void 0, + oldTarget + ); + } + return result; + } + } + ); + const iteratorMethods = [ + "keys", + "values", + "entries", + Symbol.iterator + ]; + iteratorMethods.forEach((method) => { + instrumentations[method] = createIterableMethod(method, readonly2, shallow); + }); + return instrumentations; +} +function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = createInstrumentations(isReadonly2, shallow); + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; +} +var mutableCollectionHandlers = { + get: createInstrumentationGetter(false, false) +}; +var shallowCollectionHandlers = { + get: createInstrumentationGetter(false, true) +}; +var readonlyCollectionHandlers = { + get: createInstrumentationGetter(true, false) +}; +var shallowReadonlyCollectionHandlers = { + get: createInstrumentationGetter(true, true) +}; +function checkIdentityKeys(target, has, key) { + const rawKey = toRaw(key); + if (rawKey !== key && has.call(target, rawKey)) { + const type = toRawType(target); + warn( + `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.` + ); + } +} +var reactiveMap = /* @__PURE__ */ new WeakMap(); +var shallowReactiveMap = /* @__PURE__ */ new WeakMap(); +var readonlyMap = /* @__PURE__ */ new WeakMap(); +var shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); +function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2; + default: + return 0; + } +} +function getTargetType(value) { + return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(toRawType(value)); +} +function reactive(target) { + if (isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); +} +function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); +} +function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); +} +function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); +} +function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject(target)) { + if (true) { + warn( + `value cannot be made ${isReadonly2 ? "readonly" : "reactive"}: ${String( + target + )}` + ); + } + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const targetType = getTargetType(target); + if (targetType === 0) { + return target; + } + const proxy = new Proxy( + target, + targetType === 2 ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; +} +function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); +} +function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); +} +function isShallow(value) { + return !!(value && value["__v_isShallow"]); +} +function isProxy(value) { + return value ? !!value["__v_raw"] : false; +} +function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? toRaw(raw) : observed; +} +function markRaw(value) { + if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) { + def(value, "__v_skip", true); + } + return value; +} +var toReactive = (value) => isObject(value) ? reactive(value) : value; +var toReadonly = (value) => isObject(value) ? readonly(value) : value; +function isRef2(r) { + return r ? r["__v_isRef"] === true : false; +} +function ref(value) { + return createRef(value, false); +} +function shallowRef(value) { + return createRef(value, true); +} +function createRef(rawValue, shallow) { + if (isRef2(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); +} +var RefImpl = class { + constructor(value, isShallow2) { + this.dep = new Dep(); + this["__v_isRef"] = true; + this["__v_isShallow"] = false; + this._rawValue = isShallow2 ? value : toRaw(value); + this._value = isShallow2 ? value : toReactive(value); + this["__v_isShallow"] = isShallow2; + } + get value() { + if (true) { + this.dep.track({ + target: this, + type: "get", + key: "value" + }); + } else { + this.dep.track(); + } + return this._value; + } + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue); + newValue = useDirectValue ? newValue : toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + if (true) { + this.dep.trigger({ + target: this, + type: "set", + key: "value", + newValue, + oldValue + }); + } else { + this.dep.trigger(); + } + } + } +}; +function triggerRef(ref2) { + if (ref2.dep) { + if (true) { + ref2.dep.trigger({ + target: ref2, + type: "set", + key: "value", + newValue: ref2._value + }); + } else { + ref2.dep.trigger(); + } + } +} +function unref(ref2) { + return isRef2(ref2) ? ref2.value : ref2; +} +function toValue(source) { + return isFunction(source) ? source() : unref(source); +} +var shallowUnwrapHandlers = { + get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (isRef2(oldValue) && !isRef2(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } +}; +function proxyRefs(objectWithRefs) { + return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); +} +var CustomRefImpl = class { + constructor(factory) { + this["__v_isRef"] = true; + this._value = void 0; + const dep = this.dep = new Dep(); + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)); + this._get = get; + this._set = set; + } + get value() { + return this._value = this._get(); + } + set value(newVal) { + this._set(newVal); + } +}; +function customRef(factory) { + return new CustomRefImpl(factory); +} +function toRefs(object) { + if (!isProxy(object)) { + warn(`toRefs() expects a reactive object but received a plain one.`); + } + const ret = isArray(object) ? new Array(object.length) : {}; + for (const key in object) { + ret[key] = propertyToRef(object, key); + } + return ret; +} +var ObjectRefImpl = class { + constructor(_object, _key, _defaultValue) { + this._object = _object; + this._key = _key; + this._defaultValue = _defaultValue; + this["__v_isRef"] = true; + this._value = void 0; + } + get value() { + const val = this._object[this._key]; + return this._value = val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(toRaw(this._object), this._key); + } +}; +var GetterRefImpl = class { + constructor(_getter) { + this._getter = _getter; + this["__v_isRef"] = true; + this["__v_isReadonly"] = true; + this._value = void 0; + } + get value() { + return this._value = this._getter(); + } +}; +function toRef(source, key, defaultValue) { + if (isRef2(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return ref(source); + } +} +function propertyToRef(source, key, defaultValue) { + const val = source[key]; + return isRef2(val) ? val : new ObjectRefImpl(source, key, defaultValue); +} +var ComputedRefImpl = class { + constructor(fn, setter, isSSR) { + this.fn = fn; + this.setter = setter; + this._value = void 0; + this.dep = new Dep(this); + this.__v_isRef = true; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 16; + this.globalVersion = globalVersion - 1; + this.next = void 0; + this.effect = this; + this["__v_isReadonly"] = !setter; + this.isSSR = isSSR; + } + /** + * @internal + */ + notify() { + this.flags |= 16; + if (!(this.flags & 8) && // avoid infinite self recursion + activeSub !== this) { + batch(this, true); + return true; + } else if (true) ; + } + get value() { + const link = true ? this.dep.track({ + target: this, + type: "get", + key: "value" + }) : this.dep.track(); + refreshComputed(this); + if (link) { + link.version = this.dep.version; + } + return this._value; + } + set value(newValue) { + if (this.setter) { + this.setter(newValue); + } else if (true) { + warn("Write operation failed: computed value is readonly"); + } + } +}; +function computed(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + if (isFunction(getterOrOptions)) { + getter = getterOrOptions; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, isSSR); + if (debugOptions && !isSSR) { + cRef.onTrack = debugOptions.onTrack; + cRef.onTrigger = debugOptions.onTrigger; + } + return cRef; +} +var TrackOpTypes = { + "GET": "get", + "HAS": "has", + "ITERATE": "iterate" +}; +var TriggerOpTypes = { + "SET": "set", + "ADD": "add", + "DELETE": "delete", + "CLEAR": "clear" +}; +var INITIAL_WATCHER_VALUE = {}; +var cleanupMap = /* @__PURE__ */ new WeakMap(); +var activeWatcher = void 0; +function getCurrentWatcher() { + return activeWatcher; +} +function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) { + if (owner) { + let cleanups = cleanupMap.get(owner); + if (!cleanups) cleanupMap.set(owner, cleanups = []); + cleanups.push(cleanupFn); + } else if (!failSilently) { + warn( + `onWatcherCleanup() was called when there was no active watcher to associate with.` + ); + } +} +function watch(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, once, scheduler, augmentJob, call } = options; + const warnInvalidSource = (s) => { + (options.onWarn || warn)( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.` + ); + }; + const reactiveGetter = (source2) => { + if (deep) return source2; + if (isShallow(source2) || deep === false || deep === 0) + return traverse(source2, 1); + return traverse(source2); + }; + let effect2; + let getter; + let cleanup; + let boundCleanup; + let forceTrigger = false; + let isMultiSource = false; + if (isRef2(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => source.map((s) => { + if (isRef2(s)) { + return s.value; + } else if (isReactive(s)) { + return reactiveGetter(s); + } else if (isFunction(s)) { + return call ? call(s, 2) : s(); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + getter = call ? () => call(source, 2) : source; + } else { + getter = () => { + if (cleanup) { + pauseTracking(); + try { + cleanup(); + } finally { + resetTracking(); + } + } + const currentEffect = activeWatcher; + activeWatcher = effect2; + try { + return call ? call(source, 3, [boundCleanup]) : source(boundCleanup); + } finally { + activeWatcher = currentEffect; + } + }; + } + } else { + getter = NOOP; + warnInvalidSource(source); + } + if (cb && deep) { + const baseGetter = getter; + const depth = deep === true ? Infinity : deep; + getter = () => traverse(baseGetter(), depth); + } + const scope = getCurrentScope(); + const watchHandle = () => { + effect2.stop(); + if (scope && scope.active) { + remove(scope.effects, effect2); + } + }; + if (once && cb) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + watchHandle(); + }; + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = (immediateFirstRun) => { + if (!(effect2.flags & 1) || !effect2.dirty && !immediateFirstRun) { + return; + } + if (cb) { + const newValue = effect2.run(); + if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) { + if (cleanup) { + cleanup(); + } + const currentWatcher = activeWatcher; + activeWatcher = effect2; + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + boundCleanup + ]; + call ? call(cb, 3, args) : ( + // @ts-expect-error + cb(...args) + ); + oldValue = newValue; + } finally { + activeWatcher = currentWatcher; + } + } + } else { + effect2.run(); + } + }; + if (augmentJob) { + augmentJob(job); + } + effect2 = new ReactiveEffect(getter); + effect2.scheduler = scheduler ? () => scheduler(job, false) : job; + boundCleanup = (fn) => onWatcherCleanup(fn, false, effect2); + cleanup = effect2.onStop = () => { + const cleanups = cleanupMap.get(effect2); + if (cleanups) { + if (call) { + call(cleanups, 4); + } else { + for (const cleanup2 of cleanups) cleanup2(); + } + cleanupMap.delete(effect2); + } + }; + if (true) { + effect2.onTrack = options.onTrack; + effect2.onTrigger = options.onTrigger; + } + if (cb) { + if (immediate) { + job(true); + } else { + oldValue = effect2.run(); + } + } else if (scheduler) { + scheduler(job.bind(null, true), true); + } else { + effect2.run(); + } + watchHandle.pause = effect2.pause.bind(effect2); + watchHandle.resume = effect2.resume.bind(effect2); + watchHandle.stop = watchHandle; + return watchHandle; +} +function traverse(value, depth = Infinity, seen) { + if (depth <= 0 || !isObject(value) || value["__v_skip"]) { + return value; + } + seen = seen || /* @__PURE__ */ new Set(); + if (seen.has(value)) { + return value; + } + seen.add(value); + depth--; + if (isRef2(value)) { + traverse(value.value, depth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key], depth, seen); + } + } + } + return value; +} + +// node_modules/.pnpm/@vue+runtime-core@3.5.13/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js +var stack = []; +function pushWarningContext(vnode) { + stack.push(vnode); +} +function popWarningContext() { + stack.pop(); +} +var isWarning = false; +function warn$1(msg, ...args) { + if (isWarning) return; + isWarning = true; + pauseTracking(); + const instance = stack.length ? stack[stack.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + // eslint-disable-next-line no-restricted-syntax + msg + args.map((a) => { + var _a, _b; + return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a); + }).join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); + isWarning = false; +} +function getComponentTrace() { + let currentVNode = stack[stack.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; +} +function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; +} +function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; +} +function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; +} +function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (isRef2(value)) { + value = formatProp(key, toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = toRaw(value); + return raw ? value : [`${key}=`, value]; + } +} +function assertNumber(val, type) { + if (false) return; + if (val === void 0) { + return; + } else if (typeof val !== "number") { + warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`); + } else if (isNaN(val)) { + warn$1(`${type} is NaN - the duration expression might be incorrect.`); + } +} +var ErrorCodes = { + "SETUP_FUNCTION": 0, + "0": "SETUP_FUNCTION", + "RENDER_FUNCTION": 1, + "1": "RENDER_FUNCTION", + "NATIVE_EVENT_HANDLER": 5, + "5": "NATIVE_EVENT_HANDLER", + "COMPONENT_EVENT_HANDLER": 6, + "6": "COMPONENT_EVENT_HANDLER", + "VNODE_HOOK": 7, + "7": "VNODE_HOOK", + "DIRECTIVE_HOOK": 8, + "8": "DIRECTIVE_HOOK", + "TRANSITION_HOOK": 9, + "9": "TRANSITION_HOOK", + "APP_ERROR_HANDLER": 10, + "10": "APP_ERROR_HANDLER", + "APP_WARN_HANDLER": 11, + "11": "APP_WARN_HANDLER", + "FUNCTION_REF": 12, + "12": "FUNCTION_REF", + "ASYNC_COMPONENT_LOADER": 13, + "13": "ASYNC_COMPONENT_LOADER", + "SCHEDULER": 14, + "14": "SCHEDULER", + "COMPONENT_UPDATE": 15, + "15": "COMPONENT_UPDATE", + "APP_UNMOUNT_CLEANUP": 16, + "16": "APP_UNMOUNT_CLEANUP" +}; +var ErrorTypeStrings$1 = { + ["sp"]: "serverPrefetch hook", + ["bc"]: "beforeCreate hook", + ["c"]: "created hook", + ["bm"]: "beforeMount hook", + ["m"]: "mounted hook", + ["bu"]: "beforeUpdate hook", + ["u"]: "updated", + ["bum"]: "beforeUnmount hook", + ["um"]: "unmounted hook", + ["a"]: "activated hook", + ["da"]: "deactivated hook", + ["ec"]: "errorCaptured hook", + ["rtc"]: "renderTracked hook", + ["rtg"]: "renderTriggered hook", + [0]: "setup function", + [1]: "render function", + [2]: "watcher getter", + [3]: "watcher callback", + [4]: "watcher cleanup function", + [5]: "native event handler", + [6]: "component event handler", + [7]: "vnode hook", + [8]: "directive hook", + [9]: "transition hook", + [10]: "app errorHandler", + [11]: "app warnHandler", + [12]: "ref function", + [13]: "async component loader", + [14]: "scheduler flush", + [15]: "component update", + [16]: "app unmount cleanup function" +}; +function callWithErrorHandling(fn, instance, type, args) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } +} +function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + if (isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; + } else if (true) { + warn$1( + `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}` + ); + } +} +function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = true ? ErrorTypeStrings$1[type] : `https://vuejs.org/error-reference/#runtime-${type}`; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + if (errorHandler) { + pauseTracking(); + callWithErrorHandling(errorHandler, null, 10, [ + err, + exposedInstance, + errorInfo + ]); + resetTracking(); + return; + } + } + logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction); +} +function logError(err, type, contextVNode, throwInDev = true, throwInProd = false) { + if (true) { + const info = ErrorTypeStrings$1[type]; + if (contextVNode) { + pushWarningContext(contextVNode); + } + warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`); + if (contextVNode) { + popWarningContext(); + } + if (throwInDev) { + throw err; + } else { + console.error(err); + } + } else if (throwInProd) { + throw err; + } else { + console.error(err); + } +} +var queue = []; +var flushIndex = -1; +var pendingPostFlushCbs = []; +var activePostFlushCbs = null; +var postFlushIndex = 0; +var resolvedPromise = Promise.resolve(); +var currentFlushPromise = null; +var RECURSION_LIMIT = 100; +function nextTick(fn) { + const p2 = currentFlushPromise || resolvedPromise; + return fn ? p2.then(this ? fn.bind(this) : fn) : p2; +} +function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.flags & 2) { + start = middle + 1; + } else { + end = middle; + } + } + return start; +} +function queueJob(job) { + if (!(job.flags & 1)) { + const jobId = getId(job); + const lastJob = queue[queue.length - 1]; + if (!lastJob || // fast path when the job id is larger than the tail + !(job.flags & 2) && jobId >= getId(lastJob)) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(jobId), 0, job); + } + job.flags |= 1; + queueFlush(); + } +} +function queueFlush() { + if (!currentFlushPromise) { + currentFlushPromise = resolvedPromise.then(flushJobs); + } +} +function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (activePostFlushCbs && cb.id === -1) { + activePostFlushCbs.splice(postFlushIndex + 1, 0, cb); + } else if (!(cb.flags & 1)) { + pendingPostFlushCbs.push(cb); + cb.flags |= 1; + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); +} +function flushPreFlushCbs(instance, seen, i = flushIndex + 1) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.flags & 2) { + if (instance && cb.id !== instance.uid) { + continue; + } + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + queue.splice(i, 1); + i--; + if (cb.flags & 4) { + cb.flags &= ~1; + } + cb(); + if (!(cb.flags & 4)) { + cb.flags &= ~1; + } + } + } +} +function flushPostFlushCbs(seen) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)].sort( + (a, b) => getId(a) - getId(b) + ); + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + const cb = activePostFlushCbs[postFlushIndex]; + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + if (cb.flags & 4) { + cb.flags &= ~1; + } + if (!(cb.flags & 8)) cb(); + cb.flags &= ~1; + } + activePostFlushCbs = null; + postFlushIndex = 0; + } +} +var getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id; +function flushJobs(seen) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + const check = true ? (job) => checkRecursiveUpdates(seen, job) : NOOP; + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && !(job.flags & 8)) { + if (check(job)) { + continue; + } + if (job.flags & 4) { + job.flags &= ~1; + } + callWithErrorHandling( + job, + job.i, + job.i ? 15 : 14 + ); + if (!(job.flags & 4)) { + job.flags &= ~1; + } + } + } + } finally { + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job) { + job.flags &= ~1; + } + } + flushIndex = -1; + queue.length = 0; + flushPostFlushCbs(seen); + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(seen); + } + } +} +function checkRecursiveUpdates(seen, fn) { + const count = seen.get(fn) || 0; + if (count > RECURSION_LIMIT) { + const instance = fn.i; + const componentName = instance && getComponentName(instance.type); + handleError( + `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`, + null, + 10 + ); + return true; + } + seen.set(fn, count + 1); + return false; +} +var isHmrUpdating = false; +var hmrDirtyComponents = /* @__PURE__ */ new Map(); +if (true) { + getGlobalThis().__VUE_HMR_RUNTIME__ = { + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + }; +} +var map = /* @__PURE__ */ new Map(); +function registerHMR(instance) { + const id = instance.type.__hmrId; + let record = map.get(id); + if (!record) { + createRecord(id, instance.type); + record = map.get(id); + } + record.instances.add(instance); +} +function unregisterHMR(instance) { + map.get(instance.type.__hmrId).instances.delete(instance); +} +function createRecord(id, initialDef) { + if (map.has(id)) { + return false; + } + map.set(id, { + initialDef: normalizeClassComponent(initialDef), + instances: /* @__PURE__ */ new Set() + }); + return true; +} +function normalizeClassComponent(component) { + return isClassComponent(component) ? component.__vccOpts : component; +} +function rerender(id, newRender) { + const record = map.get(id); + if (!record) { + return; + } + record.initialDef.render = newRender; + [...record.instances].forEach((instance) => { + if (newRender) { + instance.render = newRender; + normalizeClassComponent(instance.type).render = newRender; + } + instance.renderCache = []; + isHmrUpdating = true; + instance.update(); + isHmrUpdating = false; + }); +} +function reload(id, newComp) { + const record = map.get(id); + if (!record) return; + newComp = normalizeClassComponent(newComp); + updateComponentDef(record.initialDef, newComp); + const instances = [...record.instances]; + for (let i = 0; i < instances.length; i++) { + const instance = instances[i]; + const oldComp = normalizeClassComponent(instance.type); + let dirtyInstances = hmrDirtyComponents.get(oldComp); + if (!dirtyInstances) { + if (oldComp !== record.initialDef) { + updateComponentDef(oldComp, newComp); + } + hmrDirtyComponents.set(oldComp, dirtyInstances = /* @__PURE__ */ new Set()); + } + dirtyInstances.add(instance); + instance.appContext.propsCache.delete(instance.type); + instance.appContext.emitsCache.delete(instance.type); + instance.appContext.optionsCache.delete(instance.type); + if (instance.ceReload) { + dirtyInstances.add(instance); + instance.ceReload(newComp.styles); + dirtyInstances.delete(instance); + } else if (instance.parent) { + queueJob(() => { + isHmrUpdating = true; + instance.parent.update(); + isHmrUpdating = false; + dirtyInstances.delete(instance); + }); + } else if (instance.appContext.reload) { + instance.appContext.reload(); + } else if (typeof window !== "undefined") { + window.location.reload(); + } else { + console.warn( + "[HMR] Root or manually mounted instance modified. Full reload required." + ); + } + if (instance.root.ce && instance !== instance.root) { + instance.root.ce._removeChildStyle(oldComp); + } + } + queuePostFlushCb(() => { + hmrDirtyComponents.clear(); + }); +} +function updateComponentDef(oldComp, newComp) { + extend(oldComp, newComp); + for (const key in oldComp) { + if (key !== "__file" && !(key in newComp)) { + delete oldComp[key]; + } + } +} +function tryWrap(fn) { + return (id, arg) => { + try { + return fn(id, arg); + } catch (e) { + console.error(e); + console.warn( + `[HMR] Something went wrong during Vue component hot-reload. Full reload required.` + ); + } + }; +} +var devtools$1; +var buffer = []; +var devtoolsNotInstalled = false; +function emit$1(event, ...args) { + if (devtools$1) { + devtools$1.emit(event, ...args); + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }); + } +} +function setDevtoolsHook$1(hook, target) { + var _a, _b; + devtools$1 = hook; + if (devtools$1) { + devtools$1.enabled = true; + buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args)); + buffer = []; + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== "undefined" && // some envs mock window but not fully + window.HTMLElement && // also exclude jsdom + // eslint-disable-next-line no-restricted-syntax + !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes("jsdom")) + ) { + const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []; + replay.push((newHook) => { + setDevtoolsHook$1(newHook, target); + }); + setTimeout(() => { + if (!devtools$1) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null; + devtoolsNotInstalled = true; + buffer = []; + } + }, 3e3); + } else { + devtoolsNotInstalled = true; + buffer = []; + } +} +function devtoolsInitApp(app, version2) { + emit$1("app:init", app, version2, { + Fragment, + Text, + Comment, + Static + }); +} +function devtoolsUnmountApp(app) { + emit$1("app:unmount", app); +} +var devtoolsComponentAdded = createDevtoolsComponentHook( + "component:added" + /* COMPONENT_ADDED */ +); +var devtoolsComponentUpdated = createDevtoolsComponentHook( + "component:updated" + /* COMPONENT_UPDATED */ +); +var _devtoolsComponentRemoved = createDevtoolsComponentHook( + "component:removed" + /* COMPONENT_REMOVED */ +); +var devtoolsComponentRemoved = (component) => { + if (devtools$1 && typeof devtools$1.cleanupBuffer === "function" && // remove the component if it wasn't buffered + !devtools$1.cleanupBuffer(component)) { + _devtoolsComponentRemoved(component); + } +}; +function createDevtoolsComponentHook(hook) { + return (component) => { + emit$1( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : void 0, + component + ); + }; +} +var devtoolsPerfStart = createDevtoolsPerformanceHook( + "perf:start" + /* PERFORMANCE_START */ +); +var devtoolsPerfEnd = createDevtoolsPerformanceHook( + "perf:end" + /* PERFORMANCE_END */ +); +function createDevtoolsPerformanceHook(hook) { + return (component, type, time) => { + emit$1(hook, component.appContext.app, component.uid, component, type, time); + }; +} +function devtoolsComponentEmit(component, event, params) { + emit$1( + "component:emit", + component.appContext.app, + component, + event, + params + ); +} +var currentRenderingInstance = null; +var currentScopeId = null; +function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; +} +function pushScopeId(id) { + currentScopeId = id; +} +function popScopeId() { + currentScopeId = null; +} +var withScopeId = (_id) => withCtx; +function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + if (true) { + devtoolsComponentUpdated(ctx); + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; +} +function validateDirectiveName(name) { + if (isBuiltInDirective(name)) { + warn$1("Do not use built-in directive ids as custom directive id: " + name); + } +} +function withDirectives(vnode, directives) { + if (currentRenderingInstance === null) { + warn$1(`withDirectives can only be used inside render functions.`); + return vnode; + } + const instance = getComponentPublicInstance(currentRenderingInstance); + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; +} +function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } +} +var TeleportEndKey = Symbol("_vte"); +var isTeleport = (type) => type.__isTeleport; +var isTeleportDisabled = (props) => props && (props.disabled || props.disabled === ""); +var isTeleportDeferred = (props) => props && (props.defer || props.defer === ""); +var isTargetSVG = (target) => typeof SVGElement !== "undefined" && target instanceof SVGElement; +var isTargetMathML = (target) => typeof MathMLElement === "function" && target instanceof MathMLElement; +var resolveTarget = (props, select) => { + const targetSelector = props && props.to; + if (isString(targetSelector)) { + if (!select) { + warn$1( + `Current renderer does not support string target for Teleports. (missing querySelector renderer option)` + ); + return null; + } else { + const target = select(targetSelector); + if (!target && !isTeleportDisabled(props)) { + warn$1( + `Failed to locate Teleport target with selector "${targetSelector}". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.` + ); + } + return target; + } + } else { + if (!targetSelector && !isTeleportDisabled(props)) { + warn$1(`Invalid Teleport target: ${targetSelector}`); + } + return targetSelector; + } +}; +var TeleportImpl = { + name: "Teleport", + __isTeleport: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) { + const { + mc: mountChildren, + pc: patchChildren, + pbc: patchBlockChildren, + o: { insert, querySelector, createText, createComment } + } = internals; + const disabled = isTeleportDisabled(n2.props); + let { shapeFlag, children, dynamicChildren } = n2; + if (isHmrUpdating) { + optimized = false; + dynamicChildren = null; + } + if (n1 == null) { + const placeholder = n2.el = true ? createComment("teleport start") : createText(""); + const mainAnchor = n2.anchor = true ? createComment("teleport end") : createText(""); + insert(placeholder, container, anchor); + insert(mainAnchor, container, anchor); + const mount = (container2, anchor2) => { + if (shapeFlag & 16) { + if (parentComponent && parentComponent.isCE) { + parentComponent.ce._teleportTarget = container2; + } + mountChildren( + children, + container2, + anchor2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const mountToTarget = () => { + const target = n2.target = resolveTarget(n2.props, querySelector); + const targetAnchor = prepareAnchor(target, n2, createText, insert); + if (target) { + if (namespace !== "svg" && isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace !== "mathml" && isTargetMathML(target)) { + namespace = "mathml"; + } + if (!disabled) { + mount(target, targetAnchor); + updateCssVars(n2, false); + } + } else if (!disabled) { + warn$1( + "Invalid Teleport target on mount:", + target, + `(${typeof target})` + ); + } + }; + if (disabled) { + mount(container, mainAnchor); + updateCssVars(n2, true); + } + if (isTeleportDeferred(n2.props)) { + queuePostRenderEffect(() => { + mountToTarget(); + n2.el.__isMounted = true; + }, parentSuspense); + } else { + mountToTarget(); + } + } else { + if (isTeleportDeferred(n2.props) && !n1.el.__isMounted) { + queuePostRenderEffect(() => { + TeleportImpl.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals + ); + delete n1.el.__isMounted; + }, parentSuspense); + return; + } + n2.el = n1.el; + n2.targetStart = n1.targetStart; + const mainAnchor = n2.anchor = n1.anchor; + const target = n2.target = n1.target; + const targetAnchor = n2.targetAnchor = n1.targetAnchor; + const wasDisabled = isTeleportDisabled(n1.props); + const currentContainer = wasDisabled ? container : target; + const currentAnchor = wasDisabled ? mainAnchor : targetAnchor; + if (namespace === "svg" || isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace === "mathml" || isTargetMathML(target)) { + namespace = "mathml"; + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + currentContainer, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + traverseStaticChildren(n1, n2, true); + } else if (!optimized) { + patchChildren( + n1, + n2, + currentContainer, + currentAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + false + ); + } + if (disabled) { + if (!wasDisabled) { + moveTeleport( + n2, + container, + mainAnchor, + internals, + 1 + ); + } else { + if (n2.props && n1.props && n2.props.to !== n1.props.to) { + n2.props.to = n1.props.to; + } + } + } else { + if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { + const nextTarget = n2.target = resolveTarget( + n2.props, + querySelector + ); + if (nextTarget) { + moveTeleport( + n2, + nextTarget, + null, + internals, + 0 + ); + } else if (true) { + warn$1( + "Invalid Teleport target on update:", + target, + `(${typeof target})` + ); + } + } else if (wasDisabled) { + moveTeleport( + n2, + target, + targetAnchor, + internals, + 1 + ); + } + } + updateCssVars(n2, disabled); + } + }, + remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) { + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props + } = vnode; + if (target) { + hostRemove(targetStart); + hostRemove(targetAnchor); + } + doRemove && hostRemove(anchor); + if (shapeFlag & 16) { + const shouldRemove = doRemove || !isTeleportDisabled(props); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ); + } + } + }, + move: moveTeleport, + hydrate: hydrateTeleport +}; +function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) { + if (moveType === 0) { + insert(vnode.targetAnchor, container, parentAnchor); + } + const { el, anchor, shapeFlag, children, props } = vnode; + const isReorder = moveType === 2; + if (isReorder) { + insert(el, container, parentAnchor); + } + if (!isReorder || isTeleportDisabled(props)) { + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + move( + children[i], + container, + parentAnchor, + 2 + ); + } + } + } + if (isReorder) { + insert(anchor, container, parentAnchor); + } +} +function hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, { + o: { nextSibling, parentNode, querySelector, insert, createText } +}, hydrateChildren) { + const target = vnode.target = resolveTarget( + vnode.props, + querySelector + ); + if (target) { + const disabled = isTeleportDisabled(vnode.props); + const targetNode = target._lpa || target.firstChild; + if (vnode.shapeFlag & 16) { + if (disabled) { + vnode.anchor = hydrateChildren( + nextSibling(node), + vnode, + parentNode(node), + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + vnode.targetStart = targetNode; + vnode.targetAnchor = targetNode && nextSibling(targetNode); + } else { + vnode.anchor = nextSibling(node); + let targetAnchor = targetNode; + while (targetAnchor) { + if (targetAnchor && targetAnchor.nodeType === 8) { + if (targetAnchor.data === "teleport start anchor") { + vnode.targetStart = targetAnchor; + } else if (targetAnchor.data === "teleport anchor") { + vnode.targetAnchor = targetAnchor; + target._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor); + break; + } + } + targetAnchor = nextSibling(targetAnchor); + } + if (!vnode.targetAnchor) { + prepareAnchor(target, vnode, createText, insert); + } + hydrateChildren( + targetNode && nextSibling(targetNode), + vnode, + target, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } + updateCssVars(vnode, disabled); + } + return vnode.anchor && nextSibling(vnode.anchor); +} +var Teleport = TeleportImpl; +function updateCssVars(vnode, isDisabled) { + const ctx = vnode.ctx; + if (ctx && ctx.ut) { + let node, anchor; + if (isDisabled) { + node = vnode.el; + anchor = vnode.anchor; + } else { + node = vnode.targetStart; + anchor = vnode.targetAnchor; + } + while (node && node !== anchor) { + if (node.nodeType === 1) node.setAttribute("data-v-owner", ctx.uid); + node = node.nextSibling; + } + ctx.ut(); + } +} +function prepareAnchor(target, vnode, createText, insert) { + const targetStart = vnode.targetStart = createText(""); + const targetAnchor = vnode.targetAnchor = createText(""); + targetStart[TeleportEndKey] = targetAnchor; + if (target) { + insert(targetStart, target); + insert(targetAnchor, target); + } + return targetAnchor; +} +var leaveCbKey = Symbol("_leaveCb"); +var enterCbKey = Symbol("_enterCb"); +function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; +} +var TransitionHookValidator = [Function, Array]; +var BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator +}; +var recursiveGetSubtree = (instance) => { + const subTree = instance.subTree; + return subTree.component ? recursiveGetSubtree(subTree.component) : subTree; +}; +var BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + if (!children || !children.length) { + return; + } + const child = findNonCommentChild(children); + const rawProps = toRaw(props); + const { mode } = rawProps; + if (mode && mode !== "in-out" && mode !== "out-in" && mode !== "default") { + warn$1(`invalid mode: ${mode}`); + } + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getInnerChild$1(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + let enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance, + // #11061, ensure enterHooks is fresh after clone + (hooks) => enterHooks = hooks + ); + if (innerChild.type !== Comment) { + setTransitionHooks(innerChild, enterHooks); + } + let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree); + if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(innerChild, oldInnerChild) && recursiveGetSubtree(instance).type !== Comment) { + let leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in" && innerChild.type !== Comment) { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (!(instance.job.flags & 8)) { + instance.update(); + } + delete leavingHooks.afterLeave; + oldInnerChild = void 0; + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + enterHooks.delayedLeave = () => { + delayedLeave(); + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + }; + } else { + oldInnerChild = void 0; + } + } else if (oldInnerChild) { + oldInnerChild = void 0; + } + return child; + }; + } +}; +function findNonCommentChild(children) { + let child = children[0]; + if (children.length > 1) { + let hasFound = false; + for (const c of children) { + if (c.type !== Comment) { + if (hasFound) { + warn$1( + " can only be used on a single element or component. Use for lists." + ); + break; + } + child = c; + hasFound = true; + if (false) break; + } + } + } + return child; +} +var BaseTransition = BaseTransitionImpl; +function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; +} +function resolveTransitionHooks(vnode, props, state, instance, postClone) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook3 = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook3(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook3(hook, [el]); + }, + enter(el) { + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + const done = el[enterCbKey] = (cancelled) => { + if (called) return; + called = true; + if (cancelled) { + callHook3(cancelHook, [el]); + } else { + callHook3(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey] = void 0; + }; + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove2) { + const key2 = String(vnode.key); + if (el[enterCbKey]) { + el[enterCbKey]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove2(); + } + callHook3(onBeforeLeave, [el]); + let called = false; + const done = el[leaveCbKey] = (cancelled) => { + if (called) return; + called = true; + remove2(); + if (cancelled) { + callHook3(onLeaveCancelled, [el]); + } else { + callHook3(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + const hooks2 = resolveTransitionHooks( + vnode2, + props, + state, + instance, + postClone + ); + if (postClone) postClone(hooks2); + return hooks2; + } + }; + return hooks; +} +function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } +} +function getInnerChild$1(vnode) { + if (!isKeepAlive(vnode)) { + if (isTeleport(vnode.type) && vnode.children) { + return findNonCommentChild(vnode.children); + } + return vnode; + } + if (vnode.component) { + return vnode.component.subTree; + } + const { shapeFlag, children } = vnode; + if (children) { + if (shapeFlag & 16) { + return children[0]; + } + if (shapeFlag & 32 && isFunction(children.default)) { + return children.default(); + } + } +} +function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + vnode.transition = hooks; + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } +} +function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; +} +function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8236: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; +} +function useId() { + const i = getCurrentInstance(); + if (i) { + return (i.appContext.config.idPrefix || "v") + "-" + i.ids[0] + i.ids[1]++; + } else if (true) { + warn$1( + `useId() is called when there is no active component instance to be associated with.` + ); + } + return ""; +} +function markAsyncBoundary(instance) { + instance.ids = [instance.ids[0] + instance.ids[2]++ + "-", 0, 0]; +} +var knownTemplateRefs = /* @__PURE__ */ new WeakSet(); +function useTemplateRef(key) { + const i = getCurrentInstance(); + const r = shallowRef(null); + if (i) { + const refs = i.refs === EMPTY_OBJ ? i.refs = {} : i.refs; + let desc; + if ((desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable) { + warn$1(`useTemplateRef('${key}') already exists.`); + } else { + Object.defineProperty(refs, key, { + enumerable: true, + get: () => r.value, + set: (val) => r.value = val + }); + } + } else if (true) { + warn$1( + `useTemplateRef() is called when there is no active component instance to be associated with.` + ); + } + const ret = true ? readonly(r) : r; + if (true) { + knownTemplateRefs.add(ret); + } + return ret; +} +function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { + if (isArray(rawRef)) { + rawRef.forEach( + (r, i) => setRef( + r, + oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), + parentSuspense, + vnode, + isUnmount + ) + ); + return; + } + if (isAsyncWrapper(vnode) && !isUnmount) { + if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree); + } + return; + } + const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el; + const value = isUnmount ? null : refValue; + const { i: owner, r: ref2 } = rawRef; + if (!owner) { + warn$1( + `Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.` + ); + return; + } + const oldRef = oldRawRef && oldRawRef.r; + const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs; + const setupState = owner.setupState; + const rawSetupState = toRaw(setupState); + const canSetSetupRef = setupState === EMPTY_OBJ ? () => false : (key) => { + if (true) { + if (hasOwn(rawSetupState, key) && !isRef2(rawSetupState[key])) { + warn$1( + `Template ref "${key}" used on a non-ref value. It will not work in the production build.` + ); + } + if (knownTemplateRefs.has(rawSetupState[key])) { + return false; + } + } + return hasOwn(rawSetupState, key); + }; + if (oldRef != null && oldRef !== ref2) { + if (isString(oldRef)) { + refs[oldRef] = null; + if (canSetSetupRef(oldRef)) { + setupState[oldRef] = null; + } + } else if (isRef2(oldRef)) { + oldRef.value = null; + } + } + if (isFunction(ref2)) { + callWithErrorHandling(ref2, owner, 12, [value, refs]); + } else { + const _isString = isString(ref2); + const _isRef = isRef2(ref2); + if (_isString || _isRef) { + const doSet = () => { + if (rawRef.f) { + const existing = _isString ? canSetSetupRef(ref2) ? setupState[ref2] : refs[ref2] : ref2.value; + if (isUnmount) { + isArray(existing) && remove(existing, refValue); + } else { + if (!isArray(existing)) { + if (_isString) { + refs[ref2] = [refValue]; + if (canSetSetupRef(ref2)) { + setupState[ref2] = refs[ref2]; + } + } else { + ref2.value = [refValue]; + if (rawRef.k) refs[rawRef.k] = ref2.value; + } + } else if (!existing.includes(refValue)) { + existing.push(refValue); + } + } + } else if (_isString) { + refs[ref2] = value; + if (canSetSetupRef(ref2)) { + setupState[ref2] = value; + } + } else if (_isRef) { + ref2.value = value; + if (rawRef.k) refs[rawRef.k] = value; + } else if (true) { + warn$1("Invalid template ref type:", ref2, `(${typeof ref2})`); + } + }; + if (value) { + doSet.id = -1; + queuePostRenderEffect(doSet, parentSuspense); + } else { + doSet(); + } + } else if (true) { + warn$1("Invalid template ref type:", ref2, `(${typeof ref2})`); + } + } +} +var hasLoggedMismatchError = false; +var logMismatchError = () => { + if (hasLoggedMismatchError) { + return; + } + console.error("Hydration completed but contains mismatches."); + hasLoggedMismatchError = true; +}; +var isSVGContainer = (container) => container.namespaceURI.includes("svg") && container.tagName !== "foreignObject"; +var isMathMLContainer = (container) => container.namespaceURI.includes("MathML"); +var getContainerType = (container) => { + if (container.nodeType !== 1) return void 0; + if (isSVGContainer(container)) return "svg"; + if (isMathMLContainer(container)) return "mathml"; + return void 0; +}; +var isComment = (node) => node.nodeType === 8; +function createHydrationFunctions(rendererInternals) { + const { + mt: mountComponent, + p: patch, + o: { + patchProp: patchProp2, + createText, + nextSibling, + parentNode, + remove: remove2, + insert, + createComment + } + } = rendererInternals; + const hydrate2 = (vnode, container) => { + if (!container.hasChildNodes()) { + warn$1( + `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` + ); + patch(null, vnode, container); + flushPostFlushCbs(); + container._vnode = vnode; + return; + } + hydrateNode(container.firstChild, vnode, null, null, null); + flushPostFlushCbs(); + container._vnode = vnode; + }; + const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => { + optimized = optimized || !!vnode.dynamicChildren; + const isFragmentStart = isComment(node) && node.data === "["; + const onMismatch = () => handleMismatch( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + isFragmentStart + ); + const { type, ref: ref2, shapeFlag, patchFlag } = vnode; + let domType = node.nodeType; + vnode.el = node; + if (true) { + def(node, "__vnode", vnode, true); + def(node, "__vueParentComponent", parentComponent, true); + } + if (patchFlag === -2) { + optimized = false; + vnode.dynamicChildren = null; + } + let nextNode = null; + switch (type) { + case Text: + if (domType !== 3) { + if (vnode.children === "") { + insert(vnode.el = createText(""), parentNode(node), node); + nextNode = node; + } else { + nextNode = onMismatch(); + } + } else { + if (node.data !== vnode.children) { + warn$1( + `Hydration text mismatch in`, + node.parentNode, + ` + - rendered on server: ${JSON.stringify( + node.data + )} + - expected on client: ${JSON.stringify(vnode.children)}` + ); + logMismatchError(); + node.data = vnode.children; + } + nextNode = nextSibling(node); + } + break; + case Comment: + if (isTemplateNode(node)) { + nextNode = nextSibling(node); + replaceNode( + vnode.el = node.content.firstChild, + node, + parentComponent + ); + } else if (domType !== 8 || isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = nextSibling(node); + } + break; + case Static: + if (isFragmentStart) { + node = nextSibling(node); + domType = node.nodeType; + } + if (domType === 1 || domType === 3) { + nextNode = node; + const needToAdoptContent = !vnode.children.length; + for (let i = 0; i < vnode.staticCount; i++) { + if (needToAdoptContent) + vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data; + if (i === vnode.staticCount - 1) { + vnode.anchor = nextNode; + } + nextNode = nextSibling(nextNode); + } + return isFragmentStart ? nextSibling(nextNode) : nextNode; + } else { + onMismatch(); + } + break; + case Fragment: + if (!isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = hydrateFragment( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + break; + default: + if (shapeFlag & 1) { + if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) { + nextNode = onMismatch(); + } else { + nextNode = hydrateElement( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } else if (shapeFlag & 6) { + vnode.slotScopeIds = slotScopeIds; + const container = parentNode(node); + if (isFragmentStart) { + nextNode = locateClosingAnchor(node); + } else if (isComment(node) && node.data === "teleport start") { + nextNode = locateClosingAnchor(node, node.data, "teleport end"); + } else { + nextNode = nextSibling(node); + } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + optimized + ); + if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) { + let subTree; + if (isFragmentStart) { + subTree = createVNode(Fragment); + subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild; + } else { + subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div"); + } + subTree.el = node; + vnode.component.subTree = subTree; + } + } else if (shapeFlag & 64) { + if (domType !== 8) { + nextNode = onMismatch(); + } else { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized, + rendererInternals, + hydrateChildren + ); + } + } else if (shapeFlag & 128) { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + getContainerType(parentNode(node)), + slotScopeIds, + optimized, + rendererInternals, + hydrateNode + ); + } else if (true) { + warn$1("Invalid HostVNode type:", type, `(${typeof type})`); + } + } + if (ref2 != null) { + setRef(ref2, null, parentSuspense, vnode); + } + return nextNode; + }; + const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!vnode.dynamicChildren; + const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode; + const forcePatch = type === "input" || type === "option"; + if (true) { + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + let needCallTransitionHooks = false; + if (isTemplateNode(el)) { + needCallTransitionHooks = needTransition( + null, + // no need check parentSuspense in hydration + transition + ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear; + const content = el.content.firstChild; + if (needCallTransitionHooks) { + transition.beforeEnter(content); + } + replaceNode(content, el, parentComponent); + vnode.el = el = content; + } + if (shapeFlag & 16 && // skip if element has innerHTML / textContent + !(props && (props.innerHTML || props.textContent))) { + let next = hydrateChildren( + el.firstChild, + vnode, + el, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + let hasWarned2 = false; + while (next) { + if (!isMismatchAllowed( + el, + 1 + /* CHILDREN */ + )) { + if (!hasWarned2) { + warn$1( + `Hydration children mismatch on`, + el, + ` +Server rendered element contains more child nodes than client vdom.` + ); + hasWarned2 = true; + } + logMismatchError(); + } + const cur = next; + next = next.nextSibling; + remove2(cur); + } + } else if (shapeFlag & 8) { + let clientText = vnode.children; + if (clientText[0] === "\n" && (el.tagName === "PRE" || el.tagName === "TEXTAREA")) { + clientText = clientText.slice(1); + } + if (el.textContent !== clientText) { + if (!isMismatchAllowed( + el, + 0 + /* TEXT */ + )) { + warn$1( + `Hydration text content mismatch on`, + el, + ` + - rendered on server: ${el.textContent} + - expected on client: ${vnode.children}` + ); + logMismatchError(); + } + el.textContent = vnode.children; + } + } + if (props) { + if (true) { + const isCustomElement = el.tagName.includes("-"); + for (const key in props) { + if (// #11189 skip if this node has directives that have created hooks + // as it could have mutated the DOM in any possible way + !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent)) { + logMismatchError(); + } + if (forcePatch && (key.endsWith("value") || key === "indeterminate") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers + key[0] === "." || isCustomElement) { + patchProp2(el, key, null, props[key], void 0, parentComponent); + } + } + } else if (props.onClick) { + patchProp2( + el, + "onClick", + null, + props.onClick, + void 0, + parentComponent + ); + } else if (patchFlag & 4 && isReactive(props.style)) { + for (const key in props.style) props.style[key]; + } + } + let vnodeHooks; + if (vnodeHooks = props && props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + }, parentSuspense); + } + } + return el.nextSibling; + }; + const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!parentVNode.dynamicChildren; + const children = parentVNode.children; + const l = children.length; + let hasWarned2 = false; + for (let i = 0; i < l; i++) { + const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]); + const isText = vnode.type === Text; + if (node) { + if (isText && !optimized) { + if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) { + insert( + createText( + node.data.slice(vnode.children.length) + ), + container, + nextSibling(node) + ); + node.data = vnode.children; + } + } + node = hydrateNode( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } else if (isText && !vnode.children) { + insert(vnode.el = createText(""), container); + } else { + if (!isMismatchAllowed( + container, + 1 + /* CHILDREN */ + )) { + if (!hasWarned2) { + warn$1( + `Hydration children mismatch on`, + container, + ` +Server rendered element contains fewer child nodes than client vdom.` + ); + hasWarned2 = true; + } + logMismatchError(); + } + patch( + null, + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + } + } + return node; + }; + const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + const container = parentNode(node); + const next = hydrateChildren( + nextSibling(node), + vnode, + container, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && isComment(next) && next.data === "]") { + return nextSibling(vnode.anchor = next); + } else { + logMismatchError(); + insert(vnode.anchor = createComment(`]`), container, next); + return next; + } + }; + const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { + if (!isMismatchAllowed( + node.parentElement, + 1 + /* CHILDREN */ + )) { + warn$1( + `Hydration node mismatch: +- rendered on server:`, + node, + node.nodeType === 3 ? `(text)` : isComment(node) && node.data === "[" ? `(start of fragment)` : ``, + ` +- expected on client:`, + vnode.type + ); + logMismatchError(); + } + vnode.el = null; + if (isFragment) { + const end = locateClosingAnchor(node); + while (true) { + const next2 = nextSibling(node); + if (next2 && next2 !== end) { + remove2(next2); + } else { + break; + } + } + } + const next = nextSibling(node); + const container = parentNode(node); + remove2(node); + patch( + null, + vnode, + container, + next, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + if (parentComponent) { + parentComponent.vnode.el = vnode.el; + updateHOCHostEl(parentComponent, vnode.el); + } + return next; + }; + const locateClosingAnchor = (node, open = "[", close = "]") => { + let match = 0; + while (node) { + node = nextSibling(node); + if (node && isComment(node)) { + if (node.data === open) match++; + if (node.data === close) { + if (match === 0) { + return nextSibling(node); + } else { + match--; + } + } + } + } + return node; + }; + const replaceNode = (newNode, oldNode, parentComponent) => { + const parentNode2 = oldNode.parentNode; + if (parentNode2) { + parentNode2.replaceChild(newNode, oldNode); + } + let parent = parentComponent; + while (parent) { + if (parent.vnode.el === oldNode) { + parent.vnode.el = parent.subTree.el = newNode; + } + parent = parent.parent; + } + }; + const isTemplateNode = (node) => { + return node.nodeType === 1 && node.tagName === "TEMPLATE"; + }; + return [hydrate2, hydrateNode]; +} +function propHasMismatch(el, key, clientValue, vnode, instance) { + let mismatchType; + let mismatchKey; + let actual; + let expected; + if (key === "class") { + actual = el.getAttribute("class"); + expected = normalizeClass(clientValue); + if (!isSetEqual(toClassSet(actual || ""), toClassSet(expected))) { + mismatchType = 2; + mismatchKey = `class`; + } + } else if (key === "style") { + actual = el.getAttribute("style") || ""; + expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue)); + const actualMap = toStyleMap(actual); + const expectedMap = toStyleMap(expected); + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + if (dir.name === "show" && !value) { + expectedMap.set("display", "none"); + } + } + } + if (instance) { + resolveCssVars(instance, vnode, expectedMap); + } + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = 3; + mismatchKey = "style"; + } + } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key); + expected = includeBooleanAttr(clientValue); + } else if (clientValue == null) { + actual = el.hasAttribute(key); + expected = false; + } else { + if (el.hasAttribute(key)) { + actual = el.getAttribute(key); + } else if (key === "value" && el.tagName === "TEXTAREA") { + actual = el.value; + } else { + actual = false; + } + expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false; + } + if (actual !== expected) { + mismatchType = 4; + mismatchKey = key; + } + } + if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) { + const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"`; + const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`; + const postSegment = ` + - rendered on server: ${format(actual)} + - expected on client: ${format(expected)} + Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead. + You should fix the source of the mismatch.`; + { + warn$1(preSegment, el, postSegment); + } + return true; + } + return false; +} +function toClassSet(str) { + return new Set(str.trim().split(/\s+/)); +} +function isSetEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const s of a) { + if (!b.has(s)) { + return false; + } + } + return true; +} +function toStyleMap(str) { + const styleMap = /* @__PURE__ */ new Map(); + for (const item of str.split(";")) { + let [key, value] = item.split(":"); + key = key.trim(); + value = value && value.trim(); + if (key && value) { + styleMap.set(key, value); + } + } + return styleMap; +} +function isMapEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false; + } + } + return true; +} +function resolveCssVars(instance, vnode, expectedMap) { + const root = instance.subTree; + if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) { + const cssVars = instance.getCssVars(); + for (const key in cssVars) { + expectedMap.set( + `--${getEscapedCssVarName(key, false)}`, + String(cssVars[key]) + ); + } + } + if (vnode === root && instance.parent) { + resolveCssVars(instance.parent, instance.vnode, expectedMap); + } +} +var allowMismatchAttr = "data-allow-mismatch"; +var MismatchTypeString = { + [ + 0 + /* TEXT */ + ]: "text", + [ + 1 + /* CHILDREN */ + ]: "children", + [ + 2 + /* CLASS */ + ]: "class", + [ + 3 + /* STYLE */ + ]: "style", + [ + 4 + /* ATTRIBUTE */ + ]: "attribute" +}; +function isMismatchAllowed(el, allowedType) { + if (allowedType === 0 || allowedType === 1) { + while (el && !el.hasAttribute(allowMismatchAttr)) { + el = el.parentElement; + } + } + const allowedAttr = el && el.getAttribute(allowMismatchAttr); + if (allowedAttr == null) { + return false; + } else if (allowedAttr === "") { + return true; + } else { + const list = allowedAttr.split(","); + if (allowedType === 0 && list.includes("children")) { + return true; + } + return allowedAttr.split(",").includes(MismatchTypeString[allowedType]); + } +} +var requestIdleCallback = getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1)); +var cancelIdleCallback = getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id)); +var hydrateOnIdle = (timeout = 1e4) => (hydrate2) => { + const id = requestIdleCallback(hydrate2, { timeout }); + return () => cancelIdleCallback(id); +}; +function elementIsVisibleInViewport(el) { + const { top, left, bottom, right } = el.getBoundingClientRect(); + const { innerHeight, innerWidth } = window; + return (top > 0 && top < innerHeight || bottom > 0 && bottom < innerHeight) && (left > 0 && left < innerWidth || right > 0 && right < innerWidth); +} +var hydrateOnVisible = (opts) => (hydrate2, forEach) => { + const ob = new IntersectionObserver((entries) => { + for (const e of entries) { + if (!e.isIntersecting) continue; + ob.disconnect(); + hydrate2(); + break; + } + }, opts); + forEach((el) => { + if (!(el instanceof Element)) return; + if (elementIsVisibleInViewport(el)) { + hydrate2(); + ob.disconnect(); + return false; + } + ob.observe(el); + }); + return () => ob.disconnect(); +}; +var hydrateOnMediaQuery = (query) => (hydrate2) => { + if (query) { + const mql = matchMedia(query); + if (mql.matches) { + hydrate2(); + } else { + mql.addEventListener("change", hydrate2, { once: true }); + return () => mql.removeEventListener("change", hydrate2); + } + } +}; +var hydrateOnInteraction = (interactions = []) => (hydrate2, forEach) => { + if (isString(interactions)) interactions = [interactions]; + let hasHydrated = false; + const doHydrate = (e) => { + if (!hasHydrated) { + hasHydrated = true; + teardown(); + hydrate2(); + e.target.dispatchEvent(new e.constructor(e.type, e)); + } + }; + const teardown = () => { + forEach((el) => { + for (const i of interactions) { + el.removeEventListener(i, doHydrate); + } + }); + }; + forEach((el) => { + for (const i of interactions) { + el.addEventListener(i, doHydrate, { once: true }); + } + }); + return teardown; +}; +function forEachElement(node, cb) { + if (isComment(node) && node.data === "[") { + let depth = 1; + let next = node.nextSibling; + while (next) { + if (next.nodeType === 1) { + const result = cb(next); + if (result === false) { + break; + } + } else if (isComment(next)) { + if (next.data === "]") { + if (--depth === 0) break; + } else if (next.data === "[") { + depth++; + } + } + next = next.nextSibling; + } + } else { + cb(node); + } +} +var isAsyncWrapper = (i) => !!i.type.__asyncLoader; +function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + hydrate: hydrateStrategy, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve2, reject) => { + const userRetry = () => resolve2(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (!comp) { + warn$1( + `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.` + ); + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + if (comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`); + } + resolvedComp = comp; + return comp; + })); + }; + return defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + __asyncHydrate(el, instance, hydrate2) { + const doHydrate = hydrateStrategy ? () => { + const teardown = hydrateStrategy( + hydrate2, + (cb) => forEachElement(el, cb) + ); + if (teardown) { + (instance.bum || (instance.bum = [])).push(teardown); + } + } : hydrate2; + if (resolvedComp) { + doHydrate(); + } else { + load().then(() => !instance.isUnmounted && doHydrate()); + } + }, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + markAsyncBoundary(instance); + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || isInSSRComponentSetup) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = ref(false); + const error = ref(); + const delayed = ref(!!delay); + if (delay) { + setTimeout(() => { + delayed.value = false; + }, delay); + } + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.update(); + } + }).catch((err) => { + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createVNode(loadingComponent); + } + }; + } + }); +} +function createInnerComp(comp, parent) { + const { ref: ref2, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref2; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; +} +var isKeepAlive = (vnode) => vnode.type.__isKeepAlive; +var KeepAliveImpl = { + name: `KeepAlive`, + // Marker for special handling inside the renderer. We are not using a === + // check directly on KeepAlive in the renderer, because importing it directly + // would prevent it from being tree-shaken. + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number] + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const sharedContext = instance.ctx; + if (!sharedContext.renderer) { + return () => { + const children = slots.default && slots.default(); + return children && children.length === 1 ? children[0] : children; + }; + } + const cache = /* @__PURE__ */ new Map(); + const keys = /* @__PURE__ */ new Set(); + let current = null; + if (true) { + instance.__v_cache = cache; + } + const parentSuspense = instance.suspense; + const { + renderer: { + p: patch, + m: move, + um: _unmount, + o: { createElement } + } + } = sharedContext; + const storageContainer = createElement("div"); + sharedContext.activate = (vnode, container, anchor, namespace, optimized) => { + const instance2 = vnode.component; + move(vnode, container, anchor, 0, parentSuspense); + patch( + instance2.vnode, + vnode, + container, + anchor, + instance2, + parentSuspense, + namespace, + vnode.slotScopeIds, + optimized + ); + queuePostRenderEffect(() => { + instance2.isDeactivated = false; + if (instance2.a) { + invokeArrayFns(instance2.a); + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + }; + sharedContext.deactivate = (vnode) => { + const instance2 = vnode.component; + invalidateMount(instance2.m); + invalidateMount(instance2.a); + move(vnode, storageContainer, null, 1, parentSuspense); + queuePostRenderEffect(() => { + if (instance2.da) { + invokeArrayFns(instance2.da); + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + instance2.isDeactivated = true; + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + }; + function unmount(vnode) { + resetShapeFlag(vnode); + _unmount(vnode, instance, parentSuspense, true); + } + function pruneCache(filter) { + cache.forEach((vnode, key) => { + const name = getComponentName(vnode.type); + if (name && !filter(name)) { + pruneCacheEntry(key); + } + }); + } + function pruneCacheEntry(key) { + const cached = cache.get(key); + if (cached && (!current || !isSameVNodeType(cached, current))) { + unmount(cached); + } else if (current) { + resetShapeFlag(current); + } + cache.delete(key); + keys.delete(key); + } + watch2( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache((name) => matches(include, name)); + exclude && pruneCache((name) => !matches(exclude, name)); + }, + // prune post-render after `current` has been updated + { flush: "post", deep: true } + ); + let pendingCacheKey = null; + const cacheSubtree = () => { + if (pendingCacheKey != null) { + if (isSuspense(instance.subTree.type)) { + queuePostRenderEffect(() => { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + }, instance.subTree.suspense); + } else { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + } + } + }; + onMounted(cacheSubtree); + onUpdated(cacheSubtree); + onBeforeUnmount(() => { + cache.forEach((cached) => { + const { subTree, suspense } = instance; + const vnode = getInnerChild(subTree); + if (cached.type === vnode.type && cached.key === vnode.key) { + resetShapeFlag(vnode); + const da = vnode.component.da; + da && queuePostRenderEffect(da, suspense); + return; + } + unmount(cached); + }); + }); + return () => { + pendingCacheKey = null; + if (!slots.default) { + return current = null; + } + const children = slots.default(); + const rawVNode = children[0]; + if (children.length > 1) { + if (true) { + warn$1(`KeepAlive should contain exactly one component child.`); + } + current = null; + return children; + } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) { + current = null; + return rawVNode; + } + let vnode = getInnerChild(rawVNode); + if (vnode.type === Comment) { + current = null; + return vnode; + } + const comp = vnode.type; + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp + ); + const { include, exclude, max } = props; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + vnode.shapeFlag &= ~256; + current = vnode; + return rawVNode; + } + const key = vnode.key == null ? comp : vnode.key; + const cachedVNode = cache.get(key); + if (vnode.el) { + vnode = cloneVNode(vnode); + if (rawVNode.shapeFlag & 128) { + rawVNode.ssContent = vnode; + } + } + pendingCacheKey = key; + if (cachedVNode) { + vnode.el = cachedVNode.el; + vnode.component = cachedVNode.component; + if (vnode.transition) { + setTransitionHooks(vnode, vnode.transition); + } + vnode.shapeFlag |= 512; + keys.delete(key); + keys.add(key); + } else { + keys.add(key); + if (max && keys.size > parseInt(max, 10)) { + pruneCacheEntry(keys.values().next().value); + } + } + vnode.shapeFlag |= 256; + current = vnode; + return isSuspense(rawVNode.type) ? rawVNode : vnode; + }; + } +}; +var KeepAlive = KeepAliveImpl; +function matches(pattern, name) { + if (isArray(pattern)) { + return pattern.some((p2) => matches(p2, name)); + } else if (isString(pattern)) { + return pattern.split(",").includes(name); + } else if (isRegExp(pattern)) { + pattern.lastIndex = 0; + return pattern.test(name); + } + return false; +} +function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); +} +function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); +} +function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } +} +function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); +} +function resetShapeFlag(vnode) { + vnode.shapeFlag &= ~256; + vnode.shapeFlag &= ~512; +} +function getInnerChild(vnode) { + return vnode.shapeFlag & 128 ? vnode.ssContent : vnode; +} +function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + pauseTracking(); + const reset = setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + reset(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } else if (true) { + const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, "")); + warn$1( + `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` + ); + } +} +var createHook = (lifecycle) => (hook, target = currentInstance) => { + if (!isInSSRComponentSetup || lifecycle === "sp") { + injectHook(lifecycle, (...args) => hook(...args), target); + } +}; +var onBeforeMount = createHook("bm"); +var onMounted = createHook("m"); +var onBeforeUpdate = createHook( + "bu" +); +var onUpdated = createHook("u"); +var onBeforeUnmount = createHook( + "bum" +); +var onUnmounted = createHook("um"); +var onServerPrefetch = createHook( + "sp" +); +var onRenderTriggered = createHook("rtg"); +var onRenderTracked = createHook("rtc"); +function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); +} +var COMPONENTS = "components"; +var DIRECTIVES = "directives"; +function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; +} +var NULL_DYNAMIC_COMPONENT = Symbol.for("v-ndc"); +function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } +} +function resolveDirective(name) { + return resolveAsset(DIRECTIVES, name); +} +function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + if (type === COMPONENTS) { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + if (warnMissing && !res) { + const extra = type === COMPONENTS ? ` +If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``; + warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); + } + return res; + } else if (true) { + warn$1( + `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().` + ); + } +} +function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); +} +function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache && cache[index]; + const sourceIsArray = isArray(source); + if (sourceIsArray || isString(source)) { + const sourceIsReactiveArray = sourceIsArray && isReactive(source); + let needsWrap = false; + if (sourceIsReactiveArray) { + needsWrap = !isShallow(source); + source = shallowReadArray(source); + } + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem( + needsWrap ? toReactive(source[i]) : source[i], + i, + void 0, + cached && cached[i] + ); + } + } else if (typeof source === "number") { + if (!Number.isInteger(source)) { + warn$1(`The v-for range expect an integer value but got ${source}.`); + } + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]); + } + } else if (isObject(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached && cached[i]) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached && cached[i]); + } + } + } else { + ret = []; + } + if (cache) { + cache[index] = ret; + } + return ret; +} +function createSlots(slots, dynamicSlots) { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i]; + if (isArray(slot)) { + for (let j = 0; j < slot.length; j++) { + slots[slot[j].name] = slot[j].fn; + } + } else if (slot) { + slots[slot.name] = slot.key ? (...args) => { + const res = slot.fn(...args); + if (res) res.key = slot.key; + return res; + } : slot.fn; + } + } + return slots; +} +function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) { + if (name !== "default") props.name = name; + return openBlock(), createBlock( + Fragment, + null, + [createVNode("slot", props, fallback && fallback())], + 64 + ); + } + let slot = slots[name]; + if (slot && slot.length > 1) { + warn$1( + `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.` + ); + slot = () => []; + } + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key; + const rendered = createBlock( + Fragment, + { + key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content + (!validSlotContent && fallback ? "_fb" : "") + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; +} +function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) return true; + if (child.type === Comment) return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; +} +function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + if (!isObject(obj)) { + warn$1(`v-on with no argument expects an object value.`); + return ret; + } + for (const key in obj) { + ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; +} +var getPublicInstance = (i) => { + if (!i) return null; + if (isStatefulComponent(i)) return getComponentPublicInstance(i); + return getPublicInstance(i.parent); +}; +var publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => true ? shallowReadonly(i.props) : i.props, + $attrs: (i) => true ? shallowReadonly(i.attrs) : i.attrs, + $slots: (i) => true ? shallowReadonly(i.slots) : i.slots, + $refs: (i) => true ? shallowReadonly(i.refs) : i.refs, + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $host: (i) => i.ce, + $emit: (i) => i.emit, + $options: (i) => __VUE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type, + $forceUpdate: (i) => i.f || (i.f = () => { + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => __VUE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP + }) +); +var isReservedPrefix = (key) => key === "_" || key === "$"; +var hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); +var PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + if (key === "__v_skip") { + return true; + } + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key === "__isVue") { + return true; + } + let normalizedProps; + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1: + return setupState[key]; + case 2: + return data[key]; + case 4: + return ctx[key]; + case 3: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1; + return setupState[key]; + } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2; + return data[key]; + } else if ( + // only cache other properties when instance has declared (thus stable) + // props + (normalizedProps = instance.propsOptions[0]) && hasOwn(normalizedProps, key) + ) { + accessCache[key] = 3; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if (!__VUE_OPTIONS_API__ || shouldCacheAccess) { + accessCache[key] = 0; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance.attrs, "get", ""); + markAttrsAccessed(); + } else if (key === "$slots") { + track(instance, "get", key); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else if (currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf("__v") !== 0)) { + if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) { + warn$1( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.` + ); + } else if (instance === currentRenderingInstance) { + warn$1( + `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.` + ); + } + } + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (setupState.__isScriptSetup && hasOwn(setupState, key)) { + warn$1(`Cannot mutate +``` + +--- + +## 注意事项 + +1. **会话有效期**: 上传会话默认 15 分钟后过期,请在有效期内完成上传 +2. **文件大小限制**: 受系统配置 `uploadSize` 限制 +3. **过期类型**: 支持 `day`、`hour`、`minute`、`forever`、`count` +4. **CORS**: 直传模式下,S3 需要配置正确的 CORS 策略 +5. **重试机制**: 建议实现上传失败重试逻辑 diff --git a/core/base_model.py b/docs/changelog.md similarity index 100% rename from core/base_model.py rename to docs/changelog.md diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/en/api/index.md b/docs/en/api/index.md new file mode 100644 index 000000000..2addc6459 --- /dev/null +++ b/docs/en/api/index.md @@ -0,0 +1,332 @@ +# FileCodeBox API Documentation + +## API Version: 2.1.0 + +## Table of Contents +- [Authentication](#authentication) +- [Share API](#share-api) +- [Admin API](#admin-api) + +## Authentication + +Some APIs require `Authorization` header for authentication: + +``` +Authorization: Bearer +``` + +### Get Token + +When guest upload is disabled in admin panel (`openUpload=0`), you need to login first: + +```bash +curl -X POST "http://localhost:12345/admin/login" \ + -H "Content-Type: application/json" \ + -d '{"password": "FileCodeBox2023"}' +``` + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "token": "xxx.xxx.xxx", + "token_type": "Bearer" + } +} +``` + +## Share API + +### Share Text + +**POST** `/share/text/` + +Share text content and get a share code. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| text | string | Yes | - | Text content to share | +| expire_value | integer | No | 1 | Expiration time value | +| expire_style | string | No | "day" | Expiration time unit(day/hour/minute/count/forever) | + +**cURL Example:** + +```bash +# Guest upload (when openUpload=1) +curl -X POST "http://localhost:12345/share/text/" \ + -F "text=This is the text content to share" \ + -F "expire_value=1" \ + -F "expire_style=day" + +# When authentication required (openUpload=0) +curl -X POST "http://localhost:12345/share/text/" \ + -H "Authorization: Bearer xxx.xxx.xxx" \ + -F "text=This is the text content to share" +``` + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "code": "abc123" + } +} +``` + +### Share File + +**POST** `/share/file/` + +Upload and share a file, get a share code. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| file | file | Yes | - | File to upload | +| expire_value | integer | No | 1 | Expiration time value | +| expire_style | string | No | "day" | Expiration time unit(day/hour/minute/count/forever) | + +**cURL Example:** + +```bash +# Upload file (default 1 day expiration) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" + +# Upload file (7 days expiration) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" \ + -F "expire_value=7" \ + -F "expire_style=day" + +# Upload file (10 downloads limit) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" \ + -F "expire_value=10" \ + -F "expire_style=count" + +# When authentication required +curl -X POST "http://localhost:12345/share/file/" \ + -H "Authorization: Bearer xxx.xxx.xxx" \ + -F "file=@/path/to/file.txt" +``` + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "code": "abc123", + "name": "example.txt" + } +} +``` + +### Get File Info + +**GET** `/share/select/` + +Get file information by share code (direct file download). + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| code | string | Yes | File share code | + +**cURL Example:** + +```bash +# Download file by extraction code +curl -L "http://localhost:12345/share/select/?code=abc123" -o downloaded_file +``` + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "code": "abc123", + "name": "example.txt", + "size": 1024, + "text": "File content or download link" + } +} +``` + +### Select File + +**POST** `/share/select/` + +Select file by share code. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| code | string | Yes | File share code | + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "code": "abc123", + "name": "example.txt", + "size": 1024, + "text": "File content or download link" + } +} +``` + +### Download File + +**GET** `/share/download` + +Download shared file. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| key | string | Yes | Download key | +| code | string | Yes | File share code | + +## Admin API + +### Admin Login + +**POST** `/admin/login` + +Admin login to get token. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| password | string | Yes | Admin password | + +### Dashboard Data + +**GET** `/admin/dashboard` + +Get system dashboard data. + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "totalFiles": 100, + "storageUsed": "1.5GB", + "sysUptime": "10 days", + "yesterdayCount": 50, + "yesterdaySize": "500MB", + "todayCount": 30, + "todaySize": "300MB" + } +} +``` + +### File List + +**GET** `/admin/file/list` + +Get system file list. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| page | integer | No | 1 | Current page | +| size | integer | No | 10 | Page size | +| keyword | string | No | "" | Search keyword | + +**Response Example:** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "page": 1, + "size": 10, + "total": 100, + "data": [ + { + "id": 1, + "name": "example.txt", + "size": 1024, + "created_at": "2024-01-01 12:00:00" + } + ] + } +} +``` + +### Delete File + +**DELETE** `/admin/file/delete` + +Delete file from system. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| id | integer | Yes | File ID | + +### Get Config + +**GET** `/admin/config/get` + +Get system configuration. + +### Update Config + +**PATCH** `/admin/config/update` + +Update system configuration. + +## Error Response + +When an error occurs, the API will return corresponding error message: + +```json +{ + "code": 422, + "detail": [ + { + "loc": ["body", "password"], + "msg": "Password cannot be empty", + "type": "value_error" + } + ] +} +``` + +## Status Codes + +- 200: Success +- 401: Unauthorized +- 403: Forbidden +- 404: Not Found +- 422: Validation Error +- 500: Internal Server Error \ No newline at end of file diff --git a/docs/en/changelog.md b/docs/en/changelog.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/en/contributing.md b/docs/en/contributing.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/en/guide/configuration.md b/docs/en/guide/configuration.md new file mode 100644 index 000000000..89f6be39c --- /dev/null +++ b/docs/en/guide/configuration.md @@ -0,0 +1,207 @@ +# Configuration Guide + +FileCodeBox provides rich configuration options that can be customized through the admin panel or by directly modifying the configuration. This document details all available configuration options. + +## Configuration Methods + +FileCodeBox supports two configuration methods: + +1. **Admin Panel Configuration** (Recommended): Access `/admin` to enter the admin panel and modify settings on the settings page +2. **Database Configuration**: Configuration is stored in the `data/filecodebox.db` database + +::: tip Note +On first startup, the system uses default configuration from `core/settings.py`. Modified configurations are saved to the database. +::: + +## Basic Settings + +### Site Information + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `name` | string | `文件快递柜 - FileCodeBox` | Site name, displayed in page title and navigation bar | +| `description` | string | `开箱即用的文件快传系统` | Site description, used for SEO | +| `keywords` | string | `FileCodeBox, 文件快递柜...` | Site keywords, used for SEO | +| `port` | int | `12345` | Service listening port | + +### Notification Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `notify_title` | string | `系统通知` | Notification title | +| `notify_content` | string | Welcome message | Notification content, supports HTML | +| `page_explain` | string | Legal disclaimer | Footer explanation text | +| `robotsText` | string | `User-agent: *\nDisallow: /` | robots.txt content | + +## Upload Settings + +### File Upload Limits + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `openUpload` | int | `1` | Enable upload functionality (1=enabled, 0=disabled) | +| `uploadSize` | int | `10485760` | Maximum single file upload size (bytes), default 10MB | +| `enableChunk` | int | `0` | Enable chunked upload (1=enabled, 0=disabled) | + +::: warning Note +`uploadSize` is in bytes. 10MB = 10 * 1024 * 1024 = 10485760 bytes +::: + +### Upload Rate Limiting + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `uploadMinute` | int | `1` | Upload limit time window (minutes) | +| `uploadCount` | int | `10` | Maximum uploads allowed within the time window | + +Example: Default configuration allows up to 10 uploads per minute. + + +### File Expiration Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | Available expiration methods | +| `max_save_seconds` | int | `0` | Maximum file retention time (seconds), 0 means no limit | + +Expiration methods explained: +- `day` - Expire by days +- `hour` - Expire by hours +- `minute` - Expire by minutes +- `forever` - Never expire +- `count` - Expire by download count + +## Theme Settings + +### Theme Selection + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `themesSelect` | string | `themes/2024` | Currently active theme | +| `themesChoices` | list | See below | Available themes list | + +Default available themes: +```json +[ + { + "name": "2023", + "key": "themes/2023", + "author": "Lan", + "version": "1.0" + }, + { + "name": "2024", + "key": "themes/2024", + "author": "Lan", + "version": "1.0" + } +] +``` + +### Interface Style + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `opacity` | float | `0.9` | Interface opacity (0-1) | +| `background` | string | `""` | Custom background image URL, empty uses default background | + +## Admin Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `admin_token` | string | `FileCodeBox2023` | Admin login password | +| `showAdminAddr` | int | `0` | Show admin panel entry on homepage (1=show, 0=hide) | + +::: danger Security Warning +Always change the default `admin_token` in production environments! Using the default password poses serious security risks. +::: + +## Security Settings + +### Error Rate Limiting + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `errorMinute` | int | `1` | Error limit time window (minutes) | +| `errorCount` | int | `1` | Maximum errors allowed within the time window | + +This setting prevents brute-force attacks on extraction codes. + +## Storage Settings + +### Storage Type + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `file_storage` | string | `local` | Storage backend type | +| `storage_path` | string | `""` | Custom storage path | + +Supported storage types: +- `local` - Local storage +- `s3` - S3-compatible storage (AWS S3, Aliyun OSS, MinIO, etc.) +- `onedrive` - OneDrive storage +- `webdav` - WebDAV storage +- `opendal` - OpenDAL storage + +For detailed storage configuration, see [Storage Configuration](/en/guide/storage). + +## Configuration Examples + +### Example 1: Small Personal Use + +Suitable for personal or small team use with relaxed limits: + +```python +{ + "name": "My File Share", + "uploadSize": 52428800, # 50MB + "uploadMinute": 5, # 5 minutes + "uploadCount": 20, # Max 20 uploads + "expireStyle": ["day", "hour", "forever"], + "admin_token": "your-secure-password", + "showAdminAddr": 1 +} +``` + +### Example 2: Public Service + +Suitable for public services requiring stricter limits: + +```python +{ + "name": "Public File Box", + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, # 1 minute + "uploadCount": 5, # Max 5 uploads + "errorMinute": 5, # 5 minutes + "errorCount": 3, # Max 3 errors + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # Max retention 1 day + "admin_token": "very-secure-password-123", + "showAdminAddr": 0 +} +``` + +### Example 3: Enterprise Internal Use + +Suitable for enterprise internal use with large file and chunked upload support: + +```python +{ + "name": "Enterprise File Transfer", + "uploadSize": 1073741824, # 1GB + "enableChunk": 1, # Enable chunked upload + "uploadMinute": 10, # 10 minutes + "uploadCount": 100, # Max 100 uploads + "expireStyle": ["day", "forever"], + "file_storage": "s3", # Use S3 storage + "admin_token": "enterprise-secure-token", + "showAdminAddr": 1 +} +``` + +## Next Steps + +- [Storage Configuration](/en/guide/storage) - Learn how to configure different storage backends +- [Security Settings](/en/guide/security) - Learn how to enhance system security +- [File Sharing](/en/guide/share) - Learn about file sharing features diff --git a/docs/en/guide/getting-started.md b/docs/en/guide/getting-started.md new file mode 100644 index 000000000..0cb41c0cf --- /dev/null +++ b/docs/en/guide/getting-started.md @@ -0,0 +1,67 @@ +# Getting Started + +## Introduction + +FileCodeBox is a simple and efficient file sharing tool that supports temporary file transfer, sharing, and management. This guide will help you quickly deploy and use FileCodeBox. + +## Features + +- 🚀 Quick Deployment: Support Docker one-click deployment +- 🔒 Secure & Reliable: File access requires extraction code +- ⏱️ Time Control: Support setting file expiration time +- 📊 Download Limit: Can limit file download times +- 🖼️ File Preview: Support preview of images, videos, audio, and other formats +- 📱 Responsive Design: Perfect adaptation for mobile and desktop + +## Deployment Methods + +### Docker Deployment + +```bash +docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:beta +``` + +### Manual Deployment + +1. Clone the repository +```bash +git clone https://github.com/vastsa/FileCodeBox.git +``` + +2. Install dependencies +```bash +cd FileCodeBox +pip install -r requirements.txt +``` + +3. Start the service +```bash +python main.py +``` + +## Usage + +1. Access the System + Open browser and visit `http://localhost:12345` + +2. Upload Files + - Click upload button or drag files to upload area + - Set file expiration time and download limit + - Get share link and extraction code + +3. Download Files + - Visit share link + - Enter extraction code + - Download file + +4. Admin Panel + - Visit `http://localhost:12345/#/admin` + - Enter admin password: `FileCodeBox2023` + - Enter admin panel + - View system information, file list, user management, etc. + +## Next Steps + +- [Storage Configuration](/en/guide/storage) - Learn how to configure different storage methods +- [Security Settings](/en/guide/security) - Learn how to enhance system security +- [API Documentation](/en/api/) - Learn how to integrate through API \ No newline at end of file diff --git a/docs/en/guide/introduction.md b/docs/en/guide/introduction.md new file mode 100644 index 000000000..ede9a4ae4 --- /dev/null +++ b/docs/en/guide/introduction.md @@ -0,0 +1,243 @@ +
+ +FileCodeBox Logo + +

Share text and files anonymously with a passcode, like picking up a package

+ +[![GitHub stars](https://img.shields.io/github/stars/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/network) +[![GitHub issues](https://img.shields.io/github/issues/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/issues) +[![GitHub license](https://img.shields.io/github/license/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/blob/master/LICENSE) +[![QQ Group](https://img.shields.io/badge/QQ%20Group-739673698-blue.svg)](https://qm.qq.com/q/PemPzhdEIM) +[![Python Version](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://www.python.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.68+-green.svg)](https://fastapi.tiangolo.com) +[![Vue Version](https://img.shields.io/badge/Vue.js-3.x-brightgreen.svg)](https://v3.vuejs.org) + +
+ +## 📝 Introduction + +FileCodeBox is a lightweight file sharing tool developed with FastAPI + Vue3. It allows users to share text and files easily, where recipients only need a passcode to retrieve the files, just like picking up a package from a delivery locker. + +## 🖼️ Preview + +
+

+ +Frontend Repository + +    + +Demo Site + +

+
+ + +## 🎯 Use Cases + + + + + + + + + + + + +
+

📁 Temporary File Sharing

+Quick file sharing without registration +
+

📝 Quick Text Sharing

+Share code snippets and text content +
+

🕶️ Anonymous Transfer

+Privacy-protected file transfer +
+

💾 Temporary Storage

+File storage with expiration time +
+

🔄 Cross-platform Transfer

+Quick file transfer between devices +
+

🌐 Private Share Service

+Build your own file sharing service +
+ +## ✨ Core Features + + + + + + + + + + + + + + + + + + + + + + +
+

🚀 Lightweight

+Based on FastAPI + SQLite3 + Vue3 + ElementUI +
+

📤 Easy Upload

+Support copy-paste and drag-drop +
+

📦 Multiple Types

+Support text and various file types +
+

🔒 Security

+ +- IP upload limits +- Error attempt limits +- File expiration +
+

🎫 Passcode Sharing

+Random codes with customizable limits +
+

🌍 Multi-language

+Support for Simplified Chinese, Traditional Chinese, and English +
+

🎭 Anonymous

+No registration required +
+

🛠 Admin Panel

+File and system management +
+

🐳 Docker

+One-click deployment +
+

💾 Storage Options

+Local, S3, OneDrive support +
+

📱 Responsive

+Mobile-friendly design +
+

💻 CLI Support

+Command-line download +
+ +## 🚀 Quick Start + +### Docker Deployment + +```bash +docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:beta +``` + +### Manual Deployment + +1. Clone the repository +```bash +git clone https://github.com/vastsa/FileCodeBox.git +``` + +2. Install dependencies +```bash +cd FileCodeBox +pip install -r requirements.txt +``` + +3. Start the service +```bash +python main.py +``` + +## 📖 Usage Guide + +### Share Files +1. Open the website, click "Share File" +2. Select or drag files +3. Set expiration time and count +4. Get the passcode + +### Retrieve Files +1. Open the website, enter passcode +2. Click retrieve +3. Download file or view text + +### Admin Panel +1. Visit `/admin` +2. Enter admin password +3. Manage files and settings + +## 🛠 Development Guide + +### Project Structure +``` +FileCodeBox/ +├── apps/ # Application code +│ ├── admin/ # Admin backend +│ └── base/ # Base functions +├── core/ # Core functions +├── data/ # Data directory +└── fcb-fronted/ # Frontend code +``` + +### Development Environment +- Python 3.8+ +- Node.js 14+ +- Vue 3 +- FastAPI + +### Local Development +1. Backend development +```bash +python main.py +``` + +2. Frontend development +```bash +cd fcb-fronted +npm install +npm run dev +``` + +## 🤝 Contributing + +1. Fork the project +2. Create your feature branch `git checkout -b feature/xxx` +3. Commit your changes `git commit -m 'Add xxx'` +4. Push to the branch `git push origin feature/xxx` +5. Open a Pull Request + +## ❓ FAQ + +### Q: How to modify upload size limit? +A: Change `uploadSize` in admin panel + +### Q: How to configure storage engine? +A: Select storage engine and configure parameters in admin panel + +### Q: How to backup data? +A: Backup the `data` directory + +For more questions, visit [Wiki](https://github.com/vastsa/FileCodeBox/wiki/常见问题) + +## 😀 Project Statistics and Analytics + +
+Featured|HelloGitHub + +![Repobeats](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg) + +[![Star History](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](https://star-history.com/#vastsa/FileCodeBox&Date) +
+ +## 📜 Disclaimer + +This project is open-source for learning purposes only. It should not be used for any illegal purposes. The author is not responsible for any consequences. Please retain the project address and copyright information when using it. \ No newline at end of file diff --git a/docs/en/guide/management.md b/docs/en/guide/management.md new file mode 100644 index 000000000..e22a65c1e --- /dev/null +++ b/docs/en/guide/management.md @@ -0,0 +1,413 @@ +# Admin Panel + +FileCodeBox provides a fully-featured admin panel that allows administrators to conveniently manage files, view system status, and modify configurations. This document introduces the various features and usage of the admin panel. + +## Accessing the Admin Panel + +### Login Method + +The admin panel is located at the `/admin` path. Access method: + +1. Visit `http://your-domain.com/admin` in your browser +2. Enter the admin password (the value of the `admin_token` configuration) +3. Click the login button + +::: tip Tip +The default admin password is `FileCodeBox2023`. Be sure to change this password in production environments. See [Security Settings](/en/guide/security) for details. +::: + +### Show Admin Entry + +By default, the admin panel entry is not shown on the homepage. You can control whether to show it via configuration: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `showAdminAddr` | int | `0` | Show admin entry on homepage (1=show, 0=hide) | + +::: warning Security Recommendation +For public services, it's recommended to keep `showAdminAddr` at `0` and access the admin panel directly via the `/admin` path to reduce the risk of malicious scanning. +::: + +### Authentication Mechanism + +The admin panel uses JWT (JSON Web Token) for authentication: + +1. After successful login, the server returns a Token containing admin identity +2. Subsequent requests carry the Token via `Authorization: Bearer ` header +3. Token is used to verify admin identity, ensuring only authorized users can access admin functions + +## Dashboard + +After logging in, you first see the dashboard page, which displays the overall system status. + +### Statistics + +The dashboard displays the following key metrics: + +| Metric | Description | +|--------|-------------| +| **Total Files** (`totalFiles`) | Total number of files stored in the system | +| **Storage Used** (`storageUsed`) | Total storage space occupied by all files (bytes) | +| **System Uptime** (`sysUptime`) | Time when the system was first started | +| **Yesterday's Uploads** (`yesterdayCount`) | Number of files uploaded yesterday | +| **Yesterday's Upload Size** (`yesterdaySize`) | Total size of files uploaded yesterday (bytes) | +| **Today's Uploads** (`todayCount`) | Number of files uploaded today so far | +| **Today's Upload Size** (`todaySize`) | Total size of files uploaded today (bytes) | + +### Metric Notes + +- **Total Files**: Includes all unexpired files and text shares +- **Storage Used**: Shows actual storage space occupied by files, excluding database and other system files +- **Yesterday/Today Statistics**: Calculated based on file creation time, useful for understanding system usage trends + +::: tip Tip +Storage usage is displayed in bytes. For example, `10485760` represents approximately 10MB. +::: + +## File Management + +### File List + +The file management page displays all shared files in the system, supporting pagination and search. + +**List information includes:** +- File ID +- Extraction code (code) +- Filename prefix (prefix) +- File extension (suffix) +- File size +- Creation time +- Expiration time +- Remaining download count + +### Search Files + +Use the search function to quickly find specific files: + +1. Enter keywords in the search box +2. System performs fuzzy matching based on filename prefix (prefix) +3. Search results update in real-time + +**Search examples:** +- Enter `report` to find all files with "report" in the filename +- Enter `.pdf` to find all PDF files (if the filename contains this string) + +### Pagination + +The file list supports paginated display: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `page` | `1` | Current page number | +| `size` | `10` | Items per page | + + +### Delete Files + +Administrators can delete any file: + +1. Find the file to delete in the file list +2. Click the delete button +3. Confirm the delete operation + +::: danger Warning +Delete operations are irreversible! Files will be permanently deleted from the storage backend, and database records will also be removed. +::: + +**Delete process:** +1. System first deletes the actual file from the storage backend (local/S3/OneDrive, etc.) +2. Then deletes the file record from the database +3. After deletion, the corresponding extraction code becomes invalid + +### Download Files + +Administrators can directly download any file: + +1. Find the target file in the file list +2. Click the download button +3. File will be downloaded via browser + +For text shares, the system returns text content directly instead of downloading a file. + +### Modify File Information + +Administrators can modify some information of shared files: + +| Modifiable Field | Description | +|------------------|-------------| +| `code` | Extraction code (must be unique, cannot duplicate other files) | +| `prefix` | Filename prefix | +| `suffix` | File extension | +| `expired_at` | Expiration time | +| `expired_count` | Remaining download count | + +**Modify extraction code:** +``` +Original code: abc123 +New code: myfile2024 +``` + +::: warning Note +When modifying extraction codes, the system checks if the new code is already in use. If an identical extraction code exists, the modification will fail. +::: + +## Local File Management + +In addition to managing shared files, the admin panel also provides local file management functionality for managing files in the `data/local` directory. + +### View Local Files + +The local file list displays all files in the `data/local` directory: + +| Information | Description | +|-------------|-------------| +| Filename | Complete filename | +| Creation Time | File creation time | +| File Size | File size (bytes) | + +### Share Local Files + +You can quickly share local files: + +1. Select the file to share in the local file list +2. Set expiration method and value +3. Click the share button +4. System generates extraction code + +**Share parameters:** + +| Parameter | Description | +|-----------|-------------| +| `filename` | Filename to share | +| `expire_style` | Expiration method (day/hour/minute/forever/count) | +| `expire_value` | Expiration value (days/hours/minutes/download count) | + +### Delete Local Files + +You can delete files in the `data/local` directory: + +1. Find the file to delete in the local file list +2. Click the delete button +3. Confirm deletion + +::: tip Use Cases +Local file management is useful for: +- Sharing files after batch uploading to the server +- Managing files uploaded to the server through other means +- Cleaning up unnecessary local files +::: + +## System Settings + +### View Configuration + +On the system settings page, you can view the current values of all configuration items. Configuration items are grouped by category: + +- Basic settings (site name, description, etc.) +- Upload settings (file size limits, rate limits, etc.) +- Storage settings (storage type, path, etc.) +- Theme settings (theme selection, opacity, etc.) +- Security settings (admin password, error limits, etc.) + +### Modify Configuration + +Administrators can modify most configurations through the admin panel: + +1. Go to the system settings page +2. Find the configuration item to modify +3. Enter the new value +4. Click the save button + +**Modifiable configuration items:** + +| Category | Example Settings | +|----------|------------------| +| Basic Settings | `name`, `description`, `keywords`, `notify_title`, `notify_content` | +| Upload Settings | `uploadSize`, `uploadMinute`, `uploadCount`, `openUpload`, `enableChunk` | +| Expiration Settings | `expireStyle`, `max_save_seconds` | +| Theme Settings | `themesSelect`, `opacity`, `background` | +| Security Settings | `admin_token`, `showAdminAddr`, `errorMinute`, `errorCount` | +| Storage Settings | `file_storage`, `storage_path` and storage backend-specific configurations | + +::: warning Note +- `admin_token` (admin password) cannot be set to empty +- `themesChoices` (theme list) cannot be modified through the admin panel +- After modifying storage settings, existing files will not be automatically migrated +::: + +### Configuration Effect + +Configuration changes take effect immediately without restarting the service. Configurations are saved in the database and persist after restart. + +**Configuration storage location:** +- Database: `data/filecodebox.db` +- Table name: `keyvalue` +- Key name: `settings` + +## API Endpoints + +All admin panel functions are implemented through REST APIs. Here are the main endpoints: + +### Authentication Endpoint + +**Login** +``` +POST /admin/login +Content-Type: application/json + +{ + "password": "your-admin-password" +} +``` + +Response: +```json +{ + "code": 200, + "detail": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "Bearer" + } +} +``` + +### Dashboard Endpoint + +**Get Statistics** +``` +GET /admin/dashboard +Authorization: Bearer +``` + +### File Management Endpoints + +**Get File List** +``` +GET /admin/file/list?page=1&size=10&keyword= +Authorization: Bearer +``` + +**Delete File** +``` +DELETE /admin/file/delete +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123 +} +``` + +**Download File** +``` +GET /admin/file/download?id=123 +Authorization: Bearer +``` + +**Modify File Information** +``` +PATCH /admin/file/update +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123, + "code": "newcode", + "expired_at": "2024-12-31T23:59:59" +} +``` + +### Local File Endpoints + +**Get Local File List** +``` +GET /admin/local/lists +Authorization: Bearer +``` + +**Delete Local File** +``` +DELETE /admin/local/delete +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt" +} +``` + +**Share Local File** +``` +POST /admin/local/share +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt", + "expire_style": "day", + "expire_value": 7 +} +``` + +### Configuration Endpoints + +**Get Configuration** +``` +GET /admin/config/get +Authorization: Bearer +``` + +**Update Configuration** +``` +PATCH /admin/config/update +Authorization: Bearer +Content-Type: application/json + +{ + "admin_token": "new-password", + "uploadSize": 52428800 +} +``` + +## Common Issues + +### Forgot Admin Password + +If you forgot the admin password, you can reset it through the following methods: + +1. Stop the FileCodeBox service +2. Open `data/filecodebox.db` using an SQLite tool +3. Query the record with `key='settings'` in the `keyvalue` table +4. Modify the `admin_token` value in the JSON +5. Restart the service + +```sql +-- View current configuration +SELECT * FROM keyvalue WHERE key = 'settings'; + +-- Or delete configuration to restore default password +DELETE FROM keyvalue WHERE key = 'settings'; +``` + +### File Deletion Failed + +If an error occurs when deleting files, possible reasons: + +1. **Storage backend connection failed**: Check if storage configuration is correct +2. **File no longer exists**: File may have been manually deleted +3. **Insufficient permissions**: Check write permissions for storage directory + +### Configuration Changes Not Taking Effect + +If configuration changes don't take effect: + +1. Check if you clicked the save button +2. Refresh the page to see if configuration was saved +3. Check browser console for error messages +4. Confirm configuration value format is correct (e.g., don't enter strings for numeric types) + +## Next Steps + +- [Configuration Guide](/en/guide/configuration) - Learn detailed descriptions of all configuration options +- [Security Settings](/en/guide/security) - Learn how to enhance system security +- [Storage Configuration](/en/guide/storage) - Configure different storage backends diff --git a/docs/en/guide/security.md b/docs/en/guide/security.md new file mode 100644 index 000000000..3c741187e --- /dev/null +++ b/docs/en/guide/security.md @@ -0,0 +1,325 @@ +# Security Settings + +FileCodeBox provides multiple layers of security mechanisms to protect your file sharing service. This document explains how to properly configure security options to ensure secure system operation. + +## Admin Password + +### Change Default Password + +::: danger Important Security Warning +FileCodeBox's default admin password is `FileCodeBox2023`. **You must change this password immediately in production environments!** Using the default password allows anyone to access your admin panel. +::: + +There are two ways to change the admin password: + +**Method 1: Via Admin Panel (Recommended)** + +1. Access `/admin` to enter the admin panel +2. Log in with the current password +3. Go to the "System Settings" page +4. Find the `admin_token` configuration item +5. Enter a new secure password and save + +**Method 2: Via Database** + +Configuration is stored in the `keyvalue` table of the `data/filecodebox.db` database. You can directly modify the `admin_token` value. + +### Password Security Recommendations + +- Use a strong password with at least 16 characters +- Include uppercase and lowercase letters, numbers, and special characters +- Avoid common words or personal information +- Change password regularly + +```python +# Recommended password format example +"admin_token": "Xk9#mP2$vL5@nQ8&wR3" +``` + +### Hide Admin Entry + +By default, the admin panel entry is hidden. You can control whether to show the admin entry on the homepage via the `showAdminAddr` configuration: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `showAdminAddr` | int | `0` | Show admin entry (1=show, 0=hide) | + +::: tip Recommendation +For public services, it's recommended to keep `showAdminAddr` at `0` and access the admin panel directly via the `/admin` path. +::: + +## IP Rate Limiting + +FileCodeBox has built-in IP-based rate limiting mechanisms to effectively prevent abuse and attacks. + +### Upload Rate Limiting + +Limit the number of uploads from a single IP within a specified time: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `uploadMinute` | int | `1` | Upload limit time window (minutes) | +| `uploadCount` | int | `10` | Maximum uploads allowed within the time window | + +**How it works:** +- System records upload requests from each IP +- When an IP's upload count reaches `uploadCount` within `uploadMinute` minutes +- Subsequent upload requests from that IP will be rejected with HTTP 423 error +- Counter resets after the time window expires + +**Configuration examples:** + +```python +# Relaxed configuration: Max 20 uploads in 5 minutes +{ + "uploadMinute": 5, + "uploadCount": 20 +} + +# Strict configuration: Max 3 uploads in 1 minute +{ + "uploadMinute": 1, + "uploadCount": 3 +} +``` + + +### Error Rate Limiting + +Limit the number of error attempts from a single IP to prevent brute-force attacks on extraction codes: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `errorMinute` | int | `1` | Error limit time window (minutes) | +| `errorCount` | int | `1` | Maximum errors allowed within the time window | + +**How it works:** +- When a user enters an incorrect extraction code, the system records the error count for that IP +- When error count reaches `errorCount`, that IP will be temporarily locked +- Lock duration is `errorMinute` minutes +- During lockout, all extraction requests from that IP will be rejected + +**Configuration example:** + +```python +# Anti-brute-force configuration: Max 3 errors in 5 minutes +{ + "errorMinute": 5, + "errorCount": 3 +} +``` + +::: warning Note +The default configuration `errorMinute=1, errorCount=1` is very strict, meaning you need to wait 1 minute after entering one incorrect extraction code before retrying. Adjust this configuration based on actual needs. +::: + +## Upload Restrictions + +### File Size Limit + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `uploadSize` | int | `10485760` | Maximum single file upload size (bytes), default 10MB | +| `openUpload` | int | `1` | Enable upload functionality (1=enabled, 0=disabled) | + +**Common size conversions:** +- 10MB = 10 * 1024 * 1024 = `10485760` +- 50MB = 50 * 1024 * 1024 = `52428800` +- 100MB = 100 * 1024 * 1024 = `104857600` +- 1GB = 1024 * 1024 * 1024 = `1073741824` + +### File Expiration Settings + +Through file expiration mechanisms, you can automatically clean up expired files, reducing storage usage and security risks: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | Available expiration methods | +| `max_save_seconds` | int | `0` | Maximum file retention time (seconds), 0 means no limit | + +**Expiration methods explained:** +- `day` - Expire by days +- `hour` - Expire by hours +- `minute` - Expire by minutes +- `forever` - Never expire (requires alphanumeric extraction code) +- `count` - Expire by download count + +**Security recommendations:** + +For public services, it's recommended to: +1. Remove the `forever` option to avoid permanent file storage +2. Set `max_save_seconds` to limit maximum retention time +3. Prefer using `count` method for automatic deletion after download + +```python +# Recommended configuration for public services +{ + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400 # Max retention 1 day +} +``` + +### Disable Upload Functionality + +In some cases, you may need to temporarily disable upload functionality: + +```python +{ + "openUpload": 0 # Disable upload functionality +} +``` + +## Reverse Proxy Security Configuration + +In production environments, Nginx or other reverse proxy servers are typically used. Here are security configuration recommendations: + +### Nginx Configuration Example + +```nginx +server { + listen 80; + server_name your-domain.com; + + # Force HTTPS redirect + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + # SSL certificate configuration + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers on; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Limit request body size (match uploadSize configuration) + client_max_body_size 100M; + + # Pass real IP + location / { + proxy_pass http://127.0.0.1:12345; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Static resource caching + location /assets { + proxy_pass http://127.0.0.1:12345; + proxy_cache_valid 200 7d; + add_header Cache-Control "public, max-age=604800"; + } +} +``` + +### Key Security Configuration Notes + +**1. Pass Real IP** + +FileCodeBox's IP limiting functionality depends on obtaining the client's real IP. The system obtains IP in the following order: +1. `X-Real-IP` request header +2. `X-Forwarded-For` request header +3. Direct client connection IP + +Ensure the reverse proxy correctly sets these headers: + +```nginx +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + +**2. Request Body Size Limit** + +Nginx's `client_max_body_size` should match or be slightly larger than FileCodeBox's `uploadSize` configuration: + +```nginx +client_max_body_size 100M; # Allow max 100MB uploads +``` + +**3. HTTPS Encryption** + +It's strongly recommended to enable HTTPS in production environments: +- Protect uploaded file content +- Protect admin login credentials +- Prevent man-in-the-middle attacks + +### Caddy Configuration Example + +```nginx +your-domain.com { + reverse_proxy localhost:12345 + + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Strict-Transport-Security "max-age=31536000; includeSubDomains" + } +} +``` + +## Security Checklist + +Before deploying FileCodeBox, confirm the following security configurations: + +- [ ] Changed default admin password `admin_token` +- [ ] Hidden admin entry `showAdminAddr: 0` +- [ ] Configured appropriate upload rate limiting +- [ ] Configured error rate limiting to prevent brute-force attacks +- [ ] Set reasonable file size limits +- [ ] Configured file expiration policy +- [ ] Enabled HTTPS encryption +- [ ] Reverse proxy correctly passes real IP +- [ ] Set security response headers + +## Recommended Security Configurations + +### Public Service Configuration + +```python +{ + "admin_token": "your-very-secure-password", + "showAdminAddr": 0, + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, + "uploadCount": 5, + "errorMinute": 5, + "errorCount": 3, + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # Max 1 day + "openUpload": 1 +} +``` + +### Internal Service Configuration + +```python +{ + "admin_token": "internal-secure-password", + "showAdminAddr": 1, + "uploadSize": 104857600, # 100MB + "uploadMinute": 5, + "uploadCount": 50, + "errorMinute": 1, + "errorCount": 5, + "expireStyle": ["day", "hour", "forever"], + "max_save_seconds": 0, # No limit + "openUpload": 1 +} +``` + +## Next Steps + +- [Configuration Guide](/en/guide/configuration) - Learn about all configuration options +- [Storage Configuration](/en/guide/storage) - Configure secure storage backends +- [File Sharing](/en/guide/share) - Learn about file sharing features diff --git a/docs/en/guide/share.md b/docs/en/guide/share.md new file mode 100644 index 000000000..0abb1a2f4 --- /dev/null +++ b/docs/en/guide/share.md @@ -0,0 +1,342 @@ +# File Sharing + +FileCodeBox provides simple and easy-to-use file and text sharing functionality. Users can securely share and retrieve files using extraction codes. + +## Sharing Methods + +FileCodeBox supports two sharing methods: + +1. **Text Sharing** - Share text content directly, suitable for code snippets, configuration files, etc. +2. **File Sharing** - Upload files for sharing, supports various file formats + +## Text Sharing + +### How to Use + +1. Select the "Text Share" tab on the homepage +2. Enter or paste the content to share in the text box +3. Select expiration method and time +4. Click the "Share" button +5. Get the extraction code + +### Text Size Limit + +::: warning Note +The maximum content size for text sharing is **222KB** (227,328 bytes). If content exceeds this limit, it's recommended to use file sharing instead. +::: + +Text content size is calculated using UTF-8 encoding. Chinese characters typically occupy 3 bytes. + +### API Endpoint + +**POST** `/share/text/` + +Request parameters: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `text` | string | Yes | Text content to share | +| `expire_value` | int | No | Expiration value, default 1 | +| `expire_style` | string | No | Expiration method, default `day` | + +Response example: + +```json +{ + "code": 200, + "detail": { + "code": "123456" + } +} +``` + +## File Sharing + +### How to Use + +1. Select the "File Share" tab on the homepage +2. Click the upload area or drag files to the upload area +3. Select expiration method and time +4. Click the "Upload" button +5. Get the extraction code + +### File Size Limit + +The default maximum single file upload size is **10MB**. Administrators can modify this limit via the `uploadSize` configuration. + +::: tip Tip +If you need to upload large files, contact the administrator to enable chunked upload functionality or adjust the `uploadSize` configuration. +::: + +### Supported Upload Methods + +- **Click Upload** - Click the upload area to select files +- **Drag Upload** - Drag files to the upload area +- **Paste Upload** - Paste images from clipboard (supported by some themes) + +### API Endpoint + +**POST** `/share/file/` + +Request parameters (multipart/form-data): + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | file | Yes | File to upload | +| `expire_value` | int | No | Expiration value, default 1 | +| `expire_style` | string | No | Expiration method, default `day` | + +Response example: + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + + +## Expiration Settings + +FileCodeBox supports multiple flexible expiration methods: + +| Expiration Method | Parameter Value | Description | +|-------------------|-----------------|-------------| +| By Days | `day` | File expires after specified days | +| By Hours | `hour` | File expires after specified hours | +| By Minutes | `minute` | File expires after specified minutes | +| Never Expire | `forever` | File is permanently valid | +| By Count | `count` | File expires after specified download count | + +::: info Note +- Administrators can control available expiration methods via the `expireStyle` configuration +- Administrators can limit maximum file retention time via the `max_save_seconds` configuration +::: + +### Expiration Method Examples + +```bash +# File expires after 3 days +expire_value=3, expire_style=day + +# File expires after 12 hours +expire_value=12, expire_style=hour + +# File expires after 30 minutes +expire_value=30, expire_style=minute + +# File never expires +expire_value=1, expire_style=forever + +# File expires after 5 downloads +expire_value=5, expire_style=count +``` + +## Retrieving Files + +### How to Use + +1. Enter the extraction code in the "Retrieve File" area on the homepage +2. Click the "Retrieve" button +3. System displays file information (filename, size, etc.) +4. Click the "Download" button to download the file, or view text content directly + +### Extraction Code Notes + +- Extraction codes are typically **6-digit numbers** +- Files that never expire use **alphanumeric** extraction codes +- Extraction codes are case-sensitive (for alphanumeric codes) + +### API Endpoints + +**Query File Information** + +**POST** `/share/select/` + +Request parameters: + +```json +{ + "code": "123456" +} +``` + +Response example (file): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "example.pdf", + "size": 1048576, + "text": "https://example.com/download/..." + } +} +``` + +Response example (text): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "Text", + "size": 1024, + "text": "This is the shared text content..." + } +} +``` + +**Direct File Download** + +**GET** `/share/select/?code=123456` + +This endpoint returns file content directly, suitable for direct browser access. + +## Chunked Upload (Large Files) + +For large file uploads, FileCodeBox supports chunked upload functionality. This feature requires administrator enablement (`enableChunk=1`). + +### Chunked Upload Flow + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + C->>S: 1. Initialize upload (POST /chunk/upload/init/) + S-->>C: Return upload_id and chunk info + + loop Each chunk + C->>S: 2. Upload chunk (POST /chunk/upload/chunk/{upload_id}/{chunk_index}) + S-->>C: Return chunk hash + end + + C->>S: 3. Complete upload (POST /chunk/upload/complete/{upload_id}) + S-->>C: Return extraction code +``` + +### 1. Initialize Upload + +**POST** `/chunk/upload/init/` + +Request parameters: + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "sha256_hash_of_file" +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file_name` | string | Yes | Filename | +| `file_size` | int | Yes | Total file size (bytes) | +| `chunk_size` | int | No | Chunk size, default 5MB | +| `file_hash` | string | Yes | SHA256 hash of the file | + +Response example: + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +### 2. Upload Chunk + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +- `upload_id` - Upload session ID returned during initialization +- `chunk_index` - Chunk index, starting from 0 + +Request body: Chunk file data (multipart/form-data) + +Response example: + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "sha256_hash_of_chunk" + } +} +``` + +### 3. Complete Upload + +**POST** `/chunk/upload/complete/{upload_id}` + +Request parameters: + +```json +{ + "expire_value": 1, + "expire_style": "day" +} +``` + +Response example: + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### Resume Upload + +Chunked upload supports resume functionality. If upload is interrupted: + +1. Call the initialization endpoint again with the same `file_hash` +2. Server returns `uploaded_chunks` list containing already uploaded chunk indices +3. Client only needs to upload chunks not in the list + +## Error Handling + +### Common Error Codes + +| Error Code | Description | Solution | +|------------|-------------|----------| +| 403 | File size exceeds limit | Reduce file size or contact administrator to adjust limit | +| 403 | Content too large | Text exceeds 222KB, use file sharing instead | +| 403 | Upload rate limit | Wait a while before retrying | +| 404 | File not found | Check if extraction code is correct | +| 404 | File expired | File has expired or download count exhausted | + +### Rate Limiting + +To prevent abuse, the system has rate limits on upload and retrieval operations: + +- **Upload limit**: Default max 10 uploads per minute +- **Error limit**: Default max 1 error attempt per minute + +::: tip Tip +If you encounter rate limiting, wait for the limit time window to pass before retrying. +::: + +## Next Steps + +- [Configuration Guide](/en/guide/configuration) - Learn how to configure sharing-related settings +- [Storage Configuration](/en/guide/storage) - Learn about file storage methods +- [Security Settings](/en/guide/security) - Learn about security-related configurations +- [Admin Panel](/en/guide/management) - Learn how to manage shared files diff --git a/docs/en/guide/storage.md b/docs/en/guide/storage.md new file mode 100644 index 000000000..6997e439a --- /dev/null +++ b/docs/en/guide/storage.md @@ -0,0 +1,395 @@ +# Storage Configuration + +FileCodeBox supports multiple storage backends. You can choose the appropriate storage method based on your needs. This document details the configuration methods for various storage backends. + +## Storage Types Overview + +| Storage Type | Config Value | Description | +|--------------|--------------|-------------| +| Local Storage | `local` | Default storage method, files saved on local server | +| S3-Compatible Storage | `s3` | Supports AWS S3, Aliyun OSS, MinIO, etc. | +| OneDrive | `onedrive` | Microsoft OneDrive cloud storage (work/school accounts only) | +| WebDAV | `webdav` | Storage services supporting WebDAV protocol | +| OpenDAL | `opendal` | Integrate more storage services via OpenDAL | + +## Local Storage + +Local storage is the default storage method. Files are saved in the server's `data/` directory. + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | `local` | Storage type | +| `storage_path` | string | `""` | Custom storage path (optional) | + +### Configuration Example + +```bash +file_storage=local +storage_path= +``` + +### Notes + +- Files are stored by default in `data/share/data/` directory +- Subdirectories are automatically created by date: `year/month/day/fileID/` +- In production, it's recommended to mount the `data/` directory to persistent storage + +## S3-Compatible Storage + +Supports all S3-compatible object storage services, including AWS S3, Aliyun OSS, MinIO, Tencent Cloud COS, etc. + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | - | Set to `s3` | +| `s3_access_key_id` | string | `""` | Access Key ID | +| `s3_secret_access_key` | string | `""` | Secret Access Key | +| `s3_bucket_name` | string | `""` | Bucket name | +| `s3_endpoint_url` | string | `""` | S3 endpoint URL | +| `s3_region_name` | string | `auto` | Region name | +| `s3_signature_version` | string | `s3v2` | Signature version (`s3v2` or `s3v4`) | +| `s3_hostname` | string | `""` | S3 hostname (alternative) | +| `s3_proxy` | int | `0` | Download through server proxy (1=yes, 0=no) | +| `aws_session_token` | string | `""` | AWS session token (optional) | + + +### AWS S3 Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=AKIAIOSFODNN7EXAMPLE +s3_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +s3_bucket_name=my-filecodebox-bucket +s3_endpoint_url=https://s3.amazonaws.com +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +### Aliyun OSS Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=YourAccessKeyId +s3_secret_access_key=YourSecretAccessKey +s3_bucket_name=bucket-name +s3_endpoint_url=https://bucket-name.oss-cn-hangzhou.aliyuncs.com +s3_region_name=oss-cn-hangzhou +s3_signature_version=s3v4 +``` + +::: tip Aliyun OSS Endpoint Format +Endpoint URL format: `https://..aliyuncs.com` + +Common regions: +- Hangzhou: `oss-cn-hangzhou` +- Shanghai: `oss-cn-shanghai` +- Beijing: `oss-cn-beijing` +- Shenzhen: `oss-cn-shenzhen` +::: + +### MinIO Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=minioadmin +s3_secret_access_key=minioadmin +s3_bucket_name=filecodebox +s3_endpoint_url=http://localhost:9000 +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +::: warning MinIO Notes +- `s3_endpoint_url` should be the MinIO API interface address +- `s3_region_name` should match the `Server Location` in MinIO configuration +- Ensure the bucket is created and has correct access permissions +::: + +### Tencent Cloud COS Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=YourSecretId +s3_secret_access_key=YourSecretKey +s3_bucket_name=bucket-name-1250000000 +s3_endpoint_url=https://cos.ap-guangzhou.myqcloud.com +s3_region_name=ap-guangzhou +s3_signature_version=s3v4 +``` + +### Proxy Download + +When `s3_proxy=1`, file downloads are proxied through the server instead of directly from S3. This is useful when: + +- S3 bucket doesn't allow public access +- Need to hide the actual storage address +- Network environment restricts direct S3 access + +## OneDrive Storage + +OneDrive storage supports saving files to Microsoft OneDrive cloud storage. + +::: warning Important Limitation +OneDrive storage **only supports work or school accounts** and requires admin permissions to authorize the API. Personal accounts cannot use this feature. +::: + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | - | Set to `onedrive` | +| `onedrive_domain` | string | `""` | Azure AD domain | +| `onedrive_client_id` | string | `""` | Application (client) ID | +| `onedrive_username` | string | `""` | Account email | +| `onedrive_password` | string | `""` | Account password | +| `onedrive_root_path` | string | `filebox_storage` | Storage root directory in OneDrive | +| `onedrive_proxy` | int | `0` | Download through server proxy | + +### Configuration Example + +```bash +file_storage=onedrive +onedrive_domain=contoso.onmicrosoft.com +onedrive_client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +onedrive_username=user@contoso.onmicrosoft.com +onedrive_password=your_password +onedrive_root_path=filebox_storage +``` + +### Azure App Registration Steps + +To use OneDrive storage, you need to register an application in the Azure portal: + +#### 1. Get Domain + +Log in to [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade), hover over your account in the top right corner, and the **Domain** shown in the popup is the `onedrive_domain` value. + +#### 2. Register Application + +1. Click **+ New registration** in the top left +2. Enter application name (e.g., FileCodeBox) +3. **Supported account types**: Select "Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts" +4. **Redirect URI**: Select `Web`, enter `http://localhost` +5. Click **Register** + +#### 3. Get Client ID + +After registration, find the **Application (client) ID** in the **Essentials** section on the app overview page. This is the `onedrive_client_id` value. + +#### 4. Configure Authentication + +1. Select **Authentication** in the left menu +2. Find **Allow public client flows**, select **Yes** +3. Click **Save** + +#### 5. Configure API Permissions + +1. Select **API permissions** in the left menu +2. Click **+ Add a permission** +3. Select **Microsoft Graph** → **Delegated permissions** +4. Check the following permissions: + - `openid` + - `Files.Read` + - `Files.Read.All` + - `Files.ReadWrite` + - `Files.ReadWrite.All` + - `User.Read` +5. Click **Add permissions** +6. Click **Grant admin consent for xxx** +7. After confirmation, permission status should show **Granted** + +### Install Dependencies + +Using OneDrive storage requires additional Python dependencies: + +```bash +pip install msal Office365-REST-Python-Client +``` + +### Verify Configuration + +You can use the following code to test if the configuration is correct: + +```python +import msal +from office365.graph_client import GraphClient + +domain = 'your_domain' +client_id = 'your_client_id' +username = 'your_username' +password = 'your_password' + +def acquire_token_pwd(): + authority_url = f'https://login.microsoftonline.com/{domain}' + app = msal.PublicClientApplication( + authority=authority_url, + client_id=client_id + ) + result = app.acquire_token_by_username_password( + username=username, + password=password, + scopes=['https://graph.microsoft.com/.default'] + ) + return result + +# Test connection +client = GraphClient(acquire_token_pwd) +me = client.me.get().execute_query() +print(f"Login successful: {me.user_principal_name}") +``` + + +## WebDAV Storage + +WebDAV storage supports saving files to any service that supports the WebDAV protocol, such as Nextcloud, ownCloud, Nutstore, etc. + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | - | Set to `webdav` | +| `webdav_url` | string | `""` | WebDAV server URL | +| `webdav_username` | string | `""` | WebDAV username | +| `webdav_password` | string | `""` | WebDAV password | +| `webdav_root_path` | string | `filebox_storage` | Storage root directory in WebDAV | +| `webdav_proxy` | int | `0` | Download through server proxy | + +### Configuration Example + +```bash +file_storage=webdav +webdav_url=https://dav.example.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_password +webdav_root_path=filebox_storage +``` + +### Nextcloud Configuration Example + +```bash +file_storage=webdav +webdav_url=https://your-nextcloud.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip Nextcloud App Password +It's recommended to create an app password in Nextcloud instead of using your main password: +1. Log in to Nextcloud +2. Go to **Settings** → **Security** +3. Create a new app password in **Devices & sessions** +::: + +### Nutstore Configuration Example + +```bash +file_storage=webdav +webdav_url=https://dav.jianguoyun.com/dav/ +webdav_username=your_email@example.com +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip Nutstore App Password +Nutstore requires an app password: +1. Log in to Nutstore web version +2. Go to **Account Info** → **Security Options** +3. Add an app password +::: + +## OpenDAL Storage + +OpenDAL is a unified data access layer that supports multiple storage services. Through OpenDAL, you can use Google Cloud Storage, Azure Blob Storage, and more. + +### Configuration Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `file_storage` | string | Set to `opendal` | +| `opendal_scheme` | string | Storage service type (e.g., `gcs`, `azblob`) | +| `opendal__` | string | Service-specific configuration parameters | + +### Install Dependencies + +```bash +pip install opendal +``` + +### Google Cloud Storage Configuration Example + +```bash +file_storage=opendal +opendal_scheme=gcs +opendal_gcs_root=/filecodebox +opendal_gcs_bucket=your-bucket-name +opendal_gcs_credential=base64_encoded_credential +``` + +### Azure Blob Storage Configuration Example + +```bash +file_storage=opendal +opendal_scheme=azblob +opendal_azblob_root=/filecodebox +opendal_azblob_container=your-container +opendal_azblob_account_name=your_account +opendal_azblob_account_key=your_key +``` + +### Supported Services + +OpenDAL supports numerous storage services. For the complete list, see the [OpenDAL Official Documentation](https://opendal.apache.org/docs/rust/opendal/services/index.html). + +Common services include: +- `gcs` - Google Cloud Storage +- `azblob` - Azure Blob Storage +- `obs` - Huawei Cloud OBS +- `oss` - Aliyun OSS (via OpenDAL) +- `cos` - Tencent Cloud COS (via OpenDAL) +- `hdfs` - Hadoop HDFS +- `ftp` - FTP server +- `sftp` - SFTP server + +::: warning OpenDAL Notes +1. Services integrated via OpenDAL download through server proxy, consuming both storage service and server bandwidth +2. Compared to native S3/OneDrive support, OpenDAL may lack some debugging information +3. OpenDAL is written in Rust with good performance +::: + +## Storage Selection Recommendations + +| Scenario | Recommended Storage | Reason | +|----------|---------------------|--------| +| Personal/Small deployment | Local storage | Simple and easy, no extra configuration needed | +| Enterprise intranet | MinIO + S3 | Self-hosted object storage, data control | +| Public cloud deployment | Corresponding cloud provider S3 | Fast same-region access, low cost | +| Existing OneDrive | OneDrive | Utilize existing resources | +| Existing WebDAV | WebDAV | Good compatibility | +| Special storage needs | OpenDAL | Supports more storage services | + +## Common Issues + +### S3 Upload Failure + +1. Check if Access Key and Secret Key are correct +2. Confirm bucket name and region configuration are correct +3. Check bucket access permission settings +4. Confirm signature version (`s3v2` or `s3v4`) matches service provider requirements + +### OneDrive Authentication Failure + +1. Confirm using a work/school account, not a personal account +2. Check if Azure app has been granted admin consent +3. Confirm API permissions are fully configured +4. Verify username and password are correct + +### WebDAV Connection Failure + +1. Check if WebDAV URL format is correct +2. Confirm username and password (or app password) are correct +3. Check if server supports WebDAV protocol +4. Confirm network connection is normal diff --git a/docs/en/guide/upload.md b/docs/en/guide/upload.md new file mode 100644 index 000000000..1dd9306c6 --- /dev/null +++ b/docs/en/guide/upload.md @@ -0,0 +1,421 @@ +# File Upload + +FileCodeBox provides multiple flexible file upload methods, supporting both regular upload and chunked upload to meet different scenario requirements. + +## Upload Methods + +FileCodeBox supports the following upload methods: + +### Drag and Drop Upload + +Drag files directly to the upload area to start uploading. This is the most convenient upload method. + +1. Open the FileCodeBox homepage +2. Drag files from your file manager to the upload area +3. Release the mouse, file upload begins +4. Get the extraction code after upload completes + +::: tip Tip +Drag and drop upload supports dragging multiple files simultaneously (depending on theme support). +::: + +### Click Upload + +Click the upload area to select files through the system file picker. + +1. Click the "Select File" button in the upload area +2. Select the file to upload in the popup file picker +3. File upload begins after confirming selection +4. Get the extraction code after upload completes + +### Paste Upload + +Supports pasting images directly from clipboard for upload (supported by some themes). + +1. Copy an image to clipboard (screenshot or copy image) +2. Use `Ctrl+V` (Windows/Linux) or `Cmd+V` (macOS) to paste in the upload area +3. Image upload starts automatically +4. Get the extraction code after upload completes + +::: warning Note +Paste upload only supports image formats, not other file types. Specific support depends on the theme being used. +::: + +## File Size Limits + +### Default Limits + +| Setting | Default | Description | +|---------|---------|-------------| +| `uploadSize` | 10MB | Maximum single file upload size | + +### Modify Upload Limits + +Administrators can modify upload size limits through the admin panel or configuration file: + +```python +# Set maximum upload size to 100MB +uploadSize = 104857600 # 100 * 1024 * 1024 +``` + +::: info Note +`uploadSize` is in bytes. Common conversions: +- 10MB = 10485760 +- 50MB = 52428800 +- 100MB = 104857600 +- 500MB = 524288000 +- 1GB = 1073741824 +::: + +### Exceeding Limit Handling + +When an uploaded file exceeds the size limit, the system returns a 403 error: + +```json +{ + "detail": "Size exceeds limit, maximum is 10.00 MB" +} +``` + +## Regular Upload API + +### File Upload Endpoint + +**POST** `/share/file/` + +Content-Type: `multipart/form-data` + +**Request parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | file | Yes | File to upload | +| `expire_value` | int | No | Expiration value, default 1 | +| `expire_style` | string | No | Expiration method, default `day` | + +**Expiration method options:** + +| Value | Description | +|-------|-------------| +| `day` | Expire by days | +| `hour` | Expire by hours | +| `minute` | Expire by minutes | +| `forever` | Never expire | +| `count` | Expire by download count | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + +**cURL example:** + +```bash +# Upload file (default 1 day expiration) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" + +# Upload file with 7 days expiration +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" \ + -F "expire_value=7" \ + -F "expire_style=day" + +# Upload file with 10 downloads limit +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" \ + -F "expire_value=10" \ + -F "expire_style=count" + +# Share text +curl -X POST "http://localhost:12345/share/text/" \ + -F "text=This is the text content to share" + +# Download file by extraction code +curl -L "http://localhost:12345/share/select/?code=YOUR_CODE" -o downloaded_file +``` + +::: tip When Authentication Required +If guest upload is disabled in admin panel (`openUpload=0`), you need to login first: + +```bash +# 1. Login to get token +curl -X POST "http://localhost:12345/admin/login" \ + -H "Content-Type: application/json" \ + -d '{"password": "FileCodeBox2023"}' + +# Returns: {"code":200,"msg":"success","detail":{"token":"xxx.xxx.xxx","token_type":"Bearer"}} + +# 2. Upload file with token +curl -X POST "http://localhost:12345/share/file/" \ + -H "Authorization: Bearer xxx.xxx.xxx" \ + -F "file=@/path/to/file.pdf" + +# 3. Share text with token +curl -X POST "http://localhost:12345/share/text/" \ + -H "Authorization: Bearer xxx.xxx.xxx" \ + -F "text=This is the text content to share" +``` +::: + + +## Chunked Upload API + +For large files, FileCodeBox supports chunked upload functionality. Chunked upload splits large files into multiple small chunks for separate uploading, supporting resume capability. + +::: warning Prerequisite +Chunked upload functionality requires administrator enablement: `enableChunk=1` +::: + +### Chunked Upload Flow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Initialize │ ──▶ │Upload Chunks│ ──▶ │ Complete │ +│ /init/ │ │ /chunk/ │ │ /complete/ │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌───────────┐ + │ Loop │ + │each chunk │ + └───────────┘ +``` + +### 1. Initialize Upload + +**POST** `/chunk/upload/init/` + +**Request parameters:** + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} +``` + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file_name` | string | Yes | - | Filename | +| `file_size` | int | Yes | - | Total file size (bytes) | +| `chunk_size` | int | No | 5MB | Chunk size (bytes) | +| `file_hash` | string | Yes | - | SHA256 hash of the file | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456789", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +| Field | Description | +|-------|-------------| +| `existed` | Whether file already exists (instant upload) | +| `upload_id` | Upload session ID | +| `chunk_size` | Chunk size | +| `total_chunks` | Total number of chunks | +| `uploaded_chunks` | List of already uploaded chunk indices | + +### 2. Upload Chunk + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +**Path parameters:** + +| Parameter | Description | +|-----------|-------------| +| `upload_id` | Upload session ID returned during initialization | +| `chunk_index` | Chunk index, starting from 0 | + +**Request body:** + +Content-Type: `multipart/form-data` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `chunk` | file | Chunk data | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "a1b2c3d4e5f6..." + } +} +``` + +**cURL example:** + +```bash +# Upload first chunk (index 0) +curl -X POST "http://localhost:12345/chunk/upload/chunk/abc123def456789/0" \ + -F "chunk=@/path/to/chunk_0" +``` + +### 3. Complete Upload + +**POST** `/chunk/upload/complete/{upload_id}` + +**Path parameters:** + +| Parameter | Description | +|-----------|-------------| +| `upload_id` | Upload session ID | + +**Request parameters:** + +```json +{ + "expire_value": 7, + "expire_style": "day" +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `expire_value` | int | Yes | Expiration value | +| `expire_style` | string | Yes | Expiration method | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### Resume Upload + +Chunked upload supports resume functionality. When upload is interrupted: + +1. Call the initialization endpoint again with the same `file_hash` +2. Server returns `uploaded_chunks` list containing already uploaded chunk indices +3. Client only needs to upload chunks not in the list +4. Call the complete endpoint after all chunks are uploaded + +**Example flow:** + +```javascript +// 1. Initialize upload +const initResponse = await fetch('/chunk/upload/init/', { + method: 'POST', + body: JSON.stringify({ + file_name: 'large_file.zip', + file_size: fileSize, + chunk_size: 5 * 1024 * 1024, + file_hash: fileHash + }) +}); +const { upload_id, uploaded_chunks, total_chunks } = await initResponse.json(); + +// 2. Upload incomplete chunks +for (let i = 0; i < total_chunks; i++) { + if (!uploaded_chunks.includes(i)) { + const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); + await fetch(`/chunk/upload/chunk/${upload_id}/${i}`, { + method: 'POST', + body: chunk + }); + } +} + +// 3. Complete upload +await fetch(`/chunk/upload/complete/${upload_id}`, { + method: 'POST', + body: JSON.stringify({ + expire_value: 7, + expire_style: 'day' + }) +}); +``` + +## Error Handling + +### Common Errors + +| HTTP Status | Error Message | Cause | Solution | +|-------------|---------------|-------|----------| +| 403 | Size exceeds limit | File exceeds `uploadSize` limit | Reduce file size or contact administrator to adjust limit | +| 403 | Upload rate limit | Exceeded IP upload rate limit | Wait for limit time window before retrying | +| 400 | Invalid expiration type | `expire_style` value not in allowed list | Use a valid expiration method | +| 404 | Upload session not found | `upload_id` invalid or expired | Re-initialize upload | +| 400 | Invalid chunk index | `chunk_index` out of range | Check if chunk index is correct | +| 400 | Incomplete chunks | Chunk count insufficient when completing upload | Ensure all chunks are uploaded | + +### Rate Limiting + +The system has rate limits on upload operations to prevent abuse: + +| Setting | Default | Description | +|---------|---------|-------------| +| `uploadMinute` | 1 | Limit time window (minutes) | +| `uploadCount` | 10 | Maximum uploads within time window | + +When rate limit is exceeded, you need to wait for the time window to pass before continuing uploads. + +### Error Response Format + +```json +{ + "detail": "Error message description" +} +``` + +## Upload Configuration + +### Related Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `openUpload` | int | 1 | Enable upload (1=enabled, 0=disabled) | +| `uploadSize` | int | 10485760 | Maximum upload size (bytes) | +| `enableChunk` | int | 0 | Enable chunked upload (1=enabled, 0=disabled) | +| `uploadMinute` | int | 1 | Upload rate limit time window (minutes) | +| `uploadCount` | int | 10 | Maximum uploads within time window | +| `expireStyle` | list | ["day","hour","minute","forever","count"] | Allowed expiration methods | + +### Configuration Example + +```python +# Allow 100MB file uploads, enable chunked upload +uploadSize = 104857600 +enableChunk = 1 + +# Relax upload rate limit: max 50 uploads per 5 minutes +uploadMinute = 5 +uploadCount = 50 + +# Only allow expiration by days and count +expireStyle = ["day", "count"] +``` + +## Next Steps + +- [File Sharing](/en/guide/share) - Learn the complete sharing process +- [Configuration Guide](/en/guide/configuration) - Learn about all configuration options +- [Storage Configuration](/en/guide/storage) - Learn about file storage methods +- [Security Settings](/en/guide/security) - Learn about security-related configurations diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 000000000..7bd4a38d7 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,41 @@ +--- +layout: home + +hero: + name: "FileCodeBox" + text: "File Express Box" + tagline: Share text and files anonymously with access codes, just like picking up a package + image: + src: /logo_small.png + alt: FileCodeBox + actions: + - theme: brand + text: Get Started + link: /en/guide/getting-started + - theme: alt + text: Live Demo + link: https://share.lanol.cn + - theme: alt + text: View on GitHub + link: https://github.com/vastsa/FileCodeBox + +features: + - icon: 🚀 + title: Quick Deployment + details: Supports one-click Docker deployment, simple and fast, no complex configuration needed + - icon: 🔒 + title: Secure & Reliable + details: File access requires an access code, supports expiration time and download limit settings + - icon: 💻 + title: Clean Interface + details: Clean user interface with drag-and-drop upload support for excellent user experience + - icon: 🛠️ + title: Feature Rich + details: Supports file preview, online playback, image processing, and many other features + - icon: 📦 + title: Storage Extensions + details: Supports various storage methods including local storage and object storage + - icon: 🔌 + title: API Support + details: Provides complete REST API for easy integration with other systems +--- \ No newline at end of file diff --git a/docs/en/showcase.md b/docs/en/showcase.md new file mode 100644 index 000000000..01e55f86f --- /dev/null +++ b/docs/en/showcase.md @@ -0,0 +1,127 @@ +# Showcase + +Here are some excellent sites built with FileCodeBox. If you've deployed FileCodeBox, feel free to submit a PR to add your site here! + +## Official Demo + +
+ +
+ +### 🌟 FileCodeBox Demo + +- **URL**: [share.lanol.cn](https://share.lanol.cn) +- **Description**: Official demo site with the latest features +- **Highlights**: Stable, full-featured + +
+ +
+ +## Community Sites + +::: tip Submit Your Site +If you've built your own file sharing service using FileCodeBox, you can submit it through: + +1. Submit a PR on [GitHub](https://github.com/vastsa/FileCodeBox) to edit this page +2. Open an [Issue](https://github.com/vastsa/FileCodeBox/issues) with your site info +3. Join QQ Group 739673698 to contact the admin +::: + + + +
+ +
+ +### QuWenJian + +- **URL**: [www.quwenjian.cn/fby.html](https://www.quwenjian.cn/fby.html) +- **Description**: QuWenJian - Unlimited storage, portable and limitless +- **Highlights**: Free file transfer station +- **Operator**: QuWenJian + +
+ +
+ +### Pandora Box + +- **URL**: [pan.duo.la](https://pan.duo.la) +- **Description**: Pandora Box +- **Highlights**: Classic v1.6 version +- **Operator**: WuXingQueXinYan + +
+ +
+ +## Submission Requirements + +To ensure quality, please make sure your site meets the following criteria: + +1. **Stable**: Your site should be stable and accessible +2. **Legal**: Content must be legal and compliant +3. **Attribution**: We recommend keeping FileCodeBox attribution +4. **HTTPS**: We recommend enabling HTTPS + +## Submission Template + +If you want to submit your site, please use the following format: + +```markdown +### Site Name + +- **URL**: [domain](https://domain) +- **Description**: One-line description of your site +- **Highlights**: Special features or highlights +- **Operator**: Optional, your name or organization +``` + + diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md new file mode 100644 index 000000000..f32cd5114 --- /dev/null +++ b/docs/guide/configuration.md @@ -0,0 +1,206 @@ +# 配置说明 + +FileCodeBox 提供了丰富的配置选项,可以通过管理面板或直接修改配置来自定义系统行为。本文档详细介绍所有可用的配置项。 + +## 配置方式 + +FileCodeBox 支持两种配置方式: + +1. **管理面板配置**(推荐):访问 `/admin` 进入管理面板,在设置页面修改配置 +2. **数据库配置**:配置存储在 `data/filecodebox.db` 数据库中 + +::: tip 提示 +首次启动时,系统会使用 `core/settings.py` 中的默认配置。修改后的配置会保存到数据库中。 +::: + +## 基础设置 + +### 站点信息 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `name` | string | `文件快递柜 - FileCodeBox` | 站点名称,显示在页面标题和导航栏 | +| `description` | string | `开箱即用的文件快传系统` | 站点描述,用于 SEO | +| `keywords` | string | `FileCodeBox, 文件快递柜...` | 站点关键词,用于 SEO | +| `port` | int | `12345` | 服务监听端口 | + +### 通知设置 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `notify_title` | string | `系统通知` | 通知标题 | +| `notify_content` | string | 欢迎信息 | 通知内容,支持 HTML | +| `page_explain` | string | 法律声明 | 页面底部说明文字 | +| `robotsText` | string | `User-agent: *\nDisallow: /` | robots.txt 内容 | + +## 上传设置 + +### 文件上传限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `openUpload` | int | `1` | 是否开启上传功能(1=开启,0=关闭) | +| `uploadSize` | int | `10485760` | 单文件最大上传大小(字节),默认 10MB | +| `enableChunk` | int | `0` | 是否启用分片上传(1=启用,0=禁用) | + +::: warning 注意 +`uploadSize` 的单位是字节。10MB = 10 * 1024 * 1024 = 10485760 字节 +::: + +### 上传频率限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `uploadMinute` | int | `1` | 上传限制的时间窗口(分钟) | +| `uploadCount` | int | `10` | 在时间窗口内允许的最大上传次数 | + +例如:默认配置表示每 1 分钟内最多允许上传 10 次。 + +### 文件过期设置 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | 可选的过期方式 | +| `max_save_seconds` | int | `0` | 文件最大保存时间(秒),0 表示不限制 | + +过期方式说明: +- `day` - 按天过期 +- `hour` - 按小时过期 +- `minute` - 按分钟过期 +- `forever` - 永不过期 +- `count` - 按下载次数过期 + +## 主题设置 + +### 主题选择 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `themesSelect` | string | `themes/2024` | 当前使用的主题 | +| `themesChoices` | list | 见下方 | 可用主题列表 | + +默认可用主题: +```json +[ + { + "name": "2023", + "key": "themes/2023", + "author": "Lan", + "version": "1.0" + }, + { + "name": "2024", + "key": "themes/2024", + "author": "Lan", + "version": "1.0" + } +] +``` + +### 界面样式 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `opacity` | float | `0.9` | 界面透明度(0-1) | +| `background` | string | `""` | 自定义背景图片 URL,为空则使用默认背景 | + +## 管理员设置 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `admin_token` | string | `FileCodeBox2023` | 管理员登录密码 | +| `showAdminAddr` | int | `0` | 是否在首页显示管理入口(1=显示,0=隐藏) | + +::: danger 安全警告 +请务必在生产环境中修改默认的 `admin_token`!使用默认密码会导致严重的安全风险。 +::: + +## 安全设置 + +### 错误次数限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `errorMinute` | int | `1` | 错误限制的时间窗口(分钟) | +| `errorCount` | int | `1` | 在时间窗口内允许的最大错误次数 | + +此设置用于防止暴力破解提取码。 + +## 存储设置 + +### 存储类型 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `file_storage` | string | `local` | 存储后端类型 | +| `storage_path` | string | `""` | 自定义存储路径 | + +支持的存储类型: +- `local` - 本地存储 +- `s3` - S3 兼容存储(AWS S3、阿里云 OSS、MinIO 等) +- `onedrive` - OneDrive 存储 +- `webdav` - WebDAV 存储 +- `opendal` - OpenDAL 存储 + +详细的存储配置请参考 [存储配置](/guide/storage)。 + +## 配置示例 + +### 示例 1:小型个人使用 + +适合个人或小团队使用,限制较宽松: + +```python +{ + "name": "我的文件分享", + "uploadSize": 52428800, # 50MB + "uploadMinute": 5, # 5分钟 + "uploadCount": 20, # 最多20次 + "expireStyle": ["day", "hour", "forever"], + "admin_token": "your-secure-password", + "showAdminAddr": 1 +} +``` + +### 示例 2:公开服务 + +适合公开服务,需要更严格的限制: + +```python +{ + "name": "公共文件快递柜", + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, # 1分钟 + "uploadCount": 5, # 最多5次 + "errorMinute": 5, # 5分钟 + "errorCount": 3, # 最多3次错误 + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # 最长保存1天 + "admin_token": "very-secure-password-123", + "showAdminAddr": 0 +} +``` + +### 示例 3:企业内部使用 + +适合企业内部使用,支持大文件和分片上传: + +```python +{ + "name": "企业文件中转站", + "uploadSize": 1073741824, # 1GB + "enableChunk": 1, # 启用分片上传 + "uploadMinute": 10, # 10分钟 + "uploadCount": 100, # 最多100次 + "expireStyle": ["day", "forever"], + "file_storage": "s3", # 使用S3存储 + "admin_token": "enterprise-secure-token", + "showAdminAddr": 1 +} +``` + +## 下一步 + +- [存储配置](/guide/storage) - 了解如何配置不同的存储后端 +- [安全设置](/guide/security) - 了解如何增强系统安全性 +- [文件分享](/guide/share) - 了解文件分享功能 diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 000000000..b09d0698c --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,121 @@ +# 快速开始 + +## 简介 + +FileCodeBox 是一个简单高效的文件分享工具,支持文件临时中转、分享和管理。本指南将帮助您快速部署和使用 FileCodeBox。 + +## 特性 + +- 🚀 快速部署:支持 Docker 一键部署 +- 🔒 安全可靠:文件访问需要提取码 +- ⏱️ 时效控制:支持设置文件有效期 +- 📊 下载限制:可限制文件下载次数 +- 🖼️ 文件预览:支持图片、视频、音频等多种格式预览 +- 📱 响应式设计:完美适配移动端和桌面端 + +## 部署方式 + +### Docker 部署(推荐) + +#### 快速启动 + +```bash +docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest +``` + +#### Docker Compose + +```yml +version: "3" +services: + file-code-box: + image: lanol/filecodebox:latest + volumes: + - fcb-data:/app/data:rw + restart: unless-stopped + ports: + - "12345:12345" + environment: + - WORKERS=4 + - LOG_LEVEL=info +volumes: + fcb-data: + external: false +``` + +#### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `HOST` | `::` | 监听地址,支持 IPv4/IPv6 双栈 | +| `PORT` | `12345` | 服务端口 | +| `WORKERS` | `4` | 工作进程数,建议设置为 CPU 核心数 | +| `LOG_LEVEL` | `info` | 日志级别:debug/info/warning/error | + +#### 自定义配置示例 + +```bash +docker run -d --restart=always \ + -p 12345:12345 \ + -v /opt/FileCodeBox/:/app/data \ + -e WORKERS=8 \ + -e LOG_LEVEL=warning \ + --name filecodebox \ + lanol/filecodebox:latest +``` + +### 配置反向代理(Nginx) + +```nginx +location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://localhost:12345; +} +``` + +### 手动部署 + +1. 克隆项目 +```bash +git clone https://github.com/vastsa/FileCodeBox.git +``` + +2. 安装依赖 +```bash +cd FileCodeBox +pip install -r requirements.txt +``` + +3. 启动服务 +```bash +python main.py +``` + + +## 使用方法 + +1. 访问系统 + 打开浏览器访问 `http://localhost:12345` + +2. 上传文件 + - 点击上传按钮或拖拽文件到上传区域 + - 设置文件有效期和下载次数限制 + - 获取分享链接和提取码 + +3. 下载文件 + - 访问分享链接 + - 输入提取码 + - 下载文件 + +4. 后台管理 + - 访问 `http://localhost:12345/#/admin` + - 输入管理员密码:`FileCodeBox2023` + - 进入后台管理页面 + - 查看系统信息、文件列表、用户管理等 + +## 下一步 + +- [存储配置](/guide/storage) - 了解如何配置不同的存储方式 +- [安全设置](/guide/security) - 了解如何增强系统安全性 +- [API 文档](/api/) - 了解如何通过 API 集成 \ No newline at end of file diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 000000000..4005ddb3c --- /dev/null +++ b/docs/guide/introduction.md @@ -0,0 +1,235 @@ +
+ +FileCodeBox Logo + +

匿名口令分享文本和文件,像拿快递一样取文件

+ +[![GitHub stars](https://img.shields.io/github/stars/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/network) +[![GitHub issues](https://img.shields.io/github/issues/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/issues) +[![GitHub license](https://img.shields.io/github/license/vastsa/FileCodeBox)](https://github.com/vastsa/FileCodeBox/blob/master/LICENSE) +[![QQ Group](https://img.shields.io/badge/QQ%20Group-739673698-blue.svg)](https://qm.qq.com/q/PemPzhdEIM) +[![Python Version](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://www.python.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.68+-green.svg)](https://fastapi.tiangolo.com) +[![Vue Version](https://img.shields.io/badge/Vue.js-3.x-brightgreen.svg)](https://v3.vuejs.org) +
+ + +## 🚀 更新计划 +- [ ] 切片上传,同文件秒传,断点续传 +- [ ] 用户登录重构 +- [x] webdav存储 +- [x] 存储支持自定义路径 +- [x] s3优化,不修改昵称为uuid,新建目录 + +## 📝 项目简介 + +FileCodeBox 是一个基于 FastAPI + Vue3 开发的轻量级文件分享工具。它允许用户通过简单的方式分享文本和文件,接收者只需要一个提取码就可以取得文件,就像从快递柜取出快递一样简单。 + +## 🎯 应用场景 + + + + + + + + + + + + +
+

📁 临时文件分享

+快速分享单个文件,无需注册登录 +
+

📝 文本快速分享

+分享代码片段、文本内容等 +
+

🕶️ 匿名文件传输

+保护隐私的文件传输方式 +
+

💾 临时文件存储

+支持设置过期时间的文件存储 +
+

🔄 跨平台传输

+在不同设备间快速传输文件 +
+

🌐 小型分享服务

+搭建私有的文件分享服务 +
+ +## ✨ 核心特性 + + + + + + + + + + + + + + + + + + + + + + +
+

🚀 轻量简洁

+基于 FastAPI + SQLite3 + Vue3 + ElementUI,部署简单,性能出色 +
+

📤 便捷上传

+支持复制粘贴、拖拽上传,操作简单直观 +
+

📦 多种类型

+支持文本和各类文件的分享 +
+

🔒 安全机制

+ +- IP 限制上传次数 +- 错误次数限制 +- 文件过期机制 +
+

🎫 提取码分享

+随机提取码,可自定义次数及有效期 +
+

🌍 多语言支持

+支持中文简体、繁体及英文 +
+

🎭 匿名分享

+无需注册登录,保护隐私 +
+

🛠 管理面板

+文件管理和系统配置 +
+

🐳 容器部署

+支持 Docker 一键部署 +
+

💾 存储扩展

+支持本地存储、S3 协议、OneDrive 等 +
+

📱 响应式设计

+支持移动端访问 +
+

💻 终端支持

+支持命令行下载 +
+ +## 🚀 快速开始 + +### Docker 部署 + +```bash +docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:beta +``` + +### 手动部署 + +1. 克隆项目 +```bash +git clone https://github.com/vastsa/FileCodeBox.git +``` + +2. 安装依赖 +```bash +cd FileCodeBox +pip install -r requirements.txt +``` + +3. 启动服务 +```bash +python main.py +``` + +## 📖 使用说明 + +### 分享文件 +1. 打开网页,点击"分享文件" +2. 选择或拖拽文件 +3. 设置过期时间和次数 +4. 获取提取码 + +### 获取文件 +1. 打开网页,输入提取码 +2. 点击获取 +3. 下载文件或查看文本 + +### 管理面板 +1. 访问 `/admin` +2. 输入管理员密码 +3. 管理文件和配置 + +## 🛠 开发指南 + +### 项目结构 +``` +FileCodeBox/ +├── apps/ # 应用代码 +│ ├── admin/ # 管理后台 +│ └── base/ # 基础功能 +├── core/ # 核心功能 +├── data/ # 数据目录 +└── fcb-fronted/ # 前端代码 +``` + +### 开发环境 +- Python 3.8+ +- Node.js 14+ +- Vue 3 +- FastAPI + +### 本地开发 +1. 后端开发 +```bash +python main.py +``` + +2. 前端开发 +```bash +cd fcb-fronted +npm install +npm run dev +``` + +## 🤝 贡献指南 + +1. Fork 本项目 +2. 创建新分支 `git checkout -b feature/xxx` +3. 提交更改 `git commit -m 'Add xxx'` +4. 推送到分支 `git push origin feature/xxx` +5. 提交 Pull Request + +## ❓ 常见问题 + +### Q: 如何修改上传大小限制? +A: 在管理面板中修改配置项 `uploadSize` + +### Q: 如何配置存储引擎? +A: 在管理面板中选择存储引擎并配置相应参数 + +### Q: 如何备份数据? +A: 备份 `data` 目录即可 + +更多问题请访问 [Wiki](https://github.com/vastsa/FileCodeBox/wiki/常见问题) + +## 📊 项目统计 + +
+Featured|HelloGitHub + +![Repobeats](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg) + +[![Star History](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](https://star-history.com/#vastsa/FileCodeBox&Date) +
+ +## 📜 免责声明 + +本项目开源仅供学习使用,不得用于任何违法用途,否则后果自负,与作者无关。使用时请保留项目地址和版权信息。 diff --git a/docs/guide/management.md b/docs/guide/management.md new file mode 100644 index 000000000..933fa4cbe --- /dev/null +++ b/docs/guide/management.md @@ -0,0 +1,412 @@ +# 管理面板 + +FileCodeBox 提供了功能完善的管理面板,让管理员可以方便地管理文件、查看系统状态和修改配置。本文档介绍管理面板的各项功能和使用方法。 + +## 访问管理面板 + +### 登录方式 + +管理面板位于 `/admin` 路径。访问方式: + +1. 在浏览器中访问 `http://your-domain.com/admin` +2. 输入管理员密码(`admin_token` 配置项的值) +3. 点击登录按钮 + +::: tip 提示 +默认管理员密码是 `FileCodeBox2023`。请务必在生产环境中修改此密码,详见 [安全设置](/guide/security)。 +::: + +### 显示管理入口 + +默认情况下,首页不显示管理面板入口。您可以通过配置控制是否显示: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `showAdminAddr` | int | `0` | 是否在首页显示管理入口(1=显示,0=隐藏) | + +::: warning 安全建议 +在公开服务中,建议保持 `showAdminAddr` 为 `0`,通过直接访问 `/admin` 路径进入管理面板,减少被恶意扫描的风险。 +::: + +### 认证机制 + +管理面板使用 JWT(JSON Web Token)进行身份认证: + +1. 登录成功后,服务器返回一个包含管理员身份的 Token +2. 后续请求通过 `Authorization: Bearer ` 头部携带 Token +3. Token 用于验证管理员身份,确保只有授权用户可以访问管理功能 + +## 仪表盘 + +登录后首先看到的是仪表盘页面,展示系统的整体运行状态。 + +### 统计指标 + +仪表盘显示以下关键指标: + +| 指标 | 说明 | +|------|------| +| **文件总数** (`totalFiles`) | 系统中存储的文件总数量 | +| **存储使用量** (`storageUsed`) | 所有文件占用的总存储空间(字节) | +| **系统运行时间** (`sysUptime`) | 系统首次启动的时间 | +| **昨日上传数** (`yesterdayCount`) | 昨天一整天上传的文件数量 | +| **昨日上传量** (`yesterdaySize`) | 昨天上传文件的总大小(字节) | +| **今日上传数** (`todayCount`) | 今天到目前为止上传的文件数量 | +| **今日上传量** (`todaySize`) | 今天上传文件的总大小(字节) | + +### 指标说明 + +- **文件总数**:包括所有未过期的文件和文本分享 +- **存储使用量**:显示实际文件占用的存储空间,不包括数据库等系统文件 +- **昨日/今日统计**:基于文件创建时间计算,用于了解系统使用趋势 + +::: tip 提示 +存储使用量显示的是字节数。例如 `10485760` 表示约 10MB。 +::: + +## 文件管理 + +### 文件列表 + +文件管理页面展示系统中所有已分享的文件,支持分页浏览和搜索。 + +**列表信息包括:** +- 文件 ID +- 提取码(code) +- 文件名前缀(prefix) +- 文件后缀(suffix) +- 文件大小 +- 创建时间 +- 过期时间 +- 剩余下载次数 + +### 搜索文件 + +使用搜索功能可以快速找到特定文件: + +1. 在搜索框中输入关键词 +2. 系统会根据文件名前缀(prefix)进行模糊匹配 +3. 搜索结果实时更新 + +**搜索示例:** +- 输入 `report` 可以找到所有文件名包含 "report" 的文件 +- 输入 `.pdf` 可以找到所有 PDF 文件(如果文件名包含此字符串) + +### 分页浏览 + +文件列表支持分页显示: + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `page` | `1` | 当前页码 | +| `size` | `10` | 每页显示数量 | + +### 删除文件 + +管理员可以删除任意文件: + +1. 在文件列表中找到要删除的文件 +2. 点击删除按钮 +3. 确认删除操作 + +::: danger 警告 +删除操作不可恢复!文件将从存储后端永久删除,同时删除数据库中的记录。 +::: + +**删除流程:** +1. 系统首先从存储后端(本地/S3/OneDrive 等)删除实际文件 +2. 然后从数据库中删除文件记录 +3. 删除后,对应的提取码将失效 + +### 下载文件 + +管理员可以直接下载任意文件: + +1. 在文件列表中找到目标文件 +2. 点击下载按钮 +3. 文件将通过浏览器下载 + +对于文本分享,系统会直接返回文本内容而不是下载文件。 + +### 修改文件信息 + +管理员可以修改已分享文件的部分信息: + +| 可修改字段 | 说明 | +|------------|------| +| `code` | 提取码(必须唯一,不能与其他文件重复) | +| `prefix` | 文件名前缀 | +| `suffix` | 文件后缀名 | +| `expired_at` | 过期时间 | +| `expired_count` | 剩余下载次数 | + +**修改提取码:** +``` +原提取码:abc123 +新提取码:myfile2024 +``` + +::: warning 注意 +修改提取码时,系统会检查新提取码是否已被使用。如果已存在相同的提取码,修改将失败。 +::: + +## 本地文件管理 + +除了管理已分享的文件,管理面板还提供了本地文件管理功能,用于管理 `data/local` 目录中的文件。 + +### 查看本地文件 + +本地文件列表显示 `data/local` 目录中的所有文件: + +| 信息 | 说明 | +|------|------| +| 文件名 | 文件的完整名称 | +| 创建时间 | 文件的创建时间 | +| 文件大小 | 文件大小(字节) | + +### 分享本地文件 + +可以将本地文件快速分享: + +1. 在本地文件列表中选择要分享的文件 +2. 设置过期方式和过期值 +3. 点击分享按钮 +4. 系统生成提取码 + +**分享参数:** + +| 参数 | 说明 | +|------|------| +| `filename` | 要分享的文件名 | +| `expire_style` | 过期方式(day/hour/minute/forever/count) | +| `expire_value` | 过期值(天数/小时数/分钟数/下载次数) | + +### 删除本地文件 + +可以删除 `data/local` 目录中的文件: + +1. 在本地文件列表中找到要删除的文件 +2. 点击删除按钮 +3. 确认删除 + +::: tip 使用场景 +本地文件管理功能适用于: +- 批量上传文件到服务器后进行分享 +- 管理通过其他方式上传到服务器的文件 +- 清理不需要的本地文件 +::: + +## 系统设置 + +### 查看配置 + +在系统设置页面可以查看当前所有配置项的值。配置项按类别分组显示: + +- 基础设置(站点名称、描述等) +- 上传设置(文件大小限制、频率限制等) +- 存储设置(存储类型、路径等) +- 主题设置(主题选择、透明度等) +- 安全设置(管理员密码、错误限制等) + +### 修改配置 + +管理员可以通过管理面板修改大部分配置: + +1. 进入系统设置页面 +2. 找到要修改的配置项 +3. 输入新的值 +4. 点击保存按钮 + +**可修改的配置项:** + +| 类别 | 配置项示例 | +|------|------------| +| 基础设置 | `name`, `description`, `keywords`, `notify_title`, `notify_content` | +| 上传设置 | `uploadSize`, `uploadMinute`, `uploadCount`, `openUpload`, `enableChunk` | +| 过期设置 | `expireStyle`, `max_save_seconds` | +| 主题设置 | `themesSelect`, `opacity`, `background` | +| 安全设置 | `admin_token`, `showAdminAddr`, `errorMinute`, `errorCount` | +| 存储设置 | `file_storage`, `storage_path` 及各存储后端的配置 | + +::: warning 注意 +- `admin_token`(管理员密码)不能设置为空 +- `themesChoices`(主题列表)不可通过管理面板修改 +- 修改存储设置后,已有文件不会自动迁移 +::: + +### 配置生效 + +配置修改后立即生效,无需重启服务。配置保存在数据库中,重启后仍然有效。 + +**配置存储位置:** +- 数据库:`data/filecodebox.db` +- 表名:`keyvalue` +- 键名:`settings` + +## API 接口 + +管理面板的所有功能都通过 REST API 实现,以下是主要接口: + +### 认证接口 + +**登录** +``` +POST /admin/login +Content-Type: application/json + +{ + "password": "your-admin-password" +} +``` + +响应: +```json +{ + "code": 200, + "detail": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "Bearer" + } +} +``` + +### 仪表盘接口 + +**获取统计数据** +``` +GET /admin/dashboard +Authorization: Bearer +``` + +### 文件管理接口 + +**获取文件列表** +``` +GET /admin/file/list?page=1&size=10&keyword= +Authorization: Bearer +``` + +**删除文件** +``` +DELETE /admin/file/delete +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123 +} +``` + +**下载文件** +``` +GET /admin/file/download?id=123 +Authorization: Bearer +``` + +**修改文件信息** +``` +PATCH /admin/file/update +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123, + "code": "newcode", + "expired_at": "2024-12-31T23:59:59" +} +``` + +### 本地文件接口 + +**获取本地文件列表** +``` +GET /admin/local/lists +Authorization: Bearer +``` + +**删除本地文件** +``` +DELETE /admin/local/delete +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt" +} +``` + +**分享本地文件** +``` +POST /admin/local/share +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt", + "expire_style": "day", + "expire_value": 7 +} +``` + +### 配置接口 + +**获取配置** +``` +GET /admin/config/get +Authorization: Bearer +``` + +**更新配置** +``` +PATCH /admin/config/update +Authorization: Bearer +Content-Type: application/json + +{ + "admin_token": "new-password", + "uploadSize": 52428800 +} +``` + +## 常见问题 + +### 忘记管理员密码 + +如果忘记了管理员密码,可以通过以下方式重置: + +1. 停止 FileCodeBox 服务 +2. 使用 SQLite 工具打开 `data/filecodebox.db` +3. 查询 `keyvalue` 表中 `key='settings'` 的记录 +4. 修改 JSON 中的 `admin_token` 值 +5. 重启服务 + +```sql +-- 查看当前配置 +SELECT * FROM keyvalue WHERE key = 'settings'; + +-- 或者删除配置,恢复默认密码 +DELETE FROM keyvalue WHERE key = 'settings'; +``` + +### 文件删除失败 + +如果删除文件时出现错误,可能的原因: + +1. **存储后端连接失败**:检查存储配置是否正确 +2. **文件已不存在**:文件可能已被手动删除 +3. **权限不足**:检查存储目录的写入权限 + +### 配置修改不生效 + +如果修改配置后没有生效: + +1. 检查是否点击了保存按钮 +2. 刷新页面查看配置是否已保存 +3. 检查浏览器控制台是否有错误信息 +4. 确认配置值的格式是否正确(如数字类型不要输入字符串) + +## 下一步 + +- [配置说明](/guide/configuration) - 了解所有配置选项的详细说明 +- [安全设置](/guide/security) - 了解如何增强系统安全性 +- [存储配置](/guide/storage) - 配置不同的存储后端 diff --git a/docs/guide/security.md b/docs/guide/security.md new file mode 100644 index 000000000..d12c23f42 --- /dev/null +++ b/docs/guide/security.md @@ -0,0 +1,324 @@ +# 安全设置 + +FileCodeBox 提供了多层安全机制来保护您的文件分享服务。本文档介绍如何正确配置安全选项,确保系统安全运行。 + +## 管理员密码 + +### 修改默认密码 + +::: danger 重要安全警告 +FileCodeBox 的默认管理员密码是 `FileCodeBox2023`。**在生产环境中必须立即修改此密码!**使用默认密码会导致任何人都可以访问您的管理面板。 +::: + +修改管理员密码有两种方式: + +**方式一:通过管理面板修改(推荐)** + +1. 访问 `/admin` 进入管理面板 +2. 使用当前密码登录 +3. 进入「系统设置」页面 +4. 找到 `admin_token` 配置项 +5. 输入新的安全密码并保存 + +**方式二:通过数据库修改** + +配置存储在 `data/filecodebox.db` 数据库的 `keyvalue` 表中,可以直接修改 `admin_token` 的值。 + +### 密码安全建议 + +- 使用至少 16 个字符的强密码 +- 包含大小写字母、数字和特殊字符 +- 避免使用常见词汇或个人信息 +- 定期更换密码 + +```python +# 推荐的密码格式示例 +"admin_token": "Xk9#mP2$vL5@nQ8&wR3" +``` + +### 隐藏管理入口 + +默认情况下,管理面板入口是隐藏的。您可以通过 `showAdminAddr` 配置控制是否在首页显示管理入口: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `showAdminAddr` | int | `0` | 是否显示管理入口(1=显示,0=隐藏) | + +::: tip 建议 +在公开服务中,建议保持 `showAdminAddr` 为 `0`,通过直接访问 `/admin` 路径进入管理面板。 +::: + +## IP 速率限制 + +FileCodeBox 内置了基于 IP 的速率限制机制,可以有效防止滥用和攻击。 + +### 上传频率限制 + +限制单个 IP 在指定时间内的上传次数: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `uploadMinute` | int | `1` | 上传限制的时间窗口(分钟) | +| `uploadCount` | int | `10` | 在时间窗口内允许的最大上传次数 | + +**工作原理:** +- 系统记录每个 IP 的上传请求 +- 当某 IP 在 `uploadMinute` 分钟内的上传次数达到 `uploadCount` 时 +- 该 IP 的后续上传请求将被拒绝,返回 HTTP 423 错误 +- 等待时间窗口过期后,计数器重置 + +**配置示例:** + +```python +# 宽松配置:5分钟内最多上传20次 +{ + "uploadMinute": 5, + "uploadCount": 20 +} + +# 严格配置:1分钟内最多上传3次 +{ + "uploadMinute": 1, + "uploadCount": 3 +} +``` + +### 错误次数限制 + +限制单个 IP 的错误尝试次数,防止暴力破解提取码: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `errorMinute` | int | `1` | 错误限制的时间窗口(分钟) | +| `errorCount` | int | `1` | 在时间窗口内允许的最大错误次数 | + +**工作原理:** +- 当用户输入错误的提取码时,系统记录该 IP 的错误次数 +- 当错误次数达到 `errorCount` 时,该 IP 将被暂时锁定 +- 锁定时间为 `errorMinute` 分钟 +- 锁定期间,该 IP 的所有提取请求都将被拒绝 + +**配置示例:** + +```python +# 防暴力破解配置:5分钟内最多允许3次错误 +{ + "errorMinute": 5, + "errorCount": 3 +} +``` + +::: warning 注意 +默认配置 `errorMinute=1, errorCount=1` 非常严格,意味着输入一次错误的提取码后需要等待1分钟才能重试。根据实际需求调整此配置。 +::: + +## 上传限制 + +### 文件大小限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `uploadSize` | int | `10485760` | 单文件最大上传大小(字节),默认 10MB | +| `openUpload` | int | `1` | 是否开启上传功能(1=开启,0=关闭) | + +**常用大小换算:** +- 10MB = 10 * 1024 * 1024 = `10485760` +- 50MB = 50 * 1024 * 1024 = `52428800` +- 100MB = 100 * 1024 * 1024 = `104857600` +- 1GB = 1024 * 1024 * 1024 = `1073741824` + +### 文件过期设置 + +通过文件过期机制,可以自动清理过期文件,减少存储占用和安全风险: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | 可选的过期方式 | +| `max_save_seconds` | int | `0` | 文件最大保存时间(秒),0 表示不限制 | + +**过期方式说明:** +- `day` - 按天数过期 +- `hour` - 按小时过期 +- `minute` - 按分钟过期 +- `forever` - 永不过期(需要字符串提取码) +- `count` - 按下载次数过期 + +**安全建议:** + +对于公开服务,建议: +1. 移除 `forever` 选项,避免文件永久存储 +2. 设置 `max_save_seconds` 限制最长保存时间 +3. 优先使用 `count` 方式,下载后自动删除 + +```python +# 公开服务推荐配置 +{ + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400 # 最长保存1天 +} +``` + +### 关闭上传功能 + +在某些情况下,您可能需要临时关闭上传功能: + +```python +{ + "openUpload": 0 # 关闭上传功能 +} +``` + +## 反向代理安全配置 + +在生产环境中,通常会使用 Nginx 或其他反向代理服务器。以下是安全配置建议: + +### Nginx 配置示例 + +```nginx +server { + listen 80; + server_name your-domain.com; + + # 强制 HTTPS 重定向 + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + # SSL 证书配置 + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers on; + + # 安全头部 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # 限制请求体大小(与 uploadSize 配置一致) + client_max_body_size 100M; + + # 传递真实 IP + location / { + proxy_pass http://127.0.0.1:12345; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 静态资源缓存 + location /assets { + proxy_pass http://127.0.0.1:12345; + proxy_cache_valid 200 7d; + add_header Cache-Control "public, max-age=604800"; + } +} +``` + +### 关键安全配置说明 + +**1. 传递真实 IP** + +FileCodeBox 的 IP 限制功能依赖于获取客户端真实 IP。系统会按以下顺序获取 IP: +1. `X-Real-IP` 请求头 +2. `X-Forwarded-For` 请求头 +3. 直接连接的客户端 IP + +确保反向代理正确设置这些头部: + +```nginx +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + +**2. 请求体大小限制** + +Nginx 的 `client_max_body_size` 应该与 FileCodeBox 的 `uploadSize` 配置一致或略大: + +```nginx +client_max_body_size 100M; # 允许上传最大 100MB +``` + +**3. HTTPS 加密** + +强烈建议在生产环境中启用 HTTPS: +- 保护用户上传的文件内容 +- 保护管理员登录凭据 +- 防止中间人攻击 + +### Caddy 配置示例 + +```nginx +your-domain.com { + reverse_proxy localhost:12345 + + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Strict-Transport-Security "max-age=31536000; includeSubDomains" + } +} +``` + +## 安全检查清单 + +部署 FileCodeBox 前,请确认以下安全配置: + +- [ ] 已修改默认管理员密码 `admin_token` +- [ ] 已隐藏管理入口 `showAdminAddr: 0` +- [ ] 已配置合适的上传频率限制 +- [ ] 已配置错误次数限制防止暴力破解 +- [ ] 已设置合理的文件大小限制 +- [ ] 已配置文件过期策略 +- [ ] 已启用 HTTPS 加密 +- [ ] 反向代理已正确传递真实 IP +- [ ] 已设置安全响应头部 + +## 推荐安全配置 + +### 公开服务配置 + +```python +{ + "admin_token": "your-very-secure-password", + "showAdminAddr": 0, + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, + "uploadCount": 5, + "errorMinute": 5, + "errorCount": 3, + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # 最长1天 + "openUpload": 1 +} +``` + +### 内部服务配置 + +```python +{ + "admin_token": "internal-secure-password", + "showAdminAddr": 1, + "uploadSize": 104857600, # 100MB + "uploadMinute": 5, + "uploadCount": 50, + "errorMinute": 1, + "errorCount": 5, + "expireStyle": ["day", "hour", "forever"], + "max_save_seconds": 0, # 不限制 + "openUpload": 1 +} +``` + +## 下一步 + +- [配置说明](/guide/configuration) - 了解所有配置选项 +- [存储配置](/guide/storage) - 配置安全的存储后端 +- [文件分享](/guide/share) - 了解文件分享功能 diff --git a/docs/guide/share.md b/docs/guide/share.md new file mode 100644 index 000000000..6ce879334 --- /dev/null +++ b/docs/guide/share.md @@ -0,0 +1,341 @@ +# 文件分享 + +FileCodeBox 提供了简单易用的文件和文本分享功能。用户可以通过提取码安全地分享和获取文件。 + +## 分享方式 + +FileCodeBox 支持两种分享方式: + +1. **文本分享** - 直接分享文本内容,适合代码片段、配置文件等 +2. **文件分享** - 上传文件进行分享,支持各种文件格式 + +## 文本分享 + +### 使用方法 + +1. 在首页选择「文本分享」标签 +2. 在文本框中输入或粘贴要分享的内容 +3. 选择过期方式和时间 +4. 点击「分享」按钮 +5. 获取提取码 + +### 文本大小限制 + +::: warning 注意 +文本分享的最大内容大小为 **222KB**(227,328 字节)。如果内容超过此限制,建议使用文件分享方式。 +::: + +文本内容大小按 UTF-8 编码计算,中文字符通常占用 3 个字节。 + +### API 接口 + +**POST** `/share/text/` + +请求参数: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `text` | string | 是 | 要分享的文本内容 | +| `expire_value` | int | 否 | 过期数值,默认 1 | +| `expire_style` | string | 否 | 过期方式,默认 `day` | + +响应示例: + +```json +{ + "code": 200, + "detail": { + "code": "123456" + } +} +``` + +## 文件分享 + +### 使用方法 + +1. 在首页选择「文件分享」标签 +2. 点击上传区域或拖拽文件到上传区域 +3. 选择过期方式和时间 +4. 点击「上传」按钮 +5. 获取提取码 + +### 文件大小限制 + +默认单文件最大上传大小为 **10MB**。管理员可以通过 `uploadSize` 配置项修改此限制。 + +::: tip 提示 +如果需要上传大文件,请联系管理员启用分片上传功能,或调整 `uploadSize` 配置。 +::: + +### 支持的上传方式 + +- **点击上传** - 点击上传区域选择文件 +- **拖拽上传** - 将文件拖拽到上传区域 +- **粘贴上传** - 从剪贴板粘贴图片(部分主题支持) + +### API 接口 + +**POST** `/share/file/` + +请求参数(multipart/form-data): + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | file | 是 | 要上传的文件 | +| `expire_value` | int | 否 | 过期数值,默认 1 | +| `expire_style` | string | 否 | 过期方式,默认 `day` | + +响应示例: + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + +## 过期设置 + +FileCodeBox 支持多种灵活的过期方式: + +| 过期方式 | 参数值 | 说明 | +|----------|--------|------| +| 按天过期 | `day` | 文件在指定天数后过期 | +| 按小时过期 | `hour` | 文件在指定小时后过期 | +| 按分钟过期 | `minute` | 文件在指定分钟后过期 | +| 永不过期 | `forever` | 文件永久有效 | +| 按次数过期 | `count` | 文件在被下载指定次数后过期 | + +::: info 说明 +- 管理员可以通过 `expireStyle` 配置项控制用户可选的过期方式 +- 管理员可以通过 `max_save_seconds` 配置项限制文件的最长保存时间 +::: + +### 过期方式示例 + +```bash +# 文件 3 天后过期 +expire_value=3, expire_style=day + +# 文件 12 小时后过期 +expire_value=12, expire_style=hour + +# 文件 30 分钟后过期 +expire_value=30, expire_style=minute + +# 文件永不过期 +expire_value=1, expire_style=forever + +# 文件被下载 5 次后过期 +expire_value=5, expire_style=count +``` + +## 提取文件 + +### 使用方法 + +1. 在首页的「提取文件」区域输入提取码 +2. 点击「提取」按钮 +3. 系统会显示文件信息(文件名、大小等) +4. 点击「下载」按钮下载文件,或直接查看文本内容 + +### 提取码说明 + +- 提取码通常为 **6 位数字** +- 永不过期的文件使用 **字母数字混合** 的提取码 +- 提取码区分大小写(针对字母数字混合的情况) + +### API 接口 + +**查询文件信息** + +**POST** `/share/select/` + +请求参数: + +```json +{ + "code": "123456" +} +``` + +响应示例(文件): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "example.pdf", + "size": 1048576, + "text": "https://example.com/download/..." + } +} +``` + +响应示例(文本): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "Text", + "size": 1024, + "text": "这是分享的文本内容..." + } +} +``` + +**直接下载文件** + +**GET** `/share/select/?code=123456` + +此接口会直接返回文件内容,适合在浏览器中直接访问。 + +## 分片上传(大文件) + +对于大文件上传,FileCodeBox 支持分片上传功能。此功能需要管理员启用(`enableChunk=1`)。 + +### 分片上传流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + + C->>S: 1. 初始化上传 (POST /chunk/upload/init/) + S-->>C: 返回 upload_id 和分片信息 + + loop 每个分片 + C->>S: 2. 上传分片 (POST /chunk/upload/chunk/{upload_id}/{chunk_index}) + S-->>C: 返回分片哈希 + end + + C->>S: 3. 完成上传 (POST /chunk/upload/complete/{upload_id}) + S-->>C: 返回提取码 +``` + +### 1. 初始化上传 + +**POST** `/chunk/upload/init/` + +请求参数: + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "sha256_hash_of_file" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file_name` | string | 是 | 文件名 | +| `file_size` | int | 是 | 文件总大小(字节) | +| `chunk_size` | int | 否 | 分片大小,默认 5MB | +| `file_hash` | string | 是 | 文件的 SHA256 哈希值 | + +响应示例: + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +### 2. 上传分片 + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +- `upload_id` - 初始化时返回的上传会话 ID +- `chunk_index` - 分片索引,从 0 开始 + +请求体:分片文件数据(multipart/form-data) + +响应示例: + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "sha256_hash_of_chunk" + } +} +``` + +### 3. 完成上传 + +**POST** `/chunk/upload/complete/{upload_id}` + +请求参数: + +```json +{ + "expire_value": 1, + "expire_style": "day" +} +``` + +响应示例: + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### 断点续传 + +分片上传支持断点续传。如果上传中断,可以: + +1. 重新调用初始化接口,使用相同的 `file_hash` +2. 服务器会返回已上传的分片列表 `uploaded_chunks` +3. 客户端只需上传未完成的分片 + +## 错误处理 + +### 常见错误码 + +| 错误码 | 说明 | 解决方案 | +|--------|------|----------| +| 403 | 文件大小超过限制 | 减小文件大小或联系管理员调整限制 | +| 403 | 内容过多 | 文本超过 222KB,请使用文件分享 | +| 403 | 上传频率限制 | 等待一段时间后重试 | +| 404 | 文件不存在 | 检查提取码是否正确 | +| 404 | 文件已过期 | 文件已过期或下载次数已用完 | + +### 频率限制 + +为防止滥用,系统对上传和提取操作有频率限制: + +- **上传限制**:默认每分钟最多 10 次上传 +- **错误限制**:默认每分钟最多 1 次错误尝试 + +::: tip 提示 +如果遇到频率限制,请等待限制时间窗口过后再重试。 +::: + +## 下一步 + +- [配置说明](/guide/configuration) - 了解如何配置分享相关设置 +- [存储配置](/guide/storage) - 了解文件存储方式 +- [安全设置](/guide/security) - 了解安全相关配置 +- [管理面板](/guide/management) - 了解如何管理分享的文件 diff --git a/docs/guide/storage-onedrive.md b/docs/guide/storage-onedrive.md new file mode 100644 index 000000000..796a3c0c9 --- /dev/null +++ b/docs/guide/storage-onedrive.md @@ -0,0 +1,135 @@ +# OneDrive作为存储的配置方法 + +**仅支持工作或学校账户,并且需要有管理员权限以授权API** + +## 1. 需要配置的参数 + +``` +file_storage=onedrive +onedrive_domain=XXXXXX +onedrive_client_id=XXXXXX-XXXXXX-XXXXXX-XXXXXX +onedrive_username=XXXXXX@XXXXXX +onedrive_password=XXXXXX +``` + +`onedrive_username`和`onedrive_password`是你的账户名(邮箱)和密码,另外两个参数需要在[微软Azure门户](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)中注册应用后获取。 + +## 2. 应用注册 + +1. 登录[https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade),鼠标置于右上角账号处,浮窗将显示的`域`即为`onedrive_domain`的值。 +![onedrive_domain](https://api.onedrive.com/v1.0/shares/s!Au-BDzXcM6_VmGCiErO85doq9Tcu/root/content) + +2. 点击左上角的`+新注册`,输入名称, + * 受支持的帐户类型:选择任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户(例如,Skype、Xbox) + * 重定向 URI (可选):选择`Web`,并输入`http://localhost` + +3. 完成注册后进入概述页面,在概要中找到`应用程序(客户端)ID`,即为`onedrive_client_id`的值。 +![onedrive_client_id](https://api.onedrive.com/v1.0/shares/s!Au-BDzXcM6_VmGHD4CNyJxm_QBb8/root/content) + +4. 此时还需要配置允许公共客户端流和API权限 + * 在左侧选择`身份验证`,找到`允许的客户端流`,选择`是`,并**点击`保存`**。 + ![允许的客户端流](https://api.onedrive.com/v1.0/shares/s!Au-BDzXcM6_VmGJQMOlOCb2-L0Lh/root/content) + * 在左侧选择`API权限`,点击`+添加权限`,选择`Microsoft Graph`->`委托的权限`,并勾选下述权限:openid、Files中所有权限、User.Read,如下图所示。最后**点击下方的`添加权限`**。 + ![添加权限](https://api.onedrive.com/v1.0/shares/s!Au-BDzXcM6_VmGOZzz7sIrdXkD4w/root/content) + * 最后点击`授予管理员同意`,并**点击`是`**,最终状态变为`已授予`。 + ![授予管理员同意](https://api.onedrive.com/v1.0/shares/s!Au-BDzXcM6_VmGSOAnjnHUlbirbU/root/content) + +## 3. 使用下述代码测试是否配置成功 + +安装依赖:`pip install Office365-REST-Python-Client` + +```python +# common.py +import msal +domain = 'XXXXXX' +client_id = 'XXXXXX' +username = 'XXXXXX' +password = 'XXXXXX' + +def acquire_token_pwd(): + authority_url = f'https://login.microsoftonline.com/{domain}' + app = msal.PublicClientApplication( + authority=authority_url, + client_id=client_id + ) + result = app.acquire_token_by_username_password( + username=username, + password=password, + scopes=['https://graph.microsoft.com/.default'] + ) + return result +``` + +测试登录,如果成功打印出账户名,说明配置成功。 + +```python +from common import acquire_token_pwd + +from office365.graph_client import GraphClient +try: + client = GraphClient(acquire_token_pwd) + me = client.me.get().execute_query() + print(me.user_principal_name) +except Exception as e: + print(e) +``` + +测试文件上传 + +```python +import os +from office365.graph_client import GraphClient +from common import acquire_token_pwd + +remote_path = 'tmp' +local_path = '.tmp/1689843925000.png' + +def convert_link_to_download_link(link): + import re + p1 = re.search(r'https:\/\/(.+)\.sharepoint\.com', link).group(1) + p2 = re.search(r'personal\/(.+)\/', link).group(1) + p3 = re.search(rf'{p2}\/(.+)', link).group(1) + return f'https://{p1}.sharepoint.com/personal/{p2}/_layouts/52/download.aspx?share={p3}' + +client = GraphClient(acquire_token_pwd) +folder = client.me.drive.root.get_by_path(remote_path) +# 1. upload +file = folder.upload_file(local_path).execute_query() +print(f'File {file.web_url} has been uploaded') +# 2. create sharing link +remote_file = folder.get_by_path(os.path.basename(local_path)) +permission = remote_file.create_link("view", "anonymous").execute_query() +print(f"sharing link: {convert_link_to_download_link(permission.link.webUrl)}") +``` + +测试文件下载 + +```python +import os +from office365.graph_client import GraphClient +from common import acquire_token_pwd + +remote_path = 'tmp/1689843925000.png' +local_path = '.tmp' +if not os.path.exists(local_path): + os.makedirs(local_path) + +client = GraphClient(acquire_token_pwd) +remote_file = client.me.drive.root.get_by_path(remote_path).get().execute_query() +with open(os.path.join(local_path, os.path.basename(remote_path)), 'wb') as local_file: + remote_file.download(local_file).execute_query() + print(f'{remote_file.name} has been downloaded into {local_file.name}') +``` + +测试删除文件 + +```python +from office365.graph_client import GraphClient +from common import acquire_token_pwd + +remote_path = 'tmp/1689843925000.png' + +client = GraphClient(acquire_token_pwd) +file = client.me.drive.root.get_by_path(remote_path) +file.delete_object().execute_query() +``` diff --git a/docs/guide/storage-opendal.md b/docs/guide/storage-opendal.md new file mode 100644 index 000000000..c4d383f96 --- /dev/null +++ b/docs/guide/storage-opendal.md @@ -0,0 +1,30 @@ +# 通过 OpenDAL 集成存储的配置方法 + +## 需要配置的参数 + +```dotenv +file_storage=opendal +opendal_scheme= +opendal__=... +``` + +以 Gcs 为例,需要配置的参数如下: +```dotenv +file_storage=opendal +opendal_scheme=gcs +opendal_gcs_root= +opendal_gcs_bucket= +opendal_gcs_credential= +``` + +所有支持的服务可以在[此处](https://opendal.apache.org/docs/rust/opendal/services/index.html)查看。 +具体服务的配置参数与 OpenDAL 文档一致。 + +## 补充说明 + +通过 OpenDAL 集成的服务均通过服务器中转下载。因此,每次下载既消耗存储服务的流量,也消耗服务器的流量。 + +OpenDAL 和该项目本身都支持本地存储、`s3`、`onedrive`。不同之处有以下几点: +1. 项目的支持通过预签名实现,不消耗服务器流量。而 OpenDAL 通过服务器中转下载,消耗服务器流量。(本地存储除外) +2. 项目的支持对于异常情况可能会有更多的调试信息,方便排查问题。 +3. OpenDAL 项目本身采用 Rust 编写,性能更好。 \ No newline at end of file diff --git a/docs/guide/storage.md b/docs/guide/storage.md new file mode 100644 index 000000000..86686c69a --- /dev/null +++ b/docs/guide/storage.md @@ -0,0 +1,395 @@ +# 存储配置 + +FileCodeBox 支持多种存储后端,您可以根据需求选择合适的存储方式。本文档将详细介绍各种存储后端的配置方法。 + +## 存储类型概览 + +| 存储类型 | 配置值 | 说明 | +|---------|--------|------| +| 本地存储 | `local` | 默认存储方式,文件保存在服务器本地 | +| S3 兼容存储 | `s3` | 支持 AWS S3、阿里云 OSS、MinIO 等 | +| OneDrive | `onedrive` | 微软 OneDrive 云存储(仅支持工作/学校账户) | +| WebDAV | `webdav` | 支持 WebDAV 协议的存储服务 | +| OpenDAL | `opendal` | 通过 OpenDAL 集成更多存储服务 | + +## 本地存储 + +本地存储是默认的存储方式,文件将保存在服务器的 `data/` 目录下。 + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | `local` | 存储类型 | +| `storage_path` | string | `""` | 自定义存储路径(可选) | + +### 配置示例 + +```bash +file_storage=local +storage_path= +``` + +### 说明 + +- 文件默认存储在 `data/share/data/` 目录下 +- 按日期自动创建子目录:`年/月/日/文件ID/` +- 建议在生产环境中将 `data/` 目录挂载到持久化存储 + +## S3 兼容存储 + +支持所有 S3 兼容的对象存储服务,包括 AWS S3、阿里云 OSS、MinIO、腾讯云 COS 等。 + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | - | 设置为 `s3` | +| `s3_access_key_id` | string | `""` | Access Key ID | +| `s3_secret_access_key` | string | `""` | Secret Access Key | +| `s3_bucket_name` | string | `""` | 存储桶名称 | +| `s3_endpoint_url` | string | `""` | S3 端点 URL | +| `s3_region_name` | string | `auto` | 区域名称 | +| `s3_signature_version` | string | `s3v2` | 签名版本(`s3v2` 或 `s3v4`) | +| `s3_hostname` | string | `""` | S3 主机名(备用) | +| `s3_proxy` | int | `0` | 是否通过服务器代理下载(1=是,0=否) | +| `aws_session_token` | string | `""` | AWS 会话令牌(可选) | + +### AWS S3 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=AKIAIOSFODNN7EXAMPLE +s3_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +s3_bucket_name=my-filecodebox-bucket +s3_endpoint_url=https://s3.amazonaws.com +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +### 阿里云 OSS 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=您的AccessKeyId +s3_secret_access_key=您的SecretAccessKey +s3_bucket_name=bucket-name +s3_endpoint_url=https://bucket-name.oss-cn-hangzhou.aliyuncs.com +s3_region_name=oss-cn-hangzhou +s3_signature_version=s3v4 +``` + +::: tip 阿里云 OSS 端点格式 +端点 URL 格式为:`https://..aliyuncs.com` + +常用区域: +- 杭州:`oss-cn-hangzhou` +- 上海:`oss-cn-shanghai` +- 北京:`oss-cn-beijing` +- 深圳:`oss-cn-shenzhen` +::: + +### MinIO 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=minioadmin +s3_secret_access_key=minioadmin +s3_bucket_name=filecodebox +s3_endpoint_url=http://localhost:9000 +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +::: warning MinIO 注意事项 +- `s3_endpoint_url` 填写 MinIO 的 API 接口地址 +- `s3_region_name` 根据 MinIO 配置中的 `Server Location` 设置 +- 确保存储桶已创建且有正确的访问权限 +::: + +### 腾讯云 COS 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=您的SecretId +s3_secret_access_key=您的SecretKey +s3_bucket_name=bucket-name-1250000000 +s3_endpoint_url=https://cos.ap-guangzhou.myqcloud.com +s3_region_name=ap-guangzhou +s3_signature_version=s3v4 +``` + +### 代理下载 + +当 `s3_proxy=1` 时,文件下载将通过服务器中转,而不是直接从 S3 下载。这在以下情况下有用: + +- S3 存储桶不允许公开访问 +- 需要隐藏实际的存储地址 +- 网络环境限制直接访问 S3 + + + +## OneDrive 存储 + +OneDrive 存储支持将文件保存到微软 OneDrive 云存储。 + +::: warning 重要限制 +OneDrive 存储**仅支持工作或学校账户**,并且需要有管理员权限以授权 API。个人账户无法使用此功能。 +::: + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | - | 设置为 `onedrive` | +| `onedrive_domain` | string | `""` | Azure AD 域名 | +| `onedrive_client_id` | string | `""` | 应用程序(客户端)ID | +| `onedrive_username` | string | `""` | 账户邮箱 | +| `onedrive_password` | string | `""` | 账户密码 | +| `onedrive_root_path` | string | `filebox_storage` | OneDrive 中的存储根目录 | +| `onedrive_proxy` | int | `0` | 是否通过服务器代理下载 | + +### 配置示例 + +```bash +file_storage=onedrive +onedrive_domain=contoso.onmicrosoft.com +onedrive_client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +onedrive_username=user@contoso.onmicrosoft.com +onedrive_password=your_password +onedrive_root_path=filebox_storage +``` + +### Azure 应用注册步骤 + +要使用 OneDrive 存储,您需要在 Azure 门户中注册应用程序: + +#### 1. 获取域名 + +登录 [Azure 门户](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade),将鼠标置于右上角账号处,浮窗显示的**域**即为 `onedrive_domain` 的值。 + +#### 2. 注册应用 + +1. 点击左上角的 **+ 新注册** +2. 输入应用名称(如:FileCodeBox) +3. **受支持的帐户类型**:选择"任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户" +4. **重定向 URI**:选择 `Web`,输入 `http://localhost` +5. 点击**注册** + +#### 3. 获取客户端 ID + +注册完成后,在应用概述页面的**概要**中找到**应用程序(客户端)ID**,即为 `onedrive_client_id` 的值。 + +#### 4. 配置身份验证 + +1. 在左侧菜单选择**身份验证** +2. 找到**允许公共客户端流**,选择**是** +3. 点击**保存** + +#### 5. 配置 API 权限 + +1. 在左侧菜单选择 **API 权限** +2. 点击 **+ 添加权限** +3. 选择 **Microsoft Graph** → **委托的权限** +4. 勾选以下权限: + - `openid` + - `Files.Read` + - `Files.Read.All` + - `Files.ReadWrite` + - `Files.ReadWrite.All` + - `User.Read` +5. 点击**添加权限** +6. 点击**代表 xxx 授予管理员同意** +7. 确认后,权限状态应显示为**已授予** + +### 安装依赖 + +使用 OneDrive 存储需要安装额外的 Python 依赖: + +```bash +pip install msal Office365-REST-Python-Client +``` + +### 验证配置 + +您可以使用以下代码测试配置是否正确: + +```python +import msal +from office365.graph_client import GraphClient + +domain = 'your_domain' +client_id = 'your_client_id' +username = 'your_username' +password = 'your_password' + +def acquire_token_pwd(): + authority_url = f'https://login.microsoftonline.com/{domain}' + app = msal.PublicClientApplication( + authority=authority_url, + client_id=client_id + ) + result = app.acquire_token_by_username_password( + username=username, + password=password, + scopes=['https://graph.microsoft.com/.default'] + ) + return result + +# 测试连接 +client = GraphClient(acquire_token_pwd) +me = client.me.get().execute_query() +print(f"登录成功:{me.user_principal_name}") +``` + +## WebDAV 存储 + +WebDAV 存储支持将文件保存到任何支持 WebDAV 协议的服务,如 Nextcloud、ownCloud、坚果云等。 + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | - | 设置为 `webdav` | +| `webdav_url` | string | `""` | WebDAV 服务器 URL | +| `webdav_username` | string | `""` | WebDAV 用户名 | +| `webdav_password` | string | `""` | WebDAV 密码 | +| `webdav_root_path` | string | `filebox_storage` | WebDAV 中的存储根目录 | +| `webdav_proxy` | int | `0` | 是否通过服务器代理下载 | + +### 配置示例 + +```bash +file_storage=webdav +webdav_url=https://dav.example.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_password +webdav_root_path=filebox_storage +``` + +### Nextcloud 配置示例 + +```bash +file_storage=webdav +webdav_url=https://your-nextcloud.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip Nextcloud 应用密码 +建议在 Nextcloud 中创建应用密码,而不是使用主密码: +1. 登录 Nextcloud +2. 进入**设置** → **安全** +3. 在**设备与会话**中创建新的应用密码 +::: + +### 坚果云配置示例 + +```bash +file_storage=webdav +webdav_url=https://dav.jianguoyun.com/dav/ +webdav_username=your_email@example.com +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip 坚果云应用密码 +坚果云需要使用应用密码: +1. 登录坚果云网页版 +2. 进入**账户信息** → **安全选项** +3. 添加应用密码 +::: + +## OpenDAL 存储 + +OpenDAL 是一个统一的数据访问层,支持多种存储服务。通过 OpenDAL,您可以使用 Google Cloud Storage、Azure Blob Storage 等更多存储服务。 + +### 配置参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `file_storage` | string | 设置为 `opendal` | +| `opendal_scheme` | string | 存储服务类型(如 `gcs`、`azblob`) | +| `opendal__` | string | 服务特定的配置参数 | + +### 安装依赖 + +```bash +pip install opendal +``` + +### Google Cloud Storage 配置示例 + +```bash +file_storage=opendal +opendal_scheme=gcs +opendal_gcs_root=/filecodebox +opendal_gcs_bucket=your-bucket-name +opendal_gcs_credential=base64_encoded_credential +``` + +### Azure Blob Storage 配置示例 + +```bash +file_storage=opendal +opendal_scheme=azblob +opendal_azblob_root=/filecodebox +opendal_azblob_container=your-container +opendal_azblob_account_name=your_account +opendal_azblob_account_key=your_key +``` + +### 支持的服务 + +OpenDAL 支持众多存储服务,完整列表请参考 [OpenDAL 官方文档](https://opendal.apache.org/docs/rust/opendal/services/index.html)。 + +常用服务包括: +- `gcs` - Google Cloud Storage +- `azblob` - Azure Blob Storage +- `obs` - 华为云 OBS +- `oss` - 阿里云 OSS(通过 OpenDAL) +- `cos` - 腾讯云 COS(通过 OpenDAL) +- `hdfs` - Hadoop HDFS +- `ftp` - FTP 服务器 +- `sftp` - SFTP 服务器 + +::: warning OpenDAL 注意事项 +1. 通过 OpenDAL 集成的服务均通过服务器中转下载,会同时消耗存储服务和服务器的流量 +2. 相比原生 S3/OneDrive 支持,OpenDAL 方式可能缺少一些调试信息 +3. OpenDAL 采用 Rust 编写,性能较好 +::: + +## 存储选择建议 + +| 场景 | 推荐存储 | 原因 | +|------|----------|------| +| 个人/小型部署 | 本地存储 | 简单易用,无需额外配置 | +| 企业内网 | MinIO + S3 | 自建对象存储,数据可控 | +| 公有云部署 | 对应云厂商 S3 | 同区域访问快,成本低 | +| 已有 OneDrive | OneDrive | 利用现有资源 | +| 已有 WebDAV | WebDAV | 兼容性好 | +| 特殊存储需求 | OpenDAL | 支持更多存储服务 | + +## 常见问题 + +### S3 上传失败 + +1. 检查 Access Key 和 Secret Key 是否正确 +2. 确认存储桶名称和区域配置正确 +3. 检查存储桶的访问权限设置 +4. 确认签名版本(`s3v2` 或 `s3v4`)与服务商要求一致 + +### OneDrive 认证失败 + +1. 确认使用的是工作/学校账户,而非个人账户 +2. 检查 Azure 应用是否已授予管理员同意 +3. 确认 API 权限配置完整 +4. 验证用户名和密码是否正确 + +### WebDAV 连接失败 + +1. 检查 WebDAV URL 格式是否正确 +2. 确认用户名和密码(或应用密码)正确 +3. 检查服务器是否支持 WebDAV 协议 +4. 确认网络连接正常 diff --git a/docs/guide/upload.md b/docs/guide/upload.md new file mode 100644 index 000000000..a5f1b8fd5 --- /dev/null +++ b/docs/guide/upload.md @@ -0,0 +1,420 @@ +# 文件上传 + +FileCodeBox 提供了多种灵活的文件上传方式,支持普通上传和分片上传,满足不同场景的需求。 + +## 上传方式 + +FileCodeBox 支持以下几种上传方式: + +### 拖拽上传 + +将文件直接拖拽到上传区域即可开始上传。这是最便捷的上传方式。 + +1. 打开 FileCodeBox 首页 +2. 将文件从文件管理器拖拽到上传区域 +3. 松开鼠标,文件开始上传 +4. 上传完成后获取提取码 + +::: tip 提示 +拖拽上传支持同时拖拽多个文件(取决于主题支持)。 +::: + +### 点击上传 + +点击上传区域,通过系统文件选择器选择文件。 + +1. 点击上传区域的「选择文件」按钮 +2. 在弹出的文件选择器中选择要上传的文件 +3. 确认选择后文件开始上传 +4. 上传完成后获取提取码 + +### 粘贴上传 + +支持从剪贴板直接粘贴图片进行上传(部分主题支持)。 + +1. 复制图片到剪贴板(截图或复制图片) +2. 在上传区域使用 `Ctrl+V`(Windows/Linux)或 `Cmd+V`(macOS)粘贴 +3. 图片自动开始上传 +4. 上传完成后获取提取码 + +::: warning 注意 +粘贴上传仅支持图片格式,不支持其他文件类型。具体支持情况取决于所使用的主题。 +::: + +## 文件大小限制 + +### 默认限制 + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `uploadSize` | 10MB | 单文件最大上传大小 | + +### 修改上传限制 + +管理员可以通过管理面板或配置文件修改上传大小限制: + +```python +# 设置最大上传大小为 100MB +uploadSize = 104857600 # 100 * 1024 * 1024 +``` + +::: info 说明 +`uploadSize` 的单位是字节。常用换算: +- 10MB = 10485760 +- 50MB = 52428800 +- 100MB = 104857600 +- 500MB = 524288000 +- 1GB = 1073741824 +::: + +### 超出限制的处理 + +当上传文件超过大小限制时,系统会返回 403 错误: + +```json +{ + "detail": "大小超过限制,最大为10.00 MB" +} +``` + +## 普通上传 API + +### 文件上传接口 + +**POST** `/share/file/` + +Content-Type: `multipart/form-data` + +**请求参数:** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | file | 是 | 要上传的文件 | +| `expire_value` | int | 否 | 过期数值,默认 1 | +| `expire_style` | string | 否 | 过期方式,默认 `day` | + +**过期方式选项:** + +| 值 | 说明 | +|----|------| +| `day` | 按天过期 | +| `hour` | 按小时过期 | +| `minute` | 按分钟过期 | +| `forever` | 永不过期 | +| `count` | 按下载次数过期 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + +**cURL 示例:** + +```bash +# 上传文件(默认1天有效期) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" + +# 上传文件并指定有效期(7天) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" \ + -F "expire_value=7" \ + -F "expire_style=day" + +# 上传文件并指定有效期(可下载10次) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" \ + -F "expire_value=10" \ + -F "expire_style=count" + +# 分享文本 +curl -X POST "http://localhost:12345/share/text/" \ + -F "text=这是要分享的文本内容" + +# 通过取件码下载文件 +curl -L "http://localhost:12345/share/select/?code=取件码" -o downloaded_file +``` + +::: tip 需要认证时 +如果管理面板关闭了游客上传(`openUpload=0`),需要先登录获取 token: + +```bash +# 1. 登录获取 token +curl -X POST "http://localhost:12345/admin/login" \ + -H "Content-Type: application/json" \ + -d '{"password": "FileCodeBox2023"}' + +# 返回: {"code":200,"msg":"success","detail":{"token":"xxx.xxx.xxx","token_type":"Bearer"}} + +# 2. 使用 token 上传文件 +curl -X POST "http://localhost:12345/share/file/" \ + -H "Authorization: Bearer xxx.xxx.xxx" \ + -F "file=@/path/to/file.pdf" + +# 3. 使用 token 分享文本 +curl -X POST "http://localhost:12345/share/text/" \ + -H "Authorization: Bearer xxx.xxx.xxx" \ + -F "text=这是要分享的文本内容" +``` +::: + +## 分片上传 API + +对于大文件,FileCodeBox 支持分片上传功能。分片上传将大文件分割成多个小块分别上传,支持断点续传。 + +::: warning 前提条件 +分片上传功能需要管理员启用:`enableChunk=1` +::: + +### 分片上传流程 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 初始化上传 │ ──▶ │ 上传分片 │ ──▶ │ 完成上传 │ +│ /init/ │ │ /chunk/ │ │ /complete/ │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌───────────┐ + │ 循环上传 │ + │ 每个分片 │ + └───────────┘ +``` + +### 1. 初始化上传 + +**POST** `/chunk/upload/init/` + +**请求参数:** + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `file_name` | string | 是 | - | 文件名 | +| `file_size` | int | 是 | - | 文件总大小(字节) | +| `chunk_size` | int | 否 | 5MB | 分片大小(字节) | +| `file_hash` | string | 是 | - | 文件的 SHA256 哈希值 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456789", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +| 字段 | 说明 | +|------|------| +| `existed` | 文件是否已存在(秒传) | +| `upload_id` | 上传会话 ID | +| `chunk_size` | 分片大小 | +| `total_chunks` | 总分片数 | +| `uploaded_chunks` | 已上传的分片索引列表 | + +### 2. 上传分片 + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +**路径参数:** + +| 参数 | 说明 | +|------|------| +| `upload_id` | 初始化时返回的上传会话 ID | +| `chunk_index` | 分片索引,从 0 开始 | + +**请求体:** + +Content-Type: `multipart/form-data` + +| 参数 | 类型 | 说明 | +|------|------|------| +| `chunk` | file | 分片数据 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "a1b2c3d4e5f6..." + } +} +``` + +**cURL 示例:** + +```bash +# 上传第一个分片(索引为 0) +curl -X POST "http://localhost:12345/chunk/upload/chunk/abc123def456789/0" \ + -F "chunk=@/path/to/chunk_0" +``` + +### 3. 完成上传 + +**POST** `/chunk/upload/complete/{upload_id}` + +**路径参数:** + +| 参数 | 说明 | +|------|------| +| `upload_id` | 上传会话 ID | + +**请求参数:** + +```json +{ + "expire_value": 7, + "expire_style": "day" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `expire_value` | int | 是 | 过期数值 | +| `expire_style` | string | 是 | 过期方式 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### 断点续传 + +分片上传支持断点续传。当上传中断后: + +1. 使用相同的 `file_hash` 重新调用初始化接口 +2. 服务器返回 `uploaded_chunks` 列表,包含已上传的分片索引 +3. 客户端只需上传不在列表中的分片 +4. 所有分片上传完成后调用完成接口 + +**示例流程:** + +```javascript +// 1. 初始化上传 +const initResponse = await fetch('/chunk/upload/init/', { + method: 'POST', + body: JSON.stringify({ + file_name: 'large_file.zip', + file_size: fileSize, + chunk_size: 5 * 1024 * 1024, + file_hash: fileHash + }) +}); +const { upload_id, uploaded_chunks, total_chunks } = await initResponse.json(); + +// 2. 上传未完成的分片 +for (let i = 0; i < total_chunks; i++) { + if (!uploaded_chunks.includes(i)) { + const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); + await fetch(`/chunk/upload/chunk/${upload_id}/${i}`, { + method: 'POST', + body: chunk + }); + } +} + +// 3. 完成上传 +await fetch(`/chunk/upload/complete/${upload_id}`, { + method: 'POST', + body: JSON.stringify({ + expire_value: 7, + expire_style: 'day' + }) +}); +``` + +## 错误处理 + +### 常见错误 + +| HTTP 状态码 | 错误信息 | 原因 | 解决方案 | +|-------------|----------|------|----------| +| 403 | 大小超过限制 | 文件超过 `uploadSize` 限制 | 减小文件大小或联系管理员调整限制 | +| 403 | 上传频率限制 | 超过 IP 上传频率限制 | 等待限制时间窗口后重试 | +| 400 | 过期时间类型错误 | `expire_style` 值不在允许列表中 | 使用有效的过期方式 | +| 404 | 上传会话不存在 | `upload_id` 无效或已过期 | 重新初始化上传 | +| 400 | 无效的分片索引 | `chunk_index` 超出范围 | 检查分片索引是否正确 | +| 400 | 分片不完整 | 完成上传时分片数量不足 | 确保所有分片都已上传 | + +### 频率限制 + +系统对上传操作有频率限制,防止滥用: + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `uploadMinute` | 1 | 限制时间窗口(分钟) | +| `uploadCount` | 10 | 时间窗口内最大上传次数 | + +当超过频率限制时,需要等待时间窗口过后才能继续上传。 + +### 错误响应格式 + +```json +{ + "detail": "错误信息描述" +} +``` + +## 上传配置 + +### 相关配置项 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `openUpload` | int | 1 | 是否开放上传(1=开放,0=关闭) | +| `uploadSize` | int | 10485760 | 最大上传大小(字节) | +| `enableChunk` | int | 0 | 是否启用分片上传(1=启用,0=禁用) | +| `uploadMinute` | int | 1 | 上传频率限制时间窗口(分钟) | +| `uploadCount` | int | 10 | 时间窗口内最大上传次数 | +| `expireStyle` | list | ["day","hour","minute","forever","count"] | 允许的过期方式 | + +### 配置示例 + +```python +# 允许上传 100MB 文件,启用分片上传 +uploadSize = 104857600 +enableChunk = 1 + +# 放宽上传频率限制:每 5 分钟最多 50 次 +uploadMinute = 5 +uploadCount = 50 + +# 只允许按天和按次数过期 +expireStyle = ["day", "count"] +``` + +## 下一步 + +- [文件分享](/guide/share) - 了解完整的分享流程 +- [配置说明](/guide/configuration) - 了解所有配置选项 +- [存储配置](/guide/storage) - 了解文件存储方式 +- [安全设置](/guide/security) - 了解安全相关配置 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..fae59fda7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,42 @@ +--- +layout: home + +hero: + name: "FileCodeBox" + text: "文件快递柜" + tagline: 匿名口令分享文本,文件,像拿快递一样取文件 + image: + src: /logo_small.png + alt: FileCodeBox + actions: + - theme: brand + text: 快速开始 + link: /guide/getting-started + - theme: alt + text: 在线体验 + link: https://share.lanol.cn + - theme: alt + text: 在 GitHub 上查看 + link: https://github.com/vastsa/FileCodeBox + +features: + - icon: 🚀 + title: 快速部署 + details: 支持 Docker 一键部署,简单快捷,无需复杂配置 + - icon: 🔒 + title: 安全可靠 + details: 文件访问需要提取码,支持设置有效期和下载次数限制 + - icon: 💻 + title: 简洁界面 + details: 清爽的用户界面,支持拖拽上传,使用体验极佳 + - icon: 🛠️ + title: 功能丰富 + details: 支持文件预览、在线播放、图片处理等多种功能 + - icon: 📦 + title: 存储扩展 + details: 支持本地存储、对象存储等多种存储方式 + - icon: 🔌 + title: API 支持 + details: 提供完整的 REST API,方便与其他系统集成 +--- + diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..d4a7f6dc7 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "vitepress": "^1.6.3" + }, + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + } +} \ No newline at end of file diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 000000000..1c4b6eb11 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,1551 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + vitepress: + specifier: ^1.6.3 + version: 1.6.3(@algolia/client-search@5.20.2)(postcss@8.5.2)(search-insights@2.17.3) + +packages: + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.20.2': + resolution: {integrity: sha512-IS8JSFsDD33haaKIIFaL7qj3bEIG9GldZfb3ILW0QF3at7TcrIJYy58hrDvFee5T3p3E2aH/+wqIr0eha8jB/w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.20.2': + resolution: {integrity: sha512-k0KxCfcX/HZySqPasKy6GkiiDuebaMh2v/nE0HHg1PbsyeyagLapDi6Ktjkxhz8NlUq6eTJR+ddGJegippKQtQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.20.2': + resolution: {integrity: sha512-xoZcL/Uu49KYDb3feu2n06gALD17p5CslO8Zk3mZ7+uTurK3lgjLws7LNetZ172Ap/GpzPCRXI83d2iDoYQD6Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.20.2': + resolution: {integrity: sha512-fy7aCbo9y7WHt/9G03EYc471Dd5kIaM8PNP4z6AEQYr9a9X8c4inwNs6tePxAEfRHwVQi0CZ7kuVdn6/MjWx1A==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.20.2': + resolution: {integrity: sha512-ocL1ZXulfuXzJAwsKw2kMscKMD0rs/f4CFYu6Gjh4mK4um6rGfa1a6u1MSc4swFqRQer0wNP9Pi+kVfKhuKt5A==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.20.2': + resolution: {integrity: sha512-Xjs4Tj1zkLCnmq1ys8RRhLQPy002I6GuT/nbHVdSQmQu4yKCI0gOFbwxHdM6yYPEuE3cJx7A4wSQjCH21mUKsg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.20.2': + resolution: {integrity: sha512-2cD3RGB5byusLS0DAX1Nvl5MLiv7OoGgQrRs+94dTalqjvK8lGKzxxJhXoVojgx2qcROyIUAIDXFdTqv6NIHaA==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.20.2': + resolution: {integrity: sha512-S593Kmhc98+5zdzGet4GrZEBEBGl4vVtqg/MPfW8dCRf9qDRNYSkhBsIzlhQe9JWiohe9oB9LW5meibwOgRmww==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.20.2': + resolution: {integrity: sha512-bW41aWLYgBv/coJUIT85mkN3kk1VBKsM8tlwB5S/s446Mgc7r8t5TX7kA8kCR2UbwDedOK51i/85/x/rM0ZXbg==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.20.2': + resolution: {integrity: sha512-wBMf3J1L5ogvU8p8ifHkknDXWn1zdZ2epkqpt2MkUaZynE3G77rrFU9frcO+Pu1FQJQ5xCDTKcYUUcJCDD00rg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.20.2': + resolution: {integrity: sha512-w+VMzOkIq2XDGg6Ybzr74RlBZvJQnuIdKpVusQSXCXknvxwAwbO457LmoavhZWl06Lcsk9YDx1X2k0zb+iJQmw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.20.2': + resolution: {integrity: sha512-wpjnbvbi3A13b0DvijE45DRYDvwcP5Ttz7RTMkPWTkF1s6AHuo6O2UcwGyaogMAGa1QOOzFYfp5u4YQwMOQx5g==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.20.2': + resolution: {integrity: sha512-YuSSdtgUt1dFBTNYUb+2TA5j0Hd0eDXE0bVISjUvTCqmoaGsGLwW+rKI7p1eLQ1r7RESwBAvUwcY1qP2Wl3Lyw==} + engines: {node: '>= 14.0.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.8': + resolution: {integrity: sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.26.8': + resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.24': + resolution: {integrity: sha512-06ZWXZx3PHCE+02zn+iIGOKKNgE3kyPd0Yh7IUEIa0bCYI6UmGlsYYghRx8As9TnTNYMCEiy5V0zI4Jb6EY6XA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@rollup/rollup-android-arm-eabi@4.34.6': + resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.34.6': + resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.34.6': + resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.34.6': + resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.34.6': + resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.34.6': + resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.34.6': + resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.34.6': + resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-s390x-gnu@4.34.6': + resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.34.6': + resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.34.6': + resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.34.6': + resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.34.6': + resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.34.6': + resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.3.2': + resolution: {integrity: sha512-s7vyL3LzUKm3Qwf36zRWlavX9BQMZTIq9B1almM63M5xBuSldnsTHCmsXzoF/Kyw4k7Xgas7yAyJz9VR/vcP1A==} + + '@shikijs/engine-javascript@2.3.2': + resolution: {integrity: sha512-w3IEMu5HfL/OaJTsMbIfZ1HRPnWVYRANeDtmsdIIEgUOcLjzFJFQwlnkckGjKHekEzNqlMLbgB/twnfZ/EEAGg==} + + '@shikijs/engine-oniguruma@2.3.2': + resolution: {integrity: sha512-vikMY1TroyZXUHIXbMnvY/mjtOxMn+tavcfAeQPgWS9FHcgFSUoEtywF5B5sOLb9NXb8P2vb7odkh3nj15/00A==} + + '@shikijs/langs@2.3.2': + resolution: {integrity: sha512-UqI6bSxFzhexIJficZLKeB1L2Sc3xoNiAV0yHpfbg5meck93du+EKQtsGbBv66Ki53XZPhnR/kYkOr85elIuFw==} + + '@shikijs/themes@2.3.2': + resolution: {integrity: sha512-QAh7D/hhfYKHibkG2tti8vxNt3ekAH5EqkXJeJbTh7FGvTCWEI7BHqNCtMdjFvZ0vav5nvUgdvA7/HI7pfsB4w==} + + '@shikijs/transformers@2.3.2': + resolution: {integrity: sha512-2HDnJumw8A/9GecRpTgvfqSbPjEbJ4DPWq5J++OVP1gNMLvbV0MqFsP4canqRNM1LqB7VmWY45Stipb0ZIJ+0A==} + + '@shikijs/types@2.3.2': + resolution: {integrity: sha512-CBaMY+a3pepyC4SETi7+bSzO0f6hxEQJUUuS4uD7zppzjmrN4ZRtBqxaT+wOan26CR9eeJ5iBhc4qvWEwn7Eeg==} + + '@shikijs/vscode-textmate@10.0.1': + resolution: {integrity: sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/devtools-api@7.7.1': + resolution: {integrity: sha512-Cexc8GimowoDkJ6eNelOPdYIzsu2mgNyp0scOQ3tiaYSb9iok6LOESSsJvHaI+ib3joRfqRJNLkHFjhNuWA5dg==} + + '@vue/devtools-kit@7.7.1': + resolution: {integrity: sha512-yhZ4NPnK/tmxGtLNQxmll90jIIXdb2jAhPF76anvn5M/UkZCiLJy28bYgPIACKZ7FCosyKoaope89/RsFJll1w==} + + '@vue/devtools-shared@7.7.1': + resolution: {integrity: sha512-BtgF7kHq4BHG23Lezc/3W2UhK2ga7a8ohAIAGJMBr4BkxUFzhqntQtCiuL1ijo2ztWnmusymkirgqUrXoQKumA==} + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vueuse/core@12.5.0': + resolution: {integrity: sha512-GVyH1iYqNANwcahAx8JBm6awaNgvR/SwZ1fjr10b8l1HIgDp82ngNbfzJUgOgWEoxjL+URAggnlilAEXwCOZtg==} + + '@vueuse/integrations@12.5.0': + resolution: {integrity: sha512-HYLt8M6mjUfcoUOzyBcX2RjpfapIwHPBmQJtTmXOQW845Y/Osu9VuTJ5kPvnmWJ6IUa05WpblfOwZ+P0G4iZsQ==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.5.0': + resolution: {integrity: sha512-Ui7Lo2a7AxrMAXRF+fAp9QsXuwTeeZ8fIB9wsLHqzq9MQk+2gMYE2IGJW48VMJ8ecvCB3z3GsGLKLbSasQ5Qlg==} + + '@vueuse/shared@12.5.0': + resolution: {integrity: sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==} + + algoliasearch@5.20.2: + resolution: {integrity: sha512-8evxG++iWyWnhng3g5RP+kwn6j+2vKLfew8pVoekn87FcfsDm92zJXKwSrU6pl+m5eAbGFhFF/gCYEQiRdbPlA==} + engines: {node: '>= 14.0.0'} + + birpc@0.2.19: + resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + focus-trap@7.6.4: + resolution: {integrity: sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.4: + resolution: {integrity: sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + minisearch@7.1.1: + resolution: {integrity: sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-to-es@3.1.0: + resolution: {integrity: sha512-BJ3Jy22YlgejHSO7Fvmz1kKazlaPmRSUH+4adTDUS/dKQ4wLxI+gALZ8updbaux7/m7fIlpgOZ5fp/Inq5jUAw==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.2: + resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.25.4: + resolution: {integrity: sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.34.6: + resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@2.3.2: + resolution: {integrity: sha512-UZhz/gsUz7DHFbQBOJP7eXqvKyYvMGramxQiSDc83M/7OkWm6OdVHAReEc3vMLh6L6TRhgL9dvhXz9XDkCDaaw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.14: + resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress@1.6.3: + resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2) + '@algolia/client-search': 5.20.2 + algoliasearch: 5.20.2 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)': + dependencies: + '@algolia/client-search': 5.20.2 + algoliasearch: 5.20.2 + + '@algolia/client-abtesting@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/client-analytics@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/client-common@5.20.2': {} + + '@algolia/client-insights@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/client-personalization@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/client-query-suggestions@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/client-search@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/ingestion@1.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/monitoring@1.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/recommend@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + '@algolia/requester-browser-xhr@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + + '@algolia/requester-fetch@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + + '@algolia/requester-node-http@5.20.2': + dependencies: + '@algolia/client-common': 5.20.2 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.8': + dependencies: + '@babel/types': 7.26.8 + + '@babel/types@7.26.8': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.20.2)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.20.2)(search-insights@2.17.3) + preact: 10.25.4 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.20.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.20.2)(algoliasearch@5.20.2) + '@docsearch/css': 3.8.2 + algoliasearch: 5.20.2 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.24': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@rollup/rollup-android-arm-eabi@4.34.6': + optional: true + + '@rollup/rollup-android-arm64@4.34.6': + optional: true + + '@rollup/rollup-darwin-arm64@4.34.6': + optional: true + + '@rollup/rollup-darwin-x64@4.34.6': + optional: true + + '@rollup/rollup-freebsd-arm64@4.34.6': + optional: true + + '@rollup/rollup-freebsd-x64@4.34.6': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.34.6': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.34.6': + optional: true + + '@rollup/rollup-linux-x64-musl@4.34.6': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.34.6': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.34.6': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.34.6': + optional: true + + '@shikijs/core@2.3.2': + dependencies: + '@shikijs/engine-javascript': 2.3.2 + '@shikijs/engine-oniguruma': 2.3.2 + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.4 + + '@shikijs/engine-javascript@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + oniguruma-to-es: 3.1.0 + + '@shikijs/engine-oniguruma@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + + '@shikijs/langs@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + + '@shikijs/themes@2.3.2': + dependencies: + '@shikijs/types': 2.3.2 + + '@shikijs/transformers@2.3.2': + dependencies: + '@shikijs/core': 2.3.2 + '@shikijs/types': 2.3.2 + + '@shikijs/types@2.3.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.1': {} + + '@types/estree@1.0.6': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.20': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.1(vite@5.4.14)(vue@3.5.13)': + dependencies: + vite: 5.4.14 + vue: 3.5.13 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.26.8 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.26.8 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.2 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/devtools-api@7.7.1': + dependencies: + '@vue/devtools-kit': 7.7.1 + + '@vue/devtools-kit@7.7.1': + dependencies: + '@vue/devtools-shared': 7.7.1 + birpc: 0.2.19 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.1': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13)': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13 + + '@vue/shared@3.5.13': {} + + '@vueuse/core@12.5.0': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 12.5.0 + '@vueuse/shared': 12.5.0 + vue: 3.5.13 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.5.0(focus-trap@7.6.4)': + dependencies: + '@vueuse/core': 12.5.0 + '@vueuse/shared': 12.5.0 + vue: 3.5.13 + optionalDependencies: + focus-trap: 7.6.4 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.5.0': {} + + '@vueuse/shared@12.5.0': + dependencies: + vue: 3.5.13 + transitivePeerDependencies: + - typescript + + algoliasearch@5.20.2: + dependencies: + '@algolia/client-abtesting': 5.20.2 + '@algolia/client-analytics': 5.20.2 + '@algolia/client-common': 5.20.2 + '@algolia/client-insights': 5.20.2 + '@algolia/client-personalization': 5.20.2 + '@algolia/client-query-suggestions': 5.20.2 + '@algolia/client-search': 5.20.2 + '@algolia/ingestion': 1.20.2 + '@algolia/monitoring': 1.20.2 + '@algolia/recommend': 5.20.2 + '@algolia/requester-browser-xhr': 5.20.2 + '@algolia/requester-fetch': 5.20.2 + '@algolia/requester-node-http': 5.20.2 + + birpc@0.2.19: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + csstype@3.1.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + emoji-regex-xs@1.0.0: {} + + entities@4.5.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + focus-trap@7.6.4: + dependencies: + tabbable: 6.2.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + is-what@4.1.16: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mark.js@8.11.1: {} + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + minisearch@7.1.1: {} + + mitt@3.0.1: {} + + nanoid@3.3.8: {} + + oniguruma-to-es@3.1.0: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.0.1 + regex-recursion: 6.0.2 + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.2: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.25.4: {} + + property-information@6.5.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + rollup@4.34.6: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.34.6 + '@rollup/rollup-android-arm64': 4.34.6 + '@rollup/rollup-darwin-arm64': 4.34.6 + '@rollup/rollup-darwin-x64': 4.34.6 + '@rollup/rollup-freebsd-arm64': 4.34.6 + '@rollup/rollup-freebsd-x64': 4.34.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 + '@rollup/rollup-linux-arm-musleabihf': 4.34.6 + '@rollup/rollup-linux-arm64-gnu': 4.34.6 + '@rollup/rollup-linux-arm64-musl': 4.34.6 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 + '@rollup/rollup-linux-riscv64-gnu': 4.34.6 + '@rollup/rollup-linux-s390x-gnu': 4.34.6 + '@rollup/rollup-linux-x64-gnu': 4.34.6 + '@rollup/rollup-linux-x64-musl': 4.34.6 + '@rollup/rollup-win32-arm64-msvc': 4.34.6 + '@rollup/rollup-win32-ia32-msvc': 4.34.6 + '@rollup/rollup-win32-x64-msvc': 4.34.6 + fsevents: 2.3.3 + + search-insights@2.17.3: {} + + shiki@2.3.2: + dependencies: + '@shikijs/core': 2.3.2 + '@shikijs/engine-javascript': 2.3.2 + '@shikijs/engine-oniguruma': 2.3.2 + '@shikijs/langs': 2.3.2 + '@shikijs/themes': 2.3.2 + '@shikijs/types': 2.3.2 + '@shikijs/vscode-textmate': 10.0.1 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + tabbable@6.2.0: {} + + trim-lines@3.0.1: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@5.4.14: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.2 + rollup: 4.34.6 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@1.6.3(@algolia/client-search@5.20.2)(postcss@8.5.2)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.20.2)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.24 + '@shikijs/core': 2.3.2 + '@shikijs/transformers': 2.3.2 + '@shikijs/types': 2.3.2 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.1(vite@5.4.14)(vue@3.5.13) + '@vue/devtools-api': 7.7.1 + '@vue/shared': 3.5.13 + '@vueuse/core': 12.5.0 + '@vueuse/integrations': 12.5.0(focus-trap@7.6.4) + focus-trap: 7.6.4 + mark.js: 8.11.1 + minisearch: 7.1.1 + shiki: 2.3.2 + vite: 5.4.14 + vue: 3.5.13 + optionalDependencies: + postcss: 8.5.2 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vue@3.5.13: + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13) + '@vue/shared': 3.5.13 + + zwitch@2.0.4: {} diff --git a/docs/public/logo_small.png b/docs/public/logo_small.png new file mode 100644 index 000000000..04bf25727 Binary files /dev/null and b/docs/public/logo_small.png differ diff --git a/docs/showcase.md b/docs/showcase.md new file mode 100644 index 000000000..50c6ce46f --- /dev/null +++ b/docs/showcase.md @@ -0,0 +1,127 @@ +# 优秀案例 + +这里收录了一些使用 FileCodeBox 搭建的优秀站点。如果你也部署了 FileCodeBox,欢迎提交 PR 将你的站点添加到这里! + +## 官方演示站 + +
+ +
+ +### 🌟 FileCodeBox Demo + +- **网址**:[share.lanol.cn](https://share.lanol.cn) +- **简介**:官方演示站点,体验最新功能 +- **特点**:稳定运行,功能完整 + +
+ +
+ +## 社区站点 + +::: tip 提交你的站点 +如果你使用 FileCodeBox 搭建了自己的文件分享服务,欢迎通过以下方式提交: + +1. 在 [GitHub](https://github.com/vastsa/FileCodeBox) 提交 PR,编辑此页面 +2. 在 [Issues](https://github.com/vastsa/FileCodeBox/issues) 中提交你的站点信息 +3. 加入 QQ 群 739673698 联系管理员 +::: + + + +
+ +
+ +### 取文件 + +- **网址**:[www.quwenjian.cn/fby.html](https://www.quwenjian.cn/fby.html) +- **简介**:取文件 - 存储无界,便携无限 +- **特点**:永久免费的文件中转站 +- **运营者**:取文件&取文件网盘 + +
+ +
+ +### 潘多拉盒子 + +- **网址**:[pan.duo.la](https://pan.duo.la) +- **简介**:潘多拉盒子 +- **特点**:经典1.6版本 +- **运营者**:五行缺心眼 + +
+ +
+ +## 提交要求 + +为了保证收录站点的质量,请确保你的站点满足以下条件: + +1. **稳定运行**:站点需要稳定运行,能够正常访问 +2. **合法合规**:站点内容需要合法合规,不得包含违法违规内容 +3. **保留版权**:建议保留 FileCodeBox 的版权信息 +4. **HTTPS 支持**:建议启用 HTTPS 加密访问 + +## 案例展示模板 + +如果你想提交站点,请按照以下格式: + +```markdown +### 站点名称 + +- **网址**:[域名](https://域名) +- **简介**:一句话描述站点用途 +- **特点**:站点的特色功能或亮点 +- **运营者**:可选,你的名字或组织 +``` + + diff --git a/images/img.png b/images/img.png deleted file mode 100644 index 93f634e79..000000000 Binary files a/images/img.png and /dev/null differ diff --git a/images/img_1.png b/images/img_1.png deleted file mode 100644 index 0616e6e4c..000000000 Binary files a/images/img_1.png and /dev/null differ diff --git a/images/img_10.png b/images/img_10.png deleted file mode 100644 index cb9d3d230..000000000 Binary files a/images/img_10.png and /dev/null differ diff --git a/images/img_11.png b/images/img_11.png deleted file mode 100644 index ff81eec25..000000000 Binary files a/images/img_11.png and /dev/null differ diff --git a/images/img_12.png b/images/img_12.png deleted file mode 100644 index 3ca33d994..000000000 Binary files a/images/img_12.png and /dev/null differ diff --git a/images/img_13.png b/images/img_13.png deleted file mode 100644 index 8e2dd805b..000000000 Binary files a/images/img_13.png and /dev/null differ diff --git a/images/img_2.png b/images/img_2.png deleted file mode 100644 index 6f280ad37..000000000 Binary files a/images/img_2.png and /dev/null differ diff --git a/images/img_3.png b/images/img_3.png deleted file mode 100644 index ec5f2a058..000000000 Binary files a/images/img_3.png and /dev/null differ diff --git a/images/img_4.png b/images/img_4.png deleted file mode 100644 index ce6e75c49..000000000 Binary files a/images/img_4.png and /dev/null differ diff --git a/images/img_5.png b/images/img_5.png deleted file mode 100644 index e3b92ca54..000000000 Binary files a/images/img_5.png and /dev/null differ diff --git a/images/img_6.png b/images/img_6.png deleted file mode 100644 index 492856603..000000000 Binary files a/images/img_6.png and /dev/null differ diff --git a/images/img_7.png b/images/img_7.png deleted file mode 100644 index f263b313f..000000000 Binary files a/images/img_7.png and /dev/null differ diff --git a/images/img_8.png b/images/img_8.png deleted file mode 100644 index d8b688726..000000000 Binary files a/images/img_8.png and /dev/null differ diff --git a/images/img_9.png b/images/img_9.png deleted file mode 100644 index 40d19f184..000000000 Binary files a/images/img_9.png and /dev/null differ diff --git a/main.py b/main.py index 00b0a4aee..fee4d6c11 100644 --- a/main.py +++ b/main.py @@ -1,247 +1,168 @@ +# @Time : 2023/8/9 23:23 +# @Author : Lan +# @File : main.py +# @Software: PyCharm import asyncio -import datetime -import uuid -from pathlib import Path - -from pydantic import BaseModel -from sqlalchemy import select, func, update -from sqlalchemy.ext.asyncio import AsyncSession -from starlette.requests import Request -from starlette.responses import HTMLResponse, FileResponse, RedirectResponse -from starlette.staticfiles import StaticFiles -from core.utils import error_ip_limit, upload_ip_limit, get_code, storage, delete_expire_files, get_token, \ - get_expire_info -from core.depends import admin_required -from fastapi import FastAPI, Depends, UploadFile, Form, File, HTTPException, BackgroundTasks, Header -from core.database import init_models, Options, Codes, get_session -from settings import settings - -# 实例化FastAPI -app = FastAPI(debug=settings.DEBUG, redoc_url=None, ) - - -@app.on_event('startup') -async def startup(s: AsyncSession = Depends(get_session)): +import time +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from tortoise import Tortoise +from tortoise.contrib.fastapi import register_tortoise + +from apps.admin.views import admin_api +from apps.base.models import KeyValue +from apps.base.utils import ip_limit +from apps.base.views import share_api, chunk_api, presign_api +from core.config import ensure_settings_row, refresh_settings +from core.database import db_startup_lock, get_db_config, init_db +from core.logger import logger +from core.response import APIResponse +from core.settings import settings, BASE_DIR, DEFAULT_CONFIG +from core.tasks import delete_expire_files, clean_incomplete_uploads +from core.utils import hash_password, is_password_hashed + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("正在初始化应用...") # 初始化数据库 - await init_models(s) - # 启动后台任务,不定时删除过期文件 - asyncio.create_task(delete_expire_files()) - - -# 数据存储文件夹 -DATA_ROOT = Path(settings.DATA_ROOT) -if not DATA_ROOT.exists(): - DATA_ROOT.mkdir(parents=True) - -# 静态文件夹,这个固定就行了,静态资源都放在这里 -app.mount('/static', StaticFiles(directory='./static'), name="static") - -# 首页页面 -index_html = open('templates/index.html', 'r', encoding='utf-8').read() -# 管理页面 -admin_html = open('templates/admin.html', 'r', encoding='utf-8').read() - + await init_db() + + # 加载配置(多进程下串行化启动写操作) + async with db_startup_lock(): + await load_config() + app.mount( + "/assets", + StaticFiles(directory=f"./{settings.themesSelect}/assets"), + name="assets", + ) -@app.get('/') -async def index(): + # 启动后台任务 + task = asyncio.create_task(delete_expire_files()) + chunk_cleanup_task = asyncio.create_task(clean_incomplete_uploads()) + logger.info("应用初始化完成") + + try: + yield + finally: + # 清理操作 + logger.info("正在关闭应用...") + task.cancel() + chunk_cleanup_task.cancel() + await asyncio.gather(task, chunk_cleanup_task, return_exceptions=True) + await Tortoise.close_connections() + logger.info("应用已关闭") + + +async def load_config(): + await ensure_settings_row() + await KeyValue.update_or_create( + key="sys_start", defaults={"value": int(time.time() * 1000)} + ) + await refresh_settings() + + await migrate_password_to_hash() + + ip_limit["error"].minutes = settings.errorMinute + ip_limit["error"].count = settings.errorCount + ip_limit["upload"].minutes = settings.uploadMinute + ip_limit["upload"].count = settings.uploadCount + + +async def migrate_password_to_hash(): + if not is_password_hashed(settings.admin_token): + hashed = hash_password(settings.admin_token) + settings.admin_token = hashed + config_record = await KeyValue.filter(key="settings").first() + if config_record and config_record.value: + config_record.value["admin_token"] = hashed + await config_record.save() + logger.info("已将管理员密码迁移为哈希存储") + + +app = FastAPI(lifespan=lifespan) + +@app.middleware("http") +async def refresh_settings_middleware(request, call_next): + await refresh_settings() + return await call_next(request) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 使用 register_tortoise 来添加异常处理器 +register_tortoise( + app, + config=get_db_config(), + generate_schemas=False, + add_exception_handlers=True, +) + +app.include_router(share_api) +app.include_router(chunk_api) +app.include_router(presign_api) +app.include_router(admin_api) + + +@app.exception_handler(404) +@app.get("/") +async def index(request=None, exc=None): return HTMLResponse( - index_html - .replace('{{title}}', settings.TITLE) - .replace('{{description}}', settings.DESCRIPTION) - .replace('{{keywords}}', settings.KEYWORDS) - .replace("'{{fileSizeLimit}}'", str(settings.FILE_SIZE_LIMIT)) + content=open( + BASE_DIR / f"{settings.themesSelect}/index.html", "r", encoding="utf-8" + ) + .read() + .replace("{{title}}", str(settings.name)) + .replace("{{description}}", str(settings.description)) + .replace("{{keywords}}", str(settings.keywords)) + .replace("{{opacity}}", str(settings.opacity)) + .replace('"/assets/', '"assets/') + .replace("{{background}}", str(settings.background)), + media_type="text/html", + headers={"Cache-Control": "no-cache"}, ) -@app.get(f'/{settings.ADMIN_ADDRESS}', description='管理页面') -async def admin(): - return HTMLResponse( - admin_html - .replace('{{title}}', settings.TITLE) - .replace('{{description}}', settings.DESCRIPTION) - .replace('{{admin_address}}', settings.ADMIN_ADDRESS) - .replace('{{keywords}}', settings.KEYWORDS) +@app.get("/robots.txt") +async def robots(): + return HTMLResponse(content=settings.robotsText, media_type="text/plain") + + +@app.post("/") +async def get_config(): + return APIResponse( + detail={ + "name": settings.name, + "description": settings.description, + "explain": settings.page_explain, + "uploadSize": settings.uploadSize, + "expireStyle": settings.expireStyle, + "enableChunk": settings.enableChunk, + "openUpload": settings.openUpload, + "notify_title": settings.notify_title, + "notify_content": settings.notify_content, + "show_admin_address": settings.showAdminAddr, + "max_save_seconds": settings.max_save_seconds, + } ) -@app.post(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='查询数据库列表') -async def admin_post(page: int = Form(default=1), size: int = Form(default=10), s: AsyncSession = Depends(get_session)): - infos = (await s.execute(select(Codes).offset((page - 1) * size).limit(size))).scalars().all() - data = [{ - 'id': info.id, - 'code': info.code, - 'name': info.name, - 'exp_time': info.exp_time, - 'count': info.count, - 'text': info.text if info.type == 'text' else await storage.get_url(info), - } for info in infos] - return { - 'detail': '查询成功', - 'data': data, - 'paginate': { - 'page': page, - 'size': size, - 'total': (await s.execute(select(func.count(Codes.id)))).scalar() - }} - - -@app.delete(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='删除数据库记录') -async def admin_delete(code: str, s: AsyncSession = Depends(get_session)): - # 找到相应记录 - query = select(Codes).where(Codes.code == code) - # 找到第一条记录 - file = (await s.execute(query)).scalars().first() - # 如果记录存在,并且不是文本 - if file and file.type != 'text': - # 删除文件 - await storage.delete_file(file.text) - # 删除数据库记录 - await s.delete(file) - await s.commit() - return {'detail': '删除成功'} - - -@app.get(f'/{settings.ADMIN_ADDRESS}/config', description='获取系统配置', dependencies=[Depends(admin_required)]) -async def config(s: AsyncSession = Depends(get_session)): - # 查询数据库 - data = {} - for i in (await s.execute(select(Options))).scalars().all(): - data[i.key] = i.value - return {'detail': '获取成功', 'data': data, 'menus': [ - {'key': 'INSTALL', 'name': '版本信息'}, - {'key': 'WEBSITE', 'name': '网站设置'}, - {'key': 'SHARE', 'name': '分享设置'}, - {'key': 'BANNERS', 'name': 'Banner'}, - ]} - - -@app.patch(f'/{settings.ADMIN_ADDRESS}', dependencies=[Depends(admin_required)], description='修改数据库数据') -async def admin_patch(request: Request, s: AsyncSession = Depends(get_session)): - data = await request.json() - data.pop('INSTALL') - for key, value in data.items(): - await s.execute(update(Options).where(Options.key == key).values(value=value)) - await settings.update(key, value) - await s.commit() - await settings.updates([[i.id, i.key, i.value] for i in (await s.execute(select(Options))).scalars().all()]) - return {'detail': '修改成功'} - - -@app.post('/') -async def index(code: str, ip: str = Depends(error_ip_limit), s: AsyncSession = Depends(get_session)): - query = select(Codes).where(Codes.code == code) - info = (await s.execute(query)).scalars().first() - if not info: - error_count = settings.ERROR_COUNT - error_ip_limit.add_ip(ip) - raise HTTPException(status_code=404, detail=f"取件码错误,{error_count}次后将被禁止{settings.ERROR_MINUTE}分钟") - if (info.exp_time and info.exp_time < datetime.datetime.now()) or info.count == 0: - raise HTTPException(status_code=404, detail="取件码已失效,请联系寄件人") - await s.execute(update(Codes).where(Codes.id == info.id).values(count=info.count - 1)) - await s.commit() - if info.type != 'text': - info.text = f'/select?code={info.code}&token={await get_token(code, ip)}' - return { - 'detail': f'取件成功,请立即下载,避免失效!', - 'data': {'type': info.type, 'text': info.text, 'name': info.name, 'code': info.code} - } - - -@app.get('/banner') -async def banner(request: Request): - return { - 'detail': '查询成功', - 'data': settings.BANNERS, - 'enable': request.headers.get('pwd', '') == settings.ADMIN_PASSWORD or settings.ENABLE_UPLOAD, - } - - -@app.get('/select') -async def get_file(code: str, token: str, ip: str = Depends(error_ip_limit), s: AsyncSession = Depends(get_session)): - # 验证token - if token != await get_token(code, ip): - error_ip_limit.add_ip(ip) - raise HTTPException(status_code=403, detail="口令错误,或已过期,次数过多将被禁止访问") - # 查出数据库记录 - query = select(Codes).where(Codes.code == code) - info = (await s.execute(query)).scalars().first() - # 如果记录不存在,IP错误次数+1 - if not info: - error_ip_limit.add_ip(ip) - raise HTTPException(status_code=404, detail="口令不存在,次数过多将被禁止访问") - # 如果是文本,直接返回 - if info.type == 'text': - return {'detail': '查询成功', 'data': info.text} - # 如果是文件,返回文件 - elif storage.NAME != 'filesystem': - # 重定向到文件存储服务器 - return RedirectResponse(await storage.get_url(info)) - else: - filepath = await storage.get_filepath(info.text) - return FileResponse(filepath, filename=info.name) - - -@app.post('/file/create/') -async def create_file(): - # 生成随机字符串 - return {'code': 200, 'data': await storage.create_upload_file()} - - -@app.post('/file/upload/{file_key}/') -async def upload_file(file_key: str, file: bytes = File(...), chunk_index: int = Form(...), - total_chunks: int = Form(...)): - await storage.save_chunk_file(file_key, file, chunk_index, total_chunks) - return {'code': 200} - - -@app.get('/file/merge/{file_key}/') -async def merge_chunks(file_key: str, file_name: str, total_chunks: int): - return {'code': 200, 'data': await storage.merge_chunks(file_key, file_name, total_chunks)} - - -class ShareDataModel(BaseModel): - text: str - size: int = 0 - exp_style: str - exp_value: int - type: str - name: str - key: str = uuid.uuid4().hex - - -@app.post('/share/file/', dependencies=[Depends(admin_required)], description='分享文件') -async def share_file(file_model: ShareDataModel, s: AsyncSession = Depends(get_session), - ip: str = Depends(error_ip_limit)): - exp_error, exp_time, exp_count, code = await get_expire_info(file_model.exp_style, file_model.exp_value, s) - if exp_error: - raise HTTPException(status_code=400, detail='过期值异常') - s.add(Codes(code=code, text=file_model.text, size=file_model.size, type=file_model.type, name=file_model.name, - count=exp_count, exp_time=exp_time, key=file_model.key)) - await s.commit() - upload_ip_limit.add_ip(ip) - return { - 'detail': '分享成功,请点击我的文件按钮查看上传列表', - 'data': {'code': code, 'key': file_model.key, 'name': file_model.name} - } - - -@app.post('/share/text/', dependencies=[Depends(admin_required)]) -async def share_text(text_model: ShareDataModel, s: AsyncSession = Depends(get_session), - ip: str = Depends(error_ip_limit)): - exp_error, exp_time, exp_count, code = await get_expire_info(text_model.exp_style, text_model.exp_value, s, ) - if exp_error: - raise HTTPException(status_code=400, detail='过期值异常') - exp_status, exp_time, exp_count, code = await get_expire_info(text_model.exp_style, text_model.exp_value, s) - size, _text, _type, name, key = len(text_model.text), text_model.text, 'text', '文本分享', text_model.key - s.add(Codes(code=code, text=_text, size=size, type=_type, name=name, count=exp_count, exp_time=exp_time, key=key)) - await s.commit() - upload_ip_limit.add_ip(ip) - return { - 'detail': '分享成功,请点击我的文件按钮查看上传列表', - 'data': {'code': code, 'key': key, 'name': name} - } - - -if __name__ == '__main__': +if __name__ == "__main__": import uvicorn - uvicorn.run('main:app', host='0.0.0.0', port=settings.PORT, reload=settings.DEBUG) + uvicorn.run( + app="main:app", + host=settings.serverHost, + port=settings.serverPort, + reload=False, + workers=settings.serverWorkers, + ) diff --git a/readme.md b/readme.md index 8e6ca479f..3733798f1 100644 --- a/readme.md +++ b/readme.md @@ -1,308 +1,471 @@
-

文件快递柜-轻量

-

FileCodeBox-Lite

-

匿名口令分享文本,文件,像拿快递一样取文件

-

交流Q群:739673698,欢迎各位集思广益,项目构思重构中

-
- -![banner](https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/static/banners/img_1.png) ---- +# FileCodeBox -[简体中文](./readme.md) | [English](./readme_en.md) +### 文件快递柜 - 匿名口令分享文本和文件 -## 主要特色 +FileCodeBox Logo -- [x] 轻量简洁:Fastapi+Sqlite3+Vue2+ElementUI -- [x] 轻松上传:复制粘贴,拖拽选择 -- [x] 多种类型:文本,文件 -- [x] 防止爆破:错误次数限制 -- [x] 防止滥用:IP限制上传次数 -- [x] 口令分享:随机口令,存取文件,自定义次数以及有效期 -- [x] 匿名分享:无需注册,无需登录 -- [x] 管理面板:查看所有文件,删除文件 -- [x] 一键部署:docker一键部署 -- [x] 自由拓展:阿里云OSS、本地文件流,可根据需求在storage文件中新增存储引擎 -- [x] 简单明了:适合新手练手项目 +像拿快递一样取文件,无需注册,输入口令即可获取 -## 部署方式 +[![GitHub stars](https://img.shields.io/github/stars/vastsa/FileCodeBox?style=flat-square&logo=github)](https://github.com/vastsa/FileCodeBox/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/vastsa/FileCodeBox?style=flat-square&logo=github)](https://github.com/vastsa/FileCodeBox/network) +[![GitHub issues](https://img.shields.io/github/issues/vastsa/FileCodeBox?style=flat-square&logo=github)](https://github.com/vastsa/FileCodeBox/issues) +[![GitHub license](https://img.shields.io/github/license/vastsa/FileCodeBox?style=flat-square)](https://github.com/vastsa/FileCodeBox/blob/master/LICENSE) +[![Docker Pulls](https://img.shields.io/docker/pulls/lanol/filecodebox?style=flat-square&logo=docker)](https://hub.docker.com/r/lanol/filecodebox) -### Docker一键部署 +[![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.68+-009688?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com) +[![Vue.js](https://img.shields.io/badge/Vue.js-3.x-4FC08D?style=flat-square&logo=vue.js&logoColor=white)](https://vuejs.org) -#### AMD 开发版(不稳定,待测试,新增分片异步上传,永久存储,不建议使用,很多没发现的坑) +[English](./readme_en.md) | [在线演示](https://share.lanol.cn) | [部署教程](https://github.com/vastsa/FileCodeBox/wiki/部署教程) | [常见问题](https://github.com/vastsa/FileCodeBox/wiki/常见问题) | [QQ群: 739673698](https://qm.qq.com/q/PemPzhdEIM) ```bash -docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:pre - +# 🚀 一键部署 +docker run -d -p 12345:12345 -v /opt/FileCodeBox:/app/data --name filecodebox lanol/filecodebox:latest +# 国内镜像(如果上面拉取缓慢): docker.cnb.cool/aixk/filecodebox ``` -#### AMD - -```bash -docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest -``` + -#### ARM +--- -```bash -docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:arm -``` +## 目录 -### 宝塔部署 +- [项目简介](#-项目简介) +- [功能特性](#-功能特性) +- [界面预览](#-界面预览) +- [快速开始](#-快速开始) +- [使用指南](#-使用指南) +- [开发指南](#-开发指南) +- [常见问题](#-常见问题) +- [贡献指南](#-贡献指南) +- [项目统计](#-项目统计) +- [免责声明](#-免责声明) -https://www.yuque.com/lxyo/work/lc1oe0xqk8t9b976 +--- -### 更新方式 +## 📝 项目简介 -```bash -// 停止容器并删除 -docker stop filecodebox && docker rm filecodebox -// 重新运行容器 -docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest -``` +FileCodeBox 是一个轻量级的文件分享工具,基于 **FastAPI + Vue3** 开发。用户可以通过简单的方式匿名分享文本和文件,接收者只需输入提取码即可获取内容——就像从快递柜取出快递一样简单。 -### 1.6版本注意 +### 应用场景 -这一版改变比较大,如果出现问题可以尝试清空/opt/FileCodeBox目录,有问题欢迎反馈留言 -注意,如果是第一次安装,请查看docker日志获取初始密码和后台地址,参考指令 -后台本地文件列表,需要将服务器文件移动至目录/opt/FileCodeBox/data/locals,这样就可以显示了。 +| 场景 | 描述 | +|------|------| +| 📁 **临时文件分享** | 快速分享文件,无需注册登录 | +| 📝 **代码片段分享** | 分享代码、配置文件等文本内容 | +| 🕶️ **匿名文件传输** | 保护隐私的点对点传输 | +| 🔄 **跨设备传输** | 在不同设备间快速同步文件 | +| 💾 **临时存储** | 支持自定义过期时间的云存储 | +| 🌐 **私有服务** | 搭建企业或个人专属分享服务 | -```bash -docker logs filecodebox +--- -``` +## ✨ 功能特性 -### 其他方式 + + + + + + + +
-仅供参考,历史版本->[部署文档](https://www.yuque.com/lxyo/work/zd0kvzy7fofx6w7v) +### 🚀 轻量高效 +- FastAPI + SQLite3 后端 +- Vue3 + Element Plus 前端 +- Docker 一键部署 +- 资源占用极低 -## 项目规划 + -2022年12月14日 -这个项目的灵感来源于丁丁快传,然后写了这么一个基于本机存储的快传系统,本系统主要是以轻量,单用户,离线环境(`私有化` -)为主,因此也不需要加太多东西,所以其实这个项目到这基本功能已经完成了,剩下的就是维护和完善现有功能。 -也不会再加入新的大功能了,如果你有更好的想法和建议欢迎提issue。 +### 🔒 安全可靠 +- IP 上传频率限制 +- 提取码错误次数限制 +- 文件自动过期清理 +- 支持管理员认证 -## 预览 + -### 例站 +### 📤 便捷上传 +- 拖拽上传 +- 复制粘贴上传 +- 命令行 curl 上传 +- 批量文件上传 -[https://share.lanol.cn](https://share.lanol.cn) +
-### 暗黑模式 +### 🎫 灵活分享 +- 随机/自定义提取码 +- 可设置有效期(时间/次数) +- 支持永久有效 +- 文本和文件统一管理 - + + - -
-
-寄文件 +### 💾 多存储支持 +- 本地文件系统 +- S3 兼容存储 +- [OneDrive](./docs/guide/storage-onedrive.md) +- [OpenDAL](./docs/guide/storage-opendal.md) -寄文件 + + +### 🌍 国际化 +- 简体中文 +- 繁体中文 +- English +- 响应式设计 / 深色模式
-### 寄件 +--- - - - - - - - - -
-寄文件 - -寄文本 -
-寄文本 -
+## 🖼️ 界面预览 -### 取件 +> 前端源码仓库:[2024主题](https://github.com/vastsa/FileCodeBoxFronted) | [2023主题](https://github.com/vastsa/FileCodeBoxFronted2023) - - - - +
+🎨 新版界面 (2024) +
+
+
-取件 - -取件码错误 -
+ + + - - + + + + + + +
文件上传文本分享
-取文件 -
文件管理系统设置
移动端深色模式
+ + -### 管理 - - - - - +
+📦 经典界面 (2023) +
+
+
-admin - -admin -
+ + + - - + + +
首页上传
-admin -
管理设置
+ + -## 配置文件 - -如果需要修改配置,可以将该文件放在`/opt/FileCodeBox/`目录下,并命名为`.env`,然后重启容器即可。 -如果不是Docker,则需要在项目同目录下新建一个`data`文件夹,然后在创建`.env`文件 - -```dotenv -# 端口 -PORT=12345 -# Sqlite数据库文件 -DATABASE_URL=sqlite+aiosqlite:///database.db -# 静态文件夹 -DATA_ROOT=./static -# 静态文件夹URL -STATIC_URL=/static -# 开启上传 -ENABLE_UPLOAD=True -# 错误次数 -ERROR_COUNT=5 -# 错误限制分钟数 -ERROR_MINUTE=10 -# 上传次数 -UPLOAD_COUNT=60 -# 上传限制分钟数 -UPLOAD_MINUTE=1 -# 删除过期文件的间隔(分钟) -DELETE_EXPIRE_FILES_INTERVAL=10 -# 管理地址 -ADMIN_ADDRESS=admin -# 管理密码 -ADMIN_PASSWORD=admin -# 文件大小限制,默认10MB -FILE_SIZE_LIMIT=10 -# 网站标题 -TITLE=文件快递柜 -# 网站描述 -DESCRIPTION=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件 -# 网站关键词 -KEYWORDS=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件 -# 存储引擎 -STORAGE_ENGINE=filesystem -# 如果使用阿里云OSS服务的话需要额外创建如下参数: -# 阿里云账号AccessKey -KeyId=阿里云账号AccessKey -# 阿里云账号AccessKeySecret -KeySecret=阿里云账号AccessKeySecret -# 阿里云OSS Bucket的地域节点 -OSS_ENDPOINT=阿里云OSS Bucket的地域节点 -# 阿里云OSS Bucket的BucketName -BUCKET_NAME=阿里云OSS Bucket的BucketName -``` +--- -## 接口文档 +## 🚀 快速开始 -前端比较简陋,可以使用接口进行二次开发 +### Docker 部署(推荐) -### 取件 +**方式一:Docker CLI** -#### PATH +```bash +# Docker Hub(推荐) +docker run -d --restart always -p 12345:12345 -v /opt/FileCodeBox:/app/data --name filecodebox lanol/filecodebox:latest -`/` +# 国内镜像(如果 Docker Hub 拉取缓慢) +docker run -d --restart always -p 12345:12345 -v /opt/FileCodeBox:/app/data --name filecodebox docker.cnb.cool/aixk/filecodebox +``` -#### METHOD +**方式二:Docker Compose** + +```yaml +services: + filecodebox: + image: lanol/filecodebox:latest + container_name: filecodebox + restart: unless-stopped + ports: + - "12345:12345" + volumes: + - ./data:/app/data + environment: + - WORKERS=4 + - LOG_LEVEL=info +``` -`POST` +```bash +docker compose up -d +``` -#### PARAMS +**环境变量说明** -code: 取件码 +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `HOST` | `::` | 监听地址(支持 IPv4/IPv6 双栈) | +| `PORT` | `12345` | 服务端口 | +| `WORKERS` | `4` | 工作进程数(建议设为 CPU 核心数) | +| `LOG_LEVEL` | `info` | 日志级别:`debug` / `info` / `warning` / `error` | -#### Response +### 反向代理配置 -```json -{ - "detail": "msg", - "data": { - "type": "类型", - "text": "文本", - "name": "名称", - "code": "取件码" - } +使用 Nginx 时,请添加以下配置以正确获取客户端 IP: + +```nginx +location / { + proxy_pass http://127.0.0.1:12345; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 100m; # 根据需要调整上传大小限制 } ``` -### 寄件 +### 手动部署 -#### PATH +```bash +# 1. 克隆项目 +git clone https://github.com/vastsa/FileCodeBox.git +cd FileCodeBox -`/share` +# 2. 安装依赖 +pip install -r requirements.txt -#### METHOD +# 3. 启动服务 +python main.py +``` + +--- -`POST` +## 📖 使用指南 -#### PARAMS +### 基础操作 -style: 1为次数,2为时间 -value: 次数或时间 -text: 取件码 -file: 文件 +| 操作 | 步骤 | +|------|------| +| **分享文件** | 打开网页 → 选择/拖拽文件 → 设置有效期 → 获取提取码 | +| **获取文件** | 打开网页 → 输入提取码 → 下载文件或查看文本 | +| **管理后台** | 访问 `/#/admin` → 输入密码 `FileCodeBox2023` | -#### Response +### 命令行使用(curl) + +
+点击展开 curl 使用示例 + +**上传文件** + +```bash +# 基础上传(默认 1 天有效期) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" + +# 指定 1 小时有效期 +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" \ + -F "expire_value=1" \ + -F "expire_style=hour" + +# 指定下载 10 次后过期 +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" \ + -F "expire_value=10" \ + -F "expire_style=count" +``` + +**分享文本** + +```bash +curl -X POST "http://localhost:12345/share/text/" \ + -F "text=要分享的文本内容" +``` + +**下载文件** + +```bash +curl -L "http://localhost:12345/share/select/?code=提取码" -o filename +``` + +**有效期参数** + +| `expire_style` | 说明 | +|----------------|------| +| `day` | 天数 | +| `hour` | 小时 | +| `minute` | 分钟 | +| `count` | 下载次数 | +| `forever` | 永久有效 | + +**返回示例** ```json { - "detail": "msg", - "data": { - "code": "类型", - "key": "唯一ID", - "name": "名称" + "code": 200, + "msg": "success", + "detail": { + "code": "abcd1234", + "name": "file.txt" } } ``` -## 状态 +**需要认证时**(管理员关闭游客上传后) -![Alt](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg "Repobeats analytics image") +```bash +# 1. 获取 token +curl -X POST "http://localhost:12345/admin/login" \ + -H "Content-Type: application/json" \ + -d '{"password": "FileCodeBox2023"}' + +# 2. 携带 token 上传 +curl -X POST "http://localhost:12345/share/file/" \ + -H "Authorization: Bearer " \ + -F "file=@/path/to/file.txt" +``` -## Star History +
-[![Star History Chart](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](https://star-history.com/#vastsa/FileCodeBox&Date) +--- -## 赞赏 +## 🛠 开发指南 - - - - - -
-支付宝 -支付宝 - -微信 -微信 -
+### 项目结构 + +``` +FileCodeBox/ +├── apps/ # 应用模块 +│ ├── admin/ # 管理后台 +│ └── base/ # 基础功能 +├── core/ # 核心模块 +├── data/ # 数据目录(运行时生成) +├── docs/ # 文档 +└── main.py # 入口文件 +``` + +### 本地开发 + +**后端** + +```bash +pip install -r requirements.txt +python main.py +``` + +**前端** + +```bash +# 前端仓库: https://github.com/vastsa/FileCodeBoxFronted +cd fcb-fronted +npm install +npm run dev +``` + +### 技术栈 + +| 类别 | 技术 | +|------|------| +| **后端框架** | FastAPI 0.128+ / Uvicorn | +| **数据库** | SQLite + Tortoise ORM | +| **数据验证** | Pydantic 2.x | +| **异步支持** | aiofiles / aiohttp / aioboto3 | +| **对象存储** | S3 协议 / OneDrive / OpenDAL | +| **前端框架** | Vue 3 + Element Plus + Vite | +| **运行环境** | Python 3.8+ / Node.js 18+ | +| **容器化** | Docker / Docker Compose | + +--- + +## ❓ 常见问题 + +
+如何修改上传大小限制? + +在管理面板中修改 `uploadSize` 配置项。如果使用 Nginx 反向代理,还需修改 `client_max_body_size`。 +
+ +
+如何配置存储引擎? + +在管理面板中选择存储引擎类型并配置相应参数。支持本地存储、S3、OneDrive、OpenDAL 等。 +
+ +
+如何备份数据? + +备份 `data` 目录即可,包含数据库和上传的文件。 +
+ +
+如何修改管理员密码? -## 常见问题 +登录管理面板后,在系统设置中修改 `adminPassword` 配置项。 +
-1. 413 Request Entity Too Large - Nginx限制: - 找到自己主机的nginx.conf配置文件,打开 - 在http{}中加入 client_max_body_size 10m; - 然后重启nginx +更多问题请访问 [Wiki](https://github.com/vastsa/FileCodeBox/wiki/常见问题) 或加入 [QQ群: 739673698](https://qm.qq.com/q/PemPzhdEIM) -## 免责声明 +--- + +## 🤝 贡献指南 + +欢迎提交 Issue 和 Pull Request! + +```bash +# 1. Fork 并克隆 +git clone https://github.com/your-username/FileCodeBox.git + +# 2. 创建分支 +git checkout -b feature/your-feature + +# 3. 提交更改 +git commit -m "feat: add your feature" + +# 4. 推送并创建 PR +git push origin feature/your-feature +``` + +--- + +## 📊 项目统计 + +
+ + + HelloGitHub + + +![Repobeats](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg) + +[![Star History](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](https://star-history.com/#vastsa/FileCodeBox&Date) -本项目开源仅供学习使用,不得用于任何违法用途,否则后果自负,与本人无关。使用请保留项目地址谢谢。 +
+ +--- + +## 🗓 更新计划 + +- [ ] 2025 年新皮肤 +- [ ] 文件收集功能 + +--- + +## 📜 免责声明 + +本项目开源仅供学习交流使用,不得用于任何违法用途,否则后果自负,与作者无关。使用本项目时请保留项目地址和版权信息。 + +--- + +
+ +**如果觉得项目不错,欢迎 ⭐ Star 支持!** + +Made with ❤️ by [vastsa](https://github.com/vastsa) + +
diff --git a/readme_en.md b/readme_en.md index 23c9d9f4f..6448f23ac 100644 --- a/readme_en.md +++ b/readme_en.md @@ -1,230 +1,471 @@
-

File Express Cabinet - Lite

-

FileCodeBox - Lite

-

share text and files with anonymous passwords, and take files like express delivery

-
- -![banner](https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/static/banners/img_1.png) - - ---- -[简体中文](./readme.md) | [English](./readme_en.md) +# FileCodeBox -## Main features +### Anonymous File & Text Sharing with Passcode -- [x] lightweight and simple: Fastapi + Sqlite3 + Vue2 + ElementUI -- [x] easy upload: copy and paste, drag and drop -- [x] multiple types: Text, File -- [x] explosion Prevention: error count limit -- [x] prevent abuse: IP address limits the number of uploads -- [x] password sharing: random password, file access, custom times, and validity period -- [x] anonymous sharing: no registration, no login -- [x] management Panel: View all files and delete them -- [x] one-click deployment: docker one-click deployment -- [x] A variety of storage methods : Aliyun OSS、 local file flow +FileCodeBox Logo -## Deployment method +Share files like picking up a package — no registration required, just enter the passcode -### One-click Docker deployment +[![GitHub stars](https://img.shields.io/github/stars/vastsa/FileCodeBox?style=flat-square&logo=github)](https://github.com/vastsa/FileCodeBox/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/vastsa/FileCodeBox?style=flat-square&logo=github)](https://github.com/vastsa/FileCodeBox/network) +[![GitHub issues](https://img.shields.io/github/issues/vastsa/FileCodeBox?style=flat-square&logo=github)](https://github.com/vastsa/FileCodeBox/issues) +[![GitHub license](https://img.shields.io/github/license/vastsa/FileCodeBox?style=flat-square)](https://github.com/vastsa/FileCodeBox/blob/master/LICENSE) +[![Docker Pulls](https://img.shields.io/docker/pulls/lanol/filecodebox?style=flat-square&logo=docker)](https://hub.docker.com/r/lanol/filecodebox) -#### AMD +[![Python](https://img.shields.io/badge/Python-3.8+-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.128+-009688?style=flat-square&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com) +[![Vue.js](https://img.shields.io/badge/Vue.js-3.x-4FC08D?style=flat-square&logo=vue.js&logoColor=white)](https://vuejs.org) -```bash -docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest -``` - -#### ARM +[简体中文](./README.md) | [Live Demo](https://share.lanol.cn) | [Documentation](https://github.com/vastsa/FileCodeBox/wiki/Deployment-Guide) | [FAQ](https://github.com/vastsa/FileCodeBox/wiki/FAQ) ```bash -docker run -d --restart=always -p 12345:12345 -v /Users/lan/soft/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:arm +# 🚀 Quick Deploy +docker run -d -p 12345:12345 -v /opt/FileCodeBox:/app/data --name filecodebox lanol/filecodebox:latest +# China Mirror (if slow): docker.cnb.cool/aixk/filecodebox ``` -### Update + -```bash -// 找到容器ID -docker ps -a -// 停止容器并删除 -docker stop 容器ID && docker rm 容器ID -// 重新运行容器 -docker run -d --restart=always -p 12345:12345 -v /opt/FileCodeBox/:/app/data --name filecodebox lanol/filecodebox:latest -``` +--- -### Other methods +## Table of Contents -For reference only, historical version->[部署文档](https://www.yuque.com/lxyo/work/zd0kvzy7fofx6w7v) +- [Introduction](#-introduction) +- [Features](#-features) +- [Screenshots](#-screenshots) +- [Quick Start](#-quick-start) +- [Usage Guide](#-usage-guide) +- [Development](#-development) +- [FAQ](#-faq) +- [Contributing](#-contributing) +- [Statistics](#-statistics) +- [Disclaimer](#-disclaimer) -## Project Plan +--- -December 14, 2022 -This project is mainly light-weight, mainly single-user, offline environment, so there is no need to add too many -things, so in fact, the basic functions of this project have been completed, and the rest is to maintain and improve the -existing functions. +## 📝 Introduction -No new major functions will be added. If there are new functions, it will be our Pro version. Of course, it will -continue to be open source. It is an honor to be open source with @veoco. I learned from his code Many, I basically used -the Django set before, and only used Fastapi. Many of his writing methods have benefited me a lot, and I have a deeper -understanding of Fastapi, so I will also use Fastapi in the Pro version . +FileCodeBox is a lightweight file sharing tool built with **FastAPI + Vue3**. Users can anonymously share text and files, and recipients only need to enter a passcode to retrieve the content — just like picking up a package from a locker. -According to some current feedback, I hope to add multi-user functions and multi-storage engines, etc. Welcome to -continue to give comments and join us in joint development. +### Use Cases -If you have better ideas and suggestions, welcome to file an issue. +| Scenario | Description | +|----------|-------------| +| 📁 **Temporary File Sharing** | Quick file sharing without registration | +| 📝 **Code Snippet Sharing** | Share code, config files, and text content | +| 🕶️ **Anonymous Transfer** | Privacy-protected peer-to-peer transfer | +| 🔄 **Cross-Device Transfer** | Quickly sync files between devices | +| 💾 **Temporary Storage** | Cloud storage with custom expiration | +| 🌐 **Private Service** | Build your own enterprise or personal sharing service | -## Preview +--- -### Demo +## ✨ Features -[https://share.lanol.cn](https://share.lanol.cn) + + + +
-### Dark Theme +### 🚀 Lightweight & Fast +- FastAPI + SQLite3 backend +- Vue3 + Element Plus frontend +- One-click Docker deployment +- Minimal resource usage - + + - - -
-
-寄文件 +### 🔒 Secure & Reliable +- IP upload rate limiting +- Passcode attempt limiting +- Auto file expiration cleanup +- Admin authentication support -寄文件 + + +### 📤 Easy Upload +- Drag & drop upload +- Copy & paste upload +- Command line curl upload +- Batch file upload
+
-### Send +### 🎫 Flexible Sharing +- Random / custom passcodes +- Set expiration (time/count) +- Permanent validity support +- Unified text & file management - - - - - - -
-寄文件 -寄文本 + + +### 💾 Multiple Storage +- Local file system +- S3-compatible storage +- [OneDrive](./docs/guide/storage-onedrive.md) +- [OpenDAL](./docs/guide/storage-opendal.md) +
-寄文本 + + +### 🌍 Internationalization +- Simplified Chinese +- Traditional Chinese +- English +- Responsive design / Dark mode +
-### Receive +--- - - - - +## 🖼️ Screenshots + +> Frontend repositories: [2024 Theme](https://github.com/vastsa/FileCodeBoxFronted) | [2023 Theme](https://github.com/vastsa/FileCodeBoxFronted2023) + +
+🎨 New Interface (2024) +
+
+
-取件 - -取件码错误 -
+ + + - - + + + + + + +
File UploadText Share
-取文件 -
File ManagementSystem Settings
Mobile ViewDark Mode
+ + -### Manage - - - - - +
+📦 Classic Interface (2023) +
+
+
-admin - -admin -
+ + + - - + + +
HomeUpload
-admin -
ManagementSettings
+ + -## Configuration file - -if you need to modify the configuration, you can put the file in `/opt/FileCodeBox/` directory and name it `.env` , and -then restart the container. -If it is not Docker, you need to create a `data` folder in the same directory as the project, and then create a `.env` -file - -```dotenv -# 端口 -PORT=12345 -# Sqlite数据库文件 -DATABASE_URL=sqlite+aiosqlite:///database.db -# 静态文件夹 -DATA_ROOT=./static -# 静态文件夹URL -STATIC_URL=/static -# 开启上传 -ENABLE_UPLOAD=True -# 错误次数 -ERROR_COUNT=5 -# 错误限制分钟数 -ERROR_MINUTE=10 -# 上传次数 -UPLOAD_COUNT=60 -# 上传限制分钟数 -UPLOAD_MINUTE=1 -# 删除过期文件的间隔(分钟) -DELETE_EXPIRE_FILES_INTERVAL=10 -# 管理地址 -ADMIN_ADDRESS=admin -# 管理密码 -ADMIN_PASSWORD=admin -# 文件大小限制,默认10MB -FILE_SIZE_LIMIT=10 -# 网站标题 -TITLE=文件快递柜 -# 网站描述 -DESCRIPTION=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件 -# 网站关键词 -KEYWORDS=FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件,图片,视频,音频,压缩包等文件 -# 存储引擎 -STORAGE_ENGINE=filesystem -# 如果使用阿里云OSS服务的话需要额外创建如下参数: -# 阿里云账号AccessKey -KeyId=阿里云账号AccessKey -# 阿里云账号AccessKeySecret -KeySecret=阿里云账号AccessKeySecret -# 阿里云OSS Bucket的地域节点 -OSS_ENDPOINT=阿里云OSS Bucket的地域节点 -# 阿里云OSS Bucket的BucketName -BUCKET_NAME=阿里云OSS Bucket的BucketName -``` - -## Status - -![Alt](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg "Repobeats analytics image") - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](https://star-history.com/#vastsa/FileCodeBox&Date) - -## Appreciate - - - - - - -
-支付宝 -支付宝 - -微信 -微信 -
+--- + +## 🚀 Quick Start + +### Docker Deployment (Recommended) + +**Option 1: Docker CLI** + +```bash +# Docker Hub (Recommended) +docker run -d --restart always -p 12345:12345 -v /opt/FileCodeBox:/app/data --name filecodebox lanol/filecodebox:latest + +# China Mirror (if Docker Hub is slow) +docker run -d --restart always -p 12345:12345 -v /opt/FileCodeBox:/app/data --name filecodebox docker.cnb.cool/aixk/filecodebox +``` + +**Option 2: Docker Compose** + +```yaml +services: + filecodebox: + image: lanol/filecodebox:latest + container_name: filecodebox + restart: unless-stopped + ports: + - "12345:12345" + volumes: + - ./data:/app/data + environment: + - WORKERS=4 + - LOG_LEVEL=info +``` + +```bash +docker compose up -d +``` + +**Environment Variables** + +| Variable | Default | Description | +|----------|---------|-------------| +| `HOST` | `::` | Listen address (supports IPv4/IPv6 dual-stack) | +| `PORT` | `12345` | Service port | +| `WORKERS` | `4` | Worker processes (recommended: CPU cores) | +| `LOG_LEVEL` | `info` | Log level: `debug` / `info` / `warning` / `error` | + +### Reverse Proxy Configuration + +When using Nginx, add the following configuration to properly obtain client IP: + +```nginx +location / { + proxy_pass http://127.0.0.1:12345; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 100m; # Adjust upload size limit as needed +} +``` -## Disclaimer +### Manual Deployment -this project is open source for learning only and cannot be used for any illegal purposes. Otherwise, you will be -responsible for the consequences and have nothing to do with yourself. Please keep the project address. Thank you. \ No newline at end of file +```bash +# 1. Clone the repository +git clone https://github.com/vastsa/FileCodeBox.git +cd FileCodeBox + +# 2. Install dependencies +pip install -r requirements.txt + +# 3. Start the service +python main.py +``` + +--- + +## 📖 Usage Guide + +### Basic Operations + +| Operation | Steps | +|-----------|-------| +| **Share File** | Open website → Select/drag files → Set expiration → Get passcode | +| **Retrieve File** | Open website → Enter passcode → Download file or view text | +| **Admin Panel** | Visit `/#/admin` → Enter password `FileCodeBox2023` | + +### Command Line Usage (curl) + +
+Click to expand curl examples + +**Upload File** + +```bash +# Basic upload (default 1 day expiration) +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" + +# Set 1 hour expiration +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" \ + -F "expire_value=1" \ + -F "expire_style=hour" + +# Set expiration after 10 downloads +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.txt" \ + -F "expire_value=10" \ + -F "expire_style=count" +``` + +**Share Text** + +```bash +curl -X POST "http://localhost:12345/share/text/" \ + -F "text=Text content to share" +``` + +**Download File** + +```bash +curl -L "http://localhost:12345/share/select/?code=PASSCODE" -o filename +``` + +**Expiration Parameters** + +| `expire_style` | Description | +|----------------|-------------| +| `day` | Days | +| `hour` | Hours | +| `minute` | Minutes | +| `count` | Download count | +| `forever` | Never expire | + +**Response Example** + +```json +{ + "code": 200, + "msg": "success", + "detail": { + "code": "abcd1234", + "name": "file.txt" + } +} +``` + +**When Authentication Required** (after admin disables guest upload) + +```bash +# 1. Get token +curl -X POST "http://localhost:12345/admin/login" \ + -H "Content-Type: application/json" \ + -d '{"password": "FileCodeBox2023"}' + +# 2. Upload with token +curl -X POST "http://localhost:12345/share/file/" \ + -H "Authorization: Bearer " \ + -F "file=@/path/to/file.txt" +``` + +
+ +--- + +## 🛠 Development + +### Project Structure + +``` +FileCodeBox/ +├── apps/ # Application modules +│ ├── admin/ # Admin backend +│ └── base/ # Base functionality +├── core/ # Core modules +├── data/ # Data directory (generated at runtime) +├── docs/ # Documentation +└── main.py # Entry point +``` + +### Local Development + +**Backend** + +```bash +pip install -r requirements.txt +python main.py +``` + +**Frontend** + +```bash +# Frontend repo: https://github.com/vastsa/FileCodeBoxFronted +cd fcb-fronted +npm install +npm run dev +``` + +### Tech Stack + +| Category | Technology | +|----------|------------| +| **Backend Framework** | FastAPI 0.128+ / Uvicorn | +| **Database** | SQLite + Tortoise ORM | +| **Data Validation** | Pydantic 2.x | +| **Async Support** | aiofiles / aiohttp / aioboto3 | +| **Object Storage** | S3 Protocol / OneDrive / OpenDAL | +| **Frontend Framework** | Vue 3 + Element Plus + Vite | +| **Runtime** | Python 3.8+ / Node.js 18+ | +| **Containerization** | Docker / Docker Compose | + +--- + +## ❓ FAQ + +
+How to modify upload size limit? + +Modify the `uploadSize` configuration in the admin panel. If using Nginx reverse proxy, also modify `client_max_body_size`. +
+ +
+How to configure storage engine? + +Select the storage engine type and configure parameters in the admin panel. Supports local storage, S3, OneDrive, OpenDAL, etc. +
+ +
+How to backup data? + +Backup the `data` directory, which contains the database and uploaded files. +
+ +
+How to change admin password? + +After logging into the admin panel, modify the `adminPassword` configuration in system settings. +
+ +For more questions, visit [Wiki](https://github.com/vastsa/FileCodeBox/wiki/FAQ) + +--- + +## 🤝 Contributing + +Issues and Pull Requests are welcome! + +```bash +# 1. Fork and clone +git clone https://github.com/your-username/FileCodeBox.git + +# 2. Create branch +git checkout -b feature/your-feature + +# 3. Commit changes +git commit -m "feat: add your feature" + +# 4. Push and create PR +git push origin feature/your-feature +``` + +--- + +## 📊 Statistics + +
+ + + HelloGitHub + + +![Repobeats](https://repobeats.axiom.co/api/embed/7a6c92f1d96ee57e6fb67f0df371528397b0c9ac.svg) + +[![Star History](https://api.star-history.com/svg?repos=vastsa/FileCodeBox&type=Date)](https://star-history.com/#vastsa/FileCodeBox&Date) + +
+ +--- + +## 🗓 Roadmap + +- [ ] 2025 New Theme +- [ ] File Collection Feature + +--- + +## 📜 Disclaimer + +This project is open-source for learning and communication purposes only. It should not be used for any illegal purposes. The author is not responsible for any consequences. Please retain the project address and copyright information when using it. + +--- + +
+ +**If you find this project helpful, please give it a ⭐ Star!** + +Made with ❤️ by [vastsa](https://github.com/vastsa) + +
diff --git a/requirements.txt b/requirements.txt index 410690113..d5d62a87c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,8 @@ -fastapi==0.88.0 -aiosqlite==0.17.0 -SQLAlchemy==1.4.44 -python-multipart==0.0.5 -uvicorn==0.15.0 -greenlet==2.0.1 -starlette~=0.22.0 -oss2==2.16.0 -uvicorn==0.15.0 \ No newline at end of file +aioboto3==15.5.0 +aiohttp==3.13.3 +aiofiles==25.1.0 +fastapi==0.128.0 +pydantic==2.12.5 +uvicorn==0.40.0 +tortoise-orm==0.25.3 +python-multipart==0.0.21 diff --git a/settings.py b/settings.py deleted file mode 100644 index be5e82579..000000000 --- a/settings.py +++ /dev/null @@ -1,88 +0,0 @@ -import uuid - -from starlette.config import Config - -# 配置文件.env,存放为data/.env -config = Config("data/.env") - - -class Settings: - # 项目版本 - VERSION: str = config("VERSION", default="1.6") - # 是否开启DEBUG模式 - DEBUG = config('DEBUG', cast=bool, default=False) - # 端口 - PORT = config('PORT', cast=int, default=12345) - # Sqlite数据库文件 - DATABASE_FILE = config('DATABASE_FILE', cast=str, default='data/database.db') - # Sqlite套接字 - DATABASE_URL = config('DATABASE_URL', cast=str, default=f"sqlite+aiosqlite:///{DATABASE_FILE}") - # 数据存储文件夹,文件就不暴露在静态资源里面了 - DATA_ROOT = config('DATA_ROOT', cast=str, default=f"./data/static") - # 静态文件夹URL - STATIC_URL = config('STATIC_URL', cast=str, default="/static") - # 开启上传 - ENABLE_UPLOAD = config('ENABLE_UPLOAD', cast=bool, default=True) - # 最长天数 - MAX_DAYS = config('MAX_DAYS', cast=int, default=7) - # 错误次数 - ERROR_COUNT = config('ERROR_COUNT', cast=int, default=5) - # 错误限制分钟数 - ERROR_MINUTE = config('ERROR_MINUTE', cast=int, default=10) - # 上传次数 - UPLOAD_COUNT = config('UPLOAD_COUNT', cast=int, default=60) - # 是否允许永久保存 - ENABLE_PERMANENT = config('ENABLE_PERMANENT', cast=bool, default=True) - # 上传限制分钟数 - UPLOAD_MINUTE = config('UPLOAD_MINUTE', cast=int, default=1) - # 删除过期文件的间隔(分钟) - DELETE_EXPIRE_FILES_INTERVAL = config('DELETE_EXPIRE_FILES_INTERVAL', cast=int, default=10) - # 管理地址 - ADMIN_ADDRESS = config('ADMIN_ADDRESS', cast=str, default=uuid.uuid4().hex) - # 管理密码 - ADMIN_PASSWORD = config('ADMIN_PASSWORD', cast=str, default=uuid.uuid4().hex) - # 文件大小限制,默认10MB - FILE_SIZE_LIMIT = config('FILE_SIZE_LIMIT', cast=int, default=10 * 1024 * 1024) - # 网站标题 - TITLE = config('TITLE', cast=str, default="文件快递柜") - # 网站描述 - DESCRIPTION = config('DESCRIPTION', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件") - # 网站关键词 - KEYWORDS = config('KEYWORDS', cast=str, default="FileCodeBox,文件快递柜,口令传送箱,匿名口令分享文本,文件") - # 存储引擎:['aliyunsystem','filesystem'] - STORAGE_ENGINE = config('STORAGE_ENGINE', cast=str, default="filesystem") - # 存储引擎配置 - STORAGE_CONFIG = {} - # Banners - BANNERS = [{ - 'text': 'FileCodeBox', - 'url': 'https://github.com/vastsa/FileCodeBox', - 'src': '/static/banners/img_1.png' - }, { - 'text': 'LanBlog', - 'url': 'https://www.lanol.cn', - 'src': '/static/banners/img_2.png' - }] - int_dict = {'PORT', 'MAX_DAYS', 'ERROR_COUNT', 'ERROR_MINUTE', 'UPLOAD_COUNT', 'UPLOAD_MINUTE', - 'DELETE_EXPIRE_FILES_INTERVAL', 'FILE_SIZE_LIMIT'} - bool_dict = {'DEBUG', 'ENABLE_UPLOAD'} - - async def update(self, key, value) -> None: - if hasattr(self, key): - if key in self.int_dict: - value = int(value) - elif key in self.bool_dict: - value = bool(value) - setattr(self, key, value) - - async def updates(self, options) -> None: - with open('data/.env', 'w', encoding='utf-8') as f: - for i, key, value in options: - # 更新env文件 - f.write(f"{key}={value}\n") - # 更新配置 - await self.update(key, value) - f.flush() - - -settings = Settings() diff --git a/static/asserts/axios.min.js b/static/asserts/axios.min.js deleted file mode 100644 index 43fdf82ca..000000000 --- a/static/asserts/axios.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{},a=i.allOwnKeys,s=void 0!==a&&a;if(null!=t)if("object"!==e(t)&&(t=[t]),h(t))for(r=0,o=t.length;r0;)if(t===(n=r[o]).toLowerCase())return n;return null}var x="undefined"==typeof self?"undefined"==typeof global?void 0:global:self,N=function(e){return!p(e)&&e!==x};var C,k=(C="undefined"!=typeof Uint8Array&&c(Uint8Array),function(e){return C&&e instanceof C}),P=l("HTMLFormElement"),_=function(e){var t=Object.prototype.hasOwnProperty;return function(e,n){return t.call(e,n)}}(),B=l("RegExp"),D=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};j(n,(function(n,o){!1!==t(n,o,e)&&(r[o]=n)})),Object.defineProperties(e,r)},U={isArray:h,isArrayBuffer:m,isBuffer:function(e){return null!==e&&!p(e)&&null!==e.constructor&&!p(e.constructor)&&y(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:function(e){var t="[object FormData]";return e&&("function"==typeof FormData&&e instanceof FormData||u.call(e)===t||y(e.toString)&&e.toString()===t)},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&m(e.buffer)},isString:v,isNumber:b,isBoolean:function(e){return!0===e||!1===e},isObject:g,isPlainObject:w,isUndefined:p,isDate:E,isFile:O,isBlob:S,isRegExp:B,isFunction:y,isStream:function(e){return g(e)&&y(e.pipe)},isURLSearchParams:A,isTypedArray:k,isFileList:R,forEach:j,merge:function e(){for(var t=N(this)&&this||{},n=t.caseless,r={},o=function(t,o){var i=n&&T(r,o)||o;w(r[i])&&w(t)?r[i]=e(r[i],t):w(t)?r[i]=e({},t):h(t)?r[i]=t.slice():r[i]=t},i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{},o=r.allOwnKeys;return j(t,(function(t,r){n&&y(t)?e[r]=a(t,n):e[r]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,a,s={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],r&&!r(a,e,t)||s[a]||(t[a]=e[a],s[a]=!0);e=!1!==n&&c(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:f,kindOfTest:l,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(h(e))return e;var t=e.length;if(!b(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:P,hasOwnProperty:_,hasOwnProp:_,reduceDescriptors:D,freezeMethods:function(e){D(e,(function(t,n){if(y(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;var r=e[n];y(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return h(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t},findKey:T,global:x,isContextDefined:N,toJSONObject:function(e){var t=new Array(10);return function e(n,r){if(g(n)){if(t.indexOf(n)>=0)return;if(!("toJSON"in n)){t[r]=n;var o=h(n)?[]:{};return j(n,(function(t,n){var i=e(t,r+1);!p(i)&&(o[n]=i)})),t[r]=void 0,o}}return n}(e,0)}};function F(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}U.inherits(F,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:U.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var L=F.prototype,z={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){z[e]={value:e}})),Object.defineProperties(F,z),Object.defineProperty(L,"isAxiosError",{value:!0}),F.from=function(e,t,n,r,o,i){var a=Object.create(L);return U.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),F.call(a,e.message,t,n,r,o),a.cause=e,a.name=e.name,i&&Object.assign(a,i),a};var I="object"==("undefined"==typeof self?"undefined":e(self))?self.FormData:window.FormData;function J(e){return U.isPlainObject(e)||U.isArray(e)}function q(e){return U.endsWith(e,"[]")?e.slice(0,-2):e}function M(e,t,n){return e?e.concat(t).map((function(e,t){return e=q(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var W=U.toFlatObject(U,{},null,(function(e){return/^is[A-Z]/.test(e)}));function H(t,n,r){if(!U.isObject(t))throw new TypeError("target must be an object");n=n||new(I||FormData);var o,i=(r=U.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!U.isUndefined(t[e])}))).metaTokens,a=r.visitor||l,s=r.dots,u=r.indexes,c=(r.Blob||"undefined"!=typeof Blob&&Blob)&&((o=n)&&U.isFunction(o.append)&&"FormData"===o[Symbol.toStringTag]&&o[Symbol.iterator]);if(!U.isFunction(a))throw new TypeError("visitor must be a function");function f(e){if(null===e)return"";if(U.isDate(e))return e.toISOString();if(!c&&U.isBlob(e))throw new F("Blob is not supported. Use a Buffer instead.");return U.isArrayBuffer(e)||U.isTypedArray(e)?c&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function l(t,r,o){var a=t;if(t&&!o&&"object"===e(t))if(U.endsWith(r,"{}"))r=i?r:r.slice(0,-2),t=JSON.stringify(t);else if(U.isArray(t)&&function(e){return U.isArray(e)&&!e.some(J)}(t)||U.isFileList(t)||U.endsWith(r,"[]")&&(a=U.toArray(t)))return r=q(r),a.forEach((function(e,t){!U.isUndefined(e)&&null!==e&&n.append(!0===u?M([r],t,s):null===u?r:r+"[]",f(e))})),!1;return!!J(t)||(n.append(M(o,r,s),f(t)),!1)}var d=[],h=Object.assign(W,{defaultVisitor:l,convertValue:f,isVisitable:J});if(!U.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!U.isUndefined(t)){if(-1!==d.indexOf(t))throw Error("Circular reference detected in "+r.join("."));d.push(t),U.forEach(t,(function(t,o){!0===(!(U.isUndefined(t)||null===t)&&a.call(n,t,U.isString(o)?o.trim():o,r,h))&&e(t,r?r.concat(o):[o])})),d.pop()}}(t),n}function K(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function V(e,t){this._pairs=[],e&&H(e,this,t)}var $=V.prototype;function X(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function G(e,t,n){if(!t)return e;var r,o=n&&n.encode||X,i=n&&n.serialize;if(r=i?i(t,n):U.isURLSearchParams(t)?t.toString():new V(t,n).toString(o)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+r}return e}$.append=function(e,t){this._pairs.push([e,t])},$.toString=function(e){var t=e?function(t){return e.call(this,t,K)}:K;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var Q,Z=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){U.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),Y={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},ee="undefined"!=typeof URLSearchParams?URLSearchParams:V,te=FormData,ne=("undefined"==typeof navigator||"ReactNative"!==(Q=navigator.product)&&"NativeScript"!==Q&&"NS"!==Q)&&"undefined"!=typeof window&&"undefined"!=typeof document,re="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,oe={isBrowser:!0,classes:{URLSearchParams:ee,FormData:te,Blob:Blob},isStandardBrowserEnv:ne,isStandardBrowserWebWorkerEnv:re,protocols:["http","https","file","blob","url","data"]};function ie(e){function t(e,n,r,o){var i=e[o++],a=Number.isFinite(+i),s=o>=e.length;return i=!i&&U.isArray(r)?r.length:i,s?(U.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!a):(r[i]&&U.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&U.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=U.isObject(e);if(i&&U.isHTMLForm(e)&&(e=new FormData(e)),U.isFormData(e))return o&&o?JSON.stringify(ie(e)):e;if(U.isArrayBuffer(e)||U.isBuffer(e)||U.isStream(e)||U.isFile(e)||U.isBlob(e))return e;if(U.isArrayBufferView(e))return e.buffer;if(U.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return H(e,new oe.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return oe.isNode&&U.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=U.isFileList(e))||r.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return H(n?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(U.isString(e))try{return(t||JSON.parse)(e),U.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||se.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&U.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw F.from(e,F.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:oe.classes.FormData,Blob:oe.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};U.forEach(["delete","get","head"],(function(e){se.headers[e]={}})),U.forEach(["post","put","patch"],(function(e){se.headers[e]=U.merge(ae)}));var ue=se,ce=U.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),fe=Symbol("internals");function le(e){return e&&String(e).trim().toLowerCase()}function de(e){return!1===e||null==e?e:U.isArray(e)?e.map(de):String(e)}function he(e,t,n,r){return U.isFunction(r)?r.call(this,t,n):U.isString(t)?U.isString(r)?-1!==t.indexOf(r):U.isRegExp(r)?r.test(t):void 0:void 0}var pe=function(e,n){function i(e){t(this,i),e&&this.set(e)}return r(i,[{key:"set",value:function(e,t,n){var r=this;function o(e,t,n){var o=le(t);if(!o)throw new Error("header name must be a non-empty string");var i=U.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=de(e))}var i,a,s,u,c,f=function(e,t){return U.forEach(e,(function(e,n){return o(e,n,t)}))};return U.isPlainObject(e)||e instanceof this.constructor?f(e,t):U.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z]+$/.test(e.trim())?f((c={},(i=e)&&i.split("\n").forEach((function(e){u=e.indexOf(":"),a=e.substring(0,u).trim().toLowerCase(),s=e.substring(u+1).trim(),!a||c[a]&&ce[a]||("set-cookie"===a?c[a]?c[a].push(s):c[a]=[s]:c[a]=c[a]?c[a]+", "+s:s)})),c),t):null!=e&&o(t,e,n),this}},{key:"get",value:function(e,t){if(e=le(e)){var n=U.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(U.isFunction(t))return t.call(this,r,n);if(U.isRegExp(t))return t.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=le(e)){var n=U.findKey(this,e);return!(!n||t&&!he(0,this[n],n,t))}return!1}},{key:"delete",value:function(e,t){var n=this,r=!1;function o(e){if(e=le(e)){var o=U.findKey(n,e);!o||t&&!he(0,n[o],o,t)||(delete n[o],r=!0)}}return U.isArray(e)?e.forEach(o):o(e),r}},{key:"clear",value:function(){return Object.keys(this).forEach(this.delete.bind(this))}},{key:"normalize",value:function(e){var t=this,n={};return U.forEach(this,(function(r,o){var i=U.findKey(n,o);if(i)return t[i]=de(r),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=de(r),n[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r1?n-1:0),o=1;o0;){var a=o[i],s=n[a];if(s){var u=t[a],c=void 0===u||s(u,a,t);if(!0!==c)throw new F("option "+a+" must be "+c,F.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new F("Unknown option "+a,F.ERR_BAD_OPTION)}},validators:Ce},_e=Pe.validators,Be=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new Z,response:new Z}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n,r=t=xe(this.defaults,t),o=r.transitional,i=r.paramsSerializer,a=r.headers;void 0!==o&&Pe.assertOptions(o,{silentJSONParsing:_e.transitional(_e.boolean),forcedJSONParsing:_e.transitional(_e.boolean),clarifyTimeoutError:_e.transitional(_e.boolean)},!1),void 0!==i&&Pe.assertOptions(i,{encode:_e.function,serialize:_e.function},!0),t.method=(t.method||this.defaults.method||"get").toLowerCase(),(n=a&&U.merge(a.common,a[t.method]))&&U.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete a[e]})),t.headers=me.concat(n,a);var s=[],u=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(u=u&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,d=0;if(!u){var h=[je.bind(this),void 0];for(h.unshift.apply(h,s),h.push.apply(h,f),l=h.length,c=Promise.resolve(t);d0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new be(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Fe=function e(t){var n=new De(t),r=a(De.prototype.request,n);return U.extend(r,De.prototype,n,{allOwnKeys:!0}),U.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(xe(t,n))},r}(ue);return Fe.Axios=De,Fe.CanceledError=be,Fe.CancelToken=Ue,Fe.isCancel=ye,Fe.VERSION=Ne,Fe.toFormData=H,Fe.AxiosError=F,Fe.Cancel=Fe.CanceledError,Fe.all=function(e){return Promise.all(e)},Fe.spread=function(e){return function(t){return e.apply(null,t)}},Fe.isAxiosError=function(e){return U.isObject(e)&&!0===e.isAxiosError},Fe.mergeConfig=xe,Fe.AxiosHeaders=me,Fe.formToJSON=function(e){return ie(U.isHTMLForm(e)?new FormData(e):e)},Fe.default=Fe,Fe})); -//# sourceMappingURL=axios.min.js.map diff --git a/static/asserts/favicon.ico b/static/asserts/favicon.ico deleted file mode 100644 index 0f16fa56f..000000000 Binary files a/static/asserts/favicon.ico and /dev/null differ diff --git a/static/asserts/fonts/element-icons.woff b/static/asserts/fonts/element-icons.woff deleted file mode 100644 index 02b9a2539..000000000 Binary files a/static/asserts/fonts/element-icons.woff and /dev/null differ diff --git a/static/asserts/github-markdown-css/github-markdown-dark.css b/static/asserts/github-markdown-css/github-markdown-dark.css deleted file mode 100644 index 3c5a8b325..000000000 --- a/static/asserts/github-markdown-css/github-markdown-dark.css +++ /dev/null @@ -1,1007 +0,0 @@ -.markdown-body { - color-scheme: dark; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - margin: 0; - color: #c9d1d9; - background-color: #0d1117; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body .octicon { - display: inline-block; - fill: currentColor; - vertical-align: text-bottom; -} - -.markdown-body h1:hover .anchor .octicon-link:before, -.markdown-body h2:hover .anchor .octicon-link:before, -.markdown-body h3:hover .anchor .octicon-link:before, -.markdown-body h4:hover .anchor .octicon-link:before, -.markdown-body h5:hover .anchor .octicon-link:before, -.markdown-body h6:hover .anchor .octicon-link:before { - width: 16px; - height: 16px; - content: ' '; - display: inline-block; - background-color: currentColor; - -webkit-mask-image: url("data:image/svg+xml,"); - mask-image: url("data:image/svg+xml,"); -} - -.markdown-body details, -.markdown-body figcaption, -.markdown-body figure { - display: block; -} - -.markdown-body summary { - display: list-item; -} - -.markdown-body [hidden] { - display: none !important; -} - -.markdown-body a { - background-color: transparent; - color: #58a6ff; - text-decoration: none; -} - -.markdown-body abbr[title] { - border-bottom: none; - text-decoration: underline dotted; -} - -.markdown-body b, -.markdown-body strong { - font-weight: 600; -} - -.markdown-body dfn { - font-style: italic; -} - -.markdown-body h1 { - margin: .67em 0; - font-weight: 600; - padding-bottom: .3em; - font-size: 2em; - border-bottom: 1px solid #21262d; -} - -.markdown-body mark { - background-color: rgba(187,128,9,0.15); - color: #c9d1d9; -} - -.markdown-body small { - font-size: 90%; -} - -.markdown-body sub, -.markdown-body sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -.markdown-body sub { - bottom: -0.25em; -} - -.markdown-body sup { - top: -0.5em; -} - -.markdown-body img { - border-style: none; - max-width: 100%; - box-sizing: content-box; - background-color: #0d1117; -} - -.markdown-body code, -.markdown-body kbd, -.markdown-body pre, -.markdown-body samp { - font-family: monospace; - font-size: 1em; -} - -.markdown-body figure { - margin: 1em 40px; -} - -.markdown-body hr { - box-sizing: content-box; - overflow: hidden; - background: transparent; - border-bottom: 1px solid #21262d; - height: .25em; - padding: 0; - margin: 24px 0; - background-color: #30363d; - border: 0; -} - -.markdown-body input { - font: inherit; - margin: 0; - overflow: visible; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -.markdown-body [type=button], -.markdown-body [type=reset], -.markdown-body [type=submit] { - -webkit-appearance: button; -} - -.markdown-body [type=checkbox], -.markdown-body [type=radio] { - box-sizing: border-box; - padding: 0; -} - -.markdown-body [type=number]::-webkit-inner-spin-button, -.markdown-body [type=number]::-webkit-outer-spin-button { - height: auto; -} - -.markdown-body [type=search]::-webkit-search-cancel-button, -.markdown-body [type=search]::-webkit-search-decoration { - -webkit-appearance: none; -} - -.markdown-body ::-webkit-input-placeholder { - color: inherit; - opacity: .54; -} - -.markdown-body ::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body ::placeholder { - color: #6e7681; - opacity: 1; -} - -.markdown-body hr::before { - display: table; - content: ""; -} - -.markdown-body hr::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body table { - border-spacing: 0; - border-collapse: collapse; - display: block; - width: max-content; - max-width: 100%; - overflow: auto; -} - -.markdown-body td, -.markdown-body th { - padding: 0; -} - -.markdown-body details summary { - cursor: pointer; -} - -.markdown-body details:not([open])>*:not(summary) { - display: none !important; -} - -.markdown-body a:focus, -.markdown-body [role=button]:focus, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=checkbox]:focus { - outline: 2px solid #58a6ff; - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:focus:not(:focus-visible), -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body input[type=radio]:focus:not(:focus-visible), -.markdown-body input[type=checkbox]:focus:not(:focus-visible) { - outline: solid 1px transparent; -} - -.markdown-body a:focus-visible, -.markdown-body [role=button]:focus-visible, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus-visible { - outline: 2px solid #58a6ff; - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:not([class]):focus, -.markdown-body a:not([class]):focus-visible, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus, -.markdown-body input[type=checkbox]:focus-visible { - outline-offset: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: 3px 5px; - font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - line-height: 10px; - color: #c9d1d9; - vertical-align: middle; - background-color: #161b22; - border: solid 1px rgba(110,118,129,0.4); - border-bottom-color: rgba(110,118,129,0.4); - border-radius: 6px; - box-shadow: inset 0 -1px 0 rgba(110,118,129,0.4); -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; -} - -.markdown-body h2 { - font-weight: 600; - padding-bottom: .3em; - font-size: 1.5em; - border-bottom: 1px solid #21262d; -} - -.markdown-body h3 { - font-weight: 600; - font-size: 1.25em; -} - -.markdown-body h4 { - font-weight: 600; - font-size: 1em; -} - -.markdown-body h5 { - font-weight: 600; - font-size: .875em; -} - -.markdown-body h6 { - font-weight: 600; - font-size: .85em; - color: #8b949e; -} - -.markdown-body p { - margin-top: 0; - margin-bottom: 10px; -} - -.markdown-body blockquote { - margin: 0; - padding: 0 1em; - color: #8b949e; - border-left: .25em solid #30363d; -} - -.markdown-body ul, -.markdown-body ol { - margin-top: 0; - margin-bottom: 0; - padding-left: 2em; -} - -.markdown-body ol ol, -.markdown-body ul ol { - list-style-type: lower-roman; -} - -.markdown-body ul ul ol, -.markdown-body ul ol ol, -.markdown-body ol ul ol, -.markdown-body ol ol ol { - list-style-type: lower-alpha; -} - -.markdown-body dd { - margin-left: 0; -} - -.markdown-body tt, -.markdown-body code, -.markdown-body samp { - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - font-size: 12px; -} - -.markdown-body pre { - margin-top: 0; - margin-bottom: 0; - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - font-size: 12px; - word-wrap: normal; -} - -.markdown-body .octicon { - display: inline-block; - overflow: visible !important; - vertical-align: text-bottom; - fill: currentColor; -} - -.markdown-body input::-webkit-outer-spin-button, -.markdown-body input::-webkit-inner-spin-button { - margin: 0; - -webkit-appearance: none; - appearance: none; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body>*:first-child { - margin-top: 0 !important; -} - -.markdown-body>*:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .absent { - color: #f85149; -} - -.markdown-body .anchor { - float: left; - padding-right: 4px; - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre, -.markdown-body details { - margin-top: 0; - margin-bottom: 16px; -} - -.markdown-body blockquote>:first-child { - margin-top: 0; -} - -.markdown-body blockquote>:last-child { - margin-bottom: 0; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: #c9d1d9; - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 tt, -.markdown-body h1 code, -.markdown-body h2 tt, -.markdown-body h2 code, -.markdown-body h3 tt, -.markdown-body h3 code, -.markdown-body h4 tt, -.markdown-body h4 code, -.markdown-body h5 tt, -.markdown-body h5 code, -.markdown-body h6 tt, -.markdown-body h6 code { - padding: 0 .2em; - font-size: inherit; -} - -.markdown-body summary h1, -.markdown-body summary h2, -.markdown-body summary h3, -.markdown-body summary h4, -.markdown-body summary h5, -.markdown-body summary h6 { - display: inline-block; -} - -.markdown-body summary h1 .anchor, -.markdown-body summary h2 .anchor, -.markdown-body summary h3 .anchor, -.markdown-body summary h4 .anchor, -.markdown-body summary h5 .anchor, -.markdown-body summary h6 .anchor { - margin-left: -40px; -} - -.markdown-body summary h1, -.markdown-body summary h2 { - padding-bottom: 0; - border-bottom: 0; -} - -.markdown-body ul.no-list, -.markdown-body ol.no-list { - padding: 0; - list-style-type: none; -} - -.markdown-body ol[type=a] { - list-style-type: lower-alpha; -} - -.markdown-body ol[type=A] { - list-style-type: upper-alpha; -} - -.markdown-body ol[type=i] { - list-style-type: lower-roman; -} - -.markdown-body ol[type=I] { - list-style-type: upper-roman; -} - -.markdown-body ol[type="1"] { - list-style-type: decimal; -} - -.markdown-body div>ol:not([type]) { - list-style-type: decimal; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li>p { - margin-top: 16px; -} - -.markdown-body li+li { - margin-top: .25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: 16px; - font-size: 1em; - font-style: italic; - font-weight: 600; -} - -.markdown-body dl dd { - padding: 0 16px; - margin-bottom: 16px; -} - -.markdown-body table th { - font-weight: 600; -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid #30363d; -} - -.markdown-body table tr { - background-color: #0d1117; - border-top: 1px solid #21262d; -} - -.markdown-body table tr:nth-child(2n) { - background-color: #161b22; -} - -.markdown-body table img { - background-color: transparent; -} - -.markdown-body img[align=right] { - padding-left: 20px; -} - -.markdown-body img[align=left] { - padding-right: 20px; -} - -.markdown-body .emoji { - max-width: none; - vertical-align: text-top; - background-color: transparent; -} - -.markdown-body span.frame { - display: block; - overflow: hidden; -} - -.markdown-body span.frame>span { - display: block; - float: left; - width: auto; - padding: 7px; - margin: 13px 0 0; - overflow: hidden; - border: 1px solid #30363d; -} - -.markdown-body span.frame span img { - display: block; - float: left; -} - -.markdown-body span.frame span span { - display: block; - padding: 5px 0 0; - clear: both; - color: #c9d1d9; -} - -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-center>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} - -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-right>span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} - -.markdown-body span.float-left { - display: block; - float: left; - margin-right: 13px; - overflow: hidden; -} - -.markdown-body span.float-left span { - margin: 13px 0 0; -} - -.markdown-body span.float-right { - display: block; - float: right; - margin-left: 13px; - overflow: hidden; -} - -.markdown-body span.float-right>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - -.markdown-body code, -.markdown-body tt { - padding: .2em .4em; - margin: 0; - font-size: 85%; - white-space: break-spaces; - background-color: rgba(110,118,129,0.4); - border-radius: 6px; -} - -.markdown-body code br, -.markdown-body tt br { - display: none; -} - -.markdown-body del code { - text-decoration: inherit; -} - -.markdown-body samp { - font-size: 85%; -} - -.markdown-body pre code { - font-size: 100%; -} - -.markdown-body pre>code { - padding: 0; - margin: 0; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -.markdown-body .highlight { - margin-bottom: 16px; -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #161b22; - border-radius: 6px; -} - -.markdown-body pre code, -.markdown-body pre tt { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body .csv-data td, -.markdown-body .csv-data th { - padding: 5px; - overflow: hidden; - font-size: 12px; - line-height: 1; - text-align: left; - white-space: nowrap; -} - -.markdown-body .csv-data .blob-num { - padding: 10px 8px 9px; - text-align: right; - background: #0d1117; - border: 0; -} - -.markdown-body .csv-data tr { - border-top: 0; -} - -.markdown-body .csv-data th { - font-weight: 600; - background: #161b22; - border-top: 0; -} - -.markdown-body [data-footnote-ref]::before { - content: "["; -} - -.markdown-body [data-footnote-ref]::after { - content: "]"; -} - -.markdown-body .footnotes { - font-size: 12px; - color: #8b949e; - border-top: 1px solid #30363d; -} - -.markdown-body .footnotes ol { - padding-left: 16px; -} - -.markdown-body .footnotes ol ul { - display: inline-block; - padding-left: 16px; - margin-top: 16px; -} - -.markdown-body .footnotes li { - position: relative; -} - -.markdown-body .footnotes li:target::before { - position: absolute; - top: -8px; - right: -8px; - bottom: -8px; - left: -24px; - pointer-events: none; - content: ""; - border: 2px solid #1f6feb; - border-radius: 6px; -} - -.markdown-body .footnotes li:target { - color: #c9d1d9; -} - -.markdown-body .footnotes .data-footnote-backref g-emoji { - font-family: monospace; -} - -.markdown-body .pl-c { - color: #8b949e; -} - -.markdown-body .pl-c1, -.markdown-body .pl-s .pl-v { - color: #79c0ff; -} - -.markdown-body .pl-e, -.markdown-body .pl-en { - color: #d2a8ff; -} - -.markdown-body .pl-smi, -.markdown-body .pl-s .pl-s1 { - color: #c9d1d9; -} - -.markdown-body .pl-ent { - color: #7ee787; -} - -.markdown-body .pl-k { - color: #ff7b72; -} - -.markdown-body .pl-s, -.markdown-body .pl-pds, -.markdown-body .pl-s .pl-pse .pl-s1, -.markdown-body .pl-sr, -.markdown-body .pl-sr .pl-cce, -.markdown-body .pl-sr .pl-sre, -.markdown-body .pl-sr .pl-sra { - color: #a5d6ff; -} - -.markdown-body .pl-v, -.markdown-body .pl-smw { - color: #ffa657; -} - -.markdown-body .pl-bu { - color: #f85149; -} - -.markdown-body .pl-ii { - color: #f0f6fc; - background-color: #8e1519; -} - -.markdown-body .pl-c2 { - color: #f0f6fc; - background-color: #b62324; -} - -.markdown-body .pl-sr .pl-cce { - font-weight: bold; - color: #7ee787; -} - -.markdown-body .pl-ml { - color: #f2cc60; -} - -.markdown-body .pl-mh, -.markdown-body .pl-mh .pl-en, -.markdown-body .pl-ms { - font-weight: bold; - color: #1f6feb; -} - -.markdown-body .pl-mi { - font-style: italic; - color: #c9d1d9; -} - -.markdown-body .pl-mb { - font-weight: bold; - color: #c9d1d9; -} - -.markdown-body .pl-md { - color: #ffdcd7; - background-color: #67060c; -} - -.markdown-body .pl-mi1 { - color: #aff5b4; - background-color: #033a16; -} - -.markdown-body .pl-mc { - color: #ffdfb6; - background-color: #5a1e02; -} - -.markdown-body .pl-mi2 { - color: #c9d1d9; - background-color: #1158c7; -} - -.markdown-body .pl-mdr { - font-weight: bold; - color: #d2a8ff; -} - -.markdown-body .pl-ba { - color: #8b949e; -} - -.markdown-body .pl-sg { - color: #484f58; -} - -.markdown-body .pl-corl { - text-decoration: underline; - color: #a5d6ff; -} - -.markdown-body g-emoji { - display: inline-block; - min-width: 1ch; - font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; - font-size: 1em; - font-style: normal !important; - font-weight: 400; - line-height: 1; - vertical-align: -0.075em; -} - -.markdown-body g-emoji img { - width: 1em; - height: 1em; -} - -.markdown-body .task-list-item { - list-style-type: none; -} - -.markdown-body .task-list-item label { - font-weight: 400; -} - -.markdown-body .task-list-item.enabled label { - cursor: pointer; -} - -.markdown-body .task-list-item+.task-list-item { - margin-top: 4px; -} - -.markdown-body .task-list-item .handle { - display: none; -} - -.markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; - vertical-align: middle; -} - -.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body .contains-task-list { - position: relative; -} - -.markdown-body .contains-task-list:hover .task-list-item-convert-container, -.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { - display: block; - width: auto; - height: 24px; - overflow: visible; - clip: auto; -} - -.markdown-body ::-webkit-calendar-picker-indicator { - filter: invert(50%); -} diff --git a/static/asserts/github-markdown-css/github-markdown-light.css b/static/asserts/github-markdown-css/github-markdown-light.css deleted file mode 100644 index 33766e88e..000000000 --- a/static/asserts/github-markdown-css/github-markdown-light.css +++ /dev/null @@ -1,1006 +0,0 @@ -.markdown-body { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - margin: 0; - color: #24292f; - background-color: #ffffff; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body .octicon { - display: inline-block; - fill: currentColor; - vertical-align: text-bottom; -} - -.markdown-body h1:hover .anchor .octicon-link:before, -.markdown-body h2:hover .anchor .octicon-link:before, -.markdown-body h3:hover .anchor .octicon-link:before, -.markdown-body h4:hover .anchor .octicon-link:before, -.markdown-body h5:hover .anchor .octicon-link:before, -.markdown-body h6:hover .anchor .octicon-link:before { - width: 16px; - height: 16px; - content: ' '; - display: inline-block; - background-color: currentColor; - -webkit-mask-image: url("data:image/svg+xml,"); - mask-image: url("data:image/svg+xml,"); -} - -.markdown-body details, -.markdown-body figcaption, -.markdown-body figure { - display: block; -} - -.markdown-body summary { - display: list-item; -} - -.markdown-body [hidden] { - display: none !important; -} - -.markdown-body a { - background-color: transparent; - color: #0969da; - text-decoration: none; -} - -.markdown-body abbr[title] { - border-bottom: none; - text-decoration: underline dotted; -} - -.markdown-body b, -.markdown-body strong { - font-weight: 600; -} - -.markdown-body dfn { - font-style: italic; -} - -.markdown-body h1 { - margin: .67em 0; - font-weight: 600; - padding-bottom: .3em; - font-size: 2em; - border-bottom: 1px solid hsla(210,18%,87%,1); -} - -.markdown-body mark { - background-color: #fff8c5; - color: #24292f; -} - -.markdown-body small { - font-size: 90%; -} - -.markdown-body sub, -.markdown-body sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -.markdown-body sub { - bottom: -0.25em; -} - -.markdown-body sup { - top: -0.5em; -} - -.markdown-body img { - border-style: none; - max-width: 100%; - box-sizing: content-box; - background-color: #ffffff; -} - -.markdown-body code, -.markdown-body kbd, -.markdown-body pre, -.markdown-body samp { - font-family: monospace; - font-size: 1em; -} - -.markdown-body figure { - margin: 1em 40px; -} - -.markdown-body hr { - box-sizing: content-box; - overflow: hidden; - background: transparent; - border-bottom: 1px solid hsla(210,18%,87%,1); - height: .25em; - padding: 0; - margin: 24px 0; - background-color: #d0d7de; - border: 0; -} - -.markdown-body input { - font: inherit; - margin: 0; - overflow: visible; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -.markdown-body [type=button], -.markdown-body [type=reset], -.markdown-body [type=submit] { - -webkit-appearance: button; -} - -.markdown-body [type=checkbox], -.markdown-body [type=radio] { - box-sizing: border-box; - padding: 0; -} - -.markdown-body [type=number]::-webkit-inner-spin-button, -.markdown-body [type=number]::-webkit-outer-spin-button { - height: auto; -} - -.markdown-body [type=search]::-webkit-search-cancel-button, -.markdown-body [type=search]::-webkit-search-decoration { - -webkit-appearance: none; -} - -.markdown-body ::-webkit-input-placeholder { - color: inherit; - opacity: .54; -} - -.markdown-body ::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body ::placeholder { - color: #6e7781; - opacity: 1; -} - -.markdown-body hr::before { - display: table; - content: ""; -} - -.markdown-body hr::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body table { - border-spacing: 0; - border-collapse: collapse; - display: block; - width: max-content; - max-width: 100%; - overflow: auto; -} - -.markdown-body td, -.markdown-body th { - padding: 0; -} - -.markdown-body details summary { - cursor: pointer; -} - -.markdown-body details:not([open])>*:not(summary) { - display: none !important; -} - -.markdown-body a:focus, -.markdown-body [role=button]:focus, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=checkbox]:focus { - outline: 2px solid #0969da; - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:focus:not(:focus-visible), -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body input[type=radio]:focus:not(:focus-visible), -.markdown-body input[type=checkbox]:focus:not(:focus-visible) { - outline: solid 1px transparent; -} - -.markdown-body a:focus-visible, -.markdown-body [role=button]:focus-visible, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus-visible { - outline: 2px solid #0969da; - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:not([class]):focus, -.markdown-body a:not([class]):focus-visible, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus, -.markdown-body input[type=checkbox]:focus-visible { - outline-offset: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: 3px 5px; - font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - line-height: 10px; - color: #24292f; - vertical-align: middle; - background-color: #f6f8fa; - border: solid 1px rgba(175,184,193,0.2); - border-bottom-color: rgba(175,184,193,0.2); - border-radius: 6px; - box-shadow: inset 0 -1px 0 rgba(175,184,193,0.2); -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; -} - -.markdown-body h2 { - font-weight: 600; - padding-bottom: .3em; - font-size: 1.5em; - border-bottom: 1px solid hsla(210,18%,87%,1); -} - -.markdown-body h3 { - font-weight: 600; - font-size: 1.25em; -} - -.markdown-body h4 { - font-weight: 600; - font-size: 1em; -} - -.markdown-body h5 { - font-weight: 600; - font-size: .875em; -} - -.markdown-body h6 { - font-weight: 600; - font-size: .85em; - color: #57606a; -} - -.markdown-body p { - margin-top: 0; - margin-bottom: 10px; -} - -.markdown-body blockquote { - margin: 0; - padding: 0 1em; - color: #57606a; - border-left: .25em solid #d0d7de; -} - -.markdown-body ul, -.markdown-body ol { - margin-top: 0; - margin-bottom: 0; - padding-left: 2em; -} - -.markdown-body ol ol, -.markdown-body ul ol { - list-style-type: lower-roman; -} - -.markdown-body ul ul ol, -.markdown-body ul ol ol, -.markdown-body ol ul ol, -.markdown-body ol ol ol { - list-style-type: lower-alpha; -} - -.markdown-body dd { - margin-left: 0; -} - -.markdown-body tt, -.markdown-body code, -.markdown-body samp { - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - font-size: 12px; -} - -.markdown-body pre { - margin-top: 0; - margin-bottom: 0; - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - font-size: 12px; - word-wrap: normal; -} - -.markdown-body .octicon { - display: inline-block; - overflow: visible !important; - vertical-align: text-bottom; - fill: currentColor; -} - -.markdown-body input::-webkit-outer-spin-button, -.markdown-body input::-webkit-inner-spin-button { - margin: 0; - -webkit-appearance: none; - appearance: none; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body>*:first-child { - margin-top: 0 !important; -} - -.markdown-body>*:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .absent { - color: #cf222e; -} - -.markdown-body .anchor { - float: left; - padding-right: 4px; - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre, -.markdown-body details { - margin-top: 0; - margin-bottom: 16px; -} - -.markdown-body blockquote>:first-child { - margin-top: 0; -} - -.markdown-body blockquote>:last-child { - margin-bottom: 0; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: #24292f; - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 tt, -.markdown-body h1 code, -.markdown-body h2 tt, -.markdown-body h2 code, -.markdown-body h3 tt, -.markdown-body h3 code, -.markdown-body h4 tt, -.markdown-body h4 code, -.markdown-body h5 tt, -.markdown-body h5 code, -.markdown-body h6 tt, -.markdown-body h6 code { - padding: 0 .2em; - font-size: inherit; -} - -.markdown-body summary h1, -.markdown-body summary h2, -.markdown-body summary h3, -.markdown-body summary h4, -.markdown-body summary h5, -.markdown-body summary h6 { - display: inline-block; -} - -.markdown-body summary h1 .anchor, -.markdown-body summary h2 .anchor, -.markdown-body summary h3 .anchor, -.markdown-body summary h4 .anchor, -.markdown-body summary h5 .anchor, -.markdown-body summary h6 .anchor { - margin-left: -40px; -} - -.markdown-body summary h1, -.markdown-body summary h2 { - padding-bottom: 0; - border-bottom: 0; -} - -.markdown-body ul.no-list, -.markdown-body ol.no-list { - padding: 0; - list-style-type: none; -} - -.markdown-body ol[type=a] { - list-style-type: lower-alpha; -} - -.markdown-body ol[type=A] { - list-style-type: upper-alpha; -} - -.markdown-body ol[type=i] { - list-style-type: lower-roman; -} - -.markdown-body ol[type=I] { - list-style-type: upper-roman; -} - -.markdown-body ol[type="1"] { - list-style-type: decimal; -} - -.markdown-body div>ol:not([type]) { - list-style-type: decimal; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li>p { - margin-top: 16px; -} - -.markdown-body li+li { - margin-top: .25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: 16px; - font-size: 1em; - font-style: italic; - font-weight: 600; -} - -.markdown-body dl dd { - padding: 0 16px; - margin-bottom: 16px; -} - -.markdown-body table th { - font-weight: 600; -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid #d0d7de; -} - -.markdown-body table tr { - background-color: #ffffff; - border-top: 1px solid hsla(210,18%,87%,1); -} - -.markdown-body table tr:nth-child(2n) { - background-color: #f6f8fa; -} - -.markdown-body table img { - background-color: transparent; -} - -.markdown-body img[align=right] { - padding-left: 20px; -} - -.markdown-body img[align=left] { - padding-right: 20px; -} - -.markdown-body .emoji { - max-width: none; - vertical-align: text-top; - background-color: transparent; -} - -.markdown-body span.frame { - display: block; - overflow: hidden; -} - -.markdown-body span.frame>span { - display: block; - float: left; - width: auto; - padding: 7px; - margin: 13px 0 0; - overflow: hidden; - border: 1px solid #d0d7de; -} - -.markdown-body span.frame span img { - display: block; - float: left; -} - -.markdown-body span.frame span span { - display: block; - padding: 5px 0 0; - clear: both; - color: #24292f; -} - -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-center>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} - -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-right>span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} - -.markdown-body span.float-left { - display: block; - float: left; - margin-right: 13px; - overflow: hidden; -} - -.markdown-body span.float-left span { - margin: 13px 0 0; -} - -.markdown-body span.float-right { - display: block; - float: right; - margin-left: 13px; - overflow: hidden; -} - -.markdown-body span.float-right>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - -.markdown-body code, -.markdown-body tt { - padding: .2em .4em; - margin: 0; - font-size: 85%; - white-space: break-spaces; - background-color: rgba(175,184,193,0.2); - border-radius: 6px; -} - -.markdown-body code br, -.markdown-body tt br { - display: none; -} - -.markdown-body del code { - text-decoration: inherit; -} - -.markdown-body samp { - font-size: 85%; -} - -.markdown-body pre code { - font-size: 100%; -} - -.markdown-body pre>code { - padding: 0; - margin: 0; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -.markdown-body .highlight { - margin-bottom: 16px; -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f6f8fa; - border-radius: 6px; -} - -.markdown-body pre code, -.markdown-body pre tt { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body .csv-data td, -.markdown-body .csv-data th { - padding: 5px; - overflow: hidden; - font-size: 12px; - line-height: 1; - text-align: left; - white-space: nowrap; -} - -.markdown-body .csv-data .blob-num { - padding: 10px 8px 9px; - text-align: right; - background: #ffffff; - border: 0; -} - -.markdown-body .csv-data tr { - border-top: 0; -} - -.markdown-body .csv-data th { - font-weight: 600; - background: #f6f8fa; - border-top: 0; -} - -.markdown-body [data-footnote-ref]::before { - content: "["; -} - -.markdown-body [data-footnote-ref]::after { - content: "]"; -} - -.markdown-body .footnotes { - font-size: 12px; - color: #57606a; - border-top: 1px solid #d0d7de; -} - -.markdown-body .footnotes ol { - padding-left: 16px; -} - -.markdown-body .footnotes ol ul { - display: inline-block; - padding-left: 16px; - margin-top: 16px; -} - -.markdown-body .footnotes li { - position: relative; -} - -.markdown-body .footnotes li:target::before { - position: absolute; - top: -8px; - right: -8px; - bottom: -8px; - left: -24px; - pointer-events: none; - content: ""; - border: 2px solid #0969da; - border-radius: 6px; -} - -.markdown-body .footnotes li:target { - color: #24292f; -} - -.markdown-body .footnotes .data-footnote-backref g-emoji { - font-family: monospace; -} - -.markdown-body .pl-c { - color: #6e7781; -} - -.markdown-body .pl-c1, -.markdown-body .pl-s .pl-v { - color: #0550ae; -} - -.markdown-body .pl-e, -.markdown-body .pl-en { - color: #8250df; -} - -.markdown-body .pl-smi, -.markdown-body .pl-s .pl-s1 { - color: #24292f; -} - -.markdown-body .pl-ent { - color: #116329; -} - -.markdown-body .pl-k { - color: #cf222e; -} - -.markdown-body .pl-s, -.markdown-body .pl-pds, -.markdown-body .pl-s .pl-pse .pl-s1, -.markdown-body .pl-sr, -.markdown-body .pl-sr .pl-cce, -.markdown-body .pl-sr .pl-sre, -.markdown-body .pl-sr .pl-sra { - color: #0a3069; -} - -.markdown-body .pl-v, -.markdown-body .pl-smw { - color: #953800; -} - -.markdown-body .pl-bu { - color: #82071e; -} - -.markdown-body .pl-ii { - color: #f6f8fa; - background-color: #82071e; -} - -.markdown-body .pl-c2 { - color: #f6f8fa; - background-color: #cf222e; -} - -.markdown-body .pl-sr .pl-cce { - font-weight: bold; - color: #116329; -} - -.markdown-body .pl-ml { - color: #3b2300; -} - -.markdown-body .pl-mh, -.markdown-body .pl-mh .pl-en, -.markdown-body .pl-ms { - font-weight: bold; - color: #0550ae; -} - -.markdown-body .pl-mi { - font-style: italic; - color: #24292f; -} - -.markdown-body .pl-mb { - font-weight: bold; - color: #24292f; -} - -.markdown-body .pl-md { - color: #82071e; - background-color: #ffebe9; -} - -.markdown-body .pl-mi1 { - color: #116329; - background-color: #dafbe1; -} - -.markdown-body .pl-mc { - color: #953800; - background-color: #ffd8b5; -} - -.markdown-body .pl-mi2 { - color: #eaeef2; - background-color: #0550ae; -} - -.markdown-body .pl-mdr { - font-weight: bold; - color: #8250df; -} - -.markdown-body .pl-ba { - color: #57606a; -} - -.markdown-body .pl-sg { - color: #8c959f; -} - -.markdown-body .pl-corl { - text-decoration: underline; - color: #0a3069; -} - -.markdown-body g-emoji { - display: inline-block; - min-width: 1ch; - font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; - font-size: 1em; - font-style: normal !important; - font-weight: 400; - line-height: 1; - vertical-align: -0.075em; -} - -.markdown-body g-emoji img { - width: 1em; - height: 1em; -} - -.markdown-body .task-list-item { - list-style-type: none; -} - -.markdown-body .task-list-item label { - font-weight: 400; -} - -.markdown-body .task-list-item.enabled label { - cursor: pointer; -} - -.markdown-body .task-list-item+.task-list-item { - margin-top: 4px; -} - -.markdown-body .task-list-item .handle { - display: none; -} - -.markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; - vertical-align: middle; -} - -.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body .contains-task-list { - position: relative; -} - -.markdown-body .contains-task-list:hover .task-list-item-convert-container, -.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { - display: block; - width: auto; - height: 24px; - overflow: visible; - clip: auto; -} - -.markdown-body ::-webkit-calendar-picker-indicator { - filter: invert(50%); -} diff --git a/static/asserts/github-markdown-css/github-markdown.css b/static/asserts/github-markdown-css/github-markdown.css deleted file mode 100644 index 0c333ba95..000000000 --- a/static/asserts/github-markdown-css/github-markdown.css +++ /dev/null @@ -1,1102 +0,0 @@ -@media (prefers-color-scheme: dark) { - .markdown-body { - color-scheme: dark; - --color-prettylights-syntax-comment: #8b949e; - --color-prettylights-syntax-constant: #79c0ff; - --color-prettylights-syntax-entity: #d2a8ff; - --color-prettylights-syntax-storage-modifier-import: #c9d1d9; - --color-prettylights-syntax-entity-tag: #7ee787; - --color-prettylights-syntax-keyword: #ff7b72; - --color-prettylights-syntax-string: #a5d6ff; - --color-prettylights-syntax-variable: #ffa657; - --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; - --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; - --color-prettylights-syntax-invalid-illegal-bg: #8e1519; - --color-prettylights-syntax-carriage-return-text: #f0f6fc; - --color-prettylights-syntax-carriage-return-bg: #b62324; - --color-prettylights-syntax-string-regexp: #7ee787; - --color-prettylights-syntax-markup-list: #f2cc60; - --color-prettylights-syntax-markup-heading: #1f6feb; - --color-prettylights-syntax-markup-italic: #c9d1d9; - --color-prettylights-syntax-markup-bold: #c9d1d9; - --color-prettylights-syntax-markup-deleted-text: #ffdcd7; - --color-prettylights-syntax-markup-deleted-bg: #67060c; - --color-prettylights-syntax-markup-inserted-text: #aff5b4; - --color-prettylights-syntax-markup-inserted-bg: #033a16; - --color-prettylights-syntax-markup-changed-text: #ffdfb6; - --color-prettylights-syntax-markup-changed-bg: #5a1e02; - --color-prettylights-syntax-markup-ignored-text: #c9d1d9; - --color-prettylights-syntax-markup-ignored-bg: #1158c7; - --color-prettylights-syntax-meta-diff-range: #d2a8ff; - --color-prettylights-syntax-brackethighlighter-angle: #8b949e; - --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; - --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; - --color-fg-default: #c9d1d9; - --color-fg-muted: #8b949e; - --color-fg-subtle: #6e7681; - --color-canvas-default: #0d1117; - --color-canvas-subtle: #161b22; - --color-border-default: #30363d; - --color-border-muted: #21262d; - --color-neutral-muted: rgba(110,118,129,0.4); - --color-accent-fg: #58a6ff; - --color-accent-emphasis: #1f6feb; - --color-attention-subtle: rgba(187,128,9,0.15); - --color-danger-fg: #f85149; - } -} - -@media (prefers-color-scheme: light) { - .markdown-body { - color-scheme: light; - --color-prettylights-syntax-comment: #6e7781; - --color-prettylights-syntax-constant: #0550ae; - --color-prettylights-syntax-entity: #8250df; - --color-prettylights-syntax-storage-modifier-import: #24292f; - --color-prettylights-syntax-entity-tag: #116329; - --color-prettylights-syntax-keyword: #cf222e; - --color-prettylights-syntax-string: #0a3069; - --color-prettylights-syntax-variable: #953800; - --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; - --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; - --color-prettylights-syntax-invalid-illegal-bg: #82071e; - --color-prettylights-syntax-carriage-return-text: #f6f8fa; - --color-prettylights-syntax-carriage-return-bg: #cf222e; - --color-prettylights-syntax-string-regexp: #116329; - --color-prettylights-syntax-markup-list: #3b2300; - --color-prettylights-syntax-markup-heading: #0550ae; - --color-prettylights-syntax-markup-italic: #24292f; - --color-prettylights-syntax-markup-bold: #24292f; - --color-prettylights-syntax-markup-deleted-text: #82071e; - --color-prettylights-syntax-markup-deleted-bg: #ffebe9; - --color-prettylights-syntax-markup-inserted-text: #116329; - --color-prettylights-syntax-markup-inserted-bg: #dafbe1; - --color-prettylights-syntax-markup-changed-text: #953800; - --color-prettylights-syntax-markup-changed-bg: #ffd8b5; - --color-prettylights-syntax-markup-ignored-text: #eaeef2; - --color-prettylights-syntax-markup-ignored-bg: #0550ae; - --color-prettylights-syntax-meta-diff-range: #8250df; - --color-prettylights-syntax-brackethighlighter-angle: #57606a; - --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; - --color-prettylights-syntax-constant-other-reference-link: #0a3069; - --color-fg-default: #24292f; - --color-fg-muted: #57606a; - --color-fg-subtle: #6e7781; - --color-canvas-default: #ffffff; - --color-canvas-subtle: #f6f8fa; - --color-border-default: #d0d7de; - --color-border-muted: hsla(210,18%,87%,1); - --color-neutral-muted: rgba(175,184,193,0.2); - --color-accent-fg: #0969da; - --color-accent-emphasis: #0969da; - --color-attention-subtle: #fff8c5; - --color-danger-fg: #cf222e; - } -} - -.markdown-body { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - margin: 0; - color: var(--color-fg-default); - background-color: var(--color-canvas-default); - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body .octicon { - display: inline-block; - fill: currentColor; - vertical-align: text-bottom; -} - -.markdown-body h1:hover .anchor .octicon-link:before, -.markdown-body h2:hover .anchor .octicon-link:before, -.markdown-body h3:hover .anchor .octicon-link:before, -.markdown-body h4:hover .anchor .octicon-link:before, -.markdown-body h5:hover .anchor .octicon-link:before, -.markdown-body h6:hover .anchor .octicon-link:before { - width: 16px; - height: 16px; - content: ' '; - display: inline-block; - background-color: currentColor; - -webkit-mask-image: url("data:image/svg+xml,"); - mask-image: url("data:image/svg+xml,"); -} - -.markdown-body details, -.markdown-body figcaption, -.markdown-body figure { - display: block; -} - -.markdown-body summary { - display: list-item; -} - -.markdown-body [hidden] { - display: none !important; -} - -.markdown-body a { - background-color: transparent; - color: var(--color-accent-fg); - text-decoration: none; -} - -.markdown-body abbr[title] { - border-bottom: none; - text-decoration: underline dotted; -} - -.markdown-body b, -.markdown-body strong { - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body dfn { - font-style: italic; -} - -.markdown-body h1 { - margin: .67em 0; - font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; - font-size: 2em; - border-bottom: 1px solid var(--color-border-muted); -} - -.markdown-body mark { - background-color: var(--color-attention-subtle); - color: var(--color-fg-default); -} - -.markdown-body small { - font-size: 90%; -} - -.markdown-body sub, -.markdown-body sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -.markdown-body sub { - bottom: -0.25em; -} - -.markdown-body sup { - top: -0.5em; -} - -.markdown-body img { - border-style: none; - max-width: 100%; - box-sizing: content-box; - background-color: var(--color-canvas-default); -} - -.markdown-body code, -.markdown-body kbd, -.markdown-body pre, -.markdown-body samp { - font-family: monospace; - font-size: 1em; -} - -.markdown-body figure { - margin: 1em 40px; -} - -.markdown-body hr { - box-sizing: content-box; - overflow: hidden; - background: transparent; - border-bottom: 1px solid var(--color-border-muted); - height: .25em; - padding: 0; - margin: 24px 0; - background-color: var(--color-border-default); - border: 0; -} - -.markdown-body input { - font: inherit; - margin: 0; - overflow: visible; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -.markdown-body [type=button], -.markdown-body [type=reset], -.markdown-body [type=submit] { - -webkit-appearance: button; -} - -.markdown-body [type=checkbox], -.markdown-body [type=radio] { - box-sizing: border-box; - padding: 0; -} - -.markdown-body [type=number]::-webkit-inner-spin-button, -.markdown-body [type=number]::-webkit-outer-spin-button { - height: auto; -} - -.markdown-body [type=search]::-webkit-search-cancel-button, -.markdown-body [type=search]::-webkit-search-decoration { - -webkit-appearance: none; -} - -.markdown-body ::-webkit-input-placeholder { - color: inherit; - opacity: .54; -} - -.markdown-body ::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body ::placeholder { - color: var(--color-fg-subtle); - opacity: 1; -} - -.markdown-body hr::before { - display: table; - content: ""; -} - -.markdown-body hr::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body table { - border-spacing: 0; - border-collapse: collapse; - display: block; - width: max-content; - max-width: 100%; - overflow: auto; -} - -.markdown-body td, -.markdown-body th { - padding: 0; -} - -.markdown-body details summary { - cursor: pointer; -} - -.markdown-body details:not([open])>*:not(summary) { - display: none !important; -} - -.markdown-body a:focus, -.markdown-body [role=button]:focus, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=checkbox]:focus { - outline: 2px solid var(--color-accent-fg); - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:focus:not(:focus-visible), -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body input[type=radio]:focus:not(:focus-visible), -.markdown-body input[type=checkbox]:focus:not(:focus-visible) { - outline: solid 1px transparent; -} - -.markdown-body a:focus-visible, -.markdown-body [role=button]:focus-visible, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus-visible { - outline: 2px solid var(--color-accent-fg); - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:not([class]):focus, -.markdown-body a:not([class]):focus-visible, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus, -.markdown-body input[type=checkbox]:focus-visible { - outline-offset: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: 3px 5px; - font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - line-height: 10px; - color: var(--color-fg-default); - vertical-align: middle; - background-color: var(--color-canvas-subtle); - border: solid 1px var(--color-neutral-muted); - border-bottom-color: var(--color-neutral-muted); - border-radius: 6px; - box-shadow: inset 0 -1px 0 var(--color-neutral-muted); -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: var(--base-text-weight-semibold, 600); - line-height: 1.25; -} - -.markdown-body h2 { - font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; - font-size: 1.5em; - border-bottom: 1px solid var(--color-border-muted); -} - -.markdown-body h3 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: 1.25em; -} - -.markdown-body h4 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: 1em; -} - -.markdown-body h5 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: .875em; -} - -.markdown-body h6 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: .85em; - color: var(--color-fg-muted); -} - -.markdown-body p { - margin-top: 0; - margin-bottom: 10px; -} - -.markdown-body blockquote { - margin: 0; - padding: 0 1em; - color: var(--color-fg-muted); - border-left: .25em solid var(--color-border-default); -} - -.markdown-body ul, -.markdown-body ol { - margin-top: 0; - margin-bottom: 0; - padding-left: 2em; -} - -.markdown-body ol ol, -.markdown-body ul ol { - list-style-type: lower-roman; -} - -.markdown-body ul ul ol, -.markdown-body ul ol ol, -.markdown-body ol ul ol, -.markdown-body ol ol ol { - list-style-type: lower-alpha; -} - -.markdown-body dd { - margin-left: 0; -} - -.markdown-body tt, -.markdown-body code, -.markdown-body samp { - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - font-size: 12px; -} - -.markdown-body pre { - margin-top: 0; - margin-bottom: 0; - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; - font-size: 12px; - word-wrap: normal; -} - -.markdown-body .octicon { - display: inline-block; - overflow: visible !important; - vertical-align: text-bottom; - fill: currentColor; -} - -.markdown-body input::-webkit-outer-spin-button, -.markdown-body input::-webkit-inner-spin-button { - margin: 0; - -webkit-appearance: none; - appearance: none; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body>*:first-child { - margin-top: 0 !important; -} - -.markdown-body>*:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .absent { - color: var(--color-danger-fg); -} - -.markdown-body .anchor { - float: left; - padding-right: 4px; - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre, -.markdown-body details { - margin-top: 0; - margin-bottom: 16px; -} - -.markdown-body blockquote>:first-child { - margin-top: 0; -} - -.markdown-body blockquote>:last-child { - margin-bottom: 0; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: var(--color-fg-default); - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 tt, -.markdown-body h1 code, -.markdown-body h2 tt, -.markdown-body h2 code, -.markdown-body h3 tt, -.markdown-body h3 code, -.markdown-body h4 tt, -.markdown-body h4 code, -.markdown-body h5 tt, -.markdown-body h5 code, -.markdown-body h6 tt, -.markdown-body h6 code { - padding: 0 .2em; - font-size: inherit; -} - -.markdown-body summary h1, -.markdown-body summary h2, -.markdown-body summary h3, -.markdown-body summary h4, -.markdown-body summary h5, -.markdown-body summary h6 { - display: inline-block; -} - -.markdown-body summary h1 .anchor, -.markdown-body summary h2 .anchor, -.markdown-body summary h3 .anchor, -.markdown-body summary h4 .anchor, -.markdown-body summary h5 .anchor, -.markdown-body summary h6 .anchor { - margin-left: -40px; -} - -.markdown-body summary h1, -.markdown-body summary h2 { - padding-bottom: 0; - border-bottom: 0; -} - -.markdown-body ul.no-list, -.markdown-body ol.no-list { - padding: 0; - list-style-type: none; -} - -.markdown-body ol[type=a] { - list-style-type: lower-alpha; -} - -.markdown-body ol[type=A] { - list-style-type: upper-alpha; -} - -.markdown-body ol[type=i] { - list-style-type: lower-roman; -} - -.markdown-body ol[type=I] { - list-style-type: upper-roman; -} - -.markdown-body ol[type="1"] { - list-style-type: decimal; -} - -.markdown-body div>ol:not([type]) { - list-style-type: decimal; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li>p { - margin-top: 16px; -} - -.markdown-body li+li { - margin-top: .25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: 16px; - font-size: 1em; - font-style: italic; - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body dl dd { - padding: 0 16px; - margin-bottom: 16px; -} - -.markdown-body table th { - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid var(--color-border-default); -} - -.markdown-body table tr { - background-color: var(--color-canvas-default); - border-top: 1px solid var(--color-border-muted); -} - -.markdown-body table tr:nth-child(2n) { - background-color: var(--color-canvas-subtle); -} - -.markdown-body table img { - background-color: transparent; -} - -.markdown-body img[align=right] { - padding-left: 20px; -} - -.markdown-body img[align=left] { - padding-right: 20px; -} - -.markdown-body .emoji { - max-width: none; - vertical-align: text-top; - background-color: transparent; -} - -.markdown-body span.frame { - display: block; - overflow: hidden; -} - -.markdown-body span.frame>span { - display: block; - float: left; - width: auto; - padding: 7px; - margin: 13px 0 0; - overflow: hidden; - border: 1px solid var(--color-border-default); -} - -.markdown-body span.frame span img { - display: block; - float: left; -} - -.markdown-body span.frame span span { - display: block; - padding: 5px 0 0; - clear: both; - color: var(--color-fg-default); -} - -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-center>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} - -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-right>span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} - -.markdown-body span.float-left { - display: block; - float: left; - margin-right: 13px; - overflow: hidden; -} - -.markdown-body span.float-left span { - margin: 13px 0 0; -} - -.markdown-body span.float-right { - display: block; - float: right; - margin-left: 13px; - overflow: hidden; -} - -.markdown-body span.float-right>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - -.markdown-body code, -.markdown-body tt { - padding: .2em .4em; - margin: 0; - font-size: 85%; - white-space: break-spaces; - background-color: var(--color-neutral-muted); - border-radius: 6px; -} - -.markdown-body code br, -.markdown-body tt br { - display: none; -} - -.markdown-body del code { - text-decoration: inherit; -} - -.markdown-body samp { - font-size: 85%; -} - -.markdown-body pre code { - font-size: 100%; -} - -.markdown-body pre>code { - padding: 0; - margin: 0; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -.markdown-body .highlight { - margin-bottom: 16px; -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: var(--color-canvas-subtle); - border-radius: 6px; -} - -.markdown-body pre code, -.markdown-body pre tt { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body .csv-data td, -.markdown-body .csv-data th { - padding: 5px; - overflow: hidden; - font-size: 12px; - line-height: 1; - text-align: left; - white-space: nowrap; -} - -.markdown-body .csv-data .blob-num { - padding: 10px 8px 9px; - text-align: right; - background: var(--color-canvas-default); - border: 0; -} - -.markdown-body .csv-data tr { - border-top: 0; -} - -.markdown-body .csv-data th { - font-weight: var(--base-text-weight-semibold, 600); - background: var(--color-canvas-subtle); - border-top: 0; -} - -.markdown-body [data-footnote-ref]::before { - content: "["; -} - -.markdown-body [data-footnote-ref]::after { - content: "]"; -} - -.markdown-body .footnotes { - font-size: 12px; - color: var(--color-fg-muted); - border-top: 1px solid var(--color-border-default); -} - -.markdown-body .footnotes ol { - padding-left: 16px; -} - -.markdown-body .footnotes ol ul { - display: inline-block; - padding-left: 16px; - margin-top: 16px; -} - -.markdown-body .footnotes li { - position: relative; -} - -.markdown-body .footnotes li:target::before { - position: absolute; - top: -8px; - right: -8px; - bottom: -8px; - left: -24px; - pointer-events: none; - content: ""; - border: 2px solid var(--color-accent-emphasis); - border-radius: 6px; -} - -.markdown-body .footnotes li:target { - color: var(--color-fg-default); -} - -.markdown-body .footnotes .data-footnote-backref g-emoji { - font-family: monospace; -} - -.markdown-body .pl-c { - color: var(--color-prettylights-syntax-comment); -} - -.markdown-body .pl-c1, -.markdown-body .pl-s .pl-v { - color: var(--color-prettylights-syntax-constant); -} - -.markdown-body .pl-e, -.markdown-body .pl-en { - color: var(--color-prettylights-syntax-entity); -} - -.markdown-body .pl-smi, -.markdown-body .pl-s .pl-s1 { - color: var(--color-prettylights-syntax-storage-modifier-import); -} - -.markdown-body .pl-ent { - color: var(--color-prettylights-syntax-entity-tag); -} - -.markdown-body .pl-k { - color: var(--color-prettylights-syntax-keyword); -} - -.markdown-body .pl-s, -.markdown-body .pl-pds, -.markdown-body .pl-s .pl-pse .pl-s1, -.markdown-body .pl-sr, -.markdown-body .pl-sr .pl-cce, -.markdown-body .pl-sr .pl-sre, -.markdown-body .pl-sr .pl-sra { - color: var(--color-prettylights-syntax-string); -} - -.markdown-body .pl-v, -.markdown-body .pl-smw { - color: var(--color-prettylights-syntax-variable); -} - -.markdown-body .pl-bu { - color: var(--color-prettylights-syntax-brackethighlighter-unmatched); -} - -.markdown-body .pl-ii { - color: var(--color-prettylights-syntax-invalid-illegal-text); - background-color: var(--color-prettylights-syntax-invalid-illegal-bg); -} - -.markdown-body .pl-c2 { - color: var(--color-prettylights-syntax-carriage-return-text); - background-color: var(--color-prettylights-syntax-carriage-return-bg); -} - -.markdown-body .pl-sr .pl-cce { - font-weight: bold; - color: var(--color-prettylights-syntax-string-regexp); -} - -.markdown-body .pl-ml { - color: var(--color-prettylights-syntax-markup-list); -} - -.markdown-body .pl-mh, -.markdown-body .pl-mh .pl-en, -.markdown-body .pl-ms { - font-weight: bold; - color: var(--color-prettylights-syntax-markup-heading); -} - -.markdown-body .pl-mi { - font-style: italic; - color: var(--color-prettylights-syntax-markup-italic); -} - -.markdown-body .pl-mb { - font-weight: bold; - color: var(--color-prettylights-syntax-markup-bold); -} - -.markdown-body .pl-md { - color: var(--color-prettylights-syntax-markup-deleted-text); - background-color: var(--color-prettylights-syntax-markup-deleted-bg); -} - -.markdown-body .pl-mi1 { - color: var(--color-prettylights-syntax-markup-inserted-text); - background-color: var(--color-prettylights-syntax-markup-inserted-bg); -} - -.markdown-body .pl-mc { - color: var(--color-prettylights-syntax-markup-changed-text); - background-color: var(--color-prettylights-syntax-markup-changed-bg); -} - -.markdown-body .pl-mi2 { - color: var(--color-prettylights-syntax-markup-ignored-text); - background-color: var(--color-prettylights-syntax-markup-ignored-bg); -} - -.markdown-body .pl-mdr { - font-weight: bold; - color: var(--color-prettylights-syntax-meta-diff-range); -} - -.markdown-body .pl-ba { - color: var(--color-prettylights-syntax-brackethighlighter-angle); -} - -.markdown-body .pl-sg { - color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); -} - -.markdown-body .pl-corl { - text-decoration: underline; - color: var(--color-prettylights-syntax-constant-other-reference-link); -} - -.markdown-body g-emoji { - display: inline-block; - min-width: 1ch; - font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; - font-size: 1em; - font-style: normal !important; - font-weight: var(--base-text-weight-normal, 400); - line-height: 1; - vertical-align: -0.075em; -} - -.markdown-body g-emoji img { - width: 1em; - height: 1em; -} - -.markdown-body .task-list-item { - list-style-type: none; -} - -.markdown-body .task-list-item label { - font-weight: var(--base-text-weight-normal, 400); -} - -.markdown-body .task-list-item.enabled label { - cursor: pointer; -} - -.markdown-body .task-list-item+.task-list-item { - margin-top: 4px; -} - -.markdown-body .task-list-item .handle { - display: none; -} - -.markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; - vertical-align: middle; -} - -.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body .contains-task-list { - position: relative; -} - -.markdown-body .contains-task-list:hover .task-list-item-convert-container, -.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { - display: block; - width: auto; - height: 24px; - overflow: visible; - clip: auto; -} - -.markdown-body ::-webkit-calendar-picker-indicator { - filter: invert(50%); -} diff --git a/static/asserts/index.css b/static/asserts/index.css deleted file mode 100644 index 619b14062..000000000 --- a/static/asserts/index.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";@font-face{font-family:element-icons;src:url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");font-weight:400;font-display:"auto";font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\e6a0"}.el-icon-ice-cream-square:before{content:"\e6a3"}.el-icon-lollipop:before{content:"\e6a4"}.el-icon-potato-strips:before{content:"\e6a5"}.el-icon-milk-tea:before{content:"\e6a6"}.el-icon-ice-drink:before{content:"\e6a7"}.el-icon-ice-tea:before{content:"\e6a9"}.el-icon-coffee:before{content:"\e6aa"}.el-icon-orange:before{content:"\e6ab"}.el-icon-pear:before{content:"\e6ac"}.el-icon-apple:before{content:"\e6ad"}.el-icon-cherry:before{content:"\e6ae"}.el-icon-watermelon:before{content:"\e6af"}.el-icon-grape:before{content:"\e6b0"}.el-icon-refrigerator:before{content:"\e6b1"}.el-icon-goblet-square-full:before{content:"\e6b2"}.el-icon-goblet-square:before{content:"\e6b3"}.el-icon-goblet-full:before{content:"\e6b4"}.el-icon-goblet:before{content:"\e6b5"}.el-icon-cold-drink:before{content:"\e6b6"}.el-icon-coffee-cup:before{content:"\e6b8"}.el-icon-water-cup:before{content:"\e6b9"}.el-icon-hot-water:before{content:"\e6ba"}.el-icon-ice-cream:before{content:"\e6bb"}.el-icon-dessert:before{content:"\e6bc"}.el-icon-sugar:before{content:"\e6bd"}.el-icon-tableware:before{content:"\e6be"}.el-icon-burger:before{content:"\e6bf"}.el-icon-knife-fork:before{content:"\e6c1"}.el-icon-fork-spoon:before{content:"\e6c2"}.el-icon-chicken:before{content:"\e6c3"}.el-icon-food:before{content:"\e6c4"}.el-icon-dish-1:before{content:"\e6c5"}.el-icon-dish:before{content:"\e6c6"}.el-icon-moon-night:before{content:"\e6ee"}.el-icon-moon:before{content:"\e6f0"}.el-icon-cloudy-and-sunny:before{content:"\e6f1"}.el-icon-partly-cloudy:before{content:"\e6f2"}.el-icon-cloudy:before{content:"\e6f3"}.el-icon-sunny:before{content:"\e6f6"}.el-icon-sunset:before{content:"\e6f7"}.el-icon-sunrise-1:before{content:"\e6f8"}.el-icon-sunrise:before{content:"\e6f9"}.el-icon-heavy-rain:before{content:"\e6fa"}.el-icon-lightning:before{content:"\e6fb"}.el-icon-light-rain:before{content:"\e6fc"}.el-icon-wind-power:before{content:"\e6fd"}.el-icon-baseball:before{content:"\e712"}.el-icon-soccer:before{content:"\e713"}.el-icon-football:before{content:"\e715"}.el-icon-basketball:before{content:"\e716"}.el-icon-ship:before{content:"\e73f"}.el-icon-truck:before{content:"\e740"}.el-icon-bicycle:before{content:"\e741"}.el-icon-mobile-phone:before{content:"\e6d3"}.el-icon-service:before{content:"\e6d4"}.el-icon-key:before{content:"\e6e2"}.el-icon-unlock:before{content:"\e6e4"}.el-icon-lock:before{content:"\e6e5"}.el-icon-watch:before{content:"\e6fe"}.el-icon-watch-1:before{content:"\e6ff"}.el-icon-timer:before{content:"\e702"}.el-icon-alarm-clock:before{content:"\e703"}.el-icon-map-location:before{content:"\e704"}.el-icon-delete-location:before{content:"\e705"}.el-icon-add-location:before{content:"\e706"}.el-icon-location-information:before{content:"\e707"}.el-icon-location-outline:before{content:"\e708"}.el-icon-location:before{content:"\e79e"}.el-icon-place:before{content:"\e709"}.el-icon-discover:before{content:"\e70a"}.el-icon-first-aid-kit:before{content:"\e70b"}.el-icon-trophy-1:before{content:"\e70c"}.el-icon-trophy:before{content:"\e70d"}.el-icon-medal:before{content:"\e70e"}.el-icon-medal-1:before{content:"\e70f"}.el-icon-stopwatch:before{content:"\e710"}.el-icon-mic:before{content:"\e711"}.el-icon-copy-document:before{content:"\e718"}.el-icon-full-screen:before{content:"\e719"}.el-icon-switch-button:before{content:"\e71b"}.el-icon-aim:before{content:"\e71c"}.el-icon-crop:before{content:"\e71d"}.el-icon-odometer:before{content:"\e71e"}.el-icon-time:before{content:"\e71f"}.el-icon-bangzhu:before{content:"\e724"}.el-icon-close-notification:before{content:"\e726"}.el-icon-microphone:before{content:"\e727"}.el-icon-turn-off-microphone:before{content:"\e728"}.el-icon-position:before{content:"\e729"}.el-icon-postcard:before{content:"\e72a"}.el-icon-message:before{content:"\e72b"}.el-icon-chat-line-square:before{content:"\e72d"}.el-icon-chat-dot-square:before{content:"\e72e"}.el-icon-chat-dot-round:before{content:"\e72f"}.el-icon-chat-square:before{content:"\e730"}.el-icon-chat-line-round:before{content:"\e731"}.el-icon-chat-round:before{content:"\e732"}.el-icon-set-up:before{content:"\e733"}.el-icon-turn-off:before{content:"\e734"}.el-icon-open:before{content:"\e735"}.el-icon-connection:before{content:"\e736"}.el-icon-link:before{content:"\e737"}.el-icon-cpu:before{content:"\e738"}.el-icon-thumb:before{content:"\e739"}.el-icon-female:before{content:"\e73a"}.el-icon-male:before{content:"\e73b"}.el-icon-guide:before{content:"\e73c"}.el-icon-news:before{content:"\e73e"}.el-icon-price-tag:before{content:"\e744"}.el-icon-discount:before{content:"\e745"}.el-icon-wallet:before{content:"\e747"}.el-icon-coin:before{content:"\e748"}.el-icon-money:before{content:"\e749"}.el-icon-bank-card:before{content:"\e74a"}.el-icon-box:before{content:"\e74b"}.el-icon-present:before{content:"\e74c"}.el-icon-sell:before{content:"\e6d5"}.el-icon-sold-out:before{content:"\e6d6"}.el-icon-shopping-bag-2:before{content:"\e74d"}.el-icon-shopping-bag-1:before{content:"\e74e"}.el-icon-shopping-cart-2:before{content:"\e74f"}.el-icon-shopping-cart-1:before{content:"\e750"}.el-icon-shopping-cart-full:before{content:"\e751"}.el-icon-smoking:before{content:"\e752"}.el-icon-no-smoking:before{content:"\e753"}.el-icon-house:before{content:"\e754"}.el-icon-table-lamp:before{content:"\e755"}.el-icon-school:before{content:"\e756"}.el-icon-office-building:before{content:"\e757"}.el-icon-toilet-paper:before{content:"\e758"}.el-icon-notebook-2:before{content:"\e759"}.el-icon-notebook-1:before{content:"\e75a"}.el-icon-files:before{content:"\e75b"}.el-icon-collection:before{content:"\e75c"}.el-icon-receiving:before{content:"\e75d"}.el-icon-suitcase-1:before{content:"\e760"}.el-icon-suitcase:before{content:"\e761"}.el-icon-film:before{content:"\e763"}.el-icon-collection-tag:before{content:"\e765"}.el-icon-data-analysis:before{content:"\e766"}.el-icon-pie-chart:before{content:"\e767"}.el-icon-data-board:before{content:"\e768"}.el-icon-data-line:before{content:"\e76d"}.el-icon-reading:before{content:"\e769"}.el-icon-magic-stick:before{content:"\e76a"}.el-icon-coordinate:before{content:"\e76b"}.el-icon-mouse:before{content:"\e76c"}.el-icon-brush:before{content:"\e76e"}.el-icon-headset:before{content:"\e76f"}.el-icon-umbrella:before{content:"\e770"}.el-icon-scissors:before{content:"\e771"}.el-icon-mobile:before{content:"\e773"}.el-icon-attract:before{content:"\e774"}.el-icon-monitor:before{content:"\e775"}.el-icon-search:before{content:"\e778"}.el-icon-takeaway-box:before{content:"\e77a"}.el-icon-paperclip:before{content:"\e77d"}.el-icon-printer:before{content:"\e77e"}.el-icon-document-add:before{content:"\e782"}.el-icon-document:before{content:"\e785"}.el-icon-document-checked:before{content:"\e786"}.el-icon-document-copy:before{content:"\e787"}.el-icon-document-delete:before{content:"\e788"}.el-icon-document-remove:before{content:"\e789"}.el-icon-tickets:before{content:"\e78b"}.el-icon-folder-checked:before{content:"\e77f"}.el-icon-folder-delete:before{content:"\e780"}.el-icon-folder-remove:before{content:"\e781"}.el-icon-folder-add:before{content:"\e783"}.el-icon-folder-opened:before{content:"\e784"}.el-icon-folder:before{content:"\e78a"}.el-icon-edit-outline:before{content:"\e764"}.el-icon-edit:before{content:"\e78c"}.el-icon-date:before{content:"\e78e"}.el-icon-c-scale-to-original:before{content:"\e7c6"}.el-icon-view:before{content:"\e6ce"}.el-icon-loading:before{content:"\e6cf"}.el-icon-rank:before{content:"\e6d1"}.el-icon-sort-down:before{content:"\e7c4"}.el-icon-sort-up:before{content:"\e7c5"}.el-icon-sort:before{content:"\e6d2"}.el-icon-finished:before{content:"\e6cd"}.el-icon-refresh-left:before{content:"\e6c7"}.el-icon-refresh-right:before{content:"\e6c8"}.el-icon-refresh:before{content:"\e6d0"}.el-icon-video-play:before{content:"\e7c0"}.el-icon-video-pause:before{content:"\e7c1"}.el-icon-d-arrow-right:before{content:"\e6dc"}.el-icon-d-arrow-left:before{content:"\e6dd"}.el-icon-arrow-up:before{content:"\e6e1"}.el-icon-arrow-down:before{content:"\e6df"}.el-icon-arrow-right:before{content:"\e6e0"}.el-icon-arrow-left:before{content:"\e6de"}.el-icon-top-right:before{content:"\e6e7"}.el-icon-top-left:before{content:"\e6e8"}.el-icon-top:before{content:"\e6e6"}.el-icon-bottom:before{content:"\e6eb"}.el-icon-right:before{content:"\e6e9"}.el-icon-back:before{content:"\e6ea"}.el-icon-bottom-right:before{content:"\e6ec"}.el-icon-bottom-left:before{content:"\e6ed"}.el-icon-caret-top:before{content:"\e78f"}.el-icon-caret-bottom:before{content:"\e790"}.el-icon-caret-right:before{content:"\e791"}.el-icon-caret-left:before{content:"\e792"}.el-icon-d-caret:before{content:"\e79a"}.el-icon-share:before{content:"\e793"}.el-icon-menu:before{content:"\e798"}.el-icon-s-grid:before{content:"\e7a6"}.el-icon-s-check:before{content:"\e7a7"}.el-icon-s-data:before{content:"\e7a8"}.el-icon-s-opportunity:before{content:"\e7aa"}.el-icon-s-custom:before{content:"\e7ab"}.el-icon-s-claim:before{content:"\e7ad"}.el-icon-s-finance:before{content:"\e7ae"}.el-icon-s-comment:before{content:"\e7af"}.el-icon-s-flag:before{content:"\e7b0"}.el-icon-s-marketing:before{content:"\e7b1"}.el-icon-s-shop:before{content:"\e7b4"}.el-icon-s-open:before{content:"\e7b5"}.el-icon-s-management:before{content:"\e7b6"}.el-icon-s-ticket:before{content:"\e7b7"}.el-icon-s-release:before{content:"\e7b8"}.el-icon-s-home:before{content:"\e7b9"}.el-icon-s-promotion:before{content:"\e7ba"}.el-icon-s-operation:before{content:"\e7bb"}.el-icon-s-unfold:before{content:"\e7bc"}.el-icon-s-fold:before{content:"\e7a9"}.el-icon-s-platform:before{content:"\e7bd"}.el-icon-s-order:before{content:"\e7be"}.el-icon-s-cooperation:before{content:"\e7bf"}.el-icon-bell:before{content:"\e725"}.el-icon-message-solid:before{content:"\e799"}.el-icon-video-camera:before{content:"\e772"}.el-icon-video-camera-solid:before{content:"\e796"}.el-icon-camera:before{content:"\e779"}.el-icon-camera-solid:before{content:"\e79b"}.el-icon-download:before{content:"\e77c"}.el-icon-upload2:before{content:"\e77b"}.el-icon-upload:before{content:"\e7c3"}.el-icon-picture-outline-round:before{content:"\e75f"}.el-icon-picture-outline:before{content:"\e75e"}.el-icon-picture:before{content:"\e79f"}.el-icon-close:before{content:"\e6db"}.el-icon-check:before{content:"\e6da"}.el-icon-plus:before{content:"\e6d9"}.el-icon-minus:before{content:"\e6d8"}.el-icon-help:before{content:"\e73d"}.el-icon-s-help:before{content:"\e7b3"}.el-icon-circle-close:before{content:"\e78d"}.el-icon-circle-check:before{content:"\e720"}.el-icon-circle-plus-outline:before{content:"\e723"}.el-icon-remove-outline:before{content:"\e722"}.el-icon-zoom-out:before{content:"\e776"}.el-icon-zoom-in:before{content:"\e777"}.el-icon-error:before{content:"\e79d"}.el-icon-success:before{content:"\e79c"}.el-icon-circle-plus:before{content:"\e7a0"}.el-icon-remove:before{content:"\e7a2"}.el-icon-info:before{content:"\e7a1"}.el-icon-question:before{content:"\e7a4"}.el-icon-warning-outline:before{content:"\e6c9"}.el-icon-warning:before{content:"\e7a3"}.el-icon-goods:before{content:"\e7c2"}.el-icon-s-goods:before{content:"\e7b2"}.el-icon-star-off:before{content:"\e717"}.el-icon-star-on:before{content:"\e797"}.el-icon-more-outline:before{content:"\e6cc"}.el-icon-more:before{content:"\e794"}.el-icon-phone-outline:before{content:"\e6cb"}.el-icon-phone:before{content:"\e795"}.el-icon-user:before{content:"\e6e3"}.el-icon-user-solid:before{content:"\e7a5"}.el-icon-setting:before{content:"\e6ca"}.el-icon-s-tools:before{content:"\e7ac"}.el-icon-delete:before{content:"\e6d7"}.el-icon-delete-solid:before{content:"\e7c9"}.el-icon-eleme:before{content:"\e7c7"}.el-icon-platform-eleme:before{content:"\e7ca"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}@keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}.el-pagination{white-space:nowrap;padding:2px 5px;color:#303133;font-weight:700}.el-pagination::after,.el-pagination::before{display:table;content:""}.el-pagination::after{clear:both}.el-pagination button,.el-pagination span:not([class*=suffix]){display:inline-block;font-size:13px;min-width:35.5px;height:28px;line-height:28px;vertical-align:top;-webkit-box-sizing:border-box;box-sizing:border-box}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield;line-height:normal}.el-pagination .el-input__suffix{right:0;-webkit-transform:scale(.8);transform:scale(.8)}.el-pagination .el-select .el-input{width:100px;margin:0 5px}.el-pagination .el-select .el-input .el-input__inner{padding-right:25px;border-radius:3px}.el-pagination button{border:none;padding:0 6px;background:0 0}.el-pagination button:focus{outline:0}.el-pagination button:hover{color:#409EFF}.el-pagination button:disabled{color:#C0C4CC;background-color:#FFF;cursor:not-allowed}.el-pagination .btn-next,.el-pagination .btn-prev{background:center center no-repeat #FFF;background-size:16px;cursor:pointer;margin:0;color:#303133}.el-pagination .btn-next .el-icon,.el-pagination .btn-prev .el-icon{display:block;font-size:12px;font-weight:700}.el-pagination .btn-prev{padding-right:12px}.el-pagination .btn-next{padding-left:12px}.el-pagination .el-pager li.disabled{color:#C0C4CC;cursor:not-allowed}.el-pager li,.el-pager li.btn-quicknext:hover,.el-pager li.btn-quickprev:hover{cursor:pointer}.el-pagination--small .btn-next,.el-pagination--small .btn-prev,.el-pagination--small .el-pager li,.el-pagination--small .el-pager li.btn-quicknext,.el-pagination--small .el-pager li.btn-quickprev,.el-pagination--small .el-pager li:last-child{border-color:transparent;font-size:12px;line-height:22px;height:22px;min-width:22px}.el-pagination--small .arrow.disabled{visibility:hidden}.el-pagination--small .more::before,.el-pagination--small li.more::before{line-height:24px}.el-pagination--small button,.el-pagination--small span:not([class*=suffix]){height:22px;line-height:22px}.el-pagination--small .el-pagination__editor,.el-pagination--small .el-pagination__editor.el-input .el-input__inner{height:22px}.el-pagination__sizes{margin:0 10px 0 0;font-weight:400;color:#606266}.el-pagination__sizes .el-input .el-input__inner{font-size:13px;padding-left:8px}.el-pagination__sizes .el-input .el-input__inner:hover{border-color:#409EFF}.el-pagination__total{margin-right:10px;font-weight:400;color:#606266}.el-pagination__jump{margin-left:24px;font-weight:400;color:#606266}.el-pagination__jump .el-input__inner{padding:0 3px}.el-pagination__rightwrapper{float:right}.el-pagination__editor{line-height:18px;padding:0 2px;height:28px;text-align:center;margin:0 2px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:3px}.el-pager,.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev{padding:0}.el-dialog,.el-pager li{-webkit-box-sizing:border-box}.el-pagination__editor.el-input{width:50px}.el-pagination__editor.el-input .el-input__inner{height:28px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button,.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination.is-background .btn-next,.el-pagination.is-background .btn-prev,.el-pagination.is-background .el-pager li{margin:0 5px;background-color:#f4f4f5;color:#606266;min-width:30px;border-radius:2px}.el-pagination.is-background .btn-next.disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-prev.disabled,.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .el-pager li.disabled{color:#C0C4CC}.el-pagination.is-background .el-pager li:not(.disabled):hover{color:#409EFF}.el-pagination.is-background .el-pager li:not(.disabled).active{background-color:#409EFF;color:#FFF}.el-pagination.is-background.el-pagination--small .btn-next,.el-pagination.is-background.el-pagination--small .btn-prev,.el-pagination.is-background.el-pagination--small .el-pager li{margin:0 3px;min-width:22px}.el-pager,.el-pager li{vertical-align:top;margin:0;display:inline-block}.el-pager{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;list-style:none;font-size:0}.el-pager .more::before{line-height:30px}.el-pager li{padding:0 4px;background:#FFF;font-size:13px;min-width:35.5px;height:28px;line-height:28px;box-sizing:border-box;text-align:center}.el-pager li.btn-quicknext,.el-pager li.btn-quickprev{line-height:28px;color:#303133}.el-pager li.btn-quicknext.disabled,.el-pager li.btn-quickprev.disabled{color:#C0C4CC}.el-pager li.active+li{border-left:0}.el-pager li:hover{color:#409EFF}.el-pager li.active{color:#409EFF;cursor:default}@-webkit-keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}.el-dialog{position:relative;margin:0 auto 50px;background:#FFF;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#409EFF}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px;word-break:break-all}.el-dialog__footer{padding:10px 20px 20px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-autocomplete{position:relative;display:inline-block}.el-autocomplete-suggestion{margin:5px 0;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:4px;border:1px solid #E4E7ED;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:#FFF}.el-autocomplete-suggestion__wrap{max-height:280px;padding:10px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{padding:0 20px;margin:0;line-height:34px;cursor:pointer;color:#606266;font-size:14px;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-autocomplete-suggestion li.highlighted,.el-autocomplete-suggestion li:hover{background-color:#F5F7FA}.el-autocomplete-suggestion li.divider{margin-top:6px;border-top:1px solid #000}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{text-align:center;height:100px;line-height:100px;font-size:20px;color:#999}.el-autocomplete-suggestion.is-loading li::after{display:inline-block;content:"";height:100%;vertical-align:middle}.el-autocomplete-suggestion.is-loading li:hover{background-color:#FFF}.el-autocomplete-suggestion.is-loading .el-icon-loading{vertical-align:middle}.el-dropdown{display:inline-block;position:relative;color:#606266;font-size:14px}.el-dropdown .el-button-group{display:block}.el-dropdown .el-button-group .el-button{float:none}.el-dropdown .el-dropdown__caret-button{padding-left:5px;padding-right:5px;position:relative;border-left:none}.el-dropdown .el-dropdown__caret-button::before{content:'';position:absolute;display:block;width:1px;top:5px;bottom:5px;left:0;background:rgba(255,255,255,.5)}.el-dropdown .el-dropdown__caret-button.el-button--default::before{background:rgba(220,223,230,.5)}.el-dropdown .el-dropdown__caret-button:hover:not(.is-disabled)::before{top:0;bottom:0}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{padding-left:0}.el-dropdown__icon{font-size:12px;margin:0 3px}.el-dropdown .el-dropdown-selfdefine:focus:active,.el-dropdown .el-dropdown-selfdefine:focus:not(.focusing){outline-width:0}.el-dropdown [disabled]{cursor:not-allowed;color:#bbb}.el-dropdown-menu{position:absolute;top:0;left:0;z-index:10;padding:10px 0;margin:5px 0;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-dropdown-menu__item,.el-menu-item{font-size:14px;padding:0 20px;cursor:pointer}.el-dropdown-menu__item{list-style:none;line-height:36px;margin:0;color:#606266;outline:0}.el-dropdown-menu__item:focus,.el-dropdown-menu__item:not(.is-disabled):hover{background-color:#ecf5ff;color:#66b1ff}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{position:relative;margin-top:6px;border-top:1px solid #EBEEF5}.el-dropdown-menu__item--divided:before{content:'';height:6px;display:block;margin:0 -20px;background-color:#FFF}.el-dropdown-menu__item.is-disabled{cursor:default;color:#bbb;pointer-events:none}.el-dropdown-menu--medium{padding:6px 0}.el-dropdown-menu--medium .el-dropdown-menu__item{line-height:30px;padding:0 17px;font-size:14px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:6px}.el-dropdown-menu--medium .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:6px;margin:0 -17px}.el-dropdown-menu--small{padding:6px 0}.el-dropdown-menu--small .el-dropdown-menu__item{line-height:27px;padding:0 15px;font-size:13px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:4px}.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:4px;margin:0 -15px}.el-dropdown-menu--mini{padding:3px 0}.el-dropdown-menu--mini .el-dropdown-menu__item{line-height:24px;padding:0 10px;font-size:12px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided{margin-top:3px}.el-dropdown-menu--mini .el-dropdown-menu__item.el-dropdown-menu__item--divided:before{height:3px;margin:0 -10px}.el-menu{border-right:solid 1px #e6e6e6;list-style:none;position:relative;margin:0;padding-left:0;background-color:#FFF}.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus,.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover,.el-menu--horizontal>.el-submenu .el-submenu__title:hover{background-color:#fff}.el-menu::after,.el-menu::before{display:table;content:""}.el-breadcrumb__item:last-child .el-breadcrumb__separator,.el-menu--collapse>.el-menu-item .el-submenu__icon-arrow,.el-menu--collapse>.el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}.el-menu::after{clear:both}.el-menu.el-menu--horizontal{border-bottom:solid 1px #e6e6e6}.el-menu--horizontal{border-right:none}.el-menu--horizontal>.el-menu-item{float:left;height:60px;line-height:60px;margin:0;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-submenu{float:left}.el-menu--horizontal>.el-submenu:focus,.el-menu--horizontal>.el-submenu:hover{outline:0}.el-menu--horizontal>.el-submenu:focus .el-submenu__title,.el-menu--horizontal>.el-submenu:hover .el-submenu__title{color:#303133}.el-menu--horizontal>.el-submenu.is-active .el-submenu__title{border-bottom:2px solid #409EFF;color:#303133}.el-menu--horizontal>.el-submenu .el-submenu__title{height:60px;line-height:60px;border-bottom:2px solid transparent;color:#909399}.el-menu--horizontal>.el-submenu .el-submenu__icon-arrow{position:static;vertical-align:middle;margin-left:8px;margin-top:-3px}.el-menu--collapse .el-submenu,.el-menu-item{position:relative}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-submenu__title{background-color:#FFF;float:none;height:36px;line-height:36px;padding:0 10px;color:#909399}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-submenu.is-active>.el-submenu__title{color:#303133}.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,.el-menu--horizontal .el-menu-item:not(.is-disabled):hover{outline:0;color:#303133}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid #409EFF;color:#303133}.el-menu--collapse{width:64px}.el-menu--collapse>.el-menu-item [class^=el-icon-],.el-menu--collapse>.el-submenu>.el-submenu__title [class^=el-icon-]{margin:0;vertical-align:middle;width:24px;text-align:center}.el-menu--collapse>.el-menu-item span,.el-menu--collapse>.el-submenu>.el-submenu__title span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}.el-menu-item,.el-submenu__title{height:56px;line-height:56px;list-style:none}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-menu .el-submenu{min-width:200px}.el-menu--collapse .el-submenu .el-menu{position:absolute;margin-left:5px;top:0;left:100%;z-index:10;border:1px solid #E4E7ED;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--collapse .el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:none;transform:none}.el-menu--popup{z-index:100;min-width:200px;border:none;padding:5px 0;border-radius:2px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-menu--popup-bottom-start{margin-top:5px}.el-menu--popup-right-start{margin-left:5px;margin-right:5px}.el-menu-item{color:#303133;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap}.el-radio-button__inner,.el-submenu__title{-webkit-box-sizing:border-box;position:relative;white-space:nowrap}.el-menu-item *{vertical-align:middle}.el-menu-item i{color:#909399}.el-menu-item:focus,.el-menu-item:hover{outline:0;background-color:#ecf5ff}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon-]{margin-right:5px;width:24px;text-align:center;font-size:18px;vertical-align:middle}.el-menu-item.is-active{color:#409EFF}.el-menu-item.is-active i{color:inherit}.el-submenu{list-style:none;margin:0;padding-left:0}.el-submenu__title{font-size:14px;color:#303133;padding:0 20px;cursor:pointer;-webkit-transition:border-color .3s,background-color .3s,color .3s;transition:border-color .3s,background-color .3s,color .3s;box-sizing:border-box}.el-submenu__title *{vertical-align:middle}.el-submenu__title i{color:#909399}.el-submenu__title:focus,.el-submenu__title:hover{outline:0;background-color:#ecf5ff}.el-submenu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu__title:hover{background-color:#ecf5ff}.el-submenu .el-menu{border:none}.el-submenu .el-menu-item{height:50px;line-height:50px;padding:0 45px;min-width:200px}.el-submenu__icon-arrow{position:absolute;top:50%;right:20px;margin-top:-7px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:12px}.el-submenu.is-active .el-submenu__title{border-bottom-color:#409EFF}.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.el-submenu.is-disabled .el-menu-item,.el-submenu.is-disabled .el-submenu__title{opacity:.25;cursor:not-allowed;background:0 0!important}.el-submenu [class^=el-icon-]{vertical-align:middle;margin-right:5px;width:24px;text-align:center;font-size:18px}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px 20px;line-height:normal;font-size:12px;color:#909399}.el-radio-button__inner,.el-radio-group{display:inline-block;line-height:1;vertical-align:middle}.horizontal-collapse-transition .el-submenu__title .el-submenu__icon-arrow{-webkit-transition:.2s;transition:.2s;opacity:0}.el-radio-group{font-size:0}.el-radio-button{position:relative;display:inline-block;outline:0}.el-radio-button__inner{background:#FFF;border:1px solid #DCDFE6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;box-sizing:border-box;outline:0;margin:0;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#409EFF}.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-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;-webkit-box-shadow:-1px 0 0 0 #409EFF;box-shadow:-1px 0 0 0 #409EFF}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;-webkit-box-shadow:none;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#F2F6FC}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active):not(.is-disabled){-webkit-box-shadow:0 0 2px 2px #409EFF;box-shadow:0 0 2px 2px #409EFF}.el-picker-panel,.el-popover,.el-select-dropdown,.el-table-filter,.el-time-panel{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-switch{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;font-size:14px;line-height:20px;height:20px;vertical-align:middle}.el-switch__core,.el-switch__label{display:inline-block;cursor:pointer}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{-webkit-transition:.2s;transition:.2s;height:20px;font-size:14px;font-weight:500;vertical-align:middle;color:#303133}.el-switch__label.is-active{color:#409EFF}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{line-height:1;font-size:14px;display:inline-block}.el-switch__input{position:absolute;width:0;height:0;opacity:0;margin:0}.el-switch__core{margin:0;position:relative;width:40px;height:20px;border:1px solid #DCDFE6;outline:0;border-radius:10px;-webkit-box-sizing:border-box;box-sizing:border-box;background:#DCDFE6;-webkit-transition:border-color .3s,background-color .3s;transition:border-color .3s,background-color .3s;vertical-align:middle}.el-input__prefix,.el-input__suffix{-webkit-transition:all .3s;color:#C0C4CC}.el-switch__core:after{content:"";position:absolute;top:1px;left:1px;border-radius:100%;-webkit-transition:all .3s;transition:all .3s;width:16px;height:16px;background-color:#FFF}.el-switch.is-checked .el-switch__core{border-color:#409EFF;background-color:#409EFF}.el-switch.is-checked .el-switch__core::after{left:100%;margin-left:-17px}.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,.el-switch .label-fade-leave-active{opacity:0}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #E4E7ED;border-radius:4px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item{padding-right:40px}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#409EFF;background-color:#FFF}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#F5F7FA}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\e6da";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#FFF}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#F5F7FA}.el-select-dropdown__item.selected{color:#409EFF;font-weight:700}.el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#E4E7ED}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{display:inline-block;position:relative}.el-select .el-select__tags>span{display:contents}.el-select:hover .el-input__inner{border-color:#C0C4CC}.el-select .el-input__inner{cursor:pointer;padding-right:35px}.el-select .el-input__inner:focus{border-color:#409EFF}.el-select .el-input .el-select__caret{color:#C0C4CC;font-size:14px;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);cursor:pointer}.el-select .el-input .el-select__caret.is-reverse{-webkit-transform:rotateZ(0);transform:rotateZ(0)}.el-select .el-input .el-select__caret.is-show-close{font-size:14px;text-align:center;-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg);border-radius:100%;color:#C0C4CC;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-select .el-input .el-select__caret.is-show-close:hover{color:#909399}.el-select .el-input.is-disabled .el-input__inner{cursor:not-allowed}.el-select .el-input.is-disabled .el-input__inner:hover{border-color:#E4E7ED}.el-range-editor.is-active,.el-range-editor.is-active:hover,.el-select .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-select>.el-input{display:block}.el-select__input{border:none;outline:0;padding:0;margin-left:15px;color:#666;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:28px;background-color:transparent}.el-select__input.is-mini{height:14px}.el-select__close{cursor:pointer;position:absolute;top:8px;z-index:1000;right:25px;color:#C0C4CC;line-height:18px;font-size:14px}.el-select__close:hover{color:#909399}.el-select__tags{position:absolute;line-height:normal;white-space:normal;z-index:1;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-select__tags-text{overflow:hidden;text-overflow:ellipsis}.el-select .el-tag{-webkit-box-sizing:border-box;box-sizing:border-box;border-color:transparent;margin:2px 0 2px 6px;background-color:#f0f2f5;display:-webkit-box;display:-ms-flexbox;display:flex;max-width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-select .el-tag__close.el-icon-close{background-color:#C0C4CC;top:0;color:#FFF;-ms-flex-negative:0;flex-shrink:0}.el-select .el-tag__close.el-icon-close:hover{background-color:#909399}.el-table,.el-table__expanded-cell{background-color:#FFF}.el-select .el-tag__close.el-icon-close::before{display:block;-webkit-transform:translate(0,.5px);transform:translate(0,.5px)}.el-table{position:relative;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-flex:1;-ms-flex:1;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;font-size:12px;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit .el-table__cell.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th.el-table__cell{background:#F5F7FA}.el-table .el-table__cell{padding:12px 0;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.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{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table .el-table__cell.is-hidden>*{visibility:hidden}.el-table--medium .el-table__cell{padding:10px 0}.el-table--small{font-size:12px}.el-table--small .el-table__cell{padding:8px 0}.el-table--mini{font-size:12px}.el-table--mini .el-table__cell{padding:6px 0}.el-table tr{background-color:#FFF}.el-table tr input[type=checkbox]{margin:0}.el-table td.el-table__cell,.el-table th.el-table__cell.is-leaf{border-bottom:1px solid #EBEEF5}.el-table th.el-table__cell.is-sortable{cursor:pointer}.el-table th.el-table__cell{overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#FFF}.el-table th.el-table__cell>.cell{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th.el-table__cell>.cell.highlight{color:#409EFF}.el-table th.el-table__cell.required>div::before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td.el-table__cell div{-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-table td,.el-table .cell,.el-table-filter{-webkit-box-sizing:border-box}.el-table td.el-table__cell.gutter{width:0}.el-table .cell{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-left:10px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #EBEEF5}.el-table--border::after,.el-table--group::after,.el-table::before{content:'';position:absolute;background-color:#EBEEF5;z-index:1}.el-table--border::after,.el-table--group::after{top:0;right:0;width:1px;height:100%}.el-table::before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border .el-table__cell,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #EBEEF5}.el-table--border .el-table__cell:first-child .cell{padding-left:10px}.el-table--border th.el-table__cell.gutter:last-of-type{border-bottom:1px solid #EBEEF5;border-bottom-width:1px}.el-table--border th.el-table__cell,.el-table__fixed-right-patch{border-bottom:1px solid #EBEEF5}.el-table--hidden{visibility:hidden}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;-webkit-box-shadow:0 0 10px rgba(0,0,0,.12);box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right::before,.el-table__fixed::before{content:'';position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#EBEEF5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#FFF}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td.el-table__cell{border-top:1px solid #EBEEF5;background-color:#F5F7FA;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td.el-table__cell{border-top:1px solid #EBEEF5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td.el-table__cell,.el-table__header-wrapper tbody td.el-table__cell{background-color:#F5F7FA;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{-webkit-box-shadow:none;box-shadow:none}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #EBEEF5}.el-table .caret-wrapper{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#C0C4CC;top:5px}.el-table .sort-caret.descending{border-top-color:#C0C4CC;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409EFF}.el-table .descending .sort-caret.descending{border-top-color:#409EFF}.el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell{background:#FAFAFA}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td.el-table__cell{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.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>td.el-table__cell{background-color:#F5F7FA}.el-table__body tr.current-row>td.el-table__cell{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #EBEEF5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;-webkit-transform:scale(.75);transform:scale(.75)}.el-table--enable-row-transition .el-table__body td.el-table__cell{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell{background-color:#F5F7FA}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #EBEEF5;border-radius:2px;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-sizing:border-box;margin:2px 0}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409EFF;color:#FFF}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #EBEEF5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-date-table td.in-range div,.el-date-table td.in-range div:hover,.el-date-table.is-week-mode .el-date-table__row.current div,.el-date-table.is-week-mode .el-date-table__row:hover div{background-color:#F2F6FC}.el-table-filter__bottom button:hover{color:#409EFF}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#C0C4CC;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{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-date-table{font-size:12px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:#606266}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td{width:32px;height:30px;padding:4px 0;box-sizing:border-box;text-align:center;cursor:pointer;position:relative}.el-date-table td div{height:30px;padding:3px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-table td span{width:24px;height:24px;display:block;margin:0 auto;line-height:24px;position:absolute;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);border-radius:50%}.el-date-table td.next-month,.el-date-table td.prev-month{color:#C0C4CC}.el-date-table td.today{position:relative}.el-date-table td.today span{color:#409EFF;font-weight:700}.el-date-table td.today.end-date span,.el-date-table td.today.start-date span{color:#FFF}.el-date-table td.available:hover{color:#409EFF}.el-date-table td.current:not(.disabled) span{color:#FFF;background-color:#409EFF}.el-date-table td.end-date div,.el-date-table td.start-date div{color:#FFF}.el-date-table td.end-date span,.el-date-table td.start-date span{background-color:#409EFF}.el-date-table td.start-date div{margin-left:5px;border-top-left-radius:15px;border-bottom-left-radius:15px}.el-date-table td.end-date div{margin-right:5px;border-top-right-radius:15px;border-bottom-right-radius:15px}.el-date-table td.disabled div{background-color:#F5F7FA;opacity:1;cursor:not-allowed;color:#C0C4CC}.el-date-table td.selected div{margin-left:5px;margin-right:5px;background-color:#F2F6FC;border-radius:15px}.el-date-table td.selected div:hover{background-color:#F2F6FC}.el-date-table td.selected span{background-color:#409EFF;color:#FFF;border-radius:15px}.el-date-table td.week{font-size:80%;color:#606266}.el-month-table,.el-year-table{font-size:12px;border-collapse:collapse}.el-date-table th{padding:5px;color:#606266;font-weight:400;border-bottom:solid 1px #EBEEF5}.el-month-table{margin:-1px}.el-month-table td{text-align:center;padding:8px 0;cursor:pointer}.el-month-table td div{height:48px;padding:6px 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-month-table td.today .cell{color:#409EFF;font-weight:700}.el-month-table td.today.end-date .cell,.el-month-table td.today.start-date .cell{color:#FFF}.el-month-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-month-table td.disabled .cell:hover{color:#C0C4CC}.el-month-table td .cell{width:60px;height:36px;display:block;line-height:36px;color:#606266;margin:0 auto;border-radius:18px}.el-month-table td .cell:hover{color:#409EFF}.el-month-table td.in-range div,.el-month-table td.in-range div:hover{background-color:#F2F6FC}.el-month-table td.end-date div,.el-month-table td.start-date div{color:#FFF}.el-month-table td.end-date .cell,.el-month-table td.start-date .cell{color:#FFF;background-color:#409EFF}.el-month-table td.start-date div{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-month-table td.end-date div{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-month-table td.current:not(.disabled) .cell{color:#409EFF}.el-year-table{margin:-1px}.el-year-table .el-icon{color:#303133}.el-year-table td{text-align:center;padding:20px 3px;cursor:pointer}.el-year-table td.today .cell{color:#409EFF;font-weight:700}.el-year-table td.disabled .cell{background-color:#F5F7FA;cursor:not-allowed;color:#C0C4CC}.el-year-table td.disabled .cell:hover{color:#C0C4CC}.el-year-table td .cell{width:48px;height:32px;display:block;line-height:32px;color:#606266;margin:0 auto}.el-year-table td .cell:hover,.el-year-table td.current:not(.disabled) .cell{color:#409EFF}.el-date-range-picker{width:646px}.el-date-range-picker.has-sidebar{width:756px}.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{position:relative;text-align:center;height:28px}.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{font-size:16px;font-weight:500;margin-right:50px}.el-date-range-picker__content{float:left;width:50%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:16px}.el-date-range-picker__content.is-left{border-right:1px solid #e4e4e4}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{-webkit-box-sizing:border-box;box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-range-picker__time-header>.el-icon-arrow-right{font-size:20px;vertical-align:middle;display:table-cell;color:#303133}.el-date-range-picker__time-picker-wrap{position:relative;display:table-cell;padding:0 5px}.el-date-range-picker__time-picker-wrap .el-picker-panel{position:absolute;top:13px;right:0;z-index:1;background:#FFF}.el-date-picker{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{position:relative;display:table-cell;padding:0 5px}.el-date-picker__time-header{position:relative;border-bottom:1px solid #e4e4e4;font-size:12px;padding:8px 5px 5px;display:table;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.el-date-picker__header{margin:12px;text-align:center}.el-date-picker__header--bordered{margin-bottom:0;padding-bottom:12px;border-bottom:solid 1px #EBEEF5}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{font-size:16px;font-weight:500;padding:0 5px;line-height:22px;text-align:center;cursor:pointer;color:#606266}.el-date-picker__header-label.active,.el-date-picker__header-label:hover{color:#409EFF}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{padding:10px;text-align:center}.el-date-picker__time-label{float:left;cursor:pointer;line-height:30px;margin-left:10px}.time-select{margin:5px 0;min-width: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.selected:not(.disabled){color:#409EFF;font-weight:700}.time-select-item.disabled{color:#E4E7ED;cursor:not-allowed}.time-select-item:hover{background-color:#F5F7FA;font-weight:700;cursor:pointer}.el-date-editor{position:relative;display:inline-block;text-align:left}.el-date-editor.el-input,.el-date-editor.el-input__inner{width:220px}.el-date-editor--monthrange.el-input,.el-date-editor--monthrange.el-input__inner{width:300px}.el-date-editor--daterange.el-input,.el-date-editor--daterange.el-input__inner,.el-date-editor--timerange.el-input,.el-date-editor--timerange.el-input__inner{width:350px}.el-date-editor--datetimerange.el-input,.el-date-editor--datetimerange.el-input__inner{width:400px}.el-date-editor--dates .el-input__inner{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .el-icon-circle-close{cursor:pointer}.el-date-editor .el-range__icon{font-size:14px;margin-left:-5px;color:#C0C4CC;float:left;line-height:32px}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;outline:0;display:inline-block;height:100%;margin:0;padding:0;width:39%;text-align:center;font-size:14px;color:#606266}.el-date-editor .el-range-input::-webkit-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input:-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::-ms-input-placeholder{color:#C0C4CC}.el-date-editor .el-range-input::placeholder{color:#C0C4CC}.el-date-editor .el-range-separator{display:inline-block;height:100%;padding:0 5px;margin:0;text-align:center;line-height:32px;font-size:14px;width:5%;color:#303133}.el-date-editor .el-range__close-icon{font-size:14px;color:#C0C4CC;width:25px;display:inline-block;float:right;line-height:32px}.el-range-editor.el-input__inner{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:3px 10px}.el-range-editor .el-range-input{line-height:1}.el-range-editor--medium.el-input__inner{height:36px}.el-range-editor--medium .el-range-separator{line-height:28px;font-size:14px}.el-range-editor--medium .el-range-input{font-size:14px}.el-range-editor--medium .el-range__close-icon,.el-range-editor--medium .el-range__icon{line-height:28px}.el-range-editor--small.el-input__inner{height:32px}.el-range-editor--small .el-range-separator{line-height:24px;font-size:13px}.el-range-editor--small .el-range-input{font-size:13px}.el-range-editor--small .el-range__close-icon,.el-range-editor--small .el-range__icon{line-height:24px}.el-range-editor--mini.el-input__inner{height:28px}.el-range-editor--mini .el-range-separator{line-height:20px;font-size:12px}.el-range-editor--mini .el-range-input{font-size:12px}.el-range-editor--mini .el-range__close-icon,.el-range-editor--mini .el-range__icon{line-height:20px}.el-range-editor.is-disabled{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled:focus,.el-range-editor.is-disabled:hover{border-color:#E4E7ED}.el-range-editor.is-disabled input{background-color:#F5F7FA;color:#C0C4CC;cursor:not-allowed}.el-range-editor.is-disabled input::-webkit-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input:-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::-ms-input-placeholder{color:#C0C4CC}.el-range-editor.is-disabled input::placeholder{color:#C0C4CC}.el-range-editor.is-disabled .el-range-separator{color:#C0C4CC}.el-picker-panel{color:#606266;border:1px solid #E4E7ED;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);background:#FFF;border-radius:4px;line-height:30px;margin:5px 0}.el-picker-panel__body-wrapper::after,.el-picker-panel__body::after{content:"";display:table;clear:both}.el-picker-panel__content{position:relative;margin:15px}.el-picker-panel__footer{border-top:1px solid #e4e4e4;padding:4px;text-align:right;background-color:#FFF;position:relative;font-size:0}.el-picker-panel__shortcut{display:block;width:100%;border:0;background-color:transparent;line-height:28px;font-size:14px;color:#606266;padding-left:12px;text-align:left;outline:0;cursor:pointer}.el-picker-panel__shortcut:hover{color:#409EFF}.el-picker-panel__shortcut.active{background-color:#e6f1fe;color:#409EFF}.el-picker-panel__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-picker-panel__btn[disabled]{color:#ccc;cursor:not-allowed}.el-picker-panel__icon-btn{font-size:12px;color:#303133;border:0;background:0 0;cursor:pointer;outline:0;margin-top:8px}.el-picker-panel__icon-btn:hover{color:#409EFF}.el-picker-panel__icon-btn.is-disabled{color:#bbb}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{position:absolute;top:0;bottom:0;width:110px;border-right:1px solid #e4e4e4;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;background-color:#FFF;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{max-height:190px;overflow:auto;display:inline-block;width:50%;vertical-align:top;position:relative}.el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__input.el-input .el-input__inner,.el-time-spinner__list{padding:0;text-align:center}.el-time-spinner__wrapper.is-arrow{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{-webkit-transform:translateY(-32px);transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.disabled):not(.active){background:#FFF;cursor:default}.el-time-spinner__arrow{font-size:12px;color:#909399;position:absolute;left:0;width:100%;z-index:1;text-align:center;height:30px;line-height:30px;cursor:pointer}.el-time-spinner__arrow:hover{color:#409EFF}.el-time-spinner__arrow.el-icon-arrow-up{top:10px}.el-time-spinner__arrow.el-icon-arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__list{margin:0;list-style:none}.el-time-spinner__list::after,.el-time-spinner__list::before{content:'';display:block;width:100%;height:80px}.el-time-spinner__item{height:32px;line-height:32px;font-size:12px;color:#606266}.el-time-spinner__item:hover:not(.disabled):not(.active){background:#F5F7FA;cursor:pointer}.el-time-spinner__item.active:not(.disabled){color:#303133;font-weight:700}.el-time-spinner__item.disabled{color:#C0C4CC;cursor:not-allowed}.el-time-panel{margin:5px 0;border:1px solid #E4E7ED;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);border-radius:2px;position:absolute;width:180px;left:0;z-index:1000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:content-box;box-sizing:content-box}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content::after,.el-time-panel__content::before{content:"";top:50%;position:absolute;margin-top:-15px;height:32px;z-index:-1;left:0;right:0;-webkit-box-sizing:border-box;box-sizing:border-box;padding-top:6px;text-align:left;border-top:1px solid #E4E7ED;border-bottom:1px solid #E4E7ED}.el-time-panel__content::after{left:50%;margin-left:12%;margin-right:12%}.el-time-panel__content::before{padding-left:50%;margin-right:12%;margin-left:12%}.el-time-panel__content.has-seconds::after{left:calc(100% / 3 * 2)}.el-time-panel__content.has-seconds::before{padding-left:calc(100% / 3)}.el-time-panel__footer{border-top:1px solid #e4e4e4;padding:4px;height:36px;line-height:25px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-time-panel__btn{border:none;line-height:28px;padding:0 5px;margin:0 5px;cursor:pointer;background-color:transparent;outline:0;font-size:12px;color:#303133}.el-time-panel__btn.confirm{font-weight:800;color:#409EFF}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{position:relative;text-align:center;padding:10px}.el-time-range-picker__cell{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:4px 7px 7px;width:50%;display:inline-block}.el-time-range-picker__header{margin-bottom:5px;text-align:center;font-size:14px}.el-time-range-picker__body{border-radius:2px;border:1px solid #E4E7ED}.el-popover{position:absolute;background:#FFF;min-width:150px;border-radius:4px;border:1px solid #EBEEF5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover,.el-cascader__dropdown,.el-color-picker__panel,.el-message-box,.el-notification{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#FFF;border-radius:4px;border:1px solid #EBEEF5;font-size:18px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;overflow:hidden;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper::after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus,.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#F56C6C}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409EFF}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__status{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);font-size:24px!important}.el-message-box__status::before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67C23A}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#E6A23C}.el-message-box__status.el-icon-error{color:#F56C6C}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#F56C6C;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#C0C4CC}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#409EFF;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{display:inline-block;margin-right:10px;vertical-align:top}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item::after{clear:both}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label-wrap{float:left}.el-form-item__label-wrap .el-form-item__label{display:inline-block;float:none}.el-form-item__label{text-align:right;vertical-align:middle;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__content::after{clear:both}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:#F56C6C;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:'*';color:#F56C6C;margin-right:4px}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#F56C6C}.el-form-item--feedback .el-input__validateIcon{display:inline-block}.el-tabs__header{padding:0;position:relative;margin:0 0 15px}.el-tabs__active-bar{position:absolute;bottom:0;left:0;height:2px;background-color:#409EFF;z-index:1;-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);list-style:none}.el-tabs__new-tab{float:right;border:1px solid #d3dce6;height:18px;width:18px;line-height:18px;margin:12px 0 9px 10px;border-radius:3px;text-align:center;font-size:12px;color:#d3dce6;cursor:pointer;-webkit-transition:all .15s;transition:all .15s}.el-tabs__new-tab .el-icon-plus{-webkit-transform:scale(.8,.8);transform:scale(.8,.8)}.el-tabs__new-tab:hover{color:#409EFF}.el-tabs__nav-wrap{overflow:hidden;margin-bottom:-1px;position:relative}.el-tabs__nav-wrap::after{content:"";position:absolute;left:0;bottom:0;width:100%;height:2px;background-color:#E4E7ED;z-index:1}.el-tabs__nav-wrap.is-scrollable{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{position:absolute;cursor:pointer;line-height:44px;font-size:12px;color:#909399}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;position:relative;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;float:left;z-index:2}.el-tabs__nav.is-stretch{min-width:100%;display:-webkit-box;display:-ms-flexbox;display:flex}.el-tabs__nav.is-stretch>*{-webkit-box-flex:1;-ms-flex:1;flex:1;text-align:center}.el-tabs__item{padding:0 20px;height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:40px;display:inline-block;list-style:none;font-size:14px;font-weight:500;color:#303133;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:0}.el-tabs__item:focus.is-active.is-focus:not(:active){-webkit-box-shadow:0 0 2px 2px #409EFF inset;box-shadow:0 0 2px 2px #409EFF inset;border-radius:3px}.el-tabs__item .el-icon-close{border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);margin-left:5px}.el-tabs__item .el-icon-close:before{-webkit-transform:scale(.9);transform:scale(.9);display:inline-block}.el-tabs--card>.el-tabs__header .el-tabs__active-bar,.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left,.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs__item .el-icon-close:hover{background-color:#C0C4CC;color:#FFF}.el-tabs__item.is-active{color:#409EFF}.el-tabs__item:hover{color:#409EFF;cursor:pointer}.el-tabs__item.is-disabled{color:#C0C4CC;cursor:default}.el-tabs__content{overflow:hidden;position:relative}.el-tabs--card>.el-tabs__header{border-bottom:1px solid #E4E7ED}.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 #E4E7ED;border-bottom:none;border-radius:4px 4px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__item .el-icon-close{position:relative;font-size:12px;width:0;height:14px;vertical-align:middle;line-height:15px;overflow:hidden;top:-1px;right:-2px;-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid #E4E7ED;-webkit-transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1);transition:color .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.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 .el-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:#FFF}.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 .el-icon-close{width:14px}.el-tabs--border-card{background:#FFF;border:1px solid #DCDFE6;-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04);box-shadow:0 2px 4px 0 rgba(0,0,0,.12),0 0 6px 0 rgba(0,0,0,.04)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:#F5F7FA;border-bottom:1px solid #E4E7ED;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{-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);border:1px solid transparent;margin-top:-1px;color:#909399}.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item,.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child{margin-left:-1px}.el-col-offset-0,.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:#409EFF;background-color:#FFF;border-right-color:#DCDFE6;border-left-color:#DCDFE6}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:#409EFF}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:#C0C4CC}.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2),.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),.el-tabs--top .el-tabs__item.is-top:nth-child(2){padding-left:0}.el-tabs--bottom .el-tabs__item.is-bottom:last-child,.el-tabs--bottom .el-tabs__item.is-top:last-child,.el-tabs--top .el-tabs__item.is-bottom:last-child,.el-tabs--top .el-tabs__item.is-top:last-child{padding-right:0}.el-cascader-menu:last-child .el-cascader-node,.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom .el-tabs--right>.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,.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:last-child,.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{padding-right:20px}.el-tabs--bottom .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom .el-tabs--right>.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),.el-tabs--top .el-tabs--left>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top .el-tabs--right>.el-tabs__header .el-tabs__item:nth-child(2),.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){padding-left:20px}.el-tabs--bottom .el-tabs__header.is-bottom{margin-bottom:0;margin-top:10px}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid #DCDFE6}.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 transparent}.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-scroll,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-scroll,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{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{top:0;bottom:auto;width:2px;height: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-next,.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-next,.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-next,.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-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{height:30px;line-height:30px;width:100%;text-align:center;cursor:pointer}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.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-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i{-webkit-transform:rotateZ(90deg);transform:rotateZ(90deg)}.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{left:auto;top:0}.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{right:auto;bottom:0}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__nav-wrap.is-left::after{right:0;left: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{height:100%;width:2px;bottom:auto;top:0}.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{float:none}.el-tabs--left .el-tabs__item.is-left,.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-right{display:block}.el-tabs--left .el-tabs__header.is-left{float:left;margin-bottom:0;margin-right:10px}.el-button-group>.el-button:not(:last-child),.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid #E4E7ED;border-bottom:none;border-top:1px solid #E4E7ED;text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid #E4E7ED;border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid #E4E7ED;border-right-color:#fff;border-left:none;border-bottom:none}.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-radius:4px 0 0 4px;border-bottom:1px solid #E4E7ED;border-right:none}.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 #dfe4ed}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid transparent;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 transparent}.el-tabs--right .el-tabs__header.is-right{float: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__item.is-right{border-bottom:none;border-top:1px solid #E4E7ED}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid #E4E7ED;border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid #E4E7ED;border-left-color:#fff;border-right:none;border-bottom: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-radius:0 4px 4px 0;border-bottom:1px solid #E4E7ED;border-left:none}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid #dfe4ed}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid transparent;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 transparent}.slideInLeft-transition,.slideInRight-transition{display:inline-block}.slideInRight-enter{-webkit-animation:slideInRight-enter .3s;animation:slideInRight-enter .3s}.slideInRight-leave{position:absolute;left:0;right:0;-webkit-animation:slideInRight-leave .3s;animation:slideInRight-leave .3s}.slideInLeft-enter{-webkit-animation:slideInLeft-enter .3s;animation:slideInLeft-enter .3s}.slideInLeft-leave{position:absolute;left:0;right:0;-webkit-animation:slideInLeft-leave .3s;animation:slideInLeft-leave .3s}@-webkit-keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInRight-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@keyframes slideInRight-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(100%);transform:translateX(100%);opacity:0}}@-webkit-keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes slideInLeft-enter{0%{opacity:0;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}to{opacity:1;-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}@keyframes slideInLeft-leave{0%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transform:translateX(-100%);transform:translateX(-100%);opacity:0}}.el-tree{position:relative;cursor:default;background:#FFF;color:#606266}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#909399;font-size:14px}.el-tree__drop-indicator{position:absolute;left:0;right:0;height:1px;background-color:#409EFF}.el-tree-node{white-space:nowrap;outline:0}.el-tree-node:focus>.el-tree-node__content{background-color:#F5F7FA}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:#409EFF;color:#fff}.el-tree-node__content:hover,.el-upload-list__item:hover{background-color:#F5F7FA}.el-tree-node__content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:26px;cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.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:#C0C4CC;font-size:12px;-webkit-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out}.el-tree-node__expand-icon.expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__label{font-size:14px}.el-tree-node__loading-icon{margin-right:8px;font-size:14px;color:#C0C4CC}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.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:#f0f7ff}.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#FFF;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-light .el-alert__closebtn{color:#C0C4CC}.el-alert.is-dark .el-alert__closebtn,.el-alert.is-dark .el-alert__description{color:#FFF}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success.is-light{background-color:#f0f9eb;color:#67C23A}.el-alert--success.is-light .el-alert__description{color:#67C23A}.el-alert--success.is-dark{background-color:#67C23A;color:#FFF}.el-alert--info.is-light{background-color:#f4f4f5;color:#909399}.el-alert--info.is-dark{background-color:#909399;color:#FFF}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning.is-light{background-color:#fdf6ec;color:#E6A23C}.el-alert--warning.is-light .el-alert__description{color:#E6A23C}.el-alert--warning.is-dark{background-color:#E6A23C;color:#FFF}.el-alert--error.is-light{background-color:#fef0f0;color:#F56C6C}.el-alert--error.is-light .el-alert__description{color:#F56C6C}.el-alert--error.is-dark{background-color:#F56C6C;color:#FFF}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert-fade-enter,.el-alert-fade-leave-active,.el-loading-fade-enter,.el-loading-fade-leave-active,.el-notification-fade-leave-active,.el-upload iframe{opacity:0}.el-carousel__arrow--right,.el-notification.right{right:16px}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #EBEEF5;position:fixed;background-color:#FFF;box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.el-notification.left{left:16px}.el-notification__group{margin-left:13px;margin-right:8px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67C23A}.el-notification .el-icon-error{color:#F56C6C}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#E6A23C}.el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.el-input-number{position:relative;display:inline-block;width:180px;line-height:38px}.el-input-number .el-input{display:block}.el-input-number .el-input__inner{-webkit-appearance:none;padding-left:50px;padding-right:50px;text-align:center}.el-input-number__decrease,.el-input-number__increase{position:absolute;z-index:1;top:1px;width:40px;height:auto;text-align:center;background:#F5F7FA;color:#606266;cursor:pointer;font-size:13px}.el-input-number__decrease:hover,.el-input-number__increase:hover{color:#409EFF}.el-input-number__decrease:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled),.el-input-number__increase:hover:not(.is-disabled)~.el-input .el-input__inner:not(.is-disabled){border-color:#409EFF}.el-input-number__decrease.is-disabled,.el-input-number__increase.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-input-number__increase{right:1px;border-radius:0 4px 4px 0;border-left:1px solid #DCDFE6}.el-input-number__decrease{left:1px;border-radius:4px 0 0 4px;border-right:1px solid #DCDFE6}.el-input-number.is-disabled .el-input-number__decrease,.el-input-number.is-disabled .el-input-number__increase{border-color:#E4E7ED;color:#E4E7ED}.el-input-number.is-disabled .el-input-number__decrease:hover,.el-input-number.is-disabled .el-input-number__increase:hover{color:#E4E7ED;cursor:not-allowed}.el-input-number--medium{width:200px;line-height:34px}.el-input-number--medium .el-input-number__decrease,.el-input-number--medium .el-input-number__increase{width:36px;font-size:14px}.el-input-number--medium .el-input__inner{padding-left:43px;padding-right:43px}.el-input-number--small{width:130px;line-height:30px}.el-input-number--small .el-input-number__decrease,.el-input-number--small .el-input-number__increase{width:32px;font-size:13px}.el-input-number--small .el-input-number__decrease [class*=el-icon],.el-input-number--small .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.9);transform:scale(.9)}.el-input-number--small .el-input__inner{padding-left:39px;padding-right:39px}.el-input-number--mini{width:130px;line-height:26px}.el-input-number--mini .el-input-number__decrease,.el-input-number--mini .el-input-number__increase{width:28px;font-size:12px}.el-input-number--mini .el-input-number__decrease [class*=el-icon],.el-input-number--mini .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.el-input-number--mini .el-input__inner{padding-left:35px;padding-right:35px}.el-input-number.is-without-controls .el-input__inner{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__inner{padding-left:15px;padding-right:50px}.el-input-number.is-controls-right .el-input-number__decrease,.el-input-number.is-controls-right .el-input-number__increase{height:auto;line-height:19px}.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{-webkit-transform:scale(.8);transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 4px 0 0;border-bottom:1px solid #DCDFE6}.el-input-number.is-controls-right .el-input-number__decrease{right:1px;bottom:1px;top:auto;left:auto;border-right:none;border-left:1px solid #DCDFE6;border-radius:0 0 4px}.el-input-number.is-controls-right[class*=medium] [class*=decrease],.el-input-number.is-controls-right[class*=medium] [class*=increase]{line-height:17px}.el-input-number.is-controls-right[class*=small] [class*=decrease],.el-input-number.is-controls-right[class*=small] [class*=increase]{line-height:15px}.el-input-number.is-controls-right[class*=mini] [class*=decrease],.el-input-number.is-controls-right[class*=mini] [class*=increase]{line-height:13px}.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow::after{content:" ";border-width:5px}.el-button-group::after,.el-button-group::before,.el-color-dropdown__main-wrapper::after,.el-link.is-underline:hover:after,.el-page-header__left::after,.el-progress-bar__inner::after,.el-row::after,.el-row::before,.el-slider::after,.el-slider::before,.el-slider__button-wrapper::after,.el-transfer-panel .el-transfer-panel__footer::after,.el-upload-cover::after,.el-upload-list--picture-card .el-upload-list__item-actions::after{content:""}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow::after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#FFF}.el-tooltip__popper.is-light{background:#FFF;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow::after{border-top-color:#FFF}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow::after{border-bottom-color:#FFF}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow::after{border-left-color:#FFF}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow::after{border-right-color:#FFF}.el-slider::after,.el-slider::before{display:table}.el-slider__button-wrapper .el-tooltip,.el-slider__button-wrapper::after{display:inline-block;vertical-align:middle}.el-slider::after{clear:both}.el-slider__runway{width:100%;height:6px;margin:16px 0;background-color:#E4E7ED;border-radius:3px;position:relative;cursor:pointer;vertical-align:middle}.el-slider__runway.show-input{margin-right:160px;width:auto}.el-slider__runway.disabled{cursor:default}.el-slider__runway.disabled .el-slider__bar{background-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button{border-color:#C0C4CC}.el-slider__runway.disabled .el-slider__button-wrapper.dragging,.el-slider__runway.disabled .el-slider__button-wrapper.hover,.el-slider__runway.disabled .el-slider__button-wrapper:hover{cursor:not-allowed}.el-slider__runway.disabled .el-slider__button.dragging,.el-slider__runway.disabled .el-slider__button.hover,.el-slider__runway.disabled .el-slider__button:hover{-webkit-transform:scale(1);transform:scale(1);cursor:not-allowed}.el-slider__button-wrapper,.el-slider__stop{-webkit-transform:translateX(-50%);position:absolute}.el-slider__input{float:right;margin-top:3px;width:130px}.el-slider__input.el-input-number--mini{margin-top:5px}.el-slider__input.el-input-number--medium{margin-top:0}.el-slider__input.el-input-number--large{margin-top:-2px}.el-slider__bar{height:6px;background-color:#409EFF;border-top-left-radius:3px;border-bottom-left-radius:3px;position:absolute}.el-slider__button-wrapper{height:36px;width:36px;z-index:1001;top:-15px;transform:translateX(-50%);background-color:transparent;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:normal}.el-image-viewer__btn,.el-slider__button,.el-step__icon-inner{-moz-user-select:none;-ms-user-select:none}.el-slider__button-wrapper::after{height:100%}.el-slider__button-wrapper.hover,.el-slider__button-wrapper:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button-wrapper.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__button{width:16px;height:16px;border:2px solid #409EFF;background-color:#FFF;border-radius:50%;-webkit-transition:.2s;transition:.2s;-webkit-user-select:none;user-select:none}.el-slider__button.dragging,.el-slider__button.hover,.el-slider__button:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.el-slider__button.hover,.el-slider__button:hover{cursor:-webkit-grab;cursor:grab}.el-slider__button.dragging{cursor:-webkit-grabbing;cursor:grabbing}.el-slider__stop{height:6px;width:6px;border-radius:100%;background-color:#FFF;transform:translateX(-50%)}.el-slider__marks{top:0;left:12px;width:18px;height:100%}.el-slider__marks-text{position:absolute;-webkit-transform:translateX(-50%);transform:translateX(-50%);font-size:14px;color:#909399;margin-top:15px}.el-slider.is-vertical{position:relative}.el-slider.is-vertical .el-slider__runway{width:6px;height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:6px;height:auto;border-radius:0 0 3px 3px}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:-15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{-webkit-transform:translateY(50%);transform:translateY(50%)}.el-slider.is-vertical.el-slider--with-input{padding-bottom:58px}.el-slider.is-vertical.el-slider--with-input .el-slider__input{overflow:visible;float:none;position:absolute;bottom:22px;width:36px;margin-top:15px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input__inner{text-align:center;padding-left:5px;padding-right:5px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{top:32px;margin-top:-1px;border:1px solid #DCDFE6;line-height:20px;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__decrease{width:18px;right:18px;border-bottom-left-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase{width:19px;border-bottom-right-radius:4px}.el-slider.is-vertical.el-slider--with-input .el-slider__input .el-input-number__increase~.el-input .el-input__inner{border-bottom-left-radius:0;border-bottom-right-radius:0}.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:hover .el-input-number__increase{border-color:#C0C4CC}.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__decrease,.el-slider.is-vertical.el-slider--with-input .el-slider__input:active .el-input-number__increase{border-color:#409EFF}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;-webkit-transform:translateY(50%);transform:translateY(50%)}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-col-pull-0,.el-col-pull-1,.el-col-pull-10,.el-col-pull-11,.el-col-pull-12,.el-col-pull-13,.el-col-pull-14,.el-col-pull-15,.el-col-pull-16,.el-col-pull-17,.el-col-pull-18,.el-col-pull-19,.el-col-pull-2,.el-col-pull-20,.el-col-pull-21,.el-col-pull-22,.el-col-pull-23,.el-col-pull-24,.el-col-pull-3,.el-col-pull-4,.el-col-pull-5,.el-col-pull-6,.el-col-pull-7,.el-col-pull-8,.el-col-pull-9,.el-col-push-0,.el-col-push-1,.el-col-push-10,.el-col-push-11,.el-col-push-12,.el-col-push-13,.el-col-push-14,.el-col-push-15,.el-col-push-16,.el-col-push-17,.el-col-push-18,.el-col-push-19,.el-col-push-2,.el-col-push-20,.el-col-push-21,.el-col-push-22,.el-col-push-23,.el-col-push-24,.el-col-push-3,.el-col-push-4,.el-col-push-5,.el-col-push-6,.el-col-push-7,.el-col-push-8,.el-col-push-9,.el-row,.el-upload-dragger,.el-upload-list__item{position:relative}.el-loading-spinner .el-loading-text{color:#409EFF;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#409EFF;stroke-linecap:round}.el-loading-spinner i{color:#409EFF}@-webkit-keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.el-row{-webkit-box-sizing:border-box;box-sizing:border-box}.el-row::after,.el-row::before{display:table}.el-row::after{clear:both}.el-row--flex{display:-webkit-box;display:-ms-flexbox;display:flex}.el-col-0,.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-top{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.el-row--flex.is-align-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}[class*=el-col-]{float:left;-webkit-box-sizing:border-box;box-sizing:border-box}.el-col-0{width:0%}.el-col-pull-0{right:0}.el-col-push-0{left:0}.el-col-1{width:4.16667%}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{right:4.16667%}.el-col-push-1{left:4.16667%}.el-col-2{width:8.33333%}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{right:8.33333%}.el-col-push-2{left:8.33333%}.el-col-3{width:12.5%}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{right:12.5%}.el-col-push-3{left:12.5%}.el-col-4{width:16.66667%}.el-col-offset-4{margin-left:16.66667%}.el-col-pull-4{right:16.66667%}.el-col-push-4{left:16.66667%}.el-col-5{width:20.83333%}.el-col-offset-5{margin-left:20.83333%}.el-col-pull-5{right:20.83333%}.el-col-push-5{left:20.83333%}.el-col-6{width:25%}.el-col-offset-6{margin-left:25%}.el-col-pull-6{right:25%}.el-col-push-6{left:25%}.el-col-7{width:29.16667%}.el-col-offset-7{margin-left:29.16667%}.el-col-pull-7{right:29.16667%}.el-col-push-7{left:29.16667%}.el-col-8{width:33.33333%}.el-col-offset-8{margin-left:33.33333%}.el-col-pull-8{right:33.33333%}.el-col-push-8{left:33.33333%}.el-col-9{width:37.5%}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{right:37.5%}.el-col-push-9{left:37.5%}.el-col-10{width:41.66667%}.el-col-offset-10{margin-left:41.66667%}.el-col-pull-10{right:41.66667%}.el-col-push-10{left:41.66667%}.el-col-11{width:45.83333%}.el-col-offset-11{margin-left:45.83333%}.el-col-pull-11{right:45.83333%}.el-col-push-11{left:45.83333%}.el-col-12{width:50%}.el-col-offset-12{margin-left:50%}.el-col-pull-12{right:50%}.el-col-push-12{left:50%}.el-col-13{width:54.16667%}.el-col-offset-13{margin-left:54.16667%}.el-col-pull-13{right:54.16667%}.el-col-push-13{left:54.16667%}.el-col-14{width:58.33333%}.el-col-offset-14{margin-left:58.33333%}.el-col-pull-14{right:58.33333%}.el-col-push-14{left:58.33333%}.el-col-15{width:62.5%}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{right:62.5%}.el-col-push-15{left:62.5%}.el-col-16{width:66.66667%}.el-col-offset-16{margin-left:66.66667%}.el-col-pull-16{right:66.66667%}.el-col-push-16{left:66.66667%}.el-col-17{width:70.83333%}.el-col-offset-17{margin-left:70.83333%}.el-col-pull-17{right:70.83333%}.el-col-push-17{left:70.83333%}.el-col-18{width:75%}.el-col-offset-18{margin-left:75%}.el-col-pull-18{right:75%}.el-col-push-18{left:75%}.el-col-19{width:79.16667%}.el-col-offset-19{margin-left:79.16667%}.el-col-pull-19{right:79.16667%}.el-col-push-19{left:79.16667%}.el-col-20{width:83.33333%}.el-col-offset-20{margin-left:83.33333%}.el-col-pull-20{right:83.33333%}.el-col-push-20{left:83.33333%}.el-col-21{width:87.5%}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{right:87.5%}.el-col-push-21{left:87.5%}.el-col-22{width:91.66667%}.el-col-offset-22{margin-left:91.66667%}.el-col-pull-22{right:91.66667%}.el-col-push-22{left:91.66667%}.el-col-23{width:95.83333%}.el-col-offset-23{margin-left:95.83333%}.el-col-pull-23{right:95.83333%}.el-col-push-23{left:95.83333%}.el-col-24{width:100%}.el-col-offset-24{margin-left:100%}.el-col-pull-24{right:100%}.el-col-push-24{left:100%}@media only screen and (max-width:767px){.el-col-xs-0{display:none;width:0%}.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{width:4.16667%}.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{width:8.33333%}.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{width:12.5%}.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{width:16.66667%}.el-col-xs-offset-4{margin-left:16.66667%}.el-col-xs-pull-4{position:relative;right:16.66667%}.el-col-xs-push-4{position:relative;left:16.66667%}.el-col-xs-5{width:20.83333%}.el-col-xs-offset-5{margin-left:20.83333%}.el-col-xs-pull-5{position:relative;right:20.83333%}.el-col-xs-push-5{position:relative;left:20.83333%}.el-col-xs-6{width:25%}.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{width:29.16667%}.el-col-xs-offset-7{margin-left:29.16667%}.el-col-xs-pull-7{position:relative;right:29.16667%}.el-col-xs-push-7{position:relative;left:29.16667%}.el-col-xs-8{width:33.33333%}.el-col-xs-offset-8{margin-left:33.33333%}.el-col-xs-pull-8{position:relative;right:33.33333%}.el-col-xs-push-8{position:relative;left:33.33333%}.el-col-xs-9{width:37.5%}.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{width:41.66667%}.el-col-xs-offset-10{margin-left:41.66667%}.el-col-xs-pull-10{position:relative;right:41.66667%}.el-col-xs-push-10{position:relative;left:41.66667%}.el-col-xs-11{width:45.83333%}.el-col-xs-offset-11{margin-left:45.83333%}.el-col-xs-pull-11{position:relative;right:45.83333%}.el-col-xs-push-11{position:relative;left:45.83333%}.el-col-xs-12{width:50%}.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{width:54.16667%}.el-col-xs-offset-13{margin-left:54.16667%}.el-col-xs-pull-13{position:relative;right:54.16667%}.el-col-xs-push-13{position:relative;left:54.16667%}.el-col-xs-14{width:58.33333%}.el-col-xs-offset-14{margin-left:58.33333%}.el-col-xs-pull-14{position:relative;right:58.33333%}.el-col-xs-push-14{position:relative;left:58.33333%}.el-col-xs-15{width:62.5%}.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{width:66.66667%}.el-col-xs-offset-16{margin-left:66.66667%}.el-col-xs-pull-16{position:relative;right:66.66667%}.el-col-xs-push-16{position:relative;left:66.66667%}.el-col-xs-17{width:70.83333%}.el-col-xs-offset-17{margin-left:70.83333%}.el-col-xs-pull-17{position:relative;right:70.83333%}.el-col-xs-push-17{position:relative;left:70.83333%}.el-col-xs-18{width:75%}.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{width:79.16667%}.el-col-xs-offset-19{margin-left:79.16667%}.el-col-xs-pull-19{position:relative;right:79.16667%}.el-col-xs-push-19{position:relative;left:79.16667%}.el-col-xs-20{width:83.33333%}.el-col-xs-offset-20{margin-left:83.33333%}.el-col-xs-pull-20{position:relative;right:83.33333%}.el-col-xs-push-20{position:relative;left:83.33333%}.el-col-xs-21{width:87.5%}.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{width:91.66667%}.el-col-xs-offset-22{margin-left:91.66667%}.el-col-xs-pull-22{position:relative;right:91.66667%}.el-col-xs-push-22{position:relative;left:91.66667%}.el-col-xs-23{width:95.83333%}.el-col-xs-offset-23{margin-left:95.83333%}.el-col-xs-pull-23{position:relative;right:95.83333%}.el-col-xs-push-23{position:relative;left:95.83333%}.el-col-xs-24{width:100%}.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{display:none;width:0%}.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{width:4.16667%}.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{width:8.33333%}.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{width:12.5%}.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{width:16.66667%}.el-col-sm-offset-4{margin-left:16.66667%}.el-col-sm-pull-4{position:relative;right:16.66667%}.el-col-sm-push-4{position:relative;left:16.66667%}.el-col-sm-5{width:20.83333%}.el-col-sm-offset-5{margin-left:20.83333%}.el-col-sm-pull-5{position:relative;right:20.83333%}.el-col-sm-push-5{position:relative;left:20.83333%}.el-col-sm-6{width:25%}.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{width:29.16667%}.el-col-sm-offset-7{margin-left:29.16667%}.el-col-sm-pull-7{position:relative;right:29.16667%}.el-col-sm-push-7{position:relative;left:29.16667%}.el-col-sm-8{width:33.33333%}.el-col-sm-offset-8{margin-left:33.33333%}.el-col-sm-pull-8{position:relative;right:33.33333%}.el-col-sm-push-8{position:relative;left:33.33333%}.el-col-sm-9{width:37.5%}.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{width:41.66667%}.el-col-sm-offset-10{margin-left:41.66667%}.el-col-sm-pull-10{position:relative;right:41.66667%}.el-col-sm-push-10{position:relative;left:41.66667%}.el-col-sm-11{width:45.83333%}.el-col-sm-offset-11{margin-left:45.83333%}.el-col-sm-pull-11{position:relative;right:45.83333%}.el-col-sm-push-11{position:relative;left:45.83333%}.el-col-sm-12{width:50%}.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{width:54.16667%}.el-col-sm-offset-13{margin-left:54.16667%}.el-col-sm-pull-13{position:relative;right:54.16667%}.el-col-sm-push-13{position:relative;left:54.16667%}.el-col-sm-14{width:58.33333%}.el-col-sm-offset-14{margin-left:58.33333%}.el-col-sm-pull-14{position:relative;right:58.33333%}.el-col-sm-push-14{position:relative;left:58.33333%}.el-col-sm-15{width:62.5%}.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{width:66.66667%}.el-col-sm-offset-16{margin-left:66.66667%}.el-col-sm-pull-16{position:relative;right:66.66667%}.el-col-sm-push-16{position:relative;left:66.66667%}.el-col-sm-17{width:70.83333%}.el-col-sm-offset-17{margin-left:70.83333%}.el-col-sm-pull-17{position:relative;right:70.83333%}.el-col-sm-push-17{position:relative;left:70.83333%}.el-col-sm-18{width:75%}.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{width:79.16667%}.el-col-sm-offset-19{margin-left:79.16667%}.el-col-sm-pull-19{position:relative;right:79.16667%}.el-col-sm-push-19{position:relative;left:79.16667%}.el-col-sm-20{width:83.33333%}.el-col-sm-offset-20{margin-left:83.33333%}.el-col-sm-pull-20{position:relative;right:83.33333%}.el-col-sm-push-20{position:relative;left:83.33333%}.el-col-sm-21{width:87.5%}.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{width:91.66667%}.el-col-sm-offset-22{margin-left:91.66667%}.el-col-sm-pull-22{position:relative;right:91.66667%}.el-col-sm-push-22{position:relative;left:91.66667%}.el-col-sm-23{width:95.83333%}.el-col-sm-offset-23{margin-left:95.83333%}.el-col-sm-pull-23{position:relative;right:95.83333%}.el-col-sm-push-23{position:relative;left:95.83333%}.el-col-sm-24{width:100%}.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{display:none;width:0%}.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{width:4.16667%}.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{width:8.33333%}.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{width:12.5%}.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{width:16.66667%}.el-col-md-offset-4{margin-left:16.66667%}.el-col-md-pull-4{position:relative;right:16.66667%}.el-col-md-push-4{position:relative;left:16.66667%}.el-col-md-5{width:20.83333%}.el-col-md-offset-5{margin-left:20.83333%}.el-col-md-pull-5{position:relative;right:20.83333%}.el-col-md-push-5{position:relative;left:20.83333%}.el-col-md-6{width:25%}.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{width:29.16667%}.el-col-md-offset-7{margin-left:29.16667%}.el-col-md-pull-7{position:relative;right:29.16667%}.el-col-md-push-7{position:relative;left:29.16667%}.el-col-md-8{width:33.33333%}.el-col-md-offset-8{margin-left:33.33333%}.el-col-md-pull-8{position:relative;right:33.33333%}.el-col-md-push-8{position:relative;left:33.33333%}.el-col-md-9{width:37.5%}.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{width:41.66667%}.el-col-md-offset-10{margin-left:41.66667%}.el-col-md-pull-10{position:relative;right:41.66667%}.el-col-md-push-10{position:relative;left:41.66667%}.el-col-md-11{width:45.83333%}.el-col-md-offset-11{margin-left:45.83333%}.el-col-md-pull-11{position:relative;right:45.83333%}.el-col-md-push-11{position:relative;left:45.83333%}.el-col-md-12{width:50%}.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{width:54.16667%}.el-col-md-offset-13{margin-left:54.16667%}.el-col-md-pull-13{position:relative;right:54.16667%}.el-col-md-push-13{position:relative;left:54.16667%}.el-col-md-14{width:58.33333%}.el-col-md-offset-14{margin-left:58.33333%}.el-col-md-pull-14{position:relative;right:58.33333%}.el-col-md-push-14{position:relative;left:58.33333%}.el-col-md-15{width:62.5%}.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{width:66.66667%}.el-col-md-offset-16{margin-left:66.66667%}.el-col-md-pull-16{position:relative;right:66.66667%}.el-col-md-push-16{position:relative;left:66.66667%}.el-col-md-17{width:70.83333%}.el-col-md-offset-17{margin-left:70.83333%}.el-col-md-pull-17{position:relative;right:70.83333%}.el-col-md-push-17{position:relative;left:70.83333%}.el-col-md-18{width:75%}.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{width:79.16667%}.el-col-md-offset-19{margin-left:79.16667%}.el-col-md-pull-19{position:relative;right:79.16667%}.el-col-md-push-19{position:relative;left:79.16667%}.el-col-md-20{width:83.33333%}.el-col-md-offset-20{margin-left:83.33333%}.el-col-md-pull-20{position:relative;right:83.33333%}.el-col-md-push-20{position:relative;left:83.33333%}.el-col-md-21{width:87.5%}.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{width:91.66667%}.el-col-md-offset-22{margin-left:91.66667%}.el-col-md-pull-22{position:relative;right:91.66667%}.el-col-md-push-22{position:relative;left:91.66667%}.el-col-md-23{width:95.83333%}.el-col-md-offset-23{margin-left:95.83333%}.el-col-md-pull-23{position:relative;right:95.83333%}.el-col-md-push-23{position:relative;left:95.83333%}.el-col-md-24{width:100%}.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{display:none;width:0%}.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{width:4.16667%}.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{width:8.33333%}.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{width:12.5%}.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{width:16.66667%}.el-col-lg-offset-4{margin-left:16.66667%}.el-col-lg-pull-4{position:relative;right:16.66667%}.el-col-lg-push-4{position:relative;left:16.66667%}.el-col-lg-5{width:20.83333%}.el-col-lg-offset-5{margin-left:20.83333%}.el-col-lg-pull-5{position:relative;right:20.83333%}.el-col-lg-push-5{position:relative;left:20.83333%}.el-col-lg-6{width:25%}.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{width:29.16667%}.el-col-lg-offset-7{margin-left:29.16667%}.el-col-lg-pull-7{position:relative;right:29.16667%}.el-col-lg-push-7{position:relative;left:29.16667%}.el-col-lg-8{width:33.33333%}.el-col-lg-offset-8{margin-left:33.33333%}.el-col-lg-pull-8{position:relative;right:33.33333%}.el-col-lg-push-8{position:relative;left:33.33333%}.el-col-lg-9{width:37.5%}.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{width:41.66667%}.el-col-lg-offset-10{margin-left:41.66667%}.el-col-lg-pull-10{position:relative;right:41.66667%}.el-col-lg-push-10{position:relative;left:41.66667%}.el-col-lg-11{width:45.83333%}.el-col-lg-offset-11{margin-left:45.83333%}.el-col-lg-pull-11{position:relative;right:45.83333%}.el-col-lg-push-11{position:relative;left:45.83333%}.el-col-lg-12{width:50%}.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{width:54.16667%}.el-col-lg-offset-13{margin-left:54.16667%}.el-col-lg-pull-13{position:relative;right:54.16667%}.el-col-lg-push-13{position:relative;left:54.16667%}.el-col-lg-14{width:58.33333%}.el-col-lg-offset-14{margin-left:58.33333%}.el-col-lg-pull-14{position:relative;right:58.33333%}.el-col-lg-push-14{position:relative;left:58.33333%}.el-col-lg-15{width:62.5%}.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{width:66.66667%}.el-col-lg-offset-16{margin-left:66.66667%}.el-col-lg-pull-16{position:relative;right:66.66667%}.el-col-lg-push-16{position:relative;left:66.66667%}.el-col-lg-17{width:70.83333%}.el-col-lg-offset-17{margin-left:70.83333%}.el-col-lg-pull-17{position:relative;right:70.83333%}.el-col-lg-push-17{position:relative;left:70.83333%}.el-col-lg-18{width:75%}.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{width:79.16667%}.el-col-lg-offset-19{margin-left:79.16667%}.el-col-lg-pull-19{position:relative;right:79.16667%}.el-col-lg-push-19{position:relative;left:79.16667%}.el-col-lg-20{width:83.33333%}.el-col-lg-offset-20{margin-left:83.33333%}.el-col-lg-pull-20{position:relative;right:83.33333%}.el-col-lg-push-20{position:relative;left:83.33333%}.el-col-lg-21{width:87.5%}.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{width:91.66667%}.el-col-lg-offset-22{margin-left:91.66667%}.el-col-lg-pull-22{position:relative;right:91.66667%}.el-col-lg-push-22{position:relative;left:91.66667%}.el-col-lg-23{width:95.83333%}.el-col-lg-offset-23{margin-left:95.83333%}.el-col-lg-pull-23{position:relative;right:95.83333%}.el-col-lg-push-23{position:relative;left:95.83333%}.el-col-lg-24{width:100%}.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{display:none;width:0%}.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{width:4.16667%}.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{width:8.33333%}.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{width:12.5%}.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{width:16.66667%}.el-col-xl-offset-4{margin-left:16.66667%}.el-col-xl-pull-4{position:relative;right:16.66667%}.el-col-xl-push-4{position:relative;left:16.66667%}.el-col-xl-5{width:20.83333%}.el-col-xl-offset-5{margin-left:20.83333%}.el-col-xl-pull-5{position:relative;right:20.83333%}.el-col-xl-push-5{position:relative;left:20.83333%}.el-col-xl-6{width:25%}.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{width:29.16667%}.el-col-xl-offset-7{margin-left:29.16667%}.el-col-xl-pull-7{position:relative;right:29.16667%}.el-col-xl-push-7{position:relative;left:29.16667%}.el-col-xl-8{width:33.33333%}.el-col-xl-offset-8{margin-left:33.33333%}.el-col-xl-pull-8{position:relative;right:33.33333%}.el-col-xl-push-8{position:relative;left:33.33333%}.el-col-xl-9{width:37.5%}.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{width:41.66667%}.el-col-xl-offset-10{margin-left:41.66667%}.el-col-xl-pull-10{position:relative;right:41.66667%}.el-col-xl-push-10{position:relative;left:41.66667%}.el-col-xl-11{width:45.83333%}.el-col-xl-offset-11{margin-left:45.83333%}.el-col-xl-pull-11{position:relative;right:45.83333%}.el-col-xl-push-11{position:relative;left:45.83333%}.el-col-xl-12{width:50%}.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{width:54.16667%}.el-col-xl-offset-13{margin-left:54.16667%}.el-col-xl-pull-13{position:relative;right:54.16667%}.el-col-xl-push-13{position:relative;left:54.16667%}.el-col-xl-14{width:58.33333%}.el-col-xl-offset-14{margin-left:58.33333%}.el-col-xl-pull-14{position:relative;right:58.33333%}.el-col-xl-push-14{position:relative;left:58.33333%}.el-col-xl-15{width:62.5%}.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{width:66.66667%}.el-col-xl-offset-16{margin-left:66.66667%}.el-col-xl-pull-16{position:relative;right:66.66667%}.el-col-xl-push-16{position:relative;left:66.66667%}.el-col-xl-17{width:70.83333%}.el-col-xl-offset-17{margin-left:70.83333%}.el-col-xl-pull-17{position:relative;right:70.83333%}.el-col-xl-push-17{position:relative;left:70.83333%}.el-col-xl-18{width:75%}.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{width:79.16667%}.el-col-xl-offset-19{margin-left:79.16667%}.el-col-xl-pull-19{position:relative;right:79.16667%}.el-col-xl-push-19{position:relative;left:79.16667%}.el-col-xl-20{width:83.33333%}.el-col-xl-offset-20{margin-left:83.33333%}.el-col-xl-pull-20{position:relative;right:83.33333%}.el-col-xl-push-20{position:relative;left:83.33333%}.el-col-xl-21{width:87.5%}.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{width:91.66667%}.el-col-xl-offset-22{margin-left:91.66667%}.el-col-xl-pull-22{position:relative;right:91.66667%}.el-col-xl-push-22{position:relative;left:91.66667%}.el-col-xl-23{width:95.83333%}.el-col-xl-offset-23{margin-left:95.83333%}.el-col-xl-pull-23{position:relative;right:95.83333%}.el-col-xl-push-23{position:relative;left:95.83333%}.el-col-xl-24{width:100%}.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%}}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-upload{display:inline-block;text-align:center;cursor:pointer;outline:0}.el-upload__input{display:none}.el-upload__tip{font-size:12px;color:#606266;margin-top:7px}.el-upload iframe{position:absolute;z-index:-1;top:0;left:0;filter:alpha(opacity=0)}.el-upload--picture-card{background-color:#fbfdff;border:1px dashed #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;cursor:pointer;line-height:146px;vertical-align:top}.el-upload--picture-card i{font-size:28px;color:#8c939d}.el-upload--picture-card:hover,.el-upload:focus{border-color:#409EFF;color:#409EFF}.el-upload:focus .el-upload-dragger{border-color:#409EFF}.el-upload-dragger{background-color:#fff;border:1px dashed #d9d9d9;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:360px;height:180px;text-align:center;cursor:pointer;overflow:hidden}.el-upload-dragger .el-icon-upload{font-size:67px;color:#C0C4CC;margin:40px 0 16px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:1px solid #DCDFE6;margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:#606266;font-size:14px;text-align:center}.el-upload-dragger .el-upload__text em{color:#409EFF;font-style:normal}.el-upload-dragger:hover{border-color:#409EFF}.el-upload-dragger.is-dragover{background-color:rgba(32,159,255,.06);border:2px dashed #409EFF}.el-upload-list{margin:0;padding:0;list-style:none}.el-upload-list__item{-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1);font-size:14px;color:#606266;line-height:1.8;margin-top:5px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;width:100%}.el-upload-list__item .el-progress{position:absolute;top:20px;width:100%}.el-upload-list__item .el-progress__text{position:absolute;right:0;top:-13px}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item:first-child{margin-top:10px}.el-upload-list__item .el-icon-upload-success{color:#67C23A}.el-upload-list__item .el-icon-close{display:none;position:absolute;top:5px;right:5px;cursor:pointer;opacity:.75;color:#606266}.el-upload-list__item .el-icon-close:hover{opacity:1}.el-upload-list__item .el-icon-close-tip{display:none;position:absolute;top:5px;right:5px;font-size:12px;cursor:pointer;opacity:1;color:#409EFF}.el-upload-list__item:hover .el-icon-close{display:inline-block}.el-upload-list__item:hover .el-progress__text{display:none}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:block}.el-upload-list__item.is-success .el-upload-list__item-name:focus,.el-upload-list__item.is-success .el-upload-list__item-name:hover{color:#409EFF;cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon-close-tip{display:inline-block}.el-upload-list__item.is-success:active,.el-upload-list__item.is-success:not(.focusing):focus{outline-width:0}.el-upload-list__item.is-success:active .el-icon-close-tip,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:not(.focusing):focus .el-icon-close-tip{display:none}.el-upload-list.is-disabled .el-upload-list__item:hover .el-upload-list__item-status-label{display:block}.el-upload-list__item-name{color:#606266;display:block;margin-right:40px;overflow:hidden;padding-left:4px;text-overflow:ellipsis;-webkit-transition:color .3s;transition:color .3s;white-space:nowrap}.el-upload-list__item-name [class^=el-icon]{height:100%;margin-right:7px;color:#909399;line-height:inherit}.el-upload-list__item-status-label{position:absolute;right:5px;top:0;line-height:inherit;display:none}.el-upload-list__item-delete{position:absolute;right:10px;top:0;font-size:12px;color:#606266;display:none}.el-upload-list__item-delete:hover{color:#409EFF}.el-upload-list--picture-card{margin:0;display:inline;vertical-align:top}.el-upload-list--picture-card .el-upload-list__item{overflow:hidden;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:148px;height:148px;margin:0 8px 8px 0;display:inline-block}.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,.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-list--picture-card .el-upload-list__item-status-label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{position:absolute;width:100%;height:100%;left:0;top:0;cursor:default;text-align:center;color:#fff;opacity:0;font-size:20px;background-color:rgba(0,0,0,.5);-webkit-transition:opacity .3s;transition:opacity .3s}.el-upload-list--picture-card .el-upload-list__item-actions::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-list--picture-card .el-upload-list__item-actions span{display:none;cursor:pointer}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:15px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{position:static;font-size:inherit;color:inherit}.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-block}.el-upload-list--picture-card .el-progress{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);bottom:auto;width:126px}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{overflow:hidden;z-index:0;background-color:#fff;border:1px solid #c0ccda;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;margin-top:10px;padding:10px 10px 10px 90px;height:92px}.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{background:0 0;-webkit-box-shadow:none;box-shadow:none;top:-2px;right:-12px}.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{line-height:70px;margin-top:0}.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-thumbnail{vertical-align:middle;display:inline-block;width:70px;height:70px;float:left;position:relative;z-index:1;margin-left:-80px;background-color:#FFF}.el-upload-list--picture .el-upload-list__item-name{display:block;margin-top:20px}.el-upload-list--picture .el-upload-list__item-name i{font-size:70px;line-height:1;position:absolute;left:9px;top:10px}.el-upload-list--picture .el-upload-list__item-status-label{position:absolute;right:-17px;top:-7px;width:46px;height:26px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 1px 1px #ccc;box-shadow:0 1px 1px #ccc}.el-upload-list--picture .el-upload-list__item-status-label i{font-size:12px;margin-top:12px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{position:absolute;left:0;top:0;width:100%;height:100%;overflow:hidden;z-index:10;cursor:default}.el-upload-cover::after{display:inline-block;height:100%;vertical-align:middle}.el-upload-cover img{display:block;width:100%;height:100%}.el-upload-cover__label{position:absolute;right:-15px;top:-6px;width:40px;height:24px;background:#13ce66;text-align:center;-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-box-shadow:0 0 1pc 1px rgba(0,0,0,.2);box-shadow:0 0 1pc 1px rgba(0,0,0,.2)}.el-upload-cover__label i{font-size:12px;margin-top:11px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);color:#fff}.el-upload-cover__progress{display:inline-block;vertical-align:middle;position:static;width:243px}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{position:absolute;top:0;left:0;width:100%;height:100%}.el-upload-cover__interact{position:absolute;bottom:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.72);text-align:center}.el-upload-cover__interact .btn{display:inline-block;color:#FFF;font-size:14px;cursor:pointer;vertical-align:middle;-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);margin-top:60px}.el-upload-cover__interact .btn span{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{-webkit-transform:translateY(-13px);transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#FFF;display:block;font-size:24px;line-height:inherit;margin:0 auto 5px}.el-upload-cover__title{position:absolute;bottom:0;left:0;background-color:#FFF;height:36px;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;text-align:left;padding:0 10px;margin:0;line-height:36px;font-size:14px;color:#303133}.el-upload-cover+.el-upload__inner{opacity:0;position:relative;z-index:1}.el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;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{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-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{padding-right:0;margin-right:0;display:block}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67C23A}.el-progress.is-success .el-progress__text{color:#67C23A}.el-progress.is-warning .el-progress-bar__inner{background-color:#E6A23C}.el-badge__content,.el-progress.is-exception .el-progress-bar__inner{background-color:#F56C6C}.el-progress.is-warning .el-progress__text{color:#E6A23C}.el-progress.is-exception .el-progress__text{color:#F56C6C}.el-progress-bar{padding-right:50px;display:inline-block;vertical-align:middle;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__header,.el-message,.el-step__icon{-webkit-box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#EBEEF5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#409EFF;text-align:right;border-radius:100px;line-height:1;white-space:nowrap;-webkit-transition:width .6s ease;transition:width .6s ease}.el-progress-bar__inner::after{display:inline-block;height:100%;vertical-align:middle}.el-progress-bar__innerText{display:inline-block;vertical-align:middle;color:#FFF;font-size:12px;margin:0 5px}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}.el-time-spinner{width:100%;white-space:nowrap}.el-spinner{display:inline-block;vertical-align:middle}.el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}.el-message{min-width:380px;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#EBEEF5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,transform .4s,top .4s;transition:opacity .3s,transform .4s,top .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67C23A}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#E6A23C}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#F56C6C}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__content:focus{outline-width:0}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#C0C4CC;font-size:16px}.el-message__closeBtn:focus{outline-width:0}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67C23A}.el-message .el-icon-error{color:#F56C6C}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#E6A23C}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{border-radius:10px;color:#FFF;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #FFF}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#409EFF}.el-badge__content--success{background-color:#67C23A}.el-badge__content--warning{background-color:#E6A23C}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#F56C6C}.el-card{border-radius:4px;border:1px solid #EBEEF5;background-color:#FFF;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #EBEEF5;box-sizing:border-box}.el-card__body,.el-main{padding:20px}.el-rate{height:20px;line-height:1}.el-rate:active,.el-rate:focus{outline-width:0}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#C0C4CC;-webkit-transition:.3s;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{-webkit-transform:scale(1.15);transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-step.is-vertical,.el-steps{display:-webkit-box;display:-ms-flexbox}.el-rate__text{font-size:14px;vertical-align:middle}.el-steps{display:flex}.el-steps--simple{padding:13px 8%;border-radius:4px;background:#F5F7FA}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{height:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column}.el-step{position:relative;-ms-flex-negative:1;flex-shrink:1}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{-ms-flex-preferred-size:auto!important;flex-basis:auto!important;-ms-flex-negative:0;flex-shrink:0;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}.el-step:last-of-type .el-step__description,.el-step:last-of-type .el-step__main{padding-right:0}.el-step__head{position:relative;width:100%}.el-step__head.is-process{color:#303133;border-color:#303133}.el-step__head.is-wait{color:#C0C4CC;border-color:#C0C4CC}.el-step__head.is-success{color:#67C23A;border-color:#67C23A}.el-step__head.is-error{color:#F56C6C;border-color:#F56C6C}.el-step__head.is-finish{color:#409EFF;border-color:#409EFF}.el-step__icon{position:relative;z-index:1;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:24px;height:24px;font-size:14px;box-sizing:border-box;background:#FFF;-webkit-transition:.15s ease-out;transition:.15s ease-out}.el-step.is-horizontal,.el-step__icon-inner{display:inline-block}.el-step__icon.is-text{border-radius:50%;border:2px solid;border-color:inherit}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{-webkit-user-select:none;user-select:none;text-align:center;font-weight:700;line-height:1;color:inherit}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{-webkit-transform:translateY(1px);transform:translateY(1px)}.el-step__line{position:absolute;border-color:inherit;background-color:#C0C4CC}.el-step__line-inner{display:block;border-width:1px;border-style:solid;border-color:inherit;-webkit-transition:.15s ease-out;transition:.15s ease-out;-webkit-box-sizing:border-box;box-sizing:border-box;width:0;height:0}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{font-weight:700;color:#303133}.el-step__title.is-wait{color:#C0C4CC}.el-step__title.is-success{color:#67C23A}.el-step__title.is-error{color:#F56C6C}.el-step__title.is-finish{color:#409EFF}.el-step__description{padding-right:10%;margin-top:-5px;font-size:12px;line-height:20px;font-weight:400}.el-step__description.is-process{color:#303133}.el-step__description.is-wait{color:#C0C4CC}.el-step__description.is-success{color:#67C23A}.el-step__description.is-error{color:#F56C6C}.el-step__description.is-finish{color:#409EFF}.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{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{padding-left:10px;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.el-step.is-vertical .el-step__title{line-height:24px;padding-bottom:8px}.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-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{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-step.is-simple .el-step__head{width:auto;font-size:0;padding-right:10px}.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{-webkit-transform:scale(.8) translateY(1px);transform:scale(.8) translateY(1px)}.el-step.is-simple .el-step__main{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{max-width:50%;word-break:break-all}.el-step.is-simple .el-step__arrow{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-step.is-simple .el-step__arrow::after,.el-step.is-simple .el-step__arrow::before{content:'';display:inline-block;position:absolute;height:15px;width:1px;background:#C0C4CC}.el-step.is-simple .el-step__arrow::before{-webkit-transform:rotate(-45deg) translateY(-4px);transform:rotate(-45deg) translateY(-4px);-webkit-transform-origin:0 0;transform-origin:0 0}.el-step.is-simple .el-step__arrow::after{-webkit-transform:rotate(45deg) translateY(4px);transform:rotate(45deg) translateY(4px);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-carousel{position:relative}.el-carousel--horizontal{overflow-x:hidden}.el-carousel--vertical{overflow-y:hidden}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#FFF;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;margin:0;padding:0;z-index:2}.el-carousel__indicators--horizontal{bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.el-carousel__indicators--vertical{right:0;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#C0C4CC;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{background-color:transparent;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{display:inline-block;padding:12px 4px}.el-carousel__indicator--vertical{padding:4px 12px}.el-carousel__indicator--vertical .el-carousel__button{width:2px;height:15px}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#FFF;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.el-carousel__item,.el-carousel__mask{height:100%;position:absolute;top:0;left:0}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#FFF;opacity:.24;-webkit-transition:.2s;transition:.2s}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-fade-in-enter,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.collapse-transition{-webkit-transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out;transition:.3s height ease-in-out,.3s padding-top ease-in-out,.3s padding-bottom ease-in-out}.horizontal-collapse-transition{-webkit-transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out;transition:.3s width ease-in-out,.3s padding-left ease-in-out,.3s padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}.el-collapse{border-top:1px solid #EBEEF5;border-bottom:1px solid #EBEEF5}.el-collapse-item.is-disabled .el-collapse-item__header{color:#bbb;cursor:not-allowed}.el-collapse-item__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:48px;line-height:48px;background-color:#FFF;color:#303133;cursor:pointer;border-bottom:1px solid #EBEEF5;font-size:13px;font-weight:500;-webkit-transition:border-bottom-color .3s;transition:border-bottom-color .3s;outline:0}.el-collapse-item__arrow{margin:0 8px 0 auto;-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-weight:300}.el-collapse-item__arrow.is-active{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-collapse-item__header.focusing:focus:not(:hover){color:#409EFF}.el-collapse-item__header.is-active{border-bottom-color:transparent}.el-collapse-item__wrap{will-change:height;background-color:#FFF;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;border-bottom:1px solid #EBEEF5}.el-cascader__search-input,.el-cascader__tags,.el-tag{-webkit-box-sizing:border-box}.el-collapse-item__content{padding-bottom:25px;font-size:13px;color:#303133;line-height:1.769230769230769}.el-collapse-item:last-child{margin-bottom:-1px}.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-cascader,.el-tag{display:inline-block}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#EBEEF5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#FFF;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#EBEEF5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#FFF}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#EBEEF5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#FFF;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#EBEEF5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#FFF}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409EFF;border-width:1px;border-style:solid;border-radius:4px;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409EFF}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67C23A}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close::before{display:block}.el-tag--dark{background-color:#409eff;border-color:#409eff;color:#fff}.el-tag--dark.is-hit{border-color:#409EFF}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#FFF;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67C23A}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409EFF}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#FFF;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#FFF;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67C23A}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#FFF;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#E6A23C}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#FFF;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#F56C6C}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#FFF;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.el-cascader{position:relative;font-size:14px;line-height:40px}.el-cascader:not(.is-disabled):hover .el-input__inner{cursor:pointer;border-color:#C0C4CC}.el-cascader .el-input .el-input__inner:focus,.el-cascader .el-input.is-focus .el-input__inner{border-color:#409EFF}.el-cascader .el-input{cursor:pointer}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-icon-arrow-down{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;font-size:14px}.el-cascader .el-input .el-icon-arrow-down.is-reverse{-webkit-transform:rotateZ(180deg);transform:rotateZ(180deg)}.el-cascader .el-input .el-icon-circle-close:hover{color:#909399}.el-cascader--medium{font-size:14px;line-height:36px}.el-cascader--small{font-size:13px;line-height:32px}.el-cascader--mini{font-size:12px;line-height:28px}.el-cascader.is-disabled .el-cascader__label{z-index:2;color:#C0C4CC}.el-cascader__dropdown{margin:5px 0;font-size:14px;background:#FFF;border:1px solid #E4E7ED;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-cascader__tags{position:absolute;left:0;right:30px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;line-height:normal;text-align:left;box-sizing:border-box}.el-cascader__tags .el-tag{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;max-width:100%;margin:2px 0 2px 6px;text-overflow:ellipsis;background:#f0f2f5}.el-cascader__tags .el-tag:not(.is-hit){border-color:transparent}.el-cascader__tags .el-tag>span{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis}.el-cascader__tags .el-tag .el-icon-close{-webkit-box-flex:0;-ms-flex:none;flex:none;background-color:#C0C4CC;color:#FFF}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:#909399}.el-cascader__suggestion-panel{border-radius:4px}.el-cascader__suggestion-list{max-height:204px;margin:0;padding:6px 0;font-size:14px;color:#606266;text-align:center}.el-cascader__suggestion-item{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;padding:0 15px;text-align:left;outline:0;cursor:pointer}.el-cascader__suggestion-item:focus,.el-cascader__suggestion-item:hover{background:#F5F7FA}.el-cascader__suggestion-item.is-checked{color:#409EFF;font-weight:700}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__empty-text{margin:10px 0;color:#C0C4CC}.el-cascader__search-input{-webkit-box-flex:1;-ms-flex:1;flex:1;height:24px;min-width:60px;margin:2px 0 2px 15px;padding:0;color:#606266;border:none;outline:0;box-sizing:border-box}.el-cascader__search-input::-webkit-input-placeholder{color:#C0C4CC}.el-cascader__search-input:-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::-ms-input-placeholder{color:#C0C4CC}.el-cascader__search-input::placeholder{color:#C0C4CC}.el-color-predefine{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:12px;margin-top:8px;width:280px}.el-color-predefine__colors{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-wrap:wrap;flex-wrap:wrap}.el-color-predefine__color-selector{margin:0 0 8px 8px;width:20px;height:20px;border-radius:4px;cursor:pointer}.el-color-predefine__color-selector:nth-child(10n+1){margin-left:0}.el-color-predefine__color-selector.selected{-webkit-box-shadow:0 0 3px 2px #409EFF;box-shadow:0 0 3px 2px #409EFF}.el-color-predefine__color-selector>div{display:-webkit-box;display:-ms-flexbox;display:flex;height:100%;border-radius:3px}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background-color:red;padding:0 2px}.el-color-hue-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to right,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%);height:100%}.el-color-hue-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.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:-webkit-gradient(linear,left top,left bottom,from(red),color-stop(17%,#ff0),color-stop(33%,#0f0),color-stop(50%,#0ff),color-stop(67%,#00f),color-stop(83%,#f0f),to(red));background:linear-gradient(to bottom,red 0,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,red 100%)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-svpanel{position:relative;width:280px;height:180px}.el-color-svpanel__black,.el-color-svpanel__white{position:absolute;top:0;left:0;right:0;bottom:0}.el-color-svpanel__white{background:-webkit-gradient(linear,left top,right top,from(#fff),to(rgba(255,255,255,0)));background:linear-gradient(to right,#fff,rgba(255,255,255,0))}.el-color-svpanel__black{background:-webkit-gradient(linear,left bottom,left top,from(#000),to(rgba(0,0,0,0)));background:linear-gradient(to top,#000,rgba(0,0,0,0))}.el-color-svpanel__cursor{position:absolute}.el-color-svpanel__cursor>div{cursor:head;width:4px;height:4px;-webkit-box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px rgba(0,0,0,.3),0 0 1px 2px rgba(0,0,0,.4);border-radius:50%;-webkit-transform:translate(-2px,-2px);transform:translate(-2px,-2px)}.el-color-alpha-slider{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:280px;height:12px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-alpha-slider__bar{position:relative;background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%);height:100%}.el-color-alpha-slider__thumb{position:absolute;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box;left:0;top:0;width:4px;height:100%;border-radius:1px;background:#fff;border:1px solid #f0f0f0;-webkit-box-shadow:0 0 2px rgba(0,0,0,.6);box-shadow:0 0 2px rgba(0,0,0,.6);z-index:1}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),to(white));background:linear-gradient(to bottom,rgba(255,255,255,0) 0,#fff 100%)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{left:0;top:0;width:100%;height:4px}.el-color-dropdown{width:300px}.el-color-dropdown__main-wrapper{margin-bottom:6px}.el-color-dropdown__main-wrapper::after{display:table;clear:both}.el-color-dropdown__btns{margin-top:6px;text-align:right}.el-color-dropdown__value{float:left;line-height:26px;font-size:12px;color:#000;width:160px}.el-color-dropdown__btn{border:1px solid #dcdcdc;color:#333;line-height:24px;border-radius:2px;padding:0 20px;cursor:pointer;background-color:transparent;outline:0;font-size:12px}.el-color-dropdown__btn[disabled]{color:#ccc;cursor:not-allowed}.el-color-dropdown__btn:hover{color:#409EFF;border-color:#409EFF}.el-color-dropdown__link-btn{cursor:pointer;color:#409EFF;text-decoration:none;padding:15px;font-size:12px}.el-color-dropdown__link-btn:hover{color:tint(#409EFF,20%)}.el-color-picker{display:inline-block;position:relative;line-height:normal;height:40px}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed}.el-color-picker--medium{height:36px}.el-color-picker--medium .el-color-picker__trigger{height:36px;width:36px}.el-color-picker--medium .el-color-picker__mask{height:34px;width:34px}.el-color-picker--small{height:32px}.el-color-picker--small .el-color-picker__trigger{height:32px;width:32px}.el-color-picker--small .el-color-picker__mask{height:30px;width:30px}.el-color-picker--small .el-color-picker__empty,.el-color-picker--small .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker--mini{height:28px}.el-color-picker--mini .el-color-picker__trigger{height:28px;width:28px}.el-color-picker--mini .el-color-picker__mask{height:26px;width:26px}.el-color-picker--mini .el-color-picker__empty,.el-color-picker--mini .el-color-picker__icon{-webkit-transform:translate3d(-50%,-50%,0) scale(.8);transform:translate3d(-50%,-50%,0) scale(.8)}.el-color-picker__mask{height:38px;width:38px;border-radius:4px;position:absolute;top:1px;left:1px;z-index:1;cursor:not-allowed;background-color:rgba(255,255,255,.7)}.el-color-picker__trigger{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px;width:40px;padding:4px;border:1px solid #e6e6e6;border-radius:4px;font-size:0;position:relative;cursor:pointer}.el-color-picker__color{position:relative;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #999;border-radius:2px;width:100%;height:100%;text-align:center}.el-color-picker__icon,.el-input,.el-textarea{display:inline-block;width:100%}.el-color-picker__color.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-picker__color-inner{position:absolute;left:0;top:0;right:0;bottom:0}.el-color-picker__empty{font-size:12px;color:#999;position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.el-color-picker__icon{position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0);color:#FFF;text-align:center;font-size:12px}.el-input__prefix,.el-input__suffix{position:absolute;top:0;text-align:center}.el-color-picker__panel{position:absolute;z-index:10;padding:6px;-webkit-box-sizing:content-box;box-sizing:content-box;background-color:#FFF;border:1px solid #EBEEF5;border-radius:4px;box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-input__inner,.el-textarea__inner,.el-transfer-panel{-webkit-box-sizing:border-box}.el-textarea{position:relative;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#FFF;background-image:none;border:1px solid #DCDFE6;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner::-webkit-input-placeholder{color:#C0C4CC}.el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::-ms-input-placeholder{color:#C0C4CC}.el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea__inner:hover{border-color:#C0C4CC}.el-textarea__inner:focus{outline:0;border-color:#409EFF}.el-textarea .el-input__count{color:#909399;background:#FFF;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#C0C4CC}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#C0C4CC}.el-textarea.is-exceed .el-textarea__inner{border-color:#F56C6C}.el-textarea.is-exceed .el-input__count{color:#F56C6C}.el-input{position:relative;font-size:14px}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner{background:#fff}.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#C0C4CC;font-size:14px;cursor:pointer;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#909399;font-size:12px}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input,.el-input__inner{font-size:inherit}.el-input .el-input__count .el-input__count-inner{background:#FFF;line-height:initial;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#FFF;background-image:none;border-radius:4px;border:1px solid #DCDFE6;box-sizing:border-box;color:#606266;display:inline-block;height:40px;line-height:40px;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__inner::-ms-reveal{display:none}.el-input__inner::-webkit-input-placeholder{color:#C0C4CC}.el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input__inner::-ms-input-placeholder{color:#C0C4CC}.el-input__inner::placeholder{color:#C0C4CC}.el-input__inner:hover{border-color:#C0C4CC}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409EFF;outline:0}.el-input__suffix{height:100%;right:5px;transition:all .3s;pointer-events:none}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{height:100%;left:5px;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;-webkit-transition:all .3s;transition:all .3s;line-height:40px}.el-input__icon:after{content:'';height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#F5F7FA;border-color:#E4E7ED;color:#C0C4CC;cursor:not-allowed}.el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__inner::placeholder{color:#C0C4CC}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-image-viewer__btn,.el-image__preview,.el-link,.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-input.is-exceed .el-input__inner{border-color:#F56C6C}.el-input.is-exceed .el-input__suffix .el-input__count{color:#F56C6C}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#F5F7FA;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #DCDFE6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input-group--prepend .el-input__inner{border-top-left-radius:0;border-bottom-left-radius:0}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-transfer{font-size:14px}.el-transfer__buttons{display:inline-block;vertical-align:middle;padding:0 30px}.el-transfer__button{display:block;margin:0 auto;padding:10px;border-radius:50%;color:#FFF;background-color:#409EFF;font-size:0}.el-button-group>.el-button+.el-button,.el-transfer-panel__item+.el-transfer-panel__item,.el-transfer__button [class*=el-icon-]+span{margin-left:0}.el-divider__text,.el-image__error,.el-link,.el-timeline,.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer__button.is-with-texts{border-radius:4px}.el-transfer__button.is-disabled,.el-transfer__button.is-disabled:hover{border:1px solid #DCDFE6;background-color:#F5F7FA;color:#C0C4CC}.el-transfer__button:first-child{margin-bottom:10px}.el-transfer__button:nth-child(2){margin:0}.el-transfer-panel{border:1px solid #EBEEF5;border-radius:4px;overflow:hidden;background:#FFF;display:inline-block;vertical-align:middle;width:200px;max-height:100%;box-sizing:border-box;position:relative}.el-transfer-panel__body{height:246px}.el-transfer-panel__body.is-with-footer{padding-bottom:40px}.el-transfer-panel__list{margin:0;padding:6px 0;list-style:none;height:246px;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.el-transfer-panel__list.is-filterable{height:194px;padding-top:0}.el-transfer-panel__item{height:30px;line-height:30px;padding-left:15px;display:block!important}.el-transfer-panel__item.el-checkbox{color:#606266}.el-transfer-panel__item:hover{color:#409EFF}.el-transfer-panel__item.el-checkbox .el-checkbox__label{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;-webkit-box-sizing:border-box;box-sizing:border-box;padding-left:24px;line-height:30px}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;margin:15px;-webkit-box-sizing:border-box;box-sizing:border-box;display:block;width:auto}.el-transfer-panel__filter .el-input__inner{height:32px;width:100%;font-size:12px;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:16px;padding-right:10px;padding-left:30px}.el-transfer-panel__filter .el-input__icon{margin-left:5px}.el-transfer-panel .el-transfer-panel__header{height:40px;line-height:40px;background:#F5F7FA;margin:0;padding-left:15px;border-bottom:1px solid #EBEEF5;-webkit-box-sizing:border-box;box-sizing:border-box;color:#000}.el-container,.el-header{-webkit-box-sizing:border-box}.el-transfer-panel .el-transfer-panel__header .el-checkbox{display:block;line-height:40px}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{font-size:16px;color:#303133;font-weight:400}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label span{position:absolute;right:15px;color:#909399;font-size:12px;font-weight:400}.el-transfer-panel .el-transfer-panel__footer{height:40px;background:#FFF;margin:0;padding:0;border-top:1px solid #EBEEF5;position:absolute;bottom:0;left:0;width:100%;z-index:1}.el-transfer-panel .el-transfer-panel__footer::after{display:inline-block;height:100%;vertical-align:middle}.el-container,.el-timeline-item__node{display:-webkit-box;display:-ms-flexbox}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{padding-left:20px;color:#606266}.el-transfer-panel .el-transfer-panel__empty{margin:0;height:30px;line-height:30px;padding:6px 15px 0;color:#909399;text-align:center}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-transfer-panel .el-checkbox__inner{height:14px;width:14px;border-radius:3px}.el-transfer-panel .el-checkbox__inner::after{height:6px;width:3px;left:4px}.el-container{display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;box-sizing:border-box;min-width:0}.el-container.is-vertical,.el-drawer,.el-empty,.el-result{-webkit-box-orient:vertical;-webkit-box-direction:normal}.el-container.is-vertical{-ms-flex-direction:column;flex-direction:column}.el-header{padding:0 20px;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-main{display:block;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box}.el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}.el-timeline{margin:0;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline-item{position:relative;padding-bottom:20px}.el-timeline-item__wrapper{position:relative;padding-left:28px;top:-3px}.el-timeline-item__tail{position:absolute;left:4px;height:100%;border-left:2px solid #E4E7ED}.el-timeline-item__icon{color:#FFF;font-size:13px}.el-timeline-item__node{position:absolute;background-color:#E4E7ED;border-radius:50%;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-image__error,.el-timeline-item__dot{display:-webkit-box;display:-ms-flexbox}.el-timeline-item__node--normal{left:-1px;width:12px;height:12px}.el-timeline-item__node--large{left:-2px;width:14px;height:14px}.el-timeline-item__node--primary{background-color:#409EFF}.el-timeline-item__node--success{background-color:#67C23A}.el-timeline-item__node--warning{background-color:#E6A23C}.el-timeline-item__node--danger{background-color:#F56C6C}.el-timeline-item__node--info{background-color:#909399}.el-timeline-item__dot{position:absolute;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-timeline-item__content{color:#303133}.el-timeline-item__timestamp{color:#909399;line-height:1;font-size:13px}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-link{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;vertical-align:middle;position:relative;text-decoration:none;outline:0;padding:0;font-weight:500}.el-link.is-underline:hover:after{position:absolute;left:0;right:0;height:0;bottom:0;border-bottom:1px solid #409EFF}.el-link.el-link--default:after,.el-link.el-link--primary.is-underline:hover:after,.el-link.el-link--primary:after{border-color:#409EFF}.el-link.is-disabled{cursor:not-allowed}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link.el-link--default{color:#606266}.el-link.el-link--default:hover{color:#409EFF}.el-link.el-link--default.is-disabled{color:#C0C4CC}.el-link.el-link--primary{color:#409EFF}.el-link.el-link--primary:hover{color:#66b1ff}.el-link.el-link--primary.is-disabled{color:#a0cfff}.el-link.el-link--danger.is-underline:hover:after,.el-link.el-link--danger:after{border-color:#F56C6C}.el-link.el-link--danger{color:#F56C6C}.el-link.el-link--danger:hover{color:#f78989}.el-link.el-link--danger.is-disabled{color:#fab6b6}.el-link.el-link--success.is-underline:hover:after,.el-link.el-link--success:after{border-color:#67C23A}.el-link.el-link--success{color:#67C23A}.el-link.el-link--success:hover{color:#85ce61}.el-link.el-link--success.is-disabled{color:#b3e19d}.el-link.el-link--warning.is-underline:hover:after,.el-link.el-link--warning:after{border-color:#E6A23C}.el-link.el-link--warning{color:#E6A23C}.el-link.el-link--warning:hover{color:#ebb563}.el-link.el-link--warning.is-disabled{color:#f3d19e}.el-link.el-link--info.is-underline:hover:after,.el-link.el-link--info:after{border-color:#909399}.el-link.el-link--info{color:#909399}.el-link.el-link--info:hover{color:#a6a9ad}.el-link.el-link--info.is-disabled{color:#c8c9cc}.el-divider{background-color:#DCDFE6;position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative}.el-divider__text{position:absolute;background-color:#FFF;padding:0 20px;font-weight:500;color:#303133}.el-image__error,.el-image__placeholder{background:#F5F7FA}.el-divider__text.is-left{left:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-divider__text.is-center{left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-image__error,.el-image__inner,.el-image__placeholder{width:100%;height:100%}.el-image{position:relative;display:inline-block;overflow:hidden}.el-image__inner{vertical-align:top}.el-image__inner--center{position:relative;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:block}.el-image__error{display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#C0C4CC;vertical-align:middle}.el-image-viewer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0}.el-image-viewer__btn{position:absolute;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:50%;opacity:.8;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-user-select:none;user-select:none}.el-button,.el-checkbox,.el-checkbox-button__inner,.el-empty__image img,.el-radio{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-image-viewer__close{top:40px;right:40px;width:40px;height:40px;font-size:24px;color:#fff;background-color:#606266}.el-image-viewer__canvas{width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-image-viewer__actions{left:50%;bottom:30px;-webkit-transform:translateX(-50%);transform:translateX(-50%);width:282px;height:44px;padding:0 23px;background-color:#606266;border-color:#fff;border-radius:22px}.el-image-viewer__actions__inner{width:100%;height:100%;text-align:justify;cursor:default;font-size:23px;color:#fff;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-pack:distribute;justify-content:space-around}.el-image-viewer__next,.el-image-viewer__prev{width:44px;height:44px;font-size:24px;color:#fff;background-color:#606266;border-color:#fff;top:50%}.el-image-viewer__prev{-webkit-transform:translateY(-50%);transform:translateY(-50%);left:40px}.el-image-viewer__next{-webkit-transform:translateY(-50%);transform:translateY(-50%);right:40px;text-indent:2px}.el-image-viewer__mask{position:absolute;width:100%;height:100%;top:0;left:0;opacity:.5;background:#000}.viewer-fade-enter-active{-webkit-animation:viewer-fade-in .3s;animation:viewer-fade-in .3s}.viewer-fade-leave-active{-webkit-animation:viewer-fade-out .3s;animation:viewer-fade-out .3s}@-webkit-keyframes viewer-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes viewer-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes viewer-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes viewer-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button,.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-button:focus,.el-button:hover{color:#409EFF;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#FFF;border-color:#409EFF;color:#409EFF}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#FFF;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#FFF;border-color:#EBEEF5;color:#C0C4CC}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:'';position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:rgba(255,255,255,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#FFF;background-color:#409EFF;border-color:#409EFF}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#FFF}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#FFF;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409EFF;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409EFF;border-color:#409EFF;color:#FFF}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#FFF;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#FFF;background-color:#67C23A;border-color:#67C23A}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#FFF}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#FFF}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#FFF;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67C23A;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67C23A;border-color:#67C23A;color:#FFF}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#FFF;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#FFF;background-color:#E6A23C;border-color:#E6A23C}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#FFF}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#FFF}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#FFF;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#E6A23C;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#E6A23C;border-color:#E6A23C;color:#FFF}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#FFF;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#FFF;background-color:#F56C6C;border-color:#F56C6C}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#FFF}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#FFF}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#FFF;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#F56C6C;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#F56C6C;border-color:#F56C6C;color:#FFF}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#FFF;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#FFF;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#FFF}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#FFF}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#FFF;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#FFF}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#FFF;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small{padding:9px 15px;font-size:12px;border-radius:3px}.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini{font-size:12px;border-radius:3px}.el-button--mini.is-circle{padding:7px}.el-button--text{border-color:transparent;color:#409EFF;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;border-color:transparent;background-color:transparent}.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover{border-color:transparent}.el-button-group .el-button--danger:last-child,.el-button-group .el-button--danger:not(:first-child):not(:last-child),.el-button-group .el-button--info:last-child,.el-button-group .el-button--info:not(:first-child):not(:last-child),.el-button-group .el-button--primary:last-child,.el-button-group .el-button--primary:not(:first-child):not(:last-child),.el-button-group .el-button--success:last-child,.el-button-group .el-button--success:not(:first-child):not(:last-child),.el-button-group .el-button--warning:last-child,.el-button-group .el-button--warning:not(:first-child):not(:last-child),.el-button-group>.el-dropdown>.el-button{border-left-color:rgba(255,255,255,.5)}.el-button-group .el-button--danger:first-child,.el-button-group .el-button--danger:not(:first-child):not(:last-child),.el-button-group .el-button--info:first-child,.el-button-group .el-button--info:not(:first-child):not(:last-child),.el-button-group .el-button--primary:first-child,.el-button-group .el-button--primary:not(:first-child):not(:last-child),.el-button-group .el-button--success:first-child,.el-button-group .el-button--success:not(:first-child):not(:last-child),.el-button-group .el-button--warning:first-child,.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-right-color:rgba(255,255,255,.5)}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group::after,.el-button-group::before{display:table}.el-button-group::after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button.is-disabled{z-index:1}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.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.is-active,.el-button-group>.el-button:not(.is-disabled):active,.el-button-group>.el-button:not(.is-disabled):focus,.el-button-group>.el-button:not(.is-disabled):hover{z-index:1}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0}.el-calendar{background-color:#fff}.el-calendar__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:12px 20px;border-bottom:1px solid #EBEEF5}.el-backtop,.el-page-header{display:-webkit-box;display:-ms-flexbox}.el-calendar__title{color:#000;-ms-flex-item-align:center;align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{padding:12px 0;color:#606266;font-weight:400}.el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{color:#C0C4CC}.el-backtop,.el-calendar-table td.is-today{color:#409EFF}.el-calendar-table td{border-bottom:1px solid #EBEEF5;border-right:1px solid #EBEEF5;vertical-align:top;-webkit-transition:background-color .2s ease;transition:background-color .2s ease}.el-calendar-table td.is-selected{background-color:#F2F8FE}.el-calendar-table tr:first-child td{border-top:1px solid #EBEEF5}.el-calendar-table tr td:first-child{border-left:1px solid #EBEEF5}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{-webkit-box-sizing:border-box;box-sizing:border-box;padding:8px;height:85px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:#F2F8FE}.el-backtop{position:fixed;background-color:#FFF;width:40px;height:40px;border-radius:50%;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;font-size:20px;-webkit-box-shadow:0 0 6px rgba(0,0,0,.12);box-shadow:0 0 6px rgba(0,0,0,.12);cursor:pointer;z-index:5}.el-backtop:hover{background-color:#F2F6FC}.el-page-header{display:flex;line-height:24px}.el-page-header__left{display:-webkit-box;display:-ms-flexbox;display:flex;cursor:pointer;margin-right:40px;position:relative}.el-page-header__left::after{position:absolute;width:1px;height:16px;right:-20px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);background-color:#DCDFE6}.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-page-header__left .el-icon-back{font-size:18px;margin-right:6px;-ms-flex-item-align:center;align-self:center}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{font-size:18px;color:#303133}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;user-select:none;margin-right:30px}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #DCDFE6;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409EFF}.el-checkbox.is-bordered.is-disabled{border-color:#EBEEF5;cursor:not-allowed}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner::after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#DCDFE6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner::after{cursor:not-allowed;border-color:#C0C4CC}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{border-color:#C0C4CC}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#F2F6FC;border-color:#DCDFE6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner::before{background-color:#C0C4CC;border-color:#C0C4CC}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409EFF;border-color:#409EFF}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#C0C4CC;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner::after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409EFF}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409EFF}.el-checkbox__input.is-indeterminate .el-checkbox__inner::before{content:'';position:absolute;display:block;background-color:#FFF;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner::after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #DCDFE6;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#FFF;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409EFF}.el-checkbox__inner::after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #FFF;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s ease-in .05s;transition:-webkit-transform .15s ease-in .05s;transition:transform .15s ease-in .05s;transition:transform .15s ease-in .05s,-webkit-transform .15s ease-in .05s;-webkit-transform-origin:center;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{display:inline-block;position:relative}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#FFF;border:1px solid #DCDFE6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409EFF}.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;outline:0;position:absolute;margin:0;z-index:-1}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#FFF;background-color:#409EFF;border-color:#409EFF;-webkit-box-shadow:-1px 0 0 0 #8cc5ff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409EFF}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#C0C4CC;cursor:not-allowed;background-image:none;background-color:#FFF;border-color:#EBEEF5;-webkit-box-shadow:none;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#EBEEF5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #DCDFE6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409EFF}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-avatar,.el-cascader-panel,.el-radio,.el-radio--medium.is-bordered .el-radio__label,.el-radio__label{font-size:14px}.el-radio{color:#606266;font-weight:500;line-height:1;cursor:pointer;white-space:nowrap;outline:0;margin-right:30px}.el-cascader-node>.el-radio,.el-radio:last-child{margin-right:0}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #DCDFE6;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px}.el-cascader-menu,.el-cascader-menu__list,.el-radio__inner{-webkit-box-sizing:border-box}.el-radio.is-bordered.is-checked{border-color:#409EFF}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#EBEEF5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#F5F7FA;border-color:#E4E7ED}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio__input{white-space:nowrap;cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#F5F7FA}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#C0C4CC}.el-radio__input.is-disabled+span.el-radio__label{color:#C0C4CC;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#409EFF;background:#409EFF}.el-radio__input.is-checked .el-radio__inner::after{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#409EFF}.el-radio__input.is-focus .el-radio__inner{border-color:#409EFF}.el-radio__inner{border:1px solid #DCDFE6;border-radius:100%;width:14px;height:14px;background-color:#FFF;cursor:pointer;box-sizing:border-box}.el-radio__inner:hover{border-color:#409EFF}.el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#FFF;content:"";position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{-webkit-box-shadow:0 0 2px 2px #409EFF;box-shadow:0 0 2px 2px #409EFF}.el-radio__label{padding-left:10px}.el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(144,147,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.el-scrollbar__thumb:hover{background-color:rgba(144,147,153,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.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-cascader-panel{display:-webkit-box;display:-ms-flexbox;display:flex;border-radius:4px}.el-cascader-panel.is-bordered{border:1px solid #E4E7ED;border-radius:4px}.el-cascader-menu{min-width:180px;box-sizing:border-box;color:#606266;border-right:solid 1px #E4E7ED}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu__wrap{height:204px}.el-cascader-menu__list{position:relative;min-height:100%;margin:0;padding:6px 0;list-style:none;box-sizing:border-box}.el-cascader-menu__hover-zone{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.el-cascader-menu__empty-text{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-align:center;color:#C0C4CC}.el-cascader-node{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0 30px 0 20px;height:34px;line-height:34px;outline:0}.el-cascader-node.is-selectable.in-active-path{color:#606266}.el-cascader-node.in-active-path,.el-cascader-node.is-active,.el-cascader-node.is-selectable.in-checked-path{color:#409EFF;font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):focus,.el-cascader-node:not(.is-disabled):hover{background:#F5F7FA}.el-cascader-node.is-disabled{color:#C0C4CC;cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{-webkit-box-flex:1;-ms-flex:1;flex:1;padding:0 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-avatar{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;overflow:hidden;color:#fff;background:#C0C4CC;width:40px;height:40px;line-height:40px}.el-drawer,.el-drawer__body>*{-webkit-box-sizing:border-box}.el-avatar>img{display:block;height:100%;vertical-align:middle}.el-empty__image img,.el-empty__image svg{vertical-align:top;height:100%;width:100%}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:4px}.el-avatar--icon{font-size:18px}.el-avatar--large{width:40px;height:40px;line-height:40px}.el-avatar--medium{width:36px;height:36px;line-height:36px}.el-avatar--small{width:28px;height:28px;line-height:28px}@-webkit-keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes el-drawer-fade-in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes rtl-drawer-in{0%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes rtl-drawer-in{0%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes rtl-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}}@keyframes rtl-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(100%,0);transform:translate(100%,0)}}@-webkit-keyframes ltr-drawer-in{0%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes ltr-drawer-in{0%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes ltr-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}}@keyframes ltr-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}}@-webkit-keyframes ttb-drawer-in{0%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes ttb-drawer-in{0%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes ttb-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}}@keyframes ttb-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,-100%);transform:translate(0,-100%)}}@-webkit-keyframes btt-drawer-in{0%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes btt-drawer-in{0%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@-webkit-keyframes btt-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}}@keyframes btt-drawer-out{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}100%{-webkit-transform:translate(0,100%);transform:translate(0,100%)}}.el-drawer{position:absolute;box-sizing:border-box;background-color:#FFF;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-webkit-box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);overflow:hidden;outline:0}.el-drawer.rtl{-webkit-animation:rtl-drawer-out .3s;animation:rtl-drawer-out .3s;right:0}.el-drawer__open .el-drawer.rtl{-webkit-animation:rtl-drawer-in .3s 1ms;animation:rtl-drawer-in .3s 1ms}.el-drawer.ltr{-webkit-animation:ltr-drawer-out .3s;animation:ltr-drawer-out .3s;left:0}.el-drawer__open .el-drawer.ltr{-webkit-animation:ltr-drawer-in .3s 1ms;animation:ltr-drawer-in .3s 1ms}.el-drawer.ttb{-webkit-animation:ttb-drawer-out .3s;animation:ttb-drawer-out .3s;top:0}.el-drawer__open .el-drawer.ttb{-webkit-animation:ttb-drawer-in .3s 1ms;animation:ttb-drawer-in .3s 1ms}.el-drawer.btt{-webkit-animation:btt-drawer-out .3s;animation:btt-drawer-out .3s;bottom:0}.el-drawer__open .el-drawer.btt{-webkit-animation:btt-drawer-in .3s 1ms;animation:btt-drawer-in .3s 1ms}.el-drawer__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:hidden;margin:0}.el-drawer__header{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#72767b;display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:32px;padding:20px 20px 0}.el-drawer__header>:first-child{-webkit-box-flex:1;-ms-flex:1;flex:1}.el-drawer__title{margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;line-height:inherit;font-size:1rem}.el-drawer__close-btn{border:none;cursor:pointer;font-size:20px;color:inherit;background-color:transparent}.el-drawer__body{-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:auto}.el-drawer__body>*{box-sizing:border-box}.el-drawer.ltr,.el-drawer.rtl{height:100%;top:0;bottom:0}.el-drawer.btt,.el-drawer.ttb{width:100%;left:0;right:0}.el-drawer__container{position:relative;left:0;right:0;top:0;bottom:0;height:100%;width:100%}.el-drawer-fade-enter-active{-webkit-animation:el-drawer-fade-in .3s;animation:el-drawer-fade-in .3s}.el-drawer-fade-leave-active{animation:el-drawer-fade-in .3s reverse}.el-statistic{width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:#000;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum";text-align:center}.el-statistic .head{margin-bottom:4px;color:#00073;font-size:14px}.el-statistic .con{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-statistic .con .number{font-size:20px;padding:0 4px}.el-statistic .con span{display:inline-block;margin:0;line-height:100%}.el-popconfirm__main,.el-skeleton__image{display:-ms-flexbox;-webkit-box-align:center;display:-webkit-box}.el-popconfirm__main{display:flex;-ms-flex-align:center;align-items:center}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin:0}@-webkit-keyframes el-skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}@keyframes el-skeleton-loading{0%{background-position:100% 50%}100%{background-position:0 50%}}.el-skeleton{width:100%}.el-skeleton__first-line,.el-skeleton__paragraph{height:16px;margin-top:16px;background:#f2f2f2}.el-skeleton.is-animated .el-skeleton__item{background:-webkit-gradient(linear,left top,right top,color-stop(25%,#f2f2f2),color-stop(37%,#e6e6e6),color-stop(63%,#f2f2f2));background:linear-gradient(90deg,#f2f2f2 25%,#e6e6e6 37%,#f2f2f2 63%);background-size:400% 100%;-webkit-animation:el-skeleton-loading 1.4s ease infinite;animation:el-skeleton-loading 1.4s ease infinite}.el-skeleton__item{background:#f2f2f2;display:inline-block;height:16px;border-radius:4px;width:100%}.el-skeleton__circle{border-radius:50%;width:36px;height:36px;line-height:36px}.el-skeleton__circle--lg{width:40px;height:40px;line-height:40px}.el-skeleton__circle--md{width:28px;height:28px;line-height:28px}.el-skeleton__button{height:40px;width:64px;border-radius:4px}.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:13px}.el-skeleton__caption{height:12px}.el-skeleton__h1{height:20px}.el-skeleton__h3{height:18px}.el-skeleton__h5{height:16px}.el-skeleton__image{width:unset;display:flex;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:0}.el-skeleton__image svg{fill:#DCDDE0;width:22%;height:22%}.el-empty{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;padding:40px 0}.el-empty__image{width:160px}.el-empty__image img{user-select:none;-o-object-fit:contain;object-fit:contain}.el-empty__image svg{fill:#DCDDE0}.el-empty__description{margin-top:20px}.el-empty__description p{margin:0;font-size:14px;color:#909399}.el-empty__bottom,.el-result__title{margin-top:20px}.el-descriptions{-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px;color:#303133}.el-descriptions__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:20px}.el-descriptions__title{font-size:16px;font-weight:700}.el-descriptions--mini,.el-descriptions--small{font-size:12px}.el-descriptions__body{color:#606266;background-color:#FFF}.el-descriptions__body .el-descriptions__table{border-collapse:collapse;width:100%;table-layout:fixed}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell{-webkit-box-sizing:border-box;box-sizing:border-box;text-align:left;font-weight:400;line-height:1.5}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-left{text-align:left}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-center{text-align:center}.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-right{text-align:right}.el-descriptions .is-bordered{table-layout:auto}.el-descriptions .is-bordered .el-descriptions-item__cell{border:1px solid #EBEEF5;padding:12px 10px}.el-descriptions :not(.is-bordered) .el-descriptions-item__cell{padding-bottom:12px}.el-descriptions--medium.is-bordered .el-descriptions-item__cell{padding:10px}.el-descriptions--medium:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:10px}.el-descriptions--small.is-bordered .el-descriptions-item__cell{padding:8px 10px}.el-descriptions--small:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:8px}.el-descriptions--mini.is-bordered .el-descriptions-item__cell{padding:6px 10px}.el-descriptions--mini:not(.is-bordered) .el-descriptions-item__cell{padding-bottom:6px}.el-descriptions-item{vertical-align:top}.el-descriptions-item__container{display:-webkit-box;display:-ms-flexbox;display:flex}.el-descriptions-item__container .el-descriptions-item__content,.el-descriptions-item__container .el-descriptions-item__label{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.el-descriptions-item__container .el-descriptions-item__content{-webkit-box-flex:1;-ms-flex:1;flex:1}.el-descriptions-item__label.has-colon::after{content:':';position:relative;top:-.5px}.el-descriptions-item__label.is-bordered-label{font-weight:700;color:#909399;background:#fafafa}.el-descriptions-item__label:not(.is-bordered-label){margin-right:10px}.el-descriptions-item__content{word-break:break-word;overflow-wrap:break-word}.el-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-direction:column;flex-direction:column;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;padding:40px 30px}.el-result__icon svg{width:64px;height:64px}.el-result__title p{margin:0;font-size:20px;color:#303133;line-height:1.3}.el-result__subtitle{margin-top:10px}.el-result__subtitle p{margin:0;font-size:14px;color:#606266;line-height:1.3}.el-result__extra{margin-top:30px}.el-result .icon-success{fill:#67C23A}.el-result .icon-error{fill:#F56C6C}.el-result .icon-info{fill:#909399}.el-result .icon-warning{fill:#E6A23C} \ No newline at end of file diff --git a/static/asserts/index.js b/static/asserts/index.js deleted file mode 100644 index 18c1c23d4..000000000 --- a/static/asserts/index.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("vue")):"function"==typeof define&&define.amd?define("ELEMENT",["vue"],t):"object"==typeof exports?exports.ELEMENT=t(require("vue")):e.ELEMENT=t(e.Vue)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var r=t[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(i,r,function(t){return e[t]}.bind(null,r));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=51)}([function(t,n){t.exports=e},function(e,t,n){var i=n(4);e.exports=function(e,t,n){return void 0===n?i(e,t,!1):i(e,n,!1!==t)}},function(e,t,n){var i;!function(r){"use strict";var o={},s=/d{1,4}|M{1,4}|yy(?:yy)?|S{1,3}|Do|ZZ|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g,a="[^\\s]+",l=/\[([^]*?)\]/gm,u=function(){};function c(e,t){for(var n=[],i=0,r=e.length;i3?0:(e-e%10!=10)*e%10]}};var g={D:function(e){return e.getDay()},DD:function(e){return d(e.getDay())},Do:function(e,t){return t.DoFn(e.getDate())},d:function(e){return e.getDate()},dd:function(e){return d(e.getDate())},ddd:function(e,t){return t.dayNamesShort[e.getDay()]},dddd:function(e,t){return t.dayNames[e.getDay()]},M:function(e){return e.getMonth()+1},MM:function(e){return d(e.getMonth()+1)},MMM:function(e,t){return t.monthNamesShort[e.getMonth()]},MMMM:function(e,t){return t.monthNames[e.getMonth()]},yy:function(e){return d(String(e.getFullYear()),4).substr(2)},yyyy:function(e){return d(e.getFullYear(),4)},h:function(e){return e.getHours()%12||12},hh:function(e){return d(e.getHours()%12||12)},H:function(e){return e.getHours()},HH:function(e){return d(e.getHours())},m:function(e){return e.getMinutes()},mm:function(e){return d(e.getMinutes())},s:function(e){return e.getSeconds()},ss:function(e){return d(e.getSeconds())},S:function(e){return Math.round(e.getMilliseconds()/100)},SS:function(e){return d(Math.round(e.getMilliseconds()/10),2)},SSS:function(e){return d(e.getMilliseconds(),3)},a:function(e,t){return e.getHours()<12?t.amPm[0]:t.amPm[1]},A:function(e,t){return e.getHours()<12?t.amPm[0].toUpperCase():t.amPm[1].toUpperCase()},ZZ:function(e){var t=e.getTimezoneOffset();return(t>0?"-":"+")+d(100*Math.floor(Math.abs(t)/60)+Math.abs(t)%60,4)}},y={d:["\\d\\d?",function(e,t){e.day=t}],Do:["\\d\\d?"+a,function(e,t){e.day=parseInt(t,10)}],M:["\\d\\d?",function(e,t){e.month=t-1}],yy:["\\d\\d?",function(e,t){var n=+(""+(new Date).getFullYear()).substr(0,2);e.year=""+(t>68?n-1:n)+t}],h:["\\d\\d?",function(e,t){e.hour=t}],m:["\\d\\d?",function(e,t){e.minute=t}],s:["\\d\\d?",function(e,t){e.second=t}],yyyy:["\\d{4}",function(e,t){e.year=t}],S:["\\d",function(e,t){e.millisecond=100*t}],SS:["\\d{2}",function(e,t){e.millisecond=10*t}],SSS:["\\d{3}",function(e,t){e.millisecond=t}],D:["\\d\\d?",u],ddd:[a,u],MMM:[a,h("monthNamesShort")],MMMM:[a,h("monthNames")],a:[a,function(e,t,n){var i=t.toLowerCase();i===n.amPm[0]?e.isPm=!1:i===n.amPm[1]&&(e.isPm=!0)}],ZZ:["[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z",function(e,t){var n,i=(t+"").match(/([+-]|\d\d)/gi);i&&(n=60*i[1]+parseInt(i[2],10),e.timezoneOffset="+"===i[0]?n:-n)}]};y.dd=y.d,y.dddd=y.ddd,y.DD=y.D,y.mm=y.m,y.hh=y.H=y.HH=y.h,y.MM=y.M,y.ss=y.s,y.A=y.a,o.masks={default:"ddd MMM dd yyyy HH:mm:ss",shortDate:"M/D/yy",mediumDate:"MMM d, yyyy",longDate:"MMMM d, yyyy",fullDate:"dddd, MMMM d, yyyy",shortTime:"HH:mm",mediumTime:"HH:mm:ss",longTime:"HH:mm:ss.SSS"},o.format=function(e,t,n){var i=n||o.i18n;if("number"==typeof e&&(e=new Date(e)),"[object Date]"!==Object.prototype.toString.call(e)||isNaN(e.getTime()))throw new Error("Invalid Date in fecha.format");t=o.masks[t]||t||o.masks.default;var r=[];return(t=(t=t.replace(l,function(e,t){return r.push(t),"@@@"})).replace(s,function(t){return t in g?g[t](e,i):t.slice(1,t.length-1)})).replace(/@@@/g,function(){return r.shift()})},o.parse=function(e,t,n){var i=n||o.i18n;if("string"!=typeof t)throw new Error("Invalid format in fecha.parse");if(t=o.masks[t]||t,e.length>1e3)return null;var r={},a=[],u=[];t=t.replace(l,function(e,t){return u.push(t),"@@@"});var c,h=(c=t,c.replace(/[|\\{()[^$+*?.-]/g,"\\$&")).replace(s,function(e){if(y[e]){var t=y[e];return a.push(t[1]),"("+t[0]+")"}return e});h=h.replace(/@@@/g,function(){return u.shift()});var d=e.match(new RegExp(h,"i"));if(!d)return null;for(var f=1;fe?u():!0!==t&&(r=setTimeout(i?function(){r=void 0}:u,void 0===i?e-a:e))}}},function(e,t,n){(function(e,i){var r;(function(){var o,s=200,a="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",l="Expected a function",u="__lodash_hash_undefined__",c=500,h="__lodash_placeholder__",d=1,f=2,p=4,m=1,v=2,g=1,y=2,b=4,_=8,w=16,x=32,C=64,k=128,S=256,D=512,E=30,$="...",T=800,M=16,N=1,P=2,O=1/0,I=9007199254740991,A=1.7976931348623157e308,F=NaN,L=4294967295,V=L-1,B=L>>>1,z=[["ary",k],["bind",g],["bindKey",y],["curry",_],["curryRight",w],["flip",D],["partial",x],["partialRight",C],["rearg",S]],R="[object Arguments]",H="[object Array]",j="[object AsyncFunction]",W="[object Boolean]",q="[object Date]",Y="[object DOMException]",K="[object Error]",U="[object Function]",G="[object GeneratorFunction]",X="[object Map]",Z="[object Number]",J="[object Null]",Q="[object Object]",ee="[object Proxy]",te="[object RegExp]",ne="[object Set]",ie="[object String]",re="[object Symbol]",oe="[object Undefined]",se="[object WeakMap]",ae="[object WeakSet]",le="[object ArrayBuffer]",ue="[object DataView]",ce="[object Float32Array]",he="[object Float64Array]",de="[object Int8Array]",fe="[object Int16Array]",pe="[object Int32Array]",me="[object Uint8Array]",ve="[object Uint8ClampedArray]",ge="[object Uint16Array]",ye="[object Uint32Array]",be=/\b__p \+= '';/g,_e=/\b(__p \+=) '' \+/g,we=/(__e\(.*?\)|\b__t\)) \+\n'';/g,xe=/&(?:amp|lt|gt|quot|#39);/g,Ce=/[&<>"']/g,ke=RegExp(xe.source),Se=RegExp(Ce.source),De=/<%-([\s\S]+?)%>/g,Ee=/<%([\s\S]+?)%>/g,$e=/<%=([\s\S]+?)%>/g,Te=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Me=/^\w*$/,Ne=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Pe=/[\\^$.*+?()[\]{}|]/g,Oe=RegExp(Pe.source),Ie=/^\s+|\s+$/g,Ae=/^\s+/,Fe=/\s+$/,Le=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Ve=/\{\n\/\* \[wrapped with (.+)\] \*/,Be=/,? & /,ze=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Re=/\\(\\)?/g,He=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,je=/\w*$/,We=/^[-+]0x[0-9a-f]+$/i,qe=/^0b[01]+$/i,Ye=/^\[object .+?Constructor\]$/,Ke=/^0o[0-7]+$/i,Ue=/^(?:0|[1-9]\d*)$/,Ge=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Xe=/($^)/,Ze=/['\n\r\u2028\u2029\\]/g,Je="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",Qe="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",et="[\\ud800-\\udfff]",tt="["+Qe+"]",nt="["+Je+"]",it="\\d+",rt="[\\u2700-\\u27bf]",ot="[a-z\\xdf-\\xf6\\xf8-\\xff]",st="[^\\ud800-\\udfff"+Qe+it+"\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde]",at="\\ud83c[\\udffb-\\udfff]",lt="[^\\ud800-\\udfff]",ut="(?:\\ud83c[\\udde6-\\uddff]){2}",ct="[\\ud800-\\udbff][\\udc00-\\udfff]",ht="[A-Z\\xc0-\\xd6\\xd8-\\xde]",dt="(?:"+ot+"|"+st+")",ft="(?:"+ht+"|"+st+")",pt="(?:"+nt+"|"+at+")"+"?",mt="[\\ufe0e\\ufe0f]?"+pt+("(?:\\u200d(?:"+[lt,ut,ct].join("|")+")[\\ufe0e\\ufe0f]?"+pt+")*"),vt="(?:"+[rt,ut,ct].join("|")+")"+mt,gt="(?:"+[lt+nt+"?",nt,ut,ct,et].join("|")+")",yt=RegExp("['’]","g"),bt=RegExp(nt,"g"),_t=RegExp(at+"(?="+at+")|"+gt+mt,"g"),wt=RegExp([ht+"?"+ot+"+(?:['’](?:d|ll|m|re|s|t|ve))?(?="+[tt,ht,"$"].join("|")+")",ft+"+(?:['’](?:D|LL|M|RE|S|T|VE))?(?="+[tt,ht+dt,"$"].join("|")+")",ht+"?"+dt+"+(?:['’](?:d|ll|m|re|s|t|ve))?",ht+"+(?:['’](?:D|LL|M|RE|S|T|VE))?","\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",it,vt].join("|"),"g"),xt=RegExp("[\\u200d\\ud800-\\udfff"+Je+"\\ufe0e\\ufe0f]"),Ct=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,kt=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],St=-1,Dt={};Dt[ce]=Dt[he]=Dt[de]=Dt[fe]=Dt[pe]=Dt[me]=Dt[ve]=Dt[ge]=Dt[ye]=!0,Dt[R]=Dt[H]=Dt[le]=Dt[W]=Dt[ue]=Dt[q]=Dt[K]=Dt[U]=Dt[X]=Dt[Z]=Dt[Q]=Dt[te]=Dt[ne]=Dt[ie]=Dt[se]=!1;var Et={};Et[R]=Et[H]=Et[le]=Et[ue]=Et[W]=Et[q]=Et[ce]=Et[he]=Et[de]=Et[fe]=Et[pe]=Et[X]=Et[Z]=Et[Q]=Et[te]=Et[ne]=Et[ie]=Et[re]=Et[me]=Et[ve]=Et[ge]=Et[ye]=!0,Et[K]=Et[U]=Et[se]=!1;var $t={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Tt=parseFloat,Mt=parseInt,Nt="object"==typeof e&&e&&e.Object===Object&&e,Pt="object"==typeof self&&self&&self.Object===Object&&self,Ot=Nt||Pt||Function("return this")(),It=t&&!t.nodeType&&t,At=It&&"object"==typeof i&&i&&!i.nodeType&&i,Ft=At&&At.exports===It,Lt=Ft&&Nt.process,Vt=function(){try{var e=At&&At.require&&At.require("util").types;return e||Lt&&Lt.binding&&Lt.binding("util")}catch(e){}}(),Bt=Vt&&Vt.isArrayBuffer,zt=Vt&&Vt.isDate,Rt=Vt&&Vt.isMap,Ht=Vt&&Vt.isRegExp,jt=Vt&&Vt.isSet,Wt=Vt&&Vt.isTypedArray;function qt(e,t,n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)}function Yt(e,t,n,i){for(var r=-1,o=null==e?0:e.length;++r-1}function Jt(e,t,n){for(var i=-1,r=null==e?0:e.length;++i-1;);return n}function wn(e,t){for(var n=e.length;n--&&ln(t,e[n],0)>-1;);return n}var xn=fn({"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","Ç":"C","ç":"c","Ð":"D","ð":"d","È":"E","É":"E","Ê":"E","Ë":"E","è":"e","é":"e","ê":"e","ë":"e","Ì":"I","Í":"I","Î":"I","Ï":"I","ì":"i","í":"i","î":"i","ï":"i","Ñ":"N","ñ":"n","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","Ù":"U","Ú":"U","Û":"U","Ü":"U","ù":"u","ú":"u","û":"u","ü":"u","Ý":"Y","ý":"y","ÿ":"y","Æ":"Ae","æ":"ae","Þ":"Th","þ":"th","ß":"ss","Ā":"A","Ă":"A","Ą":"A","ā":"a","ă":"a","ą":"a","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","ć":"c","ĉ":"c","ċ":"c","č":"c","Ď":"D","Đ":"D","ď":"d","đ":"d","Ē":"E","Ĕ":"E","Ė":"E","Ę":"E","Ě":"E","ē":"e","ĕ":"e","ė":"e","ę":"e","ě":"e","Ĝ":"G","Ğ":"G","Ġ":"G","Ģ":"G","ĝ":"g","ğ":"g","ġ":"g","ģ":"g","Ĥ":"H","Ħ":"H","ĥ":"h","ħ":"h","Ĩ":"I","Ī":"I","Ĭ":"I","Į":"I","İ":"I","ĩ":"i","ī":"i","ĭ":"i","į":"i","ı":"i","Ĵ":"J","ĵ":"j","Ķ":"K","ķ":"k","ĸ":"k","Ĺ":"L","Ļ":"L","Ľ":"L","Ŀ":"L","Ł":"L","ĺ":"l","ļ":"l","ľ":"l","ŀ":"l","ł":"l","Ń":"N","Ņ":"N","Ň":"N","Ŋ":"N","ń":"n","ņ":"n","ň":"n","ŋ":"n","Ō":"O","Ŏ":"O","Ő":"O","ō":"o","ŏ":"o","ő":"o","Ŕ":"R","Ŗ":"R","Ř":"R","ŕ":"r","ŗ":"r","ř":"r","Ś":"S","Ŝ":"S","Ş":"S","Š":"S","ś":"s","ŝ":"s","ş":"s","š":"s","Ţ":"T","Ť":"T","Ŧ":"T","ţ":"t","ť":"t","ŧ":"t","Ũ":"U","Ū":"U","Ŭ":"U","Ů":"U","Ű":"U","Ų":"U","ũ":"u","ū":"u","ŭ":"u","ů":"u","ű":"u","ų":"u","Ŵ":"W","ŵ":"w","Ŷ":"Y","ŷ":"y","Ÿ":"Y","Ź":"Z","Ż":"Z","Ž":"Z","ź":"z","ż":"z","ž":"z","IJ":"IJ","ij":"ij","Œ":"Oe","œ":"oe","ʼn":"'n","ſ":"s"}),Cn=fn({"&":"&","<":"<",">":">",'"':""","'":"'"});function kn(e){return"\\"+$t[e]}function Sn(e){return xt.test(e)}function Dn(e){var t=-1,n=Array(e.size);return e.forEach(function(e,i){n[++t]=[i,e]}),n}function En(e,t){return function(n){return e(t(n))}}function $n(e,t){for(var n=-1,i=e.length,r=0,o=[];++n",""":'"',"'":"'"});var In=function e(t){var n,i=(t=null==t?Ot:In.defaults(Ot.Object(),t,In.pick(Ot,kt))).Array,r=t.Date,Je=t.Error,Qe=t.Function,et=t.Math,tt=t.Object,nt=t.RegExp,it=t.String,rt=t.TypeError,ot=i.prototype,st=Qe.prototype,at=tt.prototype,lt=t["__core-js_shared__"],ut=st.toString,ct=at.hasOwnProperty,ht=0,dt=(n=/[^.]+$/.exec(lt&<.keys&<.keys.IE_PROTO||""))?"Symbol(src)_1."+n:"",ft=at.toString,pt=ut.call(tt),mt=Ot._,vt=nt("^"+ut.call(ct).replace(Pe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),gt=Ft?t.Buffer:o,_t=t.Symbol,xt=t.Uint8Array,$t=gt?gt.allocUnsafe:o,Nt=En(tt.getPrototypeOf,tt),Pt=tt.create,It=at.propertyIsEnumerable,At=ot.splice,Lt=_t?_t.isConcatSpreadable:o,Vt=_t?_t.iterator:o,on=_t?_t.toStringTag:o,fn=function(){try{var e=Bo(tt,"defineProperty");return e({},"",{}),e}catch(e){}}(),An=t.clearTimeout!==Ot.clearTimeout&&t.clearTimeout,Fn=r&&r.now!==Ot.Date.now&&r.now,Ln=t.setTimeout!==Ot.setTimeout&&t.setTimeout,Vn=et.ceil,Bn=et.floor,zn=tt.getOwnPropertySymbols,Rn=gt?gt.isBuffer:o,Hn=t.isFinite,jn=ot.join,Wn=En(tt.keys,tt),qn=et.max,Yn=et.min,Kn=r.now,Un=t.parseInt,Gn=et.random,Xn=ot.reverse,Zn=Bo(t,"DataView"),Jn=Bo(t,"Map"),Qn=Bo(t,"Promise"),ei=Bo(t,"Set"),ti=Bo(t,"WeakMap"),ni=Bo(tt,"create"),ii=ti&&new ti,ri={},oi=hs(Zn),si=hs(Jn),ai=hs(Qn),li=hs(ei),ui=hs(ti),ci=_t?_t.prototype:o,hi=ci?ci.valueOf:o,di=ci?ci.toString:o;function fi(e){if($a(e)&&!ga(e)&&!(e instanceof gi)){if(e instanceof vi)return e;if(ct.call(e,"__wrapped__"))return ds(e)}return new vi(e)}var pi=function(){function e(){}return function(t){if(!Ea(t))return{};if(Pt)return Pt(t);e.prototype=t;var n=new e;return e.prototype=o,n}}();function mi(){}function vi(e,t){this.__wrapped__=e,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=o}function gi(e){this.__wrapped__=e,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=L,this.__views__=[]}function yi(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t=t?e:t)),e}function Ai(e,t,n,i,r,s){var a,l=t&d,u=t&f,c=t&p;if(n&&(a=r?n(e,i,r,s):n(e)),a!==o)return a;if(!Ea(e))return e;var h=ga(e);if(h){if(a=function(e){var t=e.length,n=new e.constructor(t);return t&&"string"==typeof e[0]&&ct.call(e,"index")&&(n.index=e.index,n.input=e.input),n}(e),!l)return no(e,a)}else{var m=Ho(e),v=m==U||m==G;if(wa(e))return Xr(e,l);if(m==Q||m==R||v&&!r){if(a=u||v?{}:Wo(e),!l)return u?function(e,t){return io(e,Ro(e),t)}(e,function(e,t){return e&&io(t,ol(t),e)}(a,e)):function(e,t){return io(e,zo(e),t)}(e,Ni(a,e))}else{if(!Et[m])return r?e:{};a=function(e,t,n){var i,r,o,s=e.constructor;switch(t){case le:return Zr(e);case W:case q:return new s(+e);case ue:return function(e,t){var n=t?Zr(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}(e,n);case ce:case he:case de:case fe:case pe:case me:case ve:case ge:case ye:return Jr(e,n);case X:return new s;case Z:case ie:return new s(e);case te:return(o=new(r=e).constructor(r.source,je.exec(r))).lastIndex=r.lastIndex,o;case ne:return new s;case re:return i=e,hi?tt(hi.call(i)):{}}}(e,m,l)}}s||(s=new xi);var g=s.get(e);if(g)return g;s.set(e,a),Oa(e)?e.forEach(function(i){a.add(Ai(i,t,n,i,e,s))}):Ta(e)&&e.forEach(function(i,r){a.set(r,Ai(i,t,n,r,e,s))});var y=h?o:(c?u?Po:No:u?ol:rl)(e);return Kt(y||e,function(i,r){y&&(i=e[r=i]),$i(a,r,Ai(i,t,n,r,e,s))}),a}function Fi(e,t,n){var i=n.length;if(null==e)return!i;for(e=tt(e);i--;){var r=n[i],s=t[r],a=e[r];if(a===o&&!(r in e)||!s(a))return!1}return!0}function Li(e,t,n){if("function"!=typeof e)throw new rt(l);return rs(function(){e.apply(o,n)},t)}function Vi(e,t,n,i){var r=-1,o=Zt,a=!0,l=e.length,u=[],c=t.length;if(!l)return u;n&&(t=Qt(t,gn(n))),i?(o=Jt,a=!1):t.length>=s&&(o=bn,a=!1,t=new wi(t));e:for(;++r-1},bi.prototype.set=function(e,t){var n=this.__data__,i=Ti(n,e);return i<0?(++this.size,n.push([e,t])):n[i][1]=t,this},_i.prototype.clear=function(){this.size=0,this.__data__={hash:new yi,map:new(Jn||bi),string:new yi}},_i.prototype.delete=function(e){var t=Lo(this,e).delete(e);return this.size-=t?1:0,t},_i.prototype.get=function(e){return Lo(this,e).get(e)},_i.prototype.has=function(e){return Lo(this,e).has(e)},_i.prototype.set=function(e,t){var n=Lo(this,e),i=n.size;return n.set(e,t),this.size+=n.size==i?0:1,this},wi.prototype.add=wi.prototype.push=function(e){return this.__data__.set(e,u),this},wi.prototype.has=function(e){return this.__data__.has(e)},xi.prototype.clear=function(){this.__data__=new bi,this.size=0},xi.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},xi.prototype.get=function(e){return this.__data__.get(e)},xi.prototype.has=function(e){return this.__data__.has(e)},xi.prototype.set=function(e,t){var n=this.__data__;if(n instanceof bi){var i=n.__data__;if(!Jn||i.length0&&n(a)?t>1?Wi(a,t-1,n,i,r):en(r,a):i||(r[r.length]=a)}return r}var qi=ao(),Yi=ao(!0);function Ki(e,t){return e&&qi(e,t,rl)}function Ui(e,t){return e&&Yi(e,t,rl)}function Gi(e,t){return Xt(t,function(t){return ka(e[t])})}function Xi(e,t){for(var n=0,i=(t=Yr(t,e)).length;null!=e&&nt}function er(e,t){return null!=e&&ct.call(e,t)}function tr(e,t){return null!=e&&t in tt(e)}function nr(e,t,n){for(var r=n?Jt:Zt,s=e[0].length,a=e.length,l=a,u=i(a),c=1/0,h=[];l--;){var d=e[l];l&&t&&(d=Qt(d,gn(t))),c=Yn(d.length,c),u[l]=!n&&(t||s>=120&&d.length>=120)?new wi(l&&d):o}d=e[0];var f=-1,p=u[0];e:for(;++f=a)return l;var u=n[i];return l*("desc"==u?-1:1)}}return e.index-t.index}(e,t,n)})}function yr(e,t,n){for(var i=-1,r=t.length,o={};++i-1;)a!==e&&At.call(a,l,1),At.call(e,l,1);return e}function _r(e,t){for(var n=e?t.length:0,i=n-1;n--;){var r=t[n];if(n==i||r!==o){var o=r;Yo(r)?At.call(e,r,1):Vr(e,r)}}return e}function wr(e,t){return e+Bn(Gn()*(t-e+1))}function xr(e,t){var n="";if(!e||t<1||t>I)return n;do{t%2&&(n+=e),(t=Bn(t/2))&&(e+=e)}while(t);return n}function Cr(e,t){return os(es(e,t,Ml),e+"")}function kr(e){return ki(fl(e))}function Sr(e,t){var n=fl(e);return ls(n,Ii(t,0,n.length))}function Dr(e,t,n,i){if(!Ea(e))return e;for(var r=-1,s=(t=Yr(t,e)).length,a=s-1,l=e;null!=l&&++ro?0:o+t),(n=n>o?o:n)<0&&(n+=o),o=t>n?0:n-t>>>0,t>>>=0;for(var s=i(o);++r>>1,s=e[o];null!==s&&!Aa(s)&&(n?s<=t:s=s){var c=t?null:Co(e);if(c)return Tn(c);a=!1,r=bn,u=new wi}else u=t?[]:l;e:for(;++i=i?e:Mr(e,t,n)}var Gr=An||function(e){return Ot.clearTimeout(e)};function Xr(e,t){if(t)return e.slice();var n=e.length,i=$t?$t(n):new e.constructor(n);return e.copy(i),i}function Zr(e){var t=new e.constructor(e.byteLength);return new xt(t).set(new xt(e)),t}function Jr(e,t){var n=t?Zr(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.length)}function Qr(e,t){if(e!==t){var n=e!==o,i=null===e,r=e==e,s=Aa(e),a=t!==o,l=null===t,u=t==t,c=Aa(t);if(!l&&!c&&!s&&e>t||s&&a&&u&&!l&&!c||i&&a&&u||!n&&u||!r)return 1;if(!i&&!s&&!c&&e1?n[r-1]:o,a=r>2?n[2]:o;for(s=e.length>3&&"function"==typeof s?(r--,s):o,a&&Ko(n[0],n[1],a)&&(s=r<3?o:s,r=1),t=tt(t);++i-1?r[s?t[a]:a]:o}}function fo(e){return Mo(function(t){var n=t.length,i=n,r=vi.prototype.thru;for(e&&t.reverse();i--;){var s=t[i];if("function"!=typeof s)throw new rt(l);if(r&&!a&&"wrapper"==Io(s))var a=new vi([],!0)}for(i=a?i:n;++i1&&_.reverse(),d&&cl))return!1;var c=s.get(e);if(c&&s.get(t))return c==t;var h=-1,d=!0,f=n&v?new wi:o;for(s.set(e,t),s.set(t,e);++h-1&&e%1==0&&e1?"& ":"")+t[i],t=t.join(n>2?", ":" "),e.replace(Le,"{\n/* [wrapped with "+t+"] */\n")}(i,function(e,t){return Kt(z,function(n){var i="_."+n[0];t&n[1]&&!Zt(e,i)&&e.push(i)}),e.sort()}(function(e){var t=e.match(Ve);return t?t[1].split(Be):[]}(i),n)))}function as(e){var t=0,n=0;return function(){var i=Kn(),r=M-(i-n);if(n=i,r>0){if(++t>=T)return arguments[0]}else t=0;return e.apply(o,arguments)}}function ls(e,t){var n=-1,i=e.length,r=i-1;for(t=t===o?i:t;++n1?e[t-1]:o;return n="function"==typeof n?(e.pop(),n):o,Ps(e,n)});function Bs(e){var t=fi(e);return t.__chain__=!0,t}function zs(e,t){return t(e)}var Rs=Mo(function(e){var t=e.length,n=t?e[0]:0,i=this.__wrapped__,r=function(t){return Oi(t,e)};return!(t>1||this.__actions__.length)&&i instanceof gi&&Yo(n)?((i=i.slice(n,+n+(t?1:0))).__actions__.push({func:zs,args:[r],thisArg:o}),new vi(i,this.__chain__).thru(function(e){return t&&!e.length&&e.push(o),e})):this.thru(r)});var Hs=ro(function(e,t,n){ct.call(e,n)?++e[n]:Pi(e,n,1)});var js=ho(vs),Ws=ho(gs);function qs(e,t){return(ga(e)?Kt:Bi)(e,Fo(t,3))}function Ys(e,t){return(ga(e)?Ut:zi)(e,Fo(t,3))}var Ks=ro(function(e,t,n){ct.call(e,n)?e[n].push(t):Pi(e,n,[t])});var Us=Cr(function(e,t,n){var r=-1,o="function"==typeof t,s=ba(e)?i(e.length):[];return Bi(e,function(e){s[++r]=o?qt(t,e,n):ir(e,t,n)}),s}),Gs=ro(function(e,t,n){Pi(e,n,t)});function Xs(e,t){return(ga(e)?Qt:dr)(e,Fo(t,3))}var Zs=ro(function(e,t,n){e[n?0:1].push(t)},function(){return[[],[]]});var Js=Cr(function(e,t){if(null==e)return[];var n=t.length;return n>1&&Ko(e,t[0],t[1])?t=[]:n>2&&Ko(t[0],t[1],t[2])&&(t=[t[0]]),gr(e,Wi(t,1),[])}),Qs=Fn||function(){return Ot.Date.now()};function ea(e,t,n){return t=n?o:t,t=e&&null==t?e.length:t,So(e,k,o,o,o,o,t)}function ta(e,t){var n;if("function"!=typeof t)throw new rt(l);return e=Ra(e),function(){return--e>0&&(n=t.apply(this,arguments)),e<=1&&(t=o),n}}var na=Cr(function(e,t,n){var i=g;if(n.length){var r=$n(n,Ao(na));i|=x}return So(e,i,t,n,r)}),ia=Cr(function(e,t,n){var i=g|y;if(n.length){var r=$n(n,Ao(ia));i|=x}return So(t,i,e,n,r)});function ra(e,t,n){var i,r,s,a,u,c,h=0,d=!1,f=!1,p=!0;if("function"!=typeof e)throw new rt(l);function m(t){var n=i,s=r;return i=r=o,h=t,a=e.apply(s,n)}function v(e){var n=e-c;return c===o||n>=t||n<0||f&&e-h>=s}function g(){var e=Qs();if(v(e))return y(e);u=rs(g,function(e){var n=t-(e-c);return f?Yn(n,s-(e-h)):n}(e))}function y(e){return u=o,p&&i?m(e):(i=r=o,a)}function b(){var e=Qs(),n=v(e);if(i=arguments,r=this,c=e,n){if(u===o)return function(e){return h=e,u=rs(g,t),d?m(e):a}(c);if(f)return Gr(u),u=rs(g,t),m(c)}return u===o&&(u=rs(g,t)),a}return t=ja(t)||0,Ea(n)&&(d=!!n.leading,s=(f="maxWait"in n)?qn(ja(n.maxWait)||0,t):s,p="trailing"in n?!!n.trailing:p),b.cancel=function(){u!==o&&Gr(u),h=0,i=c=r=u=o},b.flush=function(){return u===o?a:y(Qs())},b}var oa=Cr(function(e,t){return Li(e,1,t)}),sa=Cr(function(e,t,n){return Li(e,ja(t)||0,n)});function aa(e,t){if("function"!=typeof e||null!=t&&"function"!=typeof t)throw new rt(l);var n=function(){var i=arguments,r=t?t.apply(this,i):i[0],o=n.cache;if(o.has(r))return o.get(r);var s=e.apply(this,i);return n.cache=o.set(r,s)||o,s};return n.cache=new(aa.Cache||_i),n}function la(e){if("function"!=typeof e)throw new rt(l);return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}aa.Cache=_i;var ua=Kr(function(e,t){var n=(t=1==t.length&&ga(t[0])?Qt(t[0],gn(Fo())):Qt(Wi(t,1),gn(Fo()))).length;return Cr(function(i){for(var r=-1,o=Yn(i.length,n);++r=t}),va=rr(function(){return arguments}())?rr:function(e){return $a(e)&&ct.call(e,"callee")&&!It.call(e,"callee")},ga=i.isArray,ya=Bt?gn(Bt):function(e){return $a(e)&&Ji(e)==le};function ba(e){return null!=e&&Da(e.length)&&!ka(e)}function _a(e){return $a(e)&&ba(e)}var wa=Rn||jl,xa=zt?gn(zt):function(e){return $a(e)&&Ji(e)==q};function Ca(e){if(!$a(e))return!1;var t=Ji(e);return t==K||t==Y||"string"==typeof e.message&&"string"==typeof e.name&&!Na(e)}function ka(e){if(!Ea(e))return!1;var t=Ji(e);return t==U||t==G||t==j||t==ee}function Sa(e){return"number"==typeof e&&e==Ra(e)}function Da(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=I}function Ea(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function $a(e){return null!=e&&"object"==typeof e}var Ta=Rt?gn(Rt):function(e){return $a(e)&&Ho(e)==X};function Ma(e){return"number"==typeof e||$a(e)&&Ji(e)==Z}function Na(e){if(!$a(e)||Ji(e)!=Q)return!1;var t=Nt(e);if(null===t)return!0;var n=ct.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&ut.call(n)==pt}var Pa=Ht?gn(Ht):function(e){return $a(e)&&Ji(e)==te};var Oa=jt?gn(jt):function(e){return $a(e)&&Ho(e)==ne};function Ia(e){return"string"==typeof e||!ga(e)&&$a(e)&&Ji(e)==ie}function Aa(e){return"symbol"==typeof e||$a(e)&&Ji(e)==re}var Fa=Wt?gn(Wt):function(e){return $a(e)&&Da(e.length)&&!!Dt[Ji(e)]};var La=_o(hr),Va=_o(function(e,t){return e<=t});function Ba(e){if(!e)return[];if(ba(e))return Ia(e)?Pn(e):no(e);if(Vt&&e[Vt])return function(e){for(var t,n=[];!(t=e.next()).done;)n.push(t.value);return n}(e[Vt]());var t=Ho(e);return(t==X?Dn:t==ne?Tn:fl)(e)}function za(e){return e?(e=ja(e))===O||e===-O?(e<0?-1:1)*A:e==e?e:0:0===e?e:0}function Ra(e){var t=za(e),n=t%1;return t==t?n?t-n:t:0}function Ha(e){return e?Ii(Ra(e),0,L):0}function ja(e){if("number"==typeof e)return e;if(Aa(e))return F;if(Ea(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=Ea(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(Ie,"");var n=qe.test(e);return n||Ke.test(e)?Mt(e.slice(2),n?2:8):We.test(e)?F:+e}function Wa(e){return io(e,ol(e))}function qa(e){return null==e?"":Fr(e)}var Ya=oo(function(e,t){if(Zo(t)||ba(t))io(t,rl(t),e);else for(var n in t)ct.call(t,n)&&$i(e,n,t[n])}),Ka=oo(function(e,t){io(t,ol(t),e)}),Ua=oo(function(e,t,n,i){io(t,ol(t),e,i)}),Ga=oo(function(e,t,n,i){io(t,rl(t),e,i)}),Xa=Mo(Oi);var Za=Cr(function(e,t){e=tt(e);var n=-1,i=t.length,r=i>2?t[2]:o;for(r&&Ko(t[0],t[1],r)&&(i=1);++n1),t}),io(e,Po(e),n),i&&(n=Ai(n,d|f|p,$o));for(var r=t.length;r--;)Vr(n,t[r]);return n});var ul=Mo(function(e,t){return null==e?{}:function(e,t){return yr(e,t,function(t,n){return el(e,n)})}(e,t)});function cl(e,t){if(null==e)return{};var n=Qt(Po(e),function(e){return[e]});return t=Fo(t),yr(e,n,function(e,n){return t(e,n[0])})}var hl=ko(rl),dl=ko(ol);function fl(e){return null==e?[]:yn(e,rl(e))}var pl=uo(function(e,t,n){return t=t.toLowerCase(),e+(n?ml(t):t)});function ml(e){return Cl(qa(e).toLowerCase())}function vl(e){return(e=qa(e))&&e.replace(Ge,xn).replace(bt,"")}var gl=uo(function(e,t,n){return e+(n?"-":"")+t.toLowerCase()}),yl=uo(function(e,t,n){return e+(n?" ":"")+t.toLowerCase()}),bl=lo("toLowerCase");var _l=uo(function(e,t,n){return e+(n?"_":"")+t.toLowerCase()});var wl=uo(function(e,t,n){return e+(n?" ":"")+Cl(t)});var xl=uo(function(e,t,n){return e+(n?" ":"")+t.toUpperCase()}),Cl=lo("toUpperCase");function kl(e,t,n){return e=qa(e),(t=n?o:t)===o?function(e){return Ct.test(e)}(e)?function(e){return e.match(wt)||[]}(e):function(e){return e.match(ze)||[]}(e):e.match(t)||[]}var Sl=Cr(function(e,t){try{return qt(e,o,t)}catch(e){return Ca(e)?e:new Je(e)}}),Dl=Mo(function(e,t){return Kt(t,function(t){t=cs(t),Pi(e,t,na(e[t],e))}),e});function El(e){return function(){return e}}var $l=fo(),Tl=fo(!0);function Ml(e){return e}function Nl(e){return lr("function"==typeof e?e:Ai(e,d))}var Pl=Cr(function(e,t){return function(n){return ir(n,e,t)}}),Ol=Cr(function(e,t){return function(n){return ir(e,n,t)}});function Il(e,t,n){var i=rl(t),r=Gi(t,i);null!=n||Ea(t)&&(r.length||!i.length)||(n=t,t=e,e=this,r=Gi(t,rl(t)));var o=!(Ea(n)&&"chain"in n&&!n.chain),s=ka(e);return Kt(r,function(n){var i=t[n];e[n]=i,s&&(e.prototype[n]=function(){var t=this.__chain__;if(o||t){var n=e(this.__wrapped__);return(n.__actions__=no(this.__actions__)).push({func:i,args:arguments,thisArg:e}),n.__chain__=t,n}return i.apply(e,en([this.value()],arguments))})}),e}function Al(){}var Fl=go(Qt),Ll=go(Gt),Vl=go(rn);function Bl(e){return Uo(e)?dn(cs(e)):function(e){return function(t){return Xi(t,e)}}(e)}var zl=bo(),Rl=bo(!0);function Hl(){return[]}function jl(){return!1}var Wl=vo(function(e,t){return e+t},0),ql=xo("ceil"),Yl=vo(function(e,t){return e/t},1),Kl=xo("floor");var Ul,Gl=vo(function(e,t){return e*t},1),Xl=xo("round"),Zl=vo(function(e,t){return e-t},0);return fi.after=function(e,t){if("function"!=typeof t)throw new rt(l);return e=Ra(e),function(){if(--e<1)return t.apply(this,arguments)}},fi.ary=ea,fi.assign=Ya,fi.assignIn=Ka,fi.assignInWith=Ua,fi.assignWith=Ga,fi.at=Xa,fi.before=ta,fi.bind=na,fi.bindAll=Dl,fi.bindKey=ia,fi.castArray=function(){if(!arguments.length)return[];var e=arguments[0];return ga(e)?e:[e]},fi.chain=Bs,fi.chunk=function(e,t,n){t=(n?Ko(e,t,n):t===o)?1:qn(Ra(t),0);var r=null==e?0:e.length;if(!r||t<1)return[];for(var s=0,a=0,l=i(Vn(r/t));sr?0:r+n),(i=i===o||i>r?r:Ra(i))<0&&(i+=r),i=n>i?0:Ha(i);n>>0)?(e=qa(e))&&("string"==typeof t||null!=t&&!Pa(t))&&!(t=Fr(t))&&Sn(e)?Ur(Pn(e),0,n):e.split(t,n):[]},fi.spread=function(e,t){if("function"!=typeof e)throw new rt(l);return t=null==t?0:qn(Ra(t),0),Cr(function(n){var i=n[t],r=Ur(n,0,t);return i&&en(r,i),qt(e,this,r)})},fi.tail=function(e){var t=null==e?0:e.length;return t?Mr(e,1,t):[]},fi.take=function(e,t,n){return e&&e.length?Mr(e,0,(t=n||t===o?1:Ra(t))<0?0:t):[]},fi.takeRight=function(e,t,n){var i=null==e?0:e.length;return i?Mr(e,(t=i-(t=n||t===o?1:Ra(t)))<0?0:t,i):[]},fi.takeRightWhile=function(e,t){return e&&e.length?zr(e,Fo(t,3),!1,!0):[]},fi.takeWhile=function(e,t){return e&&e.length?zr(e,Fo(t,3)):[]},fi.tap=function(e,t){return t(e),e},fi.throttle=function(e,t,n){var i=!0,r=!0;if("function"!=typeof e)throw new rt(l);return Ea(n)&&(i="leading"in n?!!n.leading:i,r="trailing"in n?!!n.trailing:r),ra(e,t,{leading:i,maxWait:t,trailing:r})},fi.thru=zs,fi.toArray=Ba,fi.toPairs=hl,fi.toPairsIn=dl,fi.toPath=function(e){return ga(e)?Qt(e,cs):Aa(e)?[e]:no(us(qa(e)))},fi.toPlainObject=Wa,fi.transform=function(e,t,n){var i=ga(e),r=i||wa(e)||Fa(e);if(t=Fo(t,4),null==n){var o=e&&e.constructor;n=r?i?new o:[]:Ea(e)&&ka(o)?pi(Nt(e)):{}}return(r?Kt:Ki)(e,function(e,i,r){return t(n,e,i,r)}),n},fi.unary=function(e){return ea(e,1)},fi.union=$s,fi.unionBy=Ts,fi.unionWith=Ms,fi.uniq=function(e){return e&&e.length?Lr(e):[]},fi.uniqBy=function(e,t){return e&&e.length?Lr(e,Fo(t,2)):[]},fi.uniqWith=function(e,t){return t="function"==typeof t?t:o,e&&e.length?Lr(e,o,t):[]},fi.unset=function(e,t){return null==e||Vr(e,t)},fi.unzip=Ns,fi.unzipWith=Ps,fi.update=function(e,t,n){return null==e?e:Br(e,t,qr(n))},fi.updateWith=function(e,t,n,i){return i="function"==typeof i?i:o,null==e?e:Br(e,t,qr(n),i)},fi.values=fl,fi.valuesIn=function(e){return null==e?[]:yn(e,ol(e))},fi.without=Os,fi.words=kl,fi.wrap=function(e,t){return ca(qr(t),e)},fi.xor=Is,fi.xorBy=As,fi.xorWith=Fs,fi.zip=Ls,fi.zipObject=function(e,t){return jr(e||[],t||[],$i)},fi.zipObjectDeep=function(e,t){return jr(e||[],t||[],Dr)},fi.zipWith=Vs,fi.entries=hl,fi.entriesIn=dl,fi.extend=Ka,fi.extendWith=Ua,Il(fi,fi),fi.add=Wl,fi.attempt=Sl,fi.camelCase=pl,fi.capitalize=ml,fi.ceil=ql,fi.clamp=function(e,t,n){return n===o&&(n=t,t=o),n!==o&&(n=(n=ja(n))==n?n:0),t!==o&&(t=(t=ja(t))==t?t:0),Ii(ja(e),t,n)},fi.clone=function(e){return Ai(e,p)},fi.cloneDeep=function(e){return Ai(e,d|p)},fi.cloneDeepWith=function(e,t){return Ai(e,d|p,t="function"==typeof t?t:o)},fi.cloneWith=function(e,t){return Ai(e,p,t="function"==typeof t?t:o)},fi.conformsTo=function(e,t){return null==t||Fi(e,t,rl(t))},fi.deburr=vl,fi.defaultTo=function(e,t){return null==e||e!=e?t:e},fi.divide=Yl,fi.endsWith=function(e,t,n){e=qa(e),t=Fr(t);var i=e.length,r=n=n===o?i:Ii(Ra(n),0,i);return(n-=t.length)>=0&&e.slice(n,r)==t},fi.eq=fa,fi.escape=function(e){return(e=qa(e))&&Se.test(e)?e.replace(Ce,Cn):e},fi.escapeRegExp=function(e){return(e=qa(e))&&Oe.test(e)?e.replace(Pe,"\\$&"):e},fi.every=function(e,t,n){var i=ga(e)?Gt:Ri;return n&&Ko(e,t,n)&&(t=o),i(e,Fo(t,3))},fi.find=js,fi.findIndex=vs,fi.findKey=function(e,t){return sn(e,Fo(t,3),Ki)},fi.findLast=Ws,fi.findLastIndex=gs,fi.findLastKey=function(e,t){return sn(e,Fo(t,3),Ui)},fi.floor=Kl,fi.forEach=qs,fi.forEachRight=Ys,fi.forIn=function(e,t){return null==e?e:qi(e,Fo(t,3),ol)},fi.forInRight=function(e,t){return null==e?e:Yi(e,Fo(t,3),ol)},fi.forOwn=function(e,t){return e&&Ki(e,Fo(t,3))},fi.forOwnRight=function(e,t){return e&&Ui(e,Fo(t,3))},fi.get=Qa,fi.gt=pa,fi.gte=ma,fi.has=function(e,t){return null!=e&&jo(e,t,er)},fi.hasIn=el,fi.head=bs,fi.identity=Ml,fi.includes=function(e,t,n,i){e=ba(e)?e:fl(e),n=n&&!i?Ra(n):0;var r=e.length;return n<0&&(n=qn(r+n,0)),Ia(e)?n<=r&&e.indexOf(t,n)>-1:!!r&&ln(e,t,n)>-1},fi.indexOf=function(e,t,n){var i=null==e?0:e.length;if(!i)return-1;var r=null==n?0:Ra(n);return r<0&&(r=qn(i+r,0)),ln(e,t,r)},fi.inRange=function(e,t,n){return t=za(t),n===o?(n=t,t=0):n=za(n),function(e,t,n){return e>=Yn(t,n)&&e=-I&&e<=I},fi.isSet=Oa,fi.isString=Ia,fi.isSymbol=Aa,fi.isTypedArray=Fa,fi.isUndefined=function(e){return e===o},fi.isWeakMap=function(e){return $a(e)&&Ho(e)==se},fi.isWeakSet=function(e){return $a(e)&&Ji(e)==ae},fi.join=function(e,t){return null==e?"":jn.call(e,t)},fi.kebabCase=gl,fi.last=Cs,fi.lastIndexOf=function(e,t,n){var i=null==e?0:e.length;if(!i)return-1;var r=i;return n!==o&&(r=(r=Ra(n))<0?qn(i+r,0):Yn(r,i-1)),t==t?function(e,t,n){for(var i=n+1;i--;)if(e[i]===t)return i;return i}(e,t,r):an(e,cn,r,!0)},fi.lowerCase=yl,fi.lowerFirst=bl,fi.lt=La,fi.lte=Va,fi.max=function(e){return e&&e.length?Hi(e,Ml,Qi):o},fi.maxBy=function(e,t){return e&&e.length?Hi(e,Fo(t,2),Qi):o},fi.mean=function(e){return hn(e,Ml)},fi.meanBy=function(e,t){return hn(e,Fo(t,2))},fi.min=function(e){return e&&e.length?Hi(e,Ml,hr):o},fi.minBy=function(e,t){return e&&e.length?Hi(e,Fo(t,2),hr):o},fi.stubArray=Hl,fi.stubFalse=jl,fi.stubObject=function(){return{}},fi.stubString=function(){return""},fi.stubTrue=function(){return!0},fi.multiply=Gl,fi.nth=function(e,t){return e&&e.length?vr(e,Ra(t)):o},fi.noConflict=function(){return Ot._===this&&(Ot._=mt),this},fi.noop=Al,fi.now=Qs,fi.pad=function(e,t,n){e=qa(e);var i=(t=Ra(t))?Nn(e):0;if(!t||i>=t)return e;var r=(t-i)/2;return yo(Bn(r),n)+e+yo(Vn(r),n)},fi.padEnd=function(e,t,n){e=qa(e);var i=(t=Ra(t))?Nn(e):0;return t&&it){var i=e;e=t,t=i}if(n||e%1||t%1){var r=Gn();return Yn(e+r*(t-e+Tt("1e-"+((r+"").length-1))),t)}return wr(e,t)},fi.reduce=function(e,t,n){var i=ga(e)?tn:pn,r=arguments.length<3;return i(e,Fo(t,4),n,r,Bi)},fi.reduceRight=function(e,t,n){var i=ga(e)?nn:pn,r=arguments.length<3;return i(e,Fo(t,4),n,r,zi)},fi.repeat=function(e,t,n){return t=(n?Ko(e,t,n):t===o)?1:Ra(t),xr(qa(e),t)},fi.replace=function(){var e=arguments,t=qa(e[0]);return e.length<3?t:t.replace(e[1],e[2])},fi.result=function(e,t,n){var i=-1,r=(t=Yr(t,e)).length;for(r||(r=1,e=o);++iI)return[];var n=L,i=Yn(e,L);t=Fo(t),e-=L;for(var r=vn(i,t);++n=s)return e;var l=n-Nn(i);if(l<1)return i;var u=a?Ur(a,0,l).join(""):e.slice(0,l);if(r===o)return u+i;if(a&&(l+=u.length-l),Pa(r)){if(e.slice(l).search(r)){var c,h=u;for(r.global||(r=nt(r.source,qa(je.exec(r))+"g")),r.lastIndex=0;c=r.exec(h);)var d=c.index;u=u.slice(0,d===o?l:d)}}else if(e.indexOf(Fr(r),l)!=l){var f=u.lastIndexOf(r);f>-1&&(u=u.slice(0,f))}return u+i},fi.unescape=function(e){return(e=qa(e))&&ke.test(e)?e.replace(xe,On):e},fi.uniqueId=function(e){var t=++ht;return qa(e)+t},fi.upperCase=xl,fi.upperFirst=Cl,fi.each=qs,fi.eachRight=Ys,fi.first=bs,Il(fi,(Ul={},Ki(fi,function(e,t){ct.call(fi.prototype,t)||(Ul[t]=e)}),Ul),{chain:!1}),fi.VERSION="4.17.14",Kt(["bind","bindKey","curry","curryRight","partial","partialRight"],function(e){fi[e].placeholder=fi}),Kt(["drop","take"],function(e,t){gi.prototype[e]=function(n){n=n===o?1:qn(Ra(n),0);var i=this.__filtered__&&!t?new gi(this):this.clone();return i.__filtered__?i.__takeCount__=Yn(n,i.__takeCount__):i.__views__.push({size:Yn(n,L),type:e+(i.__dir__<0?"Right":"")}),i},gi.prototype[e+"Right"]=function(t){return this.reverse()[e](t).reverse()}}),Kt(["filter","map","takeWhile"],function(e,t){var n=t+1,i=n==N||3==n;gi.prototype[e]=function(e){var t=this.clone();return t.__iteratees__.push({iteratee:Fo(e,3),type:n}),t.__filtered__=t.__filtered__||i,t}}),Kt(["head","last"],function(e,t){var n="take"+(t?"Right":"");gi.prototype[e]=function(){return this[n](1).value()[0]}}),Kt(["initial","tail"],function(e,t){var n="drop"+(t?"":"Right");gi.prototype[e]=function(){return this.__filtered__?new gi(this):this[n](1)}}),gi.prototype.compact=function(){return this.filter(Ml)},gi.prototype.find=function(e){return this.filter(e).head()},gi.prototype.findLast=function(e){return this.reverse().find(e)},gi.prototype.invokeMap=Cr(function(e,t){return"function"==typeof e?new gi(this):this.map(function(n){return ir(n,e,t)})}),gi.prototype.reject=function(e){return this.filter(la(Fo(e)))},gi.prototype.slice=function(e,t){e=Ra(e);var n=this;return n.__filtered__&&(e>0||t<0)?new gi(n):(e<0?n=n.takeRight(-e):e&&(n=n.drop(e)),t!==o&&(n=(t=Ra(t))<0?n.dropRight(-t):n.take(t-e)),n)},gi.prototype.takeRightWhile=function(e){return this.reverse().takeWhile(e).reverse()},gi.prototype.toArray=function(){return this.take(L)},Ki(gi.prototype,function(e,t){var n=/^(?:filter|find|map|reject)|While$/.test(t),i=/^(?:head|last)$/.test(t),r=fi[i?"take"+("last"==t?"Right":""):t],s=i||/^find/.test(t);r&&(fi.prototype[t]=function(){var t=this.__wrapped__,a=i?[1]:arguments,l=t instanceof gi,u=a[0],c=l||ga(t),h=function(e){var t=r.apply(fi,en([e],a));return i&&d?t[0]:t};c&&n&&"function"==typeof u&&1!=u.length&&(l=c=!1);var d=this.__chain__,f=!!this.__actions__.length,p=s&&!d,m=l&&!f;if(!s&&c){t=m?t:new gi(this);var v=e.apply(t,a);return v.__actions__.push({func:zs,args:[h],thisArg:o}),new vi(v,d)}return p&&m?e.apply(this,a):(v=this.thru(h),p?i?v.value()[0]:v.value():v)})}),Kt(["pop","push","shift","sort","splice","unshift"],function(e){var t=ot[e],n=/^(?:push|sort|unshift)$/.test(e)?"tap":"thru",i=/^(?:pop|shift)$/.test(e);fi.prototype[e]=function(){var e=arguments;if(i&&!this.__chain__){var r=this.value();return t.apply(ga(r)?r:[],e)}return this[n](function(n){return t.apply(ga(n)?n:[],e)})}}),Ki(gi.prototype,function(e,t){var n=fi[t];if(n){var i=n.name+"";ct.call(ri,i)||(ri[i]=[]),ri[i].push({name:t,func:n})}}),ri[po(o,y).name]=[{name:"wrapper",func:o}],gi.prototype.clone=function(){var e=new gi(this.__wrapped__);return e.__actions__=no(this.__actions__),e.__dir__=this.__dir__,e.__filtered__=this.__filtered__,e.__iteratees__=no(this.__iteratees__),e.__takeCount__=this.__takeCount__,e.__views__=no(this.__views__),e},gi.prototype.reverse=function(){if(this.__filtered__){var e=new gi(this);e.__dir__=-1,e.__filtered__=!0}else(e=this.clone()).__dir__*=-1;return e},gi.prototype.value=function(){var e=this.__wrapped__.value(),t=this.__dir__,n=ga(e),i=t<0,r=n?e.length:0,o=function(e,t,n){for(var i=-1,r=n.length;++i=this.__values__.length;return{done:e,value:e?o:this.__values__[this.__index__++]}},fi.prototype.plant=function(e){for(var t,n=this;n instanceof mi;){var i=ds(n);i.__index__=0,i.__values__=o,t?r.__wrapped__=i:t=i;var r=i;n=n.__wrapped__}return r.__wrapped__=e,t},fi.prototype.reverse=function(){var e=this.__wrapped__;if(e instanceof gi){var t=e;return this.__actions__.length&&(t=new gi(this)),(t=t.reverse()).__actions__.push({func:zs,args:[Es],thisArg:o}),new vi(t,this.__chain__)}return this.thru(Es)},fi.prototype.toJSON=fi.prototype.valueOf=fi.prototype.value=function(){return Rr(this.__wrapped__,this.__actions__)},fi.prototype.first=fi.prototype.head,Vt&&(fi.prototype[Vt]=function(){return this}),fi}();Ot._=In,(r=function(){return In}.call(t,n,t,i))===o||(i.exports=r)}).call(this)}).call(this,n(37),n(89)(e))},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t){var n=/^(attrs|props|on|nativeOn|class|style|hook)$/;function i(e,t){return function(){e&&e.apply(this,arguments),t&&t.apply(this,arguments)}}e.exports=function(e){return e.reduce(function(e,t){var r,o,s,a,l;for(s in t)if(r=e[s],o=t[s],r&&n.test(s))if("class"===s&&("string"==typeof r&&(l=r,e[s]=r={},r[l]=!0),"string"==typeof o&&(l=o,t[s]=o={},o[l]=!0)),"on"===s||"nativeOn"===s||"hook"===s)for(a in o)r[a]=i(r[a],o[a]);else if(Array.isArray(r))e[s]=r.concat(o);else if(Array.isArray(o))e[s]=[r].concat(o);else for(a in o)r[a]=o[a];else e[s]=t[s];return e},{})}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){"use strict";t.__esModule=!0;var i,r=n(57),o=(i=r)&&i.__esModule?i:{default:i};t.default=o.default||function(e){for(var t=1;t0?i:n)(e)}},function(e,t,n){var i=n(30)("keys"),r=n(23);e.exports=function(e){return i[e]||(i[e]=r(e))}},function(e,t,n){var i=n(15),r=n(6),o=r["__core-js_shared__"]||(r["__core-js_shared__"]={});(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:i.version,mode:n(22)?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t){e.exports={}},function(e,t,n){var i=n(11).f,r=n(8),o=n(14)("toStringTag");e.exports=function(e,t,n){e&&!r(e=n?e:e.prototype,o)&&i(e,o,{configurable:!0,value:t})}},function(e,t,n){t.f=n(14)},function(e,t,n){var i=n(6),r=n(15),o=n(22),s=n(35),a=n(11).f;e.exports=function(e){var t=r.Symbol||(r.Symbol=o?{}:i.Symbol||{});"_"==e.charAt(0)||e in t||a(t,e,{value:s.f(e)})}},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){e.exports=!n(12)&&!n(17)(function(){return 7!=Object.defineProperty(n(39)("div"),"a",{get:function(){return 7}}).a})},function(e,t,n){var i=n(16),r=n(6).document,o=i(r)&&i(r.createElement);e.exports=function(e){return o?r.createElement(e):{}}},function(e,t,n){var i=n(8),r=n(13),o=n(63)(!1),s=n(29)("IE_PROTO");e.exports=function(e,t){var n,a=r(e),l=0,u=[];for(n in a)n!=s&&i(a,n)&&u.push(n);for(;t.length>l;)i(a,n=t[l++])&&(~o(u,n)||u.push(n));return u}},function(e,t,n){var i=n(42);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==i(e)?e.split(""):Object(e)}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t,n){var i=n(27);e.exports=function(e){return Object(i(e))}},function(e,t,n){"use strict";var i=n(22),r=n(25),o=n(45),s=n(10),a=n(33),l=n(70),u=n(34),c=n(73),h=n(14)("iterator"),d=!([].keys&&"next"in[].keys()),f=function(){return this};e.exports=function(e,t,n,p,m,v,g){l(n,t,p);var y,b,_,w=function(e){if(!d&&e in S)return S[e];switch(e){case"keys":case"values":return function(){return new n(this,e)}}return function(){return new n(this,e)}},x=t+" Iterator",C="values"==m,k=!1,S=e.prototype,D=S[h]||S["@@iterator"]||m&&S[m],E=D||w(m),$=m?C?w("entries"):E:void 0,T="Array"==t&&S.entries||D;if(T&&(_=c(T.call(new e)))!==Object.prototype&&_.next&&(u(_,x,!0),i||"function"==typeof _[h]||s(_,h,f)),C&&D&&"values"!==D.name&&(k=!0,E=function(){return D.call(this)}),i&&!g||!d&&!k&&S[h]||s(S,h,E),a[t]=E,a[x]=f,m)if(y={values:C?E:w("values"),keys:v?E:w("keys"),entries:$},g)for(b in y)b in S||o(S,b,y[b]);else r(r.P+r.F*(d||k),t,y);return y}},function(e,t,n){e.exports=n(10)},function(e,t,n){var i=n(19),r=n(71),o=n(31),s=n(29)("IE_PROTO"),a=function(){},l=function(){var e,t=n(39)("iframe"),i=o.length;for(t.style.display="none",n(72).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(" - - - - -
- - - - 分享文件管理 - 系统配置 - - - - - - - - -
-
取件码:${ file.code }
-
文件名:${ file.name }
-
次   数:${ file.count }
-
到   期:${ file.exp_time.slice(0,19) }
-
到   期:永不过期
-
- 内   容:${ file.text } -
-
- 链   接: - 点击下载 -
-
-
- - 删除 - -
-
-
- - -
- - - - -
${menu.name}
-
-
- - -
-
-

FileCodeBox V${config.INSTALL}

-

Github:FileCodeBox

-
-
-
- - -
- - - -
-
- 删除 -
-
- - 新增Banner - - - 更新 - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - 更新 - - -
-
- - - - - - - - - - - - 单位:(bit),1mb=1 * 1024 * 1024 - - - - - - - - 每几分钟 - - 允许错误几次 - - - - 每几分钟 - - 允许上传几次 - - - - 更新 - - -
-
-
-
-
-
-
- - 登录 - -
- -
- - - - - diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 93569b38c..000000000 --- a/templates/index.html +++ /dev/null @@ -1,576 +0,0 @@ - - - - - - - - - - {{title}} - - - - - - - - - -
- - - - - - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - - 7 - - - 8 - - - 9 - - - -
-
-
- - 0 - - - -
-
-
-
-
- - - -
- - - - - - - -
- - - - - - - - - - ${exp_style_dict[uploadData.exp_style]} - - - - 文件 - - - 文本 - - - - - -
将文字、文件拖、粘贴到此处,或点击上传
-
天数<7或限制次数(24h后删除)
-
- - -
- -
- 取件 -
- -
- 我的文件 -
- -
- 存入 -
-
-
-
-
- - -
-
取件码:${ file.code }
-
文件名:${ file.name }
-
-
-
-
- 链   接: - 点击下载 -
-
-
- -
- - - - - - - - ${ file.name } - - - 取件码: -

- ${ file.code } -

-
-
-
- - 二维码 - -
-
-
- -
- - - -