commit f2ae88b4e97fdd24971bb6ed7af6e396ffdef0b2 Author: lim39869-create Date: Mon Sep 28 11:49:29 2026 +0800 PayGateway: epay-compatible Go payment gateway with plugin channels and verifiable ledger diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a2a14ca --- /dev/null +++ b/.env.example @@ -0,0 +1,141 @@ +# PayGateway 配置示例 —— 复制为 .env 后修改 +# cp .env.example .env && vim .env +# +# 配置加载顺序:.env 文件(本文件) → 运行时设置(settings 表,可由管理后台改) +# 同名键后者覆盖前者;密钥类一律只从 .env 读取(不进 settings)。 +# +# ⚠️ 重要:以下密钥更换会导致数据/会话不可恢复 +# - ACCOUNT_MASTER_KEY:accounts.db / upstreams.db 全部密文无法解密 +# - LEDGER_PRIVATE_KEY:旧账本条目验签失败 +# - QR_TOKEN_SECRET:旧二维码套码 token 失效,需重生成 +# - JWT_SECRET:所有登录会话失效(用户被踢下线,重新登录即可,无数据风险) + +# ===== 网关监听 ===== +# 网关业务端口(易支付 /mq / 套码 /qr 等),对外暴露用 +GATEWAY_ADDR=:8080 +# 管理后台端口(Vue3 SPA + REST API + WebSocket) +# 生产建议改为 127.0.0.1:8081 配合 nginx 反代,或不暴露 +ADMIN_ADDR=:8081 + +# ===== 对外地址 ===== +# 网关公网地址(用于向支付平台回调注册等) +# 同机填 http://127.0.0.1:8080;公网填隧道/域名,留空自动推断为 http://127.0.0.1:8080 +EPAY_PUBLIC_BASE_URL=http://127.0.0.1:8080 +# 可选设备事件适配器的服务端可达地址。 +# PERSONAL_BASE_URL= +# 通道回调地址(一般无需手动设;启动时由隧道动态更新) +# ALIPAY_NOTIFY_URL=http://localhost:8080/alipay/notify +# WECHAT_NOTIFY_URL=http://localhost:8080/wechat/notify + +# ===== 上游对接(已废弃 .env 方式!)===== +# 每个上游独立 pid + 密钥,AES-256-GCM 加密存于独立 upstreams.db。 +# 网关不再读取 EPAY_PID / EPAY_KEY,未登记的上游一律拒绝下单。 +# 配置入口:http://127.0.0.1:8081 → 上游对接 → 新增上游 + +# ===== 支付账户密钥(已废弃 .env 方式!)===== +# 全部走管理后台 → 账户池 → 新增账户 配置;AES-256-GCM 加密存 accounts.db +# 以下仅为历史字段注释,均不再读取: +# ALIPAY_APP_ID / ALIPAY_PRIVATE_KEY_FILE / ALIPAY_PUBLIC_KEY_FILE +# WECHAT_APP_ID / WECHAT_MCH_ID / WECHAT_API_KEY / WECHAT_IS_PROD + +# ===== 订单自动关闭(动态关单窗口,docs/PLAN-DYNAMIC-CLOSE-ALLOC.md)===== +# 【商家通道】未支付订单自动关闭时间(分钟,0=不自动关闭)。 +# 商家通道走官方 API、可主动查单、无监听延迟问题 → 固定窗口即可(默认 30min)。 +# 关单前必查单:已支付→补账;查单失败→跳过留待调度器/人工池;确认未支付→才关闭。 +ORDER_CLOSE_MINUTES=30 +# 【个人免签通道】动态窗口由设备负载驱动,clamp 在 [MIN, MAX] 之间(仅影响 personal.* 通道)。 +# 窗口 = clamp(设备一轮耗时(活跃账户数×配额×poll成本)×1.5 + 60s, MIN, MAX) +# - 低负载(1~2 活跃账户)→ 逼近 MIN:确认快,尽早释放池配额 +# - 高负载(多账户积压)→ 逼近 MAX:给设备更多确认时间,防误关已付款订单 +# 0 或 MIN>=MAX → 个人免签退化为 ORDER_CLOSE_MINUTES 静态窗口 +# 注意:窗口只影响「未支付订单」何时关,已付款订单始终秒级入账,不受影响 +# 动态窗口下限(分钟,默认 5):下限锚点 = 用户扫码→支付操作时间(30s~2min) + 一轮完整确认 +ORDER_CLOSE_MIN=5 +# 动态窗口上限(分钟,默认 10):上限锚点 = 用户体验红线;超过转人工异常池升级处理,不是无限挂起 +ORDER_CLOSE_MAX=10 +# 关闭扫描间隔秒数(动态窗口下建议 30s 起,跟随 MAX 窗口粒度) +ORDER_CLOSE_INTERVAL=30 + +# ===== 设备负载均衡(docs/PLAN-DYNAMIC-CLOSE-ALLOC.md §4.5/§5)===== +# 单设备 WAIT_PAY 订单承载上限(0=按公式自动算:(MAX-60s)/(单次poll 4s)≈135) +# 设备超限 → 均衡分配器跳过该设备,新单路由到其他健康设备 +DEVICE_LOAD_CAP=0 +# 设备降权阈值(占 DEVICE_LOAD_CAP 比例,默认 0.8):超过后该设备在分配中降权(缓冲带) +DEVICE_OVERLOAD_RATIO=0.8 +# 单次 poll 成本估算(ms,动态窗口/设备能力计算用;埋点校准后可留 0) +# DEVICE_POLL_COST_MS=4000 + +# ===== 存储 / 日志 ===== +# 业务库 SQLite 路径(订单/异常池/回调队列/设置) +ORDER_DB=data/paygateway.db +# 日志文件路径(slog JSON + 按天分文件:{date} 自动替换为当天日期,如 paygateway-2026-08-18.log) +# 每文件 50MB 封顶、保留 10 份 × 30 天,超过自动清理;日志同时写入独立日志库 data/logs.db(长期保存) +LOG_FILE=logs/paygateway-{date}.log +# 日志级别:debug / info / warn / error +LOG_LEVEL=info + +# ===== 管理后台鉴权 ===== +# 管理员凭据已迁移到主数据库(settings 表,bcrypt 哈希),不再使用环境变量。 +# 首次启动(DB 无管理员时)访问管理后台自动进入注册页;注册后入口自动关闭。 +# 重置管理员:删除 paygateway.db settings 表中 admin_username / admin_password_hash 两行后重启。 +# JWT 签名密钥:留空 = 每次重启随机生成(重启后需重新登录) +# 持久化方法:固定为 64 位十六进制(openssl rand -hex 32);更换会踢所有已登录用户下线 +# JWT_SECRET= + +# ===== 加密 / 签名密钥(前 3 个缺失=服务拒绝启动;后 3 个可空)===== +# 【必备·固定】账户加密主密钥:accounts.db / upstreams.db 全部密文用此加解密 +# 首次部署生成:openssl rand -hex 32 +# ⚠️ 一旦部署,永不能改 —— 改了等于丢掉所有已配置账户/上游密钥 +ACCOUNT_MASTER_KEY=REPLACE_WITH_openssl_rand_hex_32 + +# 【可选·推荐固定】审计账本签名私钥(ECDSA P-256 PEM 格式) +# 留空 = 首次启动自动生成并持久化到 settings 表(重启复用) +# 显式提供可跨实例迁移账本;更换则旧账本条目验签失败 +# LEDGER_PRIVATE_KEY= + +# 【可选·推荐固定】个人免签套码 token 签名密钥 +# 留空 = 首次启动自动生成并持久化到 settings 表(重启复用) +# 更换则旧"用户扫码 → /mq/{token}"链接全部失效,需在管理后台重新生成套码 +# QR_TOKEN_SECRET= + +# ===== 个人免签风控(金额单位均为分)===== +# 单笔限额(默认 50000 = 500 元;0 = 不限) +# PERSONAL_MAX_AMOUNT=50000 +# 大额告警阈值(默认 20000 = 200 元;超此触发 Bark / Webhook 告警) +# PERSONAL_BIG_AMOUNT=20000 +# 单设备每分钟收款推送上限(默认 30;超过则限流,防止监听端异常刷消息) +# PERSONAL_MAX_PER_MIN=30 + +# ===== 告警(RESILIENCE.md P1-3)===== +# Bark 推送 URL(https://api.day.app/),空=禁用 +# ALERT_BARK_URL= +# 通用 Webhook URL(POST JSON:掉线 / 账本异常 / 风控文案检测) +# ALERT_WEBHOOK_URL= + +# ===== 公网接口 IP 限流(防刷 / 防 DoS)===== +# 窗口内单 IP 最大请求数(仅作用于 /submit.php /api.php /qr/refresh /mq/:token,不影响 /alipay/notify /wechat/notify /personal/* 签名回调),0=禁用 +# PUBLIC_RATE_MAX=60 +# 滑动窗口秒数,0=禁用 +# PUBLIC_RATE_WINDOW_SEC=60 + +# ===== 可选二维码适配器 ===== +# 具体模式由授权的业务适配器定义。 +PERSONAL_QR_MODE=direct + +# ===== Cloudflare 命名隧道(固定域名,生产环境推荐)===== +# 非空 = 命名隧道模式(cloudflared tunnel run --config,公网固定域名) +# 空 = 快速隧道(trycloudflare.com 随机域名,仅测试用,重启后域名变化) +CF_TUNNEL_HOSTNAME=your-domain.example.com +# 隧道凭据 JSON 的 base64 编码(含 TunnelID/AccountTag/TunnelSecret) +# 生成方法:base64 -w0 < ~/.cloudflared/paygateway.json +CF_TUNNEL_CREDENTIALS=your_base64_encoded_tunnel_credentials +# Cloudflare 账户邮箱(setup-domain.sh 需要) +CF_EMAIL=your-email@example.com +# Cloudflare Global API Key(setup-domain.sh 自动配置 zone/DNS/SSL) +CF_GLOBAL_KEY=your_global_api_key + +# ===== DNSHe API(de5.net 免费子域名需要;setup-domain.sh 全自动处理)===== +# 脚本能力:子域名不存在时自动注册 + 自动把子域 NS 委派给 Cloudflare +DNSHE_API_KEY=your_dnshe_api_key +DNSHE_API_SECRET=your_dnshe_api_secret +DNSHE_API_BASE=https://api005.dnshe.com diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9054d04 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: build / vet / test / race / fmt + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: gofmt 门禁(未格式化即失败) + run: | + unformatted=$(gofmt -l cmd/ internal/) + if [ -n "$unformatted" ]; then + echo "以下文件未通过 gofmt:" + echo "$unformatted" + exit 1 + fi + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Test(含竞态检测) + run: go test -race ./... -count=1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7b8d41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# ---- 环境与密钥 ---- +.env +*.pem +*.key + +# ---- 运行时数据 / 数据库 ---- +data/ +backup/ +paygateway.db +personal.db +accounts.db +*.db-shm +*.db-wal +logs/ +.gateway.pid +*.pid + +# ---- 构建产物 ---- +/gateway +internal/tunnel/cloudflared +*.test +*.out +vendor/ +__pycache__/ +*.pyc + +# ---- Removed Android listener artifacts ---- +listener-app/ +*.apk +*.aab +*.keystore +*.jks + +# ---- Web 前端 ---- +web/admin/node_modules/ +web/admin/dist/ +web/admin/.vite/ + +# ---- 系统 / IDE ---- +.DS_Store +Thumbs.db +.idea/ +*.iml +.vscode/ +*.swp +*.swo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..869e758 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +Bug reports, design feedback, tests, and documentation improvements are +welcome. Include a reproducible test or explanation when changing order, +ledger, callback, or channel behavior. + +Before opening a pull request, run: + +```bash +make check +``` + +Never include credentials, real payment data, production URLs, device +identifiers, or private logs in a contribution. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7216ead --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PayGateway contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..59be5fd --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +# PayGateway 开发门禁 —— 与 .github/workflows/ci.yml 保持一致 +# 用法: +# make check # 本地全量检查(fmt/build/vet/test/race),提交前必跑 +# make test # 仅单测 +# make race # 竞态检测 + +GO ?= go +PKG ?= ./... + +.PHONY: fmt-check build vet test race check + +# gofmt 门禁:未格式化即失败 +fmt-check: + @unformatted=$$(gofmt -l cmd/ internal/); \ + if [ -n "$$unformatted" ]; then \ + echo "❌ 以下文件未通过 gofmt:"; echo "$$unformatted"; exit 1; \ + fi + @echo "✅ gofmt 通过" + +build: + $(GO) build ./... + +vet: + $(GO) vet ./... + +test: + $(GO) test $(PKG) -count=1 + +race: + $(GO) test -race $(PKG) -count=1 + +# 提交前全量检查(CI 同一套) +check: fmt-check build vet race + @echo "✅ 全部检查通过,可以提交" diff --git a/README.md b/README.md new file mode 100755 index 0000000..a0d83ec --- /dev/null +++ b/README.md @@ -0,0 +1,263 @@ +

+ PayGateway +

+ +

可扩展、可验证、面向授权业务集成的 Go 支付网关

+ +# PayGateway 支付网关 + +一个面向学习和授权业务集成的 Go 支付网关参考实现,兼容易支付(epay)协议,支持插件化商家通道、订单编排、可验证账本、异常对账和弹性调度。仓库同时保留设备自动化相关的通用工程实现,便于研究设备访问、事件处理和故障恢复。 + +> 详细设计见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)(架构)、[`docs/MIANQIAN.md`](docs/MIANQIAN.md)(设备自动化概览)、[`docs/RESILIENCE.md`](docs/RESILIENCE.md)(弹性/对账)和 [`docs/PLAN-DYNAMIC-CLOSE-ALLOC.md`](docs/PLAN-DYNAMIC-CLOSE-ALLOC.md)(动态调度)。 + +## 项目定位 + +`paygateway` 不是一个只展示接口调用的 Demo,而是一个经过真实业务验证的支付与账务基础设施参考实现。项目重点不在于堆叠支付渠道数量,而在于把以下问题放进同一套可审查的工程模型中: + +- 支付通道可插拔,核心订单流程不依赖单一渠道; +- 订单状态、回调、补账和异常处理具备明确的状态边界; +- 资金事件进入可验证的追加式账本,历史记录的静默修改可以被发现; +- 管理后台、日志、备份、告警和测试构成完整的运行闭环; +- 设备自动化适配器与订单、账本和异常池隔离,结果不明确时默认人工核查。 + +项目当前已验证支付宝、微信相关商家通道和设备事件适配能力。国际支付、数字资产支付及其他地区通道属于可扩展方向,尚不代表仓库已经提供对应的生产适配器。 + +## 品牌资源 + +Logo 和 favicon 位于 [`assets/logo/`](assets/logo/),可用于项目主页、技术博客、后台界面和社区头像。标识的视觉语义是“插件化通道、统一编排和可验证账本链”。 + +## 生产参考部署 + +`paygateway` 当前用于作者运营的 [FlashXAPI](https://flashxapi.com) 服务,承载真实的充值、订单、余额和账务流程。FlashXAPI 是作者自己的生产参考部署,不是独立第三方测评,也不是使用本项目的前置条件。 + +这意味着仓库中的订单、回调、账本和异常处理设计并非只在示例测试中存在,而是在实际运行环境中持续验证。生产部署中的用户数据、支付凭证、渠道信息和运行日志不会随仓库公开。 + +需要模型 API 的开发者可以单独了解 FlashXAPI;只希望研究或自部署支付网关的用户可以完全忽略该服务,直接使用本项目的开源代码。 + +## 账务审计模型 + +项目的账本使用哈希链、ECDSA 签名、独立存储和数据库层约束,目标是让资金事件可追溯,并在历史记录被静默修改或删除时提供检测依据。 + +这里的“防篡改”是技术语义:正常业务流程下账本只追加,历史记录的异常变化可被检测。它不等同于绝对不可攻击,也不替代会计凭证、税务申报、司法鉴定或专业审计意见。签名密钥、运行主机、备份和访问权限仍需由部署者独立保护。 + +## 为什么持续维护需要收入 + +本项目由个人维护,代码免费开源,不设置付费墙。持续维护、测试、生产运行和问题响应都需要时间与基础设施成本。作者通过自己运营的 FlashXAPI 服务获得维护收入,而不是向开源用户募捐或对源码设置获取门槛。 + +FlashXAPI 的生产使用关系会在项目发布、博客文章和技术实践记录中保持透明。服务可用性、模型能力和价格属于独立的服务承诺,不应被理解为本仓库对任何第三方渠道的保证。 + +## 社区与实践记录 + +项目后续会通过配套博客发布架构设计、支付异常复盘、账务审计实践和版本更新记录;社区频道用于维护公告、问题交流和使用反馈。公开内容会优先讨论可复用的工程问题,不公开生产密钥、真实订单、用户请求、渠道来源或未经授权的账户操作细节。 + +- **Telegram 频道**:[@paygtway](https://t.me/paygtway) — 项目公告、版本更新和实践文章 +- **Telegram 群组**:[加入群组](https://t.me/+TyBkhTb5HT0xNDFl) — 使用交流、问题反馈和社区讨论 + +如果你在实际部署中发现回调、账务、并发、备份或文档问题,欢迎通过 GitHub Issue 提供最小复现。请先阅读 [`SECURITY.md`](SECURITY.md),不要在公开 Issue 中提交密钥、支付凭证、个人数据或生产地址。 + +--- + +## ✨ 功能特性 + +### 商家通道(官方 API) +- **支付宝**:PC/手机网站支付、当面付扫码(gopay v1.5.119) +- **微信支付**:Native/JSAPI(预留扩展) +- 多商户(上游池):每上游独立 `pid + 密钥`,下单/回调按 pid 路由验签 +- 回调验签(RSA2 / HMAC-SHA256)、查单补账、超时关单 + +### 设备自动化实验适配器(可选) +- 设备访问、事件读取、状态机、调度和健康检查采用独立包封装 +- 事件经来源认证、时间窗口、金额、状态和幂等校验后才可影响订单 +- 设备离线、解析失败和状态冲突进入异常池,不自动确认资金结果 +- 具体平台适配和授权边界见 [`docs/MIANQIAN.md`](docs/MIANQIAN.md);文档不提供第三方账户操作或绕过安全控制的步骤 + +### 基础设施 +- **账户池**:多账户轮询调度、日限额、连续失败熔断、手动/自动冻结,密钥 AES-256-GCM 加密入库 +- **审计账本**:哈希链 + ECDSA 签名(`ledger.db`),每日快照,防篡改可验证 +- **异常池**:金额不匹配 / 监听端离线 / 状态冲突 → 人工核查,绝不自动入账 +- **不漏单保护**:个人通道关单前必查池内动态 `CloseAt` + 监听端离线检查——离线时订单进异常池人工核实(`LISTEN_OFFLINE`),绝不直接关单;商家通道关单前必查单(已支付→补账,查单失败→跳过) +- **管理后台**:Vue3 + Element Plus 单页(embed 进二进制),仪表盘/通道/订单/账户/上游/对账/账本/日志 +- **告警**:Bark / Webhook(掉线、账本异常、风控) + +--- + +## 🚀 快速开始 + +### 1. 编译 + +```bash +# Go 1.25+;隧道功能运行时需要系统 cloudflared +go build -o gateway ./cmd/gateway/ + +# 可选:重新构建前端(需要 node 18+) +cd web/admin && npm install && npm run build +cd ../.. && go build -o gateway ./cmd/gateway/ +``` + +### 2. 配置 `.env` + +```bash +cp .env.example .env && vim .env +``` + +`ACCOUNT_MASTER_KEY` 必须替换(`openssl rand -hex 32` 生成)。管理员账号不再配置在 .env——首次启动访问管理后台会自动进入注册页,凭据 bcrypt 哈希存主数据库 settings 表。 + +| 变量 | 必填 | 默认 | 说明 | +|------|------|------|------| +| `GATEWAY_ADDR` | 是 | `:8080` | 网关监听地址(易支付 /mq、套码 /qr 等) | +| `ADMIN_ADDR` | 否 | `:8081` | 管理后台监听(Vue3 SPA + REST + WS) | +| `JWT_SECRET` | 建议 | 空 | 留空 = 每次重启随机生成(重启后全员被踢下线);管理员凭据存主数据库 settings 表(bcrypt),首次启动自动引导注册 | +| `ACCOUNT_MASTER_KEY` | 必填 | 空 | 账户 / 上游密钥加密主密钥。**缺失则服务拒绝启动**;首次用 `openssl rand -hex 32` 生成;**永不能改**(改 = accounts.db / upstreams.db 全部密文无法解密) | +| `LEDGER_PRIVATE_KEY` | 建议 | 空 | 账本 ECDSA 私钥(PEM);留空 = 首次启动自动生成并持久化到 settings;更改则旧账本条目验签失败 | +| `QR_TOKEN_SECRET` | 个人免签建议 | 空 | 套码 token 签名密钥;留空 = 首次启动自动生成并持久化到 settings;更改则旧套码失效 | +| `ORDER_DB` | 否 | `data/paygateway.db` | 业务库路径(订单/异常池/回调队列/settings) | +| `LOG_FILE` | 否 | `logs/paygateway.log` | 日志文件(slog JSON + lumberjack:50MB×10 份×30 天 压缩) | +| `LOG_LEVEL` | 否 | `info` | `debug` / `info` / `warn` / `error` | +| `ORDER_CLOSE_MINUTES` | 否 | `30` | **商家通道**未支付订单自动关闭分钟数(0=不自动关);商家走官方 API 可查单,固定窗口即可 | +| `ORDER_CLOSE_MIN` | 否 | `5` | 个人免签**动态关单窗口下限**(分钟);0 或 ≥MAX → 退化为静态窗口 | +| `ORDER_CLOSE_MAX` | 否 | `10` | 个人免签**动态关单窗口上限**(分钟);超过转人工异常池升级处理,不无限挂起 | +| `ORDER_CLOSE_INTERVAL` | 否 | `30` | 关单扫描间隔秒数(动态窗口下建议 30s 起) | +| `DEVICE_LOAD_CAP` | 否 | `0` | 单设备 WAIT_PAY 订单承载上限(0=按公式 `(MAX-60s)/poll成本` 自动算≈135);超限设备被均衡分配跳过 | +| `DEVICE_OVERLOAD_RATIO` | 否 | `0.8` | 设备降权阈值(占 CAP 比例):负载进入缓冲带后分配降权,避免临界抖动 | +| `DEVICE_POLL_COST_MS` | 否 | `4000` | 单次 poll 成本估算(ms),动态窗口与设备能力计算用 | +| `EPAY_PUBLIC_BASE_URL` | 否 | 自动 | 网关对外地址(用于通知注册);同机 `http://127.0.0.1:8080`,公网填隧道 / 域名 | +| `PERSONAL_BASE_URL` | 否 | =EPAY | 个人免签监听端可达地址 | +| `ALIPAY_NOTIFY_URL` / `WECHAT_NOTIFY_URL` | 否 | `http://localhost:8080/...` | 通道回调地址,一般无需手填(隧道会覆盖) | +| `PERSONAL_MAX_AMOUNT` | 否 | `50000` | 个人免签单笔限额(分;50000 = 500 元;0 = 不限) | +| `PERSONAL_BIG_AMOUNT` | 否 | `20000` | 大额告警阈值(分;超此触发 Bark / Webhook) | +| `PERSONAL_MAX_PER_MIN` | 否 | `30` | 单设备每分钟收款推送上限(防监听端异常刷消息) | +| `ALERT_BARK_URL` | 否 | 空 | Bark 推送 URL(`https://api.day.app/`),空 = 禁用 | +| `ALERT_WEBHOOK_URL` | 否 | 空 | 通用 Webhook URL(POST JSON:掉线 / 账本异常 / 风控文案检测) | +| `PUBLIC_RATE_MAX` | 否 | `60` | 公网接口滑动窗口内单 IP 最大请求数(仅覆盖 `/submit.php` `/api.php` `/qr/refresh` `/mq/:token`),0 = 禁用 | +| `PUBLIC_RATE_WINDOW_SEC` | 否 | `60` | 上一项的滑动窗口秒数,0 = 禁用 | + +> ⚠️ **密钥三件套(`ACCOUNT_MASTER_KEY` / `LEDGER_PRIVATE_KEY` / `QR_TOKEN_SECRET`)更换/丢失的影响各不同,详见 `.env.example` 头部说明**。账户 / 上游密钥一律走管理后台,不在 `.env` 里。 + +### 3. 启动 + +**推荐:systemd 守护** + +```bash +sudo cp deploy/paygateway.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now paygateway +journalctl -u paygateway -f # 实时日志 +sudo systemctl {status,restart,stop} paygateway +``` + +**裸跑:项目脚本(pid 文件 + 防双启)** + +```bash +./start.sh # 后台启动,写 ./.gateway.pid,重复运行会拒绝 +./status.sh # 查看运行状态 +./stop.sh # 优雅停止(SIGTERM → 10s 后 SIGKILL) +./run.sh # 前台调试运行(go run,不读 .gateway.pid) +tail -f logs/paygateway.log +``` + +> 启动后看 `logs/paygateway.log` 的 `=== 启动配置自检 ===` 段:`0 错误` 才算就绪。 + +### 4. 配置账户(首次必做) + +打开 `http://127.0.0.1:8081` 登录: + +1. **上游对接** → 新增上游(pid + 密钥),否则下单会被拒绝 +2. **账户池** → 按需新增: + - 商家通道:填 AppID + 私钥(`alipay` / `wechat`) + - 设备事件适配器:仅在获得授权的测试环境中配置 + +--- + +## 📡 下单 API(epay 协议兼容) + +```bash +# 参数(MD5 签名,排除 sign/sign_type/pay_type) +curl "http://127.0.0.1:8080/submit.php?pid=1001&type=alipay&out_trade_no=TEST001&name=商品&money=10.00¬ify_url=https://your.com/notify&return_url=https://your.com/return&sign=MD5签名&sign_type=MD5" +``` + +- `type`:`alipay`(个人优先→商家回退)、`wxpay`、`personal_alipay`(强制个人) +- 返回:支付页(个人免签=套码二维码+备注码提示;商家=官方二维码/跳转) +- 异步通知:`trade_status=TRADE_SUCCESS` + MD5 签名 +- 查单:`/api.php?pid=..&out_trade_no=..&sign=..` + +--- + +## 🔒 安全设计 + +- 回调验签(商家 RSA2 / 个人 HMAC-SHA256)+ 时间戳防重放(±5min) +- 金额校验(回调金额 ≠ 订单金额 → 拒绝入账 + 异常池) +- 状态机条件更新(SQL `WHERE status=?` 互斥,防并发双写) +- 账本哈希链 + ECDSA 签名,篡改可检测 +- 敏感密钥 AES-256-GCM 加密入库,仅内存明文 +- JWT 管理后台鉴权(随机密钥,禁可预测) +- **后台任务 panic 自恢复**:全部后台 goroutine(关单扫描/收款码维护/账本日任务/隧道/告警等)带 `defer recover`,单点异常不拖垮进程,panic + 栈记录日志便于定位 + +--- + +## 🧪 测试 + +```bash +go vet ./... +go test ./... +go test -race ./internal/... # 全包并发安全验证 + +# 授权设备端到端测试(需要自行准备测试设备) +bash scripts/e2e_usb_realdevice.sh +``` + +覆盖:设备池隔离、动态调度、离线安全降级、通道窗口、均衡分配、设备健康埋点、事件匹配和账本哈希链。 + +--- + +## 📁 目录结构 + +``` +cmd/gateway/ 主程序(路由/启动/配置/自检/动态关单与均衡分配装配) +internal/ + account/ 账户池(调度/加密/风控/加权分配) + adbmgr/ adb 设备管理 / 配置下发 + admin/ 管理后台 API + embed 前端 + alert/ 告警客户端(Bark/Webhook) + channel/ 支付通道抽象 + 实现(alipay/wechat/personal) + epay/ epay 协议层(签名/参数) + ledger/ 审计账本(哈希链/快照) + ledgermon/ ADB 监听编排(设备健康埋点/公平调度/账本识别) + model/ 领域模型 + notify/ 通知发送 worker + notifywatcher/ 通知栏事件驱动监听(比账本快 ~15s) + order/ 订单编排(状态机/幂等/结算/安全关单) + personal/ 个人免签(备注码/签名/风控) + pool/ WAIT_PAY 设备维度池(动态关单窗口/负载快照/CloseAt) + ratelimit/ 公网接口 IP 限流 + reconciler/ 确认调度器/对账/异常池 + scrcpy/ 真机投屏(adb forward) + store/ SQLite 存储层 + tunnel/ Cloudflare 隧道 + upstream/ 上游池(多商户) + web/ 支付页面生成 +deploy/ systemd unit + 每日 SQLite 备份脚本 +scripts/ 运维 / E2E 脚本 +web/admin/ 管理后台前端源码(Vue3) +docs/ 设计文档 +.env.example 配置示例(cp 为 .env 修改) +start.sh 裸跑启动(带 pid 文件 / 防双启) +stop.sh / status.sh 裸跑对应停止 / 状态查询 +``` + +--- + +## 📚 相关文档 + +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — 系统架构(协议/编排/通道三层) +- [`docs/PLAN-DYNAMIC-CLOSE-ALLOC.md`](docs/PLAN-DYNAMIC-CLOSE-ALLOC.md) — 动态关单窗口 + 设备/账户均衡分配设计与实现基线 +- [`docs/MIANQIAN.md`](docs/MIANQIAN.md) — 设备自动化与个人通道的学习概览 +- [`docs/RESILIENCE.md`](docs/RESILIENCE.md) — 弹性与对账(崩溃恢复/异常池/每日快照) +- [`docs/OPERATIONS.md`](docs/OPERATIONS.md) — 运维手册(升级/回滚/备份恢复/密钥轮换/常见故障/健康检查) +- [`docs/DESIGN-DEVICE-ACTOR.md`](docs/DESIGN-DEVICE-ACTOR.md) — ADB 设备操作串行化(DeviceActor 模型) +- [`docs/DESIGN-FAIR-SCHEDULER.md`](docs/DESIGN-FAIR-SCHEDULER.md) — 单设备多账户公平时间占比调度 + +## 📌 注意 + +- 本项目仅用于学习、授权测试和合法的商户支付集成。部署者必须自行确认支付合规、平台条款、隐私保护和数据安全要求。 +- 设备自动化适配器不是第三方平台授权的替代品,不得用于未经授权访问账户、绕过安全控制或其他违法活动。 +- 账本提供防篡改检测和可追溯性,不等同于法律、税务或司法意义上的审计结论。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..06331f4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +Please do not disclose credentials, private keys, payment data, personal data, +or production endpoints in a public issue. + +For a suspected security issue, contact the maintainers privately with the +affected version or commit, a minimal reproduction, the impact, and logs with +secrets and personal data removed. + +This project is a learning and research reference. Deployers are responsible +for authorization, platform terms, payment compliance, data protection, and +operational security in their own environment. diff --git a/assets/logo/README.md b/assets/logo/README.md new file mode 100644 index 0000000..13c60b8 --- /dev/null +++ b/assets/logo/README.md @@ -0,0 +1,18 @@ +# PayGateway Brand Assets + +The mark represents three ideas in one restrained route glyph: + +- converging rails for a plugin-based gateway; +- a shared spine for order and channel orchestration; +- the green seam for a verifiable ledger trail. + +The assets use the admin design tokens `#7170FF`, `#22A069`, `#0D0D0F`, and +`#FAFAFA`. Keep the mark clear of QR codes, payment-card illustrations, and +third-party platform logos when creating derivatives. + +Files: + +- `paygateway-mark.svg`: dark-background mark; +- `paygateway-mark-light.svg`: light-background mark; +- `paygateway-wordmark.svg`: README and documentation lockup; +- `favicon.svg`: compact browser/app icon. diff --git a/assets/logo/favicon.svg b/assets/logo/favicon.svg new file mode 100644 index 0000000..41d8b51 --- /dev/null +++ b/assets/logo/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/logo/paygateway-mark-light.svg b/assets/logo/paygateway-mark-light.svg new file mode 100644 index 0000000..86ca7ec --- /dev/null +++ b/assets/logo/paygateway-mark-light.svg @@ -0,0 +1,11 @@ + + PayGateway + A routed payment gateway with a verifiable ledger spine. + + + + + + + + diff --git a/assets/logo/paygateway-mark.svg b/assets/logo/paygateway-mark.svg new file mode 100644 index 0000000..e973ddf --- /dev/null +++ b/assets/logo/paygateway-mark.svg @@ -0,0 +1,12 @@ + + PayGateway + A routed payment gateway with a verifiable ledger spine. + + + + + + + + + diff --git a/assets/logo/paygateway-wordmark.svg b/assets/logo/paygateway-wordmark.svg new file mode 100644 index 0000000..73633de --- /dev/null +++ b/assets/logo/paygateway-wordmark.svg @@ -0,0 +1,8 @@ + + PayGateway + PayGateway, a verifiable and extensible payment gateway. + + + + PayGateway + diff --git a/cmd/e2e_ledger/main.go b/cmd/e2e_ledger/main.go new file mode 100644 index 0000000..0dccffd --- /dev/null +++ b/cmd/e2e_ledger/main.go @@ -0,0 +1,67 @@ +// 临时 E2E 工具:解密上游密钥 + 构造签名下单请求(验证后删除) +package main + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "time" + + "paygateway/internal/account" + "paygateway/internal/epay" + "paygateway/internal/store" +) + +func main() { + keyHex := os.Getenv("ACCOUNT_MASTER_KEY") + if err := account.InitCrypto(keyHex); err != nil { + panic("InitCrypto: " + err.Error()) + } + db, err := store.NewUpstreamDB("data/upstreams.db") + if err != nil { + panic("NewUpstreamDB: " + err.Error()) + } + defer db.Close() + up, err := db.GetUpstreamByPid(context.Background(), "2001") + if err != nil || up == nil { + panic(fmt.Sprintf("GetByPid: %v up=%v", err, up)) + } + key := up.KeyEnc + dec, err := account.Decrypt(key) + if err != nil { + panic("Decrypt: " + err.Error()) + } + fmt.Println("UPSTREAM_KEY_DECRYPTED len:", len(dec)) + + // 构造 epay 下单参数(type=alipay 会优先路由 personal.alipay) + outNo := fmt.Sprintf("E2E%d", time.Now().Unix()) + params := map[string]string{ + "pid": "2001", + "type": "alipay", + "out_trade_no": outNo, + "name": "e2e测试", + "money": "1.00", + "notify_url": "http://127.0.0.1:8080/notify", + "return_url": "http://127.0.0.1:8080/return", + "pay_type": "alipay", + "device": "pc", + } + signed := epay.BuildSignedParams(params, string(dec)) + form := url.Values{} + for k, v := range signed { + form.Set(k, v) + } + resp, err := http.Post("http://127.0.0.1:8080/submit.php", + "application/x-www-form-urlencoded", strings.NewReader(form.Encode())) + if err != nil { + panic("POST: " + err.Error()) + } + defer resp.Body.Close() + buf := make([]byte, 4096) + n, _ := resp.Body.Read(buf) + fmt.Println("RESP:", resp.StatusCode) + fmt.Println(string(buf[:n])) +} diff --git a/cmd/gateway/alloc.go b/cmd/gateway/alloc.go new file mode 100644 index 0000000..a4b9ddd --- /dev/null +++ b/cmd/gateway/alloc.go @@ -0,0 +1,263 @@ +// paygateway 支付网关 alloc(从 main.go 拆分,均衡分配算法) +package main + +import ( + "context" + "fmt" + "paygateway/internal/account" + "paygateway/internal/ledgermon" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/pool" + "paygateway/internal/store" + "time" +) + +func decryptDeviceSecret(encHex string) (string, error) { + if encHex == "" { + return "", fmt.Errorf("设备密钥为空") + } + data, err := account.Decrypt(encHex) + if err != nil { + return "", err + } + return string(data), nil +} + +// autoRegisterDevice 首连自动注册设备(极端情况:仅有固定域名 + 可访问公网即可接入)。 +// 监听端首次启动未配置时自动生成 device_id + secret,心跳携带 register_secret 完成注册。 +// 返回注册后的设备(含解密密钥)。 +func (a *App) resolveLedgerDevices(ctx context.Context) ([]ledgermon.MonitoredDevice, error) { + devs, err := a.personalDB.ListDevices(ctx) + if err != nil { + return nil, err + } + adb := adbMgr() + var out []ledgermon.MonitoredDevice + for _, d := range devs { + serial := deviceADBSerial(d) + if serial == "" { + continue + } + // 实时 adb 可达判定(含自动 adb connect 重连;~1s,reconcile 30s 周期可接受) + if !adb.Online(ctx, serial) { + continue + } + for _, id := range store.DeviceAccountIDs(ctx, a.personalDB, d.ID, d.AccountID) { + acct := a.accountPool.GetByID(id) + if acct == nil { + continue + } + provider, ok := ledgerProviderOf(acct.Channel) + if !ok { + continue // 非个人免签通道(沙箱/企业)无账本页可监听 + } + out = append(out, ledgermon.MonitoredDevice{ + DeviceID: d.DeviceID, + Serial: serial, + Provider: provider, + AccountID: id, + UserID: ledgerUserSpaceOf(ctx, a.personalDB, d.DeviceID, provider, acct.QRContent), + }) + } + } + return out, nil +} + +// deviceADBSerial 解析设备 adb serial(USB host[:port] 或 WiFi host:port),不可达返回空串。 +func deviceADBSerial(d *personal.DeviceEncrypted) string { + if d.AdbHost != "" { + if d.AdbPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort) + } + return d.AdbHost + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort) + } + return "" +} + +// accountDeviceOf 账户 → 设备 ID 映射(WAIT_PAY 池设备维度子池用)。 +// 遍历全部设备找账户归属(主账户或附加账户);一对多时取首个(业务上账户唯一绑定一台收款设备)。 +// 返回 (deviceID, found)。低频(下单时调用一次),全量遍历可接受。 +func accountDeviceOf(ctx context.Context, pdb store.PersonalStore, accountID int64) (string, bool) { + devs, err := pdb.ListDevices(ctx) + if err != nil { + logger.Warn("main", "设备列表查询失败(订单归入兜底桶,Reconcile 兜底)", "err", err) + return "", false + } + for _, d := range devs { + for _, id := range store.DeviceAccountIDs(ctx, pdb, d.ID, d.AccountID) { + if id == accountID { + return d.DeviceID, true + } + } + } + return "", false +} + +// devCandidate 均衡分配候选设备(含负载与健康度,供两层评分) +type devCandidate struct { + deviceID string + serial string + load int // 设备 WAIT_PAY 待确认总数(设备负载) + health float64 // 设备 poll 成功率(0~1) + acctIDs []int64 // 设备绑定账户(主+附加) + loadOverRatio bool // 负载进入缓冲带(ratio×cap ≤ load < cap):评分降权 +} + +// deviceLoadCapOf 计算设备订单承载上限:配置优先;0=按公式自动算。 +// 公式:(MAX窗口 - 支付操作预留) / (单次poll × 每账户最低配额1) × 排队修正。 +// 上限即"在用户可接受确认延迟内设备能扛的订单数"(PLAN-DYNAMIC-CLOSE-ALLOC §4.5)。 +func deviceLoadCapOf(cfg *Config, dynMax time.Duration) int { + if cfg.DeviceLoadCap > 0 { + return cfg.DeviceLoadCap + } + // 与 main 的退化逻辑保持一致:动态窗口配置非法(MIN≤0/MAX≤0/MIN≥MAX)→ 静态窗口语义,不启用超限过滤 + if dynMax <= 0 || cfg.OrderCloseMin <= 0 || cfg.OrderCloseMax <= 0 || cfg.OrderCloseMin >= cfg.OrderCloseMax { + return 0 + } + payReserve := 60 * time.Second + pollCost := time.Duration(cfg.DevicePollCostMS) * time.Millisecond + if pollCost <= 0 { + pollCost = 4 * time.Second + } + n := int((dynMax - payReserve) / (pollCost * 1)) + if n < 1 { + n = 1 + } + return n +} + +// balanceSelectAccount 均衡分配:先选设备(最闲最健康),再选设备内账户(最闲最健康)。 +// 设备评分 = health × 1/(load+1);账户评分 = 健康 × 1/(pool.Count+1)。 +// 设备超限(load ≥ cap)→ 排除;全部超限 → 返回 nil(下单层报错 + 告警)。 +// channelName 形如 "personal.alipay"/"personal.wechat"(账户池通道过滤)。 +func balanceSelectAccount(ctx context.Context, pdb store.PersonalStore, + pool *account.AccountPool, wp *pool.WaitPayPool, lm *ledgermon.Manager, + cfg *Config, channelName string) *account.Account { + if wp == nil || pool == nil { + return nil + } + devs, err := pdb.ListDevices(ctx) + if err != nil { + logger.Error("main", "均衡分配:设备列表查询失败", "err", err) + return nil + } + + // 健康度快照(ledgermon 未启动 → 全部 0.9 中性) + health := map[string]float64{} + if lm != nil { + health = lm.DevicePollHealth() + } + + // 收集在线设备候选(adb 可达) + var cands []devCandidate + for _, d := range devs { + serial := deviceADBSerial(d) + if serial == "" || !adbMgr().Online(ctx, serial) { + continue + } + ids := store.DeviceAccountIDs(ctx, pdb, d.ID, d.AccountID) + if len(ids) == 0 { + continue + } + st := wp.DeviceSnapshot(d.DeviceID) + load := 0 + if st != nil { + load = st.Total + } + h := health[d.DeviceID] + if h <= 0 { + h = 0.9 + } + cands = append(cands, devCandidate{deviceID: d.DeviceID, serial: serial, load: load, health: h, acctIDs: ids}) + } + if len(cands) == 0 { + return nil + } + + // 设备层:缓冲带降权(load ≥ ratio×cap → 降权)→ 超限排除(load ≥ cap)→ 评分排序 + dynMax := time.Duration(cfg.OrderCloseMax) * time.Minute + capN := deviceLoadCapOf(cfg, dynMax) + softCap := 0 + if capN > 0 && cfg.DeviceOverloadRatio > 0 && cfg.DeviceOverloadRatio < 1 { + softCap = int(float64(capN) * cfg.DeviceOverloadRatio) + } + usable := cands[:0] + for _, c := range cands { + if capN > 0 && c.load >= capN { + logger.Warn("main", "均衡分配:设备负载超限,跳过", + "device_id", c.deviceID, "load", c.load, "cap", capN) + continue + } + if softCap > 0 && c.load >= softCap { + c.loadOverRatio = true // 缓冲带:评分降权(最闲设备优先时让位) + } + usable = append(usable, c) + } + if len(usable) == 0 { + logger.Error("main", "均衡分配:全部设备负载超限,建议增加收款设备", + "loads", fmt.Sprintf("%+v", cands)) + return nil + } + // 设备评分 argmax(最闲最健康;缓冲带内降权;评分相等 → load 小优先,再按 deviceID 字典序确定性) + best := usable[0] + bestScore := deviceScore(best) + for _, c := range usable[1:] { + s := deviceScore(c) + if s > bestScore || (s == bestScore && (c.load < best.load || (c.load == best.load && c.deviceID < best.deviceID))) { + best, bestScore = c, s + } + } + + // 账户层:设备内选最闲最健康账户 + allowed := make(map[int64]bool, len(best.acctIDs)) + for _, id := range best.acctIDs { + allowed[id] = true + } + return pool.SelectFromWeighted(channelName, "default", allowed, + func(acct *account.Account) float64 { + return accountScore(acct, wp) + }) +} + +// deviceScore 设备评分:health × 1/(load+1)。 +// 缓冲带内(loadOverRatio)额外 ×0.5 降权:让最闲设备优先,避免临界抖动。 +func deviceScore(c devCandidate) float64 { + s := c.health / float64(c.load+1) + if c.loadOverRatio { + s *= 0.5 + } + return s +} + +// accountScore 账户评分:健康(状态/日限额/失败)× 1/(账户负载+1) +// 只罚短板,不叠加长处(PLAN-DYNAMIC-CLOSE-ALLOC §5.3) +func accountScore(acct *account.Account, wp *pool.WaitPayPool) float64 { + if acct == nil { + return 0 + } + health := 1.0 + if acct.DailyLimit > 0 { + health *= 1 - float64(acct.DailyUsed)/float64(acct.DailyLimit) // 日限额余量 + if health < 0 { + health = 0 + } + } + switch { + case acct.FailCount >= 3: + health *= 0.2 + case acct.FailCount >= 1: + health *= 0.5 + } + load := 1 + if wp != nil { + load = wp.Count(acct.ID) + 1 + } + return health / float64(load) +} + +// deviceSerialFromDevice 解析设备 adb serial(内存态 Device 版本,镜像 deviceADBSerial 逻辑)。 +// 探测链路用(dispatchLedgerRows 拿到的是 personalDeviceCtx 的 *personal.Device)。 diff --git a/cmd/gateway/channels.go b/cmd/gateway/channels.go new file mode 100644 index 0000000..022abce --- /dev/null +++ b/cmd/gateway/channels.go @@ -0,0 +1,169 @@ +// paygateway 支付网关 channels(从 main.go 拆分,通道注册/设备管理/中间件) +package main + +import ( + "fmt" + "paygateway/internal/account" + "paygateway/internal/adbmgr" + "paygateway/internal/admin" + "paygateway/internal/channel" + alipaych "paygateway/internal/channel/alipay" + personalch "paygateway/internal/channel/personal" + wechatch "paygateway/internal/channel/wechat" + "paygateway/internal/logger" + "paygateway/internal/store" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +func adbMgr() *adbmgr.Manager { + return adbmgr.New("") +} + +// regChannels 注册支付通道(每加一种支付方式 = 这里加一行) +// 返回个人免签通道实例(main 用于动态更新公网地址 + 推送处理) +func regChannels(cfg *Config, accountPool *account.AccountPool, st store.Store, personalDB store.PersonalStore) (*personalch.Channel, *personalch.Channel) { + // 通道始终注册(可插拔),支付账户一律从账户池读取(管理后台「账户池」配置,加密入库) + // 空 Config 仅保留回调地址:账户的 AppID/私钥/公钥/环境由各自账户决定 + alipayCh, err := alipaych.New(alipaych.Config{ + NotifyURL: cfg.AlipayNotifyURL, + Debug: cfg.ChannelDebug, + }, accountPool) + if err != nil { + logger.Error("main", "支付宝通道初始化失败", "err", err) + } else { + channel.Register(alipayCh) + logger.Info("main", "支付宝通道注册成功 ✅", "accounts", accountPool.CountByChannel("alipay")) + } + + wxCh, err := wechatch.New(wechatch.Config{ + NotifyURL: cfg.WechatNotifyURL, + Debug: cfg.ChannelDebug, + }, accountPool) + if err != nil { + logger.Error("main", "微信通道初始化失败", "err", err) + } else { + channel.Register(wxCh) + logger.Info("main", "微信通道注册成功 ✅", "accounts", accountPool.CountByChannel("wechat")) + } + + // 个人免签通道(阶段 2):personal.alipay / personal.wechat + // 账户池 channel = "personal.alipay" / "personal.wechat",每个账户 = 一个收款码(qr_content) + baseURL := cfg.PersonalBaseURL + if baseURL == "" { + port := strings.TrimPrefix(cfg.ListenAddr, ":") + baseURL = "http://127.0.0.1:" + port + } + var personalAlipay, personalWechat *personalch.Channel + personalAlipay = personalch.New(personalch.Config{BaseURL: baseURL, MaxAmount: cfg.PersonalMaxAmount}, channel.ProviderAlipay, accountPool, st, personalDB) + channel.Register(personalAlipay) + logger.Info("main", "个人免签支付宝通道注册成功 ✅", "accounts", accountPool.CountByChannel("personal.alipay")) + + personalWechat = personalch.New(personalch.Config{BaseURL: baseURL, MaxAmount: cfg.PersonalMaxAmount}, channel.ProviderWechat, accountPool, st, personalDB) + channel.Register(personalWechat) + logger.Info("main", "个人免签微信通道注册成功 ✅", "accounts", accountPool.CountByChannel("personal.wechat")) + + return personalAlipay, personalWechat +} + +// buildEpayInfo 易支付服务地址信息(admin 展示用) +// 多商户模式下不再有全局 pid/key:每个上游的 pid/密钥/对接指引在「上游对接」页独立配置 +// 展示地址优先公网(固定隧道域名):给上游看的对接信息写 127.0.0.1 会让对方配成内网兜底失效 +func buildEpayInfo(cfg *Config) *admin.EpayInfo { + base := cfg.EpayPublicBase + if base == "" { + base = cfg.PersonalBaseURL + } + if base == "" { + port := strings.TrimPrefix(cfg.ListenAddr, ":") + base = "http://127.0.0.1:" + port + } + base = strings.TrimSuffix(base, "/") + return &admin.EpayInfo{ + BaseURL: base, + SubmitURL: base + "/submit.php", + QueryURL: base + "/api.php", + Pid: "", + Key: "", + SignType: "MD5", + } +} + +// buildChannelInfos 组装通道信息给管理后台(admin 不依赖具体通道包) +func buildChannelInfos(pool *account.AccountPool) []admin.ChannelInfo { + var out []admin.ChannelInfo + for _, name := range channel.AllNames() { + ch, ok := channel.Lookup(name) + if !ok { + continue + } + info := admin.ChannelInfo{ + Name: name, + Enabled: channel.IsEnabled(name), + Kind: "callback", + } + // 按通道实际类型修正 kind(callback / listener) + if ch.Kind() == channel.KindListener { + info.Kind = "listener" + } + // 账户配置状态(引导用) + if pool != nil { + info.AccountCount = pool.CountByChannel(name) + info.Configured = info.AccountCount > 0 + } + for _, m := range ch.Modes() { + info.Modes = append(info.Modes, string(m)) + } + // 通道特有信息 + // 支付模式启停状态(有 EnabledModes 方法的通道) + type modeProvider interface { + EnabledModes() map[string]bool + } + if mp, ok := ch.(modeProvider); ok { + info.ModesEnabled = mp.EnabledModes() + } + + // 支付宝特有信息(AppID/环境从账户池取第一个账户展示) + if ac, ok := ch.(*alipaych.Channel); ok { + info.NotifyURL = ac.NotifyURL() + if accts := pool.ListByChannel("alipay"); len(accts) > 0 { + info.AppID = accts[0].AppID + info.IsProd = accts[0].IsProd + } + } + // 微信通道特有信息 + if wc, ok := ch.(*wechatch.Channel); ok { + info.NotifyURL = wc.NotifyURL() + if accts := pool.ListByChannel("wechat"); len(accts) > 0 { + info.AppID = accts[0].AppID + info.MchID = accts[0].MchID + info.IsProd = accts[0].IsProd + } + } + out = append(out, info) + } + return out +} + +// traceMiddleware 为每个请求生成 trace_id + +// traceMiddleware 为每个请求生成 trace_id +func traceMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + traceID := c.GetHeader("X-Trace-ID") + if traceID == "" { + traceID = fmt.Sprintf("tr-%d", time.Now().UnixNano()) + } + c.Set("trace_id", traceID) + c.Header("X-Trace-ID", traceID) + c.Next() + } +} + +// ========== 易支付下单接口 ========== + +// epayKeyFor 按 pid 返回验签密钥(多商户,严格模式) +// 返回 (key, true):pid 已登记且启用 +// 返回 ("", false):pid 未登记或已禁用 → 拒绝(不再有任何 .env / 默认密钥回退) diff --git a/cmd/gateway/check.go b/cmd/gateway/check.go new file mode 100755 index 0000000..65d10fe --- /dev/null +++ b/cmd/gateway/check.go @@ -0,0 +1,164 @@ +// Package main 启动配置自检(RESILIENCE.md P1-2) +// +// 目标:启动时一次性暴露配置错误,避免生产事故: +// - 沙箱/正式错配(is_prod) +// - 密钥缺失(主密钥 / 账本密钥 / 套码 token) +// - 回调地址未配置(隧道未就绪) +// - 个人免签通道缺账户 / 缺监听设备 +// - 告警未配置(可观测性缺失) +// +// 检查结果分级:✅ 正常 / ⚠️ 警告(可运行但建议修复)/ ❌ 错误(功能不可用) +// 通过结构化日志输出,管理后台「系统日志」可查。 +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "paygateway/internal/account" + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// checkItem 单条检查结果 +type checkItem struct { + level string // ok / warn / err + msg string +} + +// runStartupChecks 启动配置自检(在账户池加载、通道注册、隧道启动前调用) +func runStartupChecks(cfg *Config, accountPool *account.AccountPool, st store.Store, personalDB store.PersonalStore) { + var items []checkItem + now := time.Now() + + // 1. 密钥检查 + // 账户主密钥在 main.go 已 fail-fast:到这里一定非空,但保留检查分支以应对未来可能的"可选"开关。 + if cfg.AccountMasterKey == "" { + items = append(items, checkItem{"err", "账户主密钥未配置(应在 main.go fail-fast 退出,不会走到此处)"}) + } else { + items = append(items, checkItem{"ok", "账户主密钥:已配置"}) + } + if cfg.LedgerPrivKey == "" { + items = append(items, checkItem{"ok", "审计账本密钥:自动生成(settings 持久化)"}) + } else { + items = append(items, checkItem{"ok", "审计账本密钥:已配置"}) + } + if personal.QrTokenSecretSet() { + items = append(items, checkItem{"ok", "套码 token 密钥:已初始化"}) + } else { + items = append(items, checkItem{"err", "套码 token 密钥未初始化(个人免签二维码将无法生成)"}) + } + + // 2. 账户池检查 + total := accountPool.Count() + if total == 0 { + items = append(items, checkItem{"warn", "账户池为空:无任何支付账户,所有通道无法接单(请到管理后台「账户池」添加)"}) + } else { + items = append(items, checkItem{"ok", fmt.Sprintf("账户池:%d 个账户", total)}) + } + + // 3. 个人免签通道检查 + personalNames := []string{} + for _, name := range channel.AllNames() { + if strings.HasPrefix(name, "personal.") { + personalNames = append(personalNames, name) + } + } + if len(personalNames) == 0 { + items = append(items, checkItem{"err", "个人免签通道未注册(personal.alipay / personal.wechat)"}) + } else { + items = append(items, checkItem{"ok", "个人免签通道:已注册 " + strings.Join(personalNames, ", ")}) + // 各个人通道账户数 + for _, name := range personalNames { + n := accountPool.CountByChannel(name) + if n == 0 { + items = append(items, checkItem{"warn", fmt.Sprintf("通道 %s 无收款账户:该通道无法接单(需配置收款码 qr_content)", name)}) + } else { + items = append(items, checkItem{"ok", fmt.Sprintf("通道 %s:%d 个收款账户", name, n)}) + } + } + } + + // 4. 监听设备检查(在线 = adb 可达,2026-08-29 重构:不再依赖监听 App 心跳) + if personalDB != nil { + devs, err := personalDB.ListDevices(context.Background()) + if err != nil { + items = append(items, checkItem{"err", "监听设备列表查询失败: " + err.Error()}) + } else { + online := 0 + for _, d := range devs { + serial := deviceADBSerial(d) + if serial != "" && adbMgr().Online(context.Background(), serial) { + online++ + } + } + if len(devs) == 0 { + items = append(items, checkItem{"warn", "无监听端设备:个人免签无法确认收款(请绑定收款账户创建设备)"}) + } else { + items = append(items, checkItem{"ok", fmt.Sprintf("监听设备:%d 台(adb 在线 %d)", len(devs), online)}) + } + } + } + + // 5. 告警配置 + if cfg.AlertBarkURL == "" && cfg.AlertWebhookURL == "" { + items = append(items, checkItem{"warn", "告警未配置(ALERT_BARK_URL / ALERT_WEBHOOK_URL 为空):异常进池/账本失败/dead 通知将无法推送"}) + } else { + items = append(items, checkItem{"ok", "告警通道:已配置"}) + } + + // 6. 单笔限额风控 + if cfg.PersonalMaxAmount > 0 { + items = append(items, checkItem{"ok", fmt.Sprintf("个人通道单笔限额:¥%d.%02d", cfg.PersonalMaxAmount/100, cfg.PersonalMaxAmount%100)}) + } else { + items = append(items, checkItem{"warn", "个人通道单笔限额未设置(PERSONAL_MAX_AMOUNT=0):大额风险无防护"}) + } + + // 7. 生产/沙箱错配检查(仅在有账户时) + prodAccts, sandboxAccts := 0, 0 + for _, a := range accountPool.ListAll() { + if a.IsProd { + prodAccts++ + } else { + sandboxAccts++ + } + } + if sandboxAccts > 0 && prodAccts > 0 { + items = append(items, checkItem{"warn", fmt.Sprintf("生产/沙箱账户混用:%d 生产 + %d 沙箱(正式环境应全部 is_prod=true)", prodAccts, sandboxAccts)}) + } else if sandboxAccts == 0 && prodAccts > 0 { + items = append(items, checkItem{"ok", fmt.Sprintf("账户环境一致:全部为生产(%d 个)", prodAccts)}) + } + + // 8. 数据文件(settings 表)可读 + if _, err := st.GetAllSettings(context.Background()); err != nil { + items = append(items, checkItem{"err", "settings 表不可读: " + err.Error()}) + } else { + items = append(items, checkItem{"ok", "业务数据库:可读写"}) + } + + // 输出报告 + okCount, warnCount, errCount := 0, 0, 0 + logger.Info("main", "=== 启动配置自检 ===") + for _, it := range items { + switch it.level { + case "ok": + okCount++ + logger.Info("main", "自检 ✅ "+it.msg) + case "warn": + warnCount++ + logger.Warn("main", "自检 ⚠️ "+it.msg) + case "err": + errCount++ + logger.Error("main", "自检 ❌ "+it.msg) + } + } + logger.Info("main", fmt.Sprintf("启动自检完成:%d 通过 / %d 警告 / %d 错误(用时 %s)", + okCount, warnCount, errCount, time.Since(now).Round(time.Millisecond))) + if errCount > 0 { + logger.Warn("main", "存在错误级配置问题,请修复后重启(服务将继续启动,但相关功能不可用)") + } +} diff --git a/cmd/gateway/close_time_test.go b/cmd/gateway/close_time_test.go new file mode 100644 index 0000000..aee49e5 --- /dev/null +++ b/cmd/gateway/close_time_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" + "time" + + "paygateway/internal/model" + "paygateway/internal/pool" +) + +// TestPersonalOrderCloseAt_StaticFallback 静态关单模式(无动态窗口):使用 ORDER_CLOSE_MINUTES。 +func TestPersonalOrderCloseAt_StaticFallback(t *testing.T) { + app := &App{cfg: &Config{OrderCloseMinutes: 30}} + now := time.Now() + o := &model.Order{OutTradeNo: "X", CreatedAt: now, Channel: "personal.alipay"} + got := app.personalOrderCloseAt(o) + want := now.Add(30 * time.Minute) + if !got.Equal(want) { + t.Fatalf("静态窗口下应 = CreatedAt+30min,实际 %v want %v", got, want) + } +} + +// TestPersonalOrderCloseAt_DynamicNotOverrideStatic 动态窗口已启用:个人订单回退用动态下限 ORDER_CLOSE_MIN, +// 绝不落静态 ORDER_CLOSE_MINUTES(防止环境配置拉长个人订单关单——幽灵订单根因之一)。 +func TestPersonalOrderCloseAt_DynamicNotOverrideStatic(t *testing.T) { + app := &App{cfg: &Config{OrderCloseMinutes: 30, OrderCloseMin: 5, OrderCloseMax: 10}} + now := time.Now() + o := &model.Order{OutTradeNo: "X", CreatedAt: now, Channel: "personal.wechat"} + got := app.personalOrderCloseAt(o) + want := now.Add(5 * time.Minute) + if !got.Equal(want) { + t.Fatalf("动态窗口下个人订单应 = CreatedAt+ORDER_CLOSE_MIN(5min),实际 %v want %v", got, want) + } +} + +// TestPersonalOrderCloseAt_PoolCloseAt 订单在池中:用池内设备负载驱动的 CloseAt(最精确,与关单扫描器一致)。 +func TestPersonalOrderCloseAt_PoolCloseAt(t *testing.T) { + p := pool.NewWaitPayPool(30) + now := time.Now() + o := &model.Order{OutTradeNo: "POOL1", AccountID: 7, CreatedAt: now, Channel: "personal.alipay"} + p.Add(o) + app := &App{cfg: &Config{OrderCloseMinutes: 30, OrderCloseMin: 5, OrderCloseMax: 10}, waitPayPool: p} + got := app.personalOrderCloseAt(o) + if got.IsZero() { + t.Fatalf("池内订单应返回 CloseAt,实际零值") + } + if !got.After(now) { + t.Fatalf("池内 CloseAt 应在未来,实际 %v (now %v)", got, now) + } +} + +// TestPersonalOrderCloseAt_Disabled 自动关单未开启(所有窗口为 0):返回零值(页面不显示倒计时)。 +func TestPersonalOrderCloseAt_Disabled(t *testing.T) { + app := &App{cfg: &Config{OrderCloseMinutes: 0, OrderCloseMin: 0, OrderCloseMax: 0}} + o := &model.Order{OutTradeNo: "X", CreatedAt: time.Now(), Channel: "personal.alipay"} + if got := app.personalOrderCloseAt(o); !got.IsZero() { + t.Fatalf("自动关单未开启应返回零值,实际 %v", got) + } +} diff --git a/cmd/gateway/config.go b/cmd/gateway/config.go new file mode 100644 index 0000000..d411561 --- /dev/null +++ b/cmd/gateway/config.go @@ -0,0 +1,297 @@ +// paygateway 支付网关 config(从 main.go 拆分,配置结构/加载/env 解析/密钥) +package main + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/store" + "strconv" + "strings" + "time" +) + +type Config struct { + ListenAddr string + AdminAddr string + DBFile string + LogFile string + LogLevel string + + JWTSecret string // JWT 签名密钥(留空自动生成) + + OrderCloseMinutes int // 未支付订单自动关闭时间(分钟),0=关闭功能 + OrderCloseInterval int // 扫描间隔(秒) + // 动态关单窗口上下限(分钟,PLAN-DYNAMIC-CLOSE-ALLOC §4.1): + // 窗口 = clamp(设备一轮耗时×1.5+支付操作预留, MIN, MAX)。0 或 MIN>MAX → 用静态 OrderCloseMinutes。 + OrderCloseMin int + OrderCloseMax int + // 设备处理能力:单设备可承载的 WAIT_PAY 订单上限(0=按公式自动算)。 + // 超限 → 分配器跳过该设备;全部超限 → 拒绝下单 + 告警建议加设备。 + DeviceLoadCap int + // 设备降权阈值(占 DeviceLoadCap 比例,默认 0.8):超过后该设备在分配器中降权。 + DeviceOverloadRatio float64 + // 单次 poll 成本估算(ms,动态窗口/设备能力计算;0=默认 4000,埋点校准后设置) + DevicePollCostMS int + + // EpayPublicBase 对外易支付服务基础地址(其他项目对接用) + // 同机项目填 http://127.0.0.1:8080;局域网填 http://:8080;公网填隧道/域名 + // 留空则自动推断为 http://127.0.0.1:<端口> + EpayPublicBase string + + // 通道回调地址(启动时由隧道动态更新;账户密钥一律在管理后台「账户池」配置,不走环境变量) + AlipayNotifyURL string + WechatNotifyURL string + + // ChannelDebug 支付通道 SDK debug 日志(每笔请求/响应/签名全量 dump) + // 默认关闭:真机事故——沙箱查单失败风暴时 gopay 每次失败刷 4 条 DEBUG,两天刷出 12w 行 + ChannelDebug bool + + // NotifyAllowPrivateIPs 允许商户 notify_url 指向私网/环回地址(SSRF 防护开关)。 + // 同机部署 New API 需要置 true(默认 true 保持兼容);网关公网暴露且商户不可信时必须置 false。 + NotifyAllowPrivateIPs bool + + LedgerPrivKey string // 审计密钥(空则自动生成并持久化到 settings,重启复用) + AccountMasterKey string // 账户加密主密钥(空则自动生成) + QrTokenSecret string // 套码 token 签名密钥(空则自动生成并持久化) + PersonalBaseURL string // 个人免签公网基础地址(优先隧道域名,空则用 EpayPublicBase) + + // 个人免签风控(docs/MIANQIAN.md §10):金额单位均为分 + PersonalMaxAmount int64 // 单笔限额(默认 50000 = 500 元,0=不限) + PersonalBigAmount int64 // 大额告警阈值(默认 20000 = 200 元) + MaxOrdersPerIPPerMin int // 每 IP 每分钟下单上限(默认 10,0=不限;防脚本刷单) + + // 告警(RESILIENCE P1-3) + AlertBarkURL string // Bark 推送 URL(https://api.day.app/),空=禁用 + AlertWebhookURL string // 通用 Webhook URL(POST JSON),空=禁用 + + // 公网接口 IP 限流(防刷/防 DoS,仅作用于公网支付路由,不影响 /alipay/notify /wechat/notify 等签名回调) + PublicRateMax int + PublicRateWindowSec int + + // 账本页服务端监听(docs/PLAN-ADB-LEDGER-IMPL.md §6.3) + LedgerPollInterval time.Duration // 无活跃订单轮询间隔(默认 5s) + LedgerActiveInterval time.Duration // 有活跃订单轮询间隔(默认 1s) + + // 通知栏快速通道(NotifyWatcher,2026-08-30 新增) + NotifyPollInterval time.Duration // 有 WAIT_PAY 订单轮询间隔(默认 500ms) + NotifyIdleInterval time.Duration // 池空低频探测间隔(默认 5s) + + // TunnelHostname 固定隧道域名(如 paygateway.de5.net)。空 = 快速隧道随机域名。 + // 命名隧道需要 ~/.cloudflared/paygateway.yml + paygateway.json 凭据。 + TunnelHostname string +} + +// loadEnv 加载 .env 文件到环境变量 +func loadEnv(path string) { + data, err := os.ReadFile(path) + if err != nil { + return // 文件不存在就跳过 + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + if key != "" && val != "" { + os.Setenv(key, val) + } + } +} + +func loadConfig() *Config { + return &Config{ + ListenAddr: envOr("GATEWAY_ADDR", ":8080"), + AdminAddr: envOr("ADMIN_ADDR", ":8081"), + DBFile: envOr("ORDER_DB", "data/paygateway.db"), + LogFile: envOr("LOG_FILE", "logs/paygateway-{date}.log"), + LogLevel: envOr("LOG_LEVEL", "info"), + JWTSecret: envOr("JWT_SECRET", ""), + + OrderCloseMinutes: envInt("ORDER_CLOSE_MINUTES", 30), + OrderCloseInterval: envInt("ORDER_CLOSE_INTERVAL", 30), + OrderCloseMin: envInt("ORDER_CLOSE_MIN", 5), + OrderCloseMax: envInt("ORDER_CLOSE_MAX", 10), + DeviceLoadCap: envInt("DEVICE_LOAD_CAP", 0), + DeviceOverloadRatio: envFloat("DEVICE_OVERLOAD_RATIO", 0.8), + DevicePollCostMS: envInt("DEVICE_POLL_COST_MS", 4000), + EpayPublicBase: envOr("EPAY_PUBLIC_BASE_URL", ""), + AlipayNotifyURL: envOr("ALIPAY_NOTIFY_URL", "http://localhost:8080/alipay/notify"), + WechatNotifyURL: envOr("WECHAT_NOTIFY_URL", "http://localhost:8080/wechat/notify"), + ChannelDebug: envOr("CHANNEL_DEBUG", "0") == "1", + + NotifyAllowPrivateIPs: envBool("NOTIFY_ALLOW_PRIVATE_IPS", true), + LedgerPrivKey: envOr("LEDGER_PRIVATE_KEY", ""), + AccountMasterKey: envOr("ACCOUNT_MASTER_KEY", ""), + QrTokenSecret: envOr("QR_TOKEN_SECRET", ""), + PersonalBaseURL: envOr("PERSONAL_BASE_URL", ""), + PersonalMaxAmount: envInt64("PERSONAL_MAX_AMOUNT", 50000), + PersonalBigAmount: envInt64("PERSONAL_BIG_AMOUNT", 20000), + MaxOrdersPerIPPerMin: envInt("MAX_ORDERS_PER_IP_PER_MIN", 10), + AlertBarkURL: envOr("ALERT_BARK_URL", ""), + AlertWebhookURL: envOr("ALERT_WEBHOOK_URL", ""), + PublicRateMax: envInt("PUBLIC_RATE_MAX", 60), + PublicRateWindowSec: envInt("PUBLIC_RATE_WINDOW_SEC", 60), + LedgerPollInterval: envDur("LEDGER_POLL_INTERVAL", 5*time.Second), + LedgerActiveInterval: envDur("LEDGER_ACTIVE_INTERVAL", 1*time.Second), + NotifyPollInterval: envDur("NOTIFY_POLL_INTERVAL", 500*time.Millisecond), + NotifyIdleInterval: envDur("NOTIFY_IDLE_INTERVAL", 5*time.Second), + TunnelHostname: envOr("CF_TUNNEL_HOSTNAME", ""), + } +} + +func envInt64(key string, def int64) int64 { + if v := os.Getenv(key); v != "" { + if n, err := strconv.ParseInt(v, 10, 64); err == nil { + return n + } + } + return def +} + +func envFloat(key string, def float64) float64 { + if v := os.Getenv(key); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + } + return def +} + +func envOr(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func envBool(key string, def bool) bool { + if v := os.Getenv(key); v != "" { + b, err := strconv.ParseBool(v) + if err == nil { + return b + } + } + return def +} + +// applySettings 从 DB 加载运行时设置,覆盖 .env 配置 +func applySettings(st store.Store, cfg *Config) { + ctx := context.Background() + settings, err := st.GetAllSettings(ctx) + if err != nil || len(settings) == 0 { + return + } + if v, ok := settings["epay_public_base_url"]; ok && v != "" { + cfg.EpayPublicBase = v + } + if v, ok := settings["order_close_minutes"]; ok && v != "" { + if n, err := strconv.Atoi(v); err == nil { + cfg.OrderCloseMinutes = n + } + } + logger.Info("main", "运行时设置已加载", "count", len(settings)) +} + +// resolveLedgerKey 解析审计账本私钥:.env > settings 表 > 首次生成并持久化到 settings +// 保证重启后密钥不变,旧账本条目验签可继续通过 +func resolveLedgerKey(st store.Store, cfg *Config) (string, error) { + ctx := context.Background() + // 1. .env 优先 + if cfg.LedgerPrivKey != "" { + return cfg.LedgerPrivKey, nil + } + // 2. settings 表 + settings, err := st.GetAllSettings(ctx) + if err == nil { + if k, ok := settings["ledger_private_key"]; ok && k != "" { + return k, nil + } + } + // 3. 生成并持久化 + key, err := generateLedgerKeyPEM() + if err != nil { + return "", err + } + if err := st.SetSetting(ctx, "ledger_private_key", key); err != nil { + return "", fmt.Errorf("保存审计密钥到 settings 失败: %w", err) + } + logger.Info("main", "已生成新的审计账本密钥并持久化到 settings(重启复用)") + return key, nil +} + +// generateLedgerKeyPEM 生成 ECDSA P-256 私钥 PEM +func generateLedgerKeyPEM() (string, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return "", fmt.Errorf("生成审计密钥失败: %w", err) + } + der, err := x509.MarshalECPrivateKey(key) + if err != nil { + return "", fmt.Errorf("序列化审计密钥失败: %w", err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})), nil +} + +// applyChannelRuntimeSettings 从 DB 加载通道运行时设置(需在通道注册后调用) +// 包括:通道启停状态 + 支付模式启停 +func applyChannelRuntimeSettings(st store.Store) { + ctx := context.Background() + settings, err := st.GetAllSettings(ctx) + if err != nil || len(settings) == 0 { + return + } + // 通道启用状态 + for _, name := range channel.AllNames() { + if v, ok := settings["channel_enabled_"+name]; ok { + channel.SetEnabled(name, v == "true") + } + } + // 支付宝支付模式启停 + if ch, ok := channel.Lookup("merchant.alipay"); ok { + type modeSetter interface { + SetModeEnabled(mode string, enabled bool) + } + if ms, ok := ch.(modeSetter); ok { + for _, mode := range []string{"page", "wap", "qrcode"} { + if v, ok := settings["alipay_mode_"+mode]; ok { + ms.SetModeEnabled(mode, v == "true") + } + } + } + } + logger.Info("main", "通道运行时设置已加载", "count", len(settings)) +} + +func envInt(key string, def int) int { + if v := os.Getenv(key); v != "" { + if n, err := strconv.Atoi(v); err == nil { + return n + } + } + return def +} + +func envDur(key string, def time.Duration) time.Duration { + if v := os.Getenv(key); v != "" { + if d, err := time.ParseDuration(v); err == nil { + return d + } + } + return def +} + +// ========== 依赖容器 ========== diff --git a/cmd/gateway/devices.go b/cmd/gateway/devices.go new file mode 100644 index 0000000..a007666 --- /dev/null +++ b/cmd/gateway/devices.go @@ -0,0 +1,38 @@ +// paygateway 支付网关 devices(从 main.go 拆分,设备/渠道映射) +package main + +import ( + "fmt" + "paygateway/internal/personal" +) + +func deviceSerialFromDevice(d *personal.Device) string { + if d == nil { + return "" + } + if d.AdbHost != "" { + if d.AdbPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort) + } + return d.AdbHost + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort) + } + return "" +} + +// ledgerProviderOf 账户通道 → 账本 provider(仅个人免签通道有账本页可监听) +func ledgerProviderOf(channel string) (string, bool) { + switch channel { + case "personal.alipay": + return "alipay", true + case "personal.wechat": + return "wechat", true + } + return "", false +} + +// ledgerUserSpaceOf 账户所属用户空间:收款码内容匹配该设备 (平台, 用户空间) 最新码。 +// 主空间(user 0)与双开(user 999)的码内容不同,用 QRContent 精确匹配归属; +// 匹配不到(无码/未上报)默认主空间 0。 diff --git a/cmd/gateway/e2e_lifecycle_test.go b/cmd/gateway/e2e_lifecycle_test.go new file mode 100644 index 0000000..f38ecb7 --- /dev/null +++ b/cmd/gateway/e2e_lifecycle_test.go @@ -0,0 +1,263 @@ +package main + +// 端到端集成测试:完整资金闭环 —— +// 下单(CreateOrder) → 账本监听确认(dispatchLedgerRows) → 入账+账本写入 → 回调 outbox(EnqueueNotify)。 +// 验证个人免签通道"从下单到通知上游"全链路在真实装配下自洽(不依赖真实 adb/支付平台)。 + +import ( + "context" + "net/http" + "path/filepath" + "testing" + "time" + + "paygateway/internal/account" + "paygateway/internal/admin/capture" + "paygateway/internal/channel" + "paygateway/internal/ledger" + "paygateway/internal/model" + "paygateway/internal/order" + "paygateway/internal/pool" + "paygateway/internal/reconciler" + "paygateway/internal/store" + "paygateway/internal/upstream" +) + +// e2eStubPersonalChannel 端到端测试用个人通道桩:CreatePayment 返回固定备注码, +// 让 CreateOrder 完整走"选账户→分配备注码→生成凭据"路径。 +type e2eStubPersonalChannel struct{} + +func (e2eStubPersonalChannel) Name() string { return "personal.alipay" } +func (e2eStubPersonalChannel) Type() channel.ChannelType { return channel.TypePersonal } +func (e2eStubPersonalChannel) Kind() channel.ChannelKind { return channel.KindListener } +func (e2eStubPersonalChannel) Modes() []channel.PayMode { return []channel.PayMode{channel.ModeQRCode} } +func (e2eStubPersonalChannel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByManual } +func (e2eStubPersonalChannel) Provider() channel.Provider { return channel.ProviderAlipay } +func (e2eStubPersonalChannel) CreatePayment(_ context.Context, req *channel.PayRequest) (*channel.PayCredential, error) { + return &channel.PayCredential{ + QRCode: "https://qr.alipay.com/e2e", + AccountID: 1, // 测试账户池中的账户(真实通道由 selectServableAccount 决定) + Mode: channel.ModeQRCode, + NoteCode: "8833", // 固定备注码,账本行携带后精确匹配 + QrToken: "e2etoken", + QRContent: "https://qr.alipay.com/e2e", + }, nil +} +func (e2eStubPersonalChannel) VerifyNotify(context.Context, *http.Request) (*channel.NotifyResult, error) { + return nil, nil +} +func (e2eStubPersonalChannel) QueryOrder(context.Context, string) (*channel.OrderStatus, error) { + return nil, order.ErrNotQueryable +} +func (e2eStubPersonalChannel) Listener() channel.EventListener { return nil } + +// newE2EEnv 构建端到端测试环境:业务库 + 账本 + 账户池 + 订单服务 + 池 + 通道注册 +func newE2EEnv(t *testing.T) (*App, *store.PersonalDB) { + t.Helper() + dir := t.TempDir() + + st, err := store.NewSQLite(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("创建业务库失败: %v", err) + } + t.Cleanup(func() { st.Close() }) + + ldb, err := store.NewLedgerDB(filepath.Join(dir, "ledger.db")) + if err != nil { + t.Fatalf("创建账本库失败: %v", err) + } + t.Cleanup(func() { ldb.Close() }) + + pdb, err := store.NewPersonalDB(filepath.Join(dir, "personal.db")) + if err != nil { + t.Fatalf("创建个人库失败: %v", err) + } + t.Cleanup(func() { pdb.Close() }) + + if err := account.InitCrypto("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); err != nil { + t.Fatalf("初始化加密失败: %v", err) + } + + // 注册 e2e 个人通道(幂等) + if _, ok := channel.Lookup("personal.alipay"); !ok { + channel.Register(e2eStubPersonalChannel{}) + } + + accountPool := account.NewPool() + lg, err := ledger.New(ldb, "") + if err != nil { + t.Fatalf("创建账本失败: %v", err) + } + svc := order.New(st, lg, accountPool, upstream.NewPool()) + waitPayPool := pool.NewWaitPayPool(30) + svc.SetWaitPayHooks(&order.WaitPayHooks{ + Add: waitPayPool.Add, + Upsert: waitPayPool.Upsert, + Remove: waitPayPool.Remove, + }) + // 设备维度 + 动态窗口(与生产装配一致) + waitPayPool.SetDeviceOf(func(accountID int64) (string, bool) { + return "dev-e2e", true + }) + waitPayPool.SetWindowCalc(func(active, total int) time.Duration { return 5 * time.Minute }) + + app := &App{ + store: st, + personalDB: pdb, + accountPool: accountPool, + orders: svc, + confirmSched: reconciler.NewScheduler(st, svc, nil, nil), + waitPayPool: waitPayPool, + } + return app, pdb +} + +// TestE2E_PersonalOrderFullLifecycle 个人免签完整资金闭环: +// 下单 → 备注码匹配 → 入账(账本写入) → 回调 outbox 生成 → 订单终态 SUCCESS。 +func TestE2E_PersonalOrderFullLifecycle(t *testing.T) { + ctx := context.Background() + app, pdb := newE2EEnv(t) + + // 账户池:一个可用个人账户(CreateOrder 选账户用) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "e2e账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/e2e", + }) + // 设备 + 绑定账户(监听确认需要设备上下文) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-e2e") + + // ① 下单(走 CreateOrder 完整路径,stub 通道返回备注码 8833) + res, err := app.orders.CreateOrder(ctx, &order.CreateRequest{ + OutTradeNo: "E2E-001", + Type: "personal.alipay", + Name: "测试商品", + Money: "10.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "e2e-buyer-1", + }) + if err != nil { + t.Fatalf("下单失败: %v", err) + } + if res.Order.Status != model.StatusWaitPay { + t.Fatalf("下单后应 WAIT_PAY,实际 %s", res.Order.Status) + } + if res.Order.NoteCode != "8833" { + t.Fatalf("订单应携带备注码 8833,实际 %q", res.Order.NoteCode) + } + + // ② 账本监听:用户已转账 10.00,账本页出现收款行(携带备注码 8833) + rows := []capture.AlipayLedgerRow{{ + Payer: "*测", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+10.00", AmountMinor: 1000, + Note: "8833", + }} + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("dispatchLedgerRows 失败: %v", err) + } + + // ③ 订单入账 + 账本写入 + 回调 outbox + o, err := app.store.GetOrder(ctx, "E2E-001") + if err != nil || o == nil { + t.Fatalf("查询订单失败: %v", err) + } + if o.Status != model.StatusSuccess { + t.Fatalf("订单应 TRADE_SUCCESS,实际 %s", o.Status) + } + if o.PaidAt.IsZero() { + t.Fatalf("订单应记录 PaidAt") + } + if o.DeviceID != dev.DeviceID { + t.Fatalf("监听确认后订单应回填设备 dev-e2e,实际 %q", o.DeviceID) + } + + // 回调 outbox 应有通知任务(status=pending) + pending, err := app.store.CountNotifyTasks(ctx, "pending") + if err != nil { + t.Fatalf("CountNotifyTasks: %v", err) + } + if pending < 1 { + t.Fatalf("入账后应生成回调任务, pending=%d", pending) + } + + // ④ 池内订单应已移除(终态清理) + if e := app.waitPayPool.MatchByNote(1, "8833", time.Now().Add(-time.Hour)); e != nil { + t.Fatalf("终态订单应从池中移除, 仍命中: %+v", e) + } +} + +// TestE2E_DuplicateLedgerNoDoubleSettle 重复推送同一收款行 → 幂等不重复入账 +func TestE2E_DuplicateLedgerNoDoubleSettle(t *testing.T) { + ctx := context.Background() + app, pdb := newE2EEnv(t) + + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "e2e账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/e2e", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-e2e-dup") + + if _, err := app.orders.CreateOrder(ctx, &order.CreateRequest{ + OutTradeNo: "E2E-DUP", Type: "personal.alipay", Name: "测试", Money: "5.00", + NotifyURL: "http://example.com/notify", BuyerKey: "e2e-buyer-dup", + }); err != nil { + t.Fatalf("下单失败: %v", err) + } + + row := capture.AlipayLedgerRow{ + Payer: "*重", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+5.00", AmountMinor: 500, Note: "8833", + } + // 第一次推送 → 入账 + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, []capture.AlipayLedgerRow{row}); err != nil { + t.Fatalf("首次 dispatch 失败: %v", err) + } + // 第二次推送同一行(模拟轮询重复读取)→ 应去重,不重复入账 + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, []capture.AlipayLedgerRow{row}); err != nil { + t.Fatalf("二次 dispatch 失败: %v", err) + } + + o, _ := app.store.GetOrder(ctx, "E2E-DUP") + if o.Status != model.StatusSuccess { + t.Fatalf("订单应 SUCCESS: %s", o.Status) + } + // 重复推送不应重复入账:回调 outbox 对同一订单只有一条待发任务(订单 ID 唯一) + tasks, err := app.store.GetPendingNotifyTasks(ctx, 10) + if err != nil { + t.Fatalf("GetPendingNotifyTasks: %v", err) + } + dupCount := 0 + for _, task := range tasks { + if task.OrderID == o.ID { + dupCount++ + } + } + if dupCount > 1 { + t.Fatalf("重复推送不应产生重复回调任务, OrderID=%d 任务数=%d", o.ID, dupCount) + } +} + +// TestE2E_NoOrderNoSettle 收款行无匹配订单 → 不产生任何入账/回调 +func TestE2E_NoOrderNoSettle(t *testing.T) { + ctx := context.Background() + app, pdb := newE2EEnv(t) + + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "e2e账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/e2e", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-e2e-none") + + // 无订单的收款行 + row := capture.AlipayLedgerRow{ + Payer: "*无", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+9.99", AmountMinor: 999, Note: "0000", + } + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, []capture.AlipayLedgerRow{row}); err != nil { + t.Fatalf("dispatch 失败(应静默不匹配): %v", err) + } + // 无订单不应产生回调任务 + pending, _ := app.store.CountNotifyTasks(ctx, "pending") + if pending != 0 { + t.Fatalf("无订单不应产生回调任务, 实际 %d", pending) + } +} diff --git a/cmd/gateway/handlers_epay.go b/cmd/gateway/handlers_epay.go new file mode 100644 index 0000000..e9d4489 --- /dev/null +++ b/cmd/gateway/handlers_epay.go @@ -0,0 +1,363 @@ +// paygateway 支付网关 handlers_epay(从 main.go 拆分,易支付下单/回调/查单) +package main + +import ( + "net/http" + "paygateway/internal/account" + "paygateway/internal/channel" + wechatch "paygateway/internal/channel/wechat" + "paygateway/internal/epay" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/notify" + "paygateway/internal/order" + "paygateway/internal/upstream" + "paygateway/internal/web" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +func (a *App) epayKeyFor(pid string) (string, bool) { + if a.upstreamPool == nil || pid == "" { + return "", false + } + u := a.upstreamPool.ByPid(pid) + if u == nil || u.Status != upstream.StatusEnabled || u.Key == "" { + return "", false + } + return u.Key, true +} + +// isExplicitPersonalType 是否显式指定个人免签通道(personal_alipay / personal_wxpay / personal_qq) +// 显式指定时不回退商家(用户明确选择个人通道,回退可能造成歧义) +func isExplicitPersonalType(epayType string) bool { + switch epayType { + case "personal_alipay", "personal_wxpay", "personal_qq": + return true + } + return false +} + +// personalChannelReady 检查个人通道是否有可用收款账户 +// (normal 状态 + 已配收款码 + 未冷却 + 未达日限额,任一满足即可接单) +func (a *App) personalChannelReady(name string) bool { + for _, acct := range a.accountPool.ListByChannel(name) { + if acct.Status != account.StatusNormal { + continue + } + if acct.QRContent == "" { + continue + } + if acct.FrozenUntil != nil && acct.FrozenUntil.After(time.Now()) { + continue + } + if acct.DailyLimit > 0 && acct.DailyUsed >= acct.DailyLimit { + continue + } + return true + } + return false +} + +func (a *App) handleEpaySubmit(c *gin.Context) { + params := collectParams(c) + + // 验签(严格多商户:pid 必须已登记且启用,密钥按 pid 路由) + key, ok := a.epayKeyFor(params["pid"]) + if !ok { + logger.Error("epay", "商户未登记或已禁用 ❌", "pid", params["pid"]) + c.String(http.StatusOK, "商户ID错误或已禁用") + return + } + if !epay.VerifySign(params, key) { + logger.Error("epay", "下单签名验证失败 ❌", "pid", params["pid"]) + c.String(http.StatusOK, "签名验证失败") + return + } + logger.Info("epay", "下单签名验证通过 ✅", "pid", params["pid"], "out_trade_no", params["out_trade_no"]) + + // 设备智能识别:优先 URL 参数 device,其次 User-Agent 自动判断 + // 电脑浏览器 → pc(收银台/扫码),手机浏览器 → mobile(WAP) + device := c.Query("device") + if device == "" { + ua := c.GetHeader("User-Agent") + if web.IsMobileUA(ua) { + device = "mobile" + } else { + device = "pc" + } + logger.Info("epay", "设备智能识别", "device", device, "ua", ua) + } + + buyerKey, fpKey := buyerKeyFor(c) + req := &order.CreateRequest{ + OutTradeNo: params["out_trade_no"], + Type: params["type"], + Name: params["name"], + Money: params["money"], + NotifyURL: params["notify_url"], + ReturnURL: params["return_url"], + PayMode: params["pay_type"], + Device: device, + BuyerKey: buyerKey, + FPKey: fpKey, + RawParams: params, + } + if req.OutTradeNo == "" || req.Money == "" || req.Type == "" { + c.String(http.StatusOK, "参数不完整") + return + } + // return_url 兜底重写:上游传内网/localhost → Referer/上游公网地址逐层替换 + // (return_url 是付款人浏览器的回跳,与 notify_url 的 SSRF 校验语义不同,允许改写) + req.ReturnURL = a.sanitizeReturnURL(c, req.OutTradeNo, params["pid"], req.ReturnURL) + params["return_url"] = req.ReturnURL + + // SSRF 防护:下单时即拒绝非法回调地址(scheme 白名单 + 可选私网封锁), + // 不让恶意 URL 进入 outbox 等待发送时才暴露 + if err := notify.ValidateNotifyURL(req.NotifyURL, a.cfg.NotifyAllowPrivateIPs); err != nil { + logger.Warn("epay", "notify_url 校验失败", "err", err, "out_trade_no", req.OutTradeNo) + c.String(http.StatusOK, "notify_url 非法: "+err.Error()) + return + } + + // 校验支付类型(epay 协议 → 通道注册名映射) + // 个人免签优先:personal.alipay/personal.wechat 已注册且启用 → 优先路由(0 费率) + chName := channel.ResolveEpayType(req.Type) + // 增强:个人通道无可用收款账户(未配置/冻结/限额/冷却)→ 自动回退商家通道 + // 仅对隐式类型(alipay/wxpay/qqpay)回退;显式 personal_alipay/personal_wxpay 不回退(报错提示更明确) + if strings.HasPrefix(chName, "personal.") && !isExplicitPersonalType(req.Type) { + if !a.personalChannelReady(chName) { + if mName, ok := channel.EpayTypeToChannel[req.Type]; ok { + logger.Info("epay", "个人通道无可用账户,回退商家通道", + "type", req.Type, "from", chName, "to", mName) + chName = mName + } + } + } + if chName == "" { + logger.Warn("epay", "不支持的支付类型", "type", req.Type) + c.String(http.StatusOK, "不支持的支付类型: "+req.Type) + return + } + // 同 IP 下单限流(防脚本批量刷单;仅个人免签通道启用,商家通道不限) + if a.cfg.MaxOrdersPerIPPerMin > 0 && strings.HasPrefix(chName, "personal.") { + ip := c.ClientIP() + if !a.ipLimiter.Allow(ip) { + logger.Warn("epay", "下单限流:IP 超限", "ip", ip, "limit", a.cfg.MaxOrdersPerIPPerMin, "out_trade_no", params["out_trade_no"]) + c.String(http.StatusOK, "下单过于频繁,请稍后重试") + return + } + } + if _, ok := channel.Get(chName); !ok { + logger.Warn("epay", "支付通道未注册", "type", req.Type, "channel", chName) + c.String(http.StatusOK, "不支持的支付类型: "+req.Type) + return + } + req.Type = chName // 转为通道注册名(如 "merchant.alipay" / "personal.alipay") + + res, err := a.orders.CreateOrder(c.Request.Context(), req) + if err != nil { + if res != nil && res.Reused { + // 复用已有订单:按原凭据重新渲染 + a.renderPayment(c, res.Existing) + return + } + logger.Error("epay", "下单失败", "err", err, "out_trade_no", req.OutTradeNo) + c.String(http.StatusOK, "下单失败: "+err.Error()) + return + } + // 进入确认调度器:超时未回调 → 主动查单兜底 + a.confirmSched.OnOrderCreated(res.Order) + // 账本页 adb 监听(docs/PLAN-ADB-LEDGER-IMPL.md §2.2):下单路由账户后激活该账户 + // 监听循环 → 高频轮询 + 高优先级窗口申请(下单后用户很可能马上付款,1s 级确认) + if a.ledgermon != nil && strings.HasPrefix(res.Order.Channel, "personal.") { + a.ledgermon.ActivateAccount(res.Order.AccountID) + } + a.renderPayment(c, res.Order) +} + +// renderPayment 根据订单凭据渲染支付页面 +func (a *App) renderPayment(c *gin.Context, o *model.Order) { + c.Header("Content-Type", "text/html; charset=utf-8") + // 已支付订单:直接渲染成功页(付款人刷新/回退支付 URL 时不该再看到收款码) + if o.Status == model.StatusSuccess { + a.setPaymentPageHeaders(c) + c.String(http.StatusOK, web.BuildSuccessPage(o, o.Channel)) + return + } + chName := o.Channel + if chName == "" { + chName = "alipay" + } + // 个人免签通道:直发码支付页(真实收款码 + 关单倒计时) + if strings.HasPrefix(chName, "personal.") && o.QrCode != "" { + var imgSrc string + if strings.HasPrefix(o.QrCode, "/qr/real") { + imgSrc = o.QrCode + } else { + // 历史数据兜底:旧套码 token 二维码内嵌渲染 + imgSrc = web.QRPngDataURI(o.QrCode) + } + // 关单时间(个人免签订单):动态窗口取池内 CloseAt(设备负载驱动); + // 否则 = 创建时间 + 动态下限 ORDER_CLOSE_MIN(动态窗口已启用时绝不回退静态 + // ORDER_CLOSE_MINUTES,否则环境配置会拉长个人订单关单+轮询时长)。0=未开启自动关单。 + expireAt := a.personalOrderCloseAt(o) + a.setPaymentPageHeaders(c) + // 支付方式切换无需页面按钮:上游换支付模式入口(同号不同 type)下单时自动迁移订单 + c.String(http.StatusOK, web.BuildPersonalQRPage(o, imgSrc, chName, expireAt)) + return + } + switch { + case o.QrCode != "": + // 有二维码优先展示二维码页面(扫码/H5 备用 URL 不覆盖) + a.setPaymentPageHeaders(c) + c.String(http.StatusOK, web.BuildQRPage(o, o.QrCode, chName)) + case strings.Contains(o.PayURL, "alipay.trade.page.pay"): + c.String(http.StatusOK, web.BuildRedirectPage(o, o.PayURL, "正在跳转到支付宝收银台...")) + case strings.Contains(o.PayURL, "alipay.trade.wap.pay"): + c.String(http.StatusOK, web.BuildRedirectPage(o, o.PayURL, "正在跳转到支付宝...")) + case strings.Contains(o.PayURL, "wx.tenpay.com") || strings.Contains(o.PayURL, "weixin://"): + c.String(http.StatusOK, web.BuildRedirectPage(o, o.PayURL, "正在跳转到微信支付...")) + default: + c.String(http.StatusOK, "订单创建成功,无支付凭据") + } +} + +// personalOrderCloseAt 计算个人免签订单的有效关单时间点;零值=未开启自动关单。 +// 语义(与关单扫描器 CloseExpiredOrders 对齐,避免页面倒计时与后台关单漂移): +// 1. 动态窗口模式:取 WAIT_PAY 池内 CloseAt(设备负载驱动,最精确) +// 2. 池内无该订单:动态窗口已启用则用动态下限 ORDER_CLOSE_MIN +// 3. 动态窗口未启用:用静态 ORDER_CLOSE_MINUTES +func (a *App) personalOrderCloseAt(o *model.Order) time.Time { + if o == nil { + return time.Time{} + } + if a.waitPayPool != nil { + if ct, ok := a.waitPayPool.CloseAtOf(o.OutTradeNo); ok && !ct.IsZero() { + return ct + } + } + var win time.Duration + if a.cfg.OrderCloseMin > 0 && a.cfg.OrderCloseMax > a.cfg.OrderCloseMin { + win = time.Duration(a.cfg.OrderCloseMin) * time.Minute + } else if a.cfg.OrderCloseMinutes > 0 { + win = time.Duration(a.cfg.OrderCloseMinutes) * time.Minute + } + if win <= 0 || (o.CreatedAt.IsZero()) { + return time.Time{} + } + closeAt := o.CreatedAt.Add(win) + if !o.SessionExpiresAt.IsZero() && o.SessionExpiresAt.Before(closeAt) { + return o.SessionExpiresAt + } + return closeAt +} + +// setPaymentPageHeaders 支付页安全响应头:防二维码替换/爬取/点击劫持 +// script-src 'unsafe-inline':支付页内联 JS 用于浏览器指纹采集(防换 IP 刷单) +// connect-src 'self':轮询 /qr/status 的 XHR 与指纹 sendBeacon 需要;缺省回落 default-src 'none' +// 会静默拦截所有 XHR(真机事故:倒计时能跳 /qr/expired 但支付成功永远探测不到,根因即此)。 +// Content-Type 必须显式 text/html:c.String 默认 text/plain 会把 HTML 当源码显示 +func (a *App) setPaymentPageHeaders(c *gin.Context) { + c.Header("Content-Type", "text/html; charset=utf-8") + c.Header("Content-Security-Policy", "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'self'") + c.Header("X-Content-Type-Options", "nosniff") + c.Header("Referrer-Policy", "no-referrer") +} + +// ========== 支付宝异步回调 ========== +func (a *App) handleAlipayNotify(c *gin.Context) { + ch, ok := channel.Get("merchant.alipay") + if !ok { + c.String(http.StatusOK, "fail") + return + } + nr, err := ch.VerifyNotify(c.Request.Context(), c.Request) + if err != nil { + logger.Error("epay", "支付宝回调验签失败", "err", err) + c.String(http.StatusOK, "fail") + return + } + logger.Info("epay", "支付宝验签通过 ✅", "out_trade_no", nr.OutTradeNo, "status", nr.Status) + + if nr.Status == model.StatusSuccess { + if ok, err := a.orders.SettlePayment(c.Request.Context(), nr); err != nil || !ok { + logger.Error("epay", "支付结算失败", "err", err, "ok", ok) + c.String(http.StatusOK, "fail") + return + } + a.confirmSched.OnSettled(nr.OutTradeNo) + } else if nr.Status == model.StatusClosed { + logger.Info("epay", "订单关闭通知", "out_trade_no", nr.OutTradeNo) + a.confirmSched.OnClosed(nr.OutTradeNo) + } + + // 支付宝要求返回 success + c.String(http.StatusOK, "success") +} + +// ========== 微信异步回调 ========== +func (a *App) handleWechatNotify(c *gin.Context) { + ch, ok := channel.Get("merchant.wechat") + if !ok { + c.String(http.StatusOK, wechatch.NotifyFailResponse()) + return + } + nr, err := ch.VerifyNotify(c.Request.Context(), c.Request) + if err != nil { + logger.Error("epay", "微信回调验签失败", "err", err) + c.String(http.StatusOK, wechatch.NotifyFailResponse()) + return + } + logger.Info("epay", "微信验签通过 ✅", "out_trade_no", nr.OutTradeNo, "status", nr.Status) + + if nr.Status == model.StatusSuccess { + if ok, err := a.orders.SettlePayment(c.Request.Context(), nr); err != nil || !ok { + logger.Error("epay", "支付结算失败", "err", err, "ok", ok) + c.String(http.StatusOK, wechatch.NotifyFailResponse()) + return + } + a.confirmSched.OnSettled(nr.OutTradeNo) + } else if nr.Status == model.StatusClosed { + logger.Info("epay", "订单关闭通知", "out_trade_no", nr.OutTradeNo) + a.confirmSched.OnClosed(nr.OutTradeNo) + } + + // 微信要求返回 XML SUCCESS + c.String(http.StatusOK, wechatch.NotifySuccessResponse()) +} + +func (a *App) handleEpayQuery(c *gin.Context) { + params := make(map[string]string) + for k, v := range c.Request.URL.Query() { + if len(v) > 0 { + params[k] = v[0] + } + } + key, ok := a.epayKeyFor(params["pid"]) + if !ok || !epay.VerifySign(params, key) { + c.JSON(http.StatusOK, gin.H{"code": -1, "msg": "签名验证失败"}) + return + } + o, err := a.orders.QueryOrder(c.Request.Context(), params["out_trade_no"]) + if err != nil || o == nil { + c.JSON(http.StatusOK, gin.H{"code": -1, "msg": "订单不存在"}) + return + } + c.JSON(http.StatusOK, gin.H{ + "code": 1, + "msg": "success", + "out_trade_no": o.OutTradeNo, + "trade_no": o.GatewayNo, + "money": o.MoneyYuan(), + "trade_status": o.Status, + }) +} + +// handleQrRefresh 刷新二维码:为原订单重新生成二维码/备注码(保持原单号不变) +// 易支付协议 out_trade_no 是上游业务主键,改单号会导致回调上游找不到订单 → 入账失败 +// handleBindFingerprint 绑定浏览器指纹到订单 buyer_key(防换 IP 刷单) +// GET /personal/bind-fp?o={out_trade_no}&fp={base64(JSON指纹)} +// 指纹并入 buyer_key:原 buyer_key = H(ip|ua|device|buyer_id),追加指纹后抗换 IP 攻击。 diff --git a/cmd/gateway/handlers_qr.go b/cmd/gateway/handlers_qr.go new file mode 100644 index 0000000..ce0bd64 --- /dev/null +++ b/cmd/gateway/handlers_qr.go @@ -0,0 +1,250 @@ +// paygateway 支付网关 handlers_qr(从 main.go 拆分,QR 系接口/指纹/参数收集) +package main + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/web" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +func (a *App) handleBindFingerprint(c *gin.Context) { + orderNo := c.Query("o") + fpB64 := c.Query("fp") + if orderNo == "" || fpB64 == "" { + c.Status(http.StatusOK) + return + } + // base64 解码指纹 JSON(失败静默,指纹是增强非必须) + fpJSON, err := base64.StdEncoding.DecodeString(fpB64) + if err != nil || len(fpJSON) == 0 || len(fpJSON) > 1024 { + c.Status(http.StatusOK) + return + } + // 指纹单独存 fp_key 列(不改写 buyer_key): + // buyer_key 保持 = H(ip|ua|device|buyer_id) 不变,串行检查 buyer_key 或 fp_key 均命中; + // 换 IP 场景:buyer_key 变但 fp_key 同 → 仍被串行拦截 + fpHash := sha256.Sum256(fpJSON) + fpKey := fmt.Sprintf("%x", fpHash[:16]) + ctx := c.Request.Context() + o, err := a.store.GetOrder(ctx, orderNo) + if err != nil || o == nil || o.Status != model.StatusWaitPay { + c.Status(http.StatusOK) + return + } + if _, err := a.store.UpdateOrderStatus(ctx, orderNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.FPKey = fpKey + }); err != nil { + logger.Warn("personal", "指纹绑定失败", "err", err, "order", orderNo) + } + // Set-Cookie 写入指纹 hash:后续下单携带该 cookie,服务端按 fp_key 匹配(换 IP 串行兜底) + c.SetCookie("pg_fp", fpKey, 86400, "/", "", false, true) + logger.Info("personal", "浏览器指纹已绑定", "order", orderNo, "fp_key", fpKey[:8], "fp_len", len(fpJSON)) + c.Status(http.StatusOK) +} + +func (a *App) handleQrRefresh(c *gin.Context) { + oldNo := c.Query("out_trade_no") + if oldNo == "" { + c.String(http.StatusOK, "参数缺失: out_trade_no") + return + } + ctx := c.Request.Context() + + // 查原订单,校验状态 + o, err := a.store.GetOrder(ctx, oldNo) + if err != nil || o == nil { + c.String(http.StatusOK, "订单不存在") + return + } + if o.Status != model.StatusWaitPay { + c.String(http.StatusOK, "订单当前状态("+o.Status+")不可刷新") + return + } + + logger.Info("qr", "刷新二维码", "out_trade_no", oldNo) + + // 保持原单号刷新支付凭据(重新分配备注码 + 套码 token) + refresh, err := a.orders.RefreshPayment(ctx, o) + if err != nil { + logger.Error("qr", "刷新二维码失败", "err", err, "old", oldNo) + c.String(http.StatusOK, "刷新失败: "+err.Error()) + return + } + // 通知栏监听驱动:账本导航兜底已废弃(用户决策),刷新后由监听端通知推送匹配入账 + a.renderPayment(c, refresh) +} + +func (a *App) handleQrExpired(c *gin.Context) { + outNo := c.Query("out_trade_no") + provider := "" + if outNo != "" { + if o, err := a.store.GetOrder(c.Request.Context(), outNo); err == nil && o != nil { + if strings.HasSuffix(o.Channel, "wechat") { + provider = "微信" + } else if strings.HasSuffix(o.Channel, "alipay") { + provider = "支付宝" + } + } + } + a.setPaymentPageHeaders(c) + // 过期订单是终态,不提供复活入口(防僵尸订单刷单);引导返回上游重新下单 + c.String(http.StatusOK, web.BuildExpiredPage(provider, outNo, c.Query("return_url"))) +} + +// handleQrStatus 订单状态探测:过期页 JS 周期性轮询。 +// 上游同号重提复活订单后返回 alive=true,过期页自动跳回支付页继续支付。 +// 仅返回轻量 JSON,不携带敏感信息。 +func (a *App) handleQrStatus(c *gin.Context) { + outNo := c.Query("out_trade_no") + logger.Info("qr", "status 探测", "out_trade_no", outNo, "ua", c.Request.UserAgent()) + if outNo == "" { + c.JSON(http.StatusOK, gin.H{"alive": false}) + return + } + o, err := a.store.GetOrder(c.Request.Context(), outNo) + if err != nil || o == nil { + c.JSON(http.StatusOK, gin.H{"alive": false}) + return + } + if o.Status == model.StatusWaitPay && (o.PayURL != "" || o.QrCode != "") { + // 个人免签订单:关单时间已到(页面倒计时已归零 / 后台扫描器即将关单)→ 视为过期, + // 返回 alive=false,让过期页停在该分支,避免「过期页→/qr/refresh→支付页」死循环幽灵订单。 + if strings.HasPrefix(o.Channel, "personal.") { + if ct := a.personalOrderCloseAt(o); !ct.IsZero() && time.Now().After(ct) { + c.JSON(http.StatusOK, gin.H{"alive": false, "expired": true, "status": model.StatusClosed}) + return + } + } + c.JSON(http.StatusOK, gin.H{"alive": true, "channel": o.Channel}) + return + } + c.JSON(http.StatusOK, gin.H{"alive": false, "status": o.Status}) +} + +// handleQrSuccess 支付成功落地页:仅当订单状态为 TRADE_SUCCESS 时渲染成功页, +// 否则(未支付/已关闭/不存在)回退到支付页或过期页,防伪造"支付成功"页面。 +// GET /qr/success?out_trade_no={no} +func (a *App) handleQrSuccess(c *gin.Context) { + outNo := c.Query("out_trade_no") + if outNo == "" { + c.String(http.StatusOK, "参数缺失: out_trade_no") + return + } + o, err := a.store.GetOrder(c.Request.Context(), outNo) + if err != nil || o == nil { + c.String(http.StatusOK, "订单不存在") + return + } + if o.Status != model.StatusSuccess { + // 未支付/确认中 → 回到支付页继续支付;已关闭/失败 → 过期引导页 + if o.Status == model.StatusWaitPay || o.Status == model.StatusConfirming { + a.renderPayment(c, o) + return + } + a.setPaymentPageHeaders(c) + c.String(http.StatusOK, web.BuildExpiredPage("", outNo, o.ReturnURL)) + return + } + a.setPaymentPageHeaders(c) + c.String(http.StatusOK, web.BuildSuccessPage(o, o.Channel)) +} + +func (a *App) handleQrSwitch(c *gin.Context) { + outNo := c.Query("out_trade_no") + method := c.Query("method") // alipay / wechat + if outNo == "" || method == "" { + c.String(http.StatusOK, "参数缺失: out_trade_no / method") + return + } + ctx := c.Request.Context() + o, err := a.store.GetOrder(ctx, outNo) + if err != nil || o == nil { + c.String(http.StatusOK, "订单不存在") + return + } + if !strings.HasPrefix(o.Channel, "personal.") { + c.String(http.StatusOK, "该订单不支持切换支付方式") + return + } + // method → 个人免签通道名 + newChannel := "" + switch method { + case "alipay": + newChannel = "personal.alipay" + case "wechat": + newChannel = "personal.wechat" + } + if newChannel == "" || newChannel == o.Channel { + c.String(http.StatusOK, "无效的切换目标") + return + } + // 目标通道必须有可用收款账户(无账户切过去会白屏报错) + if !a.personalChannelReady(newChannel) { + c.String(http.StatusOK, "该支付方式暂不可用(无可用收款账户)") + return + } + logger.Info("qr", "切换支付方式", "out_trade_no", outNo, "from", o.Channel, "to", newChannel) + switched, err := a.orders.SwitchChannel(ctx, o, newChannel) + if err != nil { + logger.Error("qr", "切换支付方式失败", "err", err, "out_trade_no", outNo) + c.String(http.StatusOK, "切换支付方式失败: "+err.Error()) + return + } + a.renderPayment(c, switched) +} + +// collectParams 收集 GET query + POST form 参数 +// buyerKeyFor 计算买家环境唯一标识(买家级串行/防刷单)。 +// 环境 = 客户端 IP + User-Agent + device 参数 + 可选 buyer_id(下游可透传真实用户标识), +// SHA-256 哈希后作为 buyer_key;同环境同时只允许一笔待支付订单。 +// 浏览器指纹(cookie pg_fp)单独作为 fp_key 返回,用于换 IP 场景串行兜底。 +func buyerKeyFor(c *gin.Context) (buyerKey, fpKey string) { + ip := c.ClientIP() + ua := c.GetHeader("User-Agent") + device := c.Query("device") + bid := c.Query("buyer_id") + if ip == "" && bid == "" { + return "", "" + } + h := sha256.Sum256([]byte(ip + "|" + ua + "|" + device + "|" + bid)) + fp := c.GetHeader("Cookie") + if fp != "" { + for _, part := range strings.Split(fp, ";") { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "pg_fp=") { + fpKey = strings.TrimPrefix(part, "pg_fp=") + break + } + } + } + return fmt.Sprintf("%x", h[:16]), fpKey +} + +func collectParams(c *gin.Context) map[string]string { + params := make(map[string]string) + for k, v := range c.Request.URL.Query() { + if len(v) > 0 { + params[k] = v[0] + } + } + if c.Request.Method == http.MethodPost { + c.Request.ParseForm() + for k, v := range c.Request.PostForm { + params[k] = v[0] + } + } + return params +} + +// ========== 个人免签通道:监听端通信 ========== + +// decryptDeviceSecret 解密设备通信密钥(复用账户 AES 主密钥) +// 设备密钥以密文存 personal.db,这里用账户包的全局解密函数 diff --git a/cmd/gateway/ledger.go b/cmd/gateway/ledger.go new file mode 100644 index 0000000..9c8cb8c --- /dev/null +++ b/cmd/gateway/ledger.go @@ -0,0 +1,448 @@ +// paygateway 支付网关 ledger(从 main.go 拆分,账本监听/匹配引擎) +package main + +import ( + "context" + "fmt" + "paygateway/internal/account" + "paygateway/internal/admin/capture" + "paygateway/internal/channel" + "paygateway/internal/ledgermon" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/personal" + "paygateway/internal/pool" + "paygateway/internal/store" + "strings" + "time" +) + +func (a *App) newLedgerReader(d ledgermon.MonitoredDevice) ledgermon.LedgerReader { + switch d.Provider { + case "alipay": + return ledgermon.NewAlipayReader(adbMgr(), d.Serial, d.UserID) + case "wechat": + return ledgermon.NewWechatReader(adbMgr(), d.Serial, d.UserID) + } + return nil +} + +// dispatchLedgerRows 账本新收款行推送(ledgermon.DispatchFunc 实现): +// 复用个人通道匹配入账链路(/personal/push 同款),source=ledger,窗口 30min(账本轮询兜底)。 +// 匹配顺序(池消费模型,每笔金额独立匹配池中订单): +// 1. 备注码精确匹配(微信账本行自带"付款方备注";支付宝账本页无备注需探测) +// 2. 金额+时间窗口匹配:唯一候选 → 直接命中(免探测);零候选 → no_order 转人工 +// 3. 多候选歧义:支付宝可点金额进详情页探测备注码消歧(探测失败/无备注码 → 保持 ambiguous 转人工, +// 禁止自动挑选——资金安全不变量) +// +// 注:探测在 DeviceActor 串行窗口内执行(PollCmd.Execute → Dispatch),无 adb 并发冲突。 +func (a *App) dispatchLedgerRows(ctx context.Context, deviceID string, provider string, space int, rows []capture.AlipayLedgerRow) error { + // 1. 设备加载(验签不需要;匹配链路需要 dev.ID/AccountID 解析账户集合) + dev, err := a.personalDeviceCtx(ctx, deviceID) + if err != nil { + logger.Warn("ledgermon", "账本推送:设备加载失败", "err", err, "device_id", deviceID) + return err + } + if dev.Status == personal.DeviceFrozen { + logger.Warn("ledgermon", "账本推送:设备已冻结,跳过", "device_id", deviceID) + return nil + } + // 2. 逐行匹配入账(窗口 30min:账本轮询兜底语义,覆盖读取间隔+推送延迟) + window := 30 * time.Minute + pushed := false + for _, r := range rows { + // 幂等去重:同一设备同原文哈希只处理一次(fingerprint=时间|金额,同指纹跨轮去重) + raw := fmt.Sprintf("ledger:%s|%s|%s", r.Payer, r.Time, r.Amount) + rawHash := fmt.Sprintf("%x", sha256Hash([]byte(raw))) + dup, err := a.personalDB.PushExists(ctx, deviceID, rawHash) + if err != nil { + logger.Error("ledgermon", "账本推送去重查询失败", "err", err, "device_id", deviceID) + continue + } + if dup { + continue + } + // 大额告警(对齐原 /personal/push 链路):账本行金额超阈值 → 告警留痕。 + // listener 下线后服务端是唯一告警点,走 alertC(原实现仅 logger,此处升级)。 + if a.cfg != nil && a.cfg.PersonalBigAmount > 0 && r.AmountMinor >= a.cfg.PersonalBigAmount { + logger.Warn("ledgermon", "大额收款告警", + "device_id", deviceID, "provider", provider, + "amount", r.AmountMinor, "row", raw, "threshold", a.cfg.PersonalBigAmount) + if a.alertC != nil { + a.alertC.Send("warn", "大额收款", + fmt.Sprintf("账本监听:设备 %s 收到 %d 分,需留意", deviceID, r.AmountMinor)) + } + } + orderNo, matched, orderAccountID, effNote, err := a.matchLedgerRow(ctx, dev, provider, space, r, window, a.ledgerProber()) + if err != nil { + logger.Error("ledgermon", "账本订单匹配失败", "err", err, "device_id", deviceID, "amount", r.AmountMinor) + continue + } + r.Note = effNote // 审计:流水记录实际用于消歧的备注码(探测命中时非空) + // 记录推送流水(审计,先记后入账) + push := &personal.Push{ + DeviceID: deviceID, Provider: provider, AmountMinor: r.AmountMinor, + Note: r.Note, RawHash: rawHash, Raw: raw, + MatchedOrder: orderNo, MatchResult: matched, Space: space, + } + if err := a.personalDB.InsertPush(ctx, push); err != nil { + logger.Error("ledgermon", "账本推送流水写入失败", "err", err, "device_id", deviceID) + } + pushed = true + switch matched { + case "matched": + // 账户冻结守卫(对齐原 /personal/push 链路):冻结账户不自动入账,转异常池人工核实 + //(钱已在账户里,不会丢;只是不自动确认)。原实现仅 continue 导致行只进审计流水 + // 不进异常池、且指纹已提交永不再现——2026-08-30 迁移补 EnqueueException 保全核查路径。 + if acct := a.accountPool.GetByID(orderAccountID); acct != nil && acct.Status == account.StatusFrozen { + logger.Warn("ledgermon", "账户已冻结,账本收款转人工", "order", orderNo, "account_id", orderAccountID) + if cur, cerr := a.store.GetOrder(ctx, orderNo); cerr == nil && cur != nil { + a.orders.EnqueueException(ctx, cur, model.ExReasonListenOffline, "PAID_WHILE_FROZEN", + "", raw, "账本监听:收款到达时账户处于冻结态,需人工核实际收款后入账") + } + continue + } + nr := &channel.NotifyResult{ + Valid: true, + OutTradeNo: orderNo, + ChannelTxID: fmt.Sprintf("LEDGER-%s-%d", deviceID, time.Now().UnixNano()), + Status: model.StatusSuccess, + AmountMinor: r.AmountMinor, + Payer: "ledger:" + deviceID, + DeviceID: deviceID, + AccountID: orderAccountID, + RawPayload: raw, + } + if ok, serr := a.orders.SettlePayment(ctx, nr); serr != nil { + logger.Error("ledgermon", "账本入账失败", "err", serr, "order", orderNo) + continue + } else if !ok { + // ok=false, serr=nil:并发已支付/已关单(幂等),非异常不重复入异常池 + if cur, cerr := a.store.GetOrder(ctx, orderNo); cerr == nil && cur != nil && cur.Status == model.StatusClosed { + a.orders.EnqueueException(ctx, cur, model.ExReasonStateConflict, "TRADE_SUCCESS_AFTER_CLOSED", + nr.ChannelTxID, raw, "账本监听:关单后收到收款记录,需人工核对实际收款") + } + continue + } + // 回写订单 device_id(审计追溯) + if _, uerr := a.store.UpdateOrderStatus(ctx, orderNo, model.StatusSuccess, model.StatusSuccess, func(upd *model.Order) { + upd.DeviceID = deviceID + }); uerr != nil { + logger.Warn("ledgermon", "回写订单 device_id 失败", "err", uerr, "order", orderNo) + } + a.confirmSched.OnSettled(orderNo) + logger.Info("ledgermon", "账本监听收款入账成功 ✅", + "order", orderNo, "amount", r.AmountMinor, "device", deviceID, "row", raw) + case "settled_dup": + // 同一笔收款已被通知栏/账本另一链路入账(真机实测 5s 内双链同捕) + logger.Info("ledgermon", "账本重复捕获,另一链路已入账(幂等跳过)", + "order", orderNo, "amount", r.AmountMinor, "device", deviceID) + case "no_order": + a.saveUnmatched(ctx, provider, r.AmountMinor, r.Note, raw, r.Payer, deviceID) + logger.Warn("ledgermon", "账本无订单收款(需人工处理)", "device", deviceID, "amount", r.AmountMinor) + default: // ambiguous + a.saveUnmatched(ctx, provider, r.AmountMinor, r.Note, raw, r.Payer, deviceID) + logger.Warn("ledgermon", "账本多订单歧义(需人工确认)", "device", deviceID, "amount", r.AmountMinor) + } + } + // 有推送流水则刷新 last_push(管理后台展示真实推送时间;listener 下线后由账本链路维护) + if pushed { + if err := a.personalDB.SetDevicePushTime(ctx, deviceID); err != nil { + logger.Warn("ledgermon", "更新设备推送时间失败", "err", err, "device_id", deviceID) + } + } + return nil +} + +// alipayNoteProber 支付宝备注码探测函数(多候选歧义消解用;注入便于测试) +// 生产 = probeAlipayLedgerNote(adb 详情页探测);测试 = 桩返回固定备注码。 +type alipayNoteProber func(ctx context.Context, dev *personal.Device, bounds string) (string, error) + +// matchLedgerRow 单账本行池消费匹配: +// 1. matchPersonalOrder:备注码精确(微信行自带)> 金额+时间窗口(唯一候选直接命中,免探测) +// 2. 多候选歧义(支付宝且行有金额坐标)→ prober 探测详情页备注码 → 重新匹配消歧 +// 3. 探测失败/无备注码/重新匹配仍歧义 → 保持 ambiguous(禁止自动挑选,资金安全) +// +// 返回 (订单号, 匹配结果, 订单账户ID, 实际备注码, error)。实际备注码供审计流水记录 +// (探测命中时非空,未命中沿用行内 Note)。 +func (a *App) matchLedgerRow(ctx context.Context, dev *personal.Device, provider string, space int, r capture.AlipayLedgerRow, window time.Duration, prober alipayNoteProber) (string, string, int64, string, error) { + orderNo, matched, orderAccountID, err := a.matchPersonalOrder(ctx, dev, provider, r.Note, r.AmountMinor, time.Now(), window, space) + if err != nil { + return "", "", 0, r.Note, err + } + effNote := r.Note + // 多候选歧义消解:支付宝账本行无备注 → 探测详情页备注码后重新匹配(池消费模型第 3 步)。 + // 探测耗时 ≈3.5s/行(点击+dump+返回),仅歧义行触发(金额唯一已短路免探测)。 + if matched == "ambiguous" && provider == "alipay" && r.AmountBounds != "" && prober != nil && a.waitPayPool != nil { + probed, perr := prober(ctx, dev, r.AmountBounds) + if perr != nil { + logger.Warn("ledgermon", "支付宝备注码探测失败,保持多候选转人工", + "err", perr, "device_id", dev.DeviceID, "amount", r.AmountMinor) + } else if probed != "" { + orderNo2, matched2, acct2, err2 := a.matchPersonalOrder(ctx, dev, provider, probed, r.AmountMinor, time.Now(), window, space) + if err2 != nil { + logger.Error("ledgermon", "探测备注码重新匹配失败", "err", err2, "device_id", dev.DeviceID, "amount", r.AmountMinor) + } else if matched2 == "matched" { + effNote = probed + logger.Info("ledgermon", "支付宝备注码探测消歧成功 ✅", + "order", orderNo2, "amount", r.AmountMinor, "note", probed, "device", dev.DeviceID) + return orderNo2, matched2, acct2, effNote, nil + } + } + } + return orderNo, matched, orderAccountID, effNote, nil +} + +// ledgerProber 返回备注码探测函数:测试注入桩优先(alipayProber 字段),nil 回退生产 adb 探针。 +func (a *App) ledgerProber() alipayNoteProber { + if a.alipayProber != nil { + return a.alipayProber + } + return a.probeAlipayLedgerNote +} + +// probeAlipayLedgerNote 生产备注码探针:adb 详情页探测(DeviceActor 串行窗口内执行,无并发冲突)。 +// 无可用 serial 时返回错误(调用方保持歧义转人工,不阻塞入账)。 +func (a *App) probeAlipayLedgerNote(ctx context.Context, dev *personal.Device, bounds string) (string, error) { + serial := deviceSerialFromDevice(dev) + if serial == "" { + return "", fmt.Errorf("设备无可用 adb serial") + } + return capture.ProbeAlipayNote(ctx, adbMgr(), serial, bounds) +} +func (a *App) personalDeviceCtx(ctx context.Context, deviceID string) (*personal.Device, error) { + enc, err := a.personalDB.GetDevice(ctx, deviceID) + if err != nil { + return nil, err + } + if enc == nil { + return nil, fmt.Errorf("设备未登记: %s", deviceID) + } + secret, err := decryptDeviceSecret(enc.SecretEnc) + if err != nil { + return nil, fmt.Errorf("解密设备密钥失败: %w", err) + } + return &personal.Device{ + ID: enc.ID, DeviceID: enc.DeviceID, Name: enc.Name, + AccountID: enc.AccountID, Provider: enc.Provider, Secret: secret, + Status: personal.DeviceStatus(enc.Status), + AdbHost: enc.AdbHost, + AdbPort: enc.AdbPort, + AdbWifiHost: enc.AdbWifiHost, + AdbWifiPort: enc.AdbWifiPort, + SetupProgress: enc.SetupProgress, + TsIP: enc.TsIP, + AdbListen: enc.AdbListen != 0, + ControlState: enc.ControlState, + }, nil +} + +// matchPersonalOrder 匹配订单(通知栏监听方案:备注码可选加速 + 金额时间窗口) +// 1. 备注码精确匹配:note 非空且命中唯一 WAIT_PAY 订单 → 直接命中(通知栏方案下通常为空,自禁用) +// 2. 金额+时间窗口:候选唯一 → matched;多个 → ambiguous(转人工,禁止自动挑选);零个 → no_order +// +// 时间窗口语义(2026-08-30 通知栏方案修正): +// - anchor 为收款发生时刻(账本链路传 time.Now();通知栏链路传通知 when 毫秒时间戳); +// - 窗口 = [anchor-30min, anchor]:订单必须创建于收款之前(收款时订单必已存在), +// 且不早于窗口下界(防止远古订单被新收款误匹配)。 +// 不能用纯下界(通知不清理会一直在,旧通知会错误匹配之后创建的新订单)。 +// +// 返回 (订单号, 匹配结果, 订单账户ID, error);匹配结果: matched / no_order / ambiguous +// 订单账户ID 用于入账账本记录(区分主账户/附加账户收款),0 表示未匹配成功。 +func (a *App) matchPersonalOrder(ctx context.Context, dev *personal.Device, provider, note string, amountMinor int64, anchor time.Time, window time.Duration, space int) (string, string, int64, error) { + since := anchor.Add(-window) + // 设备可服务的账户集合(主账户 + 附加账户,应用双开多账户): + // 匹配时订单账户必须属于该集合(防串单),通道校验也按该集合的通道判断。 + acctIDs := store.DeviceAccountIDs(ctx, a.personalDB, dev.ID, dev.AccountID) + inAcctIDs := func(id int64) bool { + for _, v := range acctIDs { + if v == id { + return true + } + } + return false + } + // 空间账户定位:按通知来源空间(0主/999双开) + 设备 + 平台 定位该空间专属账户。 + // 通过 device_qrcodes 拿该空间的收款码 → 匹配账户池中同收款码的账户, + // 匹配时优先只在该账户的订单中找(区分双开账户收款,杜绝跨空间错配)。 + spaceAcctID := a.accountIDForSpace(ctx, dev, provider, space) + if spaceAcctID > 0 && !inAcctIDs(spaceAcctID) { + spaceAcctID = 0 // 空间账户不在设备绑定集合 → 不启用空间过滤(数据不一致兜底) + } + // 设备可服务通道集合(绑定账户的通道,如 {personal.alipay, personal.wechat}) + servesChannel := func(ch string) bool { + if !strings.HasPrefix(ch, "personal.") { + return false + } + for _, id := range acctIDs { + if acct := a.accountPool.GetByID(id); acct != nil && acct.Channel == ch { + return true + } + } + return false + } + + // 1. 备注码精确匹配(首选,资金不错配核心): + // 内存池索引优先(空间账户定位 + noteCode 精确命中,零 DB); + // 池未命中/池不可用 → 回退 DB GetOrderByNoteCode(DB 是真相,两路径结果等价)。 + // 命中后统一 DB 二次校验(状态仍 WAIT_PAY 才采信)+ 通道/账户校验。 + if note != "" { + // 优先池匹配(生产路径;测试环境无池自动回退 DB) + var noteTarget string + if e := a.noteMatchFromPool(dev, provider, space, note, since, spaceAcctID); e != nil { + noteTarget = e.OutTradeNo + } else if o, err := a.store.GetOrderByNoteCode(ctx, note, since); err == nil && o != nil { + noteTarget = o.OutTradeNo + } + if noteTarget != "" { + // DB 二次校验:订单仍待支付才采信(池可能落后于 DB 状态变更) + o, err := a.store.GetOrder(ctx, noteTarget) + if err != nil { + return "", "", 0, err + } + if o != nil && o.Status == model.StatusWaitPay { + // 校验通道 + 账户双重匹配:订单账户必须 ∈ 设备绑定账户集合(防串单) + if servesChannel(o.Channel) && inAcctIDs(o.AccountID) { + return o.OutTradeNo, "matched", o.AccountID, nil + } + logger.Warn("personal", "备注码命中的订单账户设备不可服务", "note", note, + "order_channel", o.Channel, "order_account", o.AccountID, "device_id", dev.DeviceID) + } + } + } + + // 2. 金额 + 时间窗口匹配(通知栏监听方案主路径:仅 包名+金额+时间 信号) + // 候选收集后按数量分派:0→no_order;1→matched;≥2→ambiguous 转人工。 + // 空间账户已定位(spaceAcctID>0)时不回退设备集合:钱落在该空间的收款码上, + // 其他账户的订单不可能是归属方,回退 = 跨账户错配。 + orders, err := a.store.GetOrdersByAmount(ctx, amountMinor, since) + if err != nil { + return "", "", 0, err + } + var candidates []*model.Order + for _, o := range orders { + if !servesChannel(o.Channel) { + continue + } + if o.AccountID > 0 && !inAcctIDs(o.AccountID) { + continue + } + if spaceAcctID > 0 && o.AccountID != spaceAcctID { + continue + } + // 上界过滤:订单必须创建于收款时刻之前(收款时订单必已存在)。 + // 纯下界会允许"通知之后创建的新订单"被旧通知误匹配(通知不清理会一直在)。 + if o.CreatedAt.After(anchor) { + continue + } + candidates = append(candidates, o) + } + switch len(candidates) { + case 0: + // 零 WAIT_PAY 候选 ≠ 无订单:通知栏/账本双链 5s 内同时捕获同一笔收款时, + // 胜者已把订单结算(TRADE_SUCCESS),败者这时匹配查不到 WAIT_PAY。 + // 回查本设备可服务账户的同金额最近已支付订单(按账户过滤,防同金额异地/他户撞车): + // - 过滤后又按空间账户精确限缩:1 笔 → settled_dup(另一链路已入账,幂等跳过) + // - 0 笔 → 真无订单(no_order) ≥2 笔 → 同账户金额歧义,保持 no_order(资金安全,不猜,转人工) + settled, serr := a.store.GetRecentSettledByAmount(ctx, amountMinor, since, acctIDs) + if serr == nil { + var owned []*model.Order + for _, o := range settled { + if servesChannel(o.Channel) && (o.AccountID == 0 || inAcctIDs(o.AccountID)) { + if spaceAcctID > 0 && o.AccountID != spaceAcctID { + continue + } + owned = append(owned, o) + } + } + if len(owned) == 1 { + logger.Info("personal", "重复捕获判定:同账户订单已由另一链路入账,幂等跳过", + "order", owned[0].OutTradeNo, "amount", amountMinor, "device", dev.DeviceID) + return owned[0].OutTradeNo, "settled_dup", owned[0].AccountID, nil + } + if len(owned) > 1 { + logger.Warn("personal", "同账户同金额多笔已支付,不可归属,保持 no_order 转人工", + "amount", amountMinor, "device", dev.DeviceID, "settled", len(owned)) + return "", "no_order", 0, nil + } + } + return "", "no_order", 0, nil + case 1: + logger.Info("personal", "金额+时间窗口匹配成功", "order", candidates[0].OutTradeNo, + "amount", amountMinor, "device", dev.DeviceID, "account_id", candidates[0].AccountID) + return candidates[0].OutTradeNo, "matched", candidates[0].AccountID, nil + default: + ids := make([]string, 0, len(candidates)) + for _, o := range candidates { + ids = append(ids, fmt.Sprintf("%s(acct=%d)", o.OutTradeNo, o.AccountID)) + } + logger.Warn("personal", "同金额多候选,拒绝自动入账转人工", + "amount", amountMinor, "device", dev.DeviceID, "candidates", strings.Join(ids, ", ")) + return "", "ambiguous", 0, nil + } +} + +// noteMatchFromPool 备注码池匹配:优先空间账户(spaceAcctID>0 时只在该账户桶内找, +// 杜绝跨空间错配);未定位空间账户时遍历设备绑定账户桶。返回池条目或 nil。 +// 注意:仅池内索引匹配,调用方必须 DB 二次校验。 +func (a *App) noteMatchFromPool(dev *personal.Device, provider string, space int, note string, since time.Time, spaceAcctID int64) *pool.Entry { + if a.waitPayPool == nil { + return nil + } + if spaceAcctID > 0 { + if e := a.waitPayPool.MatchByNote(spaceAcctID, note, since); e != nil { + return e + } + return nil // 空间已定位:其他账户桶的备注码不可能是本空间收款(防跨空间错配) + } + for _, id := range store.DeviceAccountIDs(context.Background(), a.personalDB, dev.ID, dev.AccountID) { + if e := a.waitPayPool.MatchByNote(id, note, since); e != nil { + return e + } + } + return nil +} + +// accountIDForSpace 按通知来源空间定位设备专属账户: +// 查 device_qrcodes(设备+平台+用户空间)的最新收款码 → 与账户池中账户的 QRContent 匹配。 +// space=0(主空间)与 space=999(双开)都解析:主空间收款只匹配主账户订单、双开只匹配双开账户订单, +// 彻底消除同设备同金额下主/双开账户错配。查不到(无码/未绑定/码不匹配)返回 0, +// 此时匹配范围放宽为设备绑定账户集合(歧义由多候选拒绝机制兜底,不再自动挑选)。 +func (a *App) accountIDForSpace(ctx context.Context, dev *personal.Device, provider string, space int) int64 { + if dev == nil { + return 0 + } + qr, err := a.personalDB.GetLatestQrcodeByPlatformUser(ctx, dev.DeviceID, provider, space) + if err != nil || qr == nil || qr.QRContent == "" { + return 0 + } + for _, id := range store.DeviceAccountIDs(ctx, a.personalDB, dev.ID, dev.AccountID) { + acct := a.accountPool.GetByID(id) + if acct != nil && acct.QRContent == qr.QRContent { + return acct.ID + } + } + return 0 +} + +// resolvePersonalQr 校验套码 token 并解析 (订单, 个人通道, 账户);/mq 与 /qr/real 共用 +// 校验链:token 签名/过期 → 订单存在 → WAIT_PAY → token 与订单匹配 → 账户收款码存在 + +// ledgerUserSpaceOf 账户所属用户空间:收款码内容匹配该设备 (平台, 用户空间) 最新码。 +// 主空间(user 0)与双开(user 999)的码内容不同,用 QRContent 精确匹配归属; +// 匹配不到(无码/未上报)默认主空间 0。 +func ledgerUserSpaceOf(ctx context.Context, pdb store.PersonalStore, deviceID, provider, qrContent string) int { + if qrContent == "" { + return 0 + } + for _, space := range []int{999, 0} { + qr, err := pdb.GetLatestQrcodeByPlatformUser(ctx, deviceID, provider, space) + if err == nil && qr != nil && qr.QRContent == qrContent { + return space + } + } + return 0 +} + +// newLedgerReader 按设备构造账本读取器(ledgermon.ReaderFactory 实现)。 +// Phase 1 仅支付宝 dump 文本通道;微信 OCR 通道 Phase 2 接入。 diff --git a/cmd/gateway/ledger_match_test.go b/cmd/gateway/ledger_match_test.go new file mode 100644 index 0000000..2963622 --- /dev/null +++ b/cmd/gateway/ledger_match_test.go @@ -0,0 +1,804 @@ +package main + +// 资金核心链路测试:账本新收款行 → 订单匹配 → 入账(dispatchLedgerRows / matchPersonalOrder)。 +// 覆盖资金安全关键分支:唯一匹配入账、多候选拒绝(ambiguous)、无订单(no_order)、 +// 冻结账户守卫(不自动入账)、重复去重(幂等)、通道/账户归属校验。 + +import ( + "context" + "fmt" + "path/filepath" + "testing" + "time" + + "paygateway/internal/account" + "paygateway/internal/admin/capture" + "paygateway/internal/channel" + "paygateway/internal/ledger" + "paygateway/internal/model" + "paygateway/internal/order" + "paygateway/internal/personal" + "paygateway/internal/pool" + "paygateway/internal/reconciler" + "paygateway/internal/store" + "paygateway/internal/upstream" +) + +// newMatchTestEnv 构建匹配链路测试环境:业务库 + personal.db + 账户池 + 订单服务 + App +func newMatchTestEnv(t *testing.T) (*App, *store.PersonalDB) { + t.Helper() + dir := t.TempDir() + + st, err := store.NewSQLite(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("创建业务库失败: %v", err) + } + t.Cleanup(func() { st.Close() }) + + ldb, err := store.NewLedgerDB(filepath.Join(dir, "ledger.db")) + if err != nil { + t.Fatalf("创建账本库失败: %v", err) + } + t.Cleanup(func() { ldb.Close() }) + + pdb, err := store.NewPersonalDB(filepath.Join(dir, "personal.db")) + if err != nil { + t.Fatalf("创建个人库失败: %v", err) + } + t.Cleanup(func() { pdb.Close() }) + + if err := account.InitCrypto("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); err != nil { + t.Fatalf("初始化加密失败: %v", err) + } + + accountPool := account.NewPool() + lg, err := ledger.New(ldb, "") + if err != nil { + t.Fatalf("创建账本失败: %v", err) + } + svc := order.New(st, lg, accountPool, upstream.NewPool()) + waitPayPool := pool.NewWaitPayPool(30) + svc.SetWaitPayHooks(&order.WaitPayHooks{ + Add: waitPayPool.Add, + Upsert: waitPayPool.Upsert, + Remove: waitPayPool.Remove, + }) + + app := &App{ + store: st, + personalDB: pdb, + accountPool: accountPool, + orders: svc, + confirmSched: reconciler.NewScheduler(st, svc, nil, nil), + waitPayPool: waitPayPool, + } + return app, pdb +} + +// newTestDevice 创建设备(含加密密钥)+ 绑定账户到池 +func newTestDevice(t *testing.T, pdb *store.PersonalDB, pool *account.AccountPool, accountID int64, deviceID string) *personal.Device { + t.Helper() + secretEnc, err := account.Encrypt([]byte("test-secret-1234567890")) + if err != nil { + t.Fatalf("加密设备密钥失败: %v", err) + } + rowID, err := pdb.CreateDevice(context.Background(), &personal.DeviceEncrypted{ + DeviceID: deviceID, Name: "测试机", AccountID: accountID, + Provider: "alipay", SecretEnc: secretEnc, Status: string(personal.DeviceOnline), + }) + if err != nil { + t.Fatalf("创建设备失败: %v", err) + } + return &personal.Device{ID: rowID, DeviceID: deviceID, Name: "测试机", AccountID: accountID, Provider: "alipay", Secret: "test-secret-1234567890", Status: personal.DeviceOnline} +} + +// newTestOrder 创建一笔 WAIT_PAY 订单 +func newTestOrder(t *testing.T, st store.Store, outNo string, amountMinor int64, accountID int64) { + t.Helper() + newTestOrderNote(t, st, outNo, amountMinor, accountID, "") +} + +// newTestOrderNote 创建一笔 WAIT_PAY 订单(可带备注码) +func newTestOrderNote(t *testing.T, st store.Store, outNo string, amountMinor int64, accountID int64, noteCode string) { + t.Helper() + o := &model.Order{ + OutTradeNo: outNo, GatewayNo: "G-" + outNo, Channel: "personal.alipay", + AccountID: accountID, AmountMinor: amountMinor, Status: model.StatusWaitPay, + NoteCode: noteCode, CreatedAt: time.Now(), + } + if err := st.CreateOrder(context.Background(), o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } +} + +func TestDispatchLedgerRows_MatchAndSettle(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + + // 账户池:两个支付宝账户(主 + 附加,双开场景) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/a1", + }) + app.accountPool.Add(&account.Account{ + ID: 2, Channel: "personal.alipay", Name: "附加账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/a2", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-match-1") + if err := pdb.AddDeviceAccount(ctx, dev.ID, 2); err != nil { + t.Fatalf("绑定附加账户失败: %v", err) + } + + // 一笔 WAIT_PAY 订单(金额 100 分,账户 1) + newTestOrder(t, app.store, "ORD-MATCH-1", 100, 1) + + // 账本新收款行:付款方 *怡 / 10:00:00 / +1.00(100分),30 分钟窗口内 + rows := []capture.AlipayLedgerRow{{ + Payer: "*怡", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, + }} + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("dispatchLedgerRows 失败: %v", err) + } + + // 订单应入账成功 + o, err := app.store.GetOrder(ctx, "ORD-MATCH-1") + if err != nil || o == nil { + t.Fatalf("查询订单失败: %v", err) + } + if o.Status != model.StatusSuccess { + t.Fatalf("订单应 TRADE_SUCCESS,实际 %s", o.Status) + } + if o.DeviceID != dev.DeviceID { + t.Errorf("订单 device_id 应回写,实际 %q", o.DeviceID) + } + + // 推送流水应记录 matched(rawHash = sha256("ledger:*怡|HH:MM:SS|+1.00")) + raw := "ledger:*怡|" + rows[0].Time + "|+1.00" + rawHash := fmt.Sprintf("%x", sha256Hash([]byte(raw))) + dup, err := pdb.PushExists(ctx, dev.DeviceID, rawHash) + if err != nil || !dup { + t.Fatalf("推送流水应存在(matched),err=%v dup=%v", err, dup) + } +} + +func TestDispatchLedgerRows_FrozenAccountGuard(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "冻结账户", Status: account.StatusFrozen, + QRContent: "https://qr.alipay.com/f1", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-frozen-1") + newTestOrder(t, app.store, "ORD-FROZEN-1", 100, 1) + + rows := []capture.AlipayLedgerRow{{ + Payer: "*测", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, + }} + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("dispatchLedgerRows 失败: %v", err) + } + + // 冻结账户守卫:不自动入账,订单保持 WAIT_PAY + o, err := app.store.GetOrder(ctx, "ORD-FROZEN-1") + if err != nil || o == nil { + t.Fatalf("查询订单失败: %v", err) + } + if o.Status != model.StatusWaitPay { + t.Fatalf("冻结账户订单不应自动入账,实际 %s", o.Status) + } +} + +func TestMatchPersonalOrder_NoOrder(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-noorder-1") + + // 无匹配订单 → no_order + orderNo, matched, acctID, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 999, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "no_order" || orderNo != "" || acctID != 0 { + t.Fatalf("应 no_order,实际 matched=%q order=%q acct=%d", matched, orderNo, acctID) + } +} + +func TestMatchPersonalOrder_AmbiguousRefused(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-ambig-1") + + // 同金额两笔 WAIT_PAY 订单 → ambiguous(拒绝自动挑选) + newTestOrder(t, app.store, "ORD-A1", 100, 1) + newTestOrder(t, app.store, "ORD-A2", 100, 1) + + orderNo, matched, acctID, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 100, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "ambiguous" || orderNo != "" || acctID != 0 { + t.Fatalf("同金额多订单应 ambiguous(资金安全),实际 matched=%q order=%q acct=%d", matched, orderNo, acctID) + } +} + +func TestDispatchLedgerRows_Dedup(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-dedup-1") + newTestOrder(t, app.store, "ORD-DEDUP-1", 100, 1) + + // 同一行推两次:第二次应被幂等去重(不重复入账/不重复匹配) + row := capture.AlipayLedgerRow{ + Payer: "*重", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, + } + rows := []capture.AlipayLedgerRow{row} + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("首次推送失败: %v", err) + } + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("重复推送失败: %v", err) + } + + // 订单已成功(首次入账),重复推送幂等 + o, err := app.store.GetOrder(ctx, "ORD-DEDUP-1") + if err != nil || o == nil { + t.Fatalf("查询订单失败: %v", err) + } + if o.Status != model.StatusSuccess { + t.Fatalf("订单应 TRADE_SUCCESS,实际 %s", o.Status) + } +} + +// TestMatchPersonalOrder_NoteCode 备注码精确匹配(命中唯一订单直接入账,不做金额候选扩散) +func TestMatchPersonalOrder_NoteCode(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-note-1") + + // 备注码唯一订单(即使同金额有另一笔不同备注码订单,备注码应精确命中) + newTestOrderNote(t, app.store, "ORD-N1", 100, 1, "abcd") + newTestOrderNote(t, app.store, "ORD-N2", 100, 1, "efgh") + // 池同步(等价于下单钩子):两单都进池 + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-N1", "ORD-N2")) + + orderNo, matched, acctID, err := app.matchPersonalOrder(ctx, dev, "alipay", "abcd", 0, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "matched" || orderNo != "ORD-N1" || acctID != 1 { + t.Fatalf("备注码应精确命中 ORD-N1,实际 matched=%q order=%q acct=%d", matched, orderNo, acctID) + } +} + +// TestMatchPersonalOrder_NoteCodePoolPath 池路径:备注码经内存池命中(零 DB 索引查询), +// DB 二次校验仍生效(池中订单已入账 → 池条目仍命中但 DB 状态非 WAIT_PAY → 不采信,走金额兜底)。 +func TestMatchPersonalOrder_NoteCodePoolPath(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-note-pool-1") + + newTestOrderNote(t, app.store, "ORD-POOL-1", 100, 1, "wxyz") + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-POOL-1")) + + // 池路径命中 + orderNo, matched, acctID, err := app.matchPersonalOrder(ctx, dev, "alipay", "wxyz", 0, time.Now(), 30*time.Minute, 0) + if err != nil || matched != "matched" || orderNo != "ORD-POOL-1" || acctID != 1 { + t.Fatalf("池路径备注码应命中 ORD-POOL-1,实际 matched=%q order=%q acct=%d err=%v", matched, orderNo, acctID, err) + } + // 订单入账(SUCCESS)→ 池未移除时(模拟漂移),DB 二次校验应拒绝备注码采信 + if ok, serr := app.orders.SettlePayment(ctx, &channel.NotifyResult{ + OutTradeNo: "ORD-POOL-1", AmountMinor: 100, ChannelTxID: "TX-POOL-1", + }); serr != nil || !ok { + t.Fatalf("入账失败: ok=%v err=%v", ok, serr) + } + _, matched2, _, err2 := app.matchPersonalOrder(ctx, dev, "alipay", "wxyz", 0, time.Now(), 30*time.Minute, 0) + if err2 != nil { + t.Fatalf("匹配失败: %v", err2) + } + if matched2 == "matched" { + t.Fatal("已入账订单备注码不应再采信(DB 二次校验拦截),实际仍 matched") + } +} + +// TestWaitPayPool_SyncWithOrderLifecycle 池与订单生命周期同步:下单进池 → 入账出池 +func TestWaitPayPool_SyncWithOrderLifecycle(t *testing.T) { + ctx := context.Background() + app, _ := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + // 模拟下单(hooks 已注入):DB 写订单 + 池 Add(与生产下单链路一致) + o := &model.Order{ + OutTradeNo: "ORD-LIFE-1", GatewayNo: "G-ORD-LIFE-1", Channel: "personal.alipay", AccountID: 1, + AmountMinor: 100, Status: model.StatusWaitPay, CreatedAt: time.Now(), + } + if err := app.store.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + app.waitPayPool.Add(o) + if got := app.waitPayPool.Count(1); got != 1 { + t.Fatalf("下单后池应有 1 笔,实际 %d", got) + } + // 入账 → hooks Remove → 池出 + if ok, serr := app.orders.SettlePayment(ctx, &channel.NotifyResult{ + OutTradeNo: "ORD-LIFE-1", AmountMinor: 100, ChannelTxID: "TX-LIFE-1", + }); serr != nil || !ok { + t.Fatalf("入账失败: ok=%v err=%v", ok, serr) + } + if got := app.waitPayPool.Count(1); got != 0 { + t.Fatalf("入账后池应清空该账户,实际 %d", got) + } +} + +// TestMatchPersonalOrder_ChannelMismatchRefused 通道归属校验:订单通道/账户不属于设备绑定集合 → 拒绝(防串单) +func TestMatchPersonalOrder_ChannelMismatchRefused(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + // 设备只绑定账户 1(支付宝);账户 2 是微信(不在设备集合) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + app.accountPool.Add(&account.Account{ + ID: 2, Channel: "personal.wechat", Name: "微信账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-chmismatch-1") + + // 同金额订单:一笔是微信通道(应被排除),一笔支付宝通道(应被选中) + newTestOrderChannel(t, app.store, "ORD-WX", 100, 2, "personal.wechat") + newTestOrderChannel(t, app.store, "ORD-ALI", 100, 1, "personal.alipay") + + orderNo, matched, acctID, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 100, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "matched" || orderNo != "ORD-ALI" || acctID != 1 { + t.Fatalf("应命中支付宝订单 ORD-ALI(微信订单被通道校验排除),实际 matched=%q order=%q acct=%d", matched, orderNo, acctID) + } +} + +// newTestOrderChannel 创建指定通道的 WAIT_PAY 订单(防串单测试用) +func newTestOrderChannel(t *testing.T, st store.Store, outNo string, amountMinor int64, accountID int64, channel string) { + t.Helper() + o := &model.Order{ + OutTradeNo: outNo, GatewayNo: "G-" + outNo, Channel: channel, + AccountID: accountID, AmountMinor: amountMinor, Status: model.StatusWaitPay, + CreatedAt: time.Now(), + } + if err := st.CreateOrder(context.Background(), o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } +} + +// mustOrders 按 outTradeNo 列表从 store 取订单(池重建测试用;取不到直接失败) +func mustOrders(t *testing.T, st store.Store, outNos ...string) []*model.Order { + t.Helper() + out := make([]*model.Order, 0, len(outNos)) + for _, no := range outNos { + o, err := st.GetOrder(context.Background(), no) + if err != nil || o == nil { + t.Fatalf("查询订单 %s 失败: %v", no, err) + } + out = append(out, o) + } + return out +} + +// stubNoteProber 构造备注码探测桩:固定返回 note(探测成功)/ 固定错误(探测失败) +func stubNoteProber(note string, err error) alipayNoteProber { + return func(ctx context.Context, dev *personal.Device, bounds string) (string, error) { + return note, err + } +} + +// TestMatchLedgerRow_ProbeDisambiguates 多候选歧义 → 探测详情页备注码消歧 → 精确命中唯一订单。 +// 池消费模型核心场景:同金额两笔订单,备注码探测区分归属,不再转人工。 +func TestMatchLedgerRow_ProbeDisambiguates(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/a1", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-probe-1") + + // 同金额两笔 WAIT_PAY 订单,备注码不同(jzt4 / 7x57) + newTestOrderNote(t, app.store, "ORD-PROBE-A", 100, 1, "jzt4") + newTestOrderNote(t, app.store, "ORD-PROBE-B", 100, 1, "7x57") + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-PROBE-A", "ORD-PROBE-B")) + + row := capture.AlipayLedgerRow{ + Payer: "*怡", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, AmountBounds: "[451,374][1013,470]", + } + orderNo, matched, acctID, effNote, err := app.matchLedgerRow(ctx, dev, "alipay", 0, row, 30*time.Minute, stubNoteProber("jzt4", nil)) + if err != nil { + t.Fatalf("matchLedgerRow 失败: %v", err) + } + if matched != "matched" || orderNo != "ORD-PROBE-A" || acctID != 1 { + t.Fatalf("探测应消歧命中 ORD-PROBE-A,实际 matched=%q order=%q acct=%d", matched, orderNo, acctID) + } + if effNote != "jzt4" { + t.Errorf("审计备注码应为探测到的 jzt4,实际 %q", effNote) + } +} + +// TestMatchLedgerRow_ProbeFailsStaysAmbiguous 探测失败 → 保持 ambiguous 转人工(资金安全:禁止自动挑选)。 +func TestMatchLedgerRow_ProbeFailsStaysAmbiguous(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-probe-fail-1") + + newTestOrder(t, app.store, "ORD-PF-A", 100, 1) + newTestOrder(t, app.store, "ORD-PF-B", 100, 1) + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-PF-A", "ORD-PF-B")) + + row := capture.AlipayLedgerRow{ + Payer: "*怡", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, AmountBounds: "[451,374][1013,470]", + } + orderNo, matched, _, _, err := app.matchLedgerRow(ctx, dev, "alipay", 0, row, 30*time.Minute, stubNoteProber("", fmt.Errorf("adb 不可达"))) + if err != nil { + t.Fatalf("matchLedgerRow 不应因探测失败报错(降级歧义转人工): %v", err) + } + if matched != "ambiguous" || orderNo != "" { + t.Fatalf("探测失败应保持 ambiguous,实际 matched=%q order=%q", matched, orderNo) + } +} + +// TestMatchLedgerRow_ProbeEmptyNoteStaysAmbiguous 探测成功但无备注码(详情页无付款留言)→ 保持 ambiguous。 +func TestMatchLedgerRow_ProbeEmptyNoteStaysAmbiguous(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-probe-empty-1") + + newTestOrder(t, app.store, "ORD-PE-A", 100, 1) + newTestOrder(t, app.store, "ORD-PE-B", 100, 1) + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-PE-A", "ORD-PE-B")) + + row := capture.AlipayLedgerRow{ + Payer: "*怡", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, AmountBounds: "[451,374][1013,470]", + } + orderNo, matched, _, _, err := app.matchLedgerRow(ctx, dev, "alipay", 0, row, 30*time.Minute, stubNoteProber("", nil)) + if err != nil { + t.Fatalf("matchLedgerRow 失败: %v", err) + } + if matched != "ambiguous" || orderNo != "" { + t.Fatalf("空备注码应保持 ambiguous,实际 matched=%q order=%q", matched, orderNo) + } +} + +// TestMatchLedgerRow_UniqueAmountSkipsProbe 金额唯一 → 直接命中,探测桩不应被调用(短路免探测优化)。 +func TestMatchLedgerRow_UniqueAmountSkipsProbe(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-unique-1") + + newTestOrder(t, app.store, "ORD-UNIQ-1", 100, 1) + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-UNIQ-1")) + + probeCalled := false + prober := func(ctx context.Context, dev *personal.Device, bounds string) (string, error) { + probeCalled = true + return "", fmt.Errorf("不应被调用") + } + row := capture.AlipayLedgerRow{ + Payer: "*怡", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, AmountBounds: "[451,374][1013,470]", + } + orderNo, matched, _, _, err := app.matchLedgerRow(ctx, dev, "alipay", 0, row, 30*time.Minute, prober) + if err != nil { + t.Fatalf("matchLedgerRow 失败: %v", err) + } + if matched != "matched" || orderNo != "ORD-UNIQ-1" { + t.Fatalf("金额唯一应直接命中,实际 matched=%q order=%q", matched, orderNo) + } + if probeCalled { + t.Error("金额唯一时不应触发备注码探测(短路免探测)") + } +} + +// TestMatchLedgerRow_WechatRowNoteDirect 微信账本行自带"付款方备注" → 备注码精确命中,无需探测。 +func TestMatchLedgerRow_WechatRowNoteDirect(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 2, Channel: "personal.wechat", Name: "微信账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 2, "dev-wx-note-1") + + // 微信订单备注码直接入库(与支付宝一致的 note_code 机制) + if err := app.store.CreateOrder(ctx, &model.Order{ + OutTradeNo: "ORD-WXNOTE-1", GatewayNo: "G-ORD-WXNOTE-1", Channel: "personal.wechat", + AccountID: 2, AmountMinor: 100, Status: model.StatusWaitPay, + NoteCode: "wxcode", CreatedAt: time.Now(), + }); err != nil { + t.Fatalf("创建微信订单失败: %v", err) + } + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-WXNOTE-1")) + + row := capture.AlipayLedgerRow{ + Payer: "*微", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, Note: "wxcode", // 微信账本行自带备注 + } + orderNo, matched, _, effNote, err := app.matchLedgerRow(ctx, dev, "wechat", 0, row, 30*time.Minute, nil) + if err != nil { + t.Fatalf("matchLedgerRow 失败: %v", err) + } + if matched != "matched" || orderNo != "ORD-WXNOTE-1" { + t.Fatalf("微信行备注应精确命中,实际 matched=%q order=%q", matched, orderNo) + } + if effNote != "wxcode" { + t.Errorf("审计备注码应为 wxcode,实际 %q", effNote) + } +} + +// TestDispatchLedgerRows_ProbeThroughDispatch 全链路:dispatchLedgerRows 多候选歧义 → 探测消歧 → 入账。 +// 通过临时替换 app 的探测函数注入桩(生产路径 = adb 探测,测试 = 桩)。 +func TestDispatchLedgerRows_ProbeThroughDispatch(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/a1", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-dispatch-probe-1") + + newTestOrderNote(t, app.store, "ORD-DPROBE-A", 100, 1, "jzt4") + newTestOrderNote(t, app.store, "ORD-DPROBE-B", 100, 1, "7x57") + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-DPROBE-A", "ORD-DPROBE-B")) + + // 注入探测桩 + app.alipayProber = stubNoteProber("jzt4", nil) + t.Cleanup(func() { app.alipayProber = nil }) + + rows := []capture.AlipayLedgerRow{{ + Payer: "*怡", Date: "今日", Time: time.Now().Format("15:04:05"), + Amount: "+1.00", AmountMinor: 100, AmountBounds: "[451,374][1013,470]", + }} + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("dispatchLedgerRows 失败: %v", err) + } + + o, err := app.store.GetOrder(ctx, "ORD-DPROBE-A") + if err != nil || o == nil { + t.Fatalf("查询订单失败: %v", err) + } + if o.Status != model.StatusSuccess { + t.Fatalf("探测消歧订单应入账成功,实际 %s", o.Status) + } + // 另一笔保持 WAIT_PAY(未被误配) + oB, err := app.store.GetOrder(ctx, "ORD-DPROBE-B") + if err != nil || oB == nil { + t.Fatalf("查询订单失败: %v", err) + } + if oB.Status != model.StatusWaitPay { + t.Fatalf("未命中订单应保持 WAIT_PAY,实际 %s", oB.Status) + } +} + +// TestDispatchLedgerRows_MultiRowsNoMiss 多行一次推送全部处理(不漏行):每笔金额独立匹配池中订单。 +func TestDispatchLedgerRows_MultiRowsNoMiss(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-multi-1") + + newTestOrder(t, app.store, "ORD-M1", 100, 1) + newTestOrder(t, app.store, "ORD-M2", 200, 1) + newTestOrder(t, app.store, "ORD-M3", 300, 1) + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-M1", "ORD-M2", "ORD-M3")) + + now := time.Now().Format("15:04:05") + rows := []capture.AlipayLedgerRow{ + {Payer: "*甲", Date: "今日", Time: now, Amount: "+1.00", AmountMinor: 100}, + {Payer: "*乙", Date: "今日", Time: now, Amount: "+2.00", AmountMinor: 200}, + {Payer: "*丙", Date: "今日", Time: now, Amount: "+3.00", AmountMinor: 300}, + } + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("dispatchLedgerRows 失败: %v", err) + } + + for _, no := range []string{"ORD-M1", "ORD-M2", "ORD-M3"} { + o, err := app.store.GetOrder(ctx, no) + if err != nil || o == nil { + t.Fatalf("查询订单 %s 失败: %v", no, err) + } + if o.Status != model.StatusSuccess { + t.Errorf("多行推送不应漏单:%s 应入账成功,实际 %s", no, o.Status) + } + } +} + +// TestDispatchLedgerRows_ZeroCandidateNoMiss 零候选行不阻断后续行(不漏行:歧义/无单行独立转人工)。 +func TestDispatchLedgerRows_ZeroCandidateNoMiss(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-zero-1") + + // 只有 100 分订单;推 100(命中)+ 999(零候选)+ 100 重复(幂等去重) + newTestOrder(t, app.store, "ORD-Z1", 100, 1) + app.waitPayPool.Rebuild(ctx, mustOrders(t, app.store, "ORD-Z1")) + + now := time.Now().Format("15:04:05") + rows := []capture.AlipayLedgerRow{ + {Payer: "*甲", Date: "今日", Time: now, Amount: "+1.00", AmountMinor: 100}, + {Payer: "*零", Date: "今日", Time: now, Amount: "+9.99", AmountMinor: 999}, + } + if err := app.dispatchLedgerRows(ctx, dev.DeviceID, "alipay", 0, rows); err != nil { + t.Fatalf("dispatchLedgerRows 失败: %v", err) + } + + // 命中行入账 + o, err := app.store.GetOrder(ctx, "ORD-Z1") + if err != nil || o == nil { + t.Fatalf("查询订单失败: %v", err) + } + if o.Status != model.StatusSuccess { + t.Fatalf("零候选行不应影响命中行入账,实际 %s", o.Status) + } + // 零候选行应转人工(unmatched 记录) + unmatched, err := pdb.ListUnmatched(ctx, "pending", 10) + if err != nil { + t.Fatalf("查询未匹配记录失败: %v", err) + } + found := false + for _, u := range unmatched { + if u.AmountMinor == 999 { + found = true + break + } + } + if !found { + t.Error("零候选行应记录到 unmatched_payments(转人工),未找到 999 分记录") + } +} + +// TestMatchPersonalOrder_NotifyAnchor 通知栏窗口锚点语义(2026-08-30 用户需求确认): +// 通知不清理会一直在通知栏,匹配窗口必须以通知 when 为锚,不能用 time.Now()。 +// 场景:新订单刚创建,但旧通知(when 在 2 小时前)不应匹配到它。 +func TestMatchPersonalOrder_NotifyAnchor(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/new-notify-anchor", + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-notify-anchor") + // 设备绑定空间0的收款码(让 space 账户定位生效,走同一链路) + _, _ = pdb.UpsertQrcode(ctx, &personal.DeviceQrcode{ + DeviceID: dev.DeviceID, Platform: "alipay", UserID: 0, + QRContent: "https://qr.alipay.com/new-notify-anchor", Status: "active", + }) + // 创建一笔 WAIT_PAY 订单(刚创建) + newTestOrder(t, app.store, "ORD-NOTIFY-NEW", 500, 1) + + // 旧通知:when 在 2 小时前(通知栏一直没清) + oldNotify := time.Now().Add(-2 * time.Hour) + orderNo, matched, _, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 500, oldNotify, 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "no_order" || orderNo != "" { + t.Fatalf("旧通知不应匹配新订单:matched=%q order=%q(期望 no_order)", matched, orderNo) + } + + // 对照:通知 when = 刚刚(新通知)→ 锚点窗口覆盖订单 → 应命中 + newNotify := time.Now() + orderNo2, matched2, _, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 500, newNotify, 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched2 != "matched" || orderNo2 != "ORD-NOTIFY-NEW" { + t.Fatalf("新通知应匹配新订单:matched=%q order=%q(期望 matched ORD-NOTIFY-NEW)", matched2, orderNo2) + } +} + +// 双链同捕竞态:通知栏/账本 5s 内先后捕获同一笔收款,胜者已把订单结算, +// 败者匹配时零 WAIT_PAY 候选 → 应判定 settled_dup(重复捕获),不得误报 no_order 入人工池。 +func TestMatchPersonalOrder_SettledDup(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-dup-1") + + newTestOrder(t, app.store, "ORD-DUP-1", 100, 1) + if _, err := app.store.UpdateOrderStatus(ctx, "ORD-DUP-1", model.StatusWaitPay, model.StatusSuccess, + func(upd *model.Order) { upd.ChannelTxID = "LEDGER-x" }); err != nil { + t.Fatalf("模拟另一链路入账失败: %v", err) + } + + orderNo, matched, acctID, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 100, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "settled_dup" || orderNo != "ORD-DUP-1" || acctID != 1 { + t.Fatalf("应 settled_dup,实际 matched=%q order=%q acct=%d", matched, orderNo, acctID) + } +} + +// 窗口内同金额已结算订单多于 1 笔 → 无法归属,保持 no_order(资金安全,不猜)。 +func TestMatchPersonalOrder_SettledDupAmbiguous(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.alipay", Name: "主账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-dup-2") + + newTestOrder(t, app.store, "ORD-DUP-A", 100, 1) + newTestOrder(t, app.store, "ORD-DUP-B", 100, 1) + for _, no := range []struct{ id, tx string }{{"ORD-DUP-A", "LEDGER-a"}, {"ORD-DUP-B", "LEDGER-b"}} { + if _, err := app.store.UpdateOrderStatus(ctx, no.id, model.StatusWaitPay, model.StatusSuccess, + func(upd *model.Order) { upd.ChannelTxID = no.tx }); err != nil { + t.Fatalf("模拟入账失败: %v", err) + } + } + + orderNo, matched, _, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 100, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "no_order" || orderNo != "" { + t.Fatalf("多笔已结算不可归属,应 no_order,实际 matched=%q order=%q", matched, orderNo) + } +} + +// 通道校验同样约束 settled_dup:已结算订单的通道不在设备绑定集合 → no_order。 +func TestMatchPersonalOrder_SettledDupWrongChannel(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + app.accountPool.Add(&account.Account{ + ID: 1, Channel: "personal.wechat", Name: "微信账户", Status: account.StatusNormal, + }) + dev := newTestDevice(t, pdb, app.accountPool, 1, "dev-dup-3") + + newTestOrder(t, app.store, "ORD-DUP-W", 100, 1) + if _, err := app.store.UpdateOrderStatus(ctx, "ORD-DUP-W", model.StatusWaitPay, model.StatusSuccess, + func(upd *model.Order) { upd.ChannelTxID = "LEDGER-x" }); err != nil { + t.Fatalf("模拟入账失败: %v", err) + } + + orderNo, matched, _, err := app.matchPersonalOrder(ctx, dev, "alipay", "", 100, time.Now(), 30*time.Minute, 0) + if err != nil { + t.Fatalf("matchPersonalOrder 失败: %v", err) + } + if matched != "no_order" || orderNo != "" { + t.Fatalf("通道不符不应归属,实际 matched=%q order=%q", matched, orderNo) + } +} diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go new file mode 100755 index 0000000..d59033e --- /dev/null +++ b/cmd/gateway/main.go @@ -0,0 +1,918 @@ +// paygateway 支付网关入口:main() 装配 + App 容器。 +// 配置/通道/路由/分配/账本匹配已拆分到同包文件: +// +// config.go 配置加载与环境变量 +// channels.go 通道注册/设备管理/中间件 +// handlers_epay.go 易支付下单/回调/查单 +// handlers_qr.go QR 系接口/指纹 +// alloc.go 均衡分配算法 +// devices.go 设备/渠道映射 +// ledger.go 账本监听/匹配引擎 +// notify.go QR real/通知处理 +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "runtime/debug" + "strings" + "sync" + "time" + + "paygateway/internal/account" + "paygateway/internal/admin" + "paygateway/internal/alert" + "paygateway/internal/channel" + alipaych "paygateway/internal/channel/alipay" + personalch "paygateway/internal/channel/personal" + wechatch "paygateway/internal/channel/wechat" + "paygateway/internal/ledger" + "paygateway/internal/ledgermon" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/notify" + "paygateway/internal/notifywatcher" + "paygateway/internal/order" + "paygateway/internal/personal" + "paygateway/internal/pool" + "paygateway/internal/ratelimit" + "paygateway/internal/reconciler" + "paygateway/internal/scrcpy" + "paygateway/internal/store" + "paygateway/internal/tunnel" + "paygateway/internal/upstream" + + "github.com/gin-gonic/gin" +) + +// ========== 配置 ========== + +type App struct { + cfg *Config + store store.Store + ledger *ledger.Ledger + orders *order.Service + notifyW *notify.Worker + upstreamPool *upstream.UpstreamPool // 上游池(多商户验签/签名路由) + confirmSched *reconciler.Scheduler // 订单确认调度器(主动查单兜底) + + // 个人免签通道 + personalDB store.PersonalStore + personalAlipay *personalch.Channel + personalWechat *personalch.Channel + accountPool *account.AccountPool // 账户池(离线联动冻结/解冻) + accountDB *store.AccountDB // 账户存储(收款码上报时自动创建个人账户) + ipLimiter *personal.RateLimiter // 下单限流:每 IP 每分钟下单上限(防脚本刷单) + alertC *alert.Client // 告警系统(nil=未配置) + getTun func() *tunnel.Tunnel // 当前隧道(管理后台展示用;nil=无隧道) + ledgermon *ledgermon.Manager // 账本页服务端 adb 监听编排(docs/PLAN-ADB-LEDGER-IMPL.md) + waitPayPool *pool.WaitPayPool // WAIT_PAY 内存订单池(公平调度 + 备注码/金额匹配数据源) + alipayProber alipayNoteProber // 支付宝备注码探测(nil=默认 adb 探测;测试注入桩用) +} + +// bgRecover 后台 goroutine 的统一 panic 防护:记录 panic + 栈,避免整个进程崩溃。 +// 用法:每个 go func() 第一行 defer bgRecover(component)。 +func bgRecover(component string) { + if r := recover(); r != nil { + logger.Error("main", "后台任务 panic(已恢复,进程继续运行)", + "component", component, "panic", r, "stack", string(debug.Stack())) + } +} + +func main() { + // 加载 .env 文件 + loadEnv(".env") + + cfg := loadConfig() + + // 1. 日志 + logger.Init(logger.Options{ + Level: cfg.LogLevel, + LogFile: cfg.LogFile, + MaxSizeMB: 50, MaxBackups: 10, MaxAgeDays: 30, + }) + defer logger.Close() + + logger.Info("main", "=== 支付网关启动 ===") + logger.Info("main", "监听地址", "addr", cfg.ListenAddr) + logger.Info("main", "数据库", "file", cfg.DBFile) + + // 2. 存储(SQLite) + st, err := store.NewSQLite(cfg.DBFile) + if err != nil { + logger.Error("main", "数据库初始化失败", "err", err) + os.Exit(1) + } + defer st.Close() + + // 2.5 从 DB 加载运行时设置,覆盖 .env 配置 + applySettings(st, cfg) + + // 2.6 独立账本数据库(ledger.db,0600 权限,只增不改) + ledgerDB, err := store.NewLedgerDB("data/ledger.db") + if err != nil { + logger.Error("main", "账本数据库初始化失败", "err", err) + os.Exit(1) + } + defer ledgerDB.Close() + + // 3. 审计账本(日志落库交给 store 实现 logger.DBWriter) + // 私钥持久化:.env > settings 表 > 首次生成并保存到 settings(重启复用,不再每次换新导致验签失败) + ledgerKey, err := resolveLedgerKey(st, cfg) + if err != nil { + logger.Error("main", "审计密钥初始化失败", "err", err) + os.Exit(1) + } + cfg.LedgerPrivKey = ledgerKey + lg, err := ledger.New(ledgerDB, ledgerKey) + if err != nil { + logger.Error("main", "账本初始化失败", "err", err) + os.Exit(1) + } + // 启用日志写库(管理界面查询):日志写入独立日志库 logs.db,与订单主库分离,支持长期保存 + logDB, err := store.NewLogDB("data/logs.db") + if err != nil { + logger.Error("main", "日志数据库初始化失败", "err", err) + os.Exit(1) + } + defer logDB.Close() + logger.SetDBWriter(logDB) + + // 3.5 账户池(独立 accounts.db + 内存缓存) + accountDB, err := store.NewAccountDB("data/accounts.db") + if err != nil { + logger.Error("main", "账户数据库初始化失败", "err", err) + os.Exit(1) + } + defer accountDB.Close() + + // 账户加密主密钥:必须显式通过 .env 提供。 + // 不自动生成并落盘到 settings 表——主密钥与密文同库会让"拿到 accounts.db 就能解出全部账户密钥", + // 与审计密钥/套码 token 密钥等签名用途密钥的安全等级不同,不应等同处理。 + // 缺失直接 fail-fast,避免 accoutns.db / upstreams.db 已有密文时被新随机密钥静默解密失败、服务起不来反而更难排查。 + accountMasterKey := cfg.AccountMasterKey + if accountMasterKey == "" { + logger.Error("main", "ACCOUNT_MASTER_KEY 未配置:拒绝启动。请在 .env 中设置 ACCOUNT_MASTER_KEY(首次部署可用 `openssl rand -hex 32` 生成),更换会导致已有账户/上游密文无法解密") + os.Exit(1) + } + + // 创建账户池 + 处理器(唯一访问 accounts.db 的组件) + accountPool := account.NewPool() + accountHandler, err := admin.NewAccountHandler(accountDB, accountPool, accountMasterKey) + if err != nil { + logger.Error("main", "账户处理器初始化失败", "err", err) + os.Exit(1) + } + + // 从 accounts.db 加载已有账户,解密后初始化内存池 + if err := accountHandler.InitFromDB(context.Background()); err != nil { + logger.Error("main", "加载账户池失败", "err", err) + os.Exit(1) + } + + // 3.6 上游池(独立 upstreams.db + 内存缓存,多商户) + upstreamDB, err := store.NewUpstreamDB("data/upstreams.db") + if err != nil { + logger.Error("main", "上游数据库初始化失败", "err", err) + os.Exit(1) + } + defer upstreamDB.Close() + + upstreamPool := upstream.NewPool() + upstreamHandler, err := admin.NewUpstreamHandler(upstreamDB, upstreamPool, accountMasterKey) + if err != nil { + logger.Error("main", "上游处理器初始化失败", "err", err) + os.Exit(1) + } + // 从 upstreams.db 加载已有上游,解密后初始化内存池 + // 严格模式:上游只能通过管理后台配置,不从 .env / settings 加载 + if err := upstreamHandler.InitFromDB(context.Background()); err != nil { + logger.Error("main", "加载上游池失败", "err", err) + os.Exit(1) + } + if upstreamPool.Count() == 0 { + logger.Warn("main", "上游池为空:请到管理后台「上游对接」页配置至少一个上游(pid + 密钥),否则所有下单将被拒绝") + } + + // 3.7 个人免签通道存储(独立 personal.db:设备/推送流水/无订单收款) + personalDB, err := store.NewPersonalDB("data/personal.db") + if err != nil { + logger.Error("main", "个人通道数据库初始化失败", "err", err) + os.Exit(1) + } + defer personalDB.Close() + // 自动迁移收款码表(加 user_id 列支持双开空间) + if err := personalDB.EnsureQrcodeSchema(context.Background()); err != nil { + logger.Error("main", "收款码表迁移失败", "err", err) + os.Exit(1) + } + + // 套码 token 签名密钥:.env > settings 表 > 首次生成并持久化(重启复用) + qrTokenSecret := cfg.QrTokenSecret + if qrTokenSecret == "" { + if v, err := st.GetSetting(context.Background(), "qr_token_secret"); err == nil && v != "" { + qrTokenSecret = v + } + } + if qrTokenSecret == "" { + key, err := account.GenerateMasterKey() // 32 字节 hex,复用生成器 + if err != nil { + logger.Error("main", "生成套码 token 密钥失败", "err", err) + os.Exit(1) + } + qrTokenSecret = key + if err := st.SetSetting(context.Background(), "qr_token_secret", qrTokenSecret); err != nil { + logger.Warn("main", "保存套码 token 密钥失败(重启将重新生成,旧二维码失效)", "err", err) + } + logger.Info("main", "已生成新的套码 token 密钥并持久化到 settings") + } + if err := personal.InitQrToken(qrTokenSecret); err != nil { + logger.Error("main", "套码 token 密钥初始化失败", "err", err) + os.Exit(1) + } + + // 4. 注册通道(始终注册,账户数据从池中取;管理后台「账户池」配置密钥) + personalAlipay, personalWechat := regChannels(cfg, accountPool, st, personalDB) + applyChannelRuntimeSettings(st) // 通道注册后加载启停 + 支付模式设置 + + // 4.1 启动配置自检(RESILIENCE P1-2) + runStartupChecks(cfg, accountPool, st, personalDB) + + // 5. 订单服务 + 回调 worker + orderSvc := order.New(st, lg, accountPool, upstreamPool) + orderSvc.SetCloseWindow(cfg.OrderCloseMinutes) + // 备注码释放回调(docs/MIANQIAN.md §6.4):订单终态 SUCCESS/CLOSED 时释放备注码, + // 避免仅靠 5 分钟 TTL 惰性回收;通道名 → 对应 personal 通道实例 + orderSvc.SetNoteCodeReleaser(func(channelName, noteCode string) { + var ch *personalch.Channel + switch channelName { + case "personal.alipay": + ch = personalAlipay + case "personal.wechat": + ch = personalWechat + } + if ch == nil { + return + } + ch.ReleaseNoteCode(noteCode) + }) + // 5.0.1 WAIT_PAY 内存订单池(公平调度数据源,docs/DESIGN-FAIR-SCHEDULER.md): + // 下单/入账/关单/刷新/换通道/复活 全入口同步;DB 是真相,池是索引。 + // 设备维度子池 + 动态关单窗口(docs/PLAN-DYNAMIC-CLOSE-ALLOC.md): + // - deviceOf:账户 → 设备映射(personal.db 设备绑定),多设备隔离、互不干扰 + // - winCalc:动态关单窗口 = clamp(设备一轮耗时×1.5+60s, MIN, MAX),负载驱动 + waitPayPool := pool.NewWaitPayPool(cfg.OrderCloseMinutes) + // 动态窗口配置校验:MIN/MAX 非法(0 或 MIN>MAX)→ 退化为静态窗口(OrderCloseMinutes) + dynMin := time.Duration(cfg.OrderCloseMin) * time.Minute + dynMax := time.Duration(cfg.OrderCloseMax) * time.Minute + if cfg.OrderCloseMin <= 0 || cfg.OrderCloseMax <= 0 || dynMin >= dynMax { + dynMin, dynMax = 0, 0 + logger.Warn("main", "动态关单窗口配置非法,退化为静态窗口", + "min_minutes", cfg.OrderCloseMin, "max_minutes", cfg.OrderCloseMax) + } + if personalDB != nil { + waitPayPool.SetDeviceOf(func(accountID int64) (string, bool) { + return accountDeviceOf(context.Background(), personalDB, accountID) + }) + } + if dynMin > 0 && dynMax > 0 { + // 动态关单窗口计算器:设备负载驱动,物理锚点见设计文档 §4.1。 + // 设备一轮耗时 = 活跃账户数 × 配额(3) × 单次 poll 成本(默认 4s,可配置)。 + pollCostMS := cfg.DevicePollCostMS + if pollCostMS <= 0 { + pollCostMS = 4000 + } + const ( + baseQuota = 3 // 调度器 BaseQuota(与 ledgermon.DefaultSchedulerConfig 一致) + queueFactor = 1.5 // 最坏排队位置系数 + payReserve = 60 * time.Second + activeFactor = float64(baseQuota) / 1000 + ) + waitPayPool.SetWindowCalc(func(activeAccounts, total int) time.Duration { + _ = total // 负载维度(活跃账户数已隐含;保留签名供未来按订单数加权) + est := time.Duration(float64(activeAccounts) * activeFactor * float64(pollCostMS) * queueFactor) + w := est + payReserve + if w < dynMin { + w = dynMin + } + if w > dynMax { + w = dynMax + } + return w + }) + } + // 通知栏快速通道监听器(延迟到 6 节构造;此处先声明变量,供 hooks 闭包唤醒)。 + var nw *notifywatcher.Manager + orderSvc.SetWaitPayHooks(&order.WaitPayHooks{ + Add: func(o *model.Order) { + waitPayPool.Add(o) + // 事件驱动:新订单进池 → 唤醒通知栏监听(池空休眠态立即恢复轮询) + if nw != nil { + nw.Wake() + } + }, + Upsert: waitPayPool.Upsert, + Remove: waitPayPool.Remove, + // 动态关单窗口:扫描器用池中记录的 CloseAt 判断是否真正到期(未到则跳过) + CloseAtOf: waitPayPool.CloseAtOf, + }) + notifyW := notify.NewWorker(st, cfg.NotifyAllowPrivateIPs) + logger.Info("main", "回调 worker SSRF 防护状态", "allow_private_notify", cfg.NotifyAllowPrivateIPs) + + // 5.0 告警系统(RESILIENCE P1-3):进池 / 账本失败 / dead 通知 + alertC := alert.New(alert.Options{BarkURL: cfg.AlertBarkURL, WebhookURL: cfg.AlertWebhookURL}) + if alertC != nil { + orderSvc.SetAlert(alertC) + notifyW.SetAlert(alertC) + logger.Info("main", "告警系统已启用", + "bark", cfg.AlertBarkURL != "", "webhook", cfg.AlertWebhookURL != "") + } else { + logger.Warn("main", "告警系统未配置(ALERT_BARK_URL / ALERT_WEBHOOK_URL 为空)") + } + + // 5.1 确认调度器(主动查单兜底):注入通道查单函数(遍历通道注册表) + queryFunc := func(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) { + var lastErr error + for _, name := range channel.Names() { + ch, ok := channel.Get(name) + if !ok { + continue + } + st, err := ch.QueryOrder(ctx, outTradeNo) + if err != nil { + lastErr = err + continue + } + return st, nil + } + if lastErr == nil { + lastErr = fmt.Errorf("无可查询通道") + } + return nil, lastErr + } + confirmSched := reconciler.NewScheduler(st, orderSvc, queryFunc, nil) + + // 个人通道监听端检查(ConfirmByManual 通道关单前调用): + // 设备 adb 不可达 → 订单进异常池人工核实(用户可能已付款但未监听到),绝不直接关单。 + // 在线判定 = 实时 adb 可达性(2026-08-29 重构:不再依赖监听 App 心跳/DB status)。 + // 该函数同时注入 confirmSched(事件驱动主路径)与 orderSvc(DB 扫描兜底路径), + // 保证两条关单路径的安全检查完全一致(PLAN-DYNAMIC-CLOSE-ALLOC §4.3)。 + var personalCheckFn func(ctx context.Context, o *model.Order) (bool, string) + if personalDB != nil { + personalCheckFn = func(ctx context.Context, o *model.Order) (bool, string) { + // 订单已绑定设备:adb 实时可达判定 + if o.DeviceID != "" { + enc, err := personalDB.GetDevice(ctx, o.DeviceID) + if err != nil { + return true, "设备查询失败: " + err.Error() + } + if enc == nil { + return true, "设备不存在: " + o.DeviceID + } + serial := deviceADBSerial(enc) + if serial == "" { + return true, "设备未配置 adb 通道" + } + if !adbMgr().Online(ctx, serial) { + return true, "设备 adb 不可达" + } + return false, "" + } + // 订单未绑定设备:按 account_id 反查承载该账户的设备(一台设备承载 + // 多账户多通道 device_accounts 多对多,不能按 personal_devices.provider + // 单值过滤——跨通道订单会误判设备离线致关单永久卡异常池,2026-09-13 实测) + if o.AccountID > 0 { + devIDs, err := personalDB.DeviceIDsOfAccount(ctx, o.AccountID) + if err != nil { + return true, "账户设备反查失败: " + err.Error() + } + for _, devID := range devIDs { + enc, err := personalDB.GetDeviceByID(ctx, devID) + if err != nil || enc == nil { + continue + } + serial := deviceADBSerial(enc) + if serial != "" && adbMgr().Online(ctx, serial) { + return false, "" + } + } + } + // 账户未反查到在线设备 → 按 provider 匹配任意在线设备兜底 + devs, err := personalDB.ListDevices(ctx) + if err != nil { + return true, "设备列表查询失败" + } + provider := channel.ProviderFromName(o.Channel) + for _, d := range devs { + if d.Provider != provider { + continue + } + serial := deviceADBSerial(d) + if serial != "" && adbMgr().Online(ctx, serial) { + return false, "" + } + } + return true, "无 adb 可达设备(provider=" + provider + ")" + } + confirmSched.SetPersonalCheck(personalCheckFn) + // 漏单修复:CloseExpiredOrders 对个人通道也走同一安全检查(此前直接关单绕过离线检查) + orderSvc.SetPersonalCheck(personalCheckFn) + } + + app := &App{cfg: cfg, store: st, ledger: lg, orders: orderSvc, notifyW: notifyW, upstreamPool: upstreamPool, confirmSched: confirmSched, + personalDB: personalDB, personalAlipay: personalAlipay, personalWechat: personalWechat, accountPool: accountPool, accountDB: accountDB, + ipLimiter: personal.NewRateLimiter(cfg.MaxOrdersPerIPPerMin, time.Minute, 100000), + waitPayPool: waitPayPool} + + // tun 全局隧道状态(同时用于 admin / watcher 重启逻辑取隧道 URL)。 + // 在 watcher 启动前声明,确保 watcher 的 recoverer 闭包能引用到。 + var tun *tunnel.Tunnel + var tunMu sync.RWMutex + setTun := func(t *tunnel.Tunnel) { tunMu.Lock(); tun = t; tunMu.Unlock() } + getTun := func() *tunnel.Tunnel { tunMu.RLock(); defer tunMu.RUnlock(); return tun } + app.getTun = getTun + + // 6. 启动回调 worker + 确认调度器 + 设备离线检测(后台) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go notifyW.Start(ctx) + go confirmSched.Start(ctx) + // 账本页服务端 adb 监听(docs/PLAN-ADB-LEDGER-IMPL.md Phase 1 支付宝闭环): + // 服务端直连手机读收款记录页 → 增量检测 → 复用 /personal/push 匹配入账链路。 + // 依赖 personal.db(设备+账户绑定)+ adb 管理器(WiFi adb 5555 直连/USB)。 + if personalDB != nil && adbMgr() != nil { + // 订单驱动:账户有待支付订单才轮询(导航+下拉刷新+读取),订单终结自动暂停。 + // 避免持续占用手机前台与频繁刷新支付宝(2026-08-27 真机反馈修正)。 + // 资金安全:DB 查询错误视为"仍有待支付订单"(失败开放),保持轮询不误停。 + hasPending := func(ctx context.Context, accountID int64) bool { + if accountID <= 0 { + return false + } + orders, err := st.ListWaitPayByAccount(ctx, accountID) + if err != nil { + logger.Warn("main", "待支付订单查询失败(按仍有订单处理,保持轮询)", + "err", err, "account_id", accountID) + return true + } + return len(orders) > 0 + } + app.ledgermon = ledgermon.NewManager( + adbMgr(), + app.resolveLedgerDevices, + app.newLedgerReader, + app.dispatchLedgerRows, + hasPending, + cfg.LedgerActiveInterval, + personalDB, + waitPayPool, // 公平调度模式:DeviceScheduler 按内存池快照统一决策轮询 + ) + app.ledgermon.Start(ctx) + logger.Info("main", "账本页 adb 监听已启动(DeviceActor 模型,公平时间占比调度)", + "poll", cfg.LedgerActiveInterval) + // WAIT_PAY 池启动重建(DB 全量待支付订单)+ 周期对账(30s 防漂移) + if orders, err := st.ListWaitPayAll(ctx); err != nil { + logger.Error("main", "WAIT_PAY 池重建失败(调度将按空池运行,Reconcile 兜底)", "err", err) + } else { + waitPayPool.Rebuild(ctx, orders) + logger.Info("main", "WAIT_PAY 池重建完成", "orders", len(orders)) + } + go waitPayPool.ReconcileLoop(ctx, 30*time.Second, func() ([]*model.Order, error) { + return st.ListWaitPayAll(context.Background()) + }) + // 通知栏快速通道(NotifyWatcher):独立 goroutine + 独立 adbMgr 实例(Manager.mu 独立, + // 与 ledgermon DeviceActor 串行队列天然并行,实测 dumpsys notification ∥ dump 无冲突)。 + // 事件驱动:池有 WAIT_PAY 订单 → 高频轮询;池空 → 休眠(新订单进池经 hooks Wake 唤醒)。 + // 窗口锚点用通知 when(见 handleNotify),快速通道比账本页快 ~15s。 + nw = notifywatcher.New( + adbMgr(), // 独立实例:与 ledgermon 持有的实例 mutex 隔离 + func(ctx context.Context) ([]notifywatcher.Device, error) { + devs, err := personalDB.ListDevices(ctx) + if err != nil { + return nil, err + } + var out []notifywatcher.Device + for _, d := range devs { + serial := deviceADBSerial(d) + if serial == "" { + continue + } + if !adbMgr().Online(ctx, serial) { + continue + } + out = append(out, notifywatcher.Device{DeviceID: d.DeviceID, Serial: serial}) + } + return out, nil + }, + func(ctx context.Context) bool { + return waitPayPool.Snapshot().Total > 0 + }, + app.handleNotify, + personalDB, + notifywatcher.Config{ + PollEvery: cfg.NotifyPollInterval, + IdleEvery: cfg.NotifyIdleInterval, + ShellTimeout: 3 * time.Second, + }, + ) + go nw.Start(ctx) + logger.Info("main", "通知栏快速通道监听已启动", + "poll", cfg.NotifyPollInterval, "idle", cfg.NotifyIdleInterval) + } + // 监听端离线检测:60s 无心跳 → 设备 OFFLINE → 冻结账户 → WAIT_PAY 订单进异常池 + // 注入 recoverer:handleOffline 起后台 goroutine 调 AttemptRestartByID + // 自动尝试 docker restart + am start,限频 5min/次(personalHandler 内部管理)。 + // 控制面状态机:注入 adb prober;nil 时状态机静默关闭。 + // 2026-08-29 重构:监听 App 已弃用,adb 反向隧道(relay)全部移除。 + var pushHandler *admin.PersonalHandler + if personalDB != nil { + // 设备在线判定注入(2026-08-29 重构):个人通道选号/可服务账户判定以 + // adb 可达性为准(实时探测 + 自动 connect)。 + adbOnline := func(ctx context.Context, serial string) bool { + return adbMgr().Online(ctx, serial) + } + personalAlipay.SetAdbOnline(adbOnline) + personalWechat.SetAdbOnline(adbOnline) + // 均衡分配器注入(docs/PLAN-DYNAMIC-CLOSE-ALLOC.md §5): + // 两层分配——先选设备(最闲最健康,超限排除),再选设备内账户(最闲最健康)。 + // 负载数据源:waitPayPool(设备/账户负载)+ app.ledgermon(设备 poll 健康度)。 + // provider 显式传入:支付宝/微信各自注册自己的分配器(SelectFromWeighted 按通道过滤)。 + balancerFor := func(provider channel.Provider) func(ctx context.Context) *account.Account { + return func(ctx context.Context) *account.Account { + return balanceSelectAccount(ctx, personalDB, accountPool, waitPayPool, app.ledgermon, cfg, "personal."+string(provider)) + } + } + personalAlipay.SetBalancedSelect(balancerFor(channel.ProviderAlipay)) + personalWechat.SetBalancedSelect(balancerFor(channel.ProviderWechat)) + + var watcherRecoverer reconciler.Recoverer + adbProber := reconciler.AsAdbProber(adbMgr()) + if adbMgr() != nil { + // 提前构造一份 personalHandler 用作 watcher 的 recoverer; + // adminSrv 后面会用同一参数构造自己的实例,二者独立的 restartLimiter 可接受。 + rh := admin.NewPersonalHandler(personalDB, accountPool, accountDB, adbMgr()) + watcherRecoverer = rh + pushHandler = rh + } + go reconciler.NewDeviceWatcher(personalDB, accountPool, st, orderSvc, nil, watcherRecoverer, adbProber).Start(ctx) + } + + // 6.5 自动关闭超时未支付订单(定时扫描) + // 候选集 = max(商家固定窗口 ORDER_CLOSE_MINUTES, 个人动态窗口上限 MAX) + 裕量。 + // 具体订单是否到期由 CloseExpiredOrders 二次判断: + // - 个人免签:池内动态 CloseAt(设备负载驱动,未到跳过) + // - 商家通道:ORDER_CLOSE_MINUTES 固定窗口(isExpiredWindow,不受动态窗口影响) + if cfg.OrderCloseMinutes > 0 { + go func() { + defer bgRecover("close_scanner") + interval := time.Duration(cfg.OrderCloseInterval) * time.Second + if interval <= 0 { + interval = 60 * time.Second + } + // 候选集 = min(商家固定窗口, 个人动态窗口下限)。 + // 个人订单在 age≥dynMin 时进入候选集,由池内 CloseAtOf 按各自动态窗口精确过滤; + // 商家订单在 age≥dynMin 时进入候选集,由 isExpiredWindow(ORDER_CLOSE_MINUTES) 过滤。 + // 若用 max() 则个人订单要等到 30min 才进候选集,动态窗口(5~10min)完全失效。 + timeout := time.Duration(cfg.OrderCloseMinutes) * time.Minute + if dynMin > 0 && dynMin < timeout { + timeout = dynMin + } + logger.Info("main", "超时订单自动关闭已启动", + "candidate_window", timeout.String(), "interval_sec", int(interval/time.Second), + "merchant_close_minutes", cfg.OrderCloseMinutes, + "dynamic", dynMin > 0, "personal_min", cfg.OrderCloseMin, "personal_max", cfg.OrderCloseMax) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + before := time.Now().Add(-timeout) + if _, err := orderSvc.CloseExpiredOrders(ctx, before); err != nil { + logger.Error("order", "自动关闭扫描失败", "err", err) + } + } + } + }() + } + + // 6.5b 收款码定期清理(DB 过期码 + 设备端相册旧码,防存储打满) + if pushHandler != nil { + go func() { + defer bgRecover("qrcode_cleanup") + interval := 60 * time.Minute + logger.Info("main", "收款码定期清理已启动", "interval_minutes", int(interval/time.Minute)) + // 启动时先清一次 + pushHandler.CleanupQrcodes(ctx) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + pushHandler.CleanupQrcodes(ctx) + } + } + }() + } + + // 6.5c 收款码自动续期(每设备每平台一条 active 码,缺失/过期才补捕;静态码长期有效几乎不触发) + if pushHandler != nil { + go func() { + defer bgRecover("qrcode_renew") + interval := 60 * time.Minute + logger.Info("main", "收款码自动续期已启动", "interval_minutes", int(interval/time.Minute)) + // 启动时先补一轮(确保已有有效码) + pushHandler.RenewQrcodes(ctx) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + pushHandler.RenewQrcodes(ctx) + } + } + }() + } + + // 6.5d 服务端账本读取轮询已废弃(用户决策:账本导航兜底延迟高/自动化困难, + // 实时到账完全依赖监听端通知推送;如需查账可用账本校验 API 手动触发)。 + + // 6.7 账本校验函数(调度器 + API 共用) + doVerify := func() *admin.VerifyLedgerResult { + res, err := lg.Verify(context.Background()) + r := &admin.VerifyLedgerResult{CheckedAt: time.Now().Format(time.RFC3339)} + if err != nil { + r.Errors = []string{err.Error()} + logger.Error("ledger", "账本校验失败", "err", err) + } else { + r.Valid = res.Valid + r.TotalEntries = res.TotalEntries + r.Errors = res.Errors + r.SnapshotOK = res.SnapshotOK + r.Duration = time.Since(res.StartTime).Round(time.Millisecond).String() + if res.Valid { + logger.Info("ledger", "账本完整 ✅", "count", res.TotalEntries, "snapshot_ok", res.SnapshotOK, "duration", r.Duration) + } else { + logger.Error("ledger", "账本校验失败 ❌", "errors", res.Errors) + } + } + return r + } + + // 6.8 账本定时任务:每日 00:00 快照 + 校验 + go func() { + defer bgRecover("ledger_daily") + // 先创建今日快照(刷新锚点到当前链尾),再校验 —— + // 避免"先校验"读到昨日快照锚点与今日新增条目不匹配而误报 ERROR + today := time.Now().Format("2006-01-02") + if err := lg.CreateSnapshot(ctx, today); err != nil { + logger.Warn("ledger", "创建快照失败(可能已存在)", "err", err) + } else { + logger.Info("ledger", "今日快照已创建", "date", today) + } + // 启动时执行一次校验 + doVerify() + + // 每 24 小时检查一次(每分钟检查是否到 00:00) + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + now := time.Now() + // 每天 00:01 执行(给 1 分钟缓冲,避免刚好 00:00:00 没执行到) + if now.Hour() == 0 && now.Minute() == 1 { + logger.Info("ledger", "每日账本任务开始") + // 每日重置账户日已用额度(DB + 内存池同步清零) + if err := accountDB.ResetDailyUsed(ctx); err != nil { + logger.Error("main", "重置账户日额度(DB)失败", "err", err) + } else { + accountPool.ResetDailyUsed() + logger.Info("main", "账户日额度已重置") + } + // 创建前一日快照 + yesterday := now.AddDate(0, 0, -1).Format("2006-01-02") + if err := lg.CreateSnapshot(ctx, yesterday); err != nil { + logger.Warn("ledger", "创建快照失败", "date", yesterday, "err", err) + } else { + logger.Info("ledger", "快照已创建", "date", yesterday) + } + // 全链校验 + doVerify() + // 日终对账报告(RESILIENCE P2-1):昨日全量核对 + if rep, err := reconciler.BuildDailyReport(ctx, st, yesterday); err != nil { + logger.Error("reconciler", "日终对账报告生成失败", "err", err) + } else { + rep.Log() + } + // 睡 59 秒,避免一天内重复触发 + time.Sleep(59 * time.Second) + } + } + } + }() + + // 7. 启动 Cloudflare Tunnel(后台,获取公网回调地址) + // 隧道域名每次启动随机分配,通过回调变量动态更新,不硬编码 + // 隧道重启换域名时,OnURLChange 自动更新支付宝通道的回调地址 + + go func() { + defer bgRecover("cloudflare_tunnel") + logger.Info("main", "正在启动 Cloudflare Tunnel...") + + // 从 .env(CF_TUNNEL_CREDENTIALS + CF_TUNNEL_HOSTNAME)生成 ~/.cloudflared/ 配置。 + // 换环境只需复制 .env 中对应行,无需手动创建凭据文件。 + if err := tunnel.EnsureCredentials(); err != nil { + logger.Warn("main", "自动生成隧道配置失败(不影响已有文件)", "err", err) + } + + // updateAlipayNotifyURL 更新支付宝通道的回调地址 + updateAlipayNotifyURL := func(tunURL string) { + if tunURL == "" { + return + } + notifyURL := tunURL + "/alipay/notify" + cfg.AlipayNotifyURL = notifyURL + // 通过类型断言更新支付宝通道的回调地址 + if ch, ok := channel.Get("merchant.alipay"); ok { + if alipayCh, ok := ch.(*alipaych.Channel); ok { + alipayCh.SetNotifyURL(notifyURL) + } + } + // 更新微信通道回调地址 + wxNotifyURL := tunURL + "/wechat/notify" + cfg.WechatNotifyURL = wxNotifyURL + if ch, ok := channel.Get("merchant.wechat"); ok { + if wc, ok := ch.(*wechatch.Channel); ok { + wc.SetNotifyURL(wxNotifyURL) + } + } + // 更新个人免签通道公网地址(套码二维码用) + if personalAlipay != nil { + personalAlipay.SetBaseURL(tunURL) + } + if personalWechat != nil { + personalWechat.SetBaseURL(tunURL) + } + cfg.PersonalBaseURL = tunURL + // 对接信息同源更新:env 里只写了 localhost/内网(历史默认)时由固定隧道域名接管, + // 显式配置了公网域名的场景不覆盖(尊重运维意图) + if cfg.EpayPublicBase == "" || notify.IsPrivateURL(cfg.EpayPublicBase) { + cfg.EpayPublicBase = tunURL + } + // 保存到文件,方便查看当前回调地址 + _ = os.WriteFile("current_callback.txt", []byte("alipay: "+notifyURL+"\nwechat: "+wxNotifyURL+"\ntunnel: "+tunURL+"\n"), 0644) + logger.Info("main", "回调地址已更新", "alipay", notifyURL, "wechat", wxNotifyURL, "tunnel", tunURL) + } + + // 启动隧道(阻塞直到就绪或超时) + // OnURLChange 回调:隧道就绪 + 重启换域名时自动触发 + tun, err := tunnel.New(ctx, &tunnel.Config{ + LocalPort: 8080, + CallbackPath: "/alipay/notify", + StartTimeout: 30 * time.Second, + Connections: 4, + OnURLChange: updateAlipayNotifyURL, + Hostname: cfg.TunnelHostname, + }) + if err != nil { + logger.Warn("main", "隧道启动失败(不影响本地服务,但支付宝回调不可用)", "err", err) + return + } + setTun(tun) + + logger.Info("main", "隧道就绪 ✅", "url", tun.URL(), "callback", cfg.AlipayNotifyURL) + + // 启动健康检查 + 自动重启(隧道挂了会自动换域名重连) + tun.HealthLoop(ctx, 30*time.Second) + }() + + // 8. HTTP 服务 + r := gin.Default() + r.Use(traceMiddleware()) + + // 公网支付路由组:按 IP 限流(防刷 / 防 DoS),不影响签名回调路由 + publicLimiter := ratelimit.IPRateLimit(cfg.PublicRateMax, time.Duration(cfg.PublicRateWindowSec)*time.Second) + public := r.Group("", publicLimiter) + + // 易支付协议接口(New API 调用,保持兼容) + public.GET("/submit.php", app.handleEpaySubmit) + public.POST("/submit.php", app.handleEpaySubmit) + public.GET("/api.php", app.handleEpayQuery) + + // 二维码刷新(生成新订单 + 新二维码) + public.GET("/qr/refresh", app.handleQrRefresh) + + // 订单过期页(支付页倒计时归零后跳转;return_url 指向商家页用于重新下单) + public.GET("/qr/expired", app.handleQrExpired) + + // 订单状态探测(过期页 JS 轮询:上游同号重提复活后自动跳回支付页,不死在过期页) + public.GET("/qr/status", app.handleQrStatus) + + // 支付成功落地页(付款人支付后展示成功状态;仅 TRADE_SUCCESS 渲染) + public.GET("/qr/success", app.handleQrSuccess) + + // 切换支付方式(订单智能迁移到目标通道可用账户,支持过期订单复活) + public.GET("/qr/switch", app.handleQrSwitch) + + // 浏览器指纹绑定(支付页 JS 采集 canvas/时区/屏幕后上报,增强 buyer_key 抗换 IP 刷单) + public.GET("/personal/bind-fp", app.handleBindFingerprint) + + // 支付宝异步回调 + r.POST("/alipay/notify", app.handleAlipayNotify) + + // 微信异步回调 + r.POST("/wechat/notify", app.handleWechatNotify) + + // 个人免签通道:直发码图片(真实收款码二维码;token 校验,图片不进支付页 HTML) + public.GET("/qr/real", app.handleQrReal) + + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "time": time.Now().Format(time.RFC3339)}) + }) + + // 管理后台(独立端口 8081,Vue3 前端 + JSON API) + if cfg.AdminAddr != "" { + adminSrv := admin.New(st, ledgerDB, "/alipay/notify", + func() string { + if t := getTun(); t != nil { + return t.URL() + } + return "" + }, + func() bool { + if t := getTun(); t != nil { + return t.Healthy() + } + return false + }, + func() []admin.ChannelInfo { + return buildChannelInfos(accountPool) + }, + func() *admin.EpayInfo { return buildEpayInfo(cfg) }, + func(key, value string) { + // 设置变更回调:更新通道运行时配置 + // 支付模式切换 + if strings.HasPrefix(key, "alipay_mode_") { + mode := strings.TrimPrefix(key, "alipay_mode_") + if ch, ok := channel.Lookup("merchant.alipay"); ok { + type modeSetter interface { + SetModeEnabled(m string, enabled bool) + } + if ms, ok := ch.(modeSetter); ok { + ms.SetModeEnabled(mode, value == "true") + } + } + } + }, + doVerify, + func() error { + ctx := context.Background() + today := time.Now().Format("2006-01-02") + return lg.CreateSnapshot(ctx, today) + }, + func() string { return lg.PublicKeyPEM() }, + accountHandler, + upstreamHandler, + admin.NewReconcilerHandler(st, orderSvc, confirmSched), + admin.NewPersonalHandler(personalDB, accountPool, accountDB, adbMgr()), + cfg.JWTSecret, + ) + // 实时设备流服务(云手机式操作):注入 StreamServer,管理后台提供 WebSocket 入口 + streamSrv := scrcpy.NewStreamServer() + adminSrv.SetStreamServer(streamSrv) + adminSrv.SetLogStore(logDB) + // 账本页监听状态(只读,docs/PLAN-ADB-LEDGER-IMPL.md §7) + adminSrv.SetLedgerStatus(func() any { + if app.ledgermon == nil { + return []any{} + } + return app.ledgermon.Status() + }) + // 设备手动预热(DeviceActor 模型:手动预热是队列中的一个命令,天然互斥) + adminSrv.SetLedgerWarmup(func(ctx context.Context, deviceID string, providers map[string]bool) error { + if app.ledgermon == nil { + return fmt.Errorf("账本监听未启用") + } + return app.ledgermon.WarmupDevice(ctx, deviceID, providers) + }) + go adminSrv.Run(cfg.AdminAddr) + } + + logger.Info("main", "服务启动完成 ✅") + if err := r.Run(cfg.ListenAddr); err != nil { + logger.Error("main", "服务退出", "err", err) + } +} + +// adbMgr 创建设备操作管理器(web 可视化操作 USB 真机 / adb over WiFi 设备) +// adb 二进制从系统 PATH 获取(宿主机需安装 adb);构建产物与运行环境需一致。 diff --git a/cmd/gateway/notify.go b/cmd/gateway/notify.go new file mode 100644 index 0000000..1096d0f --- /dev/null +++ b/cmd/gateway/notify.go @@ -0,0 +1,192 @@ +// paygateway 支付网关 notify(从 main.go 拆分,QR real/通知处理) +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "net/http" + "paygateway/internal/account" + "paygateway/internal/channel" + personalch "paygateway/internal/channel/personal" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/notifywatcher" + "paygateway/internal/personal" + "paygateway/internal/web" + "time" + + "github.com/gin-gonic/gin" +) + +func (a *App) resolvePersonalQr(c *gin.Context, token string) (*model.Order, *personalch.Channel, *account.Account, error) { + key, _, err := personal.ParseQrToken(token) + if err != nil { + return nil, nil, nil, fmt.Errorf("二维码无效或已过期: %w", err) + } + o, err := a.store.GetOrder(c.Request.Context(), key) + if err != nil || o == nil { + return nil, nil, nil, fmt.Errorf("订单不存在") + } + if o.Status != model.StatusWaitPay { + return nil, nil, nil, fmt.Errorf("订单已处理(状态: %s)", o.Status) + } + if o.QrToken != "" && o.QrToken != token { + return nil, nil, nil, fmt.Errorf("二维码与订单不匹配") + } + ch := a.personalChannelFor(o.Channel) + if ch == nil { + return nil, nil, nil, fmt.Errorf("通道配置错误") + } + acct := ch.GetAccountByID(o.AccountID) + if acct == nil || acct.QRContent == "" { + return nil, nil, nil, fmt.Errorf("收款码配置错误") + } + return o, ch, acct, nil +} + +// handleQrReal 直发码:校验 token → 生成真实收款码 PNG → 返回图片(不进支付页 HTML) +// GET /qr/real?token={token} +func (a *App) handleQrReal(c *gin.Context) { + token := c.Query("token") + if token == "" { + c.String(http.StatusNotFound, "无效二维码") + return + } + o, _, acct, err := a.resolvePersonalQr(c, token) + if err != nil { + logger.Warn("personal", "直发码 token 校验失败", "err", err) + c.String(http.StatusNotFound, "二维码无效或已过期") + return + } + png := web.QRPng(acct.QRContent) + if len(png) == 0 { + c.String(http.StatusInternalServerError, "收款码生成失败") + return + } + c.Header("Content-Type", "image/png") + c.Header("Cache-Control", "no-store") + c.Header("X-Content-Type-Options", "nosniff") + c.Header("Content-Security-Policy", "default-src 'none'") + logger.Info("personal", "直发码图片下发", "out_trade_no", o.OutTradeNo, "amount", o.AmountMinor) + c.Data(http.StatusOK, "image/png", png) +} + +// personalChannelFor 按订单通道名返回个人通道实例 +func (a *App) personalChannelFor(chName string) *personalch.Channel { + switch chName { + case "personal.alipay": + return a.personalAlipay + case "personal.wechat": + return a.personalWechat + } + return nil +} + +// sha256Hash 计算 SHA-256 哈希(hex) +func sha256Hash(data []byte) []byte { + h := sha256.Sum256(data) + return h[:] +} + +// handleNotify 通知栏收款事件回调(notifywatcher.Handler 实现)。 +// +// 快速通道语义(2026-08-30 用户需求确认): +// - 通知栏比账本页快 ~15s,收到收款通知即尝试快速匹配入账; +// - 匹配窗口以通知 when(收款发生时刻)为锚:since = when - 30min, +// 不能用 time.Now()(通知不清理会一直在,旧通知会错误匹配新订单); +// - 复用 matchPersonalOrder(空间账户过滤 + 金额时间窗 + 备注码), +// 与账本链路共享同一套资金安全逻辑; +// - 金额唯一 → 直接入账(冻结守卫 + SettlePayment 幂等); +// 多候选(ambiguous) / 无订单(no_order) → 不动作,留给账本复查兜底。 +func (a *App) handleNotify(ctx context.Context, ev notifywatcher.Event) error { + dev, err := a.personalDeviceCtx(ctx, ev.DeviceID) + if err != nil { + return fmt.Errorf("设备加载失败: %w", err) + } + if dev.Status == personal.DeviceFrozen { + logger.Warn("notifywatcher", "通知回调:设备已冻结,跳过", "device_id", ev.DeviceID) + return nil + } + // 窗口锚点 = 通知 when(毫秒时间戳);与账本 30min 窗口语义一致 + notifyTime := time.UnixMilli(ev.When) + window := 30 * time.Minute + + orderNo, matched, acctID, err := a.matchPersonalOrder(ctx, dev, ev.Provider, "", ev.AmountMinor, notifyTime, window, ev.Space) + if err != nil { + return fmt.Errorf("通知订单匹配失败: %w", err) + } + + // 指纹落库(幂等去重的写入侧):notify 事件此前从不回写 raw_hash,PushExists + // 每轮查空 → 同一通知每轮重复捕获+重复匹配(真机风暴实测 30s 内 21 次)。 + // 账本链路(ledger.go)本就在写本表,此处对齐(空间隔离:指纹含 space+when)。 + rawHash := ev.Fingerprint() + if rawHash != "" { + if dup, derr := a.personalDB.PushExists(ctx, ev.DeviceID, rawHash); derr == nil && !dup { + _ = a.personalDB.InsertPush(ctx, &personal.Push{ + DeviceID: ev.DeviceID, Provider: ev.Provider, AmountMinor: ev.AmountMinor, + RawHash: rawHash, MatchedOrder: orderNo, MatchResult: matched, + Space: ev.Space, Raw: ev.Raw, + }) + } + } + + raw := ev.Raw + switch matched { + case "matched": + // 冻结账户守卫(对齐账本链路):冻结账户不自动入账,转异常池人工核实 + if acct := a.accountPool.GetByID(acctID); acct != nil && acct.Status == account.StatusFrozen { + logger.Warn("notifywatcher", "账户已冻结,通知收款转人工", "order", orderNo, "account_id", acctID) + if cur, cerr := a.store.GetOrder(ctx, orderNo); cerr == nil && cur != nil { + a.orders.EnqueueException(ctx, cur, model.ExReasonListenOffline, "PAID_WHILE_FROZEN", + "", raw, "通知栏监听:收款到达时账户处于冻结态,需人工核实际收款后入账") + } + return nil + } + nr := &channel.NotifyResult{ + Valid: true, + OutTradeNo: orderNo, + ChannelTxID: fmt.Sprintf("NOTIFY-%s-%d", ev.DeviceID, ev.When), + Status: model.StatusSuccess, + AmountMinor: ev.AmountMinor, + Payer: "notify:" + ev.DeviceID, + DeviceID: ev.DeviceID, + AccountID: acctID, + RawPayload: raw, + } + if ok, serr := a.orders.SettlePayment(ctx, nr); serr != nil { + logger.Error("notifywatcher", "通知快速入账失败", "err", serr, "order", orderNo) + return serr + } else if !ok { + // ok=false, serr=nil:并发已支付/已关单(幂等),账本复查也会幂等跳过 + logger.Info("notifywatcher", "通知入账幂等跳过(已支付/已关单)", "order", orderNo) + return nil + } + if _, uerr := a.store.UpdateOrderStatus(ctx, orderNo, model.StatusSuccess, model.StatusSuccess, func(upd *model.Order) { + upd.DeviceID = ev.DeviceID + }); uerr != nil { + logger.Warn("notifywatcher", "回写订单 device_id 失败", "err", uerr, "order", orderNo) + } + a.confirmSched.OnSettled(orderNo) + logger.Info("notifywatcher", "通知栏快速入账成功 ✅(比账本快 ~15s)", + "order", orderNo, "amount", ev.AmountMinor, "device", ev.DeviceID, + "provider", ev.Provider, "space", ev.Space) + case "settled_dup": + // 同一笔收款已被账本/通知另一链路入账(双链同捕竞态,真机实测 5s 内), + // 幂等跳过:不动作(防重复/防入账),仅留审计 + logger.Info("notifywatcher", "通知重复捕获,另一链路已入账(幂等跳过)", "order", orderNo) + case "no_order": + // 通知栏可能是手动收款等非订单场景;账本是 no_order 唯一判定者,避免双写。 + // 但通知栏先到达时该笔收款尚未落 unmatched——账本 5s 后复查(含 settled_dup), + // 若账本侧也无主则重复有一次记录留痕(幂等由 CreateUnmatched 无唯一约束,可接受), + // 待账本落库后此处不再单独入 unmatched(避免双链重复记录)。 + logger.Debug("notifywatcher", "通知无匹配订单(留给账本复查)", + "device", ev.DeviceID, "amount", ev.AmountMinor, "provider", ev.Provider) + default: // ambiguous + logger.Warn("notifywatcher", "通知多订单歧义(账本复查确认)", + "device", ev.DeviceID, "amount", ev.AmountMinor, "provider", ev.Provider) + } + return nil +} + +// 防止 gopay 未使用(保持依赖) diff --git a/cmd/gateway/return_url.go b/cmd/gateway/return_url.go new file mode 100644 index 0000000..68a173d --- /dev/null +++ b/cmd/gateway/return_url.go @@ -0,0 +1,72 @@ +// paygateway return_url 兜底重写:上游商户传来的回跳地址指向内网时,付款人支付成功后 +// 跳回浏览器打不开的 localhost/内网页面。网关侧无法修改上游配置,但手里有可靠来源 +// (Referer = 真实发起支付的前端地址、上游表的公网服务地址),按可信度逐层兜底。 +package main + +import ( + "net/url" + "paygateway/internal/logger" + "paygateway/internal/notify" + "strings" + + "github.com/gin-gonic/gin" +) + +// sanitizeReturnURL 返回修正后的 return_url: +// 1. 上游传的是公网地址(或空) → 原样返回 +// 2. 内网/localhost → 用 Referer origin 重写(保留 path/query) +// 3. Referer 不可用 → 用上游后台配置的公网服务地址(BaseURL)重写 +// 4. 无法判定 → 原样返回(附警示日志) +func (a *App) sanitizeReturnURL(c *gin.Context, orderNo, pid, returnURL string) string { + if returnURL == "" || !notify.IsPrivateURL(returnURL) { + return returnURL + } + fixed, source := a.publicOriginCandidate(c, orderNo, pid) + if fixed == "" { + return returnURL + } + out := rehost(returnURL, fixed) + if out == "" { + return returnURL + } + logger.Info("epay", "return_url 指向内网,已重写为付款人可达地址", + "out_trade_no", orderNo, "source", source, "origin", fixed, "to", out) + return out +} + +// publicOriginCandidate 返回可用的公网 origin + 来源标注("" 表示无可用候选) +func (a *App) publicOriginCandidate(c *gin.Context, orderNo, pid string) (string, string) { + if ref := c.GetHeader("Referer"); ref != "" { + if u, err := url.Parse(ref); err == nil && + (u.Scheme == "https" || u.Scheme == "http") && u.Host != "" && + !notify.IsPrivateURL(u.Scheme+"://"+u.Host) { + return u.Scheme + "://" + u.Host, "referer" + } + } + if a.upstreamPool != nil { + // pid 用 handleSubmit 已解析的参数(POST 表单场景 c.Query 取不到) + if up := a.upstreamPool.ByPid(pid); up != nil && up.BaseURL != "" && + !notify.IsPrivateURL(up.BaseURL) { + return strings.TrimRight(up.BaseURL, "/"), "upstream_base_url" + } + } + logger.Warn("epay", "return_url 指向内网且无公网候选,将原样跳回(付款人可能打不开)", + "out_trade_no", orderNo) + return "", "" +} + +// rehost 以 publicOrigin 的 host/origin 重写原始 URL,path/query/fragment 保留 +func rehost(raw, origin string) string { + src, err := url.Parse(raw) + if err != nil { + return "" + } + dst, err := url.Parse(origin) + if err != nil || dst.Host == "" { + return "" + } + out := *src + out.Scheme = dst.Scheme + out.Host = dst.Host + return out.String() +} diff --git a/cmd/gateway/return_url_test.go b/cmd/gateway/return_url_test.go new file mode 100644 index 0000000..0406cb1 --- /dev/null +++ b/cmd/gateway/return_url_test.go @@ -0,0 +1,59 @@ +// return_url 兜底重写测试:上游传内网/localhost 时的三层替换与保留语义。 +// +// Given 公网 return_url(或空):原样保留,不触发重写 +// Given 内网 return_url + Referer:重写为 Referer origin,路径与查询参数无损 +// Given 内网 return_url(无有效候选):返回原值 +package main + +import ( + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func ctxWithReferer(t *testing.T, referer string) *gin.Context { + t.Helper() + req := httptest.NewRequest("GET", "/submit.php", nil) + if referer != "" { + req.Header.Set("Referer", referer) + } + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = req + return ctx +} + +func TestRehost(t *testing.T) { + got := rehost("http://localhost:3000/usage-logs/common?q=2", "https://api.example.com") + want := "https://api.example.com/usage-logs/common?q=2" + if got != want { + t.Fatalf("rehost = %q want %q", got, want) + } + if rehost("http://localhost:3000/x", "") != "" { + t.Fatal("空 origin 应返回空") + } +} + +func TestSanitizeReturnURL_PublicKept(t *testing.T) { + app := &App{} + c := ctxWithReferer(t, "https://other.tld/checkout") + got := app.sanitizeReturnURL(c, "O1", "2001", "https://shop.example.com/ok") + if got != "https://shop.example.com/ok" { + t.Fatalf("公网 return_url 不应被改写,实际 %q", got) + } + if app.sanitizeReturnURL(c, "O1", "2001", "") != "" { + t.Fatal("空 return_url 应原样保留") + } +} + +// Referer 兜底:localhost return_url 重写为 Referer origin(真实发起支付的前端)。 +func TestSanitizeReturnURL_RefererWins(t *testing.T) { + app := &App{} + c := ctxWithReferer(t, "https://xapi-public.example.com/pay/checkout") + got := app.sanitizeReturnURL(c, "O2", "2001", "http://localhost:3000/usage-logs/common") + want := "https://xapi-public.example.com/usage-logs/common" + if got != want { + t.Fatalf("应重写为 Referer origin,实际 %q want %q", got, want) + } +} diff --git a/cmd/gateway/risk.go b/cmd/gateway/risk.go new file mode 100644 index 0000000..1a1ab8f --- /dev/null +++ b/cmd/gateway/risk.go @@ -0,0 +1,104 @@ +// paygateway 无订单收款行为标记(2026-09-14 需求确认): +// 直发码无法阻止他人转账,熔断无意义且会误伤正常收款—— +// 方向改为【准确记录 + 行为标记 + 告警提示立即核实】,保留司法可核对记录。 +// 打分只做信息标注(risk_level/risk_tags),绝不影响入账与订单链路。 +package main + +import ( + "context" + "fmt" + "paygateway/internal/logger" + "paygateway/internal/personal" + "strings" + "time" +) + +// 风险等级 +const ( + riskNone = 0 + riskSuspect = 1 // 建议立即核实 + riskHighWarning = 2 // 多重特征叠加,高警戒 +) + +const riskWindow = 30 * time.Minute + +// assessUnmatched 行为模式标记:对一笔无订单收款计算风险标签。 +// 标签(保守,宁可少标不误标): +// +// big_amount 单笔 ≥ PersonalBigAmount(默认 200 元,admin 可调) +// burst 同设备窗口内无订单收款 ≥5 笔(批量二维码套现特征) +// repeat_payer 同付款人窗口内重复出现 ≥2 笔(同源资金试探特征) +func (a *App) assessUnmatched(ctx context.Context, provider string, amountMinor int64, payer, deviceID string) (tags []string, level int) { + if cfg := a.cfg; cfg != nil && cfg.PersonalBigAmount > 0 && amountMinor >= cfg.PersonalBigAmount { + tags = append(tags, "big_amount") + } + recent, err := a.personalDB.ListUnmatched(ctx, "", 200) + if err == nil { + tags = append(tags, scoreFromRows(amountMinor, payer, deviceID, recent, time.Now(), riskWindow)...) + } + level = riskNone + switch { + case len(tags) >= 2: + level = riskHighWarning + case len(tags) == 1 && tags[0] == "big_amount": + level = riskHighWarning + case len(tags) == 1: + level = riskSuspect + } + return tags, level +} + +// scoreFromRows 纯计数逻辑(便于单测):按窗口过滤历史行并产出 burst/repeat_payer 标签 +func scoreFromRows(amountMinor int64, payer, deviceID string, rows []*personal.UnmatchedPayment, now time.Time, window time.Duration) (tags []string) { + burstMax, repeatMax := 0, 0 + for _, u := range rows { + if u.CreatedAt.IsZero() || now.Sub(u.CreatedAt) > window { + continue + } + if u.DeviceID != "" && u.DeviceID == deviceID { + burstMax++ + } + if payer != "" && u.Payer != "" && payer == u.Payer { + repeatMax++ + } + } + if burstMax+1 >= 5 { + tags = append(tags, "burst") + } + if repeatMax+1 >= 2 { + tags = append(tags, "repeat_payer") + } + return tags +} + +// saveUnmatched 记录无订单收款(富化 + 行为标记 + 告警提示)。 +func (a *App) saveUnmatched(ctx context.Context, provider string, amountMinor int64, note, raw, payer, deviceID string) { + tags, level := a.assessUnmatched(ctx, provider, amountMinor, payer, deviceID) + if _, err := a.personalDB.CreateUnmatched(ctx, &personal.UnmatchedPayment{ + Provider: provider, AmountMinor: amountMinor, Note: note, Raw: raw, + Status: "pending", Payer: payer, DeviceID: deviceID, + RiskTags: strings.Join(tags, ","), RiskLevel: level, + }); err != nil { + logger.Error("risk", "无订单收款落库失败", "err", err, "amount", amountMinor) + return + } + if level <= riskNone { + logger.Info("risk", "无订单收款已记录(无风险特征)", "provider", provider, "amount", amountMinor, "device", deviceID) + return + } + logger.Warn("risk", "无订单收款行为标记,建议立即核实", + "risk_tags", strings.Join(tags, ","), "risk_level", level, + "provider", provider, "amount", amountMinor, "payer", payer, "device", deviceID) + if a.alertC != nil { + a.alertC.Send("error", "疑似无订单收款(黑钱风险提示)", + fmt.Sprintf("来源: %s\n金额: ¥%.2f\n付款人: %s\n收款设备: %s\n特征: %s\n\n疑似黑钱/赃款风险,建议立即核实该笔收款;确认异常的请保全记录并联系司法部门处理。", + provider, float64(amountMinor)/100, payerOrDash(payer), deviceID, strings.Join(tags, ","))) + } +} + +func payerOrDash(p string) string { + if p == "" { + return "(通知栏无付款人信息)" + } + return p +} diff --git a/cmd/gateway/risk_test.go b/cmd/gateway/risk_test.go new file mode 100644 index 0000000..d3a69d6 --- /dev/null +++ b/cmd/gateway/risk_test.go @@ -0,0 +1,68 @@ +// 无订单收款行为标记测试:只做信息标注(tags/level),不影响入账链路。 +package main + +import ( + "context" + "testing" + "time" + + "paygateway/internal/personal" +) + +func TestAssessUnmatched_BigAmount(t *testing.T) { + app, _ := newMatchTestEnv(t) + app.cfg = &Config{PersonalBigAmount: 20000} // 200 元 + tags, level := app.assessUnmatched(context.Background(), "alipay", 20000, "", "dev-1") + if len(tags) == 0 || tags[0] != "big_amount" || level != riskHighWarning { + t.Fatalf("大额应标记 big_amount 高风险,实际 tags=%v level=%d", tags, level) + } +} + +func TestAssessUnmatched_BurstWindow(t *testing.T) { + ctx := context.Background() + app, pdb := newMatchTestEnv(t) + for i := 0; i < 5; i++ { + _, _ = pdb.CreateUnmatched(ctx, &personal.UnmatchedPayment{ + Provider: "alipay", AmountMinor: 100, DeviceID: "dev-burst", + CreatedAt: time.Now(), + }) + } + tags, level := app.assessUnmatched(ctx, "alipay", 100, "", "dev-burst") + has := false + for _, tag := range tags { + if tag == "burst" { + has = true + } + } + if !has || level != riskSuspect { + t.Fatalf("5 笔/30min 应标记 burst 疑似核实,实际 tags=%v level=%d", tags, level) + } +} + +// 窗口外的历史记录不构成 burst(防陈旧计数)。 +func TestAssessUnmatched_StaleNotCounted(t *testing.T) { + users := make([]*personal.UnmatchedPayment, 0, 6) + for i := 0; i < 6; i++ { + users = append(users, &personal.UnmatchedPayment{ + Provider: "alipay", AmountMinor: 100, DeviceID: "dev-old", + CreatedAt: time.Now().Add(-2 * time.Hour), + }) + } + tags := scoreFromRows(100, "", "dev-old", users, time.Now(), riskWindow) + for _, tag := range tags { + if tag == "burst" { + t.Fatalf("窗口外记录不应构成 burst: %v", tags) + } + } +} + +// 同付款人窗口内重复 ≥2 笔 → repeat_payer。 +func TestAssessUnmatched_RepeatPayer(t *testing.T) { + rows := []*personal.UnmatchedPayment{ + {Provider: "alipay", AmountMinor: 200, Payer: "*杰", CreatedAt: time.Now()}, + } + tags := scoreFromRows(200, "*杰", "dev-x", rows, time.Now(), riskWindow) + if len(tags) != 1 || tags[0] != "repeat_payer" { + t.Fatalf("同付款人应标记 repeat_payer,实际 %v", tags) + } +} diff --git a/cmd/tunnel/main.go b/cmd/tunnel/main.go new file mode 100755 index 0000000..fa24953 --- /dev/null +++ b/cmd/tunnel/main.go @@ -0,0 +1,86 @@ +// Package main Cloudflare Tunnel 管理工具 +// +// 启动 trycloudflare 隧道到本地端口,输出回调地址。 +// 用法: +// +// go run ./cmd/tunnel/ # 启动隧道到 8080 +// go run ./cmd/tunnel/ --port 8080 # 指定端口 +// go run ./cmd/tunnel/ --daemon # 后台运行,域名写入文件 +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "paygateway/internal/tunnel" +) + +func main() { + port := flag.Int("port", 8080, "本地服务端口") + daemon := flag.Bool("daemon", false, "后台运行,域名写入 tunnel_domain.txt") + flag.Parse() + + fmt.Println("╔" + repeat("═", 58) + "╗") + fmt.Println("║ Cloudflare Tunnel 隧道工具 ║") + fmt.Println("╚" + repeat("═", 58) + "╝") + fmt.Printf("\n[启动] 隧道 -> localhost:%d\n", *port) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + tun, err := tunnel.New(ctx, &tunnel.Config{ + LocalPort: *port, + StartTimeout: 30 * time.Second, + }) + if err != nil { + fmt.Printf("\n[!] 隧道启动失败: %v\n", err) + fmt.Println("\n请确保已安装 cloudflared:") + fmt.Println(" curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared") + os.Exit(1) + } + + fmt.Printf("\n 🎉 隧道就绪!\n") + fmt.Printf(" 公网地址: %s\n", tun.URL()) + fmt.Printf(" 回调地址: %s/alipay/notify\n", tun.URL()) + fmt.Printf(" 本地端口: localhost:%d\n", *port) + + if *daemon { + domain := tun.URL() + if domain != "" { + _ = os.WriteFile("tunnel_domain.txt", []byte(domain+"\n"), 0644) + _ = os.WriteFile("tunnel_callback.txt", []byte(domain+"/alipay/notify\n"), 0644) + fmt.Printf("\n[✓] 域名已保存到 tunnel_domain.txt\n") + fmt.Printf("[✓] 回调地址已保存到 tunnel_callback.txt\n") + } + fmt.Println("\n[完成] 隧道已在后台运行") + return + } + + // 前台运行:启动健康检查 + go tun.HealthLoop(context.Background(), 30*time.Second) + + fmt.Println("\n[提示] 隧道会一直运行直到 Ctrl+C") + fmt.Println("按 Ctrl+C 停止隧道") + + // 等待信号 + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh + + fmt.Println("\n\n[停止] 正在关闭隧道...") + tun.Stop() + fmt.Println("[完成]") +} + +func repeat(s string, n int) string { + r := make([]byte, n) + for i := range r { + r[i] = s[0] + } + return string(r) +} diff --git a/current_callback.example.txt b/current_callback.example.txt new file mode 100644 index 0000000..f1112ff --- /dev/null +++ b/current_callback.example.txt @@ -0,0 +1,5 @@ +# Copy this file to current_callback.txt for local development only. +# Do not commit production callback URLs. +alipay: https://example.com/alipay/notify +wechat: https://example.com/wechat/notify +tunnel: https://example.com diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..68d5bb0 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,38 @@ +# 多阶段构建:golang 编译 → alpine 运行时(含 adb 客户端) +# +# 网络桥接方案:容器内 adb 通过 ADB_SERVER_SOCKET 指向宿主机 adb server(USB 设备), +# 无显式依赖(每次操作 adbmgr 自动重连),容器重启不丢设备状态。 +FROM golang:1.26-alpine AS build +WORKDIR /src +# 国内网络:模块代理镜像(proxy.golang.org 直连超时) +ENV GOPROXY=https://goproxy.cn,direct +# curl+bash: go generate 下载 cloudflared 二进制(//go:embed 需要) +# APK 国内镜像:官方 dl-cdn.alpinelinux.org 国内拉包极慢(实测 70 包卡 1 分钟+) +RUN sed -i 's#https://dl-cdn.alpinelinux.org#https://mirrors.tuna.tsinghua.edu.cn#g' /etc/apk/repositories && apk add --no-cache curl bash +COPY go.mod go.sum ./ +RUN go mod download +COPY cmd cmd +COPY internal internal +COPY web web +COPY scripts scripts +# 下载 cloudflared 并嵌入二进制(用于 CF Tunnel 回调地址) +RUN bash scripts/download-cloudflared.sh +RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o /out/gateway ./cmd/gateway + +FROM alpine:3.20 +# adb 客户端 + CA 证书(云通道/回调 HTTPS) +RUN sed -i 's#https://dl-cdn.alpinelinux.org#https://mirrors.tuna.tsinghua.edu.cn#g' /etc/apk/repositories && \ + apk add --no-cache android-tools ca-certificates tzdata && \ + addgroup -S pgw && adduser -S pgw -G pgw +COPY --from=build /out/gateway /app/gateway +WORKDIR /app +# 数据/日志/配置挂载点:sqlite(data/3 个库)、lumberjack 日志、.env 敏感配置 +VOLUME ["/app/data", "/app/logs"] +ENV GATEWAY_ADDR=":8080" \ + ADMIN_ADDR=":8081" \ + ORDER_DB="data/paygateway.db" \ + LOG_FILE="logs/paygateway-{date}.log" \ + TZ="Asia/Shanghai" +USER pgw +EXPOSE 8080 8081 +ENTRYPOINT ["/app/gateway"] diff --git a/deploy/backup.sh b/deploy/backup.sh new file mode 100755 index 0000000..51419fa --- /dev/null +++ b/deploy/backup.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# PayGateway 每日数据库备份(RESILIENCE.md P0-3) +# +# 功能: +# - 备份全部 SQLite 库(paygateway / ledger / accounts / personal / upstreams) +# - 用 sqlite3 .backup 命令在线备份(WAL 模式安全,无需停服) +# - 保留最近 N 份(默认 14 天),旧备份自动清理 +# +# 使用: +# 手动: bash deploy/backup.sh +# crontab: 0 2 * * * /opt/paygateway/deploy/backup.sh >> /var/log/paygateway-backup.log 2>&1 +# systemd timer(推荐):见 deploy/paygateway.service 同目录的 deploy/README 或 docs/OPERATIONS.md "备份" +# +# 注意:请先确认系统装有 sqlite3 命令行工具(apt install sqlite3) + +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DATA_DIR="$PROJECT_DIR/data" +BACKUP_DIR="${PAYGWAY_BACKUP_DIR:-$DATA_DIR/backups}" +KEEP_DAYS="${PAYGWAY_BACKUP_KEEP_DAYS:-14}" + +# 需要备份的数据库文件(相对 data/) +DBS=(paygateway.db ledger.db accounts.db personal.db upstreams.db) + +mkdir -p "$BACKUP_DIR" +STAMP="$(date +%Y%m%d-%H%M%S)" +BACKUP_DIR_TS="$BACKUP_DIR/$STAMP" +mkdir -p "$BACKUP_DIR_TS" + +FAILED=0 +for db in "${DBS[@]}"; do + SRC="$DATA_DIR/$db" + if [ ! -f "$SRC" ]; then + echo "[skip] $db 不存在" + continue + fi + DST="$BACKUP_DIR_TS/$db" + # 在线备份:SQLite 官方推荐方式,WAL 模式下安全(非简单 cp) + if sqlite3 "$SRC" ".backup '$DST'" 2>/dev/null; then + # 设置权限与源一致(含敏感账户库) + chmod 600 "$DST" + echo "[ok] $db ($(du -h "$DST" | cut -f1))" + else + echo "[FAIL] $db 备份失败(sqlite3 是否安装?)" + FAILED=1 + fi +done + +# 清理过期备份(按日期目录保留 KEEP_DAYS 天) +find "$BACKUP_DIR" -maxdepth 1 -type d -name "20*" -mtime +"$KEEP_DAYS" -exec rm -rf {} \; 2>/dev/null || true + +echo "=== 备份完成: $BACKUP_DIR_TS(保留 ${KEEP_DAYS} 天) ===" +exit $FAILED diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..2ae98b9 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,30 @@ +# PayGateway 容器化部署(WiFi adb 网络桥接版) +# +# 使用前准备: +# 2. .env 含敏感配置(ACCOUNT_MASTER_KEY / JWT_SECRET / ORDER_DB 等),只读挂载。 +# 3. 首次启动:docker compose -f deploy/docker-compose.yml up -d --build +# +# USB 直连手机走另一条路:宿主机跑 adb server,取消 network_mode 注释此行按需切换, +# environment: +# - ADB_SERVER_SOCKET=tcp:172.17.0.1:5037 # docker0 宿主地址 +services: + gateway: + build: + context: .. + dockerfile: deploy/Dockerfile + container_name: paygateway + network_mode: host + user: "1000:1000" + env_file: ../.env + environment: + - HOME=/app/data # pgw 用户的 HOME 默认 / 不可写,cloudflared 缓存目录需可写路径 + volumes: + - ../data:/app/data + - ../logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/deploy/paygateway.service b/deploy/paygateway.service new file mode 100644 index 0000000..1bf7a7f --- /dev/null +++ b/deploy/paygateway.service @@ -0,0 +1,44 @@ +# PayGateway systemd 守护单元(RESILIENCE.md P0-3) +# +# 安装步骤: +# sudo cp deploy/paygateway.service /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable --now paygateway +# +# 前提: +# 1. 已编译二进制:go build -o gateway ./cmd/gateway/ +# 2. .env 位于 WorkingDirectory 下(服务自动加载) +# 3. 建议先手动跑一遍 go run ./cmd/gateway 确认启动正常 +# 4. 若部署路径与下方 WorkingDirectory 不同,请同步替换 4 处路径 +# (WorkingDirectory / ExecStart / ReadWritePaths 中各 1 处) +# +# 常用命令: +# systemctl status paygateway # 查看状态 +# journalctl -u paygateway -f # 实时日志 +# systemctl restart paygateway # 重启(CF 隧道域名每次重启会变,详见 README 升级章节) +[Unit] +Description=PayGateway 支付网关(易支付协议 + 个人免签) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +# 工作目录 = 项目根目录(.env / data/ 都在这里) +WorkingDirectory=/opt/paygateway +# 编译产物路径(请先执行 go build -o gateway ./cmd/gateway/) +ExecStart=/opt/paygateway/gateway +Restart=always +RestartSec=3 +# 优雅停止:给 10s 让回调 worker 收尾 +TimeoutStopSec=10 +KillSignal=SIGTERM + +# 安全加固 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=full +ReadWritePaths=/opt/paygateway/data +ProtectHome=read-only + +[Install] +WantedBy=multi-user.target diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100755 index 0000000..3131c18 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,560 @@ +# PayGateway 架构设计(最终版) + +> 目标:高性能、高稳定、资金零差错、**支付方式可插拔**的支付网关 +> 通道底座:**gopay v1.5.119**(9 种通道现成:支付宝/微信/抖音/QQ/通联/拉卡拉/PayPal/Apple/扫呗) +> 未来扩展:链上支付(crypto)→ **预留扩展位**,不现在实现 + +--- + +## 1. 核心思想:三层各司其职,不重复造轮子 + +``` +┌────────────────────────────────────────────────────────────┐ +│ ① 协议层(对外接口,永不改变) │ +│ epay: /submit.php /api.php ← New API 等易支付系 │ +│ REST: /v1/orders ← 未来其他项目(预留) │ +│ type=alipay|wxpay|paypal|usdt → 路由到对应通道 │ +├────────────────────────────────────────────────────────────┤ +│ ② 编排层(资金安全核心,与具体通道无关) │ +│ 订单状态机 / 金额校验 / 幂等 / 可靠回调(outbox) / 对账 │ +│ → 只认统一接口 PaymentChannel,不认任何 SDK 细节 │ +├────────────────────────────────────────────────────────────┤ +│ ③ 通道层(可插拔,每个通道一个包) │ +│ ┌─────────────── gopay 库(现成 9 通道)──────────────┐ │ +│ │ alipay / wechat / paypal / douyin / qq / allinpay │ │ +│ │ lakala / saobei / apple │ │ +│ └────────────────────────────────────────────────────┘ │ +│ crypto/(★ 预留目录:未来链上支付,接口已留好) │ +└────────────────────────────────────────────────────────────┘ +``` + +**职责划分:** +- **gopay** = 通道适配器工厂(把 9 种支付 SDK 的差异封装掉) +- **paygateway** = 协议翻译 + 资金安全编排(不重复实现支付 SDK,只做编排) +- **crypto** = 未来加一个目录,接口现成 + +--- + +## 2. 通道抽象(接口 + 注册表) + +### 2.1 统一接口 + +```go +package channel + +// PaymentChannel 所有支付通道必须实现的统一接口 +type PaymentChannel interface { + // Name 通道标识:alipay / wechat / paypal / usdt ... + Name() string + + // Kind 通道类型:回调驱动 or 监听驱动(链上) + Kind() ChannelKind // CallbackKind / ListenerKind + + // Modes 支持的支付模式 + Modes() []PayMode // Page / Wap / QRCode / Address + + // CreatePayment 下单:返回支付凭据(跳转链接/二维码/链上地址) + CreatePayment(ctx context.Context, req *PayRequest) (*PayCredential, error) + + // VerifyNotify 验签回调(CallbackKind 用) + VerifyNotify(ctx context.Context, raw []byte) (*NotifyResult, error) + + // QueryOrder 主动查单(对账 + 补偿兜底) + QueryOrder(ctx context.Context, outTradeNo string) (*OrderStatus, error) + + // Listener 事件监听(ListenerKind 必须实现;CallbackKind 返回 nil) + Listener() EventListener +} + +type PayRequest struct { + OutTradeNo string // 网关订单号 + Subject string // 商品名 + AmountMinor int64 // 金额(最小单位,法币=分,链上=wei/satoshi) + Currency string // CNY / USD / USDT + NotifyURL string + ReturnURL string + PayMode PayMode // 用户选择的支付模式 +} + +type PayCredential struct { + PayURL string // 跳转链接 + QRCode string // 二维码内容 + Address string // 链上收款地址(未来) + ExpireAt time.Time +} + +type NotifyResult struct { + Valid bool // 验签是否通过 + ChannelTxID string // 通道交易号(去重依据) + Status string // SUCCESS / CLOSED / FAILED + AmountMinor int64 // 实付金额(必须与订单比对!) +} +``` + +### 2.2 注册表(配置驱动,零代码路由) + +```go +// 注册:每个通道 init() 或显式调用 +func Register(c PaymentChannel) { registry[c.Name()] = c } + +// 路由:协议层只写一行 +ch, ok := channel.Get(c.Query("type")) // "alipay" → 支付宝通道 +if !ok { c.String(400, "不支持的支付方式") } +``` + +**新增一种支付方式 = 3 步:** +1. `internal/channel/wechat/wechat.go` — 实现 4 个方法(内部调 gopay 的微信 SDK) +2. `Register(...)` 一行注册 +3. 配置 `.env` 加微信的密钥 + +→ 协议层、订单核心、存储、页面 **零改动**。 + +### 2.3 crypto 预留位(未来链上) + +```go +// internal/channel/crypto/ 目前只放: +// README.md — 设计说明(HD钱包/确认数/重组处理) +// stub.go — 返回 "未启用" 的占位实现(注册表可查但拒绝下单) + +// 未来实现时: +// Kind() = ListenerKind +// Listener() = 区块监听器(WebSocket + 轮询兜底) +// CreatePayment() = HD 钱包派生一次性地址 +// QueryOrder() = 查链上确认数 +``` + +注册表已经支持 `ListenerKind` 通道,**未来 crypto 落地 = 填实现,不动架构**。 + +--- + +## 3. 资金安全编排层(最不能错的部分) + +``` +下单: 协议层 → order.Create() → ① 事务写订单(CREATED) → ② 调通道 + → ③ 事务更新支付凭据 → ④ 返回页面 + +支付: 通道事件(回调/监听) + → ① 验签 → ② 金额比对(实付==订单,不等→拒绝+告警) + → ③ 幂等检查(channel_tx_id 唯一,重复→直接应答成功) + → ④ 条件更新状态(WHERE status='WAIT_PAY',失败→拒绝) + → ⑤ 写 payment_events 流水 → ⑥ 写 outbox 回调任务 + +结算: outbox worker → 回调接入方(失败重试 1m/5m/15m/1h → dead 告警) + 定时补偿:PAID 且未通知 > 30s → 补发 + 定时对账:通道实付 vs 订单金额 +``` + +**5 条铁律**(写入代码评审清单): +1. 金额只用最小单位 int64,永不用 float +2. 状态变更必须 `WHERE status=当前态`(防回跳/防覆盖) +3. 实付金额 ≠ 订单金额 → 拒绝 + 告警 +4. 同一 channel_tx_id 重复事件 → 幂等返回成功 +5. 每笔资金事件落 payment_events,可追溯 + +--- + +## 4. 数据库设计(资金安全地基,最关键的组件) + +### 4.1 选型决策 + +| 阶段 | 数据库 | 理由 | +|------|--------|------| +| **P1 开发/单机生产** | **SQLite**(`modernc.org/sqlite` 纯 Go,无 CGO) | 本机已有;单文件零运维;事务/行锁/唯一约束完整;交易量几百 TPS 足够 | +| **正式生产/多机** | PostgreSQL | 用户已声明基础设施有;行级锁 + 强一致 + 可扩展 | + +> **关键**:`Store` 接口抽象,SQLite 与 PG 实现可无缝切换。**P1 直接上 SQLite,不做 JSON 过渡** —— 资金数据从一开始就落在真数据库里。 + +### 4.2 数据模型(SQLite 版,PG 同构) + +```sql +-- 订单表:唯一约束防重复下单,条件更新防覆盖 +CREATE TABLE orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + out_trade_no TEXT NOT NULL UNIQUE, -- 接入方订单号(幂等依据) + gateway_no TEXT NOT NULL UNIQUE, -- 网关订单号 + channel TEXT NOT NULL, -- alipay/wechat/paypal/usdt + channel_tx_id TEXT, -- 通道交易号(回调去重) + amount_minor INTEGER NOT NULL, -- 金额(最小单位:分) + currency TEXT NOT NULL DEFAULT 'CNY', + status TEXT NOT NULL, -- CREATED/WAIT_PAY/PAID/... + payer TEXT, -- 付款方标识 + pay_url TEXT, -- 支付链接/二维码/链上地址 + return_url TEXT, + notify_url TEXT, + created_at TEXT NOT NULL, + paid_at TEXT, + notified_at TEXT, + UNIQUE (channel, channel_tx_id) -- 防回调重放 +); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_orders_created ON orders(created_at); +CREATE INDEX idx_orders_channel ON orders(channel); + +-- 交易流水:资金审计,只增不改 +CREATE TABLE payment_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + event_type TEXT NOT NULL, -- created/paid/closed/failed/refunded + channel_tx_id TEXT, + amount_minor INTEGER NOT NULL, + raw_payload TEXT, -- 原始回调/区块数据 JSON + created_at TEXT NOT NULL +); +CREATE INDEX idx_events_order ON payment_events(order_id); + +-- 可靠回调投递(Outbox) +CREATE TABLE notify_tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + payload TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', -- pending/success/dead + retry_count INTEGER NOT NULL DEFAULT 0, + next_retry_at TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE INDEX idx_notify_status ON notify_tasks(status, next_retry_at); + +-- 回调投递日志 +CREATE TABLE callback_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER NOT NULL, + http_status INTEGER, + response TEXT, + latency_ms INTEGER, + created_at TEXT NOT NULL +); + +-- 应用日志(管理界面查询用,与文件日志双写) +CREATE TABLE app_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL, -- debug/info/warn/error + scope TEXT, -- 模块:order/channel/notify/scheduler + message TEXT NOT NULL, + trace_id TEXT, -- 请求追踪 ID + fields TEXT, -- 结构化字段 JSON + created_at TEXT NOT NULL +); +CREATE INDEX idx_logs_level_time ON app_logs(level, created_at); +CREATE INDEX idx_logs_scope_time ON app_logs(scope, created_at); +CREATE INDEX idx_logs_trace ON app_logs(trace_id); +``` + +### 4.3 资金安全约束(写库必须遵守) + +```go +// 状态变更必须条件更新(防回跳/防覆盖) +UPDATE orders SET status='PAID', paid_at=?, channel_tx_id=? +WHERE out_trade_no=? AND status='WAIT_PAY' +// 影响行数 = 0 → 状态非法,拒绝(幂等/防覆盖双保险) + +// 金额校验 +实付金额(amount_minor) == 订单金额 → 才允许 PAID +不等 → 记录 payment_events(event_type='amount_mismatch') + 告警 + 拒绝 + +// 回调去重 +INSERT INTO orders(channel, channel_tx_id) ... ON CONFLICT DO NOTHING +``` + +--- + +## 5. 交易不可篡改设计(资金审计账本) + +> 目标:**每一笔交易都不可篡改** —— 任何人(包括管理员/DBA)改任何一条记录都能被检测。 +> 方案:**哈希链 + 数字签名 + 只增不改 + 每日快照锚定**(企业级审计账本标准做法)。 + +### 5.1 哈希链(账本防篡改核心) + +``` +genesis(固定) → entry1 → entry2 → entry3 → ... → entryN + ↑ ↑ ↑ + prev_hash prev_hash prev_hash ← 每条都链接前一条 + +entry_hash(i) = SHA256( prev_hash(i-1) + payload_hash(i) ) +payload_hash(i) = SHA256( 事件全部内容: 订单号+金额+状态+通道交易号+时间+原始数据 ) +``` + +**篡改任意一条 → 它后面的所有 entry_hash 全部失配 → 校验必失败。** +**数据库整体被替换 → 与每日快照锚点比对 → 发现。** + +### 5.2 数字签名(防伪造 + 来源验证) + +- 独立**审计密钥对**(与支付签名密钥物理分离),私钥存 env/密钥文件 +- 每条 entry 的 `entry_hash` 用私钥 **ECDSA 签名** +- 任何人想改数据必须重新签名 → 没有私钥就无法伪造合法记录 +- 验签失败 = 数据被改过或来源可疑 → 立即告警 + +### 5.3 数据库层硬约束(SQLite/PG 双实现) + +```sql +-- 账本表:只追加,禁止修改删除(SQLite 触发器强制) +CREATE TABLE audit_ledger ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prev_hash TEXT NOT NULL, -- 前一哈希(链) + payload_hash TEXT NOT NULL, -- 本条事件内容哈希 + entry_hash TEXT NOT NULL, -- SHA256(prev+payload) + signature TEXT NOT NULL, -- ECDSA(entry_hash) + event_id INTEGER, -- 关联 payment_events.id + created_at TEXT NOT NULL +); + +-- 禁止 UPDATE / DELETE(数据库层防篡改) +CREATE TRIGGER trg_ledger_no_update BEFORE UPDATE ON audit_ledger +BEGIN SELECT RAISE(ABORT, 'audit_ledger is append-only'); END; +CREATE TRIGGER trg_ledger_no_delete BEFORE DELETE ON audit_ledger +BEGIN SELECT RAISE(ABORT, 'audit_ledger is append-only'); END; +``` + +> PG 版:DB 账号只授予 INSERT/SELECT 权限(无 UPDATE/DELETE),双保险。 + +### 5.4 每日快照(外部锚定点) + +```sql +CREATE TABLE daily_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_date TEXT NOT NULL UNIQUE, + last_hash TEXT NOT NULL, -- 当日最后 entry_hash(锚点) + signature TEXT NOT NULL, -- 对锚点签名 + created_at TEXT NOT NULL +); +``` + +- 每日 00:00 生成快照:当前链尾哈希 + 签名 +- 可选增强:把快照哈希发布到外部(邮件/对象存储/链上)→ 防数据库整体替换 +- **改任何历史数据 → 与快照锚点比对即发现** + +### 5.5 完整性校验任务(scheduler 每日 + 手动) + +```go +VerifyLedger() → 从 genesis(或上一快照)重算整条哈希链 + → 校验每条 ECDSA 签名 + → 比对 daily_snapshots 锚点 + → 全过 → INFO 记录"账本完整" + → 任何失配 → ERROR + 管理后台红色告警(重大安全事件) +``` + +### 5.6 资金事件写入流程(唯一入口,防绕过) + +``` +通道回调/监听 → order服务校验通过 + → ① payment_events INSERT(业务库,只增) + → ② audit_ledger.Append(事件内容) ← 哈希链+签名(独立账本库 ledger.db) + → ③ 账本写入失败 → 回滚订单状态 + 告警(资金事件必须有账本记录) +``` + +**所有资金变更只能走这一个入口**,不允许其他代码直接写事件表。 +> 注:账本在独立数据库中,无法与业务库同事务;因此采用"业务库先记事件 → 账本库追加 → 失败则回滚订单状态并告警"的顺序,确保资金事件必有账本记录。 + +### 5.7 管理后台联动 + +| 功能 | 说明 | +|------|------| +| 账本状态 | 显示最后校验时间/结果/链长 | +| 手动校验 | 按钮触发全链校验 | +| 篡改告警 | 校验失败 → 顶部红色横幅 + 告警日志 | +| 事件详情 | 每条事件可看 payload_hash / signature / 原始数据 | + +### 5.8 独立账本数据库(ledger.db,安全隔离) + +> **架构升级**:账本从业务库中独立出来,单独一个 SQLite 文件,与订单/日志/回调完全隔离。 + +``` +paygateway.db(业务库) ledger.db(账本库 🛡️) +├── orders ├── audit_ledger +├── payment_events └── daily_snapshots +├── notify_tasks +├── callback_logs +├── app_logs +└── settings +``` + +**隔离带来的安全提升:** + +| 防护层 | 机制 | 效果 | +|--------|------|------| +| 文件隔离 | 账本独立 `ledger.db` | 删业务库不影响账本,审计线索永存 | +| 文件权限 | `chmod 0600` | 仅文件所有者可读写,其他用户/进程无法访问 | +| 触发器 | 两个表均有 UPDATE/DELETE 触发器 | 通过 SQL 无法修改/删除任何账本数据 | +| WAL 模式 | `PRAGMA journal_mode=WAL` | 崩溃安全,写入不丢数据 | +| FULL 同步 | `PRAGMA synchronous=FULL` | 每次写入等待落盘,防断电丢数据 | +| 连接隔离 | `SetMaxOpenConns(1)` | 单连接串行写,避免并发写入冲突 | + +**实现方式:** + +```go +// store.LedgerDB 独立账本存储(internal/store/ledgerdb.go) +ledgerDB, err := store.NewLedgerDB("ledger.db") // 0600 + WAL + 触发器 +lg, err := ledger.New(ledgerDB, cfg.LedgerPrivKey) // 账本只依赖独立库 +// admin 同时注入业务库 + 账本库 +adminSrv := admin.New(st, ledgerDB, ...) +``` + +### 5.9 管理后台 API 与前端 + +**账本相关 API:** + +| 端点 | 说明 | +|------|------| +| `GET /api/ledger` | 账本条目列表(哈希链可视化) | +| `POST /api/ledger/verify` | 手动触发全链校验(哈希链 + 签名 + 快照锚点) | +| `GET /api/ledger/snapshots` | 每日快照列表 | +| `POST /api/ledger/snapshot` | 手动创建当日快照 | +| `GET /api/ledger/public-key` | 返回 ECDSA P256 公钥(供第三方验证) | +| `GET /api/ledger/export` | 导出账本 + 快照为 CSV(审计报告) | + +**前端功能(Vue3 Ledger 页面):** + +- 📋 **账本条目**:哈希链可视化(每条含 prev_hash / entry_hash / 签名) +- 🔍 **校验账本**:一键全链校验,弹窗展示结果(通过/失败 + 错误明细 + 快照锚点状态) +- 📸 **快照管理**:卡片列表 + 详情弹窗 + **锚点核对**(对比快照链尾与当前链尾) +- 🔑 **账本公钥**:展示 PEM 公钥,可复制供外部审计验证 +- ⬇️ **导出 CSV**:一键下载完整审计报告(含所有条目 + 快照) +- 🔗 **事件跳转**:点击事件 ID 跳转订单页搜索 + +**测试覆盖(internal/ledger/ledger_test.go):** + +| 测试 | 验证点 | +|------|--------| +| TestAppendAndVerify | 追加 + 全链校验通过 | +| TestHashChainLinked | 哈希链正确连接 | +| TestTamperDetected | 篡改条目 → 校验失败 | +| TestTamperSignatureDetected | 篡改内容 → 签名校验失败 | +| TestSnapshotCreateAndVerify | 快照创建 + 锚点匹配 | +| TestSnapshotAnchorMismatch | 数据库替换 → 锚点不匹配 | +| TestSignatureUnique | 防重放(同内容不同事件不同签名) | + +--- + +## 6. 日志系统设计(健壮 + 可查询) + +### 5.1 双写架构:文件(排障)+ 数据库(查询) + +``` +应用 → slog(结构化) → ① 文件轮转日志(/var/log/paygateway/*.log) + → ② 异步批量写 app_logs 表(管理界面查询) +``` + +### 5.2 技术选型 + +| 项 | 方案 | +|----|------| +| 日志库 | Go 标准库 **slog**(结构化,JSON 输出) | +| 轮转 | `gopkg.in/natefinch/lumberjack.v2`(按大小/天数切割,防磁盘爆) | +| 写库 | 异步批量(攒 50 条或 1s flush),避免日志拖慢交易主链路 | +| 级别 | debug / info / warn / error(env 可配) | +| 追踪 | 每个 HTTP 请求生成 `trace_id`(中间件),贯穿 下单→回调→通知 全链路 | + +### 5.3 关键审计日志(钱相关的必须全记) + +```go +// 资金事件:writeEvent(orderID, "paid", amountMinor, rawPayload) +// - created / paid / closed / failed / amount_mismatch / refunded +// 回调事件:writeEvent(orderID, "notify_attempt", ..., httpStatus) +// 安全事件:writeEvent(orderID, "verify_failed", ..., 原因) ← 验签失败必须告警 +``` + +--- + +## 7. Web 管理后台设计 + +### 6.1 形态:同进程嵌入式(单二进制部署) + +- 同一 Go 进程,路由组 `/admin/*`(端口可独立配置 `ADMIN_ADDR`) +- 内置 HTML/JS/CSS(`embed` 打包),无前端构建链 +- **登录鉴权**:管理 token(`ADMIN_TOKEN` env)+ 可选 Basic Auth;生产必须启用 +- **接口全部走 `/admin/api/*`**,返回 JSON + +### 6.2 功能清单 + +| 页面 | 功能 | +|------|------| +| **仪表盘** | 今日订单量/交易额/成功率/待通知数/告警数(卡片 + 简易图表) | +| **订单管理** | 列表(状态/通道/金额/时间筛选,分页)→ 详情(状态机时间线 + 支付凭据 + 原始回调) | +| **交易流水** | payment_events 查询(按订单/事件类型/时间) | +| **日志查看** | app_logs 查询(级别/模块/trace_id 过滤,实时 tail) | +| **回调管理** | notify_tasks 列表;**手动补发**按钮(dead 任务重投);重试状态 | +| **通道管理** | 已注册通道列表 + 启用/停用开关(配置驱动) | +| **对账报告** | 定时对账结果(订单 vs 通道实付) | +| **账本校验** | 哈希链完整性校验 + 篡改告警(红色横幅) | +| **系统状态** | 运行时长/goroutine/内存/最近错误 | + +### 6.3 页面路由规划 + +``` +GET /admin/login # 登录页 +GET /admin/ # 仪表盘(SPA,JS 拉 /admin/api/*) +GET /admin/orders # 订单页 +GET /admin/logs # 日志页 +GET /admin/notify # 回调页 +GET /admin/api/dashboard # JSON API +GET /admin/api/orders?status=&channel=&page= +GET /admin/api/orders/:no # 订单详情(含事件流) +GET /admin/api/logs?level=&scope=&trace=&page= +GET /admin/api/notify/tasks?status= +POST /admin/api/notify/:id/retry # 手动补发 +GET /admin/api/channels # 通道状态 +GET /admin/api/health # 系统状态 +``` + +--- + +## 8. 目录结构(最终目标态) + +``` +paygateway/ +├── cmd/gateway/main.go # 入口:组装 config/store/registry/router +├── internal/ +│ ├── config/ # .env 加载 + 通道密钥管理 +│ ├── model/ # Order / 状态常量 / 金额类型 +│ ├── logger/ # slog 封装:文件轮转 + 异步写库 + trace_id +│ ├── epay/ # 易支付协议(submit/api/notify 签名) +│ ├── rest/ # REST API(预留) +│ ├── order/ # 编排层:状态机/幂等/金额校验/对账 +│ ├── channel/ +│ │ ├── registry.go # 注册表 + Get() +│ │ ├── interface.go # PaymentChannel / PayRequest / ... +│ │ ├── alipay/ # 支付宝(gopay)——现有代码迁入 +│ │ ├── wechat/ # 微信(gopay)——未来 +│ │ ├── paypal/ # PayPal(gopay)——未来 +│ │ └── crypto/ # ★ 预留:stub + README(未来链上) +│ ├── store/ # Store 接口 + sqlite(默认)/pg 实现 +│ │ ├── store.go # Store 接口 + 数据模型 +│ │ ├── sqlite.go # 业务库 SQLite 实现(订单/事件/日志) +│ │ ├── sqlite_rest.go # 业务库查询实现 +│ │ └── ledgerdb.go # ★ 独立账本库(ledger.db,0600 权限 + 触发器) +│ ├── notify/ # outbox worker + 重试 +│ ├── ledger/ # ★ 资金审计账本(哈希链+签名+校验) +│ ├── scheduler/ # 关单/补偿/对账/账本校验定时任务 +│ ├── admin/ # ★ 管理后台:路由 + 页面(embed) + API +│ ├── web/ # 二维码页/中转页渲染 +│ └── metrics/ # Prometheus(预留) +├── migrations/ # SQL 迁移(sqlite/pg) +├── docs/ARCHITECTURE.md # 本文档 +└── go.mod +``` + +--- + +## 9. 实施计划(保持现有功能不回归) + +| 阶段 | 内容 | 验收 | +|------|------|------| +| **P1 基础重建** | 目录分层 + Store 接口 + **SQLite 落地** + 支付宝迁入 `channel/alipay` + 日志系统(文件+DB) + **audit_ledger 哈希链** | 现有沙箱链路回归 ✅;订单/日志/账本入库 ✅ | +| **P2 管理后台** | `/admin` 页面(仪表盘/订单/日志/回调)+ API + token 鉴权 | 浏览器可查订单、日志、交易流水 | +| **P3 资金安全** | 状态机条件更新 + 金额校验 + 幂等 + outbox 回调重试 + **账本完整性校验任务** + **独立账本库 ledger.db** + **账本管理前端** | 模拟重复/篡改回调不产生错误;篡改一条记录 → 校验失败 ✅;账本独立库 + 0600 权限 + 触发器 ✅;前端校验/快照/公钥/导出 ✅ | +| **P4 通道扩展** | 用 gopay 接微信(验证"3 步加通道"流程) | 微信沙箱链路跑通 | +| **P5 crypto 预留** | crypto stub + README + 注册表支持 ListenerKind | `type=usdt` 返回"未启用" | +| **P6 生产化** | 按你们基础设施切 PG + 监控 + 对账 + 部署脚本 | 生产验收 | + +--- + +## 10. 为什么这是最佳实践 + +1. **不重复造轮子**:9 种支付 SDK 由 gopay 维护,paygateway 只做编排 → 通道越多,收益越大 +2. **加支付方式像插 U 盘**:新通道 = 新包 + 注册 + 配置,核心零改动 +3. **协议层稳定**:易支付/REST 接口永不变 → 现有上游(New API)不受影响 +4. **资金安全集中**:所有通道都过同一套状态机/金额校验/幂等 → 只维护一套安全逻辑 +5. **链上预留**:接口已含 ListenerKind + Address 凭据 → 未来 crypto 落地不动架构 +6. **存储可换**:SQLite(单机起步) → PG(生产),`Store` 接口平滑切换 +7. **可观测性内置**:日志双写(文件+DB)+ 管理后台 → 出问题能查、能补、能对账 +8. **交易不可篡改**:哈希链 + 签名 + 只增不改 + 每日快照 → 任何篡改必被发现 diff --git a/docs/DESIGN-DEVICE-ACTOR.md b/docs/DESIGN-DEVICE-ACTOR.md new file mode 100644 index 0000000..9812c68 --- /dev/null +++ b/docs/DESIGN-DEVICE-ACTOR.md @@ -0,0 +1,25 @@ +# DeviceActor:设备操作串行化 + +## 背景 + +多个后台任务可能同时需要访问同一台授权设备。直接并发执行会导致前台状态、读取结果和恢复动作互相干扰,因此设备访问需要统一的串行化入口。 + +## 模型 + +```text +caller -> queue -> DeviceActor -> device adapter + | + +-> lease / timeout / health event +``` + +- 一个设备同一时刻只有一个 active actor; +- 请求通过队列进入,调用方等待结果或超时返回; +- lease 到期后自动释放,避免异常任务永久占用设备; +- 所有失败转化为结构化健康事件并可触发告警; +- 手动动作和自动动作不能绕过 actor 直接操作设备。 + +## 适用边界 + +该模型适用于授权的移动设备测试、UI 自动化和设备实验室。它不提供未经授权访问第三方账户或绕过平台安全控制的方法。 + +实现位于 `internal/ledgermon/` 和 `internal/adbmgr/`,测试位于对应的 `*_test.go` 文件。 diff --git a/docs/DESIGN-FAIR-SCHEDULER.md b/docs/DESIGN-FAIR-SCHEDULER.md new file mode 100644 index 0000000..f06cd9f --- /dev/null +++ b/docs/DESIGN-FAIR-SCHEDULER.md @@ -0,0 +1,20 @@ +# 公平设备调度:设计概览 + +本文档描述多任务设备池的通用调度原则,不包含特定平台的账号、页面或操作细节。 + +## 目标 + +- 在设备能力上限内分配任务; +- 避免单个账户或任务长期占用设备; +- 设备异常时快速降权或排除; +- 让调度结果可解释、可观测、可测试。 + +## 评分因素 + +调度器可以综合考虑当前负载、历史任务耗时、设备健康状态、最近失败次数、账户冷却时间、任务优先级、设备能力上限和队列长度。 + +调度器只负责选择可用执行资源,不负责确认业务事实。业务确认仍由订单状态机、匹配器和审计账本完成。 + +## 安全边界 + +设备操作必须经过 actor 串行化和授权检查。设备离线、任务超时或结果不明确时,调度器应停止继续分配并生成健康事件,而不是猜测任务已成功。 diff --git a/docs/DESIGN-NOTIFY-RETRY-LATEST-URL.md b/docs/DESIGN-NOTIFY-RETRY-LATEST-URL.md new file mode 100644 index 0000000..cd40aa1 --- /dev/null +++ b/docs/DESIGN-NOTIFY-RETRY-LATEST-URL.md @@ -0,0 +1,224 @@ +# dead 通知手动重试(用最新回调地址) + 删除 实施设计(DESIGN-NOTIFY-RETRY-LATEST-URL) + +> 状态:**已实现**(`order.Service.BuildNotifyPayload` 单一来源;`handleRetryDead` 重发前按订单最新 +> NotifyURL 重建 payload 并重新签名;新增 `handleDeleteDead` + `DeleteNotifyTask`;store 新增 +> `GetNotifyTask`/`UpdateNotifyTaskPayload`;Reconciler.vue 死信页增删除按钮,NotifyTasks.vue 手动重试接入真实 API) +> 关联:docs/RESILIENCE.md(对账兜底)、docs/ARCHITECTURE.md(outbox 回调队列) +> 影响模块:`internal/order`、`internal/store`、`internal/admin/reconciler`、`web/admin` +> 目标:**死信/失败回调任务手动重试时,用订单当前最新的 notify_url 重建 payload 并重新签名投递, +> 解决「配置时 notify_url 错误 → 重发仍打到旧地址 → 一直回调不了 → 死信」;并提供彻底删除死信任务的能力。** + +--- + +## 1. 背景与问题(现状代码事实) + +### 1.1 Outbox 回调队列现状 + +- 生产者 `order.Service.EnqueueNotify`(internal/order/service.go:1126):支付成功/人工入账后调用, + 构造易支付协议参数(含 `notify_url` = `o.NotifyURL`),`epay.BuildSignedParams(params, signKey(pid))` + 签名,JSON 序列化为 `payload`,`CreateNotifyTask` 落库。 +- 消费者 `notify.Worker`(internal/notify/worker.go):`process` 解析 payload, + `notifyURL := params["notify_url"]`(worker.go:88)作为发送地址,body = 全部 params(含签名)。 + 超过 `MaxRetries=6` 标记 `dead`(worker.go:142-144)。 +- 状态:`pending` / `success` / `dead`(store.go:121)。 + +### 1.2 关键缺陷:payload 固化 notify_url,重放不刷新 + +- `EnqueueNotify` 在**任务创建时**把 `o.NotifyURL` 写进 payload。 +- worker 发送从 **payload** 读 `notify_url`,不是订单当前值。 +- 现有死信重发 `admin.Handler.handleRetryDead`(internal/admin/reconciler.go:287)只调 + `UpdateNotifyTask(ctx, id, "pending", 0, now)`——仅更新 `status/retry_count/next_retry_at/updated_at`, + **完全不碰 payload**。因此死信重发仍打到**旧地址**。 +- 后果:商户配置时把 notify_url 配错,或之后改了回调地址,死信重发永远打到旧地址 → 一直回调不了。 + +### 1.3 现状已有 vs 缺失 + +| 能力 | 现状 | +|---|---| +| 死信列表 | 已有 `GET /api/reconciler/notify-dead`(handleListDead) | +| 死信重发 | 已有 `POST /api/reconciler/notify-dead/:id/retry`,但**不刷新地址** | +| NotifyTasks.vue 手动重试 | **占位**(web/admin/src/views/NotifyTasks.vue:123-126,只弹提示) | +| 彻底删除死信 | **完全没有**(无 DeleteNotifyTask / DELETE 路由) | + +--- + +## 2. 设计决策 + +### 2.1 重试用订单最新 notify_url(核心) + +重发前:通过 `task.OrderID` → `GetOrderByID` 取订单,**用订单当前 `NotifyURL` 重建 payload 并重新签名**, +再重置为 `pending`。理由: +- worker 发送地址 = payload 里的 `notify_url`,所以必须重建 payload 才能换地址。 +- 商户端按"收到的全部参数重算签名"验签(worker.go:94-96 注释),改了 notify_url 必须**重新签名**, + 否则验签失败。因此必须复用 `EnqueueNotify` 的完整构造+签名逻辑,不能只改 notify_url 字符串。 + +**单一来源重构**:把 `EnqueueNotify` 的 payload 构造逻辑提取为 `Service` 的导出方法 +`BuildNotifyPayload(o *model.Order) string`,`EnqueueNotify` 与重试 handler 都调用它, +保证字段与签名完全一致、单一来源。 + +### 2.2 彻底删除死信任务 + +提供 `DELETE` 语义:新增 `handleDeleteDead`(`POST /reconciler/notify-dead/:id/delete`)。 +**风险提示**:若订单已支付但通知未送达,删除后无法再自动补发(人工可重新入队兜底,见下)。属"确认放弃该笔通知"操作。 + +### 2.3 前端 + +- `Reconciler.vue` 死信页:现有"重发"按钮后端已会刷新地址(无需前端改逻辑);新增"删除"按钮(二次确认)。 +- `NotifyTasks.vue`:修正占位手动重试——复用 `retryDeadNotify`/`delete`,接入真实后端;或移除占位避免误导。 + (最小改动:让占位按钮调用真实 retry API。) + +--- + +## 3. 实现细节 + +### 3.1 `internal/order/service.go` — payload 构造单一来源 + +新增导出方法(含签名),`EnqueueNotify` 内部复用它: + +```go +// BuildNotifyPayload 构造易支付协议回调参数并签名,返回 JSON payload。 +// 供 EnqueueNotify(下单入队)与死信手动重发(用订单最新 notify_url)复用,保证字段/签名单一来源。 +func (s *Service) BuildNotifyPayload(o *model.Order) string { + params := map[string]string{ + "trade_no": o.GatewayNo, + "out_trade_no": o.OutTradeNo, + "type": o.Channel, + "name": o.Subject, + "money": o.MoneyYuan(), + "trade_status": "TRADE_SUCCESS", + "notify_url": o.NotifyURL, + } + if o.Attach != "" { + params["attach"] = o.Attach + } + if o.ChannelTxID != "" { + params["trade_no"] = o.ChannelTxID + } + epay.BuildSignedParams(params, s.signKey(o.Pid)) + payload, _ := json.Marshal(params) + return string(payload) +} + +func (s *Service) EnqueueNotify(ctx context.Context, o *model.Order) { + payload := s.BuildNotifyPayload(o) + taskID, err := s.store.CreateNotifyTask(ctx, o.ID, payload) + // ...原有逻辑不变 +} +``` + +### 3.2 `internal/store/store.go` — 接口新增 + +```go +// UpdateNotifyTaskPayload 更新回调任务 payload(死信重发时按订单最新 notify_url 重建) +UpdateNotifyTaskPayload(ctx context.Context, id int64, payload string) error +// DeleteNotifyTask 删除回调任务(彻底移除死信,确认放弃该笔通知) +DeleteNotifyTask(ctx context.Context, id int64) error +``` + +### 3.3 `internal/store/sqlite_rest.go` — 实现 + +```go +func (s *SQLiteStore) UpdateNotifyTaskPayload(ctx context.Context, id int64, payload string) error { + _, err := s.db.ExecContext(ctx, + `UPDATE notify_tasks SET payload=?, updated_at=? WHERE id=?`, payload, nowStr(), id) + return err +} + +func (s *SQLiteStore) DeleteNotifyTask(ctx context.Context, id int64) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM notify_tasks WHERE id=?`, id) + return err +} +``` + +### 3.4 `internal/admin/reconciler.go` — 重发刷新地址 + 删除 + +```go +func (h *ReconcilerHandler) handleRetryDead(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + operator := c.GetString("username") + ctx := c.Request.Context() + + // 取任务 + tasks, err := h.store.ListNotifyTasks(ctx, "dead", 100) // 或新增 GetNotifyTask(id) + // (List 按 id DESC,需从列表定位 task;更干净:新增 GetNotifyTask(id)) + var task *store.NotifyTask + for _, t := range tasks { if t.ID == id { task = t; break } } + if task == nil { c.JSON(404, ...); return } + + // 取订单,用最新 NotifyURL 重建 payload + 重签 + o, err := h.store.GetOrderByID(ctx, task.OrderID) + var payload string + if err != nil || o == nil { + // 订单丢失:保留原 payload(无法重建),仅重置 pending + payload = task.Payload + } else { + payload = h.svc.BuildNotifyPayload(o) + } + + if err := h.store.UpdateNotifyTaskPayload(ctx, id, payload); err != nil { ... } + h.store.UpdateNotifyTask(ctx, id, "pending", 0, time.Now()) + logger.Warn("admin", "dead 通知重发(用最新 notify_url)", "task_id", id, "operator", operator) + c.JSON(http.StatusOK, gin.H{"data": "已按最新回调地址重新入队"}) +} + +func (h *ReconcilerHandler) handleDeleteDead(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + operator := c.GetString("username") + if err := h.store.DeleteNotifyTask(c.Request.Context(), id); err != nil { ... } + logger.Warn("admin", "dead 通知已删除", "task_id", id, "operator", operator) + c.JSON(http.StatusOK, gin.H{"data": "已删除"}) +} +``` + +> 更干净:store 新增 `GetNotifyTask(ctx, id)` 避免在列表中扫描定位。**设计决策:新增 `GetNotifyTask`**。 + +### 3.5 路由 + +```go +rg.POST("/reconciler/notify-dead/:id/retry", h.handleRetryDead) +rg.POST("/reconciler/notify-dead/:id/delete", h.handleDeleteDead) // 新增 +``` + +### 3.6 前端 `api.js` + `Reconciler.vue` + +```js +// api.js +deleteDeadNotify: (id) => request(`/reconciler/notify-dead/${id}/delete`, { method: 'POST' }), +``` + +`Reconciler.vue` 死信行操作区加"删除"按钮(ElMessageBox.confirm 二次确认), +重发按钮保持(后端已刷新地址)。 + +### 3.7 `NotifyTasks.vue` 占位手动重试 + +将 `manualRetry` 占位改为调用真实 retry API(当前选中任务 `current.id`): + +```js +async function manualRetry() { + try { await api.retryDeadNotify(current.value.id); ElMessage.success('已按最新回调地址重新入队'); } + catch (e) { ElMessage.error('重试失败: ' + e.message) } + finally { payloadDialog.value = false; load() } +} +``` + +> 依赖 api.js 暴露 `retryDeadNotify`(已有,api.js:148)。 + +--- + +## 4. 边界与风险 + +| 场景 | 处理 | 风险 | +|---|---|---| +| 订单已删除/丢失 | 无法重建 payload → 保留原 payload 仅重置 pending | 仍打旧地址;提示人工 | +| 订单 NotifyURL 为空 | 重建后 notify_url 空 → worker 标记 success 跳过 | 极端;需人工 | +| 删除已支付但未通知的死信 | 不再自动补发,人工可从异常池/重新入队 | 资金通知可能丢失——删除前二次确认 | +| 并发(worker 同时取到同任务重试) | 重置 pending 后 next_retry_at=now,worker 下一轮即发 | 重复投递一次,幂等可接受 | +| NotifyTasks 全量页与 Reconciler 死信页 | 两处都接入真实 API | 保持一致 | + +--- + +## 5. 回归测试 + +- `internal/store`:新增 `UpdateNotifyTaskPayload` / `DeleteNotifyTask` 的单测(落库、读回、删除)。 +- `internal/admin`:`handleRetryDead` 重建 payload 用最新 notify_url(伪造 task+order,断言 payload 内 notify_url 为最新值且签名有效)。 +- `internal/order`:`BuildNotifyPayload` 与 `EnqueueNotify` 产物一致(单一来源)。 diff --git a/docs/DESIGN-REUSE-UPDATE-AMOUNT.md b/docs/DESIGN-REUSE-UPDATE-AMOUNT.md new file mode 100644 index 0000000..af288e2 --- /dev/null +++ b/docs/DESIGN-REUSE-UPDATE-AMOUNT.md @@ -0,0 +1,86 @@ +# 订单金额不可变与买家订单替换设计 + +> 状态:**已修正**(原“复用订单覆盖金额”方案已废弃) +> 关联:docs/MIANQIAN.md、docs/RESILIENCE.md +> 修正原因:生产事故中,上游先创建 10 元订单,买家随后改为 1 元;网关把新请求错误地复用到旧订单号并覆盖金额,导致 1 元支付回调到了上游 10 元订单,新的 1 元上游订单则保持 pending。 + +## 1. 事故根因 + +易支付上游的 `out_trade_no` 与金额是不可变业务主键。旧实现的买家级串行保护在发现同一环境存在 WAIT_PAY 订单时,无论本次请求的订单号是否不同,都调用旧订单的 `reuseOrCreateError`: + +```text +上游 OLD=10 元 -> 网关创建 OLD WAIT_PAY +上游 NEW=1 元 -> buyer_key 命中 OLD,网关没有创建 NEW +网关把 OLD 改成 1 元并刷新 OLD 凭据 +用户支付 1 元 -> 网关回调 OLD+1 元 +上游按 OLD 的原始 10 元订单归属回调,NEW 订单无回调保持 pending +``` + +生产日志中可见 `pending=OLD,new=NEW`,随后凭据仍以 OLD 的订单号创建。 + +## 2. 不可变规则 + +### 2.1 同一订单号 + +同一 `out_trade_no` 一旦存在,金额不能变化: + +| 请求 | 处理 | +|---|---| +| 同号、同金额 | 幂等复用原订单 | +| 同号、不同金额 | 直接拒绝,提示使用新订单号 | +| 同号、已成功 | 返回已处理 | +| 同号、已关闭、同金额 | 按原有同号复活语义处理 | +| 同号、已关闭、不同金额 | 拒绝,不复活改价 | + +网关不再调用 `UpdateOrderAmount` 修改已存在订单。该方法保留在 store 接口中仅为兼容现有实现,当前订单流程不应调用它。 + +### 2.2 不同订单号 + +个人免签通道中,同一 `buyer_key/fp_key` 只能有一个有效 WAIT_PAY,但金额变化必须通过新订单号实现: + +1. 对旧 WAIT_PAY 执行条件更新 `WAIT_BUYER_PAY -> TRADE_CLOSED`。 +2. 仅首次 CAS 成功时写一条 `replaced` 事件,释放旧备注码并移出 WAIT_PAY 池。 +3. 继续正常创建请求中的新订单号、金额、凭据和 `CreatedAt`。 +4. 新订单进入 WAIT_PAY 池,动态 `CloseAt` 取设备窗口和 `SessionExpiresAt` 的较早值;前端倒计时与后端一致,但不得超过原支付会话硬截止时间。 + +旧订单号绝不能被新订单请求复用。商家通道不启用买家级个人免签锁,保持按订单号自身幂等。 + +### 2.3 支付会话生命周期 + +订单替换不能通过新的 `CreatedAt` 无限续命。个人订单首次创建时持久化 +`SessionExpiresAt`(默认使用当前订单关闭窗口),替换订单继承该值;WAIT_PAY 池和支付页倒计时 +都使用 `min(设备动态窗口截止时间, SessionExpiresAt)`。 + +当 `SessionExpiresAt` 已到期时,新的金额/订单号替换请求直接拒绝,要求重新发起充值。这样可以同时满足: + +- 支持买家在会话内改变充值金额; +- 同一时间同一环境只有一个 WAIT_PAY; +- 连续改变金额不能永久占用账户、监听和备注码资源。 + +## 3. 并发与资源 + +替换使用现有 `UpdateOrderStatus` 的条件更新作为 CAS: + +- CAS 成功:旧订单关闭,记录 `replaced`,释放备注码,移除 WAIT_PAY 池,然后创建新单。 +- CAS 失败且旧单已成功:结算胜出,不创建新单,避免重复收款。 +- CAS 失败且旧单已关闭:允许当前新订单继续正常创建,但不重复写替换事件或释放资源。 + +替换不是普通超时关单,不调用通用 `CloseOrder`,避免产生重复 generic `closed` 事件、日志和异常池记录。 + +旧二维码对应旧订单,订单关闭后按现有状态/过期逻辑失效;旧订单迟到支付不会转移到新订单,继续走关闭订单的人工核查保护。 + +## 4. 个人免签匹配安全 + +个人免签金额匹配使用 DB 中 `status=WAIT_BUYER_PAY`、精确 `amount_minor` 和收款时间窗口;`SettlePayment` 再次严格校验实付金额与订单金额。因此本次修复不改变匹配算法,修复点是下单时不能吞掉新订单号。 + +## 5. 回归测试 + +`internal/order/service_test.go` 覆盖: + +1. 旧 10 元 WAIT_PAY + 新订单号 1 元:旧单关闭,新订单号创建,金额/回调身份使用新单。 +2. 同订单号不同金额:拒绝,DB 金额、状态、凭据和事件不变。 +3. 同订单号同金额:幂等复用。 +4. 替换只产生一条 `replaced` 事件,备注码和 WAIT_PAY 池只释放一次。 +5. 并发替换/结算:CAS 保证只能由一方获胜,不重复结算、不重复创建新单。 + +旧版“同号改金额并刷新凭据”的测试已改为金额不可变测试,避免把事故逻辑重新锁定为正确行为。 diff --git a/docs/MIANQIAN.md b/docs/MIANQIAN.md new file mode 100644 index 0000000..d3e83e5 --- /dev/null +++ b/docs/MIANQIAN.md @@ -0,0 +1,39 @@ +# 设备自动化与个人通道:学习概览 + +> 本文档只保留架构和工程实践概览,不提供特定平台的收款操作、设备坐标、账号配置、连接参数或规避平台限制的方法。相关实现代码保留在仓库中,供获得授权的学习和测试使用。 + +## 目标 + +个人通道是一个可选的实验性适配器,用于研究如何把外部设备事件接入订单状态机。它复用统一的订单、幂等、异常池、账本和告警基础设施,不改变商家通道的核心流程。 + +## 工程边界 + +- 设备访问通过受控的设备管理层封装,业务代码不直接散落调用设备命令。 +- 设备事件必须经过来源认证、时间窗口、金额校验和幂等去重。 +- 设备离线、解析失败、状态冲突和无法确认的事件进入异常池,不自动入账。 +- 设备调度使用串行化访问、租约、健康状态和负载感知,避免多个任务同时操作同一设备。 +- 任何个人账户、支付平台或设备的接入,都必须获得账户所有者授权并遵守适用的平台规则。 + +## 代码阅读入口 + +| 主题 | 目录 | +| --- | --- | +| 设备连接与健康检查 | `internal/adbmgr/` | +| 设备操作编排 | `internal/ledgermon/` | +| 事件读取与去重 | `internal/notifywatcher/`、`internal/admin/capture/` | +| 个人通道抽象 | `internal/channel/personal/`、`internal/personal/` | +| 订单确认与异常处理 | `internal/order/`、`internal/reconciler/` | +| 审计账本 | `internal/ledger/`、`internal/store/` | + +## 保留的通用实践 + +- DeviceActor/租约式设备访问串行化; +- 设备离线与任务超时的安全降级; +- 事件指纹、窗口匹配和重复回调幂等; +- 设备负载驱动的任务调度; +- 账本哈希链、签名和异常事件留痕; +- 运行状态、健康指标和告警。 + +## 免责声明 + +本项目仅用于软件工程学习、授权测试和合法的商户支付集成研究。个人收款、设备自动化和第三方平台接入可能受到平台条款、隐私保护和支付监管要求限制。部署者必须自行确认授权和合规性,不得用于绕过安全控制、未经授权访问账户、欺诈、洗钱或其他违法活动。 diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..1487aa5 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,53 @@ +# Operations + +本文档覆盖通用部署、备份、健康检查和账务异常处理。设备自动化只适用于授权测试环境,具体平台操作不在公开运维手册中展开。 + +## 部署原则 + +- 使用独立 Linux 用户和 `/opt/paygateway` 等专用目录; +- `.env`、数据库、日志、备份和密钥只放在部署主机,不提交到仓库; +- 使用 systemd 或受控容器运行,限制文件系统和网络权限; +- 启动后先检查配置自检和 `/api/health`; +- 生产升级前备份数据库并保留可回滚版本。 + +## 备份与恢复 + +使用 `deploy/backup.sh` 通过 SQLite 在线备份方式复制业务库。备份目录必须设置访问权限,备份文件不能公开上传。恢复前停止服务、校验备份文件、保留当前数据快照,并在恢复后运行账本校验和健康检查。 + +## 健康检查 + +关注: + +- HTTP 健康状态; +- 数据库可读写状态; +- 回调失败率; +- 通知队列和异常池积压; +- 账本追加与校验结果; +- 后台任务崩溃恢复日志; +- 授权设备的连接状态和读取器错误。 + +设备离线或读取结果不明确时,不应猜测业务已成功。相关订单应保持可追溯状态并进入人工核查流程。 + +## 订单与账务异常 + +异常处理遵循以下顺序: + +1. 先保留原始订单和事件记录; +2. 检查签名、金额、时间窗口和状态转换; +3. 重复事件只执行一次; +4. 无法确认的情况进入异常池; +5. 人工处理必须留下操作者、原因和结果; +6. 对账完成后运行账本链校验。 + +不要直接修改数据库中的订单状态或账本记录来“修复”问题。应使用应用层提供的补单、关闭或异常解决流程。 + +## 日志与隐私 + +- 日志不应包含密钥、完整支付凭证、个人信息、API Key 或原始设备标识; +- 调试日志仅在授权环境短时启用; +- 对外分享事故记录时只使用脱敏和聚合数据; +- 日志和数据库备份按最小权限保存并设置生命周期。 + +## 合规边界 + +部署者必须自行确认商户资质、支付平台条款、隐私保护、数据留存和适用监管要求。设备自动化不得用于未经授权访问账户、绕过安全控制或其他违法活动。 diff --git a/docs/PLAN-DYNAMIC-CLOSE-ALLOC.md b/docs/PLAN-DYNAMIC-CLOSE-ALLOC.md new file mode 100644 index 0000000..8b2982b --- /dev/null +++ b/docs/PLAN-DYNAMIC-CLOSE-ALLOC.md @@ -0,0 +1,388 @@ +# 动态关单与均衡分配:设计与实现基线 + +> 本文档记录动态关单和资源均衡的设计基线,代码以实际实现为准。 +> 关联:docs/DESIGN-FAIR-SCHEDULER.md(公平调度)、docs/MIANQIAN.md(设备自动化概览)、docs/RESILIENCE.md(对账兜底) +> 目标:**不漏单(不可商量底线)+ 高/低负载自适应 + 多设备负载均衡 + 性能/稳定性兼顾** +> +> 本文档覆盖两个模块的开发设计: +> - 模块一:WAIT_PAY 订单动态关单时间智能调度(含漏单路径修复) +> - 模块二:免签账户订单自动均衡分配(设备 × 账户两层) +> - 前置改造:WAIT_PAY 池从「全局单池」升级为「设备维度子池」 + +--- + +## 1. 现状代码事实(设计依据) + +### 1.1 关单链路(两条路径,安全检查不一致 ⚠️) + +**路径 A:`order.Service.CloseExpiredOrders`**(internal/order/service.go:821,main.go:738 每 60s 触发) + +```go +for _, o := range expired { + if ch, ok := channel.Lookup(o.Channel); ok && ch.ConfirmAbility() != channel.ConfirmByQuery { + // 个人/监听通道:直接超时关单 —— 无离线检查、无最后复查 ⚠️ + if s.closeExpiredOrder(ctx, o, nil, "") { closed++ } + continue + } + // 商家通道:关单前必查单 ... +} +``` + +**路径 B:`reconciler.Scheduler.confirmPersonal`**(internal/reconciler/scheduler.go:303) + +```go +if s.personalCheck != nil { + offline, reason := s.personalCheck(ctx, o) // 有离线检查 ✅ + if offline { + s.svc.EnqueueException(...) // 进异常池,绝不直接关 + return + } +} +// 监听端在线 + 无推送 + 未超复查阈值 → 才关单 +``` + +**结论**:路径 A 对个人通道**直接关单**,绕过路径 B 的离线检查——**设备离线时用户已付款但未监听到,订单被硬关 = 钱收了单关了 = 漏单**。这是本设计首先要修的。 + +### 1.2 WAIT_PAY 池(internal/pool/waitpay_pool.go) + +- 结构:`buckets[accountID][outTradeNo] *Entry` + `byNote` + `byAmount`,单 RWMutex,`atomic.Pointer[Snapshot]` 零锁快照 +- `Entry.CloseAt = CreatedAt + closeWindow`(**固定 30min**,`NewWaitPayPool(closeWindowMinutes)` 传入) +- `Snapshot.Accounts[i] = {AccountID, Count, OldestAge, MinTTL}`(按账户聚合) +- `DeviceScheduler`(ledgermon/scheduler.go:170)每设备一个 goroutine,读**全局快照**再过滤本设备账户(`onDevice` 映射) +- 订单 → 账户 → 设备映射:订单只有 `AccountID`;设备绑定在 personal.db(`Device.AccountID` 主账户 + `DeviceAccountIDs` 附加账户) + +### 1.3 选号链路(internal/channel/personal/personal.go:244) + +```go +func (c *Channel) selectServableAccount(ctx) *account.Account { + servable := c.servableAccountIDs(ctx) // adb 在线设备的账户集(主+附加) + return c.pool.SelectFrom(c.channelName(), "default", servable) // 加权随机,无负载感知 +} +``` + +- `AccountPool.SelectFrom`(account/pool.go:268):`filterAvailable`(状态/日限额/冷却)→ priority 分组 → `weightedRandom`(Weight 加权) +- **没有任何负载维度**:不感知设备上有多少待确认订单,不感知账户池内积压 + +### 1.4 可用信号清单(无需新增即可用) + +| 信号 | 来源 | 用途 | +|---|---|---| +| 设备 adb 在线 | `adbmgr.Online` 实时探测 | 关单前检查、分配硬过滤 | +| 每账户待确认数 | `WaitPayPool.Count(accountID)` | 账户负载 | +| 每账户池快照 | `Snapshot.Accounts[i]` | 负载/最久等待/MinTTL | +| 活跃账户数 | 池快照中 Count>0 的账户数 | 设备一轮耗时估算 | +| 账户状态/失败数/日限额 | `AccountPool`(normal/cooling/frozen/disabled) | 账户健康 | +| 单次 poll 成本 | 实测:支付宝≈4.3s、微信≈2.9s(dump 2.5s/OCR 0.7s) | 设备能力估算 | +| 调度器参数 | `BaseQuota=3, CapPct=0.6, DwellMax=3, UrgentTTL=8s` | 一轮耗时估算 | + +--- + +## 2. 设计原则(不可商量) + +1. **不漏单**:设备离线/未监听时绝不关单;关单前必须「最后复查」;宁可不关转人工,绝不误关。 +2. **动态窗口有物理锚点**:下限 = 用户支付操作时间 + 一轮完整确认(5min);上限 = 用户体验红线(10min),超过转人工升级处理,不是无限挂起。 +3. **按设备负载伸缩,非全局统一**:窗口由订单所在设备的实时负载决定。 +4. **设备是物理瓶颈**:分配先看设备(最闲最健康),再看设备内账户(最闲最健康);只惩罚短板,不叠加长处。 +5. **超限显式处理**:设备超限 → 换设备;全部超限 → 拒绝下单 + 日志告警建议加设备。 +6. **已付款用户秒级到账**:窗口只影响「未支付订单」何时关,不影响确认链路(notify/ledger 照常秒级入账)。 + +--- + +## 3. 前置改造:WAIT_PAY 池 → 设备维度子池 + +### 3.1 数据结构 + +```go +// internal/pool/waitpay_pool.go —— 总池改造成设备维度管理 + +type WaitPayPool struct { + mu sync.RWMutex + devices map[string]*devicePool // deviceID → 设备子池(新增) + // 账户→设备 解析(main.go 注入;解析不到 → 归入 "" 兜底桶,Reconcile 修复) + deviceOf func(accountID int64) (deviceID string, ok bool) + snap atomic.Pointer[Snapshot] // 全局快照(notifywatcher 的 Total>0 判断用,保持) + closeWindow time.Duration // 仅作兜底(设备信息缺失时) + winCalc func(deviceLoad int) time.Duration // 动态窗口计算器(模块二注入;nil=固定窗口) +} + +type devicePool struct { + buckets map[int64]map[string]*Entry + byNote map[int64]map[string]string + byAmount map[int64]map[int64]map[string]struct{} + // 设备级聚合(每子池独立快照,调度器/分配器零锁读) + stat atomic.Pointer[DeviceStat] +} + +type DeviceStat struct { + DeviceID string + Total int // 该设备 WAIT_PAY 总数(设备负载) + Accounts []AccountStat // 该设备内按账户聚合(与现 AccountStat 同构) + ActiveAccts int // 活跃账户数 + EstRound time.Duration // 估算一轮耗时 = ActiveAccts × 配额 × pollCost + CloseWindow time.Duration // 当前动态窗口(模块二算好放这) + UpdatedAt time.Time +} +``` + +### 3.2 操作接口(保持对外语义不变,内部按设备分池) + +| 现有方法 | 改造后行为 | +|---|---| +| `Add(o)` / `Upsert(o)` | 解析 `deviceID = deviceOf(o.AccountID)` → 写入对应子池;CloseAt 用**该设备当前动态窗口**计算 | +| `Remove(outTradeNo)` | 遍历子池删除(防御性全表,语义同现在) | +| `Snapshot()` | 全局聚合(Total = Σ 设备 Total),**保持**(notifywatcher / 调度器兼容) | +| `Count(accountID)` | 不变(按账户计数) | +| `MatchByNote / MatchByAmount` | 不变(账户维度匹配,跨设备天然隔离) | +| `Rebuild / Reconcile / ReconcileLoop` | 遍历时按 deviceOf 分桶;Reconcile 兜底:解析不到设备的订单归入 `""` 桶并告警 | +| **新增** `DeviceSnapshot(deviceID) *DeviceStat` | 设备级快照(零锁),供 DeviceScheduler 与分配器使用 | + +### 3.3 关键改造点 + +1. **`orderToEntry`**:`CloseAt` 从固定 `closeWindow` 改为 `winCalc(deviceLoad)`——设备负载在 Add 时点计算。 +2. **`publishSnapshotLocked`**:先按设备分桶聚合,再汇总全局;同时发布每子池 `DeviceStat`。 +3. **`DeviceScheduler`**:`plan()` 改为读 `pool.DeviceSnapshot(s.deviceID)`(不再全局快照过滤),活跃账户判断与配额计算**只在本设备子池内进行** → 多设备天然隔离、互不干扰。 +4. **notifywatcher**:`Snapshot().Total > 0` 判断不变(全局快照保持兼容)。 +5. **注入 `deviceOf`**:main.go 构造(从 personal.db 设备 + `DeviceAccountIDs` 建 accountID→deviceID 映射;映射在设备增删时刷新)。 + +--- + +## 4. 模块一:动态关单时间算法 + +### 4.1 窗口公式(物理锚点) + +``` +设备负载 L = 该设备子池 WAIT_PAY 订单总数 +活跃账户数 A = 该设备子池内 Count>0 的账户数 +单次 poll 成本 P = 通道加权实测值(支付宝 4.3s / 微信 2.9s,默认取 4s 保守) +一轮耗时 T = A × BaseQuota(3) × P ≈ 12s × A +排队系数 = 1.5(最坏队尾位置) +支付操作预留 = 60s(用户扫码→支付操作时间下限) + +动态窗口 W = clamp( T × 1.5 + 60s, MIN=5min, MAX=10min ) +CloseAt = CreatedAt + W +``` + +**单调性(防误关)**:`CloseAt` 在订单生命周期内**只增不减**。负载升高 → 允许推后;负载降低 → 不提前(保守,避免已排队确认的订单被提前关掉)。实现:Add 时记录 `CloseAt`,`RefreshCloseAt`(Upsert 触发)仅在 `newCloseAt > oldCloseAt` 时更新。 + +### 4.2 窗口取值表 + +| 设备状态 | L / A | T | W | +|---|---|---|---| +| 空闲(1 账户 1 单) | A=1 | 12s | **5min(下限)** | +| 正常(2 账户各有单) | A=2 | 24s | **5min** | +| 高峰(4 账户多单) | A=4 | 48s | **5~6min** | +| 高负载(6 账户积压) | A=6 | 72s | **~7min** | +| 接近超限 | A≥8 | ≥96s | **8~10min** | +| 超限 | L ≥ C | — | **10min(上限)→ 换设备** | + +### 4.3 漏单路径修复(最高优先级,先行落地) + +**改造 `CloseExpiredOrders`(order/service.go:821)**: + +```go +// 个人/监听通道:不再直接关单,改为「复查后关」——与 confirmPersonal 同语义 +if ch.ConfirmAbility() != channel.ConfirmByQuery { + // ① 设备离线检查(注入 personalCheck,与 reconciler 共用) + if s.personalCheck != nil { + offline, reason := s.personalCheck(ctx, o) + if offline { + s.EnqueueException(ctx, o, ExReasonListenOffline, "LISTEN_OFFLINE", "", ..., + "监听端掉线,无法确认是否已付款,需人工核实: " + reason) + continue // 不关单 + } + } + // ② 关单前最后复查(新增):检查池子是否还有该订单 + 复查次数上限 + if !s.poolStillHolds(ctx, o.OutTradeNo) { continue } // 已入账/已移除,跳过 + // ③ 复查次数超限 → 进异常池(复用 confirmPersonal 的 QueryFailAlert 语义) + // ④ 确认未支付 → 关单(走 closeExpiredOrder,含池同步) +} +``` + +> 语义统一:个人通道关单**只有一条安全路径**(设备在线 + 无新记录 + 未超复查上限)。`CloseExpiredOrders` 是 DB 扫描兜底(防 confirmSched 堆丢失),`confirmPersonal` 是事件驱动主路径,两者最终都收敛到同一安全检查。 + +**新增 `personalCheck` 注入**:main.go 组装(`reconciler.AsPersonalCheck(personalDB, adbMgr())`),order.Service 与 reconciler.Scheduler 共用同一检查函数,避免两处逻辑分叉。 + +### 4.4 关单扫描改造 + +- 扫描器(main.go:738):`before` 从 `now - 固定30min` 改为 `now - MAX_WINDOW(10min) - 裕量(1min)`,候选集放宽到上限内所有 WAIT_PAY。 +- 命中候选后:在池子中查该订单的 `CloseAt`(动态),`now < CloseAt` → 跳过;`now ≥ CloseAt` → 走 §4.3 复查流程。 +- `ORDER_CLOSE_INTERVAL` 从 60s 收紧到 **30s**(窗口变小,扫描粒度需跟上)。 + +### 4.5 设备超限处理(配合模块三) + +``` +设备能力 C = (MAX_WINDOW - 支付操作预留) / (单次poll × 每账户最低配额1) × 排队修正 + = (600s - 60s) / (4s × 1) ≈ 135 单(理论值,可按实测 poll 成本校准) + +L ≥ C×80% → 分配器降权该设备(模块三) +L ≥ C → 分配器跳过该设备(换设备处理) +全部设备超限 → 下单返回「设备负载超限,暂无可服务设备」+ 日志 ERROR「设备负载超限,建议增加收款设备」 +``` + +--- + +## 5. 模块二:免签账户订单自动均衡分配 + +### 5.1 两层分配模型 + +``` +下单(个人通道 CreatePayment) + → 第一层:选设备(最闲 + 最健康) + → 第二层:该设备内选账户(最闲 + 最健康) + → 现有 filterAvailable 安全过滤始终保留(状态/日限额/冷却) +``` + +### 5.2 第一层:设备评分(物理瓶颈优先) + +```go +// internal/channel/personal/ 新增 deviceSelector(或 internal/pool 提供负载查询后在此实现) +type DeviceCandidate struct { + DeviceID string + Serial string + Load int // 设备子池 WAIT_PAY 总数(DeviceSnapshot().Total) + Health float64 // 最近 N 次 poll 成功率(埋点,见 §6) +} + +score_device = Health × 1 / (Load + 1) + +// 规则: +// - 设备 adb 离线 → 排除(现有 servableAccountIDs 已过滤) +// - Load ≥ C(超限)→ 排除(换设备) +// - 其余按 score_device 降序选最优;得分并列 → 按 Load 升序;再并列 → 确定性(DeviceID 字典序) +``` + +**设备健康度埋点**(新增,§6.2):`ledgermon.DeviceActor` 每次 PollCmd 执行记录成败,滑动窗口最近 20 次,`Health = 成功数/20`。设备无历史(<5 次)→ `Health = 0.9` 中性(不因无数据饿死新设备)。 + +### 5.3 第二层:账户评分(设备内) + +```go +score_account = Health × 1 / (Count + 1) + +// Health(账户健康,只罚短板): +// - 状态 normal=1(cooling/frozen/disabled 已由 filterAvailable 排除) +// - 日限额余量:1 - DailyUsed/DailyLimit(DailyLimit=0 视为 1) +// - 失败折扣:FailCount>=3 → ×0.2;FailCount>=1 → ×0.5(复用现 ReportResult 计数) +// Count = WaitPayPool.Count(accountID)(账户负载) +``` + +### 5.4 与现有代码的落点 + +改造 `selectServableAccount`(personal.go:244): + +```go +func (c *Channel) selectServableAccount(ctx) *account.Account { + // ① 现有:adb 在线设备 + 账户集(硬过滤,保留) + devices, acctIDs := c.servableDevicesAndAccounts(ctx) // 从 servableAccountIDs 拆出设备维度 + // ② 设备超限过滤(Load ≥ C → 剔除设备及其账户) + devices = filterOverloaded(devices) + if len(devices) == 0 { + logger.Error("personal", "设备负载超限,建议增加收款设备", ...) + return nil + } + // ③ 第一层:选设备(score_device 最高) + dev := pickDevice(devices) + // ④ 第二层:该设备内选账户(score_account 最高,替代 weightedRandom) + return c.pool.SelectFromWeighted(c.channelName(), "default", acctIDsOf(dev), + func(id int64) float64 { return accountScore(c.pool, id, c.loadOf, c.healthOf) }) +} +``` + +> `SelectFrom` 保留(管理后台/其他调用不受影响),新增 `SelectFromWeighted`(带评分函数参数,替换 weightedRandom 为 argmax 评分)。备注码分配器**不动**(全局唯一 + TTL 5min,均衡分配不改变码的唯一性;byNote 索引按 accountID 隔离,同码跨账户天然不冲突)。 + +### 5.5 振荡防护 + +- 评分分母 `Load+1` 天然带惯性:分配后目标负载上升 → 分数下降 → 下次转移,无需额外冷却。 +- 设备健康度滑动窗口(20 次)平滑抖动,不因单次 poll 失败跳变。 +- 多设备间负载在 `C×80%` 阈值内自然均衡,不会在两台设备间反复横跳(每单只分配一次,分配后负载立即反映)。 + +--- + +## 6. 新增埋点(轻量,均为内存滑动窗口,不落库) + +| 埋点 | 位置 | 实现 | +|---|---|---| +| 设备 poll 成功率 | `ledgermon.DeviceActor`(PollCmd 执行处) | 环形缓冲 20 次,`RecordPoll(ok bool)`;`PollHealth() float64` | +| 设备一轮实际耗时 | `DeviceScheduler` 窗口结束 | `RecordRound(dur)`;滑动平均(用于校准 pollCost 而非写死 4s) | +| 通知栏快路径命中 | `notifywatcher.Handle` 成功匹配时 | 每设备计数器(10min 窗口),供设备健康加成(可选,v0.1 不接) | +| 设备超限事件 | 分配器拒绝时 | 日志 ERROR + 可选 Bark/Webhook(复用 alert.Client) | + +--- + +## 7. 实施步骤(顺序即依赖) + +| 步骤 | 内容 | 涉及文件 | 验收 | +|---|---|---|---| +| **S1 漏单路径修复** | 个人通道关单统一安全检查(personalCheck 注入 + 关单前复查) | order/service.go、reconciler/、main.go | 设备离线时订单绝不关单(单测 + 模拟) | +| **S2 池子设备维度改造** | deviceOf 注入、devicePool 分池、DeviceSnapshot、动态窗口计算器挂载 | pool/waitpay_pool.go、ledgermon/scheduler.go、main.go | 多设备各自独立快照;调度器只读本设备池 | +| **S3 动态关单** | winCalc 实现(公式 §4.1)、CloseAt 单调更新、扫描器改造(30s/10min 窗口) | pool/、order/service.go、main.go | 窗口随负载在 5~10min 内变化;回归测试 | +| **S4 均衡分配** | 设备评分 + 账户评分 + SelectFromWeighted、设备超限过滤 | personal.go、account/pool.go、新增 deviceSelector | 多设备订单分布均衡;超限设备不再接单 | +| **S5 埋点** | 设备 poll 成功率 / 一轮耗时滑动统计 | ledgermon/ | 健康度随真实 poll 结果变化 | +| **S6 全量验证** | 编译 + vet + 单测 + 资金链路测试 + 真机验证 | — | 见 §8 | + +--- + +## 8. 验收标准 + +### 模块一(动态关单) +1. [ ] 设备离线时,个人通道 WAIT_PAY 订单**不关闭**(转异常池人工核实),无漏单 +2. [ ] 高峰(4+ 活跃账户)订单窗口自动放宽至 ≥6min;空闲(1 账户)收敛至 5min 下限 +3. [ ] 窗口恒定在 [5min, 10min] 内,不无限低/无限高 +4. [ ] 设备负载 ≥ C 时新订单不再路由该设备;全部超限时下单拒绝 + 日志告警 +5. [ ] 已付款订单在任何负载下都秒级入账(窗口不影响确认链路) +6. [ ] 回归:`go build ./... && go vet ./... && go test ./... -count=1` 全绿 + +### 模块二(均衡分配) +1. [ ] 多设备下订单按设备负载均衡分布,不出现单设备积压 +2. [ ] 某设备超限/离线 → 流量自动转移至健康设备 +3. [ ] 设备内多账户按账户负载 + 日限额余量分配,限额快满账户自动降权 +4. [ ] 备注码全局唯一性在均衡分配下不退化(byNote 账户隔离验证) +5. [ ] 分配失败(无可用设备/账户)返回明确错误,不留死单 + +--- + +## 9. 风险与兜底 + +| 风险 | 等级 | 兜底 | +|---|---|---| +| 动态窗口计算器 bug 导致窗口过短 | 高 | 下限 clamp 5min 硬保护 + 单调只增 + 关单前复查三重防线 | +| 设备→账户映射漂移(订单在池中但设备已删) | 中 | `""` 兜底桶 + Reconcile 修复 + 告警 | +| poll 成本估算偏差(真机实测 vs 4s 默认) | 中 | §6 一轮实际耗时埋点校准,env 可配 `DEVICE_POLL_COST_MS` | +| 均衡分配振荡 | 低 | Load+1 分母天然惯性 + 健康度滑动窗口 | +| 全设备超限误拒单 | 低 | 超限阈值 C×80% 降权先于 C 硬拒绝,有缓冲带 | +| 关单扫描与 confirmSched 双路径竞态 | 中 | 两者收敛同一安全检查 + UpdateOrderStatus 条件更新(WHERE status=WAIT_PAY)幂等 | + +--- + +## 10. 配置项(.env 新增) + +| 变量 | 默认 | 说明 | +|---|---|---| +| `ORDER_CLOSE_MIN` | `5` | 动态关单窗口下限(分钟) | +| `ORDER_CLOSE_MAX` | `10` | 动态关单窗口上限(分钟) | +| `ORDER_CLOSE_INTERVAL` | `30` | 关单扫描间隔(秒,从 60 收紧) | +| `DEVICE_POLL_COST_MS` | `4000` | 单次 poll 成本估算(ms,埋点校准后可不配) | +| `DEVICE_LOAD_CAP` | `0` | 设备订单承载上限(0=按公式自动算) | +| `DEVICE_OVERLOAD_RATIO` | `0.8` | 设备降权阈值(占 C 的比例) | +| `DEVICE_HEALTH_WINDOW` | `20` | 设备健康度滑动窗口(次) | + +--- + +## 11. 生产上线前代码审查修复记录(2026-08-31) + +Oracle 只读审查发现的阻断级与中等问题,均已修复并验证: + +| # | 级别 | 问题 | 修复 | +|---|------|------|------| +| P0 | 🔴 崩溃 | `waitpay_pool.go` Add 在锁外调 `orderToEntry`→`windowFor` 遍历 `dp.buckets`,与并发写操作形成 map 竞态(可致 fatal crash);`-race` 未捕获因并发测试未设 winCalc | `Add` 内 `orderToEntry` 移入写锁;并发测试注入 winCalc+deviceOf 以覆盖该路径 | +| P1 | 🔴 功能 | 扫描器候选集 `max(OrderCloseMinutes, dynMax+1min)` 使动态窗口完全失效:默认 30min>11min 时个人订单 5-10min 到期不进候选集 | 候选集改 `min(OrderCloseMinutes, dynMin)`,个人订单 age≥dynMin 起进入,由池内 `CloseAtOf` 精确过滤 | +| P2-1 | 🟡 资金 | `CloseExpiredOrders` 个人分支在线直接关单,绕过 `confirmPersonal` 的 LISTEN_TIMEOUT 复查保护(监听端僵死时误关已付款订单) | 在线且已有 LISTEN_TIMEOUT 异常记录 → 进异常池人工核实,不直接关单 | +| P2-2 | 🟡 资金 | `Add` 覆盖未删旧设备桶:设备映射变更后同订单残留双设备子池,负载虚高 | `unindexLocked(old)` 后显式 delete 旧桶 + 清理空桶/`acctDev` | +| P2-3 | 🟡 一致 | `balanceSelectAccount` 直读 `cfg.OrderCloseMax` 绕过配置退化逻辑(静态窗口下超限过滤仍生效) | `deviceLoadCapOf` 内重复校验 MIN/MAX 合法性,非法 → 返回 0(不启用过滤) | +| P2-4 | 🟡 性能 | 扫描器对每个到期订单逐次 `adb Online()` 探测,无设备级缓存 | `CloseExpiredOrders` 内扫描级设备在线缓存(仅缓存已绑定设备,单次扫描内复用) | + +**验证**:`go build`/`go vet`/14 包测试全绿;`go test -race ./internal/pool/ ./internal/order/ ./internal/ledgermon/` 全绿;新增/强化单测:`TestWaitPayPool_ConcurrentStorm`(winCalc 路径)、`TestCloseExpiredOrders_PersonalOnline_StalledListener`、`TestCloseExpiredOrders_Merchant_RespectsFixedWindow`/`OverFixedWindow`。 + +**记录为已知限制(非阻断,后续加固)**: +- ~~全代码库 0 处 `recover()`~~ **[已修复 2026-08-31]**:8 个后台 goroutine 已加 `defer recover`(关单扫描器/收款码清理+续期/账本日任务/Cloudflare Tunnel/设备加锁/视频转发/告警发送),main.go 统一 `bgRecover` helper 记录 panic+栈;同时修复 notifywatcher 测试闭包变量的 DATA RACE(`orderDriven` 改 atomic.Bool) +- `SetWindowCalc` 原地修改已发布 Entry 的 CloseAt:仅启动时调用一次,无并发读者,实际无害 +- 动态窗口公式 `clamp(active×12s×1.5+60s, 5min, 10min)` 依赖 `DEVICE_POLL_COST_MS=4000` 估算,真机实测可校准 diff --git a/docs/RESILIENCE.md b/docs/RESILIENCE.md new file mode 100755 index 0000000..d4c845a --- /dev/null +++ b/docs/RESILIENCE.md @@ -0,0 +1,173 @@ +# 支付网关容灾与对账体系设计 v3(主动查账 · 事件驱动 · 三级兜底 · 人工异常池) + +> 版本:v3.0(2026-08-09) +> 设计约束: +> - 临时隧道是防攻击特性,不引入固定域名;回调丢失是常态,主动查账是核心保障。 +> - 兜底分**三级**:① 自动查单补账(ConfirmScheduler)→ ② 退避重试(查单/通知)→ ③ **人工异常处理池**(自动化兜不住时的最终兜底)。 +> - 所有资金写入(含人工处理)只走幂等入口(SettlePayment / CloseOrder),不绕过状态机、不手改账本。 +> - 高性能:事件驱动 + timer heap,不轮询;不出错:幂等 + 关单前查单 + 冲突进差错。 + +--- + +## 一、设计哲学 + +1. **回调是优化,查单是保障** —— 快路径(回调入账)与慢路径(查单补账)殊途同归,都进幂等 `SettlePayment`。 +2. **数据经过网关,就有信息返回** —— 每个环节可自我验证,异常即触发兜底。 +3. **三级兜底**:自动补偿 → 退避重试 → **人工异常池**。自动化兜不住才进人工,人工操作也走同一幂等入口。 + +--- + +## 二、每环节"正常路径 vs 兜底" + +| 环节 | 正常 | 异常→兜底触发 | +|------|------|----------------| +| ① 上游→网关(下单) | 验签→建单→返回凭据 | 验签/参数失败→拒绝并记录(无资金影响) | +| ② 用户→支付宝(支付) | 回调→入账(快路径) | **回调丢失→ConfirmScheduler 到期→查单补账(慢路径)** | +| ③ 网关→上游(通知) | 通知 200→标记 | 失败→outbox 重试 31h→dead→人工重发 | +| ④ 日终对账 | 一致 | 不一致→差错单→人工 | +| ⑤ **任何环节异常不明** | — | **进人工异常池**(兜底链终点) | + +--- + +## 三、核心模块:ConfirmScheduler(订单确认调度器) + +内存 min-heap(按 nextCheckAt 排序),事件驱动,**非轮询**。 + +``` +OnOrderCreated → push(now + T1 60s) +OnCallbackReceived → remove + SettlePayment(幂等) +到期 Confirm(tradeNo): + ├─ 已 SUCCESS → remove + ├─ 仍 WAIT 未到关单时间 → QueryOrder 查单 + │ ├─ TRADE_SUCCESS → SettlePayment 补账 → 通知 → remove + │ ├─ 未支付 → push(+30s~5m 自适应) + │ └─ 查单失败 → push(+退避),连续失败 N 次 → 进异常池(query_failed) + └─ 到关单时间 → 最后查一次:已支付补账;未支付 CloseOrder;查单失败 → 推迟关单 + 进异常池 +``` + +冲突(查单金额≠订单金额、状态矛盾)→ **不自动入账** → 进异常池(amount_mismatch / state_conflict)。 + +--- + +## 四、人工异常处理池(Manual Exception Pool)★新增核心 + +### 4.1 定位 +**自动化兜底失败后的最终兜底**:任何"不按正常流程"的订单(回调全丢且查单不明、金额矛盾、关单存疑、通知 dead 无法重发)自动进入本池。人工核对证据 → 按流程填写必要信息 → 提交后经幂等入口恢复为正常订单,**订单永久标记"人工处理"**,全程审计。 + +### 4.2 数据模型(新表 + orders 标记) + +```sql +-- 异常订单池(只增不改,resolved 后可 reopen,不可删除) +CREATE TABLE exception_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + out_trade_no TEXT NOT NULL UNIQUE, -- 关联订单 + pid TEXT, upstream_name TEXT, channel TEXT, + amount_minor INTEGER, -- 网关记录金额(分) + order_status TEXT, -- 当前订单状态 + channel_status TEXT, -- 最近查单结果(TRADE_SUCCESS/CLOSED/QUERY_FAILED/UNKNOWN) + channel_tx_id TEXT, -- 查单拿到的通道交易号 + paid_amount INTEGER, -- 人工确认的实际支付金额(默认=amount_minor) + payer TEXT, -- 人工确认的付款方 + reason TEXT, -- query_failed / amount_mismatch / state_conflict / close_uncertain / notify_dead / manual + phase TEXT DEFAULT 'pending', -- pending / processing / resolved + resolved_action TEXT, -- settle / close / noop / refund + evidence TEXT, -- 证据时间线 JSON(自动收集,不可手改) + suggestion TEXT, -- 系统建议动作(依据最近查单结果) + handler TEXT, handler_note TEXT, + resolved_at TEXT, updated_at TEXT, + created_at TEXT +); + +-- orders 表新增人工处理标记(列表/详情可见,审计可追溯) +ALTER TABLE orders ADD COLUMN exception_id INTEGER DEFAULT 0; -- 关联异常池 +ALTER TABLE orders ADD COLUMN handled_by TEXT DEFAULT ''; -- 处理人 +ALTER TABLE orders ADD COLUMN handled_action TEXT DEFAULT ''; -- manual_settle / manual_close / noop +ALTER TABLE orders ADD COLUMN handled_at TEXT DEFAULT ''; +ALTER TABLE orders ADD COLUMN handled_note TEXT DEFAULT ''; + +-- 操作审计(谁在何时对哪个异常单做了什么) +CREATE TABLE exception_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + exception_id INTEGER, action TEXT, operator TEXT, detail TEXT, created_at TEXT +); +``` + +### 4.3 自动进池触发点(ConfirmScheduler / notify 集成) + +| 触发 | reason | 进入时自动收集的证据 | +|------|--------|----------------------| +| 查单连续失败 ≥ `query_fail_alert`(5 次) | query_failed | 下单/回调/通知记录 + 每次查单结果时间线 | +| 查单金额 ≠ 订单金额 | amount_mismatch | 查单快照(金额、交易号、时间) | +| 回调与查单状态矛盾 | state_conflict | 两方原始数据 | +| 关单时查单失败(存疑) | close_uncertain | 查单错误信息 | +| 通知 dead 且重发仍失败 | notify_dead | 通知任务重试历史 | +| 管理后台手动加入 | manual | 人工填写的说明 | + +进池时同时**暂停 ConfirmScheduler 对该订单的自动动作**(避免人工处理与自动查单打架),订单转入"待人工"。 + +### 4.4 人工处理流程(管理后台「异常订单」页) + +``` +① 列表(筛选 phase/reason/上游/日期)→ 打开详情 +② 详情:基本信息 + 证据时间线(自动收集,只读)+ 系统建议动作 + (可点「再次查单」刷新证据:调用通道查单,把最新结果并入时间线) +③ 填写处理表单: + 动作:settle(确认入账)/ close(确认未支付,关单)/ noop(无需处理) + 实际支付金额(默认=订单金额,可改) + 通道交易号(可从支付宝账单人工核对后填入) + 付款方(可选) + 备注 +④ 提交 → 后端校验(金额>0、订单未处理过、动作合法、表单与证据不冲突) + → 执行: + settle → Service.SettlePayment(幂等,参数含人工金额/交易号/付款方/来源=manual) + → 成功自动创建通知任务 → 通知上游到账 + close → Service.CloseOrder(幂等,单笔) + noop → 仅记录 + → 成功:orders 写 handled_* 标记 + exception_orders 置 resolved + exception_logs 记审计 +⑤ 该订单在列表/详情永久显示「⚠️ 人工处理」徽标(含处理人/时间/备注/动作) +``` + +### 4.5 防呆与安全 + +| 项 | 设计 | +|----|------| +| 并发防重 | 处理时 `phase=pending→processing` 行锁,同一订单不可并发处理;resolved 后不可再处理(可管理员 reopen) | +| 幂等入口 | settle/close 一律走 Service 幂等方法,**绝不直接 UPDATE 订单状态/金额** | +| 金额校验 | 人工金额必须 >0 且为整数分;与查单金额不一致时强制二次确认(输入框要求重复输入) | +| 证据只读 | evidence 自动收集,人工不可编辑(防掩盖);人工信息只写在 paid_amount/payer/handler_note | +| 权限 | 异常池操作走管理后台认证;敏感动作(settle)可加二次确认口令(可配置) | +| 审计 | exception_logs 全程留痕:谁、何时、看了什么、填了什么、做了什么 | +| 账本 | settle 走 SettlePayment 自动过账本 hash 链;人工无法绕账本 | + +### 4.6 与上游通知不到的联动 +人工 settle 成功后自动创建通知任务 → 若上游仍不可达 → outbox 重试 31h → dead → 此时订单已 SUCCESS,**dead 通知单独成清单**("上游未确认"),可在异常池关联查看/重发;上游也可主动调 `/api.php` 查单(网关数据正确,拉取是上游的事)。 + +--- + +## 五、分阶段落地 + +| 阶段 | 内容 | 说明 | +|------|------|------| +| **P0-1** | 通知重试增强(31h 窗口)+ dead 管理 API | 修复"付钱未到账" | +| **P0-2** | ConfirmScheduler 最小版(timer heap + 查单补账 + 关单前查单) | 回调丢失自动兜底 | +| **P0-3** | systemd 守护(gateway+new-api)+ 每日 sqlite 备份 | 进程/数据安全 | +| **P1-1** | **人工异常处理池**(表 + 进池触发 + 处理流程 + 管理后台页) | 最终兜底 + 人工台 | +| **P1-2** | 启动配置自检(沙箱/正式、密钥、回调地址) | is_prod 错配早暴露 | +| **P1-3** | 告警(Bark/Webhook:查单连续失败、验签失败率、dead、账本失败、**进池事件**) | 可观测 | +| **P2-1** | 日终对账报告(网关 vs 通道 vs 上游) | 资金最终核对 | + +> **存量订单 116**:P0-2 落地后由 ConfirmScheduler 自然发现;若查单亦失败(极端),P1-1 人工池接管——人工从支付宝账单核对后 settle 恢复,标记人工处理。 + +--- + +## 六、模块与现有代码对应 + +| 新组件 | 复用 | 新增 | +|--------|------|------| +| `reconciler.ConfirmScheduler` | `Channel.QueryOrder`、`Service.SettlePayment`、`CloseExpiredOrders` | timer heap、事件接口 | +| `reconciler.ExceptionPool` | admin 框架、审计日志模式、SettlePayment/CloseOrder | exception 表、进池触发、处理流程 API | +| notify 增强 | `notify.Worker`、`notify_tasks` | 重试参数、dead 管理 API | +| admin 页面 | admin 框架 | 异常订单池 Vue 页(证据时间线 + 处理表单) | +| systemd + 备份 | gateway_autostart.sh | service 单元、备份脚本 | + +**核心承诺**:所有资金写入(自动或人工)只走 `SettlePayment` / `CloseOrder` 等幂等方法;人工处理同样过账本 hash 链、同样触发通知、同样留审计;订单永久可追溯"是否人工处理过、谁处理的、依据什么"。 diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..a30d89e --- /dev/null +++ b/go.mod @@ -0,0 +1,55 @@ +module paygateway + +go 1.25.0 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/go-pay/gopay v1.5.119 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/gorilla/websocket v1.5.3 + github.com/makiuchi-d/gozxing v0.1.1 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + golang.org/x/crypto v0.48.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + modernc.org/sqlite v1.56.0 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-pay/crypto v0.0.1 // indirect + github.com/go-pay/util v0.0.4 // indirect + github.com/go-pay/xlog v0.0.3 // indirect + github.com/go-pay/xtime v0.0.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.74.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..038fd10 --- /dev/null +++ b/go.sum @@ -0,0 +1,154 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY= +github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes= +github.com/go-pay/gopay v1.5.119 h1:E+Uc3HW/A9guCmbGW8CkGUwhjfEw/xKn+azW67gPH6A= +github.com/go-pay/gopay v1.5.119/go.mod h1:2R6Ggc9FqYnbC37yoOJZH4eZMFpykPDVOobnZNP+R2A= +github.com/go-pay/util v0.0.4 h1:TuwSU9o3Qd7m9v1PbzFuIA/8uO9FJnA6P7neG/NwPyk= +github.com/go-pay/util v0.0.4/go.mod h1:Tsdhs8Ib9J9b4+NKNO1PHh5hWHhlg98PthsX0ckq6PM= +github.com/go-pay/xlog v0.0.3 h1:avyMhCL/JgBHreoGx/am/kHxfs1udDOAeVqbmzP/Yes= +github.com/go-pay/xlog v0.0.3/go.mod h1:mH47xbobrdsSHWsmFtSF5agWbMHFP+tK0ZbVCk5OAEw= +github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM= +github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= +modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= +modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/account/crypto.go b/internal/account/crypto.go new file mode 100755 index 0000000..10e134f --- /dev/null +++ b/internal/account/crypto.go @@ -0,0 +1,112 @@ +// Package account 账户池:多支付账户管理 + 加密存储 + 调度器 +package account + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" +) + +// MasterKeySize AES-256 密钥长度(32 字节) +const MasterKeySize = 32 + +// GenerateMasterKey 生成新的主密钥(hex 编码,64 字符) +func GenerateMasterKey() (string, error) { + key := make([]byte, MasterKeySize) + if _, err := rand.Read(key); err != nil { + return "", fmt.Errorf("生成主密钥失败: %w", err) + } + return hex.EncodeToString(key), nil +} + +// masterKey 运行时主密钥(字节形式) +var masterKey []byte + +// InitCrypto 初始化加密模块(传入 hex 编码的主密钥) +func InitCrypto(hexKey string) error { + key, err := hex.DecodeString(hexKey) + if err != nil { + return fmt.Errorf("主密钥格式错误: %w", err) + } + if len(key) != MasterKeySize { + return fmt.Errorf("主密钥长度错误: 需要 %d 字节,实际 %d", MasterKeySize, len(key)) + } + masterKey = key + return nil +} + +// Encrypt AES-256-GCM 加密,返回 hex 编码密文 +func Encrypt(plaintext []byte) (string, error) { + if masterKey == nil { + return "", errors.New("加密模块未初始化") + } + block, err := aes.NewCipher(masterKey) + if err != nil { + return "", fmt.Errorf("创建加密器失败: %w", err) + } + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建 GCM 失败: %w", err) + } + nonce := make([]byte, aesGCM.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("生成 nonce 失败: %w", err) + } + ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) + return hex.EncodeToString(ciphertext), nil +} + +// Decrypt 解密 hex 编码的 AES-256-GCM 密文 +func Decrypt(encryptedHex string) ([]byte, error) { + if masterKey == nil { + return nil, errors.New("加密模块未初始化") + } + ciphertext, err := hex.DecodeString(encryptedHex) + if err != nil { + return nil, fmt.Errorf("密文格式错误: %w", err) + } + block, err := aes.NewCipher(masterKey) + if err != nil { + return nil, fmt.Errorf("创建解密器失败: %w", err) + } + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("创建 GCM 失败: %w", err) + } + nonceSize := aesGCM.NonceSize() + if len(ciphertext) < nonceSize { + return nil, errors.New("密文太短") + } + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("解密失败(密钥错误或数据损坏): %w", err) + } + return plaintext, nil +} + +// EncryptJSON 将任意结构体加密为 hex 密文(JSON 序列化后加密) +func EncryptJSON(v any) (string, error) { + data, err := json.Marshal(v) + if err != nil { + return "", fmt.Errorf("JSON 序列化失败: %w", err) + } + return Encrypt(data) +} + +// DecryptJSON 解密 hex 密文并反序列化到目标结构体 +func DecryptJSON(encryptedHex string, v any) error { + data, err := Decrypt(encryptedHex) + if err != nil { + return err + } + if err := json.Unmarshal(data, v); err != nil { + return fmt.Errorf("JSON 反序列化失败: %w", err) + } + return nil +} diff --git a/internal/account/model.go b/internal/account/model.go new file mode 100755 index 0000000..6ba8b44 --- /dev/null +++ b/internal/account/model.go @@ -0,0 +1,100 @@ +// Package account 账户池:多支付账户管理 + 加密存储 + 调度器 +// +// 安全设计: +// - accounts.db 仅管理后台 API 可读写,其他模块不得直接访问 +// - 敏感字段(private_key / public_key / api_key)AES-256-GCM 加密存储 +// - AccountPool 是内存中的唯一数据源,其他模块通过它获取解密后的账户 +package account + +import "time" + +// AccountStatus 账户状态 +type AccountStatus string + +const ( + StatusNormal AccountStatus = "normal" // 正常 + StatusCooling AccountStatus = "cooling" // 风控冷却中(自动恢复) + StatusFrozen AccountStatus = "frozen" // 冻结(按 FrozenReason 区分来源) + StatusDisabled AccountStatus = "disabled" // 永久禁用 +) + +// 账户冻结原因(StatusFrozen 不区分来源,用 FrozenReason 精确控制自动解冻边界)。 +// 只有 FrozenReasonOffline 允许设备恢复时自动解冻;其余一律仅人工解冻。 +const ( + FrozenReasonOffline = "offline" // 设备离线联动:设备 adb 恢复可达时自动解冻 + FrozenReasonRisk = "risk" // 风控上报冻结:仅人工解冻 + FrozenReasonManual = "manual" // 管理员手动冻结:仅人工解冻 + FrozenReasonRetire = "retire" // 删除设备级联停用:仅人工解冻 +) + +// Account 支付账户(内存中的完整表示,含解密后的密钥) +// accounts.db 中存储的是加密版本,Admin API 解密后推送到 AccountPool +type Account struct { + ID int64 `json:"id"` + Channel string `json:"channel"` // alipay / wechat / qq / paypal / personal.alipay / personal.wechat + Name string `json:"name"` // "支付宝-主账户" + AppID string `json:"app_id"` // 明文,用于回调反查 + MchID string `json:"mch_id,omitempty"` // 微信商户号 + + // 以下字段在 accounts.db 中加密存储,AccountPool 中为明文 + PrivateKey string `json:"-"` // 应用私钥(仅内存) + PublicKey string `json:"-"` // 支付宝公钥/微信API证书(仅内存) + ApiKey string `json:"-"` // API 密钥(仅内存) + QRContent string `json:"-"` // 收款码内容(个人免签通道用,仅内存) + + // 调度配置 + Weight int `json:"weight"` // 轮询权重(越大越容易被选中) + Priority int `json:"priority"` // 优先级(越大越优先) + Status AccountStatus `json:"status"` // normal / cooling / frozen / disabled + FrozenReason string `json:"frozen_reason,omitempty"` // 冻结原因(offline / risk / manual / retire;仅 frozen 时有效) + AutoBan bool `json:"auto_ban"` // 失败自动禁用 + DailyLimit int64 `json:"daily_limit"` // 日限额(分),0=不限 + DailyUsed int64 `json:"daily_used"` // 今日已用(分) + FailCount int `json:"fail_count"` // 连续失败次数 + FrozenUntil *time.Time `json:"frozen_until,omitempty"` // 冷却到期时间 + Group string `json:"group"` // 业务分组 + IsProd bool `json:"is_prod"` // 生产环境 + Remark string `json:"remark,omitempty"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AccountEncrypted accounts.db 中存储的加密版本 +// 敏感字段以 hex 密文存储,非敏感字段明文 +type AccountEncrypted struct { + ID int64 `json:"id"` + Channel string `json:"channel"` + Name string `json:"name"` + AppID string `json:"app_id"` + MchID string `json:"mch_id,omitempty"` + + // AES-256-GCM 加密后的 hex 密文 + PrivateKeyEnc string `json:"private_key_enc"` + PublicKeyEnc string `json:"public_key_enc,omitempty"` + ApiKeyEnc string `json:"api_key_enc,omitempty"` + QRContentEnc string `json:"qr_content_enc,omitempty"` // 收款码内容(个人免签通道) + + Weight int `json:"weight"` + Priority int `json:"priority"` + Status string `json:"status"` + FrozenReason string `json:"frozen_reason,omitempty"` // 冻结原因(offline / risk / manual / retire) + AutoBan bool `json:"auto_ban"` + DailyLimit int64 `json:"daily_limit"` + DailyUsed int64 `json:"daily_used"` // 今日已用(分),持久化(每日 00:00 重置) + Group string `json:"group"` + IsProd bool `json:"is_prod"` + Remark string `json:"remark,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AccountLog 账户操作审计日志 +type AccountLog struct { + ID int64 `json:"id"` + AccountID int64 `json:"account_id"` + Action string `json:"action"` // create / update / delete / freeze / unfreeze / enable / disable + Detail string `json:"detail"` // 操作详情 + Operator string `json:"operator"` // 操作人 + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/account/pool.go b/internal/account/pool.go new file mode 100755 index 0000000..4ef1c27 --- /dev/null +++ b/internal/account/pool.go @@ -0,0 +1,492 @@ +// Package account 账户池:内存缓存 + 调度器 +// +// AccountPool 是各模块获取账户数据的唯一入口: +// - alipay.Channel 持有 pool,下单选账户 +// - order.Service 持有 pool,回调反查 app_id +// - Admin API 持有 pool,增删改查后同步到 pool +// +// 设计约束: +// - 不直接访问 accounts.db(仅 Admin API 有权限) +// - 所有账户数据在内存中解密,不落盘 +package account + +import ( + "math/rand" + "sync" + "time" +) + +// AccountPool 内存账户池(线程安全) +// 负责:账户选择、日限额检查、风控冷却 +type AccountPool struct { + mu sync.RWMutex + accounts map[int64]*Account // id → Account + byAppID map[string]*Account // app_id → Account(支付宝回调反查) + byMchID map[string]*Account // mch_id → Account(微信回调反查) + // offlineFrozen 记录因「设备离线联动」冻结的账户(仅内存,重启后由 Add 依据 FrozenReason 重建)。 + // 自动解冻只能针对此集合——风控冻结/手动冻结的账户绝不允许被设备恢复自动解冻。 + offlineFrozen map[int64]bool +} + +// NewPool 创建空账户池 +func NewPool() *AccountPool { + return &AccountPool{ + accounts: make(map[int64]*Account), + byAppID: make(map[string]*Account), + byMchID: make(map[string]*Account), + offlineFrozen: make(map[int64]bool), + } +} + +// FreezeByOffline 离线联动冻结账户(设备 adb 不可达时调用)。 +// 记录 FrozenReason=offline + offlineFrozen 标记,设备恢复时允许自动解冻。 +func (p *AccountPool) FreezeByOffline(id int64) { + p.freezeWithReason(id, FrozenReasonOffline) +} + +// FreezeByRisk 风控上报冻结账户(仅人工解冻,绝不允许自动解冻) +func (p *AccountPool) FreezeByRisk(id int64) { + p.freezeWithReason(id, FrozenReasonRisk) +} + +// FreezeManual 手动冻结账户(管理员操作,仅人工解冻) +func (p *AccountPool) FreezeManual(id int64) { + p.freezeWithReason(id, FrozenReasonManual) +} + +// FreezeByRetire 删除设备级联停用(僵尸自动账户,仅人工恢复) +func (p *AccountPool) FreezeByRetire(id int64) { + p.freezeWithReason(id, FrozenReasonRetire) +} + +// freezeWithReason 统一冻结入口:记录来源 + 维护 offlineFrozen 标记 +func (p *AccountPool) freezeWithReason(id int64, reason string) { + p.mu.Lock() + defer p.mu.Unlock() + acct, ok := p.accounts[id] + if !ok { + return + } + acct.Status = StatusFrozen + acct.FrozenReason = reason + if reason == FrozenReasonOffline { + p.offlineFrozen[id] = true + } else { + delete(p.offlineFrozen, id) + } +} + +// UnfreezeByOffline 仅解冻「离线联动冻结」的账户(设备 adb 恢复时调用)。 +// 手动冻结/风控冻结的账户(未在 offlineFrozen 中)不受影响,保持 frozen。 +func (p *AccountPool) UnfreezeByOffline(id int64) { + p.mu.Lock() + defer p.mu.Unlock() + if !p.offlineFrozen[id] { + return + } + delete(p.offlineFrozen, id) + if acct, ok := p.accounts[id]; ok { + acct.Status = StatusNormal + acct.FrozenReason = "" + } +} + +// IsOfflineFrozen 返回账户是否因「设备离线联动」冻结(仅内部测试/诊断用) +func (p *AccountPool) IsOfflineFrozen(id int64) bool { + p.mu.RLock() + defer p.mu.RUnlock() + return p.offlineFrozen[id] +} + +// ---------- 管理接口(仅 Admin API 调用)---------- + +// Add 添加账户到池中(Admin API 解密后调用) +func (p *AccountPool) Add(acct *Account) { + p.mu.Lock() + defer p.mu.Unlock() + p.accounts[acct.ID] = acct + if acct.AppID != "" { + p.byAppID[acct.AppID] = acct + } + if acct.MchID != "" { + p.byMchID[acct.MchID] = acct + } + // 重启后重建离线联动冻结标记(FrozenReason 持久化,服务重启自动解冻能力不丢) + if acct.Status == StatusFrozen && acct.FrozenReason == FrozenReasonOffline { + p.offlineFrozen[acct.ID] = true + } +} + +// Update 更新池中账户 +func (p *AccountPool) Update(acct *Account) { + p.mu.Lock() + defer p.mu.Unlock() + // 移除旧索引 + if old, ok := p.accounts[acct.ID]; ok { + if old.AppID != "" { + delete(p.byAppID, old.AppID) + } + if old.MchID != "" { + delete(p.byMchID, old.MchID) + } + } + p.accounts[acct.ID] = acct + if acct.AppID != "" { + p.byAppID[acct.AppID] = acct + } + if acct.MchID != "" { + p.byMchID[acct.MchID] = acct + } +} + +// Remove 从池中移除账户 +func (p *AccountPool) Remove(id int64) { + p.mu.Lock() + defer p.mu.Unlock() + if old, ok := p.accounts[id]; ok { + if old.AppID != "" { + delete(p.byAppID, old.AppID) + } + if old.MchID != "" { + delete(p.byMchID, old.MchID) + } + delete(p.accounts, id) + } +} + +// SetStatus 设置账户状态 +func (p *AccountPool) SetStatus(id int64, status AccountStatus) { + p.mu.Lock() + defer p.mu.Unlock() + if acct, ok := p.accounts[id]; ok { + acct.Status = status + } +} + +// ListAll 返回所有账户的快照(管理后台用) +func (p *AccountPool) ListAll() []*Account { + p.mu.RLock() + defer p.mu.RUnlock() + out := make([]*Account, 0, len(p.accounts)) + for _, acct := range p.accounts { + // 复制一份,避免外部修改 + c := *acct + out = append(out, &c) + } + return out +} + +// ListByChannel 返回某通道所有账户(查单/对账用) +func (p *AccountPool) ListByChannel(channel string) []*Account { + p.mu.RLock() + defer p.mu.RUnlock() + var out []*Account + for _, acct := range p.accounts { + if acct.Channel == channel { + c := *acct + out = append(out, &c) + } + } + return out +} + +// CountByChannel 返回某通道的账户数量 +func (p *AccountPool) CountByChannel(channel string) int { + return len(p.ListByChannel(channel)) +} + +// GetByID 按 ID 获取账户 +func (p *AccountPool) GetByID(id int64) *Account { + p.mu.RLock() + defer p.mu.RUnlock() + if acct, ok := p.accounts[id]; ok { + c := *acct + return &c + } + return nil +} + +// ---------- 查询接口(各模块调用)---------- + +// LookupByAppID 按 app_id 反查账户(支付宝回调用) +func (p *AccountPool) LookupByAppID(appID string) *Account { + p.mu.RLock() + defer p.mu.RUnlock() + if acct, ok := p.byAppID[appID]; ok { + c := *acct + return &c + } + return nil +} + +// LookupByMchID 按 mch_id 反查账户(微信回调用) +func (p *AccountPool) LookupByMchID(mchID string) *Account { + p.mu.RLock() + defer p.mu.RUnlock() + if acct, ok := p.byMchID[mchID]; ok { + c := *acct + return &c + } + return nil +} + +// ---------- 选择调度接口 ---------- + +// Select 选一个可用账户(加权随机 + 日限额 + 冷却检查) +// channel: 通道类型(alipay / wechat) +// group: 业务分组("default") +// 返回 nil 表示无可用账户 +func (p *AccountPool) Select(channel, group string) *Account { + // 1. 过滤可用账户 + candidates := p.filterAvailable(channel, group) + if len(candidates) == 0 { + return nil + } + + // 2. 按 priority 分组(高优先级优先) + groups := groupByPriority(candidates) + + // 3. 从最高优先级开始选 + priorities := sortedDesc(groups) + for _, pri := range priorities { + accts := groups[pri] + if len(accts) == 0 { + continue + } + // 4. 同优先级内加权随机 + selected := weightedRandom(accts) + if selected != nil { + return selected + } + } + + return nil +} + +// SelectFrom 在可用账户 ∩ allowedIDs 中按优先级/权重选取(设备感知路由用) +// allowedIDs 为空 = 等价于 Select(不过滤);返回 nil 表示无可用账户 +func (p *AccountPool) SelectFrom(channel, group string, allowedIDs map[int64]bool) *Account { + candidates := p.filterAvailable(channel, group) + if len(allowedIDs) > 0 && len(candidates) > 0 { + filtered := make([]*Account, 0, len(candidates)) + for _, acct := range candidates { + if allowedIDs[acct.ID] { + filtered = append(filtered, acct) + } + } + candidates = filtered + } + if len(candidates) == 0 { + return nil + } + groups := groupByPriority(candidates) + priorities := sortedDesc(groups) + for _, pri := range priorities { + if selected := weightedRandom(groups[pri]); selected != nil { + return selected + } + } + return nil +} + +// SelectFromWeighted 在可用账户 ∩ allowedIDs 中按评分函数选最优(均衡分配用)。 +// score 返回账户评分(越高越优先);评分只用于同优先级内排序,优先级分组仍生效。 +// allowedIDs 为空 = 不过滤;score 为 nil = 退化为 weightedRandom(等价 SelectFrom)。 +// 返回 nil 表示无可用账户。 +func (p *AccountPool) SelectFromWeighted(channel, group string, allowedIDs map[int64]bool, score func(acct *Account) float64) *Account { + candidates := p.filterAvailable(channel, group) + if len(allowedIDs) > 0 && len(candidates) > 0 { + filtered := make([]*Account, 0, len(candidates)) + for _, acct := range candidates { + if allowedIDs[acct.ID] { + filtered = append(filtered, acct) + } + } + candidates = filtered + } + if len(candidates) == 0 { + return nil + } + if score == nil { + return p.SelectFrom(channel, group, allowedIDs) + } + // 按 priority 分组,组内按评分 argmax(评分相等 → 按 ID 升序,确定性) + groups := groupByPriority(candidates) + priorities := sortedDesc(groups) + for _, pri := range priorities { + accts := groups[pri] + best := accts[0] + bestScore := score(best) + for _, acct := range accts[1:] { + s := score(acct) + if s > bestScore || (s == bestScore && acct.ID < best.ID) { + best, bestScore = acct, s + } + } + if best != nil { + c := *best + return &c + } + } + return nil +} + +// ReportResult 上报下单结果(成功/失败),用于熔断统计 +// 由 channel 在下单后调用 +func (p *AccountPool) ReportResult(accountID int64, success bool) { + p.mu.Lock() + defer p.mu.Unlock() + + acct, ok := p.accounts[accountID] + if !ok { + return + } + + if success { + // 成功:重置失败计数 + acct.FailCount = 0 + } else { + // 失败:递增失败计数 + acct.FailCount++ + if acct.AutoBan && acct.FailCount >= 3 { + // 连续失败 3 次 → 冷却 15 分钟 + acct.Status = StatusCooling + now := time.Now() + until := now.Add(15 * time.Minute) + acct.FrozenUntil = &until + } + } +} + +// AddDailyUsed 增加日已用额度(下单成功后调用) +func (p *AccountPool) AddDailyUsed(accountID int64, amount int64) { + p.mu.Lock() + defer p.mu.Unlock() + if acct, ok := p.accounts[accountID]; ok { + acct.DailyUsed += amount + } +} + +// ResetDailyUsed 重置所有账户的日已用额度(每日 00:00 调用,与 DB 重置同步) +func (p *AccountPool) ResetDailyUsed() { + p.mu.Lock() + defer p.mu.Unlock() + for _, acct := range p.accounts { + acct.DailyUsed = 0 + } +} + +// Count 返回池中账户总数 +func (p *AccountPool) Count() int { + p.mu.RLock() + defer p.mu.RUnlock() + return len(p.accounts) +} + +// ---------- 内部辅助 ---------- + +// filterAvailable 过滤可用账户 +// 注意:本函数会"恢复过期冷却"(写操作),因此必须持有写锁(由 Select 调用时保证) +func (p *AccountPool) filterAvailable(channel, group string) []*Account { + p.mu.Lock() + defer p.mu.Unlock() + + now := time.Now() + var out []*Account + for _, acct := range p.accounts { + // 通道匹配 + if acct.Channel != channel { + continue + } + // 分组匹配 + if acct.Group != group { + continue + } + // 冷却到期的自动恢复:必须先于状态过滤。 + // 只恢复 cooling(自动熔断到期);frozen(离线联动/风控/手动)不在此解冻, + // 由设备心跳恢复联动或管理员操作解除。 + if acct.Status == StatusCooling && acct.FrozenUntil != nil && !acct.FrozenUntil.After(now) { + acct.Status = StatusNormal + acct.FrozenUntil = nil + acct.FailCount = 0 + } + // 状态检查:仅 normal 可用 + if acct.Status != StatusNormal { + continue + } + // 日限额检查 + if acct.DailyLimit > 0 && acct.DailyUsed >= acct.DailyLimit { + continue + } + // 冷却中 + if acct.FrozenUntil != nil && acct.FrozenUntil.After(now) { + continue + } + out = append(out, acct) + } + return out +} + +// groupByPriority 按 priority 分组(高→低) +func groupByPriority(accts []*Account) map[int][]*Account { + groups := make(map[int][]*Account) + for _, acct := range accts { + groups[acct.Priority] = append(groups[acct.Priority], acct) + } + return groups +} + +// sortedDesc 返回排序后的 key 列表(降序) +func sortedDesc(m map[int][]*Account) []int { + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + // 简单排序(降序) + for i := 0; i < len(keys); i++ { + for j := i + 1; j < len(keys); j++ { + if keys[j] > keys[i] { + keys[i], keys[j] = keys[j], keys[i] + } + } + } + return keys +} + +// weightedRandom 加权随机选择 +func weightedRandom(accts []*Account) *Account { + if len(accts) == 0 { + return nil + } + if len(accts) == 1 { + c := *accts[0] + return &c + } + + totalWeight := 0 + for _, acct := range accts { + weight := acct.Weight + if weight <= 0 { + weight = 1 + } + totalWeight += weight + } + + r := rand.Intn(totalWeight) + cumulative := 0 + for _, acct := range accts { + weight := acct.Weight + if weight <= 0 { + weight = 1 + } + cumulative += weight + if r < cumulative { + c := *acct + return &c + } + } + + // fallback + c := *accts[len(accts)-1] + return &c +} diff --git a/internal/account/qrcode.go b/internal/account/qrcode.go new file mode 100644 index 0000000..c7fdb5b --- /dev/null +++ b/internal/account/qrcode.go @@ -0,0 +1,34 @@ +package account + +import "strings" + +// InvalidQRContent 校验账户收款码内容格式。返回非空字符串 = 非法原因("" = 合法)。 +// 目的(资金安全防线):QRContent 直接决定扫码后用户的支付去向,脏数据(如把报错 +// 文案/任意文本误存为码)会生成一个无法支付的二维码,直接毁掉支付体验。 +// 合法格式(对齐两平台实际码结构): +// - 支付宝(personal.alipay):https:// 开头的收款码 URL +// - 微信(personal.wechat):wxp:// 或 weixin:// 开头的私有收款协议 +// +// 两平台都要求内容非空且不含空白/控制字符(QR 内容带换行/空格会编码异常)。 +// channel 形如 "personal.alipay" / "personal.wechat";空/未知通道按 https URL 要求。 +func InvalidQRContent(channel, content string) string { + if content == "" { + return "内容为空" + } + // 内容中任何位置都不允许空白/控制字符(QR 编码空格/换行会变异,扫码后内容不符) + if strings.IndexFunc(content, func(r rune) bool { + return r == ' ' || r == '\t' || r == '\r' || r == '\n' + }) >= 0 { + return "内容含空白或控制字符" + } + if strings.HasPrefix(channel, "personal.wechat") { + if strings.HasPrefix(content, "wxp://") || strings.HasPrefix(content, "weixin://") { + return "" + } + return "微信收款码必须以 wxp:// 或 weixin:// 开头" + } + if !strings.HasPrefix(content, "https://") { + return "收款码必须为 https:// URL" + } + return "" +} diff --git a/internal/account/qrcode_test.go b/internal/account/qrcode_test.go new file mode 100644 index 0000000..22acf5c --- /dev/null +++ b/internal/account/qrcode_test.go @@ -0,0 +1,143 @@ +package account + +import "testing" + +// TestInvalidQRContent_Wechat 微信收款码格式校验:wxp:// 与 weixin:// 合法,其它非法。 +func TestInvalidQRContent_Wechat(t *testing.T) { + cases := []struct { + name string + content string + wantErr bool + }{ + {"wxp 合法", "wxp://f2f01PGBgX9q6lITQMxL7rZ14-C8jDWNkaOPZ7mw2cYhTYW8NiseyukoiyOI8nWRURWQ", false}, + {"weixin 合法", "weixin://wxpay/bizpayurl/up?pr=NwY5Mz9", false}, + {"空字符串非法", "", true}, + {"TESTMAIN 报错文案非法", "TESTMAIN-AGAIN", true}, + {"普通文本非法", "hello world", true}, + {"带换行非法", "wxp://abc\n", true}, + {"带空格非法", "wxp://ab c", true}, + {"带 tab 非法", "wxp://abc\t", true}, + {"https 微信码非法", "https://qr.wechat.com/xxx", true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + reason := InvalidQRContent("personal.wechat", tc.content) + if tc.wantErr && reason == "" { + t.Fatalf("期望非法(内容 %q),实际通过校验", tc.content) + } + if !tc.wantErr && reason != "" { + t.Fatalf("期望合法(内容 %q),实际拒绝: %s", tc.content, reason) + } + }) + } +} + +// TestInvalidQRContent_Alipay 支付宝收款码格式校验:https URL 合法,其它非法。 +func TestInvalidQRContent_Alipay(t *testing.T) { + cases := []struct { + name string + content string + wantErr bool + }{ + {"qr.alipay 合法", "https://qr.alipay.com/fkx11063uortchy1ft90ibb", false}, + {"通用 https 合法", "https://example.com/pay", false}, + {"空字符串非法", "", true}, + {"报错文案非法", "SYSTEM_ERROR", true}, + {"http 非法", "http://qr.alipay.com/xxx", true}, + {"wxp 支付宝码非法", "wxp://f2f0xxx", true}, + {"带换行非法", "https://qr.alipay.com/xx\n", true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + reason := InvalidQRContent("personal.alipay", tc.content) + if tc.wantErr && reason == "" { + t.Fatalf("期望非法(内容 %q),实际通过校验", tc.content) + } + if !tc.wantErr && reason != "" { + t.Fatalf("期望合法(内容 %q),实际拒绝: %s", tc.content, reason) + } + }) + } +} + +// TestInvalidQRContent_UnknownChannel 未知通道按 https URL 要求校验(保守默认)。 +func TestInvalidQRContent_UnknownChannel(t *testing.T) { + if reason := InvalidQRContent("merchant.wechat", "wxp://abc"); reason == "" { + t.Fatal("非 personal 通道不应接受 wxp 码") + } + if reason := InvalidQRContent("", "https://x.com/pay"); reason != "" { + t.Fatalf("空通道 https URL 应合法,实际拒绝: %s", reason) + } +} + +// TestSelectFromWeighted_PicksHighestScore 均衡分配:SelectFromWeighted 选评分最高账户 +func TestSelectFromWeighted_PicksHighestScore(t *testing.T) { + p := NewPool() + p.Add(&Account{ID: 1, Channel: "personal.alipay", Group: "default", Name: "A", Priority: 1, + DailyUsed: 900, DailyLimit: 1000, Status: StatusNormal}) + p.Add(&Account{ID: 2, Channel: "personal.alipay", Group: "default", Name: "B", Priority: 1, + DailyUsed: 100, DailyLimit: 1000, Status: StatusNormal}) + p.Add(&Account{ID: 3, Channel: "personal.alipay", Group: "default", Name: "C", Priority: 1, + DailyUsed: 0, DailyLimit: 1000, Status: StatusNormal}) + + // 评分 = 日限额余量(1 - used/limit):C(1.0) > B(0.9) > A(0.1) + allowed := map[int64]bool{1: true, 2: true, 3: true} + got := p.SelectFromWeighted("personal.alipay", "default", allowed, + func(acct *Account) float64 { + return 1 - float64(acct.DailyUsed)/float64(acct.DailyLimit) + }) + if got == nil || got.ID != 3 { + t.Fatalf("应选 C(3),got %+v", got) + } +} + +// TestSelectFromWeighted_ExcludesDisallowed 均衡分配:allowedIDs 过滤生效 +func TestSelectFromWeighted_ExcludesDisallowed(t *testing.T) { + p := NewPool() + p.Add(&Account{ID: 1, Channel: "personal.alipay", Group: "default", Name: "A", Priority: 1, + DailyUsed: 900, DailyLimit: 1000, Status: StatusNormal}) + p.Add(&Account{ID: 2, Channel: "personal.alipay", Group: "default", Name: "B", Priority: 1, + DailyUsed: 100, DailyLimit: 1000, Status: StatusNormal}) + + // 只允许 1(评分低的 A)→ 必须选 A + allowed := map[int64]bool{1: true} + got := p.SelectFromWeighted("personal.alipay", "default", allowed, + func(acct *Account) float64 { + return 1 - float64(acct.DailyUsed)/float64(acct.DailyLimit) + }) + if got == nil || got.ID != 1 { + t.Fatalf("应选 A(1)(allowed 过滤),got %+v", got) + } + + // 不允许任何 → nil + got2 := p.SelectFromWeighted("personal.alipay", "default", map[int64]bool{999: true}, func(acct *Account) float64 { return 1 }) + if got2 != nil { + t.Fatalf("无 allowed 账户应 nil,got %+v", got2) + } +} + +// TestSelectFromWeighted_ScoreTieBreakByID 评分相等 → ID 升序确定性 +func TestSelectFromWeighted_ScoreTieBreakByID(t *testing.T) { + p := NewPool() + p.Add(&Account{ID: 5, Channel: "personal.alipay", Group: "default", Name: "E", Priority: 1, + DailyUsed: 500, DailyLimit: 1000, Status: StatusNormal}) + p.Add(&Account{ID: 2, Channel: "personal.alipay", Group: "default", Name: "B", Priority: 1, + DailyUsed: 500, DailyLimit: 1000, Status: StatusNormal}) + // 评分相同(0.5)→ 应选 ID 更小的 B(2) + got := p.SelectFromWeighted("personal.alipay", "default", nil, + func(acct *Account) float64 { return 0.5 }) + if got == nil || got.ID != 2 { + t.Fatalf("评分相等应选 ID 升序的 B(2),got %+v", got) + } +} + +// TestSelectFromWeighted_NilScoreFallsBack 评分函数为 nil → 退化为 SelectFrom(不 panic) +func TestSelectFromWeighted_NilScoreFallsBack(t *testing.T) { + p := NewPool() + p.Add(&Account{ID: 1, Channel: "personal.alipay", Group: "default", Name: "A", Priority: 1, + DailyUsed: 500, DailyLimit: 1000, Status: StatusNormal}) + got := p.SelectFromWeighted("personal.alipay", "default", nil, nil) + if got == nil || got.ID != 1 { + t.Fatalf("nil score 应退化选取,got %+v", got) + } +} diff --git a/internal/adbmgr/adbmgr.go b/internal/adbmgr/adbmgr.go new file mode 100644 index 0000000..cad6af6 --- /dev/null +++ b/internal/adbmgr/adbmgr.go @@ -0,0 +1,809 @@ +// Package adbmgr 管理 adb 触达的安卓设备(USB 直连真机 / adb over WiFi) +// +// 定位:web 前端可视化操作 adb 触达的安卓设备(docs/MIANQIAN.md §7.0 增强)。 +// 不做完整云手机(无实时屏幕流/远程点击控制),只提供"按需渲染 app 界面"所需的操作: +// - Screenshot: 截图当前屏幕(按需渲染) +// - Tap/Swipe/InputText: 轻量操作(完成登录等) +// - StartApp/ForegroundApp: 启动/查看前台 app +// - Online: 设备在线检测 +// +// 实现:直接调用系统 adb 二进制(-s 定位设备)。 +// USB 真机:serial 为纯设备号(如 "4f2bdc30"),无需 adb connect,直接 get-state 验证。 +// adb over WiFi:serial 为 host:port,需 adb connect 后再操作。 +package adbmgr + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +// Device 一台 adb 触达的安卓设备(USB 真机 / adb over WiFi) +type Device struct { + Serial string // adb serial:USB 直连设备号 "4f2bdc30" 或 TCP "host:port"(adb over WiFi) + Port int // adb 端口(USB 直连为 0;adb over WiFi 使用) + Name string // 备注名(来自 personal_devices.name) +} + +// Manager adb 操作管理器(线程安全,可并发操作多设备) +type Manager struct { + adbBin string // adb 可执行路径(默认 "adb") + mu sync.Mutex + + // screenCache 设备实际生效分辨率进程内缓存(serial → 分辨率)。 + // 物理分辨率运行时不变 → 缓存无失效时间;查询/解析失败不写入(下次调用重试)。 + // 独立锁 screenMu 保护:不复用 mu(mu 是 adb 执行串行锁,ScreenSize 需在 + // 不持锁时调 Shell→adb() 再次锁 mu,共用一个非重入锁会死锁)。 + screenMu sync.Mutex + screenCache map[string]screenSize +} + +// screenSize 设备实际生效分辨率(宽/高,像素) +type screenSize struct { + w, h int +} + +// 参考分辨率:capture/ledgermon 全部硬编码坐标均按 1080x2400 真机实测。 +// TapRef/SwipeRef 调用前先按实际生效分辨率等比缩放(见各方法注释)。 +const ( + refW = 1080 + refH = 2400 +) + +// New 创建 adb 管理器;adbBin 为空时使用系统 PATH 中的 adb +func New(adbBin string) *Manager { + if adbBin == "" { + adbBin = "adb" + } + return &Manager{adbBin: adbBin, screenCache: make(map[string]screenSize)} +} + +// adb 执行单条 adb 命令(带超时),返回 stdout。 +// 全局串行化:WiFi adb 并发执行多条命令时 connect/命令互相竞争会超时 +// (2026-08-28 实测:预热导航与监听轮询并发打 adb,connect context deadline exceeded)。 +func (m *Manager) adb(ctx context.Context, timeout time.Duration, serial string, args ...string) (string, error) { + m.mu.Lock() + defer m.mu.Unlock() + + cctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + full := append([]string{"-s", serial}, args...) + cmd := exec.CommandContext(cctx, m.adbBin, full...) + var out, errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + msg := strings.TrimSpace(errb.String()) + if msg == "" { + msg = err.Error() + } + return "", fmt.Errorf("adb %s: %w (%s)", strings.Join(full, " "), err, msg) + } + return out.String(), nil +} + +// Connect 连接设备(幂等)。失败返回错误(设备离线/容器未起)。 +// serial 含 ":" 视为 TCP 地址(host:port,如 127.0.0.1:5555),执行 adb connect; +// 纯 serial(如 USB 直连设备 "4f2bdc30")跳过 connect,直接 get-state 验证。 +func (m *Manager) Connect(ctx context.Context, serial string) error { + if !strings.Contains(serial, ":") { + // USB 直连设备:无需 adb connect,验证状态即可 + out, err := m.adb(ctx, 5*time.Second, serial, "get-state") + if err != nil { + return err + } + if !strings.Contains(out, "device") { + return fmt.Errorf("adb %s: 设备状态异常 (%s)", serial, strings.TrimSpace(out)) + } + return nil + } + cctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + // 与 adb() 共享全局锁:adb connect 也串行执行,避免并发竞争超时 + m.mu.Lock() + defer m.mu.Unlock() + cmd := exec.CommandContext(cctx, m.adbBin, "connect", serial) + var out, errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + return fmt.Errorf("adb connect %s: %w", serial, err) + } + res := out.String() + errb.String() + if strings.Contains(res, "cannot connect") || strings.Contains(res, "failed") || strings.Contains(res, "unable") { + return fmt.Errorf("adb connect %s: %s", serial, strings.TrimSpace(res)) + } + return nil +} + +// Online 设备是否在线(可连且已认证)。每次调用实时检测。 +func (m *Manager) Online(ctx context.Context, serial string) bool { + if err := m.Connect(ctx, serial); err != nil { + return false + } + out, err := m.adb(ctx, 5*time.Second, serial, "get-state") + if err != nil { + return false + } + return strings.Contains(out, "device") +} + +// 设备状态枚举(来自 `adb devices -l`) +const ( + StateDevice = "device" // 已授权可用 + StateUnauthorized = "unauthorized" // 待手机端授权 USB 调试 + StateOffline = "offline" // adb 链接异常 +) + +// USBDevice 一台已连 adb 的真机/虚拟设备的瞬时状态(引导向导用) +type USBDevice struct { + Serial string // adb serial:USB 设备号(如 "4f2bdc30")或 WiFi host:port(如 "192.168.1.119:5555") + State string // device / unauthorized / offline + Model string // 设备型号(如 "MI_8"),可能为空 + Kind string // "usb"(USB 直连)或 "wifi"(adb over WiFi,host:port) +} + +// ListDevices 列出当前已连到本机的真机(adb devices -l 输出,过滤模拟器/容器)。 +// 同时返回 USB 直连设备(serial 不含 ":")与 adb over WiFi 设备(serial 含 ":",已 connect 过的)。 +func (m *Manager) ListDevices(ctx context.Context) ([]USBDevice, error) { + cctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + cmd := exec.CommandContext(cctx, m.adbBin, "devices", "-l") + var out, errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("adb devices: %w (%s)", err, strings.TrimSpace(errb.String())) + } + var devs []USBDevice + for _, line := range strings.Split(out.String(), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "List of devices") { + continue + } + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + serial := fields[0] + kind := "usb" + if strings.Contains(serial, ":") { + kind = "wifi" + } + state := fields[1] + model := "" + for _, f := range fields[2:] { + if strings.HasPrefix(f, "model:") { + model = strings.TrimPrefix(f, "model:") + break + } + } + devs = append(devs, USBDevice{Serial: serial, State: state, Model: model, Kind: kind}) + } + return devs, nil +} + +// Screenshot 截图当前屏幕,返回 PNG 字节 +func (m *Manager) Screenshot(ctx context.Context, serial string) ([]byte, error) { + if err := m.Connect(ctx, serial); err != nil { + return nil, err + } + cctx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + cmd := exec.CommandContext(cctx, m.adbBin, "-s", serial, "exec-out", "screencap", "-p") + var out bytes.Buffer + var errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("screencap %s: %w (%s)", serial, err, strings.TrimSpace(errb.String())) + } + if out.Len() == 0 { + return nil, fmt.Errorf("screencap %s: 空输出(设备可能刚启动或锁屏)", serial) + } + return out.Bytes(), nil +} + +// ListFiles 列出设备端目录(shell ls -lt,按修改时间倒序),返回文件名列表 +func (m *Manager) ListFiles(ctx context.Context, serial, dir string) ([]string, error) { + if err := m.Connect(ctx, serial); err != nil { + return nil, err + } + out, err := m.adb(ctx, 10*time.Second, serial, "shell", "ls", "-t", dir) + if err != nil { + return nil, err + } + var names []string + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if line != "" { + names = append(names, line) + } + } + return names, nil +} + +// PullFile 从设备拉取文件到本地临时目录(adb pull),返回本地路径 +func (m *Manager) PullFile(ctx context.Context, serial, devicePath string) (string, error) { + if err := m.Connect(ctx, serial); err != nil { + return "", err + } + cctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + tmp, err := os.CreateTemp("", "adbpull_*.png") + if err != nil { + return "", err + } + local := tmp.Name() + tmp.Close() + cmd := exec.CommandContext(cctx, m.adbBin, "-s", serial, "pull", devicePath, local) + var errb bytes.Buffer + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + os.Remove(local) + return "", fmt.Errorf("adb pull %s: %w (%s)", devicePath, err, strings.TrimSpace(errb.String())) + } + return local, nil +} + +// DumpUI 导出设备当前屏幕的 UI 层级树(uiautomator dump),返回 XML 文本。 +// 用途:服务端直接解析账本页/收款记录页等界面(不受 FLAG_SECURE 限制,adb 是系统级通道)。 +// 性能说明(2026-08-30 红米k60/MIUI14 实测): +// - 单次 dump 固定约 2.3s 为 uiautomator 进程启动 + MIUI 主题初始化开销(无法规避,无 root); +// - 复杂页面(支付宝账本 95-104KB)在动画中会 idle 等待至 10s+(实测偶发); +// - --compressed 实测仅省 ~8% 体积、无耗时收益,故不用(保持与解析器兼容); +// - 优化方向是减少 dump 调用次数(页面无变化时用哈希跳过,见 ledgermon reader), +// 而非压缩单次 dump。 +// +// 注意:MIUI 每次 dump 会向 stderr 打印 theme_compatibility.xml 缺失堆栈(约 2KB), +// 不影响结果,属固定开销。 +func (m *Manager) DumpUI(ctx context.Context, serial string) (string, error) { + if err := m.Connect(ctx, serial); err != nil { + return "", err + } + if _, err := m.adb(ctx, 8*time.Second, serial, "shell", "uiautomator", "dump", "/sdcard/pgw_ui.xml"); err != nil { + return "", err + } + // dump 落盘后 cat 读取(uiautomator dump 输出在 stdout 提示,XML 写入文件) + out, err := m.adb(ctx, 8*time.Second, serial, "shell", "cat", "/sdcard/pgw_ui.xml") + if err != nil { + return "", err + } + return out, nil +} + +// Shell 执行任意设备端 shell 命令(超时受控),返回 stdout +func (m *Manager) Shell(ctx context.Context, timeout time.Duration, serial, command string) (string, error) { + if err := m.Connect(ctx, serial); err != nil { + return "", err + } + return m.adb(ctx, timeout, serial, "shell", command) +} + +// ScreenSize 查询设备实际生效分辨率(adb shell wm size),返回 (宽, 高) 像素。 +// 结果按 serial 进程内缓存:同一设备多次查询不重复走 adb(缓存无失效时间, +// 分辨率运行时不变)。失败(adb 不可达/输出不可解析/数值非法)返回错误—— +// 调用方据此跳过本轮操作,不误操作设备。 +func (m *Manager) ScreenSize(ctx context.Context, serial string) (w, h int, err error) { + m.screenMu.Lock() + if sz, ok := m.screenCache[serial]; ok { + m.screenMu.Unlock() + return sz.w, sz.h, nil + } + m.screenMu.Unlock() + + out, err := m.Shell(ctx, 5*time.Second, serial, "wm size") + if err != nil { + return 0, 0, err + } + w, h, err = parseWmSize(out) + if err != nil { + return 0, 0, err + } + m.screenMu.Lock() + m.screenCache[serial] = screenSize{w: w, h: h} + m.screenMu.Unlock() + return w, h, nil +} + +// wmSizeRe 匹配 "WxH" 分辨率标记(如 1080x2400)。包级编译一次复用。 +var wmSizeRe = regexp.MustCompile(`(\d+)x(\d+)`) + +// parseWmSize 解析 `adb shell wm size` 输出 → 实际生效分辨率 (w, h)。 +// 关键:uiautomator dump / input tap 的坐标基于**当前生效分辨率**——设备设置了 +// override(开发者选项「最小宽度」/wm size 覆盖)时生效的是 Override 值而非 Physical +// (实测红米 K60:Physical 1440x3200 + Override 1080x2400,UI 坐标按 1080x2400)。 +// 解析顺序:优先 "Override size: WxH" 行;缺省回退 "Physical size: WxH"; +// 再兜底任意首个含 "WxH" 的行(裸格式)。均无合法分辨率返回错误。 +func parseWmSize(out string) (w, h int, err error) { + lines := strings.Split(out, "\n") + for _, ln := range lines { + if strings.HasPrefix(strings.TrimSpace(ln), "Override size:") { + if w, h, ok := parseSizeToken(ln); ok { + return w, h, nil + } + } + } + for _, ln := range lines { + if strings.HasPrefix(strings.TrimSpace(ln), "Physical size:") { + if w, h, ok := parseSizeToken(ln); ok { + return w, h, nil + } + } + } + for _, ln := range lines { + if w, h, ok := parseSizeToken(ln); ok { + return w, h, nil + } + } + return 0, 0, fmt.Errorf("wm size 输出无法解析出分辨率 (输出: %q)", strings.TrimSpace(out)) +} + +// parseSizeToken 提取行内首个合法 "WxH" 分辨率标记;无/数值非法返回 ok=false +func parseSizeToken(ln string) (w, h int, ok bool) { + m := wmSizeRe.FindStringSubmatch(ln) + if m == nil { + return 0, 0, false + } + w, errW := strconv.Atoi(m[1]) + h, errH := strconv.Atoi(m[2]) + if errW != nil || errH != nil || w <= 0 || h <= 0 { + return 0, 0, false + } + return w, h, true +} + +// scaleRef 把参考坐标系(refW x refH)坐标缩放到实际生效分辨率(四舍五入)。 +// ScreenSize 查询失败返回错误:宁可跳过本轮点击,也不在未知分辨率下盲点。 +func (m *Manager) scaleRef(ctx context.Context, serial string, x, y int) (int, int, error) { + w, h, err := m.ScreenSize(ctx, serial) + if err != nil { + return 0, 0, err + } + return (x*w + refW/2) / refW, (y*h + refH/2) / refH, nil +} + +// TapRef 按 1080x2400 参考坐标系自动缩放到实际分辨率后点击。 +// 用于真机实测于 1080x2400 的硬编码坐标;dump bounds 派生坐标用 Tap 不经缩放。 +// ScreenSize 缓存命中零开销;查询失败时返回错误(不盲点)。 +func (m *Manager) TapRef(ctx context.Context, serial string, x, y int) error { + sx, sy, err := m.scaleRef(ctx, serial, x, y) + if err != nil { + return fmt.Errorf("参考坐标缩放 (%d,%d) 失败: %w", x, y, err) + } + return m.Tap(ctx, serial, sx, sy) +} + +// SwipeRef 按 1080x2400 参考坐标系自动缩放到实际分辨率后滑动。 +func (m *Manager) SwipeRef(ctx context.Context, serial string, x1, y1, x2, y2, durationMs int) error { + sx1, sy1, err := m.scaleRef(ctx, serial, x1, y1) + if err != nil { + return fmt.Errorf("参考坐标缩放 (%d,%d) 失败: %w", x1, y1, err) + } + sx2, sy2, err := m.scaleRef(ctx, serial, x2, y2) + if err != nil { + return fmt.Errorf("参考坐标缩放 (%d,%d) 失败: %w", x2, y2, err) + } + return m.Swipe(ctx, serial, sx1, sy1, sx2, sy2, durationMs) +} + +// StartAppUser 以指定系统用户(双开/分身空间)启动应用;userId<=0 等价 StartApp +func (m *Manager) StartAppUser(ctx context.Context, serial, packageName, activity string, userId int) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + userArg := "" + if userId > 0 { + userArg = fmt.Sprintf(" --user %d", userId) + } + if activity != "" { + _, err := m.adb(ctx, 10*time.Second, serial, "shell", "am", "start", userArg, "-n", packageName+"/"+activity) + return err + } + _, err := m.adb(ctx, 10*time.Second, serial, "shell", "am", "start", userArg, + "-a", "android.intent.action.MAIN", + "-c", "android.intent.category.LAUNCHER", + "-p", packageName) + if err != nil { + _, err = m.adb(ctx, 10*time.Second, serial, "shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1") + } + return err +} + +// ProcessAlive 目标包名进程是否存活(任意用户空间)。 +// 安卓后台 LMK 会杀 App 进程:任务栈随之丢失,热启动 am start 无法恢复到账本页, +// 必须先检测进程再决定走热/冷路径。双开(user 999)进程同样能被 pidof 查到。 +func (m *Manager) ProcessAlive(ctx context.Context, serial, packageName string) (bool, error) { + if err := m.Connect(ctx, serial); err != nil { + return false, err + } + out, err := m.adb(ctx, 8*time.Second, serial, "shell", "pidof", packageName) + if err != nil { + // pidof 对不存在的进程返回非零退出码 + 空输出 → 视为进程不存在(非 adb 故障) + return false, nil + } + return strings.TrimSpace(out) != "", nil +} + +// RestartAppUser 强制停止并重新启动指定用户空间的应用(确保从干净首页开始, +// 避免微信等 app 恢复到旧页面导致坐标导航落空)。只影响指定 user,不影响其它空间。 +// 启动用显式 activity(MAIN/LAUNCHER 类别对双开 user 不可靠,实测启动失败)。 +// 启动后轮询 ForegroundApp 直到目标包名出现在前台(最长 waitTimeout),避免固定 sleep 的时序抖动。 +func (m *Manager) RestartAppUser(ctx context.Context, serial, packageName, activity string, userId int) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + userArg := "" + if userId > 0 { + userArg = fmt.Sprintf(" --user %d", userId) + } + if _, err := m.adb(ctx, 10*time.Second, serial, "shell", "am", "force-stop", userArg, packageName); err != nil { + return err + } + time.Sleep(1500 * time.Millisecond) + if err := m.StartAppUser(ctx, serial, packageName, activity, userId); err != nil { + return err + } + // 轮询前台确认(最长 10s);确认不了不视为失败(部分 app 前台判定口径差异), + // 由调用方后续 activity 校验兜底。 + deadline := time.Now().Add(10 * time.Second) + for time.Now().Before(deadline) { + fg, err := m.ForegroundApp(ctx, serial) + if err == nil && fg == packageName { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(800 * time.Millisecond): + } + } + return nil +} + +// Rm 删除设备端文件/目录(用于相册旧码清理) +func (m *Manager) Rm(ctx context.Context, serial, path string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, err := m.adb(ctx, 10*time.Second, serial, "shell", "rm", "-f", path) + return err +} + +// Tap 模拟点击屏幕坐标(像素,基于截图分辨率) +func (m *Manager) Tap(ctx context.Context, serial string, x, y int) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, err := m.adb(ctx, 8*time.Second, serial, "shell", "input", "tap", fmt.Sprintf("%d", x), fmt.Sprintf("%d", y)) + return err +} + +// Swipe 模拟滑动 +func (m *Manager) Swipe(ctx context.Context, serial string, x1, y1, x2, y2, durationMs int) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, err := m.adb(ctx, 8*time.Second, serial, "shell", "input", "swipe", + fmt.Sprintf("%d", x1), fmt.Sprintf("%d", y1), fmt.Sprintf("%d", x2), fmt.Sprintf("%d", y2), + fmt.Sprintf("%d", durationMs)) + return err +} + +// InputText 输入文本(模拟键盘;特殊字符按 adb input 规则转义) +func (m *Manager) InputText(ctx context.Context, serial, text string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + // adb shell input text 对空格/特殊字符敏感:用 %s 转义 + escaped := strings.ReplaceAll(text, " ", "%s") + escaped = strings.ReplaceAll(escaped, "&", "\\&") + _, err := m.adb(ctx, 8*time.Second, serial, "shell", "input", "text", escaped) + return err +} + +// KeyEvent 发送按键(如 KEYCODE_BACK=4, KEYCODE_HOME=3, KEYCODE_ENTER=66, KEYCODE_CLEAR=28) +func (m *Manager) KeyEvent(ctx context.Context, serial string, keycode int) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, err := m.adb(ctx, 8*time.Second, serial, "shell", "input", "keyevent", fmt.Sprintf("%d", keycode)) + return err +} + +// StartApp 启动应用(按包名 + 可选 activity) +// 未指定 activity 时通过 LAUNCHER 类别启动默认入口(避免依赖 monkey,精简镜像无 monkey) +func (m *Manager) StartApp(ctx context.Context, serial, packageName, activity string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + if activity != "" { + _, err := m.adb(ctx, 10*time.Second, serial, "shell", "am", "start", "-n", packageName+"/"+activity) + return err + } + // 优先:am start 带 MAIN/LAUNCHER 类别(等效 monkey 但更可靠) + _, err := m.adb(ctx, 10*time.Second, serial, "shell", "am", "start", + "-a", "android.intent.action.MAIN", + "-c", "android.intent.category.LAUNCHER", + "-p", packageName) + if err != nil { + // 兜底:monkey(部分镜像支持) + _, err = m.adb(ctx, 10*time.Second, serial, "shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1") + } + return err +} + +// ForegroundApp 当前前台 app 描述串(形如 "u0 com.eg.android.AlipayGphone/.PayeeQRActivity t30545")。 +// 保留原语义:capture 包用 strings.Contains 匹配 Activity 名(如 "PayeeQRActivity"), +// 只返回包名会破坏这些检查。需要结构化三元组(pkg/activity/userID)请用 ForegroundAppDetail。 +func (m *Manager) ForegroundApp(ctx context.Context, serial string) (string, error) { + if err := m.Connect(ctx, serial); err != nil { + return "", err + } + out, err := m.adb(ctx, 8*time.Second, serial, "shell", "dumpsys", "activity", "activities") + if err != nil { + return "", err + } + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "mResumedActivity") || strings.HasPrefix(line, "topResumedActivity") || strings.HasPrefix(line, "mFocusedApp") { + if idx := strings.Index(line, "{"); idx >= 0 { + rest := line[idx:] + if p := strings.Index(rest, " "); p >= 0 { + return strings.TrimPrefix(rest[p+1:], "u0/"), nil + } + } + } + } + return "", fmt.Errorf("未找到前台 activity") +} + +// ForegroundAppDetail 当前前台 app 三要素:包名 + Activity + 用户空间(0=主空间,999=双开分身)。 +// +// dumpsys activity activities 中 mResumedActivity 行实际格式: +// +// mResumedActivity: ActivityRecord{974988a u0 com.eg.android.AlipayGphone/.XRiverTransActivity t30545} +// mResumedActivity: ActivityRecord{974988a u999 com.tencent.mm/.ui.LauncherUI t30545} +// +// 双开(user 999)与主空间(user 0)的同一 Activity 名前缀不同(u0 / u999), +// 账本页监听必须按 userID 区分(否则 user0 的监听循环会把 user999 的账本页误判为已就绪 → 读错数据)。 +// 解析失败(如前台是桌面/无 resumed activity)返回空三元组 + 错误。 +func (m *Manager) ForegroundAppDetail(ctx context.Context, serial string) (pkg, activity string, userID int, err error) { + if err = m.Connect(ctx, serial); err != nil { + return "", "", 0, err + } + out, err := m.adb(ctx, 8*time.Second, serial, "shell", "dumpsys", "activity", "activities") + if err != nil { + return "", "", 0, err + } + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "mResumedActivity") && !strings.HasPrefix(line, "topResumedActivity") && !strings.HasPrefix(line, "mFocusedApp") { + continue + } + if idx := strings.Index(line, "{"); idx >= 0 { + rest := line[idx:] + if p := strings.Index(rest, " "); p >= 0 { + fields := strings.Fields(rest[p+1:]) + if len(fields) == 0 { + continue + } + first := fields[0] + // 用户空间前缀:u0 / u999(Android 多用户);无前缀视为主空间 + if strings.HasPrefix(first, "u") && len(first) > 1 && first[1] >= '0' && first[1] <= '9' { + if n, nerr := strconv.Atoi(first[1:]); nerr == nil { + userID = n + if len(fields) >= 2 { + first = fields[1] + } else { + continue + } + } + } + pkg, activity = splitPackageActivity(first) + if pkg == "" { + continue + } + return pkg, activity, userID, nil + } + } + } + return "", "", 0, fmt.Errorf("未找到前台 activity") +} + +// splitPackageActivity 拆分 "com.pkg/.Activity" → (包名, Activity 名) +// Activity 可能为 ".Xxx"(相对)或 "com.pkg.Xxx"(全名);无 Activity 时 activity 为空。 +func splitPackageActivity(full string) (pkg, activity string) { + if i := strings.Index(full, "/"); i >= 0 { + pkg = full[:i] + activity = full[i+1:] + if strings.HasPrefix(activity, ".") { + activity = pkg + activity + } + return pkg, activity + } + return full, "" +} + +// WakeUp 唤醒屏幕并解锁(模拟电源键 + 滑动解锁) +func (m *Manager) WakeUp(ctx context.Context, serial string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + m.adb(ctx, 5*time.Second, serial, "shell", "input", "keyevent", "224") // KEYCODE_WAKEUP + return nil +} + +// GoHome 派发 HOME 键回桌面,清掉前一个账户驻留的前台。 +// 冷启动导航前调用:MIUI 双用户隔离下,跨 user(如 u999 微信占前台时)冷启动另一 +// 用户(u0 支付宝)会被驻留前台挡住,am start 无法抢占(实测 30s 前台纹丝不动)。 +// 先清 PINNED(若残留 task lock 会拦截 HOME),再发 HOME、等桌面稳定。 +func (m *Manager) GoHome(ctx context.Context, serial string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, _ = m.adb(ctx, 5*time.Second, serial, "shell", "am", "task", "lock", "stop") // 清 PINNED(幂等) + time.Sleep(300 * time.Millisecond) + _, err := m.adb(ctx, 5*time.Second, serial, "shell", "input", "keyevent", "3") // KEYCODE_HOME + return err +} + +// PinCurrentTask 屏幕固定当前前台任务(task lock,2026-08-28 真机验证): +// 固定期间其他 app 无法抢占前台(am start 被拦截,error 101 START_CANCELED), +// 用于冷启动导航等长操作期间防止监听 app/系统弹窗打断。 +// 返回当前前台 taskId(固定成功后该 task 即被锁定)。 +func (m *Manager) PinCurrentTask(ctx context.Context, serial string) (string, error) { + if err := m.Connect(ctx, serial); err != nil { + return "", err + } + out, err := m.adb(ctx, 8*time.Second, serial, "shell", "dumpsys", "activity", "activities") + if err != nil { + return "", err + } + taskID := "" + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "topResumedActivity") || strings.HasPrefix(line, "mResumedActivity") { + if i := strings.LastIndex(line, "t"); i >= 0 { + cand := strings.TrimSuffix(line[i+1:], "}") + if cand != "" { + taskID = cand + } + } + break + } + } + if taskID == "" { + return "", fmt.Errorf("未找到前台任务 taskId") + } + if _, err := m.adb(ctx, 8*time.Second, serial, "shell", "am", "task", "lock", taskID); err != nil { + return "", fmt.Errorf("固定前台任务失败: %w", err) + } + return taskID, nil +} + +// UnpinTask 解除屏幕固定(am task lock stop)。解锁后其他 app 可正常抢占前台。 +// 幂等:非固定状态调用无副作用(返回 nil)。 +func (m *Manager) UnpinTask(ctx context.Context, serial string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, err := m.adb(ctx, 8*time.Second, serial, "shell", "am", "task", "lock", "stop") + return err +} + +// FindLedgerTaskID 扫描 dumpsys 查询指定 user 的账本页任务栈 ID(如 lite01 亲和栈)。 +// 在 dumpsys activity activities 输出中找包含 actSubstr 且匹配 userID 的 ActivityRecord, +// 提取其 taskId(如 "22233")。账本页不存在/进程被清时返回空 taskID+错误。 +func (m *Manager) FindLedgerTaskID(ctx context.Context, serial, pkg, actSubstr string, userID int) (string, error) { + if err := m.Connect(ctx, serial); err != nil { + return "", err + } + out, err := m.adb(ctx, 8*time.Second, serial, "shell", "dumpsys", "activity", "activities") + if err != nil { + return "", fmt.Errorf("dumpsys 失败: %w", err) + } + userPrefix := fmt.Sprintf("u%d", userID) + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + // 匹配 ActivityRecord{... uN pkg/...actSubstr...} tNNNN} + // 示例:ActivityRecord{fe2acf2 u999 com.eg.android.AlipayGphone/...XRiverActivity$App01} t22233} + if !strings.Contains(line, userPrefix) || !strings.Contains(line, actSubstr) { + continue + } + // 从行尾提取 tNNNN(如 "t22233}" → "22233") + if i := strings.LastIndex(line, " t"); i >= 0 { + cand := strings.TrimSuffix(line[i+2:], "}") + // 校验纯数字,避免匹配其他含 t 的 token + if cand != "" && cand[0] >= '0' && cand[0] <= '9' { + return cand, nil + } + } + } + return "", fmt.Errorf("未找到匹配的账本页任务栈(pkg=%s, act=%s, user=%d)", pkg, actSubstr, userID) +} + +// TaskLock 用 am task lock 锁定并唤醒指定任务栈到前台。相当于微信热启动方案 +// taskLock(serial, taskID),~160ms。 +// 注意:锁定后设备进入 PINNED 模式,会拦截 input tap/swipe,调用方应在确认 +// 前台后调用 UnpinTask 释放。 +func (m *Manager) TaskLock(ctx context.Context, serial, taskID string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + _, err := m.adb(ctx, 8*time.Second, serial, "shell", "am", "task", "lock", taskID) + return err +} + +// TCPIP 开启设备的无线 adb(`adb tcpip `)。 +// USB 直连时调用:设备进入 TCP/IP 监听模式,之后同一局域网可通过 : 连接。 +// 幂等:已开启会输出 "restarting in TCP mode port: N",重复调用无副作用。 +func (m *Manager) TCPIP(ctx context.Context, serial string, port int) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + out, err := m.adb(ctx, 10*time.Second, serial, "tcpip", fmt.Sprintf("%d", port)) + if err != nil { + return fmt.Errorf("adb tcpip 失败: %w", err) + } + if !strings.Contains(out, "restarting in TCP mode port") { + return fmt.Errorf("adb tcpip 未确认成功: %s", strings.TrimSpace(out)) + } + return nil +} + +// WifiIP 获取设备 WiFi 局域网 IP(引导向导自动配置 WiFi 备用通道用)。 +// 依次尝试多种方式:wlan0 网卡 → eth0 → getprop(部分 ROM 无 ip 命令时的兜底)。 +// 返回 "" 表示无法确定(设备未连 WiFi 或命令不可用)。 +func (m *Manager) WifiIP(ctx context.Context, serial string) string { + // 1. `ip addr show wlan0`(标准 Android) + if out, err := m.adb(ctx, 6*time.Second, serial, "shell", "ip", "addr", "show", "wlan0"); err == nil { + if ip := parseIPv4(out); ip != "" { + return ip + } + } + // 2. eth0(部分设备 wlan0 名为 eth0) + if out, err := m.adb(ctx, 6*time.Second, serial, "shell", "ip", "addr", "show", "eth0"); err == nil { + if ip := parseIPv4(out); ip != "" { + return ip + } + } + // 3. dhcp getprop(无 ip 命令的精简 ROM) + if out, err := m.adb(ctx, 6*time.Second, serial, "shell", "getprop", "dhcp.wlan0.ipaddress"); err == nil { + if ip := strings.TrimSpace(out); ip != "" { + return ip + } + } + return "" +} + +// parseIPv4 从 `ip addr` 输出提取首个内网 IPv4(跳过 127.0.0.1) +func parseIPv4(out string) string { + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "inet ") { + continue + } + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + addr := fields[1] + if idx := strings.Index(addr, "/"); idx >= 0 { + addr = addr[:idx] + } + if addr == "" || strings.HasPrefix(addr, "127.") { + continue + } + return addr + } + return "" +} diff --git a/internal/adbmgr/adbmgr_screen_test.go b/internal/adbmgr/adbmgr_screen_test.go new file mode 100644 index 0000000..23e8440 --- /dev/null +++ b/internal/adbmgr/adbmgr_screen_test.go @@ -0,0 +1,50 @@ +package adbmgr + +import ( + "testing" +) + +// TestParseWmSize 验证 `adb shell wm size` 输出解析(ScreenSize 内部逻辑,与 adb 解耦)。 +// 覆盖:Override/Physical/裸格式三种来源、优先级(Override 优先——input/dump 坐标基准)、 +// 空白/换行容错、非法与空输出。 +func TestParseWmSize(t *testing.T) { + cases := []struct { + name string + out string + wantW int + wantH int + wantErr bool + }{ + {"Physical 标准格式", "Physical size: 1080x2400\n", 1080, 2400, false}, + {"Override 标准格式", "Override size: 720x1600\n", 720, 1600, false}, + {"裸格式", "1440x3200", 1440, 3200, false}, + {"Override 优先于 Physical", "Physical size: 1080x2400\nOverride size: 720x1600\n", 720, 1600, false}, + {"红米K60 双尺寸实测", "Physical size: 1440x3200\nOverride size: 1080x2400\n", 1080, 2400, false}, + {"Physical 在第二行", "Override size: 720x1600\nPhysical size: 1080x2400\n", 720, 1600, false}, + {"前后多余空白", " \nOverride size: 720x1600 \n \n", 720, 1600, false}, + {"CRLF 行尾", "Physical size: 1080x2400\r\nOverride size: 720x1600\r\n", 720, 1600, false}, + {"Override 后跟无关行", "Override size: 720x1600\nsome garbage here\n", 720, 1600, false}, + {"垃圾输出", "no size available\n", 0, 0, true}, + {"空输出", "", 0, 0, true}, + {"Physical 行数值非法但有 Override", "Physical size: abcxdef\nOverride size: 720x1600\n", 720, 1600, false}, + {"半截分辨率", "Physical size: 1080x\n", 0, 0, true}, + {"零分辨率非法", "Physical size: 0x0\n", 0, 0, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + w, h, err := parseWmSize(tc.out) + if tc.wantErr { + if err == nil { + t.Fatalf("parseWmSize(%q) = (%d, %d, nil), 期望错误", tc.out, w, h) + } + return + } + if err != nil { + t.Fatalf("parseWmSize(%q) 意外错误: %v", tc.out, err) + } + if w != tc.wantW || h != tc.wantH { + t.Fatalf("parseWmSize(%q) = (%d, %d), want (%d, %d)", tc.out, w, h, tc.wantW, tc.wantH) + } + }) + } +} diff --git a/internal/adbmgr/keepalive.go b/internal/adbmgr/keepalive.go new file mode 100644 index 0000000..8a645c9 --- /dev/null +++ b/internal/adbmgr/keepalive.go @@ -0,0 +1,113 @@ +package adbmgr + +import ( + "context" + "fmt" + "strings" + "time" + + "paygateway/internal/logger" +) + +// keepalive 保活命令集:为支付通道 app(微信/支付宝)配置 Android/MIUI 后台存活保障。 +// +// 背景(2026-08-30 红米k60/MIUI14 实测): +// - 网关通过 adb 控制真机上的微信/支付宝读取账本/出收款码,账本页必须常驻后台; +// - MIUI 是"杀后台大户"(省电策略/墓碑冻结),但标准 adb 命令可覆盖其中一部分; +// - 本文件只封装"实测有效"的命令,避免给运维虚假安全感。 +// +// 实测有效的命令(全部无 root): +// - deviceidle whitelist + 加入电池优化白名单(dumpsys deviceidle 验证通过) +// - appops RUN_IN_BACKGROUND allow 允许后台运行 +// - appops RUN_ANY_IN_BACKGROUND allow 允许任意后台运行(实测微信/支付宝均已生效) +// +// 实测无效 / 被 MIUI 接管(不封装,避免误导): +// - am set-standby-bucket active 返回 bucket=5(restricted),MIUI 覆盖标准机制 +// - appops MIUIOP:1000x set Unknown operation string(MIUI 私有 op 不暴露 shell) +// - BOOT_COMPLETED / FOREGROUND_SERVICE appops key(Android 13 无此 op 名) +// - MIUI whetstone 冻结豁免(miui.whetstone.power 服务无 shell 入口,需 UI 设"无限制"省电) +// +// 补充:锁任务(LockPaymentTasks,见 alipay_lock.go)用 uiautomator 长按任务卡片实现, +// 是一键清理后仍保留的关键手段,与本命令集互补。 +// 局限:锁任务/白名单都防不住 MIUI 墓碑冻结(进程冻结后热唤醒回主进程)——这是 UI 层 +// "省电策略→无限制"才能解决的(无 root 无 adb 入口),cached_apps_freezer 只关掉原生层。 + +// 支付通道应用包名(保活目标;与 ledgermon reader 各自声明的常量一致) +const ( + WechatPkg = "com.tencent.mm" + AlipayPkg = "com.eg.android.AlipayGphone" +) + +// KeepAliveApply 对单个包应用全部实测有效的保活命令。 +// 返回实际应用了哪些命令(供日志/展示)。幂等:重复调用无害。 +func (m *Manager) KeepAliveApply(ctx context.Context, serial, pkg string) ([]string, error) { + if err := m.Connect(ctx, serial); err != nil { + return nil, err + } + applied := make([]string, 0, 3) + + // 1. 电池优化白名单(deviceidle) + if _, err := m.adb(ctx, 10*time.Second, serial, "shell", "dumpsys", "deviceidle", "whitelist", "+"+pkg); err != nil { + return applied, fmt.Errorf("deviceidle 白名单 %s: %w", pkg, err) + } + applied = append(applied, "deviceidle-whitelist") + + // 2. 允许后台运行 + for _, op := range []string{"RUN_IN_BACKGROUND", "RUN_ANY_IN_BACKGROUND"} { + if _, err := m.adb(ctx, 10*time.Second, serial, "shell", "cmd", "appops", "set", pkg, op, "allow"); err != nil { + return applied, fmt.Errorf("appops %s %s: %w", pkg, op, err) + } + applied = append(applied, "appops-"+strings.ToLower(op)) + } + + logger.Info("adbmgr", "已应用保活命令", "serial", serial, "pkg", pkg, "applied", strings.Join(applied, ",")) + return applied, nil +} + +// KeepAliveDeviceFreezer 禁用设备级 Cached Apps Freezer(AOSP 缓存应用冻结器)。 +// Android 12+ 系统会在内存压力下冻结后台缓存进程(isFrozen=true),冻结进程热唤醒 +// 可能丢任务栈状态。实测设置后 use_freezer=false(生效)。设备级设置,幂等。 +func (m *Manager) KeepAliveDeviceFreezer(ctx context.Context, serial string) error { + if err := m.Connect(ctx, serial); err != nil { + return err + } + if _, err := m.adb(ctx, 10*time.Second, serial, "shell", "settings", "put", "global", "cached_apps_freezer", "disabled"); err != nil { + return fmt.Errorf("禁用 cached_apps_freezer: %w", err) + } + logger.Info("adbmgr", "已禁用设备缓存应用冻结器", "serial", serial) + return nil +} + +// KeepAliveStatus 查询某包当前的保活状态(供运维/前端展示,只读)。 +// 返回 map:deviceidle 白名单 / RUN_IN_BACKGROUND / RUN_ANY_IN_BACKGROUND 的 allow 状态。 +func (m *Manager) KeepAliveStatus(ctx context.Context, serial, pkg string) (map[string]bool, error) { + if err := m.Connect(ctx, serial); err != nil { + return nil, err + } + st := map[string]bool{} + + // deviceidle 白名单:dumpsys deviceidle whitelist 含 "user,," 行即已加入 + out, err := m.adb(ctx, 10*time.Second, serial, "shell", "dumpsys", "deviceidle", "whitelist") + if err == nil { + st["deviceidle"] = strings.Contains(out, "user,"+pkg+",") + } + + // appops 状态:RUN_IN_BACKGROUND / RUN_ANY_IN_BACKGROUND 行含 ": allow" + out, err = m.adb(ctx, 10*time.Second, serial, "shell", "cmd", "appops", "get", pkg) + if err == nil { + for _, op := range []string{"RUN_IN_BACKGROUND", "RUN_ANY_IN_BACKGROUND"} { + st[strings.ToLower(op)] = opLineAllow(out, op) + } + } + return st, nil +} + +// opLineAllow 在 appops get 输出中查找 ": allow" 行。 +func opLineAllow(out, op string) bool { + for _, line := range strings.Split(out, "\n") { + if strings.HasPrefix(line, op+":") { + return strings.Contains(line, "allow") + } + } + return false +} diff --git a/internal/adbmgr/keepalive_test.go b/internal/adbmgr/keepalive_test.go new file mode 100644 index 0000000..8d6c6bb --- /dev/null +++ b/internal/adbmgr/keepalive_test.go @@ -0,0 +1,75 @@ +package adbmgr + +import ( + "strings" + "testing" +) + +// TestOpLineAllow 验证 appops get 输出解析(KeepAliveStatus 内部逻辑)。 +func TestOpLineAllow(t *testing.T) { + cases := []struct { + name string + out string + op string + want bool + }{ + {"allow 命中", "RUN_IN_BACKGROUND: allow\nFOO: deny", "RUN_IN_BACKGROUND", true}, + {"deny 不命中", "RUN_IN_BACKGROUND: deny\n", "RUN_IN_BACKGROUND", false}, + {"op 不存在", "SOME_OTHER: allow\n", "RUN_IN_BACKGROUND", false}, + {"allow 带时间戳", "RUN_ANY_IN_BACKGROUND: allow; time=+2d22h47m34s ago", "RUN_ANY_IN_BACKGROUND", true}, + {"前缀不误匹配", "RUN_ANY_IN_BACKGROUND_EXTRA: allow\n", "RUN_ANY_IN_BACKGROUND", false}, + {"空输出", "", "RUN_IN_BACKGROUND", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := opLineAllow(tc.out, tc.op); got != tc.want { + t.Fatalf("opLineAllow(%q, %q) = %v, want %v", tc.out, tc.op, got, tc.want) + } + }) + } +} + +// TestKeepAliveStatus 用假设备输出验证 status 组装(不依赖真实 adb)。 +// 通过覆盖 adb 命令返回模拟输出——这里直接测解析层逻辑(与 adb 解耦)。 +func TestKeepAliveStatusParsing(t *testing.T) { + // 模拟 appops get 输出 + appopsOut := "RUN_IN_BACKGROUND: allow\nRUN_ANY_IN_BACKGROUND: allow; time=+2d ago\n" + devidleOut := "user,com.tencent.mm,10277\nuser,com.eg.android.AlipayGphone,10250\n" + + if !opLineAllow(appopsOut, "RUN_IN_BACKGROUND") { + t.Fatal("RUN_IN_BACKGROUND 应解析为 allow") + } + if !opLineAllow(appopsOut, "RUN_ANY_IN_BACKGROUND") { + t.Fatal("RUN_ANY_IN_BACKGROUND 应解析为 allow") + } + // deviceidle 输出含 "user,," 即已加入白名单 + if !containsPkg(devidleOut, "com.tencent.mm") { + t.Fatal("deviceidle 输出应含 com.tencent.mm") + } +} + +func containsPkg(out, pkg string) bool { + for _, line := range splitLines(out) { + if strings.HasPrefix(line, "user,"+pkg+",") { + return true + } + } + return false +} + +func splitLines(s string) []string { + var out []string + cur := "" + for _, r := range s { + if r == '\n' { + out = append(out, cur) + cur = "" + continue + } + cur += string(r) + } + if cur != "" { + out = append(out, cur) + } + return out +} diff --git a/internal/admin/accounts.go b/internal/admin/accounts.go new file mode 100755 index 0000000..a48e4c1 --- /dev/null +++ b/internal/admin/accounts.go @@ -0,0 +1,861 @@ +// Package admin 管理后台:账户池管理 API +// +// 安全设计: +// - 本文件是唯一访问 accounts.db 的组件 +// - 敏感字段(private_key / public_key / api_key)AES-256-GCM 加密后落库 +// - 解密后的账户数据推送到 AccountPool(内存),供其他模块使用 +// - API 响应不返回明文密钥(仅 "****" + 后4位) +// - 唯一例外:GET /accounts/:id/secret(需登录,供管理后台「查看/复制完整密钥」),返回解密后的明文 +package admin + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/account" + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/store" +) + +// AccountHandler 账户管理处理器 +type AccountHandler struct { + accountDB *store.AccountDB // 唯一访问 accounts.db 的组件 + pool *account.AccountPool + crypto *accountCrypto // 加密/解密工具 +} + +// accountCrypto 封装 AES 加解密(避免直接暴露 account 包的全局函数) +type accountCrypto struct { + init bool + encFn func([]byte) (string, error) + decFn func(string) ([]byte, error) + encJFn func(any) (string, error) + decJFn func(string, any) error +} + +func newAccountCrypto(masterKey string) (*accountCrypto, error) { + if err := account.InitCrypto(masterKey); err != nil { + return nil, fmt.Errorf("账户加密初始化失败: %w", err) + } + return &accountCrypto{ + init: true, + encFn: account.Encrypt, + decFn: account.Decrypt, + encJFn: account.EncryptJSON, + decJFn: account.DecryptJSON, + }, nil +} + +func (c *accountCrypto) encrypt(plaintext []byte) (string, error) { return c.encFn(plaintext) } +func (c *accountCrypto) decrypt(hex string) ([]byte, error) { return c.decFn(hex) } + +// NewAccountHandler 创建账户管理处理器 +func NewAccountHandler(db *store.AccountDB, pool *account.AccountPool, masterKey string) (*AccountHandler, error) { + crypto, err := newAccountCrypto(masterKey) + if err != nil { + return nil, err + } + return &AccountHandler{ + accountDB: db, + pool: pool, + crypto: crypto, + }, nil +} + +// InitFromDB 启动时从 accounts.db 加载所有账户,解密后初始化 AccountPool +func (h *AccountHandler) InitFromDB(ctx context.Context) error { + encrypted, err := h.accountDB.ListAccounts(ctx, "") + if err != nil { + return fmt.Errorf("加载账户列表失败: %w", err) + } + var decryptErrs []string + for _, e := range encrypted { + acct, err := h.decryptAccount(e) + if err != nil { + logger.Error("account", "解密账户失败(主密钥不一致或数据损坏)", + "id", e.ID, "name", e.Name, "err", err) + decryptErrs = append(decryptErrs, fmt.Sprintf("id=%d name=%s: %v", e.ID, e.Name, err)) + continue + } + h.pool.Add(acct) + } + // P1 守卫:DB 非空但所有/部分记录解密失败 = 主密钥变更或数据损坏,禁止静默启动(否则网关收款链路全断) + if len(decryptErrs) > 0 { + return fmt.Errorf("accounts.db 解密失败 %d/%d 条(疑似 ACCOUNT_MASTER_KEY 变更),已拒绝启动;请恢复 .env 中的 ACCOUNT_MASTER_KEY 或清空 accounts.db 后重新建号。失败明细:\n%s", + len(decryptErrs), len(encrypted), strings.Join(decryptErrs, "\n")) + } + logger.Info("account", "账户池初始化完成", "count", h.pool.Count()) + return nil +} + +// RegisterRoutes 注册账户管理路由(需在 authMiddleware 组内) +func (h *AccountHandler) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/accounts", h.handleListAccounts) + rg.GET("/accounts/:id", h.handleGetAccount) + rg.POST("/accounts", h.handleCreateAccount) + rg.PUT("/accounts/:id", h.handleUpdateAccount) + rg.DELETE("/accounts/:id", h.handleDeleteAccount) + rg.POST("/accounts/:id/freeze", h.handleFreezeAccount) + rg.POST("/accounts/:id/unfreeze", h.handleUnfreezeAccount) + rg.POST("/accounts/:id/enable", h.handleEnableAccount) + rg.POST("/accounts/:id/disable", h.handleDisableAccount) + rg.GET("/accounts/:id/logs", h.handleAccountLogs) + rg.GET("/accounts/:id/secret", h.handleAccountSecret) + // 收款码图片解析(docs/MIANQIAN.md §7.6 阶段一:上传收款码 → 服务端解析 → 绑定账户) + rg.POST("/accounts/decode-qrcode", h.handleDecodeQRCode) +} + +// ========== API Handlers ========== + +// AccountResponse 前端展示的账户信息(密钥脱敏) +type AccountResponse struct { + ID int64 `json:"id"` + Channel string `json:"channel"` + Name string `json:"name"` + AppID string `json:"app_id"` + MchID string `json:"mch_id,omitempty"` + PrivateKey string `json:"private_key"` // 脱敏显示 + PublicKey string `json:"public_key,omitempty"` // 脱敏显示 + ApiKey string `json:"api_key,omitempty"` // 脱敏显示 + QRContent string `json:"qr_content,omitempty"` // 收款码内容(脱敏,个人通道) + Weight int `json:"weight"` + Priority int `json:"priority"` + Status string `json:"status"` + FrozenReason string `json:"frozen_reason,omitempty"` // 冻结原因(offline/risk/manual/retire;前端展示用) + AutoBan bool `json:"auto_ban"` + DailyLimit int64 `json:"daily_limit"` + DailyUsed int64 `json:"daily_used"` // 今日已用(0 必须返回,omitempty 会导致前端 NaN) + FailCount int `json:"fail_count"` // 连续失败次数(同上,0 必须返回) + Group string `json:"group"` + IsProd bool `json:"is_prod"` + Remark string `json:"remark,omitempty"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// maskKey 脱敏:只显示后 4 位;空值返回空串(前端据此显示「未配置」) +func maskKey(key string) string { + if key == "" { + return "" + } + if len(key) <= 4 { + return "****" + } + return "****" + key[len(key)-4:] +} + +// maskQR 收款码内容脱敏:只显示后 8 位(含 https:// 前缀时截断显示);空值返回空串 +func maskQR(qr string) string { + if qr == "" { + return "" + } + if len(qr) <= 8 { + return "****" + } + return "****" + qr[len(qr)-8:] +} + +// acctToResponse 将 Account 转为前端响应(脱敏密钥) +func acctToResponse(acct *account.Account) *AccountResponse { + return &AccountResponse{ + ID: acct.ID, + Channel: acct.Channel, + Name: acct.Name, + AppID: acct.AppID, + MchID: acct.MchID, + PrivateKey: maskKey(acct.PrivateKey), + PublicKey: maskKey(acct.PublicKey), + ApiKey: maskKey(acct.ApiKey), + QRContent: maskQR(acct.QRContent), + Weight: acct.Weight, + Priority: acct.Priority, + Status: string(acct.Status), + FrozenReason: acct.FrozenReason, + AutoBan: acct.AutoBan, + DailyLimit: acct.DailyLimit, + DailyUsed: acct.DailyUsed, + FailCount: acct.FailCount, + Group: acct.Group, + IsProd: acct.IsProd, + Remark: acct.Remark, + CreatedAt: acct.CreatedAt.Format(time.RFC3339), + UpdatedAt: acct.UpdatedAt.Format(time.RFC3339), + } +} + +// encryptAccount 加密账户的敏感字段,返回 AccountEncrypted(用于落库) +func (h *AccountHandler) EncryptAccount(acct *account.Account) (*account.AccountEncrypted, error) { + e := &account.AccountEncrypted{ + Channel: acct.Channel, + Name: acct.Name, + AppID: acct.AppID, + MchID: acct.MchID, + Weight: acct.Weight, + Priority: acct.Priority, + Status: string(acct.Status), + FrozenReason: acct.FrozenReason, + AutoBan: acct.AutoBan, + DailyLimit: acct.DailyLimit, + DailyUsed: acct.DailyUsed, + Group: acct.Group, + IsProd: acct.IsProd, + Remark: acct.Remark, + CreatedAt: acct.CreatedAt, + UpdatedAt: acct.UpdatedAt, + } + var err error + if acct.PrivateKey != "" { + e.PrivateKeyEnc, err = h.crypto.encrypt([]byte(acct.PrivateKey)) + if err != nil { + return nil, fmt.Errorf("加密应用私钥失败: %w", err) + } + } + if acct.PublicKey != "" { + e.PublicKeyEnc, err = h.crypto.encrypt([]byte(acct.PublicKey)) + if err != nil { + return nil, fmt.Errorf("加密公钥失败: %w", err) + } + } + if acct.ApiKey != "" { + e.ApiKeyEnc, err = h.crypto.encrypt([]byte(acct.ApiKey)) + if err != nil { + return nil, fmt.Errorf("加密 API 密钥失败: %w", err) + } + } + if acct.QRContent != "" { + e.QRContentEnc, err = h.crypto.encrypt([]byte(acct.QRContent)) + if err != nil { + return nil, fmt.Errorf("加密收款码内容失败: %w", err) + } + } + return e, nil +} + +// decryptAccount 解密账户的敏感字段,返回 Account(用于内存) +func (h *AccountHandler) decryptAccount(e *account.AccountEncrypted) (*account.Account, error) { + acct := &account.Account{ + ID: e.ID, + Channel: e.Channel, + Name: e.Name, + AppID: e.AppID, + MchID: e.MchID, + Weight: e.Weight, + Priority: e.Priority, + Status: account.AccountStatus(e.Status), + FrozenReason: e.FrozenReason, + AutoBan: e.AutoBan, + DailyLimit: e.DailyLimit, + DailyUsed: e.DailyUsed, + Group: e.Group, + IsProd: e.IsProd, + Remark: e.Remark, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } + if e.PrivateKeyEnc != "" { + data, err := h.crypto.decrypt(e.PrivateKeyEnc) + if err != nil { + return nil, fmt.Errorf("解密应用私钥失败: %w", err) + } + acct.PrivateKey = string(data) + } + if e.PublicKeyEnc != "" { + data, err := h.crypto.decrypt(e.PublicKeyEnc) + if err != nil { + return nil, fmt.Errorf("解密公钥失败: %w", err) + } + acct.PublicKey = string(data) + } + if e.ApiKeyEnc != "" { + data, err := h.crypto.decrypt(e.ApiKeyEnc) + if err != nil { + return nil, fmt.Errorf("解密 API 密钥失败: %w", err) + } + acct.ApiKey = string(data) + } + if e.QRContentEnc != "" { + data, err := h.crypto.decrypt(e.QRContentEnc) + if err != nil { + return nil, fmt.Errorf("解密收款码内容失败: %w", err) + } + acct.QRContent = string(data) + } + return acct, nil +} + +// ========== List ========== + +func (h *AccountHandler) handleListAccounts(c *gin.Context) { + channel := c.Query("channel") + + // 从内存池取(已包含运行状态:daily_used, fail_count, frozen_until) + poolAccts := h.pool.ListAll() + if channel != "" { + poolAccts = h.pool.ListByChannel(channel) + } + + resp := make([]*AccountResponse, 0, len(poolAccts)) + for _, acct := range poolAccts { + resp = append(resp, acctToResponse(acct)) + } + + c.JSON(http.StatusOK, gin.H{"data": resp}) +} + +// ========== Get ========== + +func (h *AccountHandler) handleGetAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + + // 先从内存池取 + acct := h.pool.GetByID(id) + if acct == nil { + // 再查数据库(可能被删了但池没更新) + enc, err := h.accountDB.GetAccount(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if enc == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "账户不存在"}) + return + } + acct, err = h.decryptAccount(enc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "解密账户失败"}) + return + } + } + + c.JSON(http.StatusOK, gin.H{"data": acctToResponse(acct)}) +} + +// handleAccountSecret 返回账户完整敏感字段(明文,需登录)。 +// 用途:管理后台「查看/复制完整密钥」,用于确认对接配置是否准确、复制真实可用的密钥给外部系统。 +// 安全提示:仅登录用户可调用;前端展示时应二次确认并提示勿泄露。 +func (h *AccountHandler) handleAccountSecret(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + + // 先从内存池取(明文),未命中再回退 DB 解密 + acct := h.pool.GetByID(id) + if acct == nil { + enc, err := h.accountDB.GetAccount(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if enc == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "账户不存在"}) + return + } + acct, err = h.decryptAccount(enc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "解密账户失败"}) + return + } + } + + logger.Info("account", "账户密钥被查看(明文)", "id", id, "name", acct.Name, "channel", acct.Channel, "operator", c.GetString("username")) + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "id": acct.ID, + "name": acct.Name, + "channel": acct.Channel, + "app_id": acct.AppID, + "mch_id": acct.MchID, + "private_key": acct.PrivateKey, + "public_key": acct.PublicKey, + "api_key": acct.ApiKey, + "qr_content": acct.QRContent, + }}) +} + +// ========== Create ========== + +// 账户通道白名单(前后端差异化:商家=密钥,个人=收款码) +var validAccountChannels = map[string]bool{ + "alipay": true, // 商家·支付宝(gopay 官方 API) + "wechat": true, // 商家·微信(gopay 官方 API) + "qq": true, // 商家·QQ(预留) + "paypal": true, // 商家·PayPal(预留) + "personal.alipay": true, // 个人·支付宝(收款码监听) + "personal.wechat": true, // 个人·微信(收款码监听) + "personal.qq": true, // 个人·QQ(预留) +} + +// isPersonalChannel 是否个人免签通道(收款码监听类) +func isPersonalChannel(ch string) bool { + return strings.HasPrefix(ch, "personal.") +} + +// validateAccountReq 按通道类型校验账户字段(docs/MIANQIAN.md 通道大类差异化) +// - 个人通道(personal.*):必须配置收款码内容 qr_content(无官方 API 密钥) +// - 商家通道(merchant.*):必须配置 app_id(官方 API 必需),密钥二选一 +func validateAccountReq(req createAccountReq) error { + if !validAccountChannels[req.Channel] { + return fmt.Errorf("不支持的通道类型: %s(可选:alipay/wechat/qq/paypal/personal.alipay/personal.wechat/personal.qq)", req.Channel) + } + if isPersonalChannel(req.Channel) { + // 个人免签:收款码是核心凭据,必须配置 + if strings.TrimSpace(req.QRContent) == "" { + return fmt.Errorf("个人免签通道必须配置收款码内容(qr_content),可从收款码图片上传自动解析") + } + // 个人通道不应携带官方 API 密钥(避免混淆) + if req.PrivateKey != "" || req.ApiKey != "" || req.AppID != "" { + return fmt.Errorf("个人免签通道无需配置官方密钥/AppID,请仅填写收款码内容") + } + } else { + // 商家通道:官方 API 必须配置 AppID + if strings.TrimSpace(req.AppID) == "" { + return fmt.Errorf("商家通道必须配置 AppID(官方 API 凭据)") + } + if strings.TrimSpace(req.PrivateKey) == "" && strings.TrimSpace(req.ApiKey) == "" { + return fmt.Errorf("商家通道必须配置应用私钥或 API 密钥(至少一项)") + } + } + return nil +} + +// validateAccountReqUpdate 账户编辑校验:与 validateAccountReq 一致,但个人通道允许 +// qr_content 为空——空表示「沿用已有收款码」(编辑场景,资金安全由调用方 handleUpdateAccount +// 的复用逻辑保证)。若编辑时填写了新的收款码,则必须通过 InvalidQRContent 校验(脏数据防线)。 +func validateAccountReqUpdate(req createAccountReq) error { + if !validAccountChannels[req.Channel] { + return fmt.Errorf("不支持的通道类型: %s(可选:alipay/wechat/qq/paypal/personal.alipay/personal.wechat/personal.qq)", req.Channel) + } + if isPersonalChannel(req.Channel) { + // 编辑场景:收款码可为空(沿用已有)。 + if strings.TrimSpace(req.QRContent) != "" { + if reason := account.InvalidQRContent(req.Channel, strings.TrimSpace(req.QRContent)); reason != "" { + return fmt.Errorf("收款码内容非法(%s),请重新上传解析", reason) + } + } + // 个人通道不应携带官方 API 密钥(避免混淆;更新时也保持该约束) + if req.PrivateKey != "" || req.ApiKey != "" || req.AppID != "" { + return fmt.Errorf("个人免签通道无需配置官方密钥/AppID,请仅填写收款码内容") + } + } else { + // 商家通道:官方 API 必须配置 AppID + if strings.TrimSpace(req.AppID) == "" { + return fmt.Errorf("商家通道必须配置 AppID(官方 API 凭据)") + } + if strings.TrimSpace(req.PrivateKey) == "" && strings.TrimSpace(req.ApiKey) == "" { + return fmt.Errorf("商家通道必须配置应用私钥或 API 密钥(至少一项)") + } + } + return nil +} + +type createAccountReq struct { + Channel string `json:"channel" binding:"required"` + Name string `json:"name" binding:"required"` + AppID string `json:"app_id"` + MchID string `json:"mch_id"` + PrivateKey string `json:"private_key"` + PublicKey string `json:"public_key"` + ApiKey string `json:"api_key"` + QRContent string `json:"qr_content"` + Weight int `json:"weight"` + Priority int `json:"priority"` + AutoBan bool `json:"auto_ban"` + DailyLimit int64 `json:"daily_limit"` + Group string `json:"group"` + IsProd bool `json:"is_prod"` + Remark string `json:"remark"` +} + +func (h *AccountHandler) handleCreateAccount(c *gin.Context) { + var req createAccountReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + // 按通道类型校验(商家=官方密钥 / 个人=收款码) + if err := validateAccountReq(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if req.Weight <= 0 { + req.Weight = 50 + } + if req.Group == "" { + req.Group = "default" + } + + now := time.Now() + acct := &account.Account{ + Channel: req.Channel, + Name: req.Name, + AppID: req.AppID, + MchID: req.MchID, + PrivateKey: req.PrivateKey, + PublicKey: req.PublicKey, + ApiKey: req.ApiKey, + QRContent: req.QRContent, + Weight: req.Weight, + Priority: req.Priority, + Status: account.StatusNormal, + AutoBan: req.AutoBan, + DailyLimit: req.DailyLimit, + Group: req.Group, + IsProd: req.IsProd, + Remark: req.Remark, + CreatedAt: now, + UpdatedAt: now, + } + + // 加密敏感字段 + enc, err := h.EncryptAccount(acct) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 写入 accounts.db(仅 Admin API 可访问) + id, err := h.accountDB.CreateAccount(c.Request.Context(), enc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建账户失败: " + err.Error()}) + return + } + acct.ID = id + + // 推送到内存池(其他模块立即可用) + h.pool.Add(acct) + + // 审计日志 + _ = h.accountDB.InsertAccountLog(c.Request.Context(), &account.AccountLog{ + AccountID: id, + Action: "create", + Detail: fmt.Sprintf("创建%s账户: %s", req.Channel, req.Name), + Operator: c.GetString("username"), + }) + + logger.Info("account", "账户创建成功", "id", id, "name", req.Name, "channel", req.Channel) + c.JSON(http.StatusOK, gin.H{"data": acctToResponse(acct)}) +} + +// ========== Update ========== + +type updateAccountReq struct { + Channel string `json:"channel" binding:"required"` + Name string `json:"name" binding:"required"` + AppID string `json:"app_id"` + MchID string `json:"mch_id"` + PrivateKey string `json:"private_key"` + PublicKey string `json:"public_key"` + ApiKey string `json:"api_key"` + QRContent string `json:"qr_content"` + Weight int `json:"weight"` + Priority int `json:"priority"` + AutoBan bool `json:"auto_ban"` + DailyLimit int64 `json:"daily_limit"` + Group string `json:"group"` + IsProd bool `json:"is_prod"` + Remark string `json:"remark"` +} + +func (h *AccountHandler) handleUpdateAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + + var req updateAccountReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + // 按通道类型校验(编辑场景:个人通道允许 qr_content 为空 → 沿用已有;新填须合法) + if err := validateAccountReqUpdate(createAccountReq(req)); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if req.Group == "" { + req.Group = "default" + } + + now := time.Now() + acct := &account.Account{ + ID: id, + Channel: req.Channel, + Name: req.Name, + AppID: req.AppID, + MchID: req.MchID, + PrivateKey: req.PrivateKey, + PublicKey: req.PublicKey, + ApiKey: req.ApiKey, + QRContent: req.QRContent, + Weight: req.Weight, + Priority: req.Priority, + AutoBan: req.AutoBan, + DailyLimit: req.DailyLimit, + Group: req.Group, + IsProd: req.IsProd, + Remark: req.Remark, + UpdatedAt: now, + } + + // 保留原有状态(先从内存池读,池未命中再回退 DB;二者均无则账户不存在) + existing := h.pool.GetByID(id) + if existing == nil { + // 池未命中:从 DB 拉取并解密,避免静默用零值覆盖 CreatedAt/状态/密钥 + enc, err := h.accountDB.GetAccount(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询账户失败: " + err.Error()}) + return + } + if enc == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "账户不存在"}) + return + } + existing, err = h.decryptAccount(enc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "解密账户失败"}) + return + } + } + acct.Status = existing.Status + acct.DailyUsed = existing.DailyUsed + acct.FailCount = existing.FailCount + acct.FrozenUntil = existing.FrozenUntil + acct.CreatedAt = existing.CreatedAt + // 密钥字段前端不传(空)时保留原值;qr_content 额外跳过脱敏占位("****" 前缀 = 回显值,非真实内容) + if req.PrivateKey == "" { + acct.PrivateKey = existing.PrivateKey + } + if req.PublicKey == "" { + acct.PublicKey = existing.PublicKey + } + if req.ApiKey == "" { + acct.ApiKey = existing.ApiKey + } + if req.QRContent == "" || strings.HasPrefix(req.QRContent, "****") { + acct.QRContent = existing.QRContent + } + + // 加密敏感字段 + enc, err := h.EncryptAccount(acct) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + enc.ID = id + + // 更新 accounts.db + if err := h.accountDB.UpdateAccount(c.Request.Context(), enc); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新账户失败: " + err.Error()}) + return + } + + // 更新内存池 + h.pool.Update(acct) + + // 清除对应商家通道的 gopay client 缓存(app_id/密钥变更后旧缓存会用旧参数下单) + invalidateChannelClient(acct.Channel, id) + + // 审计日志 + _ = h.accountDB.InsertAccountLog(c.Request.Context(), &account.AccountLog{ + AccountID: id, + Action: "update", + Detail: fmt.Sprintf("更新%s账户: %s", req.Channel, req.Name), + Operator: c.GetString("username"), + }) + + logger.Info("account", "账户更新成功", "id", id) + c.JSON(http.StatusOK, gin.H{"data": acctToResponse(acct)}) +} + +// invalidateChannelClient 清除账户对应商家通道的 gopay client 缓存。 +// 账户 app_id / 密钥变更后,缓存的旧 client 仍用旧参数下单(旧 app_id 会导致支付宝 +// 返回 invalid-app-id),必须强制重建。个人免签通道无 gopay client,无需处理。 +func invalidateChannelClient(accountChannel string, accountID int64) { + name := "merchant." + strings.TrimPrefix(accountChannel, "merchant.") + if strings.HasPrefix(accountChannel, "personal.") { + return + } + ch, ok := channel.Lookup(name) + if !ok { + return + } + if inv, ok := ch.(channel.ClientCacheInvalidator); ok { + inv.RemoveClient(accountID) + } +} + +// ========== Delete ========== + +func (h *AccountHandler) handleDeleteAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + + ctx := c.Request.Context() + + // 删除前先取账户(用于删除后清除通道缓存) + var delChannel string + if enc, err := h.accountDB.GetAccount(ctx, id); err == nil && enc != nil { + delChannel = enc.Channel + } + + // 删除数据库 + if err := h.accountDB.DeleteAccount(ctx, id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除账户失败: " + err.Error()}) + return + } + + // 移除内存池 + h.pool.Remove(id) + + // 清除商家通道的 client 缓存,避免残留 + if delChannel != "" { + invalidateChannelClient(delChannel, id) + } + + // 审计日志 + _ = h.accountDB.InsertAccountLog(ctx, &account.AccountLog{ + AccountID: id, + Action: "delete", + Detail: fmt.Sprintf("删除账户 ID: %d", id), + Operator: c.GetString("username"), + }) + + logger.Info("account", "账户已删除", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// ========== Freeze / Unfreeze / Enable / Disable ========== + +func (h *AccountHandler) handleFreezeAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + ctx := c.Request.Context() + + if err := h.accountDB.SetAccountFrozen(ctx, id, account.FrozenReasonManual); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + h.pool.FreezeManual(id) + + _ = h.accountDB.InsertAccountLog(ctx, &account.AccountLog{ + AccountID: id, Action: "freeze", Detail: "手动冻结", Operator: c.GetString("username"), + }) + logger.Info("account", "账户已冻结", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +func (h *AccountHandler) handleUnfreezeAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + ctx := c.Request.Context() + + // 人工解冻:任意冻结原因都可解冻(SetAccountStatus 同时清空 frozen_reason) + if err := h.accountDB.SetAccountStatus(ctx, id, string(account.StatusNormal)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + h.pool.SetStatus(id, account.StatusNormal) + + _ = h.accountDB.InsertAccountLog(ctx, &account.AccountLog{ + AccountID: id, Action: "unfreeze", Detail: "手动解冻", Operator: c.GetString("username"), + }) + logger.Info("account", "账户已解冻", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +func (h *AccountHandler) handleEnableAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + ctx := c.Request.Context() + + if err := h.accountDB.SetAccountStatus(ctx, id, string(account.StatusNormal)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + h.pool.SetStatus(id, account.StatusNormal) + + _ = h.accountDB.InsertAccountLog(ctx, &account.AccountLog{ + AccountID: id, Action: "enable", Detail: "手动启用", Operator: c.GetString("username"), + }) + logger.Info("account", "账户已启用", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +func (h *AccountHandler) handleDisableAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + ctx := c.Request.Context() + + if err := h.accountDB.SetAccountStatus(ctx, id, string(account.StatusDisabled)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + h.pool.SetStatus(id, account.StatusDisabled) + + _ = h.accountDB.InsertAccountLog(ctx, &account.AccountLog{ + AccountID: id, Action: "disable", Detail: "手动禁用", Operator: c.GetString("username"), + }) + logger.Info("account", "账户已禁用", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// ========== Account Logs ========== + +// AccountName 返回账户池中指定账户的名称(订单列表展示用;查不到返回空) +func (h *AccountHandler) AccountName(id int64) string { + if h == nil || id <= 0 { + return "" + } + acct := h.pool.GetByID(id) + if acct == nil { + return "" + } + return acct.Name +} + +func (h *AccountHandler) handleAccountLogs(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效账户ID"}) + return + } + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "30")) + if limit <= 0 || limit > 100 { + limit = 30 + } + + logs, err := h.accountDB.ListAccountLogs(c.Request.Context(), id, limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": logs}) +} diff --git a/internal/admin/admin.go b/internal/admin/admin.go new file mode 100755 index 0000000..ca5d9dc --- /dev/null +++ b/internal/admin/admin.go @@ -0,0 +1,1213 @@ +// Package admin 管理后台:静态页面 + JSON API +// +// 前端为 Vue3 + Element Plus 构建产物,embed 进二进制(internal/admin/static)。 +// 部署时只需一个 gateway 二进制,无需单独部署前端。 +package admin + +import ( + "context" + "crypto/rand" + "embed" + "encoding/hex" + "fmt" + "io/fs" + "mime" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/scrcpy" + "paygateway/internal/store" +) + +// mimeTypeByPath 根据文件扩展名返回 Content-Type +func mimeTypeByPath(p string) string { + switch ext := filepath.Ext(p); ext { + case ".js": + return "text/javascript; charset=utf-8" + case ".css": + return "text/css; charset=utf-8" + case ".html": + return "text/html; charset=utf-8" + case ".json": + return "application/json; charset=utf-8" + case ".svg": + return "image/svg+xml" + case ".png": + return "image/png" + case ".jpg", ".jpeg": + return "image/jpeg" + case ".gif": + return "image/gif" + case ".webp": + return "image/webp" + case ".woff", ".woff2": + return "font/woff2" + case ".ttf": + return "font/ttf" + case ".ico": + return "image/x-icon" + default: + if mt := mime.TypeByExtension(ext); mt != "" { + return mt + } + return "application/octet-stream" + } +} + +//go:embed all:static +var staticFS embed.FS + +// TunnelStatus 隧道状态提供者(由 main 注入,避免循环依赖) +type TunnelStatus struct { + URL string `json:"url"` // 隧道地址(空 = 未启动) + Callback string `json:"callback"` // 完整回调地址 + Healthy bool `json:"healthy"` // 健康状态 + Running bool `json:"running"` // 是否已启动 + Error string `json:"error,omitempty"` + UpdatedAt string `json:"updated_at"` +} + +// EpayInfo 易支付对接信息(其他项目配置 paygateway 时使用) +type EpayInfo struct { + BaseURL string `json:"base_url"` // 服务基础地址 + SubmitURL string `json:"submit_url"` // 下单接口(New API 的"易支付网关地址"填这个) + QueryURL string `json:"query_url"` // 订单查询接口 + Pid string `json:"pid"` // 商户ID + Key string `json:"key"` // 商户密钥 + SignType string `json:"sign_type"` // 签名方式 +} + +// ChannelInfo 支付通道信息(由 main 组装,admin 不依赖具体通道包) +type ChannelInfo struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Kind string `json:"kind"` // callback / listener + Modes []string `json:"modes"` // 支持模式 + ModesEnabled map[string]bool `json:"modes_enabled,omitempty"` // 各模式启停状态 + AppID string `json:"app_id,omitempty"` + MchID string `json:"mch_id,omitempty"` + IsProd bool `json:"is_prod"` + NotifyURL string `json:"notify_url,omitempty"` + // 账户配置状态(引导用):是否已配置账户 + 账户数量 + Configured bool `json:"configured"` + AccountCount int `json:"account_count"` + // 统计(由 admin 填充) + TodayOrders int64 `json:"today_orders"` + TodayAmount int64 `json:"today_amount"` + TotalOrders int64 `json:"total_orders"` + TotalAmount int64 `json:"total_amount"` +} + +// LogStore 独立日志库接口(查询管理界面日志用,与订单主库分离) +type LogStore interface { + QueryLogs(ctx context.Context, level, scope string, limit, offset int) ([]*store.LogRecord, error) + CountLogs(ctx context.Context, level, scope string) (int64, error) +} + +// Server 管理后台服务 +type Server struct { + st store.Store + logStore LogStore // 独立日志数据库(长期保存,查询日志用) + ledgerStore store.LedgerStore // 独立账本数据库 + callbackPath string // /alipay/notify + tunnelURL func() string // 获取隧道 URL + tunnelHealth func() bool // 隧道健康 + tunnelErr func() string // 隧道错误信息(可选) + channels func() []ChannelInfo // 通道信息提供者 + epayInfo func() *EpayInfo // 易支付对接信息提供者 + onSettingsChange func(key, value string) // 设置变更回调(通知 main 更新通道等) + onLedgerVerify func() *VerifyLedgerResult // 账本校验回调 + onLedgerSnapshot func() error // 创建快照回调 + onLedgerPublicKey func() string // 账本公钥回调 + + // 账户池管理(唯一访问 accounts.db 的组件) + accountHandler *AccountHandler + // 上游管理(唯一访问 upstreams.db 的组件) + upstreamHandler *UpstreamHandler + reconcilerHandler *ReconcilerHandler + // 个人免签通道管理(设备/推送流水/无订单收款) + personalHandler *PersonalHandler + // 实时设备流服务(云手机式操作;nil = 未启用) + streamSrv *scrcpy.StreamServer + // 账本页服务端监听状态提供者(main.go 注入;nil = 未启用)。返回 JSON 序列化数据。 + ledgerStatus func() any + // ledgerWarmup 设备手动预热函数(main.go 注入;nil = 未启用)。DeviceActor 模型中 + // 手动预热是队列中的一个命令,与自动预热/轮询天然互斥,无需窗口独占。 + ledgerWarmup func(ctx context.Context, deviceID string, providers map[string]bool) error + + // 鉴权:凭据来源 DB(settings);无凭据时 adminSetupNeeded=true 引导注册 + adminUser string + adminPass []byte // bcrypt 哈希,禁止明文比对 + adminSetupNeeded bool // 首次启动,尚无管理员,需引导注册 + setupMu sync.Mutex // 保护注册的并发竞态(防多个请求同时通过检查) + jwtSecret []byte + + startTime time.Time + + loginLimiter *loginLimiter +} + +// VerifyLedgerResult 账本校验结果(admin 展示用) +type VerifyLedgerResult struct { + Valid bool `json:"valid"` + TotalEntries int64 `json:"total_entries"` + Errors []string `json:"errors,omitempty"` + SnapshotOK bool `json:"snapshot_ok"` + Duration string `json:"duration"` + CheckedAt string `json:"checked_at"` +} + +// tokenExpiry token 过期时间 +const tokenExpiry = 24 * time.Hour + +// New 创建管理后台服务。 +// 管理员凭据完全由数据库驱动(settings 表 admin_username / admin_password_hash,bcrypt)。 +// 无凭据时 adminSetupNeeded=true,前端引导首次注册;.env 不再承载管理员凭据。 +func New(st store.Store, ledgerStore store.LedgerStore, callbackPath string, tunURL func() string, tunHealth func() bool, channels func() []ChannelInfo, epayInfo func() *EpayInfo, onSettingsChange func(key, value string), onLedgerVerify func() *VerifyLedgerResult, onLedgerSnapshot func() error, onLedgerPublicKey func() string, accountHandler *AccountHandler, upstreamHandler *UpstreamHandler, reconcilerHandler *ReconcilerHandler, personalHandler *PersonalHandler, jwtSecret string) *Server { + secret := jwtSecret + if secret == "" { + // 安全:未配置 JWT_SECRET 时生成随机密钥(禁止可预测的日期密钥,防止伪造 token) + // 注意:随机密钥仅内存持有,服务重启后已发 token 失效(重新登录即可) + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + // crypto/rand 失败几乎不可能;兜底用时间戳+pid 混合,仍不可预测 + buf = []byte(fmt.Sprintf("pgw-%d-%d", time.Now().UnixNano(), os.Getpid())) + } + secret = hex.EncodeToString(buf) + logger.Warn("admin", "JWT_SECRET 未配置,已生成随机密钥(重启后需重新登录)") + } + s := &Server{ + st: st, + ledgerStore: ledgerStore, + callbackPath: callbackPath, + tunnelURL: tunURL, + tunnelHealth: tunHealth, + channels: channels, + epayInfo: epayInfo, + onSettingsChange: onSettingsChange, + onLedgerVerify: onLedgerVerify, + onLedgerSnapshot: onLedgerSnapshot, + onLedgerPublicKey: onLedgerPublicKey, + accountHandler: accountHandler, + upstreamHandler: upstreamHandler, + reconcilerHandler: reconcilerHandler, + personalHandler: personalHandler, + jwtSecret: []byte(secret), + startTime: time.Now(), + loginLimiter: newLoginLimiter(), + } + s.loadAdminFromDB() + return s +} + +// SetLogStore 绑定独立日志数据库(New 之后调用;日志查询/统计均走该库) +func (s *Server) SetLogStore(ls LogStore) { s.logStore = ls } + +// loadAdminFromDB 从 settings 表加载管理员凭据(admin_username / admin_password_hash,bcrypt) +func (s *Server) loadAdminFromDB() { + ctx := context.Background() + user, errU := s.st.GetSetting(ctx, "admin_username") + pass, errP := s.st.GetSetting(ctx, "admin_password_hash") + if errU == nil && errP == nil && user != "" && strings.HasPrefix(pass, "$2") { + s.adminUser = user + s.adminPass = []byte(pass) + logger.Info("admin", "管理员凭据已从数据库加载", "username", user) + return + } + // 无管理员凭据 → 首次启动,引导注册 + s.adminSetupNeeded = true + logger.Info("admin", "首次启动:无管理员凭据,等待注册(POST /api/auth/register)") +} + +// SetStreamServer 注入实时设备流服务(web 控制台云手机式操作) +// 不通过 New 注入以保持签名稳定;未设置时设备控制台回退到截图模式 +func (s *Server) SetStreamServer(ss *scrcpy.StreamServer) { + s.streamSrv = ss +} + +// SetLedgerStatus 注入账本页监听状态提供者(docs/PLAN-ADB-LEDGER-IMPL.md §7 只读接口) +func (s *Server) SetLedgerStatus(fn func() any) { s.ledgerStatus = fn } + +// SetLedgerWarmup 注入设备手动预热函数(DeviceActor 模型:手动预热是队列中的一个命令,天然互斥)。 +// fn 接收 (ctx, deviceID, providers),provider 指定通道({"alipay":true,"wechat":true},空=全部)。 +func (s *Server) SetLedgerWarmup(fn func(ctx context.Context, deviceID string, providers map[string]bool) error) { + s.ledgerWarmup = fn +} + +// Run 启动管理后台(阻塞) +func (s *Server) Run(addr string) { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.Use(gin.Recovery()) + + // ---- 登录(无需鉴权)---- + r.POST("/api/auth/login", s.handleLogin) + // ---- 首次启动注册(无需鉴权;已配置管理员后自动拒绝)---- + r.POST("/api/auth/register", s.handleRegister) + r.GET("/api/auth/setup-status", s.handleSetupStatus) + + // ---- 健康检查(无需鉴权,供 LB/监控/heartbeat 用)---- + // 返回 200 = 服务进程活着 + 业务 DB 可读 + 账本表可读;隧道状态作为子项返回不影响 200。 + // 失败的子项以 503 + JSON 暴露给运维 / 监控告警。 + r.GET("/api/health", s.handleHealth) + + // ---- JSON API(需鉴权)---- + api := r.Group("/api", s.authMiddleware()) + { + api.GET("/dashboard", s.handleDashboard) + api.GET("/epay-info", s.handleEpayInfo) + api.GET("/channels", s.handleChannels) + api.GET("/orders", s.handleListOrders) + api.GET("/orders/:id/events", s.handleOrderEvents) + api.GET("/notify-tasks", s.handleNotifyTasks) + api.GET("/notify-stats", s.handleNotifyStats) + api.GET("/logs", s.handleLogs) + api.GET("/ledger", s.handleLedger) + api.POST("/ledger/verify", s.handleLedgerVerify) + api.GET("/ledger/snapshots", s.handleLedgerSnapshots) + api.POST("/ledger/snapshot", s.handleCreateSnapshot) + api.GET("/ledger/public-key", s.handleLedgerPublicKey) + api.GET("/ledger/export", s.handleLedgerExport) + api.GET("/tunnel", s.handleTunnel) + api.GET("/settings", s.handleSettings) + api.POST("/settings", s.handleUpdateSettings) + api.POST("/auth/reset", s.handleResetAdmin) + // 安全密钥查看(确认后返回完整值,含账本私钥/QR token 等) + api.GET("/settings/keys", s.handleSettingsKeys) + api.POST("/settings/channel/:name/toggle", s.handleToggleChannel) + api.POST("/settings/channel/:name/mode/:mode/toggle", s.handleToggleChannelMode) + + // 账户池管理路由(仅 Admin API 可访问 accounts.db) + if s.accountHandler != nil { + s.accountHandler.RegisterRoutes(api) + } + // 上游管理路由(仅 Admin API 可访问 upstreams.db) + if s.upstreamHandler != nil { + s.upstreamHandler.RegisterRoutes(api) + } + if s.reconcilerHandler != nil { + s.reconcilerHandler.RegisterRoutes(api) + } + // 个人免签通道管理路由(设备/推送流水/无订单收款) + if s.personalHandler != nil { + if s.ledgerWarmup != nil { + s.personalHandler.SetLedgerWarmup(s.ledgerWarmup) + } + s.personalHandler.RegisterRoutes(api) + } + // 账本页服务端监听状态(只读,docs/PLAN-ADB-LEDGER-IMPL.md §7) + if s.ledgerStatus != nil { + api.GET("/ledger-monitors", s.handleLedgerMonitors) + } + } + // 实时设备流(云手机式操作)—— WebSocket 升级无法携带 Authorization header, + // 必须注册在根路由(无 authMiddleware),token 走 query 参数在 handler 内校验 + if s.streamSrv != nil { + r.GET("/api/personal/devices/stream", s.handleDeviceStream) + } + + // ---- 静态页面(SPA,NoRoute 统一处理,避免与 /api 路由冲突)---- + sub, err := fs.Sub(staticFS, "static") + if err != nil { + logger.Error("admin", "静态资源加载失败", "err", err) + sub = nil + } + r.NoRoute(func(c *gin.Context) { + if sub != nil && c.Request.Method == http.MethodGet { + // 1) 先尝试从静态目录读真实文件(/assets/*.js 等) + p := strings.TrimPrefix(c.Request.URL.Path, "/") + if p == "" { + p = "index.html" + } + if data, err := fs.ReadFile(sub, p); err == nil { + // index.html 不缓存(保证 SPA 入口始终引用最新 bundle;JS/CSS 带 hash 名天然不冲突) + if p == "index.html" { + c.Header("Cache-Control", "no-cache, no-store, must-revalidate") + } + c.Data(http.StatusOK, mimeTypeByPath(p), data) + return + } + // 2) 读不到 → SPA 回退到 index.html(前端路由) + if data, err := fs.ReadFile(sub, "index.html"); err == nil { + c.Header("Cache-Control", "no-cache, no-store, must-revalidate") + c.Data(http.StatusOK, "text/html; charset=utf-8", data) + return + } + } + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + }) + + logger.Info("admin", "管理后台启动", "addr", addr) + if err := r.Run(addr); err != nil { + logger.Error("admin", "管理后台退出", "err", err) + } +} + +// ========== Dashboard ========== + +func (s *Server) handleDashboard(c *gin.Context) { + ctx := c.Request.Context() + d := gin.H{} + var err error + + if d["total_orders"], err = s.st.CountOrders(ctx, ""); err != nil { + s.err(c, err) + return + } + if d["success_orders"], err = s.st.CountOrders(ctx, model.StatusSuccess); err != nil { + s.err(c, err) + return + } + if d["wait_orders"], err = s.st.CountOrders(ctx, model.StatusWaitPay); err != nil { + s.err(c, err) + return + } + if d["closed_orders"], err = s.st.CountOrders(ctx, model.StatusClosed); err != nil { + s.err(c, err) + return + } + if d["total_amount_minor"], err = s.st.SumAmount(ctx, model.StatusSuccess); err != nil { + s.err(c, err) + return + } + + // 通知任务统计 + if d["notify_pending"], err = s.st.CountNotifyTasks(ctx, "pending"); err != nil { + s.err(c, err) + return + } + if d["notify_success"], err = s.st.CountNotifyTasks(ctx, "success"); err != nil { + s.err(c, err) + return + } + if d["notify_dead"], err = s.st.CountNotifyTasks(ctx, "dead"); err != nil { + s.err(c, err) + return + } + + // 日志统计(独立日志库;未配置时跳过) + if s.logStore != nil { + if d["log_errors"], err = s.logStore.CountLogs(ctx, "ERROR", ""); err != nil { + s.err(c, err) + return + } + if d["log_warns"], err = s.logStore.CountLogs(ctx, "WARN", ""); err != nil { + s.err(c, err) + return + } + } else { + d["log_errors"] = int64(0) + d["log_warns"] = int64(0) + } + + // 账本(独立数据库) + if d["ledger_count"], err = s.ledgerStore.CountLedger(ctx); err != nil { + s.err(c, err) + return + } + last, err := s.ledgerStore.GetLastLedgerEntry(ctx) + if err != nil { + s.err(c, err) + return + } + if last != nil { + d["ledger_last_hash"] = last.EntryHash + d["ledger_last_time"] = last.CreatedAt.Format(time.RFC3339) + } + + // 最近订单 + recent, err := s.st.ListOrders(ctx, "", "", 8, 0) + if err != nil { + s.err(c, err) + return + } + d["recent_orders"] = recent + + // 订单/金额趋势:支持 start/end(日历范围);无则用旧的 days 快捷参数 + start := c.Query("start") + end := c.Query("end") + if start == "" || end == "" { + days, _ := strconv.Atoi(c.DefaultQuery("days", "14")) + if days <= 0 { + days = 14 + } + end = time.Now().Format("2006-01-02") + start = time.Now().AddDate(0, 0, -(days - 1)).Format("2006-01-02") + } + trend, err := s.st.OrdersTrend(ctx, start, end) + if err != nil { + s.err(c, err) + return + } + d["trend"] = trend + + // 隧道状态 + d["tunnel"] = s.tunnelStatus() + + c.JSON(http.StatusOK, gin.H{"data": d}) +} + +// ========== Epay Info ========== + +func (s *Server) handleEpayInfo(c *gin.Context) { + if s.epayInfo == nil { + c.JSON(http.StatusOK, gin.H{"data": nil}) + return + } + c.JSON(http.StatusOK, gin.H{"data": s.epayInfo()}) +} + +// ========== Channels ========== + +func (s *Server) handleChannels(c *gin.Context) { + ctx := c.Request.Context() + var out []ChannelInfo + + if s.channels != nil { + out = s.channels() + } else { + out = []ChannelInfo{} + } + + // 填充统计:每个通道的今日/累计订单与金额 + today := time.Now().Format("2006-01-02") + for i := range out { + // 累计 + if n, err := s.st.CountOrdersByChannel(ctx, "", out[i].Name); err == nil { + out[i].TotalOrders = n + } + if a, err := s.st.SumAmountByChannel(ctx, model.StatusSuccess, out[i].Name); err == nil { + out[i].TotalAmount = a + } + // 今日(通过 trend 或直接查:简单方式查今日创建的全部订单) + if n, err := s.st.CountOrdersByChannelToday(ctx, out[i].Name, today); err == nil { + out[i].TodayOrders = n + } + if a, err := s.st.SumAmountByChannelToday(ctx, out[i].Name, today); err == nil { + out[i].TodayAmount = a + } + } + c.JSON(http.StatusOK, gin.H{"data": out}) +} + +// ========== Notify Stats ========== + +func (s *Server) handleNotifyStats(c *gin.Context) { + ctx := c.Request.Context() + d := gin.H{} + var err error + if d["pending"], err = s.st.CountNotifyTasks(ctx, "pending"); err != nil { + s.err(c, err) + return + } + if d["success"], err = s.st.CountNotifyTasks(ctx, "success"); err != nil { + s.err(c, err) + return + } + if d["dead"], err = s.st.CountNotifyTasks(ctx, "dead"); err != nil { + s.err(c, err) + return + } + // 待重试(即将到期的 pending) + if d["total"], err = s.st.CountNotifyTasks(ctx, ""); err != nil { + s.err(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"data": d}) +} + +// ========== Orders ========== + +func (s *Server) handleListOrders(c *gin.Context) { + ctx := c.Request.Context() + status := c.Query("status") + channel := c.Query("channel") + keyword := c.Query("keyword") + from := c.Query("from") + to := c.Query("to") + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) + offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) + if limit <= 0 || limit > 200 { + limit = 20 + } + + total, err := s.st.CountOrdersFiltered(ctx, status, channel, keyword, from, to) + if err != nil { + s.err(c, err) + return + } + list, err := s.st.ListOrdersFiltered(ctx, status, channel, keyword, from, to, limit, offset) + if err != nil { + s.err(c, err) + return + } + // 附加账户名称(从账户池内存查询) + if s.accountHandler != nil { + for _, o := range list { + if o.AccountID > 0 { + o.AccountName = s.accountHandler.AccountName(o.AccountID) + } + } + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"total": total, "list": list}}) +} + +func (s *Server) handleOrderEvents(c *gin.Context) { + ctx := c.Request.Context() + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + s.err(c, fmt.Errorf("无效订单ID")) + return + } + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) + if limit <= 0 || limit > 100 { + limit = 20 + } + events, err := s.st.ListEvents(ctx, id, limit) + if err != nil { + s.err(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"data": events}) +} + +// ========== Notify Tasks ========== + +func (s *Server) handleNotifyTasks(c *gin.Context) { + ctx := c.Request.Context() + status := c.Query("status") + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + if limit <= 0 || limit > 200 { + limit = 50 + } + list, err := s.st.ListNotifyTasks(ctx, status, limit) + if err != nil { + s.err(c, err) + return + } + // 关联订单号 + type taskWithOrder struct { + *store.NotifyTask + OutTradeNo string `json:"out_trade_no"` + Subject string `json:"subject"` + AmountMinor int64 `json:"amount_minor"` + } + var out []taskWithOrder + for _, t := range list { + item := taskWithOrder{NotifyTask: t} + if o, err := s.st.GetOrderByID(ctx, t.OrderID); err == nil && o != nil { + item.OutTradeNo = o.OutTradeNo + item.Subject = o.Subject + item.AmountMinor = o.AmountMinor + } + out = append(out, item) + } + c.JSON(http.StatusOK, gin.H{"data": out}) +} + +// ========== Logs ========== + +func (s *Server) handleLogs(c *gin.Context) { + ctx := c.Request.Context() + if s.logStore == nil { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"total": 0, "list": []*store.LogRecord{}}}) + return + } + level := c.Query("level") + scope := c.Query("scope") + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) + if limit <= 0 || limit > 200 { + limit = 50 + } + + total, err := s.logStore.CountLogs(ctx, level, scope) + if err != nil { + s.err(c, err) + return + } + list, err := s.logStore.QueryLogs(ctx, level, scope, limit, offset) + if err != nil { + s.err(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"total": total, "list": list}}) +} + +// ========== Ledger ========== + +func (s *Server) handleLedger(c *gin.Context) { + ctx := c.Request.Context() + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "30")) + if limit <= 0 || limit > 100 { + limit = 30 + } + list, err := s.ledgerStore.ListLedger(ctx, limit) + if err != nil { + s.err(c, err) + return + } + count, err := s.ledgerStore.CountLedger(ctx) + if err != nil { + s.err(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"total": count, "list": list}}) +} + +// handleLedgerVerify 手动触发账本校验 +func (s *Server) handleLedgerVerify(c *gin.Context) { + if s.onLedgerVerify == nil { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"valid": false, "error": "未配置账本校验回调"}}) + return + } + logger.Info("admin", "手动触发账本校验") + res := s.onLedgerVerify() + c.JSON(http.StatusOK, gin.H{"data": res}) +} + +// handleLedgerSnapshots 快照列表 +func (s *Server) handleLedgerSnapshots(c *gin.Context) { + ctx := c.Request.Context() + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "30")) + if limit <= 0 || limit > 100 { + limit = 30 + } + list, err := s.ledgerStore.ListSnapshots(ctx, limit) + if err != nil { + s.err(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"data": list}) +} + +// handleCreateSnapshot 手动创建快照 +func (s *Server) handleCreateSnapshot(c *gin.Context) { + if s.onLedgerSnapshot == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "未配置账本"}) + return + } + if err := s.onLedgerSnapshot(); err != nil { + logger.Error("admin", "创建快照失败", "err", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + logger.Info("admin", "手动创建快照成功") + c.JSON(http.StatusOK, gin.H{"data": gin.H{"message": "快照创建成功"}}) +} + +// handleLedgerPublicKey 返回账本公钥(供外部验证签名) +func (s *Server) handleLedgerPublicKey(c *gin.Context) { + if s.onLedgerPublicKey == nil { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"public_key": "未配置账本"}}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"public_key": s.onLedgerPublicKey()}}) +} + +// handleLedgerExport 导出账本为 CSV(审计报告) +func (s *Server) handleLedgerExport(c *gin.Context) { + ctx := c.Request.Context() + + // 账本条目 CSV + entries, err := s.ledgerStore.ListLedger(ctx, 100000) + if err != nil { + s.err(c, err) + return + } + + // 快照 CSV + snapshots, err := s.ledgerStore.ListSnapshots(ctx, 1000) + if err != nil { + s.err(c, err) + return + } + + var b strings.Builder + // 写入 UTF-8 BOM(Excel 中文兼容) + b.WriteString("\xEF\xBB\xBF") + b.WriteString("# PayGateway 审计账本导出\n") + b.WriteString("# 生成时间: " + time.Now().Format("2006-01-02 15:04:05") + "\n") + b.WriteString("# 账本条目数: " + strconv.Itoa(len(entries)) + ", 快照数: " + strconv.Itoa(len(snapshots)) + "\n\n") + + b.WriteString("== 账本条目 ==\n") + b.WriteString("id,event_id,prev_hash,payload_hash,entry_hash,signature,created_at\n") + for _, e := range entries { + b.WriteString(fmt.Sprintf("%d,%d,%s,%s,%s,%s,%s\n", + e.ID, e.EventID, e.PrevHash, e.PayloadHash, e.EntryHash, e.Signature, + e.CreatedAt.Format(time.RFC3339))) + } + + b.WriteString("\n== 每日快照 ==\n") + b.WriteString("id,snapshot_date,last_hash,signature,created_at\n") + for _, sn := range snapshots { + b.WriteString(fmt.Sprintf("%d,%s,%s,%s,%s\n", + sn.ID, sn.SnapshotDate, sn.LastHash, sn.Signature, + sn.CreatedAt.Format(time.RFC3339))) + } + + // 设置下载响应头 + filename := fmt.Sprintf("ledger_audit_%s.csv", time.Now().Format("20060102")) + c.Header("Content-Disposition", "attachment; filename="+filename) + c.Header("Content-Type", "text/csv; charset=utf-8") + c.Data(http.StatusOK, "text/csv; charset=utf-8", []byte(b.String())) +} + +// ========== Tunnel ========== + +func (s *Server) handleTunnel(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"data": s.tunnelStatus()}) +} + +// ========== 账本页服务端监听状态(只读) ========== + +func (s *Server) handleLedgerMonitors(c *gin.Context) { + if s.ledgerStatus == nil { + c.JSON(http.StatusOK, gin.H{"data": []any{}, "enabled": false}) + return + } + c.JSON(http.StatusOK, gin.H{"data": s.ledgerStatus(), "enabled": true}) +} + +func (s *Server) tunnelStatus() TunnelStatus { + ts := TunnelStatus{UpdatedAt: time.Now().Format(time.RFC3339)} + if s.tunnelURL != nil { + ts.URL = s.tunnelURL() + } + if s.tunnelHealth != nil { + ts.Healthy = s.tunnelHealth() + } + if s.tunnelErr != nil { + ts.Error = s.tunnelErr() + } + if ts.URL != "" { + ts.Running = true + ts.Callback = ts.URL + s.callbackPath + } + return ts +} + +// handleHealth 深度健康检查(无鉴权,挂载在 r.GET("/api/health"),供 LB / 监控 / 告警调用)。 +// +// HTTP 状态码契约(运维 / 监控据此配 alert): +// - 200 OK : 服务进程活着 + 业务 DB 可读 + 账本表可读。 +// - 503 + JSON : 任一关键子项(db / ledger)失败;tunnel 失败不影响整体 200(隧道仅是支付回调通道,本地支付能力不受影响)。 +func (s *Server) handleHealth(c *gin.Context) { + ctx := c.Request.Context() + + resp := gin.H{"status": "ok", "checked_at": time.Now().Format(time.RFC3339)} + if !s.startTime.IsZero() { + resp["uptime_sec"] = int64(time.Since(s.startTime).Seconds()) + } + + criticalFail := false + + disk := gin.H{} + if n, err := s.st.CountOrders(ctx, ""); err != nil { + disk["ok"] = false + disk["error"] = err.Error() + criticalFail = true + } else { + disk["ok"] = true + disk["orders"] = n + } + resp["db"] = disk + + led := gin.H{} + if s.ledgerStore != nil { + if n, err := s.ledgerStore.CountLedger(ctx); err != nil { + led["ok"] = false + led["error"] = err.Error() + criticalFail = true + } else { + led["ok"] = true + led["count"] = n + } + } else { + led["ok"] = false + led["error"] = "ledgerStore 未注入" + criticalFail = true + } + resp["ledger"] = led + + tun := gin.H{} + if s.tunnelHealth != nil { + tun["healthy"] = s.tunnelHealth() + } + if s.tunnelURL != nil { + tun["url"] = s.tunnelURL() + } + resp["tunnel"] = tun + + if criticalFail { + resp["status"] = "error" + c.JSON(http.StatusServiceUnavailable, resp) + return + } + c.JSON(http.StatusOK, resp) +} + +// ========== 鉴权 ========== + +type loginReq struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// authMiddleware 鉴权中间件,验证 JWT token +func (s *Server) authMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) + return + } + // 去掉 "Bearer " 前缀 + if len(token) > 7 && token[:7] == "Bearer " { + token = token[7:] + } + claims := &jwtClaims{} + parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { + return s.jwtSecret, nil + }) + if err != nil || !parsed.Valid { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token 无效或已过期"}) + return + } + c.Set("username", claims.Username) + c.Next() + } +} + +type jwtClaims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +func (s *Server) handleLogin(c *gin.Context) { + var req loginReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + + // 失败退避限速:IP + username 双维度校验,已锁定直接 429 + if s.enforceLoginLimit(c, req.Username) { + return + } + + if s.adminSetupNeeded { + // 首次启动尚未注册管理员,引导注册(与"登录未启用"区分,避免管理员困惑) + c.JSON(http.StatusConflict, gin.H{"error": "管理员未注册,请先完成初始化"}) + return + } + if s.adminUser == "" || len(s.adminPass) == 0 || !strings.HasPrefix(string(s.adminPass), "$2") { + // 后台登录被禁用(配置不全或非 bcrypt 哈希),统一返回 403 避免泄露配置状态 + c.JSON(http.StatusForbidden, gin.H{"error": "管理后台登录未启用"}) + return + } + if req.Username != s.adminUser { + // 用户名错误也走 bcrypt 路径以保持固定时间,避免用户名枚举 + _ = bcrypt.CompareHashAndPassword(s.adminPass, []byte("00"+req.Password)) + s.recordLoginOutcome(c, req.Username, true) + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) + return + } + if err := bcrypt.CompareHashAndPassword(s.adminPass, []byte(req.Password)); err != nil { + s.recordLoginOutcome(c, req.Username, true) + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) + return + } + + s.recordLoginOutcome(c, req.Username, false) + + now := time.Now() + claims := &jwtClaims{ + Username: req.Username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(tokenExpiry)), + IssuedAt: jwt.NewNumericDate(now), + Issuer: "paygateway", + }, + } + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.jwtSecret) + if err != nil { + logger.Error("admin", "token 生成失败", "err", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "生成 token 失败"}) + return + } + logger.Info("admin", "登录成功", "username", req.Username) + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "token": token, + "expires": now.Add(tokenExpiry).Unix(), + }}) +} + +// ========== 首次启动注册 ========== + +type registerReq struct { + Username string `json:"username" binding:"required,min=3,max=32"` + Password string `json:"password" binding:"required,min=8,max=72"` +} + +// handleSetupStatus 查询是否需要首次注册(前端据此决定展示注册页 or 登录页) +func (s *Server) handleSetupStatus(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"setup_needed": s.adminSetupNeeded}}) +} + +// handleRegister 首次启动注册管理员(bcrypt 哈希存入 settings 表) +// 安全:仅在尚无管理员凭据时可调用;注册成功后立即禁用 +func (s *Server) handleRegister(c *gin.Context) { + s.setupMu.Lock() + defer s.setupMu.Unlock() + // 双重校验:内存标志 + DB 实况(防内存竞态绕过导致重复注册覆盖凭据) + if !s.adminSetupNeeded { + c.JSON(http.StatusForbidden, gin.H{"error": "管理员已配置,注册已关闭"}) + return + } + ctx := c.Request.Context() + if user, err := s.st.GetSetting(ctx, "admin_username"); err == nil && user != "" { + s.adminSetupNeeded = false + c.JSON(http.StatusForbidden, gin.H{"error": "管理员已配置,注册已关闭"}) + return + } + var req registerReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误:用户名 3-32 位,密码至少 8 位"}) + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) + return + } + // 先写哈希再写用户名:任一失败都不留半配置(用户名无值 = 注册未完成) + if err := s.st.SetSetting(ctx, "admin_password_hash", string(hash)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存管理员密码失败: " + err.Error()}) + return + } + if err := s.st.SetSetting(ctx, "admin_username", req.Username); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存管理员失败: " + err.Error()}) + return + } + // 立即生效:内存凭据切换为 DB 值,关闭注册入口 + s.adminUser = req.Username + s.adminPass = hash + s.adminSetupNeeded = false + logger.Info("admin", "管理员注册成功", "username", req.Username) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} + +// ========== 重置管理员(设置页入口) ========== + +type resetAdminReq struct { + Password string `json:"password" binding:"required"` +} + +// handleResetAdmin 重置管理员凭据:验证当前密码后清空 DB 与内存凭据,系统回到首次注册态。 +// 安全:需登录 + 提交当前密码(防误操作/CSRF);重置后所有已发 token 仍有效但登录校验立即切换到注册态, +// 前端收到 ok 后清除本地 token 并跳转注册页。重置动作记审计日志。 +func (s *Server) handleResetAdmin(c *gin.Context) { + if s.adminSetupNeeded { + c.JSON(http.StatusBadRequest, gin.H{"error": "当前已是未注册状态,无需重置"}) + return + } + var req resetAdminReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + // 验证当前管理员密码(防未授权操作) + if err := bcrypt.CompareHashAndPassword(s.adminPass, []byte(req.Password)); err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "当前密码错误"}) + return + } + s.setupMu.Lock() + defer s.setupMu.Unlock() + ctx := c.Request.Context() + // 删除 DB 凭据 + _ = s.st.DeleteSetting(ctx, "admin_username") + _ = s.st.DeleteSetting(ctx, "admin_password_hash") + // 清空内存凭据,回到注册态 + s.adminUser = "" + s.adminPass = nil + s.adminSetupNeeded = true + logger.Warn("admin", "管理员凭据已重置,系统回到首次注册态", "operator", c.GetString("username")) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} + +// ========== Settings ========== + +func (s *Server) handleSettings(c *gin.Context) { + ctx := c.Request.Context() + dbSettings, err := s.st.GetAllSettings(ctx) + if err != nil { + s.err(c, err) + return + } + // 多商户模式:易支付商户凭据已迁移到「上游对接」页(upstreams.db), + // 系统设置不再返回/管理 epay_pid/epay_key/epay_public_base_url + delete(dbSettings, "epay_pid") + delete(dbSettings, "epay_key") + delete(dbSettings, "epay_public_base_url") + // 管理员凭据哈希不得暴露给前端(仅 /api/auth/* 使用) + delete(dbSettings, "admin_username") + delete(dbSettings, "admin_password_hash") + // 账本私钥 / QR token 等完整密钥不随常规设置下发,仅由 /api/settings/keys 确认后返回 + delete(dbSettings, "ledger_private_key") + delete(dbSettings, "qr_token_secret") + // 合并通道启用状态 + channels := channel.AllNames() + channelEnabled := make(map[string]bool) + for _, name := range channels { + channelEnabled[name] = channel.IsEnabled(name) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "settings": dbSettings, + "channel_enabled": channelEnabled, + "channels": channels, + }}) +} + +// handleSettingsKeys 返回安全密钥状态与完整值(需登录鉴权)。 +// 仅返回有实际意义的密钥:账本私钥 / QR token 密钥;调用即记审计日志。 +func (s *Server) handleSettingsKeys(c *gin.Context) { + ctx := c.Request.Context() + dbSettings, err := s.st.GetAllSettings(ctx) + if err != nil { + s.err(c, err) + return + } + keys := gin.H{} + if v, ok := dbSettings["ledger_private_key"]; ok && v != "" { + keys["ledger_private_key"] = v + } + if v, ok := dbSettings["qr_token_secret"]; ok && v != "" { + keys["qr_token_secret"] = v + } + logger.Info("admin", "安全密钥被查看(明文)", "operator", c.GetString("username")) + c.JSON(http.StatusOK, gin.H{"data": keys}) +} + +type settingsUpdate struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func (s *Server) handleUpdateSettings(c *gin.Context) { + var req struct { + Settings []settingsUpdate `json:"settings"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + ctx := c.Request.Context() + for _, item := range req.Settings { + // 多商户模式:易支付商户凭据一律禁止通过系统设置保存(只认「上游对接」页) + switch item.Key { + case "epay_pid", "epay_key", "epay_public_base_url": + logger.Warn("admin", "拒绝保存已废弃的易支付系统设置", "key", item.Key) + continue + } + if err := s.st.SetSetting(ctx, item.Key, item.Value); err != nil { + s.err(c, err) + return + } + // 通知主进程更新运行时配置(如通道环境切换) + if s.onSettingsChange != nil { + s.onSettingsChange(item.Key, item.Value) + } + } + logger.Info("admin", "设置已更新", "count", len(req.Settings)) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +func (s *Server) handleToggleChannel(c *gin.Context) { + name := c.Param("name") + on := !channel.IsEnabled(name) + channel.SetEnabled(name, on) + // 持久化到 settings 表 + ctx := c.Request.Context() + val := "false" + if on { + val = "true" + } + _ = s.st.SetSetting(ctx, "channel_enabled_"+name, val) + logger.Info("admin", "通道启停", "channel", name, "enabled", on) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"name": name, "enabled": on}}) +} + +// handleToggleChannelMode 开启/关闭通道的某个支付模式 +func (s *Server) handleToggleChannelMode(c *gin.Context) { + name := c.Param("name") + mode := c.Param("mode") + ch, ok := channel.Lookup(name) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "通道不存在"}) + return + } + // 通过类型断言判断是否支持模式切换 + type modeToggler interface { + IsModeEnabled(mode string) bool + SetModeEnabled(mode string, enabled bool) + EnabledModes() map[string]bool + } + toggler, ok := ch.(modeToggler) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "该通道不支持模式切换"}) + return + } + on := !toggler.IsModeEnabled(mode) + toggler.SetModeEnabled(mode, on) + // 持久化到 settings 表 + ctx := c.Request.Context() + val := "false" + if on { + val = "true" + } + _ = s.st.SetSetting(ctx, "alipay_mode_"+mode, val) + logger.Info("admin", "通道支付模式启停", "channel", name, "mode", mode, "enabled", on) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"channel": name, "mode": mode, "enabled": on}}) +} + +// handleDeviceStream 实时设备流 WebSocket(云手机式操作) +// GET /api/personal/devices/stream?serial=127.0.0.1:5556&token= +// token 走 query(浏览器 WebSocket 无法自定义 header) +func (s *Server) handleDeviceStream(c *gin.Context) { + token := c.Query("token") + if token == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) + return + } + claims := &jwtClaims{} + parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { + return s.jwtSecret, nil + }) + if err != nil || !parsed.Valid { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token 无效或已过期"}) + return + } + if s.streamSrv == nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "实时流未启用"}) + return + } + s.streamSrv.HandleStream(c.Writer, c.Request) +} + +func (s *Server) err(c *gin.Context, err error) { + logger.Error("admin", "API错误", "err", err, "path", c.Request.URL.Path) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) +} diff --git a/internal/admin/capture/alipay.go b/internal/admin/capture/alipay.go new file mode 100644 index 0000000..b707bfb --- /dev/null +++ b/internal/admin/capture/alipay.go @@ -0,0 +1,356 @@ +// 支付宝收款码服务端 adb 捕获。 +// +// 链路(微信式「保存到相册」方案,实测更稳定):启动支付宝 → 点「收付款」→ 点「收钱」进入 +// 个人收款码页(PayeeQRActivity) → 点「保存图片」→ 图片落系统相册 DCIM/Alipay(不受 +// FLAG_SECURE 限制)→ 拉取最新 → 解码。不再依赖截图链路(截图方案易卡在收付款总页/付款码页)。 +package capture + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/logger" +) + +// alipayPackage 支付宝主包名(主空间 user 0 与分身 user 999 同包名; +// 本设备主空间支付宝被禁用 enabled=0,实际使用 user 999 分身) +const alipayPackage = "com.eg.android.AlipayGphone" + +// alipayLaunchActivity 支付宝启动 activity(resolve-activity 实测) +const alipayLaunchActivity = ".AlipayLogin" + +// alipayHomePayEntry 支付宝首页「收付款」入口(手机 OCR (405,382)) +var alipayHomePayEntry = wxNavPt{405, 382} + +// alipayCollectMoneyBtn 「收付款」页(向商家付款)内的「收钱」按钮(手机 (225,1688), +// 2026-09-12 uiautomator dump 实测标定)。 +// 必须点它才能进入个人收款码页(PayeeQRActivity);漏掉会停留在付款码页。 +var alipayCollectMoneyBtn = wxNavPt{225, 1688} + +// alipaySaveImgBtn 个人收款码页(PayeeQRActivity)「保存图片」按钮(手机 (540,1202), +// 2026-09-12 uiautomator dump 实测标定)。 +// 点击后支付宝把收款码写入系统相册 DCIM/Alipay/,不受 FLAG_SECURE 限制。 +var alipaySaveImgBtn = wxNavPt{540, 1202} + +// alipayPersonalTab 「个人收钱」tab(手机 (252,290))。 +// 收款码页有「个人收钱」/「经营收钱」双态:经营态需开通且无个人收款码, +// 点此 tab 确保切回个人收钱态(与微信个人/经营收款码同机制)。 +var alipayPersonalTab = wxNavPt{252, 290} + +// AlipayQrDir 支付宝收款码图片落盘目录(user 0 主空间;双开 user 由调用方拼接)。 +const AlipayQrDir = "/storage/emulated/0/DCIM/Alipay" + +// CaptureAlipayQrcode 核心:启动支付宝 → 点「收付款」→ 点「收钱」进收款码页 → 点「保存图片」 +// → 从相册拉取最新图片 → 解码。 +// 返回解码内容。userId>0 时以该 user 启动(双开/分身空间),相册路径同步切换。 +func CaptureAlipayQrcode(ctx context.Context, adb *adbmgr.Manager, serial string, userId int) (content string, err error) { + // 1. 唤醒屏幕 + 强制重启支付宝到干净首页(指定双开 user) + if err = wakeHome(ctx, adb, serial); err != nil { + return "", fmt.Errorf("唤醒屏幕失败: %w", err) + } + if err = adb.RestartAppUser(ctx, serial, alipayPackage, alipayLaunchActivity, userId); err != nil { + return "", fmt.Errorf("启动支付宝失败: %w", err) + } + time.Sleep(5 * time.Second) // 支付宝冷启动 + 首页加载(低端机/网络慢时需更久) + + // 前台守卫:支付宝未到前台绝不开始盲点坐标(重启失败/弹窗拦截时坐标会落进错误 app) + if err = awaitForeground(ctx, adb, serial, alipayPackage, foregroundWait); err != nil { + return "", fmt.Errorf("启动支付宝失败: %w", err) + } + + // 2. 点首页「收付款」进入收付款总页(向商家付款页) + if err = tapNav(ctx, adb, serial, alipayHomePayEntry); err != nil { + return "", fmt.Errorf("点击收付款入口失败: %w", err) + } + time.Sleep(2500 * time.Millisecond) + + // 3. 点「收钱」进入个人收款码页(漏此步停留在付款码页,无收款码) + if err = tapNav(ctx, adb, serial, alipayCollectMoneyBtn); err != nil { + return "", fmt.Errorf("点击收钱按钮失败: %w", err) + } + // 必须确认已进入收款码页(PayeeQRActivity);否则坐标已偏移,后续点击全落空 + if err = requireAlipayPayeePage(ctx, adb, serial); err != nil { + return "", err + } + + // 4. 直接点「保存图片」→ 检查相册是否新增(经营态该位置无按钮不会误点,个人态点击即保存)。 + // 相册未新增 → 说明在经营态 → 点「个人收钱」tab 切回个人态 → 再点保存。 + dir := AlipayQrDir + if userId > 0 { + dir = fmt.Sprintf("/storage/emulated/%d/DCIM/Alipay", userId) + } + // 相册基线:点击前已有的收款码图(防止把旧图当成新保存的) + base := listAlipayImages(ctx, adb, serial, dir) + + // trySave 点「保存图片」并轮询等待相册新增(落盘延迟在慢网络/低端机上可达 10s+,固定 sleep 不可靠) + trySave := func() bool { + if terr := tapNav(ctx, adb, serial, alipaySaveImgBtn); terr != nil { + return false + } + return pollForNewFile(ctx, base, 20*time.Second, func() []string { + return listAlipayImages(ctx, adb, serial, dir) + }) + } + // 第一次保存失败 → 说明不在个人态(如经营引导态无保存按钮)。 + // 循环「点个人收钱 tab → 再点保存」最多 3 轮:切态点击可能被吞/未生效, + // 单次补救不稳定(实测需多轮才能落到个人态并保存成功)。 + if !trySave() { + for round := 1; round <= 3; round++ { + if err = tapNav(ctx, adb, serial, alipayPersonalTab); err != nil { + return "", fmt.Errorf("点击个人收钱 tab 失败: %w", err) + } + time.Sleep(2500 * time.Millisecond) + if trySave() { + break + } + if round == 3 { + return "", fmt.Errorf("多轮切回个人态后保存图片仍失败(可能不在收款码页或按钮位置偏移)") + } + } + } + + // 5. 拉取相册最新支付宝收款码图片(按文件名倒序取第一张) + files, lerr := adb.ListFiles(ctx, serial, dir) + if lerr != nil { + return "", fmt.Errorf("读取相册失败: %w", lerr) + } + if len(files) == 0 { + return "", fmt.Errorf("支付宝相册为空(保存图片未落盘)") + } + latest := files[0] // ListFiles 按 -t 倒序,第一个最新 + local, perr := adb.PullFile(ctx, serial, dir+"/"+latest) + if perr != nil { + return "", fmt.Errorf("拉取收款码图片失败: %w", perr) + } + imgData, rerr := os.ReadFile(local) + if rerr != nil { + os.Remove(local) + return "", fmt.Errorf("读取图片失败: %w", rerr) + } + content, derr := DecodeQRContent(imgData) + if derr != nil { + os.Remove(local) + return "", fmt.Errorf("收款码图片未识别到二维码: %w", derr) + } + return content, nil +} + +// requireAlipayPayeePage 确认支付宝前台已到达收款码页(PayeeQRActivity)。 +// 带重试(最多 6 次×1.5s)。若点「收钱」后仍停在收付款页,重试点击一次再校验。 +func requireAlipayPayeePage(ctx context.Context, adb *adbmgr.Manager, serial string) error { + for i := 0; i < 6; i++ { + fg, err := adb.ForegroundApp(ctx, serial) + // 双开 user 前台字符串形如 "u999 com.eg.android.AlipayGphone/...PayeeQRActivity",用 Contains 匹配 + if err == nil && strings.Contains(fg, "PayeeQRActivity") { + return nil + } + if i > 0 && i%3 == 0 { + // 页面加载慢导致第一次点击被吞:重试点「收钱」 + if terr := tapNav(ctx, adb, serial, alipayCollectMoneyBtn); terr != nil { + return fmt.Errorf("重试点击收钱失败: %w", terr) + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1500 * time.Millisecond): + } + } + fg, _ := adb.ForegroundApp(ctx, serial) + return fmt.Errorf("未进入支付宝收款码页(当前前台 %q)", fg) +} + +// alipayRecordSummary 收款码页(PayeeQRActivity)下方「收款记录」入口 +// (2026-09-12 uiautomator dump 实测:收款码页「收款记录」中心 (251,1401))。 +// 注意:点击后进入的是【脆弱界面】(XRiverTransActivity),仅 24h 内记录且不稳定—— +// 需按 CaptureAlipayLedger 全链路(脆弱→灰区→总览→稳定)继续导航。 +var alipayRecordSummary = wxNavPt{251, 1401} + +// 脆弱界面→稳定界面导航段(2026-09-12 uiautomator dump 实测标定): +// - alipayLedgerFragileGray 脆弱界面(XRiverTransActivity)「收款记录」标题上方约 1 公分处 +// (灰色区域),点击后收起脆弱界面回到收款总览界面(XRiverActivity)。实测脆弱界面 +// 「收款记录」标题中心 (540,426),上方约 1 公分 ≈ (540,262)。 +// - alipayLedgerOverviewEntry 收款总览界面(XRiverActivity)内的「收款记录」按钮 +// (uiautomator dump 实测中心 (203,583)),点击后进入稳定的完整收款记录页 +// (XRiverActivity$App01,历史全部记录,支持下拉刷新)。 +var alipayLedgerFragileGray = wxNavPt{540, 262} +var alipayLedgerOverviewEntry = wxNavPt{203, 583} + +// CaptureAlipayLedger 导航到支付宝稳定的完整收款记录页(监听界面,PLAN-EVENT-MONITOR §2.3)。 +// +// 链路(2026-08-27 真机反馈修正):启动支付宝 → 点「收付款」→ 点「收钱」进收款码页(PayeeQRActivity) +// → 点「收款记录」→ 进入【脆弱界面】(XRiverTransActivity,仅 24h 内记录,不稳定,可能显示成别的) +// → 点脆弱界面上方灰色区域 → 回到收款总览界面(XRiverActivity) → 收款总览界面点「收款记录」 +// → 进入【稳定界面】(XRiverActivity$App01,历史全部记录,支持下拉刷新)。 +// +// 关键:不能把 XRiverTransActivity 当作"已到达"判据——那是脆弱界面,误判成功会导致 +// 读到不完整的 24h 内记录。必须走完「脆弱→灰区→总览→稳定」全链路。userId>0 时以该 user 启动。 +func CaptureAlipayLedger(ctx context.Context, adb *adbmgr.Manager, serial string, userId int) error { + if err := wakeHome(ctx, adb, serial); err != nil { + return fmt.Errorf("唤醒屏幕失败: %w", err) + } + if err := adb.RestartAppUser(ctx, serial, alipayPackage, alipayLaunchActivity, userId); err != nil { + return fmt.Errorf("启动支付宝失败: %w", err) + } + time.Sleep(5 * time.Second) // 冷启动 + 首页加载 + + // 前台守卫(同 CaptureAlipayQrcode):支付宝未到前台绝不开始盲点坐标 + if err := awaitForeground(ctx, adb, serial, alipayPackage, foregroundWait); err != nil { + return fmt.Errorf("启动支付宝失败: %w", err) + } + + // 屏幕固定保险:固定支付宝前台任务,导航期间其他 app(监听 app/系统弹窗) + // 无法抢占前台(实测 am start 被拦截 error 101),保证坐标导航不被打断。 + // 导航结束(含失败路径)defer 解锁,恢复自由切换。 + if taskID, perr := adb.PinCurrentTask(ctx, serial); perr == nil { + defer func() { _ = adb.UnpinTask(ctx, serial) }() + _ = taskID + } else { + logger.Warn("capture", "屏幕固定失败(导航将无保护执行,可能被其他 app 打断)", + "err", perr, "serial", serial) + } + + // 广告弹窗防御:首页布局就绪判定 = dump 出现「收付款」。广告弹窗遮挡时 + // 入口不可见 → 点一次收付款位置关闭弹窗回首页,再给足时间等首页出现。 + // 简单高效:只做一轮判断,不循环轮询(导航可靠性靠固定等待,不靠状态判断)。 + xml, _ := adb.DumpUI(ctx, serial) + if !strings.Contains(xml, "收付款") { + if terr := tapPinned(ctx, adb, serial, alipayHomePayEntry.X, alipayHomePayEntry.Y); terr != nil { + return fmt.Errorf("点击收付款入口(广告弹窗防御)失败: %w", terr) + } + time.Sleep(3 * time.Second) + } + + if err := tapPinned(ctx, adb, serial, alipayHomePayEntry.X, alipayHomePayEntry.Y); err != nil { + return fmt.Errorf("点击收付款入口失败: %w", err) + } + time.Sleep(3 * time.Second) // 收付款页加载(低端机/网络慢需更久) + + if err := tapPinned(ctx, adb, serial, alipayCollectMoneyBtn.X, alipayCollectMoneyBtn.Y); err != nil { + return fmt.Errorf("点击收钱按钮失败: %w", err) + } + if err := requireAlipayPayeePage(ctx, adb, serial); err != nil { + return err + } + // 经营态处理:收款码页默认可能是「经营收钱」态(页面布局不同,「收款记录」入口位置 + // 与个人态不一致)。进账本前先点「个人收钱」tab 切回个人态,保证后续账本入口坐标有效。 + // 与 CaptureAlipayQrcode 的经营态补救同机制,但这里主动先切(幂等:个人态点它无副作用)。 + if err := tapPinned(ctx, adb, serial, alipayPersonalTab.X, alipayPersonalTab.Y); err != nil { + return fmt.Errorf("点击个人收钱 tab 失败: %w", err) + } + time.Sleep(1500 * time.Millisecond) // 切态动画 + + // ① 收款码页点「收款记录」→ 进入脆弱界面(一定脆弱,无需 if 判断) + if err := tapPinned(ctx, adb, serial, alipayRecordSummary.X, alipayRecordSummary.Y); err != nil { + return fmt.Errorf("点击收款记录入口失败: %w", err) + } + time.Sleep(2500 * time.Millisecond) // 脆弱界面加载(WebView 渲染较慢) + // ② 脆弱界面点上方灰色区域 → 回到收款总览界面 + if err := tapPinned(ctx, adb, serial, alipayLedgerFragileGray.X, alipayLedgerFragileGray.Y); err != nil { + return fmt.Errorf("点击脆弱界面灰色区域失败: %w", err) + } + // ③ 从总览界面点「收款记录」→ 进入稳定界面 + return NavigateOverviewToLedger(ctx, adb, serial) +} + +// NavigateOverviewToLedger 从收款总览界面点「收款记录」进入稳定账本页。 +// 供冷启动全链路(CaptureAlipayLedger)与热启动补点(EnsureForeground 恢复时 +// 前台在总览界面)共用。 +func NavigateOverviewToLedger(ctx context.Context, adb *adbmgr.Manager, serial string) error { + // 屏幕固定(PINNED)模式下 input tap/swipe 注入事件会被拦截(2026-08-28 实测: + // 固定态点击无效,解锁后同坐标立即生效)——必须先释放固定,否则「收款记录」 + // 点击不生效卡在总览界面。解锁后 defer 仍会幂等解锁。 + if err := adb.UnpinTask(ctx, serial); err != nil { + logger.Warn("capture", "释放屏幕固定失败(继续尝试点击)", + "err", err, "serial", serial) + } + // 总览界面含横幅加载动画,固定等待 2s 让布局稳定(2026-08-28 实测)。 + // 不做 dump 文本检测(「今日收款」)作为点击前置——dump 时序不可靠会误判 + // 未就绪导致断链(2026-08-28 实测:预热走到此步因检测失败而中断,界面实际已就绪)。 + time.Sleep(2 * time.Second) + // 点击「收款记录」进入稳定账本页;以活动名确认到达,失败重试(最多 3 次)。 + var lastErr error + for i := 0; i < 3; i++ { + if err := tapNav(ctx, adb, serial, alipayLedgerOverviewEntry); err != nil { + return fmt.Errorf("点击总览界面收款记录失败: %w", err) + } + if err := requireAlipayLedgerPage(ctx, adb, serial); err == nil { + return nil + } else { + lastErr = err + } + time.Sleep(1500 * time.Millisecond) + } + return fmt.Errorf("点击收款记录后未到达稳定账本页: %w", lastErr) +} + +// waitOverviewReady 轮询等待收款总览界面加载完成(dump 出现「今日收款」文本)。 +// 总览界面有顶部横幅加载动画,固定 sleep 可能在布局未稳定时进入下一步。 +// 用 dump 文本判断是用户认可的"快速首页判定"同款机制,非 Activity 状态判断。 +func waitOverviewReady(ctx context.Context, adb *adbmgr.Manager, serial string, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + xml, err := adb.DumpUI(ctx, serial) + if err == nil && strings.Contains(xml, "今日收款") { + return true + } + select { + case <-ctx.Done(): + return false + case <-time.After(800 * time.Millisecond): + } + } + return false +} + +// requireAlipayLedgerPage 确认支付宝前台已到达稳定的完整收款记录页。 +// 判据 = XRiverActivity$App01(稳定界面)。注意不能匹配 XRiverTransActivity—— +// 那是脆弱界面(24h 内记录,不稳定),误判会导致读到不完整数据。 +func requireAlipayLedgerPage(ctx context.Context, adb *adbmgr.Manager, serial string) error { + for i := 0; i < 6; i++ { + fg, err := adb.ForegroundApp(ctx, serial) + if err == nil && strings.Contains(fg, "XRiverActivity$App") { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1500 * time.Millisecond): + } + } + fg, _ := adb.ForegroundApp(ctx, serial) + return fmt.Errorf("未进入支付宝稳定收款记录页(当前前台 %q)", fg) +} + +// CleanupAlipayGallery 清理指定 user 空间支付宝相册里的旧收款码图(保留最新 QrcodeKeepOnDevice 张)。 +// 与微信相册清理同策略:防止频繁捕获打满存储。 +func CleanupAlipayGallery(ctx context.Context, adb *adbmgr.Manager, serial string, userId int) (deleted int) { + dir := AlipayQrDir + if userId > 0 { + dir = fmt.Sprintf("/storage/emulated/%d/DCIM/Alipay", userId) + } + files, err := adb.ListFiles(ctx, serial, dir) + if err != nil || len(files) <= QrcodeKeepOnDevice { + return 0 // 目录不存在/不可读 或 未超保留数 → 无码可清 + } + for _, f := range files[QrcodeKeepOnDevice:] { // ListFiles 按 -t 倒序,越靠后越旧 + if err := adb.Rm(ctx, serial, dir+"/"+f); err != nil { + continue + } + deleted++ + } + return deleted +} + +// listAlipayImages 列出支付宝相册图片文件名(按 -t 倒序)。 +func listAlipayImages(ctx context.Context, adb *adbmgr.Manager, serial, dir string) []string { + files, err := adb.ListFiles(ctx, serial, dir) + if err != nil { + return nil + } + return files +} diff --git a/internal/admin/capture/alipay_ledger.go b/internal/admin/capture/alipay_ledger.go new file mode 100644 index 0000000..13ad2fe --- /dev/null +++ b/internal/admin/capture/alipay_ledger.go @@ -0,0 +1,258 @@ +// 支付宝账本页(收款记录 XRiverTransActivity)服务端读取与解析。 +// 背景:账本页是小程序容器(FLAG_SECURE),监听端无障碍 rootInActiveWindow 读不到节点树; +// 但 adb shell 的 uiautomator dump 是系统级通道,不受 SECURE 限制(实测可读出完整账本行)。 +// 链路:adb uiautomator dump → XML → 解析账本行(付款方 | HH:MM:SS | +金额)。 +// 账本行无备注(支付宝账本当前无备注行,Note 恒为空),去重指纹 = 时间|金额(秒级时间戳 + 金额)。 +package capture + +import ( + "encoding/xml" + "fmt" + "regexp" + "strings" +) + +// AlipayLedgerRow 账本页单行收款记录(去重指纹 = 时间|金额) +type AlipayLedgerRow struct { + Payer string // 付款方(脱敏,如 **怡 / 朱*6) + Date string // 所属日期分组("今日/M月d日"原始文案,如 "昨日 8月24日" / "7月1日") + Time string // HH:MM:SS + Amount string // 金额字符串(支付宝 +1.00 / 微信 ¥39.00) + AmountMinor int64 // 金额(分) + Note string // 付款方备注文本(微信账本"付款方备注:xxx"行;支付宝账本页无备注→空,需详情页探测) + AmountBounds string // 金额节点 bounds "[l,t][r,b]"(探测详情页备注码的点击坐标;微信账本无需探测→空) + onScreen bool // 是否屏幕可见行(离屏虚拟化行 bounds [0,0][0,0] 为 false;解析器内部用) +} + +// Fingerprint 行去重指纹:时间|金额(秒级时间到秒,同金额多笔靠时间区分) +func (r AlipayLedgerRow) Fingerprint() string { + return r.Time + "|" + r.Amount +} + +// ReadAlipayLedger 通过 adb uiautomator dump 读取当前前台账本页,解析可见收款行。 +// 前置条件:设备前台已在账本页(XRiverTransActivity);不在则该次调用返回 0 行(调用方自行判断)。 +// 离屏虚拟化行(bounds [0,0][0,0])会被过滤,只返回当前屏幕可见行。 +func ReadAlipayLedger(xmlText string) ([]AlipayLedgerRow, error) { + root, err := parseUINodeTree(xmlText) + if err != nil { + return nil, fmt.Errorf("解析账本页 XML 失败: %w", err) + } + var rows []AlipayLedgerRow + collectLedgerRows(root, &rows) + return rows, nil +} + +// uiNode uiautomator dump XML 的最小节点结构(只保留解析所需字段) +type uiNode struct { + Text string + Desc string + Bounds string + Kids []*uiNode +} + +// parseUINodeTree 用 encoding/xml 解码 uiautomator 层级树 +func parseUINodeTree(xmlText string) (*uiNode, error) { + dec := xml.NewDecoder(strings.NewReader(xmlText)) + var stack []*uiNode + var root *uiNode + for { + tok, err := dec.Token() + if err != nil { + if err.Error() == "EOF" { + break + } + return nil, err + } + switch t := tok.(type) { + case xml.StartElement: + n := &uiNode{} + for _, a := range t.Attr { + switch a.Name.Local { + case "text": + n.Text = a.Value + case "content-desc": + n.Desc = a.Value + case "bounds": + n.Bounds = a.Value + } + } + if len(stack) > 0 { + parent := stack[len(stack)-1] + parent.Kids = append(parent.Kids, n) + } else { + root = n + } + stack = append(stack, n) + case xml.EndElement: + stack = stack[:len(stack)-1] + } + } + if root == nil { + return nil, fmt.Errorf("空 XML 层级树") + } + return root, nil +} + +var ( + ledgerTimeRe = regexp.MustCompile(`^\d{2}:\d{2}:\d{2}$`) // HH:MM:SS + ledgerAmountRe = regexp.MustCompile(`^\+\d+(\.\d{2})?$`) // +1.00 / +10 + zeroBoundsRe = regexp.MustCompile(`\[0,0\]\[0,0\]`) +) + +// collectLedgerRows 递归收集账本行:行 = 子树内恰好含 1 个时间 + 1 个金额文本的最小容器。 +// 过滤:离屏虚拟化行(bounds [0,0][0,0],Android 懒加载未渲染的真实节点)。 +func collectLedgerRows(n *uiNode, rows *[]AlipayLedgerRow) { + collectLedgerRowsGroup(n, "", rows) +} + +// collectLedgerRowsGroup 带日期分组下钻:dateGroup 为当前所属日期分组文案(如 "今日 8月25日")。 +// 日期分组头("今日/昨日/M月d日")在账本中是独立文本节点,作为行收集时下钻到其子树继承。 +func collectLedgerRowsGroup(n *uiNode, dateGroup string, rows *[]AlipayLedgerRow) { + if n == nil { + return + } + if dg, ok := ledgerDateGroup(n.Text); ok { + dateGroup = dg + } + if timeText, amountText, isRow := ledgerRowTexts(n); isRow { + if !zeroBoundsRe.MatchString(n.Bounds) { + *rows = append(*rows, AlipayLedgerRow{ + Payer: ledgerRowPayer(n, timeText, amountText), + Date: dateGroup, + Time: timeText, + Amount: amountText, + AmountMinor: ledgerAmountMinor(amountText), + AmountBounds: ledgerAmountBounds(n), + }) + } + return // 行容器内不再深入(子节点无独立行) + } + for _, k := range n.Kids { + collectLedgerRowsGroup(k, dateGroup, rows) + } +} + +// ledgerDateGroup 识别日期分组头文本("今日 8月25日" / "昨日 8月24日" / "8月19日")。 +func ledgerDateGroup(text string) (string, bool) { + if text == "" || len(text) > 16 { + return "", false + } + if strings.HasPrefix(text, "今日 ") || strings.HasPrefix(text, "昨日 ") { + return text, true + } + // 纯 "M月d日"(如 "8月19日") + if strings.Contains(text, "月") && strings.HasSuffix(text, "日") && !strings.ContainsAny(text, " 笔收额") { + return text, true + } + return "", false +} + +// ledgerRowTexts 判断节点是否为"账本行最小容器":直系子树内含恰好 1 个时间 + 1 个金额文本。 +// 返回 (时间文本, 金额文本, 是否行容器)。摘要区(收款笔数/总额 ¥2.00)无 HH:MM:SS 时间 → 不是行。 +func ledgerRowTexts(n *uiNode) (string, string, bool) { + var timeText, amountText string + for _, k := range n.Kids { + tt, at, ok := ledgerNodeTexts(k) + if !ok { + continue + } + if tt != "" { + if timeText != "" { + return "", "", false // 多时间 → 是分组容器,非单行 + } + timeText = tt + } + if at != "" { + if amountText != "" { + return "", "", false + } + amountText = at + } + } + if timeText != "" && amountText != "" { + return timeText, amountText, true + } + return "", "", false +} + +// ledgerNodeTexts 递归收集子树内的(首个)时间文本与金额文本。 +func ledgerNodeTexts(n *uiNode) (timeText, amountText string, ok bool) { + if n == nil { + return "", "", false + } + if n.Text != "" { + if ledgerTimeRe.MatchString(n.Text) { + return n.Text, "", true + } + if ledgerAmountRe.MatchString(n.Text) { + return "", n.Text, true + } + } + for _, k := range n.Kids { + tt, at, o := ledgerNodeTexts(k) + if o { + return tt, at, true + } + } + return "", "", false +} + +// ledgerAmountBounds 提取行容器内金额节点的 bounds("[l,t][r,b]")。 +// 用途:支付宝账本页金额可点击进入收款详情页读备注码——探测点击坐标 = 金额 bounds 中心。 +// 找不到返回 ""(调用方跳过探测)。 +func ledgerAmountBounds(n *uiNode) string { + if n == nil { + return "" + } + if n.Text != "" && ledgerAmountRe.MatchString(n.Text) { + return n.Bounds + } + for _, k := range n.Kids { + if b := ledgerAmountBounds(k); b != "" { + return b + } + } + return "" +} + +// ledgerRowPayer 提取行容器内付款方文本:非时间、非金额、非空白的文本(如 **怡)。 +func ledgerRowPayer(n *uiNode, timeText, amountText string) string { + var payer string + var findPayer func(*uiNode) + findPayer = func(m *uiNode) { + if payer != "" { + return + } + if m.Text != "" && m.Text != timeText && m.Text != amountText && !ledgerTimeRe.MatchString(m.Text) && !ledgerAmountRe.MatchString(m.Text) { + payer = m.Text + return + } + for _, k := range m.Kids { + findPayer(k) + } + } + findPayer(n) + return payer +} + +// ledgerAmountMinor 金额文本转分("+1.00" → 100) +func ledgerAmountMinor(amount string) int64 { + s := strings.TrimPrefix(amount, "+") + var major, minor int64 + fmt.Sscanf(s, "%d.%d", &major, &minor) + // "1.5" 补零成 "1.50";"1" 无小数视为整元 + if i := strings.IndexByte(s, '.'); i >= 0 { + frac := s[i+1:] + switch len(frac) { + case 1: + minor *= 10 + case 2: + // 保持原样 + default: + minor = 0 + } + } else { + minor = 0 + } + return major*100 + minor +} diff --git a/internal/admin/capture/alipay_ledger_test.go b/internal/admin/capture/alipay_ledger_test.go new file mode 100644 index 0000000..8765fda --- /dev/null +++ b/internal/admin/capture/alipay_ledger_test.go @@ -0,0 +1,58 @@ +package capture + +import ( + "os" + "testing" +) + +func TestReadAlipayLedger(t *testing.T) { + data, err := os.ReadFile("testdata/alipay_ledger_ref.xml") + if err != nil { + t.Fatalf("读取测试数据失败: %v", err) + } + rows, err := ReadAlipayLedger(string(data)) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + // 参考 dump 可见行:昨日(8月24日) 2 笔(18:18:38 +1.00、17:16:02 +1.00); + // 8月19日 离屏行 bounds=[0,0][0,0] 应被过滤。 + if len(rows) != 2 { + t.Fatalf("期望 2 行可见账本行(离屏行被过滤),实际 %d 行: %+v", len(rows), rows) + } + got := map[string]AlipayLedgerRow{} + for _, r := range rows { + got[r.Time] = r + } + if r, ok := got["18:18:38"]; !ok { + t.Errorf("缺少 18:18:38 行: %+v", rows) + } else if r.Amount != "+1.00" || r.AmountMinor != 100 { + t.Errorf("18:18:38 行金额错误: %+v", r) + } else if r.Payer != "**怡" { + t.Errorf("18:18:38 行付款方错误: %+v", r) + } + if r, ok := got["17:16:02"]; !ok { + t.Errorf("缺少 17:16:02 行: %+v", rows) + } else if r.Amount != "+1.00" || r.AmountMinor != 100 { + t.Errorf("17:16:02 行金额错误: %+v", r) + } +} + +func TestLedgerAmountMinor(t *testing.T) { + cases := map[string]int64{ + "+1.00": 100, + "+1.50": 150, + "+10": 1000, + "+0.01": 1, + } + for in, want := range cases { + if got := ledgerAmountMinor(in); got != want { + t.Errorf("ledgerAmountMinor(%q) = %d, 期望 %d", in, got, want) + } + } +} + +func TestReadAlipayLedgerInvalidXML(t *testing.T) { + if _, err := ReadAlipayLedger("not xml"); err == nil { + t.Error("无效 XML 应返回错误") + } +} diff --git a/internal/admin/capture/alipay_lock.go b/internal/admin/capture/alipay_lock.go new file mode 100644 index 0000000..b0f0db2 --- /dev/null +++ b/internal/admin/capture/alipay_lock.go @@ -0,0 +1,183 @@ +// 支付宝最近任务锁定(MIUI/HyperOS 防一键清理)。 +// +// MIUI 的"锁定最近任务"是纯客户端 UI 特性:状态存于 Recents 进程内存, +// 无广播/binder/shell 命令(am task lock 是 kiosk 模式,非此机制)。 +// 唯一稳定可自动化路径 = uiautomator 定位任务卡片 → 长按 → 点「锁定任务」。 +// 2026-08-28 真机实测完整闭环:加锁/解锁可逆,一键清理后锁定任务保留。 +package capture + +import ( + "context" + "fmt" + "strings" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/logger" +) + +// recentsActivity MIUI/HyperOS 最近任务界面组件(am start 直达,比 KEYCODE_APP_SWITCH +// 稳定——后者在部分机型会打开桌面多屏视图而非最近任务)。 +const recentsActivity = "com.miui.home/.recents.RecentsActivity" + +// lockTaskBtnText 长按任务卡片弹出菜单中的按钮文本(国内版 MIUI 用 TextView,无 resource id)。 +// 已锁定任务的菜单显示「解除锁定」,未锁定显示「锁定任务」,二者可区分。 +const lockTaskBtnText = "锁定任务" + +// LockPaymentTasks 锁定最近任务中指定支付应用的主任务卡片(每应用最多锁一张), +// 防止一键清理移除任务栈。一个应用锁定它的主进程任务栈即可——锁定多个同应用进程 +// 会触发 MIUI 保护性杀进程(系统判定异常)。 +// 流程:直达 RecentsActivity → dump 定位未加锁的匹配应用卡片 → 逐张长按→点锁定。 +// 锁定状态存于内存(设备重启失效),调用方应在每次预热/恢复账本页后调用。 +func LockPaymentTasks(ctx context.Context, adb *adbmgr.Manager, serial string, appNames ...string) (int, error) { + locked := 0 + // 最多 3 轮:每轮锁一张未加锁卡片,锁定后列表刷新,重进最近任务避免坐标漂移 + for attempt := 0; attempt < 3; attempt++ { + unlocked, err := findUnlockedCards(ctx, adb, serial, appNames) + if err != nil { + return locked, err + } + if len(unlocked) == 0 { + return locked, nil + } + // 只锁第一张未加锁的卡片(每应用只锁主进程任务栈,足够防清理) + if err := lockOneCard(ctx, adb, serial, unlocked[0]); err != nil { + logger.Warn("capture", "锁定支付应用任务卡片失败", + "err", err, "serial", serial) + continue + } + locked++ + // 重新进最近任务列表以避免坐标漂移 + _, _ = adb.Shell(ctx, 3*time.Second, serial, "input keyevent KEYCODE_BACK") + time.Sleep(500 * time.Millisecond) + } + return locked, nil +} + +// findCards 进入最近任务并返回所有匹配应用名卡片的 bounds(无论锁定状态)。 +func findCards(ctx context.Context, adb *adbmgr.Manager, serial string, appNames []string) ([]string, error) { + if _, err := adb.Shell(ctx, 8*time.Second, serial, "am start -n "+recentsActivity); err != nil { + return nil, fmt.Errorf("打开最近任务失败: %w", err) + } + time.Sleep(2500 * time.Millisecond) + xmlText, err := adb.DumpUI(ctx, serial) + if err != nil { + return nil, fmt.Errorf("dump 最近任务失败: %w", err) + } + root, err := parseUINodeTree(xmlText) + if err != nil { + return nil, fmt.Errorf("解析最近任务 UI 失败: %w", err) + } + var out []string + walkUINodes(root, func(n *uiNode) { + if matchAnyName(n.Desc, appNames) && n.Bounds != "" { + out = append(out, n.Bounds) + } + }) + return out, nil +} + +// matchAnyName 判断 content-desc 是否包含任一应用名(如"支付宝,未加锁"匹配"支付宝")。 +func matchAnyName(desc string, appNames []string) bool { + for _, name := range appNames { + if strings.Contains(desc, name) { + return true + } + } + return false +} + +// findUnlockedCards 进入最近任务并返回所有未加锁的匹配应用名卡片的 bounds。 +func findUnlockedCards(ctx context.Context, adb *adbmgr.Manager, serial string, appNames []string) ([]string, error) { + if _, err := adb.Shell(ctx, 8*time.Second, serial, "am start -n "+recentsActivity); err != nil { + return nil, fmt.Errorf("打开最近任务失败: %w", err) + } + time.Sleep(2500 * time.Millisecond) + xmlText, err := adb.DumpUI(ctx, serial) + if err != nil { + return nil, fmt.Errorf("dump 最近任务失败: %w", err) + } + root, err := parseUINodeTree(xmlText) + if err != nil { + return nil, fmt.Errorf("解析最近任务 UI 失败: %w", err) + } + var out []string + walkUINodes(root, func(n *uiNode) { + // 任务卡片 content-desc 形如 "支付宝,未加锁"("打开支付宝应用信息。"等 + // 信息按钮也含应用名但无锁定状态后缀,必须排除,否则长按会点到按钮) + if matchAnyName(n.Desc, appNames) && strings.Contains(n.Desc, "未加锁") && n.Bounds != "" { + out = append(out, n.Bounds) + } + }) + return out, nil +} + +// walkUINodes 深度优先遍历 UI 节点树,对每个节点调用 fn。 +func walkUINodes(n *uiNode, fn func(*uiNode)) { + if n == nil { + return + } + fn(n) + for _, k := range n.Kids { + walkUINodes(k, fn) + } +} + +// lockOneCard 长按任务卡片 → 点「锁定任务」。bounds 形如 "[l,t][r,b]"。 +func lockOneCard(ctx context.Context, adb *adbmgr.Manager, serial, bounds string) error { + cx, cy, err := parseBoundsCenter(bounds) + if err != nil { + return err + } + // 长按 = swipe 同点 + 长 duration(input tap 无法表达长按,实测 swipe 900ms 有效) + if err := adb.Swipe(ctx, serial, cx, cy, cx, cy, 900); err != nil { + return fmt.Errorf("长按任务卡片失败: %w", err) + } + time.Sleep(1500 * time.Millisecond) + xmlText, err := adb.DumpUI(ctx, serial) + if err != nil { + return fmt.Errorf("dump 锁定菜单失败: %w", err) + } + btn, err := findButtonBounds(xmlText, lockTaskBtnText) + if err != nil { + return err + } + if btn == "" { + return fmt.Errorf("未找到锁定按钮(长按菜单未弹出,HyperOS 已知卡死问题)") + } + bx, by, err := parseBoundsCenter(btn) + if err != nil { + return err + } + if err := adb.Tap(ctx, serial, bx, by); err != nil { + return fmt.Errorf("点击锁定任务失败: %w", err) + } + time.Sleep(1500 * time.Millisecond) + return nil +} + +// findButtonBounds 在 dump XML 中找匹配按钮的 bounds。 +// 菜单按钮可能是 TextView(text 属性)或 ImageView(content-desc 属性, +// 如 "锁定任务" 是 com.miui.home:id/menu_item_lock ImageView),需两者都匹配。 +func findButtonBounds(xmlText, want string) (string, error) { + root, err := parseUINodeTree(xmlText) + if err != nil { + return "", err + } + var found string + walkUINodes(root, func(n *uiNode) { + if found == "" && n.Bounds != "" && (strings.Contains(n.Text, want) || strings.Contains(n.Desc, want)) { + found = n.Bounds + } + }) + return found, nil +} + +// parseBoundsCenter 解析 "[l,t][r,b]" 为中心坐标。 +func parseBoundsCenter(bounds string) (x, y int, err error) { + var l, t, r, b int + if _, err := fmt.Sscanf(bounds, "[%d,%d][%d,%d]", &l, &t, &r, &b); err != nil { + return 0, 0, fmt.Errorf("解析 bounds %q 失败: %w", bounds, err) + } + return (l + r) / 2, (t + b) / 2, nil +} diff --git a/internal/admin/capture/alipay_note.go b/internal/admin/capture/alipay_note.go new file mode 100644 index 0000000..ded714d --- /dev/null +++ b/internal/admin/capture/alipay_note.go @@ -0,0 +1,85 @@ +// 支付宝收款详情页备注码探测(2026-08-29 真机实测)。 +// +// 背景:支付宝账本页(XRiverActivity$App01)只显示 付款方|时间|金额,不显示备注码; +// 备注码(4 位字母数字)只存在于单笔收款详情页,需点击该行金额进入详情页读取。 +// +// 实测链路(红米K60 / 主空间支付宝,2026-08-29): +// +// 账本页金额节点 bounds 中心点击 → 详情页(收款详情)→ dump → "付款留言" 标签后 +// 相邻节点文本即备注码(如 jzt4 / 7x57 / 7u6g)→ KEYCODE_BACK 返回账本页。 +// +// 耗时实测:点击 0.17s + 详情加载 1.0s + dump 2.36s + 返回 0.25s ≈ 单笔 3.5s。 +// 优化:金额唯一时跳过探测(调用方短路);详情加载等待取 0.8s(布局静态,实测 0.5s 已渲染)。 +package capture + +import ( + "context" + "fmt" + "strings" + "time" + + "paygateway/internal/adbmgr" +) + +// alipayDetailNoteLabel 详情页备注码标签文案(实测:收款详情页"付款留言"行) +const alipayDetailNoteLabel = "付款留言" + +// alipayDetailLoadWait 点击金额后详情页渲染等待(实测 0.5s 已就绪,留 0.8s 余量) +const alipayDetailLoadWait = 800 * time.Millisecond + +// ProbeAlipayNote 从支付宝账本行探测备注码:点金额 → 详情页 dump → 提取备注 → 返回账本页。 +// bounds 为账本行金额节点 bounds("[l,t][r,b]"),点击其中心进入收款详情页。 +// 返回备注码(可能为空:详情页无"付款留言"标签);失败返回错误(调用方跳过该行探测, +// 金额兜底匹配仍可用——探测只是加速精确定位,不是资金安全前置)。 +func ProbeAlipayNote(ctx context.Context, adb *adbmgr.Manager, serial, bounds string) (string, error) { + if bounds == "" { + return "", fmt.Errorf("金额 bounds 为空,无法探测备注码") + } + cx, cy, err := parseBoundsCenter(bounds) + if err != nil { + return "", fmt.Errorf("解析金额 bounds 失败: %w", err) + } + // 1. 点击金额进入收款详情页 + if err := adb.Tap(ctx, serial, cx, cy); err != nil { + return "", fmt.Errorf("点击金额进入详情失败: %w", err) + } + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(alipayDetailLoadWait): + } + // 2. dump 详情页并解析备注码 + xml, err := adb.DumpUI(ctx, serial) + if err != nil { + return "", fmt.Errorf("详情页 dump 失败: %w", err) + } + note := parseAlipayDetailNote(xml) + // 3. 返回账本页(不等动画,下轮 EnsureForeground 恢复) + _ = adb.KeyEvent(ctx, serial, 4) // KEYCODE_BACK + return note, nil +} + +// parseAlipayDetailNote 从收款详情页 dump XML 提取备注码。 +// 规则(实测结构):找 "付款留言" 标签节点,其后紧跟的兄弟节点 text 即备注码。 +// 返回 "" 表示详情页无付款留言(未命中备注码——调用方降级金额兜底)。 +func parseAlipayDetailNote(xmlText string) string { + root, err := parseUINodeTree(xmlText) + if err != nil { + return "" + } + // 扁平收集带文本节点(保序),找标签后下一个非空文本 + var nodes []textNode + collectFlatTexts(root, &nodes) + for i, n := range nodes { + if strings.TrimSpace(n.text) == alipayDetailNoteLabel { + // 备注码 = 标签后首个非空文本节点(实测 index 6→7 相邻) + for j := i + 1; j < len(nodes); j++ { + v := strings.TrimSpace(nodes[j].text) + if v != "" { + return v + } + } + } + } + return "" +} diff --git a/internal/admin/capture/alipay_note_test.go b/internal/admin/capture/alipay_note_test.go new file mode 100644 index 0000000..9304ca1 --- /dev/null +++ b/internal/admin/capture/alipay_note_test.go @@ -0,0 +1,44 @@ +package capture + +import ( + "os" + "testing" +) + +// TestParseAlipayDetailNote 详情页备注码提取(实测 fixture:付款留言标签后相邻节点文本即备注码) +func TestParseAlipayDetailNote(t *testing.T) { + data, err := os.ReadFile("testdata/alipay_detail_note.xml") + if err != nil { + t.Fatalf("读取测试数据失败: %v", err) + } + if got := parseAlipayDetailNote(string(data)); got != "jzt4" { + t.Fatalf("备注码应为 jzt4,实际 %q", got) + } +} + +// TestParseAlipayDetailNote_NoLabel 无付款留言标签(非详情页/无备注)→ 返回空 +func TestParseAlipayDetailNote_NoLabel(t *testing.T) { + if got := parseAlipayDetailNote(``); got != "" { + t.Fatalf("无标签应返回空,实际 %q", got) + } +} + +// TestLedgerAmountBounds 账本行金额节点 bounds 提取(探测点击坐标) +func TestLedgerAmountBounds(t *testing.T) { + data, err := os.ReadFile("testdata/alipay_ledger_ref.xml") + if err != nil { + t.Fatalf("读取测试数据失败: %v", err) + } + rows, err := ReadAlipayLedger(string(data)) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) == 0 { + t.Fatal("应至少 1 行") + } + for _, r := range rows { + if r.AmountBounds == "" { + t.Errorf("行 %s 缺少 AmountBounds(金额节点 bounds 应被填充): %+v", r.Time, r) + } + } +} diff --git a/internal/admin/capture/common.go b/internal/admin/capture/common.go new file mode 100644 index 0000000..042c450 --- /dev/null +++ b/internal/admin/capture/common.go @@ -0,0 +1,138 @@ +// Package capture 设备端收款码自动捕获(服务端 adb 驱动,纯自动化逻辑)。 +// +// 与 admin 包解耦:本包只依赖 adbmgr(adb 操作),不依赖 store / 账户池 / HTTP, +// 便于独立测试与复用。HTTP 层(admin 包)负责设备查找、去重、入库、账户名解析。 +// +// 背景:微信收付款页带 FLAG_SECURE(对无障碍隐藏节点树 + 屏蔽系统手势注入), +// 监听端 APP 内无法自动化微信(支付宝无此限制)。但服务器 adb shell 的 +// input tap / screencap 不受 SECURE 限制(adb 是系统级通道)→ 微信捕获改由服务器驱动。 +package capture + +import ( + "bytes" + "context" + "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "regexp" + "strings" + "time" + + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" + + "paygateway/internal/adbmgr" +) + +// MaxQRUploadSize 二维码图片大小上限(2MB) +const MaxQRUploadSize = 2 << 20 + +// QrcodeKeepOnDevice 每设备相册保留的微信收款码图片张数(每账户只留最新 1 张,防存储打满) +const QrcodeKeepOnDevice = 1 + +// foregroundWait 坐标导航启动 app 后等待其到前台的上限。 +// 30s 给慢速跨 user 冷启动留足余量(守卫仍会快速失败,只是不误杀偏慢的冷启)。 +const foregroundWait = 30 * time.Second + +// wakeHome 唤醒屏幕 + 回桌面,清掉前一个账户驻留的前台。 +// 供各 Capture*Ledger/Qrcode 冷启动前调用:避免跨 user app 冷启被驻留前台挡住。 +func wakeHome(ctx context.Context, adb *adbmgr.Manager, serial string) error { + if err := adb.WakeUp(ctx, serial); err != nil { + return err + } + return adb.GoHome(ctx, serial) +} + +// DegenerateDump 判断 dump XML 是否为退化空树(无障碍未填充/页面未渲染)。 +// 判据:整棵 hierarchy 无任何携带非空 text/content-desc 的节点。正常渲染的账本页 +// (哪怕 0 交易行)总有标题/tab/页脚等静态文本节点;退化空树只有根 FrameLayout。 +// 用于账本读取:退化空 dump 是'读取失败'而非'账户真实为空',此时不得建立空基线, +// 否则后续真实历史行会被误判为 fresh 入账(2026-09-13 资金 P0 根因)。 +var ( + degenerateTextRe = regexp.MustCompile(`text="([^"]*)"`) + degenerateDescRe = regexp.MustCompile(`content-desc="([^"]*)"`) +) + +func DegenerateDump(xmlText string) bool { + hasContent := false + for _, m := range degenerateTextRe.FindAllStringSubmatch(xmlText, -1) { + if m[1] != "" { + hasContent = true + break + } + } + if !hasContent { + for _, m := range degenerateDescRe.FindAllStringSubmatch(xmlText, -1) { + if m[1] != "" { + hasContent = true + break + } + } + } + return !hasContent +} + +// DecodeQRContent 解析二维码图片内容(纯 Go,支持 PNG/JPEG/GIF) +// 返回解码后的字符串(如 https://qr.alipay.com/xxx 或 weixin://wxpay/bizpayurl?xxx) +func DecodeQRContent(data []byte) (string, error) { + if len(data) == 0 { + return "", fmt.Errorf("图片为空") + } + if len(data) > MaxQRUploadSize { + return "", fmt.Errorf("图片超过 2MB 限制") + } + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return "", fmt.Errorf("图片解码失败(请上传清晰的收款码截图): %w", err) + } + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return "", fmt.Errorf("二维码图像预处理失败: %w", err) + } + qrReader := qrcode.NewQRCodeReader() + result, err := qrReader.Decode(bmp, nil) + if err != nil { + return "", fmt.Errorf("未识别到二维码(请确认图片包含清晰的收款码): %w", err) + } + content := result.String() + if content == "" { + return "", fmt.Errorf("二维码内容为空") + } + return content, nil +} + +// TruncateStr 截断字符串(日志安全:不打印完整码内容) +func TruncateStr(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} + +// awaitForeground 带重试等待指定包名到前台。 +// 坐标导航的前置守卫:目标 app 未到前台时绝不能开始盲点导航坐标—— +// 重启失败/弹窗拦截/切前台慢时坐标会落进wrong app(实测点到微信/桌面/其他界面), +// 这正是捕获导航"乱来"的根源。双开空间前台串形如 "u999 com.xxx/...",Contains 匹配。 +func awaitForeground(ctx context.Context, adb *adbmgr.Manager, serial, pkg string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + var last string + for { + fg, err := adb.ForegroundApp(ctx, serial) + if err == nil { + last = fg + if strings.Contains(fg, pkg) { + return nil + } + } + if time.Now().After(deadline) { + return fmt.Errorf("等待 %s 前台超时 %v(当前前台 %q)", pkg, timeout, last) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Second): + } + } +} diff --git a/internal/admin/capture/testdata/alipay_detail_note.xml b/internal/admin/capture/testdata/alipay_detail_note.xml new file mode 100644 index 0000000..e8f5302 --- /dev/null +++ b/internal/admin/capture/testdata/alipay_detail_note.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/internal/admin/capture/testdata/alipay_ledger_ref.xml b/internal/admin/capture/testdata/alipay_ledger_ref.xml new file mode 100644 index 0000000..b2b9435 --- /dev/null +++ b/internal/admin/capture/testdata/alipay_ledger_ref.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/admin/capture/wait.go b/internal/admin/capture/wait.go new file mode 100644 index 0000000..c139f1d --- /dev/null +++ b/internal/admin/capture/wait.go @@ -0,0 +1,30 @@ +// 收款码捕获公共等待辅助:轮询相册直到出现新文件或超时。 +// +// 背景:adb 走 WiFi/反代通道有网络延迟,手机冷启动/低端机有性能延迟, +// 点「保存图片」后文件落盘耗时波动大(实测 2s~15s 不等),固定 sleep 不可靠—— +// 过早检查误判"未保存",过久等待拖慢整个捕获流程。轮询在"尽快返回"和"不误判"间取得平衡。 +package capture + +import ( + "context" + "time" +) + +// pollForNewFile 轮询目录列表(listFn 过滤),直到数量超过 base 数量或超时。 +// 每 2.5s 轮询一次,总超时 timeout;ctx 取消立即返回 false。 +func pollForNewFile(ctx context.Context, base []string, timeout time.Duration, listFn func() []string) bool { + deadline := time.Now().Add(timeout) + for { + if len(listFn()) > len(base) { + return true + } + if time.Now().After(deadline) { + return false + } + select { + case <-ctx.Done(): + return false + case <-time.After(2500 * time.Millisecond): + } + } +} diff --git a/internal/admin/capture/wechat.go b/internal/admin/capture/wechat.go new file mode 100644 index 0000000..9225b54 --- /dev/null +++ b/internal/admin/capture/wechat.go @@ -0,0 +1,387 @@ +// 微信收款码自动捕获(服务器 adb 驱动)。 +// +// 链路(与支付宝不同):微信收付款页 FLAG_SECURE 下 adb 截图返回空 → 无法直接解码; +// 改为 导航到「个人收款码」页 → 点「保存收款码」→ 图片落相册(不受 SECURE 限制)→ +// 拉取最新图片 → 解码 → 入库。 +package capture + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/logger" +) + +// wxNavPt 导航坐标点(1080x2400 手机参考系,TapRef 自动缩放实际分辨率)。 +// 仅保留手机坐标系——平板坐标系及其形态判定逻辑已整体移除(引入不可控问题)。 +type wxNavPt struct { + X, Y int +} + +// wechatQrTapSeq 微信收款码导航序列(前三步跨首页→服务页,1080x2400 真机标定): +// +// 微信首页 → 「我」tab → 「服务」 → 「收付款」 +var wechatQrTapSeq = []wxNavPt{ + {945, 2300}, // 「我」tab + {540, 700}, // 「服务」 + {300, 490}, // 「收付款」绿横幅 +} + +// wechatQrTapService 「服务」步——该步偶发 PINNED 固定态下 tap 失效,需解锁点击再固定 +var wechatQrTapService = wechatQrTapSeq[1] + +// tapNav 点击导航点:手机参考系坐标 → TapRef 自动缩放到实际生效分辨率。 +func tapNav(ctx context.Context, adb *adbmgr.Manager, serial string, p wxNavPt) error { + return adb.TapRef(ctx, serial, p.X, p.Y) +} + +// wechatQrTapDelay 每步点击后的等待。微信页面切换动画 + adb 通道网络延迟 + 设备 +// 性能波动非线性(2026-08-28 实测),必须给足余量;3s 与支付宝关键步骤一致。 +const wechatQrTapDelay = 3000 * time.Millisecond + +// wechatQrEntry 收付款总页内「二维码收款」入口(手机 632 下半屏,实测 (540,1830), +// 该格下方为「面对面红包」,上方为转账等格) +var wechatQrEntry = wxNavPt{540, 1830} + +// wechatQrPersonalTab 收款码页内「个人收款码」tab(手机参考 (270,335)) +var wechatQrPersonalTab = wxNavPt{270, 335} + +// wechatQrSaveBtn 「个人收款码」页「保存收款码」按钮(手机 OCR 中心 (483,1315), +// 点击后已实测落图 mm_facetoface_collect_qrcode_.png) +var wechatQrSaveBtn = wxNavPt{483, 1315} + +// WechatQrDir 微信收款码图片落盘目录(主空间 user 0;双开空间路径由调用方拼接 user id) +const WechatQrDir = "/storage/emulated/0/Pictures/WeiXin" + +// WechatQrPrefix 微信收款码图片文件名前缀(保存收款码写入) +const WechatQrPrefix = "mm_facetoface_collect_qrcode_" + +// CaptureWechatQrcode 核心:导航到微信个人收款码页 → 点「保存收款码」→ 从相册拉取最新图片 → 解码。 +// 返回 图片本地路径(调用方负责 os.Remove)+ 文件名 + 解码内容。 +// userId>0 时以该系统用户启动微信(双开/分身空间),相册路径同步切换。 +func CaptureWechatQrcode(ctx context.Context, adb *adbmgr.Manager, serial string, userId int) (localPath, fileName, content string, err error) { + // 1. 唤醒屏幕 + 强制重启微信到干净首页(指定双开 user;force-stop 避免旧页面残留) + if err = wakeHome(ctx, adb, serial); err != nil { + return "", "", "", fmt.Errorf("唤醒屏幕失败: %w", err) + } + // 显式 LauncherUI activity:MAIN/LAUNCHER 类别对双开 user(999) 实测启动不可靠 + if err = adb.RestartAppUser(ctx, serial, "com.tencent.mm", ".ui.LauncherUI", userId); err != nil { + return "", "", "", fmt.Errorf("启动微信失败: %w", err) + } + time.Sleep(5 * time.Second) // 微信冷启动 + 首页加载(低端机/网络慢时需更久) + + // 前台守卫:微信未到前台绝不开始盲点坐标(重启失败时坐标会落进桌面/其他 app) + if err = awaitForeground(ctx, adb, serial, "com.tencent.mm", foregroundWait); err != nil { + return "", "", "", fmt.Errorf("启动微信失败: %w", err) + } + + // 2. 依次点击导航到收付款总页 + for i, step := range wechatQrTapSeq { + if err = tapNav(ctx, adb, serial, step); err != nil { + return "", "", "", fmt.Errorf("点击第 %d 步失败: %w", i+1, err) + } + time.Sleep(wechatQrTapDelay) + } + time.Sleep(500 * time.Millisecond) + + // 3. 收付款总页 → 点「二维码收款」进入收款码页(漏此步停留总页) + if err = tapNav(ctx, adb, serial, wechatQrEntry); err != nil { + return "", "", "", fmt.Errorf("点击二维码收款入口失败: %w", err) + } + time.Sleep(wechatQrTapDelay) + // 必须确认已进入收款码页(WalletCollectQrCodeUI);否则坐标导航已偏移,后续点击全落空 + if err = requireWechatCollectPage(ctx, adb, serial); err != nil { + return "", "", "", err + } + + // 4. 直接点「保存收款码」→ 检查相册是否新增(商家/经营态该位置无按钮不会误点,个人态点击即保存)。 + // 相册未新增 → 说明在经营态 → 点「个人收款码」tab 切回个人态 → 再点保存。 + dir := WechatQrDir + if userId > 0 { + dir = fmt.Sprintf("/storage/emulated/%d/Pictures/WeiXin", userId) + } + // 相册基线:点击前已有的收款码图(防止把旧图当成新保存的) + base := listWechatQrFiles(ctx, adb, serial, dir) + + // trySave 点「保存收款码」并轮询等待相册新增(落盘延迟在慢网络/低端机上可达 10s+,固定 sleep 不可靠) + trySave := func() bool { + if terr := tapNav(ctx, adb, serial, wechatQrSaveBtn); terr != nil { + return false + } + return pollForNewFile(ctx, base, 20*time.Second, func() []string { + return listWechatQrFiles(ctx, adb, serial, dir) + }) + } + // 第一次保存失败 → 说明不在个人态(如经营收款码态无保存按钮)。 + // 循环「点个人收款码 tab → 再点保存」最多 3 轮:切态点击可能被吞/未生效,单次补救不稳定。 + if !trySave() { + for round := 1; round <= 3; round++ { + if err = tapNav(ctx, adb, serial, wechatQrPersonalTab); err != nil { + return "", "", "", fmt.Errorf("点击个人收款码 tab 失败: %w", err) + } + time.Sleep(wechatQrTapDelay) + if trySave() { + break + } + if round == 3 { + return "", "", "", fmt.Errorf("多轮切回个人态后保存收款码仍失败(可能不在收款码页或按钮位置偏移)") + } + } + } + + // 5. 拉取相册最新微信收款码图片(按文件名倒序取第一张) + files, lerr := adb.ListFiles(ctx, serial, dir) + if lerr != nil { + return "", "", "", fmt.Errorf("读取相册失败: %w", lerr) + } + latest := "" + for _, f := range files { + if strings.HasPrefix(f, WechatQrPrefix) && strings.HasSuffix(f, ".png") { + latest = f + break + } + } + if latest == "" { + return "", "", "", fmt.Errorf("相册中未找到微信收款码图片(保存收款码未落盘)") + } + local, perr := adb.PullFile(ctx, serial, dir+"/"+latest) + if perr != nil { + return "", "", "", fmt.Errorf("拉取收款码图片失败: %w", perr) + } + imgData, rerr := os.ReadFile(local) + if rerr != nil { + os.Remove(local) + return "", "", "", fmt.Errorf("读取图片失败: %w", rerr) + } + content, derr := DecodeQRContent(imgData) + if derr != nil { + os.Remove(local) + return "", "", "", fmt.Errorf("收款码图片未识别到二维码: %w", derr) + } + return local, latest, content, nil +} + +// RequireWechatCollectPage 确认微信前台已到达收款码页(WalletCollectQrCodeUI)。 +// 带重试(页面切换动画 + 加载),最多尝试 5 次×1.5s。用于导航步骤间的页面就绪校验, +// 防止坐标偏移后盲目继续点击。 +func requireWechatCollectPage(ctx context.Context, adb *adbmgr.Manager, serial string) error { + for i := 0; i < 5; i++ { + fg, err := adb.ForegroundApp(ctx, serial) + // 双开 user 前台字符串形如 "u999 com.tencent.mm/...activity",用 Contains 匹配 + if err == nil && strings.Contains(fg, "com.tencent.mm/") { + if strings.Contains(fg, "WalletCollectQrCodeUI") { + return nil + } + // 前台仍是微信但不是收款页:再等一次(可能动画中) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1500 * time.Millisecond): + } + continue + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1500 * time.Millisecond): + } + } + return fmt.Errorf("未进入微信收款码页(当前前台 %q),坐标导航可能已偏移", func() string { + fg, _ := adb.ForegroundApp(ctx, serial) + return fg + }()) +} + +// CleanupWechatGallery 清理指定 user 空间微信相册里的旧收款码图(保留最新 QrcodeKeepOnDevice 张)。 +func CleanupWechatGallery(ctx context.Context, adb *adbmgr.Manager, serial string, userId int) (deleted int) { + dir := WechatQrDir + if userId > 0 { + dir = fmt.Sprintf("/storage/emulated/%d/Pictures/WeiXin", userId) + } + files, err := adb.ListFiles(ctx, serial, dir) + if err != nil { + return 0 // 目录不存在/不可读 → 无码可清 + } + var qrFiles []string + for _, f := range files { + if strings.HasPrefix(f, WechatQrPrefix) && strings.HasSuffix(f, ".png") { + qrFiles = append(qrFiles, f) + } + } + if len(qrFiles) <= QrcodeKeepOnDevice { + return 0 + } + for _, f := range qrFiles[QrcodeKeepOnDevice:] { // ListFiles 按 -t 倒序,越靠后越旧 + if err := adb.Rm(ctx, serial, dir+"/"+f); err != nil { + continue + } + deleted++ + } + return deleted +} + +// listWechatQrFiles 列出微信收款码图片文件名(mm_facetoface_collect_qrcode_*.png,按 -t 倒序)。 +func listWechatQrFiles(ctx context.Context, adb *adbmgr.Manager, serial, dir string) []string { + files, err := adb.ListFiles(ctx, serial, dir) + if err != nil { + return nil + } + var out []string + for _, f := range files { + if strings.HasPrefix(f, WechatQrPrefix) && strings.HasSuffix(f, ".png") { + out = append(out, f) + } + } + return out +} + +// 微信账本页冷启动导航坐标(PLAN-ADB-LEDGER.md §2.2 实测): +// - wechatLedgerEntry 收款码页(WalletCollectQrCodeUI)内「收款小账本」整行按钮 +// (手机 x 居中 (540,1745),整行可点区域以 x=540 居中点击最可靠,与「服务」步同理)。 +// 点击后进入收款小账本 +// - wechatLedgerRecords 小账本内「收款记录」按钮(统计卡片内蓝色小按钮,手机 y≈748), +// 点击后进入收款记录页(AppBrandUI00 小程序容器) +var wechatLedgerEntry = wxNavPt{540, 1745} +var wechatLedgerRecords = wxNavPt{540, 748} + +// CaptureWechatLedger 冷启动导航到微信稳定的收款记录页(AppBrandUI00 小程序容器)。 +// 链路:复用 CaptureWechatQrcode 前半段(首页→我→服务→收付款→二维码收款页) +// → 收款码页点「收款小账本」→ 小账本点「收款记录」→ 进入收款记录页。 +// 供预热阶段调用;下单实时回调只走 task lock 热启动(EnsureForeground)。 +// userId>0 时以该 user 启动(双开/分身空间)。 +func CaptureWechatLedger(ctx context.Context, adb *adbmgr.Manager, serial string, userId int) error { + if err := wakeHome(ctx, adb, serial); err != nil { + return fmt.Errorf("唤醒屏幕失败: %w", err) + } + if err := adb.RestartAppUser(ctx, serial, "com.tencent.mm", ".ui.LauncherUI", userId); err != nil { + return fmt.Errorf("启动微信失败: %w", err) + } + time.Sleep(5 * time.Second) // 冷启动 + 首页加载 + + // 前台守卫(同 CaptureWechatQrcode):微信未到前台绝不开始盲点坐标 + if err := awaitForeground(ctx, adb, serial, "com.tencent.mm", foregroundWait); err != nil { + return fmt.Errorf("启动微信失败: %w", err) + } + + // 屏幕固定保险:导航期间其他 app 无法抢占前台(同 CaptureAlipayLedger) + if taskID, perr := adb.PinCurrentTask(ctx, serial); perr == nil { + defer func() { _ = adb.UnpinTask(ctx, serial) }() + _ = taskID + } else { + logger.Warn("capture", "屏幕固定失败(导航将无保护执行,可能被其他 app 打断)", + "err", perr, "serial", serial) + } + + // 微信首页 → 我 → 服务 → 收付款(复用收款码导航坐标序列) + for _, p := range wechatQrTapSeq { + var err error + if p == wechatQrTapService { + // 「服务」步偶发 PINNED 固定态下 tap 失效(2026-08-28 实测): + // 先释放固定 → 点击 → 重新固定(与支付宝总览补点同 bug 处理) + err = tapPinned(ctx, adb, serial, p.X, p.Y) + } else { + err = adb.TapRef(ctx, serial, p.X, p.Y) + } + if err != nil { + return fmt.Errorf("微信导航点击失败: %w", err) + } + time.Sleep(wechatQrTapDelay) + } + // 收付款 → 二维码收款(进入收款码页,PINNED 固定态下 tap 偶发失效——先解锁再点)。 + if err := tapPinned(ctx, adb, serial, wechatQrEntry.X, wechatQrEntry.Y); err != nil { + return fmt.Errorf("点击二维码收款失败: %w", err) + } + if err := requireWechatCollectPage(ctx, adb, serial); err != nil { + return err + } + time.Sleep(1500 * time.Millisecond) + + // 收款码页(WalletCollectQrCodeUI)→ 收款小账本 → 收款记录 → 收款记录页(AppBrandUI00)。 + // 关键(2026-08-29 真机验证):PINNED 固定态会**拦截小程序新 task 启动**——「收款小账本」 + // 是独立小程序容器(新 task t22284),固定时点击无响应/被拦。必须在此**解除固定**, + // 后续点击全部在解锁态执行(不再重新固定)。 + if err := adb.UnpinTask(ctx, serial); err != nil { + logger.Warn("capture", "微信导航:进入收款码页后释放固定失败(继续尝试点击)", + "err", err, "serial", serial) + } + time.Sleep(500 * time.Millisecond) + + // 收款码页默认可能是「经营收款码」态(约 50%,页面布局不同「收款小账本」入口点击无效) + // 或「个人收款码」态(入口可点)。不可依赖单次点击——每次点「收款小账本」后用前台 + // 是否进入小账本容器(AppBrandUI00)校验:进入=个人态成功;未进=经营态点不动, + // 点「个人收款码」tab 切回个人态再续试。每轮失败都重新点 tab 兜底(对齐 + // CaptureWechatQrcode 的多轮切态机制),最多 3 轮内必达个人态(与实测一致)。 + const ledgerRounds = 3 + var lastErr error + for round := 0; round < ledgerRounds; round++ { + if round > 0 { + // 非首次:上一轮未进入账本容器 → 疑经营态,点「个人收款码」tab 切回个人态 + if err := tapNav(ctx, adb, serial, wechatQrPersonalTab); err != nil { + return fmt.Errorf("点击个人收款码 tab 失败: %w", err) + } + time.Sleep(wechatQrTapDelay) // 切态动画 + 页面重载 + } + // 点「收款小账本」→ 校验是否进入容器(个人态成功标准) + if err := tapNav(ctx, adb, serial, wechatLedgerEntry); err != nil { + return fmt.Errorf("点击收款小账本失败: %w", err) + } + time.Sleep(3000 * time.Millisecond) // 小账本加载(小程序冷启动较慢) + if err := requireWechatLedgerPage(ctx, adb, serial); err != nil { + lastErr = err + continue // 未进入容器 → 下轮切个人态再试 + } + // 已进入小账本容器 → 点「收款记录」进账本页 + if err := tapNav(ctx, adb, serial, wechatLedgerRecords); err != nil { + return fmt.Errorf("点击收款记录失败: %w", err) + } + time.Sleep(3000 * time.Millisecond) + if err := requireWechatLedgerPage(ctx, adb, serial); err != nil { + lastErr = err + continue + } + return nil + } + return fmt.Errorf("进入微信收款记录页失败(含切个人收款码态重试): %w", lastErr) +} + +// tapPinned PINNED 固定态下的安全点击:先解锁(UnpinTask)→ 点击 → 重新固定。 +// PINNED 固定态下 input tap 偶发失效(2026-08-28 实测微信「服务」步/支付宝总览界面), +// 解锁后同坐标点击立即生效。重新固定失败不致命。微信/支付宝导航通用。 +func tapPinned(ctx context.Context, adb *adbmgr.Manager, serial string, x, y int) error { + if err := adb.UnpinTask(ctx, serial); err != nil { + logger.Warn("capture", "释放屏幕固定失败(继续尝试点击)", + "err", err, "serial", serial) + } + time.Sleep(300 * time.Millisecond) + if err := adb.TapRef(ctx, serial, x, y); err != nil { + return err + } + time.Sleep(300 * time.Millisecond) + if _, err := adb.PinCurrentTask(ctx, serial); err != nil { + logger.Warn("capture", "重新固定屏幕失败(导航将无保护执行)", + "err", err, "serial", serial) + } + return nil +} + +// requireWechatLedgerPage 确认微信前台已到达收款记录页(AppBrandUI00 小程序容器)。 +// 带重试(小程序页面加载),最多尝试 6 次×1.5s。 +func requireWechatLedgerPage(ctx context.Context, adb *adbmgr.Manager, serial string) error { + for i := 0; i < 6; i++ { + fg, err := adb.ForegroundApp(ctx, serial) + if err == nil && strings.Contains(fg, "AppBrandUI00") { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1500 * time.Millisecond): + } + } + fg, _ := adb.ForegroundApp(ctx, serial) + return fmt.Errorf("未进入微信收款记录页(当前前台 %q)", fg) +} diff --git a/internal/admin/capture/wechat_ledger.go b/internal/admin/capture/wechat_ledger.go new file mode 100644 index 0000000..5c300d8 --- /dev/null +++ b/internal/admin/capture/wechat_ledger.go @@ -0,0 +1,164 @@ +// 微信收款记录页(AppBrandUI00 小程序容器)服务端读取与解析。 +// 背景:微信对无障碍严格屏蔽,uiautomator dump 默认黑盒(仅 1 个空 FrameLayout 节点)。 +// 解锁:系统无障碍开启(settings put secure accessibility_enabled 1)后 XWeb 小程序 +// 才构建/暴露 accessibility 节点树(docs/DESIGN-DEVICE-ACTOR.md §9.1 实测确认)。 +// 链路:adb uiautomator dump → XML → 解析账本行(付款方 | ¥金额 | HH:MM:SS)。 +// 与支付宝差异:微信行是扁平文本节点(无嵌套容器),金额带 ¥ 前缀,日期无"今日/昨日"。 +package capture + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +// 微信账本行正则(实测 2026-08-29 真机): +// - 金额:¥39.00 / ¥100(带 ¥ 前缀) +// - 时间:17:24:47(HH:MM:SS,与支付宝同格式) +// - 日期分组:7月1日 / 9月14日(M月d日,无"今日/昨日"前缀) +var ( + wechatAmountRe = regexp.MustCompile(`^¥\d+(\.\d{2})?$`) // ¥39.00 / ¥100 + wechatDateRe = regexp.MustCompile(`^\d{1,2}月\d{1,2}日$`) + // 付款方备注行(携带"备注码",是入账对账的反错配主信号) + wechatNoteRe = regexp.MustCompile(`^付款方备注:(.+)$`) + // 非付款方的干扰文本(页脚/摘要/入口等,付款方是短脱敏名不应含这些词) + wechatNonPayerRe = regexp.MustCompile(`点单收款|自定义查询|共收款|累计|仅展示|选择时间|查看更多`) +) + +// ReadWechatLedger 通过 adb uiautomator dump 读取当前前台微信收款记录页,解析可见收款行。 +// 前置条件:系统无障碍已开启(accessibility_enabled=1)+ 前台在账本页(AppBrandUI00)。 +// 行结构(扁平节点,同 y 带三要素): +// +// [210,y] 付款方(脱敏,如 朱*6 / *学) +// [~900,y] 金额(¥39.00) +// [210,y+81] 时间(17:24:47) +// +// 日期分组独立成行(M月d日),作为后续行的 Date 继承; +// 备注行(付款方备注:xxx)捕获到当前/下一行的 Note 字段,是入账对账的反错配主信号。 +func ReadWechatLedger(xmlText string) ([]AlipayLedgerRow, error) { + root, err := parseUINodeTree(xmlText) + if err != nil { + return nil, fmt.Errorf("解析微信账本页 XML 失败: %w", err) + } + // 扁平收集所有带文本的节点(微信账本行是扁平结构,无嵌套容器) + var nodes []textNode + collectFlatTexts(root, &nodes) + + var rows []AlipayLedgerRow + dateGroup := "" + // dump 输出顺序即屏幕从上到下(xml 深度优先),顺序扫描: + // 行 = 金额节点(触发新行)→ 补时间 → 补付款方;离屏行(bounds [0,0][0,0])过滤。 + // 付款方在金额之前出现([210,y] vs [900,y]),但此时 pending 未创建, + // 所以用"最近未消费的候选付款方"机制:非金额/时间/日期/干扰词的短文本先暂存, + // 遇到金额时若暂存候选存在且 y 相近则归属该行。 + var payerCandidate string // 金额节点前最近的付款方候选 + var pending *AlipayLedgerRow + var noteCandidate string // 备注文本候选(出现在金额之前则暂存,归属下一个 pending 行) + for _, n := range nodes { + switch { + case wechatDateRe.MatchString(n.text): + // 日期分组行:更新当前日期上下文,结束未完成行,清候选 + flushWechatRow(&rows, &pending, payerCandidate) + dateGroup = n.text + payerCandidate = "" + case wechatAmountRe.MatchString(n.text): + // 金额行:新行起点,归属最近的付款方候选(若有) + flushWechatRow(&rows, &pending, payerCandidate) + pending = &AlipayLedgerRow{ + Date: dateGroup, Payer: payerCandidate, Amount: n.text, + AmountMinor: wechatAmountMinor(n.text), onScreen: !zeroBoundsRe.MatchString(n.bounds), + } + if noteCandidate != "" { + pending.Note = noteCandidate + noteCandidate = "" + } + payerCandidate = "" + case ledgerTimeRe.MatchString(n.text) && pending != nil && pending.Time == "": + // 时间行:归属当前待完成行 + pending.Time = n.text + case wechatNoteRe.MatchString(n.text): + // 备注行:归属当前 pending(若有),否则作为候选归属下一个 pending 行 + m := wechatNoteRe.FindStringSubmatch(n.text) + note := m[1] + if pending != nil { + pending.Note = note + noteCandidate = "" + } else { + noteCandidate = note + } + case wechatPayerText(n.text): + // 付款方候选(非金额/时间/日期/干扰词的短文本) + payerCandidate = n.text + } + } + flushWechatRow(&rows, &pending, payerCandidate) + return rows, nil +} + +// wechatPayerText 判断文本是否为付款方候选(短脱敏名,非金额/时间/日期/干扰词) +func wechatPayerText(text string) bool { + if text == "" || len([]rune(text)) > 8 { + return false + } + if wechatDateRe.MatchString(text) || ledgerTimeRe.MatchString(text) || wechatAmountRe.MatchString(text) { + return false + } + if wechatNonPayerRe.MatchString(text) { + return false + } + return true +} + +// textNode 带 bounds 的扁平文本节点 +type textNode struct { + text string + bounds string +} + +// collectFlatTexts 扁平收集所有带文本的节点(按 dump 顺序即屏幕从上到下)。 +func collectFlatTexts(n *uiNode, out *[]textNode) { + if n == nil { + return + } + if n.Text != "" { + *out = append(*out, textNode{n.Text, n.Bounds}) + } + for _, k := range n.Kids { + collectFlatTexts(k, out) + } +} + +// flushWechatRow 结束当前待完成行并加入结果(缺时间/金额/离屏的行丢弃) +func flushWechatRow(rows *[]AlipayLedgerRow, pending **AlipayLedgerRow, payerCandidate string) { + p := *pending + if p == nil { + return + } + if p.Payer == "" { + p.Payer = payerCandidate + } + if p.Time != "" && p.Amount != "" && p.onScreen { + *rows = append(*rows, *p) + } + *pending = nil +} + +// wechatAmountMinor 微信金额文本转分("¥39.00" → 3900;"¥100" → 10000) +func wechatAmountMinor(amount string) int64 { + s := strings.TrimPrefix(amount, "¥") + if i := strings.IndexByte(s, '.'); i >= 0 { + major, _ := strconv.ParseInt(s[:i], 10, 64) + frac := s[i+1:] + minor := int64(0) + switch len(frac) { + case 1: + minor, _ = strconv.ParseInt(frac+"0", 10, 64) + case 2: + minor, _ = strconv.ParseInt(frac, 10, 64) + } + return major*100 + minor + } + major, _ := strconv.ParseInt(s, 10, 64) + return major * 100 +} diff --git a/internal/admin/capture/wechat_ledger_test.go b/internal/admin/capture/wechat_ledger_test.go new file mode 100644 index 0000000..fa954ce --- /dev/null +++ b/internal/admin/capture/wechat_ledger_test.go @@ -0,0 +1,240 @@ +package capture + +import ( + "strings" + "testing" +) + +// 微信账本页真实 dump 片段(2026-08-29 真机红米K60,无障碍开启后 XWeb 暴露节点树)。 +// 覆盖:多日期分组、金额 ¥ 前缀、付款方脱敏、备注行、离屏虚拟化行。 +const wechatLedgerSampleXML = ` + + + + + + + + + + + + + + + + + + + + + + + +` + +// 含离屏虚拟化行(bounds [0,0][0,0])的片段:应被过滤 +const wechatLedgerVirtualXML = ` + + + + + + + +` + +func TestReadWechatLedger(t *testing.T) { + rows, err := ReadWechatLedger(wechatLedgerSampleXML) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) != 3 { + t.Fatalf("应解析出 3 行,实际 %d 行: %+v", len(rows), rows) + } + + // 第 1 行:7月1日 / 朱*6 / ¥39.00 / 17:24:47 + r0 := rows[0] + if r0.Date != "7月1日" || r0.Payer != "朱*6" || r0.Amount != "¥39.00" || r0.Time != "17:24:47" { + t.Errorf("第1行解析错误: %+v", r0) + } + if r0.AmountMinor != 3900 { + t.Errorf("第1行金额应为 3900 分,实际 %d", r0.AmountMinor) + } + + // 第 2 行:6月3日 / *学 / ¥100.00 / 15:36:32(金额无小数补零) + r1 := rows[1] + if r1.Date != "6月3日" || r1.Payer != "*学" || r1.Amount != "¥100.00" || r1.Time != "15:36:32" { + t.Errorf("第2行解析错误: %+v", r1) + } + if r1.AmountMinor != 10000 { + t.Errorf("第2行金额应为 10000 分,实际 %d", r1.AmountMinor) + } + + // 第 3 行:9月14日 / *游 / ¥200.00 / 16:13:03 + r2 := rows[2] + if r2.Date != "9月14日" || r2.Payer != "*游" || r2.Amount != "¥200.00" || r2.Time != "16:13:03" { + t.Errorf("第3行解析错误: %+v", r2) + } + if r2.AmountMinor != 20000 { + t.Errorf("第3行金额应为 20000 分,实际 %d", r2.AmountMinor) + } + + // 备注行"付款方备注:电信这回"应捕获到 r2.Note,且不污染 Payer + if r2.Note != "电信这回" { + t.Errorf("备注应进入 r2.Note,实际 %q", r2.Note) + } + if strings.Contains(r2.Payer, "备注") { + t.Errorf("备注文本不应进入付款方: %q", r2.Payer) + } + + // 指纹(时间|金额)格式与支付宝一致 + if fp := r0.Fingerprint(); fp != "17:24:47|¥39.00" { + t.Errorf("指纹格式错误: %q", fp) + } +} + +func TestReadWechatLedgerFiltersVirtualRows(t *testing.T) { + rows, err := ReadWechatLedger(wechatLedgerVirtualXML) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) != 0 { + t.Fatalf("离屏虚拟化行应被过滤(金额/时间 bounds [0,0][0,0]),实际 %d 行: %+v", len(rows), rows) + } +} + +func TestReadWechatLedgerEmpty(t *testing.T) { + // 无障碍未开启 → 微信 dump 黑盒(仅空 FrameLayout)→ 0 行 + rows, err := ReadWechatLedger(``) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) != 0 { + t.Fatalf("空树应返回 0 行,实际 %d", len(rows)) + } +} + +// TestReadWechatLedgerNoteBeforeAmount 备注行出现在金额之前:应作为 noteCandidate 暂存, +// 在下一个 pending 行(金额触发)创建时挂到该行的 Note 上。 +func TestReadWechatLedgerNoteBeforeAmount(t *testing.T) { + const xml = ` + + + + + + + + +` + rows, err := ReadWechatLedger(xml) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) != 1 { + t.Fatalf("期望 1 行,实际 %d 行: %+v", len(rows), rows) + } + r := rows[0] + if r.Payer != "*游" || r.Amount != "¥200.00" || r.Time != "16:13:03" { + t.Errorf("行基础字段错误: %+v", r) + } + if r.Note != "先备后付" { + t.Errorf("备注应挂到下一行 Note,实际 %q", r.Note) + } +} + +// TestReadWechatLedgerTwoRemarksConsecutive 两笔相邻收款各自带备注,备注必须各自归位, +// 不能跨行漂移、不能合并、不能成为伪行。 +func TestReadWechatLedgerTwoRemarksConsecutive(t *testing.T) { + const xml = ` + + + + + + + + + + + + +` + rows, err := ReadWechatLedger(xml) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) != 2 { + t.Fatalf("期望 2 行(备注不形成伪行),实际 %d 行: %+v", len(rows), rows) + } + if rows[0].Payer != "甲" || rows[0].Amount != "¥10.00" || rows[0].Time != "10:00:00" { + t.Errorf("第 1 行基础字段错误: %+v", rows[0]) + } + if rows[0].Note != "甲备注码" { + t.Errorf("第 1 行 Note 应为 %q,实际 %q", "甲备注码", rows[0].Note) + } + if rows[1].Payer != "乙" || rows[1].Amount != "¥20.00" || rows[1].Time != "10:05:00" { + t.Errorf("第 2 行基础字段错误: %+v", rows[1]) + } + if rows[1].Note != "乙备注码" { + t.Errorf("第 2 行 Note 应为 %q,实际 %q", "乙备注码", rows[1].Note) + } +} + +// TestReadWechatLedgerNoteDoesNotChangeRowCount 备注行不构成独立行:含 2 笔收款 + 3 条备注 +// 的 dump 必须仍解析为 2 行。 +func TestReadWechatLedgerNoteDoesNotChangeRowCount(t *testing.T) { + const xml = ` + + + + + + + + + + + + + +` + rows, err := ReadWechatLedger(xml) + if err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(rows) != 2 { + t.Fatalf("期望 2 行(3 条备注均不构成伪行),实际 %d 行: %+v", len(rows), rows) + } + // 第 1 行:3 条备注全部挂到 pending 行,最后一条胜出 + if rows[0].Note != "备注C" { + t.Errorf("第 1 行 Note 应为最后一条 %q,实际 %q", "备注C", rows[0].Note) + } + if rows[1].Note != "" { + t.Errorf("第 2 行 Note 应为空(无备注行),实际 %q", rows[1].Note) + } +} + +// TestDegenerateDump 退化空 dump(无障碍未填充→只有根 FrameLayout)vs 真实渲染空页: +// 前者是'读取失败'不得建基线,后者(有标题/tab静态文本)可正常建空基线。 +func TestDegenerateDump(t *testing.T) { + degen := `` + if !DegenerateDump(degen) { + t.Errorf("退化空树应被判定为退化,实际返回 false") + } + // 真实渲染空页:有标题/页脚静态文本(即使 0 交易行)→ 非退化 + rendered := ` + + + + +` + if DegenerateDump(rendered) { + t.Errorf("真实渲染空页(有标题/页脚文本)不应被判为退化,实际返回 true") + } + // 正常有交易行的 dump 显然非退化 + if DegenerateDump(wechatLedgerSampleXML) { + t.Errorf("正常含交易行的 dump 不应被判为退化") + } +} diff --git a/internal/admin/deviceconsole.go b/internal/admin/deviceconsole.go new file mode 100644 index 0000000..68a6b87 --- /dev/null +++ b/internal/admin/deviceconsole.go @@ -0,0 +1,415 @@ +package admin + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// 设备控制台:web 前端可视化操作 adb 触达的安卓设备(USB 真机 / adb over WiFi; +// 微信 / 支付宝) +// +// 能力(按需渲染,非完整云手机): +// - 截图:查看设备当前屏幕(渲染微信 / 支付宝界面) +// - 操作:点击 / 滑动 / 输入文本 / 按键(完成 app 登录等) +// - 启动 app:拉起微信 / 支付宝 +// - 手动预热:导航到账本页并锁定任务(DeviceActor 命令,账本监听专用) +// +// 前置条件:设备已配置 adb_host / adb_wifi_host(USB 真机留空 adb_port 即视作 USB 直连)。 +// 本控制台以 adb 为协议基线,USB 直连(纯 serial)与 adb over WiFi(TCP adb)均适用。 + +// deviceSerial 从设备记录构造 adb serial(USB 优先,WiFi 备用) +// - adb_host 无端口 → USB 直连 serial(如 "4f2bdc30",真机直连) +// - adb_host 有端口 → TCP 地址(如 "192.168.1.119:5555",adb over WiFi) +// - adb_wifi_host 配置且 USB 通道不可用 → WiFi 备用 +func (h *PersonalHandler) deviceSerial(d *personal.DeviceEncrypted) (string, error) { + if d.AdbHost != "" { + if d.AdbPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort), nil + } + return d.AdbHost, nil + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort), nil + } + return "", fmt.Errorf("设备未配置 adb 连接(adb_host / adb_wifi_host 为空)") +} + +// getDeviceWithAdb 按 id 取设备并校验 adb 可用(USB 优先,失败自动切 WiFi 备用) +func (h *PersonalHandler) getDeviceWithAdb(c *gin.Context) (*personal.DeviceEncrypted, string, bool) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return nil, "", false + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "设备控制台未启用(adb 管理器未配置)"}) + return nil, "", false + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return nil, "", false + } + serial, all, ok := h.pickOnlineSerial(c.Request.Context(), dev) + if !ok { + c.JSON(http.StatusBadGateway, gin.H{"error": "设备离线:所有 adb 通道不可用(" + strings.Join(all, " / ") + ")"}) + return nil, "", false + } + return dev, serial, true +} + +// pickOnlineSerial 取设备所有候选 serial(USB 主通道优先、WiFi 备用次之)并返回首个 online 的。 +func (h *PersonalHandler) pickOnlineSerial(ctx context.Context, dev *personal.DeviceEncrypted) (serial string, allSerials []string, ok bool) { + all := make([]string, 0, 2) + if dev.AdbHost != "" { + if dev.AdbPort > 0 { + all = append(all, fmt.Sprintf("%s:%d", dev.AdbHost, dev.AdbPort)) + } else { + all = append(all, dev.AdbHost) + } + } + if dev.AdbWifiHost != "" && dev.AdbWifiPort > 0 { + all = append(all, fmt.Sprintf("%s:%d", dev.AdbWifiHost, dev.AdbWifiPort)) + } + if len(all) == 0 { + return "", all, false + } + for _, s := range all { + if h.adbMgr.Online(ctx, s) { + return s, all, true + } + } + return "", all, false +} + +// handleDeviceScreenshot 设备截图(按需渲染:前端点开设备才拉取) +// GET /api/admin/personal/devices/:id/screenshot → PNG +func (h *PersonalHandler) handleDeviceScreenshot(c *gin.Context) { + dev, serial, ok := h.getDeviceWithAdb(c) + if !ok { + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 20*time.Second) + defer cancel() + + img, err := h.adbMgr.Screenshot(ctx, serial) + if err != nil { + logger.Warn("admin", "设备截图失败", "device", dev.DeviceID, "serial", serial, "err", err) + c.JSON(http.StatusBadGateway, gin.H{"error": "设备截图失败: " + err.Error()}) + return + } + c.Header("Content-Type", "image/png") + c.Header("Cache-Control", "no-store") + c.Data(http.StatusOK, "image/png", img) +} + +// handleDeviceAction 设备操作(点击/滑动/输入/按键/启动 app/刷新前台) +// POST /api/admin/personal/devices/:id/action {"action":"tap|swipe|input|key|start|front", ...} +func (h *PersonalHandler) handleDeviceAction(c *gin.Context) { + dev, serial, ok := h.getDeviceWithAdb(c) + if !ok { + return + } + var req struct { + Action string `json:"action"` + X int `json:"x"` + Y int `json:"y"` + X2 int `json:"x2"` + Y2 int `json:"y2"` + Duration int `json:"duration"` // swipe 毫秒 + Text string `json:"text"` // input 文本 + Keycode int `json:"keycode"` // key 按键 + Package string `json:"package"` // start 包名 + Activity string `json:"activity"` // start activity(可选) + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + var err error + switch req.Action { + case "tap": + err = h.adbMgr.Tap(ctx, serial, req.X, req.Y) + case "swipe": + err = h.adbMgr.Swipe(ctx, serial, req.X, req.Y, req.X2, req.Y2, req.Duration) + case "input": + if req.Text == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "input 需要 text"}) + return + } + err = h.adbMgr.InputText(ctx, serial, req.Text) + case "key": + if req.Keycode == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "key 需要 keycode(4=返回 3=主页 66=回车 28=删除)"}) + return + } + err = h.adbMgr.KeyEvent(ctx, serial, req.Keycode) + case "start": + if req.Package == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "start 需要 package"}) + return + } + err = h.adbMgr.StartApp(ctx, serial, req.Package, req.Activity) + case "front": + var pkg string + pkg, err = h.adbMgr.ForegroundApp(ctx, serial) + if err == nil { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "foreground": pkg}}) + return + } + case "warmup": + // 手动预热:向设备 DeviceActor 投递 ManualWarmupCmd(冷启动导航到账本页 + 锁定任务)。 + // provider 参数指定通道:alipay(默认,设备全部支付宝账户主空间+双开) + // 或 wechat(设备全部微信账户);provider 为空 = 全部通道全部账户。 + // DeviceActor 模型:手动预热是队列中的一个命令,与自动预热/轮询天然互斥。 + // 冷启动导航等待波动(网络/UI 渲染非线性),微信小账本为小程序加载更慢—— + // 每步 3s + 重试,多账户累计可能超 120s,给足 240s 余量。 + ctx, cancel2 := context.WithTimeout(c.Request.Context(), 240*time.Second) + defer cancel2() + providers := map[string]bool{} + switch req.Package { + case "wechat": + providers["wechat"] = true + case "alipay": + providers["alipay"] = true + default: + providers["alipay"] = true + providers["wechat"] = true + } + if h.ledgerWarmup == nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "账本监听未启用(服务端未配置 adb)"}) + return + } + if werr := h.ledgerWarmup(ctx, dev.DeviceID, providers); werr != nil { + logger.Warn("admin", "手动预热失败", "device", dev.DeviceID, "err", werr) + c.JSON(http.StatusBadGateway, gin.H{"error": "手动预热失败: " + werr.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "ok": true, "note": "账本页已就绪,任务卡片已锁定", + }}) + return + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "未知 action: " + req.Action}) + return + } + if err != nil { + logger.Warn("admin", "设备操作失败", "device", dev.DeviceID, "action", req.Action, "err", err) + c.JSON(http.StatusBadGateway, gin.H{"error": "设备操作失败: " + err.Error()}) + return + } + logger.Info("admin", "设备操作成功", "device", dev.DeviceID, "serial", serial, "action", req.Action) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} + +// deviceAlipayUserIDs 解析设备全部支付宝账户的用户空间(0=主空间,999=双开): +// 遍历设备绑定账户中的 alipay 账户,取其收款码与该账户 QRContent 比对(与 +// cmd/gateway resolveLedgerDevices 的 ledgerUserSpaceOf 同逻辑),去重返回 userID 列表。 +func (h *PersonalHandler) deviceProviderUserIDs(c *gin.Context, dev *personal.DeviceEncrypted, channel string) ([]int, error) { + ctx := c.Request.Context() + ids := store.DeviceAccountIDs(ctx, h.pdb, dev.ID, dev.AccountID) + platform := "alipay" + if channel == "personal.wechat" { + platform = "wechat" + } + var out []int + seen := map[int]bool{} + for _, aid := range ids { + acct := h.pool.GetByID(aid) + if acct == nil || acct.Channel != channel { + continue + } + if acct.QRContent == "" { + return nil, fmt.Errorf("账户 %d 未配置收款码(无法判定用户空间)", aid) + } + for _, space := range []int{999, 0} { + qr, err := h.pdb.GetLatestQrcodeByPlatformUser(ctx, dev.DeviceID, platform, space) + if err == nil && qr != nil && qr.QRContent == acct.QRContent && !seen[space] { + seen[space] = true + out = append(out, space) + } + } + } + if len(out) == 0 { + return nil, fmt.Errorf("设备未绑定 %s 账户", platform) + } + return out, nil +} + +// handleRestartDevice 重启设备(设备离线自愈 / 用户手动触发) +// +// POST /api/admin/personal/devices/:id/restart +// +// 2026-08-30 重构:监听端 APP 弃用,恢复链路收敛为纯 adb 可达性自愈—— +// 轮询 adb.Online 直到任一 serial 可用(每 5s×9 = 45s),45s 内恢复即成功; +// 始终不可达返回 ok=false + warning。不会直接修改设备 status, +// 设备恢复后由 DeviceWatcher 按 adb 可达性自动判定上线。 +func (h *PersonalHandler) handleRestartDevice(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "adb 管理器未配置,重启功能不可用"}) + return + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + + result, rerr := h.Restart(c.Request.Context(), dev) + if rerr != nil && result == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": rerr.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": result}) +} + +// AttemptRestart 按 id 重启设备(实现 reconciler.Recoverer 接口)。 +// 用于 DeviceWatcher 自动自愈;限频在内部已处理。 +func (h *PersonalHandler) AttemptRestart(ctx context.Context, id int64) error { + if h.adbMgr == nil { + return fmt.Errorf("adb 管理器未配置") + } + dev, err := h.pdb.GetDeviceByID(ctx, id) + if err != nil { + return fmt.Errorf("查询设备失败: %w", err) + } + if dev == nil { + return fmt.Errorf("设备 %d 不存在", id) + } + if _, rerr := h.Restart(ctx, dev); rerr != nil { + return rerr + } + return nil +} + +// Restart 公开重启入口,供 admin HTTP handler 和 DeviceWatcher 自动自愈复用。 +// 限频由本方法负责(in-memory map + TTL),调用方不必关心防抖。 +// 返回的 map 结构稳定可序列化为 JSON 给 HTTP 响应;err 仅在「无法开始重启流程」时返回, +// 例:adb 管理器未配置 / serial 候选空(HTTP 侧据此返回 400)。 +// +// 2026-08-30 重构:监听端 APP 弃用,重启链路收敛为纯 adb 可达性自愈—— +// 轮询 adb.Online 直到任一 serial 可用(每 5s×9 = 45s);45s 内恢复即成功, +// 始终不可达则返回 ok=false + warning(由调用方/DeviceWatcher 按告警策略处理)。 +func (h *PersonalHandler) Restart(ctx context.Context, dev *personal.DeviceEncrypted) (map[string]any, error) { + if h.adbMgr == nil { + return nil, fmt.Errorf("adb 管理器未配置") + } + serials := h.deviceSerials(dev) + if len(serials) == 0 { + return nil, fmt.Errorf("设备未配置 adb 连接(adb_host / adb_wifi_host 为空)") + } + + // 限频:5 分钟内同设备只允许一次重启尝试;recoverer 可由 watcher 频繁触发。 + if !h.restartLimiter.allow(dev.ID, 5*time.Minute) { + return map[string]any{ + "ok": true, + "serials": serials, + "throttled": true, + "hint": "距上次重启尝试不足 5 分钟,已忽略本次请求", + }, nil + } + + cctx, cancel := context.WithTimeout(ctx, 90*time.Second) + defer cancel() + + resp := map[string]any{ + "ok": true, + "serials": serials, + "stages": []map[string]any{}, + "warnings": []string{}, + } + stages := []map[string]any{} + warnings := []string{} + + // 阶段 1:轮询 adb.Online(每 5s/次,共 9 次) + serial := "" + var online bool + var polls int + for polls = 0; polls < 9; polls++ { + for _, s := range serials { + if h.adbMgr.Online(cctx, s) { + serial = s + online = true + break + } + } + if online { + break + } + time.Sleep(5 * time.Second) + } + if online { + resp["online_recovered"] = true + resp["waited_seconds"] = (polls + 1) * 5 + stages = append(stages, map[string]any{"stage": "adb探测", "online": true, "serial": serial}) + } else { + resp["ok"] = false + resp["waited_seconds"] = 45 + warnings = append(warnings, "45s 内 adb 未恢复在线(USB/WiFi 通道均不可达)") + } + + resp["stages"] = stages + if len(warnings) > 0 { + resp["warnings"] = warnings + } + logger.Info("admin", "设备重启指令执行完成", "device", dev.DeviceID, "ok", resp["ok"], + "online_recovered", resp["online_recovered"], "stages", len(stages)) + return resp, nil +} + +// restartLimiter 限频器:每设备 5min/次的重启尝试上限;进程重启即清空,可接受。 +type restartLimiter struct { + mu sync.Mutex + last map[int64]time.Time +} + +func newRestartLimiter() *restartLimiter { + return &restartLimiter{last: make(map[int64]time.Time)} +} + +// allow 返回 true 表示放行,false 表示被限频。 +func (r *restartLimiter) allow(id int64, window time.Duration) bool { + r.mu.Lock() + defer r.mu.Unlock() + if t, ok := r.last[id]; ok && time.Since(t) < window { + return false + } + r.last[id] = time.Now() + return true +} + +// deviceSerials 返回该设备的所有 adb serial 候选(USB 主通道 + WiFi 备用通道)。 +// 用于本机指令在通道间依次尝试;与 getDeviceWithAdb 一致。 +func (h *PersonalHandler) deviceSerials(d *personal.DeviceEncrypted) []string { + out := make([]string, 0, 2) + if d.AdbHost != "" { + if d.AdbPort > 0 { + out = append(out, fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort)) + } else { + out = append(out, d.AdbHost) + } + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + out = append(out, fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort)) + } + return out +} diff --git a/internal/admin/loginlimiter.go b/internal/admin/loginlimiter.go new file mode 100644 index 0000000..6ac379f --- /dev/null +++ b/internal/admin/loginlimiter.go @@ -0,0 +1,185 @@ +package admin + +import ( + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/logger" +) + +// 登录失败退避限速参数。 +const ( + loginMaxFailures = 5 // 窗口内失败次数上限,达到即锁定 + loginWindow = 15 * time.Minute // 滑动窗口长度,同时作为锁定时长 +) + +// loginAttempt 单条失败记录的时间戳。 +type loginAttempt struct { + ts time.Time +} + +// loginBucket 单个键(IP 或 username)的失败记录桶。 +type loginBucket struct { + mu sync.Mutex + failures []time.Time // 滑动窗口内的失败时间戳 + lockedUntil time.Time // 锁定截止时间,零值表示未锁定 +} + +// loginLimiter 全局登录失败退避限速器。 +// 采用 (IP, username) 双维度独立计数:单 IP 撞多个 username 会被锁,单 username 被多 IP 撞也会被锁。 +// 设计目的:阻止暴力破解与用户名枚举。多实例部署需在外层(如 nginx)共享状态,本实现只覆盖单进程场景。 +type loginLimiter struct { + mu sync.Mutex + buckets map[string]*loginBucket +} + +func newLoginLimiter() *loginLimiter { + return &loginLimiter{buckets: make(map[string]*loginBucket)} +} + +// bucket 取或建桶。调用方负责加锁外层。 +func (l *loginLimiter) bucket(key string) *loginBucket { + b, ok := l.buckets[key] + if !ok { + b = &loginBucket{} + l.buckets[key] = b + } + return b +} + +// prune 删除超过 loginWindow 未活动的桶,避免长期泄漏内存。 +func (l *loginLimiter) prune(now time.Time) { + cutoff := now.Add(-loginWindow) + for key, b := range l.buckets { + b.mu.Lock() + stale := len(b.failures) == 0 && (b.lockedUntil.IsZero() || b.lockedUntil.Before(cutoff)) + b.mu.Unlock() + if stale { + delete(l.buckets, key) + } + } +} + +// checkLocked 返回 (是否锁定, 锁定剩余秒)。 +func (l *loginLimiter) checkLocked(key string, now time.Time) (bool, int) { + l.mu.Lock() + b := l.bucket(key) + l.mu.Unlock() + + b.mu.Lock() + defer b.mu.Unlock() + if b.lockedUntil.IsZero() || b.lockedUntil.Before(now) { + return false, 0 + } + return true, int(b.lockedUntil.Sub(now).Seconds()) +} + +// recordFailure 记录一次失败,超过阈值则置锁定。 +func (l *loginLimiter) recordFailure(key string, now time.Time) { + l.mu.Lock() + if now.IsZero() || len(l.buckets)%50 == 0 { + l.prune(now) + } + b := l.bucket(key) + l.mu.Unlock() + + b.mu.Lock() + defer b.mu.Unlock() + cutoff := now.Add(-loginWindow) + keep := b.failures[:0] + for _, t := range b.failures { + if t.After(cutoff) { + keep = append(keep, t) + } + } + b.failures = append(keep, now) + if len(b.failures) >= loginMaxFailures { + b.lockedUntil = now.Add(loginWindow) + logger.Warn("admin", "登录失败次数达上限,已锁定", "key", key, "failures", len(b.failures), "lock_sec", int(loginWindow.Seconds())) + } +} + +// recordSuccess 清零该键的失败记录与锁定状态。 +func (l *loginLimiter) recordSuccess(key string) { + l.mu.Lock() + b, ok := l.buckets[key] + l.mu.Unlock() + if !ok { + return + } + b.mu.Lock() + b.failures = nil + b.lockedUntil = time.Time{} + b.mu.Unlock() +} + +// enforceLoginLimit 在 handleLogin 入口调用。 +// 返回 true 表示已被限速、调用方应直接 return(已写 429 响应)。 +// ipKey / userKey 各自独立触发,取较严格的判定。 +func (s *Server) enforceLoginLimit(c *gin.Context, username string) bool { + now := time.Now() + ip := c.ClientIP() + + ipKey := "ip:" + ip + if locked, remain := s.loginLimiter.checkLocked(ipKey, now); locked { + logger.Warn("admin", "登录被限速(IP 锁定中)", "ip", ip, "remain_sec", remain) + c.Header("Retry-After", itoa(remain)) + c.JSON(http.StatusTooManyRequests, gin.H{"error": "登录尝试过于频繁,请稍后再试", "retry_after_sec": remain}) + return true + } + userKey := "user:" + username + if username != "" { + if locked, remain := s.loginLimiter.checkLocked(userKey, now); locked { + logger.Warn("admin", "登录被限速(用户名锁定中)", "username", username, "remain_sec", remain) + c.Header("Retry-After", itoa(remain)) + c.JSON(http.StatusTooManyRequests, gin.H{"error": "该账号尝试过于频繁,请稍后再试", "retry_after_sec": remain}) + return true + } + } + return false +} + +// recordLoginOutcome 在 handleLogin 内判定登录结果后调用, +// failure=true 记录失败,failure=false 清零计数。 +// ip 与 username 双维度同步,避免单维度绕过。 +func (s *Server) recordLoginOutcome(c *gin.Context, username string, failure bool) { + now := time.Now() + ip := c.ClientIP() + if failure { + s.loginLimiter.recordFailure("ip:"+ip, now) + if username != "" { + s.loginLimiter.recordFailure("user:"+username, now) + } + return + } + s.loginLimiter.recordSuccess("ip:" + ip) + if username != "" { + s.loginLimiter.recordSuccess("user:" + username) + } +} + +// itoa 简单十进制转字符串,避免引入 strconv 只为一处。 +func itoa(n int) string { + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var buf [12]byte + i := len(buf) + for n > 0 { + i-- + buf[i] = byte('0' + n%10) + n /= 10 + } + if neg { + i-- + buf[i] = '-' + } + return string(buf[i:]) +} diff --git a/internal/admin/personal.go b/internal/admin/personal.go new file mode 100755 index 0000000..b3fe9f5 --- /dev/null +++ b/internal/admin/personal.go @@ -0,0 +1,721 @@ +package admin + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "paygateway/internal/account" + "paygateway/internal/adbmgr" + "paygateway/internal/admin/capture" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// PersonalHandler 个人免签通道管理:监听设备 + 推送流水 + 无订单收款 +type PersonalHandler struct { + pdb store.PersonalStore + pool *account.AccountPool + // accountDB 账户存储(用于自动创建个人账户) + accountDB *store.AccountDB + // adb 设备操作管理器(web 可视化操作 USB 真机 / adb over WiFi 设备;nil = 未启用) + adbMgr *adbmgr.Manager + // restartLimiter 设备重启 5min/次的限频器(同时被 HTTP handler 和 watcher 自动自愈复用) + restartLimiter *restartLimiter + // renewLimiter 收款码自动补捕限频器(每设备每平台最小间隔,防重捕风暴) + renewLimiter *qrcodeLastRenew + // ledgerWarmup 设备手动预热(main.go 注入;nil = 未启用)。DeviceActor 模型中 + // 手动预热是队列中的一个命令,与自动预热/轮询天然互斥,无需窗口独占。 + ledgerWarmup func(ctx context.Context, deviceID string, providers map[string]bool) error +} + +// SetLedgerWarmup 注入设备手动预热函数(main 设置,warmup 走 DeviceActor 命令队列) +func (h *PersonalHandler) SetLedgerWarmup(fn func(ctx context.Context, deviceID string, providers map[string]bool) error) { + h.ledgerWarmup = fn +} + +// NewPersonalHandler 创建个人通道管理处理器 +// adbMgr: adb 操作管理器(可选,nil = 设备控制台不可用) +func NewPersonalHandler(pdb store.PersonalStore, pool *account.AccountPool, accountDB *store.AccountDB, adbMgr *adbmgr.Manager) *PersonalHandler { + return &PersonalHandler{pdb: pdb, pool: pool, accountDB: accountDB, adbMgr: adbMgr, restartLimiter: newRestartLimiter(), renewLimiter: &qrcodeLastRenew{lasts: make(map[string]time.Time)}} +} + +// RegisterRoutes 注册路由(需在 authMiddleware 组内) +func (h *PersonalHandler) RegisterRoutes(rg *gin.RouterGroup) { + // 监听设备(新设备统一走 USB 引导向导的 wizard-start,手填表单已移除) + rg.GET("/personal/devices", h.handleListDevices) + rg.DELETE("/personal/devices/:id", h.handleDeleteDevice) + rg.POST("/personal/devices/:id/freeze", h.handleFreezeDevice) + rg.POST("/personal/devices/:id/unfreeze", h.handleUnfreezeDevice) + rg.POST("/personal/devices/:id/restart", h.handleRestartDevice) + // 设备控制台(web 可视化操作 USB 真机 / adb over WiFi 设备) + rg.GET("/personal/devices/:id/screenshot", h.handleDeviceScreenshot) + rg.POST("/personal/devices/:id/action", h.handleDeviceAction) + rg.POST("/personal/devices/:id/adb", h.handleSetDeviceAdb) + // 设备 ↔ 账户 多对多绑定(应用双开多账户) + rg.GET("/personal/devices/:id/accounts", h.handleListDeviceAccounts) + rg.POST("/personal/devices/:id/accounts", h.handleAddDeviceAccount) + rg.DELETE("/personal/devices/:id/accounts/:accountId", h.handleRemoveDeviceAccount) + rg.POST("/personal/devices/batch/restart", h.handleBatchRestartDevices) + rg.POST("/personal/devices/batch/status", h.handleBatchSetDeviceStatus) + // 引导向导(USB 真机集成自动化) + rg.GET("/personal/usb-detect", h.handleUSBCheck) + rg.POST("/personal/devices/wizard-start", h.handleWizardStart) + rg.POST("/personal/devices/:id/wizard-wifi", h.handleWizardWifiBackup) + rg.POST("/personal/devices/:id/wizard-verify", h.handleWizardVerify) + rg.POST("/personal/devices/:id/wifi-reconnect", h.handleWifiReconnect) + // 推送流水(审计) + rg.GET("/personal/pushes", h.handleListPushes) + // 无订单收款(人工处理) + rg.GET("/personal/unmatched", h.handleListUnmatched) + rg.POST("/personal/unmatched/:id/resolve", h.handleResolveUnmatched) + // 动态收款码管理 + rg.GET("/personal/qrcodes", h.handleListQrcodes) + rg.DELETE("/personal/qrcodes/:deviceId/:platform/:userId", h.handleDeleteQrcode) + rg.POST("/personal/qrcodes/:deviceId/wechat/capture", h.handleCaptureWechatQrcode) + rg.POST("/personal/qrcodes/:deviceId/alipay/capture", h.handleCaptureAlipayQrcode) + // 导航到收款记录页(监听界面,PLAN-EVENT-MONITOR §2.3) + rg.POST("/personal/devices/:id/navigate-ledger", h.handleNavigateAlipayLedger) +} + +// ========== 监听设备 ========== + +// handleListDevices 设备列表(解密密钥不返回,仅展示状态) +func (h *PersonalHandler) handleListDevices(c *gin.Context) { + devs, err := h.pdb.ListDevices(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + out := make([]gin.H, 0, len(devs)) + for _, d := range devs { + out = append(out, gin.H{ + "id": d.ID, "device_id": d.DeviceID, "name": d.Name, + "account_id": d.AccountID, "provider": d.Provider, + "status": d.Status, + "last_push": d.LastPush, "ip": d.IP, "created_at": d.CreatedAt, + "adb_host": d.AdbHost, "adb_port": d.AdbPort, + "adb_wifi_host": d.AdbWifiHost, "adb_wifi_port": d.AdbWifiPort, + "setup_progress": personal.ParseSetupProgress(d.SetupProgress), + // P2P 控制面字段(docs/P2P-ADB-RECOVERY.md Phase 1) + "ts_ip": d.TsIP, + "adb_listen": d.AdbListen, + "control_state": d.ControlState, + }) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"devices": out}}) +} + +// accountHasConnectedDevice 判断指定账户是否已有 **adb 可达**的设备。 +// 用于完成度门槛守恒:新建/删除设备时若账户没有 adb 在线设备则冻结账户,避免账户裸奔接单。 +// 在线判定 = 实时 adb 连通性(2026-08-29 重构:不再依赖监听 App 心跳/setup 进度)。 +func (h *PersonalHandler) accountHasConnectedDevice(ctx context.Context, accountID int64) bool { + devs, err := h.pdb.ListDevices(ctx) + if err != nil { + return false + } + for _, d := range devs { + if d.AccountID != accountID { + continue + } + serial := adbSerialOf(d) + if serial != "" && h.adbMgr != nil && h.adbMgr.Online(ctx, serial) { + return true + } + } + return false +} + +// adbSerialOf 解析设备 adb serial(TCP host:port 或 USB 纯 serial) +func adbSerialOf(d *personal.DeviceEncrypted) string { + if d.AdbHost != "" { + if d.AdbPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort) + } + return d.AdbHost + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort) + } + return "" +} + +// handleSetDeviceAdb 设置设备 adb 连接信息(USB serial/host:port + WiFi 备用通道) +// POST /api/admin/personal/devices/:id/adb {adb_host, adb_port, adb_wifi_host?, adb_wifi_port?} +// adb_host 可为 USB 直连 serial(无端口,此时 adb_port=0)或 host:port +func (h *PersonalHandler) handleSetDeviceAdb(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + var req struct { + AdbHost string `json:"adb_host"` + AdbPort int `json:"adb_port"` + AdbWifiHost string `json:"adb_wifi_host"` + AdbWifiPort int `json:"adb_wifi_port"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.AdbHost == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误:需要 adb_host(USB serial 或 host)"}) + return + } + if req.AdbPort > 65535 || req.AdbWifiPort > 65535 { + c.JSON(http.StatusBadRequest, gin.H{"error": "端口超出范围"}) + return + } + if err := h.pdb.SetDeviceAdb(c.Request.Context(), id, req.AdbHost, req.AdbPort, req.AdbWifiHost, req.AdbWifiPort); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + logger.Info("admin", "设备 adb 信息已更新", "device_id", id, "adb", req.AdbHost, + "wifi", fmt.Sprintf("%s:%d", req.AdbWifiHost, req.AdbWifiPort)) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} + +// handleListDeviceAccounts 列出设备全部绑定账户(主账户 + 附加账户)。 +// GET /api/admin/personal/devices/:id/accounts +// 响应:{"data":{"primary_account_id":3,"accounts":[{"account_id":3,...},{"account_id":4,...}]}} +func (h *PersonalHandler) handleListDeviceAccounts(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + // 主账户 + 附加账户合并去重,附带账户信息(名称/通道/状态) + ids := store.DeviceAccountIDs(c.Request.Context(), h.pdb, id, dev.AccountID) + out := make([]gin.H, 0, len(ids)) + for _, aid := range ids { + acct := h.pool.GetByID(aid) + if acct == nil { + continue + } + out = append(out, gin.H{ + "account_id": acct.ID, "name": acct.Name, + "channel": acct.Channel, "status": string(acct.Status), + "is_primary": aid == dev.AccountID, + }) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"primary_account_id": dev.AccountID, "accounts": out}}) +} + +// handleAddDeviceAccount 绑定附加账户到设备(应用双开多账户)。 +// POST /api/admin/personal/devices/:id/accounts {account_id} +// 校验:账户存在 + 属于个人通道 + 与设备主账户同通道(双开只区分空间,不混通道)。 +func (h *PersonalHandler) handleAddDeviceAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + var req struct { + AccountID int64 `json:"account_id"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.AccountID <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误:需要 account_id"}) + return + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + acct := h.pool.GetByID(req.AccountID) + if acct == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "账户不存在"}) + return + } + if !isPersonalChannelName(acct.Channel) { + c.JSON(http.StatusBadRequest, gin.H{"error": "账户不是个人通道账户(channel=" + acct.Channel + ")"}) + return + } + if acct.QRContent == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "账户未配置收款码(qr_content),请先上传收款码"}) + return + } + // 允许跨通道绑定:一台设备(双开微信 + 支付宝)可服务多通道账户, + // 监听端按包名识别通道(com.tencent.mm / com.eg.android.AlipayGphone),单实例即可接收全部通知。 + if err := h.pdb.AddDeviceAccount(c.Request.Context(), id, req.AccountID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "绑定账户失败: " + err.Error()}) + return + } + logger.Info("admin", "设备绑定附加账户", "device_id", id, "account_id", req.AccountID, "name", acct.Name) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "account_id": req.AccountID}}) +} + +// handleRemoveDeviceAccount 解绑设备附加账户(主账户不可解绑,需删除设备或改用主账户字段)。 +// DELETE /api/admin/personal/devices/:id/accounts/:accountId +func (h *PersonalHandler) handleRemoveDeviceAccount(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + accountID, _ := strconv.ParseInt(c.Param("accountId"), 10, 64) + if id <= 0 || accountID <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + if accountID == dev.AccountID { + c.JSON(http.StatusBadRequest, gin.H{"error": "主账户不可解绑(请先删除设备或更换主账户)"}) + return + } + if err := h.pdb.RemoveDeviceAccount(c.Request.Context(), id, accountID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "解绑账户失败: " + err.Error()}) + return + } + logger.Info("admin", "设备解绑附加账户", "device_id", id, "account_id", accountID) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} + +func (h *PersonalHandler) handleDeleteDevice(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + // 删前查 account_id 用于守恒门槛:删完后若账户再无在线完成 setup 的设备则冻结账户 + dev, _ := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err := h.pdb.DeleteDevice(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + // 级联清理:收款码记录、账户绑定;仅被该设备绑定的自动账户一并停用, + // 否则幽灵设备的码卡与僵尸账户会永远留在池子里 + if dev != nil { + if err := h.pdb.DeleteQrcodeByDevice(c.Request.Context(), dev.DeviceID); err != nil { + logger.Warn("admin", "清理设备收款码失败", "device_id", dev.DeviceID, "err", err) + } + h.unbindAndRetireAccounts(c.Request.Context(), id, dev) + } + if dev != nil && dev.AccountID > 0 && !h.accountHasConnectedDevice(c.Request.Context(), dev.AccountID) { + if a := h.pool.GetByID(dev.AccountID); a != nil && a.Status == account.StatusNormal { + // 无在线设备 → 按离线联动语义冻结:新设备 wizard 接入 adb 可达时自动解冻 + h.pool.FreezeByOffline(dev.AccountID) + logger.Info("admin", "账户已冻结(删除最后一台在线设备)", "account_id", dev.AccountID, "name", a.Name) + } + } + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// handleFreezeDevice 冻结设备(不可接新单,心跳返回 disable 指令) +func (h *PersonalHandler) handleFreezeDevice(c *gin.Context) { + h.setDeviceStatus(c, string(personal.DeviceFrozen)) +} + +// unbindAndRetireAccounts 设备删除后的账户收尾:解绑全部绑定账户; +// 其中"自动创建"且不再属于任何设备的个人账户 → 停用(保留记录可查,管理员可在账户池彻底删除) +func (h *PersonalHandler) unbindAndRetireAccounts(ctx context.Context, devRowID int64, dev *personal.DeviceEncrypted) { + for _, acctID := range store.DeviceAccountIDs(ctx, h.pdb, devRowID, 0) { + if err := h.pdb.RemoveDeviceAccount(ctx, devRowID, acctID); err != nil { + logger.Warn("admin", "解除设备账户绑定失败", "device_row_id", devRowID, "account_id", acctID, "err", err) + continue + } + acct := h.pool.GetByID(acctID) + if acct == nil || !strings.HasSuffix(acct.Name, "自动") || !strings.HasPrefix(acct.Channel, "personal.") { + continue + } + if bound, err := h.pdb.AccountBound(ctx, acctID); err == nil && bound { + continue // 仍被其他设备绑定 + } + h.pool.FreezeByRetire(acctID) + if err := h.accountDB.SetAccountFrozen(ctx, acctID, account.FrozenReasonRetire); err != nil { + logger.Warn("admin", "停用僵尸自动账户失败", "account_id", acctID, "err", err) + continue + } + logger.Info("admin", "已停用无主自动账户(设备删除级联)", "account_id", acctID, "name", acct.Name) + } +} + +// handleUnfreezeDevice 解冻设备 +func (h *PersonalHandler) handleUnfreezeDevice(c *gin.Context) { + h.setDeviceStatus(c, string(personal.DeviceOffline)) +} + +func (h *PersonalHandler) setDeviceStatus(c *gin.Context, status string) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if err := h.pdb.SetDeviceStatus(c.Request.Context(), id, status); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// ========== 推送流水 ========== + +// handleListPushes 收款推送流水(审计) +func (h *PersonalHandler) handleListPushes(c *gin.Context) { + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + pushes, err := h.pdb.ListPushes(c.Request.Context(), limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"pushes": pushes}}) +} + +// ========== 无订单收款 ========== + +// handleListUnmatched 无订单收款列表(pending / resolved / refunded) +func (h *PersonalHandler) handleListUnmatched(c *gin.Context) { + status := c.Query("status") + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + list, err := h.pdb.ListUnmatched(c.Request.Context(), status, limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"unmatched": list}}) +} + +// handleResolveUnmatched 处理无订单收款 +// POST /api/admin/personal/unmatched/:id/resolve {status: resolved|refunded, note} +func (h *PersonalHandler) handleResolveUnmatched(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + var req struct { + Status string `json:"status"` + Note string `json:"note"` + } + if err := c.ShouldBindJSON(&req); err != nil || id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if req.Status != "resolved" && req.Status != "refunded" { + c.JSON(http.StatusBadRequest, gin.H{"error": "status 必须为 resolved 或 refunded"}) + return + } + if err := h.pdb.ResolveUnmatched(c.Request.Context(), id, req.Status, req.Note); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// ---------- 辅助 ---------- + +// isPersonalChannelName 判断账户 channel 是否个人通道(personal.alipay / personal.wechat ...) +func isPersonalChannelName(ch string) bool { + return len(ch) > 9 && ch[:9] == "personal." +} + +// providerFromChannelName 从账户 channel 提取 provider(personal.alipay → alipay) +func providerFromChannelName(ch string) string { + for i := len(ch) - 1; i >= 0; i-- { + if ch[i] == '.' { + return ch[i+1:] + } + } + return ch +} + +// ---------- 批量运维 ---------- + +// batchDeviceReq 批量操作的通用请求体。ids 为空时若 all=true 则作用于全部设备;ids 非空时以 ids 为准。 +type batchDeviceReq struct { + IDs []int64 `json:"ids"` + All bool `json:"all"` +} + +func (h *PersonalHandler) resolveBatchTargets(c *gin.Context) ([]*personal.DeviceEncrypted, []int64, bool) { + var req batchDeviceReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return nil, nil, false + } + ctx := c.Request.Context() + if len(req.IDs) == 0 { + if !req.All { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误:需提供 ids 或 all=true"}) + return nil, nil, false + } + devs, err := h.pdb.ListDevices(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return nil, nil, false + } + enc := make([]*personal.DeviceEncrypted, len(devs)) + for i, d := range devs { + enc[i] = d + } + return enc, nil, true + } + seen := make(map[int64]bool) + missing := make([]int64, 0) + out := make([]*personal.DeviceEncrypted, 0, len(req.IDs)) + for _, id := range req.IDs { + if seen[id] { + continue + } + seen[id] = true + dev, err := h.pdb.GetDeviceByID(ctx, id) + if err != nil || dev == nil { + missing = append(missing, id) + continue + } + out = append(out, dev) + } + return out, missing, true +} + +func (h *PersonalHandler) batchDeviceError(c *gin.Context, device *personal.DeviceEncrypted, err error) { + logger.Warn("admin", "批量设备操作失败", "device_id", device.ID, "device", device.DeviceID, "err", err) +} + +// POST /api/admin/personal/devices/batch/restart {ids:[...]|all:true} +// 逐个调 h.Restart(内部自带 5min 限频 + 90s 超时),不因单个失败中断其余设备。 +func (h *PersonalHandler) handleBatchRestartDevices(c *gin.Context) { + devs, missing, ok := h.resolveBatchTargets(c) + if !ok { + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "adb 管理器未配置,批量重启不可用"}) + return + } + results := make([]map[string]any, 0, len(devs)) + hasFail := false + for _, dev := range devs { + res, rerr := h.Restart(c.Request.Context(), dev) + if rerr != nil { + h.batchDeviceError(c, dev, rerr) + results = append(results, map[string]any{"device_id": dev.DeviceID, "id": dev.ID, "ok": false, "error": rerr.Error()}) + hasFail = true + continue + } + res["device_id"] = dev.DeviceID + res["id"] = dev.ID + results = append(results, res) + } + code := http.StatusOK + if hasFail { + code = http.StatusMultiStatus + } + c.JSON(code, gin.H{ + "data": gin.H{"results": results, "missing_ids": missing, "total": len(devs)}, + }) +} + +// POST /api/admin/personal/devices/batch/status {ids:[...]|all:true, status:"frozen"|"offline"} +func (h *PersonalHandler) handleBatchSetDeviceStatus(c *gin.Context) { + var req struct { + batchDeviceReq + Status string `json:"status"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + switch req.Status { + case string(personal.DeviceFrozen), string(personal.DeviceOffline): + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "status 必须为 frozen 或 offline"}) + return + } + devs, missing, ok := h.resolveBatchTargets(c) + if !ok { + return + } + results := make([]map[string]any, 0, len(devs)) + hasFail := false + for _, dev := range devs { + entry := map[string]any{"device_id": dev.DeviceID, "id": dev.ID} + if err := h.pdb.SetDeviceStatus(c.Request.Context(), dev.ID, req.Status); err != nil { + h.batchDeviceError(c, dev, err) + entry["ok"] = false + entry["error"] = err.Error() + hasFail = true + } else { + entry["ok"] = true + } + results = append(results, entry) + } + code := http.StatusOK + if hasFail { + code = http.StatusMultiStatus + } + c.JSON(code, gin.H{"data": gin.H{"results": results, "missing_ids": missing, "total": len(devs)}}) +} + +// ========== 动态收款码管理 ========== + +// handleListQrcodes 获取所有设备最新收款码 +func (h *PersonalHandler) handleListQrcodes(c *gin.Context) { + qs, err := h.pdb.ListQrcodes(c.Request.Context(), 100) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": qs}) +} + +// handleDeleteQrcode 移除单条收款码记录(设备退役/换绑后手动清理用) +func (h *PersonalHandler) handleDeleteQrcode(c *gin.Context) { + deviceID := c.Param("deviceId") + platform := c.Param("platform") + userID, _ := strconv.Atoi(c.Param("userId")) + if deviceID == "" || (platform != "alipay" && platform != "wechat") { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if err := h.pdb.DeleteQrcode(c.Request.Context(), deviceID, platform, userID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + logger.Info("admin", "已移除收款码记录", "device_id", deviceID, "platform", platform, "user_id", userID) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} + +// ensureAccountForQR 确保捕获到的收款码有对应的 pay_accounts 记录。 +// 如果账户池中无匹配该码内容的个人账户,自动创建并绑定到设备,使支付通道可用。 +// space: 0=主空间, 999=双开(XSpace)。返回 account_id、是否新建、error。 +func (h *PersonalHandler) ensureAccountForQR(ctx context.Context, dev *personal.DeviceEncrypted, platform, qrContent string, space int) (int64, bool, error) { + if h.accountDB == nil || h.pool == nil || qrContent == "" { + return 0, false, nil + } + return EnsureAccountForQR(ctx, h.pdb, h.pool, h.accountDB, dev, platform, qrContent, space) +} + +// EnsureAccountForQR 全局可复用版(admin handler 与 main.go 监听端上报共用)。 +// 依据:收款码内容匹配账户池 → 无则自动创建 personal.* 账户并绑定设备。 +// space: 0=主空间, 999=双开(XSpace)。同一设备+平台+空间 只保留一个账户: +// 复用已绑定账户(主空间/双开按账户名后缀区分),动态码每次上报不新建重复账户。 +func EnsureAccountForQR(ctx context.Context, pdb store.PersonalStore, pool *account.AccountPool, accountDB *store.AccountDB, dev *personal.DeviceEncrypted, platform, qrContent string, space int) (int64, bool, error) { + if accountDB == nil || pool == nil || qrContent == "" || dev == nil { + return 0, false, nil + } + prefix := "personal.alipay" + if platform == "wechat" { + prefix = "personal.wechat" + } + // 已有匹配账户 → 复用(同一设备+平台+空间,不管码内容是否变化) + // 匹配顺序:① 收款码内容完全一致(最可靠)→ ② 空间标记(账户名含"双开")。 + // 创建时必须写入空间标记,否则下次上报匹配不到 → 重复建户(历史 bug 的根源)。 + var reuse *account.Account + for _, id := range store.DeviceAccountIDs(ctx, pdb, dev.ID, dev.AccountID) { + acct := pool.GetByID(id) + if acct == nil || !strings.HasPrefix(acct.Channel, prefix) { + continue + } + if acct.QRContent == qrContent { + reuse = acct + break + } + } + if reuse == nil { + for _, id := range store.DeviceAccountIDs(ctx, pdb, dev.ID, dev.AccountID) { + acct := pool.GetByID(id) + if acct == nil || !strings.HasPrefix(acct.Channel, prefix) { + continue + } + if space == 0 && !strings.Contains(acct.Name, "双开") { + reuse = acct + break + } + if space == 999 && strings.Contains(acct.Name, "双开") { + reuse = acct + break + } + } + } + if reuse != nil { + // 收款码内容变化 → 更新到账户(保持单一账户跟随最新码) + if reuse.QRContent != qrContent { + // 资金安全防线:新码必须合法(与下单路由同规则),非法内容绝不覆盖账户 + // (脏数据一旦写入,后续下单全部路由到坏码 → 支付全挂,历史教训 2026-08-29) + if reason := account.InvalidQRContent(reuse.Channel, qrContent); reason != "" { + logger.Error("admin", "收款码内容非法,拒绝更新账户(脏数据防线)", + "device", dev.DeviceID, "platform", platform, "account_id", reuse.ID, + "reason", reason, "content", capture.TruncateStr(qrContent, 30)) + return reuse.ID, false, nil + } + qrEnc, err := account.Encrypt([]byte(qrContent)) + if err != nil { + return 0, false, fmt.Errorf("加密收款码内容失败: %w", err) + } + if err := accountDB.UpdateAccount(ctx, &account.AccountEncrypted{ID: reuse.ID, Channel: reuse.Channel, Name: reuse.Name, + AppID: reuse.AppID, MchID: reuse.MchID, QRContentEnc: qrEnc, Status: string(reuse.Status), + Weight: reuse.Weight, Priority: reuse.Priority, AutoBan: reuse.AutoBan, DailyLimit: reuse.DailyLimit, + DailyUsed: reuse.DailyUsed, Group: reuse.Group, IsProd: reuse.IsProd, Remark: reuse.Remark}); err != nil { + return 0, false, fmt.Errorf("更新账户收款码失败: %w", err) + } + reuse.QRContent = qrContent + // GetByID 返回副本,必须写回池;否则池内旧码一直残留(重启前匹配全用旧码) + pool.Update(reuse) + logger.Info("admin", "账户收款码已更新", "device", dev.DeviceID, "platform", platform, "account_id", reuse.ID) + } + return reuse.ID, false, nil + } + // 加密收款码内容 + qrContentEnc, err := account.Encrypt([]byte(qrContent)) + if err != nil { + return 0, false, fmt.Errorf("加密收款码内容失败: %w", err) + } + // 生成账户名:设备名 + 平台 + 空间标记(主空间/双开都标注,管理后台一眼可区分) + devName := dev.Name + if devName == "" { + // 设备未命名时用设备 ID 前 8 位兜底,避免多设备混淆 + devName = dev.DeviceID + if len(devName) > 8 { + devName = devName[:8] + } + } + acctName := devName + if platform == "wechat" { + acctName += " 微信自动" + } else { + acctName += " 支付宝自动" + } + if space == 999 { + acctName += "(双开)" + } else { + acctName += "(主空间)" + } + // 资金安全防线:新建账户的码也必须合法(与更新路径同规则),脏数据不建账户 + if reason := account.InvalidQRContent(prefix, qrContent); reason != "" { + logger.Error("admin", "收款码内容非法,拒绝创建账户(脏数据防线)", + "device", dev.DeviceID, "platform", platform, "user", space, "reason", reason, + "content", capture.TruncateStr(qrContent, 30)) + return 0, false, nil + } + // 创建账户记录 + newID, err := accountDB.CreateAccount(ctx, &account.AccountEncrypted{ + Channel: prefix, + Name: acctName, + QRContentEnc: qrContentEnc, + Status: "normal", + IsProd: true, + }) + if err != nil { + return 0, false, fmt.Errorf("创建个人账户失败: %w", err) + } + // 添加账户到内存池(使支付通道立即可见) + pool.Add(&account.Account{ + ID: newID, + Channel: prefix, + Name: acctName, + QRContent: qrContent, + Status: account.StatusNormal, + IsProd: true, + }) + // 绑定到设备 + if err := pdb.AddDeviceAccount(ctx, dev.ID, newID); err != nil { + return newID, true, fmt.Errorf("绑定账户到设备失败: %w", err) + } + logger.Info("admin", "已自动创建个人账户", "device", dev.DeviceID, "platform", platform, + "account_id", newID, "name", acctName) + return newID, true, nil +} diff --git a/internal/admin/qrcode.go b/internal/admin/qrcode.go new file mode 100755 index 0000000..1dc2051 --- /dev/null +++ b/internal/admin/qrcode.go @@ -0,0 +1,44 @@ +// 收款码图片解析(docs/MIANQIAN.md §7.6 阶段一)。 +// +// 决策 #8/#9:收款码获取 = 双保险(手动上传 + 监听端自动采集) +// 阶段一(最快跑通):管理后台上传收款码图片 → 服务端解析二维码内容 → 绑定账户 +// 用户从支付宝/微信 App「保存收款码」→ 上传 → 自动解析出 qr.alipay.com/xxx 或 weixin://wxpay/... 内容 +package admin + +import ( + "io" + "net/http" + + "github.com/gin-gonic/gin" + + "paygateway/internal/admin/capture" +) + +// maxQRUploadSize 收款码图片上传大小上限(2MB,对齐 capture.MaxQRUploadSize) +const maxQRUploadSize = capture.MaxQRUploadSize + +// handleDecodeQRCode 上传收款码图片 → 解析二维码内容 +// POST /api/admin/accounts/decode-qrcode (multipart/form-data: file) +func (h *AccountHandler) handleDecodeQRCode(c *gin.Context) { + // 限制上传大小(Go 1.19+ 自动处理超限) + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxQRUploadSize+1<<10) + + file, _, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "缺少收款码图片(字段名 file)"}) + return + } + defer file.Close() + + data, err := io.ReadAll(io.LimitReader(file, maxQRUploadSize)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "读取图片失败: " + err.Error()}) + return + } + content, err := capture.DecodeQRContent(data) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"content": content}}) +} diff --git a/internal/admin/qrcode_alipay.go b/internal/admin/qrcode_alipay.go new file mode 100644 index 0000000..bfd36ad --- /dev/null +++ b/internal/admin/qrcode_alipay.go @@ -0,0 +1,145 @@ +// 支付宝收款码捕获 HTTP 层:设备查找 → 调用 capture 包自动化 → 去重 → 入库。 +package admin + +import ( + "context" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/admin/capture" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// handleCaptureAlipayQrcode 通过服务器 adb 捕获支付宝收款码 +// POST /api/personal/qrcodes/:deviceId/alipay/capture?user=0|999 +func (h *PersonalHandler) handleCaptureAlipayQrcode(c *gin.Context) { + deviceID := c.Param("deviceId") + if deviceID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "设备控制台未启用(adb 管理器未配置)"}) + return + } + dev, err := h.pdb.GetDevice(c.Request.Context(), deviceID) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + userId, _ := strconv.Atoi(c.DefaultQuery("user", "0")) + serial, _, ok := h.pickOnlineSerial(c.Request.Context(), dev) + if !ok { + c.JSON(http.StatusBadGateway, gin.H{"error": "设备离线:所有 adb 通道不可用"}) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 90*time.Second) + defer cancel() + + content, cerr := capture.CaptureAlipayQrcode(ctx, h.adbMgr, serial, userId) + if cerr != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": cerr.Error()}) + return + } + // 去重:与库中最新码相同则跳过 + if latest, lerr := h.pdb.GetLatestQrcode(ctx, deviceID); lerr == nil && latest != nil && + latest.Platform == "alipay" && latest.QRContent == content && latest.Status == "active" { + logger.Info("admin", "支付宝收款码未变化,跳过入库", "device_id", deviceID) + // 码未变不代表账户同步过:仍需确保账户池 QRContent 与码一致(历史不同步的补救路径) + if _, created, aerr := h.ensureAccountForQR(c.Request.Context(), dev, "alipay", content, userId); aerr != nil { + logger.Error("admin", "同步账户池失败", "device_id", deviceID, "user", userId, "err", aerr) + } else if created { + logger.Info("admin", "已自动创建账户", "device_id", deviceID, "platform", "alipay") + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "unchanged": true, "content": content}}) + return + } + accountName := h.resolveQrcodeAccount(ctx, dev, "alipay", userId, content) + q := &personal.DeviceQrcode{ + DeviceID: dev.DeviceID, + Platform: "alipay", + UserID: userId, + QRContent: content, + Account: accountName, + ExpireIn: qrcodeExpireIn, + ExpireAt: store.ExpireAtStr(qrcodeExpireIn), + Status: "active", + CreatedAt: time.Now().Format(time.RFC3339), + } + if _, err := h.pdb.UpsertQrcode(c.Request.Context(), q); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "收款码入库失败: " + err.Error()}) + return + } + logger.Info("admin", "支付宝收款码已捕获", "device_id", dev.DeviceID, "user", userId, + "name", accountName, "content", capture.TruncateStr(content, 40)) + // 同步账户池:码内容变了 → 更新绑定账户的 QRContent;无主码 → 自动建户(与监听端上报路径同逻辑) + if _, created, aerr := h.ensureAccountForQR(c.Request.Context(), dev, "alipay", content, userId); aerr != nil { + logger.Error("admin", "同步账户池失败(码已入库但账户未更新)", "device_id", dev.DeviceID, "user", userId, "err", aerr) + } else if created { + logger.Info("admin", "监听端上报已自动创建账户", "device_id", dev.DeviceID, "platform", "alipay") + } + // 捕获后顺带清理该空间支付宝相册旧码(保留最新 1 张),防频繁捕获打满存储 + if n := capture.CleanupAlipayGallery(ctx, h.adbMgr, serial, userId); n > 0 { + logger.Info("admin", "已清理设备旧支付宝收款码图", "device", serial, "user", userId, "deleted", n) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "user": userId, "account": accountName, "content": content}}) +} + +// handleNavigateAlipayLedger 通过服务器 adb 导航到支付宝完整收款记录页(监听界面)。 +// 供事件驱动监听(PLAN-EVENT-MONITOR §2.3)手动触发/测试;监听端收到窗口事件后建立基线进入监听态。 +// POST /api/personal/devices/:id/navigate-ledger?provider=alipay&user=0|999 +func (h *PersonalHandler) handleNavigateAlipayLedger(c *gin.Context) { + deviceID := c.Param("id") + if deviceID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "设备控制台未启用(adb 管理器未配置)"}) + return + } + dev, err := h.pdb.GetDevice(c.Request.Context(), deviceID) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + userId, _ := strconv.Atoi(c.DefaultQuery("user", "0")) + serial, _, ok := h.pickOnlineSerial(c.Request.Context(), dev) + if !ok { + c.JSON(http.StatusBadGateway, gin.H{"error": "设备离线:所有 adb 通道不可用"}) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 90*time.Second) + defer cancel() + + if err := capture.CaptureAlipayLedger(ctx, h.adbMgr, serial, userId); err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) + return + } + logger.Info("admin", "已导航到支付宝收款记录页", "device_id", dev.DeviceID, "user", userId) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "user": userId}}) +} + +// alipayAccountName 从设备绑定账户解析支付宝账户名(device_accounts 附加账户优先,主账户兜底)。 +func (h *PersonalHandler) alipayAccountName(ctx context.Context, dev *personal.DeviceEncrypted) string { + if dev == nil || h.pool == nil { + return "" + } + if ids, err := h.pdb.ListDeviceAccounts(ctx, dev.ID); err == nil { + for _, id := range ids { + if acct := h.pool.GetByID(id); acct != nil && strings.HasPrefix(acct.Channel, "personal.alipay") { + return acct.Name + } + } + } + if acct := h.pool.GetByID(dev.AccountID); acct != nil && strings.HasPrefix(acct.Channel, "personal.alipay") { + return acct.Name + } + return dev.Name +} diff --git a/internal/admin/qrcode_renew.go b/internal/admin/qrcode_renew.go new file mode 100644 index 0000000..6665c26 --- /dev/null +++ b/internal/admin/qrcode_renew.go @@ -0,0 +1,247 @@ +// 收款码自动续期 + 定期清理调度。 +// 续期:扫描设备各平台收款码,缺失/非 active/过期 → 自动 adb 捕获补新码(静态码长期有效,正常不触发)。 +// 清理:DB 删过期记录(每设备每平台保留最新);设备端微信相册保留最近 N 张。 +package admin + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "paygateway/internal/admin/capture" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// qrcodeExpireIn 保存的收款码有效期(秒)。微信/支付宝「保存收款码」是静态码长期有效, +// 设 7 天作安全缓冲(防极端情况下码失效),到期由续期调度补捕。 +const qrcodeExpireIn = 7 * 24 * 3600 + +// qrcodeRenewPlatforms 续期检查的平台(服务端 adb 主动捕获) +var qrcodeRenewPlatforms = []string{"wechat", "alipay"} + +// qrcodeMinRenewInterval 每平台两次自动捕获的最小间隔(防调度异常时无限重捕风暴) +const qrcodeMinRenewInterval = 10 * time.Minute + +// qrcodeUserSpaces 每设备自动捕获的 Android 用户空间。 +// 0 = 主空间(机主),999 = 双开(小米 XSpace 分身)。 +// 覆盖微信/支付宝在双开空间也有安装的场景。 +var qrcodeUserSpaces = []int{0, 999} + +// RenewQrcodes 自动续期调度:扫描所有设备各平台各用户空间的收款码,发现缺失/非 active/已过期 +// → 自动触发服务端 adb 捕获补新码。静态码长期有效,正常情况几乎不触发; +// 仅在码丢失或明确过期时补捕,且每平台每用户空间受最小间隔限制。 +// +// 由 main 以固定间隔(默认 1 小时)调用。 +func (h *PersonalHandler) RenewQrcodes(ctx context.Context) { + if h.adbMgr == nil || h.pdb == nil { + return + } + devs, err := h.pdb.ListDevices(ctx) + if err != nil { + logger.Error("admin", "续期扫描: 获取设备列表失败", "err", err) + return + } + for _, dev := range devs { + serial, _, ok := h.pickOnlineSerial(ctx, dev) + if !ok { + continue // 设备离线,无法捕获 + } + for _, userID := range qrcodeUserSpaces { + for _, platform := range qrcodeRenewPlatforms { + if !h.qrcodeNeedsRenew(ctx, dev.DeviceID, platform, userID) { + continue + } + // 限频:距上次自动捕获不足最小间隔则跳过(防重捕风暴) + if h.renewLimiter != nil && !h.renewLimiter.allow(dev.DeviceID, platform, userID) { + logger.Debug("admin", "收款码补捕被限频跳过", "device", dev.DeviceID, "platform", platform, "user", userID) + continue + } + h.renewOneQrcode(ctx, dev, serial, platform, userID) + } + } + } +} + +// qrcodeNeedsRenew 判断设备某平台某用户空间是否需要补捕:无码 / 非 active / 已过期。 +// 静态码未过期不触发(不做提前续期,避免无意义捕获)。 +func (h *PersonalHandler) qrcodeNeedsRenew(ctx context.Context, deviceID, platform string, userID int) bool { + latest, err := h.pdb.GetLatestQrcodeByPlatformUser(ctx, deviceID, platform, userID) + if err != nil || latest == nil { + return true // 无码 → 需要 + } + if latest.Status != "active" { + return true + } + expireAt, err := time.Parse(time.RFC3339, latest.ExpireAt) + if err != nil { + return true // 过期时间解析失败 → 重捕 + } + return time.Now().After(expireAt) +} + +// qrcodeLastRenew 记录每设备每平台每用户空间最近一次自动捕获时间(限频) +type qrcodeLastRenew struct { + mu sync.Mutex + lasts map[string]time.Time // key: deviceID|platform|userID +} + +// allow 检查并记录该设备该平台该用户空间是否允许自动捕获(距上次至少 qrcodeMinRenewInterval) +func (r *qrcodeLastRenew) allow(deviceID, platform string, userID int) bool { + key := fmt.Sprintf("%s|%s|%d", deviceID, platform, userID) + r.mu.Lock() + defer r.mu.Unlock() + if t, ok := r.lasts[key]; ok && time.Since(t) < qrcodeMinRenewInterval { + return false + } + r.lasts[key] = time.Now() + return true +} + +// renewOneQrcode 对单个设备单平台单用户空间执行一次续期捕获并入库(失败记日志,下轮再试)。 +func (h *PersonalHandler) renewOneQrcode(ctx context.Context, dev *personal.DeviceEncrypted, serial, platform string, userID int) { + ctx2, cancel := context.WithTimeout(ctx, 90*time.Second) + defer cancel() + + var content string + switch platform { + case "wechat": + _, _, c, err := capture.CaptureWechatQrcode(ctx2, h.adbMgr, serial, userID) + if err != nil { + logger.Warn("admin", "微信收款码续期失败", "device", dev.DeviceID, "user", userID, "err", err) + return + } + content = c + case "alipay": + c, err := capture.CaptureAlipayQrcode(ctx2, h.adbMgr, serial, userID) + if err != nil { + logger.Warn("admin", "支付宝收款码续期失败", "device", dev.DeviceID, "user", userID, "err", err) + return + } + content = c + default: + return + } + // 确保有对应的支付账户(自动创建 + 绑定),使支付闭环可用 + if _, created, aerr := h.ensureAccountForQR(ctx, dev, platform, content, userID); aerr != nil { + logger.Error("admin", "自动创建账户失败", "device", dev.DeviceID, "platform", platform, "err", aerr) + } else if created { + logger.Info("admin", "收款码自动续期已同步创建账户", "device", dev.DeviceID, "platform", platform) + } + q := &personal.DeviceQrcode{ + DeviceID: dev.DeviceID, + Platform: platform, + UserID: userID, + QRContent: content, + Account: h.resolveQrcodeAccount(ctx, dev, platform, userID, content), + ExpireIn: qrcodeExpireIn, + ExpireAt: store.ExpireAtStr(qrcodeExpireIn), + Status: "active", + CreatedAt: time.Now().Format(time.RFC3339), + } + if _, err := h.pdb.UpsertQrcode(ctx, q); err != nil { + logger.Error("admin", "续期收款码入库失败", "device", dev.DeviceID, "platform", platform, "user", userID, "err", err) + return + } + logger.Info("admin", "收款码自动续期完成", "device", dev.DeviceID, "platform", platform, "user", userID, + "account", q.Account, "content", capture.TruncateStr(content, 40)) +} + +// platformAccountName 按平台解析设备绑定账户名(微信/支付宝),并加上用户空间后缀(主空间/双开)。 +// 使管理后台能区分同一平台不同空间的收款码归属于哪个账户。 +func (h *PersonalHandler) platformAccountName(ctx context.Context, dev *personal.DeviceEncrypted, platform string, userID int) string { + var base string + switch platform { + case "wechat": + base = h.wechatAccountName(ctx, dev) + case "alipay": + base = h.alipayAccountName(ctx, dev) + default: + base = dev.Name + } + return spaceAccountName(base, userID) +} + +// spaceAccountName 账户名追加用户空间后缀(主空间/双开),区分同一平台不同空间的收款码归属。 +func spaceAccountName(base string, userID int) string { + spaceLabel := "主空间" + if userID == 999 { + spaceLabel = "双开" + } + return base + " (" + spaceLabel + ")" +} + +// resolveQrcodeAccount 自动识别捕获到的收款码归属账户。 +// 原理:每个 personal 账户在账户池中配置了收款码内容(QRContent), +// 用本次捕获到的二维码内容与设备绑定账户逐一比对,命中即确认账户身份, +// 从而「自动知道那个账户是那个空间的什么账户」。 +// 匹配失败时回退 platformAccountName(绑定顺序取第一个同平台账户)。 +func (h *PersonalHandler) resolveQrcodeAccount(ctx context.Context, dev *personal.DeviceEncrypted, platform string, userID int, qrContent string) string { + if dev == nil || qrContent == "" { + return "" + } + if h.pool != nil { + prefix := "personal.alipay" + if platform == "wechat" { + prefix = "personal.wechat" + } + // 附加账户优先,主账户兜底:同一平台可能绑定多个账户(主空间+双开各一个) + ids := store.DeviceAccountIDs(ctx, h.pdb, dev.ID, dev.AccountID) + for _, id := range ids { + acct := h.pool.GetByID(id) + if acct == nil || !strings.HasPrefix(acct.Channel, prefix) { + continue + } + if acct.QRContent == qrContent { + logger.Info("admin", "收款码账户自动识别", "device", dev.DeviceID, "platform", platform, + "user", userID, "account", acct.Name) + return spaceAccountName(acct.Name, userID) + } + } + logger.Warn("admin", "收款码账户未匹配,回退绑定账户", "device", dev.DeviceID, + "platform", platform, "user", userID, "content", capture.TruncateStr(qrContent, 30)) + } + return h.platformAccountName(ctx, dev, platform, userID) +} + +// CleanupQrcodes 定期清理: +// 1. DB 层删除过期收款码记录(每设备保留最新一条,防清空后无码可用) +// 2. 设备端相册清理:对每台在线设备保留最近 N 张微信收款码图片,删更旧的 +// +// 由 main 以固定间隔(默认 1 小时)调用。 +func (h *PersonalHandler) CleanupQrcodes(ctx context.Context) { + // 1. DB 清理 + if h.pdb != nil { + if n, err := h.pdb.DeleteExpiredQrcodes(ctx); err != nil { + logger.Error("admin", "收款码 DB 清理失败", "err", err) + } else if n > 0 { + logger.Info("admin", "已清理过期收款码记录", "count", n) + } + } + // 2. 设备端相册清理 + if h.adbMgr == nil { + return + } + devs, err := h.pdb.ListDevices(ctx) + if err != nil { + logger.Error("admin", "收款码相册清理: 获取设备列表失败", "err", err) + return + } + for _, dev := range devs { + serial, _, ok := h.pickOnlineSerial(ctx, dev) + if !ok { + continue // 设备离线,跳过(离线时相册不增长) + } + for _, userId := range []int{0, 999} { + if n := capture.CleanupWechatGallery(ctx, h.adbMgr, serial, userId); n > 0 { + logger.Info("admin", "已清理设备旧收款码图", "device", serial, "user", userId, "deleted", n) + } + if n := capture.CleanupAlipayGallery(ctx, h.adbMgr, serial, userId); n > 0 { + logger.Info("admin", "已清理设备旧支付宝收款码图", "device", serial, "user", userId, "deleted", n) + } + } + } +} diff --git a/internal/admin/qrcode_test.go b/internal/admin/qrcode_test.go new file mode 100755 index 0000000..5702c5b --- /dev/null +++ b/internal/admin/qrcode_test.go @@ -0,0 +1,72 @@ +package admin + +import ( + "strings" + "testing" + + qrcode "github.com/skip2/go-qrcode" + + "paygateway/internal/admin/capture" +) + +// TestDecodeQRContent 生成二维码 → 解析回原文(端到端验证解码链路) +func TestDecodeQRContent(t *testing.T) { + contents := []string{ + "https://qr.alipay.com/fkx12345abc", + "weixin://wxpay/bizpayurl?pr=AbCdEf123", + } + for _, want := range contents { + png, err := qrcode.Encode(want, qrcode.Medium, 256) + if err != nil { + t.Fatalf("生成二维码失败: %v", err) + } + got, err := capture.DecodeQRContent(png) + if err != nil { + t.Fatalf("解析二维码失败: %v", err) + } + if got != want { + t.Fatalf("解析内容不一致: want=%q got=%q", want, got) + } + } +} + +// TestDecodeQRContentInvalid 非二维码图片 → 报错 +func TestDecodeQRContentInvalid(t *testing.T) { + // 纯色 PNG(无二维码) + if _, err := capture.DecodeQRContent([]byte("not an image")); err == nil { + t.Fatal("非法输入应报错") + } + // 有效图片但无二维码 + png, _ := qrcode.Encode("x", qrcode.Medium, 64) + if _, err := capture.DecodeQRContent(png[:32]); err == nil { + t.Fatal("截断图片应报错") + } +} + +// TestDecodeQRContentEmpty 空输入 → 报错 +func TestDecodeQRContentEmpty(t *testing.T) { + if _, err := capture.DecodeQRContent(nil); err == nil { + t.Fatal("空输入应报错") + } + if _, err := capture.DecodeQRContent([]byte{}); err == nil { + t.Fatal("空输入应报错") + } +} + +// TestDecodeQRContentTooLarge 超 2MB → 报错 +func TestDecodeQRContentTooLarge(t *testing.T) { + big := make([]byte, capture.MaxQRUploadSize+1) + if _, err := capture.DecodeQRContent(big); err == nil { + t.Fatal("超大图片应报错") + } + if !strings.Contains(decodeErr(capture.DecodeQRContent(big)), "2MB") { + t.Fatal("错误信息应包含大小限制说明") + } +} + +func decodeErr(_ string, err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/internal/admin/qrcode_wechat.go b/internal/admin/qrcode_wechat.go new file mode 100644 index 0000000..38fc4a4 --- /dev/null +++ b/internal/admin/qrcode_wechat.go @@ -0,0 +1,128 @@ +// 微信收款码捕获 HTTP 层:设备查找 → 调用 capture 包自动化 → 去重 → 入库。 +package admin + +import ( + "context" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/admin/capture" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// handleCaptureWechatQrcode 通过服务器 adb 全自动捕获微信个人收款码 +// POST /api/personal/qrcodes/:deviceId/wechat/capture?user=0|999 +// 流程:唤醒 → 启动微信(可指定双开 user) → 导航到收付款页 → 切「个人收款码」→ 点「保存收款码」 +// +// → 相册拉最新 → 解码 → 与库中最新码去重 → 入库 +func (h *PersonalHandler) handleCaptureWechatQrcode(c *gin.Context) { + deviceID := c.Param("deviceId") + if deviceID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "设备控制台未启用(adb 管理器未配置)"}) + return + } + dev, err := h.pdb.GetDevice(c.Request.Context(), deviceID) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + // 双开空间参数(默认 0 = 主空间;>0 如 999 = 小米 XSpace 分身) + userId, _ := strconv.Atoi(c.DefaultQuery("user", "0")) + // 找可用的 adb serial(USB 优先 → WiFi 备用) + serial, _, ok := h.pickOnlineSerial(c.Request.Context(), dev) + if !ok { + c.JSON(http.StatusBadGateway, gin.H{"error": "设备离线:所有 adb 通道不可用"}) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 90*time.Second) + defer cancel() + + // 核心捕获(自动化逻辑在 capture 包) + local, fileName, content, cerr := capture.CaptureWechatQrcode(ctx, h.adbMgr, serial, userId) + if cerr != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": cerr.Error()}) + return + } + defer os.Remove(local) + + // 去重:与库中该设备最新码比对,相同则不再入库 + if latest, lerr := h.pdb.GetLatestQrcode(ctx, deviceID); lerr == nil && latest != nil && + latest.Platform == "wechat" && latest.QRContent == content && latest.Status == "active" { + logger.Info("admin", "微信收款码未变化,跳过入库", "device_id", deviceID, "file", fileName) + // 码未变不代表账户同步过:仍需确保账户池 QRContent 与码一致(历史不同步的补救路径) + if _, created, aerr := h.ensureAccountForQR(ctx, dev, "wechat", content, userId); aerr != nil { + logger.Error("admin", "同步账户池失败", "device_id", deviceID, "user", userId, "err", aerr) + } else if created { + logger.Info("admin", "已自动创建账户", "device_id", deviceID, "platform", "wechat") + } + if n := capture.CleanupWechatGallery(ctx, h.adbMgr, serial, userId); n > 0 { + logger.Info("admin", "已清理设备旧收款码图", "device", serial, "user", userId, "deleted", n) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "unchanged": true, "content": content}}) + return + } + + // 入库(保存的静态码长期有效,7 天安全缓冲;手动 adb 捕获可随时刷新) + accountName := h.resolveQrcodeAccount(ctx, dev, "wechat", userId, content) + q := &personal.DeviceQrcode{ + DeviceID: dev.DeviceID, + Platform: "wechat", + UserID: userId, + QRContent: content, + Account: accountName, + ExpireIn: qrcodeExpireIn, + ExpireAt: store.ExpireAtStr(qrcodeExpireIn), + Status: "active", + CreatedAt: time.Now().Format(time.RFC3339), + } + if _, err := h.pdb.UpsertQrcode(c.Request.Context(), q); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "收款码入库失败: " + err.Error()}) + return + } + logger.Info("admin", "微信收款码已捕获", "device_id", dev.DeviceID, "user", userId, + "name", accountName, "file", fileName, "content", capture.TruncateStr(content, 40)) + // 同步账户池:码内容变了 → 更新绑定账户的 QRContent;无主码 → 自动建户(与监听端上报路径同逻辑) + if _, created, aerr := h.ensureAccountForQR(c.Request.Context(), dev, "wechat", content, userId); aerr != nil { + logger.Error("admin", "同步账户池失败(码已入库但账户未更新)", "device_id", dev.DeviceID, "user", userId, "err", aerr) + } else if created { + logger.Info("admin", "监听端上报已自动创建账户", "device_id", dev.DeviceID, "platform", "wechat") + } + // 每次捕获后顺带清理该空间相册旧码(保留最近 5 张),防频繁捕获打满存储 + if n := capture.CleanupWechatGallery(ctx, h.adbMgr, serial, userId); n > 0 { + logger.Info("admin", "已清理设备旧收款码图", "device", serial, "user", userId, "deleted", n) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "user": userId, "file": fileName, "account": accountName, "content": content}}) +} + +// wechatAccountName 从设备绑定账户解析微信账户名(device_accounts 附加账户优先,主账户兜底)。 +// 识别依据是账户 channel=personal.wechat,不依赖相册路径。 +func (h *PersonalHandler) wechatAccountName(ctx context.Context, dev *personal.DeviceEncrypted) string { + if dev == nil { + return "" + } + if h.pool == nil { + return "" + } + if ids, err := h.pdb.ListDeviceAccounts(ctx, dev.ID); err == nil { + for _, id := range ids { + if acct := h.pool.GetByID(id); acct != nil && strings.HasPrefix(acct.Channel, "personal.wechat") { + return acct.Name + } + } + } + if acct := h.pool.GetByID(dev.AccountID); acct != nil && strings.HasPrefix(acct.Channel, "personal.wechat") { + return acct.Name + } + return dev.Name +} diff --git a/internal/admin/reconciler.go b/internal/admin/reconciler.go new file mode 100755 index 0000000..be22812 --- /dev/null +++ b/internal/admin/reconciler.go @@ -0,0 +1,347 @@ +package admin + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/order" + "paygateway/internal/reconciler" + "paygateway/internal/store" +) + +// ReconcilerHandler 容灾对账中心:人工异常池 + dead 通知管理 +type ReconcilerHandler struct { + store store.Store + svc *order.Service + sched *reconciler.Scheduler +} + +// NewReconcilerHandler 创建 +func NewReconcilerHandler(st store.Store, svc *order.Service, sched *reconciler.Scheduler) *ReconcilerHandler { + return &ReconcilerHandler{store: st, svc: svc, sched: sched} +} + +// RegisterRoutes 注册路由(需在 authMiddleware 组内) +func (h *ReconcilerHandler) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/reconciler/exceptions", h.handleListExceptions) + rg.GET("/reconciler/exceptions/:id", h.handleGetException) + rg.POST("/reconciler/exceptions/:id/requery", h.handleRequery) + rg.POST("/reconciler/exceptions/:id/resolve", h.handleResolve) + rg.POST("/reconciler/exceptions/:id/reopen", h.handleReopen) + rg.POST("/reconciler/exceptions", h.handleManualAdd) // 手动加入异常池 + rg.GET("/reconciler/notify-dead", h.handleListDead) + rg.POST("/reconciler/notify-dead/:id/retry", h.handleRetryDead) + rg.POST("/reconciler/notify-dead/:id/delete", h.handleDeleteDead) + // 日终对账报告(RESILIENCE P2-1):GET /reconciler/daily-report?date=YYYY-MM-DD + rg.GET("/reconciler/daily-report", h.handleDailyReport) +} + +// ========== 异常池 ========== + +func (h *ReconcilerHandler) handleListExceptions(c *gin.Context) { + phase := c.Query("phase") + reason := c.Query("reason") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + limit, offset := pageSize, (page-1)*pageSize + + list, err := h.store.ListExceptions(c.Request.Context(), phase, reason, limit, offset) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + total, err := h.store.CountExceptions(c.Request.Context(), phase) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"list": list, "total": total, "page": page}}) +} + +func (h *ReconcilerHandler) handleGetException(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + e, err := h.store.GetException(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "异常单不存在"}) + return + } + logs, err := h.store.ListExceptionLogs(c.Request.Context(), id, 100) + if err != nil { + logs = nil + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"exception": e, "logs": logs}}) +} + +// handleRequery 再次查单,刷新证据(证据自动收集,人工不可编辑) +func (h *ReconcilerHandler) handleRequery(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + e, err := h.store.GetException(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "异常单不存在"}) + return + } + st, qerr := h.sched.Query(c.Request.Context(), e.OutTradeNo) + now := time.Now().Format(time.RFC3339) + evidence := map[string]any{"action": "manual_requery", "time": now} + if qerr != nil { + evidence["error"] = qerr.Error() + evidence["channel_status"] = "QUERY_FAILED" + } else { + evidence["channel_status"] = st.Status + evidence["channel_tx_id"] = st.ChannelTxID + evidence["amount_minor"] = st.AmountMinor + } + evJSON, _ := json.Marshal(evidence) + + // 追加证据(存到 evidence,简单拼接时间线) + refreshedStatus := "" + if qerr != nil { + refreshedStatus = "QUERY_FAILED" + } else { + refreshedStatus = st.Status + } + _ = h.store.UpdateException(c.Request.Context(), id, func(up *model.ExceptionOrder) { + up.ChannelStatus = refreshedStatus + if qerr == nil && st.ChannelTxID != "" { + up.ChannelTxID = st.ChannelTxID + } + if up.Evidence != "" { + up.Evidence = up.Evidence + "\n" + string(evJSON) + } else { + up.Evidence = string(evJSON) + } + }) + operator := c.GetString("username") + _ = h.store.InsertExceptionLog(c.Request.Context(), id, "requery", operator, string(evJSON)) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"channel_status": refreshedStatus, "evidence": evJSON}}) +} + +// handleResolve 人工处理:settle / close / noop +func (h *ReconcilerHandler) handleResolve(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + var req struct { + Action string `json:"action"` + PaidAmount int64 `json:"paid_amount"` + ChannelTxID string `json:"channel_tx_id"` + Payer string `json:"payer"` + Note string `json:"note"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + if req.Action != model.ExActionSettle && req.Action != model.ExActionClose && req.Action != model.ExActionNoop { + c.JSON(http.StatusBadRequest, gin.H{"error": "动作必须是 settle/close/noop"}) + return + } + operator := c.GetString("username") + ctx := c.Request.Context() + + e, err := h.store.GetException(ctx, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "异常单不存在"}) + return + } + if e.Phase == model.ExPhaseResolved { + c.JSON(http.StatusBadRequest, gin.H{"error": "该异常单已处理"}) + return + } + + // 并发防重:pending -> processing 条件更新(抢占锁) + locked, err := h.store.UpdateExceptionPhase(ctx, id, model.ExPhasePending, model.ExPhaseProcessing) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加锁失败: " + err.Error()}) + return + } + if !locked { + // 锁未抢到:说明存在并发处理(另一个请求已把 pending → processing) + c.JSON(http.StatusConflict, gin.H{"error": "该异常单正在被处理或已被抢占,请刷新后重试"}) + return + } + if req.Action == model.ExActionSettle && req.PaidAmount < 0 { + _ = h.store.UpdateException(ctx, id, func(up *model.ExceptionOrder) { up.Phase = model.ExPhasePending }) + c.JSON(http.StatusBadRequest, gin.H{"error": "金额不能为负"}) + return + } + + var result string + switch req.Action { + case model.ExActionSettle: + ok, serr := h.svc.SettleManual(ctx, e.OutTradeNo, req.ChannelTxID, req.PaidAmount, req.Payer, operator, req.Note) + if serr != nil { + _ = h.store.UpdateException(ctx, id, func(up *model.ExceptionOrder) { up.Phase = model.ExPhasePending }) + c.JSON(http.StatusInternalServerError, gin.H{"error": "入账失败: " + serr.Error()}) + return + } + if !ok { + _ = h.store.UpdateException(ctx, id, func(up *model.ExceptionOrder) { up.Phase = model.ExPhasePending }) + c.JSON(http.StatusConflict, gin.H{"error": "订单状态不允许入账(可能已被处理)"}) + return + } + // 订单人工处理标记 + _ = h.store.UpdateOrderHandled(ctx, e.OutTradeNo, id, "manual_settle", operator, req.Note) + result = "已确认入账,并通知上游" + case model.ExActionClose: + ok, cerr := h.svc.CloseOrder(ctx, e.OutTradeNo, "manual_confirm_not_paid") + if cerr != nil { + _ = h.store.UpdateException(ctx, id, func(up *model.ExceptionOrder) { up.Phase = model.ExPhasePending }) + c.JSON(http.StatusInternalServerError, gin.H{"error": "关单失败: " + cerr.Error()}) + return + } + if !ok { + _ = h.store.UpdateException(ctx, id, func(up *model.ExceptionOrder) { up.Phase = model.ExPhasePending }) + c.JSON(http.StatusConflict, gin.H{"error": "订单状态不允许关闭(可能已支付)"}) + return + } + _ = h.store.UpdateOrderHandled(ctx, e.OutTradeNo, id, "manual_close", operator, req.Note) + result = "已确认未支付并关闭" + case model.ExActionNoop: + _ = h.store.UpdateOrderHandled(ctx, e.OutTradeNo, id, "noop", operator, req.Note) + result = "无需处理,已记录" + } + + _ = h.store.UpdateException(ctx, id, func(up *model.ExceptionOrder) { + up.Phase = model.ExPhaseResolved + up.ResolvedAction = req.Action + up.Handler = operator + up.HandlerNote = req.Note + if req.PaidAmount > 0 { + up.PaidAmount = req.PaidAmount + } + if req.ChannelTxID != "" { + up.ChannelTxID = req.ChannelTxID + } + up.ResolvedAt = time.Now().Format(time.RFC3339) + }) + detail := fmt.Sprintf("action=%s note=%q", req.Action, req.Note) + _ = h.store.InsertExceptionLog(ctx, id, "resolve", operator, detail) + logger.Warn("admin", "异常单人工处理完成", "exception_id", id, "out_trade_no", e.OutTradeNo, "action", req.Action, "operator", operator) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"result": result}}) +} + +// handleReopen 重新打开异常单(处理后发现问题) +func (h *ReconcilerHandler) handleReopen(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + operator := c.GetString("username") + err := h.store.UpdateException(c.Request.Context(), id, func(up *model.ExceptionOrder) { + up.Phase = model.ExPhasePending + up.ResolvedAction = "" + up.ResolvedAt = "" + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + _ = h.store.InsertExceptionLog(c.Request.Context(), id, "reopen", operator, "") + c.JSON(http.StatusOK, gin.H{"data": "已重新打开"}) +} + +// handleManualAdd 手动加入异常池 +func (h *ReconcilerHandler) handleManualAdd(c *gin.Context) { + var req struct { + OutTradeNo string `json:"out_trade_no"` + Note string `json:"note"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.OutTradeNo == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: 需要 out_trade_no"}) + return + } + o, err := h.store.GetOrder(c.Request.Context(), req.OutTradeNo) + if err != nil || o == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "订单不存在"}) + return + } + h.svc.EnqueueException(c.Request.Context(), o, model.ExReasonManual, o.Status, "", "{}", req.Note) + operator := c.GetString("username") + // 记录 operator + if e, err := h.store.GetExceptionByOutTradeNo(c.Request.Context(), req.OutTradeNo); err == nil && e != nil { + _ = h.store.InsertExceptionLog(c.Request.Context(), e.ID, "manual_add", operator, req.Note) + } + c.JSON(http.StatusOK, gin.H{"data": "已加入异常池"}) +} + +// ========== dead 通知管理 ========== + +func (h *ReconcilerHandler) handleListDead(c *gin.Context) { + tasks, err := h.store.ListNotifyTasks(c.Request.Context(), "dead", 100) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"list": tasks, "total": len(tasks)}}) +} + +// handleRetryDead 重发 dead 通知(重置为 pending,重试计数清零)。 +// 重发前按订单当前 NotifyURL 重建 payload 并重新签名,解决「配置时回调地址错误 → 重发仍打旧地址 → 一直回调不了」。 +func (h *ReconcilerHandler) handleRetryDead(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + operator := c.GetString("username") + ctx := c.Request.Context() + + task, err := h.store.GetNotifyTask(ctx, id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if task == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "回调任务不存在"}) + return + } + // 用订单最新 NotifyURL 重建 payload;订单丢失/svc 缺失则保留原 payload(避免丢通知) + payload := task.Payload + if h.svc != nil { + if o, oerr := h.store.GetOrderByID(ctx, task.OrderID); oerr == nil && o != nil { + payload = h.svc.BuildNotifyPayload(o) + } + } + if err := h.store.UpdateNotifyTaskPayload(ctx, id, payload); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if err := h.store.UpdateNotifyTask(ctx, id, "pending", 0, time.Now()); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + logger.Warn("admin", "dead 通知重发(已刷新为最新回调地址)", "task_id", id, "operator", operator) + c.JSON(http.StatusOK, gin.H{"data": "已按最新回调地址重新入队"}) +} + +// handleDeleteDead 彻底删除 dead 通知任务(确认放弃该笔通知;若订单已支付但未通知,删除后无法自动补发) +func (h *ReconcilerHandler) handleDeleteDead(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + operator := c.GetString("username") + if err := h.store.DeleteNotifyTask(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + logger.Warn("admin", "dead 通知已删除", "task_id", id, "operator", operator) + c.JSON(http.StatusOK, gin.H{"data": "已删除"}) +} + +// ========== 日终对账报告(RESILIENCE P2-1) ========== + +// handleDailyReport 实时生成日终对账报告 +// GET /api/admin/reconciler/daily-report?date=YYYY-MM-DD(空=今天) +func (h *ReconcilerHandler) handleDailyReport(c *gin.Context) { + report, err := reconciler.BuildDailyReport(c.Request.Context(), h.store, c.Query("date")) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": report}) +} + +var _ = context.Background // keep import if unused diff --git a/internal/admin/static/assets/index-BT-lxyOz.css b/internal/admin/static/assets/index-BT-lxyOz.css new file mode 100644 index 0000000..45416bc --- /dev/null +++ b/internal/admin/static/assets/index-BT-lxyOz.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;410;500;510;590;700&display=swap>";@import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap>";:root{--el-color-white:#fff;--el-color-black:#000;--el-color-primary-rgb:64, 158, 255;--el-color-success-rgb:103, 194, 58;--el-color-warning-rgb:230, 162, 60;--el-color-danger-rgb:245, 108, 108;--el-color-error-rgb:245, 108, 108;--el-color-info-rgb:144, 147, 153;--el-font-size-extra-large:20px;--el-font-size-large:18px;--el-font-size-medium:16px;--el-font-size-base:14px;--el-font-size-small:13px;--el-font-size-extra-small:12px;--el-font-family:"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;--el-font-weight-primary:500;--el-font-line-height-primary:24px;--el-index-normal:1;--el-index-top:1000;--el-index-popper:2000;--el-border-radius-base:4px;--el-border-radius-small:2px;--el-border-radius-round:20px;--el-border-radius-circle:100%;--el-transition-duration:.3s;--el-transition-duration-fast:.2s;--el-transition-function-ease-in-out-bezier:cubic-bezier(.645, .045, .355, 1);--el-transition-function-fast-bezier:cubic-bezier(.23, 1, .32, 1);--el-transition-all:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);--el-transition-fade:opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-md-fade:transform var(--el-transition-duration) var(--el-transition-function-fast-bezier), opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-fade-linear:opacity var(--el-transition-duration-fast) linear;--el-transition-border:border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-box-shadow:box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-color:color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-component-size-large:40px;--el-component-size:32px;--el-component-size-small:24px;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--el-color-primary:#409eff;--el-color-primary-light-3:#79bbff;--el-color-primary-light-5:#a0cfff;--el-color-primary-light-7:#c6e2ff;--el-color-primary-light-8:#d9ecff;--el-color-primary-light-9:#ecf5ff;--el-color-primary-dark-2:#337ecc;--el-color-success:#67c23a;--el-color-success-light-3:#95d475;--el-color-success-light-5:#b3e19d;--el-color-success-light-7:#d1edc4;--el-color-success-light-8:#e1f3d8;--el-color-success-light-9:#f0f9eb;--el-color-success-dark-2:#529b2e;--el-color-warning:#e6a23c;--el-color-warning-light-3:#eebe77;--el-color-warning-light-5:#f3d19e;--el-color-warning-light-7:#f8e3c5;--el-color-warning-light-8:#faecd8;--el-color-warning-light-9:#fdf6ec;--el-color-warning-dark-2:#b88230;--el-color-danger:#f56c6c;--el-color-danger-light-3:#f89898;--el-color-danger-light-5:#fab6b6;--el-color-danger-light-7:#fcd3d3;--el-color-danger-light-8:#fde2e2;--el-color-danger-light-9:#fef0f0;--el-color-danger-dark-2:#c45656;--el-color-error:#f56c6c;--el-color-error-light-3:#f89898;--el-color-error-light-5:#fab6b6;--el-color-error-light-7:#fcd3d3;--el-color-error-light-8:#fde2e2;--el-color-error-light-9:#fef0f0;--el-color-error-dark-2:#c45656;--el-color-info:#909399;--el-color-info-light-3:#b1b3b8;--el-color-info-light-5:#c8c9cc;--el-color-info-light-7:#dedfe0;--el-color-info-light-8:#e9e9eb;--el-color-info-light-9:#f4f4f5;--el-color-info-dark-2:#73767a;--el-bg-color:#fff;--el-bg-color-page:#f2f3f5;--el-bg-color-overlay:#fff;--el-text-color-primary:#303133;--el-text-color-regular:#606266;--el-text-color-secondary:#909399;--el-text-color-placeholder:#a8abb2;--el-text-color-disabled:#c0c4cc;--el-border-color:#dcdfe6;--el-border-color-light:#e4e7ed;--el-border-color-lighter:#ebeef5;--el-border-color-extra-light:#f2f6fc;--el-border-color-dark:#d4d7de;--el-border-color-darker:#cdd0d6;--el-fill-color:#f0f2f5;--el-fill-color-light:#f5f7fa;--el-fill-color-lighter:#fafafa;--el-fill-color-extra-light:#fafcff;--el-fill-color-dark:#ebedf0;--el-fill-color-darker:#e6e8eb;--el-fill-color-blank:#fff;--el-box-shadow:0px 12px 32px 4px #0000000a, 0px 8px 20px #00000014;--el-box-shadow-light:0px 0px 12px #0000001f;--el-box-shadow-lighter:0px 0px 6px #0000001f;--el-box-shadow-dark:0px 16px 48px 16px #00000014, 0px 12px 32px #0000001f, 0px 8px 16px -8px #00000029;--el-disabled-bg-color:var(--el-fill-color-light);--el-disabled-text-color:var(--el-text-color-placeholder);--el-disabled-border-color:var(--el-border-color-light);--el-overlay-color:#000c;--el-overlay-color-light:#000000b3;--el-overlay-color-lighter:#00000080;--el-mask-color:#ffffffe6;--el-mask-color-extra-light:#ffffff4d;--el-border-width:1px;--el-border-style:solid;--el-border-color-hover:var(--el-text-color-disabled);--el-border:var(--el-border-width) var(--el-border-style) var(--el-border-color);--el-svg-monochrome-grey:var(--el-border-color)}.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.fade-in-linear-enter-from,.fade-in-linear-leave-to{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.el-fade-in-linear-enter-from,.el-fade-in-linear-leave-to{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-fade-in-enter-from,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter-from,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transition:var(--el-transition-md-fade);transform-origin:top;transform:scaleY(1)}.el-zoom-in-top-enter-active[data-popper-placement^=top],.el-zoom-in-top-leave-active[data-popper-placement^=top]{transform-origin:bottom}.el-zoom-in-top-enter-from,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transition:var(--el-transition-md-fade);transform-origin:bottom;transform:scaleY(1)}.el-zoom-in-bottom-enter-from,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transition:var(--el-transition-md-fade);transform-origin:0 0;transform:scale(1)}.el-zoom-in-left-enter-from,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45)}.collapse-transition{transition:var(--el-transition-duration) height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.el-collapse-transition-leave-active,.el-collapse-transition-enter-active{transition:var(--el-transition-duration) max-height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.horizontal-collapse-transition{transition:var(--el-transition-duration) width ease-in-out,var(--el-transition-duration) padding-left ease-in-out,var(--el-transition-duration) padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter-from,.el-list-leave-to{opacity:0;transform:translateY(-30px)}.el-list-leave-active{position:absolute!important}.el-opacity-transition{transition:opacity var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@keyframes rotating{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.el-icon{--color:inherit;fill:currentColor;width:1em;height:1em;color:var(--color);line-height:1em;font-size:inherit;justify-content:center;align-items:center;display:inline-flex;position:relative}.el-icon.is-loading{animation:2s linear infinite rotating}.el-icon svg{width:1em;height:1em}.el-affix--fixed{position:fixed}.el-alert{--el-alert-padding:8px 16px;--el-alert-border-radius-base:var(--el-border-radius-base);--el-alert-title-font-size:14px;--el-alert-title-with-description-font-size:16px;--el-alert-description-font-size:14px;--el-alert-close-font-size:16px;--el-alert-close-customed-font-size:14px;--el-alert-icon-size:16px;--el-alert-icon-large-size:28px;width:100%;padding:var(--el-alert-padding);box-sizing:border-box;border-radius:var(--el-alert-border-radius-base);background-color:var(--el-color-white);opacity:1;transition:opacity var(--el-transition-duration-fast);align-items:center;margin:0;display:flex;position:relative;overflow:hidden}.el-alert.is-light .el-alert__close-btn{color:var(--el-text-color-placeholder)}.el-alert.is-dark .el-alert__close-btn,.el-alert.is-dark .el-alert__description{color:var(--el-color-white)}.el-alert.is-center{justify-content:center}.el-alert--primary{--el-alert-bg-color:var(--el-color-primary-light-9)}.el-alert--primary.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-primary)}.el-alert--primary.is-light .el-alert__description{color:var(--el-color-primary)}.el-alert--primary.is-dark{background-color:var(--el-color-primary);color:var(--el-color-white)}.el-alert--success{--el-alert-bg-color:var(--el-color-success-light-9)}.el-alert--success.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-success)}.el-alert--success.is-light .el-alert__description{color:var(--el-color-success)}.el-alert--success.is-dark{background-color:var(--el-color-success);color:var(--el-color-white)}.el-alert--info{--el-alert-bg-color:var(--el-color-info-light-9)}.el-alert--info.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-info)}.el-alert--info.is-light .el-alert__description{color:var(--el-color-info)}.el-alert--info.is-dark{background-color:var(--el-color-info);color:var(--el-color-white)}.el-alert--warning{--el-alert-bg-color:var(--el-color-warning-light-9)}.el-alert--warning.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-warning)}.el-alert--warning.is-light .el-alert__description{color:var(--el-color-warning)}.el-alert--warning.is-dark{background-color:var(--el-color-warning);color:var(--el-color-white)}.el-alert--error{--el-alert-bg-color:var(--el-color-error-light-9)}.el-alert--error.is-light{background-color:var(--el-alert-bg-color);color:var(--el-color-error)}.el-alert--error.is-light .el-alert__description{color:var(--el-color-error)}.el-alert--error.is-dark{background-color:var(--el-color-error);color:var(--el-color-white)}.el-alert__content{flex-direction:column;gap:4px;display:flex}.el-alert .el-alert__icon{font-size:var(--el-alert-icon-size);width:var(--el-alert-icon-size);margin-right:8px}.el-alert .el-alert__icon.is-big{font-size:var(--el-alert-icon-large-size);width:var(--el-alert-icon-large-size);margin-right:12px}.el-alert__title{font-size:var(--el-alert-title-font-size);line-height:24px}.el-alert__title.with-description{font-size:var(--el-alert-title-with-description-font-size)}.el-alert .el-alert__description{font-size:var(--el-alert-description-font-size);margin:0}.el-alert .el-alert__close-btn{font-size:var(--el-alert-close-font-size);opacity:1;cursor:pointer;position:absolute;top:12px;right:16px}.el-alert .el-alert__close-btn.is-customed{font-style:normal;font-size:var(--el-alert-close-customed-font-size);line-height:24px;top:8px}.el-alert-fade-enter-from,.el-alert-fade-leave-active{opacity:0}.el-aside{box-sizing:border-box;width:var(--el-aside-width,300px);flex-shrink:0;overflow:auto}.el-autocomplete{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;width:var(--el-input-width);display:inline-block;position:relative}.el-autocomplete__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-box-shadow-light)}.el-autocomplete__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-autocomplete__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-autocomplete__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-autocomplete__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-autocomplete__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-autocomplete-suggestion{border-radius:var(--el-border-radius-base);box-sizing:border-box}.el-autocomplete-suggestion__header{border-bottom:1px solid var(--el-border-color-lighter);padding:10px}.el-autocomplete-suggestion__footer{border-top:1px solid var(--el-border-color-lighter);padding:10px}.el-autocomplete-suggestion__wrap{box-sizing:border-box;max-height:280px;padding:10px 0}.el-autocomplete-suggestion__list{margin:0;padding:0}.el-autocomplete-suggestion li{cursor:pointer;color:var(--el-text-color-regular);line-height:34px;font-size:var(--el-font-size-base);text-align:left;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0 20px;list-style:none;overflow:hidden}.el-autocomplete-suggestion li:hover,.el-autocomplete-suggestion li.highlighted{background-color:var(--el-fill-color-light)}.el-autocomplete-suggestion li.divider{border-top:1px solid var(--el-color-black);margin-top:6px}.el-autocomplete-suggestion li.divider:last-child{margin-bottom:-6px}.el-autocomplete-suggestion.is-loading li{cursor:default;height:100px;color:var(--el-text-color-secondary);justify-content:center;align-items:center;font-size:20px;display:flex}.el-autocomplete-suggestion.is-loading li:hover{background-color:var(--el-bg-color-overlay)}.el-avatar{--el-avatar-text-color:var(--el-color-white);--el-avatar-bg-color:var(--el-text-color-disabled);--el-avatar-text-size:14px;--el-avatar-icon-size:18px;--el-avatar-border-radius:var(--el-border-radius-base);--el-avatar-size-large:56px;--el-avatar-size:40px;--el-avatar-size-small:24px;box-sizing:border-box;text-align:center;color:var(--el-avatar-text-color);background:var(--el-avatar-bg-color);width:var(--el-avatar-size);height:var(--el-avatar-size);font-size:var(--el-avatar-text-size);outline:none;justify-content:center;align-items:center;display:inline-flex;overflow:hidden}.el-avatar>img{width:100%;height:100%;display:block}.el-avatar--circle{border-radius:50%}.el-avatar--square{border-radius:var(--el-avatar-border-radius)}.el-avatar--icon{font-size:var(--el-avatar-icon-size)}.el-avatar--small{--el-avatar-size:24px}.el-avatar--large{--el-avatar-size:56px}.el-avatar-group{--el-avatar-group-item-gap:-8px;--el-avatar-group-collapse-item-gap:4px;display:inline-flex}.el-avatar-group .el-avatar{border:1px solid var(--el-border-color-extra-light)}.el-avatar-group .el-avatar:not(:first-child){margin-left:var(--el-avatar-group-item-gap)}.el-avatar-group__collapse-avatars{--el-avatar-group-item-gap:-8px;--el-avatar-group-collapse-item-gap:4px}.el-avatar-group__collapse-avatars .el-avatar:not(:first-child){margin-left:var(--el-avatar-group-collapse-item-gap)}.el-backtop{--el-backtop-bg-color:var(--el-bg-color-overlay);--el-backtop-text-color:var(--el-color-primary);--el-backtop-hover-bg-color:var(--el-border-color-extra-light);background-color:var(--el-backtop-bg-color);width:40px;height:40px;color:var(--el-backtop-text-color);box-shadow:var(--el-box-shadow-lighter);cursor:pointer;z-index:5;border-radius:50%;justify-content:center;align-items:center;font-size:20px;display:flex;position:fixed}.el-backtop:hover{background-color:var(--el-backtop-hover-bg-color)}.el-backtop__icon{font-size:20px}.el-badge{--el-badge-bg-color:var(--el-color-danger);--el-badge-radius:10px;--el-badge-font-size:12px;--el-badge-padding:6px;--el-badge-size:18px;vertical-align:middle;width:-moz-fit-content;width:fit-content;display:inline-block;position:relative}.el-badge__content{background-color:var(--el-badge-bg-color);border-radius:var(--el-badge-radius);color:var(--el-color-white);font-size:var(--el-badge-font-size);height:var(--el-badge-size);padding:0 var(--el-badge-padding);white-space:nowrap;border:1px solid var(--el-bg-color);justify-content:center;align-items:center;display:inline-flex}.el-badge__content.is-fixed{top:0;right:calc(1px + var(--el-badge-size) / 2);z-index:var(--el-index-normal);position:absolute;transform:translateY(-50%)translate(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{border-radius:50%;width:8px;height:8px;padding:0;right:0}.el-badge__content.is-hide-zero{display:none}.el-badge__content--primary{background-color:var(--el-color-primary)}.el-badge__content--success{background-color:var(--el-color-success)}.el-badge__content--warning{background-color:var(--el-color-warning)}.el-badge__content--info{background-color:var(--el-color-info)}.el-badge__content--danger{background-color:var(--el-color-danger)}.el-breadcrumb__separator{color:var(--el-text-color-placeholder);margin:0 9px;font-weight:700}.el-breadcrumb__separator.el-icon{margin:0 6px;font-weight:400}.el-breadcrumb__separator.el-icon svg{vertical-align:middle}.el-breadcrumb__item{float:left;align-items:center;display:inline-flex}.el-breadcrumb__inner{color:var(--el-text-color-regular)}.el-breadcrumb__inner.is-link,.el-breadcrumb__inner a{transition:var(--el-transition-color);color:var(--el-text-color-primary);font-weight:700;text-decoration:none}.el-breadcrumb__inner.is-link:hover,.el-breadcrumb__inner a:hover{color:var(--el-color-primary);cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover{color:var(--el-text-color-regular);cursor:text;font-weight:400}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb:before,.el-breadcrumb:after{content:"";display:table}.el-breadcrumb:after{clear:both}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button:first-child:last-child{border-top-right-radius:var(--el-border-radius-base);border-bottom-right-radius:var(--el-border-radius-base);border-top-left-radius:var(--el-border-radius-base);border-bottom-left-radius:var(--el-border-radius-base)}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:var(--el-border-radius-round)}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:hover,.el-button-group>.el-button:focus,.el-button-group>.el-button:active,.el-button-group>.el-button.is-active{z-index:1}.el-button-group--horizontal{vertical-align:middle;display:inline-block}.el-button-group--horizontal:before,.el-button-group--horizontal:after{content:"";display:table}.el-button-group--horizontal:after{clear:both}.el-button-group--horizontal>.el-button{float:left;position:relative}.el-button-group--horizontal>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group--horizontal>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group--horizontal>.el-button:not(:last-child){margin-right:-1px}.el-button-group--horizontal .el-button--primary:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--primary:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--primary:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--success:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--success:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--success:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--warning:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--warning:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--warning:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--danger:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--danger:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--danger:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--info:first-child{border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--info:last-child{border-left-color:var(--el-button-divide-border-color)}.el-button-group--horizontal .el-button--info:not(:first-child):not(:last-child){border-left-color:var(--el-button-divide-border-color);border-right-color:var(--el-button-divide-border-color)}.el-button-group--horizontal>.el-dropdown>.el-button{border-left-color:var(--el-button-divide-border-color);border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group--vertical{flex-direction:column;align-items:stretch;display:inline-flex}.el-button-group--vertical>.el-button{margin-top:-1px}.el-button-group--vertical>.el-button:first-child{border-bottom-right-radius:0;border-bottom-left-radius:0}.el-button-group--vertical>.el-button:last-child{border-top-left-radius:0;border-top-right-radius:0}.el-button-group--vertical>.el-dropdown{margin-top:-1px}.el-button-group--vertical>.el-dropdown>.el-button{border-left-color:var(--el-button-divide-border-color);border-top-left-radius:0;border-top-right-radius:0}.el-button-group--vertical .el-button--primary:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--primary:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--primary:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--success:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--success:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--success:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--warning:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--warning:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--warning:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--danger:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--danger:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--danger:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--info:first-child{border-bottom-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--info:last-child{border-top-color:var(--el-button-divide-border-color)}.el-button-group--vertical .el-button--info:not(:first-child):not(:last-child){border-top-color:var(--el-button-divide-border-color);border-bottom-color:var(--el-button-divide-border-color)}.el-button{--el-button-font-weight:var(--el-font-weight-primary);--el-button-border-color:var(--el-border-color);--el-button-bg-color:var(--el-fill-color-blank);--el-button-text-color:var(--el-text-color-regular);--el-button-disabled-text-color:var(--el-disabled-text-color);--el-button-disabled-bg-color:var(--el-fill-color-blank);--el-button-disabled-border-color:var(--el-border-color-light);--el-button-divide-border-color:#ffffff80;--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-color-primary-light-9);--el-button-hover-border-color:var(--el-color-primary-light-7);--el-button-active-text-color:var(--el-button-hover-text-color);--el-button-active-border-color:var(--el-color-primary);--el-button-active-bg-color:var(--el-button-hover-bg-color);--el-button-outline-color:var(--el-color-primary-light-5);--el-button-hover-link-text-color:var(--el-text-color-secondary);--el-button-active-color:var(--el-text-color-primary);white-space:nowrap;cursor:pointer;height:32px;color:var(--el-button-text-color);text-align:center;box-sizing:border-box;line-height:1;font-weight:var(--el-button-font-weight);-webkit-user-select:none;user-select:none;vertical-align:middle;-webkit-appearance:none;background-color:var(--el-button-bg-color);border:var(--el-border);border-color:var(--el-button-border-color);outline:none;justify-content:center;align-items:center;transition:all .1s;display:inline-flex}.el-button:hover{color:var(--el-button-hover-text-color);border-color:var(--el-button-hover-border-color);background-color:var(--el-button-hover-bg-color);outline:none}.el-button:active{color:var(--el-button-active-text-color);border-color:var(--el-button-active-border-color);background-color:var(--el-button-active-bg-color);outline:none}.el-button:focus-visible{outline:2px solid var(--el-button-outline-color);outline-offset:1px;transition:outline-offset,outline}.el-button>span{align-items:center;display:inline-flex}.el-button+.el-button{margin-left:12px}.el-button{font-size:var(--el-font-size-base);border-radius:var(--el-border-radius-base);padding:8px 15px}.el-button.is-round{padding:8px 15px}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon]+span{margin-left:6px}.el-button [class*=el-icon] svg{vertical-align:bottom}.el-button.is-plain{--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-fill-color-blank);--el-button-hover-border-color:var(--el-color-primary)}.el-button.is-active{color:var(--el-button-active-text-color);border-color:var(--el-button-active-border-color);background-color:var(--el-button-active-bg-color);outline:none}.el-button.is-disabled,.el-button.is-disabled:hover{color:var(--el-button-disabled-text-color);cursor:not-allowed;background-image:none;background-color:var(--el-button-disabled-bg-color);border-color:var(--el-button-disabled-border-color)}.el-button.is-loading{pointer-events:none;position:relative}.el-button.is-loading:before{z-index:1;pointer-events:none;content:"";border-radius:inherit;background-color:var(--el-mask-color-extra-light);position:absolute;top:-1px;bottom:-1px;left:-1px;right:-1px}.el-button.is-round{border-radius:var(--el-border-radius-round)}.el-button.is-dashed{--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-fill-color-blank);--el-button-hover-border-color:var(--el-color-primary);border-style:dashed}.el-button.is-circle{border-radius:50%;width:32px;padding:8px}.el-button.is-text{color:var(--el-button-text-color);background-color:#0000;border:0 solid #0000}.el-button.is-text.is-disabled{color:var(--el-button-disabled-text-color);background-color:#0000!important}.el-button.is-text:not(.is-disabled):hover{background-color:var(--el-fill-color-light)}.el-button.is-text:not(.is-disabled):focus-visible{outline:2px solid var(--el-button-outline-color);outline-offset:1px;transition:outline-offset,outline}.el-button.is-text:not(.is-disabled):active{background-color:var(--el-fill-color)}.el-button.is-text:not(.is-disabled).is-has-bg{background-color:var(--el-fill-color-light)}.el-button.is-text:not(.is-disabled).is-has-bg:hover{background-color:var(--el-fill-color)}.el-button.is-text:not(.is-disabled).is-has-bg:active{background-color:var(--el-fill-color-dark)}.el-button__text--expand{letter-spacing:.3em;margin-right:-.3em}.el-button.is-link{color:var(--el-button-text-color);background:0 0;border-color:#0000;height:auto;padding:2px}.el-button.is-link:hover{color:var(--el-button-hover-link-text-color)}.el-button.is-link.is-disabled{color:var(--el-button-disabled-text-color);background-color:#0000!important;border-color:#0000!important}.el-button.is-link:not(.is-disabled):hover{background-color:#0000;border-color:#0000}.el-button.is-link:not(.is-disabled):active{color:var(--el-button-active-color);background-color:#0000;border-color:#0000}.el-button--text{color:var(--el-color-primary);background:0 0;border-color:#0000;padding-left:0;padding-right:0}.el-button--text.is-disabled{color:var(--el-button-disabled-text-color);background-color:#0000!important;border-color:#0000!important}.el-button--text:not(.is-disabled):hover{color:var(--el-color-primary-light-3);background-color:#0000;border-color:#0000}.el-button--text:not(.is-disabled):active{color:var(--el-color-primary-dark-2);background-color:#0000;border-color:#0000}.el-button__link--expand{letter-spacing:.3em;margin-right:-.3em}.el-button--primary{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-primary);--el-button-border-color:var(--el-color-primary);--el-button-outline-color:var(--el-color-primary-light-5);--el-button-active-color:var(--el-color-primary-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-primary-light-5);--el-button-hover-bg-color:var(--el-color-primary-light-3);--el-button-hover-border-color:var(--el-color-primary-light-3);--el-button-active-bg-color:var(--el-color-primary-dark-2);--el-button-active-border-color:var(--el-color-primary-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-primary-light-5);--el-button-disabled-border-color:var(--el-color-primary-light-5)}.el-button--primary.is-plain,.el-button--primary.is-text,.el-button--primary.is-link{--el-button-text-color:var(--el-color-primary);--el-button-bg-color:var(--el-color-primary-light-9);--el-button-border-color:var(--el-color-primary-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-primary);--el-button-hover-border-color:var(--el-color-primary);--el-button-active-text-color:var(--el-color-white)}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:hover,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-text.is-disabled,.el-button--primary.is-text.is-disabled:hover,.el-button--primary.is-text.is-disabled:focus,.el-button--primary.is-text.is-disabled:active,.el-button--primary.is-link.is-disabled,.el-button--primary.is-link.is-disabled:hover,.el-button--primary.is-link.is-disabled:focus,.el-button--primary.is-link.is-disabled:active{color:var(--el-color-primary-light-5);background-color:var(--el-color-primary-light-9);border-color:var(--el-color-primary-light-8)}.el-button--primary.is-dashed{--el-button-text-color:var(--el-color-primary);--el-button-bg-color:var(--el-color-primary-light-9);--el-button-border-color:var(--el-color-primary-light-5);--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-color-primary-light-9);--el-button-hover-border-color:var(--el-color-primary-light-3);--el-button-active-text-color:var(--el-color-primary-dark-2);--el-button-active-bg-color:var(--el-color-primary-light-9);--el-button-active-border-color:var(--el-color-primary-dark-2)}.el-button--primary.is-dashed.is-disabled,.el-button--primary.is-dashed.is-disabled:hover,.el-button--primary.is-dashed.is-disabled:focus,.el-button--primary.is-dashed.is-disabled:active{color:var(--el-color-primary-light-5);background-color:var(--el-color-primary-light-9);border-color:var(--el-color-primary-light-8)}.el-button--success{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-success);--el-button-border-color:var(--el-color-success);--el-button-outline-color:var(--el-color-success-light-5);--el-button-active-color:var(--el-color-success-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-success-light-5);--el-button-hover-bg-color:var(--el-color-success-light-3);--el-button-hover-border-color:var(--el-color-success-light-3);--el-button-active-bg-color:var(--el-color-success-dark-2);--el-button-active-border-color:var(--el-color-success-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-success-light-5);--el-button-disabled-border-color:var(--el-color-success-light-5)}.el-button--success.is-plain,.el-button--success.is-text,.el-button--success.is-link{--el-button-text-color:var(--el-color-success);--el-button-bg-color:var(--el-color-success-light-9);--el-button-border-color:var(--el-color-success-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-success);--el-button-hover-border-color:var(--el-color-success);--el-button-active-text-color:var(--el-color-white)}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:hover,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-text.is-disabled,.el-button--success.is-text.is-disabled:hover,.el-button--success.is-text.is-disabled:focus,.el-button--success.is-text.is-disabled:active,.el-button--success.is-link.is-disabled,.el-button--success.is-link.is-disabled:hover,.el-button--success.is-link.is-disabled:focus,.el-button--success.is-link.is-disabled:active{color:var(--el-color-success-light-5);background-color:var(--el-color-success-light-9);border-color:var(--el-color-success-light-8)}.el-button--success.is-dashed{--el-button-text-color:var(--el-color-success);--el-button-bg-color:var(--el-color-success-light-9);--el-button-border-color:var(--el-color-success-light-5);--el-button-hover-text-color:var(--el-color-success);--el-button-hover-bg-color:var(--el-color-success-light-9);--el-button-hover-border-color:var(--el-color-success-light-3);--el-button-active-text-color:var(--el-color-success-dark-2);--el-button-active-bg-color:var(--el-color-success-light-9);--el-button-active-border-color:var(--el-color-success-dark-2)}.el-button--success.is-dashed.is-disabled,.el-button--success.is-dashed.is-disabled:hover,.el-button--success.is-dashed.is-disabled:focus,.el-button--success.is-dashed.is-disabled:active{color:var(--el-color-success-light-5);background-color:var(--el-color-success-light-9);border-color:var(--el-color-success-light-8)}.el-button--warning{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-warning);--el-button-border-color:var(--el-color-warning);--el-button-outline-color:var(--el-color-warning-light-5);--el-button-active-color:var(--el-color-warning-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-warning-light-5);--el-button-hover-bg-color:var(--el-color-warning-light-3);--el-button-hover-border-color:var(--el-color-warning-light-3);--el-button-active-bg-color:var(--el-color-warning-dark-2);--el-button-active-border-color:var(--el-color-warning-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-warning-light-5);--el-button-disabled-border-color:var(--el-color-warning-light-5)}.el-button--warning.is-plain,.el-button--warning.is-text,.el-button--warning.is-link{--el-button-text-color:var(--el-color-warning);--el-button-bg-color:var(--el-color-warning-light-9);--el-button-border-color:var(--el-color-warning-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-warning);--el-button-hover-border-color:var(--el-color-warning);--el-button-active-text-color:var(--el-color-white)}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:hover,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-text.is-disabled,.el-button--warning.is-text.is-disabled:hover,.el-button--warning.is-text.is-disabled:focus,.el-button--warning.is-text.is-disabled:active,.el-button--warning.is-link.is-disabled,.el-button--warning.is-link.is-disabled:hover,.el-button--warning.is-link.is-disabled:focus,.el-button--warning.is-link.is-disabled:active{color:var(--el-color-warning-light-5);background-color:var(--el-color-warning-light-9);border-color:var(--el-color-warning-light-8)}.el-button--warning.is-dashed{--el-button-text-color:var(--el-color-warning);--el-button-bg-color:var(--el-color-warning-light-9);--el-button-border-color:var(--el-color-warning-light-5);--el-button-hover-text-color:var(--el-color-warning);--el-button-hover-bg-color:var(--el-color-warning-light-9);--el-button-hover-border-color:var(--el-color-warning-light-3);--el-button-active-text-color:var(--el-color-warning-dark-2);--el-button-active-bg-color:var(--el-color-warning-light-9);--el-button-active-border-color:var(--el-color-warning-dark-2)}.el-button--warning.is-dashed.is-disabled,.el-button--warning.is-dashed.is-disabled:hover,.el-button--warning.is-dashed.is-disabled:focus,.el-button--warning.is-dashed.is-disabled:active{color:var(--el-color-warning-light-5);background-color:var(--el-color-warning-light-9);border-color:var(--el-color-warning-light-8)}.el-button--danger{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-danger);--el-button-border-color:var(--el-color-danger);--el-button-outline-color:var(--el-color-danger-light-5);--el-button-active-color:var(--el-color-danger-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-danger-light-5);--el-button-hover-bg-color:var(--el-color-danger-light-3);--el-button-hover-border-color:var(--el-color-danger-light-3);--el-button-active-bg-color:var(--el-color-danger-dark-2);--el-button-active-border-color:var(--el-color-danger-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-danger-light-5);--el-button-disabled-border-color:var(--el-color-danger-light-5)}.el-button--danger.is-plain,.el-button--danger.is-text,.el-button--danger.is-link{--el-button-text-color:var(--el-color-danger);--el-button-bg-color:var(--el-color-danger-light-9);--el-button-border-color:var(--el-color-danger-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-danger);--el-button-hover-border-color:var(--el-color-danger);--el-button-active-text-color:var(--el-color-white)}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:hover,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-text.is-disabled,.el-button--danger.is-text.is-disabled:hover,.el-button--danger.is-text.is-disabled:focus,.el-button--danger.is-text.is-disabled:active,.el-button--danger.is-link.is-disabled,.el-button--danger.is-link.is-disabled:hover,.el-button--danger.is-link.is-disabled:focus,.el-button--danger.is-link.is-disabled:active{color:var(--el-color-danger-light-5);background-color:var(--el-color-danger-light-9);border-color:var(--el-color-danger-light-8)}.el-button--danger.is-dashed{--el-button-text-color:var(--el-color-danger);--el-button-bg-color:var(--el-color-danger-light-9);--el-button-border-color:var(--el-color-danger-light-5);--el-button-hover-text-color:var(--el-color-danger);--el-button-hover-bg-color:var(--el-color-danger-light-9);--el-button-hover-border-color:var(--el-color-danger-light-3);--el-button-active-text-color:var(--el-color-danger-dark-2);--el-button-active-bg-color:var(--el-color-danger-light-9);--el-button-active-border-color:var(--el-color-danger-dark-2)}.el-button--danger.is-dashed.is-disabled,.el-button--danger.is-dashed.is-disabled:hover,.el-button--danger.is-dashed.is-disabled:focus,.el-button--danger.is-dashed.is-disabled:active{color:var(--el-color-danger-light-5);background-color:var(--el-color-danger-light-9);border-color:var(--el-color-danger-light-8)}.el-button--info{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-info);--el-button-border-color:var(--el-color-info);--el-button-outline-color:var(--el-color-info-light-5);--el-button-active-color:var(--el-color-info-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-info-light-5);--el-button-hover-bg-color:var(--el-color-info-light-3);--el-button-hover-border-color:var(--el-color-info-light-3);--el-button-active-bg-color:var(--el-color-info-dark-2);--el-button-active-border-color:var(--el-color-info-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-info-light-5);--el-button-disabled-border-color:var(--el-color-info-light-5)}.el-button--info.is-plain,.el-button--info.is-text,.el-button--info.is-link{--el-button-text-color:var(--el-color-info);--el-button-bg-color:var(--el-color-info-light-9);--el-button-border-color:var(--el-color-info-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-info);--el-button-hover-border-color:var(--el-color-info);--el-button-active-text-color:var(--el-color-white)}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:hover,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-text.is-disabled,.el-button--info.is-text.is-disabled:hover,.el-button--info.is-text.is-disabled:focus,.el-button--info.is-text.is-disabled:active,.el-button--info.is-link.is-disabled,.el-button--info.is-link.is-disabled:hover,.el-button--info.is-link.is-disabled:focus,.el-button--info.is-link.is-disabled:active{color:var(--el-color-info-light-5);background-color:var(--el-color-info-light-9);border-color:var(--el-color-info-light-8)}.el-button--info.is-dashed{--el-button-text-color:var(--el-color-info);--el-button-bg-color:var(--el-color-info-light-9);--el-button-border-color:var(--el-color-info-light-5);--el-button-hover-text-color:var(--el-color-info);--el-button-hover-bg-color:var(--el-color-info-light-9);--el-button-hover-border-color:var(--el-color-info-light-3);--el-button-active-text-color:var(--el-color-info-dark-2);--el-button-active-bg-color:var(--el-color-info-light-9);--el-button-active-border-color:var(--el-color-info-dark-2)}.el-button--info.is-dashed.is-disabled,.el-button--info.is-dashed.is-disabled:hover,.el-button--info.is-dashed.is-disabled:focus,.el-button--info.is-dashed.is-disabled:active{color:var(--el-color-info-light-5);background-color:var(--el-color-info-light-9);border-color:var(--el-color-info-light-8)}.el-button--large{--el-button-size:40px;height:var(--el-button-size)}.el-button--large [class*=el-icon]+span{margin-left:8px}.el-button--large{font-size:var(--el-font-size-base);border-radius:var(--el-border-radius-base);padding:12px 19px}.el-button--large.is-round{padding:12px 19px}.el-button--large.is-circle{width:var(--el-button-size);padding:12px}.el-button--small{--el-button-size:24px;height:var(--el-button-size)}.el-button--small [class*=el-icon]+span{margin-left:4px}.el-button--small{border-radius:calc(var(--el-border-radius-base) - 1px);padding:5px 11px;font-size:12px}.el-button--small.is-round{padding:5px 11px}.el-button--small.is-circle{width:var(--el-button-size);padding:5px}.el-calendar{--el-calendar-border:var(--el-table-border,1px solid var(--el-border-color-lighter));--el-calendar-header-border-bottom:var(--el-calendar-border);--el-calendar-selected-bg-color:var(--el-color-primary-light-9);--el-calendar-cell-width:85px;background-color:var(--el-fill-color-blank)}.el-calendar__header{border-bottom:var(--el-calendar-header-border-bottom);justify-content:space-between;padding:12px 20px;display:flex}.el-calendar__title{color:var(--el-text-color);align-self:center}.el-calendar__body{padding:12px 20px 35px}.el-calendar__select-controller .el-select{margin-right:8px}.el-calendar__select-controller .el-calendar-select__year{width:120px}.el-calendar__select-controller .el-calendar-select__month{width:60px}.el-calendar-table{table-layout:fixed;width:100%}.el-calendar-table thead th{color:var(--el-text-color-regular);padding:12px 0;font-weight:400}.el-calendar-table:not(.is-range) td.prev,.el-calendar-table:not(.is-range) td.next{color:var(--el-text-color-placeholder)}.el-calendar-table td{border-bottom:var(--el-calendar-border);border-right:var(--el-calendar-border);vertical-align:top;transition:background-color var(--el-transition-duration-fast) ease}.el-calendar-table td.is-selected{background-color:var(--el-calendar-selected-bg-color)}.el-calendar-table td.is-today{color:var(--el-color-primary)}.el-calendar-table tr:first-child td{border-top:var(--el-calendar-border)}.el-calendar-table tr td:first-child{border-left:var(--el-calendar-border)}.el-calendar-table tr.el-calendar-table__row--hide-border td{border-top:none}.el-calendar-table .el-calendar-day{box-sizing:border-box;height:var(--el-calendar-cell-width);padding:8px}.el-calendar-table .el-calendar-day:hover{cursor:pointer;background-color:var(--el-calendar-selected-bg-color)}.el-card{--el-card-border-color:var(--el-border-color-light);--el-card-border-radius:4px;--el-card-padding:20px;--el-card-bg-color:var(--el-fill-color-blank);border-radius:var(--el-card-border-radius);border:1px solid var(--el-card-border-color);background-color:var(--el-card-bg-color);color:var(--el-text-color-primary);transition:var(--el-transition-duration);flex-direction:column;display:flex;overflow:hidden}.el-card.is-always-shadow,.el-card.is-hover-shadow:hover,.el-card.is-hover-shadow:focus{box-shadow:var(--el-box-shadow-light)}.el-card__header{padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding);border-bottom:1px solid var(--el-card-border-color);box-sizing:border-box}.el-card__body{padding:var(--el-card-padding);flex-grow:1;overflow:auto}.el-card__footer{padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding);border-top:1px solid var(--el-card-border-color);box-sizing:border-box}.el-carousel__item{width:100%;height:100%;z-index:calc(var(--el-index-normal) - 1);display:inline-block;position:absolute;top:0;left:0;overflow:hidden}.el-carousel__item.is-active{z-index:calc(var(--el-index-normal) - 1)}.el-carousel__item.is-animating{transition:transform .4s ease-in-out}.el-carousel__item--card{width:50%;transition:transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:var(--el-index-normal)}.el-carousel__item--card.is-in-stage:hover .el-carousel__mask,.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:calc(var(--el-index-normal) + 1)}.el-carousel__item--card-vertical{width:100%;height:50%}.el-carousel__mask{background-color:var(--el-color-white);opacity:.24;width:100%;height:100%;transition:var(--el-transition-duration-fast);position:absolute;top:0;left:0}.el-carousel{--el-carousel-arrow-font-size:12px;--el-carousel-arrow-size:36px;--el-carousel-arrow-background:#1f2d3d1c;--el-carousel-arrow-hover-background:#1f2d3d3b;--el-carousel-indicator-width:30px;--el-carousel-indicator-height:2px;--el-carousel-indicator-padding-horizontal:4px;--el-carousel-indicator-padding-vertical:12px;--el-carousel-indicator-out-color:var(--el-border-color-hover);position:relative}.el-carousel--horizontal,.el-carousel--vertical{overflow:hidden}.el-carousel.is-vertical-outside{flex-direction:row;align-items:center;display:flex}.el-carousel.is-vertical-outside .el-carousel__container{flex:1}.el-carousel__container{height:300px;position:relative}.el-carousel__arrow{height:var(--el-carousel-arrow-size);width:var(--el-carousel-arrow-size);cursor:pointer;transition:var(--el-transition-duration);background-color:var(--el-carousel-arrow-background);color:#fff;z-index:10;text-align:center;font-size:var(--el-carousel-arrow-font-size);border:none;border-radius:50%;outline:none;justify-content:center;align-items:center;margin:0;padding:0;display:inline-flex;position:absolute;top:50%;transform:translateY(-50%)}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:var(--el-carousel-arrow-hover-background)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{z-index:calc(var(--el-index-normal) + 1);margin:0;padding:0;list-style:none;position:absolute}.el-carousel__indicators--horizontal{bottom:0;left:50%;transform:translate(-50%)}.el-carousel__indicators--vertical{top:50%;right:0;transform:translateY(-50%)}.el-carousel__indicators--outside{text-align:center;position:static;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:var(--el-carousel-indicator-out-color);opacity:.24}.el-carousel__indicators--right{right:0}.el-carousel__indicators--labels .el-carousel__button{color:#000;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{cursor:pointer;background-color:#0000}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator--horizontal{padding:var(--el-carousel-indicator-padding-vertical) var(--el-carousel-indicator-padding-horizontal);display:inline-block}.el-carousel__indicator--vertical{padding:var(--el-carousel-indicator-padding-horizontal) var(--el-carousel-indicator-padding-vertical)}.el-carousel__indicator--vertical .el-carousel__button{width:var(--el-carousel-indicator-height);height:calc(var(--el-carousel-indicator-width) / 2)}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{opacity:.48;width:var(--el-carousel-indicator-width);height:var(--el-carousel-indicator-height);cursor:pointer;transition:var(--el-transition-duration);background-color:#fff;border:none;outline:none;margin:0;padding:0;display:block}.el-carousel__indicators--labels .el-carousel__button{width:auto;height:auto}.carousel-arrow-left-enter-from,.carousel-arrow-left-leave-active{opacity:0;transform:translateY(-50%)translate(-10px)}.carousel-arrow-right-enter-from,.carousel-arrow-right-leave-active{opacity:0;transform:translateY(-50%)translate(10px)}.el-transitioning{filter:url(#elCarouselHorizontal)}.el-transitioning-vertical{filter:url(#elCarouselVertical)}.el-cascader-panel{--el-cascader-menu-text-color:var(--el-text-color-regular);--el-cascader-menu-selected-text-color:var(--el-color-primary);--el-cascader-menu-fill:var(--el-bg-color-overlay);--el-cascader-menu-font-size:var(--el-font-size-base);--el-cascader-menu-radius:var(--el-border-radius-base);--el-cascader-menu-border:solid 1px var(--el-border-color-light);--el-cascader-menu-shadow:var(--el-box-shadow-light);--el-cascader-node-background-hover:var(--el-fill-color-light);--el-cascader-node-color-disabled:var(--el-text-color-placeholder);--el-cascader-color-empty:var(--el-text-color-placeholder);--el-cascader-tag-background:var(--el-fill-color);border-radius:var(--el-cascader-menu-radius);width:-moz-fit-content;width:fit-content;font-size:var(--el-cascader-menu-font-size);display:flex}.el-cascader-panel.is-bordered{border:var(--el-cascader-menu-border);border-radius:var(--el-cascader-menu-radius)}.el-cascader-menu{box-sizing:border-box;min-width:180px;color:var(--el-cascader-menu-text-color);border-right:var(--el-cascader-menu-border)}.el-cascader-menu:last-child{border-right:none}.el-cascader-menu:last-child .el-cascader-node{padding-right:20px}.el-cascader-menu__wrap.el-scrollbar__wrap{height:204px}.el-cascader-menu__list{box-sizing:border-box;min-height:100%;margin:0;padding:6px 0;list-style:none;position:relative}.el-cascader-menu__list.el-vl__window{margin:6px 0;padding:0}.el-cascader-menu__list.el-vl__window ul{margin:0;padding:0}.el-cascader-menu__hover-zone{pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0}.el-cascader-menu__empty-text{color:var(--el-cascader-color-empty);align-items:center;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-cascader-menu__empty-text .is-loading{margin-right:2px}.el-cascader-node{box-sizing:border-box;outline:none;align-items:center;height:34px;padding:0 30px 0 20px;line-height:34px;display:flex;position:relative}.el-cascader-node.is-selectable.in-active-path{color:var(--el-cascader-menu-text-color)}.el-cascader-node.in-active-path,.el-cascader-node.is-selectable.in-checked-path,.el-cascader-node.is-active{color:var(--el-cascader-menu-selected-text-color);font-weight:700}.el-cascader-node:not(.is-disabled){cursor:pointer}.el-cascader-node:not(.is-disabled):hover,.el-cascader-node:not(.is-disabled):focus{background:var(--el-cascader-node-background-hover)}.el-cascader-node.is-disabled{color:var(--el-cascader-node-color-disabled);cursor:not-allowed}.el-cascader-node__prefix{position:absolute;left:10px}.el-cascader-node__postfix{position:absolute;right:10px}.el-cascader-node__label{text-align:left;white-space:nowrap;text-overflow:ellipsis;flex:1;padding:0 8px;overflow:hidden}.el-cascader-node>.el-checkbox,.el-cascader-node>.el-radio{margin-right:0}.el-cascader-node>.el-radio .el-radio__label{padding-left:0}.el-cascader{--el-cascader-menu-text-color:var(--el-text-color-regular);--el-cascader-menu-selected-text-color:var(--el-color-primary);--el-cascader-menu-fill:var(--el-bg-color-overlay);--el-cascader-menu-font-size:var(--el-font-size-base);--el-cascader-menu-radius:var(--el-border-radius-base);--el-cascader-menu-border:solid 1px var(--el-border-color-light);--el-cascader-menu-shadow:var(--el-box-shadow-light);--el-cascader-node-background-hover:var(--el-fill-color-light);--el-cascader-node-color-disabled:var(--el-text-color-placeholder);--el-cascader-color-empty:var(--el-text-color-placeholder);--el-cascader-tag-background:var(--el-fill-color);vertical-align:middle;font-size:var(--el-font-size-base);outline:none;line-height:32px;display:inline-block;position:relative}.el-cascader:not(.is-disabled):hover .el-input__wrapper{cursor:pointer;box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-cascader .el-input{cursor:pointer;display:flex}.el-cascader .el-input .el-input__inner{text-overflow:ellipsis}.el-cascader .el-input .el-input__inner:read-only{cursor:pointer}.el-cascader .el-input .el-input__inner:disabled{cursor:not-allowed}.el-cascader .el-input .el-input__suffix-inner .el-icon svg{vertical-align:middle}.el-cascader .el-input .icon-arrow-down{transition:transform var(--el-transition-duration);font-size:14px}.el-cascader .el-input .icon-arrow-down.is-reverse{transform:rotate(180deg)}.el-cascader .el-input .icon-circle-close:hover{color:var(--el-input-clear-hover-color,var(--el-text-color-secondary))}.el-cascader .el-input.is-focus .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color,var(--el-color-primary)) inset}.el-cascader--large{font-size:14px;line-height:40px}.el-cascader--large .el-cascader__tags{gap:6px;padding:8px}.el-cascader--large .el-cascader__search-input{height:24px;margin-left:7px}.el-cascader--small{font-size:12px;line-height:24px}.el-cascader--small .el-cascader__tags{gap:4px;padding:2px}.el-cascader--small .el-cascader__search-input{height:20px;margin-left:5px}.el-cascader.is-disabled .el-cascader__label{z-index:calc(var(--el-index-normal) + 1);color:var(--el-disabled-text-color)}.el-cascader__dropdown{--el-cascader-menu-text-color:var(--el-text-color-regular);--el-cascader-menu-selected-text-color:var(--el-color-primary);--el-cascader-menu-fill:var(--el-bg-color-overlay);--el-cascader-menu-font-size:var(--el-font-size-base);--el-cascader-menu-radius:var(--el-border-radius-base);--el-cascader-menu-border:solid 1px var(--el-border-color-light);--el-cascader-menu-shadow:var(--el-box-shadow-light);--el-cascader-node-background-hover:var(--el-fill-color-light);--el-cascader-node-color-disabled:var(--el-text-color-placeholder);--el-cascader-color-empty:var(--el-text-color-placeholder);--el-cascader-tag-background:var(--el-fill-color);font-size:var(--el-cascader-menu-font-size);border-radius:var(--el-cascader-menu-radius)}.el-cascader__dropdown.el-popper{background:var(--el-cascader-menu-fill);border:var(--el-cascader-menu-border);box-shadow:var(--el-cascader-menu-shadow)}.el-cascader__dropdown.el-popper .el-popper__arrow:before{border:var(--el-cascader-menu-border)}.el-cascader__dropdown.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-cascader__dropdown.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-cascader__dropdown.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-cascader__dropdown.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-cascader__dropdown.el-popper{box-shadow:var(--el-cascader-menu-shadow)}.el-cascader__header{border-bottom:1px solid var(--el-border-color-light);padding:10px}.el-cascader__footer{border-top:1px solid var(--el-border-color-light);padding:10px}.el-cascader__tags{text-align:left;box-sizing:border-box;flex-wrap:wrap;gap:6px;padding:4px;line-height:normal;display:flex;position:absolute;top:50%;left:0;right:30px;transform:translateY(-50%)}.el-cascader__tags .el-tag{text-overflow:ellipsis;background:var(--el-cascader-tag-background);align-items:center;max-width:100%;display:inline-flex}.el-cascader__tags .el-tag.el-tag--dark,.el-cascader__tags .el-tag.el-tag--plain{background-color:var(--el-tag-bg-color)}.el-cascader__tags .el-tag:not(.is-hit){border-color:#0000}.el-cascader__tags .el-tag:not(.is-hit).el-tag--dark,.el-cascader__tags .el-tag:not(.is-hit).el-tag--plain{border-color:var(--el-tag-border-color)}.el-cascader__tags .el-tag>span{text-overflow:ellipsis;flex:1;line-height:normal;overflow:hidden}.el-cascader__tags .el-tag .el-icon-close{background-color:var(--el-text-color-placeholder);color:var(--el-color-white);flex:none}.el-cascader__tags .el-tag .el-icon-close:hover{background-color:var(--el-text-color-secondary)}.el-cascader__tags .el-tag+input{margin-left:0}.el-cascader__tags.is-validate{right:55px}.el-cascader__collapse-tags{white-space:normal;z-index:var(--el-index-normal)}.el-cascader__collapse-tags .el-tag{text-overflow:ellipsis;background:var(--el-fill-color);align-items:center;max-width:100%;display:inline-flex}.el-cascader__collapse-tags .el-tag.el-tag--dark,.el-cascader__collapse-tags .el-tag.el-tag--plain{background-color:var(--el-tag-bg-color)}.el-cascader__collapse-tags .el-tag:not(.is-hit){border-color:#0000}.el-cascader__collapse-tags .el-tag:not(.is-hit).el-tag--dark,.el-cascader__collapse-tags .el-tag:not(.is-hit).el-tag--plain{border-color:var(--el-tag-border-color)}.el-cascader__collapse-tags .el-tag>span{text-overflow:ellipsis;flex:1;line-height:normal;overflow:hidden}.el-cascader__collapse-tags .el-tag .el-icon-close{background-color:var(--el-text-color-placeholder);color:var(--el-color-white);flex:none}.el-cascader__collapse-tags .el-tag .el-icon-close:hover{background-color:var(--el-text-color-secondary)}.el-cascader__collapse-tags .el-tag+input{margin-left:0}.el-cascader__collapse-tags .el-tag{margin:2px 0}.el-cascader__suggestion-panel{border-radius:var(--el-cascader-menu-radius)}.el-cascader__suggestion-wrap{max-height:204px}.el-cascader__suggestion-list{font-size:var(--el-font-size-base);color:var(--el-cascader-menu-text-color);text-align:center;margin:0;padding:6px 0;list-style:none}.el-cascader__suggestion-list.el-vl__window{margin:6px 0;padding:0}.el-cascader__suggestion-list.el-vl__window ul{margin:0;padding:0}.el-cascader__suggestion-item{text-align:left;cursor:pointer;box-sizing:border-box;outline:none;justify-content:space-between;align-items:center;height:34px;padding:0 15px;display:flex}.el-cascader__suggestion-item:hover,.el-cascader__suggestion-item:focus{background:var(--el-cascader-node-background-hover)}.el-cascader__suggestion-item.is-checked{color:var(--el-cascader-menu-selected-text-color);font-weight:700}.el-cascader__suggestion-item>:not(.el-icon){white-space:nowrap;text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.el-cascader__suggestion-item>span{margin-right:10px}.el-cascader__suggestion-item .el-icon{flex-shrink:0}.el-cascader__empty-text{color:var(--el-cascader-color-empty);margin:10px 0}.el-cascader__search-input{min-width:60px;height:24px;color:var(--el-cascader-menu-text-color);box-sizing:border-box;background:0 0;border:none;outline:none;flex:1;margin-left:7px;padding:0}.el-cascader__search-input::placeholder{color:#0000}.el-check-tag{background-color:var(--el-color-info-light-9);border-radius:var(--el-border-radius-base);color:var(--el-color-info);cursor:pointer;font-size:var(--el-font-size-base);line-height:var(--el-font-size-base);transition:var(--el-transition-all);padding:7px 15px;font-weight:700;display:inline-block}.el-check-tag:hover{background-color:var(--el-color-info-light-7)}.el-check-tag.el-check-tag--primary.is-checked{background-color:var(--el-color-primary-light-8);color:var(--el-color-primary)}.el-check-tag.el-check-tag--primary.is-checked:hover{background-color:var(--el-color-primary-light-7)}.el-check-tag.el-check-tag--primary.is-checked.is-disabled{background-color:var(--el-color-primary-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--primary.is-checked.is-disabled:hover{background-color:var(--el-color-primary-light-8)}.el-check-tag.el-check-tag--primary.is-disabled{background-color:var(--el-color-info-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--primary.is-disabled:hover{background-color:var(--el-color-info-light-9)}.el-check-tag.el-check-tag--success.is-checked{background-color:var(--el-color-success-light-8);color:var(--el-color-success)}.el-check-tag.el-check-tag--success.is-checked:hover{background-color:var(--el-color-success-light-7)}.el-check-tag.el-check-tag--success.is-checked.is-disabled{background-color:var(--el-color-success-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--success.is-checked.is-disabled:hover{background-color:var(--el-color-success-light-8)}.el-check-tag.el-check-tag--success.is-disabled{background-color:var(--el-color-success-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--success.is-disabled:hover{background-color:var(--el-color-success-light-9)}.el-check-tag.el-check-tag--warning.is-checked{background-color:var(--el-color-warning-light-8);color:var(--el-color-warning)}.el-check-tag.el-check-tag--warning.is-checked:hover{background-color:var(--el-color-warning-light-7)}.el-check-tag.el-check-tag--warning.is-checked.is-disabled{background-color:var(--el-color-warning-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--warning.is-checked.is-disabled:hover{background-color:var(--el-color-warning-light-8)}.el-check-tag.el-check-tag--warning.is-disabled{background-color:var(--el-color-warning-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--warning.is-disabled:hover{background-color:var(--el-color-warning-light-9)}.el-check-tag.el-check-tag--danger.is-checked{background-color:var(--el-color-danger-light-8);color:var(--el-color-danger)}.el-check-tag.el-check-tag--danger.is-checked:hover{background-color:var(--el-color-danger-light-7)}.el-check-tag.el-check-tag--danger.is-checked.is-disabled{background-color:var(--el-color-danger-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--danger.is-checked.is-disabled:hover{background-color:var(--el-color-danger-light-8)}.el-check-tag.el-check-tag--danger.is-disabled{background-color:var(--el-color-danger-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--danger.is-disabled:hover{background-color:var(--el-color-danger-light-9)}.el-check-tag.el-check-tag--error.is-checked{background-color:var(--el-color-error-light-8);color:var(--el-color-error)}.el-check-tag.el-check-tag--error.is-checked:hover{background-color:var(--el-color-error-light-7)}.el-check-tag.el-check-tag--error.is-checked.is-disabled{background-color:var(--el-color-error-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--error.is-checked.is-disabled:hover{background-color:var(--el-color-error-light-8)}.el-check-tag.el-check-tag--error.is-disabled{background-color:var(--el-color-error-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--error.is-disabled:hover{background-color:var(--el-color-error-light-9)}.el-check-tag.el-check-tag--info.is-checked{background-color:var(--el-color-info-light-8);color:var(--el-color-info)}.el-check-tag.el-check-tag--info.is-checked:hover{background-color:var(--el-color-info-light-7)}.el-check-tag.el-check-tag--info.is-checked.is-disabled{background-color:var(--el-color-info-light-8);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--info.is-checked.is-disabled:hover{background-color:var(--el-color-info-light-8)}.el-check-tag.el-check-tag--info.is-disabled{background-color:var(--el-color-info-light-9);color:var(--el-disabled-text-color);cursor:not-allowed}.el-check-tag.el-check-tag--info.is-disabled:hover{background-color:var(--el-color-info-light-9)}.el-checkbox-button{--el-checkbox-button-checked-bg-color:var(--el-color-primary);--el-checkbox-button-checked-text-color:var(--el-color-white);--el-checkbox-button-checked-border-color:var(--el-color-primary);--el-checkbox-button-disabled-checked-fill:var(--el-border-color-extra-light);display:inline-block;position:relative}.el-checkbox-button__inner{line-height:1;font-weight:var(--el-checkbox-font-weight);white-space:nowrap;vertical-align:middle;cursor:pointer;background:var(--el-button-bg-color,var(--el-fill-color-blank));outline:var(--el-border);color:var(--el-button-text-color,var(--el-text-color-regular));-webkit-appearance:none;text-align:center;box-sizing:border-box;transition:var(--el-transition-all);-webkit-user-select:none;user-select:none;font-size:var(--el-font-size-base);border-radius:0;margin:0;padding:8px 15px;display:inline-block;position:relative}.el-checkbox-button__inner.is-round{padding:8px 15px}.el-checkbox-button__inner:hover{color:var(--el-color-primary)}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;z-index:-1;outline:none;margin:0;position:absolute}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:var(--el-checkbox-button-checked-text-color);background-color:var(--el-checkbox-button-checked-bg-color);border-color:var(--el-checkbox-button-checked-border-color);box-shadow:-1px 0 0 0 var(--el-color-primary-light-7)}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:var(--el-checkbox-button-checked-border-color)}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:var(--el-disabled-text-color);cursor:not-allowed;background-image:none;background-color:var(--el-button-disabled-bg-color,var(--el-fill-color-blank));border-color:var(--el-button-disabled-border-color,var(--el-border-color-light));box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:var(--el-button-disabled-border-color,var(--el-border-color-light))}.el-checkbox-button.is-disabled.is-checked .el-checkbox-button__inner{background-color:var(--el-checkbox-button-disabled-checked-fill)}.el-checkbox-button:first-child .el-checkbox-button__inner{border-top-left-radius:var(--el-border-radius-base);border-bottom-left-radius:var(--el-border-radius-base);box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:var(--el-checkbox-button-checked-border-color)}.el-checkbox-button:last-child .el-checkbox-button__inner{border-top-right-radius:var(--el-border-radius-base);border-bottom-right-radius:var(--el-border-radius-base)}.el-checkbox-button--large .el-checkbox-button__inner{font-size:var(--el-font-size-base);border-radius:0;padding:12px 19px}.el-checkbox-button--large .el-checkbox-button__inner.is-round{padding:12px 19px}.el-checkbox-button--small .el-checkbox-button__inner{border-radius:0;padding:5px 11px;font-size:12px}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:5px 11px}.el-checkbox-group{font-size:0;line-height:0}.el-checkbox{--el-checkbox-font-size:14px;--el-checkbox-font-weight:var(--el-font-weight-primary);--el-checkbox-text-color:var(--el-text-color-regular);--el-checkbox-input-height:14px;--el-checkbox-input-width:14px;--el-checkbox-border-radius:var(--el-border-radius-small);--el-checkbox-bg-color:var(--el-fill-color-blank);--el-checkbox-input-border:var(--el-border);--el-checkbox-disabled-border-color:var(--el-border-color);--el-checkbox-disabled-input-fill:var(--el-fill-color-light);--el-checkbox-disabled-icon-color:var(--el-text-color-placeholder);--el-checkbox-disabled-checked-input-fill:var(--el-border-color-extra-light);--el-checkbox-disabled-checked-input-border-color:var(--el-border-color);--el-checkbox-disabled-checked-icon-color:var(--el-text-color-placeholder);--el-checkbox-checked-text-color:var(--el-color-primary);--el-checkbox-checked-input-border-color:var(--el-color-primary);--el-checkbox-checked-bg-color:var(--el-color-primary);--el-checkbox-checked-icon-color:var(--el-color-white);--el-checkbox-input-border-color-hover:var(--el-color-primary);color:var(--el-checkbox-text-color);font-weight:var(--el-checkbox-font-weight);font-size:var(--el-font-size-base);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;height:var(--el-checkbox-height,32px);align-items:center;margin-right:30px;display:inline-flex;position:relative}.el-checkbox.is-disabled{cursor:not-allowed}.el-checkbox.is-bordered{border-radius:var(--el-border-radius-base);border:var(--el-border);box-sizing:border-box;padding:0 15px 0 9px}.el-checkbox.is-bordered.is-checked{border-color:var(--el-color-primary)}.el-checkbox.is-bordered.is-disabled{border-color:var(--el-border-color-lighter)}.el-checkbox.is-bordered.el-checkbox--large{border-radius:var(--el-border-radius-base);padding:0 19px 0 11px}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__label{font-size:var(--el-font-size-base)}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__inner{width:14px;height:14px}.el-checkbox.is-bordered.el-checkbox--small{border-radius:calc(var(--el-border-radius-base) - 1px);padding:0 11px 0 7px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{width:12px;height:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{width:2px;height:6px}.el-checkbox input:focus-visible+.el-checkbox__inner{outline:2px solid var(--el-checkbox-input-border-color-hover);outline-offset:1px;border-radius:var(--el-checkbox-border-radius)}.el-checkbox__input{white-space:nowrap;cursor:pointer;outline:none;display:inline-flex;position:relative}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:var(--el-checkbox-disabled-input-fill);border-color:var(--el-checkbox-disabled-border-color);cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:var(--el-checkbox-disabled-icon-color);will-change:transform}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:var(--el-checkbox-disabled-checked-icon-color);border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:var(--el-disabled-text-color);cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-checked-icon-color);transform:translate(-45%,-60%)rotate(45deg)scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:var(--el-checkbox-checked-text-color)}.el-checkbox__input.is-focus:not(.is-checked) .el-checkbox__original:not(:focus-visible){border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";background-color:var(--el-checkbox-checked-icon-color);height:2px;display:block;position:absolute;top:5px;left:0;right:0;transform:scale(.5)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{border:var(--el-checkbox-input-border);border-radius:var(--el-checkbox-border-radius);box-sizing:border-box;width:var(--el-checkbox-input-width);height:var(--el-checkbox-input-height);background-color:var(--el-checkbox-bg-color);z-index:var(--el-index-normal);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46),outline .25s cubic-bezier(.71,-.46,.29,1.46);display:inline-block;position:relative}.el-checkbox__inner:hover{border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__inner:after{box-sizing:content-box;content:"";transform-origin:50%;border:1px solid #0000;border-top:0;border-left:0;width:3px;height:7px;transition:transform .15s ease-in 50ms;position:absolute;top:50%;left:50%;transform:translate(-45%,-60%)rotate(45deg)scaleY(0)}.el-checkbox__original{opacity:0;z-index:-1;outline:none;width:0;height:0;margin:0;position:absolute}.el-checkbox__label{line-height:1;font-size:var(--el-checkbox-font-size);padding-left:8px;display:inline-block}.el-checkbox.el-checkbox--large{height:40px}.el-checkbox.el-checkbox--large .el-checkbox__label{font-size:14px}.el-checkbox.el-checkbox--large .el-checkbox__inner{width:14px;height:14px}.el-checkbox.el-checkbox--small{height:24px}.el-checkbox.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.el-checkbox--small .el-checkbox__inner{width:12px;height:12px}.el-checkbox.el-checkbox--small .el-checkbox__input.is-indeterminate .el-checkbox__inner:before{top:4px}.el-checkbox.el-checkbox--small .el-checkbox__inner:after{width:2px;height:6px}.el-checkbox:last-of-type{margin-right:0}[class*=el-col-]{box-sizing:border-box}[class*=el-col-].is-guttered{min-height:1px;display:block}.el-col-0{flex:0 0;max-width:0%;display:none}.el-col-0.is-guttered{display:none}.el-col-offset-0{margin-left:0%}.el-col-pull-0{position:relative;right:0%}.el-col-push-0{position:relative;left:0%}.el-col-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-1.is-guttered{display:block}.el-col-offset-1{margin-left:4.16667%}.el-col-pull-1{position:relative;right:4.16667%}.el-col-push-1{position:relative;left:4.16667%}.el-col-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-2.is-guttered{display:block}.el-col-offset-2{margin-left:8.33333%}.el-col-pull-2{position:relative;right:8.33333%}.el-col-push-2{position:relative;left:8.33333%}.el-col-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-3.is-guttered{display:block}.el-col-offset-3{margin-left:12.5%}.el-col-pull-3{position:relative;right:12.5%}.el-col-push-3{position:relative;left:12.5%}.el-col-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-4.is-guttered{display:block}.el-col-offset-4{margin-left:16.6667%}.el-col-pull-4{position:relative;right:16.6667%}.el-col-push-4{position:relative;left:16.6667%}.el-col-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-5.is-guttered{display:block}.el-col-offset-5{margin-left:20.8333%}.el-col-pull-5{position:relative;right:20.8333%}.el-col-push-5{position:relative;left:20.8333%}.el-col-6{flex:0 0 25%;max-width:25%;display:block}.el-col-6.is-guttered{display:block}.el-col-offset-6{margin-left:25%}.el-col-pull-6{position:relative;right:25%}.el-col-push-6{position:relative;left:25%}.el-col-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-7.is-guttered{display:block}.el-col-offset-7{margin-left:29.1667%}.el-col-pull-7{position:relative;right:29.1667%}.el-col-push-7{position:relative;left:29.1667%}.el-col-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-8.is-guttered{display:block}.el-col-offset-8{margin-left:33.3333%}.el-col-pull-8{position:relative;right:33.3333%}.el-col-push-8{position:relative;left:33.3333%}.el-col-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-9.is-guttered{display:block}.el-col-offset-9{margin-left:37.5%}.el-col-pull-9{position:relative;right:37.5%}.el-col-push-9{position:relative;left:37.5%}.el-col-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-10.is-guttered{display:block}.el-col-offset-10{margin-left:41.6667%}.el-col-pull-10{position:relative;right:41.6667%}.el-col-push-10{position:relative;left:41.6667%}.el-col-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-11.is-guttered{display:block}.el-col-offset-11{margin-left:45.8333%}.el-col-pull-11{position:relative;right:45.8333%}.el-col-push-11{position:relative;left:45.8333%}.el-col-12{flex:0 0 50%;max-width:50%;display:block}.el-col-12.is-guttered{display:block}.el-col-offset-12{margin-left:50%}.el-col-pull-12{position:relative;right:50%}.el-col-push-12{position:relative;left:50%}.el-col-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-13.is-guttered{display:block}.el-col-offset-13{margin-left:54.1667%}.el-col-pull-13{position:relative;right:54.1667%}.el-col-push-13{position:relative;left:54.1667%}.el-col-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-14.is-guttered{display:block}.el-col-offset-14{margin-left:58.3333%}.el-col-pull-14{position:relative;right:58.3333%}.el-col-push-14{position:relative;left:58.3333%}.el-col-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-15.is-guttered{display:block}.el-col-offset-15{margin-left:62.5%}.el-col-pull-15{position:relative;right:62.5%}.el-col-push-15{position:relative;left:62.5%}.el-col-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-16.is-guttered{display:block}.el-col-offset-16{margin-left:66.6667%}.el-col-pull-16{position:relative;right:66.6667%}.el-col-push-16{position:relative;left:66.6667%}.el-col-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-17.is-guttered{display:block}.el-col-offset-17{margin-left:70.8333%}.el-col-pull-17{position:relative;right:70.8333%}.el-col-push-17{position:relative;left:70.8333%}.el-col-18{flex:0 0 75%;max-width:75%;display:block}.el-col-18.is-guttered{display:block}.el-col-offset-18{margin-left:75%}.el-col-pull-18{position:relative;right:75%}.el-col-push-18{position:relative;left:75%}.el-col-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-19.is-guttered{display:block}.el-col-offset-19{margin-left:79.1667%}.el-col-pull-19{position:relative;right:79.1667%}.el-col-push-19{position:relative;left:79.1667%}.el-col-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-20.is-guttered{display:block}.el-col-offset-20{margin-left:83.3333%}.el-col-pull-20{position:relative;right:83.3333%}.el-col-push-20{position:relative;left:83.3333%}.el-col-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-21.is-guttered{display:block}.el-col-offset-21{margin-left:87.5%}.el-col-pull-21{position:relative;right:87.5%}.el-col-push-21{position:relative;left:87.5%}.el-col-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-22.is-guttered{display:block}.el-col-offset-22{margin-left:91.6667%}.el-col-pull-22{position:relative;right:91.6667%}.el-col-push-22{position:relative;left:91.6667%}.el-col-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-23.is-guttered{display:block}.el-col-offset-23{margin-left:95.8333%}.el-col-pull-23{position:relative;right:95.8333%}.el-col-push-23{position:relative;left:95.8333%}.el-col-24{flex:0 0 100%;max-width:100%;display:block}.el-col-24.is-guttered{display:block}.el-col-offset-24{margin-left:100%}.el-col-pull-24{position:relative;right:100%}.el-col-push-24{position:relative;left:100%}@media only screen and (max-width:767px){.el-col-xs-0{flex:0 0;max-width:0%;display:none}.el-col-xs-0.is-guttered{display:none}.el-col-xs-offset-0{margin-left:0%}.el-col-xs-pull-0{position:relative;right:0%}.el-col-xs-push-0{position:relative;left:0%}.el-col-xs-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-xs-1.is-guttered{display:block}.el-col-xs-offset-1{margin-left:4.16667%}.el-col-xs-pull-1{position:relative;right:4.16667%}.el-col-xs-push-1{position:relative;left:4.16667%}.el-col-xs-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-xs-2.is-guttered{display:block}.el-col-xs-offset-2{margin-left:8.33333%}.el-col-xs-pull-2{position:relative;right:8.33333%}.el-col-xs-push-2{position:relative;left:8.33333%}.el-col-xs-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-xs-3.is-guttered{display:block}.el-col-xs-offset-3{margin-left:12.5%}.el-col-xs-pull-3{position:relative;right:12.5%}.el-col-xs-push-3{position:relative;left:12.5%}.el-col-xs-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-xs-4.is-guttered{display:block}.el-col-xs-offset-4{margin-left:16.6667%}.el-col-xs-pull-4{position:relative;right:16.6667%}.el-col-xs-push-4{position:relative;left:16.6667%}.el-col-xs-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-xs-5.is-guttered{display:block}.el-col-xs-offset-5{margin-left:20.8333%}.el-col-xs-pull-5{position:relative;right:20.8333%}.el-col-xs-push-5{position:relative;left:20.8333%}.el-col-xs-6{flex:0 0 25%;max-width:25%;display:block}.el-col-xs-6.is-guttered{display:block}.el-col-xs-offset-6{margin-left:25%}.el-col-xs-pull-6{position:relative;right:25%}.el-col-xs-push-6{position:relative;left:25%}.el-col-xs-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-xs-7.is-guttered{display:block}.el-col-xs-offset-7{margin-left:29.1667%}.el-col-xs-pull-7{position:relative;right:29.1667%}.el-col-xs-push-7{position:relative;left:29.1667%}.el-col-xs-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-xs-8.is-guttered{display:block}.el-col-xs-offset-8{margin-left:33.3333%}.el-col-xs-pull-8{position:relative;right:33.3333%}.el-col-xs-push-8{position:relative;left:33.3333%}.el-col-xs-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-xs-9.is-guttered{display:block}.el-col-xs-offset-9{margin-left:37.5%}.el-col-xs-pull-9{position:relative;right:37.5%}.el-col-xs-push-9{position:relative;left:37.5%}.el-col-xs-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-xs-10.is-guttered{display:block}.el-col-xs-offset-10{margin-left:41.6667%}.el-col-xs-pull-10{position:relative;right:41.6667%}.el-col-xs-push-10{position:relative;left:41.6667%}.el-col-xs-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-xs-11.is-guttered{display:block}.el-col-xs-offset-11{margin-left:45.8333%}.el-col-xs-pull-11{position:relative;right:45.8333%}.el-col-xs-push-11{position:relative;left:45.8333%}.el-col-xs-12{flex:0 0 50%;max-width:50%;display:block}.el-col-xs-12.is-guttered{display:block}.el-col-xs-offset-12{margin-left:50%}.el-col-xs-pull-12{position:relative;right:50%}.el-col-xs-push-12{position:relative;left:50%}.el-col-xs-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-xs-13.is-guttered{display:block}.el-col-xs-offset-13{margin-left:54.1667%}.el-col-xs-pull-13{position:relative;right:54.1667%}.el-col-xs-push-13{position:relative;left:54.1667%}.el-col-xs-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-xs-14.is-guttered{display:block}.el-col-xs-offset-14{margin-left:58.3333%}.el-col-xs-pull-14{position:relative;right:58.3333%}.el-col-xs-push-14{position:relative;left:58.3333%}.el-col-xs-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-xs-15.is-guttered{display:block}.el-col-xs-offset-15{margin-left:62.5%}.el-col-xs-pull-15{position:relative;right:62.5%}.el-col-xs-push-15{position:relative;left:62.5%}.el-col-xs-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-xs-16.is-guttered{display:block}.el-col-xs-offset-16{margin-left:66.6667%}.el-col-xs-pull-16{position:relative;right:66.6667%}.el-col-xs-push-16{position:relative;left:66.6667%}.el-col-xs-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-xs-17.is-guttered{display:block}.el-col-xs-offset-17{margin-left:70.8333%}.el-col-xs-pull-17{position:relative;right:70.8333%}.el-col-xs-push-17{position:relative;left:70.8333%}.el-col-xs-18{flex:0 0 75%;max-width:75%;display:block}.el-col-xs-18.is-guttered{display:block}.el-col-xs-offset-18{margin-left:75%}.el-col-xs-pull-18{position:relative;right:75%}.el-col-xs-push-18{position:relative;left:75%}.el-col-xs-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-xs-19.is-guttered{display:block}.el-col-xs-offset-19{margin-left:79.1667%}.el-col-xs-pull-19{position:relative;right:79.1667%}.el-col-xs-push-19{position:relative;left:79.1667%}.el-col-xs-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-xs-20.is-guttered{display:block}.el-col-xs-offset-20{margin-left:83.3333%}.el-col-xs-pull-20{position:relative;right:83.3333%}.el-col-xs-push-20{position:relative;left:83.3333%}.el-col-xs-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-xs-21.is-guttered{display:block}.el-col-xs-offset-21{margin-left:87.5%}.el-col-xs-pull-21{position:relative;right:87.5%}.el-col-xs-push-21{position:relative;left:87.5%}.el-col-xs-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-xs-22.is-guttered{display:block}.el-col-xs-offset-22{margin-left:91.6667%}.el-col-xs-pull-22{position:relative;right:91.6667%}.el-col-xs-push-22{position:relative;left:91.6667%}.el-col-xs-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-xs-23.is-guttered{display:block}.el-col-xs-offset-23{margin-left:95.8333%}.el-col-xs-pull-23{position:relative;right:95.8333%}.el-col-xs-push-23{position:relative;left:95.8333%}.el-col-xs-24{flex:0 0 100%;max-width:100%;display:block}.el-col-xs-24.is-guttered{display:block}.el-col-xs-offset-24{margin-left:100%}.el-col-xs-pull-24{position:relative;right:100%}.el-col-xs-push-24{position:relative;left:100%}}@media only screen and (min-width:768px){.el-col-sm-0{flex:0 0;max-width:0%;display:none}.el-col-sm-0.is-guttered{display:none}.el-col-sm-offset-0{margin-left:0%}.el-col-sm-pull-0{position:relative;right:0%}.el-col-sm-push-0{position:relative;left:0%}.el-col-sm-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-sm-1.is-guttered{display:block}.el-col-sm-offset-1{margin-left:4.16667%}.el-col-sm-pull-1{position:relative;right:4.16667%}.el-col-sm-push-1{position:relative;left:4.16667%}.el-col-sm-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-sm-2.is-guttered{display:block}.el-col-sm-offset-2{margin-left:8.33333%}.el-col-sm-pull-2{position:relative;right:8.33333%}.el-col-sm-push-2{position:relative;left:8.33333%}.el-col-sm-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-sm-3.is-guttered{display:block}.el-col-sm-offset-3{margin-left:12.5%}.el-col-sm-pull-3{position:relative;right:12.5%}.el-col-sm-push-3{position:relative;left:12.5%}.el-col-sm-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-sm-4.is-guttered{display:block}.el-col-sm-offset-4{margin-left:16.6667%}.el-col-sm-pull-4{position:relative;right:16.6667%}.el-col-sm-push-4{position:relative;left:16.6667%}.el-col-sm-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-sm-5.is-guttered{display:block}.el-col-sm-offset-5{margin-left:20.8333%}.el-col-sm-pull-5{position:relative;right:20.8333%}.el-col-sm-push-5{position:relative;left:20.8333%}.el-col-sm-6{flex:0 0 25%;max-width:25%;display:block}.el-col-sm-6.is-guttered{display:block}.el-col-sm-offset-6{margin-left:25%}.el-col-sm-pull-6{position:relative;right:25%}.el-col-sm-push-6{position:relative;left:25%}.el-col-sm-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-sm-7.is-guttered{display:block}.el-col-sm-offset-7{margin-left:29.1667%}.el-col-sm-pull-7{position:relative;right:29.1667%}.el-col-sm-push-7{position:relative;left:29.1667%}.el-col-sm-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-sm-8.is-guttered{display:block}.el-col-sm-offset-8{margin-left:33.3333%}.el-col-sm-pull-8{position:relative;right:33.3333%}.el-col-sm-push-8{position:relative;left:33.3333%}.el-col-sm-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-sm-9.is-guttered{display:block}.el-col-sm-offset-9{margin-left:37.5%}.el-col-sm-pull-9{position:relative;right:37.5%}.el-col-sm-push-9{position:relative;left:37.5%}.el-col-sm-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-sm-10.is-guttered{display:block}.el-col-sm-offset-10{margin-left:41.6667%}.el-col-sm-pull-10{position:relative;right:41.6667%}.el-col-sm-push-10{position:relative;left:41.6667%}.el-col-sm-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-sm-11.is-guttered{display:block}.el-col-sm-offset-11{margin-left:45.8333%}.el-col-sm-pull-11{position:relative;right:45.8333%}.el-col-sm-push-11{position:relative;left:45.8333%}.el-col-sm-12{flex:0 0 50%;max-width:50%;display:block}.el-col-sm-12.is-guttered{display:block}.el-col-sm-offset-12{margin-left:50%}.el-col-sm-pull-12{position:relative;right:50%}.el-col-sm-push-12{position:relative;left:50%}.el-col-sm-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-sm-13.is-guttered{display:block}.el-col-sm-offset-13{margin-left:54.1667%}.el-col-sm-pull-13{position:relative;right:54.1667%}.el-col-sm-push-13{position:relative;left:54.1667%}.el-col-sm-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-sm-14.is-guttered{display:block}.el-col-sm-offset-14{margin-left:58.3333%}.el-col-sm-pull-14{position:relative;right:58.3333%}.el-col-sm-push-14{position:relative;left:58.3333%}.el-col-sm-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-sm-15.is-guttered{display:block}.el-col-sm-offset-15{margin-left:62.5%}.el-col-sm-pull-15{position:relative;right:62.5%}.el-col-sm-push-15{position:relative;left:62.5%}.el-col-sm-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-sm-16.is-guttered{display:block}.el-col-sm-offset-16{margin-left:66.6667%}.el-col-sm-pull-16{position:relative;right:66.6667%}.el-col-sm-push-16{position:relative;left:66.6667%}.el-col-sm-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-sm-17.is-guttered{display:block}.el-col-sm-offset-17{margin-left:70.8333%}.el-col-sm-pull-17{position:relative;right:70.8333%}.el-col-sm-push-17{position:relative;left:70.8333%}.el-col-sm-18{flex:0 0 75%;max-width:75%;display:block}.el-col-sm-18.is-guttered{display:block}.el-col-sm-offset-18{margin-left:75%}.el-col-sm-pull-18{position:relative;right:75%}.el-col-sm-push-18{position:relative;left:75%}.el-col-sm-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-sm-19.is-guttered{display:block}.el-col-sm-offset-19{margin-left:79.1667%}.el-col-sm-pull-19{position:relative;right:79.1667%}.el-col-sm-push-19{position:relative;left:79.1667%}.el-col-sm-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-sm-20.is-guttered{display:block}.el-col-sm-offset-20{margin-left:83.3333%}.el-col-sm-pull-20{position:relative;right:83.3333%}.el-col-sm-push-20{position:relative;left:83.3333%}.el-col-sm-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-sm-21.is-guttered{display:block}.el-col-sm-offset-21{margin-left:87.5%}.el-col-sm-pull-21{position:relative;right:87.5%}.el-col-sm-push-21{position:relative;left:87.5%}.el-col-sm-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-sm-22.is-guttered{display:block}.el-col-sm-offset-22{margin-left:91.6667%}.el-col-sm-pull-22{position:relative;right:91.6667%}.el-col-sm-push-22{position:relative;left:91.6667%}.el-col-sm-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-sm-23.is-guttered{display:block}.el-col-sm-offset-23{margin-left:95.8333%}.el-col-sm-pull-23{position:relative;right:95.8333%}.el-col-sm-push-23{position:relative;left:95.8333%}.el-col-sm-24{flex:0 0 100%;max-width:100%;display:block}.el-col-sm-24.is-guttered{display:block}.el-col-sm-offset-24{margin-left:100%}.el-col-sm-pull-24{position:relative;right:100%}.el-col-sm-push-24{position:relative;left:100%}}@media only screen and (min-width:992px){.el-col-md-0{flex:0 0;max-width:0%;display:none}.el-col-md-0.is-guttered{display:none}.el-col-md-offset-0{margin-left:0%}.el-col-md-pull-0{position:relative;right:0%}.el-col-md-push-0{position:relative;left:0%}.el-col-md-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-md-1.is-guttered{display:block}.el-col-md-offset-1{margin-left:4.16667%}.el-col-md-pull-1{position:relative;right:4.16667%}.el-col-md-push-1{position:relative;left:4.16667%}.el-col-md-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-md-2.is-guttered{display:block}.el-col-md-offset-2{margin-left:8.33333%}.el-col-md-pull-2{position:relative;right:8.33333%}.el-col-md-push-2{position:relative;left:8.33333%}.el-col-md-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-md-3.is-guttered{display:block}.el-col-md-offset-3{margin-left:12.5%}.el-col-md-pull-3{position:relative;right:12.5%}.el-col-md-push-3{position:relative;left:12.5%}.el-col-md-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-md-4.is-guttered{display:block}.el-col-md-offset-4{margin-left:16.6667%}.el-col-md-pull-4{position:relative;right:16.6667%}.el-col-md-push-4{position:relative;left:16.6667%}.el-col-md-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-md-5.is-guttered{display:block}.el-col-md-offset-5{margin-left:20.8333%}.el-col-md-pull-5{position:relative;right:20.8333%}.el-col-md-push-5{position:relative;left:20.8333%}.el-col-md-6{flex:0 0 25%;max-width:25%;display:block}.el-col-md-6.is-guttered{display:block}.el-col-md-offset-6{margin-left:25%}.el-col-md-pull-6{position:relative;right:25%}.el-col-md-push-6{position:relative;left:25%}.el-col-md-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-md-7.is-guttered{display:block}.el-col-md-offset-7{margin-left:29.1667%}.el-col-md-pull-7{position:relative;right:29.1667%}.el-col-md-push-7{position:relative;left:29.1667%}.el-col-md-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-md-8.is-guttered{display:block}.el-col-md-offset-8{margin-left:33.3333%}.el-col-md-pull-8{position:relative;right:33.3333%}.el-col-md-push-8{position:relative;left:33.3333%}.el-col-md-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-md-9.is-guttered{display:block}.el-col-md-offset-9{margin-left:37.5%}.el-col-md-pull-9{position:relative;right:37.5%}.el-col-md-push-9{position:relative;left:37.5%}.el-col-md-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-md-10.is-guttered{display:block}.el-col-md-offset-10{margin-left:41.6667%}.el-col-md-pull-10{position:relative;right:41.6667%}.el-col-md-push-10{position:relative;left:41.6667%}.el-col-md-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-md-11.is-guttered{display:block}.el-col-md-offset-11{margin-left:45.8333%}.el-col-md-pull-11{position:relative;right:45.8333%}.el-col-md-push-11{position:relative;left:45.8333%}.el-col-md-12{flex:0 0 50%;max-width:50%;display:block}.el-col-md-12.is-guttered{display:block}.el-col-md-offset-12{margin-left:50%}.el-col-md-pull-12{position:relative;right:50%}.el-col-md-push-12{position:relative;left:50%}.el-col-md-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-md-13.is-guttered{display:block}.el-col-md-offset-13{margin-left:54.1667%}.el-col-md-pull-13{position:relative;right:54.1667%}.el-col-md-push-13{position:relative;left:54.1667%}.el-col-md-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-md-14.is-guttered{display:block}.el-col-md-offset-14{margin-left:58.3333%}.el-col-md-pull-14{position:relative;right:58.3333%}.el-col-md-push-14{position:relative;left:58.3333%}.el-col-md-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-md-15.is-guttered{display:block}.el-col-md-offset-15{margin-left:62.5%}.el-col-md-pull-15{position:relative;right:62.5%}.el-col-md-push-15{position:relative;left:62.5%}.el-col-md-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-md-16.is-guttered{display:block}.el-col-md-offset-16{margin-left:66.6667%}.el-col-md-pull-16{position:relative;right:66.6667%}.el-col-md-push-16{position:relative;left:66.6667%}.el-col-md-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-md-17.is-guttered{display:block}.el-col-md-offset-17{margin-left:70.8333%}.el-col-md-pull-17{position:relative;right:70.8333%}.el-col-md-push-17{position:relative;left:70.8333%}.el-col-md-18{flex:0 0 75%;max-width:75%;display:block}.el-col-md-18.is-guttered{display:block}.el-col-md-offset-18{margin-left:75%}.el-col-md-pull-18{position:relative;right:75%}.el-col-md-push-18{position:relative;left:75%}.el-col-md-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-md-19.is-guttered{display:block}.el-col-md-offset-19{margin-left:79.1667%}.el-col-md-pull-19{position:relative;right:79.1667%}.el-col-md-push-19{position:relative;left:79.1667%}.el-col-md-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-md-20.is-guttered{display:block}.el-col-md-offset-20{margin-left:83.3333%}.el-col-md-pull-20{position:relative;right:83.3333%}.el-col-md-push-20{position:relative;left:83.3333%}.el-col-md-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-md-21.is-guttered{display:block}.el-col-md-offset-21{margin-left:87.5%}.el-col-md-pull-21{position:relative;right:87.5%}.el-col-md-push-21{position:relative;left:87.5%}.el-col-md-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-md-22.is-guttered{display:block}.el-col-md-offset-22{margin-left:91.6667%}.el-col-md-pull-22{position:relative;right:91.6667%}.el-col-md-push-22{position:relative;left:91.6667%}.el-col-md-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-md-23.is-guttered{display:block}.el-col-md-offset-23{margin-left:95.8333%}.el-col-md-pull-23{position:relative;right:95.8333%}.el-col-md-push-23{position:relative;left:95.8333%}.el-col-md-24{flex:0 0 100%;max-width:100%;display:block}.el-col-md-24.is-guttered{display:block}.el-col-md-offset-24{margin-left:100%}.el-col-md-pull-24{position:relative;right:100%}.el-col-md-push-24{position:relative;left:100%}}@media only screen and (min-width:1200px){.el-col-lg-0{flex:0 0;max-width:0%;display:none}.el-col-lg-0.is-guttered{display:none}.el-col-lg-offset-0{margin-left:0%}.el-col-lg-pull-0{position:relative;right:0%}.el-col-lg-push-0{position:relative;left:0%}.el-col-lg-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-lg-1.is-guttered{display:block}.el-col-lg-offset-1{margin-left:4.16667%}.el-col-lg-pull-1{position:relative;right:4.16667%}.el-col-lg-push-1{position:relative;left:4.16667%}.el-col-lg-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-lg-2.is-guttered{display:block}.el-col-lg-offset-2{margin-left:8.33333%}.el-col-lg-pull-2{position:relative;right:8.33333%}.el-col-lg-push-2{position:relative;left:8.33333%}.el-col-lg-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-lg-3.is-guttered{display:block}.el-col-lg-offset-3{margin-left:12.5%}.el-col-lg-pull-3{position:relative;right:12.5%}.el-col-lg-push-3{position:relative;left:12.5%}.el-col-lg-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-lg-4.is-guttered{display:block}.el-col-lg-offset-4{margin-left:16.6667%}.el-col-lg-pull-4{position:relative;right:16.6667%}.el-col-lg-push-4{position:relative;left:16.6667%}.el-col-lg-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-lg-5.is-guttered{display:block}.el-col-lg-offset-5{margin-left:20.8333%}.el-col-lg-pull-5{position:relative;right:20.8333%}.el-col-lg-push-5{position:relative;left:20.8333%}.el-col-lg-6{flex:0 0 25%;max-width:25%;display:block}.el-col-lg-6.is-guttered{display:block}.el-col-lg-offset-6{margin-left:25%}.el-col-lg-pull-6{position:relative;right:25%}.el-col-lg-push-6{position:relative;left:25%}.el-col-lg-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-lg-7.is-guttered{display:block}.el-col-lg-offset-7{margin-left:29.1667%}.el-col-lg-pull-7{position:relative;right:29.1667%}.el-col-lg-push-7{position:relative;left:29.1667%}.el-col-lg-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-lg-8.is-guttered{display:block}.el-col-lg-offset-8{margin-left:33.3333%}.el-col-lg-pull-8{position:relative;right:33.3333%}.el-col-lg-push-8{position:relative;left:33.3333%}.el-col-lg-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-lg-9.is-guttered{display:block}.el-col-lg-offset-9{margin-left:37.5%}.el-col-lg-pull-9{position:relative;right:37.5%}.el-col-lg-push-9{position:relative;left:37.5%}.el-col-lg-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-lg-10.is-guttered{display:block}.el-col-lg-offset-10{margin-left:41.6667%}.el-col-lg-pull-10{position:relative;right:41.6667%}.el-col-lg-push-10{position:relative;left:41.6667%}.el-col-lg-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-lg-11.is-guttered{display:block}.el-col-lg-offset-11{margin-left:45.8333%}.el-col-lg-pull-11{position:relative;right:45.8333%}.el-col-lg-push-11{position:relative;left:45.8333%}.el-col-lg-12{flex:0 0 50%;max-width:50%;display:block}.el-col-lg-12.is-guttered{display:block}.el-col-lg-offset-12{margin-left:50%}.el-col-lg-pull-12{position:relative;right:50%}.el-col-lg-push-12{position:relative;left:50%}.el-col-lg-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-lg-13.is-guttered{display:block}.el-col-lg-offset-13{margin-left:54.1667%}.el-col-lg-pull-13{position:relative;right:54.1667%}.el-col-lg-push-13{position:relative;left:54.1667%}.el-col-lg-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-lg-14.is-guttered{display:block}.el-col-lg-offset-14{margin-left:58.3333%}.el-col-lg-pull-14{position:relative;right:58.3333%}.el-col-lg-push-14{position:relative;left:58.3333%}.el-col-lg-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-lg-15.is-guttered{display:block}.el-col-lg-offset-15{margin-left:62.5%}.el-col-lg-pull-15{position:relative;right:62.5%}.el-col-lg-push-15{position:relative;left:62.5%}.el-col-lg-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-lg-16.is-guttered{display:block}.el-col-lg-offset-16{margin-left:66.6667%}.el-col-lg-pull-16{position:relative;right:66.6667%}.el-col-lg-push-16{position:relative;left:66.6667%}.el-col-lg-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-lg-17.is-guttered{display:block}.el-col-lg-offset-17{margin-left:70.8333%}.el-col-lg-pull-17{position:relative;right:70.8333%}.el-col-lg-push-17{position:relative;left:70.8333%}.el-col-lg-18{flex:0 0 75%;max-width:75%;display:block}.el-col-lg-18.is-guttered{display:block}.el-col-lg-offset-18{margin-left:75%}.el-col-lg-pull-18{position:relative;right:75%}.el-col-lg-push-18{position:relative;left:75%}.el-col-lg-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-lg-19.is-guttered{display:block}.el-col-lg-offset-19{margin-left:79.1667%}.el-col-lg-pull-19{position:relative;right:79.1667%}.el-col-lg-push-19{position:relative;left:79.1667%}.el-col-lg-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-lg-20.is-guttered{display:block}.el-col-lg-offset-20{margin-left:83.3333%}.el-col-lg-pull-20{position:relative;right:83.3333%}.el-col-lg-push-20{position:relative;left:83.3333%}.el-col-lg-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-lg-21.is-guttered{display:block}.el-col-lg-offset-21{margin-left:87.5%}.el-col-lg-pull-21{position:relative;right:87.5%}.el-col-lg-push-21{position:relative;left:87.5%}.el-col-lg-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-lg-22.is-guttered{display:block}.el-col-lg-offset-22{margin-left:91.6667%}.el-col-lg-pull-22{position:relative;right:91.6667%}.el-col-lg-push-22{position:relative;left:91.6667%}.el-col-lg-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-lg-23.is-guttered{display:block}.el-col-lg-offset-23{margin-left:95.8333%}.el-col-lg-pull-23{position:relative;right:95.8333%}.el-col-lg-push-23{position:relative;left:95.8333%}.el-col-lg-24{flex:0 0 100%;max-width:100%;display:block}.el-col-lg-24.is-guttered{display:block}.el-col-lg-offset-24{margin-left:100%}.el-col-lg-pull-24{position:relative;right:100%}.el-col-lg-push-24{position:relative;left:100%}}@media only screen and (min-width:1920px){.el-col-xl-0{flex:0 0;max-width:0%;display:none}.el-col-xl-0.is-guttered{display:none}.el-col-xl-offset-0{margin-left:0%}.el-col-xl-pull-0{position:relative;right:0%}.el-col-xl-push-0{position:relative;left:0%}.el-col-xl-1{flex:0 0 4.16667%;max-width:4.16667%;display:block}.el-col-xl-1.is-guttered{display:block}.el-col-xl-offset-1{margin-left:4.16667%}.el-col-xl-pull-1{position:relative;right:4.16667%}.el-col-xl-push-1{position:relative;left:4.16667%}.el-col-xl-2{flex:0 0 8.33333%;max-width:8.33333%;display:block}.el-col-xl-2.is-guttered{display:block}.el-col-xl-offset-2{margin-left:8.33333%}.el-col-xl-pull-2{position:relative;right:8.33333%}.el-col-xl-push-2{position:relative;left:8.33333%}.el-col-xl-3{flex:0 0 12.5%;max-width:12.5%;display:block}.el-col-xl-3.is-guttered{display:block}.el-col-xl-offset-3{margin-left:12.5%}.el-col-xl-pull-3{position:relative;right:12.5%}.el-col-xl-push-3{position:relative;left:12.5%}.el-col-xl-4{flex:0 0 16.6667%;max-width:16.6667%;display:block}.el-col-xl-4.is-guttered{display:block}.el-col-xl-offset-4{margin-left:16.6667%}.el-col-xl-pull-4{position:relative;right:16.6667%}.el-col-xl-push-4{position:relative;left:16.6667%}.el-col-xl-5{flex:0 0 20.8333%;max-width:20.8333%;display:block}.el-col-xl-5.is-guttered{display:block}.el-col-xl-offset-5{margin-left:20.8333%}.el-col-xl-pull-5{position:relative;right:20.8333%}.el-col-xl-push-5{position:relative;left:20.8333%}.el-col-xl-6{flex:0 0 25%;max-width:25%;display:block}.el-col-xl-6.is-guttered{display:block}.el-col-xl-offset-6{margin-left:25%}.el-col-xl-pull-6{position:relative;right:25%}.el-col-xl-push-6{position:relative;left:25%}.el-col-xl-7{flex:0 0 29.1667%;max-width:29.1667%;display:block}.el-col-xl-7.is-guttered{display:block}.el-col-xl-offset-7{margin-left:29.1667%}.el-col-xl-pull-7{position:relative;right:29.1667%}.el-col-xl-push-7{position:relative;left:29.1667%}.el-col-xl-8{flex:0 0 33.3333%;max-width:33.3333%;display:block}.el-col-xl-8.is-guttered{display:block}.el-col-xl-offset-8{margin-left:33.3333%}.el-col-xl-pull-8{position:relative;right:33.3333%}.el-col-xl-push-8{position:relative;left:33.3333%}.el-col-xl-9{flex:0 0 37.5%;max-width:37.5%;display:block}.el-col-xl-9.is-guttered{display:block}.el-col-xl-offset-9{margin-left:37.5%}.el-col-xl-pull-9{position:relative;right:37.5%}.el-col-xl-push-9{position:relative;left:37.5%}.el-col-xl-10{flex:0 0 41.6667%;max-width:41.6667%;display:block}.el-col-xl-10.is-guttered{display:block}.el-col-xl-offset-10{margin-left:41.6667%}.el-col-xl-pull-10{position:relative;right:41.6667%}.el-col-xl-push-10{position:relative;left:41.6667%}.el-col-xl-11{flex:0 0 45.8333%;max-width:45.8333%;display:block}.el-col-xl-11.is-guttered{display:block}.el-col-xl-offset-11{margin-left:45.8333%}.el-col-xl-pull-11{position:relative;right:45.8333%}.el-col-xl-push-11{position:relative;left:45.8333%}.el-col-xl-12{flex:0 0 50%;max-width:50%;display:block}.el-col-xl-12.is-guttered{display:block}.el-col-xl-offset-12{margin-left:50%}.el-col-xl-pull-12{position:relative;right:50%}.el-col-xl-push-12{position:relative;left:50%}.el-col-xl-13{flex:0 0 54.1667%;max-width:54.1667%;display:block}.el-col-xl-13.is-guttered{display:block}.el-col-xl-offset-13{margin-left:54.1667%}.el-col-xl-pull-13{position:relative;right:54.1667%}.el-col-xl-push-13{position:relative;left:54.1667%}.el-col-xl-14{flex:0 0 58.3333%;max-width:58.3333%;display:block}.el-col-xl-14.is-guttered{display:block}.el-col-xl-offset-14{margin-left:58.3333%}.el-col-xl-pull-14{position:relative;right:58.3333%}.el-col-xl-push-14{position:relative;left:58.3333%}.el-col-xl-15{flex:0 0 62.5%;max-width:62.5%;display:block}.el-col-xl-15.is-guttered{display:block}.el-col-xl-offset-15{margin-left:62.5%}.el-col-xl-pull-15{position:relative;right:62.5%}.el-col-xl-push-15{position:relative;left:62.5%}.el-col-xl-16{flex:0 0 66.6667%;max-width:66.6667%;display:block}.el-col-xl-16.is-guttered{display:block}.el-col-xl-offset-16{margin-left:66.6667%}.el-col-xl-pull-16{position:relative;right:66.6667%}.el-col-xl-push-16{position:relative;left:66.6667%}.el-col-xl-17{flex:0 0 70.8333%;max-width:70.8333%;display:block}.el-col-xl-17.is-guttered{display:block}.el-col-xl-offset-17{margin-left:70.8333%}.el-col-xl-pull-17{position:relative;right:70.8333%}.el-col-xl-push-17{position:relative;left:70.8333%}.el-col-xl-18{flex:0 0 75%;max-width:75%;display:block}.el-col-xl-18.is-guttered{display:block}.el-col-xl-offset-18{margin-left:75%}.el-col-xl-pull-18{position:relative;right:75%}.el-col-xl-push-18{position:relative;left:75%}.el-col-xl-19{flex:0 0 79.1667%;max-width:79.1667%;display:block}.el-col-xl-19.is-guttered{display:block}.el-col-xl-offset-19{margin-left:79.1667%}.el-col-xl-pull-19{position:relative;right:79.1667%}.el-col-xl-push-19{position:relative;left:79.1667%}.el-col-xl-20{flex:0 0 83.3333%;max-width:83.3333%;display:block}.el-col-xl-20.is-guttered{display:block}.el-col-xl-offset-20{margin-left:83.3333%}.el-col-xl-pull-20{position:relative;right:83.3333%}.el-col-xl-push-20{position:relative;left:83.3333%}.el-col-xl-21{flex:0 0 87.5%;max-width:87.5%;display:block}.el-col-xl-21.is-guttered{display:block}.el-col-xl-offset-21{margin-left:87.5%}.el-col-xl-pull-21{position:relative;right:87.5%}.el-col-xl-push-21{position:relative;left:87.5%}.el-col-xl-22{flex:0 0 91.6667%;max-width:91.6667%;display:block}.el-col-xl-22.is-guttered{display:block}.el-col-xl-offset-22{margin-left:91.6667%}.el-col-xl-pull-22{position:relative;right:91.6667%}.el-col-xl-push-22{position:relative;left:91.6667%}.el-col-xl-23{flex:0 0 95.8333%;max-width:95.8333%;display:block}.el-col-xl-23.is-guttered{display:block}.el-col-xl-offset-23{margin-left:95.8333%}.el-col-xl-pull-23{position:relative;right:95.8333%}.el-col-xl-push-23{position:relative;left:95.8333%}.el-col-xl-24{flex:0 0 100%;max-width:100%;display:block}.el-col-xl-24.is-guttered{display:block}.el-col-xl-offset-24{margin-left:100%}.el-col-xl-pull-24{position:relative;right:100%}.el-col-xl-push-24{position:relative;left:100%}}.el-collapse-item.is-disabled .el-collapse-item__header{color:var(--el-text-color-disabled);cursor:not-allowed}.el-collapse-item__header{width:100%;min-height:var(--el-collapse-header-height);line-height:var(--el-collapse-header-height);background-color:var(--el-collapse-header-bg-color);color:var(--el-collapse-header-text-color);cursor:pointer;border:none;border-bottom:1px solid var(--el-collapse-border-color);font-size:var(--el-collapse-header-font-size);transition:border-bottom-color var(--el-transition-duration);box-sizing:border-box;outline:none;align-items:center;padding:0;font-weight:500;display:flex}.el-collapse-item__arrow{transition:transform var(--el-transition-duration);font-weight:300}.el-collapse-item__arrow.is-active{transform:rotate(90deg)}.el-collapse-item__title{text-align:left;flex:auto}.el-collapse-item__header.focusing:focus:not(:hover){color:var(--el-color-primary)}.el-collapse-item__header.is-active{border-bottom-color:#0000}.el-collapse-item__wrap{will-change:height;background-color:var(--el-collapse-content-bg-color);box-sizing:border-box;border-bottom:1px solid var(--el-collapse-border-color);overflow:hidden}.el-collapse-item__content{font-size:var(--el-collapse-content-font-size);color:var(--el-collapse-content-text-color);padding-bottom:25px;line-height:1.76923}.el-collapse-item:last-child{margin-bottom:-1px}.el-collapse{--el-collapse-border-color:var(--el-border-color-lighter);--el-collapse-header-height:48px;--el-collapse-header-bg-color:var(--el-fill-color-blank);--el-collapse-header-text-color:var(--el-text-color-primary);--el-collapse-header-font-size:13px;--el-collapse-content-bg-color:var(--el-fill-color-blank);--el-collapse-content-font-size:13px;--el-collapse-content-text-color:var(--el-text-color-primary);border-top:1px solid var(--el-collapse-border-color);border-bottom:1px solid var(--el-collapse-border-color)}.el-collapse-icon-position-left .el-collapse-item__header{gap:8px}.el-collapse-icon-position-left .el-collapse-item__title{order:1}.el-collapse-icon-position-right .el-collapse-item__header{padding-right:8px}.el-color-picker-panel{--el-colorpicker-bg-color:var(--el-bg-color-overlay);--el-fill-color-blank:var(--el-colorpicker-bg-color);box-sizing:content-box;background:var(--el-colorpicker-bg-color);width:300px;padding:12px}.el-color-picker-panel.is-border{border:solid 1px var(--el-border-color-lighter);border-radius:4px}.el-color-picker-panel__wrapper{margin-bottom:6px}.el-color-picker-panel__footer{text-align:right;justify-content:space-between;margin-top:12px;display:flex}.el-color-picker-panel__footer .el-input{color:#000;width:160px;font-size:12px;line-height:26px}.el-color-picker-panel.is-disabled .el-color-svpanel,.el-color-picker-panel.is-disabled .el-color-hue-slider{cursor:not-allowed;opacity:.3}.el-color-picker-panel.is-disabled .el-color-hue-slider__thumb{cursor:not-allowed}.el-color-picker-panel.is-disabled .el-color-alpha-slider,.el-color-picker-panel.is-disabled .el-color-predefine .el-color-predefine__color-selector{cursor:not-allowed;opacity:.3}.el-color-predefine{width:280px;margin-top:8px;font-size:12px;display:flex}.el-color-predefine__colors{flex-wrap:wrap;flex:1;gap:8px;display:flex}.el-color-predefine__color-selector{border-radius:var(--el-border-radius-base);cursor:pointer;border:none;outline:none;width:20px;height:20px;padding:0;overflow:hidden}.el-color-predefine__color-selector.selected{box-shadow:0 0 3px 2px var(--el-color-primary)}.el-color-predefine__color-selector:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-color-predefine__color-selector>div{height:100%;display:flex}.el-color-predefine__color-selector.is-alpha{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)}.el-color-hue-slider{box-sizing:border-box;float:right;background-color:red;width:280px;height:12px;padding:0 2px;position:relative}.el-color-hue-slider__bar{background:linear-gradient(90deg,red,#ff0 17%,#0f0 33%,#0ff,#00f 67%,#f0f 83%,red);height:100%;position:relative}.el-color-hue-slider__thumb{cursor:pointer;box-sizing:border-box;border:1px solid var(--el-border-color-lighter);z-index:1;background:#fff;border-radius:1px;width:4px;height:100%;position:absolute;top:0;left:0;box-shadow:0 0 2px #0009}.el-color-hue-slider__thumb:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:1px}.el-color-hue-slider.is-vertical{width:12px;height:180px;padding:2px 0}.el-color-hue-slider.is-vertical .el-color-hue-slider__bar{background:linear-gradient(red,#ff0 17%,#0f0 33%,#0ff,#00f 67%,#f0f 83%,red)}.el-color-hue-slider.is-vertical .el-color-hue-slider__thumb{width:100%;height:4px;top:0;left:0}.el-color-svpanel{background-image:linear-gradient(#0000,#000),linear-gradient(90deg,#fff,#fff0);width:280px;height:180px;position:relative}.el-color-svpanel__cursor{cursor:pointer;border-radius:50%;width:4px;height:4px;position:absolute;transform:translate(-2px,-2px);box-shadow:0 0 0 1.5px #fff,inset 0 0 1px 1px #0000004d,0 0 1px 2px #0006}.el-color-svpanel__cursor:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-color-alpha-slider{box-sizing:border-box;background-image:linear-gradient(45deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(45deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%);background-position:0 0,6px 0,6px -6px,0 6px;background-size:12px 12px;width:280px;height:12px;position:relative}.el-color-alpha-slider.is-disabled .el-color-alpha-slider__thumb{cursor:not-allowed}.el-color-alpha-slider__bar{background:linear-gradient(to right,#fff0 0%,var(--el-bg-color) 100%);height:100%;position:relative}.el-color-alpha-slider__thumb{cursor:pointer;box-sizing:border-box;border:1px solid var(--el-border-color-lighter);z-index:1;background:#fff;border-radius:1px;width:4px;height:100%;position:absolute;top:0;left:0;box-shadow:0 0 2px #0009}.el-color-alpha-slider__thumb:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:1px}.el-color-alpha-slider.is-vertical{width:20px;height:180px}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__bar{background:linear-gradient(#fff0,#fff)}.el-color-alpha-slider.is-vertical .el-color-alpha-slider__thumb{width:100%;height:4px;top:0;left:0}.el-color-picker-panel{--el-color-picker-alpha-bg-a:#ccc;--el-color-picker-alpha-bg-b:transparent}.dark .el-color-picker-panel{--el-color-picker-alpha-bg-a:#333}.el-color-picker{outline:none;width:32px;height:32px;line-height:normal;display:inline-block;position:relative}.el-color-picker:hover:not(:-webkit-any(.is-disabled,.is-focused)) .el-color-picker__trigger{border-color:var(--el-border-color-hover)}.el-color-picker:hover:not(:is(.is-disabled,.is-focused)) .el-color-picker__trigger{border-color:var(--el-border-color-hover)}.el-color-picker:focus-visible:not(.is-disabled) .el-color-picker__trigger{outline:2px solid var(--el-color-primary);outline-offset:1px}.el-color-picker.is-focused .el-color-picker__trigger{border-color:var(--el-color-primary)}.el-color-picker.is-disabled .el-color-picker__trigger{cursor:not-allowed;background-color:var(--el-fill-color-light)}.el-color-picker.is-disabled .el-color-picker__color{opacity:.3}.el-color-picker--large{width:40px;height:40px}.el-color-picker--small{width:24px;height:24px}.el-color-picker--small .el-color-picker__icon,.el-color-picker--small .el-color-picker__empty{transform:scale(.8)}.el-color-picker__trigger{box-sizing:border-box;border:1px solid var(--el-border-color);cursor:pointer;border-radius:4px;justify-content:center;align-items:center;width:100%;height:100%;padding:4px;font-size:0;display:inline-flex;position:relative}.el-color-picker__color{box-sizing:border-box;border:1px solid var(--el-text-color-secondary);border-radius:var(--el-border-radius-small);text-align:center;width:100%;height:100%;display:block;position:relative}.el-color-picker__color.is-alpha{background-image:linear-gradient(45deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-a) 25%,var(--el-color-picker-alpha-bg-b) 25%),linear-gradient(45deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%),linear-gradient(135deg,var(--el-color-picker-alpha-bg-b) 75%,var(--el-color-picker-alpha-bg-a) 75%);background-position:0 0,6px 0,6px -6px,0 6px;background-size:12px 12px}.el-color-picker__color-inner{justify-content:center;align-items:center;width:100%;height:100%;display:inline-flex}.el-color-picker .el-color-picker__empty{color:var(--el-text-color-secondary);font-size:12px}.el-color-picker .el-color-picker__icon{color:#fff;justify-content:center;align-items:center;font-size:12px;display:inline-flex}.el-color-picker__panel{border-radius:var(--el-border-radius-base);box-shadow:var(--el-box-shadow-light);background-color:#fff}.el-color-picker__panel.el-popper{border:1px solid var(--el-border-color-lighter)}.el-color-picker,.el-color-picker__panel{--el-color-picker-alpha-bg-a:#ccc;--el-color-picker-alpha-bg-b:transparent}.dark .el-color-picker,.dark .el-color-picker__panel{--el-color-picker-alpha-bg-a:#333}.el-container{box-sizing:border-box;flex-direction:row;flex:auto;min-width:0;display:flex}.el-container.is-vertical{flex-direction:column}.el-date-table{-webkit-user-select:none;user-select:none;font-size:12px}.el-date-table.is-week-mode .el-date-table__row:hover .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-date-table.is-week-mode .el-date-table__row:hover td.available:hover{color:var(--el-datepicker-text-color)}.el-date-table.is-week-mode .el-date-table__row:hover td:first-child .el-date-table-cell{border-top-left-radius:15px;border-bottom-left-radius:15px;margin-left:5px}.el-date-table.is-week-mode .el-date-table__row:hover td:last-child .el-date-table-cell{border-top-right-radius:15px;border-bottom-right-radius:15px;margin-right:5px}.el-date-table.is-week-mode .el-date-table__row.current .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-date-table td{box-sizing:border-box;text-align:center;cursor:pointer;width:32px;height:30px;padding:4px 0;position:relative}.el-date-table td .el-date-table-cell{box-sizing:border-box;height:30px;padding:3px 0}.el-date-table td .el-date-table-cell .el-date-table-cell__text{border-radius:50%;width:24px;height:24px;margin:0 auto;line-height:24px;display:block;position:absolute;left:50%;transform:translate(-50%)}.el-date-table td.next-month,.el-date-table td.prev-month{color:var(--el-datepicker-off-text-color)}.el-date-table td.today{position:relative}.el-date-table td.today .el-date-table-cell__text{color:var(--el-color-primary);font-weight:700}.el-date-table td.today.start-date .el-date-table-cell__text,.el-date-table td.today.end-date .el-date-table-cell__text{color:#fff}.el-date-table td.available:hover{color:var(--el-datepicker-hover-text-color)}.el-date-table td.in-range .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-date-table td.in-range .el-date-table-cell:hover{background-color:var(--el-datepicker-inrange-hover-bg-color)}.el-date-table td.current:not(.disabled) .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-date-table td.current:not(.disabled):focus-visible .el-date-table-cell__text{outline:2px solid var(--el-datepicker-active-color);outline-offset:1px}.el-date-table td.start-date .el-date-table-cell,.el-date-table td.end-date .el-date-table-cell{color:#fff}.el-date-table td.start-date .el-date-table-cell__text,.el-date-table td.end-date .el-date-table-cell__text{background-color:var(--el-datepicker-active-color)}.el-date-table td.start-date .el-date-table-cell{border-top-left-radius:15px;border-bottom-left-radius:15px;margin-left:5px}.el-date-table td.end-date .el-date-table-cell{border-top-right-radius:15px;border-bottom-right-radius:15px;margin-right:5px}.el-date-table td.disabled .el-date-table-cell{background-color:var(--el-fill-color-light);opacity:1;cursor:not-allowed;color:var(--el-text-color-placeholder)}.el-date-table td.selected .el-date-table-cell{border-radius:15px;margin-left:5px;margin-right:5px}.el-date-table td.selected .el-date-table-cell__text{background-color:var(--el-datepicker-active-color);color:#fff;border-radius:15px}.el-date-table td.week{color:var(--el-datepicker-off-text-color);cursor:default;font-size:80%}.el-date-table td:focus{outline:none}.el-date-table th{color:var(--el-datepicker-header-text-color);border-bottom:solid 1px var(--el-border-color-lighter);padding:5px;font-weight:400}.el-date-table th.el-date-table__week-header{width:24px;padding:0}.el-month-table{border-collapse:collapse;margin:-1px;font-size:12px}.el-month-table td{text-align:center;cursor:pointer;width:68px;padding:8px 0;position:relative}.el-month-table td .el-date-table-cell{box-sizing:border-box;height:48px;padding:6px 0}.el-month-table td.today .el-date-table-cell__text{color:var(--el-color-primary);font-weight:700}.el-month-table td.today.start-date .el-date-table-cell__text,.el-month-table td.today.end-date .el-date-table-cell__text{color:#fff}.el-month-table td.disabled .el-date-table-cell__text{background-color:var(--el-fill-color-light);cursor:not-allowed;color:var(--el-text-color-placeholder)}.el-month-table td.disabled .el-date-table-cell__text:hover{color:var(--el-text-color-placeholder)}.el-month-table td .el-date-table-cell__text{width:54px;height:36px;color:var(--el-datepicker-text-color);border-radius:18px;margin:0 auto;line-height:36px;display:block;position:absolute;left:50%;transform:translate(-50%)}.el-month-table td .el-date-table-cell__text:hover{color:var(--el-datepicker-hover-text-color)}.el-month-table td.in-range .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-month-table td.in-range .el-date-table-cell:hover{background-color:var(--el-datepicker-inrange-hover-bg-color)}.el-month-table td.start-date .el-date-table-cell,.el-month-table td.end-date .el-date-table-cell{color:#fff}.el-month-table td.start-date .el-date-table-cell__text,.el-month-table td.end-date .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-month-table td.start-date .el-date-table-cell{border-top-left-radius:24px;border-bottom-left-radius:24px;margin-left:3px}.el-month-table td.end-date .el-date-table-cell{border-top-right-radius:24px;border-bottom-right-radius:24px;margin-right:3px}.el-month-table td.current:not(.disabled) .el-date-table-cell{border-radius:24px;margin-left:3px;margin-right:3px}.el-month-table td.current:not(.disabled) .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-month-table td:focus-visible{outline:none}.el-month-table td:focus-visible .el-date-table-cell__text{outline:2px solid var(--el-datepicker-active-color);outline-offset:1px}.el-year-table{border-collapse:collapse;margin:-1px;font-size:12px}.el-year-table .el-icon{color:var(--el-datepicker-icon-color)}.el-year-table td{text-align:center;cursor:pointer;width:68px;padding:8px 0;position:relative}.el-year-table td .el-date-table-cell{box-sizing:border-box;height:48px;padding:6px 0}.el-year-table td.today .el-date-table-cell__text{color:var(--el-color-primary);font-weight:700}.el-year-table td.today.start-date .el-date-table-cell__text,.el-year-table td.today.end-date .el-date-table-cell__text{color:#fff}.el-year-table td.disabled .el-date-table-cell__text{background-color:var(--el-fill-color-light);cursor:not-allowed;color:var(--el-text-color-placeholder)}.el-year-table td.disabled .el-date-table-cell__text:hover{color:var(--el-text-color-placeholder)}.el-year-table td .el-date-table-cell__text{width:60px;height:36px;color:var(--el-datepicker-text-color);border-radius:18px;margin:0 auto;line-height:36px;display:block;position:absolute;left:50%;transform:translate(-50%)}.el-year-table td .el-date-table-cell__text:hover{color:var(--el-datepicker-hover-text-color)}.el-year-table td.in-range .el-date-table-cell{background-color:var(--el-datepicker-inrange-bg-color)}.el-year-table td.in-range .el-date-table-cell:hover{background-color:var(--el-datepicker-inrange-hover-bg-color)}.el-year-table td.start-date .el-date-table-cell,.el-year-table td.end-date .el-date-table-cell{color:#fff}.el-year-table td.start-date .el-date-table-cell__text,.el-year-table td.end-date .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-year-table td.start-date .el-date-table-cell{border-top-left-radius:24px;border-bottom-left-radius:24px}.el-year-table td.end-date .el-date-table-cell{border-top-right-radius:24px;border-bottom-right-radius:24px}.el-year-table td.current:not(.disabled) .el-date-table-cell__text{color:#fff;background-color:var(--el-datepicker-active-color)}.el-year-table td:focus-visible{outline:none}.el-year-table td:focus-visible .el-date-table-cell__text{outline:2px solid var(--el-datepicker-active-color);outline-offset:1px}.el-time-spinner.has-seconds .el-time-spinner__wrapper{width:33.3%}.el-time-spinner__wrapper{vertical-align:top;width:50%;max-height:192px;display:inline-block;position:relative;overflow:auto}.el-time-spinner__wrapper.el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default){padding-bottom:15px}.el-time-spinner__wrapper.is-arrow{box-sizing:border-box;text-align:center;overflow:hidden}.el-time-spinner__wrapper.is-arrow .el-time-spinner__list{transform:translateY(-32px)}.el-time-spinner__wrapper.is-arrow .el-time-spinner__item:hover:not(.is-disabled):not(.is-active){background:var(--el-fill-color-light);cursor:default}.el-time-spinner__arrow{color:var(--el-text-color-secondary);width:100%;z-index:var(--el-index-normal);text-align:center;cursor:pointer;height:30px;font-size:12px;line-height:30px;position:absolute;left:0}.el-time-spinner__arrow:hover{color:var(--el-color-primary)}.el-time-spinner__arrow.arrow-up{top:10px}.el-time-spinner__arrow.arrow-down{bottom:10px}.el-time-spinner__input.el-input{width:70%}.el-time-spinner__input.el-input .el-input__inner{text-align:center;padding:0}.el-time-spinner__list{text-align:center;margin:0;padding:0;list-style:none}.el-time-spinner__list:after,.el-time-spinner__list:before{content:"";width:100%;height:80px;display:block}.el-time-spinner__item{height:32px;color:var(--el-text-color-regular);font-size:12px;line-height:32px}.el-time-spinner__item:hover:not(.is-disabled):not(.is-active){background:var(--el-fill-color-light);cursor:pointer}.el-time-spinner__item.is-active:not(.is-disabled){color:var(--el-text-color-primary);font-weight:700}.el-time-spinner__item.is-disabled{color:var(--el-text-color-placeholder);cursor:not-allowed}.el-picker__popper{--el-datepicker-border-color:var(--el-disabled-border-color)}.el-picker__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-datepicker-border-color);box-shadow:var(--el-box-shadow-light)}.el-picker__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-datepicker-border-color)}.el-picker__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-picker__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-picker__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-picker__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-date-editor{--el-date-editor-width:220px;--el-date-editor-monthrange-width:300px;--el-date-editor-daterange-width:350px;--el-date-editor-datetimerange-width:400px;--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;text-align:left;vertical-align:middle;position:relative}.el-date-editor.el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset}.el-date-editor.el-input__wrapper:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-date-editor.is-focus .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-date-editor.el-input,.el-date-editor.el-input__wrapper{width:var(--el-date-editor-width);height:var(--el-input-height,var(--el-component-size))}.el-date-editor--monthrange{--el-date-editor-width:var(--el-date-editor-monthrange-width)}.el-date-editor--daterange,.el-date-editor--timerange{--el-date-editor-width:var(--el-date-editor-daterange-width)}.el-date-editor--datetimerange{--el-date-editor-width:var(--el-date-editor-datetimerange-width)}.el-date-editor--dates .el-input__wrapper{text-overflow:ellipsis;white-space:nowrap}.el-date-editor .close-icon,.el-date-editor .clear-icon{cursor:pointer}.el-date-editor .clear-icon:hover{color:var(--el-input-clear-hover-color)}.el-date-editor .el-range__icon{height:inherit;color:var(--el-text-color-placeholder);float:left;font-size:14px}.el-date-editor .el-range__icon svg{vertical-align:middle}.el-date-editor .el-range-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;text-align:center;width:39%;height:30px;line-height:30px;font-size:var(--el-font-size-base);color:var(--el-text-color-regular);background-color:#0000;border:none;outline:none;margin:0;padding:0;display:inline-block}.el-date-editor .el-range-input::placeholder{color:var(--el-text-color-placeholder)}.el-date-editor .el-range-separator{overflow-wrap:break-word;height:100%;color:var(--el-text-color-primary);flex:1;justify-content:center;align-items:center;margin:0;padding:0 5px;font-size:14px;display:inline-flex}.el-date-editor .el-range__close-icon{color:var(--el-text-color-placeholder);height:inherit;width:unset;cursor:pointer;font-size:14px}.el-date-editor .el-range__close-icon:hover{color:var(--el-input-clear-hover-color)}.el-date-editor .el-range__close-icon svg{vertical-align:middle}.el-date-editor .el-range__close-icon--hidden{opacity:0;visibility:hidden}.el-range-editor.el-input__wrapper{vertical-align:middle;align-items:center;padding:0 10px;display:inline-flex}.el-range-editor.is-active,.el-range-editor.is-active:hover{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-range-editor--large{line-height:var(--el-component-size-large)}.el-range-editor--large.el-input__wrapper{height:var(--el-component-size-large)}.el-range-editor--large .el-range-separator{font-size:14px;line-height:40px}.el-range-editor--large .el-range-input{height:38px;font-size:14px;line-height:38px}.el-range-editor--small{line-height:var(--el-component-size-small)}.el-range-editor--small.el-input__wrapper{height:var(--el-component-size-small)}.el-range-editor--small .el-range-separator{font-size:12px;line-height:24px}.el-range-editor--small .el-range-input{height:22px;font-size:12px;line-height:22px}.el-range-editor.is-disabled{background-color:var(--el-disabled-bg-color);border-color:var(--el-disabled-border-color);color:var(--el-disabled-text-color);cursor:not-allowed}.el-range-editor.is-disabled:hover,.el-range-editor.is-disabled:focus{border-color:var(--el-disabled-border-color)}.el-range-editor.is-disabled input{background-color:var(--el-disabled-bg-color);color:var(--el-disabled-text-color);cursor:not-allowed}.el-range-editor.is-disabled input::placeholder{color:var(--el-text-color-placeholder)}.el-range-editor.is-disabled .el-range-separator{color:var(--el-disabled-text-color)}.el-picker-panel{color:var(--el-text-color-regular);background:var(--el-datepicker-bg-color);border-radius:var(--el-popper-border-radius,var(--el-border-radius-base));line-height:30px}.el-picker-panel .el-time-panel{border:solid 1px var(--el-datepicker-border-color);background-color:var(--el-datepicker-bg-color);box-shadow:var(--el-box-shadow-light);margin:5px 0}.el-picker-panel__body:after,.el-picker-panel__body-wrapper:after{content:"";clear:both;display:table}.el-picker-panel__content{margin:15px;position:relative}.el-picker-panel__footer{border-top:1px solid var(--el-datepicker-inner-border-color);text-align:right;background-color:var(--el-datepicker-bg-color);padding:4px 12px;font-size:0;position:relative}.el-picker-panel__shortcut{width:100%;color:var(--el-datepicker-text-color);text-align:left;cursor:pointer;background-color:#0000;border:0;outline:none;padding-left:12px;font-size:14px;line-height:28px;display:block}.el-picker-panel__shortcut:hover{color:var(--el-datepicker-hover-text-color)}.el-picker-panel__shortcut.active{color:var(--el-datepicker-active-color);background-color:#e6f1fe}.el-picker-panel__btn{border:1px solid var(--el-fill-color-darker);color:var(--el-text-color-primary);cursor:pointer;background-color:#0000;border-radius:2px;outline:none;padding:0 20px;font-size:12px;line-height:24px}.el-picker-panel__btn[disabled]{color:var(--el-text-color-disabled);cursor:not-allowed}.el-picker-panel__icon-btn{color:var(--el-datepicker-icon-color);cursor:pointer;background:0 0;border:0;outline:none;margin-top:8px;padding:1px 6px;font-size:12px;line-height:1}.el-picker-panel__icon-btn:hover{color:var(--el-datepicker-hover-text-color)}.el-picker-panel__icon-btn:focus-visible{color:var(--el-datepicker-hover-text-color)}.el-picker-panel__icon-btn.is-disabled{color:var(--el-text-color-disabled)}.el-picker-panel__icon-btn.is-disabled:hover{cursor:not-allowed}.el-picker-panel__icon-btn.is-disabled .el-icon{cursor:inherit}.el-picker-panel__icon-btn .el-icon{cursor:pointer;font-size:inherit}.el-picker-panel__link-btn{vertical-align:middle}.el-picker-panel.is-disabled .el-picker-panel__prev-btn{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__prev-btn:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__prev-btn .el-icon{cursor:inherit}.el-picker-panel.is-disabled .el-picker-panel__next-btn{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__next-btn:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__next-btn .el-icon{cursor:inherit}.el-picker-panel.is-disabled .el-picker-panel__icon-btn{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__icon-btn:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__icon-btn .el-icon{cursor:inherit}.el-picker-panel.is-disabled .el-picker-panel__shortcut{color:var(--el-text-color-disabled)}.el-picker-panel.is-disabled .el-picker-panel__shortcut:hover{cursor:not-allowed}.el-picker-panel.is-disabled .el-picker-panel__shortcut .el-icon{cursor:inherit}.el-picker-panel [slot=sidebar],.el-picker-panel__sidebar{border-right:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:110px;padding-top:6px;position:absolute;top:0;bottom:0;overflow:auto}.el-picker-panel [slot=sidebar]+.el-picker-panel__body,.el-picker-panel__sidebar+.el-picker-panel__body{margin-left:110px}.el-date-picker{--el-datepicker-text-color:var(--el-text-color-regular);--el-datepicker-off-text-color:var(--el-text-color-placeholder);--el-datepicker-header-text-color:var(--el-text-color-regular);--el-datepicker-icon-color:var(--el-text-color-primary);--el-datepicker-border-color:var(--el-disabled-border-color);--el-datepicker-inner-border-color:var(--el-border-color-light);--el-datepicker-inrange-bg-color:var(--el-border-color-extra-light);--el-datepicker-inrange-hover-bg-color:var(--el-border-color-extra-light);--el-datepicker-active-color:var(--el-color-primary);--el-datepicker-hover-text-color:var(--el-color-primary);--el-datepicker-bg-color:var(--el-bg-color-overlay);--el-fill-color-blank:var(--el-datepicker-bg-color);width:322px}.el-date-picker.has-sidebar.has-time{width:434px}.el-date-picker.has-sidebar{width:438px}.el-date-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-picker .el-picker-panel__content{width:292px}.el-date-picker table{table-layout:fixed;width:100%}.el-date-picker__editor-wrap{padding:0 5px;display:table-cell;position:relative}.el-date-picker__time-header{border-bottom:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:100%;padding:8px 5px 5px;font-size:12px;display:table;position:relative}.el-date-picker__header{text-align:center;padding:12px 12px 0}.el-date-picker__header--bordered{border-bottom:solid 1px var(--el-border-color-lighter);margin-bottom:0;padding-bottom:12px}.el-date-picker__header--bordered+.el-picker-panel__content{margin-top:0}.el-date-picker__header-label{text-align:center;cursor:pointer;color:var(--el-text-color-regular);padding:0 5px;font-size:16px;font-weight:500;line-height:22px}.el-date-picker__header-label:hover{color:var(--el-datepicker-hover-text-color)}.el-date-picker__header-label:focus-visible{color:var(--el-datepicker-hover-text-color);outline:none}.el-date-picker__header-label.active{color:var(--el-datepicker-active-color)}.el-date-picker__prev-btn{float:left}.el-date-picker__next-btn{float:right}.el-date-picker__time-wrap{text-align:center;padding:10px}.el-date-picker__time-label{float:left;cursor:pointer;margin-left:10px;line-height:30px}.el-date-picker .el-time-panel{position:absolute}.el-date-picker.is-disabled .el-date-picker__header-label{color:var(--el-text-color-disabled)}.el-date-picker.is-disabled .el-date-picker__header-label:hover{cursor:not-allowed}.el-date-picker.is-disabled .el-date-picker__header-label .el-icon{cursor:inherit}.el-date-range-picker{--el-datepicker-text-color:var(--el-text-color-regular);--el-datepicker-off-text-color:var(--el-text-color-placeholder);--el-datepicker-header-text-color:var(--el-text-color-regular);--el-datepicker-icon-color:var(--el-text-color-primary);--el-datepicker-border-color:var(--el-disabled-border-color);--el-datepicker-inner-border-color:var(--el-border-color-light);--el-datepicker-inrange-bg-color:var(--el-border-color-extra-light);--el-datepicker-inrange-hover-bg-color:var(--el-border-color-extra-light);--el-datepicker-active-color:var(--el-color-primary);--el-datepicker-hover-text-color:var(--el-color-primary);--el-datepicker-bg-color:var(--el-bg-color-overlay);width:646px}.el-date-range-picker.has-sidebar{width:756px}.el-date-range-picker.has-time .el-picker-panel__body-wrapper{position:relative}.el-date-range-picker table{table-layout:fixed;width:100%}.el-date-range-picker .el-picker-panel__body{min-width:513px}.el-date-range-picker .el-picker-panel__content{margin:0}.el-date-range-picker.single-panel{width:322px}.el-date-range-picker.single-panel .el-picker-panel__body{min-width:322px}.el-date-range-picker.single-panel.has-sidebar.has-time{width:434px}.el-date-range-picker.single-panel.has-sidebar{width:438px}.el-date-range-picker.single-panel .el-picker-panel__content{width:292px;margin:15px;padding:0;display:block;position:relative}.el-date-range-picker__header{text-align:center;height:28px;position:relative}.el-date-range-picker__header [class*=arrow-left]{float:left}.el-date-range-picker__header [class*=arrow-right]{float:right}.el-date-range-picker__header div{margin-right:50px;font-size:16px;font-weight:500}.el-date-range-picker__header-label{text-align:center;cursor:pointer;color:var(--el-text-color-regular);padding:0 5px;font-size:16px;font-weight:500;line-height:22px}.el-date-range-picker__header-label:hover{color:var(--el-datepicker-hover-text-color)}.el-date-range-picker__header-label:focus-visible{color:var(--el-datepicker-hover-text-color);outline:none}.el-date-range-picker__header-label.active{color:var(--el-datepicker-active-color)}.el-date-range-picker__content{box-sizing:border-box;width:50%;margin:0;padding:16px;display:table-cell}.el-date-range-picker__content.is-left{border-right:1px solid var(--el-datepicker-inner-border-color)}.el-date-range-picker__content .el-date-range-picker__header div{margin-left:50px;margin-right:50px}.el-date-range-picker__editors-wrap{box-sizing:border-box;display:table-cell}.el-date-range-picker__editors-wrap.is-right{text-align:right}.el-date-range-picker__time-header{border-bottom:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:100%;padding:8px 5px 5px;font-size:12px;display:table;position:relative}.el-date-range-picker__time-header>.el-icon-arrow-right{vertical-align:middle;color:var(--el-datepicker-icon-color);font-size:20px;display:table-cell}.el-date-range-picker__time-picker-wrap{padding:0 5px;display:table-cell;position:relative}.el-date-range-picker__time-picker-wrap .el-picker-panel{z-index:1;background:#fff;position:absolute;top:13px;right:0}.el-date-range-picker__time-picker-wrap .el-time-panel{position:absolute}.el-date-range-picker.is-disabled .el-date-range-picker__header-label{color:var(--el-text-color-disabled)}.el-date-range-picker.is-disabled .el-date-range-picker__header-label:hover{cursor:not-allowed}.el-date-range-picker.is-disabled .el-date-range-picker__header-label .el-icon{cursor:inherit}.el-time-range-picker{width:354px;overflow:visible}.el-time-range-picker__content{text-align:center;z-index:1;padding:10px;position:relative}.el-time-range-picker__cell{box-sizing:border-box;width:50%;margin:0;padding:4px 7px 7px;display:inline-block}.el-time-range-picker__header{text-align:center;margin-bottom:5px;font-size:14px}.el-time-range-picker__body{border:1px solid var(--el-datepicker-border-color);border-radius:2px}.el-time-panel{width:180px;z-index:var(--el-index-top);-webkit-user-select:none;user-select:none;box-sizing:content-box;border-radius:2px;position:relative;left:0}.el-time-panel__content{font-size:0;position:relative;overflow:hidden}.el-time-panel__content:after,.el-time-panel__content:before{content:"";z-index:-1;box-sizing:border-box;text-align:left;height:32px;margin-top:-16px;padding-top:6px;position:absolute;top:50%;left:0;right:0}.el-time-panel__content:after{margin-left:12%;margin-right:12%;left:50%}.el-time-panel__content:before{border-top:1px solid var(--el-border-color-light);border-bottom:1px solid var(--el-border-color-light);margin-left:12%;margin-right:12%;padding-left:50%}.el-time-panel__content.has-seconds:after{left:66.6667%}.el-time-panel__content.has-seconds:before{padding-left:33.3333%}.el-time-panel__footer{border-top:1px solid var(--el-timepicker-inner-border-color,var(--el-border-color-light));text-align:right;box-sizing:border-box;height:36px;padding:4px;line-height:25px}.el-time-panel__btn{cursor:pointer;color:var(--el-text-color-primary);background-color:#0000;border:none;outline:none;margin:0 5px;padding:0 5px;font-size:12px;line-height:28px}.el-time-panel__btn.confirm{color:var(--el-timepicker-active-color,var(--el-color-primary));font-weight:800}.el-picker-panel.is-border{border:solid 1px var(--el-border-color-lighter)}.el-picker-panel.is-border .el-picker-panel__body-wrapper{position:relative}.el-picker-panel.is-border.el-picker-panel [slot=sidebar],.el-picker-panel.is-border.el-picker-panel__sidebar{border-right:1px solid var(--el-datepicker-inner-border-color);box-sizing:border-box;width:110px;height:100%;padding-top:6px;position:absolute;top:0;overflow:auto}.el-descriptions{--el-descriptions-table-border:1px solid var(--el-border-color-lighter);--el-descriptions-item-bordered-label-background:var(--el-fill-color-light);box-sizing:border-box;font-size:var(--el-font-size-base);color:var(--el-text-color-primary)}.el-descriptions__header{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.el-descriptions__title{color:var(--el-text-color-primary);font-size:16px;font-weight:700}.el-descriptions__body{background-color:var(--el-fill-color-blank)}.el-descriptions__body .el-descriptions__table{border-collapse:collapse;width:100%}.el-descriptions__body .el-descriptions__table .el-descriptions__cell{box-sizing:border-box;text-align:left;font-size:14px;line-height:23px}.el-descriptions__body .el-descriptions__table .el-descriptions__cell.is-left{text-align:left}.el-descriptions__body .el-descriptions__table .el-descriptions__cell.is-center{text-align:center}.el-descriptions__body .el-descriptions__table .el-descriptions__cell.is-right{text-align:right}.el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell{border:var(--el-descriptions-table-border);padding:8px 11px}.el-descriptions__body .el-descriptions__table:not(.is-bordered) .el-descriptions__cell{padding-bottom:12px}.el-descriptions--large{font-size:14px}.el-descriptions--large .el-descriptions__header{margin-bottom:20px}.el-descriptions--large .el-descriptions__header .el-descriptions__title{font-size:16px}.el-descriptions--large .el-descriptions__body .el-descriptions__table .el-descriptions__cell{font-size:14px}.el-descriptions--large .el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell{padding:12px 15px}.el-descriptions--large .el-descriptions__body .el-descriptions__table:not(.is-bordered) .el-descriptions__cell{padding-bottom:16px}.el-descriptions--small{font-size:12px}.el-descriptions--small .el-descriptions__header{margin-bottom:12px}.el-descriptions--small .el-descriptions__header .el-descriptions__title{font-size:14px}.el-descriptions--small .el-descriptions__body .el-descriptions__table .el-descriptions__cell{font-size:12px}.el-descriptions--small .el-descriptions__body .el-descriptions__table.is-bordered .el-descriptions__cell{padding:4px 7px}.el-descriptions--small .el-descriptions__body .el-descriptions__table:not(.is-bordered) .el-descriptions__cell{padding-bottom:8px}.el-descriptions__label.el-descriptions__cell.is-bordered-label{color:var(--el-text-color-regular);background:var(--el-descriptions-item-bordered-label-background);font-weight:700}.el-descriptions__label:not(.is-bordered-label){color:var(--el-text-color-primary);margin-right:16px}.el-descriptions__label.el-descriptions__cell:not(.is-bordered-label).is-vertical-label{padding-bottom:6px}.el-descriptions__content.el-descriptions__cell.is-bordered-content{color:var(--el-text-color-primary)}.el-descriptions__content:not(.is-bordered-label){color:var(--el-text-color-regular)}.el-descriptions--large .el-descriptions__label:not(.is-bordered-label){margin-right:16px}.el-descriptions--large .el-descriptions__label.el-descriptions__cell:not(.is-bordered-label).is-vertical-label{padding-bottom:8px}.el-descriptions--small .el-descriptions__label:not(.is-bordered-label){margin-right:12px}.el-descriptions--small .el-descriptions__label.el-descriptions__cell:not(.is-bordered-label).is-vertical-label{padding-bottom:4px}:root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{width:100%;height:100%;opacity:var(--el-popup-modal-opacity);background:var(--el-popup-modal-bg-color);position:fixed;top:0;left:0}.el-popup-parent--hidden{overflow:hidden}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:16px;--el-dialog-border-radius:var(--el-border-radius-base);margin:var(--el-dialog-margin-top,15vh) auto 50px;background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;padding:var(--el-dialog-padding-primary);width:var(--el-dialog-width,50%);overflow-wrap:break-word;position:relative}.el-dialog:focus{outline:none!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;border-radius:0;height:100%;margin-bottom:0;overflow:auto}.el-dialog__wrapper{margin:0;position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;user-select:none}.el-dialog__header{padding-bottom:var(--el-dialog-padding-primary)}.el-dialog__header.show-close{padding-right:calc(var(--el-dialog-padding-primary) + var(--el-message-close-size,16px))}.el-dialog__headerbtn{cursor:pointer;width:48px;height:48px;font-size:var(--el-message-close-size,16px);background:0 0;border:none;outline:none;padding:0;position:absolute;top:0;right:0}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{line-height:var(--el-dialog-font-line-height);font-size:var(--el-dialog-title-font-size);color:var(--el-text-color-primary)}.el-dialog__body{color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{padding-top:var(--el-dialog-padding-primary);text-align:right;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-modal-dialog.is-penetrable{pointer-events:none}.el-modal-dialog.is-penetrable .el-dialog{pointer-events:auto}.el-overlay-dialog{position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.el-overlay-dialog.is-closing .el-dialog{pointer-events:none}.dialog-fade-enter-active{animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{animation:dialog-fade-out var(--el-transition-duration)}@keyframes dialog-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translate(0)}}@keyframes dialog-fade-out{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translateY(-20px)}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}.el-divider{position:relative}.el-divider--horizontal{border-top:1px var(--el-border-color) var(--el-border-style);width:100%;height:1px;margin:24px 0;display:block}.el-divider--vertical{vertical-align:middle;border-left:1px var(--el-border-color) var(--el-border-style);width:1px;height:1em;margin:0 8px;display:inline-block;position:relative}.el-divider__text{background-color:var(--el-bg-color);color:var(--el-text-color-primary);padding:0 20px;font-size:14px;font-weight:500;position:absolute}.el-divider__text.is-left{left:20px;transform:translateY(-50%)}.el-divider__text.is-center{left:50%;transform:translate(-50%)translateY(-50%)}.el-divider__text.is-right{right:20px;transform:translateY(-50%)}.el-overlay.is-drawer{overflow:hidden}.el-drawer{--el-drawer-bg-color:var(--el-dialog-bg-color,var(--el-bg-color));--el-drawer-padding-primary:var(--el-dialog-padding-primary,20px);--el-drawer-dragger-size:8px;box-sizing:border-box;background-color:var(--el-drawer-bg-color);box-shadow:var(--el-box-shadow-dark);transition:all var(--el-transition-duration);flex-direction:column;display:flex;position:absolute}.el-drawer .rtl,.el-drawer .ltr,.el-drawer .ttb,.el-drawer .btt{transform:translate(0)}.el-drawer__sr-focus:focus{outline:none!important}.el-drawer__header{color:var(--el-text-color-primary);padding:var(--el-drawer-padding-primary);align-items:center;margin-bottom:32px;padding-bottom:0;display:flex;overflow:hidden}.el-drawer__header>:first-child{flex:1}.el-drawer__title{line-height:inherit;flex:1;margin:0;font-size:16px}.el-drawer__footer{padding:var(--el-drawer-padding-primary);text-align:right;padding-top:10px;overflow:hidden}.el-drawer__close-btn{cursor:pointer;font-size:var(--el-font-size-extra-large);color:inherit;background-color:#0000;border:none;outline:none;display:inline-flex}.el-drawer__close-btn:focus i,.el-drawer__close-btn:hover i{color:var(--el-color-primary)}.el-drawer__body{padding:var(--el-drawer-padding-primary);flex:1;overflow:auto}.el-drawer__body>*{box-sizing:border-box}.el-drawer.is-dragging{transition:none}.el-drawer__dragger{-webkit-user-select:none;user-select:none;background-color:#0000;transition:all .2s;position:absolute}.el-drawer__dragger:before{content:"";background-color:#0000;transition:all .2s;position:absolute}.el-drawer__dragger:hover:before{background-color:var(--el-color-primary)}.el-drawer.ltr,.el-drawer.rtl{height:100%;top:0;bottom:0}.el-drawer.ltr>.el-drawer__dragger,.el-drawer.rtl>.el-drawer__dragger{height:100%;width:var(--el-drawer-dragger-size);cursor:ew-resize;top:0;bottom:0}.el-drawer.ltr>.el-drawer__dragger:before,.el-drawer.rtl>.el-drawer__dragger:before{width:3px;top:0;bottom:0}.el-drawer.ttb,.el-drawer.btt{width:100%;left:0;right:0}.el-drawer.ttb>.el-drawer__dragger,.el-drawer.btt>.el-drawer__dragger{width:100%;height:var(--el-drawer-dragger-size);cursor:ns-resize;left:0;right:0}.el-drawer.ttb>.el-drawer__dragger:before,.el-drawer.btt>.el-drawer__dragger:before{height:3px;left:0;right:0}.el-drawer.ltr{left:0}.el-drawer.ltr>.el-drawer__dragger{right:0}.el-drawer.ltr>.el-drawer__dragger:before{right:-2px}.el-drawer.rtl{right:0}.el-drawer.rtl>.el-drawer__dragger{left:0}.el-drawer.rtl>.el-drawer__dragger:before{left:-2px}.el-drawer.ttb{top:0}.el-drawer.ttb>.el-drawer__dragger{bottom:0}.el-drawer.ttb>.el-drawer__dragger:before{bottom:-2px}.el-drawer.btt{bottom:0}.el-drawer.btt>.el-drawer__dragger{top:0}.el-drawer.btt>.el-drawer__dragger:before{top:-2px}.el-modal-drawer.is-penetrable{pointer-events:none}.el-modal-drawer.is-penetrable .el-drawer{pointer-events:auto}.el-drawer-fade-enter-active,.el-drawer-fade-leave-active{transition:all var(--el-transition-duration)}.el-drawer-fade-enter-from,.el-drawer-fade-enter-active,.el-drawer-fade-enter-to,.el-drawer-fade-leave-from,.el-drawer-fade-leave-active,.el-drawer-fade-leave-to{overflow:hidden!important}.el-drawer-fade-enter-from,.el-drawer-fade-leave-to{background-color:#0000!important}.el-drawer-fade-enter-from .rtl,.el-drawer-fade-leave-to .rtl{transform:translate(100%)}.el-drawer-fade-enter-from .ltr,.el-drawer-fade-leave-to .ltr{transform:translate(-100%)}.el-drawer-fade-enter-from .ttb,.el-drawer-fade-leave-to .ttb{transform:translateY(-100%)}.el-drawer-fade-enter-from .btt,.el-drawer-fade-leave-to .btt{transform:translateY(100%)}.el-dropdown{--el-dropdown-menu-box-shadow:var(--el-box-shadow-light);--el-dropdown-menuItem-hover-fill:var(--el-color-primary-light-9);--el-dropdown-menuItem-hover-color:var(--el-color-primary);--el-dropdown-menu-index:10;color:var(--el-text-color-regular);font-size:var(--el-font-size-base);vertical-align:top;line-height:1;display:inline-flex;position:relative}.el-dropdown.is-disabled{color:var(--el-text-color-placeholder);cursor:not-allowed}.el-dropdown__popper{--el-dropdown-menu-box-shadow:var(--el-box-shadow-light);--el-dropdown-menuItem-hover-fill:var(--el-color-primary-light-9);--el-dropdown-menuItem-hover-color:var(--el-color-primary);--el-dropdown-menu-index:10}.el-dropdown__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-dropdown-menu-box-shadow)}.el-dropdown__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-dropdown__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-dropdown__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-dropdown__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-dropdown__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-dropdown__popper .el-dropdown-menu{border:none}.el-dropdown__popper .el-dropdown__popper-selfdefine{outline:none}.el-dropdown__popper .el-scrollbar__bar{z-index:calc(var(--el-dropdown-menu-index) + 1)}.el-dropdown__popper .el-dropdown__list{box-sizing:border-box;margin:0;padding:0;list-style:none}.el-dropdown .el-dropdown__caret-button{border-left:none;justify-content:center;align-items:center;width:32px;padding-left:0;padding-right:0;display:inline-flex}.el-dropdown .el-dropdown__caret-button>span{display:inline-flex}.el-dropdown .el-dropdown__caret-button:before{content:"";background:var(--el-overlay-color-lighter);width:1px;display:block;position:absolute;top:-1px;bottom:-1px;left:0}.el-dropdown .el-dropdown__caret-button.el-button:before{background:var(--el-border-color);opacity:.5}.el-dropdown .el-dropdown__caret-button .el-dropdown__icon{font-size:inherit;padding-left:0}.el-dropdown .el-dropdown-selfdefine{outline:none}.el-dropdown--large .el-dropdown__caret-button{width:40px}.el-dropdown--small .el-dropdown__caret-button{width:24px}.el-dropdown-menu{z-index:var(--el-dropdown-menu-index);background-color:var(--el-bg-color-overlay);border-radius:var(--el-border-radius-base);box-shadow:none;border:none;margin:0;padding:5px 0;list-style:none;position:relative;top:0;left:0}.el-dropdown-menu__item{white-space:nowrap;line-height:22px;font-size:var(--el-font-size-base);color:var(--el-text-color-regular);cursor:pointer;outline:none;align-items:center;margin:0;padding:5px 16px;list-style:none;display:flex}.el-dropdown-menu__item:not(.is-disabled):hover,.el-dropdown-menu__item:not(.is-disabled):focus{background-color:var(--el-dropdown-menuItem-hover-fill);color:var(--el-dropdown-menuItem-hover-color)}.el-dropdown-menu__item i{margin-right:5px}.el-dropdown-menu__item--divided{border-top:1px solid var(--el-border-color-lighter);margin:6px 0}.el-dropdown-menu__item.is-disabled{cursor:not-allowed;color:var(--el-text-color-disabled)}.el-dropdown-menu--large{padding:7px 0}.el-dropdown-menu--large .el-dropdown-menu__item{padding:7px 20px;font-size:14px;line-height:22px}.el-dropdown-menu--large .el-dropdown-menu__item--divided{margin:8px 0}.el-dropdown-menu--small{padding:3px 0}.el-dropdown-menu--small .el-dropdown-menu__item{padding:2px 12px;font-size:12px;line-height:20px}.el-dropdown-menu--small .el-dropdown-menu__item--divided{margin:4px 0}.el-empty{--el-empty-padding:40px 0;--el-empty-image-width:160px;--el-empty-description-margin-top:20px;--el-empty-bottom-margin-top:20px;--el-empty-fill-color-0:var(--el-color-white);--el-empty-fill-color-1:#fcfcfd;--el-empty-fill-color-2:#f8f9fb;--el-empty-fill-color-3:#f7f8fc;--el-empty-fill-color-4:#eeeff3;--el-empty-fill-color-5:#edeef2;--el-empty-fill-color-6:#e9ebef;--el-empty-fill-color-7:#e5e7e9;--el-empty-fill-color-8:#e0e3e9;--el-empty-fill-color-9:#d5d7de;text-align:center;box-sizing:border-box;padding:var(--el-empty-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.el-empty__image{width:var(--el-empty-image-width)}.el-empty__image img{-webkit-user-select:none;user-select:none;vertical-align:top;object-fit:contain;width:100%;height:100%}.el-empty__image svg{color:var(--el-svg-monochrome-grey);fill:currentColor;vertical-align:top;width:100%;height:100%}.el-empty__description{margin-top:var(--el-empty-description-margin-top)}.el-empty__description p{font-size:var(--el-font-size-base);color:var(--el-text-color-secondary);margin:0}.el-empty__bottom{margin-top:var(--el-empty-bottom-margin-top)}.el-footer{--el-footer-padding:0 20px;--el-footer-height:60px;padding:var(--el-footer-padding);box-sizing:border-box;height:var(--el-footer-height);flex-shrink:0}.el-form-item{--font-size:14px;margin-bottom:18px;display:flex}.el-form-item .el-form-item{margin-bottom:0}.el-form-item .el-input__validateIcon{display:none}.el-form-item--large{--font-size:14px;--el-form-label-font-size:var(--font-size);margin-bottom:22px}.el-form-item--large .el-form-item__label{height:40px;line-height:40px}.el-form-item--large .el-form-item__content{line-height:40px}.el-form-item--large .el-form-item__error{padding-top:4px}.el-form-item--default{--font-size:14px;--el-form-label-font-size:var(--font-size);margin-bottom:18px}.el-form-item--default .el-form-item__label{height:32px;line-height:32px}.el-form-item--default .el-form-item__content{line-height:32px}.el-form-item--default .el-form-item__error{padding-top:2px}.el-form-item--small{--font-size:12px;--el-form-label-font-size:var(--font-size);margin-bottom:18px}.el-form-item--small .el-form-item__label{height:24px;line-height:24px}.el-form-item--small .el-form-item__content{line-height:24px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--label-left .el-form-item__label{text-align:left;justify-content:flex-start}.el-form-item--label-right .el-form-item__label{text-align:right;justify-content:flex-end}.el-form-item--label-top{display:block}.el-form-item--label-top .el-form-item__label{text-align:left;width:-moz-fit-content;width:fit-content;height:auto;margin-bottom:8px;padding-right:0;line-height:22px;display:block}.el-form-item__label-wrap{display:flex}.el-form-item__label{font-size:var(--el-form-label-font-size);color:var(--el-text-color-regular);box-sizing:border-box;flex:none;align-items:flex-start;height:32px;padding:0 12px 0 0;line-height:32px;display:inline-flex}.el-form-item__content{line-height:32px;font-size:var(--font-size);flex-wrap:wrap;flex:1;align-items:center;min-width:0;display:flex;position:relative}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:var(--el-color-danger);padding-top:2px;font-size:12px;line-height:1;position:absolute;top:100%;left:0}.el-form-item__error--inline{margin-left:10px;display:inline-block;position:relative;top:auto;left:auto}.el-form-item.is-required:not(.is-no-asterisk).asterisk-left>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk).asterisk-left>.el-form-item__label-wrap>.el-form-item__label:before{content:"*";color:var(--el-color-danger);margin-right:4px}.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label:after,.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label-wrap>.el-form-item__label:after{content:"*";color:var(--el-color-danger);margin-left:4px}.el-form-item.is-error .el-form-item__content .el-input__wrapper,.el-form-item.is-error .el-form-item__content .el-input__wrapper:hover,.el-form-item.is-error .el-form-item__content .el-input__wrapper:focus,.el-form-item.is-error .el-form-item__content .el-input__wrapper.is-focus,.el-form-item.is-error .el-form-item__content .el-textarea__inner,.el-form-item.is-error .el-form-item__content .el-textarea__inner:hover,.el-form-item.is-error .el-form-item__content .el-textarea__inner:focus,.el-form-item.is-error .el-form-item__content .el-textarea__inner.is-focus,.el-form-item.is-error .el-form-item__content .el-select__wrapper,.el-form-item.is-error .el-form-item__content .el-select__wrapper:hover,.el-form-item.is-error .el-form-item__content .el-select__wrapper:focus,.el-form-item.is-error .el-form-item__content .el-select__wrapper.is-focus,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper:hover,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper:focus,.el-form-item.is-error .el-form-item__content .el-input-tag__wrapper.is-focus,.el-form-item.is-error .el-form-item__content :not(.el-input-otp--underlined) .el-input-otp__input-field,.el-form-item.is-error .el-form-item__content :not(.el-input-otp--underlined) .el-input-otp__input-field:hover,.el-form-item.is-error .el-form-item__content :not(.el-input-otp--underlined) .el-input-otp__input-field:focus,.el-form-item.is-error .el-form-item__content :not(.el-input-otp--underlined) .el-input-otp__input-field.is-focus,.el-form-item.is-error .el-form-item__content .el-input-otp--underlined .el-input-otp__input-field:after,.el-form-item.is-error .el-form-item__content .el-input-otp--underlined .el-input-otp__input-field:hover:after,.el-form-item.is-error .el-form-item__content .el-input-otp--underlined .el-input-otp__input-field:focus:after,.el-form-item.is-error .el-form-item__content .el-input-otp--underlined .el-input-otp__input-field.is-focus:after{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-form-item.is-error .el-form-item__content .el-input-group__append .el-input__wrapper,.el-form-item.is-error .el-form-item__content .el-input-group__prepend .el-input__wrapper{box-shadow:inset 0 0 0 1px #0000}.el-form-item.is-error .el-form-item__content .el-input-group__append .el-input__validateIcon,.el-form-item.is-error .el-form-item__content .el-input-group__prepend .el-input__validateIcon{display:none}.el-form-item.is-error .el-form-item__content .el-input__validateIcon{color:var(--el-color-danger)}.el-form-item--feedback .el-input__validateIcon{display:inline-flex}.el-form{--el-form-label-font-size:var(--el-font-size-base);--el-form-inline-content-width:220px}.el-form--inline .el-form-item{vertical-align:middle;margin-right:32px;display:inline-flex}.el-form--inline .el-form-item:last-child{margin-right:0}.el-form--inline.el-form--label-top{flex-wrap:wrap;display:flex}.el-form--inline.el-form--label-top .el-form-item{display:block}.el-header{--el-header-padding:0 20px;--el-header-height:60px;padding:var(--el-header-padding);box-sizing:border-box;height:var(--el-header-height);flex-shrink:0}.el-image-viewer__wrapper{position:fixed;top:0;bottom:0;left:0;right:0}.el-image-viewer__wrapper:focus{outline:none!important}.el-image-viewer__btn{z-index:1;opacity:.8;cursor:pointer;box-sizing:border-box;-webkit-user-select:none;user-select:none;border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute}.el-image-viewer__btn .el-icon{cursor:pointer}.el-image-viewer__close{width:40px;height:40px;font-size:40px;top:40px;right:40px}.el-image-viewer__canvas{-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;width:100%;height:100%;display:flex;position:static}.el-image-viewer__actions{background-color:var(--el-text-color-regular);border-color:#fff;border-radius:22px;height:44px;padding:0 23px;bottom:30px;left:50%;transform:translate(-50%)}.el-image-viewer__actions__inner{cursor:default;color:#fff;justify-content:space-around;align-items:center;gap:22px;width:100%;height:100%;padding:0 6px;font-size:23px;display:flex}.el-image-viewer__actions__divider{margin:0 -6px}.el-image-viewer__progress{cursor:default;color:#fff;bottom:90px;left:50%;transform:translate(-50%)}.el-image-viewer__prev{color:#fff;background-color:var(--el-text-color-regular);border-color:#fff;width:44px;height:44px;font-size:24px;top:50%;left:40px;transform:translateY(-50%)}.el-image-viewer__next{text-indent:2px;color:#fff;background-color:var(--el-text-color-regular);border-color:#fff;width:44px;height:44px;font-size:24px;top:50%;right:40px;transform:translateY(-50%)}.el-image-viewer__close{color:#fff;background-color:var(--el-text-color-regular);border-color:#fff;width:44px;height:44px;font-size:24px}.el-image-viewer__mask{opacity:.5;background:#000;width:100%;height:100%;position:absolute;top:0;left:0}.el-image-viewer-parent--hidden{overflow:hidden}.viewer-fade-enter-active{animation:viewer-fade-in var(--el-transition-duration)}.viewer-fade-leave-active{animation:viewer-fade-out var(--el-transition-duration)}@keyframes viewer-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translate(0)}}@keyframes viewer-fade-out{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translateY(-20px)}}.el-image__error,.el-image__placeholder,.el-image__wrapper,.el-image__inner{width:100%;height:100%}.el-image{display:inline-block;position:relative;overflow:hidden}.el-image__inner{vertical-align:top;opacity:1}.el-image__inner.is-loading{opacity:0}.el-image__wrapper{position:absolute;top:0;left:0}.el-image__placeholder{background:var(--el-fill-color-light)}.el-image__error{background:var(--el-fill-color-light);color:var(--el-text-color-placeholder);vertical-align:middle;justify-content:center;align-items:center;font-size:14px;display:flex}.el-image__preview{cursor:pointer}.el-textarea{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;vertical-align:bottom;width:100%;font-size:var(--el-font-size-base);display:inline-block;position:relative}.el-textarea__inner{cursor:auto;resize:vertical;box-sizing:border-box;width:100%;line-height:1.5;font-size:inherit;color:var(--el-input-text-color,var(--el-text-color-regular));background-color:var(--el-input-bg-color,var(--el-fill-color-blank));-webkit-appearance:none;box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset;border-radius:var(--el-input-border-radius,var(--el-border-radius-base));transition:var(--el-transition-box-shadow);background-image:none;border:none;padding:5px 11px;font-family:inherit;display:block;position:relative}.el-textarea__inner.is-clearable{padding:5px 26px 5px 11px}.el-textarea__inner::placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-textarea__inner:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-textarea__inner:focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset;outline:none}.el-textarea__clear{color:var(--el-input-icon-color);cursor:pointer;font-size:14px;position:absolute;top:15px;right:11px;transform:translateY(-50%)}.el-textarea__clear:hover{color:var(--el-input-clear-hover-color)}.el-textarea .el-input__count{color:var(--el-color-info);background:var(--el-fill-color-blank);font-size:12px;line-height:14px;position:absolute;bottom:5px;right:10px}.el-textarea .el-input__count.is-outside{top:100%;right:0;bottom:unset;background:0 0;padding-top:2px;line-height:1;position:absolute}.el-textarea.is-disabled .el-textarea__inner{box-shadow:0 0 0 1px var(--el-disabled-border-color) inset;background-color:var(--el-disabled-bg-color);color:var(--el-disabled-text-color);cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:var(--el-text-color-placeholder)}.el-textarea.is-exceed .el-textarea__inner{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-textarea.is-exceed .el-input__count{color:var(--el-color-danger)}.el-input{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;--el-input-height:var(--el-component-size);font-size:var(--el-font-size-base);width:var(--el-input-width);line-height:var(--el-input-height);box-sizing:border-box;vertical-align:middle;display:inline-flex;position:relative}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{background:var(--el-text-color-disabled);border-radius:5px;width:6px}.el-input::-webkit-scrollbar-corner{background:var(--el-fill-color-blank)}.el-input::-webkit-scrollbar-track{background:var(--el-fill-color-blank)}.el-input::-webkit-scrollbar-track-piece{background:var(--el-fill-color-blank);width:6px}.el-input .el-input__clear,.el-input .el-input__password{color:var(--el-input-icon-color);cursor:pointer;font-size:14px}.el-input .el-input__clear:hover,.el-input .el-input__password:hover{color:var(--el-input-clear-hover-color)}.el-input .el-input__count{height:100%;color:var(--el-color-info);align-items:center;font-size:12px;display:inline-flex}.el-input .el-input__count .el-input__count-inner{background:var(--el-fill-color-blank);line-height:initial;padding-left:8px;display:inline-block}.el-input .el-input__count.is-outside{height:unset;padding-top:2px;position:absolute;top:100%;right:0}.el-input .el-input__count.is-outside .el-input__count-inner{background:0 0;padding-left:0;line-height:1}.el-input__wrapper{background-color:var(--el-input-bg-color,var(--el-fill-color-blank));border-radius:var(--el-input-border-radius,var(--el-border-radius-base));cursor:text;transition:var(--el-transition-box-shadow);box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset;background-image:none;flex-grow:1;justify-content:center;align-items:center;padding:1px 11px;display:inline-flex;transform:translateZ(0)}.el-input__wrapper:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-input__wrapper.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-input{--el-input-inner-height:calc(var(--el-input-height,32px) - 2px)}.el-input__inner{-webkit-appearance:none;width:100%;color:var(--el-input-text-color,var(--el-text-color-regular));font-size:inherit;height:var(--el-input-inner-height);line-height:var(--el-input-inner-height);box-sizing:border-box;background:0 0;border:none;outline:none;flex-grow:1;padding:0}.el-input__inner:focus{outline:none}.el-input__inner::placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-input__inner[type=password]::-ms-reveal{display:none}.el-input__inner[type=number]{line-height:1}.el-input__prefix{white-space:nowrap;height:100%;line-height:var(--el-input-inner-height);text-align:center;color:var(--el-input-icon-color,var(--el-text-color-placeholder));transition:all var(--el-transition-duration);pointer-events:none;flex-wrap:nowrap;flex-shrink:0;display:inline-flex}.el-input__prefix-inner{pointer-events:all;justify-content:center;align-items:center;display:inline-flex}.el-input__prefix-inner>:last-child{margin-right:8px}.el-input__prefix-inner>:first-child,.el-input__prefix-inner>:first-child.el-input__icon{margin-left:0}.el-input__suffix{white-space:nowrap;height:100%;line-height:var(--el-input-inner-height);text-align:center;color:var(--el-input-icon-color,var(--el-text-color-placeholder));transition:all var(--el-transition-duration);pointer-events:none;flex-wrap:nowrap;flex-shrink:0;display:inline-flex}.el-input__suffix-inner{pointer-events:all;justify-content:center;align-items:center;display:inline-flex}.el-input__suffix-inner>:first-child{margin-left:8px}.el-input .el-input__icon{height:inherit;line-height:inherit;transition:all var(--el-transition-duration);justify-content:center;align-items:center;margin-left:8px;display:flex}.el-input .el-input__clear{transition:color var(--el-transition-duration)}.el-input__validateIcon{pointer-events:none}.el-input.is-active .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-color, ) inset}.el-input.is-disabled{cursor:not-allowed}.el-input.is-disabled .el-input__wrapper{background-color:var(--el-disabled-bg-color);cursor:not-allowed;box-shadow:0 0 0 1px var(--el-disabled-border-color) inset}.el-input.is-disabled .el-input__inner{color:var(--el-disabled-text-color);-webkit-text-fill-color:var(--el-disabled-text-color);cursor:not-allowed}.el-input.is-disabled .el-input__inner::placeholder{color:var(--el-text-color-placeholder)}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-input.is-disabled .el-input__prefix-inner,.el-input.is-disabled .el-input__suffix-inner{pointer-events:none}.el-input.is-exceed .el-input__wrapper{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-input.is-exceed .el-input__suffix .el-input__count{color:var(--el-color-danger)}.el-input--large{--el-input-height:var(--el-component-size-large);font-size:14px}.el-input--large .el-input__wrapper{padding:1px 15px}.el-input--large{--el-input-inner-height:calc(var(--el-input-height,40px) - 2px)}.el-input--small{--el-input-height:var(--el-component-size-small);font-size:12px}.el-input--small .el-input__wrapper{padding:1px 7px}.el-input--small{--el-input-inner-height:calc(var(--el-input-height,24px) - 2px)}.el-input-group{align-items:stretch;width:100%;display:inline-flex}.el-input-group__append,.el-input-group__prepend{background-color:var(--el-fill-color-light);color:var(--el-color-info);border-radius:var(--el-input-border-radius);white-space:nowrap;justify-content:center;align-items:center;min-height:100%;padding:0 20px;display:inline-flex;position:relative}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:none}.el-input-group__append .el-select,.el-input-group__append .el-button,.el-input-group__prepend .el-select,.el-input-group__prepend .el-button{flex:1;margin:0 -20px;display:inline-block}.el-input-group__append button.el-button,.el-input-group__append button.el-button:hover,.el-input-group__append div.el-select .el-select__wrapper,.el-input-group__append div.el-select:hover .el-select__wrapper,.el-input-group__prepend button.el-button,.el-input-group__prepend button.el-button:hover,.el-input-group__prepend div.el-select .el-select__wrapper,.el-input-group__prepend div.el-select:hover .el-select__wrapper{color:inherit;background-color:#0000;border-color:#0000}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{box-shadow:1px 0 0 0 var(--el-input-border-color) inset,0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset;border-right:0;border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append{box-shadow:0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset,-1px 0 0 0 var(--el-input-border-color) inset;border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--prepend>.el-input__wrapper{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--prepend .el-input-group__prepend .el-select .el-select__wrapper{box-shadow:1px 0 0 0 var(--el-input-border-color) inset,0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset;border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group--append>.el-input__wrapper{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group--append .el-input-group__append .el-select .el-select__wrapper{box-shadow:0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset,-1px 0 0 0 var(--el-input-border-color) inset;border-top-left-radius:0;border-bottom-left-radius:0}.el-input-hidden{display:none!important}.el-input-number{vertical-align:middle;width:150px;line-height:30px;display:inline-flex;position:relative}.el-input-number .el-input__wrapper{padding-left:42px;padding-right:42px}.el-input-number .el-input__inner{-webkit-appearance:none;-moz-appearance:textfield;text-align:center;line-height:1}.el-input-number .el-input__inner::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.el-input-number .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-input-number.is-left .el-input__inner{text-align:left}.el-input-number.is-right .el-input__inner{text-align:right}.el-input-number.is-center .el-input__inner{text-align:center}.el-input-number__increase,.el-input-number__decrease{z-index:1;background:var(--el-fill-color-light);width:32px;height:auto;color:var(--el-text-color-regular);cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;font-size:13px;display:flex;position:absolute;top:1px;bottom:1px}.el-input-number__increase:hover,.el-input-number__decrease:hover{color:var(--el-color-primary)}.el-input-number__increase:hover~.el-input:not(.is-disabled) .el-input__wrapper,.el-input-number__decrease:hover~.el-input:not(.is-disabled) .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color,var(--el-color-primary)) inset}.el-input-number__increase.is-disabled,.el-input-number__decrease.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-input-number__increase{border-radius:0 var(--el-border-radius-base) var(--el-border-radius-base) 0;border-left:var(--el-border);right:1px}.el-input-number__decrease{border-radius:var(--el-border-radius-base) 0 0 var(--el-border-radius-base);border-right:var(--el-border);left:1px}.el-input-number.is-disabled .el-input-number__increase,.el-input-number.is-disabled .el-input-number__decrease{border-color:var(--el-disabled-border-color);color:var(--el-disabled-border-color)}.el-input-number.is-disabled .el-input-number__increase:hover,.el-input-number.is-disabled .el-input-number__decrease:hover{color:var(--el-disabled-border-color);cursor:not-allowed}.el-input-number--large{width:180px;line-height:38px}.el-input-number--large .el-input-number__increase,.el-input-number--large .el-input-number__decrease{width:40px;font-size:14px}.el-input-number--large.is-controls-right .el-input--large .el-input__wrapper{padding-right:47px}.el-input-number--large .el-input--large .el-input__wrapper{padding-left:47px;padding-right:47px}.el-input-number--small{width:120px;line-height:22px}.el-input-number--small .el-input-number__increase,.el-input-number--small .el-input-number__decrease{width:24px;font-size:12px}.el-input-number--small.is-controls-right .el-input--small .el-input__wrapper{padding-right:31px}.el-input-number--small .el-input--small .el-input__wrapper{padding-left:31px;padding-right:31px}.el-input-number--small .el-input-number__increase [class*=el-icon],.el-input-number--small .el-input-number__decrease [class*=el-icon]{transform:scale(.9)}.el-input-number.is-without-controls .el-input__wrapper{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__wrapper{padding-left:15px;padding-right:42px}.el-input-number.is-controls-right .el-input-number__increase,.el-input-number.is-controls-right .el-input-number__decrease{--el-input-number-controls-height:15px;height:var(--el-input-number-controls-height);line-height:var(--el-input-number-controls-height)}.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon],.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon]{transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-radius:0 var(--el-border-radius-base) 0 0;border-bottom:var(--el-border);bottom:auto;left:auto}.el-input-number.is-controls-right .el-input-number__decrease{border-right:none;border-left:var(--el-border);border-radius:0 0 var(--el-border-radius-base) 0;top:auto;left:auto;right:1px}.el-input-number.is-controls-right[class*=large] [class*=increase],.el-input-number.is-controls-right[class*=large] [class*=decrease]{--el-input-number-controls-height:19px}.el-input-number.is-controls-right[class*=small] [class*=increase],.el-input-number.is-controls-right[class*=small] [class*=decrease]{--el-input-number-controls-height:11px}.el-input-tag{--el-input-tag-border-color-hover:var(--el-border-color-hover);--el-input-tag-placeholder-color:var(--el-text-color-placeholder);--el-input-tag-disabled-color:var(--el-disabled-text-color);--el-input-tag-disabled-border:var(--el-disabled-border-color);--el-input-tag-font-size:var(--el-font-size-base);--el-input-tag-close-hover-color:var(--el-text-color-secondary);--el-input-tag-text-color:var(--el-text-color-regular);--el-input-tag-input-focus-border-color:var(--el-color-primary);--el-input-tag-width:100%;--el-input-tag-mini-height:var(--el-component-size);--el-input-tag-gap:6px;--el-input-tag-padding:4px;--el-input-tag-inner-padding:8px;--el-input-tag-line-height:24px;box-sizing:border-box;cursor:pointer;font-size:var(--el-input-tag-font-size);padding:var(--el-input-tag-padding);width:var(--el-input-tag-width);min-height:var(--el-input-tag-mini-height);line-height:var(--el-input-tag-line-height);border-radius:var(--el-border-radius-base);background-color:var(--el-fill-color-blank);transition:var(--el-transition-duration);box-shadow:0 0 0 1px var(--el-border-color) inset;align-items:center;display:flex;transform:translate(0)}.el-input-tag.is-focused{box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-input-tag.is-hovering:not(.is-focused){box-shadow:0 0 0 1px var(--el-border-color-hover) inset}.el-input-tag.is-disabled{cursor:not-allowed;background-color:var(--el-fill-color-light);box-shadow:0 0 0 1px var(--el-input-tag-disabled-border) inset}.el-input-tag.is-disabled:hover{box-shadow:0 0 0 1px var(--el-input-tag-disabled-border) inset}.el-input-tag.is-disabled.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-input-tag.is-disabled .el-input-tag__inner .el-input-tag__input,.el-input-tag.is-disabled .el-input-tag__inner .el-tag{cursor:not-allowed}.el-input-tag__prefix{padding:0 var(--el-input-tag-inner-padding);color:var(--el-input-icon-color,var(--el-text-color-placeholder));flex-shrink:0;align-items:center;display:flex}.el-input-tag__suffix{padding:0 var(--el-input-tag-inner-padding);color:var(--el-input-icon-color,var(--el-text-color-placeholder));flex-shrink:0;align-items:center;gap:8px;display:flex}.el-input-tag__collapse-tag{line-height:1}.el-input-tag__input-tag-list{flex-wrap:wrap;flex:1;align-items:center;gap:6px;min-width:0;display:flex;position:relative}.el-input-tag__input-tag-list.is-near{margin-left:-8px}.el-input-tag__input-tag-list .el-tag{cursor:pointer;border-color:#0000}.el-input-tag__input-tag-list .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-input-tag__input-tag-list .el-tag .el-tag__content{min-width:0}.el-input-tag__inner{align-items:center;gap:var(--el-input-tag-gap);flex-wrap:wrap;flex:1;min-width:0;max-width:100%;display:flex;position:relative}.el-input-tag__inner.is-left-space{margin-left:var(--el-input-tag-inner-padding)}.el-input-tag__inner.is-right-space{margin-right:var(--el-input-tag-inner-padding)}.el-input-tag__inner.is-draggable .el-tag{cursor:move;-webkit-user-select:none;user-select:none}.el-input-tag__drop-indicator{width:1px;height:var(--el-input-tag-line-height);background-color:var(--el-color-primary);position:absolute;top:0}.el-input-tag__inner .el-tag{cursor:pointer;border-color:#0000;max-width:100%}.el-input-tag__inner .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-input-tag__inner .el-tag .el-tag__content{text-overflow:ellipsis;white-space:nowrap;min-width:0;line-height:normal;overflow:hidden}.el-input-tag__input-wrapper{flex:1}.el-input-tag__input{color:var(--el-input-tag-text-color);font-size:inherit;font-family:inherit;line-height:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0000;border:none;outline:none;width:100%;padding:0}.el-input-tag__input::placeholder{color:var(--el-input-tag-placeholder-color)}.el-input-tag__input-calculator{visibility:hidden;white-space:pre;max-width:100%;position:absolute;top:0;left:0;overflow:hidden}.el-input-tag--large{--el-input-tag-gap:6px;--el-input-tag-padding:8px;--el-input-tag-padding-left:8px;--el-input-tag-font-size:14px}.el-input-tag--small{--el-input-tag-gap:4px;--el-input-tag-padding:2px;--el-input-tag-padding-left:6px;--el-input-tag-font-size:12px;--el-input-tag-line-height:20px;--el-input-tag-mini-height:var(--el-component-size-small)}.el-input-otp{--el-input-otp-text-color:var(--el-text-color-regular);--el-input-otp-field-width:calc(var(--el-input-otp-size) - 4px);--el-input-otp-gap:8px;--el-input-otp-font-size:14px;--el-input-otp-size:32px}.el-input-otp--large{--el-input-otp-gap:12px;--el-input-otp-font-size:14px;--el-input-otp-size:40px}.el-input-otp--small{--el-input-otp-gap:6px;--el-input-otp-font-size:12px;--el-input-otp-size:24px}.el-input-otp{align-items:center;column-gap:var(--el-input-otp-gap);font-size:var(--el-input-otp-font-size);height:var(--el-input-otp-size);flex-wrap:nowrap;display:inline-flex}.el-input-otp.is-disabled,.el-input-otp.is-disabled .el-input-otp__input{cursor:not-allowed}.el-input-otp__input-field{width:var(--el-input-otp-field-width,calc(var(--el-input-otp-size) - 4px));border-radius:var(--el-border-radius-base);height:100%;line-height:1;position:relative}.el-input-otp__input{text-align:center;width:100%;height:100%;line-height:1;font-size:inherit;color:var(--el-input-otp-text-color,var(--el-text-color-regular));background-color:#0000;border:none;outline:none;padding:0}.el-input-otp.is-disabled .el-input-otp__input{color:var(--el-text-color-disabled)}.el-input-otp--outlined .el-input-otp__input-field{box-shadow:0 0 0 1px var(--el-border-color) inset}@media (hover:hover){.el-input-otp--outlined .el-input-otp__input-field:hover{box-shadow:0 0 0 1px var(--el-color-primary-light-5) inset}}.el-input-otp--outlined .el-input-otp__input-field:focus-within{box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-input-otp--outlined.is-disabled .el-input-otp__input-field{background-color:var(--el-fill-color-light)}@media (hover:hover){.el-input-otp--outlined.is-disabled .el-input-otp__input-field:hover{box-shadow:0 0 0 1px var(--el-border-color) inset}}.el-input-otp--filled .el-input-otp__input-field{background-color:var(--el-fill-color)}@media (hover:hover){.el-input-otp--filled .el-input-otp__input-field:hover{background-color:var(--el-fill-color-dark)}}.el-input-otp--filled .el-input-otp__input-field:focus-within{background-color:var(--el-fill-color-blank);box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-input-otp--filled.is-disabled .el-input-otp__input-field{background-color:var(--el-fill-color-light)}@media (hover:hover){.el-input-otp--filled.is-disabled .el-input-otp__input-field:hover{background-color:var(--el-fill-color-light)}}.el-input-otp--underlined .el-input-otp__input-field:after{content:"";background-color:var(--el-border-color);width:100%;height:1px;display:block;position:absolute;bottom:0;left:0}@media (hover:hover){.el-input-otp--underlined .el-input-otp__input-field:hover:after{background-color:var(--el-color-primary-light-5)}}.el-input-otp--underlined .el-input-otp__input-field:focus-within:after{background-color:var(--el-color-primary)}@media (hover:hover){.el-input-otp--underlined.is-disabled .el-input-otp__input-field:hover:after{background-color:var(--el-border-color)}}.el-link{--el-link-font-size:var(--el-font-size-base);--el-link-font-weight:var(--el-font-weight-primary);--el-link-text-color:var(--el-text-color-regular);--el-link-hover-text-color:var(--el-color-primary);--el-link-disabled-text-color:var(--el-text-color-placeholder);vertical-align:middle;cursor:pointer;font-size:var(--el-link-font-size);font-weight:var(--el-link-font-weight);color:var(--el-link-text-color);outline:none;flex-direction:row;justify-content:center;align-items:center;padding:0;text-decoration:none;display:inline-flex;position:relative}.el-link.is-hover-underline:hover:after{content:"";border-bottom:1px solid var(--el-link-hover-text-color);height:0;position:absolute;bottom:0;left:0;right:0}.el-link.is-underline:after{content:"";border-bottom:1px solid var(--el-link-text-color);height:0;position:absolute;bottom:0;left:0;right:0}.el-link:hover{color:var(--el-link-hover-text-color)}.el-link:hover:after{border-color:var(--el-link-hover-text-color)}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link__inner{justify-content:center;align-items:center;display:inline-flex}.el-link.el-link--primary{--el-link-text-color:var(--el-color-primary);--el-link-hover-text-color:var(--el-color-primary-light-3);--el-link-disabled-text-color:var(--el-color-primary-light-5)}.el-link.el-link--success{--el-link-text-color:var(--el-color-success);--el-link-hover-text-color:var(--el-color-success-light-3);--el-link-disabled-text-color:var(--el-color-success-light-5)}.el-link.el-link--warning{--el-link-text-color:var(--el-color-warning);--el-link-hover-text-color:var(--el-color-warning-light-3);--el-link-disabled-text-color:var(--el-color-warning-light-5)}.el-link.el-link--danger{--el-link-text-color:var(--el-color-danger);--el-link-hover-text-color:var(--el-color-danger-light-3);--el-link-disabled-text-color:var(--el-color-danger-light-5)}.el-link.el-link--error{--el-link-text-color:var(--el-color-error);--el-link-hover-text-color:var(--el-color-error-light-3);--el-link-disabled-text-color:var(--el-color-error-light-5)}.el-link.el-link--info{--el-link-text-color:var(--el-color-info);--el-link-hover-text-color:var(--el-color-info-light-3);--el-link-disabled-text-color:var(--el-color-info-light-5)}.el-link.is-disabled{color:var(--el-link-disabled-text-color);cursor:not-allowed}.el-link.is-disabled:after{border-color:var(--el-link-disabled-text-color)}:root{--el-loading-spinner-size:42px;--el-loading-fullscreen-spinner-size:50px}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{z-index:2000;background-color:var(--el-mask-color);transition:opacity var(--el-transition-duration);margin:0;position:absolute;top:0;bottom:0;left:0;right:0}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:calc((0px - var(--el-loading-fullscreen-spinner-size)) / 2)}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:var(--el-loading-fullscreen-spinner-size);width:var(--el-loading-fullscreen-spinner-size)}.el-loading-spinner{margin-top:calc((0px - var(--el-loading-spinner-size)) / 2);text-align:center;width:100%;position:absolute;top:50%}.el-loading-spinner .el-loading-text{color:var(--el-color-primary);margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:var(--el-loading-spinner-size);width:var(--el-loading-spinner-size);animation:2s linear infinite loading-rotate;display:inline}.el-loading-spinner .path{stroke-dasharray:90 150;stroke-dashoffset:0;stroke-width:2px;stroke:var(--el-color-primary);stroke-linecap:round;animation:1.5s ease-in-out infinite loading-dash}.el-loading-spinner i{color:var(--el-color-primary)}.el-loading-fade-enter-from,.el-loading-fade-leave-to{opacity:0}@keyframes loading-rotate{to{transform:rotate(360deg)}}@keyframes loading-dash{0%{stroke-dasharray:1 200;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-40px}to{stroke-dasharray:90 150;stroke-dashoffset:-120px}}.el-main{--el-main-padding:20px;box-sizing:border-box;padding:var(--el-main-padding);flex:auto;display:block;overflow:auto}:root{--el-menu-active-color:var(--el-color-primary);--el-menu-text-color:var(--el-text-color-primary);--el-menu-hover-text-color:var(--el-color-primary);--el-menu-bg-color:var(--el-fill-color-blank);--el-menu-hover-bg-color:var(--el-color-primary-light-9);--el-menu-item-height:56px;--el-menu-sub-item-height:calc(var(--el-menu-item-height) - 6px);--el-menu-horizontal-height:60px;--el-menu-horizontal-sub-item-height:36px;--el-menu-item-font-size:var(--el-font-size-base);--el-menu-item-hover-fill:var(--el-color-primary-light-9);--el-menu-border-color:var(--el-border-color);--el-menu-base-level-padding:20px;--el-menu-level-padding:20px;--el-menu-icon-width:24px}.el-menu{border-right:solid 1px var(--el-menu-border-color);background-color:var(--el-menu-bg-color);box-sizing:border-box;margin:0;padding-left:0;list-style:none;position:relative}.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item,.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-sub-menu__title,.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item-group__title{white-space:nowrap;padding-left:calc(var(--el-menu-base-level-padding) + var(--el-menu-level) * var(--el-menu-level-padding))}.el-menu:not(.el-menu--collapse) .el-sub-menu__title{padding-right:calc(var(--el-menu-base-level-padding) + var(--el-menu-icon-width))}.el-menu--horizontal{height:var(--el-menu-horizontal-height);border-right:none;flex-wrap:nowrap;display:flex}.el-menu--horizontal.el-menu--popup-container{height:unset}.el-menu--horizontal.el-menu{border-bottom:solid 1px var(--el-menu-border-color)}.el-menu--horizontal>.el-menu-item{height:100%;color:var(--el-menu-text-color);border-bottom:2px solid #0000;justify-content:center;align-items:center;margin:0;display:inline-flex}.el-menu--horizontal>.el-menu-item a,.el-menu--horizontal>.el-menu-item a:hover{color:inherit}.el-menu--horizontal>.el-sub-menu:focus,.el-menu--horizontal>.el-sub-menu:hover{outline:none}.el-menu--horizontal>.el-sub-menu:hover .el-sub-menu__title{color:var(--el-menu-hover-text-color)}.el-menu--horizontal>.el-sub-menu.is-active .el-sub-menu__title{border-bottom:2px solid var(--el-menu-active-color);color:var(--el-menu-active-color)}.el-menu--horizontal>.el-sub-menu .el-sub-menu__title{height:100%;color:var(--el-menu-text-color);border-bottom:2px solid #0000}.el-menu--horizontal>.el-sub-menu .el-sub-menu__title:hover{background-color:var(--el-menu-bg-color)}.el-menu--horizontal .el-menu .el-menu-item,.el-menu--horizontal .el-menu .el-sub-menu__title{background-color:var(--el-menu-bg-color);height:var(--el-menu-horizontal-sub-item-height);line-height:var(--el-menu-horizontal-sub-item-height);color:var(--el-menu-text-color);align-items:center;padding:0 10px;display:flex}.el-menu--horizontal .el-menu .el-sub-menu__title{padding-right:40px}.el-menu--horizontal .el-menu .el-menu-item.is-active,.el-menu--horizontal .el-menu .el-menu-item.is-active:hover,.el-menu--horizontal .el-menu .el-sub-menu.is-active>.el-sub-menu__title,.el-menu--horizontal .el-menu .el-sub-menu.is-active>.el-sub-menu__title:hover{color:var(--el-menu-active-color)}.el-menu--horizontal .el-menu-item:not(.is-disabled):hover,.el-menu--horizontal .el-menu-item:not(.is-disabled):focus{color:var(--el-menu-active-color,var(--el-menu-hover-text-color));background-color:var(--el-menu-hover-bg-color);outline:none}.el-menu--horizontal>.el-menu-item.is-active{border-bottom:2px solid var(--el-menu-active-color);color:var(--el-menu-active-color)!important}.el-menu--collapse{width:calc(var(--el-menu-icon-width) + var(--el-menu-base-level-padding) * 2)}.el-menu--collapse>.el-menu-item [class^=el-icon],.el-menu--collapse>.el-sub-menu>.el-sub-menu__title [class^=el-icon],.el-menu--collapse>.el-menu-item-group>ul>.el-sub-menu>.el-sub-menu__title [class^=el-icon]{vertical-align:middle;width:var(--el-menu-icon-width);text-align:center;margin:0}.el-menu--collapse>.el-menu-item .el-sub-menu__icon-arrow,.el-menu--collapse>.el-sub-menu>.el-sub-menu__title .el-sub-menu__icon-arrow,.el-menu--collapse>.el-menu-item-group>ul>.el-sub-menu>.el-sub-menu__title .el-sub-menu__icon-arrow{display:none}.el-menu--collapse>.el-menu-item>span,.el-menu--collapse>.el-sub-menu>.el-sub-menu__title>span,.el-menu--collapse>.el-menu-item-group>ul>.el-sub-menu>.el-sub-menu__title>span{visibility:hidden;width:0;height:0;display:inline-block;overflow:hidden}.el-menu--collapse>.el-menu-item.is-active i{color:inherit}.el-menu--collapse .el-menu .el-sub-menu{min-width:200px}.el-menu--collapse .el-sub-menu.is-active .el-sub-menu__title{color:var(--el-menu-active-color)}.el-menu--popup{z-index:100;border-radius:var(--el-border-radius-small);min-width:200px;box-shadow:var(--el-box-shadow-light);border:none;padding:5px 0}.el-menu .el-icon{flex-shrink:0}.el-menu-item{height:var(--el-menu-item-height);line-height:var(--el-menu-item-height);font-size:var(--el-menu-item-font-size);color:var(--el-menu-text-color);padding:0 var(--el-menu-base-level-padding);cursor:pointer;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration),color var(--el-transition-duration);box-sizing:border-box;white-space:nowrap;align-items:center;list-style:none;display:flex;position:relative}.el-menu-item *{vertical-align:bottom}.el-menu-item i{color:inherit}.el-menu-item:hover,.el-menu-item:focus{outline:none}.el-menu-item:hover{background-color:var(--el-menu-hover-bg-color)}.el-menu-item.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-menu-item [class^=el-icon]{width:var(--el-menu-icon-width);text-align:center;vertical-align:middle;margin-right:5px;font-size:18px}.el-menu-item.is-active{color:var(--el-menu-active-color)}.el-menu-item.is-active i{color:inherit}.el-menu-item .el-menu-tooltip__trigger{box-sizing:border-box;width:100%;height:100%;padding:0 var(--el-menu-base-level-padding);align-items:center;display:inline-flex;position:absolute;top:0;left:0}.el-sub-menu{margin:0;padding-left:0;list-style:none}.el-sub-menu__title{height:var(--el-menu-item-height);line-height:var(--el-menu-item-height);font-size:var(--el-menu-item-font-size);color:var(--el-menu-text-color);padding:0 var(--el-menu-base-level-padding);cursor:pointer;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration),color var(--el-transition-duration);box-sizing:border-box;white-space:nowrap;align-items:center;list-style:none;display:flex;position:relative}.el-sub-menu__title *{vertical-align:bottom}.el-sub-menu__title i{color:inherit}.el-sub-menu__title:hover,.el-sub-menu__title:focus{outline:none}.el-sub-menu__title.is-disabled{opacity:.25;cursor:not-allowed;background:0 0!important}.el-sub-menu__title:hover{background-color:var(--el-menu-hover-bg-color)}.el-sub-menu .el-menu{border:none}.el-sub-menu .el-menu-item{height:var(--el-menu-sub-item-height);line-height:var(--el-menu-sub-item-height)}.el-sub-menu.el-sub-menu__hide-arrow .el-sub-menu__title{padding-right:var(--el-menu-base-level-padding)}.el-sub-menu__hide-arrow .el-sub-menu__icon-arrow{display:none!important}.el-sub-menu.is-active .el-sub-menu__title{border-bottom-color:var(--el-menu-active-color)}.el-sub-menu.is-disabled .el-sub-menu__title,.el-sub-menu.is-disabled .el-menu-item{opacity:.25;cursor:not-allowed;background:0 0!important}.el-sub-menu .el-icon{vertical-align:middle;width:var(--el-menu-icon-width);text-align:center;margin-right:5px;font-size:18px}.el-sub-menu .el-icon.el-sub-menu__icon-more{margin-right:0!important}.el-sub-menu .el-sub-menu__icon-arrow{top:50%;right:var(--el-menu-base-level-padding);transition:transform var(--el-transition-duration);width:inherit;margin-top:-6px;margin-right:0;font-size:12px;position:absolute}.el-menu-item-group>ul{padding:0}.el-menu-item-group__title{padding:7px 0 7px var(--el-menu-base-level-padding);color:var(--el-text-color-secondary);font-size:12px;line-height:normal}.horizontal-collapse-transition .el-sub-menu__title .el-sub-menu__icon-arrow{transition:var(--el-transition-duration-fast);opacity:0}.el-popper,.el-menu--popup-container,.el-menu{outline:none}.el-message-box{--el-messagebox-title-color:var(--el-text-color-primary);--el-messagebox-width:420px;--el-messagebox-border-radius:4px;--el-messagebox-box-shadow:var(--el-box-shadow);--el-messagebox-font-size:var(--el-font-size-large);--el-messagebox-content-font-size:var(--el-font-size-base);--el-messagebox-content-color:var(--el-text-color-regular);--el-messagebox-error-font-size:12px;--el-messagebox-padding-primary:12px;--el-messagebox-font-line-height:var(--el-font-line-height-primary);max-width:var(--el-messagebox-width);width:100%;padding:var(--el-messagebox-padding-primary);vertical-align:middle;background-color:var(--el-bg-color);border-radius:var(--el-messagebox-border-radius);font-size:var(--el-messagebox-font-size);box-shadow:var(--el-messagebox-box-shadow);text-align:left;-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;overflow-wrap:break-word;display:inline-block;position:relative;overflow:hidden}.el-message-box:focus{outline:none!important}.is-message-box .el-overlay-message-box{text-align:center;padding:16px;position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.is-message-box .el-overlay-message-box:after{content:"";vertical-align:middle;width:0;height:100%;display:inline-block}.el-message-box.is-draggable .el-message-box__header{cursor:move;-webkit-user-select:none;user-select:none}.el-message-box__header{padding-bottom:var(--el-messagebox-padding-primary)}.el-message-box__header.show-close{padding-right:calc(var(--el-messagebox-padding-primary) + var(--el-message-close-size,16px))}.el-message-box__title{font-size:var(--el-messagebox-font-size);line-height:var(--el-messagebox-font-line-height);color:var(--el-messagebox-title-color)}.el-message-box__headerbtn{width:40px;height:40px;font-size:var(--el-message-close-size,16px);cursor:pointer;background:0 0;border:none;outline:none;padding:0;position:absolute;top:0;right:0}.el-message-box__headerbtn .el-message-box__close{color:var(--el-color-info);font-size:inherit}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:var(--el-color-primary)}.el-message-box__content{color:var(--el-messagebox-content-color);font-size:var(--el-messagebox-content-font-size)}.el-message-box__container{align-items:center;gap:12px;display:flex}.el-message-box__input{padding-top:12px}.el-message-box__input div.invalid>input,.el-message-box__input div.invalid>input:focus{border-color:var(--el-color-error)}.el-message-box__status{font-size:24px}.el-message-box__status.el-message-box-icon--primary{--el-messagebox-color:var(--el-color-primary);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--success{--el-messagebox-color:var(--el-color-success);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--info{--el-messagebox-color:var(--el-color-info);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--warning{--el-messagebox-color:var(--el-color-warning);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--error{--el-messagebox-color:var(--el-color-error);color:var(--el-messagebox-color)}.el-message-box__message{min-width:0;margin:0}.el-message-box__message p{line-height:var(--el-messagebox-font-line-height);margin:0}.el-message-box__errormsg{color:var(--el-color-error);font-size:var(--el-messagebox-error-font-size);line-height:var(--el-messagebox-font-line-height)}.el-message-box__btns{padding-top:var(--el-messagebox-padding-primary);flex-wrap:wrap;justify-content:flex-end;align-items:center;display:flex}.el-message-box--center .el-message-box__title{justify-content:center;align-items:center;gap:6px;display:flex}.el-message-box--center .el-message-box__status{font-size:inherit}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__container{justify-content:center}.el-message-box-parent--hidden{overflow:hidden}.fade-in-linear-enter-active .el-overlay-message-box{animation:msgbox-fade-in var(--el-transition-duration)}.fade-in-linear-leave-active .el-overlay-message-box{animation:msgbox-fade-in var(--el-transition-duration) reverse}@keyframes msgbox-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translate(0)}}.el-message{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-border-color-lighter);--el-message-padding:11px 15px;--el-message-close-size:16px;--el-message-close-icon-color:var(--el-text-color-placeholder);--el-message-close-hover-color:var(--el-text-color-secondary);box-sizing:border-box;border-radius:var(--el-border-radius-base);border-width:var(--el-border-width);border-style:var(--el-border-style);border-color:var(--el-message-border-color);background-color:var(--el-message-bg-color);width:max-content;max-width:calc(100% - 32px);transition:opacity var(--el-transition-duration),transform .4s,top .4s,bottom .4s;padding:var(--el-message-padding);align-items:center;gap:8px;display:flex;position:fixed}.el-message.is-left{left:16px}.el-message.is-right{right:16px}.el-message.is-center{left:50%;transform:translate(-50%)}.el-message.is-plain{background-color:var(--el-bg-color-overlay);border-color:var(--el-bg-color-overlay);box-shadow:var(--el-box-shadow-light)}.el-message p{margin:0}.el-message--primary{--el-message-bg-color:var(--el-color-primary-light-9);--el-message-border-color:var(--el-color-primary-light-8);--el-message-text-color:var(--el-color-primary)}.el-message--primary .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--primary{color:var(--el-message-text-color)}.el-message--success{--el-message-bg-color:var(--el-color-success-light-9);--el-message-border-color:var(--el-color-success-light-8);--el-message-text-color:var(--el-color-success)}.el-message--success .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--success{color:var(--el-message-text-color)}.el-message--info{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-color-info-light-8);--el-message-text-color:var(--el-color-info)}.el-message--info .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--info{color:var(--el-message-text-color)}.el-message--warning{--el-message-bg-color:var(--el-color-warning-light-9);--el-message-border-color:var(--el-color-warning-light-8);--el-message-text-color:var(--el-color-warning)}.el-message--warning .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--warning{color:var(--el-message-text-color)}.el-message--error{--el-message-bg-color:var(--el-color-error-light-9);--el-message-border-color:var(--el-color-error-light-8);--el-message-text-color:var(--el-color-error)}.el-message--error .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--error{color:var(--el-message-text-color)}.el-message .el-message__badge{position:absolute;top:-8px;right:-8px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__content:focus{outline-width:0}.el-message .el-message__closeBtn{cursor:pointer;color:var(--el-message-close-icon-color);font-size:var(--el-message-close-size)}.el-message .el-message__closeBtn:focus{outline-width:0}.el-message .el-message__closeBtn:hover{color:var(--el-message-close-hover-color)}.el-message-fade-enter-from,.el-message-fade-leave-to{opacity:0}.el-message-fade-enter-from.is-left,.el-message-fade-enter-from.is-right,.el-message-fade-leave-to.is-left,.el-message-fade-leave-to.is-right{transform:translateY(-100%)}.el-message-fade-enter-from.is-left.is-bottom,.el-message-fade-enter-from.is-right.is-bottom,.el-message-fade-leave-to.is-left.is-bottom,.el-message-fade-leave-to.is-right.is-bottom{transform:translateY(100%)}.el-message-fade-enter-from.is-center,.el-message-fade-leave-to.is-center{transform:translate(-50%,-100%)}.el-message-fade-enter-from.is-center.is-bottom,.el-message-fade-leave-to.is-center.is-bottom{transform:translate(-50%,100%)}.el-notification{--el-notification-width:330px;--el-notification-padding:14px 26px 14px 13px;--el-notification-radius:8px;--el-notification-shadow:var(--el-box-shadow-light);--el-notification-border-color:var(--el-border-color-lighter);--el-notification-icon-size:24px;--el-notification-close-font-size:var(--el-message-close-size,16px);--el-notification-group-margin-left:13px;--el-notification-group-margin-right:8px;--el-notification-content-font-size:var(--el-font-size-base);--el-notification-content-color:var(--el-text-color-regular);--el-notification-title-font-size:16px;--el-notification-title-color:var(--el-text-color-primary);--el-notification-close-color:var(--el-text-color-secondary);--el-notification-close-hover-color:var(--el-text-color-regular);width:var(--el-notification-width);padding:var(--el-notification-padding);border-radius:var(--el-notification-radius);box-sizing:border-box;border:1px solid var(--el-notification-border-color);background-color:var(--el-bg-color-overlay);box-shadow:var(--el-notification-shadow);transition:opacity var(--el-transition-duration),transform var(--el-transition-duration),left var(--el-transition-duration),right var(--el-transition-duration),top .4s,bottom var(--el-transition-duration);overflow-wrap:break-word;z-index:9999;display:flex;position:fixed;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{min-width:0;margin-left:var(--el-notification-group-margin-left);margin-right:var(--el-notification-group-margin-right);flex:1}.el-notification__title{font-weight:700;font-size:var(--el-notification-title-font-size);line-height:var(--el-notification-icon-size);color:var(--el-notification-title-color);margin:0}.el-notification__content{font-size:var(--el-notification-content-font-size);color:var(--el-notification-content-color);margin:6px 0 0;line-height:24px}.el-notification__content p{margin:0}.el-notification .el-notification__icon{height:var(--el-notification-icon-size);width:var(--el-notification-icon-size);font-size:var(--el-notification-icon-size);flex-shrink:0}.el-notification .el-notification__closeBtn{cursor:pointer;color:var(--el-notification-close-color);font-size:var(--el-notification-close-font-size);position:absolute;top:18px;right:15px}.el-notification .el-notification__closeBtn:hover{color:var(--el-notification-close-hover-color)}.el-notification .el-notification--primary{--el-notification-icon-color:var(--el-color-primary);color:var(--el-notification-icon-color)}.el-notification .el-notification--success{--el-notification-icon-color:var(--el-color-success);color:var(--el-notification-icon-color)}.el-notification .el-notification--info{--el-notification-icon-color:var(--el-color-info);color:var(--el-notification-icon-color)}.el-notification .el-notification--warning{--el-notification-icon-color:var(--el-color-warning);color:var(--el-notification-icon-color)}.el-notification .el-notification--error{--el-notification-icon-color:var(--el-color-error);color:var(--el-notification-icon-color)}.el-notification__progress{position:absolute;bottom:0;left:0;right:0}.el-notification__progress .el-progress-bar__inner{transition:width .1s linear}.el-notification-fade-enter-from.right{right:0;transform:translate(100%)}.el-notification-fade-enter-from.left{left:0;transform:translate(-100%)}.el-notification-fade-leave-to{opacity:0}.el-overlay{z-index:2000;background-color:var(--el-overlay-color-lighter);height:100%;position:fixed;top:0;bottom:0;left:0;right:0;overflow:auto}.el-overlay .el-overlay-root{height:0}.el-page-header.is-contentful .el-page-header__main{border-top:1px solid var(--el-border-color-light);margin-top:16px}.el-page-header__header{justify-content:space-between;align-items:center;line-height:24px;display:flex}.el-page-header__left{align-items:center;margin-right:40px;display:flex;position:relative}.el-page-header__back{cursor:pointer;align-items:center;display:flex}.el-page-header__left .el-divider--vertical{margin:0 16px}.el-page-header__icon{align-items:center;margin-right:10px;font-size:16px;display:flex}.el-page-header__icon .el-icon{font-size:inherit}.el-page-header__title{font-size:14px;font-weight:500}.el-page-header__content{color:var(--el-text-color-primary);font-size:18px}.el-page-header__breadcrumb{margin-bottom:16px}.el-pagination{--el-pagination-font-size:14px;--el-pagination-bg-color:var(--el-fill-color-blank);--el-pagination-text-color:var(--el-text-color-primary);--el-pagination-border-radius:2px;--el-pagination-button-color:var(--el-text-color-primary);--el-pagination-button-width:32px;--el-pagination-button-height:32px;--el-pagination-button-disabled-color:var(--el-text-color-placeholder);--el-pagination-button-disabled-bg-color:var(--el-fill-color-blank);--el-pagination-button-bg-color:var(--el-fill-color);--el-pagination-hover-color:var(--el-color-primary);--el-pagination-font-size-small:12px;--el-pagination-button-width-small:24px;--el-pagination-button-height-small:24px;--el-pagination-button-width-large:40px;--el-pagination-button-height-large:40px;--el-pagination-item-gap:16px;white-space:nowrap;color:var(--el-pagination-text-color);font-size:var(--el-pagination-font-size);align-items:center;font-weight:400;display:flex}.el-pagination .el-input__inner{text-align:center;-moz-appearance:textfield}.el-pagination .el-select{width:128px}.el-pagination .btn-prev,.el-pagination .btn-next{font-size:var(--el-pagination-font-size);min-width:var(--el-pagination-button-width);height:var(--el-pagination-button-height);line-height:var(--el-pagination-button-height);color:var(--el-pagination-button-color);background:var(--el-pagination-bg-color);border-radius:var(--el-pagination-border-radius);cursor:pointer;text-align:center;box-sizing:border-box;border:none;justify-content:center;align-items:center;padding:0 4px;display:flex}.el-pagination .btn-prev *,.el-pagination .btn-next *{pointer-events:none}.el-pagination .btn-prev:focus,.el-pagination .btn-next:focus{outline:none}.el-pagination .btn-prev:hover,.el-pagination .btn-next:hover{color:var(--el-pagination-hover-color)}.el-pagination .btn-prev.is-active,.el-pagination .btn-next.is-active{color:var(--el-pagination-hover-color);cursor:default;font-weight:700}.el-pagination .btn-prev.is-active.is-disabled,.el-pagination .btn-next.is-active.is-disabled{color:var(--el-text-color-secondary);font-weight:700}.el-pagination .btn-prev:disabled,.el-pagination .btn-prev.is-disabled,.el-pagination .btn-next:disabled,.el-pagination .btn-next.is-disabled{color:var(--el-pagination-button-disabled-color);background-color:var(--el-pagination-button-disabled-bg-color);cursor:not-allowed}.el-pagination .btn-prev:focus-visible{outline:1px solid var(--el-pagination-hover-color);outline-offset:-1px}.el-pagination .btn-next:focus-visible{outline:1px solid var(--el-pagination-hover-color);outline-offset:-1px}.el-pagination .btn-prev .el-icon,.el-pagination .btn-next .el-icon{width:inherit;font-size:12px;font-weight:700;display:block}.el-pagination>.is-first{margin-left:0!important}.el-pagination>.is-last{margin-right:0!important}.el-pagination .btn-prev{margin-left:var(--el-pagination-item-gap)}.el-pagination__sizes,.el-pagination__total{margin-left:var(--el-pagination-item-gap);color:var(--el-text-color-regular);font-weight:400}.el-pagination__total[disabled=true]{color:var(--el-text-color-placeholder)}.el-pagination__jump{margin-left:var(--el-pagination-item-gap);color:var(--el-text-color-regular);align-items:center;font-weight:400;display:flex}.el-pagination__jump[disabled=true]{color:var(--el-text-color-placeholder)}.el-pagination__goto{margin-right:8px}.el-pagination__editor{text-align:center;box-sizing:border-box}.el-pagination__editor.el-input{width:56px}.el-pagination__editor .el-input__inner::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.el-pagination__editor .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-pagination__classifier{margin-left:8px}.el-pagination__rightwrapper{flex:1;justify-content:flex-end;align-items:center;display:flex}.el-pagination.is-background .btn-prev,.el-pagination.is-background .btn-next,.el-pagination.is-background .el-pager li{background-color:var(--el-pagination-button-bg-color);margin:0 4px}.el-pagination.is-background .btn-prev.is-active,.el-pagination.is-background .btn-next.is-active,.el-pagination.is-background .el-pager li.is-active{background-color:var(--el-color-primary);color:var(--el-color-white)}.el-pagination.is-background .btn-prev:disabled,.el-pagination.is-background .btn-prev.is-disabled,.el-pagination.is-background .btn-next:disabled,.el-pagination.is-background .btn-next.is-disabled,.el-pagination.is-background .el-pager li:disabled,.el-pagination.is-background .el-pager li.is-disabled{color:var(--el-text-color-placeholder);background-color:var(--el-disabled-bg-color)}.el-pagination.is-background .btn-prev:disabled.is-active,.el-pagination.is-background .btn-prev.is-disabled.is-active,.el-pagination.is-background .btn-next:disabled.is-active,.el-pagination.is-background .btn-next.is-disabled.is-active,.el-pagination.is-background .el-pager li:disabled.is-active,.el-pagination.is-background .el-pager li.is-disabled.is-active{color:var(--el-text-color-secondary);background-color:var(--el-fill-color-dark)}.el-pagination.is-background .btn-prev{margin-left:var(--el-pagination-item-gap)}.el-pagination--small .btn-prev,.el-pagination--small .btn-next,.el-pagination--small .el-pager li{height:var(--el-pagination-button-height-small);line-height:var(--el-pagination-button-height-small);font-size:var(--el-pagination-font-size-small);min-width:var(--el-pagination-button-width-small)}.el-pagination--small span:not([class*=suffix]),.el-pagination--small button{font-size:var(--el-pagination-font-size-small)}.el-pagination--small .el-select{width:100px}.el-pagination--large .btn-prev,.el-pagination--large .btn-next,.el-pagination--large .el-pager li{height:var(--el-pagination-button-height-large);line-height:var(--el-pagination-button-height-large);min-width:var(--el-pagination-button-width-large)}.el-pagination--large .el-select .el-input{width:160px}.el-pager{-webkit-user-select:none;user-select:none;align-items:center;margin:0;padding:0;font-size:0;list-style:none;display:flex}.el-pager li{font-size:var(--el-pagination-font-size);min-width:var(--el-pagination-button-width);height:var(--el-pagination-button-height);line-height:var(--el-pagination-button-height);color:var(--el-pagination-button-color);background:var(--el-pagination-bg-color);border-radius:var(--el-pagination-border-radius);cursor:pointer;text-align:center;box-sizing:border-box;border:none;justify-content:center;align-items:center;padding:0 4px;display:flex}.el-pager li *{pointer-events:none}.el-pager li:focus{outline:none}.el-pager li:hover{color:var(--el-pagination-hover-color)}.el-pager li.is-active{color:var(--el-pagination-hover-color);cursor:default;font-weight:700}.el-pager li.is-active.is-disabled{color:var(--el-text-color-secondary);font-weight:700}.el-pager li:disabled,.el-pager li.is-disabled{color:var(--el-pagination-button-disabled-color);background-color:var(--el-pagination-button-disabled-bg-color);cursor:not-allowed}.el-pager li:focus-visible{outline:1px solid var(--el-pagination-hover-color);outline-offset:-1px}.el-popconfirm{outline:none}.el-popconfirm__main{align-items:center;display:flex}.el-popconfirm__icon{margin-right:5px}.el-popconfirm__action{text-align:right;margin-top:8px}.el-popover{--el-popover-bg-color:var(--el-bg-color-overlay);--el-popover-font-size:var(--el-font-size-base);--el-popover-border-color:var(--el-border-color-lighter);--el-popover-padding:12px;--el-popover-padding-large:18px 20px;--el-popover-title-font-size:16px;--el-popover-title-text-color:var(--el-text-color-primary);--el-popover-border-radius:4px}.el-popover.el-popper{background:var(--el-popover-bg-color);border-radius:var(--el-popover-border-radius);border:1px solid var(--el-popover-border-color);min-width:150px;padding:var(--el-popover-padding);z-index:var(--el-index-popper);color:var(--el-text-color-regular);line-height:1.4;font-size:var(--el-popover-font-size);box-shadow:var(--el-box-shadow-light);overflow-wrap:break-word;box-sizing:border-box}.el-popover.el-popper--plain{padding:var(--el-popover-padding-large)}.el-popover__title{color:var(--el-popover-title-text-color);font-size:var(--el-popover-title-font-size);margin-bottom:12px;line-height:1}.el-popover__reference:focus:not(.focusing),.el-popover__reference:focus:hover{outline-width:0}.el-popover.el-popper.is-dark{--el-popover-bg-color:var(--el-text-color-primary);--el-popover-border-color:var(--el-text-color-primary);--el-popover-title-text-color:var(--el-bg-color);color:var(--el-bg-color)}.el-popover.el-popper:focus:active,.el-popover.el-popper:focus{outline-width:0}.el-progress{align-items:center;line-height:1;display:flex;position:relative}.el-progress__text{color:var(--el-text-color-regular);min-width:50px;margin-left:5px;font-size:14px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle,.el-progress--dashboard{display:inline-block}.el-progress--circle .el-progress__text,.el-progress--dashboard .el-progress__text{text-align:center;width:100%;margin:0;position:absolute;top:50%;left:0;transform:translateY(-50%)}.el-progress--circle .el-progress__text i,.el-progress--dashboard .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{margin-right:0;padding-right:0;display:block}.el-progress--text-inside .el-progress-bar{margin-right:0;padding-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:var(--el-color-success)}.el-progress.is-success .el-progress__text{color:var(--el-color-success)}.el-progress.is-warning .el-progress-bar__inner{background-color:var(--el-color-warning)}.el-progress.is-warning .el-progress__text{color:var(--el-color-warning)}.el-progress.is-exception .el-progress-bar__inner{background-color:var(--el-color-danger)}.el-progress.is-exception .el-progress__text{color:var(--el-color-danger)}.el-progress-bar{box-sizing:border-box;flex-grow:1}.el-progress-bar__outer{background-color:var(--el-border-color-lighter);vertical-align:middle;border-radius:100px;height:6px;position:relative;overflow:hidden}.el-progress-bar__inner{background-color:var(--el-color-primary);text-align:right;white-space:nowrap;border-radius:100px;height:100%;line-height:1;transition:width .6s;position:absolute;top:0;left:0}.el-progress-bar__inner:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-progress-bar__inner--indeterminate{animation:3s infinite indeterminate;transform:translateZ(0)}.el-progress-bar__inner--striped{background-image:linear-gradient(45deg,#0000001a 25%,#0000 25%,#0000 50%,#0000001a 50%,#0000001a 75%,#0000 75%,#0000);background-size:1.25em 1.25em}.el-progress-bar__inner--striped.el-progress-bar__inner--striped-flow{animation:3s linear infinite striped-flow}.el-progress-bar__innerText{vertical-align:middle;color:#fff;margin:0 5px;font-size:12px;display:inline-block}@keyframes progress{0%{background-position:0 0}to{background-position:32px 0}}@keyframes indeterminate{0%{left:-100%}to{left:100%}}@keyframes striped-flow{0%{background-position:-100%}to{background-position:100%}}.el-radio-button{--el-radio-button-checked-bg-color:var(--el-color-primary);--el-radio-button-checked-text-color:var(--el-color-white);--el-radio-button-checked-border-color:var(--el-color-primary);--el-radio-button-disabled-checked-fill:var(--el-border-color-extra-light);outline:none;display:inline-block;position:relative}.el-radio-button__inner{white-space:nowrap;vertical-align:middle;background:var(--el-button-bg-color,var(--el-fill-color-blank));outline:var(--el-border);line-height:1;font-weight:var(--el-button-font-weight,var(--el-font-weight-primary));color:var(--el-button-text-color,var(--el-text-color-regular));-webkit-appearance:none;text-align:center;box-sizing:border-box;cursor:pointer;transition:var(--el-transition-all);-webkit-user-select:none;user-select:none;font-size:var(--el-font-size-base);border-radius:0;margin:0;padding:8px 15px;display:inline-block;position:relative}.el-radio-button__inner.is-round{padding:8px 15px}.el-radio-button__inner:hover{color:var(--el-color-primary)}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-radius:var(--el-border-radius-base) 0 0 var(--el-border-radius-base);box-shadow:none!important}.el-radio-button.is-active .el-radio-button__original-radio:not(:disabled)+.el-radio-button__inner{color:var(--el-radio-button-checked-text-color,var(--el-color-white));background-color:var(--el-radio-button-checked-bg-color,var(--el-color-primary));border-color:var(--el-radio-button-checked-border-color,var(--el-color-primary));box-shadow:-1px 0 0 0 var(--el-radio-button-checked-border-color,var(--el-color-primary))}.el-radio-button__original-radio{opacity:0;z-index:-1;outline:none;position:absolute}.el-radio-button__original-radio:focus-visible+.el-radio-button__inner{border-left:var(--el-border);border-left-color:var(--el-radio-button-checked-border-color,var(--el-color-primary));outline:2px solid var(--el-radio-button-checked-border-color);outline-offset:1px;z-index:2;border-radius:var(--el-border-radius-base);box-shadow:none}.el-radio-button__original-radio:disabled+.el-radio-button__inner{color:var(--el-disabled-text-color);cursor:not-allowed;background-image:none;background-color:var(--el-button-disabled-bg-color,var(--el-fill-color-blank));border-color:var(--el-button-disabled-border-color,var(--el-border-color-light));box-shadow:none}.el-radio-button__original-radio:disabled:checked+.el-radio-button__inner{background-color:var(--el-radio-button-disabled-checked-fill)}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 var(--el-border-radius-base) var(--el-border-radius-base) 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:var(--el-border-radius-base)}.el-radio-button--large .el-radio-button__inner{font-size:var(--el-font-size-base);border-radius:0;padding:12px 19px}.el-radio-button--large .el-radio-button__inner.is-round{padding:12px 19px}.el-radio-button--small .el-radio-button__inner{border-radius:0;padding:5px 11px;font-size:12px}.el-radio-button--small .el-radio-button__inner.is-round{padding:5px 11px}.el-radio-group{flex-wrap:wrap;align-items:center;font-size:0;display:inline-flex}.el-radio{--el-radio-font-size:var(--el-font-size-base);--el-radio-text-color:var(--el-text-color-regular);--el-radio-font-weight:var(--el-font-weight-primary);--el-radio-input-height:14px;--el-radio-input-width:14px;--el-radio-input-border-radius:var(--el-border-radius-circle);--el-radio-input-bg-color:var(--el-fill-color-blank);--el-radio-input-border:var(--el-border);--el-radio-input-border-color:var(--el-border-color);--el-radio-input-border-color-hover:var(--el-color-primary);color:var(--el-radio-text-color);font-weight:var(--el-radio-font-weight);cursor:pointer;white-space:nowrap;font-size:var(--el-font-size-base);-webkit-user-select:none;user-select:none;outline:none;align-items:center;height:32px;margin-right:30px;display:inline-flex;position:relative}.el-radio.el-radio--large{height:40px}.el-radio.el-radio--small{height:24px}.el-radio.is-bordered{border-radius:var(--el-border-radius-base);border:var(--el-border);box-sizing:border-box;padding:0 15px 0 9px}.el-radio.is-bordered.is-checked{border-color:var(--el-color-primary)}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:var(--el-border-color-lighter)}.el-radio.is-bordered.el-radio--large{border-radius:var(--el-border-radius-base);padding:0 19px 0 11px}.el-radio.is-bordered.el-radio--large .el-radio__label{font-size:var(--el-font-size-base)}.el-radio.is-bordered.el-radio--large .el-radio__inner{width:14px;height:14px}.el-radio.is-bordered.el-radio--small{border-radius:var(--el-border-radius-base);padding:0 11px 0 7px}.el-radio.is-bordered.el-radio--small .el-radio__label{font-size:12px}.el-radio.is-bordered.el-radio--small .el-radio__inner{width:12px;height:12px}.el-radio:last-child{margin-right:0}.el-radio__input{white-space:nowrap;cursor:pointer;vertical-align:middle;outline:none;display:inline-flex;position:relative}.el-radio__input.is-disabled .el-radio__inner{background-color:var(--el-disabled-bg-color);border-color:var(--el-disabled-border-color);cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner:after{cursor:not-allowed;background-color:var(--el-disabled-bg-color)}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:var(--el-disabled-bg-color);border-color:var(--el-disabled-border-color)}.el-radio__input.is-disabled.is-checked .el-radio__inner:after{background-color:var(--el-text-color-placeholder)}.el-radio__input.is-disabled+span.el-radio__label{color:var(--el-text-color-placeholder);cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:var(--el-color-primary);background:var(--el-color-primary)}.el-radio__input.is-checked .el-radio__inner:after{background-color:var(--el-color-white);transform:translate(-50%,-50%)scale(1)}.el-radio__input.is-checked+.el-radio__label{color:var(--el-color-primary)}.el-radio__input.is-focus .el-radio__inner{border-color:var(--el-radio-input-border-color-hover)}.el-radio__inner{border:var(--el-radio-input-border);border-radius:var(--el-radio-input-border-radius);width:var(--el-radio-input-width);height:var(--el-radio-input-height);background-color:var(--el-radio-input-bg-color);cursor:pointer;box-sizing:border-box;transition:all .3s;display:inline-block;position:relative}.el-radio__inner:hover{border-color:var(--el-radio-input-border-color-hover)}.el-radio__inner:after{border-radius:var(--el-radio-input-border-radius);content:"";width:4px;height:4px;transition:transform .15s ease-in;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)scale(0)}.el-radio__original{opacity:0;z-index:-1;outline:none;margin:0;position:absolute;top:0;bottom:0;left:0;right:0}.el-radio__original:focus-visible+.el-radio__inner{outline:2px solid var(--el-radio-input-border-color-hover);outline-offset:1px;border-radius:var(--el-radio-input-border-radius)}.el-radio:focus:not(:focus-visible):not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{box-shadow:0 0 2px 2px var(--el-radio-input-border-color-hover)}.el-radio__label{font-size:var(--el-radio-font-size);padding-left:8px}.el-radio.el-radio--large .el-radio__label{font-size:14px}.el-radio.el-radio--large .el-radio__inner{width:14px;height:14px}.el-radio.el-radio--small .el-radio__label{font-size:12px}.el-radio.el-radio--small .el-radio__inner{width:12px;height:12px}.el-rate{--el-rate-height:20px;--el-rate-font-size:var(--el-font-size-base);--el-rate-icon-size:18px;--el-rate-icon-margin:6px;--el-rate-void-color:var(--el-border-color-darker);--el-rate-fill-color:#f7ba2a;--el-rate-disabled-void-color:var(--el-fill-color);--el-rate-text-color:var(--el-text-color-primary);--el-rate-outline-color:var(--el-color-primary-light-5);align-items:center;height:32px;display:inline-flex}.el-rate:focus,.el-rate:active{outline:none}.el-rate:focus-visible .el-rate__item .el-rate__icon.is-focus-visible{outline:2px solid var(--el-rate-outline-color);transition:outline-offset,outline}.el-rate__item{cursor:pointer;vertical-align:middle;color:var(--el-rate-void-color);font-size:0;line-height:normal;display:inline-block;position:relative}.el-rate .el-rate__icon{font-size:var(--el-rate-icon-size);margin-right:var(--el-rate-icon-margin);transition:var(--el-transition-duration);display:inline-block;position:relative}.el-rate .el-rate__icon.hover{transform:scale(1.15)}.el-rate .el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate .el-rate__icon.is-active{color:var(--el-rate-fill-color)}.el-rate__decimal{color:var(--el-rate-fill-color);display:inline-block;position:absolute;top:0;left:0;overflow:hidden}.el-rate__decimal--box{position:absolute;top:0;left:0}.el-rate__text{font-size:var(--el-rate-font-size);vertical-align:middle;color:var(--el-rate-text-color)}.el-rate--large{height:40px}.el-rate--small{height:24px}.el-rate--small .el-rate__icon{font-size:14px}.el-rate.is-disabled .el-rate__item{cursor:not-allowed;color:var(--el-rate-disabled-void-color)}.el-result{--el-result-padding:40px 30px;--el-result-icon-font-size:64px;--el-result-title-font-size:20px;--el-result-title-margin-top:20px;--el-result-subtitle-margin-top:10px;--el-result-extra-margin-top:30px;text-align:center;box-sizing:border-box;padding:var(--el-result-padding);flex-direction:column;justify-content:center;align-items:center;display:flex}.el-result__icon svg{width:var(--el-result-icon-font-size);height:var(--el-result-icon-font-size)}.el-result__title{margin-top:var(--el-result-title-margin-top)}.el-result__title p{font-size:var(--el-result-title-font-size);color:var(--el-text-color-primary);margin:0;line-height:1.3}.el-result__subtitle{margin-top:var(--el-result-subtitle-margin-top)}.el-result__subtitle p{font-size:var(--el-font-size-base);color:var(--el-text-color-regular);margin:0;line-height:1.3}.el-result__extra{margin-top:var(--el-result-extra-margin-top)}.el-result .icon-primary{--el-result-color:var(--el-color-primary);color:var(--el-result-color)}.el-result .icon-success{--el-result-color:var(--el-color-success);color:var(--el-result-color)}.el-result .icon-warning{--el-result-color:var(--el-color-warning);color:var(--el-result-color)}.el-result .icon-danger{--el-result-color:var(--el-color-danger);color:var(--el-result-color)}.el-result .icon-error{--el-result-color:var(--el-color-error);color:var(--el-result-color)}.el-result .icon-info{--el-result-color:var(--el-color-info);color:var(--el-result-color)}.el-row{box-sizing:border-box;flex-wrap:wrap;display:flex;position:relative}.el-row.is-justify-center{justify-content:center}.el-row.is-justify-end{justify-content:flex-end}.el-row.is-justify-space-between{justify-content:space-between}.el-row.is-justify-space-around{justify-content:space-around}.el-row.is-justify-space-evenly{justify-content:space-evenly}.el-row.is-align-top{align-items:flex-start}.el-row.is-align-middle{align-items:center}.el-row.is-align-bottom{align-items:flex-end}.el-scrollbar{--el-scrollbar-opacity:.3;--el-scrollbar-bg-color:var(--el-text-color-secondary);--el-scrollbar-hover-opacity:.5;--el-scrollbar-hover-bg-color:var(--el-text-color-secondary);height:100%;position:relative;overflow:hidden}.el-scrollbar__wrap{height:100%;overflow:auto}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{display:none}.el-scrollbar__thumb{cursor:pointer;border-radius:inherit;background-color:var(--el-scrollbar-bg-color,var(--el-text-color-secondary));width:0;height:0;transition:var(--el-transition-duration) background-color;opacity:var(--el-scrollbar-opacity,.3);display:block;position:relative}.el-scrollbar__thumb:hover{background-color:var(--el-scrollbar-hover-bg-color,var(--el-text-color-secondary));opacity:var(--el-scrollbar-hover-opacity,.5)}.el-scrollbar__bar{z-index:1;border-radius:4px;position:absolute;bottom:2px;right:2px}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-scrollbar-fade-enter-active{transition:opacity .34s ease-out}.el-scrollbar-fade-leave-active{transition:opacity .12s ease-out}.el-scrollbar-fade-enter-from,.el-scrollbar-fade-leave-active{opacity:0}.el-select-dropdown{z-index:calc(var(--el-index-top) + 1);border-radius:var(--el-border-radius-base);box-sizing:border-box}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__loading,.el-select-dropdown__empty{text-align:center;color:var(--el-text-color-secondary);font-size:var(--el-select-font-size);margin:0;padding:10px 0}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{box-sizing:border-box;margin:0;padding:6px 0;list-style:none}.el-select-dropdown__list.el-vl__window{margin:6px 0;padding:0}.el-select-dropdown__header{border-bottom:1px solid var(--el-border-color-light);padding:10px}.el-select-dropdown__footer{border-top:1px solid var(--el-border-color-light);padding:10px}.el-select-dropdown__item{font-size:var(--el-font-size-base);white-space:nowrap;text-overflow:ellipsis;color:var(--el-text-color-regular);box-sizing:border-box;cursor:pointer;height:34px;padding:0 32px 0 20px;line-height:34px;position:relative;overflow:hidden}.el-select-dropdown__item.is-hovering{background-color:var(--el-fill-color-light)}.el-select-dropdown__item.is-selected{color:var(--el-color-primary);font-weight:700}.el-select-dropdown__item.is-disabled{color:var(--el-text-color-placeholder);cursor:not-allowed;background-color:unset}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-selected:after{content:"";background-position:50%;background-repeat:no-repeat;background-color:var(--el-color-primary);-webkit-mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") 0 0/100% 100% no-repeat;mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") 0 0/100% 100% no-repeat;border-top:none;border-right:none;width:12px;height:12px;position:absolute;top:50%;right:20px;transform:translateY(-50%);-webkit-mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") 0 0/100% 100% no-repeat}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-disabled:after{background-color:var(--el-text-color-placeholder)}.el-select-group{margin:0;padding:0}.el-select-group__wrap{margin:0;padding:0;list-style:none;position:relative}.el-select-group__title{box-sizing:border-box;color:var(--el-color-info);text-overflow:ellipsis;white-space:nowrap;padding:0 20px;font-size:12px;line-height:34px;overflow:hidden}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{--el-select-border-color-hover:var(--el-border-color-hover);--el-select-disabled-color:var(--el-disabled-text-color);--el-select-disabled-border:var(--el-disabled-border-color);--el-select-font-size:var(--el-font-size-base);--el-select-close-hover-color:var(--el-text-color-secondary);--el-select-input-color:var(--el-text-color-placeholder);--el-select-multiple-input-color:var(--el-text-color-regular);--el-select-input-focus-border-color:var(--el-color-primary);--el-select-input-font-size:14px;--el-select-width:100%;vertical-align:middle;width:var(--el-select-width);display:inline-block;position:relative}.el-select__wrapper{box-sizing:border-box;cursor:pointer;text-align:left;border-radius:var(--el-border-radius-base);background-color:var(--el-fill-color-blank);min-height:32px;transition:var(--el-transition-duration);box-shadow:0 0 0 1px var(--el-border-color) inset;align-items:center;gap:6px;padding:4px 12px;font-size:14px;line-height:24px;display:flex;position:relative;transform:translateZ(0)}.el-select__wrapper.is-filterable{cursor:text}.el-select__wrapper.is-focused{box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-select__wrapper.is-hovering:not(.is-focused){box-shadow:0 0 0 1px var(--el-border-color-hover) inset}.el-select__wrapper.is-disabled{cursor:not-allowed;background-color:var(--el-fill-color-light);color:var(--el-text-color-placeholder);box-shadow:0 0 0 1px var(--el-select-disabled-border) inset}.el-select__wrapper.is-disabled:hover{box-shadow:0 0 0 1px var(--el-select-disabled-border) inset}.el-select__wrapper.is-disabled.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-select__wrapper.is-disabled .el-select__selected-item{color:var(--el-select-disabled-color)}.el-select__wrapper.is-disabled .el-select__caret,.el-select__wrapper.is-disabled .el-tag,.el-select__wrapper.is-disabled input{cursor:not-allowed}.el-select__wrapper.is-disabled .el-select__prefix,.el-select__wrapper.is-disabled .el-select__suffix{pointer-events:none}.el-select__prefix,.el-select__suffix{color:var(--el-input-icon-color,var(--el-text-color-placeholder));flex-shrink:0;align-items:center;gap:6px;display:flex}.el-select__caret{color:var(--el-select-input-color);font-size:var(--el-select-input-font-size);transition:var(--el-transition-duration);cursor:pointer;transform:rotate(0)}.el-select__caret.is-reverse{transform:rotate(180deg)}.el-select__clear{cursor:pointer}.el-select__clear:hover{color:var(--el-select-close-hover-color)}.el-select__selection{flex-wrap:wrap;flex:1;align-items:center;gap:6px;min-width:0;display:flex;position:relative}.el-select__selection.is-near{margin-left:-8px}.el-select__selection .el-tag{cursor:pointer;border-color:#0000}.el-select__selection .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-select__selection .el-tag .el-tag__content{min-width:0}.el-select__selected-item{-webkit-user-select:none;user-select:none;flex-wrap:wrap;display:flex}.el-select__tags-text{text-overflow:ellipsis;white-space:nowrap;line-height:normal;display:block;overflow:hidden}.el-select__placeholder{z-index:-1;text-overflow:ellipsis;white-space:nowrap;width:100%;color:var(--el-input-text-color,var(--el-text-color-regular));display:block;position:absolute;top:50%;overflow:hidden;transform:translateY(-50%)}.el-select__placeholder.is-transparent{-webkit-user-select:none;user-select:none;color:var(--el-text-color-placeholder)}.el-select__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-box-shadow-light)}.el-select__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-select__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-select__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-select__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-select__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-select__input-wrapper{flex:1}.el-select__input-wrapper.is-hidden{opacity:0;z-index:-1;position:absolute}.el-select__input{color:var(--el-select-multiple-input-color);font-size:inherit;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#0000;border:none;outline:none;width:100%;height:24px;padding:0;font-family:inherit}.el-select__input-calculator{visibility:hidden;white-space:pre;max-width:100%;position:absolute;top:0;left:0;overflow:hidden}.el-select--large .el-select__wrapper{gap:6px;min-height:40px;padding:8px 16px;font-size:14px;line-height:24px}.el-select--large .el-select__selection{gap:6px}.el-select--large .el-select__selection.is-near{margin-left:-8px}.el-select--large .el-select__prefix,.el-select--large .el-select__suffix{gap:6px}.el-select--large .el-select__input{height:24px}.el-select--small .el-select__wrapper{gap:4px;min-height:24px;padding:2px 8px;font-size:12px;line-height:20px}.el-select--small .el-select__selection{gap:4px}.el-select--small .el-select__selection.is-near{margin-left:-6px}.el-select--small .el-select__prefix,.el-select--small .el-select__suffix{gap:4px}.el-select--small .el-select__input{height:20px}.el-skeleton{--el-skeleton-circle-size:var(--el-avatar-size)}.el-skeleton__item{background:var(--el-skeleton-color);border-radius:var(--el-border-radius-base);width:100%;height:16px;display:inline-block}.el-skeleton__circle{width:var(--el-skeleton-circle-size);height:var(--el-skeleton-circle-size);line-height:var(--el-skeleton-circle-size);border-radius:50%}.el-skeleton__button{border-radius:4px;width:64px;height:40px}.el-skeleton__p{width:100%}.el-skeleton__p.is-last{width:61%}.el-skeleton__p.is-first{width:33%}.el-skeleton__text{width:100%;height:var(--el-font-size-small)}.el-skeleton__caption{height:var(--el-font-size-extra-small)}.el-skeleton__h1{height:var(--el-font-size-extra-large)}.el-skeleton__h3{height:var(--el-font-size-large)}.el-skeleton__h5{height:var(--el-font-size-medium)}.el-skeleton__image{width:unset;border-radius:0;justify-content:center;align-items:center;display:flex}.el-skeleton__image svg{color:var(--el-svg-monochrome-grey);fill:currentColor;width:22%;height:22%}.el-skeleton{--el-skeleton-color:var(--el-fill-color);--el-skeleton-to-color:var(--el-fill-color-darker)}@keyframes el-skeleton-loading{0%{background-position:100%}to{background-position:0}}.el-skeleton{width:100%}.el-skeleton__first-line,.el-skeleton__paragraph{background:var(--el-skeleton-color);height:16px;margin-top:16px}.el-skeleton.is-animated .el-skeleton__item{background:linear-gradient(90deg,var(--el-skeleton-color) 25%,var(--el-skeleton-to-color) 37%,var(--el-skeleton-color) 63%);background-size:400% 100%;animation:1.4s infinite el-skeleton-loading}.el-slider{--el-slider-main-bg-color:var(--el-color-primary);--el-slider-runway-bg-color:var(--el-border-color-light);--el-slider-stop-bg-color:var(--el-color-white);--el-slider-disabled-color:var(--el-text-color-placeholder);--el-slider-border-radius:3px;--el-slider-height:6px;--el-slider-button-size:20px;--el-slider-button-wrapper-size:36px;--el-slider-button-wrapper-offset:-15px;align-items:center;width:100%;height:32px;display:flex}.el-slider__runway{height:var(--el-slider-height);background-color:var(--el-slider-runway-bg-color);border-radius:var(--el-slider-border-radius);cursor:pointer;flex:1;position:relative}.el-slider__runway.show-input{width:auto;margin-right:30px}.el-slider__runway.is-disabled{cursor:default}.el-slider__runway.is-disabled .el-slider__bar{background-color:var(--el-slider-disabled-color)}.el-slider__runway.is-disabled .el-slider__button{border-color:var(--el-slider-disabled-color)}.el-slider__runway.is-disabled .el-slider__button-wrapper:hover,.el-slider__runway.is-disabled .el-slider__button-wrapper.hover,.el-slider__runway.is-disabled .el-slider__button-wrapper.dragging{cursor:not-allowed}.el-slider__runway.is-disabled .el-slider__button:hover,.el-slider__runway.is-disabled .el-slider__button.hover,.el-slider__runway.is-disabled .el-slider__button.dragging{cursor:not-allowed;transform:scale(1)}.el-slider__input{flex-shrink:0;width:130px}.el-slider__bar{height:var(--el-slider-height);background-color:var(--el-slider-main-bg-color);border-top-left-radius:var(--el-slider-border-radius);border-bottom-left-radius:var(--el-slider-border-radius);position:absolute}.el-slider__button-wrapper{height:var(--el-slider-button-wrapper-size);width:var(--el-slider-button-wrapper-size);z-index:1;top:var(--el-slider-button-wrapper-offset);text-align:center;-webkit-user-select:none;user-select:none;background-color:#0000;outline:none;line-height:normal;position:absolute;transform:translate(-50%)}.el-slider__button-wrapper:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-slider__button-wrapper:hover,.el-slider__button-wrapper.hover{cursor:grab}.el-slider__button-wrapper.dragging{cursor:grabbing}.el-slider__button{width:var(--el-slider-button-size);height:var(--el-slider-button-size);vertical-align:middle;border:solid 2px var(--el-slider-main-bg-color);background-color:var(--el-color-white);box-sizing:border-box;transition:var(--el-transition-duration-fast);-webkit-user-select:none;user-select:none;border-radius:50%;display:inline-block}.el-slider__button:hover,.el-slider__button.hover,.el-slider__button.dragging{transform:scale(1.2)}.el-slider__button:hover,.el-slider__button.hover{cursor:grab}.el-slider__button.dragging{cursor:grabbing}.el-slider__stop{height:var(--el-slider-height);width:var(--el-slider-height);border-radius:var(--el-border-radius-circle);background-color:var(--el-slider-stop-bg-color);position:absolute;transform:translate(-50%)}.el-slider__marks{width:18px;height:100%;top:0;left:12px}.el-slider__marks-text{color:var(--el-color-info);white-space:pre;margin-top:15px;font-size:14px;position:absolute;transform:translate(-50%)}.el-slider.is-vertical{flex:0;width:auto;height:100%;display:inline-flex;position:relative}.el-slider.is-vertical .el-slider__runway{width:var(--el-slider-height);height:100%;margin:0 16px}.el-slider.is-vertical .el-slider__bar{width:var(--el-slider-height);border-radius:0 0 3px 3px;height:auto}.el-slider.is-vertical .el-slider__button-wrapper{top:auto;left:var(--el-slider-button-wrapper-offset);transform:translateY(50%)}.el-slider.is-vertical .el-slider__stop{transform:translateY(50%)}.el-slider.is-vertical .el-slider__marks-text{margin-top:0;left:15px;transform:translateY(50%)}.el-slider--large{height:40px}.el-slider--small{height:24px}.el-space{vertical-align:top;display:inline-flex}.el-space__item{flex-wrap:wrap;display:flex}.el-space__item>*{flex:1}.el-space--vertical{flex-direction:column}.el-time-spinner{white-space:nowrap;width:100%}.el-spinner{vertical-align:middle;display:inline-block}.el-spinner-inner{width:50px;height:50px;animation:2s linear infinite rotate}.el-spinner-inner .path{stroke:var(--el-border-color-lighter);stroke-linecap:round;animation:1.5s ease-in-out infinite dash}@keyframes rotate{to{transform:rotate(360deg)}}@keyframes dash{0%{stroke-dasharray:1 150;stroke-dashoffset:0}50%{stroke-dasharray:90 150;stroke-dashoffset:-35px}to{stroke-dasharray:90 150;stroke-dashoffset:-124px}}.el-step{flex-shrink:1;position:relative}.el-step:last-of-type .el-step__line{display:none}.el-step:last-of-type.is-flex{flex-grow:0;flex-shrink:0;flex-basis:auto!important}.el-step:last-of-type .el-step__main,.el-step:last-of-type .el-step__description{padding-right:0}.el-step__head{width:100%;position:relative}.el-step__head.is-process{color:var(--el-text-color-primary);border-color:var(--el-text-color-primary)}.el-step__head.is-wait{color:var(--el-text-color-placeholder);border-color:var(--el-text-color-placeholder)}.el-step__head.is-success{color:var(--el-color-success);border-color:var(--el-color-success)}.el-step__head.is-error{color:var(--el-color-danger);border-color:var(--el-color-danger)}.el-step__head.is-finish{color:var(--el-color-primary);border-color:var(--el-color-primary)}.el-step__icon{z-index:1;box-sizing:border-box;background:var(--el-bg-color);justify-content:center;align-items:center;width:24px;height:24px;font-size:14px;transition:all .15s ease-out;display:inline-flex;position:relative}.el-step__icon.is-text{border:2px solid;border-radius:50%}.el-step__icon.is-icon{width:40px}.el-step__icon-inner{-webkit-user-select:none;user-select:none;text-align:center;color:inherit;font-weight:700;line-height:1;display:inline-block}.el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:25px;font-weight:400}.el-step__icon-inner.is-status{transform:translateY(1px)}.el-step__line{background-color:var(--el-text-color-placeholder);border-color:currentColor;position:absolute}.el-step__line-inner{box-sizing:border-box;border:1px solid;width:0;height:0;transition:all .15s ease-out;display:block}.el-step__main{white-space:normal;text-align:left}.el-step__title{font-size:16px;line-height:38px}.el-step__title.is-process{color:var(--el-text-color-primary);font-weight:700}.el-step__title.is-wait{color:var(--el-text-color-placeholder)}.el-step__title.is-success{color:var(--el-color-success)}.el-step__title.is-error{color:var(--el-color-danger)}.el-step__title.is-finish{color:var(--el-color-primary)}.el-step__description{margin-top:-5px;padding-right:10%;font-size:12px;font-weight:400;line-height:20px}.el-step__description.is-process{color:var(--el-text-color-primary)}.el-step__description.is-wait{color:var(--el-text-color-placeholder)}.el-step__description.is-success{color:var(--el-color-success)}.el-step__description.is-error{color:var(--el-color-danger)}.el-step__description.is-finish{color:var(--el-color-primary)}.el-step.is-horizontal{display:inline-block}.el-step.is-horizontal .el-step__line{height:2px;top:11px;left:0;right:0}.el-step.is-vertical{display:flex}.el-step.is-vertical .el-step__head{flex-grow:0;width:24px}.el-step.is-vertical .el-step__main{flex-grow:1;padding-left:10px}.el-step.is-vertical .el-step__title{padding-bottom:8px;line-height:24px}.el-step.is-vertical .el-step__line{width:2px;top:0;bottom:0;left:11px}.el-step.is-vertical .el-step__icon.is-icon{width:24px}.el-step.is-vertical .el-step__description{padding-right:0}.el-step.is-center .el-step__head,.el-step.is-center .el-step__main{text-align:center}.el-step.is-center .el-step__description{padding-left:20%;padding-right:20%}.el-step.is-center .el-step__line{left:50%;right:-50%}.el-step.is-simple{align-items:center;display:flex}.el-step.is-simple .el-step__head{width:auto;padding-right:10px;font-size:0}.el-step.is-simple .el-step__icon{background:0 0;width:16px;height:16px;font-size:12px}.el-step.is-simple .el-step__icon-inner[class*=el-icon]:not(.is-status){font-size:18px}.el-step.is-simple .el-step__icon-inner.is-status{transform:scale(.8)translateY(1px)}.el-step.is-simple .el-step__main{flex-grow:1;align-items:stretch;display:flex;position:relative}.el-step.is-simple .el-step__title{font-size:16px;line-height:20px}.el-step.is-simple:not(:last-of-type) .el-step__title{overflow-wrap:break-word;max-width:50%}.el-step.is-simple .el-step__arrow{flex-grow:1;justify-content:center;align-items:center;display:flex}.el-step.is-simple .el-step__arrow:before,.el-step.is-simple .el-step__arrow:after{content:"";background:var(--el-text-color-placeholder);width:1px;height:15px;display:inline-block;position:absolute}.el-step.is-simple .el-step__arrow:before{transform-origin:0 0;transform:rotate(-45deg)translateY(-4px)}.el-step.is-simple .el-step__arrow:after{transform-origin:100% 100%;transform:rotate(45deg)translateY(4px)}.el-step.is-simple:last-of-type .el-step__arrow{display:none}.el-steps{line-height:normal;display:flex}.el-steps--simple{background:var(--el-fill-color-light);border-radius:4px;padding:13px 8%}.el-steps--horizontal{white-space:nowrap}.el-steps--vertical{flex-flow:column;height:100%}.el-switch{--el-switch-on-color:var(--el-color-primary);--el-switch-off-color:var(--el-border-color);vertical-align:middle;align-items:center;height:32px;font-size:14px;line-height:20px;display:inline-flex;position:relative}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{transition:var(--el-transition-duration-fast);cursor:pointer;vertical-align:middle;height:20px;color:var(--el-text-color-primary);font-size:14px;font-weight:500;display:inline-block}.el-switch__label.is-active{color:var(--el-color-primary)}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{font-size:14px;line-height:1;display:inline-block}.el-switch__label .el-icon{height:inherit}.el-switch__label .el-icon svg{vertical-align:middle}.el-switch__input{opacity:0;width:0;height:0;margin:0;position:absolute}.el-switch__input:focus-visible~.el-switch__core{outline:2px solid var(--el-switch-on-color);outline-offset:1px}.el-switch__core{border:1px solid var(--el-switch-border-color,var(--el-switch-off-color));box-sizing:border-box;background:var(--el-switch-off-color);cursor:pointer;min-width:40px;height:20px;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration);border-radius:10px;outline:none;align-items:center;display:inline-flex;position:relative}.el-switch__core .el-switch__inner{width:100%;transition:all var(--el-transition-duration);justify-content:center;align-items:center;height:16px;padding:0 4px 0 18px;display:flex;overflow:hidden}.el-switch__core .el-switch__inner-wrapper{color:var(--el-color-white);-webkit-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;align-items:center;font-size:12px;display:flex;overflow:hidden}.el-switch__core .el-switch__action{border-radius:var(--el-border-radius-circle);transition:all var(--el-transition-duration);background-color:var(--el-color-white);width:16px;height:16px;color:var(--el-switch-off-color);justify-content:center;align-items:center;display:flex;position:absolute;left:1px}.el-switch.is-checked .el-switch__core{border-color:var(--el-switch-border-color,var(--el-switch-on-color));background-color:var(--el-switch-on-color)}.el-switch.is-checked .el-switch__core .el-switch__action{color:var(--el-switch-on-color);left:calc(100% - 17px)}.el-switch.is-checked .el-switch__core .el-switch__inner{padding:0 18px 0 4px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter-from,.el-switch .label-fade-leave-active{opacity:0}.el-switch--large{height:40px;font-size:14px;line-height:24px}.el-switch--large .el-switch__label{height:24px;font-size:14px}.el-switch--large .el-switch__label *{font-size:14px}.el-switch--large .el-switch__core{border-radius:12px;min-width:50px;height:24px}.el-switch--large .el-switch__core .el-switch__inner{height:20px;padding:0 6px 0 22px}.el-switch--large .el-switch__core .el-switch__action{width:20px;height:20px}.el-switch--large.is-checked .el-switch__core .el-switch__action{left:calc(100% - 21px)}.el-switch--large.is-checked .el-switch__core .el-switch__inner{padding:0 22px 0 6px}.el-switch--small{height:24px;font-size:12px;line-height:16px}.el-switch--small .el-switch__label{height:16px;font-size:12px}.el-switch--small .el-switch__label *{font-size:12px}.el-switch--small .el-switch__core{border-radius:8px;min-width:30px;height:16px}.el-switch--small .el-switch__core .el-switch__inner{height:12px;padding:0 2px 0 14px}.el-switch--small .el-switch__core .el-switch__action{width:12px;height:12px}.el-switch--small.is-checked .el-switch__core .el-switch__action{left:calc(100% - 13px)}.el-switch--small.is-checked .el-switch__core .el-switch__inner{padding:0 14px 0 2px}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:solid 1px var(--el-border-color-lighter);box-shadow:var(--el-box-shadow-light);box-sizing:border-box;background-color:#fff;border-radius:2px}.el-table-filter__list{outline:none;min-width:100px;margin:0;padding:5px 0;list-style:none}.el-table-filter__list-item{cursor:pointer;line-height:36px;font-size:var(--el-font-size-base);outline:none;padding:0 10px}.el-table-filter__list-item:hover,.el-table-filter__list-item:focus{background-color:var(--el-color-primary-light-9);color:var(--el-color-primary)}.el-table-filter__list-item.is-active{background-color:var(--el-color-primary);color:#fff}.el-table-filter__multiple{outline:none}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid var(--el-border-color-lighter);padding:8px}.el-table-filter__bottom button{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table-filter__bottom button:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table-filter__bottom button{color:var(--el-text-color-regular);font-size:var(--el-font-size-small);padding:0 3px}.el-table-filter__bottom button:hover{color:var(--el-color-primary)}.el-table-filter__bottom button.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{height:unset;align-items:center;margin-bottom:12px;margin-left:5px;margin-right:5px;display:flex}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-table{--el-table-border-color:var(--el-border-color-lighter);--el-table-border:1px solid var(--el-table-border-color);--el-table-text-color:var(--el-text-color-regular);--el-table-header-text-color:var(--el-text-color-secondary);--el-table-row-hover-bg-color:var(--el-fill-color-light);--el-table-current-row-bg-color:var(--el-color-primary-light-9);--el-table-header-bg-color:var(--el-fill-color-blank);--el-table-fixed-box-shadow:var(--el-box-shadow-light);--el-table-bg-color:var(--el-fill-color-blank);--el-table-tr-bg-color:var(--el-fill-color-blank);--el-table-expanded-cell-bg-color:var(--el-fill-color-blank);--el-table-fixed-left-column:inset 10px 0 10px -10px #00000026;--el-table-fixed-right-column:inset -10px 0 10px -10px #00000026;--el-table-index:var(--el-index-normal);box-sizing:border-box;background-color:var(--el-table-bg-color);width:100%;max-width:100%;height:-moz-fit-content;height:fit-content;font-size:var(--el-font-size-base);color:var(--el-table-text-color);position:relative;overflow:hidden}.el-table__inner-wrapper{flex-direction:column;height:100%;display:flex;position:relative}.el-table__inner-wrapper:before{height:1px;bottom:0;left:0}.el-table tbody:focus-visible{outline:none}.el-table.has-footer.el-table--scrollable-y tr:last-child td.el-table__cell,.el-table.has-footer.el-table--fluid-height tr:last-child td.el-table__cell{border-bottom-color:#0000}.el-table__empty-block{text-align:center;justify-content:center;align-items:center;width:100%;min-height:60px;display:flex;position:sticky;left:0}.el-table__empty-text{width:50%;color:var(--el-text-color-secondary);line-height:60px}.el-table__expand-column .cell{text-align:center;-webkit-user-select:none;user-select:none;padding:0}.el-table__expand-icon{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table__expand-icon:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:-2px}.el-table__expand-icon{color:var(--el-text-color-regular);width:min(23px,100%);height:23px;font-size:12px;line-height:12px}.el-table__expand-icon.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-table__expand-icon--expanded{transform:rotate(90deg)}.el-table__expand-icon>.el-icon{font-size:12px}.el-table__expanded-cell{background-color:var(--el-table-expanded-cell-bg-color)}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:#0000!important}.el-table__placeholder{width:20px;display:inline-block}.el-table__append-wrapper{overflow:hidden}.el-table--fit{border-bottom:0;border-right:0}.el-table--fit .el-table__cell.gutter{border-right-width:1px}.el-table--fit .el-table__inner-wrapper:before{width:100%}.el-table thead{color:var(--el-table-header-text-color)}.el-table thead th{font-weight:600}.el-table thead.is-group th.el-table__cell{background:var(--el-fill-color-light)}.el-table .el-table__cell{box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;text-align:left;min-width:0;z-index:var(--el-table-index);padding:8px 0;position:relative}.el-table .el-table__cell.is-center{text-align:center}.el-table .el-table__cell.is-right{text-align:right}.el-table .el-table__cell.gutter{border-bottom-width:0;border-right-width:0;width:15px;padding:0}.el-table .el-table__cell.is-hidden>*{visibility:hidden}.el-table .cell{box-sizing:border-box;text-overflow:ellipsis;white-space:normal;overflow-wrap:break-word;padding:0 12px;line-height:23px;overflow:hidden}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--large{font-size:var(--el-font-size-base)}.el-table--large .el-table__cell{padding:12px 0}.el-table--large .cell{padding:0 16px}.el-table--default{font-size:var(--el-font-size-base)}.el-table--default .el-table__cell{padding:8px 0}.el-table--default .cell{padding:0 12px}.el-table--small{font-size:var(--el-font-size-extra-small)}.el-table--small .el-table__cell{padding:4px 0}.el-table--small .cell{padding:0 8px}.el-table tr{background-color:var(--el-table-tr-bg-color)}.el-table tr input[type=checkbox]{margin:0}.el-table th.el-table__cell.is-leaf,.el-table td.el-table__cell{border-bottom:var(--el-table-border)}.el-table th.el-table__cell.is-sortable{cursor:pointer}.el-table th.el-table__cell{background-color:var(--el-table-header-bg-color)}.el-table th.el-table__cell>.cell.highlight{color:var(--el-color-primary)}.el-table th.el-table__cell.required>div:before{content:"";vertical-align:middle;background:#ff4d51;border-radius:50%;width:8px;height:8px;margin-right:5px;display:inline-block}.el-table td.el-table__cell div{box-sizing:border-box}.el-table td.el-table__cell.gutter{width:0}.el-table--border:after,.el-table--border:before,.el-table--border .el-table__inner-wrapper:after,.el-table__inner-wrapper:before{content:"";background-color:var(--el-table-border-color);z-index:calc(var(--el-table-index) + 2);position:absolute}.el-table--border .el-table__inner-wrapper:after{width:100%;height:1px;z-index:calc(var(--el-table-index) + 2);top:0;left:0}.el-table--border:before{width:1px;height:100%;top:-1px;left:0}.el-table--border:after{width:1px;height:100%;top:-1px;right:0}.el-table--border .el-table__inner-wrapper{border-bottom:none;border-right:none}.el-table--border .el-table__footer-wrapper{flex-shrink:0;position:relative}.el-table--border .el-table__cell{border-right:var(--el-table-border)}.el-table--border th.el-table__cell.gutter:last-of-type{border-bottom:var(--el-table-border);border-bottom-width:1px}.el-table--border th.el-table__cell{border-bottom:var(--el-table-border)}.el-table--hidden{visibility:hidden}.el-table__header-wrapper,.el-table__body-wrapper,.el-table__footer-wrapper{width:100%}.el-table__header-wrapper tr td.el-table-fixed-column--left,.el-table__header-wrapper tr td.el-table-fixed-column--right,.el-table__header-wrapper tr th.el-table-fixed-column--left,.el-table__header-wrapper tr th.el-table-fixed-column--right,.el-table__body-wrapper tr td.el-table-fixed-column--left,.el-table__body-wrapper tr td.el-table-fixed-column--right,.el-table__body-wrapper tr th.el-table-fixed-column--left,.el-table__body-wrapper tr th.el-table-fixed-column--right,.el-table__footer-wrapper tr td.el-table-fixed-column--left,.el-table__footer-wrapper tr td.el-table-fixed-column--right,.el-table__footer-wrapper tr th.el-table-fixed-column--left,.el-table__footer-wrapper tr th.el-table-fixed-column--right{background:inherit;z-index:calc(var(--el-table-index) + 1);position:sticky!important}.el-table__header-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-first-column:before{content:"";width:10px;box-shadow:none;touch-action:none;pointer-events:none;position:absolute;top:0;bottom:0;overflow:hidden}.el-table__header-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-first-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-first-column:before{left:-10px}.el-table__header-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__header-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__body-wrapper tr th.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-last-column:before,.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-last-column:before{right:-10px}.el-table__header-wrapper tr td.el-table__fixed-right-patch,.el-table__header-wrapper tr th.el-table__fixed-right-patch,.el-table__body-wrapper tr td.el-table__fixed-right-patch,.el-table__body-wrapper tr th.el-table__fixed-right-patch,.el-table__footer-wrapper tr td.el-table__fixed-right-patch,.el-table__footer-wrapper tr th.el-table__fixed-right-patch{z-index:calc(var(--el-table-index) + 1);background:#fff;right:0;position:sticky!important}.el-table__header-wrapper{flex-shrink:0}.el-table__header-wrapper tr th.el-table-fixed-column--left,.el-table__header-wrapper tr th.el-table-fixed-column--right{background-color:var(--el-table-header-bg-color)}.el-table__header,.el-table__body,.el-table__footer{table-layout:fixed;border-collapse:separate}.el-table__header-wrapper{overflow:hidden}.el-table__header-wrapper tbody td.el-table__cell{background-color:var(--el-table-row-hover-bg-color);color:var(--el-table-text-color)}.el-table__footer-wrapper{flex-shrink:0;overflow:hidden}.el-table__footer-wrapper tfoot td.el-table__cell{background-color:var(--el-table-row-hover-bg-color);color:var(--el-table-text-color)}.el-table__header-wrapper .el-table-column--selection>.cell,.el-table__body-wrapper .el-table-column--selection>.cell{align-items:center;height:23px;display:inline-flex}.el-table__header-wrapper .el-table-column--selection .el-checkbox,.el-table__body-wrapper .el-table-column--selection .el-checkbox{height:unset}.el-table.is-scrolling-left .el-table-fixed-column--right.is-first-column:before{box-shadow:var(--el-table-fixed-right-column)}.el-table.is-scrolling-left.el-table--border .el-table-fixed-column--left.is-last-column.el-table__cell{border-right:var(--el-table-border)}.el-table.is-scrolling-left th.el-table-fixed-column--left{background-color:var(--el-table-header-bg-color)}.el-table.is-scrolling-right .el-table-fixed-column--left.is-last-column:before{box-shadow:var(--el-table-fixed-left-column)}.el-table.is-scrolling-right .el-table-fixed-column--left.is-last-column.el-table__cell{border-right:none}.el-table.is-scrolling-right th.el-table-fixed-column--right{background-color:var(--el-table-header-bg-color)}.el-table.is-scrolling-middle .el-table-fixed-column--left.is-last-column.el-table__cell{border-right:none}.el-table.is-scrolling-middle .el-table-fixed-column--right.is-first-column:before{box-shadow:var(--el-table-fixed-right-column)}.el-table.is-scrolling-middle .el-table-fixed-column--left.is-last-column:before{box-shadow:var(--el-table-fixed-left-column)}.el-table.is-scrolling-none .el-table-fixed-column--left.is-first-column:before,.el-table.is-scrolling-none .el-table-fixed-column--left.is-last-column:before,.el-table.is-scrolling-none .el-table-fixed-column--right.is-first-column:before,.el-table.is-scrolling-none .el-table-fixed-column--right.is-last-column:before{box-shadow:none}.el-table.is-scrolling-none th.el-table-fixed-column--left,.el-table.is-scrolling-none th.el-table-fixed-column--right{background-color:var(--el-table-header-bg-color)}.el-table__body-wrapper{flex:1;position:relative;overflow:hidden}.el-table__body-wrapper .el-scrollbar__bar{z-index:calc(var(--el-table-index) + 2)}.el-table .caret-wrapper{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table .caret-wrapper:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table .caret-wrapper{vertical-align:middle;width:24px;height:14px;overflow:initial;flex-direction:column;align-items:center;display:inline-flex;position:relative}.el-table .sort-caret{border:5px solid #0000;width:0;height:0;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:var(--el-text-color-placeholder);top:-5px}.el-table .sort-caret.descending{border-top-color:var(--el-text-color-placeholder);bottom:-3px}.el-table .ascending .sort-caret.ascending{border-bottom-color:var(--el-color-primary)}.el-table .descending .sort-caret.descending{border-top-color:var(--el-color-primary)}.el-table .hidden-columns{visibility:hidden;z-index:-1;position:absolute}.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell{background:var(--el-fill-color-lighter)}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td.el-table__cell{background-color:var(--el-table-current-row-bg-color)}.el-table__body tr.hover-row>td.el-table__cell,.el-table__body tr.hover-row.current-row>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped>td.el-table__cell,.el-table__body tr.hover-row.el-table__row--striped.current-row>td.el-table__cell,.el-table__body tr>td.hover-cell{background-color:var(--el-table-row-hover-bg-color)}.el-table__body tr.current-row>td.el-table__cell{background-color:var(--el-table-current-row-bg-color)}.el-table.el-table--scrollable-y .el-table__body-header{z-index:calc(var(--el-table-index) + 2);position:sticky;top:0}.el-table.el-table--scrollable-y .el-table__body-footer{z-index:calc(var(--el-table-index) + 2);position:sticky;bottom:0}.el-table__column-resize-proxy{border-left:var(--el-table-border);width:0;z-index:calc(var(--el-table-index) + 9);position:absolute;top:0;bottom:0;left:200px}.el-table__column-filter-trigger{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table__column-filter-trigger:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table__column-filter-trigger{display:inline-block}.el-table__column-filter-trigger i{color:var(--el-color-info);vertical-align:middle;font-size:14px}.el-table__border-left-patch{width:1px;height:100%;z-index:calc(var(--el-table-index) + 2);background-color:var(--el-table-border-color);position:absolute;top:0;left:0}.el-table__border-bottom-patch{height:1px;z-index:calc(var(--el-table-index) + 2);background-color:var(--el-table-border-color);position:absolute;left:0}.el-table__border-right-patch{width:1px;height:100%;z-index:calc(var(--el-table-index) + 2);background-color:var(--el-table-border-color);position:absolute;top:0}.el-table--enable-row-transition .el-table__body td.el-table__cell{transition:background-color .25s 1ms}.el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell{background-color:var(--el-table-row-hover-bg-color)}.el-table [class*=el-table__row--level] .el-table__expand-icon{text-align:center;width:20px;display:inline-block}.el-table .el-table.el-table--border .el-table__cell{border-right:var(--el-table-border)}.el-table:not(.el-table--border) .el-table__cell{border-right:none}.el-table:not(.el-table--border)>.el-table__inner-wrapper:after{content:none}.el-table-v2{--el-table-border-color:var(--el-border-color-lighter);--el-table-border:1px solid var(--el-table-border-color);--el-table-text-color:var(--el-text-color-regular);--el-table-header-text-color:var(--el-text-color-secondary);--el-table-row-hover-bg-color:var(--el-fill-color-light);--el-table-current-row-bg-color:var(--el-color-primary-light-9);--el-table-header-bg-color:var(--el-fill-color-blank);--el-table-fixed-box-shadow:var(--el-box-shadow-light);--el-table-bg-color:var(--el-fill-color-blank);--el-table-tr-bg-color:var(--el-fill-color-blank);--el-table-expanded-cell-bg-color:var(--el-fill-color-blank);--el-table-fixed-left-column:inset 10px 0 10px -10px #00000026;--el-table-fixed-right-column:inset -10px 0 10px -10px #00000026;--el-table-index:var(--el-index-normal);font-size:var(--el-font-size-base)}.el-table-v2 *{box-sizing:border-box}.el-table-v2__root{position:relative}.el-table-v2__root:hover .el-table-v2__main .el-virtual-scrollbar{opacity:1}.el-table-v2__main{background-color:var(--el-bg-color);flex-direction:column-reverse;display:flex;position:absolute;top:0;left:0;overflow:hidden}.el-table-v2__main .el-vl__horizontal,.el-table-v2__main .el-vl__vertical{z-index:2}.el-table-v2__left{background-color:var(--el-bg-color);flex-direction:column-reverse;display:flex;position:absolute;top:0;left:0;overflow:hidden;box-shadow:2px 0 4px #0000000f}.el-table-v2__left .el-virtual-scrollbar{opacity:0}.el-table-v2__left .el-vl__vertical,.el-table-v2__left .el-vl__horizontal{z-index:-1}.el-table-v2__right{background-color:var(--el-bg-color);flex-direction:column-reverse;display:flex;position:absolute;top:0;right:0;overflow:hidden;box-shadow:-2px 0 4px #0000000f}.el-table-v2__right .el-virtual-scrollbar{opacity:0}.el-table-v2__right .el-vl__vertical,.el-table-v2__right .el-vl__horizontal{z-index:-1}.el-table-v2__header-row,.el-table-v2__row{padding-inline-end:var(--el-table-scrollbar-size)}.el-table-v2__header-wrapper{overflow:hidden}.el-table-v2__header{position:relative;overflow:hidden}.el-table-v2__header .el-checkbox{z-index:0}.el-table-v2__footer{position:absolute;bottom:0;left:0;right:0;overflow:hidden}.el-table-v2__empty{position:absolute;left:0}.el-table-v2__overlay{z-index:9999;position:absolute;top:0;bottom:0;left:0;right:0}.el-table-v2__header-row{border-bottom:var(--el-table-border);display:flex}.el-table-v2__header-cell{-webkit-user-select:none;user-select:none;background-color:var(--el-table-header-bg-color);height:100%;color:var(--el-table-header-text-color);align-items:center;padding:0 8px;font-weight:700;display:flex;overflow:hidden}.el-table-v2__header-cell.is-align-center{text-align:center;justify-content:center}.el-table-v2__header-cell.is-align-right{text-align:right;justify-content:flex-end}.el-table-v2__header-cell.is-sortable{cursor:pointer}.el-table-v2__header-cell:hover .el-icon{display:block}.el-table-v2__sort-icon{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table-v2__sort-icon:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table-v2__sort-icon{transition:opacity,display var(--el-transition-duration);opacity:.6;display:none}.el-table-v2__sort-icon.is-sorting{opacity:1;display:flex}.el-table-v2__row{border-bottom:var(--el-table-border);transition:background-color var(--el-transition-duration);align-items:center;display:flex}.el-table-v2__row.is-hovered,.el-table-v2__row:hover{background-color:var(--el-table-row-hover-bg-color)}.el-table-v2__row-cell{align-items:center;height:100%;padding:0 8px;display:flex;overflow:hidden}.el-table-v2__row-cell.is-align-center{text-align:center;justify-content:center}.el-table-v2__row-cell.is-align-right{text-align:right;justify-content:flex-end}.el-table-v2__expand-icon{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:var(--el-border-radius-base);transition:transform var(--el-transition-duration-fast) ease-in-out;background-color:#0000;border:none;outline:none;margin:0;padding:0}.el-table-v2__expand-icon:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-table-v2__expand-icon{-webkit-user-select:none;user-select:none;margin:0 4px}.el-table-v2__expand-icon svg{transition:transform var(--el-transition-duration)}.el-table-v2__expand-icon.is-expanded svg{transform:rotate(90deg)}.el-table-v2:not(.is-dynamic) .el-table-v2__cell-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.el-table-v2.is-dynamic .el-table-v2__row{align-items:stretch;overflow:hidden}.el-table-v2.is-dynamic .el-table-v2__row .el-table-v2__row-cell{overflow-wrap:break-word}.el-tabs{--el-tabs-header-height:40px;display:flex}.el-tabs__header{justify-content:space-between;align-items:center;margin:0 0 15px;padding:0;display:flex;position:relative}.el-tabs__header-vertical{flex-direction:column}.el-tabs__active-bar{background-color:var(--el-color-primary);z-index:1;height:2px;transition:width var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier),transform var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);list-style:none;position:absolute;bottom:0;left:0}.el-tabs__active-bar.is-bottom{bottom:auto}.el-tabs__new-tab{border:1px solid var(--el-border-color);text-align:center;width:20px;height:20px;color:var(--el-text-color-primary);cursor:pointer;border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;margin:10px 0 10px 10px;font-size:12px;line-height:20px;transition:all .15s;display:flex}.el-tabs__new-tab .is-icon-plus{height:inherit;width:inherit;transform:scale(.8)}.el-tabs__new-tab .is-icon-plus svg{vertical-align:middle}.el-tabs__new-tab:hover{color:var(--el-color-primary)}.el-tabs__new-tab-vertical{margin-left:0}.el-tabs__nav-wrap{flex:auto;margin-bottom:-1px;position:relative;overflow:hidden}.el-tabs__nav-wrap:after{content:"";background-color:var(--el-border-color-light);width:100%;height:2px;z-index:var(--el-index-normal);position:absolute;bottom:0;left:0}.el-tabs__nav-wrap.is-bottom:after{top:0;bottom:auto}.el-tabs__nav-wrap.is-scrollable{box-sizing:border-box;padding:0 20px}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{cursor:pointer;color:var(--el-text-color-secondary);text-align:center;width:20px;font-size:12px;line-height:44px;position:absolute}.el-tabs__nav-next.is-disabled,.el-tabs__nav-prev.is-disabled{color:var(--el-text-color-disabled);cursor:not-allowed}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{white-space:nowrap;transition:transform var(--el-transition-duration);float:left;z-index:calc(var(--el-index-normal) + 1);display:flex;position:relative}.el-tabs__nav.is-stretch{min-width:100%;display:flex}.el-tabs__nav.is-stretch>*{text-align:center;flex:1}.el-tabs__item{height:var(--el-tabs-header-height);box-sizing:border-box;font-size:var(--el-font-size-base);color:var(--el-text-color-primary);justify-content:center;align-items:center;padding:0 20px;font-weight:500;list-style:none;display:flex;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:none}.el-tabs__item:focus-visible{box-shadow:0 0 2px 2px var(--el-color-primary) inset;border-radius:3px}.el-tabs__item .is-icon-close{text-align:center;transition:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);border-radius:50%;margin-left:5px}.el-tabs__item .is-icon-close:before{display:inline-block;transform:scale(.9)}.el-tabs__item .is-icon-close:hover{background-color:var(--el-text-color-placeholder);color:#fff}.el-tabs__item.is-active{color:var(--el-color-primary)}.el-tabs__item:hover{color:var(--el-color-primary);cursor:pointer}.el-tabs__item.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-tabs__content{flex-grow:1;position:relative;overflow:hidden}.el-tabs--top>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:0}.el-tabs--top>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom>.el-tabs__header .el-tabs__item:last-child{padding-right:0}.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--card>.el-tabs__header{border-bottom:1px solid var(--el-border-color-light);height:var(--el-tabs-header-height);box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid var(--el-border-color-light);box-sizing:border-box;border-bottom:none;border-radius:4px 4px 0 0}.el-tabs--card>.el-tabs__header .el-tabs__active-bar{display:none}.el-tabs--card>.el-tabs__header .el-tabs__item .is-icon-close{transform-origin:100%;width:0;height:14px;font-size:12px;position:relative;right:-2px;overflow:hidden}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid #0000;border-left:1px solid var(--el-border-color-light);transition:color var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier),padding var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);margin-top:-1px}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .is-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:var(--el-bg-color)}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .is-icon-close{width:14px}.el-tabs--border-card{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:var(--el-fill-color-light);border-bottom:1px solid var(--el-border-color-light);margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs--border-card>.el-tabs__header .el-tabs__item{transition:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);color:var(--el-text-color-secondary);border:1px solid #0000;margin-top:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child,.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item{margin-left:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{color:var(--el-color-primary);background-color:var(--el-bg-color-overlay);border-right-color:var(--el-border-color);border-left-color:var(--el-border-color)}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:var(--el-color-primary)}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:var(--el-disabled-text-color)}.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--bottom{flex-direction:column}.el-tabs--bottom .el-tabs__header.is-bottom{margin-top:10px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid var(--el-border-color)}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-top:-1px;margin-bottom:0}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid #0000}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--card>.el-tabs__header.is-left,.el-tabs--card>.el-tabs__header.is-right,.el-tabs--border-card>.el-tabs__header.is-left,.el-tabs--border-card>.el-tabs__header.is-right{border-bottom:none}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-scroll{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{width:2px;height:auto;top:0;bottom:auto}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{text-align:center;cursor:pointer;width:100%;height:30px;line-height:30px}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i{transform:rotate(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev.is-disabled,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next.is-disabled,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev.is-disabled,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev.is-disabled,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next.is-disabled{cursor:not-allowed}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{top:0;left:auto}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{bottom:0;right:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left:after,.el-tabs--left .el-tabs__nav-wrap.is-right:after,.el-tabs--right .el-tabs__nav-wrap.is-left:after,.el-tabs--right .el-tabs__nav-wrap.is-right:after{width:2px;height:100%;top:0;bottom:auto}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{flex-direction:column}.el-tabs--left .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-left{justify-content:flex-end}.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-right{justify-content:flex-start}.el-tabs--left{flex-direction:row}.el-tabs--left .el-tabs__header.is-left{margin-bottom:0;margin-right:10px}.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__nav-wrap.is-left:after,.el-tabs--left .el-tabs__active-bar.is-left{left:auto;right:0}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left{display:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-left:none;border-right:1px solid var(--el-border-color-light);border-bottom:none;border-top:1px solid var(--el-border-color-light);text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid var(--el-border-color-light);border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid var(--el-border-color-light);border-bottom:none;border-left:none;border-right-color:#fff}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-bottom:1px solid var(--el-border-color-light);border-right:none;border-radius:4px 0 0 4px}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid var(--el-border-color)}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid #0000;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:#d1dbe5 #0000}.el-tabs--left>.el-tabs__content+.el-tabs__header{order:-1}.el-tabs--right .el-tabs__header.is-right{margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right:after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid var(--el-border-color-light)}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid var(--el-border-color-light);border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid var(--el-border-color-light);border-bottom:none;border-left-color:#fff;border-right:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-bottom:1px solid var(--el-border-color-light);border-left:none;border-radius:0 4px 4px 0}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid var(--el-border-color)}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid #0000;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:#d1dbe5 #0000}.el-tabs--top{flex-direction:column}.el-tabs--top>.el-tabs__content+.el-tabs__header{order:-1}.slideInRight-transition,.slideInLeft-transition{display:inline-block}.slideInRight-enter{animation:slideInRight-enter var(--el-transition-duration)}.slideInRight-leave{animation:slideInRight-leave var(--el-transition-duration);position:absolute;left:0;right:0}.slideInLeft-enter{animation:slideInLeft-enter var(--el-transition-duration)}.slideInLeft-leave{animation:slideInLeft-leave var(--el-transition-duration);position:absolute;left:0;right:0}@keyframes slideInRight-enter{0%{opacity:0;transform-origin:0 0;transform:translate(100%)}to{opacity:1;transform-origin:0 0;transform:translate(0)}}@keyframes slideInRight-leave{0%{transform-origin:0 0;opacity:1;transform:translate(0)}to{transform-origin:0 0;opacity:0;transform:translate(100%)}}@keyframes slideInLeft-enter{0%{opacity:0;transform-origin:0 0;transform:translate(-100%)}to{opacity:1;transform-origin:0 0;transform:translate(0)}}@keyframes slideInLeft-leave{0%{transform-origin:0 0;opacity:1;transform:translate(0)}to{transform-origin:0 0;opacity:0;transform:translate(-100%)}}.el-tag{--el-tag-font-size:12px;--el-tag-border-radius:4px;--el-tag-border-radius-rounded:9999px;background-color:var(--el-tag-bg-color);border-color:var(--el-tag-border-color);color:var(--el-tag-text-color);vertical-align:middle;height:24px;font-size:var(--el-tag-font-size);border-radius:var(--el-tag-border-radius);box-sizing:border-box;white-space:nowrap;--el-icon-size:14px;--el-tag-bg-color:var(--el-color-primary-light-9);--el-tag-border-color:var(--el-color-primary-light-8);--el-tag-hover-color:var(--el-color-primary);border-style:solid;border-width:1px;justify-content:center;align-items:center;padding:0 9px;line-height:1;display:inline-flex}.el-tag.el-tag--primary{--el-tag-bg-color:var(--el-color-primary-light-9);--el-tag-border-color:var(--el-color-primary-light-8);--el-tag-hover-color:var(--el-color-primary)}.el-tag.el-tag--success{--el-tag-bg-color:var(--el-color-success-light-9);--el-tag-border-color:var(--el-color-success-light-8);--el-tag-hover-color:var(--el-color-success)}.el-tag.el-tag--warning{--el-tag-bg-color:var(--el-color-warning-light-9);--el-tag-border-color:var(--el-color-warning-light-8);--el-tag-hover-color:var(--el-color-warning)}.el-tag.el-tag--danger{--el-tag-bg-color:var(--el-color-danger-light-9);--el-tag-border-color:var(--el-color-danger-light-8);--el-tag-hover-color:var(--el-color-danger)}.el-tag.el-tag--error{--el-tag-bg-color:var(--el-color-error-light-9);--el-tag-border-color:var(--el-color-error-light-8);--el-tag-hover-color:var(--el-color-error)}.el-tag.el-tag--info{--el-tag-bg-color:var(--el-color-info-light-9);--el-tag-border-color:var(--el-color-info-light-8);--el-tag-hover-color:var(--el-color-info)}.el-tag.is-hit{border-color:var(--el-color-primary)}.el-tag.is-round{border-radius:var(--el-tag-border-radius-rounded)}.el-tag .el-tag__close{color:var(--el-tag-text-color);flex-shrink:0}.el-tag .el-tag__close:hover{color:var(--el-color-white);background-color:var(--el-tag-hover-color)}.el-tag.el-tag--primary{--el-tag-text-color:var(--el-color-primary)}.el-tag.el-tag--success{--el-tag-text-color:var(--el-color-success)}.el-tag.el-tag--warning{--el-tag-text-color:var(--el-color-warning)}.el-tag.el-tag--danger{--el-tag-text-color:var(--el-color-danger)}.el-tag.el-tag--error{--el-tag-text-color:var(--el-color-error)}.el-tag.el-tag--info{--el-tag-text-color:var(--el-color-info)}.el-tag .el-icon{cursor:pointer;font-size:calc(var(--el-icon-size) - 2px);height:var(--el-icon-size);width:var(--el-icon-size);border-radius:50%}.el-tag .el-tag__close{background-color:#0000;border:none;border-radius:50%;outline:none;margin-left:6px;padding:0;overflow:hidden}.el-tag .el-tag__close:focus-visible{outline:2px solid var(--el-color-primary);outline-offset:2px}.el-tag .el-tag__close .el-icon{display:flex}.el-tag--dark{--el-tag-text-color:var(--el-color-white);--el-tag-bg-color:var(--el-color-primary);--el-tag-border-color:var(--el-color-primary);--el-tag-hover-color:var(--el-color-primary-light-3)}.el-tag--dark.el-tag--primary{--el-tag-bg-color:var(--el-color-primary);--el-tag-border-color:var(--el-color-primary);--el-tag-hover-color:var(--el-color-primary-light-3)}.el-tag--dark.el-tag--success{--el-tag-bg-color:var(--el-color-success);--el-tag-border-color:var(--el-color-success);--el-tag-hover-color:var(--el-color-success-light-3)}.el-tag--dark.el-tag--warning{--el-tag-bg-color:var(--el-color-warning);--el-tag-border-color:var(--el-color-warning);--el-tag-hover-color:var(--el-color-warning-light-3)}.el-tag--dark.el-tag--danger{--el-tag-bg-color:var(--el-color-danger);--el-tag-border-color:var(--el-color-danger);--el-tag-hover-color:var(--el-color-danger-light-3)}.el-tag--dark.el-tag--error{--el-tag-bg-color:var(--el-color-error);--el-tag-border-color:var(--el-color-error);--el-tag-hover-color:var(--el-color-error-light-3)}.el-tag--dark.el-tag--info{--el-tag-bg-color:var(--el-color-info);--el-tag-border-color:var(--el-color-info);--el-tag-hover-color:var(--el-color-info-light-3)}.el-tag--dark.el-tag--primary,.el-tag--dark.el-tag--success,.el-tag--dark.el-tag--warning,.el-tag--dark.el-tag--danger,.el-tag--dark.el-tag--error,.el-tag--dark.el-tag--info{--el-tag-text-color:var(--el-color-white)}.el-tag--plain,.el-tag--plain.el-tag--primary{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-primary-light-5);--el-tag-hover-color:var(--el-color-primary)}.el-tag--plain.el-tag--success{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-success-light-5);--el-tag-hover-color:var(--el-color-success)}.el-tag--plain.el-tag--warning{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-warning-light-5);--el-tag-hover-color:var(--el-color-warning)}.el-tag--plain.el-tag--danger{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-danger-light-5);--el-tag-hover-color:var(--el-color-danger)}.el-tag--plain.el-tag--error{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-error-light-5);--el-tag-hover-color:var(--el-color-error)}.el-tag--plain.el-tag--info{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-info-light-5);--el-tag-hover-color:var(--el-color-info)}.el-tag.is-closable{padding-right:5px}.el-tag--large{--el-icon-size:16px;height:32px;padding:0 11px}.el-tag--large .el-tag__close{margin-left:8px}.el-tag--large.is-closable{padding-right:7px}.el-tag--small{--el-icon-size:12px;height:20px;padding:0 7px}.el-tag--small .el-tag__close{margin-left:4px}.el-tag--small.is-closable{padding-right:3px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag.el-tag--primary.is-hit{border-color:var(--el-color-primary)}.el-tag.el-tag--success.is-hit{border-color:var(--el-color-success)}.el-tag.el-tag--warning.is-hit{border-color:var(--el-color-warning)}.el-tag.el-tag--danger.is-hit{border-color:var(--el-color-danger)}.el-tag.el-tag--error.is-hit{border-color:var(--el-color-error)}.el-tag.el-tag--info.is-hit{border-color:var(--el-color-info)}.el-text{--el-text-font-size:var(--el-font-size-base);--el-text-color:var(--el-text-color-regular);font-size:var(--el-text-font-size);color:var(--el-text-color);overflow-wrap:break-word;align-self:center;margin:0;padding:0}.el-text.is-truncated{text-overflow:ellipsis;white-space:nowrap;max-width:100%;display:inline-block;overflow:hidden}.el-text.is-line-clamp{-webkit-box-orient:vertical;display:-webkit-inline-box;overflow:hidden}.el-text--large{--el-text-font-size:var(--el-font-size-medium)}.el-text--default{--el-text-font-size:var(--el-font-size-base)}.el-text--small{--el-text-font-size:var(--el-font-size-extra-small)}.el-text.el-text--primary{--el-text-color:var(--el-color-primary)}.el-text.el-text--success{--el-text-color:var(--el-color-success)}.el-text.el-text--warning{--el-text-color:var(--el-color-warning)}.el-text.el-text--danger{--el-text-color:var(--el-color-danger)}.el-text.el-text--error{--el-text-color:var(--el-color-error)}.el-text.el-text--info{--el-text-color:var(--el-color-info)}.el-text>.el-icon{vertical-align:-2px}.time-select{min-width:0;margin:5px 0}.time-select .el-picker-panel__content{max-height:200px;margin:0}.time-select-item{padding:8px 10px;font-size:14px;line-height:20px}.time-select-item.disabled{color:var(--el-datepicker-border-color);cursor:not-allowed}.time-select-item:hover{background-color:var(--el-fill-color-light);cursor:pointer;font-weight:700}.time-select .time-select-item.selected:not(.disabled){color:var(--el-color-primary);font-weight:700}.el-timeline-item{padding-bottom:20px;position:relative}.el-timeline-item__wrapper{box-sizing:content-box;position:relative;top:-3px}.el-timeline-item__tail{border-left:2px solid var(--el-timeline-node-color);height:100%;position:absolute}.el-timeline-item .el-timeline-item__icon{color:var(--el-color-white);font-size:var(--el-font-size-small)}.el-timeline-item__node{background-color:var(--el-timeline-node-color);border-color:var(--el-timeline-node-color);box-sizing:border-box;border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute}.el-timeline-item__node--normal{width:var(--el-timeline-node-size-normal);height:var(--el-timeline-node-size-normal)}.el-timeline-item__node--large{width:var(--el-timeline-node-size-large);height:var(--el-timeline-node-size-large)}.el-timeline-item__node.is-hollow{background:var(--el-color-white);border-style:solid;border-width:2px}.el-timeline-item__node--primary{background-color:var(--el-color-primary);border-color:var(--el-color-primary)}.el-timeline-item__node--success{background-color:var(--el-color-success);border-color:var(--el-color-success)}.el-timeline-item__node--warning{background-color:var(--el-color-warning);border-color:var(--el-color-warning)}.el-timeline-item__node--danger{background-color:var(--el-color-danger);border-color:var(--el-color-danger)}.el-timeline-item__node--info{background-color:var(--el-color-info);border-color:var(--el-color-info)}.el-timeline-item__dot{justify-content:center;align-items:center;display:flex;position:absolute}.el-timeline-item__content{color:var(--el-text-color-primary)}.el-timeline-item__timestamp{color:var(--el-text-color-secondary);line-height:1;font-size:var(--el-font-size-small)}.el-timeline-item__timestamp.is-top{margin-bottom:8px;padding-top:4px}.el-timeline-item__timestamp.is-bottom{margin-top:8px}.el-timeline-item.is-start .el-timeline-item__wrapper{padding-left:28px}.el-timeline-item.is-start .el-timeline-item__tail{left:4px}.el-timeline-item.is-start .el-timeline-item__node--normal{left:-1px}.el-timeline-item.is-start .el-timeline-item__node--large{left:-2px}.el-timeline-item.is-end .el-timeline-item__wrapper{text-align:right;padding-right:28px}.el-timeline-item.is-end .el-timeline-item__tail{right:4px}.el-timeline-item.is-end .el-timeline-item__node--normal{right:-1px}.el-timeline-item.is-end .el-timeline-item__node--large{right:-2px}.el-timeline-item.is-alternate .el-timeline-item__tail,.el-timeline-item.is-alternate .el-timeline-item__node,.el-timeline-item.is-alternate-reverse .el-timeline-item__tail,.el-timeline-item.is-alternate-reverse .el-timeline-item__node{left:50%;transform:translate(-50%)}.el-timeline{--el-timeline-node-size-normal:12px;--el-timeline-node-size-large:14px;--el-timeline-node-color:var(--el-border-color-light);font-size:var(--el-font-size-base);margin:0;list-style:none}.el-timeline .el-timeline-item:last-child .el-timeline-item__tail{display:none}.el-timeline .el-timeline-item__center{align-items:center;display:flex}.el-timeline .el-timeline-item__center .el-timeline-item__wrapper{width:100%}.el-timeline .el-timeline-item__center .el-timeline-item__tail{top:0}.el-timeline .el-timeline-item__center:first-child .el-timeline-item__tail{height:calc(50% + 10px);top:calc(50% - 10px)}.el-timeline .el-timeline-item__center:last-child .el-timeline-item__tail{height:calc(50% - 10px);display:block}.el-timeline.is-start{padding-left:40px;padding-right:0}.el-timeline.is-end{padding-left:0;padding-right:40px}.el-timeline.is-alternate{padding-left:20px;padding-right:20px}.el-timeline.is-alternate .el-timeline-item:nth-child(odd) .el-timeline-item__wrapper{width:calc(50% - 28px);left:calc(50% - var(--el-timeline-node-size-large) / 2);padding-left:28px}.el-timeline.is-alternate .el-timeline-item:nth-child(2n) .el-timeline-item__wrapper{width:calc(50% - 28px + var(--el-timeline-node-size-large) / 2);text-align:right;padding-right:28px}.el-timeline.is-alternate-reverse{padding-left:20px;padding-right:20px}.el-timeline.is-alternate-reverse .el-timeline-item:nth-child(odd) .el-timeline-item__wrapper{width:calc(50% - 28px + var(--el-timeline-node-size-large) / 2);text-align:right;padding-right:28px}.el-timeline.is-alternate-reverse .el-timeline-item:nth-child(2n) .el-timeline-item__wrapper{width:calc(50% - 28px);left:calc(50% - var(--el-timeline-node-size-large) / 2);padding-left:28px}.el-transfer{--el-transfer-border-color:var(--el-border-color-lighter);--el-transfer-border-radius:var(--el-border-radius-base);--el-transfer-panel-width:200px;--el-transfer-panel-header-height:40px;--el-transfer-panel-header-bg-color:var(--el-fill-color-light);--el-transfer-panel-footer-height:40px;--el-transfer-panel-body-height:278px;--el-transfer-item-height:30px;--el-transfer-filter-height:32px;font-size:var(--el-font-size-base)}.el-transfer__buttons{vertical-align:middle;padding:0 30px;display:inline-block}.el-transfer__button{vertical-align:top}.el-transfer__button:nth-child(2){margin:0 0 0 10px}.el-transfer__button i,.el-transfer__button span{font-size:14px}.el-transfer__button .el-icon+span{margin-left:0}.el-transfer-panel{background:var(--el-bg-color-overlay);text-align:left;vertical-align:middle;width:var(--el-transfer-panel-width);box-sizing:border-box;max-height:100%;display:inline-block;position:relative;overflow:hidden}.el-transfer-panel__body{height:var(--el-transfer-panel-body-height);border-left:1px solid var(--el-transfer-border-color);border-right:1px solid var(--el-transfer-border-color);border-bottom:1px solid var(--el-transfer-border-color);border-bottom-left-radius:var(--el-transfer-border-radius);border-bottom-right-radius:var(--el-transfer-border-radius);overflow:hidden}.el-transfer-panel__body.is-with-footer{border-bottom:none;border-bottom-right-radius:0;border-bottom-left-radius:0}.el-transfer-panel__list{height:var(--el-transfer-panel-body-height);box-sizing:border-box;margin:0;padding:6px 0;list-style:none;overflow:auto}.el-transfer-panel__list.is-filterable{height:calc(100% - var(--el-transfer-filter-height) - 30px);padding-top:0}.el-transfer-panel__item{height:var(--el-transfer-item-height);line-height:var(--el-transfer-item-height);padding-left:15px;display:block!important}.el-transfer-panel__item+.el-transfer-panel__item{margin-left:0}.el-transfer-panel__item.el-checkbox{color:var(--el-text-color-regular);margin-right:30px}.el-transfer-panel__item:hover{color:var(--el-color-primary)}.el-transfer-panel__item.el-checkbox .el-checkbox__label{text-overflow:ellipsis;white-space:nowrap;box-sizing:border-box;width:100%;line-height:var(--el-transfer-item-height);padding-left:22px;display:block;overflow:hidden}.el-transfer-panel__item .el-checkbox__input{position:absolute;top:8px}.el-transfer-panel__filter{text-align:center;box-sizing:border-box;padding:15px}.el-transfer-panel__filter .el-input__inner{height:var(--el-transfer-filter-height);box-sizing:border-box;width:100%;font-size:12px;display:inline-block}.el-transfer-panel__filter .el-icon-circle-close{cursor:pointer}.el-transfer-panel .el-transfer-panel__header{height:var(--el-transfer-panel-header-height);background:var(--el-transfer-panel-header-bg-color);border:1px solid var(--el-transfer-border-color);border-top-left-radius:var(--el-transfer-border-radius);border-top-right-radius:var(--el-transfer-border-radius);box-sizing:border-box;color:var(--el-color-black);align-items:center;margin:0;padding-left:15px;display:flex}.el-transfer-panel .el-transfer-panel__header .el-checkbox{align-items:center;width:100%;display:flex;position:relative}.el-transfer-panel .el-transfer-panel__header .el-checkbox .el-checkbox__label{min-width:0;color:var(--el-text-color-primary);flex:1;align-items:center;font-size:16px;font-weight:400;display:flex}.el-transfer-panel .el-transfer-panel__header-title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.el-transfer-panel .el-transfer-panel__header-count{color:var(--el-text-color-secondary);flex-shrink:0;margin-left:8px;margin-right:15px;font-size:12px}.el-transfer-panel .el-transfer-panel__footer{height:var(--el-transfer-panel-footer-height);background:var(--el-bg-color-overlay);border:1px solid var(--el-transfer-border-color);border-bottom-left-radius:var(--el-transfer-border-radius);border-bottom-right-radius:var(--el-transfer-border-radius);margin:0;padding:0}.el-transfer-panel .el-transfer-panel__footer:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-transfer-panel .el-transfer-panel__footer .el-checkbox{color:var(--el-text-color-regular);padding-left:20px}.el-transfer-panel .el-transfer-panel__empty{height:var(--el-transfer-item-height);line-height:var(--el-transfer-item-height);color:var(--el-text-color-secondary);text-align:center;margin:0;padding:6px 15px 0}.el-transfer-panel .el-checkbox__label{padding-left:8px}.el-tree{--el-tree-node-content-height:26px;--el-tree-node-hover-bg-color:var(--el-fill-color-light);--el-tree-text-color:var(--el-text-color-regular);--el-tree-expand-icon-color:var(--el-text-color-placeholder);cursor:default;background:var(--el-fill-color-blank);color:var(--el-tree-text-color);font-size:var(--el-font-size-base);position:relative}.el-tree__empty-block{text-align:center;width:100%;height:100%;min-height:60px;position:relative}.el-tree__empty-text{color:var(--el-text-color-secondary);font-size:var(--el-font-size-base);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-tree__drop-indicator{background-color:var(--el-color-primary);height:1px;position:absolute;left:0;right:0}.el-tree-node{white-space:nowrap;outline:none}.el-tree-node:focus>.el-tree-node__content{background-color:var(--el-tree-node-hover-bg-color)}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:var(--el-color-primary);color:#fff}.el-tree-node__content{--el-checkbox-height:var(--el-tree-node-content-height);height:var(--el-tree-node-content-height);cursor:pointer;align-items:center;display:flex}.el-tree-node__content>.el-tree-node__expand-icon{box-sizing:content-box;padding:6px}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree-node__content:hover{background-color:var(--el-tree-node-hover-bg-color)}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:var(--el-tree-expand-icon-color);transition:transform var(--el-transition-duration) ease-in-out;font-size:12px;transform:rotate(0)}.el-tree-node__expand-icon.expanded{transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:#0000;cursor:default;visibility:hidden}.el-tree-node__expand-icon.is-hidden{visibility:hidden}.el-tree-node__loading-icon{font-size:var(--el-font-size-base);color:var(--el-tree-expand-icon-color);margin-right:8px}.el-tree-node>.el-tree-node__children{background-color:#0000;overflow:hidden}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:var(--el-color-primary-light-9)}.el-tree-select{--el-tree-node-content-height:26px;--el-tree-node-hover-bg-color:var(--el-fill-color-light);--el-tree-text-color:var(--el-text-color-regular);--el-tree-expand-icon-color:var(--el-text-color-placeholder)}.el-tree-select__popper .el-tree-node__expand-icon{margin-left:8px}.el-tree-select__popper .el-tree-node.is-checked>.el-tree-node__content .el-select-dropdown__item.selected:after{content:none}.el-tree-select__popper .el-select-dropdown__list>.el-select-dropdown__item{padding-left:32px}.el-tree-select__popper .el-select-dropdown__item{flex:1;height:20px;padding-left:0;line-height:20px;background:0 0!important}.el-upload{--el-upload-dragger-padding-horizontal:10px;--el-upload-dragger-padding-vertical:40px;--el-upload-list-picture-card-size:var(--el-upload-picture-card-size);--el-upload-picture-card-size:148px;cursor:pointer;outline:none;justify-content:center;align-items:center;display:inline-flex}.el-upload.is-disabled{cursor:not-allowed}.el-upload.is-disabled:focus{border-color:var(--el-border-color-darker);color:inherit}.el-upload.is-disabled:focus .el-upload-dragger{border-color:var(--el-border-color-darker)}.el-upload.is-disabled .el-upload-dragger{cursor:not-allowed;background-color:var(--el-disabled-bg-color)}.el-upload.is-disabled .el-upload-dragger .el-upload__text{color:var(--el-text-color-placeholder)}.el-upload.is-disabled .el-upload-dragger .el-upload__text em{color:var(--el-disabled-text-color)}.el-upload.is-disabled .el-upload-dragger:hover{border-color:var(--el-border-color-darker)}.el-upload__input{display:none}.el-upload__tip{color:var(--el-text-color-regular);margin-top:7px;font-size:12px}.el-upload iframe{z-index:-1;opacity:0;filter:alpha(opacity=0);position:absolute;top:0;left:0}.el-upload--picture-card{background-color:var(--el-fill-color-lighter);border:1px dashed var(--el-border-color-darker);box-sizing:border-box;width:var(--el-upload-picture-card-size);height:var(--el-upload-picture-card-size);cursor:pointer;vertical-align:top;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.el-upload--picture-card>i{color:var(--el-text-color-secondary);font-size:28px}.el-upload--picture-card:hover{border-color:var(--el-color-primary);color:var(--el-color-primary)}.el-upload.is-drag{display:block}.el-upload:focus{border-color:var(--el-color-primary);color:var(--el-color-primary)}.el-upload:focus .el-upload-dragger{border-color:var(--el-color-primary)}.el-upload-dragger{padding:var(--el-upload-dragger-padding-vertical) var(--el-upload-dragger-padding-horizontal);background-color:var(--el-fill-color-blank);border:1px dashed var(--el-border-color);box-sizing:border-box;text-align:center;cursor:pointer;border-radius:6px;position:relative;overflow:hidden}.el-upload-dragger .el-icon--upload{color:var(--el-text-color-placeholder);margin-bottom:16px;font-size:67px;line-height:50px}.el-upload-dragger+.el-upload__tip{text-align:center}.el-upload-dragger~.el-upload__files{border-top:var(--el-border);margin-top:7px;padding-top:5px}.el-upload-dragger .el-upload__text{color:var(--el-text-color-regular);text-align:center;font-size:14px}.el-upload-dragger .el-upload__text em{color:var(--el-color-primary);font-style:normal}.el-upload-dragger:hover{border-color:var(--el-color-primary)}.el-upload-dragger.is-dragover{padding:calc(var(--el-upload-dragger-padding-vertical) - 1px) calc(var(--el-upload-dragger-padding-horizontal) - 1px);background-color:var(--el-color-primary-light-9);border:2px dashed var(--el-color-primary)}.el-upload-list{--el-upload-dragger-padding-horizontal:10px;--el-upload-dragger-padding-vertical:40px;--el-upload-list-picture-card-size:var(--el-upload-picture-card-size);--el-upload-picture-card-size:148px;margin:10px 0 0;padding:0;list-style:none;position:relative}.el-upload-list__item{color:var(--el-text-color-regular);box-sizing:border-box;border-radius:4px;width:100%;margin-bottom:5px;font-size:14px;transition:all .5s cubic-bezier(.55,0,.1,1);position:relative}.el-upload-list__item .el-progress{width:100%;position:absolute;top:20px}.el-upload-list__item .el-progress__text{position:absolute;top:-13px;right:0}.el-upload-list__item .el-progress-bar{margin-right:0;padding-right:0}.el-upload-list__item .el-icon--upload-success{color:var(--el-color-success)}.el-upload-list__item .el-icon--close{cursor:pointer;opacity:.75;color:var(--el-text-color-regular);transition:opacity var(--el-transition-duration);display:none;position:absolute;top:50%;right:5px;transform:translateY(-50%)}.el-upload-list__item .el-icon--close:hover{opacity:1;color:var(--el-color-primary)}.el-upload-list__item .el-icon--close-tip{cursor:pointer;opacity:1;color:var(--el-color-primary);font-size:12px;font-style:normal;display:none;position:absolute;top:1px;right:5px}.el-upload-list__item:hover,.el-upload-list__item:focus-within{background-color:var(--el-fill-color-light)}.el-upload-list__item:hover .el-icon--close,.el-upload-list__item:focus-within .el-icon--close{display:inline-flex}.el-upload-list__item:hover .el-icon--close-tip,.el-upload-list__item:focus-within .el-icon--close-tip{right:24px}.el-upload-list__item:hover .el-progress__text,.el-upload-list__item:focus-within .el-progress__text{display:none}.el-upload-list__item .el-upload-list__item-info{flex-direction:column;justify-content:center;width:calc(100% - 30px);margin-left:4px;display:inline-flex}.el-upload-list__item.is-success .el-upload-list__item-status-label{display:inline-flex}.el-upload-list__item.is-success .el-upload-list__item-name:hover,.el-upload-list__item.is-success .el-upload-list__item-name:focus{color:var(--el-color-primary);cursor:pointer}.el-upload-list__item.is-success:focus:not(:hover) .el-icon--close-tip{display:inline-block}.el-upload-list__item.is-success:not(.focusing):focus,.el-upload-list__item.is-success:active{outline-width:0}.el-upload-list__item.is-success:not(.focusing):focus .el-icon--close-tip,.el-upload-list__item.is-success:active .el-icon--close-tip{display:none}.el-upload-list__item.is-success:hover .el-upload-list__item-status-label,.el-upload-list__item.is-success:focus .el-upload-list__item-status-label,.el-upload-list__item.is-success:focus-within .el-upload-list__item-status-label{opacity:0;display:none}.el-upload-list__item-name{color:var(--el-text-color-regular);text-align:center;transition:color var(--el-transition-duration);font-size:var(--el-font-size-base);align-items:center;padding:0 4px;display:inline-flex}.el-upload-list__item-name .el-icon{color:var(--el-text-color-secondary);margin-right:6px}.el-upload-list__item-file-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.el-upload-list__item-status-label{line-height:inherit;height:100%;transition:opacity var(--el-transition-duration);justify-content:center;align-items:center;display:none;position:absolute;top:0;right:5px}.el-upload-list__item-delete{color:var(--el-text-color-regular);font-size:12px;display:none;position:absolute;top:0;right:10px}.el-upload-list__item-delete:hover{color:var(--el-color-primary)}.el-upload-list--picture-card{flex-wrap:wrap;margin:0;display:inline-flex}.el-upload-list--picture-card .el-upload-list__item{background-color:var(--el-fill-color-blank);border:1px solid var(--el-border-color);box-sizing:border-box;width:var(--el-upload-list-picture-card-size);height:var(--el-upload-list-picture-card-size);border-radius:6px;margin:0 8px 8px 0;padding:0;display:inline-flex;overflow:hidden}.el-upload-list--picture-card .el-upload-list__item .el-icon--check,.el-upload-list--picture-card .el-upload-list__item .el-icon--circle-check{color:#fff}.el-upload-list--picture-card .el-upload-list__item .el-icon--close{display:none}.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label{opacity:0;display:block}.el-upload-list--picture-card .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture-card .el-upload-list__item .el-upload-list__item-name{display:none}.el-upload-list--picture-card .el-upload-list__item-thumbnail{object-fit:contain;width:100%;height:100%}.el-upload-list--picture-card .el-upload-list__item-status-label{background:var(--el-color-success);text-align:center;width:40px;height:24px;top:-6px;right:-15px;transform:rotate(45deg)}.el-upload-list--picture-card .el-upload-list__item-status-label i{margin-top:11px;font-size:12px;transform:rotate(-45deg)}.el-upload-list--picture-card .el-upload-list__item-actions{cursor:default;color:#fff;opacity:0;background-color:var(--el-overlay-color-lighter);width:100%;height:100%;transition:opacity var(--el-transition-duration);justify-content:center;align-items:center;font-size:20px;display:inline-flex;position:absolute;top:0;left:0}.el-upload-list--picture-card .el-upload-list__item-actions span{cursor:pointer;display:none}.el-upload-list--picture-card .el-upload-list__item-actions span+span{margin-left:16px}.el-upload-list--picture-card .el-upload-list__item-actions .el-upload-list__item-delete{font-size:inherit;color:inherit;position:static}.el-upload-list--picture-card .el-upload-list__item-actions:hover{opacity:1}.el-upload-list--picture-card .el-upload-list__item-actions:hover span{display:inline-flex}.el-upload-list--picture-card .el-progress{width:126px;top:50%;bottom:auto;left:50%;transform:translate(-50%,-50%)}.el-upload-list--picture-card .el-progress .el-progress__text{top:50%}.el-upload-list--picture .el-upload-list__item{z-index:0;background-color:var(--el-fill-color-blank);border:1px solid var(--el-border-color);box-sizing:border-box;border-radius:6px;align-items:center;margin-top:10px;padding:10px;display:flex;overflow:hidden}.el-upload-list--picture .el-upload-list__item .el-icon--check,.el-upload-list--picture .el-upload-list__item .el-icon--circle-check{color:#fff}.el-upload-list--picture .el-upload-list__item:hover .el-upload-list__item-status-label{opacity:0;display:inline-flex}.el-upload-list--picture .el-upload-list__item:hover .el-progress__text{display:block}.el-upload-list--picture .el-upload-list__item.is-success .el-upload-list__item-name i{display:none}.el-upload-list--picture .el-upload-list__item .el-icon--close{top:5px;transform:translateY(0)}.el-upload-list--picture .el-upload-list__item-thumbnail{object-fit:contain;z-index:1;background-color:var(--el-color-white);justify-content:center;align-items:center;width:70px;height:70px;display:inline-flex;position:relative}.el-upload-list--picture .el-upload-list__item-status-label{background:var(--el-color-success);text-align:center;width:46px;height:26px;position:absolute;top:-7px;right:-17px;transform:rotate(45deg)}.el-upload-list--picture .el-upload-list__item-status-label i{margin-top:12px;font-size:12px;transform:rotate(-45deg)}.el-upload-list--picture .el-progress{position:relative;top:-7px}.el-upload-cover{z-index:10;cursor:default;width:100%;height:100%;position:absolute;top:0;left:0;overflow:hidden}.el-upload-cover:after{content:"";vertical-align:middle;height:100%;display:inline-block}.el-upload-cover img{width:100%;height:100%;display:block}.el-upload-cover__label{background:var(--el-color-success);text-align:center;width:40px;height:24px;top:-6px;right:-15px;transform:rotate(45deg)}.el-upload-cover__label i{color:#fff;margin-top:11px;font-size:12px;transform:rotate(-45deg)}.el-upload-cover__progress{vertical-align:middle;width:243px;display:inline-block;position:static}.el-upload-cover__progress+.el-upload__inner{opacity:0}.el-upload-cover__content{width:100%;height:100%;position:absolute;top:0;left:0}.el-upload-cover__interact{background-color:var(--el-overlay-color-light);text-align:center;width:100%;height:100%;position:absolute;bottom:0;left:0}.el-upload-cover__interact .btn{color:#fff;cursor:pointer;vertical-align:middle;transition:var(--el-transition-md-fade);margin-top:60px;font-size:14px;display:inline-block}.el-upload-cover__interact .btn i{margin-top:0}.el-upload-cover__interact .btn span{opacity:0;transition:opacity .15s linear}.el-upload-cover__interact .btn:not(:first-child){margin-left:35px}.el-upload-cover__interact .btn:hover{transform:translateY(-13px)}.el-upload-cover__interact .btn:hover span{opacity:1}.el-upload-cover__interact .btn i{color:#fff;font-size:24px;line-height:inherit;margin:0 auto 5px;display:block}.el-upload-cover__title{text-overflow:ellipsis;white-space:nowrap;text-align:left;width:100%;height:36px;color:var(--el-text-color-primary);background-color:#fff;margin:0;padding:0 10px;font-size:14px;font-weight:400;line-height:36px;position:absolute;bottom:0;left:0;overflow:hidden}.el-upload-cover+.el-upload__inner{opacity:0;z-index:1;position:relative}.el-vl__wrapper{position:relative}.el-vl__wrapper:hover .el-virtual-scrollbar,.el-vl__wrapper.always-on .el-virtual-scrollbar{opacity:1}.el-vl__window{scrollbar-width:none}.el-vl__window::-webkit-scrollbar{display:none}.el-virtual-scrollbar{opacity:0;transition:opacity .34s ease-out}.el-virtual-scrollbar.always-on{opacity:1}.el-vg__wrapper{position:relative}.el-popper{--el-popper-border-radius:var(--el-popover-border-radius,4px);--el-popper-bg-color-light:var(--el-bg-color-overlay);--el-popper-bg-color-dark:var(--el-text-color-primary);border-radius:var(--el-popper-border-radius);z-index:2000;overflow-wrap:break-word;word-break:normal;visibility:visible;min-width:10px;padding:5px 11px;font-size:12px;line-height:20px;position:absolute}.el-popper.is-dark{--el-fill-color-blank:var(--el-popper-bg-color-dark);color:var(--el-bg-color);background:var(--el-popper-bg-color-dark);border:1px solid var(--el-text-color-primary)}.el-popper.is-dark>.el-popper__arrow:before{border:1px solid var(--el-text-color-primary);background:var(--el-popper-bg-color-dark);right:0}.el-popper.is-light{--el-fill-color-blank:var(--el-popper-bg-color-light);background:var(--el-popper-bg-color-light);border:1px solid var(--el-border-color-light)}.el-popper.is-light>.el-popper__arrow:before{border:1px solid var(--el-border-color-light);background:var(--el-popper-bg-color-light);right:0}.el-popper.is-pure{padding:0}.el-popper__arrow{z-index:-1;width:10px;height:10px;position:absolute}.el-popper__arrow:before{z-index:-1;content:" ";background:var(--el-text-color-primary);box-sizing:border-box;width:10px;height:10px;position:absolute;transform:rotate(45deg)}.el-popper[data-popper-placement^=top]>.el-popper__arrow{bottom:-5px}.el-popper[data-popper-placement^=top]>.el-popper__arrow:before{border-bottom-right-radius:2px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow{top:-5px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow:before{border-top-left-radius:2px}.el-popper[data-popper-placement^=left]>.el-popper__arrow{right:-5px}.el-popper[data-popper-placement^=left]>.el-popper__arrow:before{border-top-right-radius:2px}.el-popper[data-popper-placement^=right]>.el-popper__arrow{left:-5px}.el-popper[data-popper-placement^=right]>.el-popper__arrow:before{border-bottom-left-radius:2px}.el-popper[data-popper-placement^=top]>.el-popper__arrow:before{border-top-color:#0000!important;border-left-color:#0000!important}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow:before{border-bottom-color:#0000!important;border-right-color:#0000!important}.el-popper[data-popper-placement^=left]>.el-popper__arrow:before{border-bottom-color:#0000!important;border-left-color:#0000!important}.el-popper[data-popper-placement^=right]>.el-popper__arrow:before{border-top-color:#0000!important;border-right-color:#0000!important}.el-statistic{--el-statistic-title-font-weight:400;--el-statistic-title-font-size:var(--el-font-size-extra-small);--el-statistic-title-color:var(--el-text-color-regular);--el-statistic-content-font-weight:400;--el-statistic-content-font-size:var(--el-font-size-extra-large);--el-statistic-content-color:var(--el-text-color-primary)}.el-statistic__head{font-weight:var(--el-statistic-title-font-weight);font-size:var(--el-statistic-title-font-size);color:var(--el-statistic-title-color);margin-bottom:4px;line-height:20px}.el-statistic__content{font-weight:var(--el-statistic-content-font-weight);font-size:var(--el-statistic-content-font-size);color:var(--el-statistic-content-color)}.el-statistic__value{display:inline-block}.el-statistic__prefix{margin-right:4px;display:inline-block}.el-statistic__suffix{margin-left:4px;display:inline-block}.el-tour{--el-tour-width:520px;--el-tour-padding-primary:12px;--el-tour-font-line-height:var(--el-font-line-height-primary);--el-tour-title-font-size:16px;--el-tour-title-text-color:var(--el-text-color-primary);--el-tour-title-font-weight:400;--el-tour-close-color:var(--el-color-info);--el-tour-font-size:14px;--el-tour-color:var(--el-text-color-primary);--el-tour-bg-color:var(--el-bg-color);--el-tour-border-radius:4px}.el-tour__hollow{transition:all var(--el-transition-duration) ease}.el-tour__content{border-radius:var(--el-tour-border-radius);width:var(--el-tour-width);padding:var(--el-tour-padding-primary);background:var(--el-tour-bg-color);box-shadow:var(--el-box-shadow-light);box-sizing:border-box;overflow-wrap:break-word;outline:none}.el-tour__arrow{background:var(--el-tour-bg-color);pointer-events:none;box-sizing:border-box;width:10px;height:10px;position:absolute;transform:rotate(45deg)}.el-tour__content[data-side^=top] .el-tour__arrow{border-top-color:#0000;border-left-color:#0000}.el-tour__content[data-side^=bottom] .el-tour__arrow{border-bottom-color:#0000;border-right-color:#0000}.el-tour__content[data-side^=left] .el-tour__arrow{border-bottom-color:#0000;border-left-color:#0000}.el-tour__content[data-side^=right] .el-tour__arrow{border-top-color:#0000;border-right-color:#0000}.el-tour__content[data-side^=top] .el-tour__arrow{bottom:-5px}.el-tour__content[data-side^=bottom] .el-tour__arrow{top:-5px}.el-tour__content[data-side^=left] .el-tour__arrow{right:-5px}.el-tour__content[data-side^=right] .el-tour__arrow{left:-5px}.el-tour__closebtn{cursor:pointer;width:40px;height:40px;font-size:var(--el-message-close-size,16px);background:0 0;border:none;outline:none;padding:0;position:absolute;top:0;right:0}.el-tour__closebtn .el-tour__close{color:var(--el-tour-close-color);font-size:inherit}.el-tour__closebtn:focus .el-tour__close,.el-tour__closebtn:hover .el-tour__close{color:var(--el-color-primary)}.el-tour__header{padding-bottom:var(--el-tour-padding-primary)}.el-tour__header.show-close{padding-right:calc(var(--el-tour-padding-primary) + var(--el-message-close-size,16px))}.el-tour__title{line-height:var(--el-tour-font-line-height);font-size:var(--el-tour-title-font-size);color:var(--el-tour-title-text-color);font-weight:var(--el-tour-title-font-weight)}.el-tour__body{color:var(--el-tour-text-color);font-size:var(--el-tour-font-size)}.el-tour__body img,.el-tour__body video{max-width:100%}.el-tour__footer{padding-top:var(--el-tour-padding-primary);box-sizing:border-box;justify-content:space-between;display:flex}.el-tour__content .el-tour-indicators{flex:1;display:inline-block}.el-tour__content .el-tour-indicator{background:var(--el-color-info-light-9);border-radius:50%;width:6px;height:6px;margin-right:6px;display:inline-block}.el-tour__content .el-tour-indicator.is-active{background:var(--el-color-primary)}.el-tour.el-tour--primary{--el-tour-title-text-color:#fff;--el-tour-text-color:#fff;--el-tour-bg-color:var(--el-color-primary);--el-tour-close-color:#fff}.el-tour.el-tour--primary .el-tour__closebtn:focus .el-tour__close,.el-tour.el-tour--primary .el-tour__closebtn:hover .el-tour__close{color:var(--el-tour-title-text-color)}.el-tour.el-tour--primary .el-button--default{color:var(--el-color-primary);border-color:var(--el-color-primary);background:#fff}.el-tour.el-tour--primary .el-button--primary{border-color:#fff}.el-tour.el-tour--primary .el-tour-indicator{background:#ffffff26}.el-tour.el-tour--primary .el-tour-indicator.is-active{background:#fff}.el-tour-parent--hidden{overflow:hidden}.el-anchor{--el-anchor-bg-color:var(--el-bg-color);--el-anchor-padding-indent:14px;--el-anchor-line-height:22px;--el-anchor-font-size:12px;--el-anchor-color:var(--el-text-color-secondary);--el-anchor-active-color:var(--el-color-primary);--el-anchor-hover-color:var(--el-text-color-regular);--el-anchor-marker-bg-color:var(--el-color-primary);background-color:var(--el-anchor-bg-color);position:relative}.el-anchor__marker{background-color:var(--el-anchor-marker-bg-color);opacity:0;z-index:0;border-radius:4px;position:absolute}.el-anchor.el-anchor--vertical .el-anchor__marker{width:4px;height:14px;transition:top .25s ease-in-out,opacity .25s;top:8px;left:0}.el-anchor.el-anchor--vertical .el-anchor__list{padding-left:var(--el-anchor-padding-indent)}.el-anchor.el-anchor--vertical.el-anchor--underline:before{content:"";background-color:#0505050f;width:2px;height:100%;position:absolute;left:0}.el-anchor.el-anchor--vertical.el-anchor--underline .el-anchor__marker{border-radius:unset;width:2px}.el-anchor.el-anchor--horizontal .el-anchor__marker{width:20px;height:2px;transition:left .25s ease-in-out,opacity .25s,width .25s;bottom:0}.el-anchor.el-anchor--horizontal .el-anchor__list{padding-bottom:4px;display:flex}.el-anchor.el-anchor--horizontal .el-anchor__list .el-anchor__item{padding-left:16px}.el-anchor.el-anchor--horizontal .el-anchor__list .el-anchor__item:first-child{padding-left:0}.el-anchor.el-anchor--horizontal.el-anchor--underline:before{content:"";background-color:#0505050f;width:100%;height:2px;position:absolute;bottom:0}.el-anchor.el-anchor--horizontal.el-anchor--underline .el-anchor__marker{border-radius:unset;height:2px}.el-anchor__item{flex-direction:column;display:flex}.el-anchor__link{font-size:var(--el-anchor-font-size);line-height:var(--el-anchor-line-height);color:var(--el-anchor-color);transition:color var(--el-transition-duration);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;outline:none;max-width:100%;padding:4px 0;text-decoration:none;overflow:hidden}.el-anchor__link:hover,.el-anchor__link:focus{color:var(--el-hover-color)}.el-anchor__link:focus-visible{border-radius:var(--el-border-radius-base);outline:2px solid var(--el-color-primary)}.el-anchor__link.is-active{color:var(--el-anchor-active-color)}.el-anchor .el-anchor__list .el-anchor__item a{display:inline-block}.el-segmented--vertical{flex-direction:column}.el-segmented--vertical .el-segmented__item{padding:11px}.el-segmented{--el-segmented-color:var(--el-text-color-regular);--el-segmented-bg-color:var(--el-fill-color-light);--el-segmented-padding:2px;--el-segmented-item-selected-color:var(--el-color-white);--el-segmented-item-selected-bg-color:var(--el-color-primary);--el-segmented-item-selected-disabled-bg-color:var(--el-color-primary-light-5);--el-segmented-item-hover-color:var(--el-text-color-primary);--el-segmented-item-hover-bg-color:var(--el-fill-color-dark);--el-segmented-item-active-bg-color:var(--el-fill-color-darker);--el-segmented-item-disabled-color:var(--el-text-color-placeholder);background:var(--el-segmented-bg-color);min-height:32px;padding:var(--el-segmented-padding);border-radius:var(--el-border-radius-base);color:var(--el-segmented-color);box-sizing:border-box;align-items:stretch;font-size:14px;display:inline-flex}.el-segmented__group{align-items:stretch;width:100%;display:flex;position:relative}.el-segmented__item-selected{background:var(--el-segmented-item-selected-bg-color);border-radius:calc(var(--el-border-radius-base) - 2px);pointer-events:none;width:10px;height:100%;transition:all .3s;position:absolute;top:0;left:0}.el-segmented__item-selected.is-disabled{background:var(--el-segmented-item-selected-disabled-bg-color)}.el-segmented__item-selected.is-focus-visible:before{content:"";border-radius:inherit;outline:2px solid var(--el-segmented-item-selected-bg-color);outline-offset:1px;position:absolute;top:0;bottom:0;left:0;right:0}.el-segmented__item{cursor:pointer;border-radius:calc(var(--el-border-radius-base) - 2px);flex:1;align-items:center;padding:0 11px;display:flex}.el-segmented__item:not(.is-disabled):not(.is-selected):hover{color:var(--el-segmented-item-hover-color);background:var(--el-segmented-item-hover-bg-color)}.el-segmented__item:not(.is-disabled):not(.is-selected):active{background:var(--el-segmented-item-active-bg-color)}.el-segmented__item.is-selected,.el-segmented__item.is-selected.is-disabled{color:var(--el-segmented-item-selected-color)}.el-segmented__item.is-disabled{cursor:not-allowed;color:var(--el-segmented-item-disabled-color)}.el-segmented__item-input{opacity:0;pointer-events:none;width:0;height:0;margin:0;position:absolute}.el-segmented__item-label{text-align:center;text-overflow:ellipsis;white-space:nowrap;z-index:1;flex:1;line-height:normal;transition:color .3s;overflow:hidden}.el-segmented.is-block{display:flex}.el-segmented.is-block .el-segmented__item{min-width:0}.el-segmented--large{border-radius:var(--el-border-radius-base);min-height:40px;font-size:16px}.el-segmented--large .el-segmented__item-selected{border-radius:calc(var(--el-border-radius-base) - 2px)}.el-segmented--large .el-segmented--vertical .el-segmented__item{padding:11px}.el-segmented--large .el-segmented__item{border-radius:calc(var(--el-border-radius-base) - 2px);padding:0 11px}.el-segmented--small{border-radius:calc(var(--el-border-radius-base) - 1px);min-height:24px;font-size:14px}.el-segmented--small .el-segmented__item-selected{border-radius:calc(calc(var(--el-border-radius-base) - 1px) - 2px)}.el-segmented--small .el-segmented--vertical .el-segmented__item{padding:7px}.el-segmented--small .el-segmented__item{border-radius:calc(calc(var(--el-border-radius-base) - 1px) - 2px);padding:0 7px}.el-mention{width:100%;position:relative}.el-mention__popper.el-popper{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light);box-shadow:var(--el-box-shadow-light)}.el-mention__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-mention__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-top-color:#0000;border-left-color:#0000}.el-mention__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:#0000;border-right-color:#0000}.el-mention__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:#0000;border-left-color:#0000}.el-mention__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-top-color:#0000;border-right-color:#0000}.el-mention-dropdown{--el-mention-font-size:var(--el-font-size-base);--el-mention-bg-color:var(--el-bg-color-overlay);--el-mention-shadow:var(--el-box-shadow-light);--el-mention-border:1px solid var(--el-border-color-light);--el-mention-option-color:var(--el-text-color-regular);--el-mention-option-height:34px;--el-mention-option-min-width:100px;--el-mention-option-hover-background:var(--el-fill-color-light);--el-mention-option-selected-color:var(--el-color-primary);--el-mention-option-disabled-color:var(--el-text-color-placeholder);--el-mention-option-loading-color:var(--el-text-color-secondary);--el-mention-option-loading-padding:10px 0;--el-mention-max-height:174px;--el-mention-padding:6px 0;--el-mention-header-padding:10px;--el-mention-footer-padding:10px}.el-mention-dropdown__item{font-size:var(--el-mention-font-size);white-space:nowrap;text-overflow:ellipsis;color:var(--el-mention-option-color);height:var(--el-mention-option-height);line-height:var(--el-mention-option-height);box-sizing:border-box;min-width:var(--el-mention-option-min-width);cursor:pointer;padding:0 20px;position:relative;overflow:hidden}.el-mention-dropdown__item.is-hovering{background-color:var(--el-mention-option-hover-background)}.el-mention-dropdown__item.is-selected{color:var(--el-mention-option-selected-color);font-weight:700}.el-mention-dropdown__item.is-disabled{color:var(--el-mention-option-disabled-color);cursor:not-allowed;background-color:unset}.el-mention-dropdown{z-index:calc(var(--el-index-top) + 1);border-radius:var(--el-border-radius-base);box-sizing:border-box}.el-mention-dropdown__loading{text-align:center;color:var(--el-mention-option-loading-color);min-width:var(--el-mention-option-min-width);margin:0;padding:10px 0;font-size:12px}.el-mention-dropdown__wrap{max-height:var(--el-mention-max-height)}.el-mention-dropdown__list{padding:var(--el-mention-padding);box-sizing:border-box;margin:0;list-style:none}.el-mention-dropdown__header{padding:var(--el-mention-header-padding);border-bottom:var(--el-mention-border)}.el-mention-dropdown__footer{padding:var(--el-mention-footer-padding);border-top:var(--el-mention-border)}.el-splitter{width:100%;height:100%;margin:0;padding:0;display:flex;position:relative}.el-splitter__mask{z-index:999;position:absolute;top:0;bottom:0;left:0;right:0}.el-splitter__mask-horizontal{cursor:ew-resize}.el-splitter__mask-vertical{cursor:ns-resize}.el-splitter__horizontal{flex-direction:row}.el-splitter__vertical{flex-direction:column}.el-splitter-bar{-webkit-user-select:none;user-select:none;flex:none;position:relative}.el-splitter-bar__dragger{z-index:1;background:0 0;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-splitter-bar__dragger:before,.el-splitter-bar__dragger:after{content:"";background-color:var(--el-border-color-light);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.el-splitter-bar__dragger:not(.is-lazy):after{display:none}.el-splitter-bar__dragger:after{opacity:.4}.el-splitter-bar__dragger:hover:not(.is-disabled):before{background-color:var(--el-color-primary-light-5)}.el-splitter-bar__dragger-horizontal:before,.el-splitter-bar__dragger-horizontal:after{width:2px;height:100%}.el-splitter-bar__dragger-vertical:before,.el-splitter-bar__dragger-vertical:after{width:100%;height:2px}.el-splitter-bar__dragger-active:before,.el-splitter-bar__dragger-active:after{background-color:var(--el-color-primary-light-3)}.el-splitter-bar__dragger-active.el-splitter-bar__dragger-horizontal:after{transform:translate(calc(-50% + var(--el-splitter-bar-offset)),-50%)}.el-splitter-bar__dragger-active.el-splitter-bar__dragger-vertical:after{transform:translate(-50%,calc(-50% + var(--el-splitter-bar-offset)))}.el-splitter-bar:hover .el-splitter-bar__collapse-icon{opacity:1}.el-splitter-bar__collapse-icon{background:var(--el-border-color-light);cursor:pointer;opacity:0;z-index:9;border-radius:2px;justify-content:center;align-items:center;display:flex;position:absolute}.el-splitter-bar__collapse-icon:hover{opacity:1;background-color:var(--el-color-primary-light-5)}.el-splitter-bar__horizontal-collapse-icon-start{width:16px;height:24px;top:50%;left:-12px;transform:translate(-50%,-50%)}.el-splitter-bar__horizontal-collapse-icon-end{width:16px;height:24px;top:50%;left:12px;transform:translate(-50%,-50%)}.el-splitter-bar__vertical-collapse-icon-start{width:24px;height:16px;top:-12px;right:50%;transform:translate(50%,-50%)}.el-splitter-bar__vertical-collapse-icon-end{width:24px;height:16px;top:12px;right:50%;transform:translate(50%,-50%)}.el-splitter-panel{scrollbar-width:thin;box-sizing:border-box;flex-grow:0;overflow:auto}html.dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--el-color-primary:#409eff;--el-color-primary-light-3:#3375b9;--el-color-primary-light-5:#2a598a;--el-color-primary-light-7:#213d5b;--el-color-primary-light-8:#1d3043;--el-color-primary-light-9:#18222b;--el-color-primary-dark-2:#66b1ff;--el-color-success:#67c23a;--el-color-success-light-3:#4e8e2f;--el-color-success-light-5:#3e6b27;--el-color-success-light-7:#2d481f;--el-color-success-light-8:#25371c;--el-color-success-light-9:#1c2518;--el-color-success-dark-2:#85ce61;--el-color-warning:#e6a23c;--el-color-warning-light-3:#a77730;--el-color-warning-light-5:#7d5b28;--el-color-warning-light-7:#533f20;--el-color-warning-light-8:#3e301c;--el-color-warning-light-9:#292218;--el-color-warning-dark-2:#ebb563;--el-color-danger:#f56c6c;--el-color-danger-light-3:#b25252;--el-color-danger-light-5:#854040;--el-color-danger-light-7:#582e2e;--el-color-danger-light-8:#412626;--el-color-danger-light-9:#2a1d1d;--el-color-danger-dark-2:#f78989;--el-color-error:#f56c6c;--el-color-error-light-3:#b25252;--el-color-error-light-5:#854040;--el-color-error-light-7:#582e2e;--el-color-error-light-8:#412626;--el-color-error-light-9:#2a1d1d;--el-color-error-dark-2:#f78989;--el-color-info:#909399;--el-color-info-light-3:#6b6d71;--el-color-info-light-5:#525457;--el-color-info-light-7:#393a3c;--el-color-info-light-8:#2d2d2f;--el-color-info-light-9:#202121;--el-color-info-dark-2:#a6a9ad;--el-box-shadow:0px 12px 32px 4px #0000005c, 0px 8px 20px #000000b8;--el-box-shadow-light:0px 0px 12px #000000b8;--el-box-shadow-lighter:0px 0px 6px #000000b8;--el-box-shadow-dark:0px 16px 48px 16px #000000b8, 0px 12px 32px #000, 0px 8px 16px -8px #000;--el-bg-color-page:#0a0a0a;--el-bg-color:#141414;--el-bg-color-overlay:#1d1e1f;--el-text-color-primary:#e5eaf3;--el-text-color-regular:#cfd3dc;--el-text-color-secondary:#a3a6ad;--el-text-color-placeholder:#8d9095;--el-text-color-disabled:#6c6e72;--el-border-color-darker:#636466;--el-border-color-dark:#58585b;--el-border-color:#4c4d4f;--el-border-color-light:#414243;--el-border-color-lighter:#363637;--el-border-color-extra-light:#2b2b2c;--el-fill-color-darker:#424243;--el-fill-color-dark:#39393a;--el-fill-color:#303030;--el-fill-color-light:#262727;--el-fill-color-lighter:#1d1d1d;--el-fill-color-extra-light:#191919;--el-fill-color-blank:#141414;--el-mask-color:#000c;--el-mask-color-extra-light:#0000004d}html.dark .el-button{--el-button-disabled-text-color:#ffffff80}html.dark .el-card{--el-card-bg-color:var(--el-bg-color-overlay);--el-fill-color-blank:var(--el-card-bg-color)}html.dark .el-empty{--el-empty-fill-color-0:var(--el-color-black);--el-empty-fill-color-1:#4b4b52;--el-empty-fill-color-2:#36383d;--el-empty-fill-color-3:#1e1e20;--el-empty-fill-color-4:#262629;--el-empty-fill-color-5:#202124;--el-empty-fill-color-6:#212224;--el-empty-fill-color-7:#1b1c1f;--el-empty-fill-color-8:#1c1d1f;--el-empty-fill-color-9:#18181a}.login-page[data-v-40e3134b]{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg-base);transition:background .18s ease}.login-card[data-v-40e3134b]{background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:36px 32px 28px;width:360px;box-shadow:0 2px 12px #0000000f;transition:background .18s ease,border-color .18s ease,box-shadow .18s ease}.login-header[data-v-40e3134b]{text-align:center;margin-bottom:28px}.login-logo[data-v-40e3134b]{width:44px;height:44px;border-radius:10px;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;margin:0 auto 12px}.login-title[data-v-40e3134b]{margin:0 0 5px;font-size:18px;font-weight:590;color:var(--text-primary);letter-spacing:-.02em}.login-sub[data-v-40e3134b]{margin:0;font-size:12px;color:var(--text-muted);font-weight:410}.mb12[data-v-40e3134b]{margin-bottom:12px}[data-v-40e3134b] .login-input .el-input__wrapper{background:var(--bg-subtle)!important;border:1px solid var(--border)!important;box-shadow:none!important;border-radius:6px!important;padding:2px 12px!important;transition:all .12s ease!important}[data-v-40e3134b] .login-input .el-input__wrapper:hover{border-color:var(--border-strong)!important}[data-v-40e3134b] .login-input .el-input__wrapper.is-focus{border-color:var(--accent)!important;box-shadow:0 0 0 2px var(--accent-light)!important;background:var(--bg-surface)!important}[data-v-40e3134b] .login-input .el-input__inner{color:var(--text-primary)!important;font-size:13px!important}[data-v-40e3134b] .login-input .el-input__inner::placeholder{color:var(--text-placeholder)!important}[data-v-40e3134b] .login-input .el-input__prefix .el-icon{color:var(--text-muted)!important;font-size:14px!important}.login-btn[data-v-40e3134b]{height:40px!important;font-size:13px!important;font-weight:590!important;border-radius:6px!important;border:none!important;background:var(--accent)!important;color:#fff!important;letter-spacing:.02em!important;transition:background .12s ease!important}.login-btn[data-v-40e3134b]:hover{background:var(--accent-hover)!important}.login-btn[data-v-40e3134b]:active{background:var(--accent)!important;transform:scale(.99)}.login-error[data-v-40e3134b]{display:flex;align-items:center;justify-content:center;gap:6px;color:var(--danger);font-size:12px;margin-top:10px;padding:8px 12px;background:var(--danger-bg);border:1px solid var(--danger-border);border-radius:6px}.dash[data-v-a6ea8b68]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:20px}.kpi-grid[data-v-a6ea8b68]{display:grid;grid-template-columns:repeat(5,1fr);gap:14px}.kpi-card[data-v-a6ea8b68]{display:flex;flex-direction:column;padding:18px 20px;cursor:default}.kpi-label[data-v-a6ea8b68]{font-size:11px;font-weight:510;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;line-height:1.2}.kpi-value[data-v-a6ea8b68]{font-size:24px;font-weight:590;color:var(--text-primary);letter-spacing:-.02em;line-height:1.2;margin-top:6px;font-feature-settings:"tnum"}.kpi-delta[data-v-a6ea8b68]{font-size:11px;color:var(--text-muted);margin-top:4px;font-weight:400}.chart-row[data-v-a6ea8b68]{display:grid;grid-template-columns:2fr 1fr;gap:14px}.chart-card[data-v-a6ea8b68]{overflow:hidden}.chart-title[data-v-a6ea8b68]{font-weight:590;font-size:13px;color:var(--text-primary)}.trend-controls[data-v-a6ea8b68]{display:flex;align-items:center;gap:10px}.chart-box[data-v-a6ea8b68]{height:220px;padding:8px 8px 10px}.status-row[data-v-a6ea8b68]{display:grid;grid-template-columns:1fr 1fr;gap:14px}.status-card[data-v-a6ea8b68]{overflow:hidden}.tunnel-badge[data-v-a6ea8b68]{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--danger);font-weight:510}.tunnel-badge.ok[data-v-a6ea8b68]{color:var(--success)}.tunnel-badge .dot[data-v-a6ea8b68]{width:6px;height:6px;border-radius:50%;background:currentColor}.tunnel-info[data-v-a6ea8b68]{display:flex;align-items:center;gap:12px}.tunnel-icon[data-v-a6ea8b68]{color:var(--text-muted);flex-shrink:0}.tunnel-url[data-v-a6ea8b68]{font-size:13px;color:var(--text-body);word-break:break-all;font-feature-settings:"tnum"}.tunnel-cb[data-v-a6ea8b68]{font-size:11px;color:var(--text-muted);margin-top:2px;word-break:break-all}.queue-grid[data-v-a6ea8b68]{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.queue-item[data-v-a6ea8b68]{text-align:center;padding:12px 6px;background:var(--bg-subtle);border-radius:6px}.queue-num[data-v-a6ea8b68]{font-size:18px;font-weight:590;color:var(--text-primary);font-feature-settings:"tnum"}.queue-label[data-v-a6ea8b68]{font-size:10px;color:var(--text-muted);margin-top:4px;font-weight:510;text-transform:uppercase;letter-spacing:.05em}.rate-label[data-v-a6ea8b68]{font-size:11px;margin-top:10px;text-align:center}.ledger-grid[data-v-a6ea8b68]{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.ledger-item[data-v-a6ea8b68]{text-align:center;padding:14px 6px;background:var(--bg-subtle);border-radius:6px}.ledger-num[data-v-a6ea8b68]{font-size:17px;font-weight:590;color:var(--text-primary)}.ledger-label[data-v-a6ea8b68]{font-size:10px;color:var(--text-muted);margin-top:4px;font-weight:510;text-transform:uppercase;letter-spacing:.05em}.link-btn[data-v-a6ea8b68]{display:flex;align-items:center;gap:4px;border:none;background:transparent;color:var(--accent);font-size:12px;font-weight:510;cursor:pointer;padding:4px 8px;border-radius:4px;transition:background .12s ease}.link-btn[data-v-a6ea8b68]:hover{background:var(--accent-light)}.text-mono[data-v-a6ea8b68]{font-family:JetBrains Mono,SF Mono,Consolas,monospace;font-feature-settings:"tnum"}.text-muted[data-v-a6ea8b68]{color:var(--text-muted)}.text-success[data-v-a6ea8b68]{color:var(--success)}.text-warning[data-v-a6ea8b68]{color:var(--warning)}.text-danger[data-v-a6ea8b68]{color:var(--danger)}.font-mono[data-v-a6ea8b68]{font-family:JetBrains Mono,SF Mono,Consolas,monospace;font-feature-settings:"tnum"}.mt14[data-v-a6ea8b68]{margin-top:14px}@media (max-width: 1200px){.kpi-grid[data-v-a6ea8b68]{grid-template-columns:repeat(3,1fr)}.chart-row[data-v-a6ea8b68],.status-row[data-v-a6ea8b68]{grid-template-columns:1fr}}.channels-page[data-v-77316d40]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:16px}.section-label[data-v-77316d40]{font-weight:590;font-size:13px;color:var(--text-primary)}.channel-grid[data-v-77316d40]{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px}.channel-card[data-v-77316d40]{padding:16px;cursor:default}.channel-card[data-v-77316d40]:hover{border-color:var(--border-strong)}.channel-head[data-v-77316d40]{display:flex;align-items:center;gap:10px}.channel-logo[data-v-77316d40]{width:38px;height:38px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:600;color:#fff;background:var(--accent);flex-shrink:0}.channel-logo.wechat[data-v-77316d40]{background:#22a069}.channel-name-wrap[data-v-77316d40]{flex:1;min-width:0}.channel-name[data-v-77316d40]{font-size:13px;font-weight:590;color:var(--text-primary)}.channel-id[data-v-77316d40]{font-size:11px;font-feature-settings:"tnum"}.channel-stats[data-v-77316d40]{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:14px}.stat[data-v-77316d40]{text-align:center;padding:10px 4px;background:var(--bg-subtle);border-radius:6px}.stat-num[data-v-77316d40]{font-size:14px;font-weight:590;color:var(--text-primary);font-feature-settings:"tnum"}.stat-label[data-v-77316d40]{font-size:10px;color:var(--text-muted);margin-top:2px;font-weight:510;text-transform:uppercase;letter-spacing:.04em}.channel-foot[data-v-77316d40]{display:flex;justify-content:space-between;align-items:center;margin-top:14px;padding-top:12px;border-top:1px solid var(--border-subtle)}.step[data-v-77316d40]{padding:16px;border-radius:8px;background:var(--bg-subtle)}.step-num[data-v-77316d40]{width:22px;height:22px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:590;margin-bottom:10px}.step-title[data-v-77316d40]{font-size:13px;font-weight:590;margin-bottom:4px;color:var(--text-primary)}.step-desc[data-v-77316d40]{font-size:11px;color:var(--text-muted);line-height:1.5}.mode-switches[data-v-77316d40]{display:flex;flex-wrap:wrap;gap:8px}.mode-switch-item[data-v-77316d40]{display:flex;align-items:center;gap:4px}.mt10[data-v-77316d40]{margin-top:10px}.mt12[data-v-77316d40]{margin-top:12px}.mb16[data-v-77316d40]{margin-bottom:16px}.epay-info-card[data-v-77316d40]{border:1px solid var(--success-border)}.cfg-item[data-v-77316d40]{background:var(--bg-subtle);border-radius:6px;padding:12px 14px;margin-bottom:8px}.cfg-label[data-v-77316d40]{font-size:11px;color:var(--text-muted);margin-bottom:6px;font-weight:510;text-transform:uppercase;letter-spacing:.04em}.cfg-value[data-v-77316d40]{font-size:13px;word-break:break-all;display:flex;align-items:center;gap:8px}.cfg-value code[data-v-77316d40]{background:var(--bg-surface);padding:2px 8px;border-radius:4px;font-size:12px;border:1px solid var(--border);font-family:JetBrains Mono,monospace}.font-mono[data-v-77316d40]{font-family:JetBrains Mono,SF Mono,Consolas,monospace;font-feature-settings:"tnum"}.text-muted[data-v-77316d40]{color:var(--text-muted)}.orders-page[data-v-490d8a35]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:16px}.section-label[data-v-490d8a35]{font-weight:590;font-size:13px;color:var(--text-primary)}.filter-bar[data-v-490d8a35]{display:flex;gap:8px;flex-wrap:wrap;padding:14px 16px 0}.pager[data-v-490d8a35]{display:flex;justify-content:flex-end;padding:0 20px 16px}.mt10[data-v-490d8a35]{margin-top:10px}.mt16[data-v-490d8a35]{margin-top:16px}.detail-head[data-v-490d8a35]{display:flex;align-items:center;gap:12px;margin-bottom:20px}.detail-amount[data-v-490d8a35]{font-size:26px;font-weight:700;color:var(--text-primary)}.section-title[data-v-490d8a35]{margin:20px 0 12px;font-size:14px;font-weight:600;color:var(--text-primary)}.empty-tip[data-v-490d8a35]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px 0;gap:6px}.empty-text[data-v-490d8a35]{font-size:13px;color:var(--text-muted)}.empty-sub[data-v-490d8a35]{font-size:12px;color:var(--text-muted)}.font-mono[data-v-490d8a35]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.text-muted[data-v-490d8a35]{color:var(--text-muted)}.notify-page[data-v-52866146]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:12px}.section-label[data-v-52866146]{font-weight:590;font-size:13px;color:var(--text-primary)}.stat-grid[data-v-52866146]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.stat-card[data-v-52866146]{display:flex;align-items:center;gap:14px;padding:16px 18px}.stat-icon[data-v-52866146]{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.stat-icon.success[data-v-52866146]{background:var(--success-bg);color:var(--success)}.stat-icon.warn[data-v-52866146]{background:var(--warning-bg);color:var(--warning)}.stat-icon.danger[data-v-52866146]{background:var(--danger-bg);color:var(--danger)}.stat-icon.info[data-v-52866146]{background:var(--info-bg);color:var(--info)}.stat-num[data-v-52866146]{font-size:20px;font-weight:590;color:var(--text-primary);font-family:JetBrains Mono,monospace;font-feature-settings:"tnum"}.stat-label[data-v-52866146]{font-size:12px;color:var(--text-muted);margin-top:2px;font-weight:500}.mt12[data-v-52866146]{margin-top:12px}.payload-head[data-v-52866146]{display:flex;gap:10px;align-items:center;margin-bottom:12px}.payload[data-v-52866146]{background:var(--bg-subtle);padding:14px;border-radius:8px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;max-height:400px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin:0;color:var(--text-body);border:1px solid var(--border)}.font-mono[data-v-52866146]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.text-muted[data-v-52866146]{color:var(--text-muted)}.logs-page[data-v-d4a5eb1d]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:12px}.section-label[data-v-d4a5eb1d]{font-weight:590;font-size:13px;color:var(--text-primary)}.stat-grid[data-v-d4a5eb1d]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.stat-card[data-v-d4a5eb1d]{display:flex;align-items:center;gap:12px;padding:16px 18px}.stat-icon[data-v-d4a5eb1d]{width:34px;height:34px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.stat-icon.danger[data-v-d4a5eb1d]{background:var(--danger-bg);color:var(--danger)}.stat-icon.warn[data-v-d4a5eb1d]{background:var(--warning-bg);color:var(--warning)}.stat-icon.info[data-v-d4a5eb1d]{background:var(--info-bg);color:var(--info)}.stat-icon.primary[data-v-d4a5eb1d]{background:var(--accent-light);color:var(--accent)}.stat-num[data-v-d4a5eb1d]{font-size:20px;font-weight:590;color:var(--text-primary);font-family:JetBrains Mono,monospace;font-feature-settings:"tnum"}.stat-label[data-v-d4a5eb1d]{font-size:12px;color:var(--text-muted);margin-top:2px;font-weight:500}.log-stream[data-v-d4a5eb1d]{padding:6px 12px;max-height:520px;overflow-y:auto;font-family:JetBrains Mono,Fira Code,Consolas,monospace;font-size:12px}.log-line[data-v-d4a5eb1d]{display:flex;align-items:center;gap:10px;padding:5px 8px;border-radius:5px;transition:background .1s}.log-line[data-v-d4a5eb1d]:hover{background:var(--bg-hover)}.log-time[data-v-d4a5eb1d]{color:var(--text-muted);flex-shrink:0;font-size:11px}.log-level[data-v-d4a5eb1d]{flex-shrink:0;width:46px;text-align:center;padding:1px 5px;border-radius:4px;font-size:10px;font-weight:700}.log-level.lv-error[data-v-d4a5eb1d],.log-line.lv-error .log-time[data-v-d4a5eb1d]{color:var(--danger)}.log-level.lv-error[data-v-d4a5eb1d]{background:var(--danger-bg)}.log-level.lv-warn[data-v-d4a5eb1d],.log-line.lv-warn .log-time[data-v-d4a5eb1d]{color:var(--warning)}.log-level.lv-warn[data-v-d4a5eb1d]{background:var(--warning-bg)}.log-level.lv-info[data-v-d4a5eb1d],.log-line.lv-info .log-time[data-v-d4a5eb1d]{color:var(--info)}.log-level.lv-info[data-v-d4a5eb1d]{background:var(--info-bg)}.log-level.lv-debug[data-v-d4a5eb1d],.log-line.lv-debug .log-time[data-v-d4a5eb1d]{color:var(--text-muted)}.log-level.lv-debug[data-v-d4a5eb1d]{background:var(--bg-subtle)}.log-scope[data-v-d4a5eb1d]{color:var(--text-muted);flex-shrink:0;width:52px;font-size:11px}.log-msg[data-v-d4a5eb1d]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pager[data-v-d4a5eb1d]{display:flex;justify-content:flex-end;padding:0 16px 16px}.mt16[data-v-d4a5eb1d]{margin-top:16px}.empty-tip[data-v-d4a5eb1d]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 0;gap:8px}.empty-text[data-v-d4a5eb1d]{font-size:13px;color:var(--text-muted)}.empty-sub[data-v-d4a5eb1d]{font-size:12px;color:var(--text-muted)}.fields[data-v-d4a5eb1d]{background:var(--bg-subtle);padding:10px;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;margin:0;white-space:pre-wrap;word-break:break-all;border:1px solid var(--border)}.font-mono[data-v-d4a5eb1d]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.ledger-page[data-v-05660c6d]{max-width:1000px;margin:0 auto}.mb16[data-v-05660c6d]{margin-bottom:16px}.mt12[data-v-05660c6d]{margin-top:12px}.mt16[data-v-05660c6d]{margin-top:16px}.section-label[data-v-05660c6d]{font-weight:600;font-size:14px;color:var(--text-primary)}.action-bar[data-v-05660c6d]{display:flex;align-items:center;gap:10px;padding:14px 18px;flex-wrap:wrap}.verify-tag[data-v-05660c6d]{margin-left:auto}.verify-tag .tag-muted[data-v-05660c6d]{opacity:.6;margin-left:4px;font-size:11px}.chain[data-v-05660c6d]{display:flex;flex-direction:column;align-items:center;padding:8px 0}.chain-node[data-v-05660c6d]{width:100%;display:flex;align-items:center;gap:14px;padding:12px 16px;background:var(--bg-subtle);border-radius:8px;border:1px solid var(--border)}.chain-node.head[data-v-05660c6d]{border-color:var(--success);box-shadow:0 0 0 1px var(--success),0 4px 12px -4px #00875a33}.chain-badge[data-v-05660c6d]{width:36px;height:36px;border-radius:50%;background:var(--accent-gradient);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0}.chain-node.head .chain-badge[data-v-05660c6d]{background:var(--success)}.chain-body[data-v-05660c6d]{flex:1;min-width:0}.chain-row[data-v-05660c6d]{display:flex;gap:10px;font-size:12px;padding:2px 0;align-items:center}.chain-label[data-v-05660c6d]{color:var(--text-muted);width:66px;flex-shrink:0;font-weight:500}.chain-link[data-v-05660c6d]{color:var(--success);padding:3px 0;display:flex;justify-content:center}.snapshot-grid[data-v-05660c6d]{display:grid;gap:10px}.snapshot-card[data-v-05660c6d]{display:flex;align-items:flex-start;gap:14px;padding:14px;background:var(--bg-subtle);border-radius:8px;border:1px solid var(--border);position:relative}.snapshot-card[data-v-05660c6d]:hover{border-color:var(--accent)}.snapshot-date[data-v-05660c6d]{width:72px;height:56px;border-radius:8px;background:var(--accent-gradient);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;flex-shrink:0;text-align:center;line-height:1.3}.snapshot-body[data-v-05660c6d]{flex:1;min-width:0}.sn-row[data-v-05660c6d]{display:flex;gap:10px;font-size:12px;padding:2px 0;align-items:center}.sn-label[data-v-05660c6d]{color:var(--text-muted);width:66px;flex-shrink:0;font-weight:500}.detail-icon[data-v-05660c6d]{position:absolute;right:14px;top:50%;transform:translateY(-50%);opacity:.5}.verify-result[data-v-05660c6d]{text-align:center}.verify-status[data-v-05660c6d]{display:flex;flex-direction:column;align-items:center;gap:8px;padding:20px;border-radius:10px}.verify-status.pass[data-v-05660c6d]{background:var(--success-bg);color:var(--success)}.verify-status.fail[data-v-05660c6d]{background:var(--danger-bg);color:var(--danger)}.verify-status-text[data-v-05660c6d]{font-size:18px;font-weight:590}.verify-errors[data-v-05660c6d]{text-align:left;background:var(--danger-bg);border:1px solid var(--danger-border);border-radius:8px;padding:14px}.verify-errors-title[data-v-05660c6d]{font-weight:590;font-size:13px;margin-bottom:8px;color:var(--danger)}.verify-error-item[data-v-05660c6d]{display:flex;align-items:center;gap:8px;font-size:12px;padding:4px 0;color:var(--danger)}.verify-loading[data-v-05660c6d]{min-height:80px;display:flex;align-items:center;justify-content:center}.pubkey-container[data-v-05660c6d]{padding:8px 0}.pubkey-block[data-v-05660c6d]{background:#1a1e2e;color:#c9d1d9;padding:14px;border-radius:8px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;max-height:280px;overflow-y:auto}.event-link[data-v-05660c6d]{color:var(--accent);cursor:pointer;text-decoration:underline;font-weight:600}.event-link[data-v-05660c6d]:hover{color:var(--accent-hover)}.snap-detail[data-v-05660c6d]{padding:8px 0}.anchor-match[data-v-05660c6d]{display:flex;align-items:center;gap:8px;padding:12px 14px;background:var(--success-bg);border-radius:8px;color:var(--success);font-weight:600;font-size:13px}.anchor-mismatch[data-v-05660c6d]{display:flex;align-items:center;gap:8px;padding:12px 14px;background:var(--danger-bg);border-radius:8px;color:var(--danger);font-weight:600;font-size:13px}.font-mono[data-v-05660c6d]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.hash[data-v-05660c6d]{font-size:11px;word-break:break-all}.muted[data-v-05660c6d]{color:var(--text-muted)}.font-muted[data-v-05660c6d]{color:var(--text-muted);font-size:12px}.accounts-page[data-v-e8a4a312]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:16px}.section-label[data-v-e8a4a312]{font-weight:590;font-size:13px;color:var(--text-primary)}.account-grid[data-v-e8a4a312]{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:12px}.account-card[data-v-e8a4a312]{padding:18px;cursor:default}.account-card[data-v-e8a4a312]:hover{border-color:var(--border-strong)}.account-head[data-v-e8a4a312]{display:flex;align-items:center;gap:10px}.account-logo[data-v-e8a4a312]{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:#fff;background:linear-gradient(135deg,#5c6ac4,#7c7fe4);flex-shrink:0}.account-logo.wechat[data-v-e8a4a312]{background:linear-gradient(135deg,#07c160,#3ddc84)}.account-info[data-v-e8a4a312]{flex:1;min-width:0}.account-name[data-v-e8a4a312]{font-size:14px;font-weight:600;color:var(--text-primary)}.account-channel[data-v-e8a4a312]{font-size:11px}.account-actions[data-v-e8a4a312]{display:flex;gap:2px;flex-wrap:wrap;margin-top:14px;padding-top:12px;border-top:1px solid var(--border)}.filter-bar[data-v-e8a4a312]{display:flex;gap:10px;align-items:center}.header-actions[data-v-e8a4a312]{display:flex;align-items:center;gap:10px}.mt10[data-v-e8a4a312]{margin-top:10px}.mt12[data-v-e8a4a312]{margin-top:12px}.ml8[data-v-e8a4a312]{margin-left:8px}.mb16[data-v-e8a4a312]{margin-bottom:16px}.log-item[data-v-e8a4a312]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.log-detail[data-v-e8a4a312]{font-size:13px}.log-operator[data-v-e8a4a312]{font-size:11px}.text-muted[data-v-e8a4a312]{color:var(--text-muted)}.text-danger[data-v-e8a4a312]{color:var(--danger)}.font-mono[data-v-e8a4a312]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.qr-upload-row[data-v-e8a4a312]{display:flex;align-items:center;gap:10px;margin-top:8px;flex-wrap:wrap}.secret-box[data-v-e8a4a312]{display:flex;flex-direction:column;gap:10px}.secret-item[data-v-e8a4a312]{background:var(--bg-subtle);border-radius:6px;padding:10px 12px}.secret-label[data-v-e8a4a312]{font-size:11px;color:var(--text-muted);margin-bottom:6px;font-weight:510;text-transform:uppercase;letter-spacing:.04em}.secret-value[data-v-e8a4a312]{font-size:13px;display:flex;align-items:center;gap:10px}.secret-value .font-mono[data-v-e8a4a312]{flex:1;min-width:0;font-size:12px}.upstreams-page[data-v-cefda71d]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:16px}.section-label[data-v-cefda71d]{font-weight:590;font-size:13px;color:var(--text-primary)}.mb16[data-v-cefda71d]{margin-bottom:16px}.mt10[data-v-cefda71d]{margin-top:10px}.upstream-grid[data-v-cefda71d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}.upstream-card[data-v-cefda71d]{padding:16px;cursor:default}.upstream-card[data-v-cefda71d]:hover{border-color:var(--border-strong)}.upstream-head[data-v-cefda71d]{display:flex;align-items:center;gap:10px;margin-bottom:12px}.upstream-logo[data-v-cefda71d]{width:36px;height:36px;border-radius:9px;background:var(--accent-gradient);color:#fff;font-weight:700;font-size:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.upstream-info[data-v-cefda71d]{flex:1;min-width:0}.upstream-name[data-v-cefda71d]{font-weight:600;font-size:14px;color:var(--text-primary)}.upstream-pid[data-v-cefda71d]{font-size:11px;margin-top:2px}.upstream-actions[data-v-cefda71d]{display:flex;gap:2px;margin-top:12px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:10px}.log-item[data-v-cefda71d]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.log-detail[data-v-cefda71d]{font-size:13px}.log-operator[data-v-cefda71d]{font-size:11px}.text-muted[data-v-cefda71d]{color:var(--text-muted)}.font-mono[data-v-cefda71d]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.header-actions[data-v-cefda71d]{display:flex;align-items:center;gap:10px}.setup-steps[data-v-cefda71d]{margin-top:16px}.guide-steps[data-v-cefda71d]{display:flex;flex-direction:column;gap:14px}.guide-step[data-v-cefda71d]{display:flex;gap:12px}.guide-step-num[data-v-cefda71d]{width:24px;height:24px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:590;flex-shrink:0;margin-top:1px}.guide-step-body[data-v-cefda71d]{flex:1;min-width:0}.guide-step-title[data-v-cefda71d]{font-weight:590;font-size:13px;color:var(--text-primary);margin-bottom:4px}.guide-step-desc[data-v-cefda71d]{font-size:12px;color:var(--text-body);line-height:1.7}.guide-step-desc code[data-v-cefda71d]{background:var(--bg-subtle);padding:1px 6px;border-radius:4px;font-family:JetBrains Mono,Consolas,monospace;font-size:11px}.guide-callback[data-v-cefda71d]{margin-top:6px;padding:8px 10px;background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text-primary);word-break:break-all}.guide-note[data-v-cefda71d]{margin-top:8px;padding:8px 10px;background:var(--warning-bg, #fff7e6);border:1px solid var(--warning, #e6a23c);border-radius:6px;font-size:12px;color:#b88230;line-height:1.7}.guide-note code[data-v-cefda71d]{background:#0000000f;padding:1px 5px;border-radius:4px}.help-text[data-v-cefda71d]{font-size:12px;color:var(--text-muted);line-height:1.7}.help-text code[data-v-cefda71d]{background:var(--bg-subtle);padding:1px 5px;border-radius:4px}.reconciler-page[data-v-e25116cd]{max-width:1400px;margin:0 auto}.filter-bar[data-v-e25116cd]{display:flex;gap:8px;flex-wrap:wrap;padding:14px 16px 0}.pager[data-v-e25116cd]{display:flex;justify-content:flex-end;padding:0 16px 16px}.mt10[data-v-e25116cd]{margin-top:10px}.mt16[data-v-e25116cd]{margin-top:16px}.section-title[data-v-e25116cd]{margin:18px 0 10px;font-size:14px;font-weight:590;color:var(--text-primary)}.evidence-box[data-v-e25116cd]{background:var(--bg-subtle);padding:12px;border-radius:6px;font-size:12px;white-space:pre-wrap;max-height:200px;overflow-y:auto;border:1px solid var(--border);margin:0}.report-head[data-v-e25116cd]{display:flex;align-items:center;gap:8px;margin-bottom:12px}.report-stats[data-v-e25116cd]{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}.stat-box[data-v-e25116cd]{padding:16px;text-align:center}.stat-val[data-v-e25116cd]{font-size:20px;font-weight:590;color:var(--text-primary);font-family:JetBrains Mono,monospace;font-feature-settings:"tnum"}.stat-label[data-v-e25116cd]{font-size:11px;color:var(--text-muted);margin-top:4px;font-weight:500}.empty-text[data-v-e25116cd]{text-align:center;padding:40px 0;color:var(--text-muted);font-size:13px}.font-mono[data-v-e25116cd]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.text-muted[data-v-e25116cd]{color:var(--text-muted)}.text-success[data-v-e25116cd]{color:var(--success)}.text-warning[data-v-e25116cd]{color:var(--warning)}.text-danger[data-v-e25116cd]{color:var(--danger)}.device-stream[data-v-c99e0b77]{display:flex;flex-direction:column;gap:10px;height:100%}.stream-wrap[data-v-c99e0b77]{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;overflow:hidden}.stream-stage[data-v-c99e0b77]{position:relative;background:#000;border-radius:10px;overflow:hidden;height:100%;aspect-ratio:9 / 16;max-width:100%}.stream-canvas[data-v-c99e0b77]{width:100%;height:100%;display:block;cursor:crosshair;touch-action:none}.stream-overlay[data-v-c99e0b77]{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;gap:12px;align-items:center;justify-content:center;color:#999;background:#0000004d}.stream-msg[data-v-c99e0b77]{font-size:13px;color:#bbb}.personal-devices-page[data-v-3b83dd8d]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:16px}.section-label[data-v-3b83dd8d]{font-weight:600;font-size:14px;color:var(--text-primary)}.mb16[data-v-3b83dd8d]{margin-bottom:16px}.header-actions[data-v-3b83dd8d]{display:flex;align-items:center;gap:10px}.font-mono[data-v-3b83dd8d]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.text-muted[data-v-3b83dd8d]{color:var(--text-muted)}.device-grid[data-v-3b83dd8d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px}.phone-card[data-v-3b83dd8d]{background:var(--bg-surface);border-radius:14px;border:1px solid var(--border);padding:16px 16px 12px;cursor:pointer;transition:box-shadow .2s,transform .2s;text-align:center;position:relative}.phone-card[data-v-3b83dd8d]:hover{box-shadow:0 4px 16px #0000001a;transform:translateY(-2px)}.phone-frame[data-v-3b83dd8d]{width:100%;max-width:180px;margin:0 auto;background:#1a1a1a;border-radius:20px;padding:6px;position:relative;box-shadow:0 2px 8px #00000040}.phone-notch[data-v-3b83dd8d]{width:80px;height:16px;background:#1a1a1a;border-radius:0 0 10px 10px;margin:-6px auto 0}.phone-screen[data-v-3b83dd8d]{aspect-ratio:9 / 16;background:#111;border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center;margin:0;position:relative}.phone-screenshot[data-v-3b83dd8d]{width:100%;height:100%;object-fit:cover;display:block}.phone-placeholder[data-v-3b83dd8d]{text-align:center;padding:24px 8px;color:#555}.phone-placeholder-icon[data-v-3b83dd8d]{font-size:32px;margin-bottom:4px}.phone-placeholder-name[data-v-3b83dd8d]{font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.phone-home[data-v-3b83dd8d]{width:30px;height:30px;border:1.5px solid #333;border-radius:50%;margin:4px auto 0;cursor:pointer}.phone-info[data-v-3b83dd8d]{margin-top:10px}.phone-name[data-v-3b83dd8d]{font-weight:600;font-size:13px;color:var(--text-primary);margin-bottom:4px}.phone-meta[data-v-3b83dd8d]{display:flex;align-items:center;justify-content:center;gap:4px;flex-wrap:wrap}.snapshot-tag[data-v-3b83dd8d]{font-size:10px;color:var(--text-muted)}.phone-actions[data-v-3b83dd8d]{display:flex;justify-content:center;gap:2px;margin-top:6px;flex-wrap:wrap}.empty-state[data-v-3b83dd8d]{grid-column:1 / -1;text-align:center;padding:60px 0}.console-wrap[data-v-3b83dd8d]{display:flex;flex-direction:column;gap:12px;height:640px}.console-header[data-v-3b83dd8d]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.console-device-info[data-v-3b83dd8d]{font-size:13px}.console-stream[data-v-3b83dd8d]{flex:1;min-height:0}.integration-guide[data-v-3b83dd8d]{margin-top:4px;border-top:1px dashed var(--border);padding-top:8px}.integration-guide[data-v-3b83dd8d] .el-collapse-item__header{font-weight:500}.integration-guide[data-v-3b83dd8d] .el-collapse-item__wrap{padding-bottom:20px;border-bottom:none}.integration-guide[data-v-3b83dd8d] .el-collapse-item__content{padding-bottom:0}.guide-title[data-v-3b83dd8d]{margin-right:8px;color:var(--text-primary)}.guide-tag[data-v-3b83dd8d]{margin-left:auto}.guide-content[data-v-3b83dd8d]{padding:12px 0 4px 8px}.step-list[data-v-3b83dd8d]{margin:4px 0 0;padding-left:20px;line-height:1.8;color:var(--text-primary)}.step-list li[data-v-3b83dd8d]{margin-bottom:4px}.step-list code[data-v-3b83dd8d]{background:var(--bg-surface);padding:1px 6px;border-radius:4px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px}.mt8[data-v-3b83dd8d]{margin-top:8px;margin-bottom:16px}.phone-checkbox[data-v-3b83dd8d]{position:absolute;top:8px;left:8px;z-index:10}.phone-card-selected[data-v-3b83dd8d]{border-color:var(--el-color-warning);box-shadow:0 0 0 2px var(--el-color-warning-light-7)}.batch-actions-bar[data-v-3b83dd8d]{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;margin-top:16px;background:var(--bg-surface);border-radius:8px;border:1px solid var(--border)}.batch-actions-left[data-v-3b83dd8d]{display:flex;align-items:center;gap:12px}.batch-actions-right[data-v-3b83dd8d]{display:flex;align-items:center;gap:8px}.wizard-body[data-v-3b83dd8d]{min-height:200px}.usb-device-list[data-v-3b83dd8d]{display:flex;flex-direction:column;gap:8px}.usb-device-item[data-v-3b83dd8d]{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:border-color .2s,background .2s}.usb-device-item[data-v-3b83dd8d]:hover{border-color:var(--el-color-primary);background:var(--el-fill-color-light)}.usb-device-item-selected[data-v-3b83dd8d]{border-color:var(--el-color-success);background:var(--el-color-success-light-9)}.usb-device-info[data-v-3b83dd8d]{display:flex;flex-direction:column;gap:2px}.usb-device-serial[data-v-3b83dd8d]{font-size:13px;color:var(--text-primary)}.usb-device-right[data-v-3b83dd8d]{display:flex;align-items:center}.unmatched-page[data-v-1df8350a]{max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:16px}.section-label[data-v-1df8350a]{font-weight:600;font-size:14px;color:var(--text-primary)}.mb16[data-v-1df8350a]{margin-bottom:16px}.header-actions[data-v-1df8350a]{display:flex;align-items:center;gap:10px}.raw-cell[data-v-1df8350a]{max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12px;color:var(--text-muted)}.font-mono[data-v-1df8350a]{font-family:JetBrains Mono,Fira Code,Consolas,monospace}.text-muted[data-v-1df8350a]{color:var(--text-muted)}.text-danger[data-v-1df8350a]{color:var(--danger)}.settings-page[data-v-49bef0e4]{max-width:800px;margin:0 auto}.section-label[data-v-49bef0e4]{font-weight:590;font-size:13px;color:var(--text-primary)}.mb16[data-v-49bef0e4]{margin-bottom:16px}.mt16[data-v-49bef0e4]{margin-top:16px}.sec-title[data-v-49bef0e4]{font-size:14px;margin:0 0 12px;color:var(--text-primary);font-weight:600}.form-tip[data-v-49bef0e4]{font-size:11px;color:var(--text-muted);margin-top:4px}.saved-hint[data-v-49bef0e4]{display:inline-flex;align-items:center;gap:4px;margin-left:12px;color:var(--success);font-size:13px;font-weight:500}.channel-grid[data-v-49bef0e4]{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.channel-switch[data-v-49bef0e4]{display:flex;align-items:center;gap:14px;padding:12px 16px;background:var(--bg-subtle);border-radius:8px;border:1px solid var(--border);min-width:150px}.channel-switch.disabled[data-v-49bef0e4]{opacity:.5}.ch-name[data-v-49bef0e4]{font-weight:600;font-size:13px;color:var(--text-primary)}.ch-status[data-v-49bef0e4]{font-size:11px;color:var(--text-muted);margin-top:2px}.mb12[data-v-49bef0e4]{margin-bottom:12px}.mt12[data-v-49bef0e4]{margin-top:12px}.key-grid[data-v-49bef0e4]{display:flex;flex-direction:column;gap:10px}.key-item[data-v-49bef0e4]{background:var(--bg-subtle);border:1px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}.key-label[data-v-49bef0e4]{font-size:12px;color:var(--text-muted);font-weight:510;width:180px;flex-shrink:0}.key-value[data-v-49bef0e4]{flex:1;min-width:160px;font-size:12px;color:var(--text-primary);word-break:break-all;font-family:JetBrains Mono,Fira Code,Consolas,monospace}.key-actions[data-v-49bef0e4]{display:flex;gap:2px;flex-shrink:0}.admin-item[data-v-49bef0e4]{display:flex;align-items:center;justify-content:space-between;gap:14px;background:var(--bg-subtle);border:1px solid var(--border);border-radius:8px;padding:12px 14px}.admin-label[data-v-49bef0e4]{font-size:11px;color:var(--text-muted);margin-bottom:4px;font-weight:510}.admin-value[data-v-49bef0e4]{font-size:13px;color:var(--text-primary)}.qrcode-grid[data-v-9290643a]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;padding:16px 0}.qrcode-card[data-v-9290643a]{background:#fff;border-radius:8px;border:1px solid var(--el-border-color-lighter);padding:16px;transition:box-shadow .2s}.qrcode-card[data-v-9290643a]:hover{box-shadow:0 2px 12px #00000014}:root.dark .qrcode-card[data-v-9290643a]{background:var(--el-bg-color-overlay)}.card-header[data-v-9290643a]{display:flex;gap:8px;align-items:center;margin-bottom:12px}.device-name[data-v-9290643a]{font-weight:600;font-size:14px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-account[data-v-9290643a]{font-size:13px;color:#666;margin-bottom:10px;padding-left:2px}.card-account .account-name[data-v-9290643a]{color:var(--el-color-primary);font-weight:500}.card-account .account-unknown[data-v-9290643a]{color:#bbb}.card-body[data-v-9290643a]{display:flex;justify-content:center;margin-bottom:12px}.qr-area[data-v-9290643a]{text-align:center}.qr-img[data-v-9290643a]{width:180px;height:180px;display:block;margin:0 auto;border:1px solid #eee;border-radius:4px}.qr-deadline[data-v-9290643a]{margin-top:8px;font-size:13px;color:#999}.qr-deadline .urgent[data-v-9290643a]{color:#f56c6c;font-weight:600}.no-qr[data-v-9290643a]{text-align:center;color:#ccc;font-size:14px;padding:40px 0}.card-footer[data-v-9290643a]{display:flex;justify-content:center}.empty[data-v-9290643a]{margin-top:60px}.mb-3[data-v-9290643a]{margin-bottom:12px}.app-shell[data-v-b117130e]{display:flex;height:100vh;overflow:hidden}.sidebar[data-v-b117130e]{width:var(--sidebar-width);flex-shrink:0;display:flex;flex-direction:column;background:var(--bg-sidebar);border-right:1px solid var(--border);transition:background .18s ease,border-color .18s ease}.brand[data-v-b117130e]{display:flex;align-items:center;gap:10px;padding:14px 14px 12px;border-bottom:1px solid var(--border)}.brand-logo[data-v-b117130e]{width:30px;height:30px;border-radius:7px;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0}.brand-name[data-v-b117130e]{font-size:13px;font-weight:590;color:var(--text-primary);letter-spacing:-.01em;line-height:1.2}.brand-sub[data-v-b117130e]{font-size:11px;color:var(--text-muted);line-height:1.2;margin-top:1px}.nav[data-v-b117130e]{flex:1;padding:8px;display:flex;flex-direction:column;gap:1px;overflow-y:auto}.nav-item[data-v-b117130e]{display:flex;align-items:center;gap:8px;padding:0 10px;height:32px;border:none;background:transparent;border-radius:6px;color:var(--text-muted);font-size:12px;font-weight:410;cursor:pointer;transition:background .12s ease,color .12s ease;text-align:left;width:100%;position:relative;letter-spacing:.01em}.nav-item[data-v-b117130e]:hover{background:var(--bg-hover);color:var(--text-body)}.nav-item.active[data-v-b117130e]{background:var(--accent-light);color:var(--accent);font-weight:510}.nav-item.active[data-v-b117130e]:before{content:"";position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:0 2px 2px 0;background:var(--accent)}.nav-badge[data-v-b117130e]{margin-left:auto;font-size:10px;font-weight:590;padding:1px 5px;border-radius:4px;background:var(--danger-bg);color:var(--danger);border:1px solid var(--danger-border);letter-spacing:.02em}.nav-badge.warn[data-v-b117130e]{background:var(--warning-bg);color:var(--warning);border-color:var(--warning-border)}.sidebar-foot[data-v-b117130e]{padding:10px 14px;border-top:1px solid var(--border);margin-top:auto}.tunnel-mini[data-v-b117130e]{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--text-muted);font-weight:410}.tunnel-mini .dot[data-v-b117130e]{width:6px;height:6px;border-radius:50%;background:var(--danger);flex-shrink:0;transition:background .2s}.tunnel-mini.ok .dot[data-v-b117130e]{background:var(--success)}.tunnel-mini.ok .tunnel-text[data-v-b117130e]{color:var(--success)}.main-area[data-v-b117130e]{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--bg-base)}.topbar[data-v-b117130e]{height:var(--topbar-height);flex-shrink:0;display:flex;align-items:center;justify-content:space-between;padding:0 20px;background:var(--bg-topbar);border-bottom:1px solid var(--border);transition:background .18s ease,border-color .18s ease}.page-title[data-v-b117130e]{font-size:13px;font-weight:590;color:var(--text-primary);letter-spacing:-.01em}.topbar-right[data-v-b117130e]{display:flex;align-items:center;gap:4px}.icon-btn[data-v-b117130e]{width:30px;height:30px;border:1px solid transparent;background:transparent;border-radius:6px;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s ease}.icon-btn[data-v-b117130e]:hover{color:var(--accent);border-color:var(--border);background:var(--bg-hover)}.avatar[data-v-b117130e]{width:28px;height:28px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:590;font-size:12px;cursor:pointer;margin-left:4px;letter-spacing:.02em}.content[data-v-b117130e]{flex:1;overflow-y:auto;padding:20px 24px}.page-enter-active[data-v-b117130e]{transition:opacity .15s ease,transform .15s cubic-bezier(.16,1,.3,1)}.page-leave-active[data-v-b117130e]{transition:opacity .1s ease}.page-enter-from[data-v-b117130e]{opacity:0;transform:translateY(4px)}.page-leave-to[data-v-b117130e]{opacity:0}:root{--bg-base: #FAFAFA;--bg-surface: #FFFFFF;--bg-subtle: #F4F4F5;--bg-hover: rgba(0, 0, 0, .04);--bg-active: rgba(0, 0, 0, .06);--bg-sidebar: #FFFFFF;--bg-topbar: #FFFFFF;--border: #E4E4E7;--border-subtle: rgba(0, 0, 0, .06);--border-strong: #D1D1D6;--text-primary: #09090B;--text-body: #3F3F46;--text-muted: #71717A;--text-placeholder: #A1A1AA;--text-inverse: #FFFFFF;--accent: #7170FF;--accent-hover: #8284FF;--accent-light: rgba(113, 112, 255, .08);--accent-muted: rgba(113, 112, 255, .14);--success: #16A34A;--success-bg: rgba(22, 163, 74, .08);--success-border: rgba(22, 163, 74, .18);--warning: #B45309;--warning-bg: rgba(180, 83, 9, .08);--warning-border: rgba(180, 83, 9, .18);--danger: #DC2626;--danger-bg: rgba(220, 38, 38, .08);--danger-border: rgba(220, 38, 38, .18);--info: #2563EB;--info-bg: rgba(37, 99, 235, .08);--info-border: rgba(37, 99, 235, .18);--shadow-xs: 0 1px 2px rgba(0, 0, 0, .04);--shadow-sm: 0 1px 4px rgba(0, 0, 0, .06), 0 0 0 1px rgba(0, 0, 0, .04);--shadow-md: 0 2px 8px rgba(0, 0, 0, .08), 0 0 0 1px rgba(0, 0, 0, .04);--sidebar-width: 200px;--topbar-height: 48px;--card-radius: 8px;--input-radius: 6px;--btn-radius: 6px;--el-color-primary: #7170FF;--el-color-primary-light-3: #9899FF;--el-color-primary-light-5: #B5B5FF;--el-color-primary-light-7: #D5D5FF;--el-color-primary-light-8: #E8E8FF;--el-color-primary-light-9: #F4F4FF;--el-color-primary-dark-2: #5B5BD6;--el-color-success: #22A069;--el-color-warning: #C27803;--el-color-danger: #D92D20;--el-color-info: #0A6FD4;--el-border-radius-base: 6px;--el-border-radius-small: 4px;--el-border-radius-round: 9999px;--el-font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--el-font-size-base: 13px;--el-text-color-primary: var(--text-primary);--el-text-color-regular: var(--text-body);--el-text-color-secondary: var(--text-muted);--el-text-color-placeholder: var(--text-placeholder);--el-border-color: var(--border);--el-border-color-light: var(--border);--el-border-color-lighter: var(--border);--el-fill-color-blank: var(--bg-surface);--el-fill-color-light: var(--bg-subtle);--el-fill-color-lighter: var(--bg-base);--el-mask-color: rgba(0, 0, 0, .15);--el-overlay-color: rgba(0, 0, 0, .15)}html.dark{--bg-base: #0D0D0F;--bg-surface: #18181B;--bg-subtle: #222226;--bg-hover: rgba(255, 255, 255, .05);--bg-active: rgba(255, 255, 255, .08);--bg-sidebar: #111114;--bg-topbar: #18181B;--border: rgba(255, 255, 255, .08);--border-subtle: rgba(255, 255, 255, .05);--border-strong: rgba(255, 255, 255, .14);--text-primary: #FAFAFA;--text-body: #C4C4C8;--text-muted: #71717A;--text-placeholder: #52525B;--text-inverse: #09090B;--accent: #7170FF;--accent-hover: #8284FF;--accent-light: rgba(113, 112, 255, .12);--accent-muted: rgba(113, 112, 255, .2);--success: #22C55E;--success-bg: rgba(34, 197, 94, .1);--success-border: rgba(34, 197, 94, .22);--warning: #D97706;--warning-bg: rgba(217, 119, 6, .1);--warning-border: rgba(217, 119, 6, .22);--danger: #EF4444;--danger-bg: rgba(239, 68, 68, .1);--danger-border: rgba(239, 68, 68, .22);--info: #3B82F6;--info-bg: rgba(59, 130, 246, .1);--info-border: rgba(59, 130, 246, .22);--shadow-sm: 0 1px 4px rgba(0, 0, 0, .3), 0 0 0 1px rgba(255, 255, 255, .04);--shadow-md: 0 2px 8px rgba(0, 0, 0, .4), 0 0 0 1px rgba(255, 255, 255, .04);--el-color-primary: #7170FF;--el-color-primary-light-3: #9899FF;--el-color-primary-light-5: #B5B5FF;--el-color-primary-light-7: #D5D5FF;--el-color-primary-light-8: #5B5BD6;--el-color-primary-light-9: #18181B;--el-color-primary-dark-2: #5B5BD6;--el-bg-color: #18181B;--el-bg-color-overlay: #222226;--el-fill-color-blank: #18181B;--el-text-color-primary: var(--text-primary);--el-text-color-regular: var(--text-body);--el-text-color-secondary: var(--text-muted);--el-text-color-placeholder: var(--text-placeholder);--el-border-color: var(--border);--el-border-color-light: var(--border);--el-border-color-lighter: var(--border);--el-fill-color-light: #222226;--el-fill-color-lighter: #18181B;--el-mask-color: rgba(0, 0, 0, .65);--el-overlay-color: rgba(0, 0, 0, .55)}*,*:before,*:after{box-sizing:border-box}html{font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;font-feature-settings:"cv01","ss03"}html,body{height:100%;margin:0;padding:0}body{background:var(--bg-base);color:var(--text-body);transition:background .18s ease,color .18s ease}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:3px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}::selection{background:var(--accent-light);color:var(--accent)}html.dark ::selection{background:#7170ff40;color:var(--text-primary)}.font-mono{font-family:JetBrains Mono,SF Mono,Consolas,monospace;font-feature-settings:normal;font-variant-numeric:tabular-nums}.font-medium{font-weight:510}.font-semibold{font-weight:590}.font-bold{font-weight:700}.text-primary{color:var(--text-primary)}.text-body{color:var(--text-body)}.text-muted{color:var(--text-muted)}.text-accent{color:var(--accent)}.text-success{color:var(--success)}.text-warning{color:var(--warning)}.text-danger{color:var(--danger)}.text-info{color:var(--info)}.pg-card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--card-radius);transition:background .15s ease,border-color .15s ease}.pg-card:hover{border-color:var(--border-strong)}html.dark .pg-card:hover{border-color:#ffffff1f}.pg-card-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border-subtle);font-size:13px;font-weight:590;color:var(--text-primary)}.pg-card-body{padding:16px}.el-table{--el-table-bg-color: transparent;--el-table-tr-bg-color: transparent;--el-table-header-bg-color: var(--bg-subtle);--el-table-row-hover-bg-color: var(--bg-hover);--el-table-border-color: var(--border-subtle);--el-table-text-color: var(--text-body);--el-table-header-text-color: var(--text-muted);border:none!important;border-radius:0}.el-table__inner-wrapper:before{display:none}.el-table th.el-table__cell{font-size:11px;font-weight:590;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted)!important;background:var(--bg-subtle)!important;border-bottom:1px solid var(--border-subtle)!important;padding:8px 12px!important}.el-table td.el-table__cell{font-size:13px;color:var(--text-body);padding:10px 12px!important;border-bottom:1px solid var(--border-subtle)!important}.el-table__body tr:hover>td.el-table__cell{background:var(--bg-hover)!important}.el-table__body tr:last-child>td.el-table__cell{border-bottom:none!important}.el-table__empty-text{color:var(--text-muted);font-size:13px}.el-tag{border-radius:4px!important;font-size:11px!important;font-weight:590!important;letter-spacing:.02em;padding:0 6px!important;height:20px!important;line-height:18px!important;border:1px solid!important}.el-tag--success{background:var(--success-bg)!important;border-color:var(--success-border)!important;color:var(--success)!important}.el-tag--warning{background:var(--warning-bg)!important;border-color:var(--warning-border)!important;color:var(--warning)!important}.el-tag--danger{background:var(--danger-bg)!important;border-color:var(--danger-border)!important;color:var(--danger)!important}.el-tag--info{background:var(--info-bg)!important;border-color:var(--info-border)!important;color:var(--info)!important}html.dark .el-tag--success{background:var(--success-bg)!important;color:#4ade80!important}html.dark .el-tag--danger{background:var(--danger-bg)!important;color:#f87171!important}.el-button{border-radius:var(--btn-radius)!important;font-weight:510!important;font-size:13px!important;height:32px!important;padding:0 14px!important;transition:all .12s cubic-bezier(.16,1,.3,1)!important;border:1px solid var(--border)!important;background:var(--bg-surface)!important;color:var(--text-body)!important}.el-button:hover{border-color:var(--border-strong)!important;background:var(--bg-hover)!important;color:var(--text-primary)!important}.el-button:active{transform:scale(.98)}.el-button--primary{background:var(--accent)!important;border-color:var(--accent)!important;color:var(--text-inverse)!important}.el-button--primary:hover{background:var(--accent-hover)!important;border-color:var(--accent-hover)!important;color:var(--text-inverse)!important}.el-button--primary:active{background:var(--accent)!important;transform:scale(.98)}.el-button--danger{background:var(--danger)!important;border-color:var(--danger)!important;color:var(--text-inverse)!important}.el-button--small{height:28px!important;padding:0 10px!important;font-size:12px!important}.el-button--small.is-icon--inline{width:28px!important;padding:0!important}.el-button--text{border:none!important;background:transparent!important;color:var(--accent)!important;padding:0 6px!important}.el-button--text:hover{background:var(--accent-light)!important;color:var(--accent-hover)!important}.el-input__wrapper{border-radius:var(--input-radius)!important;box-shadow:0 0 0 1px var(--border) inset!important;background:var(--bg-subtle)!important;transition:box-shadow .12s ease!important;padding:0 12px!important}.el-input__wrapper:hover{box-shadow:0 0 0 1px var(--border-strong) inset!important}.el-input__wrapper.is-focus{box-shadow:0 0 0 2px var(--accent-light),0 0 0 1px var(--accent) inset!important;background:var(--bg-surface)!important}.el-input__inner{font-size:13px!important;color:var(--text-primary)!important}.el-input__prefix .el-icon{color:var(--text-muted)!important}.el-select .el-input__wrapper{border-radius:var(--input-radius)!important}.el-dialog{border-radius:10px!important;border:1px solid var(--border)!important;box-shadow:var(--shadow-md)!important;overflow:hidden}.el-dialog__header{padding:16px 20px 12px!important;border-bottom:1px solid var(--border-subtle)!important;margin-right:0!important}.el-dialog__title{font-weight:590;font-size:14px;color:var(--text-primary)}.el-dialog__body{padding:20px!important}.el-dialog__footer{padding:12px 20px 16px!important;border-top:1px solid var(--border-subtle)!important}.el-drawer__header{padding:16px 20px 12px!important;border-bottom:1px solid var(--border-subtle)!important;margin-bottom:0!important;font-weight:590;font-size:14px;color:var(--text-primary)}.el-drawer__body{padding:16px 20px!important}.el-pagination{--el-pagination-hover-color: var(--accent);font-weight:510;font-size:13px}.el-pagination.is-background .el-pager li:not(.is-disabled).is-active{background:var(--accent)!important;color:var(--text-inverse)!important}.el-pager li{font-weight:510;border-radius:4px}.el-tabs__nav-wrap:after{display:none}.el-tabs__item{font-weight:510;font-size:13px;color:var(--text-muted);transition:color .12s ease}.el-tabs__item:hover{color:var(--text-primary)}.el-tabs__item.is-active{color:var(--accent);font-weight:590}.el-tabs__active-bar{background:var(--accent);transition:all .18s cubic-bezier(.16,1,.3,1)}.el-alert{border-radius:6px!important;border:1px solid!important;padding:10px 14px!important}.el-alert--success{background:var(--success-bg)!important;border-color:var(--success-border)!important;color:var(--success)!important}.el-alert--warning{background:var(--warning-bg)!important;border-color:var(--warning-border)!important;color:var(--warning)!important}.el-alert--error{background:var(--danger-bg)!important;border-color:var(--danger-border)!important;color:var(--danger)!important}.el-alert--info{background:var(--info-bg)!important;border-color:var(--info-border)!important;color:var(--info)!important}.el-alert__title{font-size:12px!important;font-weight:500!important}html.dark .el-alert--success{color:#4ade80!important}html.dark .el-alert--error{color:#f87171!important}.el-form-item__label{font-weight:510;font-size:13px;color:var(--text-body)}.el-descriptions__cell{padding:8px 12px!important}.el-descriptions__label{color:var(--text-muted)!important;font-weight:510;font-size:12px}.el-descriptions__content{color:var(--text-body);font-size:13px}.el-progress__text{font-weight:590;font-size:11px!important}.el-progress-bar__outer{background:var(--bg-subtle)!important}.el-step__icon{border:none!important}.el-timeline-item__node{background:var(--accent)!important;border-color:var(--accent)!important}.el-empty__description{color:var(--text-muted);font-size:13px}.el-popper.is-light{border:1px solid var(--border)!important;border-radius:6px!important;background:var(--bg-surface)!important;color:var(--text-body)!important;font-size:12px!important;box-shadow:var(--shadow-sm)!important}.el-popper.is-light .el-popper__arrow:before{background:var(--bg-surface)!important;border-color:var(--border)!important}.el-message{border-radius:8px!important;border:1px solid var(--border)!important;background:var(--bg-surface)!important;box-shadow:var(--shadow-md)!important}.el-notification{border-radius:8px!important;border:1px solid var(--border)!important;background:var(--bg-surface)!important}.el-radio-button__inner{border-radius:4px!important;font-weight:510!important;font-size:12px!important}.el-radio-button__original-radio:checked+.el-radio-button__inner{background-color:var(--accent)!important;border-color:var(--accent)!important;color:var(--text-inverse)!important;box-shadow:none!important}.el-switch.is-checked .el-switch__core{background-color:var(--accent)!important;border-color:var(--accent)!important}.el-dropdown-menu{border:1px solid var(--border)!important;border-radius:8px!important;background:var(--bg-surface)!important;box-shadow:var(--shadow-md)!important;padding:4px!important}.el-dropdown-menu__item{font-size:13px!important;font-weight:510!important;color:var(--text-body)!important;border-radius:4px!important;padding:6px 10px!important}.el-dropdown-menu__item:hover{background:var(--bg-hover)!important;color:var(--text-primary)!important}.el-date-editor{border-radius:var(--input-radius)!important}.el-picker-panel{border:1px solid var(--border)!important;border-radius:8px!important;background:var(--bg-surface)!important}.mt8{margin-top:8px}.mt10{margin-top:10px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.gap8{gap:8px}.gap12{gap:12px}.gap16{gap:16px} diff --git a/internal/admin/static/assets/index-Bdo7HwZH.js b/internal/admin/static/assets/index-Bdo7HwZH.js new file mode 100644 index 0000000..2b9396e --- /dev/null +++ b/internal/admin/static/assets/index-Bdo7HwZH.js @@ -0,0 +1,127 @@ +var uV=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var lze=uV((no,ao)=>{(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))a(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&a(i)}).observe(document,{childList:!0,subtree:!0});function n(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function a(r){if(r.ep)return;r.ep=!0;const o=n(r);fetch(r.href,o)}})();/** +* @vue/shared v3.5.41 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function $3(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const jn={},hd=[],kt=()=>{},cE=()=>!1,a1=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),r1=e=>e.startsWith("onUpdate:"),ka=Object.assign,R3=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},cV=Object.prototype.hasOwnProperty,en=(e,t)=>cV.call(e,t),Ae=Array.isArray,pd=e=>zh(e)==="[object Map]",o1=e=>zh(e)==="[object Set]",Is=e=>zh(e)==="[object Date]",Qe=e=>typeof e=="function",Ye=e=>typeof e=="string",io=e=>typeof e=="symbol",_t=e=>e!==null&&typeof e=="object",Ls=e=>(_t(e)||Qe(e))&&Qe(e.then)&&Qe(e.catch),dE=Object.prototype.toString,zh=e=>dE.call(e),dV=e=>zh(e).slice(8,-1),qv=e=>zh(e)==="[object Object]",i1=e=>Ye(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Sv=$3(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),l1=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},fV=/-\w/g,Va=l1(e=>e.replace(fV,t=>t.slice(1).toUpperCase())),vV=/\B([A-Z])/g,$l=l1(e=>e.replace(vV,"-$1").toLowerCase()),Fh=l1(e=>e.charAt(0).toUpperCase()+e.slice(1)),Wu=l1(e=>e?`on${Fh(e)}`:""),Mi=(e,t)=>!Object.is(e,t),G0=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:a,value:n})},N3=e=>{const t=parseFloat(e);return isNaN(t)?e:t},hV=e=>{const t=Ye(e)?Number(e):NaN;return isNaN(t)?e:t};let TS;const s1=()=>TS||(TS=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function et(e){if(Ae(e)){const t={};for(let n=0;n{if(n){const a=n.split(mV);a.length>1&&(t[a[0].trim()]=a[1].trim())}}),t}function U(e){let t="";if(Ye(e))t=e;else if(Ae(e))for(let n=0;nrc(n,t))}const pE=e=>!!(e&&e.__v_isRef===!0),Q=e=>Ye(e)?e:e==null?"":Ae(e)||_t(e)&&(e.toString===dE||!Qe(e.toString))?pE(e)?Q(e.value):JSON.stringify(e,mE,2):String(e),mE=(e,t)=>pE(t)?mE(e,t.value):pd(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[a,r],o)=>(n[k2(a,o)+" =>"]=r,n),{})}:o1(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>k2(n))}:io(t)?k2(t):_t(t)&&!Ae(t)&&!qv(t)?String(t):t,k2=(e,t="")=>{var n;return io(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.41 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let La;class gE{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&La&&(La.active?(this.parent=La,this.index=(La.scopes||(La.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes){const a=this.scopes.slice();for(t=0,n=a.length;t0&&--this._on===0){if(La===this)La=this.prevScope;else{let t=La;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,a;for(n=0,a=this.effects.length;n0)return;if(xv){let t=xv;for(xv=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Cv;){let t=Cv;for(Cv=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(a){e||(e=a)}t=n}}if(e)throw e}function SE(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function CE(e){let t,n=e.depsTail,a=n;for(;a;){const r=a.prevDep;a.version===-1?(a===n&&(n=r),F3(a),CV(a)):t=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=r}e.deps=t,e.depsTail=n}function Gb(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(xE(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function xE(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Xv)||(e.globalVersion=Xv,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Gb(e))))return;e.flags|=2;const t=e.dep,n=ta,a=qo;ta=e,qo=!0;try{SE(e);const r=e.fn(e._value);(t.version===0||Mi(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{ta=n,qo=a,CE(e),e.flags&=-3}}function F3(e,t=!1){const{dep:n,prevSub:a,nextSub:r}=e;if(a&&(a.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=a,e.nextSub=void 0),n.subs===e&&(n.subs=a,!a&&n.computed)){n.computed.flags&=-5;for(let o=n.computed.deps;o;o=o.nextDep)F3(o,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function CV(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let qo=!0;const kE=[];function kl(){kE.push(qo),qo=!1}function Tl(){const e=kE.pop();qo=e===void 0?!0:e}function MS(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ta;ta=void 0;try{t()}finally{ta=n}}}let Xv=0;class xV{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class H3{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!ta||!qo||ta===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ta)n=this.activeLink=new xV(ta,this),ta.deps?(n.prevDep=ta.depsTail,ta.depsTail.nextDep=n,ta.depsTail=n):ta.deps=ta.depsTail=n,TE(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const a=n.nextDep;a.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=a),n.prevDep=ta.depsTail,n.nextDep=void 0,ta.depsTail.nextDep=n,ta.depsTail=n,ta.deps===n&&(ta.deps=a)}return n}trigger(t){this.version++,Xv++,this.notify(t)}notify(t){V3();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{z3()}}}function TE(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let a=t.deps;a;a=a.nextDep)TE(a)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Im=new WeakMap,Ku=Symbol(""),Yb=Symbol(""),Zv=Symbol("");function nr(e,t,n){if(qo&&ta){let a=Im.get(e);a||Im.set(e,a=new Map);let r=a.get(n);r||(a.set(n,r=new H3),r.map=a,r.key=n),r.track()}}function hl(e,t,n,a,r,o){const i=Im.get(e);if(!i){Xv++;return}const l=s=>{s&&s.trigger()};if(V3(),t==="clear")i.forEach(l);else{const s=Ae(e),u=s&&i1(n);if(s&&n==="length"){const c=Number(a);i.forEach((d,v)=>{(v==="length"||v===Zv||!io(v)&&v>=c)&&l(d)})}else switch((n!==void 0||i.has(void 0))&&l(i.get(n)),u&&l(i.get(Zv)),t){case"add":s?u&&l(i.get("length")):(l(i.get(Ku)),pd(e)&&l(i.get(Yb)));break;case"delete":s||(l(i.get(Ku)),pd(e)&&l(i.get(Yb)));break;case"set":pd(e)&&l(i.get(Ku));break}}z3()}function kV(e,t){const n=Im.get(e);return n&&n.get(t)}function Pc(e){const t=wn(e);return t===e?t:(nr(t,"iterate",Zv),ro(e)?t:t.map(Qo))}function u1(e){return nr(e=wn(e),"iterate",Zv),e}function ki(e,t){return Ml(e)?Pd(Uu(e)?Qo(t):t):Qo(t)}const TV={__proto__:null,[Symbol.iterator](){return M2(this,Symbol.iterator,e=>ki(this,e))},concat(...e){return Pc(this).concat(...e.map(t=>Ae(t)?Pc(t):t))},entries(){return M2(this,"entries",e=>(e[1]=ki(this,e[1]),e))},every(e,t){return ol(this,"every",e,t,void 0,arguments)},filter(e,t){return ol(this,"filter",e,t,n=>n.map(a=>ki(this,a)),arguments)},find(e,t){return ol(this,"find",e,t,n=>ki(this,n),arguments)},findIndex(e,t){return ol(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return ol(this,"findLast",e,t,n=>ki(this,n),arguments)},findLastIndex(e,t){return ol(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return ol(this,"forEach",e,t,void 0,arguments)},includes(...e){return E2(this,"includes",e)},indexOf(...e){return E2(this,"indexOf",e)},join(e){return Pc(this).join(e)},lastIndexOf(...e){return E2(this,"lastIndexOf",e)},map(e,t){return ol(this,"map",e,t,void 0,arguments)},pop(){return Tf(this,"pop")},push(...e){return Tf(this,"push",e)},reduce(e,...t){return ES(this,"reduce",e,t)},reduceRight(e,...t){return ES(this,"reduceRight",e,t)},shift(){return Tf(this,"shift")},some(e,t){return ol(this,"some",e,t,void 0,arguments)},splice(...e){return Tf(this,"splice",e)},toReversed(){return Pc(this).toReversed()},toSorted(e){return Pc(this).toSorted(e)},toSpliced(...e){return Pc(this).toSpliced(...e)},unshift(...e){return Tf(this,"unshift",e)},values(){return M2(this,"values",e=>ki(this,e))}};function M2(e,t,n){const a=u1(e),r=a[t]();return a!==e&&!ro(e)&&(r._next=r.next,r.next=()=>{const o=r._next();return o.done||(o.value=n(o.value)),o}),r}const MV=Array.prototype;function ol(e,t,n,a,r,o){const i=u1(e),l=i!==e&&!ro(e),s=i[t];if(s!==MV[t]){const d=s.apply(e,o);return l?Qo(d):d}let u=n;i!==e&&(l?u=function(d,v){return n.call(this,ki(e,d),v,e)}:n.length>2&&(u=function(d,v){return n.call(this,d,v,e)}));const c=s.call(i,u,a);return l&&r?r(c):c}function ES(e,t,n,a){const r=u1(e),o=r!==e&&!ro(e);let i=n,l=!1;r!==e&&(o?(l=a.length===0,i=function(u,c,d){return l&&(l=!1,u=ki(e,u)),n.call(this,u,ki(e,c),d,e)}):n.length>3&&(i=function(u,c,d){return n.call(this,u,c,d,e)}));const s=r[t](i,...a);return l?ki(e,s):s}function E2(e,t,n){const a=wn(e);nr(a,"iterate",Zv);const r=a[t](...n);return(r===-1||r===!1)&&f1(n[0])?(n[0]=wn(n[0]),a[t](...n)):r}function Tf(e,t,n=[]){kl(),V3();const a=wn(e)[t].apply(e,n);return z3(),Tl(),a}const EV=$3("__proto__,__v_isRef,__isVue"),ME=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(io));function AV(e){io(e)||(e=String(e));const t=wn(this);return nr(t,"has",e),t.hasOwnProperty(e)}class EE{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,a){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return o;if(n==="__v_raw")return a===(r?o?OE:DE:o?PE:LE).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(a)?t:void 0;const i=Ae(t);if(!r){let s;if(i&&(s=TV[n]))return s;if(n==="hasOwnProperty")return AV}const l=Reflect.get(t,n,xn(t)?t:a);if((io(n)?ME.has(n):EV(n))||(r||nr(t,"get",n),o))return l;if(xn(l)){const s=i&&i1(n)?l:l.value;return r&&_t(s)?Ld(s):s}return _t(l)?r?Ld(l):nn(l):l}}class AE extends EE{constructor(t=!1){super(!1,t)}set(t,n,a,r){let o=t[n];const i=Ae(t)&&i1(n);if(!this._isShallow){const u=Ml(o);if(!ro(a)&&!Ml(a)&&(o=wn(o),a=wn(a)),!i&&xn(o)&&!xn(a))return u||(o.value=a),!0}const l=i?Number(n)e,Mp=e=>Reflect.getPrototypeOf(e);function OV(e,t,n){return function(...a){const r=this.__v_raw,o=wn(r),i=pd(o),l=e==="entries"||e===Symbol.iterator&&i,s=e==="keys"&&i,u=r[e](...a),c=n?jb:t?Pd:Qo;return!t&&nr(o,"iterate",s?Yb:Ku),ka(Object.create(u),{next(){const{value:d,done:v}=u.next();return v?{value:d,done:v}:{value:l?[c(d[0]),c(d[1])]:c(d),done:v}}})}}function Ep(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function $V(e,t){const n={get(r){const o=this.__v_raw,i=wn(o),l=wn(r);e||(Mi(r,l)&&nr(i,"get",r),nr(i,"get",l));const{has:s}=Mp(i),u=t?jb:e?Pd:Qo;if(s.call(i,r))return u(o.get(r));if(s.call(i,l))return u(o.get(l));o!==i&&o.get(r)},get size(){const r=this.__v_raw;return!e&&nr(wn(r),"iterate",Ku),r.size},has(r){const o=this.__v_raw,i=wn(o),l=wn(r);return e||(Mi(r,l)&&nr(i,"has",r),nr(i,"has",l)),r===l?o.has(r):o.has(r)||o.has(l)},forEach(r,o){const i=this,l=i.__v_raw,s=wn(l),u=t?jb:e?Pd:Qo;return!e&&nr(s,"iterate",Ku),l.forEach((c,d)=>r.call(o,u(c),u(d),i))}};return ka(n,e?{add:Ep("add"),set:Ep("set"),delete:Ep("delete"),clear:Ep("clear")}:{add(r){const o=wn(this),i=Mp(o),l=wn(r),s=!t&&!ro(r)&&!Ml(r)?l:r;return i.has.call(o,s)||Mi(r,s)&&i.has.call(o,r)||Mi(l,s)&&i.has.call(o,l)||(o.add(s),hl(o,"add",s,s)),this},set(r,o){!t&&!ro(o)&&!Ml(o)&&(o=wn(o));const i=wn(this),{has:l,get:s}=Mp(i);let u=l.call(i,r);u||(r=wn(r),u=l.call(i,r));const c=s.call(i,r);return i.set(r,o),u?Mi(o,c)&&hl(i,"set",r,o):hl(i,"add",r,o),this},delete(r){const o=wn(this),{has:i,get:l}=Mp(o);let s=i.call(o,r);s||(r=wn(r),s=i.call(o,r)),l&&l.call(o,r);const u=o.delete(r);return s&&hl(o,"delete",r,void 0),u},clear(){const r=wn(this),o=r.size!==0,i=r.clear();return o&&hl(r,"clear",void 0,void 0),i}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=OV(r,e,t)}),n}function c1(e,t){const n=$V(e,t);return(a,r,o)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?a:Reflect.get(en(n,r)&&r in a?n:a,r,o)}const RV={get:c1(!1,!1)},NV={get:c1(!1,!0)},BV={get:c1(!0,!1)},VV={get:c1(!0,!0)},LE=new WeakMap,PE=new WeakMap,DE=new WeakMap,OE=new WeakMap;function zV(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function nn(e){return Ml(e)?e:d1(e,!1,IV,RV,LE)}function W3(e){return d1(e,!1,PV,NV,PE)}function Ld(e){return d1(e,!0,LV,BV,DE)}function K3(e){return d1(e,!0,DV,VV,OE)}function d1(e,t,n,a,r){if(!_t(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const o=r.get(e);if(o)return o;const i=zV(dV(e));if(i===0)return e;const l=new Proxy(e,i===2?a:n);return r.set(e,l),l}function Uu(e){return Ml(e)?Uu(e.__v_raw):!!(e&&e.__v_isReactive)}function Ml(e){return!!(e&&e.__v_isReadonly)}function ro(e){return!!(e&&e.__v_isShallow)}function f1(e){return e?!!e.__v_raw:!1}function wn(e){const t=e&&e.__v_raw;return t?wn(t):e}function $i(e){return!en(e,"__v_skip")&&Object.isExtensible(e)&&fE(e,"__v_skip",!0),e}const Qo=e=>_t(e)?nn(e):e,Pd=e=>_t(e)?Ld(e):e;function xn(e){return e?e.__v_isRef===!0:!1}function F(e){return $E(e,!1)}function Ht(e){return $E(e,!0)}function $E(e,t){return xn(e)?e:new FV(e,t)}class FV{constructor(t,n){this.dep=new H3,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:wn(t),this._value=n?t:Qo(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,a=this.__v_isShallow||ro(t)||Ml(t);t=a?t:wn(t),Mi(t,n)&&(this._rawValue=t,this._value=a?t:Qo(t),this.dep.trigger())}}function Y0(e){e.dep&&e.dep.trigger()}function p(e){return xn(e)?e.value:e}function ia(e){return Qe(e)?e():p(e)}const HV={get:(e,t,n)=>t==="__v_raw"?e:p(Reflect.get(e,t,n)),set:(e,t,n,a)=>{const r=e[t];return xn(r)&&!xn(n)?(r.value=n,!0):Reflect.set(e,t,n,a)}};function RE(e){return Uu(e)?e:new Proxy(e,HV)}function Ta(e){const t=Ae(e)?new Array(e.length):{};for(const n in e)t[n]=NE(e,n);return t}class WV{constructor(t,n,a){this._object=t,this._defaultValue=a,this.__v_isRef=!0,this._value=void 0,this._key=io(n)?n:String(n),this._raw=wn(t);let r=!0,o=t;if(!Ae(t)||io(this._key)||!i1(this._key))do r=!f1(o)||ro(o);while(r&&(o=o.__v_raw));this._shallow=r}get value(){let t=this._object[this._key];return this._shallow&&(t=p(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&xn(this._raw[this._key])){const n=this._object[this._key];if(xn(n)){n.value=t;return}}this._object[this._key]=t}get dep(){return kV(this._raw,this._key)}}class KV{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function cn(e,t,n){return xn(e)?e:Qe(e)?new KV(e):_t(e)&&arguments.length>1?NE(e,t,n):F(e)}function NE(e,t,n){return new WV(e,t,n)}class UV{constructor(t,n,a){this.fn=t,this.setter=n,this._value=void 0,this.dep=new H3(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Xv-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=a}notify(){if(this.flags|=16,!(this.flags&8)&&ta!==this)return wE(this,!0),!0}get value(){const t=this.dep.track();return xE(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function GV(e,t,n=!1){let a,r;return Qe(e)?a=e:(a=e.get,r=e.set),new UV(a,r,n)}const Ap={},Lm=new WeakMap;let Mu;function YV(e,t=!1,n=Mu){if(n){let a=Lm.get(n);a||Lm.set(n,a=[]),a.push(e)}}function jV(e,t,n=jn){const{immediate:a,deep:r,once:o,scheduler:i,augmentJob:l,call:s}=n,u=w=>r?w:ro(w)||r===!1||r===0?pl(w,1):pl(w);let c,d,v,f,h=!1,m=!1;if(xn(e)?(d=()=>e.value,h=ro(e)):Uu(e)?(d=()=>u(e),h=!0):Ae(e)?(m=!0,h=e.some(w=>Uu(w)||ro(w)),d=()=>e.map(w=>{if(xn(w))return w.value;if(Uu(w))return u(w);if(Qe(w))return s?s(w,2):w()})):Qe(e)?t?d=s?()=>s(e,2):e:d=()=>{if(v){kl();try{v()}finally{Tl()}}const w=Mu;Mu=c;try{return s?s(e,3,[f]):e(f)}finally{Mu=w}}:d=kt,t&&r){const w=d,S=r===!0?1/0:r;d=()=>pl(w(),S)}const g=B3(),y=()=>{c.stop(),g&&g.active&&R3(g.effects,c)};if(o&&t){const w=t;t=(...S)=>{const k=w(...S);return y(),k}}let b=m?new Array(e.length).fill(Ap):Ap;const _=w=>{if(!(!(c.flags&1)||!c.dirty&&!w))if(t){const S=c.run();if(w||r||h||(m?S.some((k,T)=>Mi(k,b[T])):Mi(S,b))){v&&v();const k=Mu;Mu=c;try{const T=[S,b===Ap?void 0:m&&b[0]===Ap?[]:b,f];b=S,s?s(t,3,T):t(...T)}finally{Mu=k}}}else c.run()};return l&&l(_),c=new bE(d),c.scheduler=i?()=>i(_,!1):_,f=w=>YV(w,!1,c),v=c.onStop=()=>{const w=Lm.get(c);if(w){if(s)s(w,4);else for(const S of w)S();Lm.delete(c)}},t?a?_(!0):b=c.run():i?i(_.bind(null,!0),!0):c.run(),y.pause=c.pause.bind(c),y.resume=c.resume.bind(c),y.stop=y,y}function pl(e,t=1/0,n){if(t<=0||!_t(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,xn(e))pl(e.value,t,n);else if(Ae(e))for(let a=0;a{pl(a,t,n)});else if(qv(e)){for(const a in e)pl(e[a],t,n);for(const a of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,a)&&pl(e[a],t,n)}return e}/** +* @vue/runtime-core v3.5.41 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Hh(e,t,n,a){try{return a?e(...a):e()}catch(r){v1(r,t,n)}}function Ro(e,t,n,a){if(Qe(e)){const r=Hh(e,t,n,a);return r&&Ls(r)&&r.catch(o=>{v1(o,t,n)}),r}if(Ae(e)){const r=[];for(let o=0;o>>1,r=br[a],o=Jv(r);o=Jv(n)?br.push(e):br.splice(XV(t),0,e),e.flags|=1,VE()}}function VE(){Pm||(Pm=BE.then(FE))}function ZV(e){if(!Ae(e))ds&&e.id===-1?ds.splice(td+1,0,e):e.flags&1||(md.push(e),e.flags|=1);else for(let t=0;tJv(n)-Jv(a));if(md.length=0,ds){for(let n=0;ne.id==null?e.flags&2?-1:1/0:e.id;function FE(e){try{for(xi=0;xi{a._d&&Nm(-1);const o=Dm(t),i=_l.length;let l;try{l=e(...r)}finally{for(let s=_l.length;s>i;s--)t6();Dm(o),a._d&&Nm(1)}return l};return a._n=!0,a._c=!0,a._d=!0,a}function st(e,t){if(Ga===null)return e;const n=b1(Ga),a=e.dirs||(e.dirs=[]);for(let r=0;r1)return n&&Qe(t)?t.call(a&&a.proxy):t}}function WE(){return!!(Et()||Gu)}const JV=Symbol.for("v-scx"),QV=()=>Ze(JV);function ur(e,t){return G3(e,null,t)}function ye(e,t,n){return G3(e,t,n)}function G3(e,t,n=jn){const{immediate:a,deep:r,flush:o,once:i}=n,l=ka({},n),s=t&&a||!t&&o!=="post";let u;if(nh){if(o==="sync"){const f=QV();u=f.__watcherHandles||(f.__watcherHandles=[])}else if(!s){const f=()=>{};return f.stop=kt,f.resume=kt,f.pause=kt,f}}const c=rr;l.call=(f,h,m)=>Ro(f,c,h,m);let d=!1;o==="post"?l.scheduler=f=>{mr(f,c&&c.suspense)}:o!=="sync"&&(d=!0,l.scheduler=(f,h)=>{h?f():U3(f)}),l.augmentJob=f=>{t&&(f.flags|=4),d&&(f.flags|=2,c&&(f.id=c.uid,f.i=c))};const v=jV(e,t,l);return nh&&(u?u.push(v):s&&v()),v}function ez(e,t,n){const a=this.proxy,r=Ye(e)?e.includes(".")?KE(a,e):()=>a[e]:e.bind(a,a);let o;Qe(t)?o=t:(o=t.handler,n=t);const i=Kh(this),l=G3(r,o.bind(a),n);return i(),l}function KE(e,t){const n=t.split(".");return()=>{let a=e;for(let r=0;re.__isTeleport,Au=e=>e&&(e.disabled||e.disabled===""),tz=e=>e&&(e.defer||e.defer===""),IS=e=>typeof SVGElement<"u"&&e instanceof SVGElement,LS=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,qb=(e,t)=>{const n=e&&e.to;return Ye(n)?t?t(n):null:n},nz={name:"Teleport",__isTeleport:!0,process(e,t,n,a,r,o,i,l,s,u){const{mc:c,pc:d,pbc:v,o:{insert:f,querySelector:h,createText:m,createComment:g,parentNode:y}}=u,b=Au(t.props);let{dynamicChildren:_}=t;const w=(T,M,E)=>{T.shapeFlag&16&&c(T.children,M,E,r,o,i,l,s)},S=(T=t)=>{const M=Au(T.props),E=T.target=qb(T.props,h),A=Xb(E,T,m,f);E&&(i!=="svg"&&IS(E)?i="svg":i!=="mathml"&&LS(E)&&(i="mathml"),r&&r.isCE&&(r.ce._teleportTargets||(r.ce._teleportTargets=new Set)).add(E),M||(w(T,E,A),tv(T,!1)))},k=T=>{const M=()=>{if(as.get(T)===M){if(as.delete(T),Au(T.props)){const E=y(T.el)||n;w(T,E,T.anchor),tv(T,!0)}S(T)}};as.set(T,M),mr(M,o)};if(e==null){const T=t.el=m(""),M=t.anchor=m("");if(f(T,n,a),f(M,n,a),tz(t.props)||o&&o.pendingBranch){k(t);return}b&&(w(t,n,M),tv(t,!0)),S()}else{t.el=e.el;const T=t.anchor=e.anchor,M=as.get(e);if(M){M.flags|=8,as.delete(e),k(t);return}t.targetStart=e.targetStart;const E=t.target=e.target,A=t.targetAnchor=e.targetAnchor,I=Au(e.props),P=I?n:E,R=I?T:A;if(i==="svg"||IS(E)?i="svg":(i==="mathml"||LS(E))&&(i="mathml"),_?(v(e.dynamicChildren,_,P,r,o,i,l),e6(e,t,!0)):s||d(e,t,P,R,r,o,i,l,!1),b)I?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Ip(t,n,T,u,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const G=qb(t.props,h);G&&(t.target=G,Ip(t,G,null,u,0))}else I&&Ip(t,E,A,u,1);tv(t,b)}},remove(e,t,n,{um:a,o:{remove:r}},o){const{shapeFlag:i,children:l,anchor:s,targetStart:u,targetAnchor:c,target:d,props:v}=e,f=Au(v),h=o||!f,m=as.get(e);if(m&&(m.flags|=8,as.delete(e)),d&&(r(u),r(c)),o&&r(s),!m&&(f||d)&&i&16)for(let g=0;g{e.isMounted=!0}),rn(()=>{e.isUnmounting=!0}),e}const fo=[Function,Array],YE={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:fo,onEnter:fo,onAfterEnter:fo,onEnterCancelled:fo,onBeforeLeave:fo,onLeave:fo,onAfterLeave:fo,onLeaveCancelled:fo,onBeforeAppear:fo,onAppear:fo,onAfterAppear:fo,onAppearCancelled:fo},jE=e=>{const t=e.subTree;return t.component?jE(t.component):t},rz={name:"BaseTransition",props:YE,setup(e,{slots:t}){const n=Et(),a=GE();return()=>{const r=t.default&&Y3(t.default(),!0),o=r&&r.length?qE(r):n.subTree?te():void 0;if(!o)return;const i=wn(e),{mode:l}=i;if(a.isLeaving)return A2(o);const s=Om(o);if(!s)return A2(o);let u=Qv(s,i,a,n,d=>u=d);s.type!==oa&&oc(s,u);let c=n.subTree&&Om(n.subTree);if(c&&c.type!==oa&&!Iu(c,s)&&jE(n).type!==oa){let d=Qv(c,i,a,n);if(oc(c,d),l==="out-in"&&s.type!==oa)return a.isLeaving=!0,d.afterLeave=()=>{a.isLeaving=!1,n.job.flags&8||n.update(),delete d.afterLeave,c=void 0},A2(o);l==="in-out"&&s.type!==oa?d.delayLeave=(v,f,h)=>{const m=XE(a,c);m[String(c.key)]=c,v[_o]=()=>{f(),v[_o]=void 0,delete u.delayedLeave,c=void 0},u.delayedLeave=()=>{h(),delete u.delayedLeave,c=void 0}}:c=void 0}else c&&(c=void 0);return o}}};function qE(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==oa){t=n;break}}return t}const oz=rz;function XE(e,t){const{leavingVNodes:n}=e;let a=n.get(t.type);return a||(a=Object.create(null),n.set(t.type,a)),a}function Qv(e,t,n,a,r){const{appear:o,mode:i,persisted:l=!1,onBeforeEnter:s,onEnter:u,onAfterEnter:c,onEnterCancelled:d,onBeforeLeave:v,onLeave:f,onAfterLeave:h,onLeaveCancelled:m,onBeforeAppear:g,onAppear:y,onAfterAppear:b,onAppearCancelled:_}=t,w=String(e.key),S=XE(n,e),k=(E,A)=>{E&&Ro(E,a,9,A)},T=(E,A)=>{const I=A[1];k(E,A),Ae(E)?E.every(P=>P.length<=1)&&I():E.length<=1&&I()},M={mode:i,persisted:l,beforeEnter(E){let A=s;if(!n.isMounted)if(o)A=g||s;else return;E[_o]&&E[_o](!0);const I=S[w];I&&Iu(e,I)&&I.el[_o]&&I.el[_o](),k(A,[E])},enter(E){if(S[w]===e)return;let A=u,I=c,P=d;if(!n.isMounted)if(o)A=y||u,I=b||c,P=_||d;else return;let R=!1;E[Mf]=K=>{R||(R=!0,K?k(P,[E]):k(I,[E]),M.delayedLeave&&M.delayedLeave(),E[Mf]=void 0)};const G=E[Mf].bind(null,!1);A?T(A,[E,G]):G()},leave(E,A){const I=String(e.key);if(E[Mf]&&E[Mf](!0),n.isUnmounting)return A();k(v,[E]);let P=!1;E[_o]=G=>{P||(P=!0,A(),G?k(m,[E]):k(h,[E]),E[_o]=void 0,S[I]===e&&delete S[I])};const R=E[_o].bind(null,!1);S[I]=e,f?T(f,[E,R]):R()},clone(E){const A=Qv(E,t,n,a,r);return r&&r(A),A}};return M}function A2(e){if(p1(e))return e=ei(e),e.children=null,e}function Om(e){if(!p1(e))return h1(e.type)&&e.children?qE(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&Qe(n.default))return n.default()}}function oc(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;const n=e.component.subTree;oc(h1(n.type)&&Om(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Y3(e,t=!1,n){let a=[],r=0;for(let o=0;o1)for(let o=0;okv(m,t&&(Ae(t)?t[g]:t),n,a,r));return}if(gd(a)&&!r){a.shapeFlag&512&&a.type.__asyncResolved&&a.component.subTree.component&&kv(e,t,n,a.component.subTree);return}const o=a.shapeFlag&4?b1(a.component):a.el,i=r?null:o,{i:l,r:s}=e,u=t&&t.r,c=l.refs===jn?l.refs={}:l.refs,d=l.setupState,v=wn(d),f=d===jn?cE:m=>PS(c,m)?!1:en(v,m),h=(m,g)=>!(g&&PS(c,g));if(u!=null&&u!==s){if(DS(t),Ye(u))c[u]=null,f(u)&&(d[u]=null);else if(xn(u)){const m=t;h(u,m.k)&&(u.value=null),m.k&&(c[m.k]=null)}}if(Qe(s))Hh(s,l,12,[i,c]);else{const m=Ye(s),g=xn(s);if(m||g){const y=()=>{if(e.f){const b=m?f(s)?d[s]:c[s]:h()||!e.k?s.value:c[e.k];if(r)Ae(b)&&R3(b,o);else if(Ae(b))b.includes(o)||b.push(o);else if(m)c[s]=[o],f(s)&&(d[s]=c[s]);else{const _=[o];h(s,e.k)&&(s.value=_),e.k&&(c[e.k]=_)}}else m?(c[s]=i,f(s)&&(d[s]=i)):g&&(h(s,e.k)&&(s.value=i),e.k&&(c[e.k]=i))};if(i){const b=()=>{y(),$m.delete(e)};b.id=-1,$m.set(e,b),mr(b,n)}else DS(e),y()}}}function DS(e){const t=$m.get(e);t&&(t.flags|=8,$m.delete(e))}s1().requestIdleCallback;s1().cancelIdleCallback;const gd=e=>!!e.type.__asyncLoader,p1=e=>e.type.__isKeepAlive;function Wh(e,t){JE(e,"a",t)}function j3(e,t){JE(e,"da",t)}function JE(e,t,n=rr){const a=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(m1(t,a,n),n){let r=n.parent;for(;r&&r.parent;)p1(r.parent.vnode)&&iz(a,t,n,r),r=r.parent}}function iz(e,t,n,a){const r=m1(t,e,a,!0);Us(()=>{R3(a[t],r)},n)}function m1(e,t,n=rr,a=!1){if(n){const r=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{kl();const l=Kh(n),s=Ro(t,n,e,i);return l(),Tl(),s});return a?r.unshift(o):r.push(o),o}}const Rl=e=>(t,n=rr)=>{(!nh||e==="sp")&&m1(e,(...a)=>t(...a),n)},g1=Rl("bm"),mt=Rl("m"),q3=Rl("bu"),oi=Rl("u"),rn=Rl("bum"),Us=Rl("um"),lz=Rl("sp"),sz=Rl("rtg"),uz=Rl("rtc");function cz(e,t=rr){m1("ec",e,t)}const X3="components",dz="directives";function be(e,t){return Z3(X3,e,!0,t)||e}const QE=Symbol.for("v-ndc");function St(e){return Ye(e)?Z3(X3,e,!1)||e:e||QE}function Gr(e){return Z3(dz,e)}function Z3(e,t,n=!0,a=!1){const r=Ga||rr;if(r){const o=r.type;if(e===X3){const l=Gz(o,!1);if(l&&(l===t||l===Va(t)||l===Fh(Va(t))))return o}const i=OS(r[e]||o[e],t)||OS(r.appContext[e],t);return!i&&a?o:i}}function OS(e,t){return e&&(e[t]||e[Va(t)]||e[Fh(Va(t))])}function dt(e,t,n,a){let r;const o=n,i=Ae(e);if(i||Ye(e)){const l=i&&Uu(e);let s=!1,u=!1;l&&(s=!ro(e),u=Ml(e),e=u1(e)),r=new Array(e.length);for(let c=0,d=e.length;ct(l,s,void 0,o));else{const l=Object.keys(e);r=new Array(l.length);for(let s=0,u=l.length;s{const o=a.fn(...r);return o&&(o.key=a.key),o}:a.fn)}return e}function ce(e,t,n,a,r,o){if(n==null&&(n={}),Ga.ce||Ga.parent&&gd(Ga.parent)&&Ga.parent.ce){const u=n,c=Object.keys(u).length>0;return t!=="default"&&(u.name=t),x(),ne(Ve,null,[L("slot",u,a&&a())],c?-2:64)}let i=e[t];i&&i._c&&(i._d=!1);const l=_l.length;x();let s;try{const u=i&&e7(i(n)),c=n.key||o||u&&u.key;s=ne(Ve,{key:(c&&!io(c)?c:`_${t}`)+(!u&&a?"_fb":"")},u||(a?a():[]),u&&e._===1?64:-2)}catch(u){for(let c=_l.length;c>l;c--)t6();throw u}finally{i&&i._c&&(i._d=!0)}return s.scopeId&&(s.slotScopeIds=[s.scopeId+"-s"]),s}function e7(e){return e.some(t=>bn(t)?!(t.type===oa||t.type===Ve&&!e7(t.children)):!0)?e:null}function fz(e,t){const n={};for(const a in e)n[Wu(a)]=e[a];return n}const Zb=e=>e?w7(e)?b1(e):Zb(e.parent):null,Tv=ka(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Zb(e.parent),$root:e=>Zb(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>a7(e),$forceUpdate:e=>e.f||(e.f=()=>{U3(e.update)}),$nextTick:e=>e.n||(e.n=He.bind(e.proxy)),$watch:e=>ez.bind(e)}),I2=(e,t)=>e!==jn&&!e.__isScriptSetup&&en(e,t),vz={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:a,data:r,props:o,accessCache:i,type:l,appContext:s}=e;if(t[0]!=="$"){const v=i[t];if(v!==void 0)switch(v){case 1:return a[t];case 2:return r[t];case 4:return n[t];case 3:return o[t]}else{if(I2(a,t))return i[t]=1,a[t];if(r!==jn&&en(r,t))return i[t]=2,r[t];if(en(o,t))return i[t]=3,o[t];if(n!==jn&&en(n,t))return i[t]=4,n[t];Jb&&(i[t]=0)}}const u=Tv[t];let c,d;if(u)return t==="$attrs"&&nr(e.attrs,"get",""),u(e);if((c=l.__cssModules)&&(c=c[t]))return c;if(n!==jn&&en(n,t))return i[t]=4,n[t];if(d=s.config.globalProperties,en(d,t))return d[t]},set({_:e},t,n){const{data:a,setupState:r,ctx:o}=e;return I2(r,t)?(r[t]=n,!0):a!==jn&&en(a,t)?(a[t]=n,!0):en(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:a,appContext:r,props:o,type:i}},l){let s;return!!(n[l]||e!==jn&&l[0]!=="$"&&en(e,l)||I2(t,l)||en(o,l)||en(a,l)||en(Tv,l)||en(r.config.globalProperties,l)||(s=i.__cssModules)&&s[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:en(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Xn(){return t7().slots}function Nl(){return t7().attrs}function t7(e){const t=Et();return t.setupContext||(t.setupContext=C7(t))}function $S(e){return Ae(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Jb=!0;function hz(e){const t=a7(e),n=e.proxy,a=e.ctx;Jb=!1,t.beforeCreate&&RS(t.beforeCreate,e,"bc");const{data:r,computed:o,methods:i,watch:l,provide:s,inject:u,created:c,beforeMount:d,mounted:v,beforeUpdate:f,updated:h,activated:m,deactivated:g,beforeDestroy:y,beforeUnmount:b,destroyed:_,unmounted:w,render:S,renderTracked:k,renderTriggered:T,errorCaptured:M,serverPrefetch:E,expose:A,inheritAttrs:I,components:P,directives:R,filters:G}=t;if(u&&pz(u,a,null),i)for(const z in i){const B=i[z];Qe(B)&&(a[z]=B.bind(n))}if(r){const z=r.call(n,n);_t(z)&&(e.data=nn(z))}if(Jb=!0,o)for(const z in o){const B=o[z],W=Qe(B)?B.bind(n,n):Qe(B.get)?B.get.bind(n,n):kt,N=!Qe(B)&&Qe(B.set)?B.set.bind(n):kt,V=D({get:W,set:N});Object.defineProperty(a,z,{enumerable:!0,configurable:!0,get:()=>V.value,set:Y=>V.value=Y})}if(l)for(const z in l)n7(l[z],a,n,z);if(s){const z=Qe(s)?s.call(n):s;Reflect.ownKeys(z).forEach(B=>{Ft(B,z[B])})}c&&RS(c,e,"c");function H(z,B){Ae(B)?B.forEach(W=>z(W.bind(n))):B&&z(B.bind(n))}if(H(g1,d),H(mt,v),H(q3,f),H(oi,h),H(Wh,m),H(j3,g),H(cz,M),H(uz,k),H(sz,T),H(rn,b),H(Us,w),H(lz,E),Ae(A))if(A.length){const z=e.exposed||(e.exposed={});A.forEach(B=>{Object.defineProperty(z,B,{get:()=>n[B],set:W=>n[B]=W,enumerable:!0})})}else e.exposed||(e.exposed={});S&&e.render===kt&&(e.render=S),I!=null&&(e.inheritAttrs=I),P&&(e.components=P),R&&(e.directives=R),E&&ZE(e)}function pz(e,t,n=kt){Ae(e)&&(e=Qb(e));for(const a in e){const r=e[a];let o;_t(r)?"default"in r?o=Ze(r.from||a,r.default,!0):o=Ze(r.from||a):o=Ze(r),xn(o)?Object.defineProperty(t,a,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[a]=o}}function RS(e,t,n){Ro(Ae(e)?e.map(a=>a.bind(t.proxy)):e.bind(t.proxy),t,n)}function n7(e,t,n,a){let r=a.includes(".")?KE(n,a):()=>n[a];if(Ye(e)){const o=t[e];Qe(o)&&ye(r,o)}else if(Qe(e))ye(r,e.bind(n));else if(_t(e))if(Ae(e))e.forEach(o=>n7(o,t,n,a));else{const o=Qe(e.handler)?e.handler.bind(n):t[e.handler];Qe(o)&&ye(r,o,e)}}function a7(e){const t=e.type,{mixins:n,extends:a}=t,{mixins:r,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let s;return l?s=l:!r.length&&!n&&!a?s=t:(s={},r.length&&r.forEach(u=>Rm(s,u,i,!0)),Rm(s,t,i)),_t(t)&&o.set(t,s),s}function Rm(e,t,n,a=!1){const{mixins:r,extends:o}=t;o&&Rm(e,o,n,!0),r&&r.forEach(i=>Rm(e,i,n,!0));for(const i in t)if(!(a&&i==="expose")){const l=mz[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const mz={data:NS,props:BS,emits:BS,methods:nv,computed:nv,beforeCreate:pr,created:pr,beforeMount:pr,mounted:pr,beforeUpdate:pr,updated:pr,beforeDestroy:pr,beforeUnmount:pr,destroyed:pr,unmounted:pr,activated:pr,deactivated:pr,errorCaptured:pr,serverPrefetch:pr,components:nv,directives:nv,watch:yz,provide:NS,inject:gz};function NS(e,t){return t?e?function(){return ka(Qe(e)?e.call(this,this):e,Qe(t)?t.call(this,this):t)}:t:e}function gz(e,t){return nv(Qb(e),Qb(t))}function Qb(e){if(Ae(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Va(t)}Modifiers`]||e[`${$l(t)}Modifiers`];function Sz(e,t,...n){if(e.isUnmounted)return;const a=e.vnode.props||jn;let r=n;const o=t.startsWith("update:"),i=o&&wz(a,t.slice(7));i&&(i.trim&&(r=n.map(c=>Ye(c)?c.trim():c)),i.number&&(r=n.map(N3)));let l,s=a[l=Wu(t)]||a[l=Wu(Va(t))];!s&&o&&(s=a[l=Wu($l(t))]),s&&Ro(s,e,6,r);const u=a[l+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ro(u,e,6,r)}}const Cz=new WeakMap;function o7(e,t,n=!1){const a=n?Cz:t.emitsCache,r=a.get(e);if(r!==void 0)return r;const o=e.emits;let i={},l=!1;if(!Qe(e)){const s=u=>{const c=o7(u,t,!0);c&&(l=!0,ka(i,c))};!n&&t.mixins.length&&t.mixins.forEach(s),e.extends&&s(e.extends),e.mixins&&e.mixins.forEach(s)}return!o&&!l?(_t(e)&&a.set(e,null),null):(Ae(o)?o.forEach(s=>i[s]=null):ka(i,o),_t(e)&&a.set(e,i),i)}function y1(e,t){return!e||!a1(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),en(e,t[0].toLowerCase()+t.slice(1))||en(e,$l(t))||en(e,t))}function VS(e){const{type:t,vnode:n,proxy:a,withProxy:r,propsOptions:[o],slots:i,attrs:l,emit:s,render:u,renderCache:c,props:d,data:v,setupState:f,ctx:h,inheritAttrs:m}=e,g=Dm(e);let y,b;try{if(n.shapeFlag&4){const w=r||a,S=w;y=Ti(u.call(S,w,c,d,f,v,h)),b=l}else{const w=t;y=Ti(w.length>1?w(d,{attrs:l,slots:i,emit:s}):w(d,null)),b=t.props?l:xz(l)}}catch(w){_l.length=0,v1(w,e,1),y=L(oa)}let _=y;if(b&&m!==!1){const w=Object.keys(b),{shapeFlag:S}=_;w.length&&S&7&&(o&&w.some(r1)&&(b=kz(b,o)),_=ei(_,b,!1,!0))}if(n.dirs&&(_=ei(_,null,!1,!0),_.dirs=_.dirs?_.dirs.concat(n.dirs):n.dirs),n.transition){const w=h1(_.type)&&Om(_)||_;oc(w,n.transition)}return y=_,Dm(g),y}const xz=e=>{let t;for(const n in e)(n==="class"||n==="style"||a1(n))&&((t||(t={}))[n]=e[n]);return t},kz=(e,t)=>{const n={};for(const a in e)(!r1(a)||!(a.slice(9)in t))&&(n[a]=e[a]);return n};function Tz(e,t,n){const{props:a,children:r,component:o}=e,{props:i,children:l,patchFlag:s}=t,u=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&s>=0){if(s&1024)return!0;if(s&16)return a?zS(a,i,u):!!i;if(s&8){const c=t.dynamicProps;for(let d=0;dObject.create(l7),u7=e=>Object.getPrototypeOf(e)===l7;function Ez(e,t,n,a=!1){const r={},o=s7();e.propsDefaults=Object.create(null),c7(e,t,r,o);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);n?e.props=a?r:W3(r):e.type.props?e.props=r:e.props=o,e.attrs=o}function Az(e,t,n,a){const{props:r,attrs:o,vnode:{patchFlag:i}}=e,l=wn(r),[s]=e.propsOptions;let u=!1;if((a||i>0)&&!(i&16)){if(i&8){const c=e.vnode.dynamicProps;for(let d=0;d{s=!0;const[v,f]=d7(d,t,!0);ka(i,v),f&&l.push(...f)};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}if(!o&&!s)return _t(e)&&a.set(e,hd),hd;if(Ae(o))for(let c=0;ce==="_"||e==="_ctx"||e==="$stable",Q3=e=>Ae(e)?e.map(Ti):[Ti(e)],Lz=(e,t,n)=>{if(t._n)return t;const a=$((...r)=>Q3(t(...r)),n);return a._c=!1,a},f7=(e,t,n)=>{const a=e._ctx;for(const r in e){if(J3(r))continue;const o=e[r];if(Qe(o))t[r]=Lz(r,o,a);else if(o!=null){const i=Q3(o);t[r]=()=>i}}},v7=(e,t)=>{const n=Q3(t);e.slots.default=()=>n},h7=(e,t,n)=>{for(const a in t)(n||!J3(a))&&(e[a]=t[a])},Pz=(e,t,n)=>{const a=e.slots=s7();if(e.vnode.shapeFlag&32){const r=t._;r?(h7(a,t,n),n&&fE(a,"_",r,!0)):f7(t,a)}else t&&v7(e,t)},Dz=(e,t,n)=>{const{vnode:a,slots:r}=e;let o=!0,i=jn;if(a.shapeFlag&32){const l=t._;l?n&&l===1?o=!1:h7(r,t,n):(o=!t.$stable,f7(t,r)),i=t}else t&&(v7(e,t),i={default:1});if(o)for(const l in r)!J3(l)&&i[l]==null&&delete r[l]},mr=Bz;function Oz(e){return $z(e)}function $z(e,t){const n=s1();n.__VUE__=!0;const{insert:a,remove:r,patchProp:o,createElement:i,createText:l,createComment:s,setText:u,setElementText:c,parentNode:d,nextSibling:v,setScopeId:f=kt,insertStaticContent:h}=e,m=(j,oe,se,fe=null,ge=null,Se=null,De=void 0,Te=null,Re=!!oe.dynamicChildren)=>{if(j===oe)return;j&&!Iu(j,oe)&&(fe=le(j),Y(j,ge,Se,!0),j=null),oe.patchFlag===-2&&(Re=!1,oe.dynamicChildren=null);const{type:Ce,ref:Le,shapeFlag:me}=oe;switch(Ce){case uf:g(j,oe,se,fe);break;case oa:y(j,oe,se,fe);break;case j0:j==null&&b(oe,se,fe,De);break;case Ve:P(j,oe,se,fe,ge,Se,De,Te,Re);break;default:me&1?S(j,oe,se,fe,ge,Se,De,Te,Re):me&6?R(j,oe,se,fe,ge,Se,De,Te,Re):(me&64||me&128)&&Ce.process(j,oe,se,fe,ge,Se,De,Te,Re,J)}Le!=null&&ge?kv(Le,j&&j.ref,Se,oe||j,!oe):Le==null&&j&&j.ref!=null&&kv(j.ref,null,Se,j,!0)},g=(j,oe,se,fe)=>{if(j==null)a(oe.el=l(oe.children),se,fe);else{const ge=oe.el=j.el;oe.children!==j.children&&u(ge,oe.children)}},y=(j,oe,se,fe)=>{j==null?a(oe.el=s(oe.children||""),se,fe):oe.el=j.el},b=(j,oe,se,fe)=>{[j.el,j.anchor]=h(j.children,oe,se,fe,j.el,j.anchor)},_=({el:j,anchor:oe},se,fe)=>{let ge;for(;j&&j!==oe;)ge=v(j),a(j,se,fe),j=ge;a(oe,se,fe)},w=({el:j,anchor:oe})=>{let se;for(;j&&j!==oe;)se=v(j),r(j),j=se;r(oe)},S=(j,oe,se,fe,ge,Se,De,Te,Re)=>{if(oe.type==="svg"?De="svg":oe.type==="math"&&(De="mathml"),j==null)k(oe,se,fe,ge,Se,De,Te,Re);else{const Ce=j.el&&j.el._isVueCE?j.el:null;try{Ce&&Ce._beginPatch(),E(j,oe,ge,Se,De,Te,Re)}finally{Ce&&Ce._endPatch()}}},k=(j,oe,se,fe,ge,Se,De,Te)=>{let Re,Ce;const{props:Le,shapeFlag:me,transition:Be,dirs:ze}=j;if(Re=j.el=i(j.type,Se,Le&&Le.is,Le),me&8?c(Re,j.children):me&16&&M(j.children,Re,null,fe,ge,L2(j,Se),De,Te),ze&&Zs(j,null,fe,"created"),T(Re,j,j.scopeId,De,fe),Le){for(const rt in Le)rt!=="value"&&!Sv(rt)&&o(Re,rt,null,Le[rt],Se,fe);"value"in Le&&o(Re,"value",null,Le.value,Se),(Ce=Le.onVnodeBeforeMount)&&mi(Ce,fe,j)}ze&&Zs(j,null,fe,"beforeMount");const xe=Rz(ge,Be);xe&&Be.beforeEnter(Re),a(Re,oe,se),((Ce=Le&&Le.onVnodeMounted)||xe||ze)&&mr(()=>{try{Ce&&mi(Ce,fe,j),xe&&Be.enter(Re),ze&&Zs(j,null,fe,"mounted")}finally{}},ge)},T=(j,oe,se,fe,ge)=>{if(se&&f(j,se),fe)for(let Se=0;Se{for(let Ce=Re;Ce{const Te=oe.el=j.el;let{patchFlag:Re,dynamicChildren:Ce,dirs:Le}=oe;Re|=j.patchFlag&16;const me=j.props||jn,Be=oe.props||jn;let ze;if(se&&Js(se,!1),(ze=Be.onVnodeBeforeUpdate)&&mi(ze,se,oe,j),Le&&Zs(oe,j,se,"beforeUpdate"),se&&Js(se,!0),Ce&&(!j.dynamicChildren||j.dynamicChildren.length!==Ce.length)&&(Re=0,De=!1,Ce=null),(me.innerHTML&&Be.innerHTML==null||me.textContent&&Be.textContent==null)&&c(Te,""),Ce?A(j.dynamicChildren,Ce,Te,se,fe,L2(oe,ge),Se):De||B(j,oe,Te,null,se,fe,L2(oe,ge),Se,!1),Re>0){if(Re&16)I(Te,me,Be,se,ge);else if(Re&2&&me.class!==Be.class&&o(Te,"class",null,Be.class,ge),Re&4&&o(Te,"style",me.style,Be.style,ge),Re&8){const xe=oe.dynamicProps;for(let rt=0;rt{ze&&mi(ze,se,oe,j),Le&&Zs(oe,j,se,"updated")},fe)},A=(j,oe,se,fe,ge,Se,De)=>{for(let Te=0;Te{if(oe!==se){if(oe!==jn)for(const Se in oe)!Sv(Se)&&!(Se in se)&&o(j,Se,oe[Se],null,ge,fe);for(const Se in se){if(Sv(Se))continue;const De=se[Se],Te=oe[Se];De!==Te&&Se!=="value"&&o(j,Se,Te,De,ge,fe)}"value"in se&&o(j,"value",oe.value,se.value,ge)}},P=(j,oe,se,fe,ge,Se,De,Te,Re)=>{const Ce=oe.el=j?j.el:l(""),Le=oe.anchor=j?j.anchor:l("");let{patchFlag:me,dynamicChildren:Be,slotScopeIds:ze}=oe;ze&&(Te=Te?Te.concat(ze):ze),j==null?(a(Ce,se,fe),a(Le,se,fe),M(oe.children||[],se,Le,ge,Se,De,Te,Re)):me>0&&me&64&&Be&&j.dynamicChildren&&j.dynamicChildren.length===Be.length?(A(j.dynamicChildren,Be,se,ge,Se,De,Te),(oe.key!=null||ge&&oe===ge.subTree)&&e6(j,oe,!0)):B(j,oe,se,Le,ge,Se,De,Te,Re)},R=(j,oe,se,fe,ge,Se,De,Te,Re)=>{oe.slotScopeIds=Te,j==null?oe.shapeFlag&512?ge.ctx.activate(oe,se,fe,De,Re):G(oe,se,fe,ge,Se,De,Re):K(j,oe,Re)},G=(j,oe,se,fe,ge,Se,De)=>{const Te=j.component=Hz(j,fe,ge);if(p1(j)&&(Te.ctx.renderer=J),Wz(Te,!1,De),Te.asyncDep){if(ge&&ge.registerDep(Te,H,De),!j.el){const Re=Te.subTree=L(oa);y(null,Re,oe,se),j.placeholder=Re.el}}else H(Te,j,oe,se,ge,Se,De)},K=(j,oe,se)=>{const fe=oe.component=j.component;if(Tz(j,oe,se))if(fe.asyncDep&&!fe.asyncResolved){z(fe,oe,se);return}else fe.next=oe,fe.update();else oe.el=j.el,fe.vnode=oe},H=(j,oe,se,fe,ge,Se,De)=>{const Te=()=>{if(j.isMounted){let{next:me,bu:Be,u:ze,parent:xe,vnode:rt}=j;{const Rt=p7(j);if(Rt){me&&(me.el=rt.el,z(j,me,De)),Rt.asyncDep.then(()=>{mr(()=>{j.isUnmounted||Ce()},ge)});return}}let nt=me,Pt;Js(j,!1),me?(me.el=rt.el,z(j,me,De)):me=rt,Be&&G0(Be),(Pt=me.props&&me.props.onVnodeBeforeUpdate)&&mi(Pt,xe,me,rt),Js(j,!0);const Wt=VS(j),Jt=j.subTree;j.subTree=Wt,m(Jt,Wt,d(Jt.el),le(Jt),j,ge,Se),me.el=Wt.el,nt===null&&Mz(j,Wt.el),ze&&mr(ze,ge),(Pt=me.props&&me.props.onVnodeUpdated)&&mr(()=>mi(Pt,xe,me,rt),ge)}else{let me;const{el:Be,props:ze}=oe,{bm:xe,m:rt,parent:nt,root:Pt,type:Wt}=j,Jt=gd(oe);Js(j,!1),xe&&G0(xe),!Jt&&(me=ze&&ze.onVnodeBeforeMount)&&mi(me,nt,oe),Js(j,!0);{Pt.ce&&Pt.ce._hasShadowRoot()&&Pt.ce._injectChildStyle(Wt,j.parent?j.parent.type:void 0);const Rt=j.subTree=VS(j);m(null,Rt,se,fe,j,ge,Se),oe.el=Rt.el}if(rt&&mr(rt,ge),!Jt&&(me=ze&&ze.onVnodeMounted)){const Rt=oe;mr(()=>mi(me,nt,Rt),ge)}(oe.shapeFlag&256||nt&&gd(nt.vnode)&&nt.vnode.shapeFlag&256)&&j.a&&mr(j.a,ge),j.isMounted=!0,oe=se=fe=null}};j.scope.on();const Re=j.effect=new bE(Te);j.scope.off();const Ce=j.update=Re.run.bind(Re),Le=j.job=Re.runIfDirty.bind(Re);Le.i=j,Le.id=j.uid,Re.scheduler=()=>U3(Le),Js(j,!0),Ce()},z=(j,oe,se)=>{oe.component=j;const fe=j.vnode.props;j.vnode=oe,j.next=null,Az(j,oe.props,fe,se),Dz(j,oe.children,se),kl(),AS(j),Tl()},B=(j,oe,se,fe,ge,Se,De,Te,Re=!1)=>{const Ce=j&&j.children,Le=j?j.shapeFlag:0,me=oe.children,{patchFlag:Be,shapeFlag:ze}=oe;if(Be>0){if(Be&128){N(Ce,me,se,fe,ge,Se,De,Te,Re);return}else if(Be&256){W(Ce,me,se,fe,ge,Se,De,Te,Re);return}}ze&8?(Le&16&&ie(Ce,ge,Se),me!==Ce&&c(se,me)):Le&16?ze&16?N(Ce,me,se,fe,ge,Se,De,Te,Re):ie(Ce,ge,Se,!0):(Le&8&&c(se,""),ze&16&&M(me,se,fe,ge,Se,De,Te,Re))},W=(j,oe,se,fe,ge,Se,De,Te,Re)=>{j=j||hd,oe=oe||hd;const Ce=j.length,Le=oe.length,me=Math.min(Ce,Le);let Be;for(Be=0;BeLe?ie(j,ge,Se,!0,!1,me):M(oe,se,fe,ge,Se,De,Te,Re,me)},N=(j,oe,se,fe,ge,Se,De,Te,Re)=>{let Ce=0;const Le=oe.length;let me=j.length-1,Be=Le-1;for(;Ce<=me&&Ce<=Be;){const ze=j[Ce],xe=oe[Ce]=Re?fl(oe[Ce]):Ti(oe[Ce]);if(Iu(ze,xe))m(ze,xe,se,null,ge,Se,De,Te,Re);else break;Ce++}for(;Ce<=me&&Ce<=Be;){const ze=j[me],xe=oe[Be]=Re?fl(oe[Be]):Ti(oe[Be]);if(Iu(ze,xe))m(ze,xe,se,null,ge,Se,De,Te,Re);else break;me--,Be--}if(Ce>me){if(Ce<=Be){const ze=Be+1,xe=zeBe)for(;Ce<=me;)Y(j[Ce],ge,Se,!0),Ce++;else{const ze=Ce,xe=Ce,rt=new Map;for(Ce=xe;Ce<=Be;Ce++){const Ne=oe[Ce]=Re?fl(oe[Ce]):Ti(oe[Ce]);Ne.key!=null&&rt.set(Ne.key,Ce)}let nt,Pt=0;const Wt=Be-xe+1;let Jt=!1,Rt=0;const Qt=new Array(Wt);for(Ce=0;Ce=Wt){Y(Ne,ge,Se,!0);continue}let tt;if(Ne.key!=null)tt=rt.get(Ne.key);else for(nt=xe;nt<=Be;nt++)if(Qt[nt-xe]===0&&Iu(Ne,oe[nt])){tt=nt;break}tt===void 0?Y(Ne,ge,Se,!0):(Qt[tt-xe]=Ce+1,tt>=Rt?Rt=tt:Jt=!0,m(Ne,oe[tt],se,null,ge,Se,De,Te,Re),Pt++)}const _e=Jt?Nz(Qt):hd;for(nt=_e.length-1,Ce=Wt-1;Ce>=0;Ce--){const Ne=xe+Ce,tt=oe[Ne],Me=oe[Ne+1],Ue=Ne+1{const{el:Se,type:De,transition:Te,children:Re,shapeFlag:Ce}=j;if(Ce&6){V(j.component.subTree,oe,se,fe);return}if(Ce&128){j.suspense.move(oe,se,fe);return}if(Ce&64){De.move(j,oe,se,J);return}if(De===Ve){a(Se,oe,se);for(let me=0;meTe.enter(Se),ge));else{const{leave:me,delayLeave:Be,afterLeave:ze}=Te,xe=()=>{j.ctx.isUnmounted?r(Se):a(Se,oe,se)},rt=()=>{const nt=Se._isLeaving||!!Se[_o];Se._isLeaving&&Se[_o](!0),Te.persisted&&!nt?xe():me(Se,()=>{xe(),ze&&ze()})};Be?Be(Se,xe,rt):rt()}else a(Se,oe,se)},Y=(j,oe,se,fe=!1,ge=!1)=>{const{type:Se,props:De,ref:Te,children:Re,dynamicChildren:Ce,shapeFlag:Le,patchFlag:me,dirs:Be,cacheIndex:ze,memo:xe}=j;if(me===-2&&(ge=!1),Te!=null&&(kl(),kv(Te,null,se,j,!0),Tl()),ze!=null&&(oe.renderCache[ze]=void 0),Le&256){oe.ctx.deactivate(j);return}const rt=Le&1&&Be,nt=!gd(j);let Pt;if(nt&&(Pt=De&&De.onVnodeBeforeUnmount)&&mi(Pt,oe,j),Le&6)re(j.component,se,fe);else{if(Le&128){j.suspense.unmount(se,fe);return}rt&&Zs(j,null,oe,"beforeUnmount"),Le&64?j.type.remove(j,oe,se,J,fe):Ce&&!Ce.hasOnce&&(Se!==Ve||me>0&&me&64)?ie(Ce,oe,se,!1,!0):(Se===Ve&&me&384||!ge&&Le&16)&&ie(Re,oe,se),fe&&X(j)}const Wt=xe!=null&&ze==null;(nt&&(Pt=De&&De.onVnodeUnmounted)||rt||Wt)&&mr(()=>{Pt&&mi(Pt,oe,j),rt&&Zs(j,null,oe,"unmounted"),Wt&&(j.el=null)},se)},X=j=>{const{type:oe,el:se,anchor:fe,transition:ge}=j;if(oe===Ve){ee(se,fe);return}if(oe===j0){w(j);return}const Se=()=>{r(se),ge&&!ge.persisted&&ge.afterLeave&&ge.afterLeave()};if(j.shapeFlag&1&&ge&&!ge.persisted){const{leave:De,delayLeave:Te}=ge,Re=()=>De(se,Se);Te?Te(j.el,Se,Re):Re()}else Se()},ee=(j,oe)=>{let se;for(;j!==oe;)se=v(j),r(j),j=se;r(oe)},re=(j,oe,se)=>{const{bum:fe,scope:ge,job:Se,subTree:De,um:Te,m:Re,a:Ce}=j;HS(Re),HS(Ce),fe&&G0(fe),ge.stop(),Se&&(Se.flags|=8,Y(De,j,oe,se)),Te&&mr(Te,oe),mr(()=>{j.isUnmounted=!0},oe)},ie=(j,oe,se,fe=!1,ge=!1,Se=0)=>{for(let De=Se;De{if(j.shapeFlag&6)return le(j.component.subTree);if(j.shapeFlag&128)return j.suspense.next();const oe=v(j.anchor||j.el),se=oe&&oe[UE];return se?v(se):oe};let ve=!1;const he=(j,oe,se)=>{let fe;j==null?oe._vnode&&(Y(oe._vnode,null,null,!0),fe=oe._vnode.component):m(oe._vnode||null,j,oe,null,null,null,se),oe._vnode=j,ve||(ve=!0,AS(fe),zE(),ve=!1)},J={p:m,um:Y,m:V,r:X,mt:G,mc:M,pc:B,pbc:A,n:le,o:e};return{render:he,hydrate:void 0,createApp:_z(he)}}function L2({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Js({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Rz(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function e6(e,t,n=!1){const a=e.children,r=t.children;if(Ae(a)&&Ae(r))for(let o=0;o>1,e[n[l]]0&&(t[a]=n[o-1]),n[o]=a)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}function p7(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:p7(t)}function HS(e){if(e)for(let t=0;te.__isSuspense;function Bz(e,t){t&&t.pendingBranch?Ae(e)?t.effects.push(...e):t.effects.push(e):ZV(e)}const Ve=Symbol.for("v-fgt"),uf=Symbol.for("v-txt"),oa=Symbol.for("v-cmt"),j0=Symbol.for("v-stc"),_l=[];let Jr=null;function x(e=!1){_l.push(Jr=e?null:[])}function t6(){_l.pop(),Jr=_l[_l.length-1]||null}let eh=1;function Nm(e,t=!1){eh+=e,e<0&&Jr&&t&&(Jr.hasOnce=!0)}function y7(e){return e.dynamicChildren=eh>0?Jr||hd:null,t6(),eh>0&&Jr&&Jr.push(e),e}function O(e,t,n,a,r,o){return y7(C(e,t,n,a,r,o,!0))}function ne(e,t,n,a,r){return y7(L(e,t,n,a,r,!0))}function bn(e){return e?e.__v_isVNode===!0:!1}function Iu(e,t){return e.type===t.type&&e.key===t.key}const b7=({key:e})=>e??null,q0=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?Ye(e)||xn(e)||Qe(e)?{i:Ga,r:e,k:t,f:!!n}:e:null);function C(e,t=null,n=null,a=0,r=null,o=e===Ve?0:1,i=!1,l=!1){const s={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&b7(t),ref:t&&q0(t),scopeId:HE,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:a,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Ga};return l?(Bm(s,n),o&128&&e.normalize(s)):n&&(s.shapeFlag|=Ye(n)?8:16),eh>0&&!i&&Jr&&(s.patchFlag>0||o&6)&&s.patchFlag!==32&&Jr.push(s),s}const L=Vz;function Vz(e,t=null,n=null,a=0,r=null,o=!1){if((!e||e===QE)&&(e=oa),bn(e)){const l=ei(e,t,!0);return n&&Bm(l,n),eh>0&&!o&&Jr&&(l.shapeFlag&6?Jr[Jr.indexOf(e)]=l:Jr.push(l)),l.patchFlag=-2,l}if(Yz(e)&&(e=e.__vccOpts),t){t=wl(t);let{class:l,style:s}=t;l&&!Ye(l)&&(t.class=U(l)),_t(s)&&(f1(s)&&!Ae(s)&&(s=ka({},s)),t.style=et(s))}const i=Ye(e)?1:g7(e)?128:h1(e)?64:_t(e)?4:Qe(e)?2:0;return C(e,t,n,a,r,i,o,!0)}function wl(e){return e?f1(e)||u7(e)?ka({},e):e:null}function ei(e,t,n=!1,a=!1){const{props:r,ref:o,patchFlag:i,children:l,transition:s}=e,u=t?Tt(r||{},t):r,c={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&b7(u),ref:t&&t.ref?n&&o?Ae(o)?o.concat(q0(t)):[o,q0(t)]:q0(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ve?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:s,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&ei(e.ssContent),ssFallback:e.ssFallback&&ei(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return s&&a&&oc(c,s.clone(c)),c}function ue(e=" ",t=0){return L(uf,null,e,t)}function _7(e,t){const n=L(j0,null,e);return n.staticCount=t,n}function te(e="",t=!1){return t?(x(),ne(oa,null,e)):L(oa,null,e)}function Ti(e){return e==null||typeof e=="boolean"?L(oa):Ae(e)?L(Ve,null,e.slice()):bn(e)?fl(e):L(uf,null,String(e))}function fl(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:ei(e)}function Bm(e,t){let n=0;const{shapeFlag:a}=e;if(t==null)t=null;else if(Ae(t))n=16;else if(typeof t=="object")if(a&65){const r=t.default;r&&(r._c&&(r._d=!1),Bm(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!u7(t)?t._ctx=Ga:r===3&&Ga&&(Ga.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(Qe(t)){if(a&65){Bm(e,{default:t});return}t={default:t,_ctx:Ga},n=32}else t=String(t),a&64?(n=16,t=[ue(t)]):n=8;e.children=t,e.shapeFlag|=n}function Tt(...e){const t={};for(let n=0;nrr||Ga;let Vm,th;{const e=s1(),t=(n,a)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(a),o=>{r.length>1?r.forEach(i=>i(o)):r[0](o)}};Vm=t("__VUE_INSTANCE_SETTERS__",n=>rr=n),th=t("__VUE_SSR_SETTERS__",n=>nh=n)}const Kh=e=>{const t=rr;return Vm(e),e.scope.on(),()=>{e.scope.off(),Vm(t)}},WS=()=>{rr&&rr.scope.off(),Vm(null)};function w7(e){return e.vnode.shapeFlag&4}let nh=!1;function Wz(e,t=!1,n=!1){t&&th(t);const{props:a,children:r}=e.vnode,o=w7(e);Ez(e,a,o,t),Pz(e,r,n||t);const i=o?Kz(e,t):void 0;return t&&th(!1),i}function Kz(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,vz);const{setup:a}=n;if(a){kl();const r=e.setupContext=a.length>1?C7(e):null,o=Kh(e),i=Hh(a,e,0,[e.props,r]),l=Ls(i);if(Tl(),o(),(l||e.sp)&&!gd(e)&&ZE(e),l){if(i.then(WS,WS),t)return i.then(s=>{th(!0);try{KS(e,s,t)}finally{th(!1)}}).catch(s=>{v1(s,e,0)});e.asyncDep=i}else KS(e,i)}else S7(e)}function KS(e,t,n){Qe(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:_t(t)&&(e.setupState=RE(t)),S7(e)}function S7(e,t,n){const a=e.type;e.render||(e.render=a.render||kt);{const r=Kh(e);kl();try{hz(e)}finally{Tl(),r()}}}const Uz={get(e,t){return nr(e,"get",""),e[t]}};function C7(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,Uz),slots:e.slots,emit:e.emit,expose:t}}function b1(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(RE($i(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Tv)return Tv[n](e)},has(t,n){return n in t||n in Tv}})):e.proxy}function Gz(e,t=!0){return Qe(e)?e.displayName||e.name:e.name||t&&e.__name}function Yz(e){return Qe(e)&&"__vccOpts"in e}const D=(e,t)=>GV(e,t,nh);function ot(e,t,n){try{Nm(-1);const a=arguments.length;return a===2?_t(t)&&!Ae(t)?bn(t)?L(e,null,[t]):L(e,t):L(e,null,t):(a>3?n=Array.prototype.slice.call(arguments,2):a===3&&bn(n)&&(n=[n]),L(e,t,n))}finally{Nm(1)}}const jz="3.5.41",qz=kt;/** +* @vue/runtime-dom v3.5.41 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let t4;const US=typeof window<"u"&&window.trustedTypes;if(US)try{t4=US.createPolicy("vue",{createHTML:e=>e})}catch{}const x7=t4?e=>t4.createHTML(e):e=>e,Xz="http://www.w3.org/2000/svg",Zz="http://www.w3.org/1998/Math/MathML",cl=typeof document<"u"?document:null,GS=cl&&cl.createElement("template"),Jz={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,a)=>{const r=t==="svg"?cl.createElementNS(Xz,e):t==="mathml"?cl.createElementNS(Zz,e):n?cl.createElement(e,{is:n}):cl.createElement(e);return e==="select"&&a&&a.multiple!=null&&r.setAttribute("multiple",a.multiple),r},createText:e=>cl.createTextNode(e),createComment:e=>cl.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>cl.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,a,r,o){const i=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{GS.innerHTML=x7(a==="svg"?`${e}`:a==="mathml"?`${e}`:e);const l=GS.content;if(a==="svg"||a==="mathml"){const s=l.firstChild;for(;s.firstChild;)l.appendChild(s.firstChild);l.removeChild(s)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},jl="transition",Ef="animation",Dd=Symbol("_vtc"),k7={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},T7=ka({},YE,k7),Qz=e=>(e.displayName="Transition",e.props=T7,e),Pa=Qz((e,{slots:t})=>ot(oz,M7(e),t)),Qs=(e,t=[])=>{Ae(e)?e.forEach(n=>n(...t)):e&&e(...t)},YS=e=>e?Ae(e)?e.some(t=>t.length>1):e.length>1:!1;function M7(e){const t={};for(const P in e)P in k7||(t[P]=e[P]);if(e.css===!1)return t;const{name:n="v",type:a,duration:r,enterFromClass:o=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:s=o,appearActiveClass:u=i,appearToClass:c=l,leaveFromClass:d=`${n}-leave-from`,leaveActiveClass:v=`${n}-leave-active`,leaveToClass:f=`${n}-leave-to`}=e,h=eF(r),m=h&&h[0],g=h&&h[1],{onBeforeEnter:y,onEnter:b,onEnterCancelled:_,onLeave:w,onLeaveCancelled:S,onBeforeAppear:k=y,onAppear:T=b,onAppearCancelled:M=_}=t,E=(P,R,G,K)=>{P._enterCancelled=K,rs(P,R?c:l),rs(P,R?u:i),G&&G()},A=(P,R)=>{P._isLeaving=!1,rs(P,d),rs(P,f),rs(P,v),R&&R()},I=P=>(R,G)=>{const K=P?T:b,H=()=>E(R,P,G);Qs(K,[R,H]),jS(()=>{rs(R,P?s:o),Ci(R,P?c:l),YS(K)||qS(R,a,m,H)})};return ka(t,{onBeforeEnter(P){Qs(y,[P]),Ci(P,o),Ci(P,i)},onBeforeAppear(P){Qs(k,[P]),Ci(P,s),Ci(P,u)},onEnter:I(!1),onAppear:I(!0),onLeave(P,R){P._isLeaving=!0;const G=()=>A(P,R);Ci(P,d),P._enterCancelled?(Ci(P,v),n4(P)):(n4(P),Ci(P,v)),jS(()=>{P._isLeaving&&(rs(P,d),Ci(P,f),YS(w)||qS(P,a,g,G))}),Qs(w,[P,G])},onEnterCancelled(P){E(P,!1,void 0,!0),Qs(_,[P])},onAppearCancelled(P){E(P,!0,void 0,!0),Qs(M,[P])},onLeaveCancelled(P){A(P),Qs(S,[P])}})}function eF(e){if(e==null)return null;if(_t(e))return[P2(e.enter),P2(e.leave)];{const t=P2(e);return[t,t]}}function P2(e){return hV(e)}function Ci(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Dd]||(e[Dd]=new Set)).add(t)}function rs(e,t){t.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const n=e[Dd];n&&(n.delete(t),n.size||(e[Dd]=void 0))}function jS(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let tF=0;function qS(e,t,n,a){const r=e._endId=++tF,o=()=>{r===e._endId&&a()};if(n!=null)return setTimeout(o,n);const{type:i,timeout:l,propCount:s}=E7(e,t);if(!i)return a();const u=i+"end";let c=0;const d=()=>{e.removeEventListener(u,v),o()},v=f=>{f.target===e&&++c>=s&&d()};setTimeout(()=>{c(n[h]||"").split(", "),r=a(`${jl}Delay`),o=a(`${jl}Duration`),i=XS(r,o),l=a(`${Ef}Delay`),s=a(`${Ef}Duration`),u=XS(l,s);let c=null,d=0,v=0;t===jl?i>0&&(c=jl,d=i,v=o.length):t===Ef?u>0&&(c=Ef,d=u,v=s.length):(d=Math.max(i,u),c=d>0?i>u?jl:Ef:null,v=c?c===jl?o.length:s.length:0);const f=c===jl&&/\b(?:transform|all)(?:,|$)/.test(a(`${jl}Property`).toString());return{type:c,timeout:d,propCount:v,hasTransform:f}}function XS(e,t){for(;e.lengthZS(n)+ZS(e[a])))}function ZS(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function n4(e){return(e?e.ownerDocument:document).body.offsetHeight}function nF(e,t,n){const a=e[Dd];a&&(t=(t?[t,...a]:[...a]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const zm=Symbol("_vod"),n6=Symbol("_vsh"),Zt={name:"show",beforeMount(e,{value:t},{transition:n}){e[zm]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):Af(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:a}){!t!=!n&&(a?t?(a.beforeEnter(e),Af(e,!0),a.enter(e)):a.leave(e,()=>{Af(e,!1)}):Af(e,t))},beforeUnmount(e,{value:t}){Af(e,t)}};function Af(e,t){e.style.display=t?e[zm]:"none",e[n6]=!t}const aF=Symbol(""),rF=/(?:^|;)\s*display\s*:/;function oF(e,t,n){const a=e.style,r=Ye(n);let o=!1;if(n&&!r){if(t)if(Ye(t))for(const i of t.split(";")){const l=i.slice(0,i.indexOf(":")).trim();n[l]==null&&av(a,l,"")}else for(const i in t)n[i]==null&&av(a,i,"");for(const i in n){i==="display"&&(o=!0);const l=n[i];l!=null?lF(e,i,!Ye(t)&&t?t[i]:void 0,l)||av(a,i,l):av(a,i,"")}}else if(r){if(t!==n){const i=a[aF];i&&(n+=";"+i),a.cssText=n,o=rF.test(n)}}else t&&e.removeAttribute("style");zm in e&&(e[zm]=o?a.display:"",e[n6]&&(a.display="none"))}const JS=/\s*!important$/;function av(e,t,n){if(Ae(n))n.forEach(a=>av(e,t,a));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const a=iF(e,t);JS.test(n)?e.setProperty($l(a),n.replace(JS,""),"important"):e[a]=n}}const QS=["Webkit","Moz","ms"],D2={};function iF(e,t){const n=D2[t];if(n)return n;let a=Va(t);if(a!=="filter"&&a in e)return D2[t]=a;a=Fh(a);for(let r=0;rO2||(vF.then(()=>O2=0),O2=Date.now());function pF(e,t){const n=a=>{if(!a._vts)a._vts=Date.now();else if(a._vts<=n.attached)return;const r=n.value;if(Ae(r)){const o=a.stopImmediatePropagation;a.stopImmediatePropagation=()=>{o.call(a),a._stopped=!0};const i=r.slice(),l=[a];for(let s=0;se.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,mF=(e,t,n,a,r,o)=>{const i=r==="svg";t==="class"?nF(e,a,i):t==="style"?oF(e,n,a):a1(t)?r1(t)||uF(e,t,n,a,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):gF(e,t,a,i))?(nC(e,t,a),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&tC(e,t,a,i,o,t!=="value")):e._isVueCE&&(yF(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!Ye(a)))?nC(e,Va(t),a,o,t):(t==="true-value"?e._trueValue=a:t==="false-value"&&(e._falseValue=a),tC(e,t,a,i))};function gF(e,t,n,a){if(a)return!!(t==="innerHTML"||t==="textContent"||t in e&&rC(t)&&Qe(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return rC(t)&&Ye(n)?!1:t in e}function yF(e,t){const n=e._def.props;if(!n)return!1;const a=Va(t);return Array.isArray(n)?n.some(r=>Va(r)===a):Object.keys(n).some(r=>Va(r)===a)}const A7=new WeakMap,I7=new WeakMap,Fm=Symbol("_moveCb"),oC=Symbol("_enterCb"),bF=e=>(delete e.props.mode,e),_F=bF({name:"TransitionGroup",props:ka({},T7,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=Et(),a=GE();let r,o;return oi(()=>{if(!r.length)return;const i=e.moveClass||`${e.name||"v"}-move`;if(!xF(r[0].el,n.vnode.el,i)){r=[];return}r.forEach(wF),r.forEach(SF);const l=r.filter(CF);n4(n.vnode.el),l.forEach(s=>{const u=s.el,c=u.style;Ci(u,i),c.transform=c.webkitTransform=c.transitionDuration="";const d=u[Fm]=v=>{v&&v.target!==u||(!v||v.propertyName.endsWith("transform"))&&(u.removeEventListener("transitionend",d),u[Fm]=null,rs(u,i))};u.addEventListener("transitionend",d)}),r=[]}),()=>{const i=wn(e),l=M7(i);let s=i.tag||Ve;if(r=[],o)for(let u=0;u{l.split(/\s+/).forEach(s=>s&&a.classList.remove(s))}),n.split(/\s+/).forEach(l=>l&&a.classList.add(l)),a.style.display="none";const o=t.nodeType===1?t:t.parentNode;o.appendChild(a);const{hasTransform:i}=E7(a);return o.removeChild(a),i}const Od=e=>{const t=e.props["onUpdate:modelValue"]||!1;return Ae(t)?n=>G0(t,n):t};function kF(e){e.target.composing=!0}function iC(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Ii=Symbol("_assign"),Lp=Symbol("_initialValue");function $2(e,t,n){return t&&(e=e.trim()),n&&(e=N3(e)),e}const D7={created(e,{modifiers:{lazy:t,trim:n,number:a}},r){e.parentNode&&(e.type==="text"?e[Lp]=e.defaultValue.replace(/[\r\n]/g,""):e.type==="textarea"&&(e[Lp]=e.defaultValue.replace(/\r\n?/g,` +`))),e[Ii]=Od(r);const o=a||r.props&&r.props.type==="number";fs(e,t?"change":"input",i=>{i.target.composing||e[Ii]($2(e.value,n,o))}),(n||o)&&fs(e,"change",()=>{e.value=$2(e.value,n,o)}),t||(fs(e,"compositionstart",kF),fs(e,"compositionend",iC),fs(e,"change",iC))},mounted(e,{value:t,modifiers:{trim:n,number:a}}){const r=t??"",o=e[Lp];delete e[Lp],o!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==o?e[Ii]($2(e.value,n,a)):e.value=r},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:a,trim:r,number:o}},i){if(e[Ii]=Od(i),e.composing)return;const l=(o||e.type==="number")&&!/^0\d/.test(e.value)?N3(e.value):e.value,s=t??"";if(l===s)return;const u=e.getRootNode();(u instanceof Document||u instanceof ShadowRoot)&&u.activeElement===e&&e.type!=="range"&&(a&&t===n||r&&e.value.trim()===s)||(e.value=s)}},O7={deep:!0,created(e,t,n){e[Ii]=Od(n),fs(e,"change",()=>{const a=e._modelValue,r=R7(e),o=e.checked,i=e[Ii];if(Ae(a)){const l=hE(a,r),s=l!==-1;if(o&&!s)i(a.concat(r));else if(!o&&s){const u=[...a];u.splice(l,1),i(u)}}else if(o1(a)){const l=new Set(a);o?l.add(r):l.delete(r),i(l)}else i(N7(e,o))})},mounted:lC,beforeUpdate(e,t,n){e[Ii]=Od(n),lC(e,t,n)}};function lC(e,{value:t,oldValue:n},a){e._modelValue=t;let r;if(Ae(t))r=hE(t,a.props.value)>-1;else if(o1(t))r=t.has(a.props.value);else{if(t===n)return;r=rc(t,N7(e,!0))}e.checked!==r&&(e.checked=r)}const $7={created(e,{value:t},n){e.checked=rc(t,n.props.value),e[Ii]=Od(n),fs(e,"change",()=>{e[Ii](R7(e))})},beforeUpdate(e,{value:t,oldValue:n},a){e[Ii]=Od(a),t!==n&&(e.checked=rc(t,a.props.value))}};function R7(e){return"_value"in e?e._value:e.value}function N7(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const TF=["ctrl","shift","alt","meta"],MF={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>TF.some(n=>e[`${n}Key`]&&!t.includes(n))},at=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),a=t.join(".");return n[a]||(n[a]=(r,...o)=>{for(let i=0;i{const n=e._withKeys||(e._withKeys={}),a=t.join(".");return n[a]||(n[a]=r=>{if(!("key"in r))return;const o=$l(r.key);if(t.some(i=>i===o||EF[i]===o))return e(r)})},AF=ka({patchProp:mF},Jz);let sC;function B7(){return sC||(sC=Oz(AF))}const Ps=(...e)=>{B7().render(...e)},V7=(...e)=>{const t=B7().createApp(...e),{mount:n}=t;return t.mount=a=>{const r=LF(a);if(!r)return;const o=t._component;!Qe(o)&&!o.render&&!o.template&&(o.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const i=n(r,!1,IF(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t};function IF(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function LF(e){return Ye(e)?document.querySelector(e):e}const Pe={tab:"Tab",enter:"Enter",space:"Space",left:"ArrowLeft",up:"ArrowUp",right:"ArrowRight",down:"ArrowDown",esc:"Escape",delete:"Delete",backspace:"Backspace",numpadEnter:"NumpadEnter",pageUp:"PageUp",pageDown:"PageDown",home:"Home",end:"End"},PF=["year","years","month","months","date","dates","week","datetime","datetimerange","daterange","monthrange","yearrange"],R2=["sun","mon","tue","wed","thu","fri","sat"],bt="update:modelValue",Ot="change",ua="input",uC=Symbol("INSTALLED_KEY"),ii=["","default","small","large"],cC=["left","center","right"];var z7=typeof global=="object"&&global&&global.Object===Object&&global,DF=typeof self=="object"&&self&&self.Object===Object&&self,li=z7||DF||Function("return this")(),No=li.Symbol,F7=Object.prototype,OF=F7.hasOwnProperty,$F=F7.toString,If=No?No.toStringTag:void 0;function RF(e){var t=OF.call(e,If),n=e[If];try{e[If]=void 0;var a=!0}catch{}var r=$F.call(e);return a&&(t?e[If]=n:delete e[If]),r}var NF=Object.prototype,BF=NF.toString;function VF(e){return BF.call(e)}var zF="[object Null]",FF="[object Undefined]",dC=No?No.toStringTag:void 0;function Cc(e){return e==null?e===void 0?FF:zF:dC&&dC in Object(e)?RF(e):VF(e)}function Gi(e){return e!=null&&typeof e=="object"}var HF="[object Symbol]";function _1(e){return typeof e=="symbol"||Gi(e)&&Cc(e)==HF}function a6(e,t){for(var n=-1,a=e==null?0:e.length,r=Array(a);++n0){if(++t>=mH)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}function _H(e){return function(){return e}}var Hm=function(){try{var e=kc(Object,"defineProperty");return e({},"",{}),e}catch{}}(),wH=Hm?function(e,t){return Hm(e,"toString",{configurable:!0,enumerable:!1,value:_H(t),writable:!0})}:r6,K7=bH(wH);function SH(e,t){for(var n=-1,a=e==null?0:e.length;++n-1}var MH=9007199254740991,EH=/^(?:0|[1-9]\d*)$/;function w1(e,t){var n=typeof e;return t=t??MH,!!t&&(n=="number"||n!="symbol"&&EH.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=LH}function df(e){return e!=null&&l6(e.length)&&!o6(e)}function PH(e,t,n){if(!Tr(n))return!1;var a=typeof t;return(a=="number"?df(n)&&w1(t,n.length):a=="string"&&t in n)?Uh(n[t],e):!1}function DH(e){return Y7(function(t,n){var a=-1,r=n.length,o=r>1?n[r-1]:void 0,i=r>2?n[2]:void 0;for(o=e.length>3&&typeof o=="function"?(r--,o):void 0,i&&PH(n[0],n[1],i)&&(o=r<3?void 0:o,r=1),t=Object(t);++a-1}function WW(e,t){var n=this.__data__,a=C1(n,e);return a<0?(++this.size,n.push([e,t])):n[a][1]=t,this}function Bl(e){var t=-1,n=e==null?0:e.length;for(this.clear();++t0&&n(l)?t>1?jh(l,t-1,n,a,r):f6(r,l):a||(r[r.length]=l)}return r}function Wm(e){var t=e==null?0:e.length;return t?jh(e,1):[]}function Q7(e){return K7(G7(e,void 0,Wm),e+"")}var v6=J7(Object.getPrototypeOf,Object),rK="[object Object]",oK=Function.prototype,iK=Object.prototype,eA=oK.toString,lK=iK.hasOwnProperty,sK=eA.call(Object);function h6(e){if(!Gi(e)||Cc(e)!=rK)return!1;var t=v6(e);if(t===null)return!0;var n=lK.call(t,"constructor")&&t.constructor;return typeof n=="function"&&n instanceof n&&eA.call(n)==sK}function uK(e,t,n){var a=-1,r=e.length;t<0&&(t=-t>r?0:r+t),n=n>r?r:n,n<0&&(n+=r),r=t>n?0:n-t>>>0,t>>>=0;for(var o=Array(r);++a=t?e:t)),e}function Br(e,t,n){return n===void 0&&(n=t,t=void 0),n!==void 0&&(n=Mv(n),n=n===n?n:0),t!==void 0&&(t=Mv(t),t=t===t?t:0),cK(Mv(e),t,n)}function dK(){this.__data__=new Bl,this.size=0}function fK(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n}function vK(e){return this.__data__.get(e)}function hK(e){return this.__data__.has(e)}var pK=200;function mK(e,t){var n=this.__data__;if(n instanceof Bl){var a=n.__data__;if(!ih||a.lengthl))return!1;var u=o.get(e),c=o.get(t);if(u&&c)return u==t&&c==e;var d=-1,v=!0,f=n&KU?new lh:void 0;for(o.set(e,t),o.set(t,e);++d=t||T<0||d&&M>=o}function y(){var k=z2();if(g(k))return b(k);l=setTimeout(y,m(k))}function b(k){return l=void 0,v&&a?f(k):(a=r=void 0,i)}function _(){l!==void 0&&clearTimeout(l),u=0,a=s=r=l=void 0}function w(){return l===void 0?i:b(z2())}function S(){var k=z2(),T=g(k);if(a=arguments,r=this,s=k,T){if(l===void 0)return h(s);if(d)return clearTimeout(l),l=setTimeout(y,t),f(s)}return l===void 0&&(l=setTimeout(y,t)),i}return S.cancel=_,S.flush=w,S}function l4(e,t,n){(n!==void 0&&!Uh(e[t],n)||n===void 0&&!(t in e))&&S1(e,t,n)}function bA(e){return Gi(e)&&df(e)}function s4(e,t){if(!(t==="constructor"&&typeof e[t]=="function")&&t!="__proto__")return e[t]}function DG(e){return cf(e,Yh(e))}function OG(e,t,n,a,r,o,i){var l=s4(e,n),s=s4(t,n),u=i.get(s);if(u){l4(e,n,u);return}var c=o?o(l,s,n+"",e,t,i):void 0,d=c===void 0;if(d){var v=kr(s),f=!v&&rh(s),h=!v&&!f&&c6(s);c=s,v||f||h?kr(l)?c=l:bA(l)?c=W7(l):f?(d=!1,c=nA(s,!0)):h?(d=!1,c=lA(s,!0)):c=[]:h6(s)||ah(s)?(c=l,ah(l)?c=DG(l):(!Tr(l)||o6(l))&&(c=sA(s))):d=!1}d&&(i.set(s,c),r(c,s,a,o,i),i.delete(s)),l4(e,n,c)}function _A(e,t,n,a,r){e!==t&&yA(t,function(o,i){if(r||(r=new Xo),Tr(o))OG(e,t,i,n,_A,a,r);else{var l=a?a(s4(e,i),o,i+"",e,t,r):void 0;l===void 0&&(l=o),l4(e,i,l)}},Yh)}function $G(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}function wA(e,t,n){var a=e==null?0:e.length;if(!a)return-1;var r=a-1;return U7(e,gA(t),r,!0)}function RG(e,t){var n=-1,a=df(e)?Array(e.length):[];return AG(e,function(r,o,i){a[++n]=t(r,o,i)}),a}function NG(e,t){var n=kr(e)?a6:RG;return n(e,gA(t))}function SA(e,t){return jh(NG(e,t),1)}var BG=1/0;function VG(e){var t=e==null?0:e.length;return t?jh(e,BG):[]}function Rd(e){for(var t=-1,n=e==null?0:e.length,a={};++t1),o}),cf(e,iA(e),n),a&&(n=Av(n,UG|GG|YG,KG));for(var r=t.length;r--;)WG(n,t[r]);return n});function kA(e,t,n,a){if(!Tr(e))return e;t=ff(t,e);for(var r=-1,o=t.length,i=o-1,l=e;l!=null&&++r=eY){var u=QG(e);if(u)return g6(u);i=!1,r=fA,s=new lh}else s=l;e:for(;++ae===void 0,vn=e=>typeof e=="boolean",Je=e=>typeof e=="number",_r=e=>!e&&e!==0||Ae(e)&&e.length===0||_t(e)&&!Object.keys(e).length,Vr=e=>typeof Element>"u"?!1:e instanceof Element,Qr=e=>Un(e),nY=e=>Ye(e)?!Number.isNaN(Number(e)):!1,qh=e=>e===window,sh=e=>Object.keys(e),TA=e=>Object.entries(e),Cs=(e,t,n)=>({get value(){return na(e,t,n)},set value(a){XG(e,t,a)}}),aY="__epPropKey",ae=e=>e,rY=e=>_t(e)&&!!e.__epPropKey,si=(e,t)=>{if(!_t(e)||rY(e))return e;const{values:n,required:a,default:r,type:o,validator:i}=e,l={type:o,required:!!a,validator:n||i?s=>{let u=!1,c=[];if(n&&(c=Array.from(n),en(e,"default")&&c.push(r),u||(u=c.includes(s))),i&&(u||(u=i(s))),!u&&c.length>0){const d=[...new Set(c)].map(v=>JSON.stringify(v)).join(", ");qz(`Invalid prop: validation failed${t?` for prop "${t}"`:""}. Expected one of [${d}], got value ${JSON.stringify(s)}.`)}return u}:void 0,[aY]:!0};return en(e,"default")&&(l.default=r),l},$e=e=>Rd(Object.entries(e).map(([t,n])=>[t,si(n,t)])),oY=$e({zIndex:{type:ae([Number,String]),default:100},target:{type:String,default:""},offset:{type:Number,default:0},position:{type:String,values:["top","bottom"],default:"top"},teleported:Boolean,appendTo:{type:ae([String,Object]),default:"body"}}),iY={scroll:({scrollTop:e,fixed:t})=>Je(e)&&vn(t),[Ot]:e=>vn(e)};var MA=class extends Error{constructor(e){super(e),this.name="ElementPlusError"}};function Dn(e,t){throw new MA(`[${e}] ${t}`)}function un(e,t){{const n=Ye(e)?new MA(`[${e}] ${t}`):e;console.warn(n)}}const lY=["class","style"],sY=/^on[A-Z]/,A1=(e={})=>{const{excludeListeners:t=!1,excludeKeys:n}=e,a=D(()=>((n==null?void 0:n.value)||[]).concat(lY)),r=Et();return r?D(()=>{var o;return Rd(Object.entries((o=r.proxy)==null?void 0:o.$attrs).filter(([i])=>!a.value.includes(i)&&!(t&&sY.test(i))))}):(un("use-attrs","getCurrentInstance() returned null. useAttrs() must be called at the top of a setup function"),D(()=>({})))};function EA(e,t){var n;const a=Ht();return ur(()=>{a.value=e()},{...t,flush:(n=t==null?void 0:t.flush)!==null&&n!==void 0?n:"sync"}),Ld(a)}function Ds(e,t){return B3()?(yE(e,t),!0):!1}const H2=new WeakMap,uY=(...e)=>{var t;const n=e[0],a=(t=Et())===null||t===void 0?void 0:t.proxy,r=a??B3();if(r==null&&!WE())throw new Error("injectLocal must be called in setup");return r&&H2.has(r)&&n in H2.get(r)?H2.get(r)[n]:Ze(...e)},qt=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const cY=e=>typeof e<"u",AA=e=>e!=null,dY=Object.prototype.toString,fY=e=>dY.call(e)==="[object Object]",I1=(e,t,n)=>Math.min(n,Math.max(t,e)),Dr=()=>{},Nd=vY();function vY(){var e,t,n;return qt&&!!(!((e=window)===null||e===void 0||(e=e.navigator)===null||e===void 0)&&e.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((t=window)===null||t===void 0||(t=t.navigator)===null||t===void 0?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test((n=window)===null||n===void 0?void 0:n.navigator.userAgent))}function IA(e,t){function n(...a){return new Promise((r,o)=>{Promise.resolve(e(()=>t.apply(this,a),{fn:t,thisArg:this,args:a})).then(r).catch(o)})}return n}function hY(e,t={}){let n,a,r=Dr;const o=s=>{clearTimeout(s),r(),r=Dr};let i;return s=>{const u=ia(e),c=ia(t.maxWait);return n&&o(n),u<=0||c!==void 0&&c<=0?(a&&(o(a),a=void 0),Promise.resolve(s())):new Promise((d,v)=>{r=t.rejectOnCancel?v:d,i=s,c&&!a&&(a=setTimeout(()=>{n&&o(n),a=void 0,d(i())},c)),n=setTimeout(()=>{a&&o(a),a=void 0,d(s())},u)})}}function pY(...e){let t=0,n,a=!0,r=Dr,o,i,l,s,u;!xn(e[0])&&typeof e[0]=="object"?{delay:i,trailing:l=!0,leading:s=!0,rejectOnCancel:u=!1}=e[0]:[i,l=!0,s=!0,u=!1]=e;const c=()=>{n&&(clearTimeout(n),n=void 0,r(),r=Dr)};return v=>{const f=ia(i),h=Date.now()-t,m=()=>o=v();return c(),f<=0?(t=Date.now(),m()):(h>f?(t=Date.now(),(s||!a)&&m()):l&&(o=new Promise((g,y)=>{r=u?y:g,n=setTimeout(()=>{t=Date.now(),a=!0,g(m()),c()},Math.max(0,f-h))})),!s&&!n&&(n=setTimeout(()=>a=!0,f)),a=!1,o)}}function zC(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function bd(e){return Array.isArray(e)?e:[e]}function mY(e){return Et()}function gY(e){return nn(xn(e)?new Proxy({},{get(t,n,a){return p(Reflect.get(e.value,n,a))},set(t,n,a){return xn(e.value[n])&&!xn(a)?e.value[n].value=a:e.value[n]=a,!0},deleteProperty(t,n){return Reflect.deleteProperty(e.value,n)},has(t,n){return Reflect.has(e.value,n)},ownKeys(){return Object.keys(e.value)},getOwnPropertyDescriptor(){return{enumerable:!0,configurable:!0}}}):e)}function yY(e){return gY(D(e))}function Xh(e,t=200,n={}){return IA(hY(t,n),e)}function bY(e,t=200,n={}){const a=F(ia(e)),r=Xh(()=>{a.value=e.value},t,n);return ye(e,()=>r()),K3(a)}function LA(e,t=200,n=!1,a=!0,r=!1){return IA(pY(t,n,a,r),e)}function y6(e,t=!0,n){mY()?mt(e,n):t?e():He(e)}function _Y(e,t=1e3,n={}){const{immediate:a=!0,immediateCallback:r=!1}=n;let o=null;const i=Ht(!1);function l(){o&&(clearInterval(o),o=null)}function s(){i.value=!1,l()}function u(){const c=ia(t);c<=0||(i.value=!0,r&&e(),l(),i.value&&(o=setInterval(e,c)))}return a&&qt&&u(),(xn(t)||typeof t=="function")&&Ds(ye(t,()=>{i.value&&qt&&u()})),Ds(s),{isActive:K3(i),pause:s,resume:u}}function Bd(e,t,n={}){const{immediate:a=!0,immediateCallback:r=!1}=n,o=Ht(!1);let i;function l(){i&&(clearTimeout(i),i=void 0)}function s(){o.value=!1,l()}function u(...c){r&&e(),l(),o.value=!0,i=setTimeout(()=>{o.value=!1,i=void 0,e(...c)},ia(t))}return a&&(o.value=!0,qt&&u()),Ds(s),{isPending:K3(o),start:u,stop:s}}function wY(e,t,n){return ye(e,t,{...n,immediate:!0})}const Ho=qt?window:void 0,SY=qt?window.document:void 0;function da(e){var t;const n=ia(e);return(t=n==null?void 0:n.$el)!==null&&t!==void 0?t:n}function on(...e){const t=(a,r,o,i)=>(a.addEventListener(r,o,i),()=>a.removeEventListener(r,o,i)),n=D(()=>{const a=bd(ia(e[0])).filter(r=>r!=null);return a.every(r=>typeof r!="string")?a:void 0});return wY(()=>{var a,r;return[(a=(r=n.value)===null||r===void 0?void 0:r.map(o=>da(o)))!==null&&a!==void 0?a:[Ho].filter(o=>o!=null),bd(ia(n.value?e[1]:e[0])),bd(p(n.value?e[2]:e[1])),ia(n.value?e[3]:e[2])]},([a,r,o,i],l,s)=>{if(!(a!=null&&a.length)||!(r!=null&&r.length)||!(o!=null&&o.length))return;const u=fY(i)?{...i}:i,c=a.flatMap(d=>r.flatMap(v=>o.map(f=>t(d,v,f,u))));s(()=>{c.forEach(d=>d())})},{flush:"post"})}let FC=!1;function b6(e,t,n={}){const{window:a=Ho,ignore:r=[],capture:o=!0,detectIframe:i=!1,controls:l=!1}=n;if(!a)return l?{stop:Dr,cancel:Dr,trigger:Dr}:Dr;if(Nd&&!FC){FC=!0;const g={passive:!0};Array.from(a.document.body.children).forEach(y=>y.addEventListener("click",Dr,g)),a.document.documentElement.addEventListener("click",Dr,g)}let s=!0;const u=g=>ia(r).some(y=>{if(typeof y=="string")return Array.from(a.document.querySelectorAll(y)).some(b=>b===g.target||g.composedPath().includes(b));{const b=da(y);return b&&(g.target===b||g.composedPath().includes(b))}});function c(g){const y=ia(g);return y&&y.$.subTree.shapeFlag===16}function d(g,y){const b=ia(g),_=b.$.subTree&&b.$.subTree.children;return _==null||!Array.isArray(_)?!1:_.some(w=>w.el===y.target||y.composedPath().includes(w.el))}const v=g=>{const y=da(e);if(g.target!=null&&!(!(y instanceof Element)&&c(e)&&d(e,g))&&!(!y||y===g.target||g.composedPath().includes(y))){if("detail"in g&&g.detail===0&&(s=!u(g)),!s){s=!0;return}t(g)}};let f=!1;const h=[on(a,"click",g=>{f||(f=!0,setTimeout(()=>{f=!1},0),v(g))},{passive:!0,capture:o}),on(a,"pointerdown",g=>{const y=da(e);s=!u(g)&&!!(y&&!g.composedPath().includes(y))},{passive:!0}),i&&on(a,"blur",g=>{setTimeout(()=>{const y=da(e);let b=a.document.activeElement;for(;b!=null&&b.shadowRoot;)b=b.shadowRoot.activeElement;(b==null?void 0:b.tagName)==="IFRAME"&&!(y!=null&&y.contains(a.document.activeElement))&&t(g)},0)},{passive:!0})].filter(Boolean),m=()=>h.forEach(g=>g());return l?{stop:m,cancel:()=>{s=!1},trigger:g=>{s=!0,v(g),s=!1}}:m}function CY(){const e=Ht(!1),t=Et();return t&&mt(()=>{e.value=!0},t),e}function L1(e){const t=CY();return D(()=>(t.value,!!e()))}function Zh(e,t,n={}){const{window:a=Ho,...r}=n;let o;const i=L1(()=>a&&"MutationObserver"in a),l=()=>{o&&(o.disconnect(),o=void 0)},s=ye(D(()=>{const d=bd(ia(e)).map(da).filter(AA);return new Set(d)}),d=>{l(),i.value&&d.size&&(o=new MutationObserver(t),d.forEach(v=>o.observe(v,r)))},{immediate:!0,flush:"post"}),u=()=>o==null?void 0:o.takeRecords(),c=()=>{s(),l()};return Ds(c),{isSupported:i,stop:c,takeRecords:u}}function xY(e,t,n={}){const{window:a=Ho,document:r=a==null?void 0:a.document,flush:o="sync"}=n;if(!a||!r)return Dr;let i;const l=c=>{i==null||i(),i=c},s=ur(()=>{const c=da(e);if(c){const{stop:d}=Zh(r,v=>{v.map(f=>[...f.removedNodes]).flat().some(f=>f===c||f.contains(c))&&t(v)},{window:a,childList:!0,subtree:!0});l(d)}},{flush:o}),u=()=>{s(),l()};return Ds(u),u}function kY(e={}){var t;const{window:n=Ho,deep:a=!0,triggerOnRemoval:r=!1}=e,o=(t=e.document)!==null&&t!==void 0?t:n==null?void 0:n.document,i=()=>{let u=o==null?void 0:o.activeElement;if(a)for(var c;u!=null&&u.shadowRoot;)u=u==null||(c=u.shadowRoot)===null||c===void 0?void 0:c.activeElement;return u},l=Ht(),s=()=>{l.value=i()};if(n){const u={capture:!0,passive:!0};on(n,"blur",c=>{c.relatedTarget===null&&s()},u),on(n,"focus",s,u)}return r&&xY(l,s,{document:o}),s(),l}const TY=Symbol("vueuse-ssr-width");function MY(){const e=WE()?uY(TY,null):null;return typeof e=="number"?e:void 0}function EY(e,t={}){const{window:n=Ho,ssrWidth:a=MY()}=t,r=L1(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function"),o=Ht(typeof a=="number"),i=Ht(),l=Ht(!1),s=u=>{l.value=u.matches};return ur(()=>{if(o.value){o.value=!r.value,l.value=ia(e).split(",").some(u=>{const c=u.includes("not all"),d=u.match(/\(\s*min-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/),v=u.match(/\(\s*max-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/);let f=!!(d||v);return d&&f&&(f=a>=zC(d[1])),v&&f&&(f=a<=zC(v[1])),c?!f:f});return}r.value&&(i.value=n.matchMedia(ia(e)),l.value=i.value.matches)}),on(i,"change",s,{passive:!0}),D(()=>l.value)}function AY(e){return JSON.parse(JSON.stringify(e))}function IY(e,t,n={}){const{window:a=Ho,initialValue:r,observe:o=!1}=n,i=Ht(r),l=D(()=>{var u;return da(t)||(a==null||(u=a.document)===null||u===void 0?void 0:u.documentElement)});function s(){const u=ia(e),c=ia(l);if(c&&a&&u){var d;i.value=((d=a.getComputedStyle(c).getPropertyValue(u))===null||d===void 0?void 0:d.trim())||i.value||r}}return o&&Zh(l,s,{attributeFilter:["style","class"],window:a}),ye([l,()=>ia(e)],(u,c)=>{c[0]&&c[1]&&c[0].style.removeProperty(c[1]),s()},{immediate:!0}),ye([i,l],([u,c])=>{const d=ia(e);c!=null&&c.style&&d&&(u==null?c.style.removeProperty(d):c.style.setProperty(d,u))},{immediate:!0}),i}function LY(e={}){const{document:t=SY}=e;if(!t)return Ht("visible");const n=Ht(t.visibilityState);return on(t,"visibilitychange",()=>{n.value=t.visibilityState},{passive:!0}),n}function Mn(e,t,n={}){const{window:a=Ho,...r}=n;let o;const i=L1(()=>a&&"ResizeObserver"in a),l=()=>{o&&(o.disconnect(),o=void 0)},s=ye(D(()=>{const c=ia(e);return Array.isArray(c)?c.map(d=>da(d)):[da(c)]}),c=>{if(l(),i.value&&a){o=new ResizeObserver(t);for(const d of c)d&&o.observe(d,r)}},{immediate:!0,flush:"post"}),u=()=>{l(),s()};return Ds(u),{isSupported:i,stop:u}}function HC(e,t={}){const{reset:n=!0,windowResize:a=!0,windowScroll:r=!0,immediate:o=!0,updateTiming:i="sync"}=t,l=Ht(0),s=Ht(0),u=Ht(0),c=Ht(0),d=Ht(0),v=Ht(0),f=Ht(0),h=Ht(0);function m(){const y=da(e);if(!y){n&&(l.value=0,s.value=0,u.value=0,c.value=0,d.value=0,v.value=0,f.value=0,h.value=0);return}const b=y.getBoundingClientRect();l.value=b.height,s.value=b.bottom,u.value=b.left,c.value=b.right,d.value=b.top,v.value=b.width,f.value=b.x,h.value=b.y}function g(){i==="sync"?m():i==="next-frame"&&requestAnimationFrame(()=>m())}return Mn(e,g),ye(()=>da(e),y=>!y&&g()),Zh(e,g,{attributeFilter:["style","class"]}),r&&on("scroll",g,{capture:!0,passive:!0}),a&&on("resize",g,{passive:!0}),y6(()=>{o&&g()}),{height:l,bottom:s,left:u,right:c,top:d,width:v,x:f,y:h,update:g}}function Um(e,t={width:0,height:0},n={}){const{window:a=Ho,box:r="content-box"}=n,o=D(()=>{var d;return(d=da(e))===null||d===void 0||(d=d.namespaceURI)===null||d===void 0?void 0:d.includes("svg")}),i=Ht(t.width),l=Ht(t.height),{stop:s}=Mn(e,([d])=>{const v=r==="border-box"?d.borderBoxSize:r==="content-box"?d.contentBoxSize:d.devicePixelContentBoxSize;if(a&&o.value){const f=da(e);if(f){const h=f.getBoundingClientRect();i.value=h.width,l.value=h.height}}else if(v){const f=bd(v);i.value=f.reduce((h,{inlineSize:m})=>h+m,0),l.value=f.reduce((h,{blockSize:m})=>h+m,0)}else i.value=d.contentRect.width,l.value=d.contentRect.height},n);y6(()=>{const d=da(e);d&&(i.value="offsetWidth"in d?d.offsetWidth:t.width,l.value="offsetHeight"in d?d.offsetHeight:t.height)});const u=ye(()=>da(e),d=>{i.value=d?t.width:0,l.value=d?t.height:0});function c(){s(),u()}return{width:i,height:l,stop:c}}function PY(e,t,n={}){const{root:a,rootMargin:r,threshold:o=0,window:i=Ho,immediate:l=!0}=n,s=L1(()=>i&&"IntersectionObserver"in i),u=D(()=>bd(ia(e)).map(da).filter(AA));let c=Dr;const d=Ht(l),v=s.value?ye(()=>[u.value,da(a),ia(r),d.value],([h,m,g])=>{if(c(),!d.value||!h.length)return;const y=new IntersectionObserver(t,{root:da(m),rootMargin:g,threshold:o});h.forEach(b=>b&&y.observe(b)),c=()=>{y.disconnect(),c=Dr}},{immediate:l,flush:"post"}):Dr,f=()=>{c(),v(),d.value=!1};return Ds(f),{isSupported:s,isActive:d,pause(){c(),d.value=!1},resume(){d.value=!0},stop:f}}function DY(e,t,n,a={}){var r,o;const{clone:i=!1,passive:l=!1,eventName:s,deep:u=!1,defaultValue:c,shouldEmit:d}=a,v=Et(),f=(v==null?void 0:v.emit)||(v==null||(r=v.$emit)===null||r===void 0?void 0:r.bind(v))||(v==null||(o=v.proxy)===null||o===void 0||(o=o.$emit)===null||o===void 0?void 0:o.bind(v==null?void 0:v.proxy));let h=s;h=h||`update:${t.toString()}`;const m=b=>i?typeof i=="function"?i(b):AY(b):b,g=()=>cY(e[t])?m(e[t]):c,y=b=>{d?d(b)&&f(h,b):f(h,b)};if(l){const b=F(g());let _=!1;return ye(()=>e[t],w=>{_||(_=!0,b.value=m(w),He(()=>_=!1))}),ye(b,w=>{!_&&(w!==e[t]||u)&&y(w)},{deep:u}),b}else return D({get(){return g()},set(b){y(b)}})}function OY(e={}){const{window:t=Ho}=e;if(!t)return Ht(!1);const n=Ht(t.document.hasFocus()),a={passive:!0};return on(t,"blur",()=>{n.value=!1},a),on(t,"focus",()=>{n.value=!0},a),n}function _6(e={}){const{window:t=Ho,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:a=Number.POSITIVE_INFINITY,listenOrientation:r=!0,includeScrollbar:o=!0,type:i="inner"}=e,l=Ht(n),s=Ht(a),u=()=>{if(t)if(i==="outer")l.value=t.outerWidth,s.value=t.outerHeight;else if(i==="visual"&&t.visualViewport){const{width:d,height:v,scale:f}=t.visualViewport;l.value=Math.round(d*f),s.value=Math.round(v*f)}else o?(l.value=t.innerWidth,s.value=t.innerHeight):(l.value=t.document.documentElement.clientWidth,s.value=t.document.documentElement.clientHeight)};u(),y6(u);const c={passive:!0};return on("resize",u,c),t&&i==="visual"&&t.visualViewport&&on(t.visualViewport,"resize",u,c),r&&ye(EY("(orientation: portrait)"),()=>u()),{width:l,height:s}}function w6(){const e=Ht(),t=F(0),n=D(()=>({minWidth:`${Math.max(t.value,11)}px`}));return Mn(e,()=>{var r;t.value=((r=e.value)==null?void 0:r.getBoundingClientRect().width)??0}),{calculatorRef:e,calculatorWidth:t,inputStyle:n}}const Ri=({from:e,replacement:t,scope:n,version:a,ref:r,type:o="API"},i)=>{ye(()=>p(i),l=>{l&&un(n,`[${o}] ${e} is about to be deprecated in version ${a}, please use ${t} instead. +For more detail, please visit: ${r} +`)},{immediate:!0})},$Y='a[href],button:not([disabled]),button:not([hidden]),:not([tabindex="-1"]),input:not([disabled]),input:not([type="hidden"]),select:not([disabled]),textarea:not([disabled])',PA=e=>typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot,WC=e=>typeof Element>"u"?!1:e instanceof Element,RY=e=>getComputedStyle(e).position==="fixed"?!1:e.offsetParent!==null,KC=e=>Array.from(e.querySelectorAll($Y)).filter(t=>sc(t)&&RY(t)),sc=e=>{if(e.tabIndex>0||e.tabIndex===0&&e.getAttribute("tabIndex")!==null)return!0;if(e.tabIndex<0||e.hasAttribute("disabled")||e.getAttribute("aria-disabled")==="true")return!1;switch(e.nodeName){case"A":return!!e.href&&e.rel!=="ignore";case"INPUT":return!(e.type==="hidden"||e.type==="file");case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},X0=function(e,t,...n){let a;t.includes("mouse")||t.includes("click")?a="MouseEvents":t.includes("key")?a="KeyboardEvent":a="HTMLEvents";const r=document.createEvent(a);return r.initEvent(t,...n),e.dispatchEvent(r),e},DA=e=>!e.getAttribute("aria-owns"),OA=(e,t,n)=>{const{parentNode:a}=e;if(!a)return null;const r=a.querySelectorAll(n);return r[Array.prototype.indexOf.call(r,e)+t]||null},Jh=(e,t)=>{if(!e||!e.focus)return;let n=!1;WC(e)&&!sc(e)&&!e.getAttribute("tabindex")&&(e.setAttribute("tabindex","-1"),n=!0),e.focus(t),WC(e)&&n&&e.removeAttribute("tabindex")},_d=e=>{e&&(Jh(e),!DA(e)&&e.click())},P1=()=>qt&&/firefox/i.test(window.navigator.userAgent),$A=()=>qt&&/android/i.test(window.navigator.userAgent),S6=(e="")=>e.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),W2=e=>Fh(e),NY="utils/dom/style",RA=(e="")=>e.split(" ").filter(t=>!!t.trim()),Ni=(e,t)=>{if(!e||!t)return!1;if(t.includes(" "))throw new Error("className should not contain space.");return e.classList.contains(t)},Eo=(e,t)=>{!e||!t.trim()||e.classList.add(...RA(t))},ir=(e,t)=>{!e||!t.trim()||e.classList.remove(...RA(t))},ml=(e,t)=>{var a;if(!qt||!e||!t||PA(e))return"";let n=Va(t);n==="float"&&(n="cssFloat");try{const r=e.style[n];if(r)return r;const o=(a=document.defaultView)==null?void 0:a.getComputedStyle(e,"");return o?o[n]:""}catch{return e.style[n]}},NA=(e,t,n)=>{if(!(!e||!t))if(_t(t))TA(t).forEach(([a,r])=>NA(e,a,r));else{const a=Va(t);e.style[a]=n}};function Vn(e,t="px"){if(!e&&e!==0)return"";if(Je(e)||nY(e))return`${e}${t}`;if(Ye(e))return e;un(NY,"binding value must be a string or number")}const BA=(e,t,n,a)=>{const r={offsetX:0,offsetY:0},o=F(!1),i=(v,f)=>{if(e.value){const{offsetX:h,offsetY:m}=r,g=e.value.getBoundingClientRect(),y=Math.max(g.left,0),b=Math.max(g.top,0),_=g.width,w=g.height,S=document.documentElement.clientWidth,k=document.documentElement.clientHeight,T=-y+h,M=-b+m,E=S-y-_+h,A=k-b-(w{const f=v.clientX,h=v.clientY,{offsetX:m,offsetY:g}=r,y=_=>{o.value||(o.value=!0),i(m+_.clientX-f,g+_.clientY-h)},b=()=>{o.value=!1,document.removeEventListener("mousemove",y),document.removeEventListener("mouseup",b)};document.addEventListener("mousemove",y),document.addEventListener("mouseup",b)},s=()=>{t.value&&e.value&&(t.value.addEventListener("mousedown",l),window.addEventListener("resize",d))},u=()=>{t.value&&e.value&&(t.value.removeEventListener("mousedown",l),window.removeEventListener("resize",d))},c=()=>{r.offsetX=0,r.offsetY=0,e.value&&(e.value.style.transform="")},d=()=>{const{offsetX:v,offsetY:f}=r;i(v,f)};return mt(()=>{ur(()=>{n.value?s():u()})}),rn(()=>{u()}),{isDragging:o,resetPosition:c,updatePosition:d}};var BY={name:"en",el:{breadcrumb:{label:"Breadcrumb"},colorpicker:{confirm:"OK",clear:"Clear",defaultLabel:"color picker",description:"current color is {color}. press enter to select a new color.",alphaLabel:"pick alpha value",alphaDescription:"alpha {alpha}, current color is {color}",hueLabel:"pick hue value",hueDescription:"hue {hue}, current color is {color}",svLabel:"pick saturation and brightness value",svDescription:"saturation {saturation}, brightness {brightness}, current color is {color}",predefineDescription:"select {value} as the color"},datepicker:{now:"Now",today:"Today",cancel:"Cancel",clear:"Clear",confirm:"OK",dateTablePrompt:"Use the arrow keys and enter to select the day of the month",monthTablePrompt:"Use the arrow keys and enter to select the month",yearTablePrompt:"Use the arrow keys and enter to select the year",selectedDate:"Selected date",selectDate:"Select date",selectTime:"Select time",startDate:"Start Date",startTime:"Start Time",endDate:"End Date",endTime:"End Time",prevYear:"Previous Year",nextYear:"Next Year",prevMonth:"Previous Month",nextMonth:"Next Month",year:"",month1:"January",month2:"February",month3:"March",month4:"April",month5:"May",month6:"June",month7:"July",month8:"August",month9:"September",month10:"October",month11:"November",month12:"December",weeks:{sun:"Sun",mon:"Mon",tue:"Tue",wed:"Wed",thu:"Thu",fri:"Fri",sat:"Sat"},weeksFull:{sun:"Sunday",mon:"Monday",tue:"Tuesday",wed:"Wednesday",thu:"Thursday",fri:"Friday",sat:"Saturday"},months:{jan:"Jan",feb:"Feb",mar:"Mar",apr:"Apr",may:"May",jun:"Jun",jul:"Jul",aug:"Aug",sep:"Sep",oct:"Oct",nov:"Nov",dec:"Dec"}},inputNumber:{decrease:"decrease number",increase:"increase number"},select:{loading:"Loading",noMatch:"No matching data",noData:"No data",placeholder:"Select"},mention:{loading:"Loading"},dropdown:{toggleDropdown:"Toggle Dropdown"},cascader:{noMatch:"No matching data",loading:"Loading",placeholder:"Select",noData:"No data"},pagination:{goto:"Go to",pagesize:"/page",total:"Total {total}",pageClassifier:"",page:"Page",prev:"Go to previous page",next:"Go to next page",currentPage:"page {pager}",prevPages:"Previous {pager} pages",nextPages:"Next {pager} pages",deprecationWarning:"Deprecated usages detected, please refer to the el-pagination documentation for more details"},dialog:{close:"Close this dialog"},drawer:{close:"Close this dialog"},messagebox:{title:"Message",confirm:"OK",cancel:"Cancel",error:"Illegal input",close:"Close this dialog"},upload:{deleteTip:"press delete to remove",delete:"Delete",preview:"Preview",continue:"Continue"},slider:{defaultLabel:"slider between {min} and {max}",defaultRangeStartLabel:"pick start value",defaultRangeEndLabel:"pick end value"},table:{emptyText:"No Data",confirmFilter:"Confirm",resetFilter:"Reset",clearFilter:"All",sumText:"Sum",selectAllLabel:"Select all rows",selectRowLabel:"Select this row",expandRowLabel:"Expand this row",collapseRowLabel:"Collapse this row",sortLabel:"Sort by {column}",filterLabel:"Filter by {column}"},tag:{close:"Close this tag"},tour:{next:"Next",previous:"Previous",finish:"Finish",close:"Close this dialog"},tree:{emptyText:"No Data"},transfer:{noMatch:"No matching data",noData:"No data",titles:["List 1","List 2"],filterPlaceholder:"Enter keyword",noCheckedFormat:"{total} items",hasCheckedFormat:"{checked}/{total} checked"},image:{error:"FAILED"},pageHeader:{title:"Back"},popconfirm:{confirmButtonText:"Yes",cancelButtonText:"No"},carousel:{leftArrow:"Carousel arrow left",rightArrow:"Carousel arrow right",indicator:"Carousel switch to index {index}"},inputOTP:{groupLabel:"OTP Input",defaultLabel:"Please enter OTP character {index}"}}};const VY=e=>(t,n)=>zY(t,n,p(e)),zY=(e,t,n)=>na(n,e,e).replace(/\{(\w+)\}/g,(a,r)=>`${(t==null?void 0:t[r])??`{${r}}`}`),FY=e=>({lang:D(()=>p(e).name),locale:xn(e)?e:F(e),t:VY(e)}),VA=Symbol("localeContextKey"),Vt=e=>{const t=e||Ze(VA,F());return FY(D(()=>t.value||BY))},HY="is-",eu=(e,t,n,a,r)=>{let o=`${e}-${t}`;return n&&(o+=`-${n}`),a&&(o+=`__${a}`),r&&(o+=`--${r}`),o},zA=Symbol("namespaceContextKey"),C6=e=>{const t=e||(Et()?Ze(zA,F("el")):F("el"));return D(()=>p(t)||"el")},ke=(e,t)=>{const n=C6(t);return{namespace:n,b:(m="")=>eu(n.value,e,m,"",""),e:m=>m?eu(n.value,e,"",m,""):"",m:m=>m?eu(n.value,e,"","",m):"",be:(m,g)=>m&&g?eu(n.value,e,m,g,""):"",em:(m,g)=>m&&g?eu(n.value,e,"",m,g):"",bm:(m,g)=>m&&g?eu(n.value,e,m,"",g):"",bem:(m,g,y)=>m&&g&&y?eu(n.value,e,m,g,y):"",is:(m,...g)=>{const y=g.length>=1?g[0]:!0;return m&&y?`${HY}${m}`:""},cssVar:m=>{const g={};for(const y in m)m[y]&&(g[`--${n.value}-${y}`]=m[y]);return g},cssVarName:m=>`--${n.value}-${m}`,cssVarBlock:m=>{const g={};for(const y in m)m[y]&&(g[`--${n.value}-${e}-${y}`]=m[y]);return g},cssVarBlockName:m=>`--${n.value}-${e}-${m}`}};function WY(e,t,n,a){const r=n-t;return e/=a/2,e<1?r/2*e*e*e+t:r/2*((e-=2)*e*e+2)+t}const Wr=e=>qt?window.requestAnimationFrame(e):setTimeout(e,16),ti=e=>qt?window.cancelAnimationFrame(e):clearTimeout(e),KY=(e,t)=>{if(!qt)return!1;const n={undefined:"overflow",true:"overflow-y",false:"overflow-x"}[String(t)],a=ml(e,n);return["scroll","auto","overlay"].some(r=>a.includes(r))},x6=(e,t)=>{if(!qt)return;let n=e;for(;n;){if([window,document,document.documentElement].includes(n))return window;if(KY(n,t))return n;PA(n)?n=n.host:n=n.parentNode}return n};let Dp;const FA=e=>{var o;if(!qt)return 0;if(Dp!==void 0)return Dp;const t=document.createElement("div");t.className=`${e}-scrollbar__wrap`,t.style.visibility="hidden",t.style.width="100px",t.style.position="absolute",t.style.top="-9999px",document.body.appendChild(t);const n=t.offsetWidth;t.style.overflow="scroll";const a=document.createElement("div");a.style.width="100%",t.appendChild(a);const r=a.offsetWidth;return(o=t.parentNode)==null||o.removeChild(t),Dp=n-r,Dp};function k6(e,t){if(!qt)return;if(!t){e.scrollTop=0;return}const n=[];let a=t.offsetParent;for(;a!==null&&e!==a&&e.contains(a);)n.push(a),a=a.offsetParent;const r=t.offsetTop+n.reduce((s,u)=>s+u.offsetTop,0),o=r+t.offsetHeight,i=e.scrollTop,l=i+e.clientHeight;rl&&(e.scrollTop=o-e.clientHeight)}function UY(e,t,n,a,r){const o=Date.now();let i;const l=()=>{const s=Date.now()-o,u=WY(s>a?a:s,t,n,a);qh(e)?e.scrollTo(window.pageXOffset,u):e.scrollTop=u,s{i&&ti(i)}}const UC=(e,t)=>qh(t)?e.ownerDocument.documentElement:t,GC=e=>qh(e)?window.scrollY:e.scrollTop,D1=(e,t={})=>{xn(e)||Dn("[useLockscreen]","You need to pass a ref param to this function");const n=t.ns||ke("popup"),a=D(()=>n.bm("parent","hidden"));let r=0,o=!1,i="0",l=!1;const s=()=>{l||(l=!0,setTimeout(()=>{typeof document>"u"||o&&document&&(document.body.style.width=i,ir(document.body,a.value))},200))};ye(e,u=>{if(!u){s();return}l=!1,o=!Ni(document.body,a.value),o&&(i=document.body.style.width,Eo(document.body,a.value)),r=FA(n.namespace.value);const c=document.documentElement.clientHeight0&&(c||d==="scroll")&&o&&(document.body.style.width=`calc(100% - ${r}px)`)}),yE(()=>s())},_a=(e,t,{checkForDefaultPrevented:n=!0}={})=>r=>{const o=e==null?void 0:e(r);if(n===!1||!o)return t==null?void 0:t(r)},YC=e=>t=>t.pointerType==="mouse"?e(t):void 0,mn=e=>{if(e.code&&e.code!=="Unidentified")return e.code;const t=HA(e);if(t){if(Object.values(Pe).includes(t))return t;switch(t){case" ":return Pe.space;default:return""}}return""},HA=e=>{let t=e.key&&e.key!=="Unidentified"?e.key:"";if(!t&&e.type==="keyup"&&$A()){const n=e.target;t=n.value.charAt(n.selectionStart-1)}return t},GY=si({type:ae(Boolean),default:null}),YY=si({type:ae(Function)}),jY=e=>{const t=`update:${e}`,n=`onUpdate:${e}`,a=[t],r={[e]:GY,[n]:YY};return{useModelToggle:({indicator:i,toggleReason:l,shouldHideWhenRouteChanges:s,shouldProceed:u,onShow:c,onHide:d})=>{const v=Et(),{emit:f}=v,h=v.props,m=D(()=>Qe(h[n])),g=D(()=>h[e]===null),y=T=>{i.value!==!0&&(i.value=!0,l&&(l.value=T),Qe(c)&&c(T))},b=T=>{i.value!==!1&&(i.value=!1,l&&(l.value=T),Qe(d)&&d(T))},_=T=>{if(h.disabled===!0||Qe(u)&&!u())return;const M=m.value&&qt;M&&f(t,!0),(g.value||!M)&&y(T)},w=T=>{if(h.disabled===!0||!qt)return;const M=m.value&&qt;M&&f(t,!1),(g.value||!M)&&b(T)},S=T=>{vn(T)&&(h.disabled&&T?m.value&&f(t,!1):i.value!==T&&(T?y():b()))},k=()=>{i.value?w():_()};return ye(()=>h[e],S),s&&v.appContext.config.globalProperties.$route!==void 0&&ye(()=>({...v.proxy.$route}),()=>{s.value&&i.value&&w()}),mt(()=>{S(h[e])}),{hide:w,show:_,toggle:k,hasUpdateHandler:m}},useModelToggleProps:r,useModelToggleEmits:a}},WA=e=>{const t=Et();return D(()=>{var n,a;return(a=(n=t==null?void 0:t.proxy)==null?void 0:n.$props)==null?void 0:a[e]})};var zr="top",Bo="bottom",Vo="right",Fr="left",T6="auto",Qh=[zr,Bo,Vo,Fr],Vd="start",uh="end",qY="clippingParents",KA="viewport",Lf="popper",XY="reference",jC=Qh.reduce(function(e,t){return e.concat([t+"-"+Vd,t+"-"+uh])},[]),tl=[].concat(Qh,[T6]).reduce(function(e,t){return e.concat([t,t+"-"+Vd,t+"-"+uh])},[]),ZY="beforeRead",JY="read",QY="afterRead",ej="beforeMain",tj="main",nj="afterMain",aj="beforeWrite",rj="write",oj="afterWrite",ij=[ZY,JY,QY,ej,tj,nj,aj,rj,oj];function qi(e){return e?(e.nodeName||"").toLowerCase():null}function lo(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function uc(e){var t=lo(e).Element;return e instanceof t||e instanceof Element}function Lo(e){var t=lo(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}function M6(e){if(typeof ShadowRoot>"u")return!1;var t=lo(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}function lj(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var a=t.styles[n]||{},r=t.attributes[n]||{},o=t.elements[n];!Lo(o)||!qi(o)||(Object.assign(o.style,a),Object.keys(r).forEach(function(i){var l=r[i];l===!1?o.removeAttribute(i):o.setAttribute(i,l===!0?"":l)}))})}function sj(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(a){var r=t.elements[a],o=t.attributes[a]||{},i=Object.keys(t.styles.hasOwnProperty(a)?t.styles[a]:n[a]),l=i.reduce(function(s,u){return s[u]="",s},{});!Lo(r)||!qi(r)||(Object.assign(r.style,l),Object.keys(o).forEach(function(s){r.removeAttribute(s)}))})}}var UA={name:"applyStyles",enabled:!0,phase:"write",fn:lj,effect:sj,requires:["computeStyles"]};function Bi(e){return e.split("-")[0]}var Yu=Math.max,Gm=Math.min,zd=Math.round;function u4(){var e=navigator.userAgentData;return e!=null&&e.brands&&Array.isArray(e.brands)?e.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function GA(){return!/^((?!chrome|android).)*safari/i.test(u4())}function Fd(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!1);var a=e.getBoundingClientRect(),r=1,o=1;t&&Lo(e)&&(r=e.offsetWidth>0&&zd(a.width)/e.offsetWidth||1,o=e.offsetHeight>0&&zd(a.height)/e.offsetHeight||1);var i=uc(e)?lo(e):window,l=i.visualViewport,s=!GA()&&n,u=(a.left+(s&&l?l.offsetLeft:0))/r,c=(a.top+(s&&l?l.offsetTop:0))/o,d=a.width/r,v=a.height/o;return{width:d,height:v,top:c,right:u+d,bottom:c+v,left:u,x:u,y:c}}function E6(e){var t=Fd(e),n=e.offsetWidth,a=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-a)<=1&&(a=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:a}}function YA(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&M6(n)){var a=t;do{if(a&&e.isSameNode(a))return!0;a=a.parentNode||a.host}while(a)}return!1}function El(e){return lo(e).getComputedStyle(e)}function uj(e){return["table","td","th"].indexOf(qi(e))>=0}function Gs(e){return((uc(e)?e.ownerDocument:e.document)||window.document).documentElement}function O1(e){return qi(e)==="html"?e:e.assignedSlot||e.parentNode||(M6(e)?e.host:null)||Gs(e)}function qC(e){return!Lo(e)||El(e).position==="fixed"?null:e.offsetParent}function cj(e){var t=/firefox/i.test(u4()),n=/Trident/i.test(u4());if(n&&Lo(e)){var a=El(e);if(a.position==="fixed")return null}var r=O1(e);for(M6(r)&&(r=r.host);Lo(r)&&["html","body"].indexOf(qi(r))<0;){var o=El(r);if(o.transform!=="none"||o.perspective!=="none"||o.contain==="paint"||["transform","perspective"].indexOf(o.willChange)!==-1||t&&o.willChange==="filter"||t&&o.filter&&o.filter!=="none")return r;r=r.parentNode}return null}function ep(e){for(var t=lo(e),n=qC(e);n&&uj(n)&&El(n).position==="static";)n=qC(n);return n&&(qi(n)==="html"||qi(n)==="body"&&El(n).position==="static")?t:n||cj(e)||t}function A6(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function Iv(e,t,n){return Yu(e,Gm(t,n))}function dj(e,t,n){var a=Iv(e,t,n);return a>n?n:a}function jA(){return{top:0,right:0,bottom:0,left:0}}function qA(e){return Object.assign({},jA(),e)}function XA(e,t){return t.reduce(function(n,a){return n[a]=e,n},{})}var fj=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,qA(typeof e!="number"?e:XA(e,Qh))};function vj(e){var t,n=e.state,a=e.name,r=e.options,o=n.elements.arrow,i=n.modifiersData.popperOffsets,l=Bi(n.placement),s=A6(l),u=[Fr,Vo].indexOf(l)>=0,c=u?"height":"width";if(!(!o||!i)){var d=fj(r.padding,n),v=E6(o),f=s==="y"?zr:Fr,h=s==="y"?Bo:Vo,m=n.rects.reference[c]+n.rects.reference[s]-i[s]-n.rects.popper[c],g=i[s]-n.rects.reference[s],y=ep(o),b=y?s==="y"?y.clientHeight||0:y.clientWidth||0:0,_=m/2-g/2,w=d[f],S=b-v[c]-d[h],k=b/2-v[c]/2+_,T=Iv(w,k,S),M=s;n.modifiersData[a]=(t={},t[M]=T,t.centerOffset=T-k,t)}}function hj(e){var t=e.state,n=e.options,a=n.element,r=a===void 0?"[data-popper-arrow]":a;r!=null&&(typeof r=="string"&&(r=t.elements.popper.querySelector(r),!r)||YA(t.elements.popper,r)&&(t.elements.arrow=r))}var pj={name:"arrow",enabled:!0,phase:"main",fn:vj,effect:hj,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Hd(e){return e.split("-")[1]}var mj={top:"auto",right:"auto",bottom:"auto",left:"auto"};function gj(e,t){var n=e.x,a=e.y,r=t.devicePixelRatio||1;return{x:zd(n*r)/r||0,y:zd(a*r)/r||0}}function XC(e){var t,n=e.popper,a=e.popperRect,r=e.placement,o=e.variation,i=e.offsets,l=e.position,s=e.gpuAcceleration,u=e.adaptive,c=e.roundOffsets,d=e.isFixed,v=i.x,f=v===void 0?0:v,h=i.y,m=h===void 0?0:h,g=typeof c=="function"?c({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var y=i.hasOwnProperty("x"),b=i.hasOwnProperty("y"),_=Fr,w=zr,S=window;if(u){var k=ep(n),T="clientHeight",M="clientWidth";if(k===lo(n)&&(k=Gs(n),El(k).position!=="static"&&l==="absolute"&&(T="scrollHeight",M="scrollWidth")),k=k,r===zr||(r===Fr||r===Vo)&&o===uh){w=Bo;var E=d&&k===S&&S.visualViewport?S.visualViewport.height:k[T];m-=E-a.height,m*=s?1:-1}if(r===Fr||(r===zr||r===Bo)&&o===uh){_=Vo;var A=d&&k===S&&S.visualViewport?S.visualViewport.width:k[M];f-=A-a.width,f*=s?1:-1}}var I=Object.assign({position:l},u&&mj),P=c===!0?gj({x:f,y:m},lo(n)):{x:f,y:m};if(f=P.x,m=P.y,s){var R;return Object.assign({},I,(R={},R[w]=b?"0":"",R[_]=y?"0":"",R.transform=(S.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",R))}return Object.assign({},I,(t={},t[w]=b?m+"px":"",t[_]=y?f+"px":"",t.transform="",t))}function yj(e){var t=e.state,n=e.options,a=n.gpuAcceleration,r=a===void 0?!0:a,o=n.adaptive,i=o===void 0?!0:o,l=n.roundOffsets,s=l===void 0?!0:l,u={placement:Bi(t.placement),variation:Hd(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:r,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,XC(Object.assign({},u,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:i,roundOffsets:s})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,XC(Object.assign({},u,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:s})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}var ZA={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:yj,data:{}},Op={passive:!0};function bj(e){var t=e.state,n=e.instance,a=e.options,r=a.scroll,o=r===void 0?!0:r,i=a.resize,l=i===void 0?!0:i,s=lo(t.elements.popper),u=[].concat(t.scrollParents.reference,t.scrollParents.popper);return o&&u.forEach(function(c){c.addEventListener("scroll",n.update,Op)}),l&&s.addEventListener("resize",n.update,Op),function(){o&&u.forEach(function(c){c.removeEventListener("scroll",n.update,Op)}),l&&s.removeEventListener("resize",n.update,Op)}}var JA={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:bj,data:{}},_j={left:"right",right:"left",bottom:"top",top:"bottom"};function Z0(e){return e.replace(/left|right|bottom|top/g,function(t){return _j[t]})}var wj={start:"end",end:"start"};function ZC(e){return e.replace(/start|end/g,function(t){return wj[t]})}function I6(e){var t=lo(e),n=t.pageXOffset,a=t.pageYOffset;return{scrollLeft:n,scrollTop:a}}function L6(e){return Fd(Gs(e)).left+I6(e).scrollLeft}function Sj(e,t){var n=lo(e),a=Gs(e),r=n.visualViewport,o=a.clientWidth,i=a.clientHeight,l=0,s=0;if(r){o=r.width,i=r.height;var u=GA();(u||!u&&t==="fixed")&&(l=r.offsetLeft,s=r.offsetTop)}return{width:o,height:i,x:l+L6(e),y:s}}function Cj(e){var t,n=Gs(e),a=I6(e),r=(t=e.ownerDocument)==null?void 0:t.body,o=Yu(n.scrollWidth,n.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),i=Yu(n.scrollHeight,n.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),l=-a.scrollLeft+L6(e),s=-a.scrollTop;return El(r||n).direction==="rtl"&&(l+=Yu(n.clientWidth,r?r.clientWidth:0)-o),{width:o,height:i,x:l,y:s}}function P6(e){var t=El(e),n=t.overflow,a=t.overflowX,r=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+r+a)}function QA(e){return["html","body","#document"].indexOf(qi(e))>=0?e.ownerDocument.body:Lo(e)&&P6(e)?e:QA(O1(e))}function Lv(e,t){var n;t===void 0&&(t=[]);var a=QA(e),r=a===((n=e.ownerDocument)==null?void 0:n.body),o=lo(a),i=r?[o].concat(o.visualViewport||[],P6(a)?a:[]):a,l=t.concat(i);return r?l:l.concat(Lv(O1(i)))}function c4(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function xj(e,t){var n=Fd(e,!1,t==="fixed");return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}function JC(e,t,n){return t===KA?c4(Sj(e,n)):uc(t)?xj(t,n):c4(Cj(Gs(e)))}function kj(e){var t=Lv(O1(e)),n=["absolute","fixed"].indexOf(El(e).position)>=0,a=n&&Lo(e)?ep(e):e;return uc(a)?t.filter(function(r){return uc(r)&&YA(r,a)&&qi(r)!=="body"}):[]}function Tj(e,t,n,a){var r=t==="clippingParents"?kj(e):[].concat(t),o=[].concat(r,[n]),i=o[0],l=o.reduce(function(s,u){var c=JC(e,u,a);return s.top=Yu(c.top,s.top),s.right=Gm(c.right,s.right),s.bottom=Gm(c.bottom,s.bottom),s.left=Yu(c.left,s.left),s},JC(e,i,a));return l.width=l.right-l.left,l.height=l.bottom-l.top,l.x=l.left,l.y=l.top,l}function eI(e){var t=e.reference,n=e.element,a=e.placement,r=a?Bi(a):null,o=a?Hd(a):null,i=t.x+t.width/2-n.width/2,l=t.y+t.height/2-n.height/2,s;switch(r){case zr:s={x:i,y:t.y-n.height};break;case Bo:s={x:i,y:t.y+t.height};break;case Vo:s={x:t.x+t.width,y:l};break;case Fr:s={x:t.x-n.width,y:l};break;default:s={x:t.x,y:t.y}}var u=r?A6(r):null;if(u!=null){var c=u==="y"?"height":"width";switch(o){case Vd:s[u]=s[u]-(t[c]/2-n[c]/2);break;case uh:s[u]=s[u]+(t[c]/2-n[c]/2);break}}return s}function ch(e,t){t===void 0&&(t={});var n=t,a=n.placement,r=a===void 0?e.placement:a,o=n.strategy,i=o===void 0?e.strategy:o,l=n.boundary,s=l===void 0?qY:l,u=n.rootBoundary,c=u===void 0?KA:u,d=n.elementContext,v=d===void 0?Lf:d,f=n.altBoundary,h=f===void 0?!1:f,m=n.padding,g=m===void 0?0:m,y=qA(typeof g!="number"?g:XA(g,Qh)),b=v===Lf?XY:Lf,_=e.rects.popper,w=e.elements[h?b:v],S=Tj(uc(w)?w:w.contextElement||Gs(e.elements.popper),s,c,i),k=Fd(e.elements.reference),T=eI({reference:k,element:_,placement:r}),M=c4(Object.assign({},_,T)),E=v===Lf?M:k,A={top:S.top-E.top+y.top,bottom:E.bottom-S.bottom+y.bottom,left:S.left-E.left+y.left,right:E.right-S.right+y.right},I=e.modifiersData.offset;if(v===Lf&&I){var P=I[r];Object.keys(A).forEach(function(R){var G=[Vo,Bo].indexOf(R)>=0?1:-1,K=[zr,Bo].indexOf(R)>=0?"y":"x";A[R]+=P[K]*G})}return A}function Mj(e,t){t===void 0&&(t={});var n=t,a=n.placement,r=n.boundary,o=n.rootBoundary,i=n.padding,l=n.flipVariations,s=n.allowedAutoPlacements,u=s===void 0?tl:s,c=Hd(a),d=c?l?jC:jC.filter(function(h){return Hd(h)===c}):Qh,v=d.filter(function(h){return u.indexOf(h)>=0});v.length===0&&(v=d);var f=v.reduce(function(h,m){return h[m]=ch(e,{placement:m,boundary:r,rootBoundary:o,padding:i})[Bi(m)],h},{});return Object.keys(f).sort(function(h,m){return f[h]-f[m]})}function Ej(e){if(Bi(e)===T6)return[];var t=Z0(e);return[ZC(e),t,ZC(t)]}function Aj(e){var t=e.state,n=e.options,a=e.name;if(!t.modifiersData[a]._skip){for(var r=n.mainAxis,o=r===void 0?!0:r,i=n.altAxis,l=i===void 0?!0:i,s=n.fallbackPlacements,u=n.padding,c=n.boundary,d=n.rootBoundary,v=n.altBoundary,f=n.flipVariations,h=f===void 0?!0:f,m=n.allowedAutoPlacements,g=t.options.placement,y=Bi(g),b=y===g,_=s||(b||!h?[Z0(g)]:Ej(g)),w=[g].concat(_).reduce(function(ee,re){return ee.concat(Bi(re)===T6?Mj(t,{placement:re,boundary:c,rootBoundary:d,padding:u,flipVariations:h,allowedAutoPlacements:m}):re)},[]),S=t.rects.reference,k=t.rects.popper,T=new Map,M=!0,E=w[0],A=0;A=0,K=G?"width":"height",H=ch(t,{placement:I,boundary:c,rootBoundary:d,altBoundary:v,padding:u}),z=G?R?Vo:Fr:R?Bo:zr;S[K]>k[K]&&(z=Z0(z));var B=Z0(z),W=[];if(o&&W.push(H[P]<=0),l&&W.push(H[z]<=0,H[B]<=0),W.every(function(ee){return ee})){E=I,M=!1;break}T.set(I,W)}if(M)for(var N=h?3:1,V=function(ee){var re=w.find(function(ie){var le=T.get(ie);if(le)return le.slice(0,ee).every(function(ve){return ve})});if(re)return E=re,"break"},Y=N;Y>0;Y--){var X=V(Y);if(X==="break")break}t.placement!==E&&(t.modifiersData[a]._skip=!0,t.placement=E,t.reset=!0)}}var Ij={name:"flip",enabled:!0,phase:"main",fn:Aj,requiresIfExists:["offset"],data:{_skip:!1}};function QC(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function e5(e){return[zr,Vo,Bo,Fr].some(function(t){return e[t]>=0})}function Lj(e){var t=e.state,n=e.name,a=t.rects.reference,r=t.rects.popper,o=t.modifiersData.preventOverflow,i=ch(t,{elementContext:"reference"}),l=ch(t,{altBoundary:!0}),s=QC(i,a),u=QC(l,r,o),c=e5(s),d=e5(u);t.modifiersData[n]={referenceClippingOffsets:s,popperEscapeOffsets:u,isReferenceHidden:c,hasPopperEscaped:d},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":c,"data-popper-escaped":d})}var Pj={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Lj};function Dj(e,t,n){var a=Bi(e),r=[Fr,zr].indexOf(a)>=0?-1:1,o=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,i=o[0],l=o[1];return i=i||0,l=(l||0)*r,[Fr,Vo].indexOf(a)>=0?{x:l,y:i}:{x:i,y:l}}function Oj(e){var t=e.state,n=e.options,a=e.name,r=n.offset,o=r===void 0?[0,0]:r,i=tl.reduce(function(c,d){return c[d]=Dj(d,t.rects,o),c},{}),l=i[t.placement],s=l.x,u=l.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=s,t.modifiersData.popperOffsets.y+=u),t.modifiersData[a]=i}var $j={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Oj};function Rj(e){var t=e.state,n=e.name;t.modifiersData[n]=eI({reference:t.rects.reference,element:t.rects.popper,placement:t.placement})}var tI={name:"popperOffsets",enabled:!0,phase:"read",fn:Rj,data:{}};function Nj(e){return e==="x"?"y":"x"}function Bj(e){var t=e.state,n=e.options,a=e.name,r=n.mainAxis,o=r===void 0?!0:r,i=n.altAxis,l=i===void 0?!1:i,s=n.boundary,u=n.rootBoundary,c=n.altBoundary,d=n.padding,v=n.tether,f=v===void 0?!0:v,h=n.tetherOffset,m=h===void 0?0:h,g=ch(t,{boundary:s,rootBoundary:u,padding:d,altBoundary:c}),y=Bi(t.placement),b=Hd(t.placement),_=!b,w=A6(y),S=Nj(w),k=t.modifiersData.popperOffsets,T=t.rects.reference,M=t.rects.popper,E=typeof m=="function"?m(Object.assign({},t.rects,{placement:t.placement})):m,A=typeof E=="number"?{mainAxis:E,altAxis:E}:Object.assign({mainAxis:0,altAxis:0},E),I=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,P={x:0,y:0};if(k){if(o){var R,G=w==="y"?zr:Fr,K=w==="y"?Bo:Vo,H=w==="y"?"height":"width",z=k[w],B=z+g[G],W=z-g[K],N=f?-M[H]/2:0,V=b===Vd?T[H]:M[H],Y=b===Vd?-M[H]:-T[H],X=t.elements.arrow,ee=f&&X?E6(X):{width:0,height:0},re=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:jA(),ie=re[G],le=re[K],ve=Iv(0,T[H],ee[H]),he=_?T[H]/2-N-ve-ie-A.mainAxis:V-ve-ie-A.mainAxis,J=_?-T[H]/2+N+ve+le+A.mainAxis:Y+ve+le+A.mainAxis,Z=t.elements.arrow&&ep(t.elements.arrow),j=Z?w==="y"?Z.clientTop||0:Z.clientLeft||0:0,oe=(R=I==null?void 0:I[w])!=null?R:0,se=z+he-oe-j,fe=z+J-oe,ge=Iv(f?Gm(B,se):B,z,f?Yu(W,fe):W);k[w]=ge,P[w]=ge-z}if(l){var Se,De=w==="x"?zr:Fr,Te=w==="x"?Bo:Vo,Re=k[S],Ce=S==="y"?"height":"width",Le=Re+g[De],me=Re-g[Te],Be=[zr,Fr].indexOf(y)!==-1,ze=(Se=I==null?void 0:I[S])!=null?Se:0,xe=Be?Le:Re-T[Ce]-M[Ce]-ze+A.altAxis,rt=Be?Re+T[Ce]+M[Ce]-ze-A.altAxis:me,nt=f&&Be?dj(xe,Re,rt):Iv(f?xe:Le,Re,f?rt:me);k[S]=nt,P[S]=nt-Re}t.modifiersData[a]=P}}var Vj={name:"preventOverflow",enabled:!0,phase:"main",fn:Bj,requiresIfExists:["offset"]};function zj(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}function Fj(e){return e===lo(e)||!Lo(e)?I6(e):zj(e)}function Hj(e){var t=e.getBoundingClientRect(),n=zd(t.width)/e.offsetWidth||1,a=zd(t.height)/e.offsetHeight||1;return n!==1||a!==1}function Wj(e,t,n){n===void 0&&(n=!1);var a=Lo(t),r=Lo(t)&&Hj(t),o=Gs(t),i=Fd(e,r,n),l={scrollLeft:0,scrollTop:0},s={x:0,y:0};return(a||!a&&!n)&&((qi(t)!=="body"||P6(o))&&(l=Fj(t)),Lo(t)?(s=Fd(t,!0),s.x+=t.clientLeft,s.y+=t.clientTop):o&&(s.x=L6(o))),{x:i.left+l.scrollLeft-s.x,y:i.top+l.scrollTop-s.y,width:i.width,height:i.height}}function Kj(e){var t=new Map,n=new Set,a=[];e.forEach(function(o){t.set(o.name,o)});function r(o){n.add(o.name);var i=[].concat(o.requires||[],o.requiresIfExists||[]);i.forEach(function(l){if(!n.has(l)){var s=t.get(l);s&&r(s)}}),a.push(o)}return e.forEach(function(o){n.has(o.name)||r(o)}),a}function Uj(e){var t=Kj(e);return ij.reduce(function(n,a){return n.concat(t.filter(function(r){return r.phase===a}))},[])}function Gj(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}function Yj(e){var t=e.reduce(function(n,a){var r=n[a.name];return n[a.name]=r?Object.assign({},r,a,{options:Object.assign({},r.options,a.options),data:Object.assign({},r.data,a.data)}):a,n},{});return Object.keys(t).map(function(n){return t[n]})}var t5={placement:"bottom",modifiers:[],strategy:"absolute"};function n5(){for(var e=arguments.length,t=new Array(e),n=0;n{const a={name:"updateState",enabled:!0,phase:"write",fn:({state:s})=>{const u=Jj(s);Object.assign(i.value,u)},requires:["computeStyles"]},r=D(()=>{const{onFirstUpdate:s,placement:u,strategy:c,modifiers:d}=p(n);return{onFirstUpdate:s,placement:u||"bottom",strategy:c||"absolute",modifiers:[...d||[],a,{name:"applyStyles",enabled:!1}]}}),o=Ht(),i=F({styles:{popper:{position:p(r).strategy,left:"0",top:"0"},arrow:{position:"absolute"}},attributes:{}}),l=()=>{o.value&&(o.value.destroy(),o.value=void 0)};return ye(r,s=>{const u=p(o);u&&u.setOptions(s)},{deep:!0}),ye([e,t],([s,u])=>{l(),!(!s||!u)&&(o.value=Xj(s,u,p(r)))}),rn(()=>{l()}),{state:D(()=>{var s;return{...((s=p(o))==null?void 0:s.state)||{}}}),styles:D(()=>p(i).styles),attributes:D(()=>p(i).attributes),update:()=>{var s;return(s=p(o))==null?void 0:s.update()},forceUpdate:()=>{var s;return(s=p(o))==null?void 0:s.forceUpdate()},instanceRef:D(()=>p(o))}};function Jj(e){const t=Object.keys(e.elements);return{styles:Rd(t.map(n=>[n,e.styles[n]||{}])),attributes:Rd(t.map(n=>[n,e.attributes[n]]))}}const O6=e=>{if(!e)return{onClick:kt,onMousedown:kt,onMouseup:kt};let t=!1,n=!1;return{onClick:i=>{t&&n&&e(i),t=n=!1},onMousedown:i=>{t=i.target===i.currentTarget},onMouseup:i=>{n=i.target===i.currentTarget}}},Qj=(e,t=0)=>{if(t===0)return e;const n=F(_t(t)&&!!t.initVal);let a=null;const r=i=>{if(zt(i)){n.value=e.value;return}a&&clearTimeout(a),a=setTimeout(()=>{n.value=e.value},i)},o=i=>{i==="leading"?Je(t)?r(t):r(t.leading):_t(t)?r(t.trailing):n.value=!1};return mt(()=>o("leading")),ye(()=>e.value,i=>{o(i?"leading":"trailing")}),n};function a5(){let e;const t=(a,r)=>{n(),e=globalThis.setTimeout(a,r)},n=()=>{e!==void 0&&(globalThis.clearTimeout(e),e=void 0)};return Ds(()=>n()),{registerTimeout:t,cancelTimeout:n}}const d4={prefix:Math.floor(Math.random()*1e4),current:0},eq=Symbol("elIdInjection"),$6=()=>Et()?Ze(eq,d4):d4,Fa=e=>{const t=$6();!qt&&t===d4&&un("IdInjection",`Looks like you are using server rendering, you must provide a id provider to ensure the hydration process to be succeed +usage: app.provide(ID_INJECTION_KEY, { + prefix: number, + current: number, +})`);const n=C6();return EA(()=>p(e)||`${n.value}-id-${t.prefix}-${t.current++}`)};let nd=[];const r5=e=>{mn(e)===Pe.esc&&nd.forEach(t=>t(e))},tq=e=>{mt(()=>{nd.length===0&&document.addEventListener("keydown",r5),qt&&nd.push(e)}),rn(()=>{nd=nd.filter(t=>t!==e),nd.length===0&&qt&&document.removeEventListener("keydown",r5)})},nI=()=>{const e=C6(),t=$6(),n=D(()=>`${e.value}-popper-container-${t.prefix}`);return{id:n,selector:D(()=>`#${n.value}`)}},nq=e=>{const t=document.createElement("div");return t.id=e,document.body.appendChild(t),t},aq=()=>{const{id:e,selector:t}=nI();return g1(()=>{qt&&(document.body.querySelector(t.value)||nq(e.value))}),{id:e,selector:t}},rq=$e({showAfter:{type:Number,default:0},hideAfter:{type:Number,default:200},autoClose:{type:Number,default:0}}),oq={showAfter:0,hideAfter:200,autoClose:0},iq=({showAfter:e,hideAfter:t,autoClose:n,open:a,close:r})=>{const{registerTimeout:o}=a5(),{registerTimeout:i,cancelTimeout:l}=a5();return{onOpen:(c,d=p(e))=>{o(()=>{a(c);const v=p(n);Je(v)&&v>0&&i(()=>{r(c)},v)},d)},onClose:(c,d=p(t))=>{l(),o(()=>{r(c)},d)}}},aI=Symbol("elForwardRef"),lq=e=>{Ft(aI,{setForwardRef:n=>{e.value=n}})},sq=e=>({mounted(t){e(t)},updated(t){e(t)},unmounted(){e(null)}}),o5={current:0},i5=F(0),rI=2e3,l5=Symbol("elZIndexContextKey"),oI=Symbol("zIndexContextKey"),tp=e=>{const t=Et()?Ze(l5,o5):o5,n=e||(Et()?Ze(oI,void 0):void 0),a=D(()=>{const i=p(n);return Je(i)?i:rI}),r=D(()=>a.value+i5.value),o=()=>(t.current++,i5.value=t.current,r.value);return!qt&&!Ze(l5)&&un("ZIndexInjection",`Looks like you are using server rendering, you must provide a z-index provider to ensure the hydration process to be succeed +usage: app.provide(ZINDEX_INJECTION_KEY, { current: 0 })`),{initialZIndex:a,currentZIndex:r,nextZIndex:o}},Wd=Math.min,ju=Math.max,Ym=Math.round,$p=Math.floor,Sl=e=>({x:e,y:e}),uq={left:"right",right:"left",bottom:"top",top:"bottom"};function iI(e,t,n){return ju(e,Wd(t,n))}function np(e,t){return typeof e=="function"?e(t):e}function Kd(e){return e.split("-")[0]}function ap(e){return e.split("-")[1]}function lI(e){return e==="x"?"y":"x"}function R6(e){return e==="y"?"height":"width"}function ps(e){const t=e[0];return t==="t"||t==="b"?"y":"x"}function N6(e){return lI(ps(e))}function cq(e,t,n){n===void 0&&(n=!1);const a=ap(e),r=N6(e),o=R6(r);let i=r==="x"?a===(n?"end":"start")?"right":"left":a==="start"?"bottom":"top";return t.reference[o]>t.floating[o]&&(i=jm(i)),[i,jm(i)]}function dq(e){const t=jm(e);return[f4(e),t,f4(t)]}function f4(e){return e.includes("start")?e.replace("start","end"):e.replace("end","start")}const s5=["left","right"],u5=["right","left"],fq=["top","bottom"],vq=["bottom","top"];function hq(e,t,n){switch(e){case"top":case"bottom":return n?t?u5:s5:t?s5:u5;case"left":case"right":return t?fq:vq;default:return[]}}function pq(e,t,n,a){const r=ap(e);let o=hq(Kd(e),n==="start",a);return r&&(o=o.map(i=>i+"-"+r),t&&(o=o.concat(o.map(f4)))),o}function jm(e){const t=Kd(e);return uq[t]+e.slice(t.length)}function mq(e){var t,n,a,r;return{top:(t=e.top)!=null?t:0,right:(n=e.right)!=null?n:0,bottom:(a=e.bottom)!=null?a:0,left:(r=e.left)!=null?r:0}}function sI(e){return typeof e!="number"?mq(e):{top:e,right:e,bottom:e,left:e}}function qm(e){const{x:t,y:n,width:a,height:r}=e;return{width:a,height:r,top:n,left:t,right:t+a,bottom:n+r,x:t,y:n}}function c5(e,t,n){let{reference:a,floating:r}=e;const o=ps(t),i=N6(t),l=R6(i),s=Kd(t),u=o==="y",c=a.x+a.width/2-r.width/2,d=a.y+a.height/2-r.height/2,v=a[l]/2-r[l]/2;let f;switch(s){case"top":f={x:c,y:a.y-r.height};break;case"bottom":f={x:c,y:a.y+a.height};break;case"right":f={x:a.x+a.width,y:d};break;case"left":f={x:a.x-r.width,y:d};break;default:f={x:a.x,y:a.y}}const h=ap(t);return h&&(f[i]+=v*(h==="end"?1:-1)*(n&&u?-1:1)),f}async function uI(e,t){var n;t===void 0&&(t={});const{x:a,y:r,platform:o,rects:i,elements:l,strategy:s}=e,{boundary:u="clippingAncestors",rootBoundary:c="viewport",elementContext:d="floating",altBoundary:v=!1,padding:f=0}=np(t,e),h=sI(f),g=l[v?d==="floating"?"reference":"floating":d],y=qm(await o.getClippingRect({element:(n=await(o.isElement==null?void 0:o.isElement(g)))==null||n?g:g.contextElement||await(o.getDocumentElement==null?void 0:o.getDocumentElement(l.floating)),boundary:u,rootBoundary:c,strategy:s})),b=d==="floating"?{x:a,y:r,width:i.floating.width,height:i.floating.height}:i.reference,_=await(o.getOffsetParent==null?void 0:o.getOffsetParent(l.floating)),w=await(o.isElement==null?void 0:o.isElement(_))&&await(o.getScale==null?void 0:o.getScale(_))||{x:1,y:1},S=qm(o.convertOffsetParentRelativeRectToViewportRelativeRect?await o.convertOffsetParentRelativeRectToViewportRelativeRect({elements:l,rect:b,offsetParent:_,strategy:s}):b);return{top:(y.top-S.top+h.top)/w.y,bottom:(S.bottom-y.bottom+h.bottom)/w.y,left:(y.left-S.left+h.left)/w.x,right:(S.right-y.right+h.right)/w.x}}const gq=50,yq=async(e,t,n)=>{const{placement:a="bottom",strategy:r="absolute",middleware:o=[],platform:i}=n,l=i.detectOverflow?i:{...i,detectOverflow:uI},s=await(i.isRTL==null?void 0:i.isRTL(t));let u=await i.getElementRects({reference:e,floating:t,strategy:r}),{x:c,y:d}=c5(u,a,s),v=a,f=0;const h={};for(let m=0;m({name:"arrow",options:e,async fn(t){const{x:n,y:a,placement:r,rects:o,platform:i,elements:l,middlewareData:s}=t,{element:u,padding:c=0}=np(e,t)||{};if(u==null)return{};const d=sI(c),v={x:n,y:a},f=N6(r),h=R6(f),m=await i.getDimensions(u),g=f==="y",y=g?"top":"left",b=g?"bottom":"right",_=g?"clientHeight":"clientWidth",w=o.reference[h]+o.reference[f]-v[f]-o.floating[h],S=v[f]-o.reference[f],k=await(i.getOffsetParent==null?void 0:i.getOffsetParent(u));let T=k?k[_]:0;(!T||!await(i.isElement==null?void 0:i.isElement(k)))&&(T=l.floating[_]||o.floating[h]);const M=w/2-S/2,E=T/2-m[h]/2-1,A=Wd(d[y],E),I=Wd(d[b],E),P=T-m[h]-I,R=T/2-m[h]/2+M,G=iI(A,R,P),K=!s.arrow&&ap(r)!=null&&R!==G&&o.reference[h]/2-(RG<=0)){var I,P;const G=(((I=o.flip)==null?void 0:I.index)||0)+1,K=T[G];if(K&&(!(d==="alignment"?b!==ps(K):!1)||A.every(B=>ps(B.placement)===b?B.overflows[0]>0:!0)))return{data:{index:G,overflows:A},reset:{placement:K}};let H=(P=A.filter(z=>z.overflows[0]<=0).sort((z,B)=>z.overflows[1]-B.overflows[1])[0])==null?void 0:P.placement;if(!H)switch(f){case"bestFit":{var R;const z=(R=A.filter(B=>{if(k){const W=ps(B.placement);return W===b||W==="y"}return!0}).map(B=>[B.placement,B.overflows.filter(W=>W>0).reduce((W,N)=>W+N,0)]).sort((B,W)=>B[1]-W[1])[0])==null?void 0:R[0];z&&(H=z);break}case"initialPlacement":H=l;break}if(r!==H)return{reset:{placement:H}}}return{}}}},wq=new Set(["left","top"]);async function Sq(e,t){const{placement:n,platform:a,elements:r}=e,o=await(a.isRTL==null?void 0:a.isRTL(r.floating)),i=Kd(n),l=ap(n),s=ps(n)==="y",u=wq.has(i)?-1:1,c=o&&s?-1:1,d=np(t,e);let{mainAxis:v,crossAxis:f,alignmentAxis:h}=typeof d=="number"?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return l&&typeof h=="number"&&(f=l==="end"?h*-1:h),s?{x:f*c,y:v*u}:{x:v*u,y:f*c}}const Cq=function(e){return e===void 0&&(e=0),{name:"offset",options:e,async fn(t){var n,a;const{x:r,y:o,placement:i,middlewareData:l}=t,s=await Sq(t,e);return i===((n=l.offset)==null?void 0:n.placement)&&(a=l.arrow)!=null&&a.alignmentOffset?{}:{x:r+s.x,y:o+s.y,data:{...s,placement:i}}}}},xq=function(e){return e===void 0&&(e={}),{name:"shift",options:e,async fn(t){const{x:n,y:a,placement:r,platform:o}=t,{mainAxis:i=!0,crossAxis:l=!1,limiter:s={fn:b=>{let{x:_,y:w}=b;return{x:_,y:w}}},...u}=np(e,t),c={x:n,y:a},d=await o.detectOverflow(t,u),v=ps(r),f=lI(v);let h=c[f],m=c[v];const g=(b,_)=>iI(_+d[b==="y"?"top":"left"],_,_-d[b==="y"?"bottom":"right"]);i&&(h=g(f,h)),l&&(m=g(v,m));const y=s.fn({...t,[f]:h,[v]:m});return{...y,data:{x:y.x-n,y:y.y-a,enabled:{[f]:i,[v]:l}}}}}};function $1(){return typeof window<"u"}function vf(e){return cI(e)?(e.nodeName||"").toLowerCase():"#document"}function Hr(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function zl(e){var t;return(t=(cI(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function cI(e){return $1()?e instanceof Node||e instanceof Hr(e).Node:!1}function Xi(e){return $1()?e instanceof Element||e instanceof Hr(e).Element:!1}function Ys(e){return $1()?e instanceof HTMLElement||e instanceof Hr(e).HTMLElement:!1}function d5(e){return!$1()||typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot||e instanceof Hr(e).ShadowRoot}function R1(e){const{overflow:t,overflowX:n,overflowY:a,display:r}=Zi(e);return/auto|scroll|overlay|hidden|clip/.test(t+a+n)&&r!=="inline"&&r!=="contents"}function kq(e){return/^(table|td|th)$/.test(vf(e))}function N1(e){try{if(e.matches(":popover-open"))return!0}catch{}try{return e.matches(":modal")}catch{return!1}}const Tq=/transform|translate|scale|rotate|perspective|filter/,Mq=/paint|layout|strict|content/,tu=e=>!!e&&e!=="none";let K2;function B6(e){const t=Xi(e)?Zi(e):e;return tu(t.transform)||tu(t.translate)||tu(t.scale)||tu(t.rotate)||tu(t.perspective)||!V6()&&(tu(t.backdropFilter)||tu(t.filter))||Tq.test(t.willChange||"")||Mq.test(t.contain||"")}function Eq(e){let t=cc(e);for(;Ys(t)&&!dh(t);){if(B6(t))return t;if(N1(t))return null;t=cc(t)}return null}function V6(){return K2==null&&(K2=typeof CSS<"u"&&CSS.supports&&CSS.supports("-webkit-backdrop-filter","none")),K2}function dh(e){return/^(html|body|#document)$/.test(vf(e))}function Zi(e){return Hr(e).getComputedStyle(e)}function B1(e){return Xi(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function cc(e){if(vf(e)==="html")return e;const t=e.assignedSlot||e.parentNode||d5(e)&&e.host||zl(e);return d5(t)?t.host:t}function dI(e){const t=cc(e);return dh(t)?(e.ownerDocument||e).body:Ys(t)&&R1(t)?t:dI(t)}function fh(e,t,n){var a;t===void 0&&(t=[]),n===void 0&&(n=!0);const r=dI(e),o=r===((a=e.ownerDocument)==null?void 0:a.body),i=Hr(r);if(o){const l=v4(i);return t.concat(i,i.visualViewport||[],R1(r)?r:[],l&&n?fh(l):[])}else return t.concat(r,fh(r,[],n))}function v4(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function fI(e){const t=Zi(e);let n=parseFloat(t.width)||0,a=parseFloat(t.height)||0;const r=Ys(e),o=r?e.offsetWidth:n,i=r?e.offsetHeight:a,l=Ym(n)!==o||Ym(a)!==i;return l&&(n=o,a=i),{width:n,height:a,$:l}}function z6(e){return Xi(e)?e:e.contextElement}function wd(e){const t=z6(e);if(!Ys(t))return Sl(1);const n=t.getBoundingClientRect(),{width:a,height:r,$:o}=fI(t);let i=(o?Ym(n.width):n.width)/a,l=(o?Ym(n.height):n.height)/r;return(!i||!Number.isFinite(i))&&(i=1),(!l||!Number.isFinite(l))&&(l=1),{x:i,y:l}}const Aq=Sl(0);function vI(e){const t=Hr(e);return!V6()||!t.visualViewport?Aq:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function Iq(e,t,n){return t===void 0&&(t=!1),!!n&&t&&n===Hr(e)}function dc(e,t,n,a){t===void 0&&(t=!1),n===void 0&&(n=!1);const r=e.getBoundingClientRect(),o=z6(e);let i=Sl(1);t&&(a?Xi(a)&&(i=wd(a)):i=wd(e));const l=Iq(o,n,a)?vI(o):Sl(0);let s=(r.left+l.x)/i.x,u=(r.top+l.y)/i.y,c=r.width/i.x,d=r.height/i.y;if(o&&a){const v=Hr(o),f=Xi(a)?Hr(a):a;let h=v,m=v4(h);for(;m&&f!==h;){const g=wd(m),y=m.getBoundingClientRect(),b=Zi(m),_=y.left+(m.clientLeft+parseFloat(b.paddingLeft))*g.x,w=y.top+(m.clientTop+parseFloat(b.paddingTop))*g.y;s*=g.x,u*=g.y,c*=g.x,d*=g.y,s+=_,u+=w,h=Hr(m),m=v4(h)}}return qm({width:c,height:d,x:s,y:u})}function V1(e,t){const n=B1(e).scrollLeft;return t?t.left+n:dc(zl(e)).left+n}function hI(e,t){const n=e.getBoundingClientRect(),a=n.left+t.scrollLeft-V1(e,n),r=n.top+t.scrollTop;return{x:a,y:r}}function Lq(e){let{elements:t,rect:n,offsetParent:a,strategy:r}=e;const o=r==="fixed",i=zl(a),l=t?N1(t.floating):!1;if(a===i||l&&o)return n;let s={scrollLeft:0,scrollTop:0},u=Sl(1);const c=Sl(0),d=Ys(a);if((d||!o)&&((vf(a)!=="body"||R1(i))&&(s=B1(a)),d)){const f=dc(a);u=wd(a),c.x=f.x+a.clientLeft,c.y=f.y+a.clientTop}const v=i&&!d&&!o?hI(i,s):Sl(0);return{width:n.width*u.x,height:n.height*u.y,x:n.x*u.x-s.scrollLeft*u.x+c.x+v.x,y:n.y*u.y-s.scrollTop*u.y+c.y+v.y}}function Pq(e){return e.getClientRects?Array.from(e.getClientRects()):[]}function Dq(e){const t=B1(e),n=e.ownerDocument.body,a=ju(e.scrollWidth,e.clientWidth,n.scrollWidth,n.clientWidth),r=ju(e.scrollHeight,e.clientHeight,n.scrollHeight,n.clientHeight);let o=-t.scrollLeft+V1(e);const i=-t.scrollTop;return Zi(n).direction==="rtl"&&(o+=ju(e.clientWidth,n.clientWidth)-a),{width:a,height:r,x:o,y:i}}const Oq=25;function $q(e,t,n){n===void 0&&(n="viewport");const a=n==="layoutViewport",r=Hr(e),o=zl(e),i=r.visualViewport;let l=o.clientWidth,s=o.clientHeight,u=0,c=0;if(i){const v=!V6()||t==="fixed";a?v||(u=-i.offsetLeft,c=-i.offsetTop):(l=i.width,s=i.height,v&&(u=i.offsetLeft,c=i.offsetTop))}if(V1(o)<=0){const v=o.ownerDocument,f=v.body,h=getComputedStyle(f),m=v.compatMode==="CSS1Compat"&&parseFloat(h.marginLeft)+parseFloat(h.marginRight)||0,g=Math.abs(o.clientWidth-f.clientWidth-m),y=getComputedStyle(o).scrollbarGutter==="stable both-edges"?g/2:g;y<=Oq&&(l-=y)}return{width:l,height:s,x:u,y:c}}function Rq(e,t){const n=dc(e,!0,t==="fixed"),a=n.top+e.clientTop,r=n.left+e.clientLeft,o=wd(e),i=e.clientWidth*o.x,l=e.clientHeight*o.y,s=r*o.x,u=a*o.y;return{width:i,height:l,x:s,y:u}}function f5(e,t,n){let a;if(t==="viewport"||t==="layoutViewport")a=$q(e,n,t);else if(t==="document")a=Dq(zl(e));else if(Xi(t))a=Rq(t,n);else{const r=vI(e);a={x:t.x-r.x,y:t.y-r.y,width:t.width,height:t.height}}return qm(a)}function Nq(e,t){const n=t.get(e);if(n)return n;let a=fh(e,[],!1).filter(l=>Xi(l)&&vf(l)!=="body"),r=null;const o=Zi(e).position==="fixed";let i=o?cc(e):e;for(;Xi(i)&&!dh(i);){const l=Zi(i),s=B6(i),u=r?r.position:o?"fixed":"";!s&&(u==="fixed"||u==="absolute"&&l.position==="static")?a=a.filter(d=>d!==i):r=l,i=cc(i)}return t.set(e,a),a}function Bq(e){let{element:t,boundary:n,rootBoundary:a,strategy:r}=e;const i=[...n==="clippingAncestors"?N1(t)?[]:Nq(t,this._c):[].concat(n),a],l=f5(t,i[0],r);let s=l.top,u=l.right,c=l.bottom,d=l.left;for(let v=1;v{l(!1,1e-7)},1e3)}T=!1}try{a=new IntersectionObserver(M,{...k,root:o.ownerDocument})}catch{a=new IntersectionObserver(M,k)}a.observe(e)}const s=Hr(e),u=()=>l(n);return s.addEventListener("resize",u),l(!0),()=>{s.removeEventListener("resize",u),i()}}function Uq(e,t,n,a){a===void 0&&(a={});const{ancestorScroll:r=!0,ancestorResize:o=!0,elementResize:i=typeof ResizeObserver=="function",layoutShift:l=typeof IntersectionObserver=="function",animationFrame:s=!1}=a,u=z6(e),c=r||o?[...u?fh(u):[],...t?fh(t):[]]:[];c.forEach(y=>{r&&y.addEventListener("scroll",n),o&&y.addEventListener("resize",n)});const d=u&&l?Kq(u,n,o):null;let v=-1,f=null;i&&(f=new ResizeObserver(y=>{let[b]=y;b&&b.target===u&&f&&t&&(f.unobserve(t),cancelAnimationFrame(v),v=requestAnimationFrame(()=>{var _;(_=f)==null||_.observe(t)})),n()}),u&&!s&&f.observe(u),t&&f.observe(t));let h,m=s?dc(e):null;s&&g();function g(){const y=dc(e);m&&!mI(m,y)&&n(),m=y,h=requestAnimationFrame(g)}return n(),()=>{var y;c.forEach(b=>{r&&b.removeEventListener("scroll",n),o&&b.removeEventListener("resize",n)}),d==null||d(),(y=f)==null||y.disconnect(),f=null,s&&cancelAnimationFrame(h)}}const Gq=uI,Yq=Cq,jq=xq,qq=_q,Xq=bq,Zq=(e,t,n)=>{const a=new Map,r=n??{},o={...Wq,...r.platform,_c:a};return yq(e,t,{...r,platform:o})};function Jq(e){let t;function n(){if(e.value==null)return;const{selectionStart:r,selectionEnd:o,value:i}=e.value;r==null||o==null||(t={selectionStart:r,selectionEnd:o,value:i,beforeTxt:i.slice(0,Math.max(0,r)),afterTxt:i.slice(Math.max(0,o))})}function a(){if(e.value==null||t==null)return;const{value:r}=e.value,{beforeTxt:o,afterTxt:i,selectionStart:l}=t;if(o==null||i==null||l==null)return;let s=r.length;if(r.endsWith(i))s=r.length-i.length;else if(r.startsWith(o))s=o.length;else{const u=o[l-1],c=r.indexOf(u,l-1);c!==-1&&(s=c+1)}e.value.setSelectionRange(s,s)}return[n,a]}const Qq="utils/vue/vnode";function h4(e){return bn(e)&&e.type===Ve}function gI(e){return bn(e)&&e.type===oa}function eX(e){return bn(e)&&!h4(e)&&!gI(e)}const tX=e=>{if(!bn(e))return un(Qq,"[getNormalizedProps] must be a VNode"),{};const t=e.props||{},n=(bn(e.type)?e.type.props:void 0)||{},a={};return Object.keys(n).forEach(r=>{en(n[r],"default")&&(a[r]=n[r].default)}),Object.keys(t).forEach(r=>{a[Va(r)]=t[r]}),a},eo=e=>{const t=Ae(e)?e:[e],n=[];return t.forEach(a=>{var r;Ae(a)?n.push(...eo(a)):bn(a)&&((r=a.component)!=null&&r.subTree)?n.push(a,...eo(a.component.subTree)):bn(a)&&Ae(a.children)?n.push(...eo(a.children)):bn(a)&&a.shapeFlag===2?n.push(...eo(a.type())):n.push(a)}),n},nX=(e,t,n)=>eo(e.subTree).filter(a=>{var r;return bn(a)&&((r=a.type)==null?void 0:r.name)===t&&!!a.component}).map(a=>a.component.uid).map(a=>n[a]).filter(a=>!!a),z1=(e,t)=>{const n=Ht({}),a=Ht([]),r=new WeakMap,o=u=>{n.value[u.uid]=u,Y0(n),mt(()=>{const c=u.getVnode().el,d=c.parentNode;if(!r.has(d)){r.set(d,[]);const v=d.insertBefore.bind(d);d.insertBefore=(f,h)=>(r.get(d).some(m=>f===m||h===m)&&Y0(n),v(f,h))}r.get(d).push(c)})},i=u=>{delete n.value[u.uid],Y0(n);const c=u.getVnode().el,d=c.parentNode,v=r.get(d),f=v.indexOf(c);v.splice(f,1)},l=()=>{a.value=nX(e,t,n.value)},s=u=>u.render();return{children:a,addChild:o,removeChild:i,ChildrenSorter:q({setup(u,{slots:c}){return()=>(l(),c.default?ot(s,{render:c.default}):null)}})}},pa=si({type:String,values:ii,required:!1}),yI=Symbol("size"),bI=()=>{const e=Ze(yI,{});return D(()=>p(e.size)||"")};function Fl(e,{disabled:t,beforeFocus:n,afterFocus:a,beforeBlur:r,afterBlur:o}={}){const{emit:i}=Et(),l=Ht(),s=F(!1),u=v=>{const f=Qe(n)?n(v):!1;p(t)||s.value||f||(s.value=!0,i("focus",v),a==null||a())},c=v=>{var h;const f=Qe(r)?r(v):!1;p(t)||v.relatedTarget&&((h=l.value)!=null&&h.contains(v.relatedTarget))||f||(s.value=!1,i("blur",v),o==null||o())},d=v=>{var f,h;p(t)||sc(v.target)||(f=l.value)!=null&&f.contains(document.activeElement)&&l.value!==document.activeElement||(h=e.value)==null||h.focus()};return ye([l,()=>p(t)],([v,f])=>{v&&(f?v.removeAttribute("tabindex"):v.setAttribute("tabindex","-1"))}),on(l,"focus",u,!0),on(l,"blur",c,!0),on(l,"click",d,!0),{isFocused:s,wrapperRef:l,handleFocus:u,handleBlur:c}}function rp({afterComposition:e,emit:t}){const n=F(!1),a=l=>{t==null||t("compositionstart",l),n.value=!0},r=l=>{t==null||t("compositionupdate",l),n.value=!0},o=l=>{t==null||t("compositionend",l),n.value&&(n.value=!1,He(()=>e(l)))};return{isComposing:n,handleComposition:l=>{l.type==="compositionend"?o(l):r(l)},handleCompositionStart:a,handleCompositionUpdate:r,handleCompositionEnd:o}}const _I=Symbol("emptyValuesContextKey"),aX="use-empty-values",rX=["",void 0,null],Mc=$e({emptyValues:Array,valueOnClear:{type:ae([String,Number,Boolean,Function]),default:void 0,validator:e=>(e=Qe(e)?e():e,Ae(e)?e.every(t=>!t):!e)}}),op=(e,t)=>{const n=Et()?Ze(_I,F({})):F({}),a=D(()=>e.emptyValues||n.value.emptyValues||rX),r=D(()=>Qe(e.valueOnClear)?e.valueOnClear():e.valueOnClear!==void 0?e.valueOnClear:Qe(n.value.valueOnClear)?n.value.valueOnClear():n.value.valueOnClear!==void 0?n.value.valueOnClear:t!==void 0?t:void 0),o=i=>{let l=!0;return Ae(i)?l=a.value.some(s=>Rn(i,s)):l=a.value.includes(i),l};return o(r.value)||un(aX,"value-on-clear should be a value of empty-values"),{emptyValues:a,valueOnClear:r,isEmptyValue:o}},oX=$e({ariaLabel:String,ariaOrientation:{type:String,values:["horizontal","vertical","undefined"]},ariaControls:String}),dr=e=>ji(oX,e),wI=e=>{const t=e.props,n=Ae(t)?Rd(t.map(a=>[a,{}])):t;e.setPropsDefaults=a=>{if(n){for(const[r,o]of Object.entries(a)){const i=n[r];if(en(n,r)){if(h6(i)){n[r]={...i,default:o};continue}n[r]={type:i,default:o}}}e.props=n}}},wt=(e,t)=>{if(e.install=n=>{for(const a of[e,...Object.values(t??{})])n.component(a.name,a)},t)for(const[n,a]of Object.entries(t))e[n]=a;return wI(e),e},SI=(e,t)=>(e.install=n=>{e._context=n._context,n.config.globalProperties[t]=e},e),iX=(e,t)=>(e.install=n=>{n.directive(t,e)},e),On=e=>(e.install=kt,wI(e),e),h5="ElAffix";var lX=q({name:h5,__name:"affix",props:oY,emits:iY,setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("affix"),i=Ht(),l=Ht(),s=Ht(),{height:u}=_6(),{height:c,width:d,top:v,bottom:f,left:h,update:m}=HC(l,{windowScroll:!1}),g=HC(i),y=F(!1),b=F(0),_=F(0),w=D(()=>!a.teleported||!y.value),S=D(()=>({display:"flow-root",height:y.value?`${c.value}px`:"",width:y.value?`${d.value}px`:""})),k=D(()=>{if(!y.value)return{};const A=Vn(a.offset);return{height:`${c.value}px`,width:`${d.value}px`,top:a.position==="top"?A:"",bottom:a.position==="bottom"?A:"",left:a.teleported?`${h.value}px`:"",transform:_.value?`translateY(${_.value}px)`:"",zIndex:a.zIndex}}),T=()=>{if(!s.value)return;b.value=s.value instanceof Window?document.documentElement.scrollTop:s.value.scrollTop||0;const{position:A,target:I,offset:P}=a,R=P+c.value;if(A==="top")if(I){const G=g.bottom.value-R;y.value=P>v.value&&g.bottom.value>0,_.value=G<0?G:0}else y.value=P>v.value;else if(I){const G=u.value-g.top.value-R;y.value=u.value-Pg.top.value,_.value=G<0?-G:0}else y.value=u.value-P{if(!y.value){m();return}y.value=!1,await He(),m(),y.value=!0},E=async()=>{m(),await He(),r("scroll",{scrollTop:b.value,fixed:y.value})};return ye(y,A=>r(Ot,A)),mt(()=>{a.target?(i.value=document.querySelector(a.target)??void 0,i.value||Dn(h5,`Target does not exist: ${a.target}`)):i.value=document.documentElement,s.value=x6(l.value,!0),m()}),Wh(()=>{He(M)}),j3(()=>{y.value=!1}),on(s,"scroll",E),ur(T),t({update:T,updateRoot:M}),(A,I)=>(x(),O("div",{ref_key:"root",ref:l,class:U(p(o).b()),style:et(S.value)},[(x(),ne(sf,{disabled:w.value,to:e.appendTo},[C("div",{class:U({[p(o).m("fixed")]:y.value}),style:et(k.value)},[ce(A.$slots,"default")],6)],8,["disabled","to"]))],6))}}),sX=lX;const uX=wt(sX);/*! Element Plus Icons Vue v2.3.2 */var cX=q({name:"AddLocation",__name:"add-location",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),C("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),C("path",{fill:"currentColor",d:"M544 384h96a32 32 0 1 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96v-96a32 32 0 0 1 64 0z"})]))}}),dX=cX,fX=q({name:"Aim",__name:"aim",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),C("path",{fill:"currentColor",d:"M512 96a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V128a32 32 0 0 1 32-32m0 576a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V704a32 32 0 0 1 32-32M96 512a32 32 0 0 1 32-32h192a32 32 0 0 1 0 64H128a32 32 0 0 1-32-32m576 0a32 32 0 0 1 32-32h192a32 32 0 1 1 0 64H704a32 32 0 0 1-32-32"})]))}}),vX=fX,hX=q({name:"AlarmClock",__name:"alarm-clock",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 832a320 320 0 1 0 0-640 320 320 0 0 0 0 640m0 64a384 384 0 1 1 0-768 384 384 0 0 1 0 768"}),C("path",{fill:"currentColor",d:"m292.288 824.576 55.424 32-48 83.136a32 32 0 1 1-55.424-32zm439.424 0-55.424 32 48 83.136a32 32 0 1 0 55.424-32zM512 512h160a32 32 0 1 1 0 64H480a32 32 0 0 1-32-32V320a32 32 0 0 1 64 0zM90.496 312.256A160 160 0 0 1 312.32 90.496l-46.848 46.848a96 96 0 0 0-128 128L90.56 312.256zm835.264 0A160 160 0 0 0 704 90.496l46.848 46.848a96 96 0 0 1 128 128z"})]))}}),pX=hX,mX=q({name:"Apple",__name:"apple",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M599.872 203.776a189.4 189.4 0 0 1 64.384-4.672l2.624.128c31.168 1.024 51.2 4.096 79.488 16.32 37.632 16.128 74.496 45.056 111.488 89.344 96.384 115.264 82.752 372.8-34.752 521.728-7.68 9.728-32 41.6-30.72 39.936a427 427 0 0 1-30.08 35.776c-31.232 32.576-65.28 49.216-110.08 50.048-31.36.64-53.568-5.312-84.288-18.752l-6.528-2.88c-20.992-9.216-30.592-11.904-47.296-11.904-18.112 0-28.608 2.88-51.136 12.672l-6.464 2.816c-28.416 12.224-48.32 18.048-76.16 19.2-74.112 2.752-116.928-38.08-180.672-132.16-96.64-142.08-132.608-349.312-55.04-486.4 46.272-81.92 129.92-133.632 220.672-135.04 32.832-.576 60.288 6.848 99.648 22.72 27.136 10.88 34.752 13.76 37.376 14.272 16.256-20.16 27.776-36.992 34.56-50.24 13.568-26.304 27.2-59.968 40.704-100.8a32 32 0 1 1 60.8 20.224c-12.608 37.888-25.408 70.4-38.528 97.664m-51.52 78.08c-14.528 17.792-31.808 37.376-51.904 58.816a32 32 0 1 1-46.72-43.776l12.288-13.248c-28.032-11.2-61.248-26.688-95.68-26.112-70.4 1.088-135.296 41.6-171.648 105.792C121.6 492.608 176 684.16 247.296 788.992c34.816 51.328 76.352 108.992 130.944 106.944 52.48-2.112 72.32-34.688 135.872-34.688s81.28 34.688 136.96 33.536c56.448-1.088 75.776-39.04 126.848-103.872 107.904-136.768 107.904-362.752 35.776-449.088-72.192-86.272-124.672-84.096-151.68-85.12-41.472-4.288-81.6 12.544-113.664 25.152"})]))}}),gX=mX,yX=q({name:"ArrowDownBold",__name:"arrow-down-bold",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M104.704 338.752a64 64 0 0 1 90.496 0l316.8 316.8 316.8-316.8a64 64 0 0 1 90.496 90.496L557.248 791.296a64 64 0 0 1-90.496 0L104.704 429.248a64 64 0 0 1 0-90.496"})]))}}),bX=yX,_X=q({name:"ArrowDown",__name:"arrow-down",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M831.872 340.864 512 652.672 192.128 340.864a30.59 30.59 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.59 30.59 0 0 0-42.752 0z"})]))}}),ui=_X,wX=q({name:"ArrowLeftBold",__name:"arrow-left-bold",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M685.248 104.704a64 64 0 0 1 0 90.496L368.448 512l316.8 316.8a64 64 0 0 1-90.496 90.496L232.704 557.248a64 64 0 0 1 0-90.496l362.048-362.048a64 64 0 0 1 90.496 0"})]))}}),SX=wX,CX=q({name:"ArrowLeft",__name:"arrow-left",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.59 30.59 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.59 30.59 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0"})]))}}),Ji=CX,xX=q({name:"ArrowRightBold",__name:"arrow-right-bold",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M338.752 104.704a64 64 0 0 0 0 90.496l316.8 316.8-316.8 316.8a64 64 0 0 0 90.496 90.496l362.048-362.048a64 64 0 0 0 0-90.496L429.248 104.704a64 64 0 0 0-90.496 0"})]))}}),kX=xX,TX=q({name:"ArrowRight",__name:"arrow-right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M340.864 149.312a30.59 30.59 0 0 0 0 42.752L652.736 512 340.864 831.872a30.59 30.59 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"})]))}}),za=TX,MX=q({name:"ArrowUpBold",__name:"arrow-up-bold",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M104.704 685.248a64 64 0 0 0 90.496 0l316.8-316.8 316.8 316.8a64 64 0 0 0 90.496-90.496L557.248 232.704a64 64 0 0 0-90.496 0L104.704 594.752a64 64 0 0 0 0 90.496"})]))}}),EX=MX,AX=q({name:"ArrowUp",__name:"arrow-up",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0"})]))}}),ip=AX,IX=q({name:"Avatar",__name:"avatar",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M628.736 528.896A416 416 0 0 1 928 928H96a415.87 415.87 0 0 1 299.264-399.104L512 704zM720 304a208 208 0 1 1-416 0 208 208 0 0 1 416 0"})]))}}),LX=IX,PX=q({name:"Back",__name:"back",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 480h640a32 32 0 1 1 0 64H224a32 32 0 0 1 0-64"}),C("path",{fill:"currentColor",d:"m237.248 512 265.408 265.344a32 32 0 0 1-45.312 45.312l-288-288a32 32 0 0 1 0-45.312l288-288a32 32 0 1 1 45.312 45.312z"})]))}}),CI=PX,DX=q({name:"Baseball",__name:"baseball",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M195.2 828.8a448 448 0 1 1 633.6-633.6 448 448 0 0 1-633.6 633.6m45.248-45.248a384 384 0 1 0 543.104-543.104 384 384 0 0 0-543.104 543.104"}),C("path",{fill:"currentColor",d:"M497.472 96.896c22.784 4.672 44.416 9.472 64.896 14.528a256.128 256.128 0 0 0 350.208 350.208c5.056 20.48 9.856 42.112 14.528 64.896A320.128 320.128 0 0 1 497.472 96.896M108.48 491.904a320.128 320.128 0 0 1 423.616 423.68c-23.04-3.648-44.992-7.424-65.728-11.52a256.128 256.128 0 0 0-346.496-346.432 1737 1737 0 0 1-11.392-65.728"})]))}}),OX=DX,$X=q({name:"Basketball",__name:"basketball",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M778.752 788.224a382.46 382.46 0 0 0 116.032-245.632 256.51 256.51 0 0 0-241.728-13.952 762.9 762.9 0 0 1 125.696 259.584m-55.04 44.224a699.65 699.65 0 0 0-125.056-269.632 256.13 256.13 0 0 0-56.064 331.968 382.7 382.7 0 0 0 181.12-62.336m-254.08 61.248A320.13 320.13 0 0 1 557.76 513.6a716 716 0 0 0-48.192-48.128 320.13 320.13 0 0 1-379.264 88.384 382.4 382.4 0 0 0 110.144 229.696 382.4 382.4 0 0 0 229.184 110.08zM129.28 481.088a256.13 256.13 0 0 0 331.072-56.448 699.65 699.65 0 0 0-268.8-124.352 382.66 382.66 0 0 0-62.272 180.8m106.56-235.84a762.9 762.9 0 0 1 258.688 125.056 256.51 256.51 0 0 0-13.44-241.088A382.46 382.46 0 0 0 235.84 245.248m318.08-114.944c40.576 89.536 37.76 193.92-8.448 281.344a780 780 0 0 1 66.176 66.112 320.83 320.83 0 0 1 282.112-8.128 382.4 382.4 0 0 0-110.144-229.12 382.4 382.4 0 0 0-229.632-110.208zM828.8 828.8a448 448 0 1 1-633.6-633.6 448 448 0 0 1 633.6 633.6"})]))}}),RX=$X,NX=q({name:"BellFilled",__name:"bell-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M640 832a128 128 0 0 1-256 0zm192-64H134.4a38.4 38.4 0 0 1 0-76.8H192V448c0-154.88 110.08-284.16 256.32-313.6a64 64 0 1 1 127.36 0A320.13 320.13 0 0 1 832 448v243.2h57.6a38.4 38.4 0 0 1 0 76.8z"})]))}}),BX=NX,VX=q({name:"Bell",__name:"bell",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a64 64 0 0 1 64 64v64H448v-64a64 64 0 0 1 64-64"}),C("path",{fill:"currentColor",d:"M256 768h512V448a256 256 0 1 0-512 0zm256-640a320 320 0 0 1 320 320v384H192V448a320 320 0 0 1 320-320"}),C("path",{fill:"currentColor",d:"M96 768h832q32 0 32 32t-32 32H96q-32 0-32-32t32-32m352 128h128a64 64 0 0 1-128 0"})]))}}),zX=VX,FX=q({name:"Bicycle",__name:"bicycle",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 832a128 128 0 1 0 0-256 128 128 0 0 0 0 256m0 64a192 192 0 1 1 0-384 192 192 0 0 1 0 384"}),C("path",{fill:"currentColor",d:"M288 672h320q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),C("path",{fill:"currentColor",d:"M768 832a128 128 0 1 0 0-256 128 128 0 0 0 0 256m0 64a192 192 0 1 1 0-384 192 192 0 0 1 0 384"}),C("path",{fill:"currentColor",d:"M480 192a32 32 0 0 1 0-64h160a32 32 0 0 1 31.04 24.256l96 384a32 32 0 0 1-62.08 15.488L615.04 192zM96 384a32 32 0 0 1 0-64h128a32 32 0 0 1 30.336 21.888l64 192a32 32 0 1 1-60.672 20.224L200.96 384z"}),C("path",{fill:"currentColor",d:"m373.376 599.808-42.752-47.616 320-288 42.752 47.616z"})]))}}),HX=FX,WX=q({name:"BottomLeft",__name:"bottom-left",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 768h416a32 32 0 1 1 0 64H224a32 32 0 0 1-32-32V352a32 32 0 0 1 64 0z"}),C("path",{fill:"currentColor",d:"M246.656 822.656a32 32 0 0 1-45.312-45.312l544-544a32 32 0 0 1 45.312 45.312z"})]))}}),KX=WX,UX=q({name:"BottomRight",__name:"bottom-right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M352 768a32 32 0 1 0 0 64h448a32 32 0 0 0 32-32V352a32 32 0 0 0-64 0v416z"}),C("path",{fill:"currentColor",d:"M777.344 822.656a32 32 0 0 0 45.312-45.312l-544-544a32 32 0 0 0-45.312 45.312z"})]))}}),GX=UX,YX=q({name:"Bottom",__name:"bottom",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 805.888V168a32 32 0 1 0-64 0v637.888L246.656 557.952a30.72 30.72 0 0 0-45.312 0 35.52 35.52 0 0 0 0 48.064l288 306.048a30.72 30.72 0 0 0 45.312 0l288-306.048a35.52 35.52 0 0 0 0-48 30.72 30.72 0 0 0-45.312 0L544 805.824z"})]))}}),jX=YX,qX=q({name:"Bowl",__name:"bowl",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M714.432 704a351.74 351.74 0 0 0 148.16-256H161.408a351.74 351.74 0 0 0 148.16 256zM288 766.592A415.68 415.68 0 0 1 96 416a32 32 0 0 1 32-32h768a32 32 0 0 1 32 32 415.68 415.68 0 0 1-192 350.592V832a64 64 0 0 1-64 64H352a64 64 0 0 1-64-64zM493.248 320h-90.496l254.4-254.4a32 32 0 1 1 45.248 45.248zm187.328 0h-128l269.696-155.712a32 32 0 0 1 32 55.424zM352 768v64h320v-64z"})]))}}),XX=qX,ZX=q({name:"Box",__name:"box",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M317.056 128 128 344.064V896h768V344.064L706.944 128zm-14.528-64h418.944a32 32 0 0 1 24.064 10.88l206.528 236.096A32 32 0 0 1 960 332.032V928a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V332.032a32 32 0 0 1 7.936-21.12L278.4 75.008A32 32 0 0 1 302.528 64"}),C("path",{fill:"currentColor",d:"M64 320h896v64H64z"}),C("path",{fill:"currentColor",d:"M448 327.872V640h128V327.872L526.08 128h-28.16zM448 64h128l64 256v352a32 32 0 0 1-32 32H416a32 32 0 0 1-32-32V320z"})]))}}),JX=ZX,QX=q({name:"Briefcase",__name:"briefcase",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M320 320V128h384v192h192v192H128V320zM128 576h768v320H128zm256-256h256.064V192H384z"})]))}}),eZ=QX,tZ=q({name:"BrushFilled",__name:"brush-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M608 704v160a96 96 0 0 1-192 0V704h-96a128 128 0 0 1-128-128h640a128 128 0 0 1-128 128zM192 512V128.064h640V512z"})]))}}),nZ=tZ,aZ=q({name:"Brush",__name:"brush",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M896 448H128v192a64 64 0 0 0 64 64h192v192h256V704h192a64 64 0 0 0 64-64zm-770.752-64c0-47.552 5.248-90.24 15.552-128 14.72-54.016 42.496-107.392 83.2-160h417.28l-15.36 70.336L736 96h211.2c-24.832 42.88-41.92 96.256-51.2 160a664 664 0 0 0-6.144 128H960v256a128 128 0 0 1-128 128H704v160a32 32 0 0 1-32 32H352a32 32 0 0 1-32-32V768H192A128 128 0 0 1 64 640V384zm64 0h636.544c-2.048-45.824.256-91.584 6.848-137.216 4.48-30.848 10.688-59.776 18.688-86.784h-96.64l-221.12 141.248L561.92 160H256.512c-25.856 37.888-43.776 75.456-53.952 112.832-8.768 32.064-13.248 69.12-13.312 111.168"})]))}}),rZ=aZ,oZ=q({name:"Burger",__name:"burger",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 512a32 32 0 0 0-32 32v64a32 32 0 0 0 30.08 32H864a32 32 0 0 0 32-32v-64a32 32 0 0 0-32-32zm736-58.56A96 96 0 0 1 960 544v64a96 96 0 0 1-51.968 85.312L855.36 833.6a96 96 0 0 1-89.856 62.272H258.496A96 96 0 0 1 168.64 833.6l-52.608-140.224A96 96 0 0 1 64 608v-64a96 96 0 0 1 64-90.56V448a384 384 0 1 1 768 5.44M832 448a320 320 0 0 0-640 0zM512 704H188.352l40.192 107.136a32 32 0 0 0 29.952 20.736h507.008a32 32 0 0 0 29.952-20.736L835.648 704z"})]))}}),iZ=oZ,lZ=q({name:"Calendar",__name:"calendar",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 384v512h768V192H768v32a32 32 0 1 1-64 0v-32H320v32a32 32 0 0 1-64 0v-32H128v128h768v64zm192-256h384V96a32 32 0 1 1 64 0v32h160a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h160V96a32 32 0 0 1 64 0zm-32 384h64a32 32 0 0 1 0 64h-64a32 32 0 0 1 0-64m0 192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64m192-192h64a32 32 0 0 1 0 64h-64a32 32 0 0 1 0-64m0 192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64m192-192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64m0 192h64a32 32 0 1 1 0 64h-64a32 32 0 1 1 0-64"})]))}}),xI=lZ,sZ=q({name:"CameraFilled",__name:"camera-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 224a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h704a64 64 0 0 0 64-64V288a64 64 0 0 0-64-64H748.416l-46.464-92.672A64 64 0 0 0 644.736 96H379.328a64 64 0 0 0-57.216 35.392L275.776 224zm352 435.2a115.2 115.2 0 1 0 0-230.4 115.2 115.2 0 0 0 0 230.4m0 140.8a256 256 0 1 1 0-512 256 256 0 0 1 0 512"})]))}}),uZ=sZ,cZ=q({name:"Camera",__name:"camera",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M896 256H128v576h768zm-199.424-64-32.064-64h-304.96l-32 64zM96 192h160l46.336-92.608A64 64 0 0 1 359.552 64h304.96a64 64 0 0 1 57.216 35.328L768.192 192H928a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32m416 512a160 160 0 1 0 0-320 160 160 0 0 0 0 320m0 64a224 224 0 1 1 0-448 224 224 0 0 1 0 448"})]))}}),kI=cZ,dZ=q({name:"CaretBottom",__name:"caret-bottom",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m192 384 320 384 320-384z"})]))}}),fZ=dZ,vZ=q({name:"CaretLeft",__name:"caret-left",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M672 192 288 511.936 672 832z"})]))}}),hZ=vZ,pZ=q({name:"CaretRight",__name:"caret-right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M384 192v640l384-320.064z"})]))}}),F6=pZ,mZ=q({name:"CaretTop",__name:"caret-top",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 320 192 704h639.936z"})]))}}),TI=mZ,gZ=q({name:"Cellphone",__name:"cellphone",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 128a64 64 0 0 0-64 64v640a64 64 0 0 0 64 64h512a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h512a128 128 0 0 1 128 128v640a128 128 0 0 1-128 128H256a128 128 0 0 1-128-128V192A128 128 0 0 1 256 64m128 128h256a32 32 0 1 1 0 64H384a32 32 0 0 1 0-64m128 640a64 64 0 1 1 0-128 64 64 0 0 1 0 128"})]))}}),yZ=gZ,bZ=q({name:"ChatDotRound",__name:"chat-dot-round",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m174.72 855.68 135.296-45.12 23.68 11.84C388.096 849.536 448.576 864 512 864c211.84 0 384-166.784 384-352S723.84 160 512 160 128 326.784 128 512c0 69.12 24.96 139.264 70.848 199.232l22.08 28.8-46.272 115.584zm-45.248 82.56A32 32 0 0 1 89.6 896l58.368-145.92C94.72 680.32 64 596.864 64 512 64 299.904 256 96 512 96s448 203.904 448 416-192 416-448 416a461.06 461.06 0 0 1-206.912-48.384l-175.616 58.56z"}),C("path",{fill:"currentColor",d:"M512 563.2a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4m192 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4m-384 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4"})]))}}),_Z=bZ,wZ=q({name:"ChatDotSquare",__name:"chat-dot-square",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M273.536 736H800a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H224a64 64 0 0 0-64 64v570.88zM296 800 147.968 918.4A32 32 0 0 1 96 893.44V256a128 128 0 0 1 128-128h576a128 128 0 0 1 128 128v416a128 128 0 0 1-128 128z"}),C("path",{fill:"currentColor",d:"M512 499.2a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4m192 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4m-384 0a51.2 51.2 0 1 1 0-102.4 51.2 51.2 0 0 1 0 102.4"})]))}}),SZ=wZ,CZ=q({name:"ChatLineRound",__name:"chat-line-round",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m174.72 855.68 135.296-45.12 23.68 11.84C388.096 849.536 448.576 864 512 864c211.84 0 384-166.784 384-352S723.84 160 512 160 128 326.784 128 512c0 69.12 24.96 139.264 70.848 199.232l22.08 28.8-46.272 115.584zm-45.248 82.56A32 32 0 0 1 89.6 896l58.368-145.92C94.72 680.32 64 596.864 64 512 64 299.904 256 96 512 96s448 203.904 448 416-192 416-448 416a461.06 461.06 0 0 1-206.912-48.384l-175.616 58.56z"}),C("path",{fill:"currentColor",d:"M352 576h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32m32-192h256q32 0 32 32t-32 32H384q-32 0-32-32t32-32"})]))}}),xZ=CZ,kZ=q({name:"ChatLineSquare",__name:"chat-line-square",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 826.88 273.536 736H800a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H224a64 64 0 0 0-64 64zM296 800 147.968 918.4A32 32 0 0 1 96 893.44V256a128 128 0 0 1 128-128h576a128 128 0 0 1 128 128v416a128 128 0 0 1-128 128z"}),C("path",{fill:"currentColor",d:"M352 512h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32m0-192h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32"})]))}}),TZ=kZ,MZ=q({name:"ChatRound",__name:"chat-round",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m174.72 855.68 130.048-43.392 23.424 11.392C382.4 849.984 444.352 864 512 864c223.744 0 384-159.872 384-352 0-192.832-159.104-352-384-352S128 319.168 128 512a341.12 341.12 0 0 0 69.248 204.288l21.632 28.8-44.16 110.528zm-45.248 82.56A32 32 0 0 1 89.6 896l56.512-141.248A405.12 405.12 0 0 1 64 512C64 299.904 235.648 96 512 96s448 203.904 448 416-173.44 416-448 416c-79.68 0-150.848-17.152-211.712-46.72l-170.88 56.96z"})]))}}),EZ=MZ,AZ=q({name:"ChatSquare",__name:"chat-square",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M273.536 736H800a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H224a64 64 0 0 0-64 64v570.88zM296 800 147.968 918.4A32 32 0 0 1 96 893.44V256a128 128 0 0 1 128-128h576a128 128 0 0 1 128 128v416a128 128 0 0 1-128 128z"})]))}}),IZ=AZ,LZ=q({name:"Check",__name:"check",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M406.656 706.944 195.84 496.256a32 32 0 1 0-45.248 45.248l256 256 512-512a32 32 0 0 0-45.248-45.248L406.592 706.944z"})]))}}),Os=LZ,PZ=q({name:"Checked",__name:"checked",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 192h160v736H160V192h160.064v64H704zM311.616 537.28l-45.312 45.248L447.36 763.52l316.8-316.8-45.312-45.184L447.36 673.024zM384 192V96h256v96z"})]))}}),DZ=PZ,OZ=q({name:"Cherry",__name:"cherry",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M261.056 449.6c13.824-69.696 34.88-128.96 63.36-177.728 23.744-40.832 61.12-88.64 112.256-143.872H320a32 32 0 0 1 0-64h384a32 32 0 1 1 0 64H554.752c14.912 39.168 41.344 86.592 79.552 141.76 47.36 68.48 84.8 106.752 106.304 114.304a224 224 0 1 1-84.992 14.784c-22.656-22.912-47.04-53.76-73.92-92.608-38.848-56.128-67.008-105.792-84.352-149.312-55.296 58.24-94.528 107.52-117.76 147.2-23.168 39.744-41.088 88.768-53.568 147.072a224.064 224.064 0 1 1-64.96-1.6M288 832a160 160 0 1 0 0-320 160 160 0 0 0 0 320m448-64a160 160 0 1 0 0-320 160 160 0 0 0 0 320"})]))}}),$Z=OZ,RZ=q({name:"Chicken",__name:"chicken",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M349.952 716.992 478.72 588.16a106.7 106.7 0 0 1-26.176-19.072 106.7 106.7 0 0 1-19.072-26.176L304.704 671.744c.768 3.072 1.472 6.144 2.048 9.216l2.048 31.936 31.872 1.984c3.136.64 6.208 1.28 9.28 2.112m57.344 33.152a128 128 0 1 1-216.32 114.432l-1.92-32-32-1.92a128 128 0 1 1 114.432-216.32L416.64 469.248c-2.432-101.44 58.112-239.104 149.056-330.048 107.328-107.328 231.296-85.504 316.8 0 85.44 85.44 107.328 209.408 0 316.8-91.008 90.88-228.672 151.424-330.112 149.056L407.296 750.08zm90.496-226.304c49.536 49.536 233.344-7.04 339.392-113.088 78.208-78.208 63.232-163.072 0-226.304-63.168-63.232-148.032-78.208-226.24 0C504.896 290.496 448.32 474.368 497.792 523.84M244.864 708.928a64 64 0 1 0-59.84 59.84l56.32-3.52zm8.064 127.68a64 64 0 1 0 59.84-59.84l-56.32 3.52z"})]))}}),NZ=RZ,BZ=q({name:"ChromeFilled",__name:"chrome-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M938.67 512.01c0-44.59-6.82-87.6-19.54-128H682.67a212.37 212.37 0 0 1 42.67 128c.06 38.71-10.45 76.7-30.42 109.87l-182.91 316.8c235.65-.01 426.66-191.02 426.66-426.67"}),C("path",{fill:"currentColor",d:"M576.79 401.63a127.9 127.9 0 0 0-63.56-17.6c-22.36-.22-44.39 5.43-63.89 16.38s-35.79 26.82-47.25 46.02a128 128 0 0 0-2.16 127.44l1.24 2.13a127.9 127.9 0 0 0 46.36 46.61 127.9 127.9 0 0 0 63.38 17.44c22.29.2 44.24-5.43 63.68-16.33a127.94 127.94 0 0 0 47.16-45.79v-.01l1.11-1.92a127.98 127.98 0 0 0 .29-127.46 127.96 127.96 0 0 0-46.36-46.91"}),C("path",{fill:"currentColor",d:"M394.45 333.96A213.34 213.34 0 0 1 512 298.67h369.58A426.5 426.5 0 0 0 512 85.34a425.6 425.6 0 0 0-171.74 35.98 425.6 425.6 0 0 0-142.62 102.22l118.14 204.63a213.4 213.4 0 0 1 78.67-94.21m117.56 604.72H512zm-97.25-236.73a213.3 213.3 0 0 1-89.54-86.81L142.48 298.6c-36.35 62.81-57.13 135.68-57.13 213.42 0 203.81 142.93 374.22 333.95 416.55h.04l118.19-204.71a213.3 213.3 0 0 1-122.77-21.91"})]))}}),VZ=BZ,zZ=q({name:"CircleCheckFilled",__name:"circle-check-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.27 38.27 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z"})]))}}),MI=zZ,FZ=q({name:"CircleCheck",__name:"circle-check",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),C("path",{fill:"currentColor",d:"M745.344 361.344a32 32 0 0 1 45.312 45.312l-288 288a32 32 0 0 1-45.312 0l-160-160a32 32 0 1 1 45.312-45.312L480 626.752z"})]))}}),Ao=FZ,HZ=q({name:"CircleCloseFilled",__name:"circle-close-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336z"})]))}}),fc=HZ,WZ=q({name:"CircleClose",__name:"circle-close",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m466.752 512-90.496-90.496a32 32 0 0 1 45.248-45.248L512 466.752l90.496-90.496a32 32 0 1 1 45.248 45.248L557.248 512l90.496 90.496a32 32 0 1 1-45.248 45.248L512 557.248l-90.496 90.496a32 32 0 0 1-45.248-45.248z"}),C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),ci=WZ,KZ=q({name:"CirclePlusFilled",__name:"circle-plus-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-38.4 409.6H326.4a38.4 38.4 0 1 0 0 76.8h147.2v147.2a38.4 38.4 0 0 0 76.8 0V550.4h147.2a38.4 38.4 0 0 0 0-76.8H550.4V326.4a38.4 38.4 0 1 0-76.8 0z"})]))}}),UZ=KZ,GZ=q({name:"CirclePlus",__name:"circle-plus",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M352 480h320a32 32 0 1 1 0 64H352a32 32 0 0 1 0-64"}),C("path",{fill:"currentColor",d:"M480 672V352a32 32 0 1 1 64 0v320a32 32 0 0 1-64 0"}),C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),YZ=GZ,jZ=q({name:"Clock",__name:"clock",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),C("path",{fill:"currentColor",d:"M480 256a32 32 0 0 1 32 32v256a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M480 512h256q32 0 32 32t-32 32H480q-32 0-32-32t32-32"})]))}}),H6=jZ,qZ=q({name:"CloseBold",__name:"close-bold",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496"})]))}}),XZ=qZ,ZZ=q({name:"Close",__name:"close",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"})]))}}),so=ZZ,JZ=q({name:"Cloudy",__name:"cloudy",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M598.4 831.872H328.192a256 256 0 0 1-34.496-510.528A352 352 0 1 1 598.4 831.872m-271.36-64h272.256a288 288 0 1 0-248.512-417.664L335.04 381.44l-34.816 3.584a192 192 0 0 0 26.88 382.848z"})]))}}),QZ=JZ,eJ=q({name:"CoffeeCup",__name:"coffee-cup",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M768 192a192 192 0 1 1-8 383.808A256.13 256.13 0 0 1 512 768H320A256 256 0 0 1 64 512V160a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zm0 64v256a128 128 0 1 0 0-256M96 832h640a32 32 0 1 1 0 64H96a32 32 0 1 1 0-64m32-640v320a192 192 0 0 0 192 192h192a192 192 0 0 0 192-192V192z"})]))}}),tJ=eJ,nJ=q({name:"Coffee",__name:"coffee",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M822.592 192h14.272a32 32 0 0 1 31.616 26.752l21.312 128A32 32 0 0 1 858.24 384h-49.344l-39.04 546.304A32 32 0 0 1 737.92 960H285.824a32 32 0 0 1-32-29.696L214.912 384H165.76a32 32 0 0 1-31.552-37.248l21.312-128A32 32 0 0 1 187.136 192h14.016l-6.72-93.696A32 32 0 0 1 226.368 64h571.008a32 32 0 0 1 31.936 34.304zm-64.128 0 4.544-64H260.736l4.544 64zm-548.16 128H820.48l-10.688-64H214.208l-10.688 64zm68.736 64 36.544 512H708.16l36.544-512z"})]))}}),aJ=nJ,rJ=q({name:"Coin",__name:"coin",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m161.92 580.736 29.888 58.88C171.328 659.776 160 681.728 160 704c0 82.304 155.328 160 352 160s352-77.696 352-160c0-22.272-11.392-44.16-31.808-64.32l30.464-58.432C903.936 615.808 928 657.664 928 704c0 129.728-188.544 224-416 224S96 833.728 96 704c0-46.592 24.32-88.576 65.92-123.264"}),C("path",{fill:"currentColor",d:"m161.92 388.736 29.888 58.88C171.328 467.84 160 489.792 160 512c0 82.304 155.328 160 352 160s352-77.696 352-160c0-22.272-11.392-44.16-31.808-64.32l30.464-58.432C903.936 423.808 928 465.664 928 512c0 129.728-188.544 224-416 224S96 641.728 96 512c0-46.592 24.32-88.576 65.92-123.264"}),C("path",{fill:"currentColor",d:"M512 544c-227.456 0-416-94.272-416-224S284.544 96 512 96s416 94.272 416 224-188.544 224-416 224m0-64c196.672 0 352-77.696 352-160S708.672 160 512 160s-352 77.696-352 160 155.328 160 352 160"})]))}}),oJ=rJ,iJ=q({name:"ColdDrink",__name:"cold-drink",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M768 64a192 192 0 1 1-69.952 370.88L480 725.376V896h96a32 32 0 1 1 0 64H320a32 32 0 1 1 0-64h96V725.376L76.8 273.536a64 64 0 0 1-12.8-38.4v-10.688a32 32 0 0 1 32-32h71.808l-65.536-83.84a32 32 0 0 1 50.432-39.424l96.256 123.264h337.728A192.06 192.06 0 0 1 768 64M656.896 192.448H800a32 32 0 0 1 32 32v10.624a64 64 0 0 1-12.8 38.4l-80.448 107.2a128 128 0 1 0-81.92-188.16v-.064zm-357.888 64 129.472 165.76a32 32 0 0 1-50.432 39.36l-160.256-205.12H144l304 404.928 304-404.928z"})]))}}),lJ=iJ,sJ=q({name:"CollectionTag",__name:"collection-tag",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 128v698.88l196.032-156.864a96 96 0 0 1 119.936 0L768 826.816V128zm-32-64h576a32 32 0 0 1 32 32v797.44a32 32 0 0 1-51.968 24.96L531.968 720a32 32 0 0 0-39.936 0L243.968 918.4A32 32 0 0 1 192 893.44V96a32 32 0 0 1 32-32"})]))}}),uJ=sJ,cJ=q({name:"Collection",__name:"collection",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 736h640V128H256a64 64 0 0 0-64 64zm64-672h608a32 32 0 0 1 32 32v672a32 32 0 0 1-32 32H160l-32 57.536V192A128 128 0 0 1 256 64"}),C("path",{fill:"currentColor",d:"M240 800a48 48 0 1 0 0 96h592v-96zm0-64h656v160a64 64 0 0 1-64 64H240a112 112 0 0 1 0-224m144-608v250.88l96-76.8 96 76.8V128zm-64-64h320v381.44a32 32 0 0 1-51.968 24.96L480 384l-108.032 86.4A32 32 0 0 1 320 445.44z"})]))}}),dJ=cJ,fJ=q({name:"Comment",__name:"comment",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M736 504a56 56 0 1 1 0-112 56 56 0 0 1 0 112m-224 0a56 56 0 1 1 0-112 56 56 0 0 1 0 112m-224 0a56 56 0 1 1 0-112 56 56 0 0 1 0 112M128 128v640h192v160l224-160h352V128z"})]))}}),vJ=fJ,hJ=q({name:"Compass",__name:"compass",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),C("path",{fill:"currentColor",d:"M725.888 315.008C676.48 428.672 624 513.28 568.576 568.64c-55.424 55.424-139.968 107.904-253.568 157.312a12.8 12.8 0 0 1-16.896-16.832c49.536-113.728 102.016-198.272 157.312-253.632 55.36-55.296 139.904-107.776 253.632-157.312a12.8 12.8 0 0 1 16.832 16.832"})]))}}),pJ=hJ,mJ=q({name:"Connection",__name:"connection",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M640 384v64H448a128 128 0 0 0-128 128v128a128 128 0 0 0 128 128h320a128 128 0 0 0 128-128V576a128 128 0 0 0-64-110.848V394.88c74.56 26.368 128 97.472 128 181.056v128a192 192 0 0 1-192 192H448a192 192 0 0 1-192-192V576a192 192 0 0 1 192-192z"}),C("path",{fill:"currentColor",d:"M384 640v-64h192a128 128 0 0 0 128-128V320a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128v128a128 128 0 0 0 64 110.848v70.272A192.06 192.06 0 0 1 64 448V320a192 192 0 0 1 192-192h320a192 192 0 0 1 192 192v128a192 192 0 0 1-192 192z"})]))}}),p4=mJ,gJ=q({name:"Coordinate",__name:"coordinate",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 512h64v320h-64z"}),C("path",{fill:"currentColor",d:"M192 896h640a64 64 0 0 0-64-64H256a64 64 0 0 0-64 64m64-128h512a128 128 0 0 1 128 128v64H128v-64a128 128 0 0 1 128-128m256-256a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512"})]))}}),yJ=gJ,bJ=q({name:"CopyDocument",__name:"copy-document",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64z"}),C("path",{fill:"currentColor",d:"M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64"})]))}}),_J=bJ,wJ=q({name:"Cpu",__name:"cpu",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M320 256a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h384a64 64 0 0 0 64-64V320a64 64 0 0 0-64-64zm0-64h384a128 128 0 0 1 128 128v384a128 128 0 0 1-128 128H320a128 128 0 0 1-128-128V320a128 128 0 0 1 128-128"}),C("path",{fill:"currentColor",d:"M512 64a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m160 0a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m-320 0a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m160 896a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32m160 0a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32m-320 0a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32M64 512a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m0-160a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m0 320a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m896-160a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32m0-160a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32m0 320a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32"})]))}}),SJ=wJ,CJ=q({name:"CreditCard",__name:"credit-card",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M896 324.096c0-42.368-2.496-55.296-9.536-68.48a52.35 52.35 0 0 0-22.144-22.08c-13.12-7.04-26.048-9.536-68.416-9.536H228.096c-42.368 0-55.296 2.496-68.48 9.536a52.35 52.35 0 0 0-22.08 22.144c-7.04 13.12-9.536 26.048-9.536 68.416v375.808c0 42.368 2.496 55.296 9.536 68.48a52.35 52.35 0 0 0 22.144 22.08c13.12 7.04 26.048 9.536 68.416 9.536h567.808c42.368 0 55.296-2.496 68.48-9.536a52.35 52.35 0 0 0 22.08-22.144c7.04-13.12 9.536-26.048 9.536-68.416zm64 0v375.808c0 57.088-5.952 77.76-17.088 98.56-11.136 20.928-27.52 37.312-48.384 48.448S852.928 864 795.968 864H228.032c-57.088 0-77.76-5.952-98.56-17.088a116.3 116.3 0 0 1-48.448-48.384c-11.136-20.864-17.088-41.6-17.088-98.56V324.032c0-57.088 5.952-77.76 17.088-98.56 11.136-20.928 27.52-37.312 48.384-48.448s41.6-17.088 98.56-17.088H795.84c57.088 0 77.76 5.952 98.56 17.088 20.928 11.136 37.312 27.52 48.448 48.384s17.088 41.6 17.088 98.56z"}),C("path",{fill:"currentColor",d:"M64 320h896v64H64zm0 128h896v64H64zm128 192h256v64H192z"})]))}}),xJ=CJ,kJ=q({name:"Crop",__name:"crop",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 768h672a32 32 0 1 1 0 64H224a32 32 0 0 1-32-32V96a32 32 0 0 1 64 0z"}),C("path",{fill:"currentColor",d:"M832 224v704a32 32 0 1 1-64 0V256H96a32 32 0 0 1 0-64h704a32 32 0 0 1 32 32"})]))}}),TJ=kJ,MJ=q({name:"DArrowLeft",__name:"d-arrow-left",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M529.408 149.376a29.12 29.12 0 0 1 41.728 0 30.59 30.59 0 0 1 0 42.688L259.264 511.936l311.872 319.936a30.59 30.59 0 0 1-.512 43.264 29.12 29.12 0 0 1-41.216-.512L197.76 534.272a32 32 0 0 1 0-44.672zm256 0a29.12 29.12 0 0 1 41.728 0 30.59 30.59 0 0 1 0 42.688L515.264 511.936l311.872 319.936a30.59 30.59 0 0 1-.512 43.264 29.12 29.12 0 0 1-41.216-.512L453.76 534.272a32 32 0 0 1 0-44.672z"})]))}}),Al=MJ,EJ=q({name:"DArrowRight",__name:"d-arrow-right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M452.864 149.312a29.12 29.12 0 0 1 41.728.064L826.24 489.664a32 32 0 0 1 0 44.672L494.592 874.624a29.12 29.12 0 0 1-41.728 0 30.59 30.59 0 0 1 0-42.752L764.736 512 452.864 192a30.59 30.59 0 0 1 0-42.688m-256 0a29.12 29.12 0 0 1 41.728.064L570.24 489.664a32 32 0 0 1 0 44.672L238.592 874.624a29.12 29.12 0 0 1-41.728 0 30.59 30.59 0 0 1 0-42.752L508.736 512 196.864 192a30.59 30.59 0 0 1 0-42.688"})]))}}),Il=EJ,AJ=q({name:"DCaret",__name:"d-caret",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m512 128 288 320H224zM224 576h576L512 896z"})]))}}),IJ=AJ,LJ=q({name:"DataAnalysis",__name:"data-analysis",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m665.216 768 110.848 192h-73.856L591.36 768H433.024L322.176 960H248.32l110.848-192H160a32 32 0 0 1-32-32V192H64a32 32 0 0 1 0-64h896a32 32 0 1 1 0 64h-64v544a32 32 0 0 1-32 32zM832 192H192v512h640zM352 448a32 32 0 0 1 32 32v64a32 32 0 0 1-64 0v-64a32 32 0 0 1 32-32m160-64a32 32 0 0 1 32 32v128a32 32 0 0 1-64 0V416a32 32 0 0 1 32-32m160-64a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V352a32 32 0 0 1 32-32"})]))}}),PJ=LJ,DJ=q({name:"DataBoard",__name:"data-board",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M32 128h960v64H32z"}),C("path",{fill:"currentColor",d:"M192 192v512h640V192zm-64-64h768v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32z"}),C("path",{fill:"currentColor",d:"M322.176 960H248.32l144.64-250.56 55.424 32zm453.888 0h-73.856L576 741.44l55.424-32z"})]))}}),OJ=DJ,$J=q({name:"DataLine",__name:"data-line",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M359.168 768H160a32 32 0 0 1-32-32V192H64a32 32 0 0 1 0-64h896a32 32 0 1 1 0 64h-64v544a32 32 0 0 1-32 32H665.216l110.848 192h-73.856L591.36 768H433.024L322.176 960H248.32zM832 192H192v512h640zM342.656 534.656a32 32 0 1 1-45.312-45.312L444.992 341.76l125.44 94.08L679.04 300.032a32 32 0 1 1 49.92 39.936L581.632 524.224 451.008 426.24 342.656 534.592z"})]))}}),RJ=$J,NJ=q({name:"DeleteFilled",__name:"delete-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M352 192V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64H96a32 32 0 0 1 0-64zm64 0h192v-64H416zM192 960a32 32 0 0 1-32-32V256h704v672a32 32 0 0 1-32 32zm224-192a32 32 0 0 0 32-32V416a32 32 0 0 0-64 0v320a32 32 0 0 0 32 32m192 0a32 32 0 0 0 32-32V416a32 32 0 0 0-64 0v320a32 32 0 0 0 32 32"})]))}}),BJ=NJ,VJ=q({name:"DeleteLocation",__name:"delete-location",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),C("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),C("path",{fill:"currentColor",d:"M384 384h256q32 0 32 32t-32 32H384q-32 0-32-32t32-32"})]))}}),zJ=VJ,FJ=q({name:"Delete",__name:"delete",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32zm448-64v-64H416v64zM224 896h576V256H224zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32m192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32"})]))}}),W6=FJ,HJ=q({name:"Dessert",__name:"dessert",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 416v-48a144 144 0 0 1 168.64-141.888 224.128 224.128 0 0 1 430.72 0A144 144 0 0 1 896 368v48a384 384 0 0 1-352 382.72V896h-64v-97.28A384 384 0 0 1 128 416m287.104-32.064h193.792a143.81 143.81 0 0 1 58.88-132.736 160.064 160.064 0 0 0-311.552 0 143.81 143.81 0 0 1 58.88 132.8zm-72.896 0a72 72 0 1 0-140.48 0zm339.584 0h140.416a72 72 0 1 0-140.48 0zM512 736a320 320 0 0 0 318.4-288.064H193.6A320 320 0 0 0 512 736M384 896.064h256a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64"})]))}}),WJ=HJ,KJ=q({name:"Discount",__name:"discount",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 704h576V318.336L552.512 115.84a64 64 0 0 0-81.024 0L224 318.336zm0 64v128h576V768zM593.024 66.304l259.2 212.096A32 32 0 0 1 864 303.168V928a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V303.168a32 32 0 0 1 11.712-24.768l259.2-212.096a128 128 0 0 1 162.112 0"}),C("path",{fill:"currentColor",d:"M512 448a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"})]))}}),UJ=KJ,GJ=q({name:"DishDot",__name:"dish-dot",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m384.064 274.56.064-50.688A128 128 0 0 1 512.128 96c70.528 0 127.68 57.152 127.68 127.68v50.752A448.19 448.19 0 0 1 955.392 768H68.544A448.19 448.19 0 0 1 384 274.56zM96 832h832a32 32 0 1 1 0 64H96a32 32 0 1 1 0-64m32-128h768a384 384 0 1 0-768 0m447.808-448v-32.32a63.68 63.68 0 0 0-63.68-63.68 64 64 0 0 0-64 63.936V256z"})]))}}),YJ=GJ,jJ=q({name:"Dish",__name:"dish",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 257.152V192h-96a32 32 0 0 1 0-64h256a32 32 0 1 1 0 64h-96v65.152A448 448 0 0 1 955.52 768H68.48A448 448 0 0 1 480 257.152M128 704h768a384 384 0 1 0-768 0M96 832h832a32 32 0 1 1 0 64H96a32 32 0 1 1 0-64"})]))}}),qJ=jJ,XJ=q({name:"DocumentAdd",__name:"document-add",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M832 384H576V128H192v768h640zm-26.496-64L640 154.496V320zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m320 512V448h64v128h128v64H544v128h-64V640H352v-64z"})]))}}),ZJ=XJ,JJ=q({name:"DocumentChecked",__name:"document-checked",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m318.4 582.144 180.992-180.992L704.64 510.4 478.4 736.64 320 578.304l45.248-45.312z"})]))}}),QJ=JJ,eQ=q({name:"DocumentCopy",__name:"document-copy",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 320v576h576V320zm-32-64h640a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32M960 96v704a32 32 0 0 1-32 32h-96v-64h64V128H384v64h-64V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32M256 672h320v64H256zm0-192h320v64H256z"})]))}}),tQ=eQ,nQ=q({name:"DocumentDelete",__name:"document-delete",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m308.992 546.304-90.496-90.624 45.248-45.248 90.56 90.496 90.496-90.432 45.248 45.248-90.496 90.56 90.496 90.496-45.248 45.248-90.496-90.496-90.56 90.496-45.248-45.248z"})]))}}),aQ=nQ,rQ=q({name:"DocumentRemove",__name:"document-remove",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m192 512h320v64H352z"})]))}}),oQ=rQ,iQ=q({name:"Document",__name:"document",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M832 384H576V128H192v768h640zm-26.496-64L640 154.496V320zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m160 448h384v64H320zm0-192h160v64H320zm0 384h384v64H320z"})]))}}),EI=iQ,lQ=q({name:"Download",__name:"download",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64z"})]))}}),AI=lQ,sQ=q({name:"Drizzling",__name:"drizzling",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m739.328 291.328-35.2-6.592-12.8-33.408a192.064 192.064 0 0 0-365.952 23.232l-9.92 40.896-41.472 7.04a176.32 176.32 0 0 0-146.24 173.568c0 97.28 78.72 175.936 175.808 175.936h400a192 192 0 0 0 35.776-380.672M959.552 480a256 256 0 0 1-256 256h-400A239.81 239.81 0 0 1 63.744 496.192a240.32 240.32 0 0 1 199.488-236.8 256.128 256.128 0 0 1 487.872-30.976A256.064 256.064 0 0 1 959.552 480M288 800h64v64h-64zm192 0h64v64h-64zm-96 96h64v64h-64zm192 0h64v64h-64zm96-96h64v64h-64z"})]))}}),uQ=sQ,cQ=q({name:"EditPen",__name:"edit-pen",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m199.04 672.64 193.984 112 224-387.968-193.92-112-224 388.032zm-23.872 60.16 32.896 148.288 144.896-45.696zM455.04 229.248l193.92 112 56.704-98.112-193.984-112zM104.32 708.8l384-665.024 304.768 175.936L409.152 884.8h.064l-248.448 78.336zm384 254.272v-64h448v64z"})]))}}),dQ=cQ,fQ=q({name:"Edit",__name:"edit",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M832 512a32 32 0 1 1 64 0v352a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h352a32 32 0 0 1 0 64H192v640h640z"}),C("path",{fill:"currentColor",d:"m469.952 554.24 52.8-7.552L847.104 222.4a32 32 0 1 0-45.248-45.248L477.44 501.44l-7.552 52.8zm422.4-422.4a96 96 0 0 1 0 135.808l-331.84 331.84a32 32 0 0 1-18.112 9.088L436.8 623.68a32 32 0 0 1-36.224-36.224l15.104-105.6a32 32 0 0 1 9.024-18.112l331.904-331.84a96 96 0 0 1 135.744 0z"})]))}}),vQ=fQ,hQ=q({name:"ElemeFilled",__name:"eleme-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M176 64h672c61.824 0 112 50.176 112 112v672a112 112 0 0 1-112 112H176A112 112 0 0 1 64 848V176c0-61.824 50.176-112 112-112m150.528 173.568c-152.896 99.968-196.544 304.064-97.408 456.96a330.69 330.69 0 0 0 456.96 96.64c9.216-5.888 17.6-11.776 25.152-18.56a18.24 18.24 0 0 0 4.224-24.32L700.352 724.8a47.55 47.55 0 0 0-65.536-14.272A234.56 234.56 0 0 1 310.592 641.6C240 533.248 271.104 387.968 379.456 316.48a234.3 234.3 0 0 1 276.352 15.168c1.664.832 2.56 2.56 3.392 4.224 5.888 8.384 3.328 19.328-5.12 25.216L456.832 489.6a47.55 47.55 0 0 0-14.336 65.472l16 24.384c5.888 8.384 16.768 10.88 25.216 5.056l308.224-199.936a19.584 19.584 0 0 0 6.72-23.488v-.896c-4.992-9.216-10.048-17.6-15.104-26.88-99.968-151.168-304.064-194.88-456.96-95.744zM786.88 504.704l-62.208 40.32c-8.32 5.888-10.88 16.768-4.992 25.216L760 632.32c5.888 8.448 16.768 11.008 25.152 5.12l31.104-20.16a55.36 55.36 0 0 0 16-76.48l-20.224-31.04a19.52 19.52 0 0 0-25.152-5.12z"})]))}}),pQ=hQ,mQ=q({name:"Eleme",__name:"eleme",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M300.032 188.8c174.72-113.28 408-63.36 522.24 109.44 5.76 10.56 11.52 20.16 17.28 30.72v.96a22.4 22.4 0 0 1-7.68 26.88l-352.32 228.48c-9.6 6.72-22.08 3.84-28.8-5.76l-18.24-27.84a54.336 54.336 0 0 1 16.32-74.88l225.6-146.88c9.6-6.72 12.48-19.2 5.76-28.8-.96-1.92-1.92-3.84-3.84-4.8a267.84 267.84 0 0 0-315.84-17.28c-123.84 81.6-159.36 247.68-78.72 371.52a268.096 268.096 0 0 0 370.56 78.72 54.336 54.336 0 0 1 74.88 16.32l17.28 26.88c5.76 9.6 3.84 21.12-4.8 27.84-8.64 7.68-18.24 14.4-28.8 21.12a377.92 377.92 0 0 1-522.24-110.4c-113.28-174.72-63.36-408 111.36-522.24m526.08 305.28a22.336 22.336 0 0 1 28.8 5.76l23.04 35.52a63.23 63.23 0 0 1-18.24 87.36l-35.52 23.04c-9.6 6.72-22.08 3.84-28.8-5.76l-46.08-71.04c-6.72-9.6-3.84-22.08 5.76-28.8z"})]))}}),gQ=mQ,yQ=q({name:"ElementPlus",__name:"element-plus",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M839.7 734.7c0 33.3-17.9 41-17.9 41S519.7 949.8 499.2 960c-10.2 5.1-20.5 5.1-30.7 0 0 0-314.9-184.3-325.1-192-5.1-5.1-10.2-12.8-12.8-20.5V368.6c0-17.9 20.5-28.2 20.5-28.2L466 158.6q19.2-7.65 38.4 0s279 161.3 309.8 179.2c17.9 7.7 28.2 25.6 25.6 46.1-.1-5-.1 317.5-.1 350.8M714.2 371.2c-64-35.8-217.6-125.4-217.6-125.4-7.7-5.1-20.5-5.1-30.7 0L217.6 389.1s-17.9 10.2-17.9 23v297c0 5.1 5.1 12.8 7.7 17.9 7.7 5.1 256 148.5 256 148.5 7.7 5.1 17.9 5.1 25.6 0 15.4-7.7 250.9-145.9 250.9-145.9s12.8-5.1 12.8-30.7v-74.2l-276.5 169v-64c0-17.9 7.7-30.7 20.5-46.1L745 535c5.1-7.7 10.2-20.5 10.2-30.7v-66.6l-279 169v-69.1c0-15.4 5.1-30.7 17.9-38.4zM919 135.7c0-5.1-5.1-7.7-7.7-7.7h-58.9V66.6c0-5.1-5.1-5.1-10.2-5.1l-30.7 5.1c-5.1 0-5.1 2.6-5.1 5.1V128h-56.3c-5.1 0-5.1 5.1-7.7 5.1v38.4h69.1v64c0 5.1 5.1 5.1 10.2 5.1l30.7-5.1c5.1 0 5.1-2.6 5.1-5.1v-56.3h64z"})]))}}),bQ=yQ,_Q=q({name:"Expand",__name:"expand",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 192h768v128H128zm0 256h512v128H128zm0 256h768v128H128zm576-352 192 160-192 128z"})]))}}),wQ=_Q,SQ=q({name:"Failed",__name:"failed",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m557.248 608 135.744-135.744-45.248-45.248-135.68 135.744-135.808-135.68-45.248 45.184L466.752 608l-135.68 135.68 45.184 45.312L512 653.248l135.744 135.744 45.248-45.248L557.312 608zM704 192h160v736H160V192h160v64h384zm-320 0V96h256v96z"})]))}}),CQ=SQ,xQ=q({name:"Female",__name:"female",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 640a256 256 0 1 0 0-512 256 256 0 0 0 0 512m0 64a320 320 0 1 1 0-640 320 320 0 0 1 0 640"}),C("path",{fill:"currentColor",d:"M512 640q32 0 32 32v256q0 32-32 32t-32-32V672q0-32 32-32"}),C("path",{fill:"currentColor",d:"M352 800h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32"})]))}}),kQ=xQ,TQ=q({name:"Files",__name:"files",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 384v448h768V384zm-32-64h832a32 32 0 0 1 32 32v512a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V352a32 32 0 0 1 32-32m64-128h704v64H160zm96-128h512v64H256z"})]))}}),MQ=TQ,EQ=q({name:"Film",__name:"film",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 160v704h704V160zm-32-64h768a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M320 288V128h64v352h256V128h64v160h160v64H704v128h160v64H704v128h160v64H704v160h-64V544H384v352h-64V736H128v-64h192V544H128v-64h192V352H128v-64z"})]))}}),AQ=EQ,IQ=q({name:"Filter",__name:"filter",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M384 523.392V928a32 32 0 0 0 46.336 28.608l192-96A32 32 0 0 0 640 832V523.392l280.768-343.104a32 32 0 1 0-49.536-40.576l-288 352A32 32 0 0 0 576 512v300.224l-128 64V512a32 32 0 0 0-7.232-20.288L195.52 192H704a32 32 0 1 0 0-64H128a32 32 0 0 0-24.768 52.288z"})]))}}),LQ=IQ,PQ=q({name:"Finished",__name:"finished",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M280.768 753.728 691.456 167.04a32 32 0 1 1 52.416 36.672L314.24 817.472a32 32 0 0 1-45.44 7.296l-230.4-172.8a32 32 0 0 1 38.4-51.2zM736 448a32 32 0 1 1 0-64h192a32 32 0 1 1 0 64zM608 640a32 32 0 0 1 0-64h319.936a32 32 0 1 1 0 64zM480 832a32 32 0 1 1 0-64h447.936a32 32 0 1 1 0 64z"})]))}}),DQ=PQ,OQ=q({name:"FirstAidKit",__name:"first-aid-kit",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 256a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V320a64 64 0 0 0-64-64zm0-64h640a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H192A128 128 0 0 1 64 768V320a128 128 0 0 1 128-128"}),C("path",{fill:"currentColor",d:"M544 512h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96v-96a32 32 0 0 1 64 0zM352 128v64h320v-64zm-32-64h384a32 32 0 0 1 32 32v128a32 32 0 0 1-32 32H320a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"})]))}}),$Q=OQ,RQ=q({name:"Flag",__name:"flag",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 128h608L736 384l160 256H288v320h-96V64h96z"})]))}}),NQ=RQ,BQ=q({name:"Fold",__name:"fold",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M896 192H128v128h768zm0 256H384v128h512zm0 256H128v128h768zM320 384 128 512l192 128z"})]))}}),VQ=BQ,zQ=q({name:"FolderAdd",__name:"folder-add",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m384 416V416h64v128h128v64H544v128h-64V608H352v-64z"})]))}}),FQ=zQ,HQ=q({name:"FolderChecked",__name:"folder-checked",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m414.08 502.144 180.992-180.992L736.32 494.4 510.08 720.64l-158.4-158.336 45.248-45.312z"})]))}}),WQ=HQ,KQ=q({name:"FolderDelete",__name:"folder-delete",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m370.752 448-90.496-90.496 45.248-45.248L512 530.752l90.496-90.496 45.248 45.248L557.248 576l90.496 90.496-45.248 45.248L512 621.248l-90.496 90.496-45.248-45.248z"})]))}}),UQ=KQ,GQ=q({name:"FolderOpened",__name:"folder-opened",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M878.08 448H241.92l-96 384h636.16zM832 384v-64H485.76L357.504 192H128v448l57.92-231.744A32 32 0 0 1 216.96 384zm-24.96 512H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h287.872l128.384 128H864a32 32 0 0 1 32 32v96h23.04a32 32 0 0 1 31.04 39.744l-112 448A32 32 0 0 1 807.04 896"})]))}}),YQ=GQ,jQ=q({name:"FolderRemove",__name:"folder-remove",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32m256 416h320v64H352z"})]))}}),qQ=jQ,XQ=q({name:"Folder",__name:"folder",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32"})]))}}),ZQ=XQ,JQ=q({name:"Food",__name:"food",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 352.576V352a288 288 0 0 1 491.072-204.224 192 192 0 0 1 274.24 204.48 64 64 0 0 1 57.216 74.24C921.6 600.512 850.048 710.656 736 756.992V800a96 96 0 0 1-96 96H384a96 96 0 0 1-96-96v-43.008c-114.048-46.336-185.6-156.48-214.528-330.496A64 64 0 0 1 128 352.64zm64-.576h64a160 160 0 0 1 320 0h64a224 224 0 0 0-448 0m128 0h192a96 96 0 0 0-192 0m439.424 0h68.544A128.256 128.256 0 0 0 704 192c-15.36 0-29.952 2.688-43.52 7.616 11.328 18.176 20.672 37.76 27.84 58.304A64.128 64.128 0 0 1 759.424 352M672 768H352v32a32 32 0 0 0 32 32h256a32 32 0 0 0 32-32zm-342.528-64h365.056c101.504-32.64 165.76-124.928 192.896-288H136.576c27.136 163.072 91.392 255.36 192.896 288"})]))}}),QQ=JQ,eee=q({name:"Football",__name:"football",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 960a448 448 0 1 1 0-896 448 448 0 0 1 0 896m0-64a384 384 0 1 0 0-768 384 384 0 0 0 0 768"}),C("path",{fill:"currentColor",d:"M186.816 268.288c16-16.384 31.616-31.744 46.976-46.08 17.472 30.656 39.808 58.112 65.984 81.28l-32.512 56.448a386 386 0 0 1-80.448-91.648m653.696-5.312a385.9 385.9 0 0 1-83.776 96.96l-32.512-56.384a322.9 322.9 0 0 0 68.48-85.76c15.552 14.08 31.488 29.12 47.808 45.184M465.984 445.248l11.136-63.104a323.6 323.6 0 0 0 69.76 0l11.136 63.104a388 388 0 0 1-92.032 0m-62.72-12.8A381.8 381.8 0 0 1 320 396.544l32-55.424a320 320 0 0 0 62.464 27.712l-11.2 63.488zm300.8-35.84a381.8 381.8 0 0 1-83.328 35.84l-11.2-63.552A320 320 0 0 0 672 341.184l32 55.424zm-520.768 364.8a385.9 385.9 0 0 1 83.968-97.28l32.512 56.32c-26.88 23.936-49.856 52.352-67.52 84.032-16-13.44-32.32-27.712-48.96-43.072m657.536.128a1443 1443 0 0 1-49.024 43.072 321.4 321.4 0 0 0-67.584-84.16l32.512-56.32c33.216 27.456 61.696 60.352 84.096 97.408M465.92 578.752a388 388 0 0 1 92.032 0l-11.136 63.104a323.6 323.6 0 0 0-69.76 0zm-62.72 12.8 11.2 63.552a320 320 0 0 0-62.464 27.712L320 627.392a381.8 381.8 0 0 1 83.264-35.84zm300.8 35.84-32 55.424a318.3 318.3 0 0 0-62.528-27.712l11.2-63.488c29.44 8.64 57.28 20.736 83.264 35.776z"})]))}}),tee=eee,nee=q({name:"ForkSpoon",__name:"fork-spoon",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 410.304V96a32 32 0 0 1 64 0v314.304a96 96 0 0 0 64-90.56V96a32 32 0 0 1 64 0v223.744a160 160 0 0 1-128 156.8V928a32 32 0 1 1-64 0V476.544a160 160 0 0 1-128-156.8V96a32 32 0 0 1 64 0v223.744a96 96 0 0 0 64 90.56M672 572.48C581.184 552.128 512 446.848 512 320c0-141.44 85.952-256 192-256s192 114.56 192 256c0 126.848-69.184 232.128-160 252.48V928a32 32 0 1 1-64 0zM704 512c66.048 0 128-82.56 128-192s-61.952-192-128-192-128 82.56-128 192 61.952 192 128 192"})]))}}),aee=nee,ree=q({name:"Fries",__name:"fries",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M608 224v-64a32 32 0 0 0-64 0v336h26.88A64 64 0 0 0 608 484.096zm101.12 160A64 64 0 0 0 672 395.904V384h64V224a32 32 0 1 0-64 0v160zm74.88 0a92.928 92.928 0 0 1 91.328 110.08l-60.672 323.584A96 96 0 0 1 720.32 896H303.68a96 96 0 0 1-94.336-78.336L148.672 494.08A92.928 92.928 0 0 1 240 384h-16V224a96 96 0 0 1 188.608-25.28A95.74 95.74 0 0 1 480 197.44V160a96 96 0 0 1 188.608-25.28A96 96 0 0 1 800 224v160zM670.784 512a128 128 0 0 1-99.904 48H453.12a128 128 0 0 1-99.84-48H352v-1.536a128 128 0 0 1-9.984-14.976L314.88 448H240a28.928 28.928 0 0 0-28.48 34.304L241.088 640h541.824l29.568-157.696A28.928 28.928 0 0 0 784 448h-74.88l-27.136 47.488A132 132 0 0 1 672 510.464V512zM480 288a32 32 0 0 0-64 0v196.096A64 64 0 0 0 453.12 496H480zm-128 96V224a32 32 0 0 0-64 0v160zh-37.12A64 64 0 0 1 352 395.904zm-98.88 320 19.072 101.888A32 32 0 0 0 303.68 832h416.64a32 32 0 0 0 31.488-26.112L770.88 704z"})]))}}),oee=ree,iee=q({name:"FullScreen",__name:"full-screen",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m160 96.064 192 .192a32 32 0 0 1 0 64l-192-.192V352a32 32 0 0 1-64 0V96h64zm0 831.872V928H96V672a32 32 0 1 1 64 0v191.936l192-.192a32 32 0 1 1 0 64zM864 96.064V96h64v256a32 32 0 1 1-64 0V160.064l-192 .192a32 32 0 1 1 0-64zm0 831.872-192-.192a32 32 0 0 1 0-64l192 .192V672a32 32 0 1 1 64 0v256h-64z"})]))}}),II=iee,lee=q({name:"GobletFull",__name:"goblet-full",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 320h512c0-78.592-12.608-142.4-36.928-192h-434.24C269.504 192.384 256 256.256 256 320m503.936 64H264.064a256.128 256.128 0 0 0 495.872 0M544 638.4V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96V638.4A320 320 0 0 1 192 320c0-85.632 21.312-170.944 64-256h512c42.688 64.32 64 149.632 64 256a320 320 0 0 1-288 318.4"})]))}}),see=lee,uee=q({name:"GobletSquareFull",__name:"goblet-square-full",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 270.912c10.048 6.72 22.464 14.912 28.992 18.624a220.16 220.16 0 0 0 114.752 30.72c30.592 0 49.408-9.472 91.072-41.152l.64-.448c52.928-40.32 82.368-55.04 132.288-54.656 55.552.448 99.584 20.8 142.72 57.408l1.536 1.28V128H256zm.96 76.288C266.368 482.176 346.88 575.872 512 576c157.44.064 237.952-85.056 253.248-209.984a952 952 0 0 1-40.192-35.712c-32.704-27.776-63.36-41.92-101.888-42.24-31.552-.256-50.624 9.28-93.12 41.6l-.576.448c-52.096 39.616-81.024 54.208-129.792 54.208-54.784 0-100.48-13.376-142.784-37.056zM480 638.848C250.624 623.424 192 442.496 192 319.68V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v224c0 122.816-58.624 303.68-288 318.912V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96z"})]))}}),cee=uee,dee=q({name:"GobletSquare",__name:"goblet-square",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 638.912V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96V638.848C250.624 623.424 192 442.496 192 319.68V96a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v224c0 122.816-58.624 303.68-288 318.912M256 319.68c0 149.568 80 256.192 256 256.256C688.128 576 768 469.568 768 320V128H256z"})]))}}),fee=dee,vee=q({name:"Goblet",__name:"goblet",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 638.4V896h96a32 32 0 1 1 0 64H384a32 32 0 1 1 0-64h96V638.4A320 320 0 0 1 192 320c0-85.632 21.312-170.944 64-256h512c42.688 64.32 64 149.632 64 256a320 320 0 0 1-288 318.4M256 320a256 256 0 1 0 512 0c0-78.592-12.608-142.4-36.928-192h-434.24C269.504 192.384 256 256.256 256 320"})]))}}),hee=vee,pee=q({name:"GoldMedal",__name:"gold-medal",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m772.13 452.84 53.86-351.81c1.32-10.01-1.17-18.68-7.49-26.02S804.35 64 795.01 64H228.99v-.01h-.06c-9.33 0-17.15 3.67-23.49 11.01s-8.83 16.01-7.49 26.02l53.87 351.89C213.54 505.73 193.59 568.09 192 640c2 90.67 33.17 166.17 93.5 226.5S421.33 957.99 512 960c90.67-2 166.17-33.17 226.5-93.5 60.33-60.34 91.49-135.83 93.5-226.5-1.59-71.94-21.56-134.32-59.87-187.16M640.01 128h117.02l-39.01 254.02c-20.75-10.64-40.74-19.73-59.94-27.28-5.92-3-11.95-5.8-18.08-8.41V128zM576 128v198.76c-13.18-2.58-26.74-4.43-40.67-5.55-8.07-.8-15.85-1.2-23.33-1.2-10.54 0-21.09.66-31.64 1.96a360 360 0 0 0-32.36 4.79V128zm-192 0h.04v218.3c-6.22 2.66-12.34 5.5-18.36 8.56-19.13 7.54-39.02 16.6-59.66 27.16L267.01 128zm308.99 692.99c-48 48-108.33 73-180.99 75.01-72.66-2.01-132.99-27.01-180.99-75.01S258.01 712.66 256 640c2.01-72.66 27.01-132.99 75.01-180.99 19.67-19.67 41.41-35.47 65.22-47.41 38.33-15.04 71.15-23.92 98.44-26.65 5.07-.41 10.2-.7 15.39-.88.63-.01 1.28-.03 1.91-.03.66 0 1.35.03 2.02.04 5.11.17 10.15.46 15.13.86 27.4 2.71 60.37 11.65 98.91 26.79 23.71 11.93 45.36 27.69 64.96 47.29 48 48 73 108.33 75.01 180.99-2.01 72.65-27.01 132.98-75.01 180.98"}),C("path",{fill:"currentColor",d:"M544 480H416v64h64v192h-64v64h192v-64h-64z"})]))}}),mee=pee,gee=q({name:"GoodsFilled",__name:"goods-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 352h640l64 544H128zm128 224h64V448h-64zm320 0h64V448h-64zM384 288h-64a192 192 0 1 1 384 0h-64a128 128 0 1 0-256 0"})]))}}),yee=gee,bee=q({name:"Goods",__name:"goods",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M320 288v-22.336C320 154.688 405.504 64 512 64s192 90.688 192 201.664v22.4h131.072a32 32 0 0 1 31.808 28.8l57.6 576a32 32 0 0 1-31.808 35.2H131.328a32 32 0 0 1-31.808-35.2l57.6-576a32 32 0 0 1 31.808-28.8H320zm64 0h256v-22.336C640 189.248 582.272 128 512 128s-128 61.248-128 137.664v22.4zm-64 64H217.92l-51.2 512h690.56l-51.264-512H704v96a32 32 0 1 1-64 0v-96H384v96a32 32 0 0 1-64 0z"})]))}}),_ee=bee,wee=q({name:"Grape",__name:"grape",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 195.2a160 160 0 0 1 96 60.8 160 160 0 1 1 146.24 254.976 160 160 0 0 1-128 224 160 160 0 1 1-292.48 0 160 160 0 0 1-128-224A160 160 0 1 1 384 256a160 160 0 0 1 96-60.8V128h-64a32 32 0 0 1 0-64h192a32 32 0 0 1 0 64h-64zM512 448a96 96 0 1 0 0-192 96 96 0 0 0 0 192m-256 0a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128 224a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128 224a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128-224a96 96 0 1 0 0-192 96 96 0 0 0 0 192m128-224a96 96 0 1 0 0-192 96 96 0 0 0 0 192"})]))}}),See=wee,Cee=q({name:"Grid",__name:"grid",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M640 384v256H384V384zm64 0h192v256H704zm-64 512H384V704h256zm64 0V704h192v192zm-64-768v192H384V128zm64 0h192v192H704zM320 384v256H128V384zm0 512H128V704h192zm0-768v192H128V128z"})]))}}),xee=Cee,kee=q({name:"Guide",__name:"guide",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M640 608h-64V416h64zm0 160v160a32 32 0 0 1-32 32H416a32 32 0 0 1-32-32V768h64v128h128V768zM384 608V416h64v192zm256-352h-64V128H448v128h-64V96a32 32 0 0 1 32-32h192a32 32 0 0 1 32 32z"}),C("path",{fill:"currentColor",d:"m220.8 256-71.232 80 71.168 80H768V256zm-14.4-64H800a32 32 0 0 1 32 32v224a32 32 0 0 1-32 32H206.4a32 32 0 0 1-23.936-10.752l-99.584-112a32 32 0 0 1 0-42.496l99.584-112A32 32 0 0 1 206.4 192m678.784 496-71.104 80H266.816V608h547.2zm-56.768-144H234.88a32 32 0 0 0-32 32v224a32 32 0 0 0 32 32h593.6a32 32 0 0 0 23.936-10.752l99.584-112a32 32 0 0 0 0-42.496l-99.584-112A32 32 0 0 0 828.48 544z"})]))}}),Tee=kee,Mee=q({name:"Handbag",__name:"handbag",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M887.01 264.99c-6-5.99-13.67-8.99-23.01-8.99H704c-1.34-54.68-20.01-100.01-56-136s-81.32-54.66-136-56c-54.68 1.34-100.01 20.01-136 56s-54.66 81.32-56 136H160c-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.67-8.99 23.01v640c0 9.35 2.99 17.02 8.99 23.01S150.66 960 160 960h704c9.35 0 17.02-2.99 23.01-8.99S896 937.34 896 928V288c0-9.35-2.99-17.02-8.99-23.01M421.5 165.5c24.32-24.34 54.49-36.84 90.5-37.5 35.99.68 66.16 13.18 90.5 37.5s36.84 54.49 37.5 90.5H384c.68-35.99 13.18-66.16 37.5-90.5M832 896H192V320h128v128h64V320h256v128h64V320h128z"})]))}}),Eee=Mee,Aee=q({name:"Headset",__name:"headset",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M896 529.152V512a384 384 0 1 0-768 0v17.152A128 128 0 0 1 320 640v128a128 128 0 1 1-256 0V512a448 448 0 1 1 896 0v256a128 128 0 1 1-256 0V640a128 128 0 0 1 192-110.848M896 640a64 64 0 0 0-128 0v128a64 64 0 0 0 128 0zm-768 0v128a64 64 0 0 0 128 0V640a64 64 0 1 0-128 0"})]))}}),Iee=Aee,Lee=q({name:"HelpFilled",__name:"help-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M926.784 480H701.312A192.51 192.51 0 0 0 544 322.688V97.216A416.064 416.064 0 0 1 926.784 480m0 64A416.064 416.064 0 0 1 544 926.784V701.312A192.51 192.51 0 0 0 701.312 544zM97.28 544h225.472A192.51 192.51 0 0 0 480 701.312v225.472A416.064 416.064 0 0 1 97.216 544zm0-64A416.064 416.064 0 0 1 480 97.216v225.472A192.51 192.51 0 0 0 322.688 480H97.216z"})]))}}),Pee=Lee,Dee=q({name:"Help",__name:"help",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m759.936 805.248-90.944-91.008A254.9 254.9 0 0 1 512 768a254.9 254.9 0 0 1-156.992-53.76l-90.944 91.008A382.46 382.46 0 0 0 512 896c94.528 0 181.12-34.176 247.936-90.752m45.312-45.312A382.46 382.46 0 0 0 896 512c0-94.528-34.176-181.12-90.752-247.936l-91.008 90.944C747.904 398.4 768 452.864 768 512s-20.096 113.6-53.76 156.992zm-45.312-541.184A382.46 382.46 0 0 0 512 128c-94.528 0-181.12 34.176-247.936 90.752l90.944 91.008A254.9 254.9 0 0 1 512 256c59.136 0 113.6 20.096 156.992 53.76zm-541.184 45.312A382.46 382.46 0 0 0 128 512c0 94.528 34.176 181.12 90.752 247.936l91.008-90.944A254.9 254.9 0 0 1 256 512c0-59.136 20.096-113.6 53.76-156.992zm417.28 394.496a194.6 194.6 0 0 0 22.528-22.528C686.912 602.56 704 559.232 704 512a191.23 191.23 0 0 0-67.968-146.56A191.3 191.3 0 0 0 512 320a191.23 191.23 0 0 0-146.56 67.968C337.088 421.44 320 464.768 320 512a191.23 191.23 0 0 0 67.968 146.56C421.44 686.912 464.768 704 512 704c47.296 0 90.56-17.088 124.032-45.44M512 960a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),Oee=Dee,$ee=q({name:"Hide",__name:"hide",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M876.8 156.8c0-9.6-3.2-16-9.6-22.4s-12.8-9.6-22.4-9.6-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176S0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4m-646.4 528Q115.2 579.2 76.8 512q43.2-72 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4m140.8-96Q352 555.2 352 512c0-44.8 16-83.2 48-112s67.2-48 112-48c28.8 0 54.4 6.4 73.6 19.2zM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6q-43.2 72-153.6 172.8c-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176S1024 528 1024 512s-48.001-73.6-134.401-176"}),C("path",{fill:"currentColor",d:"M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112s-67.2 48-112 48"})]))}}),LI=$ee,Ree=q({name:"Histogram",__name:"histogram",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M416 896V128h192v768zm-288 0V448h192v448zm576 0V320h192v576z"})]))}}),Nee=Ree,Bee=q({name:"HomeFilled",__name:"home-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 128 128 447.936V896h255.936V640H640v256h255.936V447.936z"})]))}}),Vee=Bee,zee=q({name:"HotWater",__name:"hot-water",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M273.067 477.867h477.866V409.6H273.067zm0 68.266v51.2A187.733 187.733 0 0 0 460.8 785.067h102.4a187.733 187.733 0 0 0 187.733-187.734v-51.2zm-34.134-204.8h546.134a34.133 34.133 0 0 1 34.133 34.134v221.866a256 256 0 0 1-256 256H460.8a256 256 0 0 1-256-256V375.467a34.133 34.133 0 0 1 34.133-34.134M512 34.133a34.133 34.133 0 0 1 34.133 34.134v170.666a34.133 34.133 0 0 1-68.266 0V68.267A34.133 34.133 0 0 1 512 34.133M375.467 102.4a34.133 34.133 0 0 1 34.133 34.133v102.4a34.133 34.133 0 0 1-68.267 0v-102.4a34.133 34.133 0 0 1 34.134-34.133m273.066 0a34.133 34.133 0 0 1 34.134 34.133v102.4a34.133 34.133 0 1 1-68.267 0v-102.4a34.133 34.133 0 0 1 34.133-34.133M170.667 921.668h682.666a34.133 34.133 0 1 1 0 68.267H170.667a34.133 34.133 0 1 1 0-68.267"})]))}}),Fee=zee,Hee=q({name:"House",__name:"house",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 413.952V896h640V413.952L512 147.328zM139.52 374.4l352-293.312a32 32 0 0 1 40.96 0l352 293.312A32 32 0 0 1 896 398.976V928a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V398.976a32 32 0 0 1 11.52-24.576"})]))}}),Wee=Hee,Kee=q({name:"IceCreamRound",__name:"ice-cream-round",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m308.352 489.344 226.304 226.304a32 32 0 0 0 45.248 0L783.552 512A192 192 0 1 0 512 240.448L308.352 444.16a32 32 0 0 0 0 45.248zm135.744 226.304L308.352 851.392a96 96 0 0 1-135.744-135.744l135.744-135.744-45.248-45.248a96 96 0 0 1 0-135.808L466.752 195.2A256 256 0 0 1 828.8 557.248L625.152 760.96a96 96 0 0 1-135.808 0l-45.248-45.248zM398.848 670.4 353.6 625.152 217.856 760.896a32 32 0 0 0 45.248 45.248zm248.96-384.64a32 32 0 0 1 0 45.248L466.624 512a32 32 0 1 1-45.184-45.248l180.992-181.056a32 32 0 0 1 45.248 0zm90.496 90.496a32 32 0 0 1 0 45.248L557.248 602.496A32 32 0 1 1 512 557.248l180.992-180.992a32 32 0 0 1 45.312 0"})]))}}),Uee=Kee,Gee=q({name:"IceCreamSquare",__name:"ice-cream-square",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M416 640h256a32 32 0 0 0 32-32V160a32 32 0 0 0-32-32H352a32 32 0 0 0-32 32v448a32 32 0 0 0 32 32zm192 64v160a96 96 0 0 1-192 0V704h-64a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96h320a96 96 0 0 1 96 96v448a96 96 0 0 1-96 96zm-64 0h-64v160a32 32 0 1 0 64 0z"})]))}}),Yee=Gee,jee=q({name:"IceCream",__name:"ice-cream",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128.64 448a208 208 0 0 1 193.536-191.552 224 224 0 0 1 445.248 15.488A208.13 208.13 0 0 1 894.784 448H896L548.8 983.68a32 32 0 0 1-53.248.704L128 448zm64.256 0h286.208a144 144 0 0 0-286.208 0m351.36 0h286.272a144 144 0 0 0-286.272 0m-294.848 64 271.808 396.608L778.24 512zM511.68 352.64a207.87 207.87 0 0 1 189.184-96.192 160 160 0 0 0-314.752 5.632c52.608 12.992 97.28 46.08 125.568 90.56"})]))}}),qee=jee,Xee=q({name:"IceDrink",__name:"ice-drink",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 448v128h239.68l16.064-128zm-64 0H256.256l16.064 128H448zm64-255.36V384h247.744A256.13 256.13 0 0 0 512 192.64m-64 8.064A256.45 256.45 0 0 0 264.256 384H448zm64-72.064A320.13 320.13 0 0 1 825.472 384H896a32 32 0 1 1 0 64h-64v1.92l-56.96 454.016A64 64 0 0 1 711.552 960H312.448a64 64 0 0 1-63.488-56.064L192 449.92V448h-64a32 32 0 0 1 0-64h70.528A320.38 320.38 0 0 1 448 135.04V96a96 96 0 0 1 96-96h128a32 32 0 1 1 0 64H544a32 32 0 0 0-32 32zM743.68 640H280.32l32.128 256h399.104z"})]))}}),Zee=Xee,Jee=q({name:"IceTea",__name:"ice-tea",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M197.696 259.648a320.128 320.128 0 0 1 628.608 0A96 96 0 0 1 896 352v64a96 96 0 0 1-71.616 92.864l-49.408 395.072A64 64 0 0 1 711.488 960H312.512a64 64 0 0 1-63.488-56.064l-49.408-395.072A96 96 0 0 1 128 416v-64a96 96 0 0 1 69.696-92.352M264.064 256h495.872a256.128 256.128 0 0 0-495.872 0m495.424 256H264.512l48 384h398.976zM224 448h576a32 32 0 0 0 32-32v-64a32 32 0 0 0-32-32H224a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32m160 192h64v64h-64zm192 64h64v64h-64zm-128 64h64v64h-64zm64-192h64v64h-64z"})]))}}),Qee=Jee,ete=q({name:"InfoFilled",__name:"info-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896.064A448 448 0 0 1 512 64m67.2 275.072c33.28 0 60.288-23.104 60.288-57.344s-27.072-57.344-60.288-57.344c-33.28 0-60.16 23.104-60.16 57.344s26.88 57.344 60.16 57.344M590.912 699.2c0-6.848 2.368-24.64 1.024-34.752l-52.608 60.544c-10.88 11.456-24.512 19.392-30.912 17.28a12.99 12.99 0 0 1-8.256-14.72l87.68-276.992c7.168-35.136-12.544-67.2-54.336-71.296-44.096 0-108.992 44.736-148.48 101.504 0 6.784-1.28 23.68.064 33.792l52.544-60.608c10.88-11.328 23.552-19.328 29.952-17.152a12.8 12.8 0 0 1 7.808 16.128L388.48 728.576c-10.048 32.256 8.96 63.872 55.04 71.04 67.84 0 107.904-43.648 147.456-100.416z"})]))}}),Ud=ete,tte=q({name:"Iphone",__name:"iphone",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 768v96.064a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V768zm0-64h576V160a64 64 0 0 0-64-64H288a64 64 0 0 0-64 64zm32 288a96 96 0 0 1-96-96V128a96 96 0 0 1 96-96h512a96 96 0 0 1 96 96v768a96 96 0 0 1-96 96zm304-144a48 48 0 1 1-96 0 48 48 0 0 1 96 0"})]))}}),nte=tte,ate=q({name:"Key",__name:"key",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M448 456.064V96a32 32 0 0 1 32-32.064L672 64a32 32 0 0 1 0 64H512v128h160a32 32 0 0 1 0 64H512v128a256 256 0 1 1-64 8.064M512 896a192 192 0 1 0 0-384 192 192 0 0 0 0 384"})]))}}),PI=ate,rte=q({name:"KnifeFork",__name:"knife-fork",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 410.56V96a32 32 0 0 1 64 0v314.56A96 96 0 0 0 384 320V96a32 32 0 0 1 64 0v224a160 160 0 0 1-128 156.8V928a32 32 0 1 1-64 0V476.8A160 160 0 0 1 128 320V96a32 32 0 0 1 64 0v224a96 96 0 0 0 64 90.56m384-250.24V544h126.72c-3.328-78.72-12.928-147.968-28.608-207.744-14.336-54.528-46.848-113.344-98.112-175.872zM640 608v320a32 32 0 1 1-64 0V64h64c85.312 89.472 138.688 174.848 160 256s32 177.152 32 288z"})]))}}),ote=rte,ite=q({name:"Lightning",__name:"lightning",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 671.36v64.128A239.81 239.81 0 0 1 63.744 496.192a240.32 240.32 0 0 1 199.488-236.8 256.128 256.128 0 0 1 487.872-30.976A256.064 256.064 0 0 1 736 734.016v-64.768a192 192 0 0 0 3.328-377.92l-35.2-6.592-12.8-33.408a192.064 192.064 0 0 0-365.952 23.232l-9.92 40.896-41.472 7.04a176.32 176.32 0 0 0-146.24 173.568c0 91.968 70.464 167.36 160.256 175.232z"}),C("path",{fill:"currentColor",d:"M416 736a32 32 0 0 1-27.776-47.872l128-224a32 32 0 1 1 55.552 31.744L471.168 672H608a32 32 0 0 1 27.776 47.872l-128 224a32 32 0 1 1-55.68-31.744L552.96 736z"})]))}}),lte=ite,ste=q({name:"Link",__name:"link",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M715.648 625.152 670.4 579.904l90.496-90.56c75.008-74.944 85.12-186.368 22.656-248.896-62.528-62.464-173.952-52.352-248.96 22.656L444.16 353.6l-45.248-45.248 90.496-90.496c100.032-99.968 251.968-110.08 339.456-22.656 87.488 87.488 77.312 239.424-22.656 339.456l-90.496 90.496zm-90.496 90.496-90.496 90.496C434.624 906.112 282.688 916.224 195.2 828.8c-87.488-87.488-77.312-239.424 22.656-339.456l90.496-90.496 45.248 45.248-90.496 90.56c-75.008 74.944-85.12 186.368-22.656 248.896 62.528 62.464 173.952 52.352 248.96-22.656l90.496-90.496zm0-362.048 45.248 45.248L398.848 670.4 353.6 625.152z"})]))}}),DI=ste,ute=q({name:"List",__name:"list",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 192h160v736H160V192h160v64h384zM288 512h448v-64H288zm0 256h448v-64H288zm96-576V96h256v96z"})]))}}),cte=ute,dte=q({name:"Loading",__name:"loading",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248m452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248M828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0"})]))}}),Mr=dte,fte=q({name:"LocationFilled",__name:"location-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 928c23.936 0 117.504-68.352 192.064-153.152C803.456 661.888 864 535.808 864 416c0-189.632-155.84-320-352-320S160 226.368 160 416c0 120.32 60.544 246.4 159.936 359.232C394.432 859.84 488 928 512 928m0-435.2a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 140.8a204.8 204.8 0 1 1 0-409.6 204.8 204.8 0 0 1 0 409.6"})]))}}),vte=fte,hte=q({name:"LocationInformation",__name:"location-information",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"}),C("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),C("path",{fill:"currentColor",d:"M512 512a96 96 0 1 0 0-192 96 96 0 0 0 0 192m0 64a160 160 0 1 1 0-320 160 160 0 0 1 0 320"})]))}}),pte=hte,mte=q({name:"Location",__name:"location",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),C("path",{fill:"currentColor",d:"M512 512a96 96 0 1 0 0-192 96 96 0 0 0 0 192m0 64a160 160 0 1 1 0-320 160 160 0 0 1 0 320"})]))}}),gte=mte,yte=q({name:"Lock",__name:"lock",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 448a32 32 0 0 0-32 32v384a32 32 0 0 0 32 32h576a32 32 0 0 0 32-32V480a32 32 0 0 0-32-32zm0-64h576a96 96 0 0 1 96 96v384a96 96 0 0 1-96 96H224a96 96 0 0 1-96-96V480a96 96 0 0 1 96-96"}),C("path",{fill:"currentColor",d:"M512 544a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V576a32 32 0 0 1 32-32m192-160v-64a192 192 0 1 0-384 0v64zM512 64a256 256 0 0 1 256 256v128H256V320A256 256 0 0 1 512 64"})]))}}),bte=yte,_te=q({name:"Lollipop",__name:"lollipop",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M513.28 448a64 64 0 1 1 76.544 49.728A96 96 0 0 0 768 448h64a160 160 0 0 1-320 0zm-126.976-29.696a256 256 0 1 0 43.52-180.48A256 256 0 0 1 832 448h-64a192 192 0 0 0-381.696-29.696m105.664 249.472L285.696 874.048a96 96 0 0 1-135.68-135.744l206.208-206.272a320 320 0 1 1 135.744 135.744m-54.464-36.032a322 322 0 0 1-45.248-45.248L195.2 783.552a32 32 0 1 0 45.248 45.248l197.056-197.12z"})]))}}),wte=_te,Ste=q({name:"MagicStick",__name:"magic-stick",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64h64v192h-64zm0 576h64v192h-64zM160 480v-64h192v64zm576 0v-64h192v64zM249.856 199.04l45.248-45.184L430.848 289.6 385.6 334.848 249.856 199.104zM657.152 606.4l45.248-45.248 135.744 135.744-45.248 45.248zM114.048 923.2 68.8 877.952l316.8-316.8 45.248 45.248zM702.4 334.848 657.152 289.6l135.744-135.744 45.248 45.248z"})]))}}),Cte=Ste,xte=q({name:"Magnet",__name:"magnet",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M832 320V192H704v320a192 192 0 1 1-384 0V192H192v128h128v64H192v128a320 320 0 0 0 640 0V384H704v-64zM640 512V128h256v384a384 384 0 1 1-768 0V128h256v384a128 128 0 1 0 256 0"})]))}}),kte=xte,Tte=q({name:"Male",__name:"male",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M399.5 849.5a225 225 0 1 0 0-450 225 225 0 0 0 0 450m0 56.25a281.25 281.25 0 1 1 0-562.5 281.25 281.25 0 0 1 0 562.5m253.125-787.5h225q28.125 0 28.125 28.125T877.625 174.5h-225q-28.125 0-28.125-28.125t28.125-28.125"}),C("path",{fill:"currentColor",d:"M877.625 118.25q28.125 0 28.125 28.125v225q0 28.125-28.125 28.125T849.5 371.375v-225q0-28.125 28.125-28.125"}),C("path",{fill:"currentColor",d:"M604.813 458.9 565.1 419.131l292.613-292.668 39.825 39.824z"})]))}}),Mte=Tte,Ete=q({name:"Management",__name:"management",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M576 128v288l96-96 96 96V128h128v768H320V128zm-448 0h128v768H128z"})]))}}),Ate=Ete,Ite=q({name:"MapLocation",__name:"map-location",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416M512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544"}),C("path",{fill:"currentColor",d:"M512 448a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256m345.6 192L960 960H672v-64H352v64H64l102.4-256zm-68.928 0H235.328l-76.8 192h706.944z"})]))}}),Lte=Ite,Pte=q({name:"Medal",__name:"medal",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a256 256 0 1 0 0-512 256 256 0 0 0 0 512m0 64a320 320 0 1 1 0-640 320 320 0 0 1 0 640"}),C("path",{fill:"currentColor",d:"M576 128H448v200a286.7 286.7 0 0 1 64-8c19.52 0 40.832 2.688 64 8zm64 0v219.648c24.448 9.088 50.56 20.416 78.4 33.92L757.44 128zm-256 0H266.624l39.04 253.568c27.84-13.504 53.888-24.832 78.336-33.92zM229.312 64h565.376a32 32 0 0 1 31.616 36.864L768 480c-113.792-64-199.104-96-256-96s-142.208 32-256 96l-58.304-379.136A32 32 0 0 1 229.312 64"})]))}}),Dte=Pte,Ote=q({name:"Memo",__name:"memo",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 320h192c21.33 0 32-10.67 32-32s-10.67-32-32-32H480c-21.33 0-32 10.67-32 32s10.67 32 32 32"}),C("path",{fill:"currentColor",d:"M887.01 72.99C881.01 67 873.34 64 864 64H160c-9.35 0-17.02 3-23.01 8.99C131 78.99 128 86.66 128 96v832c0 9.35 2.99 17.02 8.99 23.01S150.66 960 160 960h704c9.35 0 17.02-2.99 23.01-8.99S896 937.34 896 928V96c0-9.35-3-17.02-8.99-23.01M192 896V128h96v768zm640 0H352V128h480z"}),C("path",{fill:"currentColor",d:"M480 512h192c21.33 0 32-10.67 32-32s-10.67-32-32-32H480c-21.33 0-32 10.67-32 32s10.67 32 32 32m0 192h192c21.33 0 32-10.67 32-32s-10.67-32-32-32H480c-21.33 0-32 10.67-32 32s10.67 32 32 32"})]))}}),$te=Ote,Rte=q({name:"Menu",__name:"menu",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 448a32 32 0 0 1-32-32V160.064a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V416a32 32 0 0 1-32 32zm448 0a32 32 0 0 1-32-32V160.064a32 32 0 0 1 32-32h255.936a32 32 0 0 1 32 32V416a32 32 0 0 1-32 32zM160 896a32 32 0 0 1-32-32V608a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32v256a32 32 0 0 1-32 32zm448 0a32 32 0 0 1-32-32V608a32 32 0 0 1 32-32h255.936a32 32 0 0 1 32 32v256a32 32 0 0 1-32 32z"})]))}}),Nte=Rte,Bte=q({name:"MessageBox",__name:"message-box",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 384h448v64H288zm96-128h256v64H384zM131.456 512H384v128h256V512h252.544L721.856 192H302.144zM896 576H704v128H320V576H128v256h768zM275.776 128h472.448a32 32 0 0 1 28.608 17.664l179.84 359.552A32 32 0 0 1 960 519.552V864a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V519.552a32 32 0 0 1 3.392-14.336l179.776-359.552A32 32 0 0 1 275.776 128"})]))}}),Vte=Bte,zte=q({name:"Message",__name:"message",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 224v512a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V224zm0-64h768a64 64 0 0 1 64 64v512a128 128 0 0 1-128 128H192A128 128 0 0 1 64 736V224a64 64 0 0 1 64-64"}),C("path",{fill:"currentColor",d:"M904 224 656.512 506.88a192 192 0 0 1-289.024 0L120 224zm-698.944 0 210.56 240.704a128 128 0 0 0 192.704 0L818.944 224z"})]))}}),Fte=zte,Hte=q({name:"Mic",__name:"mic",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 704h160a64 64 0 0 0 64-64v-32h-96a32 32 0 0 1 0-64h96v-96h-96a32 32 0 0 1 0-64h96v-96h-96a32 32 0 0 1 0-64h96v-32a64 64 0 0 0-64-64H384a64 64 0 0 0-64 64v32h96a32 32 0 0 1 0 64h-96v96h96a32 32 0 0 1 0 64h-96v96h96a32 32 0 0 1 0 64h-96v32a64 64 0 0 0 64 64zm64 64v128h192a32 32 0 1 1 0 64H288a32 32 0 1 1 0-64h192V768h-96a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64h256a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128z"})]))}}),Wte=Hte,Kte=q({name:"Microphone",__name:"microphone",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 128a128 128 0 0 0-128 128v256a128 128 0 1 0 256 0V256a128 128 0 0 0-128-128m0-64a192 192 0 0 1 192 192v256a192 192 0 1 1-384 0V256A192 192 0 0 1 512 64m-32 832v-64a288 288 0 0 1-288-288v-32a32 32 0 0 1 64 0v32a224 224 0 0 0 224 224h64a224 224 0 0 0 224-224v-32a32 32 0 1 1 64 0v32a288 288 0 0 1-288 288v64h64a32 32 0 1 1 0 64H416a32 32 0 1 1 0-64z"})]))}}),Ute=Kte,Gte=q({name:"MilkTea",__name:"milk-tea",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M416 128V96a96 96 0 0 1 96-96h128a32 32 0 1 1 0 64H512a32 32 0 0 0-32 32v32h320a96 96 0 0 1 11.712 191.296l-39.68 581.056A64 64 0 0 1 708.224 960H315.776a64 64 0 0 1-63.872-59.648l-39.616-581.056A96 96 0 0 1 224 128zM276.48 320l39.296 576h392.448l4.8-70.784a224.064 224.064 0 0 1 30.016-439.808L747.52 320zM224 256h576a32 32 0 1 0 0-64H224a32 32 0 0 0 0 64m493.44 503.872 21.12-309.12a160 160 0 0 0-21.12 309.12"})]))}}),Yte=Gte,jte=q({name:"Minus",__name:"minus",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 544h768a32 32 0 1 0 0-64H128a32 32 0 0 0 0 64"})]))}}),OI=jte,qte=q({name:"Money",__name:"money",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 640v192h640V384H768v-64h150.976c14.272 0 19.456 1.472 24.64 4.288a29.06 29.06 0 0 1 12.16 12.096c2.752 5.184 4.224 10.368 4.224 24.64v493.952c0 14.272-1.472 19.456-4.288 24.64a29.06 29.06 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H233.024c-14.272 0-19.456-1.472-24.64-4.288a29.06 29.06 0 0 1-12.16-12.096c-2.688-5.184-4.224-10.368-4.224-24.576V640z"}),C("path",{fill:"currentColor",d:"M768 192H128v448h640zm64-22.976v493.952c0 14.272-1.472 19.456-4.288 24.64a29.06 29.06 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H105.024c-14.272 0-19.456-1.472-24.64-4.288a29.06 29.06 0 0 1-12.16-12.096C65.536 682.432 64 677.248 64 663.04V169.024c0-14.272 1.472-19.456 4.288-24.64a29.06 29.06 0 0 1 12.096-12.16C85.568 129.536 90.752 128 104.96 128h685.952c14.272 0 19.456 1.472 24.64 4.288a29.06 29.06 0 0 1 12.16 12.096c2.752 5.184 4.224 10.368 4.224 24.64z"}),C("path",{fill:"currentColor",d:"M448 576a160 160 0 1 1 0-320 160 160 0 0 1 0 320m0-64a96 96 0 1 0 0-192 96 96 0 0 0 0 192"})]))}}),Xte=qte,Zte=q({name:"Monitor",__name:"monitor",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 768v128h192a32 32 0 1 1 0 64H288a32 32 0 1 1 0-64h192V768H192A128 128 0 0 1 64 640V256a128 128 0 0 1 128-128h640a128 128 0 0 1 128 128v384a128 128 0 0 1-128 128zM192 192a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64z"})]))}}),Jte=Zte,Qte=q({name:"MoonNight",__name:"moon-night",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M384 512a448 448 0 0 1 215.872-383.296A384 384 0 0 0 213.76 640h188.8A448.3 448.3 0 0 1 384 512M171.136 704a448 448 0 0 1 636.992-575.296A384 384 0 0 0 499.328 704h-328.32z"}),C("path",{fill:"currentColor",d:"M32 640h960q32 0 32 32t-32 32H32q-32 0-32-32t32-32m128 128h384a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m160 127.68 224 .256a32 32 0 0 1 32 32V928a32 32 0 0 1-32 32l-224-.384a32 32 0 0 1-32-32v-.064a32 32 0 0 1 32-32z"})]))}}),ene=Qte,tne=q({name:"Moon",__name:"moon",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M240.448 240.448a384 384 0 1 0 559.424 525.696 448 448 0 0 1-542.016-542.08 391 391 0 0 0-17.408 16.384m181.056 362.048a384 384 0 0 0 525.632 16.384A448 448 0 1 1 405.056 76.8a384 384 0 0 0 16.448 525.696"})]))}}),nne=tne,ane=q({name:"MoreFilled",__name:"more-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M176 416a112 112 0 1 1 0 224 112 112 0 0 1 0-224m336 0a112 112 0 1 1 0 224 112 112 0 0 1 0-224m336 0a112 112 0 1 1 0 224 112 112 0 0 1 0-224"})]))}}),m4=ane,rne=q({name:"More",__name:"more",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M176 416a112 112 0 1 0 0 224 112 112 0 0 0 0-224m0 64a48 48 0 1 1 0 96 48 48 0 0 1 0-96m336-64a112 112 0 1 1 0 224 112 112 0 0 1 0-224m0 64a48 48 0 1 0 0 96 48 48 0 0 0 0-96m336-64a112 112 0 1 1 0 224 112 112 0 0 1 0-224m0 64a48 48 0 1 0 0 96 48 48 0 0 0 0-96"})]))}}),$I=rne,one=q({name:"MostlyCloudy",__name:"mostly-cloudy",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M737.216 357.952 704 349.824l-11.776-32a192.064 192.064 0 0 0-367.424 23.04l-8.96 39.04-39.04 8.96A192.064 192.064 0 0 0 320 768h368a207.81 207.81 0 0 0 207.808-208 208.32 208.32 0 0 0-158.592-202.048m15.168-62.208A272.32 272.32 0 0 1 959.744 560a271.81 271.81 0 0 1-271.552 272H320a256 256 0 0 1-57.536-505.536 256.128 256.128 0 0 1 489.92-30.72"})]))}}),ine=one,lne=q({name:"Mouse",__name:"mouse",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M438.144 256c-68.352 0-92.736 4.672-117.76 18.112q-30.144 16.128-46.272 46.272C260.672 345.408 256 369.792 256 438.144v275.712c0 68.352 4.672 92.736 18.112 117.76q16.128 30.144 46.272 46.272C345.408 891.328 369.792 896 438.144 896h147.712c68.352 0 92.736-4.672 117.76-18.112q30.144-16.128 46.272-46.272C763.328 806.592 768 782.208 768 713.856V438.144c0-68.352-4.672-92.736-18.112-117.76a110.46 110.46 0 0 0-46.272-46.272C678.592 260.672 654.208 256 585.856 256zm0-64h147.712c85.568 0 116.608 8.96 147.904 25.6 31.36 16.768 55.872 41.344 72.576 72.64C823.104 321.536 832 352.576 832 438.08v275.84c0 85.504-8.96 116.544-25.6 147.84a174.46 174.46 0 0 1-72.64 72.576C702.464 951.104 671.424 960 585.92 960H438.08c-85.504 0-116.544-8.96-147.84-25.6a174.46 174.46 0 0 1-72.64-72.704c-16.768-31.296-25.664-62.336-25.664-147.84v-275.84c0-85.504 8.96-116.544 25.6-147.84a174.46 174.46 0 0 1 72.768-72.576c31.232-16.704 62.272-25.6 147.776-25.6z"}),C("path",{fill:"currentColor",d:"M512 320q32 0 32 32v128q0 32-32 32t-32-32V352q0-32 32-32m32-96a32 32 0 0 1-64 0v-64a32 32 0 0 0-32-32h-96a32 32 0 0 1 0-64h96a96 96 0 0 1 96 96z"})]))}}),sne=lne,une=q({name:"Mug",__name:"mug",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M736 800V160H160v640a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64m64-544h63.552a96 96 0 0 1 96 96v224a96 96 0 0 1-96 96H800v128a128 128 0 0 1-128 128H224A128 128 0 0 1 96 800V128a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zm0 64v288h63.552a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32z"})]))}}),cne=une,dne=q({name:"MuteNotification",__name:"mute-notification",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m241.216 832 63.616-64H768V448c0-42.368-10.24-82.304-28.48-117.504l46.912-47.232C815.36 331.392 832 387.84 832 448v320h96a32 32 0 1 1 0 64zm-90.24 0H96a32 32 0 1 1 0-64h96V448a320.13 320.13 0 0 1 256-313.6V128a64 64 0 1 1 128 0v6.4a319.55 319.55 0 0 1 171.648 97.088l-45.184 45.44A256 256 0 0 0 256 448v278.336L151.04 832zM448 896h128a64 64 0 0 1-128 0"}),C("path",{fill:"currentColor",d:"M150.72 859.072a32 32 0 0 1-45.44-45.056l704-708.544a32 32 0 0 1 45.44 45.056z"})]))}}),fne=dne,vne=q({name:"Mute",__name:"mute",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m412.16 592.128-45.44 45.44A191.23 191.23 0 0 1 320 512V256a192 192 0 1 1 384 0v44.352l-64 64V256a128 128 0 1 0-256 0v256c0 30.336 10.56 58.24 28.16 80.128m51.968 38.592A128 128 0 0 0 640 512v-57.152l64-64V512a192 192 0 0 1-287.68 166.528zM314.88 779.968l46.144-46.08A223 223 0 0 0 480 768h64a224 224 0 0 0 224-224v-32a32 32 0 1 1 64 0v32a288 288 0 0 1-288 288v64h64a32 32 0 1 1 0 64H416a32 32 0 1 1 0-64h64v-64c-61.44 0-118.4-19.2-165.12-52.032M266.752 737.6A286.98 286.98 0 0 1 192 544v-32a32 32 0 0 1 64 0v32c0 56.832 21.184 108.8 56.064 148.288z"}),C("path",{fill:"currentColor",d:"M150.72 859.072a32 32 0 0 1-45.44-45.056l704-708.544a32 32 0 0 1 45.44 45.056z"})]))}}),hne=vne,pne=q({name:"NoSmoking",__name:"no-smoking",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M440.256 576H256v128h56.256l-64 64H224a32 32 0 0 1-32-32V544a32 32 0 0 1 32-32h280.256zm143.488 128H704V583.744L775.744 512H928a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H519.744zM768 576v128h128V576zm-29.696-207.552 45.248 45.248-497.856 497.856-45.248-45.248zM256 64h64v320h-64zM128 192h64v192h-64zM64 512h64v256H64z"})]))}}),mne=pne,gne=q({name:"Notebook",__name:"notebook",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 128v768h640V128zm-32-64h704a32 32 0 0 1 32 32v832a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M672 128h64v768h-64zM96 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32m0 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32m0 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32m0 192h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32"})]))}}),yne=gne,bne=q({name:"Notification",__name:"notification",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 128v64H256a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h512a64 64 0 0 0 64-64V512h64v256a128 128 0 0 1-128 128H256a128 128 0 0 1-128-128V256a128 128 0 0 1 128-128z"}),C("path",{fill:"currentColor",d:"M768 384a128 128 0 1 0 0-256 128 128 0 0 0 0 256m0 64a192 192 0 1 1 0-384 192 192 0 0 1 0 384"})]))}}),_ne=bne,wne=q({name:"Odometer",__name:"odometer",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),C("path",{fill:"currentColor",d:"M192 512a320 320 0 1 1 640 0 32 32 0 1 1-64 0 256 256 0 1 0-512 0 32 32 0 0 1-64 0"}),C("path",{fill:"currentColor",d:"M570.432 627.84A96 96 0 1 1 509.568 608l60.992-187.776A32 32 0 1 1 631.424 440l-60.992 187.776zM502.08 734.464a32 32 0 1 0 19.84-60.928 32 32 0 0 0-19.84 60.928"})]))}}),Sne=wne,Cne=q({name:"OfficeBuilding",__name:"office-building",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 128v704h384V128zm-32-64h448a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M256 256h256v64H256zm0 192h256v64H256zm0 192h256v64H256zm384-128h128v64H640zm0 128h128v64H640zM64 832h896v64H64z"}),C("path",{fill:"currentColor",d:"M640 384v448h192V384zm-32-64h256a32 32 0 0 1 32 32v512a32 32 0 0 1-32 32H608a32 32 0 0 1-32-32V352a32 32 0 0 1 32-32"})]))}}),xne=Cne,kne=q({name:"Open",__name:"open",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M329.956 257.138a254.862 254.862 0 0 0 0 509.724h364.088a254.862 254.862 0 0 0 0-509.724zm0-72.818h364.088a327.68 327.68 0 1 1 0 655.36H329.956a327.68 327.68 0 1 1 0-655.36"}),C("path",{fill:"currentColor",d:"M694.044 621.227a109.227 109.227 0 1 0 0-218.454 109.227 109.227 0 0 0 0 218.454m0 72.817a182.044 182.044 0 1 1 0-364.088 182.044 182.044 0 0 1 0 364.088"})]))}}),Tne=kne,Mne=q({name:"Operation",__name:"operation",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M389.44 768a96.064 96.064 0 0 1 181.12 0H896v64H570.56a96.064 96.064 0 0 1-181.12 0H128v-64zm192-288a96.064 96.064 0 0 1 181.12 0H896v64H762.56a96.064 96.064 0 0 1-181.12 0H128v-64zm-320-288a96.064 96.064 0 0 1 181.12 0H896v64H442.56a96.064 96.064 0 0 1-181.12 0H128v-64z"})]))}}),Ene=Mne,Ane=q({name:"Opportunity",__name:"opportunity",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M384 960v-64h192.064v64zm448-544a350.66 350.66 0 0 1-128.32 271.424C665.344 719.04 640 763.776 640 813.504V832H320v-14.336c0-48-19.392-95.36-57.216-124.992a351.55 351.55 0 0 1-128.448-344.256c25.344-136.448 133.888-248.128 269.76-276.48A352.384 352.384 0 0 1 832 416m-544 32c0-132.288 75.904-224 192-224v-64c-154.432 0-256 122.752-256 288z"})]))}}),Ine=Ane,Lne=q({name:"Orange",__name:"orange",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 894.72a382.34 382.34 0 0 0 215.936-89.472L577.024 622.272c-10.24 6.016-21.248 10.688-33.024 13.696v258.688zm261.248-134.784A382.34 382.34 0 0 0 894.656 544H635.968c-3.008 11.776-7.68 22.848-13.696 33.024zM894.656 480a382.34 382.34 0 0 0-89.408-215.936L622.272 446.976c6.016 10.24 10.688 21.248 13.696 33.024zm-134.72-261.248A382.34 382.34 0 0 0 544 129.344v258.688c11.776 3.008 22.848 7.68 33.024 13.696zM480 129.344a382.34 382.34 0 0 0-215.936 89.408l182.912 182.976c10.24-6.016 21.248-10.688 33.024-13.696zm-261.248 134.72A382.34 382.34 0 0 0 129.344 480h258.688c3.008-11.776 7.68-22.848 13.696-33.024zM129.344 544a382.34 382.34 0 0 0 89.408 215.936l182.976-182.912A127.2 127.2 0 0 1 388.032 544zm134.72 261.248A382.34 382.34 0 0 0 480 894.656V635.968a127.2 127.2 0 0 1-33.024-13.696zM512 960a448 448 0 1 1 0-896 448 448 0 0 1 0 896m0-384a64 64 0 1 0 0-128 64 64 0 0 0 0 128"})]))}}),Pne=Lne,Dne=q({name:"Paperclip",__name:"paperclip",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M602.496 240.448A192 192 0 1 1 874.048 512l-316.8 316.8A256 256 0 0 1 195.2 466.752L602.496 59.456l45.248 45.248L240.448 512A192 192 0 0 0 512 783.552l316.8-316.8a128 128 0 1 0-181.056-181.056L353.6 579.904a32 32 0 1 0 45.248 45.248l294.144-294.144 45.312 45.248L444.096 670.4a96 96 0 1 1-135.744-135.744z"})]))}}),One=Dne,$ne=q({name:"PartlyCloudy",__name:"partly-cloudy",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M598.4 895.872H328.192a256 256 0 0 1-34.496-510.528A352 352 0 1 1 598.4 895.872m-271.36-64h272.256a288 288 0 1 0-248.512-417.664L335.04 445.44l-34.816 3.584a192 192 0 0 0 26.88 382.848z"}),C("path",{fill:"currentColor",d:"M139.84 501.888a256 256 0 1 1 417.856-277.12c-17.728 2.176-38.208 8.448-61.504 18.816A192 192 0 1 0 189.12 460.48a6004 6004 0 0 0-49.28 41.408"})]))}}),Rne=$ne,Nne=q({name:"Pear",__name:"pear",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M542.336 258.816a443 443 0 0 0-9.024 25.088 32 32 0 1 1-60.8-20.032l1.088-3.328a162.69 162.69 0 0 0-122.048 131.392l-17.088 102.72-20.736 15.36C256.192 552.704 224 610.88 224 672c0 120.576 126.4 224 288 224s288-103.424 288-224c0-61.12-32.192-119.296-89.728-161.92l-20.736-15.424-17.088-102.72a162.69 162.69 0 0 0-130.112-133.12m-40.128-66.56c7.936-15.552 16.576-30.08 25.92-43.776 23.296-33.92 49.408-59.776 78.528-77.12a32 32 0 1 1 32.704 55.04c-20.544 12.224-40.064 31.552-58.432 58.304a317 317 0 0 0-9.792 15.104 226.69 226.69 0 0 1 164.48 181.568l12.8 77.248C819.456 511.36 864 587.392 864 672c0 159.04-157.568 288-352 288S160 831.04 160 672c0-84.608 44.608-160.64 115.584-213.376l12.8-77.248a226.624 226.624 0 0 1 213.76-189.184z"})]))}}),Bne=Nne,Vne=q({name:"PhoneFilled",__name:"phone-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M199.232 125.568 90.624 379.008a32 32 0 0 0 6.784 35.2l512.384 512.384a32 32 0 0 0 35.2 6.784l253.44-108.608a32 32 0 0 0 10.048-52.032L769.6 633.92a32 32 0 0 0-36.928-5.952l-130.176 65.088-271.488-271.552 65.024-130.176a32 32 0 0 0-5.952-36.928L251.2 115.52a32 32 0 0 0-51.968 10.048"})]))}}),zne=Vne,Fne=q({name:"Phone",__name:"phone",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M79.36 432.256 591.744 944.64a32 32 0 0 0 35.2 6.784l253.44-108.544a32 32 0 0 0 9.984-52.032l-153.856-153.92a32 32 0 0 0-36.928-6.016l-69.888 34.944L358.08 394.24l35.008-69.888a32 32 0 0 0-5.952-36.928L233.152 133.568a32 32 0 0 0-52.032 10.048L72.512 397.056a32 32 0 0 0 6.784 35.2zm60.48-29.952 81.536-190.08L325.568 316.48l-24.64 49.216-20.608 41.216 32.576 32.64 271.552 271.552 32.64 32.64 41.216-20.672 49.28-24.576 104.192 104.128-190.08 81.472zM512 320v-64a256 256 0 0 1 256 256h-64a192 192 0 0 0-192-192m0-192V64a448 448 0 0 1 448 448h-64a384 384 0 0 0-384-384"})]))}}),Hne=Fne,Wne=q({name:"PictureFilled",__name:"picture-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M96 896a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h832a32 32 0 0 1 32 32v704a32 32 0 0 1-32 32zm315.52-228.48-68.928-68.928a32 32 0 0 0-45.248 0L128 768.064h778.688l-242.112-290.56a32 32 0 0 0-49.216 0L458.752 665.408a32 32 0 0 1-47.232 2.112M256 384a96 96 0 1 0 192.064-.064A96 96 0 0 0 256 384"})]))}}),RI=Wne,Kne=q({name:"PictureRounded",__name:"picture-rounded",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 128a384 384 0 1 0 0 768 384 384 0 0 0 0-768m0-64a448 448 0 1 1 0 896 448 448 0 0 1 0-896"}),C("path",{fill:"currentColor",d:"M640 288q64 0 64 64t-64 64-64-64 64-64M214.656 790.656l-45.312-45.312 185.664-185.6a96 96 0 0 1 123.712-10.24l138.24 98.688a32 32 0 0 0 39.872-2.176L906.688 422.4l42.624 47.744L699.52 693.696a96 96 0 0 1-119.808 6.592l-138.24-98.752a32 32 0 0 0-41.152 3.456l-185.664 185.6z"})]))}}),Une=Kne,Gne=q({name:"Picture",__name:"picture",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 160v704h704V160zm-32-64h768a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M384 288q64 0 64 64t-64 64-64-64 64-64M185.408 876.992l-50.816-38.912L350.72 556.032a96 96 0 0 1 134.592-17.856l1.856 1.472 122.88 99.136a32 32 0 0 0 44.992-4.864l216-269.888 49.92 39.936-215.808 269.824-.256.32a96 96 0 0 1-135.04 14.464l-122.88-99.072-.64-.512a32 32 0 0 0-44.8 5.952z"})]))}}),NI=Gne,Yne=q({name:"PieChart",__name:"pie-chart",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M448 68.48v64.832A384.128 384.128 0 0 0 512 896a384.13 384.13 0 0 0 378.688-320h64.768A448.128 448.128 0 0 1 64 512 448.13 448.13 0 0 1 448 68.48"}),C("path",{fill:"currentColor",d:"M576 97.28V448h350.72A384.064 384.064 0 0 0 576 97.28M512 64V33.152A448 448 0 0 1 990.848 512H512z"})]))}}),jne=Yne,qne=q({name:"Place",__name:"place",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512"}),C("path",{fill:"currentColor",d:"M512 512a32 32 0 0 1 32 32v256a32 32 0 1 1-64 0V544a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M384 649.088v64.96C269.76 732.352 192 771.904 192 800c0 37.696 139.904 96 320 96s320-58.304 320-96c0-28.16-77.76-67.648-192-85.952v-64.96C789.12 671.04 896 730.368 896 800c0 88.32-171.904 160-384 160s-384-71.68-384-160c0-69.696 106.88-128.96 256-150.912"})]))}}),Xne=qne,Zne=q({name:"Platform",__name:"platform",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M448 832v-64h128v64h192v64H256v-64zM128 704V128h768v576z"})]))}}),Jne=Zne,Qne=q({name:"Plus",__name:"plus",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 480V128a32 32 0 0 1 64 0v352h352a32 32 0 1 1 0 64H544v352a32 32 0 1 1-64 0V544H128a32 32 0 0 1 0-64z"})]))}}),hf=Qne,eae=q({name:"Pointer",__name:"pointer",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M511.552 128c-35.584 0-64.384 28.8-64.384 64.448v516.48L274.048 570.88a94.27 94.27 0 0 0-112.896-3.456 44.416 44.416 0 0 0-8.96 62.208L332.8 870.4A64 64 0 0 0 384 896h512V575.232a64 64 0 0 0-45.632-61.312l-205.952-61.76A96 96 0 0 1 576 360.192V192.448C576 156.8 547.2 128 511.552 128M359.04 556.8l24.128 19.2V192.448a128.448 128.448 0 1 1 256.832 0v167.744a32 32 0 0 0 22.784 30.656l206.016 61.76A128 128 0 0 1 960 575.232V896a64 64 0 0 1-64 64H384a128 128 0 0 1-102.4-51.2L101.056 668.032A108.416 108.416 0 0 1 128 512.512a158.27 158.27 0 0 1 185.984 8.32z"})]))}}),tae=eae,nae=q({name:"Position",__name:"position",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m249.6 417.088 319.744 43.072 39.168 310.272L845.12 178.88zm-129.024 47.168a32 32 0 0 1-7.68-61.44l777.792-311.04a32 32 0 0 1 41.6 41.6l-310.336 775.68a32 32 0 0 1-61.44-7.808L512 516.992z"})]))}}),aae=nae,rae=q({name:"Postcard",__name:"postcard",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 224a32 32 0 0 0-32 32v512a32 32 0 0 0 32 32h704a32 32 0 0 0 32-32V256a32 32 0 0 0-32-32zm0-64h704a96 96 0 0 1 96 96v512a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V256a96 96 0 0 1 96-96"}),C("path",{fill:"currentColor",d:"M704 320a64 64 0 1 1 0 128 64 64 0 0 1 0-128M288 448h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32m0 128h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32"})]))}}),oae=rae,iae=q({name:"Pouring",__name:"pouring",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m739.328 291.328-35.2-6.592-12.8-33.408a192.064 192.064 0 0 0-365.952 23.232l-9.92 40.896-41.472 7.04a176.32 176.32 0 0 0-146.24 173.568c0 97.28 78.72 175.936 175.808 175.936h400a192 192 0 0 0 35.776-380.672M959.552 480a256 256 0 0 1-256 256h-400A239.81 239.81 0 0 1 63.744 496.192a240.32 240.32 0 0 1 199.488-236.8 256.128 256.128 0 0 1 487.872-30.976A256.064 256.064 0 0 1 959.552 480M224 800a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32m192 0a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32m192 0a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32m192 0a32 32 0 0 1 32 32v96a32 32 0 1 1-64 0v-96a32 32 0 0 1 32-32"})]))}}),lae=iae,sae=q({name:"Present",__name:"present",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 896V640H192v-64h288V320H192v576zm64 0h288V320H544v256h288v64H544zM128 256h768v672a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32z"}),C("path",{fill:"currentColor",d:"M96 256h832q32 0 32 32t-32 32H96q-32 0-32-32t32-32"}),C("path",{fill:"currentColor",d:"M416 256a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"}),C("path",{fill:"currentColor",d:"M608 256a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"})]))}}),uae=sae,cae=q({name:"PriceTag",__name:"price-tag",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 318.336V896h576V318.336L552.512 115.84a64 64 0 0 0-81.024 0zM593.024 66.304l259.2 212.096A32 32 0 0 1 864 303.168V928a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V303.168a32 32 0 0 1 11.712-24.768l259.2-212.096a128 128 0 0 1 162.112 0"}),C("path",{fill:"currentColor",d:"M512 448a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"})]))}}),dae=cae,fae=q({name:"Printer",__name:"printer",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 768H105.024c-14.272 0-19.456-1.472-24.64-4.288a29.06 29.06 0 0 1-12.16-12.096C65.536 746.432 64 741.248 64 727.04V379.072c0-42.816 4.48-58.304 12.8-73.984 8.384-15.616 20.672-27.904 36.288-36.288 15.68-8.32 31.168-12.8 73.984-12.8H256V64h512v192h68.928c42.816 0 58.304 4.48 73.984 12.8 15.616 8.384 27.904 20.672 36.288 36.288 8.32 15.68 12.8 31.168 12.8 73.984v347.904c0 14.272-1.472 19.456-4.288 24.64a29.06 29.06 0 0 1-12.096 12.16c-5.184 2.752-10.368 4.224-24.64 4.224H768v192H256zm64-192v320h384V576zm-64 128V512h512v192h128V379.072c0-29.376-1.408-36.48-5.248-43.776a23.3 23.3 0 0 0-10.048-10.048c-7.232-3.84-14.4-5.248-43.776-5.248H187.072c-29.376 0-36.48 1.408-43.776 5.248a23.3 23.3 0 0 0-10.048 10.048c-3.84 7.232-5.248 14.4-5.248 43.776V704zm64-448h384V128H320zm-64 128h64v64h-64zm128 0h64v64h-64z"})]))}}),vae=fae,hae=q({name:"Promotion",__name:"promotion",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m64 448 832-320-128 704-446.08-243.328L832 192 242.816 545.472zm256 512V657.024L512 768z"})]))}}),pae=hae,mae=q({name:"QuartzWatch",__name:"quartz-watch",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M422.02 602.01v-.03c-6.68-5.99-14.35-8.83-23.01-8.51q-13.005.48-22.5 10.02c-6.33 6.36-9.5 13.7-9.5 22.02s3 15.82 8.99 22.5c8.68 8.68 19.02 11.35 31.01 8s19.49-10.85 22.5-22.5.51-22.15-7.49-31.49zM384 512c0-9.35-3-17.02-8.99-23.01-6-5.99-13.66-8.99-23.01-8.99s-17.02 3-23.01 8.99c-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.66 8.99 23.01 8.99s17.02-3 23.01-8.99c5.99-6 8.99-13.67 8.99-23.01m6.53-82.49c11.65 3.01 22.15.51 31.49-7.49h.04c5.99-6.68 8.83-14.34 8.51-23.01s-3.66-16.16-10.02-22.5c-6.36-6.33-13.7-9.5-22.02-9.5s-15.82 3-22.5 8.99c-8.68 8.69-11.35 19.02-8 31.01q5.025 17.985 22.5 22.5m242.94 0q17.505-4.545 22.02-22.02c3.01-11.65.51-22.15-7.49-31.49h.01c-6.68-5.99-14.18-8.99-22.5-8.99s-15.66 3.16-22.02 9.5q-9.54 9.51-10.02 22.5c-.32 8.66 2.52 16.33 8.51 23.01 9.32 8.02 19.82 10.52 31.49 7.49M512 640c-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.67 8.99 23.01 8.99 9.35 0 17.02-3 23.01-8.99 5.99-6 8.99-13.66 8.99-23.01s-3-17.02-8.99-23.01c-6-5.99-13.66-8.99-23.01-8.99m183.01-151.01c-6-5.99-13.66-8.99-23.01-8.99s-17.02 3-23.01 8.99c-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.66 8.99 23.01 8.99s17.02-3 23.01-8.99c5.99-6 8.99-13.67 8.99-23.01 0-9.35-3-17.02-8.99-23.01"}),C("path",{fill:"currentColor",d:"M832 512c-2-90.67-33.17-166.17-93.5-226.5-20.43-20.42-42.6-37.49-66.5-51.23V64H352v170.26c-23.9 13.74-46.07 30.81-66.5 51.24-60.33 60.33-91.49 135.83-93.5 226.5 2 90.67 33.17 166.17 93.5 226.5 20.43 20.43 42.6 37.5 66.5 51.24V960h320V789.74c23.9-13.74 46.07-30.81 66.5-51.24 60.33-60.34 91.49-135.83 93.5-226.5M416 128h192v78.69c-29.85-9.03-61.85-13.93-96-14.69-34.15.75-66.15 5.65-96 14.68zm192 768H416v-78.68c29.85 9.03 61.85 13.93 96 14.68 34.15-.75 66.15-5.65 96-14.68zm-96-128c-72.66-2.01-132.99-27.01-180.99-75.01S258.01 584.66 256 512c2.01-72.66 27.01-132.99 75.01-180.99S439.34 258.01 512 256c72.66 2.01 132.99 27.01 180.99 75.01S765.99 439.34 768 512c-2.01 72.66-27.01 132.99-75.01 180.99S584.66 765.99 512 768"}),C("path",{fill:"currentColor",d:"M512 320c-9.35 0-17.02 3-23.01 8.99-5.99 6-8.99 13.66-8.99 23.01s3 17.02 8.99 23.01c6 5.99 13.67 8.99 23.01 8.99 9.35 0 17.02-3 23.01-8.99 5.99-6 8.99-13.66 8.99-23.01s-3-17.02-8.99-23.01c-6-5.99-13.66-8.99-23.01-8.99m112.99 273.5c-8.66-.32-16.33 2.52-23.01 8.51-7.98 9.32-10.48 19.82-7.49 31.49s10.49 19.17 22.5 22.5 22.35.66 31.01-8v.04c5.99-6.68 8.99-14.18 8.99-22.5s-3.16-15.66-9.5-22.02-13.84-9.7-22.5-10.02"})]))}}),gae=mae,yae=q({name:"QuestionFilled",__name:"question-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m23.744 191.488c-52.096 0-92.928 14.784-123.2 44.352-30.976 29.568-45.76 70.4-45.76 122.496h80.256c0-29.568 5.632-52.8 17.6-68.992 13.376-19.712 35.2-28.864 66.176-28.864 23.936 0 42.944 6.336 56.32 19.712 12.672 13.376 19.712 31.68 19.712 54.912 0 17.6-6.336 34.496-19.008 49.984l-8.448 9.856c-45.76 40.832-73.216 70.4-82.368 89.408-9.856 19.008-14.08 42.24-14.08 68.992v9.856h80.96v-9.856c0-16.896 3.52-31.68 10.56-45.76 6.336-12.672 15.488-24.64 28.16-35.2 33.792-29.568 54.208-48.576 60.544-55.616 16.896-22.528 26.048-51.392 26.048-86.592q0-64.416-42.24-101.376c-28.16-25.344-65.472-37.312-111.232-37.312m-12.672 406.208a54.27 54.27 0 0 0-38.72 14.784 49.4 49.4 0 0 0-15.488 38.016c0 15.488 4.928 28.16 15.488 38.016A54.85 54.85 0 0 0 523.072 768c15.488 0 28.16-4.928 38.72-14.784a51.52 51.52 0 0 0 16.192-38.72 51.97 51.97 0 0 0-15.488-38.016 55.94 55.94 0 0 0-39.424-14.784"})]))}}),BI=yae,bae=q({name:"Rank",__name:"rank",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m186.496 544 41.408 41.344a32 32 0 1 1-45.248 45.312l-96-96a32 32 0 0 1 0-45.312l96-96a32 32 0 1 1 45.248 45.312L186.496 480h290.816V186.432l-41.472 41.472a32 32 0 1 1-45.248-45.184l96-96.128a32 32 0 0 1 45.312 0l96 96.064a32 32 0 0 1-45.248 45.184l-41.344-41.28V480H832l-41.344-41.344a32 32 0 0 1 45.248-45.312l96 96a32 32 0 0 1 0 45.312l-96 96a32 32 0 0 1-45.248-45.312L832 544H541.312v293.44l41.344-41.28a32 32 0 1 1 45.248 45.248l-96 96a32 32 0 0 1-45.312 0l-96-96a32 32 0 1 1 45.312-45.248l41.408 41.408V544z"})]))}}),_ae=bae,wae=q({name:"ReadingLamp",__name:"reading-lamp",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M352 896h320q32 0 32 32t-32 32H352q-32 0-32-32t32-32m-44.672-768-99.52 448h608.384l-99.52-448zm-25.6-64h460.608a32 32 0 0 1 31.232 25.088l113.792 512A32 32 0 0 1 856.128 640H167.872a32 32 0 0 1-31.232-38.912l113.792-512A32 32 0 0 1 281.664 64z"}),C("path",{fill:"currentColor",d:"M672 576q32 0 32 32v128q0 32-32 32t-32-32V608q0-32 32-32m-192-.064h64V960h-64z"})]))}}),Sae=wae,Cae=q({name:"Reading",__name:"reading",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m512 863.36 384-54.848v-638.72L525.568 222.72a96 96 0 0 1-27.136 0L128 169.792v638.72zM137.024 106.432l370.432 52.928a32 32 0 0 0 9.088 0l370.432-52.928A64 64 0 0 1 960 169.792v638.72a64 64 0 0 1-54.976 63.36l-388.48 55.488a32 32 0 0 1-9.088 0l-388.48-55.488A64 64 0 0 1 64 808.512v-638.72a64 64 0 0 1 73.024-63.36"}),C("path",{fill:"currentColor",d:"M480 192h64v704h-64z"})]))}}),xae=Cae,kae=q({name:"RefreshLeft",__name:"refresh-left",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M289.088 296.704h92.992a32 32 0 0 1 0 64H232.96a32 32 0 0 1-32-32V179.712a32 32 0 0 1 64 0v50.56a384 384 0 0 1 643.84 282.88 384 384 0 0 1-383.936 384 384 384 0 0 1-384-384h64a320 320 0 1 0 640 0 320 320 0 0 0-555.712-216.448z"})]))}}),VI=kae,Tae=q({name:"RefreshRight",__name:"refresh-right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M784.512 230.272v-50.56a32 32 0 1 1 64 0v149.056a32 32 0 0 1-32 32H667.52a32 32 0 1 1 0-64h92.992A320 320 0 1 0 524.8 833.152a320 320 0 0 0 320-320h64a384 384 0 0 1-384 384 384 384 0 0 1-384-384 384 384 0 0 1 643.712-282.88"})]))}}),zI=Tae,Mae=q({name:"Refresh",__name:"refresh",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M771.776 794.88A384 384 0 0 1 128 512h64a320 320 0 0 0 555.712 216.448H654.72a32 32 0 1 1 0-64h149.056a32 32 0 0 1 32 32v148.928a32 32 0 1 1-64 0v-50.56zM276.288 295.616h92.992a32 32 0 0 1 0 64H220.16a32 32 0 0 1-32-32V178.56a32 32 0 0 1 64 0v50.56A384 384 0 0 1 896.128 512h-64a320 320 0 0 0-555.776-216.384z"})]))}}),g4=Mae,Eae=q({name:"Refrigerator",__name:"refrigerator",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 448h512V160a32 32 0 0 0-32-32H288a32 32 0 0 0-32 32zm0 64v352a32 32 0 0 0 32 32h448a32 32 0 0 0 32-32V512zm32-448h448a96 96 0 0 1 96 96v704a96 96 0 0 1-96 96H288a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96m32 224h64v96h-64zm0 288h64v96h-64z"})]))}}),Aae=Eae,Iae=q({name:"RemoveFilled",__name:"remove-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896M288 512a38.4 38.4 0 0 0 38.4 38.4h371.2a38.4 38.4 0 0 0 0-76.8H326.4A38.4 38.4 0 0 0 288 512"})]))}}),Lae=Iae,Pae=q({name:"Remove",__name:"remove",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M352 480h320a32 32 0 1 1 0 64H352a32 32 0 0 1 0-64"}),C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),Dae=Pae,Oae=q({name:"Right",__name:"right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M754.752 480H160a32 32 0 1 0 0 64h594.752L521.344 777.344a32 32 0 0 0 45.312 45.312l288-288a32 32 0 0 0 0-45.312l-288-288a32 32 0 1 0-45.312 45.312z"})]))}}),$ae=Oae,Rae=q({name:"ScaleToOriginal",__name:"scale-to-original",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M813.176 180.706a60.235 60.235 0 0 1 60.236 60.235v481.883a60.235 60.235 0 0 1-60.236 60.235H210.824a60.235 60.235 0 0 1-60.236-60.235V240.94a60.235 60.235 0 0 1 60.236-60.235h602.352zm0-60.235H210.824A120.47 120.47 0 0 0 90.353 240.94v481.883a120.47 120.47 0 0 0 120.47 120.47h602.353a120.47 120.47 0 0 0 120.471-120.47V240.94a120.47 120.47 0 0 0-120.47-120.47zm-120.47 180.705a30.12 30.12 0 0 0-30.118 30.118v301.177a30.118 30.118 0 0 0 60.236 0V331.294a30.12 30.12 0 0 0-30.118-30.118m-361.412 0a30.12 30.12 0 0 0-30.118 30.118v301.177a30.118 30.118 0 1 0 60.236 0V331.294a30.12 30.12 0 0 0-30.118-30.118M512 361.412a30.12 30.12 0 0 0-30.118 30.117v30.118a30.118 30.118 0 0 0 60.236 0V391.53A30.12 30.12 0 0 0 512 361.412M512 512a30.12 30.12 0 0 0-30.118 30.118v30.117a30.118 30.118 0 0 0 60.236 0v-30.117A30.12 30.12 0 0 0 512 512"})]))}}),FI=Rae,Nae=q({name:"School",__name:"school",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 128v704h576V128zm-32-64h640a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M64 832h896v64H64zm256-640h128v96H320z"}),C("path",{fill:"currentColor",d:"M384 832h256v-64a128 128 0 1 0-256 0zm128-256a192 192 0 0 1 192 192v128H320V768a192 192 0 0 1 192-192M320 384h128v96H320zm256-192h128v96H576zm0 192h128v96H576z"})]))}}),Bae=Nae,Vae=q({name:"Scissor",__name:"scissor",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m512.064 578.368-106.88 152.768a160 160 0 1 1-23.36-78.208L472.96 522.56 196.864 128.256a32 32 0 1 1 52.48-36.736l393.024 561.344a160 160 0 1 1-23.36 78.208l-106.88-152.704zm54.4-189.248 208.384-297.6a32 32 0 0 1 52.48 36.736l-221.76 316.672-39.04-55.808zm-376.32 425.856a96 96 0 1 0 110.144-157.248 96 96 0 0 0-110.08 157.248zm643.84 0a96 96 0 1 0-110.08-157.248 96 96 0 0 0 110.08 157.248"})]))}}),zae=Vae,Fae=q({name:"Search",__name:"search",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704"})]))}}),HI=Fae,Hae=q({name:"Select",__name:"select",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496"})]))}}),Wae=Hae,Kae=q({name:"Sell",__name:"sell",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 288h131.072a32 32 0 0 1 31.808 28.8L886.4 512h-64.384l-16-160H704v96a32 32 0 1 1-64 0v-96H384v96a32 32 0 0 1-64 0v-96H217.92l-51.2 512H512v64H131.328a32 32 0 0 1-31.808-35.2l57.6-576a32 32 0 0 1 31.808-28.8H320v-22.336C320 154.688 405.504 64 512 64s192 90.688 192 201.664v22.4zm-64 0v-22.336C640 189.248 582.272 128 512 128s-128 61.248-128 137.664v22.4h256zm201.408 483.84L768 698.496V928a32 32 0 1 1-64 0V698.496l-73.344 73.344a32 32 0 1 1-45.248-45.248l128-128a32 32 0 0 1 45.248 0l128 128a32 32 0 1 1-45.248 45.248"})]))}}),Uae=Kae,Gae=q({name:"SemiSelect",__name:"semi-select",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 448h768q64 0 64 64t-64 64H128q-64 0-64-64t64-64"})]))}}),Yae=Gae,jae=q({name:"Service",__name:"service",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M864 409.6a192 192 0 0 1-37.888 349.44A256.064 256.064 0 0 1 576 960h-96a32 32 0 1 1 0-64h96a192.06 192.06 0 0 0 181.12-128H736a32 32 0 0 1-32-32V416a32 32 0 0 1 32-32h32c10.368 0 20.544.832 30.528 2.432a288 288 0 0 0-573.056 0A193 193 0 0 1 256 384h32a32 32 0 0 1 32 32v320a32 32 0 0 1-32 32h-32a192 192 0 0 1-96-358.4 352 352 0 0 1 704 0M256 448a128 128 0 1 0 0 256zm640 128a128 128 0 0 0-128-128v256a128 128 0 0 0 128-128"})]))}}),qae=jae,Xae=q({name:"SetUp",__name:"set-up",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64zm0-64h576a128 128 0 0 1 128 128v576a128 128 0 0 1-128 128H224A128 128 0 0 1 96 800V224A128 128 0 0 1 224 96"}),C("path",{fill:"currentColor",d:"M384 416a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"}),C("path",{fill:"currentColor",d:"M480 320h256q32 0 32 32t-32 32H480q-32 0-32-32t32-32m160 416a64 64 0 1 0 0-128 64 64 0 0 0 0 128m0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256"}),C("path",{fill:"currentColor",d:"M288 640h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32"})]))}}),Zae=Xae,Jae=q({name:"Setting",__name:"setting",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357 357 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a352 352 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357 357 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294 294 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293 293 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294 294 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288 288 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293 293 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a288 288 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256"})]))}}),Qae=Jae,ere=q({name:"Share",__name:"share",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m679.872 348.8-301.76 188.608a127.8 127.8 0 0 1 5.12 52.16l279.936 104.96a128 128 0 1 1-22.464 59.904l-279.872-104.96a128 128 0 1 1-16.64-166.272l301.696-188.608a128 128 0 1 1 33.92 54.272z"})]))}}),tre=ere,nre=q({name:"Ship",__name:"ship",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 386.88V448h405.568a32 32 0 0 1 30.72 40.768l-76.48 267.968A192 192 0 0 1 687.168 896H336.832a192 192 0 0 1-184.64-139.264L75.648 488.768A32 32 0 0 1 106.368 448H448V117.888a32 32 0 0 1 47.36-28.096l13.888 7.616L512 96v2.88l231.68 126.4a32 32 0 0 1-2.048 57.216zm0-70.272 144.768-65.792L512 171.84zM512 512H148.864l18.24 64H856.96l18.24-64zM185.408 640l28.352 99.2A128 128 0 0 0 336.832 832h350.336a128 128 0 0 0 123.072-92.8l28.352-99.2z"})]))}}),are=nre,rre=q({name:"Shop",__name:"shop",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 704h64v192H256V704h64v64h384zm188.544-152.192C894.528 559.616 896 567.616 896 576a96 96 0 1 1-192 0 96 96 0 1 1-192 0 96 96 0 1 1-192 0 96 96 0 1 1-192 0c0-8.384 1.408-16.384 3.392-24.192L192 128h640z"})]))}}),ore=rre,ire=q({name:"ShoppingBag",__name:"shopping-bag",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 320v96a32 32 0 0 1-32 32h-32V320H384v128h-32a32 32 0 0 1-32-32v-96H192v576h640V320zm-384-64a192 192 0 1 1 384 0h160a32 32 0 0 1 32 32v640a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32zm64 0h256a128 128 0 1 0-256 0"}),C("path",{fill:"currentColor",d:"M192 704h640v64H192z"})]))}}),lre=ire,sre=q({name:"ShoppingCartFull",__name:"shopping-cart-full",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M432 928a48 48 0 1 1 0-96 48 48 0 0 1 0 96m320 0a48 48 0 1 1 0-96 48 48 0 0 1 0 96M96 128a32 32 0 0 1 0-64h160a32 32 0 0 1 31.36 25.728L320.64 256H928a32 32 0 0 1 31.296 38.72l-96 448A32 32 0 0 1 832 768H384a32 32 0 0 1-31.36-25.728L229.76 128zm314.24 576h395.904l82.304-384H333.44z"}),C("path",{fill:"currentColor",d:"M699.648 256 608 145.984 516.352 256zm-140.8-151.04a64 64 0 0 1 98.304 0L836.352 320H379.648z"})]))}}),ure=sre,cre=q({name:"ShoppingCart",__name:"shopping-cart",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M432 928a48 48 0 1 1 0-96 48 48 0 0 1 0 96m320 0a48 48 0 1 1 0-96 48 48 0 0 1 0 96M96 128a32 32 0 0 1 0-64h160a32 32 0 0 1 31.36 25.728L320.64 256H928a32 32 0 0 1 31.296 38.72l-96 448A32 32 0 0 1 832 768H384a32 32 0 0 1-31.36-25.728L229.76 128zm314.24 576h395.904l82.304-384H333.44z"})]))}}),dre=cre,fre=q({name:"ShoppingTrolley",__name:"shopping-trolley",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M368 833c-13.3 0-24.5 4.5-33.5 13.5S321 866.7 321 880s4.5 24.5 13.5 33.5 20.2 13.8 33.5 14.5c13.3-.7 24.5-5.5 33.5-14.5S415 893.3 415 880s-4.5-24.5-13.5-33.5S381.3 833 368 833m439-193c7.4 0 13.8-2.2 19.5-6.5S836 623.3 838 616l112-448c2-10-.2-19.2-6.5-27.5S929 128 919 128H96c-9.3 0-17 3-23 9s-9 13.7-9 23 3 17 9 23 13.7 9 23 9h96v576h672c9.3 0 17-3 23-9s9-13.7 9-23-3-17-9-23-13.7-9-23-9H256v-64zM256 192h622l-96 384H256zm432 641c-13.3 0-24.5 4.5-33.5 13.5S641 866.7 641 880s4.5 24.5 13.5 33.5 20.2 13.8 33.5 14.5c13.3-.7 24.5-5.5 33.5-14.5S735 893.3 735 880s-4.5-24.5-13.5-33.5S701.3 833 688 833"})]))}}),vre=fre,hre=q({name:"Smoking",__name:"smoking",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 576v128h640V576zm-32-64h704a32 32 0 0 1 32 32v192a32 32 0 0 1-32 32H224a32 32 0 0 1-32-32V544a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M704 576h64v128h-64zM256 64h64v320h-64zM128 192h64v192h-64zM64 512h64v256H64z"})]))}}),pre=hre,mre=q({name:"Soccer",__name:"soccer",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M418.496 871.04 152.256 604.8c-16.512 94.016-2.368 178.624 42.944 224 44.928 44.928 129.344 58.752 223.296 42.24m72.32-18.176a573.06 573.06 0 0 0 224.832-137.216 573.1 573.1 0 0 0 137.216-224.832L533.888 171.84a578.56 578.56 0 0 0-227.52 138.496A567.7 567.7 0 0 0 170.432 532.48zM871.04 418.496c16.512-93.952 2.688-178.368-42.24-223.296-44.544-44.544-128.704-58.048-222.592-41.536zM149.952 874.048c-112.96-112.96-88.832-408.96 111.168-608.96C461.056 65.152 760.96 36.928 874.048 149.952c113.024 113.024 86.784 411.008-113.152 610.944s-497.92 226.112-610.944 113.152m452.544-497.792 22.656-22.656a32 32 0 0 1 45.248 45.248l-22.656 22.656 45.248 45.248A32 32 0 1 1 647.744 512l-45.248-45.248L557.248 512l45.248 45.248a32 32 0 1 1-45.248 45.248L512 557.248l-45.248 45.248L512 647.744a32 32 0 1 1-45.248 45.248l-45.248-45.248-22.656 22.656a32 32 0 1 1-45.248-45.248l22.656-22.656-45.248-45.248A32 32 0 1 1 376.256 512l45.248 45.248L466.752 512l-45.248-45.248a32 32 0 1 1 45.248-45.248L512 466.752l45.248-45.248L512 376.256a32 32 0 0 1 45.248-45.248z"})]))}}),gre=mre,yre=q({name:"SoldOut",__name:"sold-out",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 288h131.072a32 32 0 0 1 31.808 28.8L886.4 512h-64.384l-16-160H704v96a32 32 0 1 1-64 0v-96H384v96a32 32 0 0 1-64 0v-96H217.92l-51.2 512H512v64H131.328a32 32 0 0 1-31.808-35.2l57.6-576a32 32 0 0 1 31.808-28.8H320v-22.336C320 154.688 405.504 64 512 64s192 90.688 192 201.664v22.4zm-64 0v-22.336C640 189.248 582.272 128 512 128s-128 61.248-128 137.664v22.4h256zm201.408 476.16a32 32 0 1 1 45.248 45.184l-128 128a32 32 0 0 1-45.248 0l-128-128a32 32 0 1 1 45.248-45.248L704 837.504V608a32 32 0 1 1 64 0v229.504l73.408-73.408z"})]))}}),bre=yre,_re=q({name:"SortDown",__name:"sort-down",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M576 96v709.568L333.312 562.816A32 32 0 1 0 288 608l297.408 297.344A32 32 0 0 0 640 882.688V96a32 32 0 0 0-64 0"})]))}}),WI=_re,wre=q({name:"SortUp",__name:"sort-up",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M384 141.248V928a32 32 0 1 0 64 0V218.56l242.688 242.688A32 32 0 1 0 736 416L438.592 118.656A32 32 0 0 0 384 141.248"})]))}}),KI=wre,Sre=q({name:"Sort",__name:"sort",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M384 96a32 32 0 0 1 64 0v786.752a32 32 0 0 1-54.592 22.656L95.936 608a32 32 0 0 1 0-45.312h.128a32 32 0 0 1 45.184 0L384 805.632zm192 45.248a32 32 0 0 1 54.592-22.592L928.064 416a32 32 0 0 1 0 45.312h-.128a32 32 0 0 1-45.184 0L640 218.496V928a32 32 0 1 1-64 0z"})]))}}),Cre=Sre,xre=q({name:"Stamp",__name:"stamp",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M624 475.968V640h144a128 128 0 0 1 128 128H128a128 128 0 0 1 128-128h144V475.968a192 192 0 1 1 224 0M128 896v-64h768v64z"})]))}}),kre=xre,Tre=q({name:"StarFilled",__name:"star-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M313.6 924.48a70.4 70.4 0 0 1-74.152-5.365 70.4 70.4 0 0 1-27.992-68.875l37.888-220.928L88.96 472.96a70.4 70.4 0 0 1 3.788-104.225A70.4 70.4 0 0 1 128 352.896l221.76-32.256 99.2-200.96a70.4 70.4 0 0 1 100.246-28.595 70.4 70.4 0 0 1 25.962 28.595l99.2 200.96 221.824 32.256a70.4 70.4 0 0 1 39.04 120.064L774.72 629.376l37.888 220.928a70.4 70.4 0 0 1-102.144 74.24L512 820.096l-198.4 104.32z"})]))}}),rv=Tre,Mre=q({name:"Star",__name:"star",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m512 747.84 228.16 119.936a6.4 6.4 0 0 0 9.28-6.72l-43.52-254.08 184.512-179.904a6.4 6.4 0 0 0-3.52-10.88l-255.104-37.12L517.76 147.904a6.4 6.4 0 0 0-11.52 0L392.192 379.072l-255.104 37.12a6.4 6.4 0 0 0-3.52 10.88L318.08 606.976l-43.584 254.08a6.4 6.4 0 0 0 9.28 6.72zM313.6 924.48a70.4 70.4 0 0 1-102.144-74.24l37.888-220.928L88.96 472.96A70.4 70.4 0 0 1 128 352.896l221.76-32.256 99.2-200.96a70.4 70.4 0 0 1 126.208 0l99.2 200.96 221.824 32.256a70.4 70.4 0 0 1 39.04 120.064L774.72 629.376l37.888 220.928a70.4 70.4 0 0 1-102.144 74.24L512 820.096l-198.4 104.32z"})]))}}),UI=Mre,Ere=q({name:"Stopwatch",__name:"stopwatch",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),C("path",{fill:"currentColor",d:"M672 234.88c-39.168 174.464-80 298.624-122.688 372.48-64 110.848-202.624 30.848-138.624-80C453.376 453.44 540.48 355.968 672 234.816z"})]))}}),Are=Ere,Ire=q({name:"SuccessFilled",__name:"success-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.27 38.27 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z"})]))}}),K6=Ire,Lre=q({name:"Sugar",__name:"sugar",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m801.728 349.184 4.48 4.48a128 128 0 0 1 0 180.992L534.656 806.144a128 128 0 0 1-181.056 0l-4.48-4.48-19.392 109.696a64 64 0 0 1-108.288 34.176L78.464 802.56a64 64 0 0 1 34.176-108.288l109.76-19.328-4.544-4.544a128 128 0 0 1 0-181.056l271.488-271.488a128 128 0 0 1 181.056 0l4.48 4.48 19.392-109.504a64 64 0 0 1 108.352-34.048l142.592 143.04a64 64 0 0 1-34.24 108.16zm-548.8 198.72h447.168v2.24l60.8-60.8a63.8 63.8 0 0 0 18.752-44.416h-426.88l-89.664 89.728a64 64 0 0 0-10.24 13.248zm0 64q4.128 7.104 10.176 13.248l135.744 135.744a64 64 0 0 0 90.496 0L638.4 611.904zm490.048-230.976L625.152 263.104a64 64 0 0 0-90.496 0L416.768 380.928zM123.712 757.312l142.976 142.976 24.32-137.6a25.6 25.6 0 0 0-29.696-29.632zm633.6-633.344-24.32 137.472a25.6 25.6 0 0 0 29.632 29.632l137.28-24.064-142.656-143.04z"})]))}}),Pre=Lre,Dre=q({name:"SuitcaseLine",__name:"suitcase-line",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M922.5 229.5c-24.32-24.34-54.49-36.84-90.5-37.5H704v-64c-.68-17.98-7.02-32.98-19.01-44.99S658.01 64.66 640 64H384c-17.98.68-32.98 7.02-44.99 19.01S320.66 110 320 128v64H192c-35.99.68-66.16 13.18-90.5 37.5S64.66 283.99 64 320v448c.68 35.99 13.18 66.16 37.5 90.5s54.49 36.84 90.5 37.5h640c35.99-.68 66.16-13.18 90.5-37.5s36.84-54.49 37.5-90.5V320c-.68-35.99-13.18-66.16-37.5-90.5M384 128h256v64H384zM256 832h-64c-17.98-.68-32.98-7.02-44.99-19.01S128.66 786.01 128 768V448h128zm448 0H320V448h384zm192-64c-.68 17.98-7.02 32.98-19.01 44.99S850.01 831.34 832 832h-64V448h128zm0-384H128v-64c.69-17.98 7.02-32.98 19.01-44.99S173.99 256.66 192 256h640c17.98.69 32.98 7.02 44.99 19.01S895.34 301.99 896 320z"})]))}}),Ore=Dre,$re=q({name:"Suitcase",__name:"suitcase",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 384h768v-64a64 64 0 0 0-64-64H192a64 64 0 0 0-64 64zm0 64v320a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V448zm64-256h640a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H192A128 128 0 0 1 64 768V320a128 128 0 0 1 128-128"}),C("path",{fill:"currentColor",d:"M384 128v64h256v-64zm0-64h256a64 64 0 0 1 64 64v64a64 64 0 0 1-64 64H384a64 64 0 0 1-64-64v-64a64 64 0 0 1 64-64"})]))}}),Rre=$re,Nre=q({name:"Sunny",__name:"sunny",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 704a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512m0-704a32 32 0 0 1 32 32v64a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 768a32 32 0 0 1 32 32v64a32 32 0 1 1-64 0v-64a32 32 0 0 1 32-32M195.2 195.2a32 32 0 0 1 45.248 0l45.248 45.248a32 32 0 1 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248m543.104 543.104a32 32 0 0 1 45.248 0l45.248 45.248a32 32 0 0 1-45.248 45.248l-45.248-45.248a32 32 0 0 1 0-45.248M64 512a32 32 0 0 1 32-32h64a32 32 0 0 1 0 64H96a32 32 0 0 1-32-32m768 0a32 32 0 0 1 32-32h64a32 32 0 1 1 0 64h-64a32 32 0 0 1-32-32M195.2 828.8a32 32 0 0 1 0-45.248l45.248-45.248a32 32 0 0 1 45.248 45.248L240.448 828.8a32 32 0 0 1-45.248 0m543.104-543.104a32 32 0 0 1 0-45.248l45.248-45.248a32 32 0 0 1 45.248 45.248l-45.248 45.248a32 32 0 0 1-45.248 0"})]))}}),Bre=Nre,Vre=q({name:"Sunrise",__name:"sunrise",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M32 768h960a32 32 0 1 1 0 64H32a32 32 0 1 1 0-64m129.408-96a352 352 0 0 1 701.184 0h-64.32a288 288 0 0 0-572.544 0zM512 128a32 32 0 0 1 32 32v96a32 32 0 0 1-64 0v-96a32 32 0 0 1 32-32m407.296 168.704a32 32 0 0 1 0 45.248l-67.84 67.84a32 32 0 1 1-45.248-45.248l67.84-67.84a32 32 0 0 1 45.248 0m-814.592 0a32 32 0 0 1 45.248 0l67.84 67.84a32 32 0 1 1-45.248 45.248l-67.84-67.84a32 32 0 0 1 0-45.248"})]))}}),zre=Vre,Fre=q({name:"Sunset",__name:"sunset",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M82.56 640a448 448 0 1 1 858.88 0h-67.2a384 384 0 1 0-724.288 0zM32 704h960q32 0 32 32t-32 32H32q-32 0-32-32t32-32m256 128h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32"})]))}}),Hre=Fre,Wre=q({name:"SwitchButton",__name:"switch-button",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M352 159.872V230.4a352 352 0 1 0 320 0v-70.528A416.128 416.128 0 0 1 512 960a416 416 0 0 1-160-800.128"}),C("path",{fill:"currentColor",d:"M512 64q32 0 32 32v320q0 32-32 32t-32-32V96q0-32 32-32"})]))}}),Kre=Wre,Ure=q({name:"SwitchFilled",__name:"switch-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M247.47 358.4v.04c.07 19.17 7.72 37.53 21.27 51.09s31.92 21.2 51.09 21.27c39.86 0 72.41-32.6 72.41-72.4s-32.6-72.36-72.41-72.36-72.36 32.55-72.36 72.36"}),C("path",{fill:"currentColor",d:"M492.38 128H324.7c-52.16 0-102.19 20.73-139.08 57.61a196.66 196.66 0 0 0-57.61 139.08V698.7c-.01 25.84 5.08 51.42 14.96 75.29s24.36 45.56 42.63 63.83 39.95 32.76 63.82 42.65a196.7 196.7 0 0 0 75.28 14.98h167.68c3.03 0 5.46-2.43 5.46-5.42V133.42c.6-2.99-1.83-5.42-5.46-5.42m-56.11 705.88H324.7c-17.76.13-35.36-3.33-51.75-10.18s-31.22-16.94-43.61-29.67c-25.3-25.35-39.81-59.1-39.81-95.32V324.69c-.13-17.75 3.33-35.35 10.17-51.74a131.7 131.7 0 0 1 29.64-43.62c25.39-25.3 59.14-39.81 95.36-39.81h111.57zm402.12-647.67a196.66 196.66 0 0 0-139.08-57.61H580.48c-3.03 0-4.82 2.43-4.82 4.82v757.16c-.6 2.99 1.79 5.42 5.42 5.42h118.23a196.7 196.7 0 0 0 139.08-57.61A196.66 196.66 0 0 0 896 699.31V325.29a196.7 196.7 0 0 0-57.61-139.08m-111.3 441.92c-42.83 0-77.82-34.99-77.82-77.82s34.98-77.82 77.82-77.82c42.83 0 77.82 34.99 77.82 77.82s-34.99 77.82-77.82 77.82"})]))}}),Gre=Ure,Yre=q({name:"Switch",__name:"switch",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M118.656 438.656a32 32 0 0 1 0-45.248L416 96l4.48-3.776A32 32 0 0 1 461.248 96l3.712 4.48a32.064 32.064 0 0 1-3.712 40.832L218.56 384H928a32 32 0 1 1 0 64H141.248a32 32 0 0 1-22.592-9.344M64 608a32 32 0 0 1 32-32h786.752a32 32 0 0 1 22.656 54.592L608 928l-4.48 3.776a32.064 32.064 0 0 1-40.832-49.024L805.632 640H96a32 32 0 0 1-32-32"})]))}}),jre=Yre,qre=q({name:"TakeawayBox",__name:"takeaway-box",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M832 384H192v448h640zM96 320h832V128H96zm800 64v480a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V384H64a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32h896a32 32 0 0 1 32 32v256a32 32 0 0 1-32 32zM416 512h192a32 32 0 0 1 0 64H416a32 32 0 0 1 0-64"})]))}}),Xre=qre,Zre=q({name:"Ticket",__name:"ticket",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M640 832H64V640a128 128 0 1 0 0-256V192h576v160h64V192h256v192a128 128 0 1 0 0 256v192H704V672h-64zm0-416v192h64V416z"})]))}}),Jre=Zre,Qre=q({name:"Tickets",__name:"tickets",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M192 128v768h640V128zm-32-64h704a32 32 0 0 1 32 32v832a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m160 448h384v64H320zm0-192h192v64H320zm0 384h384v64H320z"})]))}}),eoe=Qre,toe=q({name:"Timer",__name:"timer",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 896a320 320 0 1 0 0-640 320 320 0 0 0 0 640m0 64a384 384 0 1 1 0-768 384 384 0 0 1 0 768"}),C("path",{fill:"currentColor",d:"M512 320a32 32 0 0 1 32 32l-.512 224a32 32 0 1 1-64 0L480 352a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M448 576a64 64 0 1 0 128 0 64 64 0 1 0-128 0m96-448v128h-64V128h-96a32 32 0 0 1 0-64h256a32 32 0 1 1 0 64z"})]))}}),noe=toe,aoe=q({name:"ToiletPaper",__name:"toilet-paper",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M595.2 128H320a192 192 0 0 0-192 192v576h384V352c0-90.496 32.448-171.2 83.2-224M736 64c123.712 0 224 128.96 224 288S859.712 640 736 640H576v320H64V320A256 256 0 0 1 320 64zM576 352v224h160c84.352 0 160-97.28 160-224s-75.648-224-160-224-160 97.28-160 224"}),C("path",{fill:"currentColor",d:"M736 448c-35.328 0-64-43.008-64-96s28.672-96 64-96 64 43.008 64 96-28.672 96-64 96"})]))}}),roe=aoe,ooe=q({name:"Tools",__name:"tools",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M764.416 254.72a351.7 351.7 0 0 1 86.336 149.184H960v192.064H850.752a351.7 351.7 0 0 1-86.336 149.312l54.72 94.72-166.272 96-54.592-94.72a352.64 352.64 0 0 1-172.48 0L371.136 936l-166.272-96 54.72-94.72a351.7 351.7 0 0 1-86.336-149.312H64v-192h109.248a351.7 351.7 0 0 1 86.336-149.312L204.8 160l166.208-96h.192l54.656 94.592a352.64 352.64 0 0 1 172.48 0L652.8 64h.128L819.2 160l-54.72 94.72zM704 499.968a192 192 0 1 0-384 0 192 192 0 0 0 384 0"})]))}}),ioe=ooe,loe=q({name:"TopLeft",__name:"top-left",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M256 256h416a32 32 0 1 0 0-64H224a32 32 0 0 0-32 32v448a32 32 0 0 0 64 0z"}),C("path",{fill:"currentColor",d:"M246.656 201.344a32 32 0 0 0-45.312 45.312l544 544a32 32 0 0 0 45.312-45.312z"})]))}}),soe=loe,uoe=q({name:"TopRight",__name:"top-right",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M768 256H353.6a32 32 0 1 1 0-64H800a32 32 0 0 1 32 32v448a32 32 0 0 1-64 0z"}),C("path",{fill:"currentColor",d:"M777.344 201.344a32 32 0 0 1 45.312 45.312l-544 544a32 32 0 0 1-45.312-45.312z"})]))}}),coe=uoe,doe=q({name:"Top",__name:"top",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M572.235 205.282v600.365a30.118 30.118 0 1 1-60.235 0V205.282L292.382 438.633a28.913 28.913 0 0 1-42.646 0 33.43 33.43 0 0 1 0-45.236l271.058-288.045a28.913 28.913 0 0 1 42.647 0L834.5 393.397a33.43 33.43 0 0 1 0 45.176 28.913 28.913 0 0 1-42.647 0l-219.618-233.23z"})]))}}),foe=doe,voe=q({name:"TrendCharts",__name:"trend-charts",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128 896V128h768v768zm291.712-327.296 128 102.4 180.16-201.792-47.744-42.624-139.84 156.608-128-102.4-180.16 201.792 47.744 42.624zM816 352a48 48 0 1 0-96 0 48 48 0 0 0 96 0"})]))}}),hoe=voe,poe=q({name:"TrophyBase",__name:"trophy-base",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M918.4 201.6c-6.4-6.4-12.8-9.6-22.4-9.6H768V96c0-9.6-3.2-16-9.6-22.4S745.6 64 736 64H288c-9.6 0-16 3.2-22.4 9.6S256 86.4 256 96v96H128c-9.6 0-16 3.2-22.4 9.6S96 217.6 96 224c3.2 108.8 25.6 185.6 64 224 34.4 34.4 77.56 55.65 127.65 61.99 10.91 20.44 24.78 39.25 41.95 56.41 40.86 40.86 91 65.47 150.4 71.9V768h-96c-9.6 0-16 3.2-22.4 9.6S352 790.4 352 800s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6h256c9.6 0 16-3.2 22.4-9.6s9.6-12.8 9.6-22.4-3.2-16-9.6-22.4-12.8-9.6-22.4-9.6h-96V637.26c59.4-7.71 109.54-30.01 150.4-70.86 17.2-17.2 31.51-36.06 42.81-56.55 48.93-6.51 90.02-27.7 126.79-61.85 38.4-38.4 60.8-112 64-224 0-6.4-3.2-16-9.6-22.4M256 438.4c-19.2-6.4-35.2-19.2-51.2-35.2-22.4-22.4-35.2-70.4-41.6-147.2H256zm390.4 80C608 553.6 566.4 576 512 576s-99.2-19.2-134.4-57.6S320 438.4 320 384V128h384v256q0 81.6-57.6 134.4m172.8-115.2c-16 16-32 25.6-51.2 35.2V256h92.8c-6.4 76.8-19.2 124.8-41.6 147.2M768 896H256c-9.6 0-16 3.2-22.4 9.6S224 918.4 224 928s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6h512c9.6 0 16-3.2 22.4-9.6s9.6-12.8 9.6-22.4-3.2-16-9.6-22.4-12.8-9.6-22.4-9.6"})]))}}),moe=poe,goe=q({name:"Trophy",__name:"trophy",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M480 896V702.08A256.26 256.26 0 0 1 264.064 512h-32.64a96 96 0 0 1-91.968-68.416L93.632 290.88a76.8 76.8 0 0 1 73.6-98.88H256V96a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v96h88.768a76.8 76.8 0 0 1 73.6 98.88L884.48 443.52A96 96 0 0 1 792.576 512h-32.64A256.26 256.26 0 0 1 544 702.08V896h128a32 32 0 1 1 0 64H352a32 32 0 1 1 0-64zm224-448V128H320v320a192 192 0 1 0 384 0m64 0h24.576a32 32 0 0 0 30.656-22.784l45.824-152.768A12.8 12.8 0 0 0 856.768 256H768zm-512 0V256h-88.768a12.8 12.8 0 0 0-12.288 16.448l45.824 152.768A32 32 0 0 0 231.424 448z"})]))}}),yoe=goe,boe=q({name:"TurnOff",__name:"turn-off",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M329.956 257.138a254.862 254.862 0 0 0 0 509.724h364.088a254.862 254.862 0 0 0 0-509.724zm0-72.818h364.088a327.68 327.68 0 1 1 0 655.36H329.956a327.68 327.68 0 1 1 0-655.36"}),C("path",{fill:"currentColor",d:"M329.956 621.227a109.227 109.227 0 1 0 0-218.454 109.227 109.227 0 0 0 0 218.454m0 72.817a182.044 182.044 0 1 1 0-364.088 182.044 182.044 0 0 1 0 364.088"})]))}}),_oe=boe,woe=q({name:"Umbrella",__name:"umbrella",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M320 768a32 32 0 1 1 64 0 64 64 0 0 0 128 0V512H64a448 448 0 1 1 896 0H576v256a128 128 0 1 1-256 0m570.688-320a384.128 384.128 0 0 0-757.376 0z"})]))}}),Soe=woe,Coe=q({name:"Unlock",__name:"unlock",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M224 448a32 32 0 0 0-32 32v384a32 32 0 0 0 32 32h576a32 32 0 0 0 32-32V480a32 32 0 0 0-32-32zm0-64h576a96 96 0 0 1 96 96v384a96 96 0 0 1-96 96H224a96 96 0 0 1-96-96V480a96 96 0 0 1 96-96"}),C("path",{fill:"currentColor",d:"M512 544a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V576a32 32 0 0 1 32-32m178.304-295.296A192.064 192.064 0 0 0 320 320v64h352l96 38.4V448H256V320a256 256 0 0 1 493.76-95.104z"})]))}}),xoe=Coe,koe=q({name:"UploadFilled",__name:"upload-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M544 864V672h128L512 480 352 672h128v192H320v-1.6c-5.376.32-10.496 1.6-16 1.6A240 240 0 0 1 64 624c0-123.136 93.12-223.488 212.608-237.248A239.81 239.81 0 0 1 512 192a239.87 239.87 0 0 1 235.456 194.752c119.488 13.76 212.48 114.112 212.48 237.248a240 240 0 0 1-240 240c-5.376 0-10.56-1.28-16-1.6v1.6z"})]))}}),Toe=koe,Moe=q({name:"Upload",__name:"upload",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-578.304V704h-64V247.296L237.248 490.048 192 444.8 508.8 128l316.8 316.8-45.312 45.248z"})]))}}),Eoe=Moe,Aoe=q({name:"UserFilled",__name:"user-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M288 320a224 224 0 1 0 448 0 224 224 0 1 0-448 0m544 608H160a32 32 0 0 1-32-32v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 0 1-32 32z"})]))}}),Ioe=Aoe,Loe=q({name:"User",__name:"user",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 512a192 192 0 1 0 0-384 192 192 0 0 0 0 384m0 64a256 256 0 1 1 0-512 256 256 0 0 1 0 512m320 320v-96a96 96 0 0 0-96-96H288a96 96 0 0 0-96 96v96a32 32 0 1 1-64 0v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 1 1-64 0"})]))}}),Poe=Loe,Doe=q({name:"Van",__name:"van",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M128.896 736H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32h576a32 32 0 0 1 32 32v96h164.544a32 32 0 0 1 31.616 27.136l54.144 352A32 32 0 0 1 922.688 736h-91.52a144 144 0 1 1-286.272 0H415.104a144 144 0 1 1-286.272 0zm23.36-64a143.872 143.872 0 0 1 239.488 0H568.32c17.088-25.6 42.24-45.376 71.744-55.808V256H128v416zm655.488 0h77.632l-19.648-128H704v64.896A144 144 0 0 1 807.744 672m48.128-192-14.72-96H704v96zM688 832a80 80 0 1 0 0-160 80 80 0 0 0 0 160m-416 0a80 80 0 1 0 0-160 80 80 0 0 0 0 160"})]))}}),Ooe=Doe,$oe=q({name:"VideoCameraFilled",__name:"video-camera-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m768 576 192-64v320l-192-64v96a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V480a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zM192 768v64h384v-64zm192-480a160 160 0 0 1 320 0 160 160 0 0 1-320 0m64 0a96 96 0 1 0 192.064-.064A96 96 0 0 0 448 288m-320 32a128 128 0 1 1 256.064.064A128 128 0 0 1 128 320m64 0a64 64 0 1 0 128 0 64 64 0 0 0-128 0"})]))}}),Roe=$oe,Noe=q({name:"VideoCamera",__name:"video-camera",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M704 768V256H128v512zm64-416 192-96v512l-192-96v128a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32zm0 71.552v176.896l128 64V359.552zM192 320h192v64H192z"})]))}}),Boe=Noe,Voe=q({name:"VideoPause",__name:"video-pause",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m-96-544q32 0 32 32v256q0 32-32 32t-32-32V384q0-32 32-32m192 0q32 0 32 32v256q0 32-32 32t-32-32V384q0-32 32-32"})]))}}),zoe=Voe,Foe=q({name:"VideoPlay",__name:"video-play",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m-48-247.616L668.608 512 464 375.616zm10.624-342.656 249.472 166.336a48 48 0 0 1 0 79.872L474.624 718.272A48 48 0 0 1 400 678.336V345.6a48 48 0 0 1 74.624-39.936z"})]))}}),Hoe=Foe,Woe=q({name:"View",__name:"view",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288m0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.19 160.19 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160"})]))}}),U6=Woe,Koe=q({name:"WalletFilled",__name:"wallet-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M688 512a112 112 0 1 0 0 224h208v160H128V352h768v160zm32 160h-32a48 48 0 0 1 0-96h32a48 48 0 0 1 0 96m-80-544 128 160H384z"})]))}}),Uoe=Koe,Goe=q({name:"Wallet",__name:"wallet",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M640 288h-64V128H128v704h384v32a32 32 0 0 0 32 32H96a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32h512a32 32 0 0 1 32 32z"}),C("path",{fill:"currentColor",d:"M128 320v512h768V320zm-32-64h832a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M704 640a64 64 0 1 1 0-128 64 64 0 0 1 0 128"})]))}}),Yoe=Goe,joe=q({name:"WarnTriangleFilled",__name:"warn-triangle-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg","xml:space":"preserve",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M928.99 755.83 574.6 203.25c-12.89-20.16-36.76-32.58-62.6-32.58s-49.71 12.43-62.6 32.58L95.01 755.83c-12.91 20.12-12.9 44.91.01 65.03 12.92 20.12 36.78 32.51 62.59 32.49h708.78c25.82.01 49.68-12.37 62.59-32.49s12.92-44.91.01-65.03M554.67 768h-85.33v-85.33h85.33zm0-426.67v298.66h-85.33V341.32z"})]))}}),qoe=joe,Xoe=q({name:"WarningFilled",__name:"warning-filled",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 192a58.43 58.43 0 0 0-58.24 63.744l23.36 256.384a35.072 35.072 0 0 0 69.76 0l23.296-256.384A58.43 58.43 0 0 0 512 256m0 512a51.2 51.2 0 1 0 0-102.4 51.2 51.2 0 0 0 0 102.4"})]))}}),$s=Xoe,Zoe=q({name:"Warning",__name:"warning",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m48-176a48 48 0 1 1-96 0 48 48 0 0 1 96 0m-48-464a32 32 0 0 1 32 32v288a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32"})]))}}),Joe=Zoe,Qoe=q({name:"Watch",__name:"watch",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M512 768a256 256 0 1 0 0-512 256 256 0 0 0 0 512m0 64a320 320 0 1 1 0-640 320 320 0 0 1 0 640"}),C("path",{fill:"currentColor",d:"M480 352a32 32 0 0 1 32 32v160a32 32 0 0 1-64 0V384a32 32 0 0 1 32-32"}),C("path",{fill:"currentColor",d:"M480 512h128q32 0 32 32t-32 32H480q-32 0-32-32t32-32m128-256V128H416v128h-64V64h320v192zM416 768v128h192V768h64v192H352V768z"})]))}}),eie=Qoe,tie=q({name:"Watermelon",__name:"watermelon",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m683.072 600.32-43.648 162.816-61.824-16.512 53.248-198.528L576 493.248l-158.4 158.4-45.248-45.248 158.4-158.4-55.616-55.616-198.528 53.248-16.512-61.824 162.816-43.648L282.752 200A384 384 0 0 0 824 741.248zm231.552 141.056a448 448 0 1 1-632-632z"})]))}}),nie=tie,aie=q({name:"WindPower",__name:"wind-power",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"M160 64q32 0 32 32v832q0 32-32 32t-32-32V96q0-32 32-32m416 354.624 128-11.584V168.96l-128-11.52v261.12zm-64 5.824V151.552L320 134.08V160h-64V64l616.704 56.064A96 96 0 0 1 960 215.68v144.64a96 96 0 0 1-87.296 95.616L256 512V224h64v217.92zm256-23.232 98.88-8.96A32 32 0 0 0 896 360.32V215.68a32 32 0 0 0-29.12-31.872l-98.88-8.96z"})]))}}),rie=aie,oie=q({name:"ZoomIn",__name:"zoom-in",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704m-32-384v-96a32 32 0 0 1 64 0v96h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64z"})]))}}),G6=oie,iie=q({name:"ZoomOut",__name:"zoom-out",setup(e){return(t,n)=>(x(),O("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[C("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704M352 448h256a32 32 0 0 1 0 64H352a32 32 0 0 1 0-64"})]))}}),GI=iie;const lie=Object.freeze(Object.defineProperty({__proto__:null,AddLocation:dX,Aim:vX,AlarmClock:pX,Apple:gX,ArrowDown:ui,ArrowDownBold:bX,ArrowLeft:Ji,ArrowLeftBold:SX,ArrowRight:za,ArrowRightBold:kX,ArrowUp:ip,ArrowUpBold:EX,Avatar:LX,Back:CI,Baseball:OX,Basketball:RX,Bell:zX,BellFilled:BX,Bicycle:HX,Bottom:jX,BottomLeft:KX,BottomRight:GX,Bowl:XX,Box:JX,Briefcase:eZ,Brush:rZ,BrushFilled:nZ,Burger:iZ,Calendar:xI,Camera:kI,CameraFilled:uZ,CaretBottom:fZ,CaretLeft:hZ,CaretRight:F6,CaretTop:TI,Cellphone:yZ,ChatDotRound:_Z,ChatDotSquare:SZ,ChatLineRound:xZ,ChatLineSquare:TZ,ChatRound:EZ,ChatSquare:IZ,Check:Os,Checked:DZ,Cherry:$Z,Chicken:NZ,ChromeFilled:VZ,CircleCheck:Ao,CircleCheckFilled:MI,CircleClose:ci,CircleCloseFilled:fc,CirclePlus:YZ,CirclePlusFilled:UZ,Clock:H6,Close:so,CloseBold:XZ,Cloudy:QZ,Coffee:aJ,CoffeeCup:tJ,Coin:oJ,ColdDrink:lJ,Collection:dJ,CollectionTag:uJ,Comment:vJ,Compass:pJ,Connection:p4,Coordinate:yJ,CopyDocument:_J,Cpu:SJ,CreditCard:xJ,Crop:TJ,DArrowLeft:Al,DArrowRight:Il,DCaret:IJ,DataAnalysis:PJ,DataBoard:OJ,DataLine:RJ,Delete:W6,DeleteFilled:BJ,DeleteLocation:zJ,Dessert:WJ,Discount:UJ,Dish:qJ,DishDot:YJ,Document:EI,DocumentAdd:ZJ,DocumentChecked:QJ,DocumentCopy:tQ,DocumentDelete:aQ,DocumentRemove:oQ,Download:AI,Drizzling:uQ,Edit:vQ,EditPen:dQ,Eleme:gQ,ElemeFilled:pQ,ElementPlus:bQ,Expand:wQ,Failed:CQ,Female:kQ,Files:MQ,Film:AQ,Filter:LQ,Finished:DQ,FirstAidKit:$Q,Flag:NQ,Fold:VQ,Folder:ZQ,FolderAdd:FQ,FolderChecked:WQ,FolderDelete:UQ,FolderOpened:YQ,FolderRemove:qQ,Food:QQ,Football:tee,ForkSpoon:aee,Fries:oee,FullScreen:II,Goblet:hee,GobletFull:see,GobletSquare:fee,GobletSquareFull:cee,GoldMedal:mee,Goods:_ee,GoodsFilled:yee,Grape:See,Grid:xee,Guide:Tee,Handbag:Eee,Headset:Iee,Help:Oee,HelpFilled:Pee,Hide:LI,Histogram:Nee,HomeFilled:Vee,HotWater:Fee,House:Wee,IceCream:qee,IceCreamRound:Uee,IceCreamSquare:Yee,IceDrink:Zee,IceTea:Qee,InfoFilled:Ud,Iphone:nte,Key:PI,KnifeFork:ote,Lightning:lte,Link:DI,List:cte,Loading:Mr,Location:gte,LocationFilled:vte,LocationInformation:pte,Lock:bte,Lollipop:wte,MagicStick:Cte,Magnet:kte,Male:Mte,Management:Ate,MapLocation:Lte,Medal:Dte,Memo:$te,Menu:Nte,Message:Fte,MessageBox:Vte,Mic:Wte,Microphone:Ute,MilkTea:Yte,Minus:OI,Money:Xte,Monitor:Jte,Moon:nne,MoonNight:ene,More:$I,MoreFilled:m4,MostlyCloudy:ine,Mouse:sne,Mug:cne,Mute:hne,MuteNotification:fne,NoSmoking:mne,Notebook:yne,Notification:_ne,Odometer:Sne,OfficeBuilding:xne,Open:Tne,Operation:Ene,Opportunity:Ine,Orange:Pne,Paperclip:One,PartlyCloudy:Rne,Pear:Bne,Phone:Hne,PhoneFilled:zne,Picture:NI,PictureFilled:RI,PictureRounded:Une,PieChart:jne,Place:Xne,Platform:Jne,Plus:hf,Pointer:tae,Position:aae,Postcard:oae,Pouring:lae,Present:uae,PriceTag:dae,Printer:vae,Promotion:pae,QuartzWatch:gae,QuestionFilled:BI,Rank:_ae,Reading:xae,ReadingLamp:Sae,Refresh:g4,RefreshLeft:VI,RefreshRight:zI,Refrigerator:Aae,Remove:Dae,RemoveFilled:Lae,Right:$ae,ScaleToOriginal:FI,School:Bae,Scissor:zae,Search:HI,Select:Wae,Sell:Uae,SemiSelect:Yae,Service:qae,SetUp:Zae,Setting:Qae,Share:tre,Ship:are,Shop:ore,ShoppingBag:lre,ShoppingCart:dre,ShoppingCartFull:ure,ShoppingTrolley:vre,Smoking:pre,Soccer:gre,SoldOut:bre,Sort:Cre,SortDown:WI,SortUp:KI,Stamp:kre,Star:UI,StarFilled:rv,Stopwatch:Are,SuccessFilled:K6,Sugar:Pre,Suitcase:Rre,SuitcaseLine:Ore,Sunny:Bre,Sunrise:zre,Sunset:Hre,Switch:jre,SwitchButton:Kre,SwitchFilled:Gre,TakeawayBox:Xre,Ticket:Jre,Tickets:eoe,Timer:noe,ToiletPaper:roe,Tools:ioe,Top:foe,TopLeft:soe,TopRight:coe,TrendCharts:hoe,Trophy:yoe,TrophyBase:moe,TurnOff:_oe,Umbrella:Soe,Unlock:xoe,Upload:Eoe,UploadFilled:Toe,User:Poe,UserFilled:Ioe,Van:Ooe,VideoCamera:Boe,VideoCameraFilled:Roe,VideoPause:zoe,VideoPlay:Hoe,View:U6,Wallet:Yoe,WalletFilled:Uoe,WarnTriangleFilled:qoe,Warning:Joe,WarningFilled:$s,Watch:eie,Watermelon:nie,WindPower:rie,ZoomIn:G6,ZoomOut:GI},Symbol.toStringTag,{value:"Module"})),pn=ae([String,Object,Function]),YI={Close:so},Y6={Close:so,SuccessFilled:K6,InfoFilled:Ud,WarningFilled:$s,CircleCloseFilled:fc},Rs={primary:Ud,success:K6,warning:$s,error:fc,info:Ud},F1={validating:Mr,success:Ao,error:ci},sie=["light","dark"],uie=$e({title:{type:String,default:""},description:{type:String,default:""},type:{type:String,values:sh(Rs),default:"info"},closable:{type:Boolean,default:!0},closeText:{type:String,default:""},showIcon:Boolean,center:Boolean,effect:{type:String,values:sie,default:"light"}}),cie={close:e=>e instanceof MouseEvent},die=$e({size:{type:ae([Number,String])},color:{type:String}});var fie=q({name:"ElIcon",inheritAttrs:!1,__name:"icon",props:die,setup(e){const t=e,n=ke("icon"),a=D(()=>{const{size:r,color:o}=t,i=Vn(r);return!i&&!o?{}:{fontSize:i,"--color":o}});return(r,o)=>(x(),O("i",Tt({class:p(n).b(),style:a.value},r.$attrs),[ce(r.$slots,"default")],16))}}),vie=fie;const Xe=wt(vie);var hie=q({name:"ElAlert",__name:"alert",props:uie,emits:cie,setup(e,{emit:t}){const{Close:n}=Y6,a=e,r=t,o=Xn(),i=ke("alert"),l=F(!0),s=D(()=>Rs[a.type]),u=D(()=>{var v;if(a.description)return!0;const d=(v=o.default)==null?void 0:v.call(o);return d?eo(d).some(f=>!gI(f)):!1}),c=d=>{l.value=!1,r("close",d)};return(d,v)=>(x(),ne(Pa,{name:p(i).b("fade"),persisted:""},{default:$(()=>[st(C("div",{class:U([p(i).b(),p(i).m(e.type),p(i).is("center",e.center),p(i).is(e.effect)]),role:"alert"},[e.showIcon&&(d.$slots.icon||s.value)?(x(),ne(p(Xe),{key:0,class:U([p(i).e("icon"),p(i).is("big",u.value)])},{default:$(()=>[ce(d.$slots,"icon",{},()=>[(x(),ne(St(s.value)))])]),_:3},8,["class"])):te("v-if",!0),C("div",{class:U(p(i).e("content"))},[e.title||d.$slots.title?(x(),O("span",{key:0,class:U([p(i).e("title"),{"with-description":u.value}])},[ce(d.$slots,"title",{},()=>[ue(Q(e.title),1)])],2)):te("v-if",!0),u.value?(x(),O("p",{key:1,class:U(p(i).e("description"))},[ce(d.$slots,"default",{},()=>[ue(Q(e.description),1)])],2)):te("v-if",!0),e.closable?(x(),O(Ve,{key:2},[e.closeText?(x(),O("div",{key:0,class:U([p(i).e("close-btn"),p(i).is("customed")]),onClick:c},Q(e.closeText),3)):(x(),ne(p(Xe),{key:1,class:U(p(i).e("close-btn")),onClick:c},{default:$(()=>[L(p(n))]),_:1},8,["class"]))],64)):te("v-if",!0)],2)],2),[[Zt,l.value]])]),_:3},8,["name"]))}}),pie=hie;const mie=wt(pie),jI=["dialog","grid","group","listbox","menu","navigation","tooltip","tree"],qI=$e({role:{type:String,values:jI,default:"tooltip"}}),j6=Symbol("popper"),XI=Symbol("popperContent");var gie=q({name:"ElPopperArrow",inheritAttrs:!1,__name:"arrow",setup(e,{expose:t}){const n=ke("popper"),{arrowRef:a,arrowStyle:r}=Ze(XI,void 0);return rn(()=>{a.value=void 0}),t({arrowRef:a}),(o,i)=>(x(),O("span",{ref_key:"arrowRef",ref:a,class:U(p(n).e("arrow")),style:et(p(r)),"data-popper-arrow":""},null,6))}}),yie=gie;const ZI=$e({virtualRef:{type:ae(Object)},virtualTriggering:Boolean,onMouseenter:{type:ae(Function)},onMouseleave:{type:ae(Function)},onClick:{type:ae(Function)},onKeydown:{type:ae(Function)},onFocus:{type:ae(Function)},onBlur:{type:ae(Function)},onContextmenu:{type:ae(Function)},id:String,open:Boolean}),G2="ElOnlyChild",JI=q({name:G2,setup(e,{slots:t,attrs:n}){var r;const a=sq(((r=Ze(aI))==null?void 0:r.setForwardRef)??kt);return()=>{var s;const o=(s=t.default)==null?void 0:s.call(t,n);if(!o)return null;const[i,l]=QI(o);return i?(l>1&&un(G2,"requires exact only one valid child."),st(ei(i,n),[[a]])):(un(G2,"no valid child node found"),null)}}});function QI(e){if(!e)return[null,0];const t=e,n=t.filter(a=>a.type!==oa).length;for(const a of t){if(_t(a))switch(a.type){case oa:continue;case uf:case"svg":return[p5(a),n];case Ve:return QI(a.children);default:return[a,n]}return[p5(a),n]}return[null,0]}function p5(e){return L("span",{class:ke("only-child").e("content")},[e])}var bie=q({name:"ElPopperTrigger",inheritAttrs:!1,__name:"trigger",props:ZI,setup(e,{expose:t}){const n=e,{role:a,triggerRef:r}=Ze(j6,void 0);lq(r);const o=D(()=>l.value?n.id:void 0),i=D(()=>{if(a&&a.value==="tooltip")return n.open&&n.id?n.id:void 0}),l=D(()=>{if(a&&a.value!=="tooltip")return a.value}),s=D(()=>l.value?`${n.open}`:void 0);let u;const c=["onMouseenter","onMouseleave","onClick","onKeydown","onFocus","onBlur","onContextmenu"];return mt(()=>{ye(()=>n.virtualRef,d=>{d&&(r.value=da(d))},{immediate:!0}),ye(r,(d,v)=>{u==null||u(),u=void 0,Vr(v)&&c.forEach(f=>{const h=n[f];h&&v.removeEventListener(f.slice(2).toLowerCase(),h,["onFocus","onBlur"].includes(f))}),Vr(d)&&(c.forEach(f=>{const h=n[f];h&&d.addEventListener(f.slice(2).toLowerCase(),h,["onFocus","onBlur"].includes(f))}),sc(d)&&(u=ye([o,i,l,s],f=>{["aria-controls","aria-describedby","aria-haspopup","aria-expanded"].forEach((h,m)=>{Un(f[m])?d.removeAttribute(h):d.setAttribute(h,f[m])})},{immediate:!0}))),Vr(v)&&sc(v)&&["aria-controls","aria-describedby","aria-haspopup","aria-expanded"].forEach(f=>v.removeAttribute(f))},{immediate:!0})}),rn(()=>{if(u==null||u(),u=void 0,r.value&&Vr(r.value)){const d=r.value;c.forEach(v=>{const f=n[v];f&&d.removeEventListener(v.slice(2).toLowerCase(),f,["onFocus","onBlur"].includes(v))}),r.value=void 0}}),t({triggerRef:r}),(d,v)=>e.virtualTriggering?te("v-if",!0):(x(),ne(p(JI),Tt({key:0},d.$attrs,{"aria-controls":o.value,"aria-describedby":i.value,"aria-expanded":s.value,"aria-haspopup":l.value}),{default:$(()=>[ce(d.$slots,"default")]),_:3},16,["aria-controls","aria-describedby","aria-expanded","aria-haspopup"]))}}),_ie=bie;const eL=$e({arrowOffset:{type:Number,default:5}}),wie={arrowOffset:5},Sie=$e({boundariesPadding:{type:Number,default:0},fallbackPlacements:{type:ae(Array),default:void 0},gpuAcceleration:{type:Boolean,default:!0},offset:{type:Number,default:12},placement:{type:String,values:tl,default:"bottom"},popperOptions:{type:ae(Object),default:()=>({})},strategy:{type:String,values:["fixed","absolute"],default:"absolute"}}),tL=$e({...Sie,...eL,id:String,style:{type:ae([String,Array,Object,Boolean]),default:void 0},className:{type:ae([String,Array,Object,Boolean])},effect:{type:ae(String),default:"dark"},visible:Boolean,enterable:{type:Boolean,default:!0},pure:Boolean,focusOnShow:Boolean,trapping:Boolean,popperClass:{type:ae([String,Array,Object,Boolean]),default:void 0},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},referenceEl:{type:ae(Object)},triggerTargetEl:{type:ae(Object)},stopPopperMouseEvent:{type:Boolean,default:!0},virtualTriggering:Boolean,zIndex:Number,...dr(["ariaLabel"]),loop:Boolean}),Cie={boundariesPadding:0,gpuAcceleration:!0,offset:12,placement:"bottom",popperOptions:()=>({}),strategy:"absolute"},xie={...Cie,...wie,effect:"dark",enterable:!0,stopPopperMouseEvent:!0,visible:!1,pure:!1,focusOnShow:!1,trapping:!1,virtualTriggering:!1,loop:!1,style:void 0,popperStyle:void 0,popperClass:void 0},kie={mouseenter:e=>e instanceof MouseEvent,mouseleave:e=>e instanceof MouseEvent,focus:()=>!0,blur:()=>!0,close:()=>!0},Tie=$e({size:{type:String,values:ii},disabled:Boolean}),Mie=$e({...Tie,model:Object,rules:{type:ae(Object)},labelPosition:{type:String,values:["left","right","top"],default:"right"},requireAsteriskPosition:{type:String,values:["left","right"],default:"left"},labelWidth:{type:[String,Number],default:""},labelSuffix:{type:String,default:""},inline:Boolean,inlineMessage:Boolean,statusIcon:Boolean,showMessage:{type:Boolean,default:!0},validateOnRuleChange:{type:Boolean,default:!0},hideRequiredAsterisk:Boolean,scrollToError:Boolean,scrollIntoViewOptions:{type:ae([Object,Boolean]),default:!0}}),Eie={validate:(e,t,n)=>(Ae(e)||Ye(e))&&vn(t)&&Ye(n)},Ec=Symbol("formContextKey"),Qi=Symbol("formItemContextKey"),ca=(e,t={})=>{const n=F(void 0),a=t.prop?n:WA("size"),r=t.global?n:bI(),o=t.form?{size:void 0}:Ze(Ec,void 0),i=t.formItem?{size:void 0}:Ze(Qi,void 0);return D(()=>a.value||p(e)||(i==null?void 0:i.size)||(o==null?void 0:o.size)||r.value||"")},$n=e=>{const t=WA("disabled"),n=Ze(Ec,void 0);return D(()=>t.value??p(e)??(n==null?void 0:n.disabled)??!1)},Ea=()=>({form:Ze(Ec,void 0),formItem:Ze(Qi,void 0)}),Yr=(e,{formItemContext:t,disableIdGeneration:n,disableIdManagement:a})=>{n||(n=F(!1)),a||(a=F(!1));const r=Et(),o=()=>{let u=r==null?void 0:r.parent;for(;u;){if(u.type.name==="ElFormItem")return!1;if(u.type.name==="ElLabelWrap")return!0;u=u.parent}return!1},i=F();let l;const s=D(()=>{var u;return!!(!(e.label||e.ariaLabel)&&t&&t.inputIds&&((u=t.inputIds)==null?void 0:u.length)<=1)});return mt(()=>{l=ye([cn(e,"id"),n],([u,c])=>{const d=u??(c?void 0:Fa().value);d!==i.value&&(t!=null&&t.removeInputId&&!o()&&(i.value&&t.removeInputId(i.value),!(a!=null&&a.value)&&!c&&d&&t.addInputId(d)),i.value=d)},{immediate:!0})}),Us(()=>{l&&l(),t!=null&&t.removeInputId&&i.value&&t.removeInputId(i.value)}),{isLabeledByFormItem:s,inputId:i}},Aie=["","error","validating","success"],Iie=$e({label:String,labelWidth:{type:[String,Number]},labelPosition:{type:String,values:["left","right","top",""],default:""},prop:{type:ae([String,Array])},required:{type:Boolean,default:void 0},rules:{type:ae([Object,Array])},error:String,validateStatus:{type:String,values:Aie},for:String,inlineMessage:{type:Boolean,default:void 0},showMessage:{type:Boolean,default:!0},size:{type:String,values:ii}}),m5=e=>[...new Set(e)],Pf=e=>Ae(e)?e[0]:e,or=e=>!e&&e!==0?[]:Ae(e)?e:[e],Lie="ElForm";function Pie(){const e=F([]),t=D(()=>{if(!e.value.length)return"0";const o=Math.max(...e.value);return o?`${o}px`:""});function n(o){const i=e.value.indexOf(o);return i===-1&&t.value==="0"&&un(Lie,`unexpected width ${o}`),i}function a(o,i){if(o&&i){const l=n(i);e.value.splice(l,1,o)}else o&&e.value.push(o)}function r(o){const i=n(o);i>-1&&e.value.splice(i,1)}return{autoLabelWidth:t,registerLabelWidth:a,deregisterLabelWidth:r}}const Rp=(e,t)=>{const n=wa(t).map(a=>Ae(a)?a.join("."):a);return n.length>0?e.filter(a=>a.propString&&n.includes(a.propString)):e},Dc="ElForm";var Die=q({name:Dc,__name:"form",props:Mie,emits:Eie,setup(e,{expose:t,emit:n}){const a=e,r=n,o=F(),i=nn([]),l=new Map,s=ca(),u=ke("form"),c=D(()=>{const{labelPosition:T,inline:M}=a;return[u.b(),u.m(s.value||"default"),{[u.m(`label-${T}`)]:T,[u.m("inline")]:M}]}),d=T=>Rp(i,[T])[0],v=T=>{i.includes(T)||i.push(T),T.propString&&(l.has(T.propString)?T.setInitialValue(l.get(T.propString)):l.set(T.propString,Li(T.fieldValue)))},f=(T,M)=>{if(M){l.delete(M);return}const E=i.indexOf(T);E>-1&&(i.splice(E,1),T.propString&&l.set(T.propString,Li(T.getInitialValue())))},h=T=>{if(!a.model){un(Dc,"model is required for setInitialValues to work.");return}if(!T){un(Dc,"initModel is required for setInitialValues to work.");return}for(const M of l.keys())l.set(M,Li(Cs(T,M).value));i.forEach(M=>{M.prop&&M.setInitialValue(Cs(T,M.prop).value)})},m=(T=[])=>{if(!a.model){un(Dc,"model is required for resetFields to work.");return}Rp(i,T).forEach(A=>A.resetField());const M=new Set(i.map(A=>A.propString).filter(Boolean)),E=T.length>0?wa(T).map(A=>Ae(A)?A.join("."):A):[...l.keys()];for(const A of E)!M.has(A)&&l.has(A)&&(Cs(a.model,A).value=Li(l.get(A)))},g=(T=[])=>{Rp(i,T).forEach(M=>M.clearValidate())},y=D(()=>{const T=!!a.model;return T||un(Dc,"model is required for validate to work."),T}),b=T=>{if(i.length===0)return[];const M=Rp(i,T);return M.length?M:(un(Dc,"please pass correct props!"),[])},_=async T=>S(void 0,T),w=async(T=[])=>{if(!y.value)return!1;const M=b(T);if(M.length===0)return!0;let E={};for(const A of M)try{await A.validate(""),A.validateState==="error"&&!A.error&&A.resetField()}catch(I){E={...E,...I}}return Object.keys(E).length===0?!0:Promise.reject(E)},S=async(T=[],M)=>{var I;let E=!1;const A=!Qe(M);try{return E=await w(T),E===!0&&await(M==null?void 0:M(E)),E}catch(P){if(P instanceof Error)throw P;const R=P;return a.scrollToError&&o.value&&((I=o.value.querySelector(`.${u.b()}-item.is-error`))==null||I.scrollIntoView(a.scrollIntoViewOptions)),!E&&await(M==null?void 0:M(!1,R)),A&&Promise.reject(R)}},k=T=>{var E;const M=d(T);M&&((E=M.$el)==null||E.scrollIntoView(a.scrollIntoViewOptions))};return ye(()=>a.rules,()=>{a.validateOnRuleChange&&_().catch(kt)},{deep:!0,flush:"post"}),Ft(Ec,nn({...Ta(a),emit:r,resetFields:m,clearValidate:g,validateField:S,getField:d,addField:v,removeField:f,setInitialValues:h,...Pie()})),t({validate:_,validateField:S,resetFields:m,clearValidate:g,scrollToField:k,getField:d,fields:i,setInitialValues:h}),(T,M)=>(x(),O("form",{ref_key:"formRef",ref:o,class:U(c.value)},[ce(T.$slots,"default")],2))}}),Oie=Die;const g5="ElLabelWrap";var $ie=q({name:g5,props:{isAutoWidth:Boolean,updateAll:Boolean},setup(e,{slots:t}){const n=Ze(Ec,void 0),a=Ze(Qi);a||Dn(g5,"usage: ");const r=ke("form"),o=F(),i=F(0),l=()=>{var c;if((c=o.value)!=null&&c.firstElementChild){const d=window.getComputedStyle(o.value.firstElementChild).width;return Math.ceil(Number.parseFloat(d))}else return 0},s=(c="update")=>{He(()=>{t.default&&e.isAutoWidth&&(c==="update"?i.value=l():c==="remove"&&(n==null||n.deregisterLabelWidth(i.value)))})},u=()=>s("update");return mt(()=>{u()}),rn(()=>{s("remove")}),oi(()=>u()),ye(i,(c,d)=>{e.updateAll&&(n==null||n.registerLabelWidth(c,d))}),Mn(D(()=>{var c;return((c=o.value)==null?void 0:c.firstElementChild)??null}),u),()=>{var d,v;if(!t)return null;const{isAutoWidth:c}=e;if(c){const f=n==null?void 0:n.autoLabelWidth,h=a==null?void 0:a.hasLabel,m={};if(h&&f&&f!=="auto"){const g=Math.max(0,Number.parseInt(f,10)-i.value),y=(a.labelPosition||n.labelPosition)==="left"?"marginRight":"marginLeft";g&&(m[y]=`${g}px`)}return L("div",{ref:o,class:[r.be("item","label-wrap")],style:m},[(d=t.default)==null?void 0:d.call(t)])}else return L(Ve,{ref:o},[(v=t.default)==null?void 0:v.call(t)])}}});function $u(){return $u=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function J0(e,t,n){return Nie()?J0=Reflect.construct.bind():J0=function(r,o,i){var l=[null];l.push.apply(l,o);var s=Function.bind.apply(r,l),u=new s;return i&&vh(u,i.prototype),u},J0.apply(null,arguments)}function Bie(e){return Function.toString.call(e).indexOf("[native code]")!==-1}function b4(e){var t=typeof Map=="function"?new Map:void 0;return b4=function(a){if(a===null||!Bie(a))return a;if(typeof a!="function")throw new TypeError("Super expression must either be null or a function");if(typeof t<"u"){if(t.has(a))return t.get(a);t.set(a,r)}function r(){return J0(a,arguments,y4(this).constructor)}return r.prototype=Object.create(a.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),vh(r,a)},b4(e)}var Vie=/%[sdj%]/g,zie=function(){};function _4(e){if(!e||!e.length)return null;var t={};return e.forEach(function(n){var a=n.field;t[a]=t[a]||[],t[a].push(n)}),t}function to(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),a=1;a=o)return l;switch(l){case"%s":return String(n[r++]);case"%d":return Number(n[r++]);case"%j":try{return JSON.stringify(n[r++])}catch{return"[Circular]"}break;default:return l}});return i}return e}function Fie(e){return e==="string"||e==="url"||e==="hex"||e==="email"||e==="date"||e==="pattern"}function Da(e,t){return!!(e==null||t==="array"&&Array.isArray(e)&&!e.length||Fie(t)&&typeof e=="string"&&!e)}function Hie(e,t,n){var a=[],r=0,o=e.length;function i(l){a.push.apply(a,l||[]),r++,r===o&&n(a)}e.forEach(function(l){t(l,i)})}function y5(e,t,n){var a=0,r=e.length;function o(i){if(i&&i.length){n(i);return}var l=a;a=a+1,l()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,hex:/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i},ov={integer:function(t){return ov.number(t)&&parseInt(t,10)===t},float:function(t){return ov.number(t)&&!ov.integer(t)},array:function(t){return Array.isArray(t)},regexp:function(t){if(t instanceof RegExp)return!0;try{return!!new RegExp(t)}catch{return!1}},date:function(t){return typeof t.getTime=="function"&&typeof t.getMonth=="function"&&typeof t.getYear=="function"&&!isNaN(t.getTime())},number:function(t){return isNaN(t)?!1:typeof t=="number"},object:function(t){return typeof t=="object"&&!ov.array(t)},method:function(t){return typeof t=="function"},email:function(t){return typeof t=="string"&&t.length<=320&&!!t.match(S5.email)},url:function(t){return typeof t=="string"&&t.length<=2048&&!!t.match(jie())},hex:function(t){return typeof t=="string"&&!!t.match(S5.hex)}},qie=function(t,n,a,r,o){if(t.required&&n===void 0){nL(t,n,a,r,o);return}var i=["integer","float","array","regexp","object","method","email","number","date","url","hex"],l=t.type;i.indexOf(l)>-1?ov[l](n)||r.push(to(o.messages.types[l],t.fullField,t.type)):l&&typeof n!==t.type&&r.push(to(o.messages.types[l],t.fullField,t.type))},Xie=function(t,n,a,r,o){var i=typeof t.len=="number",l=typeof t.min=="number",s=typeof t.max=="number",u=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,c=n,d=null,v=typeof n=="number",f=typeof n=="string",h=Array.isArray(n);if(v?d="number":f?d="string":h&&(d="array"),!d)return!1;h&&(c=n.length),f&&(c=n.replace(u,"_").length),i?c!==t.len&&r.push(to(o.messages[d].len,t.fullField,t.len)):l&&!s&&ct.max?r.push(to(o.messages[d].max,t.fullField,t.max)):l&&s&&(ct.max)&&r.push(to(o.messages[d].range,t.fullField,t.min,t.max))},Oc="enum",Zie=function(t,n,a,r,o){t[Oc]=Array.isArray(t[Oc])?t[Oc]:[],t[Oc].indexOf(n)===-1&&r.push(to(o.messages[Oc],t.fullField,t[Oc].join(", ")))},Jie=function(t,n,a,r,o){if(t.pattern){if(t.pattern instanceof RegExp)t.pattern.lastIndex=0,t.pattern.test(n)||r.push(to(o.messages.pattern.mismatch,t.fullField,n,t.pattern));else if(typeof t.pattern=="string"){var i=new RegExp(t.pattern);i.test(n)||r.push(to(o.messages.pattern.mismatch,t.fullField,n,t.pattern))}}},Cn={required:nL,whitespace:Yie,type:qie,range:Xie,enum:Zie,pattern:Jie},Qie=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n,"string")&&!t.required)return a();Cn.required(t,n,r,i,o,"string"),Da(n,"string")||(Cn.type(t,n,r,i,o),Cn.range(t,n,r,i,o),Cn.pattern(t,n,r,i,o),t.whitespace===!0&&Cn.whitespace(t,n,r,i,o))}a(i)},ele=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&Cn.type(t,n,r,i,o)}a(i)},tle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(n===""&&(n=void 0),Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&(Cn.type(t,n,r,i,o),Cn.range(t,n,r,i,o))}a(i)},nle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&Cn.type(t,n,r,i,o)}a(i)},ale=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),Da(n)||Cn.type(t,n,r,i,o)}a(i)},rle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&(Cn.type(t,n,r,i,o),Cn.range(t,n,r,i,o))}a(i)},ole=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&(Cn.type(t,n,r,i,o),Cn.range(t,n,r,i,o))}a(i)},ile=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(n==null&&!t.required)return a();Cn.required(t,n,r,i,o,"array"),n!=null&&(Cn.type(t,n,r,i,o),Cn.range(t,n,r,i,o))}a(i)},lle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&Cn.type(t,n,r,i,o)}a(i)},sle="enum",ule=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o),n!==void 0&&Cn[sle](t,n,r,i,o)}a(i)},cle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n,"string")&&!t.required)return a();Cn.required(t,n,r,i,o),Da(n,"string")||Cn.pattern(t,n,r,i,o)}a(i)},dle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n,"date")&&!t.required)return a();if(Cn.required(t,n,r,i,o),!Da(n,"date")){var s;n instanceof Date?s=n:s=new Date(n),Cn.type(t,s,r,i,o),s&&Cn.range(t,s.getTime(),r,i,o)}}a(i)},fle=function(t,n,a,r,o){var i=[],l=Array.isArray(n)?"array":typeof n;Cn.required(t,n,r,i,o,l),a(i)},Y2=function(t,n,a,r,o){var i=t.type,l=[],s=t.required||!t.required&&r.hasOwnProperty(t.field);if(s){if(Da(n,i)&&!t.required)return a();Cn.required(t,n,r,l,o,i),Da(n,i)||Cn.type(t,n,r,l,o)}a(l)},vle=function(t,n,a,r,o){var i=[],l=t.required||!t.required&&r.hasOwnProperty(t.field);if(l){if(Da(n)&&!t.required)return a();Cn.required(t,n,r,i,o)}a(i)},Pv={string:Qie,method:ele,number:tle,boolean:nle,regexp:ale,integer:rle,float:ole,array:ile,object:lle,enum:ule,pattern:cle,date:dle,url:Y2,hex:Y2,email:Y2,required:fle,any:vle};function w4(){return{default:"Validation error on field %s",required:"%s is required",enum:"%s must be one of %s",whitespace:"%s cannot be empty",date:{format:"%s date %s is invalid for format %s",parse:"%s date could not be parsed, %s is invalid ",invalid:"%s date %s is invalid"},types:{string:"%s is not a %s",method:"%s is not a %s (function)",array:"%s is not an %s",object:"%s is not an %s",number:"%s is not a %s",date:"%s is not a %s",boolean:"%s is not a %s",integer:"%s is not an %s",float:"%s is not a %s",regexp:"%s is not a valid %s",email:"%s is not a valid %s",url:"%s is not a valid %s",hex:"%s is not a valid %s"},string:{len:"%s must be exactly %s characters",min:"%s must be at least %s characters",max:"%s cannot be longer than %s characters",range:"%s must be between %s and %s characters"},number:{len:"%s must equal %s",min:"%s cannot be less than %s",max:"%s cannot be greater than %s",range:"%s must be between %s and %s"},array:{len:"%s must be exactly %s in length",min:"%s cannot be less than %s in length",max:"%s cannot be greater than %s in length",range:"%s must be between %s and %s in length"},pattern:{mismatch:"%s value %s does not match pattern %s"},clone:function(){var t=JSON.parse(JSON.stringify(this));return t.clone=this.clone,t}}}var S4=w4(),lp=function(){function e(n){this.rules=null,this._messages=S4,this.define(n)}var t=e.prototype;return t.define=function(a){var r=this;if(!a)throw new Error("Cannot configure a schema with no rules");if(typeof a!="object"||Array.isArray(a))throw new Error("Rules must be an object");this.rules={},Object.keys(a).forEach(function(o){var i=a[o];r.rules[o]=Array.isArray(i)?i:[i]})},t.messages=function(a){return a&&(this._messages=w5(w4(),a)),this._messages},t.validate=function(a,r,o){var i=this;r===void 0&&(r={}),o===void 0&&(o=function(){});var l=a,s=r,u=o;if(typeof s=="function"&&(u=s,s={}),!this.rules||Object.keys(this.rules).length===0)return u&&u(null,l),Promise.resolve(l);function c(m){var g=[],y={};function b(w){if(Array.isArray(w)){var S;g=(S=g).concat.apply(S,w)}else g.push(w)}for(var _=0;_n.labelPosition||(r==null?void 0:r.labelPosition)),y=D(()=>g.value==="top"?{}:{width:Vn(n.labelWidth??(r==null?void 0:r.labelWidth))}),b=D(()=>{if(g.value==="top"||r!=null&&r.inline)return{};if(!n.label&&!n.labelWidth&&A)return{};const J=Vn(n.labelWidth??(r==null?void 0:r.labelWidth));return!n.label&&!a.label?{marginLeft:J}:{}}),_=D(()=>[l.b(),l.m(i.value),l.is("error",c.value==="error"),l.is("validating",c.value==="validating"),l.is("success",c.value==="success"),l.is("required",K.value||n.required),l.is("no-asterisk",r==null?void 0:r.hideRequiredAsterisk),(r==null?void 0:r.requireAsteriskPosition)==="right"?"asterisk-right":"asterisk-left",{[l.m("feedback")]:r==null?void 0:r.statusIcon,[l.m(`label-${g.value}`)]:g.value}]),w=D(()=>vn(n.inlineMessage)?n.inlineMessage:(r==null?void 0:r.inlineMessage)||!1),S=D(()=>[l.e("error"),{[l.em("error","inline")]:w.value}]),k=D(()=>n.prop?Ae(n.prop)?n.prop.join("."):n.prop:""),T=D(()=>!!(n.label||a.label)),M=D(()=>n.for??(u.value.length===1?u.value[0]:void 0)),E=D(()=>!M.value&&T.value),A=!!o,I=D(()=>{const J=r==null?void 0:r.model;if(!(!J||!n.prop))return Cs(J,n.prop).value}),P=D(()=>{const{required:J}=n,Z=[];n.rules&&Z.push(...wa(n.rules));const j=r==null?void 0:r.rules;if(j&&n.prop){const oe=Cs(j,n.prop).value;oe&&Z.push(...wa(oe))}if(J!==void 0){const oe=Z.map((se,fe)=>[se,fe]).filter(([se])=>"required"in se);if(oe.length>0)for(const[se,fe]of oe)se.required!==J&&(Z[fe]={...se,required:J});else Z.push({required:J})}return Z}),R=D(()=>P.value.length>0),G=J=>P.value.filter(Z=>!Z.trigger||!J?!0:Ae(Z.trigger)?Z.trigger.includes(J):Z.trigger===J).map(({trigger:Z,...j})=>j),K=D(()=>P.value.some(J=>J.required)),H=D(()=>d.value==="error"&&n.showMessage&&((r==null?void 0:r.showMessage)??!0)),z=D(()=>`${n.label||""}${(r==null?void 0:r.labelSuffix)||""}`),B=J=>{c.value=J},W=J=>{var oe;const{errors:Z,fields:j}=J;(!Z||!j)&&console.error(J),B("error"),v.value=Z?((oe=Z==null?void 0:Z[0])==null?void 0:oe.message)??`${n.prop} is required`:"",r==null||r.emit("validate",n.prop,!1,v.value)},N=()=>{B("success"),r==null||r.emit("validate",n.prop,!0,"")},V=async J=>{const Z=k.value;return new lp({[Z]:J}).validate({[Z]:I.value},{firstFields:!0}).then(()=>(N(),!0)).catch(j=>(W(j),Promise.reject(j)))},Y=async(J,Z)=>{if(m||!n.prop)return!1;const j=Qe(Z);if(!R.value)return Z==null||Z(!1),!1;const oe=G(J);return oe.length===0?(Z==null||Z(!0),!0):(B("validating"),V(oe).then(()=>(Z==null||Z(!0),!0)).catch(se=>{const{fields:fe}=se;return Z==null||Z(!1,fe),j?!1:Promise.reject(fe)}))},X=()=>{B(""),v.value="",m=!1},ee=async()=>{const J=r==null?void 0:r.model;if(!J||!n.prop)return;const Z=Cs(J,n.prop);m=!0,Z.value=Li(h),await He(),X(),m=!1},re=J=>{u.value.includes(J)||u.value.push(J)},ie=J=>{u.value=u.value.filter(Z=>Z!==J)},le=J=>{h=Li(J)},ve=()=>h;ye(()=>n.error,J=>{v.value=J||"",B(J?"error":"")},{immediate:!0}),ye(()=>n.validateStatus,J=>B(J||""));const he=nn({...Ta(n),$el:f,size:i,validateMessage:v,validateState:c,labelId:s,inputIds:u,isGroup:E,hasLabel:T,fieldValue:I,addInputId:re,removeInputId:ie,resetField:ee,clearValidate:X,validate:Y,propString:k,setInitialValue:le,getInitialValue:ve});return Ft(Qi,he),ye(k,(J,Z)=>{!r||!Z||(r.removeField(he,Z),J&&(le(I.value),r.addField(he)))}),mt(()=>{n.prop&&(le(I.value),r==null||r.addField(he))}),rn(()=>{r==null||r.removeField(he)}),t({size:i,validateMessage:v,validateState:c,validate:Y,clearValidate:X,resetField:ee,setInitialValue:le}),(J,Z)=>{var j;return x(),O("div",{ref_key:"formItemRef",ref:f,class:U(_.value),role:E.value?"group":void 0,"aria-labelledby":E.value?p(s):void 0},[L(p($ie),{"is-auto-width":y.value.width==="auto","update-all":((j=p(r))==null?void 0:j.labelWidth)==="auto"},{default:$(()=>[e.label||J.$slots.label?(x(),ne(St(M.value?"label":"div"),{key:0,id:p(s),for:M.value,class:U(p(l).e("label")),style:et(y.value)},{default:$(()=>[ce(J.$slots,"label",{label:z.value},()=>[ue(Q(z.value),1)])]),_:3},8,["id","for","class","style"])):te("v-if",!0)]),_:3},8,["is-auto-width","update-all"]),C("div",{class:U(p(l).e("content")),style:et(b.value)},[ce(J.$slots,"default"),L(L7,{name:`${p(l).namespace.value}-zoom-in-top`},{default:$(()=>[H.value?ce(J.$slots,"error",{key:0,error:v.value},()=>[C("div",{class:U(S.value)},Q(v.value),3)]):te("v-if",!0)]),_:3},8,["name"])],6)],10,hle)}}}),aL=ple;const mle=wt(Oie,{FormItem:aL}),gle=On(aL),j2="focus-trap.focus-after-trapped",q2="focus-trap.focus-after-released",yle="focus-trap.focusout-prevented",C5={cancelable:!0,bubbles:!1},ble={cancelable:!0,bubbles:!1},x5="focusAfterTrapped",k5="focusAfterReleased",rL=Symbol("elFocusTrap"),q6=F(),H1=F(0),X6=F(0);let Bp=0;const oL=e=>{const t=[],n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:a=>{const r=a.tagName==="INPUT"&&a.type==="hidden";return a.disabled||a.hidden||r?NodeFilter.FILTER_SKIP:a.tabIndex>=0||a===document.activeElement?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;n.nextNode();)t.push(n.currentNode);return t},T5=(e,t)=>{for(const n of e)if(!_le(n,t))return n},_le=(e,t)=>{if(getComputedStyle(e).visibility==="hidden")return!0;for(;e;){if(t&&e===t)return!1;if(getComputedStyle(e).display==="none")return!0;e=e.parentElement}return!1},wle=e=>{const t=oL(e);return[T5(t,e),T5(t.reverse(),e)]},Sle=e=>e instanceof HTMLInputElement&&"select"in e,os=(e,t)=>{if(e){const n=document.activeElement;Jh(e,{preventScroll:!0}),X6.value=window.performance.now(),e!==n&&Sle(e)&&t&&e.select()}};function M5(e,t){const n=[...e],a=e.indexOf(t);return a!==-1&&n.splice(a,1),n}const Cle=()=>{let e=[];return{push:a=>{const r=e[0];r&&a!==r&&r.pause(),e=M5(e,a),e.unshift(a)},remove:a=>{var r,o;e=M5(e,a),(o=(r=e[0])==null?void 0:r.resume)==null||o.call(r)}}},xle=(e,t=!1)=>{const n=document.activeElement;for(const a of e)if(os(a,t),document.activeElement!==n)return},E5=Cle(),kle=()=>H1.value>X6.value,Vp=()=>{q6.value="pointer",H1.value=window.performance.now()},A5=()=>{q6.value="keyboard",H1.value=window.performance.now()},Tle=()=>(mt(()=>{Bp===0&&(document.addEventListener("mousedown",Vp),document.addEventListener("touchstart",Vp),document.addEventListener("keydown",A5)),Bp++}),rn(()=>{Bp--,Bp<=0&&(document.removeEventListener("mousedown",Vp),document.removeEventListener("touchstart",Vp),document.removeEventListener("keydown",A5))}),{focusReason:q6,lastUserFocusTimestamp:H1,lastAutomatedFocusTimestamp:X6}),zp=e=>new CustomEvent(yle,{...ble,detail:e});var Mle=q({name:"ElFocusTrap",inheritAttrs:!1,props:{loop:Boolean,trapped:Boolean,focusTrapEl:Object,focusStartEl:{type:[Object,String],default:"first"}},emits:[x5,k5,"focusin","focusout","focusout-prevented","release-requested"],setup(e,{emit:t}){const n=F();let a,r;const{focusReason:o}=Tle();tq(h=>{e.trapped&&!i.paused&&t("release-requested",h)});const i={paused:!1,pause(){this.paused=!0},resume(){this.paused=!1}},l=h=>{if(!e.loop&&!e.trapped||i.paused)return;const{altKey:m,ctrlKey:g,metaKey:y,currentTarget:b,shiftKey:_}=h,{loop:w}=e,S=mn(h)===Pe.tab&&!m&&!g&&!y,k=document.activeElement;if(S&&k){const T=b,[M,E]=wle(T);if(M&&E){if(!_&&k===E){const A=zp({focusReason:o.value});t("focusout-prevented",A),A.defaultPrevented||(h.preventDefault(),w&&os(M,!0))}else if(_&&[M,T].includes(k)){const A=zp({focusReason:o.value});t("focusout-prevented",A),A.defaultPrevented||(h.preventDefault(),w&&os(E,!0))}}else if(k===T){const A=zp({focusReason:o.value});t("focusout-prevented",A),A.defaultPrevented||h.preventDefault()}}};Ft(rL,{focusTrapRef:n,onKeydown:l}),ye(()=>e.focusTrapEl,h=>{h&&(n.value=h)},{immediate:!0}),ye([n],([h],[m])=>{h&&(h.addEventListener("keydown",l),h.addEventListener("focusin",c),h.addEventListener("focusout",d)),m&&(m.removeEventListener("keydown",l),m.removeEventListener("focusin",c),m.removeEventListener("focusout",d))});const s=h=>{t(x5,h)},u=h=>t(k5,h),c=h=>{const m=p(n);if(!m)return;const g=h.target,y=h.relatedTarget,b=g&&m.contains(g);e.trapped||y&&m.contains(y)||(a=y),b&&t("focusin",h),!i.paused&&e.trapped&&(b?r=g:os(r,!0))},d=h=>{const m=p(n);if(!(i.paused||!m))if(e.trapped){const g=h.relatedTarget;!Un(g)&&!m.contains(g)&&setTimeout(()=>{if(!i.paused&&e.trapped){const y=zp({focusReason:o.value});t("focusout-prevented",y),y.defaultPrevented||os(r,!0)}},0)}else{const g=h.target;g&&m.contains(g)||t("focusout",h)}};async function v(){await He();const h=p(n);if(h){E5.push(i);const m=h.contains(document.activeElement)?a:document.activeElement;if(a=m,!h.contains(m)){const g=new Event(j2,C5);h.addEventListener(j2,s),h.dispatchEvent(g),g.defaultPrevented||He(()=>{let y=e.focusStartEl;Ye(y)||(os(y),document.activeElement!==y&&(y="first")),y==="first"&&xle(oL(h),!0),(document.activeElement===m||y==="container")&&os(h)})}}}function f(){const h=p(n);if(h){h.removeEventListener(j2,s);const m=new CustomEvent(q2,{...C5,detail:{focusReason:o.value}});h.addEventListener(q2,u),h.dispatchEvent(m),!m.defaultPrevented&&(o.value=="keyboard"||!kle()||h.contains(document.activeElement))&&os(a??document.body),h.removeEventListener(q2,u),E5.remove(i),a=null,r=null}}return mt(()=>{e.trapped&&v(),ye(()=>e.trapped,h=>{h?v():f()})}),rn(()=>{e.trapped&&f(),n.value&&(n.value.removeEventListener("keydown",l),n.value.removeEventListener("focusin",c),n.value.removeEventListener("focusout",d),n.value=void 0),a=null,r=null}),{onKeydown:l}}}),ya=(e,t)=>{const n=e.__vccOpts||e;for(const[a,r]of t)n[a]=r;return n};function Ele(e,t,n,a,r,o){return ce(e.$slots,"default",{handleKeydown:e.onKeydown})}var Ale=ya(Mle,[["render",Ele]]),pf=Ale;const Ile=(e,t=[])=>{const{placement:n,strategy:a,popperOptions:r}=e,o={placement:n,strategy:a,...r,modifiers:[...Ple(e),...t]};return Dle(o,r==null?void 0:r.modifiers),o},Lle=e=>{if(qt)return da(e)};function Ple(e){const{offset:t,gpuAcceleration:n,fallbackPlacements:a}=e;return[{name:"offset",options:{offset:[0,t??12]}},{name:"preventOverflow",options:{padding:{top:0,bottom:0,left:0,right:0}}},{name:"flip",options:{padding:5,fallbackPlacements:a}},{name:"computeStyles",options:{gpuAcceleration:n}}]}function Dle(e,t){t&&(e.modifiers=[...e.modifiers,...t??[]])}const Ole=0,$le=e=>{const{popperInstanceRef:t,contentRef:n,triggerRef:a,role:r}=Ze(j6,void 0),o=F(),i=D(()=>e.arrowOffset),l=D(()=>({name:"eventListeners",enabled:!!e.visible})),s=D(()=>{const b=p(o),_=p(i)??Ole;return{name:"arrow",enabled:!CA(b),options:{element:b,padding:_}}}),u=D(()=>({onFirstUpdate:()=>{h()},...Ile(e,[p(s),p(l)])})),c=D(()=>Lle(e.referenceEl)||p(a)),{attributes:d,state:v,styles:f,update:h,forceUpdate:m,instanceRef:g}=Zj(c,n,u);ye(g,b=>t.value=b,{flush:"sync"}),mt(()=>{ye(()=>{var b,_;return(_=(b=p(c))==null?void 0:b.getBoundingClientRect)==null?void 0:_.call(b)},()=>{h()})});let y;return ye(()=>e.visible,b=>{y==null||y(),y=void 0,b&&(y=Mn(n,h).stop)}),rn(()=>{t.value=void 0,y==null||y(),y=void 0}),{attributes:d,arrowRef:o,contentRef:n,instanceRef:g,state:v,styles:f,role:r,forceUpdate:m,update:h}},Rle=(e,{attributes:t,styles:n,role:a})=>{const{nextZIndex:r}=tp(),o=ke("popper"),i=D(()=>p(t).popper),l=F(Je(e.zIndex)?e.zIndex:r()),s=D(()=>[o.b(),o.is("pure",e.pure),o.is(e.effect),e.popperClass]),u=D(()=>[{zIndex:p(l)},p(n).popper,e.popperStyle||{}]),c=D(()=>a.value==="dialog"?"false":void 0),d=D(()=>p(n).arrow||{});return{ariaModal:c,arrowStyle:d,contentAttrs:i,contentClass:s,contentStyle:u,contentZIndex:l,updateZIndex:()=>{l.value=Je(e.zIndex)?e.zIndex:r()}}},Nle=(e,t)=>{const n=F(!1),a=F(),r=()=>{t("focus")},o=u=>{var c;((c=u.detail)==null?void 0:c.focusReason)!=="pointer"&&(a.value="first",t("blur"))},i=u=>{e.visible&&!n.value&&(u.target&&(a.value=u.target),n.value=!0)},l=u=>{e.trapping||(u.detail.focusReason==="pointer"&&u.preventDefault(),n.value=!1)},s=()=>{n.value=!1,t("close")};return rn(()=>{a.value=void 0}),{focusStartRef:a,trapped:n,onFocusAfterReleased:o,onFocusAfterTrapped:r,onFocusInTrap:i,onFocusoutPrevented:l,onReleaseRequested:s}};var Ble=q({name:"ElPopperContent",__name:"content",props:tL,emits:kie,setup(e,{expose:t,emit:n}){const a=n,r=e,{focusStartRef:o,trapped:i,onFocusAfterReleased:l,onFocusAfterTrapped:s,onFocusInTrap:u,onFocusoutPrevented:c,onReleaseRequested:d}=Nle(r,a),{attributes:v,arrowRef:f,contentRef:h,styles:m,instanceRef:g,role:y,update:b}=$le(r),{ariaModal:_,arrowStyle:w,contentAttrs:S,contentClass:k,contentStyle:T,updateZIndex:M}=Rle(r,{styles:m,attributes:v,role:y}),E=Ze(Qi,void 0);Ft(XI,{arrowStyle:w,arrowRef:f}),E&&Ft(Qi,{...E,addInputId:kt,removeInputId:kt});let A;const I=(R=!0)=>{b(),R&&M()},P=()=>{I(!1),r.visible&&r.focusOnShow?i.value=!0:r.visible===!1&&(i.value=!1)};return mt(()=>{ye(()=>r.triggerTargetEl,(R,G)=>{A==null||A(),A=void 0;const K=p(R||h.value),H=p(G||h.value);Vr(K)&&(A=ye([y,()=>r.ariaLabel,_,()=>r.id],z=>{["role","aria-label","aria-modal","id"].forEach((B,W)=>{Un(z[W])?K.removeAttribute(B):K.setAttribute(B,z[W])})},{immediate:!0})),H!==K&&Vr(H)&&["role","aria-label","aria-modal","id"].forEach(z=>{H.removeAttribute(z)})},{immediate:!0}),ye(()=>r.visible,P,{immediate:!0})}),rn(()=>{A==null||A(),A=void 0,h.value=void 0}),t({popperContentRef:h,popperInstanceRef:g,updatePopper:I,contentStyle:T}),(R,G)=>(x(),O("div",Tt({ref_key:"contentRef",ref:h},p(S),{style:p(T),class:p(k),tabindex:"-1",onMouseenter:G[0]||(G[0]=K=>R.$emit("mouseenter",K)),onMouseleave:G[1]||(G[1]=K=>R.$emit("mouseleave",K))}),[L(p(pf),{loop:e.loop,trapped:p(i),"trap-on-focus-in":!0,"focus-trap-el":p(h),"focus-start-el":p(o),onFocusAfterTrapped:p(s),onFocusAfterReleased:p(l),onFocusin:p(u),onFocusoutPrevented:p(c),onReleaseRequested:p(d)},{default:$(()=>[ce(R.$slots,"default")]),_:3},8,["loop","trapped","focus-trap-el","focus-start-el","onFocusAfterTrapped","onFocusAfterReleased","onFocusin","onFocusoutPrevented","onReleaseRequested"])],16))}}),Vle=Ble,zle=q({name:"ElPopper",inheritAttrs:!1,__name:"popper",props:qI,setup(e,{expose:t}){const n=e,a={triggerRef:F(),popperInstanceRef:F(),contentRef:F(),referenceRef:F(),role:D(()=>n.role)};return t(a),Ft(j6,a),(r,o)=>ce(r.$slots,"default")}}),Fle=zle;const iL=wt(Fle),Hle={...oq,...xie,content:"",visible:null,teleported:!0},Tn=$e({...rq,...tL,appendTo:{type:ae([String,Object])},content:{type:String,default:""},rawContent:Boolean,persistent:Boolean,visible:{type:ae(Boolean),default:null},transition:String,teleported:{type:Boolean,default:!0},disabled:Boolean,...dr(["ariaLabel"])}),qu=$e({...ZI,disabled:Boolean,trigger:{type:ae([String,Array]),default:"hover"},triggerKeys:{type:ae(Array),default:()=>[Pe.enter,Pe.numpadEnter,Pe.space]},focusOnTarget:Boolean}),{useModelToggleProps:Wle,useModelToggleEmits:Kle,useModelToggle:Ule}=jY("visible"),iv=$e({...qI,...Wle,...Tn,...qu,...eL,showArrow:{type:Boolean,default:!0}}),Gle=[...Kle,"before-show","before-hide","show","hide","open","close"],Z6=Symbol("elTooltip"),C4=(e,t)=>Ae(e)?e.includes(t):e===t,$c=(e,t,n)=>a=>{C4(p(e),t)&&n(a)};var Yle=q({name:"ElTooltipTrigger",__name:"trigger",props:qu,setup(e,{expose:t}){const n=e,a=ke("tooltip"),{controlled:r,id:o,open:i,onOpen:l,onClose:s,onToggle:u}=Ze(Z6,void 0),c=F(null),d=()=>{if(p(r)||n.disabled)return!0},v=cn(n,"trigger"),f=_a(d,$c(v,"hover",w=>{l(w),n.focusOnTarget&&w.target&&He(()=>{Jh(w.target,{preventScroll:!0})})})),h=_a(d,$c(v,"hover",s)),m=_a(d,$c(v,"click",w=>{w.button===0&&u(w)})),g=_a(d,$c(v,"focus",l)),y=_a(d,$c(v,"focus",s)),b=_a(d,$c(v,"contextmenu",w=>{w.preventDefault(),u(w)})),_=_a(d,w=>{const S=mn(w);n.triggerKeys.includes(S)&&(w.preventDefault(),u(w))});return t({triggerRef:c}),(w,S)=>(x(),ne(p(_ie),{id:p(o),"virtual-ref":e.virtualRef,open:p(i),"virtual-triggering":e.virtualTriggering,class:U(p(a).e("trigger")),onBlur:p(y),onClick:p(m),onContextmenu:p(b),onFocus:p(g),onMouseenter:p(f),onMouseleave:p(h),onKeydown:p(_)},{default:$(()=>[ce(w.$slots,"default")]),_:3},8,["id","virtual-ref","open","virtual-triggering","class","onBlur","onClick","onContextmenu","onFocus","onMouseenter","onMouseleave","onKeydown"]))}}),jle=Yle,qle=q({name:"ElTooltipContent",inheritAttrs:!1,__name:"content",props:Tn,setup(e,{expose:t}){const n=e,{selector:a}=nI(),r=ke("tooltip"),o=F(),i=EA(()=>{var B;return(B=o.value)==null?void 0:B.popperContentRef});let l;const{controlled:s,id:u,open:c,trigger:d,onClose:v,onOpen:f,onShow:h,onHide:m,onBeforeShow:g,onBeforeHide:y}=Ze(Z6,void 0),b=D(()=>n.transition||`${r.namespace.value}-fade-in-linear`),_=D(()=>n.persistent);rn(()=>{l==null||l()});const w=D(()=>p(_)?!0:p(c)),S=D(()=>n.disabled?!1:p(c)),k=D(()=>n.appendTo||a.value),T=D(()=>n.style??{}),M=F(!0),E=()=>{m(),z()&&Jh(document.body,{preventScroll:!0}),M.value=!0},A=()=>{if(p(s))return!0},I=_a(A,()=>{n.enterable&&C4(p(d),"hover")&&f()}),P=_a(A,()=>{C4(p(d),"hover")&&v()}),R=()=>{var B,W;(W=(B=o.value)==null?void 0:B.updatePopper)==null||W.call(B),g==null||g()},G=()=>{y==null||y()},K=()=>{h()},H=()=>{n.virtualTriggering||v()},z=B=>{var V;const W=(V=o.value)==null?void 0:V.popperContentRef,N=(B==null?void 0:B.relatedTarget)||document.activeElement;return W==null?void 0:W.contains(N)};return ye(()=>p(c),B=>{B?(M.value=!1,l=b6(i,()=>{p(s)||or(p(d)).every(W=>W!=="hover"&&W!=="focus")&&v()},{detectIframe:!0})):l==null||l()},{flush:"post"}),t({contentRef:o,isFocusInsideContent:z}),(B,W)=>(x(),ne(sf,{disabled:!e.teleported,to:k.value},[w.value||!M.value?(x(),ne(Pa,{key:0,name:b.value,appear:!_.value,onAfterLeave:E,onBeforeEnter:R,onAfterEnter:K,onBeforeLeave:G,persisted:""},{default:$(()=>[st(L(p(Vle),Tt({id:p(u),ref_key:"contentRef",ref:o},B.$attrs,{"aria-label":e.ariaLabel,"aria-hidden":M.value,"boundaries-padding":e.boundariesPadding,"fallback-placements":e.fallbackPlacements,"gpu-acceleration":e.gpuAcceleration,offset:e.offset,placement:e.placement,"popper-options":e.popperOptions,"arrow-offset":e.arrowOffset,strategy:e.strategy,effect:e.effect,enterable:e.enterable,pure:e.pure,"popper-class":e.popperClass,"popper-style":[e.popperStyle,T.value],"reference-el":e.referenceEl,"trigger-target-el":e.triggerTargetEl,visible:S.value,"z-index":e.zIndex,loop:e.loop,onMouseenter:p(I),onMouseleave:p(P),onBlur:H,onClose:p(v)}),{default:$(()=>[ce(B.$slots,"default")]),_:3},16,["id","aria-label","aria-hidden","boundaries-padding","fallback-placements","gpu-acceleration","offset","placement","popper-options","arrow-offset","strategy","effect","enterable","pure","popper-class","popper-style","reference-el","trigger-target-el","visible","z-index","loop","onMouseenter","onMouseleave","onClose"]),[[Zt,S.value]])]),_:3},8,["name","appear"])):te("v-if",!0)],8,["disabled","to"]))}}),Xle=qle;const Zle=["innerHTML"],Jle={key:1};var Qle=q({name:"ElTooltip",__name:"tooltip",props:iv,emits:Gle,setup(e,{expose:t,emit:n}){const a=e,r=n;aq();const o=ke("tooltip"),i=Fa(),l=F(),s=F(),u=()=>{var S;const w=p(l);w&&((S=w.popperInstanceRef)==null||S.update())},c=F(!1),d=F(),{show:v,hide:f,hasUpdateHandler:h}=Ule({indicator:c,toggleReason:d}),{onOpen:m,onClose:g}=iq({showAfter:cn(a,"showAfter"),hideAfter:cn(a,"hideAfter"),autoClose:cn(a,"autoClose"),open:v,close:f}),y=D(()=>vn(a.visible)&&!h.value),b=D(()=>[o.b(),a.popperClass]);Ft(Z6,{controlled:y,id:i,open:Ld(c),trigger:cn(a,"trigger"),onOpen:m,onClose:g,onToggle:w=>{p(c)?g(w):m(w)},onShow:()=>{r("show",d.value)},onHide:()=>{r("hide",d.value)},onBeforeShow:()=>{r("before-show",d.value)},onBeforeHide:()=>{r("before-hide",d.value)},updatePopper:u}),ye(()=>a.disabled,w=>{w&&c.value&&(c.value=!1),!w&&vn(a.visible)&&(c.value=a.visible)});const _=w=>{var S;return(S=s.value)==null?void 0:S.isFocusInsideContent(w)};return j3(()=>c.value&&f()),rn(()=>{d.value=void 0}),t({popperRef:l,contentRef:s,isFocusInsideContent:_,updatePopper:u,onOpen:m,onClose:g,hide:f}),(w,S)=>(x(),ne(p(iL),{ref_key:"popperRef",ref:l,role:e.role},{default:$(()=>[L(jle,{disabled:e.disabled,trigger:e.trigger,"trigger-keys":e.triggerKeys,"virtual-ref":e.virtualRef,"virtual-triggering":e.virtualTriggering,"focus-on-target":e.focusOnTarget},{default:$(()=>[w.$slots.default?ce(w.$slots,"default",{key:0}):te("v-if",!0)]),_:3},8,["disabled","trigger","trigger-keys","virtual-ref","virtual-triggering","focus-on-target"]),L(Xle,{ref_key:"contentRef",ref:s,"aria-label":e.ariaLabel,"boundaries-padding":e.boundariesPadding,content:e.content,disabled:e.disabled,effect:e.effect,enterable:e.enterable,"fallback-placements":e.fallbackPlacements,"hide-after":e.hideAfter,"gpu-acceleration":e.gpuAcceleration,offset:e.offset,persistent:e.persistent,"popper-class":b.value,"popper-style":e.popperStyle,placement:e.placement,"popper-options":e.popperOptions,"arrow-offset":e.arrowOffset,pure:e.pure,"raw-content":e.rawContent,"reference-el":e.referenceEl,"trigger-target-el":e.triggerTargetEl,"show-after":e.showAfter,strategy:e.strategy,teleported:e.teleported,transition:e.transition,"virtual-triggering":e.virtualTriggering,"z-index":e.zIndex,"append-to":e.appendTo,loop:e.loop},{default:$(()=>[ce(w.$slots,"content",{},()=>[e.rawContent?(x(),O("span",{key:0,innerHTML:e.content},null,8,Zle)):(x(),O("span",Jle,Q(e.content),1))]),e.showArrow?(x(),ne(p(yie),{key:0})):te("v-if",!0)]),_:3},8,["aria-label","boundaries-padding","content","disabled","effect","enterable","fallback-placements","hide-after","gpu-acceleration","offset","persistent","popper-class","popper-style","placement","popper-options","arrow-offset","pure","raw-content","reference-el","trigger-target-el","show-after","strategy","teleported","transition","virtual-triggering","z-index","append-to","loop"])]),_:3},8,["role"]))}}),ese=Qle;const Ma=wt(ese),Nn=e=>e,J6=$e({id:{type:String,default:void 0},size:pa,disabled:{type:Boolean,default:void 0},modelValue:{type:ae([String,Number,Object]),default:""},modelModifiers:{type:ae(Object),default:()=>({})},maxlength:{type:[String,Number]},minlength:{type:[String,Number]},type:{type:ae(String),default:"text"},resize:{type:String,values:["none","both","horizontal","vertical"]},autosize:{type:ae([Boolean,Object]),default:!1},autocomplete:{type:ae(String),default:"off"},formatter:{type:Function},parser:{type:Function},placeholder:{type:String},form:{type:String},readonly:Boolean,clearable:Boolean,clearIcon:{type:pn,default:ci},showPassword:Boolean,showWordLimit:Boolean,wordLimitPosition:{type:String,values:["inside","outside"],default:"inside"},suffixIcon:{type:pn},prefixIcon:{type:pn},containerRole:{type:String,default:void 0},tabindex:{type:[String,Number],default:0},validateEvent:{type:Boolean,default:!0},inputStyle:{type:ae([Object,Array,String,Boolean]),default:()=>Nn({})},countGraphemes:{type:ae(Function)},autofocus:Boolean,rows:{type:Number,default:2},...dr(["ariaLabel"]),inputmode:{type:ae(String),default:void 0},name:String}),tse={[bt]:e=>Ye(e),input:e=>Ye(e),change:(e,t)=>Ye(e)&&(t instanceof Event||t===void 0),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:e=>e===void 0||e instanceof MouseEvent,mouseleave:e=>e instanceof MouseEvent,mouseenter:e=>e instanceof MouseEvent,keydown:e=>e instanceof Event,compositionstart:e=>e instanceof CompositionEvent,compositionupdate:e=>e instanceof CompositionEvent,compositionend:e=>e instanceof CompositionEvent};$i(ci);let qa;const nse={height:"0",visibility:"hidden",overflow:P1()?"":"hidden",position:"absolute","z-index":"-1000",top:"0",right:"0"},ase=["letter-spacing","line-height","padding-top","padding-bottom","font-family","font-weight","font-size","text-rendering","text-transform","width","text-indent","padding-left","padding-right","border-width","box-sizing","word-break"],I5=e=>{const t=Number.parseFloat(e);return Number.isNaN(t)?e:t};function rse(e){const t=window.getComputedStyle(e),n=t.getPropertyValue("box-sizing"),a=Number.parseFloat(t.getPropertyValue("padding-bottom"))+Number.parseFloat(t.getPropertyValue("padding-top")),r=Number.parseFloat(t.getPropertyValue("border-bottom-width"))+Number.parseFloat(t.getPropertyValue("border-top-width"));return{contextStyle:ase.map(o=>[o,t.getPropertyValue(o)]),paddingSize:a,borderSize:r,boxSizing:n}}function L5(e,t=1,n){var c;if(!qa){qa=document.createElement("textarea");let d=document.body;!P1()&&e.parentNode&&(d=e.parentNode),d.appendChild(qa)}const{paddingSize:a,borderSize:r,boxSizing:o,contextStyle:i}=rse(e);i.forEach(([d,v])=>qa==null?void 0:qa.style.setProperty(d,v)),Object.entries(nse).forEach(([d,v])=>qa==null?void 0:qa.style.setProperty(d,v,"important")),qa.value=e.value||e.placeholder||"";let l=qa.scrollHeight;const s={};o==="border-box"?l=l+r:o==="content-box"&&(l=l-a),qa.value="";const u=qa.scrollHeight-a;if(Je(t)){let d=u*t;o==="border-box"&&(d=d+a+r),l=Math.max(d,l),s.minHeight=`${d}px`}if(Je(n)){let d=u*n;o==="border-box"&&(d=d+a+r),l=Math.min(d,l)}return s.height=`${l}px`,(c=qa.parentNode)==null||c.removeChild(qa),qa=void 0,s}const ose=["id","name","minlength","maxlength","type","disabled","readonly","autocomplete","tabindex","aria-label","placeholder","form","autofocus","role","inputmode"],ise=["id","name","minlength","maxlength","tabindex","disabled","readonly","autocomplete","aria-label","placeholder","form","autofocus","rows","role","inputmode"],P5="ElInput";var lse=q({name:P5,inheritAttrs:!1,__name:"input",props:J6,emits:tse,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Nl(),i=Xn(),l=D(()=>[a.type==="textarea"?y.b():g.b(),g.m(h.value),g.is("disabled",m.value),g.is("exceed",he.value),{[g.b("group")]:i.prepend||i.append,[g.m("prefix")]:i.prefix||a.prefixIcon,[g.m("suffix")]:i.suffix||a.suffixIcon||a.clearable||a.showPassword,[g.bm("suffix","password-clear")]:re.value&&ie.value,[g.b("hidden")]:a.type==="hidden"},o.class]),s=D(()=>[g.e("wrapper"),g.is("focus",G.value)]),u=A1(),c=D(()=>{var _e;return(_e=a.maxlength)==null?void 0:_e.toString()}),{form:d,formItem:v}=Ea(),{inputId:f}=Yr(a,{formItemContext:v}),h=ca(),m=$n(),g=ke("input"),y=ke("textarea"),b=Ht(),_=Ht(),w=F(!1),S=F(!1),k=F(),T=F(),M=Ht(a.inputStyle),E=F("");let A;const I=F(),P=D(()=>b.value||_.value),{wrapperRef:R,isFocused:G,handleFocus:K,handleBlur:H}=Fl(P,{disabled:m,afterFocus(){var _e;a.showPassword&&(A=(_e=P.value)==null?void 0:_e.value)},beforeBlur(_e){var Ne;if(a.showPassword){const tt=P.value;if(_e.relatedTarget&&((Ne=tt==null?void 0:tt.parentElement)!=null&&Ne.contains(_e.relatedTarget)))return;const Me=tt==null?void 0:tt.value;!Un(Me)&&!Un(A)&&Me!==A&&(tt==null||tt.dispatchEvent(new Event("change",{bubbles:!0}))),A=void 0}},afterBlur(){var _e;a.validateEvent&&((_e=v==null?void 0:v.validate)==null||_e.call(v,"blur").catch(kt))}}),z=D(()=>(d==null?void 0:d.statusIcon)??!1),B=D(()=>(v==null?void 0:v.validateState)||""),W=D(()=>B.value&&F1[B.value]),N=D(()=>S.value?U6:LI),V=D(()=>[o.style]),Y=D(()=>[a.inputStyle,M.value,{resize:a.resize},I.value?{height:I.value}:void 0]),X=D(()=>Un(a.modelValue)?"":String(a.modelValue)),ee=D(()=>a.clearable&&!m.value&&!a.readonly),re=D(()=>ee.value&&!!X.value&&(G.value||w.value)),ie=D(()=>a.showPassword&&!m.value&&!!X.value),le=D(()=>a.showWordLimit&&!!c.value&&(a.type==="text"||a.type==="textarea")&&!m.value&&!a.readonly&&!a.showPassword),ve=D(()=>a.countGraphemes&&a.showWordLimit?a.countGraphemes(X.value):X.value.length),he=D(()=>!!le.value&&ve.value>Number(c.value)),J=D(()=>!!i.suffix||!!a.suffixIcon||a.clearable||a.showPassword||le.value||!!B.value&&z.value),Z=D(()=>!!Object.keys(a.modelModifiers).length),[j,oe]=Jq(b);let se;Mn(_,_e=>{if(Se(),!le.value&&!ee.value||a.resize!=="both"&&a.resize!=="horizontal")return;const{width:Ne}=_e[0].target.getBoundingClientRect(),tt=()=>{se=void 0,k.value={right:`calc(100% - ${Ne-10}px)`},T.value={right:`calc(100% - ${Ne-11}px)`}};se&&ti(se),se=Wr(tt)});const fe=()=>{const{type:_e,autosize:Ne}=a;if(!(!qt||_e!=="textarea"||!_.value))if(Ne){const tt=_t(Ne)?Ne.minRows:void 0,Me=_t(Ne)?Ne.maxRows:void 0,Ue=L5(_.value,tt,Me);M.value={overflowY:"hidden",...Ue},He(()=>{_.value.offsetHeight,M.value=Ue})}else M.value={minHeight:L5(_.value).minHeight}},Se=(_e=>{let Ne=!1;return()=>{var tt;if(Ne||!a.autosize){a.resize!=="none"&&setTimeout(()=>{var Me;I.value=(Me=_.value)==null?void 0:Me.style.height});return}((tt=_.value)==null?void 0:tt.offsetParent)!==null&&(setTimeout(_e),Ne=!0)}})(fe),De=()=>{const _e=P.value,Ne=a.formatter?a.formatter(X.value):X.value;!_e||_e.value===Ne||a.type==="file"||(_e.value=Ne)},Te=_e=>{const{trim:Ne,number:tt}=a.modelModifiers;return Ne&&(_e=_e.trim()),tt&&(_e=`${I5(_e)}`),a.formatter&&a.parser&&(_e=a.parser(_e)),_e},Re=async _e=>{if(Le.value)return;const{lazy:Ne}=a.modelModifiers;let{value:tt}=_e.target,Me=!1;if(Ne){r(ua,tt);return}if(tt=Te(tt),a.countGraphemes&&c.value!=null){const Ue=Number(c.value),Gt=a.countGraphemes(tt),je=a.countGraphemes(E.value);if(Gt>Ue&&Gt>je)if(je>Ue)tt=E.value,Me=!0;else{const ft=E.value,ht=tt;let pt=0;for(;ptpt&&Ct>pt&&ft[It-1]===ht[Ct-1];)It--,Ct--;const $t=ht.slice(0,pt),Sn=ft.slice(pt,It),Fe=ht.slice(pt,Ct),qe=ht.slice(Ct),ut=je-a.countGraphemes(Sn),Ie=Math.max(0,Ue-ut);let Ke="";if(Ie>0)if(typeof Intl<"u"&&"Segmenter"in Intl){const Ge=new Intl.Segmenter(void 0,{granularity:"grapheme"});for(const{segment:Ee}of Ge.segment(Fe)){const de=Ke+Ee;if(a.countGraphemes(de)>Ie)break;Ke=de}}else for(const Ge of Array.from(Fe)){const Ee=Ke+Ge;if(a.countGraphemes(Ee)>Ie)break;Ke=Ee}tt=$t+Ke+qe,Me=!0}}if(String(tt)===X.value){if(a.formatter||Me){const Ue=_e.target,Gt=Ue.value,je=Ue.selectionStart,ft=Ue.selectionEnd;if(De(),Me&&P.value&&je!=null&&ft!=null){const ht=P.value.value,pt=Gt.slice(Math.max(0,ft));let It=Math.min(je,ht.length);pt&&ht.endsWith(pt)&&(It=ht.length-pt.length),P.value.setSelectionRange(It,It)}}return}E.value=tt,j(),r(bt,tt),r(ua,tt),await He(),(a.formatter&&a.parser||!Z.value)&&De(),oe()},Ce=async _e=>{let{value:Ne}=_e.target;a.showPassword&&(A=Ne),Ne=Te(Ne),a.modelModifiers.lazy&&r(bt,Ne),r(Ot,Ne,_e),await He(),De()},{isComposing:Le,handleCompositionStart:me,handleCompositionUpdate:Be,handleCompositionEnd:ze}=rp({emit:r,afterComposition:Re}),xe=()=>{S.value=!S.value},rt=()=>{var _e;return(_e=P.value)==null?void 0:_e.focus()},nt=()=>{var _e;return(_e=P.value)==null?void 0:_e.blur()},Pt=_e=>{w.value=!1,r("mouseleave",_e)},Wt=_e=>{w.value=!0,r("mouseenter",_e)},Jt=_e=>{r("keydown",_e)},Rt=()=>{var _e;(_e=P.value)==null||_e.select()},Qt=_e=>{r(bt,""),A="",r(Ot,""),r("clear",_e),r(ua,"")};return ye(()=>a.modelValue,()=>{var _e;He(()=>{fe(),a.autosize&&(I.value=void 0)}),a.validateEvent&&((_e=v==null?void 0:v.validate)==null||_e.call(v,"change").catch(kt))}),ye(()=>X.value,_e=>{E.value=_e},{immediate:!0}),ye(X,_e=>{if(!P.value)return;const{trim:Ne,number:tt}=a.modelModifiers,Me=P.value.value,Ue=(tt||a.type==="number")&&!/^0\d/.test(Me)?`${I5(Me)}`:Me;Ue!==_e&&(document.activeElement===P.value&&P.value.type!=="range"&&Ne&&Ue.trim()===_e||De())}),ye(()=>a.type,async()=>{await He(),De(),fe()}),mt(()=>{!a.formatter&&a.parser&&un(P5,"If you set the parser, you also need to set the formatter."),De(),He(fe)}),rn(()=>{se&&ti(se)}),t({input:b,textarea:_,ref:P,textareaStyle:Y,autosize:cn(a,"autosize"),isComposing:Le,passwordVisible:S,focus:rt,blur:nt,select:Rt,clear:Qt,resizeTextarea:fe}),(_e,Ne)=>(x(),O("div",{class:U([l.value,{[p(g).bm("group","append")]:_e.$slots.append,[p(g).bm("group","prepend")]:_e.$slots.prepend}]),style:et(V.value),onMouseenter:Wt,onMouseleave:Pt},[te(" input "),e.type!=="textarea"?(x(),O(Ve,{key:0},[te(" prepend slot "),_e.$slots.prepend?(x(),O("div",{key:0,class:U(p(g).be("group","prepend"))},[ce(_e.$slots,"prepend")],2)):te("v-if",!0),C("div",{ref_key:"wrapperRef",ref:R,class:U(s.value)},[te(" prefix slot "),_e.$slots.prefix||e.prefixIcon?(x(),O("span",{key:0,class:U(p(g).e("prefix"))},[C("span",{class:U(p(g).e("prefix-inner"))},[ce(_e.$slots,"prefix"),e.prefixIcon?(x(),ne(p(Xe),{key:0,class:U(p(g).e("icon"))},{default:$(()=>[(x(),ne(St(e.prefixIcon)))]),_:1},8,["class"])):te("v-if",!0)],2)],2)):te("v-if",!0),C("input",Tt({id:p(f),ref_key:"input",ref:b,class:p(g).e("inner")},p(u),{name:e.name,minlength:e.countGraphemes?void 0:e.minlength,maxlength:e.countGraphemes?void 0:c.value,type:e.showPassword?S.value?"text":"password":e.type,disabled:p(m),readonly:e.readonly,autocomplete:e.autocomplete,tabindex:e.tabindex,"aria-label":e.ariaLabel,placeholder:e.placeholder,style:e.inputStyle,form:e.form,autofocus:e.autofocus,role:e.containerRole,inputmode:e.inputmode,onCompositionstart:Ne[0]||(Ne[0]=(...tt)=>p(me)&&p(me)(...tt)),onCompositionupdate:Ne[1]||(Ne[1]=(...tt)=>p(Be)&&p(Be)(...tt)),onCompositionend:Ne[2]||(Ne[2]=(...tt)=>p(ze)&&p(ze)(...tt)),onInput:Re,onChange:Ce,onKeydown:Jt}),null,16,ose),te(" suffix slot "),J.value?(x(),O("span",{key:1,class:U(p(g).e("suffix"))},[C("span",{class:U(p(g).e("suffix-inner"))},[ee.value?(x(),ne(p(Xe),{key:0,class:U([p(g).e("icon"),p(g).e("clear")]),style:et({visibility:re.value?"visible":"hidden"}),onMousedown:at(p(kt),["prevent"]),onClick:Qt},{default:$(()=>[(x(),ne(St(e.clearIcon)))]),_:1},8,["class","style","onMousedown"])):te("v-if",!0),!re.value||!ie.value||!le.value?(x(),O(Ve,{key:1},[ce(_e.$slots,"suffix"),e.suffixIcon?(x(),ne(p(Xe),{key:0,class:U(p(g).e("icon"))},{default:$(()=>[(x(),ne(St(e.suffixIcon)))]),_:1},8,["class"])):te("v-if",!0)],64)):te("v-if",!0),ie.value?(x(),ne(p(Xe),{key:2,class:U([p(g).e("icon"),p(g).e("password")]),onClick:xe,onMousedown:at(p(kt),["prevent"]),onMouseup:at(p(kt),["prevent"])},{default:$(()=>[ce(_e.$slots,"password-icon",{visible:S.value},()=>[(x(),ne(St(N.value)))])]),_:3},8,["class","onMousedown","onMouseup"])):te("v-if",!0),le.value?(x(),O("span",{key:3,class:U([p(g).e("count"),p(g).is("outside",e.wordLimitPosition==="outside")])},[C("span",{class:U(p(g).e("count-inner"))},Q(ve.value)+" / "+Q(c.value),3)],2)):te("v-if",!0),B.value&&W.value&&z.value?(x(),ne(p(Xe),{key:4,class:U([p(g).e("icon"),p(g).e("validateIcon"),p(g).is("loading",B.value==="validating")])},{default:$(()=>[(x(),ne(St(W.value)))]),_:1},8,["class"])):te("v-if",!0)],2)],2)):te("v-if",!0)],2),te(" append slot "),_e.$slots.append?(x(),O("div",{key:1,class:U(p(g).be("group","append"))},[ce(_e.$slots,"append")],2)):te("v-if",!0)],64)):(x(),O(Ve,{key:1},[te(" textarea "),C("textarea",Tt({id:p(f),ref_key:"textarea",ref:_,class:[p(y).e("inner"),p(g).is("focus",p(G)),p(y).is("clearable",e.clearable)]},p(u),{name:e.name,minlength:e.countGraphemes?void 0:e.minlength,maxlength:e.countGraphemes?void 0:c.value,tabindex:e.tabindex,disabled:p(m),readonly:e.readonly,autocomplete:e.autocomplete,style:Y.value,"aria-label":e.ariaLabel,placeholder:e.placeholder,form:e.form,autofocus:e.autofocus,rows:e.rows,role:e.containerRole,inputmode:e.inputmode,onCompositionstart:Ne[3]||(Ne[3]=(...tt)=>p(me)&&p(me)(...tt)),onCompositionupdate:Ne[4]||(Ne[4]=(...tt)=>p(Be)&&p(Be)(...tt)),onCompositionend:Ne[5]||(Ne[5]=(...tt)=>p(ze)&&p(ze)(...tt)),onInput:Re,onFocus:Ne[6]||(Ne[6]=(...tt)=>p(K)&&p(K)(...tt)),onBlur:Ne[7]||(Ne[7]=(...tt)=>p(H)&&p(H)(...tt)),onChange:Ce,onKeydown:Jt}),null,16,ise),re.value?(x(),ne(p(Xe),{key:0,class:U([p(y).e("icon"),p(y).e("clear")]),style:et(T.value),onMousedown:at(p(kt),["prevent"]),onClick:Qt},{default:$(()=>[(x(),ne(St(e.clearIcon)))]),_:1},8,["class","style","onMousedown"])):te("v-if",!0),le.value?(x(),O("span",{key:1,style:et(k.value),class:U([p(g).e("count"),p(g).is("outside",e.wordLimitPosition==="outside")])},Q(ve.value)+" / "+Q(c.value),7)):te("v-if",!0)],64))],38))}}),sse=lse;const Ba=wt(sse),use=$e({...J6,valueKey:{type:String,default:"value"},modelValue:{type:[String,Number],default:""},debounce:{type:Number,default:300},placement:{type:ae(String),values:["top","top-start","top-end","bottom","bottom-start","bottom-end"],default:"bottom-start"},fetchSuggestions:{type:ae([Function,Array]),default:kt},popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,popperOptions:Tn.popperOptions,showArrow:{type:Boolean,default:!0},triggerOnFocus:{type:Boolean,default:!0},selectWhenUnmatched:Boolean,hideLoading:Boolean,teleported:Tn.teleported,appendTo:Tn.appendTo,highlightFirstItem:Boolean,fitInputWidth:Boolean,loopNavigation:{type:Boolean,default:!0}}),cse={[bt]:e=>Ye(e)||Je(e),[ua]:e=>Ye(e)||Je(e),[Ot]:e=>Ye(e)||Je(e),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0,select:e=>_t(e)},dse=$e({distance:{type:Number,default:0},height:{type:[String,Number],default:""},maxHeight:{type:[String,Number],default:""},native:Boolean,wrapStyle:{type:ae([String,Object,Array,Boolean]),default:""},wrapClass:{type:ae([String,Array,Object,Boolean]),default:""},viewClass:{type:ae([String,Array,Object,Boolean]),default:""},viewStyle:{type:ae([String,Object,Array,Boolean]),default:""},noresize:Boolean,tag:{type:String,default:"div"},always:Boolean,minSize:{type:Number,default:20},tabindex:{type:[String,Number],default:void 0},id:String,role:String,...dr(["ariaLabel","ariaOrientation"])}),hh={"end-reached":e=>["left","right","top","bottom"].includes(e),scroll:({scrollTop:e,scrollLeft:t})=>[e,t].every(Je)},lL={vertical:{offset:"offsetHeight",scroll:"scrollTop",scrollSize:"scrollHeight",size:"height",key:"vertical",axis:"Y",client:"clientY",direction:"top"},horizontal:{offset:"offsetWidth",scroll:"scrollLeft",scrollSize:"scrollWidth",size:"width",key:"horizontal",axis:"X",client:"clientX",direction:"left"}},fse=({move:e,size:t,bar:n})=>({[n.size]:t,transform:`translate${n.axis}(${e}%)`}),vse=$e({vertical:Boolean,size:String,move:Number,ratio:{type:Number,required:!0},always:Boolean}),Q6=Symbol("scrollbarContextKey");function Vi(e,t,n=.03){return e-t>n}const hse=$e({always:{type:Boolean,default:!0},minSize:{type:Number,required:!0}}),pse="Thumb";var mse=q({__name:"thumb",props:vse,setup(e){const t=e,n=Ze(Q6),a=ke("scrollbar");n||Dn(pse,"can not inject scrollbar context");const r=F(),o=F(),i=F({}),l=F(!1);let s=!1,u=!1,c=0,d=0,v=qt?document.onselectstart:null;const f=D(()=>lL[t.vertical?"vertical":"horizontal"]),h=D(()=>fse({size:t.size,move:t.move,bar:f.value})),m=D(()=>r.value[f.value.offset]**2/n.wrapElement[f.value.scrollSize]/t.ratio/o.value[f.value.offset]),g=M=>{var A;if(M.stopPropagation(),M.ctrlKey||[1,2].includes(M.button))return;(A=window.getSelection())==null||A.removeAllRanges(),b(M);const E=M.currentTarget;E&&(i.value[f.value.axis]=E[f.value.offset]-(M[f.value.client]-E.getBoundingClientRect()[f.value.direction]))},y=M=>{if(!o.value||!r.value||!n.wrapElement)return;const E=(Math.abs(M.target.getBoundingClientRect()[f.value.direction]-M[f.value.client])-o.value[f.value.offset]/2)*100*m.value/r.value[f.value.offset];n.wrapElement[f.value.scroll]=E*n.wrapElement[f.value.scrollSize]/100},b=M=>{M.stopImmediatePropagation(),s=!0,c=n.wrapElement.scrollHeight,d=n.wrapElement.scrollWidth,document.addEventListener("mousemove",_),document.addEventListener("mouseup",w),v=document.onselectstart,document.onselectstart=()=>!1},_=M=>{if(!r.value||!o.value||s===!1)return;const E=i.value[f.value.axis];if(!E)return;const A=((r.value.getBoundingClientRect()[f.value.direction]-M[f.value.client])*-1-(o.value[f.value.offset]-E))*100*m.value/r.value[f.value.offset];f.value.scroll==="scrollLeft"?n.wrapElement[f.value.scroll]=A*d/100:n.wrapElement[f.value.scroll]=A*c/100},w=()=>{s=!1,i.value[f.value.axis]=0,document.removeEventListener("mousemove",_),document.removeEventListener("mouseup",w),T(),u&&(l.value=!1)},S=()=>{u=!1,l.value=!!t.size},k=()=>{u=!0,l.value=s};rn(()=>{T(),document.removeEventListener("mouseup",w)});const T=()=>{document.onselectstart!==v&&(document.onselectstart=v)};return on(cn(n,"scrollbarElement"),"mousemove",S),on(cn(n,"scrollbarElement"),"mouseleave",k),(M,E)=>(x(),ne(Pa,{name:p(a).b("fade"),persisted:""},{default:$(()=>[st(C("div",{ref_key:"instance",ref:r,class:U([p(a).e("bar"),p(a).is(f.value.key)]),onMousedown:y,onClick:E[0]||(E[0]=at(()=>{},["stop"]))},[C("div",{ref_key:"thumb",ref:o,class:U(p(a).e("thumb")),style:et(h.value),onMousedown:g},null,38)],34),[[Zt,e.always||l.value]])]),_:1},8,["name"]))}}),D5=mse,gse=q({__name:"bar",props:hse,setup(e,{expose:t}){const n=e,a=Ze(Q6),r=F(0),o=F(0),i=F(""),l=F(""),s=F(1),u=F(1);return t({handleScroll:v=>{if(v){const f=v.offsetHeight-4,h=v.offsetWidth-4;o.value=v.scrollTop*100/f*s.value,r.value=v.scrollLeft*100/h*u.value}},update:()=>{const v=a==null?void 0:a.wrapElement;if(!v)return;const f=v.offsetHeight-4,h=v.offsetWidth-4,m=f**2/v.scrollHeight,g=h**2/v.scrollWidth,y=Math.max(m,n.minSize),b=Math.max(g,n.minSize);s.value=m/(f-m)/(y/(f-y)),u.value=g/(h-g)/(b/(h-b)),l.value=y+4(x(),O(Ve,null,[L(D5,{move:r.value,ratio:u.value,size:i.value,always:e.always},null,8,["move","ratio","size","always"]),L(D5,{move:o.value,ratio:s.value,size:l.value,vertical:"",always:e.always},null,8,["move","ratio","size","always"])],64))}}),yse=gse;const bse=["tabindex"],X2="ElScrollbar";var _se=q({name:X2,__name:"scrollbar",props:dse,emits:hh,setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("scrollbar");let i,l,s,u=0,c=0,d=0,v="";const f={bottom:!1,top:!1,right:!1,left:!1},h=F(),m=F(),g=F(),y=F(),b=D(()=>{const G={},K=Vn(a.height),H=Vn(a.maxHeight);return K&&(G.height=K),H&&(G.maxHeight=H),[a.wrapStyle,G]}),_=D(()=>[a.wrapClass,o.e("wrap"),{[o.em("wrap","hidden-default")]:!a.native}]),w=D(()=>[o.e("view"),a.viewClass]),S=G=>f[G]??!1,k={top:"bottom",bottom:"top",left:"right",right:"left"},T=G=>{const K=k[v];if(!K)return;const H=G[v],z=G[K];H&&!f[v]&&(f[v]=!0),!z&&f[K]&&(f[K]=!1)},M=()=>{var G;if(m.value){(G=y.value)==null||G.handleScroll(m.value);const K=c,H=d;c=m.value.scrollTop,d=m.value.scrollLeft;const z={bottom:!Vi(m.value.scrollHeight-a.distance,m.value.clientHeight+c),top:c<=a.distance&&K!==0,right:!Vi(m.value.scrollWidth-a.distance,m.value.clientWidth+d)&&H!==d,left:d<=a.distance&&H!==0};if(r("scroll",{scrollTop:c,scrollLeft:d}),K!==c&&(v=c>K?"bottom":"top"),H!==d&&(v=d>H?"right":"left"),a.distance>0){if(S(v))return;T(z)}z[v]&&r("end-reached",v)}};function E(G,K){_t(G)?m.value.scrollTo(G):Je(G)&&Je(K)&&m.value.scrollTo(G,K)}const A=G=>{if(!Je(G)){un(X2,"value must be a number");return}m.value.scrollTop=G},I=G=>{if(!Je(G)){un(X2,"value must be a number");return}m.value.scrollLeft=G},P=()=>{var G,K;(G=y.value)==null||G.update(),f[v]=!1,m.value&&((K=y.value)==null||K.handleScroll(m.value))},R=()=>{u||(u=requestAnimationFrame(()=>{var G,K;u=0,m.value&&((G=y.value)==null||G.update(),(K=y.value)==null||K.handleScroll(m.value))}))};return ye(()=>a.noresize,G=>{G?(i==null||i(),l==null||l(),s==null||s()):({stop:i}=Mn(g,P),{stop:l}=Mn(m,P),s=on("resize",P))},{immediate:!0}),ye(()=>[a.maxHeight,a.height],()=>{a.native||He(()=>{P()})}),Ft(Q6,nn({scrollbarElement:h,wrapElement:m})),Wh(()=>{m.value&&(m.value.scrollTop=c,m.value.scrollLeft=d)}),mt(()=>{a.native||He(()=>{P()})}),oi(()=>P()),t({wrapRef:m,update:P,scrollTo:E,setScrollTop:A,setScrollLeft:I,handleScroll:M}),(G,K)=>(x(),O("div",{ref_key:"scrollbarRef",ref:h,class:U(p(o).b())},[C("div",{ref_key:"wrapRef",ref:m,class:U(_.value),style:et(b.value),tabindex:e.tabindex,onScroll:M,onTransitionend:R,onAnimationend:R},[(x(),ne(St(e.tag),{id:e.id,ref_key:"resizeRef",ref:g,class:U(w.value),style:et(e.viewStyle),role:e.role,"aria-label":e.ariaLabel,"aria-orientation":e.ariaOrientation},{default:$(()=>[ce(G.$slots,"default")]),_:3},8,["id","class","style","role","aria-label","aria-orientation"]))],46,bse),e.native?te("v-if",!0):(x(),ne(yse,{key:0,ref_key:"barRef",ref:y,always:e.always,"min-size":e.minSize},null,8,["always","min-size"]))],2))}}),wse=_se;const ni=wt(wse),Sse=["aria-expanded","aria-owns"],Cse={key:0},xse=["id","aria-selected","onClick"],O5="ElAutocomplete";var kse=q({name:O5,inheritAttrs:!1,__name:"autocomplete",props:use,emits:cse,setup(e,{expose:t,emit:n}){const a=e,r=n,o=D(()=>{const J=Ba.props??[];return ji(a,Ae(J)?J:Object.keys(J))}),i=Nl(),l=$n(),s=ke("autocomplete"),u=F(),c=F(),d=F(),v=F();let f=!1,h=!1;const m=F([]),g=F(-1),y=F(""),b=F(!1),_=F(!1),w=F(!1),S=Fa(),k=D(()=>i.style),T=D(()=>(m.value.length>0||w.value)&&b.value),M=D(()=>!a.hideLoading&&w.value),E=D(()=>u.value?Array.from(u.value.$el.querySelectorAll("input")):[]),A=()=>{T.value&&(y.value=`${u.value.$el.offsetWidth}px`)},I=()=>{g.value=-1},P=async J=>{if(_.value)return;const Z=j=>{w.value=!1,!_.value&&(Ae(j)?(m.value=j,g.value=a.highlightFirstItem?0:-1):Dn(O5,"autocomplete suggestions must be an array"))};if(w.value=!0,Ae(a.fetchSuggestions))Z(a.fetchSuggestions);else{const j=await a.fetchSuggestions(J,Z);Ae(j)&&Z(j)}},R=Xh(P,D(()=>a.debounce)),G=J=>{const Z=!!J;if(r(ua,J),r(bt,J),_.value=!1,b.value||(b.value=Z),!a.triggerOnFocus&&!J){_.value=!0,m.value=[];return}R(J)},K=J=>{var Z;l.value||(((Z=J.target)==null?void 0:Z.tagName)!=="INPUT"||E.value.includes(document.activeElement))&&(b.value=!0)},H=J=>{r(Ot,J)},z=J=>{if(h)h=!1;else{b.value=!0,r("focus",J);const Z=a.modelValue??"";a.triggerOnFocus&&!f&&(m.value=[],g.value=-1,R(String(Z)))}},B=J=>{setTimeout(()=>{var Z;if((Z=d.value)!=null&&Z.isFocusInsideContent()){h=!0;return}b.value&&Y(),r("blur",J)})},W=()=>{b.value=!1,r(bt,""),r("clear")},N=async()=>{var J;(J=u.value)!=null&&J.isComposing||(T.value&&g.value>=0&&g.value{T.value&&(J.preventDefault(),J.stopPropagation(),Y())},Y=()=>{b.value=!1},X=()=>{var J;(J=u.value)==null||J.focus()},ee=()=>{var J;(J=u.value)==null||J.blur()},re=async J=>{r(ua,J[a.valueKey]),r(bt,J[a.valueKey]),r("select",J),m.value=[],g.value=-1},ie=J=>{var Se,De;if(!T.value||w.value)return;if(J<0){if(!a.loopNavigation){g.value=-1;return}J=m.value.length-1}J>=m.value.length&&(J=a.loopNavigation?0:m.value.length-1);const[Z,j]=le(),oe=j[J],se=Z.scrollTop,{offsetTop:fe,scrollHeight:ge}=oe;fe+ge>se+Z.clientHeight&&(Z.scrollTop=fe+ge-Z.clientHeight),fe{const J=c.value.querySelector(`.${s.be("suggestion","wrap")}`);return[J,J.querySelectorAll(`.${s.be("suggestion","list")} li`)]},ve=b6(v,J=>{var j;if((j=d.value)!=null&&j.isFocusInsideContent())return;const Z=h;h=!1,T.value&&(Z?B(new FocusEvent("blur",J)):Y())}),he=J=>{switch(mn(J)){case Pe.up:J.preventDefault(),ie(g.value-1);break;case Pe.down:J.preventDefault(),ie(g.value+1);break;case Pe.enter:case Pe.numpadEnter:J.preventDefault(),N();break;case Pe.tab:Y();break;case Pe.esc:V(J);break;case Pe.home:J.preventDefault(),ie(0);break;case Pe.end:J.preventDefault(),ie(m.value.length-1);break;case Pe.pageUp:J.preventDefault(),ie(Math.max(0,g.value-10));break;case Pe.pageDown:J.preventDefault(),ie(Math.min(m.value.length-1,g.value+10));break}};return rn(()=>{ve==null||ve()}),mt(()=>{var Z;const J=(Z=u.value)==null?void 0:Z.ref;J&&([{key:"role",value:"textbox"},{key:"aria-autocomplete",value:"list"},{key:"aria-controls",value:S.value},{key:"aria-activedescendant",value:`${S.value}-item-${g.value}`}].forEach(({key:j,value:oe})=>J.setAttribute(j,oe)),f=J.hasAttribute("readonly"))}),t({highlightedIndex:g,activated:b,loading:w,inputRef:u,popperRef:d,suggestions:m,handleSelect:re,handleKeyEnter:N,focus:X,blur:ee,close:Y,highlight:ie,getData:P}),(J,Z)=>(x(),ne(p(Ma),{ref_key:"popperRef",ref:d,visible:T.value,placement:e.placement,"fallback-placements":["bottom-start","top-start"],"popper-class":[p(s).e("popper"),e.popperClass],"popper-style":e.popperStyle,"popper-options":e.popperOptions,"show-arrow":e.showArrow,teleported:e.teleported,"append-to":e.appendTo,"gpu-acceleration":!1,pure:"","manual-mode":"",effect:"light",trigger:"click",transition:`${p(s).namespace.value}-zoom-in-top`,persistent:"",role:"listbox",onBeforeShow:A,onHide:I},{content:$(()=>[C("div",{ref_key:"regionRef",ref:c,class:U([p(s).b("suggestion"),p(s).is("loading",M.value)]),style:et({[e.fitInputWidth?"width":"minWidth"]:y.value,outline:"none"}),role:"region"},[J.$slots.header?(x(),O("div",{key:0,class:U(p(s).be("suggestion","header")),onClick:Z[0]||(Z[0]=at(()=>{},["stop"]))},[ce(J.$slots,"header")],2)):te("v-if",!0),L(p(ni),{id:p(S),tag:"ul","wrap-class":p(s).be("suggestion","wrap"),"view-class":p(s).be("suggestion","list"),role:"listbox"},{default:$(()=>[M.value?(x(),O("li",Cse,[ce(J.$slots,"loading",{},()=>[L(p(Xe),{class:U(p(s).is("loading"))},{default:$(()=>[L(p(Mr))]),_:1},8,["class"])])])):(x(!0),O(Ve,{key:1},dt(m.value,(j,oe)=>(x(),O("li",{id:`${p(S)}-item-${oe}`,key:oe,class:U({highlighted:g.value===oe}),role:"option","aria-selected":g.value===oe,onClick:se=>re(j)},[ce(J.$slots,"default",{item:j},()=>[ue(Q(j[e.valueKey]),1)])],10,xse))),128))]),_:3},8,["id","wrap-class","view-class"]),J.$slots.footer?(x(),O("div",{key:1,class:U(p(s).be("suggestion","footer")),onClick:Z[1]||(Z[1]=at(()=>{},["stop"]))},[ce(J.$slots,"footer")],2)):te("v-if",!0)],6)]),default:$(()=>[C("div",{ref_key:"listboxRef",ref:v,class:U([p(s).b(),J.$attrs.class]),style:et(k.value),role:"combobox","aria-haspopup":"listbox","aria-expanded":T.value,"aria-owns":p(S)},[L(p(Ba),Tt({ref_key:"inputRef",ref:u},Tt(o.value,J.$attrs),{"model-value":e.modelValue,disabled:p(l),onInput:G,onChange:H,onFocus:z,onBlur:B,onClear:W,onKeydown:he,onMousedown:K}),xr({_:2},[J.$slots.prepend?{name:"prepend",fn:$(()=>[ce(J.$slots,"prepend")]),key:"0"}:void 0,J.$slots.append?{name:"append",fn:$(()=>[ce(J.$slots,"append")]),key:"1"}:void 0,J.$slots.prefix?{name:"prefix",fn:$(()=>[ce(J.$slots,"prefix")]),key:"2"}:void 0,J.$slots.suffix?{name:"suffix",fn:$(()=>[ce(J.$slots,"suffix")]),key:"3"}:void 0]),1040,["model-value","disabled"])],14,Sse)]),_:3},8,["visible","placement","popper-class","popper-style","popper-options","show-arrow","teleported","append-to","transition"]))}}),Tse=kse;const Mse=wt(Tse),Ese=$e({size:{type:[Number,String],values:ii,validator:e=>Je(e)},shape:{type:String,values:["circle","square"]},icon:{type:pn},src:{type:String,default:""},alt:String,srcSet:String,fit:{type:ae(String),default:"cover"}}),Ase={error:e=>e instanceof Event},sL=Symbol("avatarGroupContextKey"),Ise={size:{type:ae([Number,String]),values:ii,validator:e=>Je(e)},shape:{type:ae(String),values:["circle","square"]},collapseAvatars:Boolean,collapseAvatarsTooltip:Boolean,maxCollapseAvatars:{type:Number,default:1},effect:{type:ae(String),default:"light"},placement:{type:ae(String),values:tl,default:"top"},popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,collapseClass:{type:ae([String,Array,Object,Boolean]),default:void 0},collapseStyle:{type:ae([String,Array,Object,Boolean]),default:void 0}},Lse=["src","alt","srcset"];var Pse=q({name:"ElAvatar",__name:"avatar",props:Ese,emits:Ase,setup(e,{emit:t}){const n=e,a=t,r=Ze(sL,void 0),o=ke("avatar"),i=F(!1),l=D(()=>n.size??(r==null?void 0:r.size)),s=D(()=>n.shape??(r==null?void 0:r.shape)??"circle"),u=D(()=>{const{icon:f}=n,h=[o.b()];return Ye(l.value)&&h.push(o.m(l.value)),f&&h.push(o.m("icon")),s.value&&h.push(o.m(s.value)),h}),c=D(()=>Je(l.value)?o.cssVarBlock({size:Vn(l.value)}):void 0),d=D(()=>({objectFit:n.fit}));ye(()=>[n.src,n.srcSet],()=>i.value=!1);function v(f){i.value=!0,a("error",f)}return(f,h)=>(x(),O("span",{class:U(u.value),style:et(c.value)},[(e.src||e.srcSet)&&!i.value?(x(),O("img",{key:0,src:e.src,alt:e.alt,srcset:e.srcSet,style:et(d.value),onError:v},null,44,Lse)):e.icon?(x(),ne(p(Xe),{key:1},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1})):ce(f.$slots,"default",{key:2})],6))}}),uL=Pse,cL=q({name:"ElAvatarGroup",props:Ise,setup(e,{slots:t}){const n=ke("avatar-group");return Ft(sL,nn({size:cn(e,"size"),shape:cn(e,"shape")})),()=>{var o;const a=eo(((o=t.default)==null?void 0:o.call(t))??[]);let r=a;if(e.collapseAvatars&&a.length>e.maxCollapseAvatars){r=a.slice(0,e.maxCollapseAvatars);const i=a.slice(e.maxCollapseAvatars);r.push(L(Ma,{popperClass:e.popperClass,popperStyle:e.popperStyle,placement:e.placement,effect:e.effect,disabled:!e.collapseAvatarsTooltip},{default:()=>L(uL,{size:e.size,shape:e.shape,class:e.collapseClass,style:e.collapseStyle},{default:()=>[ue("+ "),i.length]}),content:()=>L("div",{class:n.e("collapse-avatars")},[i.map((l,s)=>bn(l)?ei(l,{key:l.key??s}):l)])}))}return L("div",{class:n.b()},[r])}}});const Dse=wt(uL,{AvatarGroup:cL}),Ose=On(cL),$se={visibilityHeight:{type:Number,default:200},target:{type:String,default:""},right:{type:Number,default:40},bottom:{type:Number,default:40}},Rse={click:e=>e instanceof MouseEvent},Nse=(e,t,n)=>{const a=Ht(),r=Ht(),o=F(!1),i=()=>{a.value&&(o.value=a.value.scrollTop>=e.visibilityHeight)},l=s=>{var u;(u=a.value)==null||u.scrollTo({top:0,behavior:"smooth"}),t("click",s)};return on(r,"scroll",LA(i,300,!0)),mt(()=>{r.value=document,a.value=document.documentElement,e.target&&(a.value=document.querySelector(e.target)??void 0,a.value||Dn(n,`target does not exist: ${e.target}`),r.value=a.value),i()}),{visible:o,handleClick:l}},$5="ElBacktop";var Bse=q({name:$5,__name:"backtop",props:$se,emits:Rse,setup(e,{emit:t}){const n=e,a=t,r=ke("backtop"),{handleClick:o,visible:i}=Nse(n,a,$5),l=D(()=>({right:`${n.right}px`,bottom:`${n.bottom}px`}));return(s,u)=>(x(),ne(Pa,{name:`${p(r).namespace.value}-fade-in`},{default:$(()=>[p(i)?(x(),O("div",{key:0,style:et(l.value),class:U(p(r).b()),onClick:u[0]||(u[0]=at((...c)=>p(o)&&p(o)(...c),["stop"]))},[ce(s.$slots,"default",{},()=>[L(p(Xe),{class:U(p(r).e("icon"))},{default:$(()=>[L(p(TI))]),_:1},8,["class"])])],6)):te("v-if",!0)]),_:3},8,["name"]))}}),Vse=Bse;const zse=wt(Vse),Fse=$e({value:{type:[String,Number],default:""},max:{type:Number,default:99},isDot:Boolean,hidden:Boolean,type:{type:String,values:["primary","success","warning","info","danger"],default:"danger"},showZero:{type:Boolean,default:!0},color:String,badgeStyle:{type:ae([String,Object,Array,Boolean]),default:void 0},offset:{type:ae(Array),default:()=>[0,0]},badgeClass:{type:ae([String,Array,Object,Boolean]),default:void 0}});var Hse=q({name:"ElBadge",__name:"badge",props:Fse,setup(e,{expose:t}){const n=e,a=ke("badge"),r=D(()=>n.isDot?"":Je(n.value)&&Je(n.max)?n.max[{backgroundColor:n.color,marginRight:Vn(-n.offset[0]),marginTop:Vn(n.offset[1])},n.badgeStyle??{}]);return t({content:r}),(i,l)=>(x(),O("div",{class:U(p(a).b())},[ce(i.$slots,"default"),L(Pa,{name:`${p(a).namespace.value}-zoom-in-center`},{default:$(()=>[!e.hidden&&(r.value||e.isDot||i.$slots.content)?(x(),O("sup",{key:0,class:U([p(a).e("content"),p(a).em("content",e.type),p(a).is("fixed",!!i.$slots.default),p(a).is("dot",e.isDot),p(a).is("hide-zero",!e.showZero&&e.value===0),e.badgeClass]),style:et(o.value)},[ce(i.$slots,"content",{value:r.value},()=>[ue(Q(r.value),1)])],6)):te("v-if",!0)]),_:3},8,["name"])],2))}}),Wse=Hse;const dL=wt(Wse),Kse=$e({separator:{type:String,default:"/"},separatorIcon:{type:pn}}),fL=Symbol("breadcrumbKey"),Use=$e({to:{type:ae([String,Object]),default:""},replace:Boolean}),Gse=["aria-label"];var Yse=q({name:"ElBreadcrumb",__name:"breadcrumb",props:Kse,setup(e){const{t}=Vt(),n=e,a=ke("breadcrumb"),r=F();return Ft(fL,n),mt(()=>{const o=r.value.querySelectorAll(`.${a.e("item")}`);o.length&&o[o.length-1].setAttribute("aria-current","page")}),(o,i)=>(x(),O("div",{ref_key:"breadcrumb",ref:r,class:U(p(a).b()),"aria-label":p(t)("el.breadcrumb.label"),role:"navigation"},[ce(o.$slots,"default")],10,Gse))}}),jse=Yse,qse=q({name:"ElBreadcrumbItem",__name:"breadcrumb-item",props:Use,setup(e){const t=e,n=Et(),a=Ze(fL,void 0),r=ke("breadcrumb"),o=n.appContext.config.globalProperties.$router,i=()=>{!t.to||!o||(t.replace?o.replace(t.to):o.push(t.to))};return(l,s)=>{var u,c;return x(),O("span",{class:U(p(r).e("item"))},[C("span",{class:U([p(r).e("inner"),p(r).is("link",!!e.to)]),role:"link",onClick:i},[ce(l.$slots,"default")],2),(u=p(a))!=null&&u.separatorIcon?(x(),ne(p(Xe),{key:0,class:U(p(r).e("separator"))},{default:$(()=>[(x(),ne(St(p(a).separatorIcon)))]),_:1},8,["class"])):(x(),O("span",{key:1,class:U(p(r).e("separator")),role:"presentation"},Q((c=p(a))==null?void 0:c.separator),3))],2)}}}),vL=qse;const Xse=wt(jse,{BreadcrumbItem:vL}),Zse=On(vL),x4=["default","primary","success","warning","info","danger","text",""],Jse=["button","submit","reset"],k4=$e({size:pa,disabled:{type:Boolean,default:void 0},type:{type:String,values:x4,default:""},icon:{type:pn},nativeType:{type:String,values:Jse,default:"button"},loading:Boolean,loadingIcon:{type:pn,default:()=>Mr},plain:{type:Boolean,default:void 0},text:{type:Boolean,default:void 0},link:Boolean,bg:Boolean,autofocus:Boolean,round:{type:Boolean,default:void 0},circle:Boolean,dashed:{type:Boolean,default:void 0},color:String,dark:Boolean,autoInsertSpace:{type:Boolean,default:void 0},tag:{type:ae([String,Object]),default:"button"}}),Qse={click:e=>e instanceof MouseEvent},hL=Symbol(),Xm=F();function Hl(e,t=void 0){const n=Et()?Ze(hL,Xm):Xm;return e?D(()=>{var a;return((a=n.value)==null?void 0:a[e])??t}):n}function W1(e,t){const n=Hl(),a=ke(e,D(()=>{var l;return((l=n.value)==null?void 0:l.namespace)||"el"})),r=Vt(D(()=>{var l;return(l=n.value)==null?void 0:l.locale})),o=tp(D(()=>{var s;const l=(s=n.value)==null?void 0:s.zIndex;return Un(l)||Number.isNaN(l)?rI:l})),i=D(()=>{var l;return p(t)||((l=n.value)==null?void 0:l.size)||""});return ew(D(()=>p(n)||{})),{ns:a,locale:r,zIndex:o,size:i}}const ew=(e,t,n=!1)=>{const a=!!Et(),r=a?Hl():void 0,o=(t==null?void 0:t.provide)??(a?Ft:void 0);if(!o){un("provideGlobalConfig","provideGlobalConfig() can only be used inside setup().");return}const i=D(()=>{const l=p(e);return r!=null&&r.value?eue(r.value,l):l});return o(hL,i),o(VA,D(()=>i.value.locale)),o(zA,D(()=>i.value.namespace)),o(oI,D(()=>i.value.zIndex)),o(yI,{size:D(()=>i.value.size||"")}),o(_I,D(()=>({emptyValues:i.value.emptyValues,valueOnClear:i.value.valueOnClear}))),(n||!Xm.value)&&(Xm.value=i.value),i},eue=(e,t)=>{const n=[...new Set([...sh(e),...sh(t)])],a={};for(const r of n)a[r]=t[r]!==void 0?t[r]:e[r];return a},tue=$e({a11y:{type:Boolean,default:!0},locale:{type:ae(Object)},size:pa,button:{type:ae(Object)},card:{type:ae(Object)},dialog:{type:ae(Object)},link:{type:ae(Object)},experimentalFeatures:{type:ae(Object)},keyboardNavigation:{type:Boolean,default:!0},message:{type:ae(Object)},zIndex:Number,namespace:{type:String,default:"el"},table:{type:ae(Object)},...Mc}),Qa={placement:"top"},nue=q({name:"ElConfigProvider",props:tue,setup(e,{slots:t}){const n=ew(e);return ye(()=>e.message,a=>{var r;Object.assign(Qa,((r=n==null?void 0:n.value)==null?void 0:r.message)??{},a??{})},{immediate:!0,deep:!0}),()=>ce(t,"default",{config:n==null?void 0:n.value})}}),aue=wt(nue),pL=Symbol("buttonGroupContextKey"),rue=(e,t)=>{Ri({from:"type.text",replacement:"link",version:"3.0.0",scope:"props",ref:"https://element-plus.org/en-US/component/button.html#button-attributes"},D(()=>e.type==="text"));const n=Ze(pL,void 0),a=Hl("button"),{form:r}=Ea(),o=ca(D(()=>n==null?void 0:n.size)),i=$n(),l=F(),s=Xn(),u=D(()=>{var b;return e.type||(n==null?void 0:n.type)||((b=a.value)==null?void 0:b.type)||""}),c=D(()=>{var b;return e.autoInsertSpace??((b=a.value)==null?void 0:b.autoInsertSpace)??!1}),d=D(()=>{var b;return e.plain??((b=a.value)==null?void 0:b.plain)??!1}),v=D(()=>{var b;return e.round??((b=a.value)==null?void 0:b.round)??!1}),f=D(()=>{var b;return e.text??((b=a.value)==null?void 0:b.text)??!1}),h=D(()=>{var b;return e.dashed??((b=a.value)==null?void 0:b.dashed)??!1}),m=D(()=>e.tag==="button"?{ariaDisabled:i.value||e.loading,disabled:i.value||e.loading,autofocus:e.autofocus,type:e.nativeType}:{}),g=D(()=>{var _;const b=(_=s.default)==null?void 0:_.call(s);if(c.value&&(b==null?void 0:b.length)===1){const w=b[0];if((w==null?void 0:w.type)===uf){const S=w.children;return new RegExp("^\\p{Unified_Ideograph}{2}$","u").test(S.trim())}}return!1});return{_disabled:i,_size:o,_type:u,_ref:l,_props:m,_plain:d,_round:v,_text:f,_dashed:h,shouldAddSpace:g,handleClick:b=>{if(i.value||e.loading){b.stopPropagation();return}e.nativeType==="reset"&&(r==null||r.resetFields()),t("click",b)}}};function Ya(e,t){oue(e)&&(e="100%");const n=iue(e);return e=t===360?e:Math.min(t,Math.max(0,parseFloat(e))),n&&(e=parseInt(String(e*t),10)/100),Math.abs(e-t)<1e-6?1:(t===360?e=(e<0?e%t+t:e%t)/parseFloat(String(t)):e=e%t/parseFloat(String(t)),e)}function Fp(e){return Math.min(1,Math.max(0,e))}function oue(e){return typeof e=="string"&&e.indexOf(".")!==-1&&parseFloat(e)===1}function iue(e){return typeof e=="string"&&e.indexOf("%")!==-1}function mL(e){return e=parseFloat(e),(isNaN(e)||e<0||e>1)&&(e=1),e}function Hp(e){return Number(e)<=1?`${Number(e)*100}%`:e}function Ru(e){return e.length===1?"0"+e:String(e)}function lue(e,t,n){return{r:Ya(e,255)*255,g:Ya(t,255)*255,b:Ya(n,255)*255}}function R5(e,t,n){e=Ya(e,255),t=Ya(t,255),n=Ya(n,255);const a=Math.max(e,t,n),r=Math.min(e,t,n);let o=0,i=0;const l=(a+r)/2;if(a===r)i=0,o=0;else{const s=a-r;switch(i=l>.5?s/(2-a-r):s/(a+r),a){case e:o=(t-n)/s+(t1&&(n-=1),n<1/6?e+(t-e)*(6*n):n<1/2?t:n<2/3?e+(t-e)*(2/3-n)*6:e}function sue(e,t,n){let a,r,o;if(e=Ya(e,360),t=Ya(t,100),n=Ya(n,100),t===0)r=n,o=n,a=n;else{const i=n<.5?n*(1+t):n+t-n*t,l=2*n-i;a=Z2(l,i,e+1/3),r=Z2(l,i,e),o=Z2(l,i,e-1/3)}return{r:a*255,g:r*255,b:o*255}}function N5(e,t,n){e=Ya(e,255),t=Ya(t,255),n=Ya(n,255);const a=Math.max(e,t,n),r=Math.min(e,t,n);let o=0;const i=a,l=a-r,s=a===0?0:l/a;if(a===r)o=0;else{switch(a){case e:o=(t-n)/l+(t>16,g:(e&65280)>>8,b:e&255}}const T4={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",goldenrod:"#daa520",gold:"#ffd700",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavenderblush:"#fff0f5",lavender:"#e6e6fa",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};function hue(e){let t={r:0,g:0,b:0},n=1,a=null,r=null,o=null,i=!1,l=!1;return typeof e=="string"&&(e=gue(e)),typeof e=="object"&&(qr(e.r)&&qr(e.g)&&qr(e.b)?(t=lue(e.r,e.g,e.b),i=!0,l=String(e.r).substr(-1)==="%"?"prgb":"rgb"):qr(e.h)&&qr(e.s)&&qr(e.v)?(a=Hp(e.s),r=Hp(e.v),t=uue(e.h,a,r),i=!0,l="hsv"):qr(e.h)&&qr(e.s)&&qr(e.l)?(a=Hp(e.s),o=Hp(e.l),t=sue(e.h,a,o),i=!0,l="hsl"):qr(e.c)&&qr(e.m)&&qr(e.y)&&qr(e.k)&&(t=due(e.c,e.m,e.y,e.k),i=!0,l="cmyk"),Object.prototype.hasOwnProperty.call(e,"a")&&(n=e.a)),n=mL(n),{ok:i,format:e.format||l,r:Math.min(255,Math.max(t.r,0)),g:Math.min(255,Math.max(t.g,0)),b:Math.min(255,Math.max(t.b,0)),a:n}}const pue="[-\\+]?\\d+%?",mue="[-\\+]?\\d*\\.\\d+%?",ms="(?:"+mue+")|(?:"+pue+")",J2="[\\s|\\(]+("+ms+")[,|\\s]+("+ms+")[,|\\s]+("+ms+")\\s*\\)?",Wp="[\\s|\\(]+("+ms+")[,|\\s]+("+ms+")[,|\\s]+("+ms+")[,|\\s]+("+ms+")\\s*\\)?",ho={CSS_UNIT:new RegExp(ms),rgb:new RegExp("rgb"+J2),rgba:new RegExp("rgba"+Wp),hsl:new RegExp("hsl"+J2),hsla:new RegExp("hsla"+Wp),hsv:new RegExp("hsv"+J2),hsva:new RegExp("hsva"+Wp),cmyk:new RegExp("cmyk"+Wp),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/};function gue(e){if(e=e.trim().toLowerCase(),e.length===0)return!1;let t=!1;if(T4[e])e=T4[e],t=!0;else if(e==="transparent")return{r:0,g:0,b:0,a:0,format:"name"};let n=ho.rgb.exec(e);return n?{r:n[1],g:n[2],b:n[3]}:(n=ho.rgba.exec(e),n?{r:n[1],g:n[2],b:n[3],a:n[4]}:(n=ho.hsl.exec(e),n?{h:n[1],s:n[2],l:n[3]}:(n=ho.hsla.exec(e),n?{h:n[1],s:n[2],l:n[3],a:n[4]}:(n=ho.hsv.exec(e),n?{h:n[1],s:n[2],v:n[3]}:(n=ho.hsva.exec(e),n?{h:n[1],s:n[2],v:n[3],a:n[4]}:(n=ho.cmyk.exec(e),n?{c:n[1],m:n[2],y:n[3],k:n[4]}:(n=ho.hex8.exec(e),n?{r:Xr(n[1]),g:Xr(n[2]),b:Xr(n[3]),a:z5(n[4]),format:t?"name":"hex8"}:(n=ho.hex6.exec(e),n?{r:Xr(n[1]),g:Xr(n[2]),b:Xr(n[3]),format:t?"name":"hex"}:(n=ho.hex4.exec(e),n?{r:Xr(n[1]+n[1]),g:Xr(n[2]+n[2]),b:Xr(n[3]+n[3]),a:z5(n[4]+n[4]),format:t?"name":"hex8"}:(n=ho.hex3.exec(e),n?{r:Xr(n[1]+n[1]),g:Xr(n[2]+n[2]),b:Xr(n[3]+n[3]),format:t?"name":"hex"}:!1))))))))))}function qr(e){return typeof e=="number"?!Number.isNaN(e):ho.CSS_UNIT.test(e)}class ea{constructor(t="",n={}){if(t instanceof ea)return t;typeof t=="number"&&(t=vue(t)),this.originalInput=t;const a=hue(t);this.originalInput=t,this.r=a.r,this.g=a.g,this.b=a.b,this.a=a.a,this.roundA=Math.round(100*this.a)/100,this.format=n.format??a.format,this.gradientType=n.gradientType,this.r<1&&(this.r=Math.round(this.r)),this.g<1&&(this.g=Math.round(this.g)),this.b<1&&(this.b=Math.round(this.b)),this.isValid=a.ok}isDark(){return this.getBrightness()<128}isLight(){return!this.isDark()}getBrightness(){const t=this.toRgb();return(t.r*299+t.g*587+t.b*114)/1e3}getLuminance(){const t=this.toRgb();let n,a,r;const o=t.r/255,i=t.g/255,l=t.b/255;return o<=.03928?n=o/12.92:n=Math.pow((o+.055)/1.055,2.4),i<=.03928?a=i/12.92:a=Math.pow((i+.055)/1.055,2.4),l<=.03928?r=l/12.92:r=Math.pow((l+.055)/1.055,2.4),.2126*n+.7152*a+.0722*r}getAlpha(){return this.a}setAlpha(t){return this.a=mL(t),this.roundA=Math.round(100*this.a)/100,this}isMonochrome(){const{s:t}=this.toHsl();return t===0}toHsv(){const t=N5(this.r,this.g,this.b);return{h:t.h*360,s:t.s,v:t.v,a:this.a}}toHsvString(){const t=N5(this.r,this.g,this.b),n=Math.round(t.h*360),a=Math.round(t.s*100),r=Math.round(t.v*100);return this.a===1?`hsv(${n}, ${a}%, ${r}%)`:`hsva(${n}, ${a}%, ${r}%, ${this.roundA})`}toHsl(){const t=R5(this.r,this.g,this.b);return{h:t.h*360,s:t.s,l:t.l,a:this.a}}toHslString(){const t=R5(this.r,this.g,this.b),n=Math.round(t.h*360),a=Math.round(t.s*100),r=Math.round(t.l*100);return this.a===1?`hsl(${n}, ${a}%, ${r}%)`:`hsla(${n}, ${a}%, ${r}%, ${this.roundA})`}toHex(t=!1){return B5(this.r,this.g,this.b,t)}toHexString(t=!1){return"#"+this.toHex(t)}toHex8(t=!1){return cue(this.r,this.g,this.b,this.a,t)}toHex8String(t=!1){return"#"+this.toHex8(t)}toHexShortString(t=!1){return this.a===1?this.toHexString(t):this.toHex8String(t)}toRgb(){return{r:Math.round(this.r),g:Math.round(this.g),b:Math.round(this.b),a:this.a}}toRgbString(){const t=Math.round(this.r),n=Math.round(this.g),a=Math.round(this.b);return this.a===1?`rgb(${t}, ${n}, ${a})`:`rgba(${t}, ${n}, ${a}, ${this.roundA})`}toPercentageRgb(){const t=n=>`${Math.round(Ya(n,255)*100)}%`;return{r:t(this.r),g:t(this.g),b:t(this.b),a:this.a}}toPercentageRgbString(){const t=n=>Math.round(Ya(n,255)*100);return this.a===1?`rgb(${t(this.r)}%, ${t(this.g)}%, ${t(this.b)}%)`:`rgba(${t(this.r)}%, ${t(this.g)}%, ${t(this.b)}%, ${this.roundA})`}toCmyk(){return{...V5(this.r,this.g,this.b)}}toCmykString(){const{c:t,m:n,y:a,k:r}=V5(this.r,this.g,this.b);return`cmyk(${t}, ${n}, ${a}, ${r})`}toName(){if(this.a===0)return"transparent";if(this.a<1)return!1;const t="#"+B5(this.r,this.g,this.b,!1);for(const[n,a]of Object.entries(T4))if(t===a)return n;return!1}toString(t){const n=!!t;t=t??this.format;let a=!1;const r=this.a<1&&this.a>=0;return!n&&r&&(t.startsWith("hex")||t==="name")?t==="name"&&this.a===0?this.toName():this.toRgbString():(t==="rgb"&&(a=this.toRgbString()),t==="prgb"&&(a=this.toPercentageRgbString()),(t==="hex"||t==="hex6")&&(a=this.toHexString()),t==="hex3"&&(a=this.toHexString(!0)),t==="hex4"&&(a=this.toHex8String(!0)),t==="hex8"&&(a=this.toHex8String()),t==="name"&&(a=this.toName()),t==="hsl"&&(a=this.toHslString()),t==="hsv"&&(a=this.toHsvString()),t==="cmyk"&&(a=this.toCmykString()),a||this.toHexString())}toNumber(){return(Math.round(this.r)<<16)+(Math.round(this.g)<<8)+Math.round(this.b)}clone(){return new ea(this.toString())}lighten(t=10){const n=this.toHsl();return n.l+=t/100,n.l=Fp(n.l),new ea(n)}brighten(t=10){const n=this.toRgb();return n.r=Math.max(0,Math.min(255,n.r-Math.round(255*-(t/100)))),n.g=Math.max(0,Math.min(255,n.g-Math.round(255*-(t/100)))),n.b=Math.max(0,Math.min(255,n.b-Math.round(255*-(t/100)))),new ea(n)}darken(t=10){const n=this.toHsl();return n.l-=t/100,n.l=Fp(n.l),new ea(n)}tint(t=10){return this.mix("white",t)}shade(t=10){return this.mix("black",t)}desaturate(t=10){const n=this.toHsl();return n.s-=t/100,n.s=Fp(n.s),new ea(n)}saturate(t=10){const n=this.toHsl();return n.s+=t/100,n.s=Fp(n.s),new ea(n)}greyscale(){return this.desaturate(100)}spin(t){const n=this.toHsl(),a=(n.h+t)%360;return n.h=a<0?360+a:a,new ea(n)}mix(t,n=50){const a=this.toRgb(),r=new ea(t).toRgb(),o=n/100,i={r:(r.r-a.r)*o+a.r,g:(r.g-a.g)*o+a.g,b:(r.b-a.b)*o+a.b,a:(r.a-a.a)*o+a.a};return new ea(i)}analogous(t=6,n=30){const a=this.toHsl(),r=360/n,o=[this];for(a.h=(a.h-(r*t>>1)+720)%360;--t;)a.h=(a.h+r)%360,o.push(new ea(a));return o}complement(){const t=this.toHsl();return t.h=(t.h+180)%360,new ea(t)}monochromatic(t=6){const n=this.toHsv(),{h:a}=n,{s:r}=n;let{v:o}=n;const i=[],l=1/t;for(;t--;)i.push(new ea({h:a,s:r,v:o})),o=(o+l)%1;return i}splitcomplement(){const t=this.toHsl(),{h:n}=t;return[this,new ea({h:(n+72)%360,s:t.s,l:t.l}),new ea({h:(n+216)%360,s:t.s,l:t.l})]}onBackground(t){const n=this.toRgb(),a=new ea(t).toRgb(),r=n.a+a.a*(1-n.a);return new ea({r:(n.r*n.a+a.r*a.a*(1-n.a))/r,g:(n.g*n.a+a.g*a.a*(1-n.a))/r,b:(n.b*n.a+a.b*a.a*(1-n.a))/r,a:r})}triad(){return this.polyad(3)}tetrad(){return this.polyad(4)}polyad(t){const n=this.toHsl(),{h:a}=n,r=[this],o=360/t;for(let i=1;i{let a={},r=e.color;if(r){const o=r.match(/var\((.*?)\)/);o&&(r=window.getComputedStyle(window.document.documentElement).getPropertyValue(o[1]));const i=new ea(r),l=e.dark?i.tint(20).toString():gi(i,20);if(e.plain)a=n.cssVarBlock({"bg-color":e.dark?gi(i,90):i.tint(90).toString(),"text-color":r,"border-color":e.dark?gi(i,50):i.tint(50).toString(),"hover-text-color":`var(${n.cssVarName("color-white")})`,"hover-bg-color":r,"hover-border-color":r,"active-bg-color":l,"active-text-color":`var(${n.cssVarName("color-white")})`,"active-border-color":l}),t.value&&(a[n.cssVarBlockName("disabled-bg-color")]=e.dark?gi(i,90):i.tint(90).toString(),a[n.cssVarBlockName("disabled-text-color")]=e.dark?gi(i,50):i.tint(50).toString(),a[n.cssVarBlockName("disabled-border-color")]=e.dark?gi(i,80):i.tint(80).toString());else if(e.link||e.text){const s=e.dark?gi(i,30):i.tint(30).toString();if(a=n.cssVarBlock({"text-color":r,"hover-text-color":s,"active-text-color":l}),e.link&&(a[n.cssVarBlockName("hover-link-text-color")]=s,a[n.cssVarBlockName("active-color")]=l),t.value){const u=e.dark?gi(i,50):i.tint(50).toString();a[n.cssVarBlockName("disabled-bg-color")]="transparent",a[n.cssVarBlockName("disabled-text-color")]=u,a[n.cssVarBlockName("disabled-border-color")]="transparent"}}else{const s=e.dark?gi(i,30):i.tint(30).toString(),u=i.isDark()?`var(${n.cssVarName("color-white")})`:`var(${n.cssVarName("color-black")})`;if(a=n.cssVarBlock({"bg-color":r,"text-color":u,"border-color":r,"hover-bg-color":s,"hover-text-color":u,"hover-border-color":s,"active-bg-color":l,"active-border-color":l}),t.value){const c=e.dark?gi(i,50):i.tint(50).toString();a[n.cssVarBlockName("disabled-bg-color")]=c,a[n.cssVarBlockName("disabled-text-color")]=e.dark?"rgba(255, 255, 255, 0.5)":`var(${n.cssVarName("color-white")})`,a[n.cssVarBlockName("disabled-border-color")]=c}}}return a})}var bue=q({name:"ElButton",__name:"button",props:k4,emits:Qse,setup(e,{expose:t,emit:n}){const a=e,r=n,o=yue(a),i=ke("button"),{_ref:l,_size:s,_type:u,_disabled:c,_props:d,_plain:v,_round:f,_text:h,_dashed:m,shouldAddSpace:g,handleClick:y}=rue(a,r),b=D(()=>[i.b(),i.m(u.value),i.m(s.value),i.is("disabled",c.value),i.is("loading",a.loading),i.is("plain",v.value),i.is("round",f.value),i.is("circle",a.circle),i.is("text",h.value),i.is("dashed",m.value),i.is("link",a.link),i.is("has-bg",a.bg)]);return t({ref:l,size:s,type:u,disabled:c,shouldAddSpace:g}),(_,w)=>(x(),ne(St(e.tag),Tt({ref_key:"_ref",ref:l},p(d),{class:b.value,style:p(o),onClick:p(y)}),{default:$(()=>[e.loading?(x(),O(Ve,{key:0},[_.$slots.loading?ce(_.$slots,"loading",{key:0}):(x(),ne(p(Xe),{key:1,class:U(p(i).is("loading"))},{default:$(()=>[(x(),ne(St(e.loadingIcon)))]),_:1},8,["class"]))],64)):e.icon||_.$slots.icon?(x(),ne(p(Xe),{key:1},{default:$(()=>[e.icon?(x(),ne(St(e.icon),{key:0})):ce(_.$slots,"icon",{key:1})]),_:3})):te("v-if",!0),_.$slots.default?(x(),O("span",{key:2,class:U({[p(i).em("text","expand")]:p(g)})},[ce(_.$slots,"default")],2)):te("v-if",!0)]),_:3},16,["class","style","onClick"]))}}),_ue=bue;const wue={size:k4.size,type:k4.type,direction:{type:ae(String),values:["horizontal","vertical"],default:"horizontal"}};var Sue=q({name:"ElButtonGroup",__name:"button-group",props:wue,setup(e){const t=e;Ft(pL,nn({size:cn(t,"size"),type:cn(t,"type")}));const n=ke("button");return(a,r)=>(x(),O("div",{class:U([p(n).b("group"),p(n).bm("group",t.direction)])},[ce(a.$slots,"default")],2))}}),gL=Sue;const xa=wt(_ue,{ButtonGroup:gL}),yL=On(gL),Cue=e=>Ae(e)&&e.length===2&&e.every(t=>Is(t)),xue=$e({modelValue:{type:Date},range:{type:ae(Array),validator:Cue},controllerType:{type:String,values:["button","select"],default:"button"},formatter:{type:ae(Function)}}),kue={[bt]:e=>Is(e),[ua]:e=>Is(e)},F5=["hours","minutes","seconds"],nl="EP_PICKER_BASE",tw="ElPopperOptions",bL=Symbol("commonPickerContextKey"),H5="HH:mm:ss",ad="YYYY-MM-DD",Tue={date:ad,dates:ad,week:"gggg[w]ww",year:"YYYY",years:"YYYY",month:"YYYY-MM",months:"YYYY-MM",datetime:`${ad} ${H5}`,monthrange:"YYYY-MM",yearrange:"YYYY",daterange:ad,datetimerange:`${ad} ${H5}`};var Wl=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Kl(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var _L={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){var n=1e3,a=6e4,r=36e5,o="millisecond",i="second",l="minute",s="hour",u="day",c="week",d="month",v="quarter",f="year",h="date",m="Invalid Date",g=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,b={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(G){var K=["th","st","nd","rd"],H=G%100;return"["+G+(K[(H-20)%10]||K[H]||K[0])+"]"}},_=function(G,K,H){var z=String(G);return!z||z.length>=K?G:""+Array(K+1-z.length).join(H)+G},w={s:_,z:function(G){var K=-G.utcOffset(),H=Math.abs(K),z=Math.floor(H/60),B=H%60;return(K<=0?"+":"-")+_(z,2,"0")+":"+_(B,2,"0")},m:function G(K,H){if(K.date()1)return G(N[0])}else{var V=K.name;k[V]=K,B=V}return!z&&B&&(S=B),B||!z&&S},A=function(G,K){if(M(G))return G.clone();var H=typeof K=="object"?K:{};return H.date=G,H.args=arguments,new P(H)},I=w;I.l=E,I.i=M,I.w=function(G,K){return A(G,{locale:K.$L,utc:K.$u,x:K.$x,$offset:K.$offset})};var P=function(){function G(H){this.$L=E(H.locale,null,!0),this.parse(H),this.$x=this.$x||H.x||{},this[T]=!0}var K=G.prototype;return K.parse=function(H){this.$d=function(z){var B=z.date,W=z.utc;if(B===null)return new Date(NaN);if(I.u(B))return new Date;if(B instanceof Date)return new Date(B);if(typeof B=="string"&&!/Z$/i.test(B)){var N=B.match(g);if(N){var V=N[2]-1||0,Y=(N[7]||"0").substring(0,3);return W?new Date(Date.UTC(N[1],V,N[3]||1,N[4]||0,N[5]||0,N[6]||0,Y)):new Date(N[1],V,N[3]||1,N[4]||0,N[5]||0,N[6]||0,Y)}}return new Date(B)}(H),this.init()},K.init=function(){var H=this.$d;this.$y=H.getFullYear(),this.$M=H.getMonth(),this.$D=H.getDate(),this.$W=H.getDay(),this.$H=H.getHours(),this.$m=H.getMinutes(),this.$s=H.getSeconds(),this.$ms=H.getMilliseconds()},K.$utils=function(){return I},K.isValid=function(){return this.$d.toString()!==m},K.isSame=function(H,z){var B=A(H);return this.startOf(z)<=B&&B<=this.endOf(z)},K.isAfter=function(H,z){return A(H)[e>0?e-1:void 0,e,eArray.from(Array.from({length:e}).keys()),wL=e=>e.replace(/\W?m{1,2}|\W?ZZ/g,"").replace(/\W?h{1,2}|\W?s{1,3}|\W?a/gi,"").trim(),SL=e=>e.replace(/\W?D{1,2}|\W?Do|\W?d{1,4}|\W?M{1,4}|\W?Y{2,4}/g,"").trim(),W5=function(e,t){const n=Is(e),a=Is(t);return n&&a?e.getTime()===t.getTime():!n&&!a?e===t:!1},CL=function(e,t){const n=Ae(e),a=Ae(t);return n&&a?e.length!==t.length?!1:e.every((r,o)=>W5(r,t[o])):!n&&!a?W5(e,t):!1},K5=function(e,t,n){const a=_r(t)||t==="x"?gt(e).locale(n):gt(e,t).locale(n);return a.isValid()?a:void 0},U5=function(e,t,n){return _r(t)?e:t==="x"?+e:gt(e).locale(n).format(t)},ey=(e,t)=>{const n=[],a=t==null?void 0:t();for(let r=0;rAe(e)?e.map(t=>t.toDate()):e.toDate(),nw=$e({disabledHours:{type:ae(Function)},disabledMinutes:{type:ae(Function)},disabledSeconds:{type:ae(Function)}}),xL=$e({visible:Boolean,actualVisible:{type:Boolean,default:void 0},format:{type:String,default:""}}),aw=$e({automaticDropdown:{type:Boolean,default:!0},id:{type:ae([Array,String])},name:{type:ae([Array,String])},popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,format:String,valueFormat:String,dateFormat:String,timeFormat:String,type:{type:String,default:""},clearable:{type:Boolean,default:!0},clearIcon:{type:ae([String,Object]),default:ci},editable:{type:Boolean,default:!0},saveOnBlur:{type:Boolean,default:!0},prefixIcon:{type:ae([String,Object]),default:""},size:pa,readonly:Boolean,disabled:{type:Boolean,default:void 0},placeholder:{type:String,default:""},popperOptions:{type:ae(Object),default:()=>({})},modelValue:{type:ae([Date,Array,String,Number]),default:""},rangeSeparator:{type:String,default:"-"},startPlaceholder:String,endPlaceholder:String,defaultValue:{type:ae([Date,Array])},defaultTime:{type:ae([Date,Array])},isRange:Boolean,...nw,disabledDate:{type:Function},cellClassName:{type:Function},shortcuts:{type:Array,default:()=>[]},arrowControl:Boolean,tabindex:{type:ae([String,Number]),default:0},validateEvent:{type:Boolean,default:!0},unlinkPanels:Boolean,singlePanel:Boolean,placement:{type:ae(String),values:tl,default:"bottom"},fallbackPlacements:{type:ae(Array),default:["bottom","top","right","left"]},...Mc,...dr(["ariaLabel"]),showNow:{type:Boolean,default:!0},showConfirm:{type:Boolean,default:!0},showFooter:{type:Boolean,default:!0},showWeekNumber:Boolean}),Eue=$e({id:{type:ae(Array)},name:{type:ae(Array)},modelValue:{type:ae([Array,String])},startPlaceholder:String,endPlaceholder:String,disabled:Boolean}),kL=(e,t)=>{const{lang:n}=Vt(),a=F(!1),r=F(!1),o=F(null),i=D(()=>{const{modelValue:h}=e;return!h||Ae(h)&&!h.filter(Boolean).length}),l=h=>{if(!CL(e.modelValue,h)){let m;Ae(h)?m=h.map(g=>U5(g,e.valueFormat,n.value)):h&&(m=U5(h,e.valueFormat,n.value)),t(bt,h&&m,n.value)}},s=D(()=>{let h;if(i.value?u.value.getDefaultValue&&(h=u.value.getDefaultValue()):Ae(e.modelValue)?h=e.modelValue.map(m=>K5(m,e.valueFormat,n.value)):h=K5(e.modelValue??"",e.valueFormat,n.value),u.value.getRangeAvailableTime){const m=u.value.getRangeAvailableTime(h);Rn(m,h)||(h=m,i.value||l(Q0(h)))}return Ae(h)&&h.some(m=>!m)&&(h=[]),h}),u=F({});return{parsedValue:s,pickerActualVisible:r,pickerOptions:u,pickerVisible:a,userInput:o,valueIsEmpty:i,emitInput:l,onCalendarChange:h=>{t("calendar-change",h)},onPanelChange:(h,m,g)=>{t("panel-change",h,m,g)},onPick:(h="",m=!1)=>{a.value=m;let g;Ae(h)?g=h.map(y=>y.toDate()):g=h&&h.toDate(),o.value=null,l(g)},onSetPickerOption:h=>{u.value[h[0]]=h[1],u.value.panelReady=!0}}},Aue=["id","name","placeholder","value","disabled"],Iue=["id","name","placeholder","value","disabled"];var Lue=q({name:"PickerRangeTrigger",inheritAttrs:!1,__name:"picker-range-trigger",props:Eue,emits:["mouseenter","mouseleave","click","touchstart","focus","blur","startInput","endInput","startChange","endChange"],setup(e,{expose:t,emit:n}){const a=e,r=n,{formItem:o}=Ea(),{inputId:i}=Yr(nn({id:D(()=>{var M;return(M=a.id)==null?void 0:M[0]})}),{formItemContext:o}),l=A1(),s=ke("date"),u=ke("range"),c=F(),d=F(),{wrapperRef:v,isFocused:f}=Fl(c,{disabled:D(()=>a.disabled)}),h=M=>{r("click",M)},m=M=>{r("mouseenter",M)},g=M=>{r("mouseleave",M)},y=M=>{r("touchstart",M)},b=M=>{r("startInput",M)},_=M=>{r("endInput",M)},w=M=>{r("startChange",M)},S=M=>{r("endChange",M)};return t({focus:()=>{var M;(M=c.value)==null||M.focus()},blur:()=>{var M,E;(M=c.value)==null||M.blur(),(E=d.value)==null||E.blur()}}),(M,E)=>(x(),O("div",{ref_key:"wrapperRef",ref:v,class:U([p(s).is("active",p(f)),M.$attrs.class]),style:et(M.$attrs.style),onClick:h,onMouseenter:m,onMouseleave:g,onTouchstartPassive:y},[ce(M.$slots,"prefix"),C("input",Tt(p(l),{id:p(i),ref_key:"inputRef",ref:c,name:M.name&&M.name[0],placeholder:M.startPlaceholder,value:M.modelValue&&M.modelValue[0],class:p(u).b("input"),disabled:M.disabled,onInput:b,onChange:w}),null,16,Aue),ce(M.$slots,"range-separator"),C("input",Tt(p(l),{id:M.id&&M.id[1],ref_key:"endInputRef",ref:d,name:M.name&&M.name[1],placeholder:M.endPlaceholder,value:M.modelValue&&M.modelValue[1],class:p(u).b("input"),disabled:M.disabled,onInput:_,onChange:S}),null,16,Iue),ce(M.$slots,"suffix")],38))}}),Pue=Lue,Due=q({name:"Picker",__name:"picker",props:aw,emits:[bt,Ot,"focus","blur","clear","calendar-change","panel-change","visible-change","keydown"],setup(e,{expose:t,emit:n}){const a=e,r=n,o=Nl(),i=ke("date"),l=ke("input"),s=ke("range"),{formItem:u}=Ea(),c=Ze(tw,{}),d=op(a,null),v=F(),f=F(),h=F(null);let m=!1;const g=$n(),y=kL(a,r),{parsedValue:b,pickerActualVisible:_,userInput:w,pickerVisible:S,pickerOptions:k,valueIsEmpty:T,emitInput:M,onPick:E,onSetPickerOption:A,onCalendarChange:I,onPanelChange:P}=y,{isFocused:R,handleFocus:G,handleBlur:K}=Fl(f,{disabled:g,beforeFocus(){return a.readonly},afterFocus(){a.automaticDropdown&&(S.value=!0)},beforeBlur(Me){var Ue;return!m&&((Ue=v.value)==null?void 0:Ue.isFocusInsideContent(Me))},afterBlur(){var Me,Ue;J.value&&!a.saveOnBlur?T.value||(Ue=(Me=k.value).handleCancel)==null||Ue.call(Me):ze(),S.value=!1,m=!1,a.validateEvent&&(u==null||u.validate("blur").catch(kt))}}),H=F(!1),z=D(()=>[i.b("editor"),i.bm("editor",a.type),l.e("wrapper"),i.is("disabled",g.value),i.is("active",S.value),s.b("editor"),Le?s.bm("editor",Le.value):"",o.class]),B=D(()=>[l.e("icon"),s.e("close-icon"),fe.value?"":s.em("close-icon","hidden")]);ye(S,Me=>{Me?He(()=>{Me&&(h.value=a.modelValue)}):(w.value=null,He(()=>{W(a.modelValue)}))});const W=(Me,Ue)=>{(Ue||!CL(Me,h.value))&&(r(Ot,Me),Ue&&(h.value=Me),a.validateEvent&&(u==null||u.validate("change").catch(kt)))},N=Me=>{r("keydown",Me)},V=D(()=>f.value?Array.from(f.value.$el.querySelectorAll("input")):[]),Y=(Me,Ue,Gt)=>{const je=V.value;je.length&&(!Gt||Gt==="min"?(je[0].setSelectionRange(Me,Ue),je[0].focus()):Gt==="max"&&(je[1].setSelectionRange(Me,Ue),je[1].focus()))},X=()=>{_.value=!0},ee=()=>{r("visible-change",!0)},re=()=>{_.value=!1,S.value=!1,r("visible-change",!1)},ie=()=>{S.value=!0},le=()=>{S.value=!1},ve=D(()=>{const Me=rt(b.value);return Ae(w.value)?[w.value[0]??(Me&&Me[0])??"",w.value[1]??(Me&&Me[1])??""]:w.value!==null?w.value:J.value&&T.value&&!a.saveOnBlur||!J.value&&T.value||!S.value&&T.value?"":Me?Z.value||j.value||oe.value?Me.join(", "):Me:""}),he=D(()=>a.type.includes("time")),J=D(()=>a.type.startsWith("time")),Z=D(()=>a.type==="dates"),j=D(()=>a.type==="months"),oe=D(()=>a.type==="years"),se=D(()=>a.prefixIcon||(he.value?H6:xI)),fe=D(()=>a.clearable&&!g.value&&!a.readonly&&!T.value&&(H.value||R.value)),ge=Me=>{a.readonly||g.value||(fe.value&&(Me==null||Me.stopPropagation(),k.value.handleClear?k.value.handleClear():M(d.valueOnClear.value),W(d.valueOnClear.value,!0),re()),r("clear"))},Se=async Me=>{var Ue;a.readonly||g.value||(((Ue=Me.target)==null?void 0:Ue.tagName)!=="INPUT"||R.value||!a.automaticDropdown)&&(S.value=!0)},De=()=>{a.readonly||g.value||!T.value&&a.clearable&&(H.value=!0)},Te=()=>{H.value=!1},Re=Me=>{var Ue;a.readonly||g.value||(((Ue=Me.touches[0].target)==null?void 0:Ue.tagName)!=="INPUT"||R.value||!a.automaticDropdown)&&(S.value=!0)},Ce=D(()=>a.type.includes("range")),Le=ca(),me=D(()=>{var Me,Ue;return(Ue=(Me=p(v))==null?void 0:Me.popperRef)==null?void 0:Ue.contentRef}),Be=b6(f,Me=>{const Ue=p(me),Gt=da(f);Ue&&(Me.target===Ue||Me.composedPath().includes(Ue))||Me.target===Gt||Gt&&Me.composedPath().includes(Gt)||(S.value=!1)});rn(()=>{Be==null||Be()});const ze=()=>{if(J.value&&!a.saveOnBlur)return;const Me=Ae(w.value)&&w.value.every(Ue=>Ue==="");if(w.value&&!Me){const Ue=xe(ve.value);Ue&&(nt(Ue)&&M(Q0(Ue)),w.value=null)}(w.value===""||Me)&&(M(d.valueOnClear.value),W(d.valueOnClear.value,!0),w.value=null)},xe=Me=>Me?k.value.parseUserInput(Me):null,rt=Me=>Me?Ae(Me)?Me.map(Ue=>Ue.format(a.format)):Me.format(a.format):null,nt=Me=>k.value.isValidValue(Me),Pt=async Me=>{if(a.readonly||g.value)return;const Ue=mn(Me);if(N(Me),Ue===Pe.esc){S.value===!0&&(S.value=!1,Me.preventDefault(),Me.stopPropagation());return}if(Ue===Pe.down&&(k.value.handleFocusPicker&&(Me.preventDefault(),Me.stopPropagation()),S.value===!1&&(S.value=!0,await He()),k.value.handleFocusPicker)){k.value.handleFocusPicker();return}if(Ue===Pe.tab){m=!0;return}if(Ue===Pe.enter||Ue===Pe.numpadEnter){S.value?w.value===null||w.value===""||nt(xe(ve.value))?(ze(),S.value=!1):nt(xe(ve.value))||un("Picker",`Invalid user input: ${ve.value}. Please check the format of the input.`):S.value=!0,Me.preventDefault(),Me.stopPropagation();return}if(w.value){Me.stopPropagation();return}k.value.handleKeydownInput&&k.value.handleKeydownInput(Me)},Wt=Me=>{w.value=Me,S.value||(S.value=!0)},Jt=Me=>{const Ue=Me.target;w.value?w.value=[Ue.value,w.value[1]]:w.value=[Ue.value,null]},Rt=Me=>{const Ue=Me.target;w.value?w.value=[w.value[0],Ue.value]:w.value=[null,Ue.value]},Qt=()=>{var je;const Me=w.value,Ue=xe(Me&&Me[0]),Gt=p(b);if(Ue&&Ue.isValid()){w.value=[rt(Ue),((je=ve.value)==null?void 0:je[1])||null];const ft=[Ue,Gt&&(Gt[1]||null)];nt(ft)&&(M(Q0(ft)),w.value=null)}},_e=()=>{var je;const Me=p(w),Ue=xe(Me&&Me[1]),Gt=p(b);if(Ue&&Ue.isValid()){w.value=[((je=p(ve))==null?void 0:je[0])||null,rt(Ue)];const ft=[Gt&&Gt[0],Ue];nt(ft)&&(M(Q0(ft)),w.value=null)}},Ne=()=>{var Me;(Me=f.value)==null||Me.focus()},tt=()=>{var Me;(Me=f.value)==null||Me.blur()};return Ft(nl,{props:a,emptyValues:d}),Ft(bL,y),t({focus:Ne,blur:tt,handleOpen:ie,handleClose:le,onPick:E}),(Me,Ue)=>(x(),ne(p(Ma),Tt({ref_key:"refPopper",ref:v,visible:p(S),effect:"light",pure:"",trigger:"click"},Me.$attrs,{role:"dialog",teleported:"",transition:`${p(i).namespace.value}-zoom-in-top`,"popper-class":[`${p(i).namespace.value}-picker__popper`,Me.popperClass],"popper-style":Me.popperStyle,"popper-options":p(c),"fallback-placements":Me.fallbackPlacements,"gpu-acceleration":!1,placement:Me.placement,"stop-popper-mouse-event":!1,"hide-after":0,persistent:"",onBeforeShow:X,onShow:ee,onHide:re}),{default:$(()=>[Ce.value?(x(),ne(Pue,{key:1,id:Me.id,ref_key:"inputRef",ref:f,"model-value":ve.value,name:Me.name,disabled:p(g),readonly:!Me.editable||Me.readonly,"start-placeholder":Me.startPlaceholder,"end-placeholder":Me.endPlaceholder,class:U(z.value),style:et(Me.$attrs.style),"aria-label":Me.ariaLabel,tabindex:Me.tabindex,autocomplete:"off",role:"combobox",onClick:Se,onFocus:p(G),onBlur:p(K),onStartInput:Jt,onStartChange:Qt,onEndInput:Rt,onEndChange:_e,onMousedown:Se,onMouseenter:De,onMouseleave:Te,onTouchstartPassive:Re,onKeydown:Pt},{prefix:$(()=>[se.value?(x(),ne(p(Xe),{key:0,class:U([p(l).e("icon"),p(s).e("icon")])},{default:$(()=>[(x(),ne(St(se.value)))]),_:1},8,["class"])):te("v-if",!0)]),"range-separator":$(()=>[ce(Me.$slots,"range-separator",{},()=>[C("span",{class:U(p(s).b("separator"))},Q(Me.rangeSeparator),3)])]),suffix:$(()=>[Me.clearIcon?(x(),ne(p(Xe),{key:0,class:U(B.value),onMousedown:at(p(kt),["prevent"]),onClick:ge},{default:$(()=>[(x(),ne(St(Me.clearIcon)))]),_:1},8,["class","onMousedown"])):te("v-if",!0)]),_:3},8,["id","model-value","name","disabled","readonly","start-placeholder","end-placeholder","class","style","aria-label","tabindex","onFocus","onBlur"])):(x(),ne(p(Ba),{key:0,id:Me.id,ref_key:"inputRef",ref:f,"container-role":"combobox","model-value":ve.value,name:Me.name,size:p(Le),disabled:p(g),placeholder:Me.placeholder,class:U([p(i).b("editor"),p(i).bm("editor",Me.type),p(i).is("focus",p(S)),Me.$attrs.class]),style:et(Me.$attrs.style),readonly:!Me.editable||Me.readonly||Z.value||j.value||oe.value||Me.type==="week","aria-label":Me.ariaLabel,tabindex:Me.tabindex,"validate-event":!1,onInput:Wt,onFocus:p(G),onBlur:p(K),onKeydown:Pt,onChange:ze,onMousedown:Se,onMouseenter:De,onMouseleave:Te,onTouchstartPassive:Re,onClick:Ue[0]||(Ue[0]=at(()=>{},["stop"]))},{prefix:$(()=>[se.value?(x(),ne(p(Xe),{key:0,class:U(p(l).e("icon")),onMousedown:at(Se,["prevent"]),onTouchstartPassive:Re},{default:$(()=>[(x(),ne(St(se.value)))]),_:1},8,["class"])):te("v-if",!0)]),suffix:$(()=>[fe.value&&Me.clearIcon?(x(),ne(p(Xe),{key:0,class:U(`${p(l).e("icon")} clear-icon`),onMousedown:at(p(kt),["prevent"]),onClick:ge},{default:$(()=>[(x(),ne(St(Me.clearIcon)))]),_:1},8,["class","onMousedown"])):te("v-if",!0)]),_:1},8,["id","model-value","name","size","disabled","placeholder","class","style","readonly","aria-label","tabindex","onFocus","onBlur"]))]),content:$(()=>[ce(Me.$slots,"default",{visible:p(S),actualVisible:p(_),parsedValue:p(b),format:Me.format,dateFormat:Me.dateFormat,timeFormat:Me.timeFormat,unlinkPanels:Me.unlinkPanels,type:Me.type,defaultValue:Me.defaultValue,showNow:Me.showNow,showConfirm:Me.showConfirm,showFooter:Me.showFooter,showWeekNumber:Me.showWeekNumber,singlePanel:Me.singlePanel,onPick:Ue[1]||(Ue[1]=(...Gt)=>p(E)&&p(E)(...Gt)),onSelectRange:Y,onSetPickerOption:Ue[2]||(Ue[2]=(...Gt)=>p(A)&&p(A)(...Gt)),onCalendarChange:Ue[3]||(Ue[3]=(...Gt)=>p(I)&&p(I)(...Gt)),onClear:ge,onPanelChange:Ue[4]||(Ue[4]=(...Gt)=>p(P)&&p(P)(...Gt)),onMousedown:Ue[5]||(Ue[5]=at(()=>{},["stop"]))})]),_:3},16,["visible","transition","popper-class","popper-style","popper-options","fallback-placements","placement"]))}}),TL=Due;const is=new Map;if(qt){let e;document.addEventListener("mousedown",t=>e=t),document.addEventListener("mouseup",t=>{if(e){for(const n of is.values())for(const{documentHandler:a}of n)a(t,e);e=void 0}})}function G5(e,t){let n=[];return Ae(t.arg)?n=t.arg:Vr(t.arg)&&n.push(t.arg),function(a,r){const o=t.instance.popperRef,i=a.target,l=r==null?void 0:r.target,s=!t||!t.instance,u=!i||!l,c=e.contains(i)||e.contains(l),d=e===i,v=n.length&&n.some(h=>h==null?void 0:h.contains(i))||n.length&&n.includes(l),f=o&&(o.contains(i)||o.contains(l));s||u||c||d||v||f||t.value(a,r)}}const Ns={beforeMount(e,t){is.has(e)||is.set(e,[]),is.get(e).push({documentHandler:G5(e,t),bindingFn:t.value})},updated(e,t){is.has(e)||is.set(e,[]);const n=is.get(e),a=n.findIndex(o=>o.bindingFn===t.oldValue),r={documentHandler:G5(e,t),bindingFn:t.value};a>=0?n.splice(a,1,r):n.push(r)},unmounted(e){is.delete(e)}},Kp="_RepeatClick",Zm={beforeMount(e,t){const n=t.value,{interval:a=100,delay:r=600}=Qe(n)?{}:n;let o,i;const l=()=>Qe(n)?n():n.handler(),s=()=>{i&&(clearTimeout(i),i=void 0),o&&(clearInterval(o),o=void 0)},u=c=>{c.button===0&&(s(),l(),document.addEventListener("mouseup",s,{once:!0,capture:!0}),i=setTimeout(()=>{o=setInterval(()=>{l()},a)},r))};e[Kp]={start:u,clear:s},e.addEventListener("mousedown",u)},unmounted(e){if(!e[Kp])return;const{start:t,clear:n}=e[Kp];t&&e.removeEventListener("mousedown",t),n&&(n(),document.removeEventListener("mouseup",n,!0)),e[Kp]=null}},M4="_trap-focus-children",Nu=[],Y5=e=>{if(Nu.length===0)return;const t=mn(e),n=Nu[Nu.length-1][M4];if(n.length>0&&t===Pe.tab){if(n.length===1){e.preventDefault(),document.activeElement!==n[0]&&n[0].focus();return}const a=e.shiftKey,r=e.target===n[0],o=e.target===n[n.length-1];r&&a&&(e.preventDefault(),n[n.length-1].focus()),o&&!a&&(e.preventDefault(),n[0].focus())}},Oue={beforeMount(e){e[M4]=KC(e),Nu.push(e),Nu.length<=1&&document.addEventListener("keydown",Y5)},updated(e){He(()=>{e[M4]=KC(e)})},unmounted(){Nu.shift(),Nu.length===0&&document.removeEventListener("keydown",Y5)}};var j5=!1,Lu,E4,A4,em,tm,ML,nm,I4,L4,P4,EL,D4,O4,AL,IL;function Lr(){if(!j5){j5=!0;var e=navigator.userAgent,t=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(e),n=/(Mac OS X)|(Windows)|(Linux)/.exec(e);if(D4=/\b(iPhone|iP[ao]d)/.exec(e),O4=/\b(iP[ao]d)/.exec(e),P4=/Android/i.exec(e),AL=/FBAN\/\w+;/i.exec(e),IL=/Mobile/i.exec(e),EL=!!/Win64/.exec(e),t){Lu=t[1]?parseFloat(t[1]):t[5]?parseFloat(t[5]):NaN,Lu&&document&&document.documentMode&&(Lu=document.documentMode);var a=/(?:Trident\/(\d+.\d+))/.exec(e);ML=a?parseFloat(a[1])+4:Lu,E4=t[2]?parseFloat(t[2]):NaN,A4=t[3]?parseFloat(t[3]):NaN,em=t[4]?parseFloat(t[4]):NaN,em?(t=/(?:Chrome\/(\d+\.\d+))/.exec(e),tm=t&&t[1]?parseFloat(t[1]):NaN):tm=NaN}else Lu=E4=A4=tm=em=NaN;if(n){if(n[1]){var r=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(e);nm=r?parseFloat(r[1].replace("_",".")):!0}else nm=!1;I4=!!n[2],L4=!!n[3]}else nm=I4=L4=!1}}var $4={ie:function(){return Lr()||Lu},ieCompatibilityMode:function(){return Lr()||ML>Lu},ie64:function(){return $4.ie()&&EL},firefox:function(){return Lr()||E4},opera:function(){return Lr()||A4},webkit:function(){return Lr()||em},safari:function(){return $4.webkit()},chrome:function(){return Lr()||tm},windows:function(){return Lr()||I4},osx:function(){return Lr()||nm},linux:function(){return Lr()||L4},iphone:function(){return Lr()||D4},mobile:function(){return Lr()||D4||O4||P4||IL},nativeApp:function(){return Lr()||AL},android:function(){return Lr()||P4},ipad:function(){return Lr()||O4}},$ue=$4,Rue=!!(typeof window<"u"&&window.document&&window.document.createElement),Nue={canUseDOM:Rue},LL=Nue,PL;LL.canUseDOM&&(PL=document.implementation&&document.implementation.hasFeature&&document.implementation.hasFeature("","")!==!0);function Bue(e,t){if(!LL.canUseDOM||t&&!("addEventListener"in document))return!1;var n="on"+e,a=n in document;if(!a){var r=document.createElement("div");r.setAttribute(n,"return;"),a=typeof r[n]=="function"}return!a&&PL&&e==="wheel"&&(a=document.implementation.hasFeature("Events.wheel","3.0")),a}var Vue=Bue,q5=10,X5=40,Z5=800;function DL(e){var t=0,n=0,a=0,r=0;return"detail"in e&&(n=e.detail),"wheelDelta"in e&&(n=-e.wheelDelta/120),"wheelDeltaY"in e&&(n=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=n,n=0),a=t*q5,r=n*q5,"deltaY"in e&&(r=e.deltaY),"deltaX"in e&&(a=e.deltaX),(a||r)&&e.deltaMode&&(e.deltaMode==1?(a*=X5,r*=X5):(a*=Z5,r*=Z5)),a&&!t&&(t=a<1?-1:1),r&&!n&&(n=r<1?-1:1),{spinX:t,spinY:n,pixelX:a,pixelY:r}}DL.getEventType=function(){return $ue.firefox()?"DOMMouseScroll":Vue("wheel")?"wheel":"mousewheel"};var zue=DL;/** +* Checks if an event is supported in the current execution environment. +* +* NOTE: This will not work correctly for non-generic events such as `change`, +* `reset`, `load`, `error`, and `select`. +* +* Borrows from Modernizr. +* +* @param {string} eventNameSuffix Event name, e.g. "click". +* @param {?boolean} capture Check if the capture phase is supported. +* @return {boolean} True if the event is supported. +* @internal +* @license Modernizr 3.0.0pre (Custom Build) | MIT +*/const R4="_Mousewheel",J5=function(e,t){if(e&&e.addEventListener){OL(e);const n=function(a){const r=zue(a);t&&Reflect.apply(t,this,[a,r])};e[R4]={wheelHandler:n},e.addEventListener("wheel",n,{passive:!0})}},OL=e=>{var t;(t=e._Mousewheel)!=null&&t.wheelHandler&&(e.removeEventListener("wheel",e[R4].wheelHandler),e[R4]=null)},Q5={beforeMount(e,t){J5(e,t.value)},unmounted(e){OL(e)},updated(e,t){t.value!==t.oldValue&&J5(e,t.value)}},Fue=$e({...xL,datetimeRole:String,parsedValue:{type:ae(Object)}}),$L=({getAvailableHours:e,getAvailableMinutes:t,getAvailableSeconds:n})=>{const a=(i,l,s,u)=>{const c={hour:e,minute:t,second:n};let d=i;return["hour","minute","second"].forEach(v=>{if(c[v]){let f;const h=c[v];switch(v){case"minute":f=h(d.hour(),l,u);break;case"second":f=h(d.hour(),d.minute(),l,u);break;default:f=h(l,u);break}if(f!=null&&f.length&&!f.includes(d[v]())){const m=s?0:f.length-1;d=d[v](f[m])}}}),d},r={};return{timePickerOptions:r,getAvailableTime:a,onSetOption:([i,l])=>{r[i]=l}}},ty=e=>{const t=(a,r)=>a||r,n=a=>a!==!0;return e.map(t).filter(n)},RL=(e,t,n)=>({getHoursList:(i,l)=>ey(24,e&&(()=>e==null?void 0:e(i,l))),getMinutesList:(i,l,s)=>ey(60,t&&(()=>t==null?void 0:t(i,l,s))),getSecondsList:(i,l,s,u)=>ey(60,n&&(()=>n==null?void 0:n(i,l,s,u)))}),NL=(e,t,n)=>{const{getHoursList:a,getMinutesList:r,getSecondsList:o}=RL(e,t,n);return{getAvailableHours:(u,c)=>ty(a(u,c)),getAvailableMinutes:(u,c,d)=>ty(r(u,c,d)),getAvailableSeconds:(u,c,d,v)=>ty(o(u,c,d,v))}},BL=(e,t)=>{const n=F(e.parsedValue);return ye(()=>e.visible,a=>{const r=ia(t.modelValue),o=ia(t.valueOnClear);if(a&&r===o){n.value=o;return}a||(n.value=e.parsedValue)}),n},Hue=$e({role:{type:String,required:!0},spinnerDate:{type:ae(Object),required:!0},showSeconds:{type:Boolean,default:!0},arrowControl:Boolean,amPmMode:{type:ae(String),default:""},...nw}),Wue=["onClick"],Kue=["onMouseenter"];var Uue=q({__name:"basic-time-spinner",props:Hue,emits:[Ot,"select-range","set-option"],setup(e,{emit:t}){const n=e,{isRange:a,format:r,saveOnBlur:o}=Ze(nl).props,i=t,l=ke("time"),{getHoursList:s,getMinutesList:u,getSecondsList:c}=RL(n.disabledHours,n.disabledMinutes,n.disabledSeconds);let d=!1;const v={hours:!1,minutes:!1,seconds:!1},f=F(),h={hours:F(),minutes:F(),seconds:F()},m=D(()=>n.showSeconds?F5:F5.slice(0,2)),g=D(()=>{const{spinnerDate:V}=n;return{hours:V.hour(),minutes:V.minute(),seconds:V.second()}}),y=D(()=>{const{hours:V,minutes:Y}=p(g),{role:X,spinnerDate:ee}=n,re=a?void 0:ee;return{hours:s(X,re),minutes:u(V,X,re),seconds:c(V,Y,X,re)}}),b=D(()=>{const{hours:V,minutes:Y,seconds:X}=p(g);return{hours:Q2(V,23),minutes:Q2(Y,59),seconds:Q2(X,59)}}),_=Yi(V=>{d=!1,k(V)},200),w=V=>{if(!n.amPmMode)return"";const Y=n.amPmMode==="A";let X=V<12?" am":" pm";return Y&&(X=X.toUpperCase()),X},S=V=>{let Y=[0,0];const X=r||"HH:mm:ss",ee=X.indexOf("HH"),re=X.indexOf("mm"),ie=X.indexOf("ss");switch(V){case"hours":ee!==-1&&(Y=[ee,ee+2]);break;case"minutes":re!==-1&&(Y=[re,re+2]);break;case"seconds":ie!==-1&&(Y=[ie,ie+2]);break}const[le,ve]=Y;i("select-range",le,ve),f.value=V},k=V=>{E(V,p(g)[V])},T=()=>{k("hours"),k("minutes"),k("seconds")},M=V=>V.querySelector(`.${l.namespace.value}-scrollbar__wrap`),E=(V,Y)=>{if(n.arrowControl)return;const X=p(h[V]);X&&X.$el&&(o||(v[V]=!0,Wr(()=>{v[V]=!1})),M(X.$el).scrollTop=Math.max(0,Y*A(V)))},A=V=>{var X;const Y=(X=p(h[V]))==null?void 0:X.$el.querySelector("li");return Y&&Number.parseFloat(ml(Y,"height"))||0},I=()=>{R(1)},P=()=>{R(-1)},R=V=>{f.value||S("hours");const Y=f.value,X=p(g)[Y],ee=G(Y,X,V,f.value==="hours"?24:60);K(Y,ee),E(Y,ee),He(()=>S(Y))},G=(V,Y,X,ee)=>{let re=(Y+X+ee)%ee;const ie=p(y)[V];for(;ie[re]&&re!==Y;)re=(re+X+ee)%ee;return re},K=(V,Y)=>{if(p(y)[V][Y])return;const{hours:X,minutes:ee,seconds:re}=p(g);let ie;switch(V){case"hours":ie=n.spinnerDate.hour(Y).minute(ee).second(re);break;case"minutes":ie=n.spinnerDate.hour(X).minute(Y).second(re);break;case"seconds":ie=n.spinnerDate.hour(X).minute(ee).second(Y);break}i(Ot,ie)},H=(V,{value:Y,disabled:X})=>{X||(K(V,Y),S(V),E(V,Y))},z=V=>{if(!o&&v[V])return;const Y=p(h[V]);Y&&(d=!0,_(V),K(V,Math.min(Math.round((M(Y.$el).scrollTop-(B(V)*.5-10)/A(V)+3)/A(V)),V==="hours"?23:59)))},B=V=>p(h[V]).$el.offsetHeight,W=()=>{const V=Y=>{const X=p(h[Y]);X&&X.$el&&(M(X.$el).onscroll=()=>{z(Y)})};V("hours"),V("minutes"),V("seconds")};mt(()=>{He(()=>{!n.arrowControl&&W(),T(),n.role==="start"&&S("hours")})});const N=(V,Y)=>{h[Y].value=V??void 0};return i("set-option",[`${n.role}_scrollDown`,R]),i("set-option",[`${n.role}_emitSelectRange`,S]),ye(()=>n.spinnerDate,()=>{d||T()}),(V,Y)=>(x(),O("div",{class:U([p(l).b("spinner"),{"has-seconds":V.showSeconds}])},[V.arrowControl?te("v-if",!0):(x(!0),O(Ve,{key:0},dt(m.value,X=>(x(),ne(p(ni),{key:X,ref_for:!0,ref:ee=>N(ee,X),class:U(p(l).be("spinner","wrapper")),"wrap-style":"max-height: inherit;","view-class":p(l).be("spinner","list"),noresize:"",tag:"ul",onMouseenter:ee=>S(X),onMousemove:ee=>k(X)},{default:$(()=>[(x(!0),O(Ve,null,dt(y.value[X],(ee,re)=>(x(),O("li",{key:re,class:U([p(l).be("spinner","item"),p(l).is("active",re===g.value[X]),p(l).is("disabled",ee)]),onClick:ie=>H(X,{value:re,disabled:ee})},[X==="hours"?(x(),O(Ve,{key:0},[ue(Q(("0"+(V.amPmMode?re%12||12:re)).slice(-2))+Q(w(re)),1)],64)):(x(),O(Ve,{key:1},[ue(Q(("0"+re).slice(-2)),1)],64))],10,Wue))),128))]),_:2},1032,["class","view-class","onMouseenter","onMousemove"]))),128)),V.arrowControl?(x(!0),O(Ve,{key:1},dt(m.value,X=>(x(),O("div",{key:X,class:U([p(l).be("spinner","wrapper"),p(l).is("arrow")]),onMouseenter:ee=>S(X)},[st((x(),ne(p(Xe),{class:U(["arrow-up",p(l).be("spinner","arrow")])},{default:$(()=>[L(p(ip))]),_:1},8,["class"])),[[p(Zm),P]]),st((x(),ne(p(Xe),{class:U(["arrow-down",p(l).be("spinner","arrow")])},{default:$(()=>[L(p(ui))]),_:1},8,["class"])),[[p(Zm),I]]),C("ul",{class:U(p(l).be("spinner","list"))},[(x(!0),O(Ve,null,dt(b.value[X],(ee,re)=>(x(),O("li",{key:re,class:U([p(l).be("spinner","item"),p(l).is("active",ee===g.value[X]),p(l).is("disabled",y.value[X][ee])])},[p(Je)(ee)?(x(),O(Ve,{key:0},[X==="hours"?(x(),O(Ve,{key:0},[ue(Q(("0"+(V.amPmMode?ee%12||12:ee)).slice(-2))+Q(w(ee)),1)],64)):(x(),O(Ve,{key:1},[ue(Q(("0"+ee).slice(-2)),1)],64))],64)):te("v-if",!0)],2))),128))],2)],42,Kue))),128)):te("v-if",!0)],2))}}),N4=Uue,Gue=q({__name:"panel-time-pick",props:Fue,emits:["pick","select-range","set-picker-option"],setup(e,{emit:t}){const n=e,a=t,r=Ze(nl),{arrowControl:o,disabledHours:i,disabledMinutes:l,disabledSeconds:s,defaultValue:u}=r.props,{getAvailableHours:c,getAvailableMinutes:d,getAvailableSeconds:v}=NL(i,l,s),f=ke("time"),{t:h,lang:m}=Vt(),g=F([0,2]),y=BL(n,{modelValue:D(()=>r.props.modelValue),valueOnClear:D(()=>r!=null&&r.emptyValues?r.emptyValues.valueOnClear.value:null)}),b=D(()=>zt(n.actualVisible)?`${f.namespace.value}-zoom-in-top`:""),_=D(()=>n.format.includes("ss")),w=D(()=>n.format.includes("A")?"A":n.format.includes("a")?"a":""),S=B=>{const W=gt(B).locale(m.value),N=K(W);return W.isSame(N)},k=()=>{const B=y.value;a("pick",B,!1),He(()=>{y.value=B})},T=(B=!1,W=!1)=>{W||a("pick",n.parsedValue,B)},M=B=>{n.visible&&a("pick",K(B).millisecond(0),!0)},E=(B,W)=>{a("select-range",B,W),g.value=[B,W]},A=B=>{const W=n.format,N=W.indexOf("HH"),V=W.indexOf("mm"),Y=W.indexOf("ss"),X=[],ee=[];N!==-1&&(X.push(N),ee.push("hours")),V!==-1&&(X.push(V),ee.push("minutes")),Y!==-1&&_.value&&(X.push(Y),ee.push("seconds"));const re=(X.indexOf(g.value[0])+B+X.length)%X.length;P.start_emitSelectRange(ee[re])},I=B=>{const W=mn(B),{left:N,right:V,up:Y,down:X}=Pe;if([N,V].includes(W)){A(W===N?-1:1),B.preventDefault();return}if([Y,X].includes(W)){const ee=W===Y?-1:1;P.start_scrollDown(ee),B.preventDefault();return}},{timePickerOptions:P,onSetOption:R,getAvailableTime:G}=$L({getAvailableHours:c,getAvailableMinutes:d,getAvailableSeconds:v}),K=B=>G(B,n.datetimeRole||"",!0),H=B=>B?gt(B,n.format).locale(m.value):null,z=()=>gt(u).locale(m.value);return a("set-picker-option",["isValidValue",S]),a("set-picker-option",["parseUserInput",H]),a("set-picker-option",["handleKeydownInput",I]),a("set-picker-option",["getRangeAvailableTime",K]),a("set-picker-option",["getDefaultValue",z]),a("set-picker-option",["handleCancel",k]),(B,W)=>(x(),ne(Pa,{name:b.value},{default:$(()=>[B.actualVisible||B.visible?(x(),O("div",{key:0,class:U(p(f).b("panel"))},[C("div",{class:U([p(f).be("panel","content"),{"has-seconds":_.value}])},[L(N4,{ref:"spinner",role:B.datetimeRole||"start","arrow-control":p(o),"show-seconds":_.value,"am-pm-mode":w.value,"spinner-date":B.parsedValue,"disabled-hours":p(i),"disabled-minutes":p(l),"disabled-seconds":p(s),onChange:M,onSetOption:p(R),onSelectRange:E},null,8,["role","arrow-control","show-seconds","am-pm-mode","spinner-date","disabled-hours","disabled-minutes","disabled-seconds","onSetOption"])],2),C("div",{class:U(p(f).be("panel","footer"))},[C("button",{type:"button",class:U([p(f).be("panel","btn"),"cancel"]),onClick:k},Q(p(h)("el.datepicker.cancel")),3),C("button",{type:"button",class:U([p(f).be("panel","btn"),"confirm"]),onClick:W[0]||(W[0]=N=>T())},Q(p(h)("el.datepicker.confirm")),3)],2)],2)):te("v-if",!0)]),_:1},8,["name"]))}}),Jm=Gue;const Yue=$e({...xL,parsedValue:{type:ae(Array)}}),jue=["disabled"];var que=q({__name:"panel-time-range",props:Yue,emits:["pick","select-range","set-picker-option"],setup(e,{emit:t}){const n=e,a=t,r=(Z,j)=>{const oe=[];for(let se=Z;se<=j;se++)oe.push(se);return oe},{t:o,lang:i}=Vt(),l=ke("time"),s=ke("picker"),u=Ze(nl),{arrowControl:c,disabledHours:d,disabledMinutes:v,disabledSeconds:f,defaultValue:h}=u.props,m=D(()=>[l.be("range-picker","body"),l.be("panel","content"),l.is("arrow",c),S.value?"has-seconds":""]),g=D(()=>[l.be("range-picker","body"),l.be("panel","content"),l.is("arrow",c),S.value?"has-seconds":""]),y=D(()=>n.parsedValue[0]),b=D(()=>n.parsedValue[1]),_=BL(n,{modelValue:D(()=>u.props.modelValue),valueOnClear:D(()=>u!=null&&u.emptyValues?u.emptyValues.valueOnClear.value:null)}),w=()=>{const Z=_.value;a("pick",Z,!1),He(()=>{_.value=Z})},S=D(()=>n.format.includes("ss")),k=D(()=>n.format.includes("A")?"A":n.format.includes("a")?"a":""),T=(Z=!1)=>{a("pick",[y.value,b.value],Z)},M=Z=>{I(Z.millisecond(0),b.value)},E=Z=>{I(y.value,Z.millisecond(0))},A=Z=>{const j=Z.map(se=>gt(se).locale(i.value)),oe=Y(j);return j[0].isSame(oe[0])&&j[1].isSame(oe[1])},I=(Z,j)=>{n.visible&&a("pick",[Z,j],!0)},P=D(()=>y.value>b.value),R=F([0,2]),G=(Z,j)=>{a("select-range",Z,j,"min"),R.value=[Z,j]},K=D(()=>S.value?11:8),H=(Z,j)=>{a("select-range",Z,j,"max");const oe=p(K);R.value=[Z+oe,j+oe]},z=Z=>{const j=S.value?[0,3,6,11,14,17]:[0,3,8,11],oe=["hours","minutes"].concat(S.value?["seconds"]:[]),se=(j.indexOf(R.value[0])+Z+j.length)%j.length,fe=j.length/2;se{const j=mn(Z),{left:oe,right:se,up:fe,down:ge}=Pe;if([oe,se].includes(j)){z(j===oe?-1:1),Z.preventDefault();return}if([fe,ge].includes(j)){const Se=j===fe?-1:1;ie[`${R.value[0]{const oe=d?d(Z):[],se=Z==="start",fe=(j||(se?b.value:y.value)).hour();return F2(oe,se?r(fe+1,23):r(0,fe-1))},N=(Z,j,oe)=>{const se=v?v(Z,j):[],fe=j==="start",ge=oe||(fe?b.value:y.value);if(Z!==ge.hour())return se;const Se=ge.minute();return F2(se,fe?r(Se+1,59):r(0,Se-1))},V=(Z,j,oe,se)=>{const fe=f?f(Z,j,oe):[],ge=oe==="start",Se=se||(ge?b.value:y.value),De=Se.hour(),Te=Se.minute();if(Z!==De||j!==Te)return fe;const Re=Se.second();return F2(fe,ge?r(Re+1,59):r(0,Re-1))},Y=([Z,j])=>[le(Z,"start",!0,j),le(j,"end",!1,Z)],{getAvailableHours:X,getAvailableMinutes:ee,getAvailableSeconds:re}=NL(W,N,V),{timePickerOptions:ie,getAvailableTime:le,onSetOption:ve}=$L({getAvailableHours:X,getAvailableMinutes:ee,getAvailableSeconds:re}),he=Z=>Z?Ae(Z)?Z.map(j=>gt(j,n.format).locale(i.value)):gt(Z,n.format).locale(i.value):null,J=()=>{if(Ae(h))return h.map(j=>gt(j).locale(i.value));const Z=gt(h).locale(i.value);return[Z,Z.add(60,"m")]};return a("set-picker-option",["parseUserInput",he]),a("set-picker-option",["isValidValue",A]),a("set-picker-option",["handleKeydownInput",B]),a("set-picker-option",["getDefaultValue",J]),a("set-picker-option",["getRangeAvailableTime",Y]),a("set-picker-option",["handleCancel",w]),(Z,j)=>Z.actualVisible?(x(),O("div",{key:0,class:U([p(l).b("range-picker"),p(s).b("panel")])},[C("div",{class:U(p(l).be("range-picker","content"))},[C("div",{class:U(p(l).be("range-picker","cell"))},[C("div",{class:U(p(l).be("range-picker","header"))},Q(p(o)("el.datepicker.startTime")),3),C("div",{class:U(m.value)},[L(N4,{ref:"minSpinner",role:"start","show-seconds":S.value,"am-pm-mode":k.value,"arrow-control":p(c),"spinner-date":y.value,"disabled-hours":W,"disabled-minutes":N,"disabled-seconds":V,onChange:M,onSetOption:p(ve),onSelectRange:G},null,8,["show-seconds","am-pm-mode","arrow-control","spinner-date","onSetOption"])],2)],2),C("div",{class:U(p(l).be("range-picker","cell"))},[C("div",{class:U(p(l).be("range-picker","header"))},Q(p(o)("el.datepicker.endTime")),3),C("div",{class:U(g.value)},[L(N4,{ref:"maxSpinner",role:"end","show-seconds":S.value,"am-pm-mode":k.value,"arrow-control":p(c),"spinner-date":b.value,"disabled-hours":W,"disabled-minutes":N,"disabled-seconds":V,onChange:E,onSetOption:p(ve),onSelectRange:H},null,8,["show-seconds","am-pm-mode","arrow-control","spinner-date","onSetOption"])],2)],2)],2),C("div",{class:U(p(l).be("panel","footer"))},[C("button",{type:"button",class:U([p(l).be("panel","btn"),"cancel"]),onClick:j[0]||(j[0]=oe=>w())},Q(p(o)("el.datepicker.cancel")),3),C("button",{type:"button",class:U([p(l).be("panel","btn"),"confirm"]),disabled:P.value,onClick:j[1]||(j[1]=oe=>T())},Q(p(o)("el.datepicker.confirm")),11,jue)],2)],2)):te("v-if",!0)}}),Xue=que,VL={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){var n={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},a=/(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|Q|YYYY|YY?|ww?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,r=/\d/,o=/\d\d/,i=/\d\d?/,l=/\d*[^-_:/,()\s\d]+/,s={},u=function(g){return(g=+g)+(g>68?1900:2e3)},c=function(g){return function(y){this[g]=+y}},d=[/[+-]\d\d:?(\d\d)?|Z/,function(g){(this.zone||(this.zone={})).offset=function(y){if(!y||y==="Z")return 0;var b=y.match(/([+-]|\d\d)/g),_=60*b[1]+(+b[2]||0);return _===0?0:b[0]==="+"?-_:_}(g)}],v=function(g){var y=s[g];return y&&(y.indexOf?y:y.s.concat(y.f))},f=function(g,y){var b,_=s.meridiem;if(_){for(var w=1;w<=24;w+=1)if(g.indexOf(_(w,0,y))>-1){b=w>12;break}}else b=g===(y?"pm":"PM");return b},h={A:[l,function(g){this.afternoon=f(g,!1)}],a:[l,function(g){this.afternoon=f(g,!0)}],Q:[r,function(g){this.month=3*(g-1)+1}],S:[r,function(g){this.milliseconds=100*+g}],SS:[o,function(g){this.milliseconds=10*+g}],SSS:[/\d{3}/,function(g){this.milliseconds=+g}],s:[i,c("seconds")],ss:[i,c("seconds")],m:[i,c("minutes")],mm:[i,c("minutes")],H:[i,c("hours")],h:[i,c("hours")],HH:[i,c("hours")],hh:[i,c("hours")],D:[i,c("day")],DD:[o,c("day")],Do:[l,function(g){var y=s.ordinal,b=g.match(/\d+/);if(this.day=b[0],y)for(var _=1;_<=31;_+=1)y(_).replace(/\[|\]/g,"")===g&&(this.day=_)}],w:[i,c("week")],ww:[o,c("week")],M:[i,c("month")],MM:[o,c("month")],MMM:[l,function(g){var y=v("months"),b=(v("monthsShort")||y.map(function(_){return _.slice(0,3)})).indexOf(g)+1;if(b<1)throw new Error;this.month=b%12||b}],MMMM:[l,function(g){var y=v("months").indexOf(g)+1;if(y<1)throw new Error;this.month=y%12||y}],Y:[/[+-]?\d+/,c("year")],YY:[o,function(g){this.year=u(g)}],YYYY:[/\d{4}/,c("year")],Z:d,ZZ:d};function m(g){var y,b;y=g,b=s&&s.formats;for(var _=(g=y.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,function(A,I,P){var R=P&&P.toUpperCase();return I||b[P]||n[P]||b[R].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(G,K,H){return K||H.slice(1)})})).match(a),w=_.length,S=0;S-1)return new Date((B==="X"?1e3:1)*z);var V=m(B)(z),Y=V.year,X=V.month,ee=V.day,re=V.hours,ie=V.minutes,le=V.seconds,ve=V.milliseconds,he=V.zone,J=V.week,Z=new Date,j=ee||(Y||X?1:Z.getDate()),oe=Y||Z.getFullYear(),se=0;Y&&!X||(se=X>0?X-1:Z.getMonth());var fe,ge=re||0,Se=ie||0,De=le||0,Te=ve||0;return he?new Date(Date.UTC(oe,se,j,ge,Se,De,Te+60*he.offset*1e3)):W?new Date(Date.UTC(oe,se,j,ge,Se,De,Te)):(fe=new Date(oe,se,j,ge,Se,De,Te),J&&(fe=N(fe).week(J).toDate()),fe)}catch{return new Date("")}}(k,E,T,b),this.init(),R&&R!==!0&&(this.$L=this.locale(R).$L),P&&k!=this.format(E)&&(this.$d=new Date("")),s={}}else if(E instanceof Array)for(var G=E.length,K=1;K<=G;K+=1){M[1]=E[K-1];var H=b.apply(this,M);if(H.isValid()){this.$d=H.$d,this.$L=H.$L,this.init();break}K===G&&(this.$d=new Date(""))}else w.call(this,S)}}})})(VL);var Zue=VL.exports;const rw=Kl(Zue);gt.extend(rw);var Jue=q({name:"ElTimePicker",install:null,props:{...aw,isRange:Boolean},emits:[bt],setup(e,t){const n=F(),[a,r]=e.isRange?["timerange",Xue]:["time",Jm],o=i=>t.emit(bt,i);return Ft(tw,e.popperOptions),t.expose({focus:()=>{var i;(i=n.value)==null||i.focus()},blur:()=>{var i;(i=n.value)==null||i.blur()},handleOpen:()=>{var i;(i=n.value)==null||i.handleOpen()},handleClose:()=>{var i;(i=n.value)==null||i.handleClose()}}),()=>L(TL,Tt(e,{ref:n,type:a,format:e.format??"HH:mm:ss","onUpdate:modelValue":o}),{default:i=>L(r,i,null)})}});const Que=wt(Jue),Ll=$e({type:{type:String,values:["primary","success","info","warning","danger"],default:"primary"},closable:Boolean,disableTransitions:Boolean,hit:Boolean,color:String,size:{type:String,values:ii},effect:{type:String,values:["dark","light","plain"],default:"light"},round:Boolean}),ece={close:e=>e instanceof MouseEvent,click:e=>e instanceof MouseEvent},tce=["aria-label"],nce=["aria-label"];var ace=q({name:"ElTag",__name:"tag",props:Ll,emits:ece,setup(e,{emit:t}){const n=e,a=t,r=ca(),{t:o}=Vt(),i=ke("tag"),l=D(()=>{const{type:d,hit:v,effect:f,closable:h,round:m}=n;return[i.b(),i.is("closable",h),i.m(d||"primary"),i.m(r.value),i.m(f),i.is("hit",v),i.is("round",m)]}),s=d=>{a("close",d)},u=d=>{a("click",d)},c=d=>{var v,f,h;(h=(f=(v=d==null?void 0:d.component)==null?void 0:v.subTree)==null?void 0:f.component)!=null&&h.bum&&(d.component.subTree.component.bum=null)};return(d,v)=>e.disableTransitions?(x(),O("span",{key:0,class:U(l.value),style:et({backgroundColor:e.color}),onClick:u},[C("span",{class:U(p(i).e("content"))},[ce(d.$slots,"default")],2),e.closable?(x(),O("button",{key:0,"aria-label":p(o)("el.tag.close"),class:U(p(i).e("close")),type:"button",onClick:at(s,["stop"])},[L(p(Xe),null,{default:$(()=>[L(p(so))]),_:1})],10,tce)):te("v-if",!0)],6)):(x(),ne(Pa,{key:1,name:`${p(i).namespace.value}-zoom-in-center`,appear:"",onVnodeMounted:c},{default:$(()=>[C("span",{class:U(l.value),style:et({backgroundColor:e.color}),onClick:u},[C("span",{class:U(p(i).e("content"))},[ce(d.$slots,"default")],2),e.closable?(x(),O("button",{key:0,"aria-label":p(o)("el.tag.close"),class:U(p(i).e("close")),type:"button",onClick:at(s,["stop"])},[L(p(Xe),null,{default:$(()=>[L(p(so))]),_:1})],10,nce)):te("v-if",!0)],6)]),_:3},8,["name"]))}}),rce=ace;const Cl=wt(rce),zL=Symbol("ElSelectGroup"),sp=Symbol("ElSelect"),Qm={label:"label",value:"value",disabled:"disabled",options:"options"};function up(e){const t=F({...Qm,...e.props});let n={...e.props};return ye(()=>e.props,l=>{Rn(l,n)||(t.value={...Qm,...l},n={...l})},{deep:!0}),{aliasProps:t,getLabel:l=>na(l,t.value.label),getValue:l=>na(l,t.value.value),getDisabled:l=>na(l,t.value.disabled),getOptions:l=>na(l,t.value.options)}}const FL=$e({name:String,id:String,modelValue:{type:ae([Array,String,Number,Boolean,Object]),default:void 0},autocomplete:{type:String,default:"off"},automaticDropdown:Boolean,size:pa,effect:{type:ae(String),default:"light"},disabled:{type:Boolean,default:void 0},clearable:Boolean,filterable:Boolean,allowCreate:Boolean,loading:Boolean,popperClass:{type:ae([String,Array,Object,Boolean]),default:""},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},popperOptions:{type:ae(Object),default:()=>({})},remote:Boolean,debounce:{type:Number,default:300},loadingText:String,noMatchText:String,noDataText:String,remoteMethod:{type:ae(Function)},filterMethod:{type:ae(Function)},multiple:Boolean,multipleLimit:{type:Number,default:0},placeholder:{type:String},defaultFirstOption:Boolean,reserveKeyword:{type:Boolean,default:!0},valueKey:{type:String,default:"value"},collapseTags:Boolean,collapseTagsTooltip:Boolean,tagTooltip:{type:ae(Object),default:()=>({})},maxCollapseTags:{type:Number,default:1},teleported:Tn.teleported,persistent:{type:Boolean,default:!0},clearIcon:{type:pn,default:ci},fitInputWidth:Boolean,suffixIcon:{type:pn,default:ui},tagType:{...Ll.type,default:"info"},tagEffect:{...Ll.effect,default:"light"},validateEvent:{type:Boolean,default:!0},remoteShowSuffix:Boolean,showArrow:{type:Boolean,default:!0},offset:{type:Number,default:12},placement:{type:ae(String),values:tl,default:"bottom-start"},fallbackPlacements:{type:ae(Array),default:["bottom-start","top-start","right","left"]},tabindex:{type:[String,Number],default:0},appendTo:Tn.appendTo,options:{type:ae(Array)},props:{type:ae(Object),default:()=>Qm},...Mc,...dr(["ariaLabel"])}),oce={[bt]:e=>!0,[Ot]:e=>!0,"popup-scroll":hh.scroll,"end-reached":hh["end-reached"],"remove-tag":e=>!0,"visible-change":e=>!0,focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0},B4="ElOption",ice=$e({value:{type:[String,Number,Boolean,Object],required:!0},label:{type:[String,Number]},created:Boolean,disabled:Boolean});function lce(e,t){const n=Ze(sp);n||Dn(B4,"usage: ");const a=Ze(zL,{disabled:!1}),r=D(()=>c(wa(n.props.modelValue),e.value)),o=D(()=>{if(n.props.multiple){const f=wa(n.props.modelValue??[]);return!r.value&&f.length>=n.props.multipleLimit&&n.props.multipleLimit>0}else return!1}),i=D(()=>e.label??(_t(e.value)?"":e.value)),l=D(()=>e.value||e.label||""),s=D(()=>e.disabled||t.groupDisabled||o.value),u=Et(),c=(f=[],h)=>{if(_t(e.value)){const m=n.props.valueKey;return f&&f.some(g=>wn(na(g,m))===na(h,m))}else return f&&f.includes(h)},d=()=>{s.value||(n.states.hoveringIndex=n.optionsArray.indexOf(u.proxy))},v=f=>{t.visible=new RegExp(S6(f),"i").test(String(i.value))||e.created};return ye(()=>i.value,()=>{!e.created&&!n.props.remote&&n.setSelected()}),ye(()=>e.value,(f,h)=>{const{remote:m,valueKey:g}=n.props;if((m?f!==h:!Rn(f,h))&&(n.onOptionDestroy(h,u.proxy),n.onOptionCreate(u.proxy)),!e.created&&!m){if(g&&_t(f)&&_t(h)&&f[g]===h[g])return;n.setSelected()}}),ye(()=>a.disabled,()=>{t.groupDisabled=a.disabled},{immediate:!0}),{select:n,currentLabel:i,currentValue:l,itemSelected:r,isDisabled:s,hoverItem:d,updateOption:v}}var sce=q({name:B4,componentName:B4,props:ice,setup(e){const t=ke("select"),n=Fa(),a=D(()=>[t.be("dropdown","item"),t.is("disabled",p(s)),t.is("selected",p(l)),t.is("hovering",p(f))]),r=nn({index:-1,groupDisabled:!1,visible:!0,hover:!1}),o=Nd?null:"mousemove",{currentLabel:i,itemSelected:l,isDisabled:s,select:u,hoverItem:c,updateOption:d}=lce(e,r),{visible:v,hover:f}=Ta(r),h=Et().proxy;u.onOptionCreate(h),rn(()=>{const y=h.value;He(()=>{const{selected:b}=u.states,_=b.some(w=>w.value===h.value);u.states.cachedOptions.get(y)===h&&!_&&u.states.cachedOptions.delete(y)}),u.onOptionDestroy(y,h)});function m(){s.value||u.handleOptionSelect(h)}return{ns:t,id:n,containerKls:a,currentLabel:i,itemSelected:l,isDisabled:s,select:u,visible:v,hover:f,states:r,mouseMoveEventName:o,hoverItem:c,handleMousedown:y=>{let b=y.target;const _=y.currentTarget;for(;b&&b!==_;){if(sc(b))return;b=b.parentElement}y.preventDefault()},updateOption:d,selectOptionClick:m}}});const uce=["id","aria-disabled","aria-selected"];function cce(e,t,n,a,r,o){return st((x(),O("li",Tt({id:e.id,class:e.containerKls,role:"option","aria-disabled":e.isDisabled||void 0,"aria-selected":e.itemSelected},{[Wu(e.mouseMoveEventName)]:t[0]||(t[0]=(...i)=>e.hoverItem&&e.hoverItem(...i))},{onMousedown:t[1]||(t[1]=(...i)=>e.handleMousedown&&e.handleMousedown(...i)),onClick:t[2]||(t[2]=at((...i)=>e.selectOptionClick&&e.selectOptionClick(...i),["stop"]))}),[ce(e.$slots,"default",{},()=>[C("span",null,Q(e.currentLabel),1)])],16,uce)),[[Zt,e.visible]])}var ow=ya(sce,[["render",cce]]),dce=q({name:"ElOptionGroup",componentName:"ElOptionGroup",props:{label:String,disabled:Boolean},setup(e){const t=ke("select"),n=F(),a=Et(),r=F([]);Ft(zL,nn({...Ta(e)}));const o=D(()=>r.value.some(u=>u.visible===!0)),i=u=>{var c;return u.type.name==="ElOption"&&!!((c=u.component)!=null&&c.proxy)},l=u=>{const c=wa(u),d=[];return c.forEach(v=>{var f;bn(v)&&(i(v)?d.push(v.component.proxy):Ae(v.children)&&v.children.length?d.push(...l(v.children)):(f=v.component)!=null&&f.subTree&&d.push(...l(v.component.subTree)))}),d},s=()=>{r.value=l(a.subTree)};return mt(()=>{s()}),Zh(n,s,{attributes:!0,subtree:!0,childList:!0}),{groupRef:n,visible:o,ns:t}}});function fce(e,t,n,a,r,o){return st((x(),O("ul",{ref:"groupRef",class:U(e.ns.be("group","wrap"))},[C("li",{class:U(e.ns.be("group","title"))},Q(e.label),3),C("li",null,[C("ul",{class:U(e.ns.b("group"))},[ce(e.$slots,"default")],2)])],2)),[[Zt,e.visible]])}var iw=ya(dce,[["render",fce]]),vce=q({name:"ElSelectDropdown",componentName:"ElSelectDropdown",setup(){const e=Ze(sp),t=ke("select"),n=D(()=>e.props.popperClass),a=D(()=>e.props.multiple),r=D(()=>e.props.fitInputWidth),o=F("");function i(){var s;const l=(s=e.selectRef)==null?void 0:s.offsetWidth;l?o.value=`${l-2}px`:o.value=""}return mt(()=>{i(),Mn(e.selectRef,i)}),{ns:t,minWidth:o,popperClass:n,isMultiple:a,isFitInputWidth:r}}});function hce(e,t,n,a,r,o){return x(),O("div",{class:U([e.ns.b("dropdown"),e.ns.is("multiple",e.isMultiple),e.popperClass]),style:et({[e.isFitInputWidth?"width":"minWidth"]:e.minWidth})},[e.$slots.header?(x(),O("div",{key:0,class:U(e.ns.be("dropdown","header"))},[ce(e.$slots,"header")],2)):te("v-if",!0),ce(e.$slots,"default"),e.$slots.footer?(x(),O("div",{key:1,class:U(e.ns.be("dropdown","footer"))},[ce(e.$slots,"footer")],2)):te("v-if",!0)],6)}var pce=ya(vce,[["render",hce]]);const mce=(e,t)=>{const{t:n}=Vt(),a=Xn(),r=Fa(),o=ke("select"),i=ke("input"),l=nn({inputValue:"",options:new Map,cachedOptions:new Map,optionValues:[],selected:[],selectionWidth:0,collapseItemWidth:0,selectedLabel:"",hoveringIndex:-1,previousQuery:null,inputHovering:!1,menuVisibleOnFocus:!1,isBeforeHide:!1}),s=F(),u=F(),c=F(),d=F(),v=F(),f=F(),h=F(),m=F(),g=F(),y=F(),b=F(),_=F(!1),w=F(),S=F(!1),{form:k,formItem:T}=Ea(),{inputId:M}=Yr(e,{formItemContext:T}),{valueOnClear:E,isEmptyValue:A}=op(e),{isComposing:I,handleCompositionStart:P,handleCompositionUpdate:R,handleCompositionEnd:G}=rp({afterComposition:Oe=>Wt(Oe)}),K=$n(),{wrapperRef:H,isFocused:z,handleBlur:B}=Fl(v,{disabled:K,afterFocus(){e.automaticDropdown&&!_.value&&(_.value=!0,l.menuVisibleOnFocus=!0)},beforeBlur(Oe){var lt,Bt;return((lt=c.value)==null?void 0:lt.isFocusInsideContent(Oe))||((Bt=d.value)==null?void 0:Bt.isFocusInsideContent(Oe))},afterBlur(){var Oe;_.value=!1,l.menuVisibleOnFocus=!1,e.validateEvent&&((Oe=T==null?void 0:T.validate)==null||Oe.call(T,"blur").catch(kt))}}),W=D(()=>Ae(e.modelValue)?e.modelValue.length>0:!A(e.modelValue)),N=D(()=>(k==null?void 0:k.statusIcon)??!1),V=D(()=>e.clearable&&!K.value&&W.value&&(z.value||l.inputHovering)),Y=D(()=>e.remote&&e.filterable&&!e.remoteShowSuffix?"":e.suffixIcon),X=D(()=>o.is("reverse",!!(Y.value&&_.value))),ee=D(()=>(T==null?void 0:T.validateState)||""),re=D(()=>ee.value&&F1[ee.value]),ie=D(()=>e.remote?e.debounce:0),le=D(()=>e.remote&&!l.inputValue&&l.options.size===0),ve=D(()=>e.loading?e.loadingText||n("el.select.loading"):e.filterable&&l.inputValue&&l.options.size>0&&he.value===0?e.noMatchText||n("el.select.noMatch"):l.options.size===0?e.noDataText||n("el.select.noData"):null),he=D(()=>J.value.filter(Oe=>Oe.visible).length),J=D(()=>{const Oe=Array.from(l.options.values()),lt=[];return l.optionValues.forEach(Bt=>{const _n=Oe.findIndex(Yn=>Yn.value===Bt);_n>-1&<.push(Oe[_n])}),lt.length>=Oe.length?lt:Oe}),Z=D(()=>Array.from(l.cachedOptions.values())),j=D(()=>{const Oe=J.value.filter(lt=>!lt.created).some(lt=>lt.currentLabel===l.inputValue);return e.filterable&&e.allowCreate&&l.inputValue!==""&&!Oe}),oe=()=>{e.filterable&&Qe(e.filterMethod)||e.filterable&&e.remote&&Qe(e.remoteMethod)||J.value.forEach(Oe=>{var lt;(lt=Oe.updateOption)==null||lt.call(Oe,l.inputValue)})},se=ca(),fe=D(()=>["small"].includes(se.value)?"small":"default"),ge=D({get(){return _.value&&(e.loading||!le.value||e.remote&&!!a.empty)&&(!S.value||!_r(l.previousQuery)||l.options.size>0)},set(Oe){_.value=Oe}}),Se=D(()=>{if(e.multiple&&!zt(e.modelValue))return wa(e.modelValue).length===0&&!l.inputValue;const Oe=Ae(e.modelValue)?e.modelValue[0]:e.modelValue;return e.filterable||zt(Oe)?!l.inputValue:!0}),De=D(()=>{const Oe=e.placeholder??n("el.select.placeholder");return e.multiple||!W.value?Oe:l.selectedLabel}),Te=Nd?null:"mouseenter";ye(()=>e.modelValue,(Oe,lt)=>{e.multiple&&e.filterable&&!e.reserveKeyword&&(l.inputValue="",Re("")),Le(),!Rn(Oe,lt)&&e.validateEvent&&(T==null||T.validate("change").catch(kt))},{flush:"post",deep:!0}),ye(()=>_.value,Oe=>{Oe?Re(l.inputValue):(l.inputValue="",l.previousQuery=null,l.isBeforeHide=!0,l.menuVisibleOnFocus=!1)}),ye(()=>l.options.entries(),()=>{qt&&(Le(),e.defaultFirstOption&&(e.filterable||e.remote)&&he.value&&Ce())},{flush:"post"}),ye([()=>l.hoveringIndex,J],([Oe])=>{Je(Oe)&&Oe>-1?w.value=J.value[Oe]||{}:w.value={},J.value.forEach(lt=>{lt.hover=w.value===lt})}),ur(()=>{l.isBeforeHide||oe()});const Re=Oe=>{l.previousQuery===Oe||I.value||(l.previousQuery=Oe,e.filterable&&Qe(e.filterMethod)?e.filterMethod(Oe):e.filterable&&e.remote&&Qe(e.remoteMethod)&&e.remoteMethod(Oe),e.defaultFirstOption&&(e.filterable||e.remote)&&he.value?He(Ce):He(Be))},Ce=()=>{const Oe=J.value.filter(_n=>_n.visible&&!_n.disabled&&!_n.states.groupDisabled),lt=Oe.find(_n=>_n.created),Bt=Oe[0];l.hoveringIndex=Ue(J.value.map(_n=>_n.value),lt||Bt)},Le=()=>{if(e.multiple)l.selectedLabel="";else{const lt=me(Ae(e.modelValue)?e.modelValue[0]:e.modelValue);l.selectedLabel=lt.currentLabel,l.selected=[lt];return}const Oe=[];zt(e.modelValue)||wa(e.modelValue).forEach(lt=>{Oe.push(me(lt))}),l.selected=Oe},me=Oe=>{let lt;const Bt=qv(Oe);for(let Yn=l.cachedOptions.size-1;Yn>=0;Yn--){const Zn=Z.value[Yn];if(Bt?na(Zn.value,e.valueKey)===na(Oe,e.valueKey):Zn.value===Oe){lt={index:J.value.filter(ja=>!ja.created).indexOf(Zn),value:Oe,currentLabel:Zn.currentLabel,get isDisabled(){return Zn.isDisabled}};break}}if(lt)return lt;const _n=l.selected.find(Yn=>Bt?na(Yn.value,e.valueKey)===na(Oe,e.valueKey):Yn.value===Oe);return{index:-1,value:Oe,currentLabel:Bt?Oe.label:_n?_n.currentLabel:Oe??""}},Be=()=>{const Oe=l.selected.length;if(Oe>0){const lt=l.selected[Oe-1];l.hoveringIndex=J.value.findIndex(Bt=>Ie(lt)===Ie(Bt))}else l.hoveringIndex=-1},ze=()=>{l.selectionWidth=Number.parseFloat(window.getComputedStyle(u.value).width)},xe=()=>{l.collapseItemWidth=y.value.getBoundingClientRect().width},rt=()=>{var Oe,lt;(lt=(Oe=c.value)==null?void 0:Oe.updatePopper)==null||lt.call(Oe)},nt=()=>{var Oe,lt;(lt=(Oe=d.value)==null?void 0:Oe.updatePopper)==null||lt.call(Oe)},Pt=()=>{l.inputValue.length>0&&!_.value&&(_.value=!0),Re(l.inputValue)},Wt=Oe=>{if(l.inputValue=Oe.target.value,e.remote)S.value=!0,Jt();else return Pt()},Jt=Xh(()=>{Pt(),S.value=!1},ie),Rt=Oe=>{Rn(e.modelValue,Oe)||t(Ot,Oe)},Qt=Oe=>wA(Oe,lt=>{const Bt=l.cachedOptions.get(lt);return!(Bt!=null&&Bt.disabled)&&!(Bt!=null&&Bt.states.groupDisabled)}),_e=Oe=>{const lt=mn(Oe);if(e.multiple&<!==Pe.delete&&Oe.target.value.length<=0){const Bt=wa(e.modelValue).slice(),_n=Qt(Bt);if(_n<0)return;const Yn=Bt[_n];Bt.splice(_n,1),t(bt,Bt),Rt(Bt),t("remove-tag",Yn)}},Ne=(Oe,lt)=>{const Bt=l.selected.indexOf(lt);if(Bt>-1&&!K.value){const _n=wa(e.modelValue).slice();_n.splice(Bt,1),t(bt,_n),Rt(_n),t("remove-tag",lt.value)}Oe.stopPropagation(),It()},tt=Oe=>{Oe.stopPropagation();const lt=e.multiple?[]:E.value;if(e.multiple)for(const Bt of l.selected)Bt.isDisabled&<.push(Bt.value);t(bt,lt),Rt(lt),l.hoveringIndex=-1,_.value=!1,t("clear"),It()},Me=Oe=>{if(e.multiple){const lt=wa(e.modelValue??[]).slice(),Bt=Ue(lt,Oe);Bt>-1?lt.splice(Bt,1):(e.multipleLimit<=0||lt.length{Gt(Oe)})},Ue=(Oe,lt)=>zt(lt)?-1:_t(lt.value)?Oe.findIndex(Bt=>Rn(na(Bt,e.valueKey),Ie(lt))):Oe.indexOf(lt.value),Gt=Oe=>{var _n,Yn,Zn,ja,Gl;const lt=Ae(Oe)?Oe[Oe.length-1]:Oe;let Bt=null;if(!Un(lt==null?void 0:lt.value)){const co=J.value.filter(Yl=>Yl.value===lt.value);co.length>0&&(Bt=co[0].$el)}if(c.value&&Bt){const co=(ja=(Zn=(Yn=(_n=c.value)==null?void 0:_n.popperRef)==null?void 0:Yn.contentRef)==null?void 0:Zn.querySelector)==null?void 0:ja.call(Zn,`.${o.be("dropdown","wrap")}`);co&&k6(co,Bt)}(Gl=b.value)==null||Gl.handleScroll()},je=Oe=>{l.options.set(Oe.value,Oe),l.cachedOptions.set(Oe.value,Oe)},ft=(Oe,lt)=>{l.options.get(Oe)===lt&&l.options.delete(Oe)},ht=D(()=>{var Oe,lt;return(lt=(Oe=c.value)==null?void 0:Oe.popperRef)==null?void 0:lt.contentRef}),pt=()=>{l.isBeforeHide=!1,He(()=>{var Oe;(Oe=b.value)==null||Oe.update(),Gt(l.selected)})},It=()=>{var Oe;(Oe=v.value)==null||Oe.focus()},Ct=()=>{var Oe;if(_.value){_.value=!1,He(()=>{var lt;return(lt=v.value)==null?void 0:lt.blur()});return}(Oe=v.value)==null||Oe.blur()},$t=Oe=>{tt(Oe)},Sn=Oe=>{if(_.value=!1,z.value){const lt=new FocusEvent("blur",Oe);He(()=>B(lt))}},Fe=()=>{l.inputValue.length>0?l.inputValue="":_.value=!1},qe=Oe=>{var lt;K.value||e.filterable&&_.value&&Oe&&!((lt=h.value)!=null&<.contains(Oe.target))||(Nd&&(l.inputHovering=!0),l.menuVisibleOnFocus?l.menuVisibleOnFocus=!1:_.value=!_.value)},ut=()=>{if(!_.value)qe();else{const Oe=J.value[l.hoveringIndex];Oe&&!Oe.isDisabled&&Me(Oe)}},Ie=Oe=>_t(Oe.value)?na(Oe.value,e.valueKey):Oe.value,Ke=D(()=>J.value.filter(Oe=>Oe.visible).every(Oe=>Oe.isDisabled)),Ge=D(()=>e.multiple?e.collapseTags?l.selected.slice(0,e.maxCollapseTags):l.selected:[]),Ee=D(()=>e.multiple?e.collapseTags?l.selected.slice(e.maxCollapseTags):[]:[]),de=Oe=>{if(!_.value){_.value=!0;return}if(!(l.options.size===0||he.value===0||I.value)&&!Ke.value){Oe==="next"?(l.hoveringIndex++,l.hoveringIndex===l.options.size&&(l.hoveringIndex=0)):Oe==="prev"&&(l.hoveringIndex--,l.hoveringIndex<0&&(l.hoveringIndex=l.options.size-1));const lt=J.value[l.hoveringIndex];(lt.isDisabled||!lt.visible)&&de(Oe),He(()=>Gt(w.value))}},it=(Oe,lt,Bt,_n)=>{for(let Yn=lt;Yn>=0&&Yn<_n;Yn+=Bt){const Zn=Oe[Yn];if(!(Zn!=null&&Zn.isDisabled)&&(Zn!=null&&Zn.visible))return Yn}return null},Dt=(Oe,lt)=>{const Bt=l.options.size;if(Bt===0)return;const _n=Br(Oe,0,Bt-1),Yn=J.value,Zn=lt==="up"?-1:1,ja=it(Yn,_n,Zn,Bt)??it(Yn,_n-Zn,-Zn,Bt);ja!=null&&(l.hoveringIndex=ja,He(()=>Gt(w.value)))},sn=Oe=>{const lt=mn(Oe);let Bt=!0;switch(lt){case Pe.up:de("prev");break;case Pe.down:de("next");break;case Pe.enter:case Pe.numpadEnter:I.value||ut();break;case Pe.esc:Fe();break;case Pe.backspace:Bt=!1,_e(Oe);return;case Pe.home:if(!_.value)return;Dt(0,"down");break;case Pe.end:if(!_.value)return;Dt(l.options.size-1,"up");break;case Pe.pageUp:if(!_.value)return;Dt(l.hoveringIndex-10,"up");break;case Pe.pageDown:if(!_.value)return;Dt(l.hoveringIndex+10,"down");break;default:Bt=!1;break}Bt&&(Oe.preventDefault(),Oe.stopPropagation())},gn=()=>{if(!u.value)return 0;const Oe=window.getComputedStyle(u.value);return Number.parseFloat(Oe.gap||"6px")},Aa=D(()=>{const Oe=gn(),lt=e.filterable?Oe+11:0;return{maxWidth:`${y.value&&e.maxCollapseTags===1?l.selectionWidth-l.collapseItemWidth-Oe-lt:l.selectionWidth-lt}px`}}),jr=D(()=>({maxWidth:`${l.selectionWidth}px`})),vr=Oe=>{t("popup-scroll",Oe)},Ia=Oe=>{t("end-reached",Oe)};Mn(u,ze),Mn(H,rt),Mn(g,nt),Mn(y,xe);let Oa;return ye(()=>ge.value,Oe=>{Oe?Oa=Mn(m,rt).stop:(Oa==null||Oa(),Oa=void 0),t("visible-change",Oe)}),mt(()=>{Le()}),{inputId:M,contentId:r,nsSelect:o,nsInput:i,states:l,isFocused:z,expanded:_,optionsArray:J,hoverOption:w,selectSize:se,filteredOptionsCount:he,updateTooltip:rt,updateTagTooltip:nt,debouncedOnInputChange:Jt,onInput:Wt,deletePrevTag:_e,deleteTag:Ne,deleteSelected:tt,handleOptionSelect:Me,scrollToOption:Gt,hasModelValue:W,shouldShowPlaceholder:Se,currentPlaceholder:De,mouseEnterEventName:Te,needStatusIcon:N,showClearBtn:V,iconComponent:Y,iconReverse:X,validateState:ee,validateIcon:re,showNewOption:j,updateOptions:oe,collapseTagSize:fe,setSelected:Le,selectDisabled:K,emptyText:ve,handleCompositionStart:P,handleCompositionUpdate:R,handleCompositionEnd:G,handleKeydown:sn,onOptionCreate:je,onOptionDestroy:ft,handleMenuEnter:pt,focus:It,blur:Ct,handleClearClick:$t,handleClickOutside:Sn,handleEsc:Fe,toggleMenu:qe,selectOption:ut,getValueKey:Ie,navigateOptions:de,dropdownMenuVisible:ge,showTagList:Ge,collapseTagList:Ee,popupScroll:vr,getOption:me,endReached:Ia,tagStyle:Aa,collapseTagStyle:jr,popperRef:ht,inputRef:v,tooltipRef:c,tagTooltipRef:d,prefixRef:f,suffixRef:h,selectRef:s,wrapperRef:H,selectionRef:u,scrollbarRef:b,menuRef:m,tagMenuRef:g,collapseItemRef:y}};var gce=q({name:"ElOptions",setup(e,{slots:t}){const n=Ze(sp);let a=[];return()=>{var l,s;const r=(l=t.default)==null?void 0:l.call(t),o=[];function i(u){Ae(u)&&u.forEach(c=>{var v,f,h,m;const d=(v=(c==null?void 0:c.type)||{})==null?void 0:v.name;d==="ElOptionGroup"?i(!Ye(c.children)&&!Ae(c.children)&&Qe((f=c.children)==null?void 0:f.default)?(h=c.children)==null?void 0:h.default():c.children):d==="ElOption"?o.push((m=c.props)==null?void 0:m.value):Ae(c.children)&&i(c.children)})}return r.length&&i((s=r[0])==null?void 0:s.children),Rn(o,a)||(a=o,n&&(n.states.optionValues=o)),r}}});const ex="ElSelect",ph=new WeakMap,yce=e=>(...t)=>{var r,o;const n=t[0];if(!n||n.includes('Slot "default" invoked outside of the render function')&&((r=t[2])!=null&&r.includes("ElTreeSelect")))return;const a=(o=ph.get(e))==null?void 0:o.originalWarnHandler;if(a){a(...t);return}console.warn(...t)},bce=e=>{let t=ph.get(e);return t||(t={originalWarnHandler:e.config.warnHandler,handler:yce(e),count:0},ph.set(e,t)),t};var _ce=q({name:ex,componentName:ex,components:{ElSelectMenu:pce,ElOption:ow,ElOptions:gce,ElOptionGroup:iw,ElTag:Cl,ElScrollbar:ni,ElTooltip:Ma,ElIcon:Xe},directives:{ClickOutside:Ns},props:FL,emits:oce,setup(e,{emit:t,slots:n}){const a=Et(),r=bce(a.appContext);r.count+=1,a.appContext.config.warnHandler=r.handler;const o=D(()=>{const{modelValue:b,multiple:_}=e,w=_?[]:void 0;return Ae(b)?_?b:w:_?w:b}),i=nn({...Ta(e),modelValue:o}),l=mce(i,t),{calculatorRef:s,inputStyle:u}=w6(),{getLabel:c,getValue:d,getOptions:v,getDisabled:f}=up(e),h=b=>({label:c(b),value:d(b),disabled:f(b)}),m=b=>b.reduce((_,w)=>(_.push(w),w.children&&w.children.length>0&&_.push(...m(w.children)),_),[]),g=b=>{eo(b||[]).forEach(_=>{var w;if(_t(_)&&(_.type.name==="ElOption"||_.type.name==="ElTree")){const S=_.type.name;if(S==="ElTree")m(((w=_.props)==null?void 0:w.data)||[]).forEach(k=>{k.currentLabel=k.label??(_t(k.value)?"":k.value),l.onOptionCreate(k)});else if(S==="ElOption"){const k={..._.props};k.currentLabel=k.label??(_t(k.value)?"":k.value),l.onOptionCreate(k)}}})};ye(()=>{var b;return[e.persistent||l.expanded.value||!n.default||(b=n.default)==null?void 0:b.call(n),o.value]},()=>{var b;e.persistent||l.expanded.value||n.default&&(l.states.options.clear(),g((b=n.default)==null?void 0:b.call(n)))},{immediate:!0}),Ft(sp,nn({props:i,states:l.states,selectRef:l.selectRef,optionsArray:l.optionsArray,setSelected:l.setSelected,handleOptionSelect:l.handleOptionSelect,onOptionCreate:l.onOptionCreate,onOptionDestroy:l.onOptionDestroy}));const y=D(()=>e.multiple?l.states.selected.map(b=>b.currentLabel):l.states.selectedLabel);return rn(()=>{const b=ph.get(a.appContext);b&&(b.count-=1,b.count<=0&&(a.appContext.config.warnHandler=b.originalWarnHandler,ph.delete(a.appContext)))}),{...l,modelValue:o,selectedLabel:y,calculatorRef:s,inputStyle:u,getLabel:c,getValue:d,getOptions:v,getDisabled:f,getOptionProps:h}}});const wce=["id","value","name","disabled","autocomplete","tabindex","readonly","aria-activedescendant","aria-controls","aria-expanded","aria-label"],Sce=["textContent"],Cce={key:1};function xce(e,t,n,a,r,o){const i=be("el-tag"),l=be("el-tooltip"),s=be("el-icon"),u=be("el-option"),c=be("el-option-group"),d=be("el-options"),v=be("el-scrollbar"),f=be("el-select-menu"),h=Gr("click-outside");return st((x(),O("div",Tt({ref:"selectRef",class:[e.nsSelect.b(),e.nsSelect.m(e.selectSize)]},{[Wu(e.mouseEnterEventName)]:t[11]||(t[11]=m=>e.states.inputHovering=!0)},{onMouseleave:t[12]||(t[12]=m=>e.states.inputHovering=!1)}),[L(l,{ref:"tooltipRef",visible:e.dropdownMenuVisible,placement:e.placement,teleported:e.teleported,"popper-class":[e.nsSelect.e("popper"),e.popperClass],"popper-style":e.popperStyle,"popper-options":e.popperOptions,"fallback-placements":e.fallbackPlacements,effect:e.effect,pure:"",trigger:"click",transition:`${e.nsSelect.namespace.value}-zoom-in-top`,"stop-popper-mouse-event":!1,"gpu-acceleration":!1,persistent:e.persistent,"append-to":e.appendTo,"show-arrow":e.showArrow,offset:e.offset,onBeforeShow:e.handleMenuEnter,onHide:t[10]||(t[10]=m=>e.states.isBeforeHide=!1)},{default:$(()=>{var m;return[C("div",{ref:"wrapperRef",class:U([e.nsSelect.e("wrapper"),e.nsSelect.is("focused",e.isFocused),e.nsSelect.is("hovering",e.states.inputHovering),e.nsSelect.is("filterable",e.filterable),e.nsSelect.is("disabled",e.selectDisabled)]),onClick:t[7]||(t[7]=at((...g)=>e.toggleMenu&&e.toggleMenu(...g),["prevent"]))},[e.$slots.prefix?(x(),O("div",{key:0,ref:"prefixRef",class:U(e.nsSelect.e("prefix"))},[ce(e.$slots,"prefix")],2)):te("v-if",!0),C("div",{ref:"selectionRef",class:U([e.nsSelect.e("selection"),e.nsSelect.is("near",e.multiple&&!e.$slots.prefix&&!!e.states.selected.length)])},[e.multiple?ce(e.$slots,"tag",{key:0,data:e.states.selected,deleteTag:e.deleteTag,selectDisabled:e.selectDisabled},()=>{var g,y,b,_,w,S,k,T,M,E,A,I,P;return[(x(!0),O(Ve,null,dt(e.showTagList,R=>(x(),O("div",{key:e.getValueKey(R),class:U(e.nsSelect.e("selected-item"))},[L(i,{closable:!e.selectDisabled&&!R.isDisabled,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",style:et(e.tagStyle),onClose:G=>e.deleteTag(G,R)},{default:$(()=>[C("span",{class:U(e.nsSelect.e("tags-text"))},[ce(e.$slots,"label",{index:R.index,label:R.currentLabel,value:R.value},()=>[ue(Q(R.currentLabel),1)])],2)]),_:2},1032,["closable","size","type","effect","style","onClose"])],2))),128)),e.collapseTags&&e.states.selected.length>e.maxCollapseTags?(x(),ne(l,{key:0,ref:"tagTooltipRef",disabled:e.dropdownMenuVisible||!e.collapseTagsTooltip,"fallback-placements":((g=e.tagTooltip)==null?void 0:g.fallbackPlacements)??["bottom","top","right","left"],effect:((y=e.tagTooltip)==null?void 0:y.effect)??e.effect,placement:((b=e.tagTooltip)==null?void 0:b.placement)??"bottom","popper-class":((_=e.tagTooltip)==null?void 0:_.popperClass)??e.popperClass,"popper-style":((w=e.tagTooltip)==null?void 0:w.popperStyle)??e.popperStyle,teleported:((S=e.tagTooltip)==null?void 0:S.teleported)??e.teleported,"append-to":((k=e.tagTooltip)==null?void 0:k.appendTo)??e.appendTo,"popper-options":((T=e.tagTooltip)==null?void 0:T.popperOptions)??e.popperOptions,transition:(M=e.tagTooltip)==null?void 0:M.transition,"show-after":(E=e.tagTooltip)==null?void 0:E.showAfter,"hide-after":(A=e.tagTooltip)==null?void 0:A.hideAfter,"auto-close":(I=e.tagTooltip)==null?void 0:I.autoClose,offset:(P=e.tagTooltip)==null?void 0:P.offset},{default:$(()=>[C("div",{ref:"collapseItemRef",class:U(e.nsSelect.e("selected-item"))},[L(i,{closable:!1,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",style:et(e.collapseTagStyle)},{default:$(()=>[C("span",{class:U(e.nsSelect.e("tags-text"))}," + "+Q(e.states.selected.length-e.maxCollapseTags),3)]),_:1},8,["size","type","effect","style"])],2)]),content:$(()=>[C("div",{ref:"tagMenuRef",class:U(e.nsSelect.e("selection"))},[(x(!0),O(Ve,null,dt(e.collapseTagList,R=>(x(),O("div",{key:e.getValueKey(R),class:U(e.nsSelect.e("selected-item"))},[L(i,{class:"in-tooltip",closable:!e.selectDisabled&&!R.isDisabled,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",onClose:G=>e.deleteTag(G,R)},{default:$(()=>[C("span",{class:U(e.nsSelect.e("tags-text"))},[ce(e.$slots,"label",{index:R.index,label:R.currentLabel,value:R.value},()=>[ue(Q(R.currentLabel),1)])],2)]),_:2},1032,["closable","size","type","effect","onClose"])],2))),128))],2)]),_:3},8,["disabled","fallback-placements","effect","placement","popper-class","popper-style","teleported","append-to","popper-options","transition","show-after","hide-after","auto-close","offset"])):te("v-if",!0)]}):te("v-if",!0),C("div",{class:U([e.nsSelect.e("selected-item"),e.nsSelect.e("input-wrapper"),e.nsSelect.is("hidden",!e.filterable||e.selectDisabled||e.multiple&&!e.states.inputValue&&!e.isFocused)])},[C("input",{id:e.inputId,ref:"inputRef",value:e.states.inputValue,type:"text",name:e.name,class:U([e.nsSelect.e("input"),e.nsSelect.is(e.selectSize)]),disabled:e.selectDisabled,autocomplete:e.autocomplete,style:et(e.inputStyle),tabindex:e.tabindex,role:"combobox",readonly:!e.filterable,spellcheck:"false","aria-activedescendant":((m=e.hoverOption)==null?void 0:m.id)||"","aria-controls":e.contentId,"aria-expanded":e.dropdownMenuVisible,"aria-label":e.ariaLabel,"aria-autocomplete":"none","aria-haspopup":"listbox",onKeydown:t[0]||(t[0]=(...g)=>e.handleKeydown&&e.handleKeydown(...g)),onCompositionstart:t[1]||(t[1]=(...g)=>e.handleCompositionStart&&e.handleCompositionStart(...g)),onCompositionupdate:t[2]||(t[2]=(...g)=>e.handleCompositionUpdate&&e.handleCompositionUpdate(...g)),onCompositionend:t[3]||(t[3]=(...g)=>e.handleCompositionEnd&&e.handleCompositionEnd(...g)),onInput:t[4]||(t[4]=(...g)=>e.onInput&&e.onInput(...g)),onChange:t[5]||(t[5]=at(()=>{},["stop"])),onClick:t[6]||(t[6]=at((...g)=>e.toggleMenu&&e.toggleMenu(...g),["stop"]))},null,46,wce),e.filterable?(x(),O("span",{key:0,ref:"calculatorRef","aria-hidden":"true",class:U(e.nsSelect.e("input-calculator")),textContent:Q(e.states.inputValue)},null,10,Sce)):te("v-if",!0)],2),e.shouldShowPlaceholder?(x(),O("div",{key:1,class:U([e.nsSelect.e("selected-item"),e.nsSelect.e("placeholder"),e.nsSelect.is("transparent",!e.hasModelValue||e.expanded&&!e.states.inputValue)])},[e.hasModelValue?ce(e.$slots,"label",{key:0,index:e.getOption(e.modelValue).index,label:e.currentPlaceholder,value:e.modelValue},()=>[C("span",null,Q(e.currentPlaceholder),1)]):(x(),O("span",Cce,Q(e.currentPlaceholder),1))],2)):te("v-if",!0)],2),C("div",{ref:"suffixRef",class:U(e.nsSelect.e("suffix"))},[e.iconComponent&&!e.showClearBtn?(x(),ne(s,{key:0,class:U([e.nsSelect.e("caret"),e.nsSelect.e("icon"),e.iconReverse])},{default:$(()=>[(x(),ne(St(e.iconComponent)))]),_:1},8,["class"])):te("v-if",!0),e.showClearBtn&&e.clearIcon?(x(),ne(s,{key:1,class:U([e.nsSelect.e("caret"),e.nsSelect.e("icon"),e.nsSelect.e("clear")]),onClick:e.handleClearClick},{default:$(()=>[(x(),ne(St(e.clearIcon)))]),_:1},8,["class","onClick"])):te("v-if",!0),e.validateState&&e.validateIcon&&e.needStatusIcon?(x(),ne(s,{key:2,class:U([e.nsInput.e("icon"),e.nsInput.e("validateIcon"),e.nsInput.is("loading",e.validateState==="validating")])},{default:$(()=>[(x(),ne(St(e.validateIcon)))]),_:1},8,["class"])):te("v-if",!0)],2)],2)]}),content:$(()=>[L(f,{ref:"menuRef"},{default:$(()=>[e.$slots.header?(x(),O("div",{key:0,class:U(e.nsSelect.be("dropdown","header")),onClick:t[8]||(t[8]=at(()=>{},["stop"]))},[ce(e.$slots,"header")],2)):te("v-if",!0),st(L(v,{id:e.contentId,ref:"scrollbarRef",tag:"ul","wrap-class":e.nsSelect.be("dropdown","wrap"),"view-class":e.nsSelect.be("dropdown","list"),class:U([e.nsSelect.is("empty",e.filteredOptionsCount===0)]),role:"listbox","aria-label":e.ariaLabel,"aria-orientation":"vertical",onScroll:e.popupScroll,onEndReached:e.endReached},{default:$(()=>[e.showNewOption?(x(),ne(u,{key:0,value:e.states.inputValue,created:!0},null,8,["value"])):te("v-if",!0),L(d,null,{default:$(()=>[ce(e.$slots,"default",{},()=>[(x(!0),O(Ve,null,dt(e.options,(m,g)=>{var y;return x(),O(Ve,{key:g},[(y=e.getOptions(m))!=null&&y.length?(x(),ne(c,{key:0,label:e.getLabel(m),disabled:e.getDisabled(m)},{default:$(()=>[(x(!0),O(Ve,null,dt(e.getOptions(m),b=>(x(),ne(u,Tt({key:e.getValue(b)},{ref_for:!0},e.getOptionProps(b)),null,16))),128))]),_:2},1032,["label","disabled"])):(x(),ne(u,Tt({key:1,ref_for:!0},e.getOptionProps(m)),null,16))],64)}),128))])]),_:3})]),_:3},8,["id","wrap-class","view-class","class","aria-label","onScroll","onEndReached"]),[[Zt,e.states.options.size>0&&!e.loading]]),e.$slots.loading&&e.loading?(x(),O("div",{key:1,class:U(e.nsSelect.be("dropdown","loading"))},[ce(e.$slots,"loading")],2)):e.loading||e.filteredOptionsCount===0?(x(),O("div",{key:2,class:U(e.nsSelect.be("dropdown","empty"))},[ce(e.$slots,"empty",{},()=>[C("span",null,Q(e.emptyText),1)])],2)):te("v-if",!0),e.$slots.footer?(x(),O("div",{key:3,class:U(e.nsSelect.be("dropdown","footer")),onClick:t[9]||(t[9]=at(()=>{},["stop"]))},[ce(e.$slots,"footer")],2)):te("v-if",!0)]),_:3},512)]),_:3},8,["visible","placement","teleported","popper-class","popper-style","popper-options","fallback-placements","effect","transition","persistent","append-to","show-arrow","offset","onBeforeShow"])],16)),[[h,e.handleClickOutside,e.popperRef]])}var kce=ya(_ce,[["render",xce]]);const Bs=wt(kce,{Option:ow,OptionGroup:iw}),eg=On(ow),Tce=On(iw),Mce=(e,t)=>{const n=e.subtract(1,"month").endOf("month").date();return xs(t).map((a,r)=>n-(t-r-1))},Ece=e=>xs(e.daysInMonth()).map((t,n)=>n+1),Ace=e=>xs(e.length/7).map(t=>{const n=t*7;return e.slice(n,n+7)}),Ice=$e({selectedDay:{type:ae(Object)},range:{type:ae(Array)},date:{type:ae(Object),required:!0},hideHeader:{type:Boolean}}),Lce={pick:e=>_t(e)};var HL={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){return function(n,a,r){var o=a.prototype,i=function(d){return d&&(d.indexOf?d:d.s)},l=function(d,v,f,h,m){var g=d.name?d:d.$locale(),y=i(g[v]),b=i(g[f]),_=y||b.map(function(S){return S.slice(0,h)});if(!m)return _;var w=g.weekStart;return _.map(function(S,k){return _[(k+(w||0))%7]})},s=function(){return r.Ls[r.locale()]},u=function(d,v){return d.formats[v]||function(f){return f.replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(h,m,g){return m||g.slice(1)})}(d.formats[v.toUpperCase()])},c=function(){var d=this;return{months:function(v){return v?v.format("MMMM"):l(d,"months")},monthsShort:function(v){return v?v.format("MMM"):l(d,"monthsShort","months",3)},firstDayOfWeek:function(){return d.$locale().weekStart||0},weekdays:function(v){return v?v.format("dddd"):l(d,"weekdays")},weekdaysMin:function(v){return v?v.format("dd"):l(d,"weekdaysMin","weekdays",2)},weekdaysShort:function(v){return v?v.format("ddd"):l(d,"weekdaysShort","weekdays",3)},longDateFormat:function(v){return u(d.$locale(),v)},meridiem:this.$locale().meridiem,ordinal:this.$locale().ordinal}};o.localeData=function(){return c.bind(this)()},r.localeData=function(){var d=s();return{firstDayOfWeek:function(){return d.weekStart||0},weekdays:function(){return r.weekdays()},weekdaysShort:function(){return r.weekdaysShort()},weekdaysMin:function(){return r.weekdaysMin()},months:function(){return r.months()},monthsShort:function(){return r.monthsShort()},longDateFormat:function(v){return u(d,v)},meridiem:d.meridiem,ordinal:d.ordinal}},r.months=function(){return l(s(),"months")},r.monthsShort=function(){return l(s(),"monthsShort","months",3)},r.weekdays=function(d){return l(s(),"weekdays",null,null,d)},r.weekdaysShort=function(d){return l(s(),"weekdaysShort","weekdays",3,d)},r.weekdaysMin=function(d){return l(s(),"weekdaysMin","weekdays",2,d)}}})})(HL);var Pce=HL.exports;const WL=Kl(Pce),Dce=(e,t)=>{gt.extend(WL);const n=gt.localeData().firstDayOfWeek(),{t:a,lang:r}=Vt(),o=gt().locale(r.value),i=D(()=>!!e.range&&!!e.range.length),l=D(()=>{let v=[];if(i.value){const[f,h]=e.range,m=xs(h.date()-f.date()+1).map(b=>({text:f.date()+b,type:"current"}));let g=m.length%7;g=g===0?0:7-g;const y=xs(g).map((b,_)=>({text:_+1,type:"next"}));v=m.concat(y)}else{const f=e.date.startOf("month").day(),h=Mce(e.date,(f-n+7)%7).map(y=>({text:y,type:"prev"})),m=Ece(e.date).map(y=>({text:y,type:"current"}));v=[...h,...m];const g=xs(7-(v.length%7||7)).map((y,b)=>({text:b+1,type:"next"}));v=v.concat(g)}return Ace(v)}),s=D(()=>{const v=n;return v===0?R2.map(f=>a(`el.datepicker.weeks.${f}`)):R2.slice(v).concat(R2.slice(0,v)).map(f=>a(`el.datepicker.weeks.${f}`))}),u=(v,f)=>{switch(f){case"prev":return e.date.startOf("month").subtract(1,"month").date(v);case"next":return e.date.startOf("month").add(1,"month").date(v);case"current":return e.date.date(v)}};return{now:o,isInRange:i,rows:l,weekDays:s,getFormattedDate:u,handlePickDay:({text:v,type:f})=>{t("pick",u(v,f))},getSlotData:({text:v,type:f})=>{const h=u(v,f);return{isSelected:h.isSame(e.selectedDay),type:`${f}-month`,day:h.format(ad),date:h.toDate()}}}},Oce={key:0},$ce=["onClick"];var Rce=q({name:"DateTable",__name:"date-table",props:Ice,emits:Lce,setup(e,{expose:t,emit:n}){const a=e,{isInRange:r,now:o,rows:i,weekDays:l,getFormattedDate:s,handlePickDay:u,getSlotData:c}=Dce(a,n),d=ke("calendar-table"),v=ke("calendar-day"),f=({text:h,type:m})=>{const g=[m];if(m==="current"){const y=s(h,m);y.isSame(a.selectedDay,"day")&&g.push(v.is("selected")),y.isSame(o,"day")&&g.push(v.is("today"))}return g};return t({getFormattedDate:s}),(h,m)=>(x(),O("table",{class:U([p(d).b(),p(d).is("range",p(r))]),cellspacing:"0",cellpadding:"0"},[e.hideHeader?te("v-if",!0):(x(),O("thead",Oce,[C("tr",null,[(x(!0),O(Ve,null,dt(p(l),g=>(x(),O("th",{key:g,scope:"col"},Q(g),1))),128))])])),C("tbody",null,[(x(!0),O(Ve,null,dt(p(i),(g,y)=>(x(),O("tr",{key:y,class:U({[p(d).e("row")]:!0,[p(d).em("row","hide-border")]:y===0&&e.hideHeader})},[(x(!0),O(Ve,null,dt(g,(b,_)=>(x(),O("td",{key:_,class:U(f(b)),onClick:w=>p(u)(b)},[C("div",{class:U(p(v).b())},[ce(h.$slots,"date-cell",{data:p(c)(b)},()=>[C("span",null,Q(b.text),1)])],2)],10,$ce))),128))],2))),128))])],2))}}),tx=Rce;const Nce=(e,t)=>{const n=e.endOf("month"),a=t.startOf("month"),r=n.isSame(a,"week")?a.add(1,"week"):a;return[[e,n],[r.startOf("week"),t]]},Bce=(e,t)=>{const n=e.endOf("month"),a=e.add(1,"month").startOf("month"),r=n.isSame(a,"week")?a.add(1,"week"):a,o=r.endOf("month"),i=t.startOf("month"),l=o.isSame(i,"week")?i.add(1,"week"):i;return[[e,n],[r.startOf("week"),o],[l.startOf("week"),t]]},Vce=(e,t,n)=>{const{lang:a}=Vt(),r=F(),o=gt().locale(a.value),i=D({get(){return e.modelValue?s.value:r.value},set(y){if(!y)return;r.value=y;const b=y.toDate();t(ua,b),t(bt,b)}}),l=D(()=>{if(!e.range||!Ae(e.range)||e.range.length!==2||e.range.some(_=>!Is(_)))return[];const[y,b]=e.range.map(_=>gt(_).locale(a.value));return y.isAfter(b)?(un(n,"end time should be greater than start time"),[]):y.isSame(b,"month")||y.add(1,"month").isSame(b,"month")?f(y,b):(un(n,"start time and end time interval must not exceed two months"),[])}),s=D(()=>e.modelValue?gt(e.modelValue).locale(a.value):i.value||(l.value.length?l.value[0][0]:o)),u=D(()=>s.value.subtract(1,"month").date(1)),c=D(()=>s.value.add(1,"month").date(1)),d=D(()=>s.value.subtract(1,"year").date(1)),v=D(()=>s.value.add(1,"year").date(1)),f=(y,b)=>{const _=y.startOf("week"),w=b.endOf("week"),S=_.get("month"),k=w.get("month");return S===k?[[_,w]]:(S+1)%12===k?Nce(_,w):S+2===k||(S+1)%11===k?Bce(_,w):(un(n,"start time and end time interval must not exceed two months"),[])},h=y=>{i.value=y},m=y=>{const b={"prev-month":u.value,"next-month":c.value,"prev-year":d.value,"next-year":v.value,today:o}[y];b.isSame(s.value,"day")||h(b)};return{calculateValidatedDateRange:f,date:s,realSelectedDay:i,pickDay:h,selectDate:m,validatedRange:l,handleDateChange:y=>{y==="today"?m("today"):h(y)}}},zce=$e({date:{type:ae(Object),required:!0},formatter:{type:ae(Function)}}),Fce={"date-change":e=>_t(e)||Ye(e)};var Hce=q({name:"SelectController",__name:"select-controller",props:zce,emits:Fce,setup(e,{emit:t}){const n=e,a=t,r=ke("calendar-select"),{t:o,lang:i}=Vt(),l=Array.from({length:12},(h,m)=>{const g=m+1;return{value:g,label:Qe(n.formatter)?n.formatter(g,"month"):g}}),s=D(()=>n.date.year()),u=D(()=>n.date.month()+1),c=D(()=>{const h=[];for(let m=-10;m<10;m++){const g=s.value+m;if(g>0){const y=Qe(n.formatter)?n.formatter(g,"year"):g;h.push({value:g,label:y})}}return h}),d=h=>{a("date-change",gt(new Date(h,u.value-1,1)).locale(i.value))},v=h=>{a("date-change",gt(new Date(s.value,h-1,1)).locale(i.value))},f=()=>{a("date-change","today")};return(h,m)=>(x(),O(Ve,null,[L(p(Bs),{"model-value":s.value,size:"small",class:U(p(r).e("year")),"validate-event":!1,options:c.value,onChange:d},null,8,["model-value","class","options"]),L(p(Bs),{"model-value":u.value,size:"small",class:U(p(r).e("month")),"validate-event":!1,options:p(l),onChange:v},null,8,["model-value","class","options"]),L(p(xa),{size:"small",onClick:f},{default:$(()=>[ue(Q(p(o)("el.datepicker.today")),1)]),_:1})],64))}}),Wce=Hce;const nx="ElCalendar";var Kce=q({name:nx,__name:"calendar",props:xue,emits:kue,setup(e,{expose:t,emit:n}){const a=ke("calendar"),{calculateValidatedDateRange:r,date:o,pickDay:i,realSelectedDay:l,selectDate:s,validatedRange:u,handleDateChange:c}=Vce(e,n,nx),{t:d}=Vt(),v=D(()=>{const f=`el.datepicker.month${o.value.format("M")}`;return`${o.value.year()} ${d("el.datepicker.year")} ${d(f)}`});return t({selectedDay:l,pickDay:i,selectDate:s,calculateValidatedDateRange:r}),(f,h)=>(x(),O("div",{class:U(p(a).b())},[C("div",{class:U(p(a).e("header"))},[ce(f.$slots,"header",{date:v.value},()=>[C("div",{class:U(p(a).e("title"))},Q(v.value),3),p(u).length===0&&e.controllerType==="button"?(x(),O("div",{key:0,class:U(p(a).e("button-group"))},[L(p(yL),null,{default:$(()=>[L(p(xa),{size:"small",onClick:h[0]||(h[0]=m=>p(s)("prev-month"))},{default:$(()=>[ue(Q(p(d)("el.datepicker.prevMonth")),1)]),_:1}),L(p(xa),{size:"small",onClick:h[1]||(h[1]=m=>p(s)("today"))},{default:$(()=>[ue(Q(p(d)("el.datepicker.today")),1)]),_:1}),L(p(xa),{size:"small",onClick:h[2]||(h[2]=m=>p(s)("next-month"))},{default:$(()=>[ue(Q(p(d)("el.datepicker.nextMonth")),1)]),_:1})]),_:1})],2)):p(u).length===0&&e.controllerType==="select"?(x(),O("div",{key:1,class:U(p(a).e("select-controller"))},[L(Wce,{date:p(o),formatter:e.formatter,onDateChange:p(c)},null,8,["date","formatter","onDateChange"])],2)):te("v-if",!0)])],2),p(u).length===0?(x(),O("div",{key:0,class:U(p(a).e("body"))},[L(tx,{date:p(o),"selected-day":p(l),onPick:p(i)},xr({_:2},[f.$slots["date-cell"]?{name:"date-cell",fn:$(m=>[ce(f.$slots,"date-cell",bl(wl(m)))]),key:"0"}:void 0]),1032,["date","selected-day","onPick"])],2)):(x(),O("div",{key:1,class:U(p(a).e("body"))},[(x(!0),O(Ve,null,dt(p(u),(m,g)=>(x(),ne(tx,{key:g,date:m[0],"selected-day":p(l),range:m,"hide-header":g!==0,onPick:p(i)},xr({_:2},[f.$slots["date-cell"]?{name:"date-cell",fn:$(y=>[ce(f.$slots,"date-cell",Tt({ref_for:!0},y))]),key:"0"}:void 0]),1032,["date","selected-day","range","hide-header","onPick"]))),128))],2))],2))}}),Uce=Kce;const Gce=wt(Uce),Yce=$e({header:{type:String,default:""},footer:{type:String,default:""},bodyStyle:{type:ae([String,Object,Array,Boolean]),default:""},headerClass:{type:ae([String,Array,Object,Boolean]),default:void 0},bodyClass:{type:ae([String,Array,Object,Boolean]),default:void 0},footerClass:{type:ae([String,Array,Object,Boolean]),default:void 0},shadow:{type:String,values:["always","hover","never"],default:void 0}});var jce=q({name:"ElCard",__name:"card",props:Yce,setup(e){const t=Hl("card"),n=ke("card");return(a,r)=>{var o;return x(),O("div",{class:U([p(n).b(),p(n).is(`${e.shadow||((o=p(t))==null?void 0:o.shadow)||"always"}-shadow`)])},[a.$slots.header||e.header?(x(),O("div",{key:0,class:U([p(n).e("header"),e.headerClass])},[ce(a.$slots,"header",{},()=>[ue(Q(e.header),1)])],2)):te("v-if",!0),C("div",{class:U([p(n).e("body"),e.bodyClass]),style:et(e.bodyStyle)},[ce(a.$slots,"default")],6),a.$slots.footer||e.footer?(x(),O("div",{key:1,class:U([p(n).e("footer"),e.footerClass])},[ce(a.$slots,"footer",{},()=>[ue(Q(e.footer),1)])],2)):te("v-if",!0)],2)}}}),qce=jce;const Xce=wt(qce),Zce=$e({initialIndex:{type:Number,default:0},height:{type:String,default:""},trigger:{type:String,values:["hover","click"],default:"hover"},autoplay:{type:Boolean,default:!0},interval:{type:Number,default:3e3},indicatorPosition:{type:String,values:["","none","outside"],default:""},arrow:{type:String,values:["always","hover","never"],default:"hover"},type:{type:String,values:["","card"],default:""},cardScale:{type:Number,default:.83},loop:{type:Boolean,default:!0},direction:{type:String,values:["horizontal","vertical"],default:"horizontal"},pauseOnHover:{type:Boolean,default:!0},motionBlur:Boolean}),Jce={change:(e,t)=>[e,t].every(Je)},KL=Symbol("carouselContextKey"),tg="ElCarouselItem",Qce=$e({name:{type:String,default:""},label:{type:[String,Number],default:""}}),ax=300,ede=(e,t,n)=>{const{children:a,addChild:r,removeChild:o,ChildrenSorter:i}=z1(Et(),tg),l=Xn(),s=F(-1),u=F(null),c=F(!1),d=F(),v=F(0),f=F(!0),h=D(()=>e.arrow!=="never"&&!p(y)),m=D(()=>a.value.some(re=>re.props.label.toString().length>0)),g=D(()=>e.type==="card"),y=D(()=>e.direction==="vertical"),b=D(()=>e.height!=="auto"?{height:e.height}:{height:`${v.value}px`,overflow:"hidden"}),_=hs(re=>{E(re)},ax,{trailing:!0}),w=hs(re=>{z(re)},ax),S=re=>f.value?s.value<=1?re<=1:re>1:!0;function k(){u.value&&(clearInterval(u.value),u.value=null)}function T(){e.interval<=0||!e.autoplay||u.value||(u.value=setInterval(()=>M(),e.interval))}const M=()=>{s.valuehe.props.name===re);ve.length>0&&(re=a.value.indexOf(ve[0]))}if(re=Number(re),Number.isNaN(re)||re!==Math.floor(re)){un(n,"index must be integer.");return}const ie=a.value.length,le=s.value;re<0?s.value=e.loop?ie-1:0:re>=ie?s.value=e.loop?0:ie-1:s.value=re,le===s.value&&A(le),N()}function A(re){a.value.forEach((ie,le)=>{ie.translateItem(le,s.value,re)})}function I(re,ie){var ge,Se,De,Te;const le=p(a),ve=le.length;if(ve===0||!re.states.inStage)return!1;const he=ie+1,J=ie-1,Z=ve-1,j=le[Z].states.active,oe=le[0].states.active,se=(Se=(ge=le[he])==null?void 0:ge.states)==null?void 0:Se.active,fe=(Te=(De=le[J])==null?void 0:De.states)==null?void 0:Te.active;return ie===Z&&oe||se?"left":ie===0&&j||fe?"right":!1}function P(){c.value=!0,e.pauseOnHover&&k()}function R(){c.value=!1,T()}function G(re){p(y)||a.value.forEach((ie,le)=>{re===I(ie,le)&&(ie.states.hover=!0)})}function K(){p(y)||a.value.forEach(re=>{re.states.hover=!1})}function H(re){s.value=re}function z(re){e.trigger==="hover"&&re!==s.value&&(s.value=re)}function B(){E(s.value-1)}function W(){E(s.value+1)}function N(){k(),(!e.pauseOnHover||!c.value)&&T()}function V(re){e.height==="auto"&&(v.value=re)}function Y(){var le;const re=(le=l.default)==null?void 0:le.call(l);if(!re)return null;const ie=eo(re).filter(ve=>bn(ve)&&ve.type.name==="ElCarouselItem");return(ie==null?void 0:ie.length)===2&&e.loop&&!g.value?(f.value=!0,ie):(f.value=!1,null)}ye(()=>s.value,(re,ie)=>{A(ie),f.value&&(re=re%2,ie=ie%2),ie>-1&&t(Ot,re,ie)});const X=D({get:()=>f.value?s.value%2:s.value,set:re=>s.value=re});ye(()=>e.autoplay,re=>{re?T():k()}),ye(()=>e.loop,()=>{E(s.value)}),ye(()=>e.interval,()=>{N()});const ee=Ht();return mt(()=>{ye(()=>a.value,()=>{a.value.length>0&&E(e.initialIndex)},{immediate:!0}),ee.value=Mn(d.value,()=>{A()}),T()}),rn(()=>{k(),d.value&&ee.value&&ee.value.stop()}),Ft(KL,{root:d,isCardType:g,isVertical:y,items:a,loop:e.loop,cardScale:e.cardScale,addItem:r,removeItem:o,setActiveItem:E,setContainerHeight:V}),{root:d,activeIndex:s,exposeActiveIndex:X,arrowDisplay:h,hasLabel:m,hover:c,isCardType:g,items:a,isVertical:y,containerStyle:b,isItemsTwoLength:f,handleButtonEnter:G,handleButtonLeave:K,handleIndicatorClick:H,handleMouseEnter:P,handleMouseLeave:R,setActiveItem:E,prev:B,next:W,PlaceholderItem:Y,isTwoLengthShow:S,ItemsSorter:i,throttledArrowClick:_,throttledIndicatorHover:w}},tde=["aria-label"],nde=["aria-label"],ade=["onMouseenter","onClick"],rde=["aria-label"],ode={key:0},ide={key:2,xmlns:"http://www.w3.org/2000/svg",version:"1.1",style:{display:"none"}},rx="ElCarousel";var lde=q({name:rx,__name:"carousel",props:Zce,emits:Jce,setup(e,{expose:t,emit:n}){const a=e,{root:r,activeIndex:o,exposeActiveIndex:i,arrowDisplay:l,hasLabel:s,hover:u,isCardType:c,items:d,isVertical:v,containerStyle:f,handleButtonEnter:h,handleButtonLeave:m,handleIndicatorClick:g,handleMouseEnter:y,handleMouseLeave:b,setActiveItem:_,prev:w,next:S,PlaceholderItem:k,isTwoLengthShow:T,ItemsSorter:M,throttledArrowClick:E,throttledIndicatorHover:A}=ede(a,n,rx),I=ke("carousel"),{t:P}=Vt(),R=D(()=>{const z=[I.b(),I.m(a.direction)];return p(c)&&z.push(I.m("card")),z.push(I.is("vertical-outside",p(v)&&a.indicatorPosition==="outside")),z}),G=D(()=>{const z=[I.e("indicators"),I.em("indicators",a.direction)];return p(s)&&z.push(I.em("indicators","labels")),a.indicatorPosition==="outside"&&z.push(I.em("indicators","outside")),p(v)&&z.push(I.em("indicators","right")),z});function K(z){if(!a.motionBlur)return;const B=p(v)?`${I.namespace.value}-transitioning-vertical`:`${I.namespace.value}-transitioning`;z.currentTarget.classList.add(B)}function H(z){if(!a.motionBlur)return;const B=p(v)?`${I.namespace.value}-transitioning-vertical`:`${I.namespace.value}-transitioning`;z.currentTarget.classList.remove(B)}return t({activeIndex:i,setActiveItem:_,prev:w,next:S}),(z,B)=>(x(),O("div",{ref_key:"root",ref:r,class:U(R.value),onMouseenter:B[6]||(B[6]=at((...W)=>p(y)&&p(y)(...W),["stop"])),onMouseleave:B[7]||(B[7]=at((...W)=>p(b)&&p(b)(...W),["stop"]))},[p(l)?(x(),ne(Pa,{key:0,name:"carousel-arrow-left",persisted:""},{default:$(()=>[st(C("button",{type:"button",class:U([p(I).e("arrow"),p(I).em("arrow","left")]),"aria-label":p(P)("el.carousel.leftArrow"),onMouseenter:B[0]||(B[0]=W=>p(h)("left")),onMouseleave:B[1]||(B[1]=(...W)=>p(m)&&p(m)(...W)),onClick:B[2]||(B[2]=at(W=>p(E)(p(o)-1),["stop"]))},[L(p(Xe),null,{default:$(()=>[L(p(Ji))]),_:1})],42,tde),[[Zt,(e.arrow==="always"||p(u))&&(e.loop||p(o)>0)]])]),_:1})):te("v-if",!0),p(l)?(x(),ne(Pa,{key:1,name:"carousel-arrow-right",persisted:""},{default:$(()=>[st(C("button",{type:"button",class:U([p(I).e("arrow"),p(I).em("arrow","right")]),"aria-label":p(P)("el.carousel.rightArrow"),onMouseenter:B[3]||(B[3]=W=>p(h)("right")),onMouseleave:B[4]||(B[4]=(...W)=>p(m)&&p(m)(...W)),onClick:B[5]||(B[5]=at(W=>p(E)(p(o)+1),["stop"]))},[L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})],42,nde),[[Zt,(e.arrow==="always"||p(u))&&(e.loop||p(o)[e.indicatorPosition!=="none"?(x(),O("ul",{key:0,class:U(G.value)},[(x(!0),O(Ve,null,dt(p(d),(W,N)=>st((x(),O("li",{key:N,class:U([p(I).e("indicator"),p(I).em("indicator",e.direction),p(I).is("active",N===p(o))]),onMouseenter:V=>p(A)(N),onClick:at(V=>p(g)(N),["stop"])},[C("button",{class:U(p(I).e("button")),"aria-label":p(P)("el.carousel.indicator",{index:N+1})},[p(s)?(x(),O("span",ode,Q(W.props.label),1)):te("v-if",!0)],10,rde)],42,ade)),[[Zt,p(T)(N)]])),128))],2)):te("v-if",!0)]),_:1}),e.motionBlur?(x(),O("svg",ide,[...B[8]||(B[8]=[C("defs",null,[C("filter",{id:"elCarouselHorizontal"},[C("feGaussianBlur",{in:"SourceGraphic",stdDeviation:"12,0"})]),C("filter",{id:"elCarouselVertical"},[C("feGaussianBlur",{in:"SourceGraphic",stdDeviation:"0,10"})])],-1)])])):te("v-if",!0)],34))}}),sde=lde;const ude=e=>{const t=Ze(KL),n=Et();t||un(tg,"usage: "),n||un(tg,"compositional hook can only be invoked inside setups");const a=F(),r=F(!1),o=F(0),i=F(1),l=F(!1),s=F(!1),u=F(!1),c=F(!1),{isCardType:d,isVertical:v,cardScale:f}=t;function h(w,S,k){const T=k-1,M=S-1,E=S+1,A=k/2;return S===0&&w===T?-1:S===T&&w===0?k:w=A?k+1:w>E&&w-S>=A?-2:w}function m(w,S){var T,M;const k=p(v)?((T=t.root.value)==null?void 0:T.offsetHeight)||0:((M=t.root.value)==null?void 0:M.offsetWidth)||0;return u.value?k*((2-f)*(w-S)+1)/4:w{const T=p(d),M=t.items.value.length??NaN,E=w===S;!T&&!zt(k)&&(c.value=E||w===k),!E&&M>2&&t.loop&&(w=h(w,S,M));const A=p(v);l.value=E,T?(u.value=Math.round(Math.abs(w-S))<=1,o.value=m(w,S),i.value=p(l)?1:f):o.value=g(w,S,A),s.value=!0,E&&a.value&&t.setContainerHeight(a.value.offsetHeight)};function b(){if(t&&p(d)){const w=t.items.value.findIndex(({uid:S})=>S===n.uid);t.setActiveItem(w)}}const _={props:e,states:nn({hover:r,translate:o,scale:i,active:l,ready:s,inStage:u,animating:c}),uid:n.uid,getVnode:()=>n.vnode,translateItem:y};return t.addItem(_),rn(()=>{t.removeItem(_)}),{carouselItemRef:a,active:l,animating:c,hover:r,inStage:u,isVertical:v,translate:o,isCardType:d,scale:i,ready:s,handleItemClick:b}};var cde=q({name:tg,__name:"carousel-item",props:Qce,setup(e){const t=e,n=ke("carousel"),{carouselItemRef:a,active:r,animating:o,hover:i,inStage:l,isVertical:s,translate:u,isCardType:c,scale:d,ready:v,handleItemClick:f}=ude(t),h=D(()=>[n.e("item"),n.is("active",r.value),n.is("in-stage",l.value),n.is("hover",i.value),n.is("animating",o.value),{[n.em("item","card")]:c.value,[n.em("item","card-vertical")]:c.value&&s.value}]),m=D(()=>({transform:[`${`translate${p(s)?"Y":"X"}`}(${p(u)}px)`,`scale(${p(d)})`].join(" ")}));return(g,y)=>st((x(),O("div",{ref_key:"carouselItemRef",ref:a,class:U(h.value),style:et(m.value),onClick:y[0]||(y[0]=(...b)=>p(f)&&p(f)(...b))},[p(c)?st((x(),O("div",{key:0,class:U(p(n).e("mask"))},null,2)),[[Zt,!p(r)]]):te("v-if",!0),ce(g.$slots,"default")],6)),[[Zt,p(v)]])}}),UL=cde;const dde=wt(sde,{CarouselItem:UL}),fde=On(UL),GL=$e({modelValue:{type:ae([Number,String,Array,Object])},options:{type:ae(Array),default:()=>[]},props:{type:ae(Object),default:()=>({})},virtualScroll:Boolean,itemSize:{type:Number,default:34},height:{type:Number,default:204}}),vde={expandTrigger:"click",multiple:!1,checkStrictly:!1,emitPath:!0,lazy:!1,lazyLoad:kt,value:"value",label:"label",children:"children",leaf:"leaf",disabled:"disabled",hoverThreshold:500,checkOnClickNode:!1,checkOnClickLeaf:!0,showPrefix:!0},hde=$e({...GL,border:{type:Boolean,default:!0},renderLabel:{type:Function}}),ox=e=>!0,pde={[bt]:ox,[Ot]:ox,close:()=>!0,"expand-change":e=>e},mde=e=>D(()=>({...vde,...e.props})),ng="itemRendered",ag="scroll",ny="end-reached",rg="forward",gde="backward",Io="auto",mh="start",Pi="center",yde="horizontal",YL="vertical",gh="negative",lw="positive-ascending",sw="positive-descending",bde={[yde]:"left",[YL]:"top"},V4=si({type:ae([Number,Function]),required:!0}),z4=si({type:Number}),F4=si({type:Number,default:2}),_de=si({type:String,values:["ltr","rtl"],default:"ltr"}),H4=si({type:Number,default:0}),og=si({type:Number,required:!0}),jL=si({type:String,values:["horizontal","vertical"],default:YL}),qL=$e({className:{type:ae([String,Array,Object,Boolean]),default:""},containerElement:{type:ae([String,Object]),default:"div"},data:{type:ae(Array),default:()=>Nn([])},direction:_de,height:{type:[String,Number],required:!0},innerElement:{type:[String,Object],default:"div"},innerProps:{type:ae(Object),default:()=>({})},style:{type:ae([Object,String,Array,Boolean]),default:void 0},useIsScrolling:Boolean,width:{type:[Number,String],required:!1},innerWidth:{type:[Number,String],required:!1},perfMode:{type:Boolean,default:!0},scrollbarAlwaysOn:Boolean}),XL=$e({cache:F4,estimatedItemSize:z4,layout:jL,initScrollOffset:H4,total:og,itemSize:V4,...qL}),W4={type:Number,default:6},ZL={type:Number,default:0},JL={type:Number,default:2},Xu=$e({columnCache:F4,columnWidth:V4,estimatedColumnWidth:z4,estimatedRowHeight:z4,initScrollLeft:H4,initScrollTop:H4,itemKey:{type:ae(Function),default:({columnIndex:e,rowIndex:t})=>`${t}:${e}`},rowCache:F4,rowHeight:V4,totalColumn:og,totalRow:og,hScrollbarSize:W4,vScrollbarSize:W4,scrollbarStartGap:ZL,scrollbarEndGap:JL,role:String,...qL}),QL=$e({alwaysOn:Boolean,class:{type:ae([String,Array,Object,Boolean]),default:""},layout:jL,total:og,ratio:{type:Number,required:!0},clientSize:{type:Number,required:!0},scrollFrom:{type:Number,required:!0},scrollbarSize:W4,startGap:ZL,endGap:JL,visible:Boolean}),lv=(e,t)=>ee==="ltr"||e==="rtl"||e==="horizontal",ix=e=>e==="rtl";let Rc=null;function ig(e=!1){if(Rc===null||e){const t=document.createElement("div"),n=t.style;n.width="50px",n.height="50px",n.overflow="scroll",n.direction="rtl";const a=document.createElement("div"),r=a.style;return r.width="100px",r.height="100px",t.appendChild(a),document.body.appendChild(t),t.scrollLeft>0?Rc=sw:(t.scrollLeft=1,t.scrollLeft===0?Rc=gh:Rc=lw),document.body.removeChild(t),Rc}return Rc}function wde({move:e,size:t,bar:n},a){const r={},o=`translate${n.axis}(${e}px)`;return r[n.size]=t,r.transform=o,a==="horizontal"?r.height="100%":r.width="100%",r}var lx=Number.isNaN||function(t){return typeof t=="number"&&t!==t};function Sde(e,t){return!!(e===t||lx(e)&&lx(t))}function Cde(e,t){if(e.length!==t.length)return!1;for(var n=0;n{const e=Et().proxy.$props;return D(()=>{const t=(n,a,r)=>({});return e.perfMode?k1(t):xde(t)})},tP=({atEndEdge:e,atStartEdge:t,layout:n},a)=>{let r,o=0;const i=s=>s<0&&t.value||s>0&&e.value;return{hasReachedEdge:i,onWheel:s=>{ti(r);let{deltaX:u,deltaY:c}=s;s.shiftKey&&c!==0&&(u=c,c=0);const d=n.value==="horizontal"?u:c;i(d)||(o+=d,!P1()&&d!==0&&s.preventDefault(),r=Wr(()=>{a(o),o=0}))}}},K4=q({name:"ElVirtualScrollBar",props:QL,emits:["scroll","start-move","stop-move"],setup(e,{emit:t}){const n=D(()=>e.startGap+e.endGap),a=ke("virtual-scrollbar"),r=ke("scrollbar"),o=F(),i=F();let l=null,s=null;const u=nn({isDragging:!1,traveled:0}),c=D(()=>lL[e.layout]),d=D(()=>e.clientSize-p(n)),v=D(()=>({position:"absolute",width:`${e.layout==="horizontal"?d.value:e.scrollbarSize}px`,height:`${e.layout==="horizontal"?e.scrollbarSize:d.value}px`,[bde[e.layout]]:"2px",right:"2px",bottom:"2px",borderRadius:"4px"})),f=D(()=>{const k=e.ratio;if(k>=100)return Number.POSITIVE_INFINITY;if(k>=50)return k*d.value/100;const T=d.value/3;return Math.floor(Math.min(Math.max(k*d.value/100,20),T))}),h=D(()=>{if(!Number.isFinite(f.value))return{display:"none"};const k=`${f.value}px`;return wde({bar:c.value,size:k,move:u.traveled},e.layout)}),m=D(()=>Math.ceil(e.clientSize-f.value-p(n))),g=()=>{window.addEventListener("mousemove",w),window.addEventListener("mouseup",_);const k=p(i);k&&(s=document.onselectstart,document.onselectstart=()=>!1,k.addEventListener("touchmove",w,{passive:!0}),k.addEventListener("touchend",_))},y=()=>{window.removeEventListener("mousemove",w),window.removeEventListener("mouseup",_),document.onselectstart=s,s=null;const k=p(i);k&&(k.removeEventListener("touchmove",w),k.removeEventListener("touchend",_))},b=k=>{k.stopImmediatePropagation(),!(k.ctrlKey||[1,2].includes(k.button))&&(u.isDragging=!0,u[c.value.axis]=k.currentTarget[c.value.offset]-(k[c.value.client]-k.currentTarget.getBoundingClientRect()[c.value.direction]),t("start-move"),g())},_=()=>{u.isDragging=!1,u[c.value.axis]=0,t("stop-move"),y()},w=k=>{const{isDragging:T}=u;if(!T||!i.value||!o.value)return;const M=u[c.value.axis];if(!M)return;ti(l);const E=(o.value.getBoundingClientRect()[c.value.direction]-k[c.value.client])*-1-(i.value[c.value.offset]-M);l=Wr(()=>{u.traveled=Math.max(0,Math.min(E,m.value)),t("scroll",E,m.value)})},S=k=>{const T=Math.abs(k.target.getBoundingClientRect()[c.value.direction]-k[c.value.client])-i.value[c.value.offset]/2;u.traveled=Math.max(0,Math.min(T,m.value)),t("scroll",T,m.value)};return ye(()=>e.scrollFrom,k=>{u.isDragging||(u.traveled=Math.ceil(k*m.value))}),rn(()=>{y()}),()=>ot("div",{role:"presentation",ref:o,class:[a.b(),e.class,(e.alwaysOn||u.isDragging)&&"always-on"],style:v.value,onMousedown:at(S,["stop","prevent"]),onTouchstartPrevent:b},ot("div",{ref:i,class:r.e("thumb"),style:h.value,onMousedown:b},[]))}}),nP=({name:e,getOffset:t,getItemSize:n,getItemOffset:a,getEstimatedTotalSize:r,getStartIndexForOffset:o,getStopIndexForStartIndex:i,initCache:l,clearCache:s,validateProps:u})=>q({name:e??"ElVirtualList",props:XL,emits:[ng,ag,ny],setup(c,{emit:d,expose:v}){u(c);const f=Et(),h=ke("vl"),m=F(l(c,f)),g=eP(),y=F(),b=F(),_=F(),w=F({isScrolling:!1,scrollDir:rg,scrollOffset:Je(c.initScrollOffset)?c.initScrollOffset:0,updateRequested:!1,isScrollbarDragging:!1}),S=D(()=>{const{total:j,cache:oe}=c,{isScrolling:se,scrollDir:fe,scrollOffset:ge}=p(w);if(j===0)return[0,0,0,0];const Se=o(c,ge,p(m)),De=i(c,Se,ge,p(m)),Te=!se||fe==="backward"?Math.max(1,oe):1,Re=!se||fe==="forward"?Math.max(1,oe):1;return[Math.max(0,Se-Te),Math.max(0,Math.min(j-1,De+Re)),Se,De]}),k=D(()=>r(c,p(m))),T=D(()=>yh(c.layout)),M=D(()=>[{position:"relative",[`overflow-${T.value?"x":"y"}`]:"scroll",WebkitOverflowScrolling:"touch",willChange:"transform"},{direction:c.direction,height:Je(c.height)?`${c.height}px`:c.height,width:Je(c.width)?`${c.width}px`:c.width},c.style]),E=D(()=>{const j=p(k),oe=p(T),se=c.innerWidth;return{height:oe?"100%":`${j}px`,pointerEvents:p(w).isScrolling?"none":void 0,width:oe?`${j}px`:se!==void 0?Je(se)?`${se}px`:se:"100%",margin:0,boxSizing:"border-box"}}),A=D(()=>T.value?c.width:c.height),I=D(()=>Math.max(0,k.value-A.value)),P=j=>I1(j,0,I.value),R=1,G=j=>({start:!Vi(j,0,R),end:!Vi(I.value,j,R)}),K=D(()=>P(w.value.scrollOffset)),H=D(()=>G(K.value));let z=H.value;const{onWheel:B}=tP({atStartEdge:D(()=>H.value.start),atEndEdge:D(()=>H.value.end),layout:D(()=>c.layout)},j=>{var oe,se;(se=(oe=_.value).onMouseUp)==null||se.call(oe),ie(Math.min(w.value.scrollOffset+j,I.value))});on(y,"wheel",B,{passive:!1});const W=()=>{const{total:j}=c;if(j>0){const[ge,Se,De,Te]=p(S);d(ng,ge,Se,De,Te)}const{scrollDir:oe,scrollOffset:se,updateRequested:fe}=p(w);d(ag,oe,se,fe)},N=(j,oe)=>{const se=G(oe),fe=c.direction==="rtl"?"left":"right",ge=c.direction==="rtl"?"right":"left";j==="forward"&&se.end&&!z.end&&d(ny,T.value?fe:"bottom"),j==="backward"&&se.start&&!z.start&&d(ny,T.value?ge:"top"),z=se},V=(j,{isScrolling:oe,updateRequested:se})=>{const fe=p(w),ge=Math.max(j,0);if(ge===fe.scrollOffset)return;const Se=lv(fe.scrollOffset,ge);w.value={...fe,isScrolling:oe,scrollDir:Se,scrollOffset:ge,updateRequested:se},N(Se,P(ge)),He(he)},Y=j=>{const{clientHeight:oe,scrollHeight:se,scrollTop:fe}=j.currentTarget;p(w).scrollOffset!==fe&&V(Math.min(fe,se-oe),{isScrolling:!0,updateRequested:!1})},X=j=>{const{clientWidth:oe,scrollLeft:se,scrollWidth:fe}=j.currentTarget;if(p(w).scrollOffset===se)return;const{direction:ge}=c;let Se=se;if(ge==="rtl")switch(ig()){case gh:Se=-se;break;case sw:Se=fe-oe-se;break}V(Math.min(Se,fe-oe),{isScrolling:!0,updateRequested:!1})},ee=j=>{p(T)?X(j):Y(j),W()},re=(j,oe)=>{const se=I.value/oe*j;ie(Math.min(I.value,se))},ie=j=>{V(j,{isScrolling:p(w).isScrolling,updateRequested:!0})},le=(j,oe=Io)=>{const{scrollOffset:se}=p(w);j=Math.max(0,Math.min(j,c.total-1)),ie(t(c,j,oe,se,p(m)))},ve=j=>{const{direction:oe,itemSize:se,layout:fe}=c,ge=g.value(s&&se,s&&fe,s&&oe);let Se;if(en(ge,String(j)))Se=ge[j];else{const De=a(c,j,p(m)),Te=n(c,j,p(m)),Re=p(T),Ce=oe==="rtl",Le=Re?De:0;ge[j]=Se={position:"absolute",left:Ce?void 0:`${Le}px`,right:Ce?`${Le}px`:void 0,top:Re?0:`${De}px`,height:Re?"100%":`${Te}px`,width:Re?`${Te}px`:"100%"}}return Se},he=()=>{w.value.isScrolling=!1,He(()=>{g.value(-1,null,null)})},J=()=>{const j=y.value;j&&(j.scrollTop=0)};mt(()=>{if(!qt)return;const{initScrollOffset:j}=c,oe=p(y);Je(j)&&oe&&(p(T)?oe.scrollLeft=j:oe.scrollTop=j),W()}),oi(()=>{const{direction:j,layout:oe}=c,{scrollOffset:se,updateRequested:fe}=p(w),ge=p(y);if(fe&&ge)if(oe==="horizontal")if(j==="rtl")switch(ig()){case gh:ge.scrollLeft=-se;break;case lw:ge.scrollLeft=se;break;default:{const{clientWidth:Se,scrollWidth:De}=ge;ge.scrollLeft=De-Se-se;break}}else ge.scrollLeft=se;else ge.scrollTop=se}),Wh(()=>{p(y).scrollTop=p(w).scrollOffset}),ye(I,()=>{z=H.value});const Z={ns:h,clientSize:A,estimatedTotalSize:k,windowStyle:M,windowRef:y,innerRef:b,innerStyle:E,itemsToRender:S,scrollbarRef:_,states:w,getItemStyle:ve,onScroll:ee,onScrollbarScroll:re,onWheel:B,scrollTo:ie,scrollToItem:le,resetScrollTop:J};return v({windowRef:y,innerRef:b,getItemStyleCache:g,scrollTo:ie,scrollToItem:le,resetScrollTop:J,states:w}),Z},render(c){var V;const{$slots:d,className:v,clientSize:f,containerElement:h,data:m,getItemStyle:g,innerElement:y,itemsToRender:b,innerStyle:_,layout:w,scrollbarAlwaysOn:S,total:k,onScroll:T,onScrollbarScroll:M,states:E,useIsScrolling:A,windowStyle:I,ns:P}=c,[R,G]=b,K=St(h),H=St(y),z=[];if(k>0)for(let Y=R;Y<=G;Y++)z.push(ot(Ve,{key:Y},(V=d.default)==null?void 0:V.call(d,{data:m,index:Y,isScrolling:A?E.isScrolling:void 0,style:g(Y)})));const B=[ot(H,Tt(c.innerProps,{style:_,ref:"innerRef"}),Ye(H)?z:{default:()=>z})],W=ot(K4,{ref:"scrollbarRef",clientSize:f,layout:w,onScroll:M,ratio:f*100/this.estimatedTotalSize,scrollFrom:E.scrollOffset/(this.estimatedTotalSize-f),total:k,alwaysOn:S}),N=ot(K,{class:[P.e("window"),v],style:I,onScroll:T,ref:"windowRef",key:0},Ye(K)?[B]:{default:()=>[B]});return ot("div",{key:0,class:[P.e("wrapper"),S?"always-on":""]},[N,W])}}),cp=nP({name:"ElFixedSizeList",getItemOffset:({itemSize:e},t)=>t*e,getItemSize:({itemSize:e})=>e,getEstimatedTotalSize:({total:e,itemSize:t})=>t*e,getOffset:({height:e,total:t,itemSize:n,layout:a,width:r},o,i,l)=>{const s=yh(a)?r:e;Ye(s)&&Dn("[ElVirtualList]",` + You should set + width/height + to number when your layout is + horizontal/vertical + `);const u=Math.max(0,t*n-s),c=Math.min(u,o*n),d=Math.max(0,(o+1)*n-s);switch(i==="smart"&&(l>=d-s&&l<=c+s?i=Io:i=Pi),i){case mh:return c;case"end":return d;case Pi:{const v=Math.round(d+(c-d)/2);return vu+Math.floor(s/2)?u:v}case Io:default:return l>=d&&l<=c?l:lMath.max(0,Math.min(e-1,Math.floor(n/t))),getStopIndexForStartIndex:({height:e,total:t,itemSize:n,layout:a,width:r},o,i)=>{const l=o*n,s=yh(a)?r:e,u=Math.ceil((s+i-l)/n);return Math.max(0,Math.min(t-1,o+u-1))},initCache(){},clearCache:!0,validateProps(){}}),kde="ElDynamicSizeList",id=(e,t,n)=>{const{itemSize:a}=e,{items:r,lastVisitedIndex:o}=n;if(t>o){let i=0;if(o>=0){const l=r[o];i=l.offset+l.size}for(let l=o+1;l<=t;l++){const s=a(l);r[l]={offset:i,size:s},i+=s}n.lastVisitedIndex=t}return r[t]},Tde=(e,t,n)=>{const{items:a,lastVisitedIndex:r}=t;return(r>0?a[r].offset:0)>=n?aP(e,t,0,r,n):Mde(e,t,Math.max(0,r),n)},aP=(e,t,n,a,r)=>{for(;n<=a;){const o=n+Math.floor((a-n)/2),i=id(e,o,t).offset;if(i===r)return o;ir&&(a=o-1)}return Math.max(0,n-1)},Mde=(e,t,n,a)=>{const{total:r}=e;let o=1;for(;n{let r=0;if(a>=e&&(a=e-1),a>=0){const i=t[a];r=i.offset+i.size}const o=(e-a-1)*n;return r+o},Ede=nP({name:"ElDynamicSizeList",getItemOffset:(e,t,n)=>id(e,t,n).offset,getItemSize:(e,t,{items:n})=>n[t].size,getEstimatedTotalSize:sx,getOffset:(e,t,n,a,r)=>{const{height:o,layout:i,width:l}=e,s=yh(i)?l:o,u=id(e,t,r),c=sx(e,r),d=Math.max(0,Math.min(c-s,u.offset)),v=Math.max(0,u.offset-s+u.size);switch(n==="smart"&&(a>=v-s&&a<=d+s?n=Io:n=Pi),n){case mh:return d;case"end":return v;case Pi:return Math.round(v+(d-v)/2);case Io:default:return a>=v&&a<=d?a:aTde(e,n,t),getStopIndexForStartIndex:(e,t,n,a)=>{const{height:r,total:o,layout:i,width:l}=e,s=yh(i)?l:r,u=id(e,t,a),c=n+s;let d=u.offset+u.size,v=t;for(;v{var o,i;n.lastVisitedIndex=Math.min(n.lastVisitedIndex,a-1),(o=t.exposed)==null||o.getItemStyleCache(-1),r&&((i=t.proxy)==null||i.$forceUpdate())},n},clearCache:!1,validateProps:({itemSize:e})=>{typeof e!="function"&&Dn(kde,` + itemSize is required as function, but the given value was ${typeof e} + `)}}),Ade=({atXEndEdge:e,atXStartEdge:t,atYEndEdge:n,atYStartEdge:a},r)=>{let o=null,i=0,l=0;const s=(c,d)=>{const v=c<0&&t.value||c>0&&e.value,f=d<0&&a.value||d>0&&n.value;return v||f};return{hasReachedEdge:s,onWheel:c=>{ti(o);let d=c.deltaX,v=c.deltaY;if(Math.abs(d)>Math.abs(v)?v=0:d=0,c.shiftKey&&v!==0&&(d=v,v=0),s(d,v)){c.deltaX!==0&&d===0&&c.preventDefault();return}i+=d,l+=v,c.preventDefault(),o=Wr(()=>{r(i,l),i=0,l=0})}}},Ide=(e,t,n,a,r,o,i)=>{const l=F(0),s=F(0);let u,c=0,d=0;const v=h=>{ti(u),l.value=h.touches[0].clientX,s.value=h.touches[0].clientY,c=0,d=0},f=h=>{h.preventDefault(),ti(u),c+=l.value-h.touches[0].clientX,d+=s.value-h.touches[0].clientY,l.value=h.touches[0].clientX,s.value=h.touches[0].clientY,u=Wr(()=>{const m=a.value-p(o),g=r.value-p(i);n({scrollLeft:Math.min(t.value.scrollLeft+c,m),scrollTop:Math.min(t.value.scrollTop+d,g)}),c=0,d=0})};return on(e,"touchstart",v,{passive:!0}),on(e,"touchmove",f,{passive:!1}),{touchStartX:l,touchStartY:s,handleTouchStart:v,handleTouchMove:f}},rP=({name:e,clearCache:t,getColumnPosition:n,getColumnStartIndexForOffset:a,getColumnStopIndexForStartIndex:r,getEstimatedTotalHeight:o,getEstimatedTotalWidth:i,getColumnOffset:l,getRowOffset:s,getRowPosition:u,getRowStartIndexForOffset:c,getRowStopIndexForStartIndex:d,initCache:v,injectToInstance:f,validateProps:h})=>q({name:e??"ElVirtualList",props:Xu,emits:[ng,ag],setup(m,{emit:g,expose:y,slots:b}){const _=ke("vl");h(m);const w=Et(),S=F(v(m,w));f==null||f(w,S);const k=F(),T=F(),M=F(),E=F(),A=F({isScrolling:!1,scrollLeft:Je(m.initScrollLeft)?m.initScrollLeft:0,scrollTop:Je(m.initScrollTop)?m.initScrollTop:0,updateRequested:!1,xAxisScrollDir:rg,yAxisScrollDir:rg}),I=eP(),P=D(()=>Number.parseInt(`${m.height}`,10)),R=D(()=>Number.parseInt(`${m.width}`,10)),G=D(()=>{const{totalColumn:Ce,totalRow:Le,columnCache:me}=m,{isScrolling:Be,xAxisScrollDir:ze,scrollLeft:xe}=p(A);if(Ce===0||Le===0)return[0,0,0,0];const rt=a(m,xe,p(S)),nt=r(m,rt,xe,p(S)),Pt=!Be||ze==="backward"?Math.max(1,me):1,Wt=!Be||ze==="forward"?Math.max(1,me):1;return[Math.max(0,rt-Pt),Math.max(0,Math.min(Ce-1,nt+Wt)),rt,nt]}),K=D(()=>{const{totalColumn:Ce,totalRow:Le,rowCache:me}=m,{isScrolling:Be,yAxisScrollDir:ze,scrollTop:xe}=p(A);if(Ce===0||Le===0)return[0,0,0,0];const rt=c(m,xe,p(S)),nt=d(m,rt,xe,p(S)),Pt=!Be||ze==="backward"?Math.max(1,me):1,Wt=!Be||ze==="forward"?Math.max(1,me):1;return[Math.max(0,rt-Pt),Math.max(0,Math.min(Le-1,nt+Wt)),rt,nt]}),H=D(()=>o(m,p(S))),z=D(()=>i(m,p(S))),B=D(()=>[{position:"relative",overflow:"hidden",WebkitOverflowScrolling:"touch",willChange:"transform"},{direction:m.direction,height:Je(m.height)?`${m.height}px`:m.height,width:Je(m.width)?`${m.width}px`:m.width},m.style??{}]),W=D(()=>{const Ce=`${p(z)}px`;return{height:`${p(H)}px`,pointerEvents:p(A).isScrolling?"none":void 0,width:Ce,margin:0,boxSizing:"border-box"}}),N=()=>{const{totalColumn:Ce,totalRow:Le}=m;if(Ce>0&&Le>0){const[nt,Pt,Wt,Jt]=p(G),[Rt,Qt,_e,Ne]=p(K);g(ng,{columnCacheStart:nt,columnCacheEnd:Pt,rowCacheStart:Rt,rowCacheEnd:Qt,columnVisibleStart:Wt,columnVisibleEnd:Jt,rowVisibleStart:_e,rowVisibleEnd:Ne})}const{scrollLeft:me,scrollTop:Be,updateRequested:ze,xAxisScrollDir:xe,yAxisScrollDir:rt}=p(A);g(ag,{xAxisScrollDir:xe,scrollLeft:me,yAxisScrollDir:rt,scrollTop:Be,updateRequested:ze})},V=Ce=>{const{clientHeight:Le,clientWidth:me,scrollHeight:Be,scrollLeft:ze,scrollTop:xe,scrollWidth:rt}=Ce.currentTarget,nt=p(A);if(nt.scrollTop===xe&&nt.scrollLeft===ze)return;let Pt=ze;if(ix(m.direction))switch(ig()){case gh:Pt=-ze;break;case sw:Pt=rt-me-ze;break}A.value={...nt,isScrolling:!0,scrollLeft:Pt,scrollTop:Math.max(0,Math.min(xe,Be-Le)),updateRequested:!0,xAxisScrollDir:lv(nt.scrollLeft,Pt),yAxisScrollDir:lv(nt.scrollTop,xe)},He(()=>j()),oe(),N()},Y=(Ce,Le)=>{const me=p(P),Be=(H.value-me)/Le*Ce;re({scrollTop:Math.min(H.value-me,Be)})},X=(Ce,Le)=>{const me=p(R),Be=(z.value-me)/Le*Ce;re({scrollLeft:Math.min(z.value-me,Be)})},{onWheel:ee}=Ade({atXStartEdge:D(()=>A.value.scrollLeft<=0),atXEndEdge:D(()=>A.value.scrollLeft>=z.value-p(R)),atYStartEdge:D(()=>A.value.scrollTop<=0),atYEndEdge:D(()=>A.value.scrollTop>=H.value-p(P))},(Ce,Le)=>{var ze,xe,rt,nt;(xe=(ze=T.value)==null?void 0:ze.onMouseUp)==null||xe.call(ze),(nt=(rt=M.value)==null?void 0:rt.onMouseUp)==null||nt.call(rt);const me=p(R),Be=p(P);re({scrollLeft:Math.min(A.value.scrollLeft+Ce,z.value-me),scrollTop:Math.min(A.value.scrollTop+Le,H.value-Be)})});on(k,"wheel",ee,{passive:!1});const re=({scrollLeft:Ce=A.value.scrollLeft,scrollTop:Le=A.value.scrollTop})=>{Ce=Math.max(Ce,0),Le=Math.max(Le,0);const me=p(A);Le===me.scrollTop&&Ce===me.scrollLeft||(A.value={...me,xAxisScrollDir:lv(me.scrollLeft,Ce),yAxisScrollDir:lv(me.scrollTop,Le),scrollLeft:Ce,scrollTop:Le,updateRequested:!0},He(()=>j()),oe(),N())},{touchStartX:ie,touchStartY:le,handleTouchStart:ve,handleTouchMove:he}=Ide(k,A,re,z,H,R,P),J=(Ce=0,Le=0,me=Io)=>{const Be=p(A);Le=Math.max(0,Math.min(Le,m.totalColumn-1)),Ce=Math.max(0,Math.min(Ce,m.totalRow-1));const ze=FA(_.namespace.value),xe=p(S),rt=o(m,xe),nt=i(m,xe);re({scrollLeft:l(m,Le,me,Be.scrollLeft,xe,nt>m.width?ze:0),scrollTop:s(m,Ce,me,Be.scrollTop,xe,rt>m.height?ze:0)})},Z=(Ce,Le)=>{const{columnWidth:me,direction:Be,rowHeight:ze}=m,xe=I.value(t&&me,t&&ze,t&&Be),rt=`${Ce},${Le}`;if(en(xe,rt))return xe[rt];{const[,nt]=n(m,Le,p(S)),Pt=p(S),Wt=ix(Be),[Jt,Rt]=u(m,Ce,Pt),[Qt]=n(m,Le,Pt);return xe[rt]={position:"absolute",left:Wt?void 0:`${nt}px`,right:Wt?`${nt}px`:void 0,top:`${Rt}px`,height:`${Jt}px`,width:`${Qt}px`},xe[rt]}},j=()=>{A.value.isScrolling=!1,He(()=>{I.value(-1,null,null)})};mt(()=>{if(!qt)return;const{initScrollLeft:Ce,initScrollTop:Le}=m,me=p(k);me&&(Je(Ce)&&(me.scrollLeft=Ce),Je(Le)&&(me.scrollTop=Le)),N()});const oe=()=>{const{direction:Ce}=m,{scrollLeft:Le,scrollTop:me,updateRequested:Be}=p(A),ze=p(k);if(Be&&ze){if(Ce==="rtl")switch(ig()){case gh:ze.scrollLeft=-Le;break;case lw:ze.scrollLeft=Le;break;default:{const{clientWidth:xe,scrollWidth:rt}=ze;ze.scrollLeft=rt-xe-Le;break}}else ze.scrollLeft=Math.max(0,Le);ze.scrollTop=Math.max(0,me)}},{resetAfterColumnIndex:se,resetAfterRowIndex:fe,resetAfter:ge}=w.proxy;y({windowRef:k,innerRef:E,getItemStyleCache:I,touchStartX:ie,touchStartY:le,handleTouchStart:ve,handleTouchMove:he,scrollTo:re,scrollToItem:J,states:A,resetAfterColumnIndex:se,resetAfterRowIndex:fe,resetAfter:ge});const Se=()=>{const{scrollbarAlwaysOn:Ce,scrollbarStartGap:Le,scrollbarEndGap:me,totalColumn:Be,totalRow:ze}=m,xe=p(R),rt=p(P),nt=p(z),Pt=p(H),{scrollLeft:Wt,scrollTop:Jt}=p(A);return{horizontalScrollbar:ot(K4,{ref:T,alwaysOn:Ce,startGap:Le,endGap:me,class:_.e("horizontal"),clientSize:xe,layout:"horizontal",onScroll:X,ratio:xe*100/nt,scrollFrom:Wt/(nt-xe),total:ze,visible:!0}),verticalScrollbar:ot(K4,{ref:M,alwaysOn:Ce,startGap:Le,endGap:me,class:_.e("vertical"),clientSize:rt,layout:"vertical",onScroll:Y,ratio:rt*100/Pt,scrollFrom:Jt/(Pt-rt),total:Be,visible:!0})}},De=()=>{var Jt;const[Ce,Le]=p(G),[me,Be]=p(K),{data:ze,totalColumn:xe,totalRow:rt,useIsScrolling:nt,itemKey:Pt}=m,Wt=[];if(rt>0&&xe>0)for(let Rt=me;Rt<=Be;Rt++)for(let Qt=Ce;Qt<=Le;Qt++){const _e=Pt({columnIndex:Qt,data:ze,rowIndex:Rt});Wt.push(ot(Ve,{key:_e},(Jt=b.default)==null?void 0:Jt.call(b,{columnIndex:Qt,data:ze,isScrolling:nt?p(A).isScrolling:void 0,style:Z(Rt,Qt),rowIndex:Rt})))}return Wt},Te=()=>{const Ce=St(m.innerElement),Le=De();return[ot(Ce,Tt(m.innerProps,{style:p(W),ref:E}),Ye(Ce)?Le:{default:()=>Le})]};return()=>{const Ce=St(m.containerElement),{horizontalScrollbar:Le,verticalScrollbar:me}=Se(),Be=Te();return ot("div",{key:0,class:_.e("wrapper"),role:m.role},[ot(Ce,{class:m.className,style:p(B),onScroll:V,ref:k},Ye(Ce)?Be:{default:()=>Be}),Le,me])}}}),ux="ElFixedSizeGrid",Lde=rP({name:"ElFixedSizeGrid",getColumnPosition:({columnWidth:e},t)=>[e,t*e],getRowPosition:({rowHeight:e},t)=>[e,t*e],getEstimatedTotalHeight:({totalRow:e,rowHeight:t})=>t*e,getEstimatedTotalWidth:({totalColumn:e,columnWidth:t})=>t*e,getColumnOffset:({totalColumn:e,columnWidth:t,width:n},a,r,o,i,l)=>{n=Number(n);const s=Math.max(0,e*t-n),u=Math.min(s,a*t),c=Math.max(0,a*t-n+l+t);switch(r==="smart"&&(o>=c-n&&o<=u+n?r=Io:r=Pi),r){case mh:return u;case"end":return c;case Pi:{const d=Math.round(c+(u-c)/2);return ds+Math.floor(n/2)?s:d}case Io:default:return o>=c&&o<=u?o:c>u||o{t=Number(t);const s=Math.max(0,n*e-t),u=Math.min(s,a*e),c=Math.max(0,a*e-t+l+e);switch(r==="smart"&&(o>=c-t&&o<=u+t?r=Io:r=Pi),r){case mh:return u;case"end":return c;case Pi:{const d=Math.round(c+(u-c)/2);return ds+Math.floor(t/2)?s:d}case Io:default:return o>=c&&o<=u?o:c>u||oMath.max(0,Math.min(t-1,Math.floor(n/e))),getColumnStopIndexForStartIndex:({columnWidth:e,totalColumn:t,width:n},a,r)=>{const o=a*e,i=Math.ceil((n+r-o)/e);return Math.max(0,Math.min(t-1,a+i-1))},getRowStartIndexForOffset:({rowHeight:e,totalRow:t},n)=>Math.max(0,Math.min(t-1,Math.floor(n/e))),getRowStopIndexForStartIndex:({rowHeight:e,totalRow:t,height:n},a,r)=>{const o=a*e,i=Math.ceil((n+r-o)/e);return Math.max(0,Math.min(t-1,a+i-1))},initCache:()=>{},clearCache:!0,validateProps:({columnWidth:e,rowHeight:t})=>{Je(e)||Dn(ux,` + "columnWidth" must be passed as number, + instead ${typeof e} was given. + `),Je(t)||Dn(ux,` + "columnWidth" must be passed as number, + instead ${typeof t} was given. + `)}}),{max:lg,min:oP,floor:iP}=Math,cx="ElDynamicSizeGrid",Pde={column:"columnWidth",row:"rowHeight"},U4={column:"lastVisitedColumnIndex",row:"lastVisitedRowIndex"},vl=(e,t,n,a)=>{const[r,o,i]=[n[a],e[Pde[a]],n[U4[a]]];if(t>i){let l=0;if(i>=0){const s=r[i];l=s.offset+s.size}for(let s=i+1;s<=t;s++){const u=o(s);r[s]={offset:l,size:u},l+=u}n[U4[a]]=t}return r[t]},lP=(e,t,n,a,r,o)=>{for(;n<=a;){const i=n+iP((a-n)/2),l=vl(e,i,t,o).offset;if(l===r)return i;l{const o=r==="column"?e.totalColumn:e.totalRow;let i=1;for(;n{const[r,o]=[t[a],t[U4[a]]];return(o>0?r[o].offset:0)>=n?lP(e,t,0,o,n,a):Dde(e,t,lg(0,o),n,a)},sP=({totalRow:e},{estimatedRowHeight:t,lastVisitedRowIndex:n,row:a})=>{let r=0;if(n>=e&&(n=e-1),n>=0){const i=a[n];r=i.offset+i.size}const o=(e-n-1)*t;return r+o},uP=({totalColumn:e},{column:t,estimatedColumnWidth:n,lastVisitedColumnIndex:a})=>{let r=0;if(a>e&&(a=e-1),a>=0){const i=t[a];r=i.offset+i.size}const o=(e-a-1)*n;return r+o},Ode={column:uP,row:sP},fx=(e,t,n,a,r,o,i)=>{const[l,s]=[o==="row"?e.height:e.width,Ode[o]],u=vl(e,t,r,o),c=lg(0,oP(s(e,r)-l,u.offset)),d=lg(0,u.offset-l+i+u.size);switch(n==="smart"&&(a>=d-l&&a<=c+l?n=Io:n=Pi),n){case mh:return c;case"end":return d;case Pi:return Math.round(d+(c-d)/2);case Io:default:return a>=d&&a<=c?a:d>c||a{const a=vl(e,t,n,"column");return[a.size,a.offset]},getRowPosition:(e,t,n)=>{const a=vl(e,t,n,"row");return[a.size,a.offset]},getColumnOffset:(e,t,n,a,r,o)=>fx(e,t,n,a,r,"column",o),getRowOffset:(e,t,n,a,r,o)=>fx(e,t,n,a,r,"row",o),getColumnStartIndexForOffset:(e,t,n)=>dx(e,n,t,"column"),getColumnStopIndexForStartIndex:(e,t,n,a)=>{const r=vl(e,t,a,"column"),o=n+e.width;let i=r.offset+r.size,l=t;for(;ldx(e,n,t,"row"),getRowStopIndexForStartIndex:(e,t,n,a)=>{const{totalRow:r,height:o}=e,i=vl(e,t,a,"row"),l=n+o;let s=i.size+i.offset,u=t;for(;u{const n=({columnIndex:o,rowIndex:i},l)=>{var s,u;l=zt(l)?!0:l,Je(o)&&(t.value.lastVisitedColumnIndex=Math.min(t.value.lastVisitedColumnIndex,o-1)),Je(i)&&(t.value.lastVisitedRowIndex=Math.min(t.value.lastVisitedRowIndex,i-1)),(s=e.exposed)==null||s.getItemStyleCache.value(-1,null,null),l&&((u=e.proxy)==null||u.$forceUpdate())},a=(o,i)=>{n({columnIndex:o},i)},r=(o,i)=>{n({rowIndex:o},i)};Object.assign(e.proxy,{resetAfterColumnIndex:a,resetAfterRowIndex:r,resetAfter:n})},initCache:({estimatedColumnWidth:e=50,estimatedRowHeight:t=50})=>({column:{},estimatedColumnWidth:e,estimatedRowHeight:t,lastVisitedColumnIndex:-1,lastVisitedRowIndex:-1,row:{}}),clearCache:!1,validateProps:({columnWidth:e,rowHeight:t})=>{Qe(e)||Dn(cx,` + "columnWidth" must be passed as function, + instead ${typeof e} was given. + `),Qe(t)||Dn(cx,` + "rowHeight" must be passed as function, + instead ${typeof t} was given. + `)}}),cP={modelValue:{type:[Number,String,Boolean],default:void 0},label:{type:[String,Boolean,Number,Object],default:void 0},value:{type:[String,Boolean,Number,Object],default:void 0},indeterminate:Boolean,disabled:{type:Boolean,default:void 0},checked:Boolean,name:{type:String,default:void 0},trueValue:{type:[String,Number],default:void 0},falseValue:{type:[String,Number],default:void 0},trueLabel:{type:[String,Number],default:void 0},falseLabel:{type:[String,Number],default:void 0},id:{type:String,default:void 0},border:Boolean,size:pa,tabindex:[String,Number],validateEvent:{type:Boolean,default:!0},ariaLabel:String,...dr(["ariaControls"])},dP={[bt]:e=>Ye(e)||Je(e)||vn(e),change:e=>Ye(e)||Je(e)||vn(e)},mf=Symbol("checkboxGroupContextKey"),Rde=$e({modelValue:{type:ae(Array),default:()=>[]},disabled:{type:Boolean,default:void 0},min:Number,max:Number,size:pa,fill:String,textColor:String,tag:{type:String,default:"div"},validateEvent:{type:Boolean,default:!0},options:{type:ae(Array)},props:{type:ae(Object),default:()=>fP},type:{type:String,values:["checkbox","button"],default:"checkbox"},...dr(["ariaLabel"])}),Nde={[bt]:e=>Ae(e),change:e=>Ae(e)},fP={label:"label",value:"value",disabled:"disabled"},Bde=({model:e,isChecked:t})=>{const n=Ze(mf,void 0),a=Ze(Ec,void 0),r=D(()=>{var l,s;const o=(l=n==null?void 0:n.max)==null?void 0:l.value,i=(s=n==null?void 0:n.min)==null?void 0:s.value;return!zt(o)&&e.value.length>=o&&!t.value||!zt(i)&&e.value.length<=i&&t.value});return{isDisabled:$n(D(()=>{var o;return n===void 0?(a==null?void 0:a.disabled)??r.value:((o=n.disabled)==null?void 0:o.value)||r.value})),isLimitDisabled:r}},Vde=(e,{model:t,isLimitExceeded:n,hasOwnLabel:a,isDisabled:r,isLabeledByFormItem:o})=>{const i=Ze(mf,void 0),{formItem:l}=Ea(),{emit:s}=Et();function u(h){return[!0,e.trueValue,e.trueLabel].includes(h)?e.trueValue??e.trueLabel??!0:e.falseValue??e.falseLabel??!1}function c(h,m){s(Ot,u(h),m)}function d(h){if(n.value)return;const m=h.target;s(Ot,u(m.checked),h)}async function v(h){n.value||!a.value&&!r.value&&o.value&&(h.composedPath().some(m=>m.tagName==="LABEL")||(t.value=u([!1,e.falseValue,e.falseLabel].includes(t.value)),await He(),c(t.value,h)))}const f=D(()=>(i==null?void 0:i.validateEvent)||e.validateEvent);return ye(()=>e.modelValue,()=>{f.value&&(l==null||l.validate("change").catch(kt))}),{handleChange:d,onClickRoot:v}},zde=e=>{const t=F(!1),{emit:n,vnode:a}=Et(),r=Ze(mf,void 0),o=D(()=>zt(r)===!1),i=F(!1),l=D(()=>{const u=a.props??{};return"modelValue"in u||"model-value"in u}),s=D({get(){var u;return o.value?(u=r==null?void 0:r.modelValue)==null?void 0:u.value:l.value?e.modelValue:t.value},set(u){var c,d;o.value&&Ae(u)?(i.value=((c=r==null?void 0:r.max)==null?void 0:c.value)!==void 0&&u.length>(r==null?void 0:r.max.value)&&u.length>s.value.length,i.value===!1&&((d=r==null?void 0:r.changeEvent)==null||d.call(r,u))):(n(bt,u),t.value=u)}});return{model:s,isGroup:o,isLimitExceeded:i}},Fde=(e,t,{model:n})=>{const a=Ze(mf,void 0),r=F(!1),o=D(()=>Qr(e.value)?e.label:e.value),i=D(()=>{const l=n.value;return vn(l)?l:Ae(l)?_t(o.value)?l.map(wn).some(s=>Rn(s,o.value)):l.map(wn).includes(o.value):l!=null?l===e.trueValue||l===e.trueLabel:!!l});return{checkboxButtonSize:ca(D(()=>{var l;return(l=a==null?void 0:a.size)==null?void 0:l.value}),{prop:!0}),isChecked:i,isFocused:r,checkboxSize:ca(D(()=>{var l;return(l=a==null?void 0:a.size)==null?void 0:l.value})),hasOwnLabel:D(()=>!!t.default||!Qr(o.value)),actualValue:o}},vP=(e,t)=>{const{formItem:n}=Ea(),{model:a,isGroup:r,isLimitExceeded:o}=zde(e),{isFocused:i,isChecked:l,checkboxButtonSize:s,checkboxSize:u,hasOwnLabel:c,actualValue:d}=Fde(e,t,{model:a}),{isDisabled:v}=Bde({model:a,isChecked:l}),{inputId:f,isLabeledByFormItem:h}=Yr(e,{formItemContext:n,disableIdGeneration:c,disableIdManagement:r}),{handleChange:m,onClickRoot:g}=Vde(e,{model:a,isLimitExceeded:o,hasOwnLabel:c,isDisabled:v,isLabeledByFormItem:h});return(()=>{function b(){Ae(a.value)&&!a.value.includes(d.value)?a.value.push(d.value):a.value=e.trueValue??e.trueLabel??!0}e.checked&&b()})(),Ri({from:"label act as value",replacement:"value",version:"3.0.0",scope:"el-checkbox",ref:"https://element-plus.org/en-US/component/checkbox.html"},D(()=>r.value&&Qr(e.value))),Ri({from:"true-label",replacement:"true-value",version:"3.0.0",scope:"el-checkbox",ref:"https://element-plus.org/en-US/component/checkbox.html"},D(()=>!!e.trueLabel)),Ri({from:"false-label",replacement:"false-value",version:"3.0.0",scope:"el-checkbox",ref:"https://element-plus.org/en-US/component/checkbox.html"},D(()=>!!e.falseLabel)),{inputId:f,isLabeledByFormItem:h,isChecked:l,isDisabled:v,isFocused:i,checkboxButtonSize:s,checkboxSize:u,hasOwnLabel:c,model:a,actualValue:d,handleChange:m,onClickRoot:g}},Hde=["id","indeterminate","name","tabindex","disabled"];var Wde=q({name:"ElCheckbox",__name:"checkbox",props:cP,emits:dP,setup(e){const t=e,{inputId:n,isLabeledByFormItem:a,isChecked:r,isDisabled:o,isFocused:i,checkboxSize:l,hasOwnLabel:s,model:u,actualValue:c,handleChange:d,onClickRoot:v}=vP(t,Xn()),f=D(()=>t.trueValue||t.falseValue||t.trueLabel||t.falseLabel?{"true-value":t.trueValue??t.trueLabel??!0,"false-value":t.falseValue??t.falseLabel??!1}:{value:c.value}),h=ke("checkbox"),m=D(()=>[h.b(),h.m(l.value),h.is("disabled",o.value),h.is("bordered",t.border),h.is("checked",r.value)]),g=D(()=>[h.e("input"),h.is("disabled",o.value),h.is("checked",r.value),h.is("indeterminate",t.indeterminate),h.is("focus",i.value)]);return(y,b)=>(x(),ne(St(!p(s)&&p(a)?"span":"label"),{for:!p(s)&&p(a)?null:p(n),class:U(m.value),"aria-controls":e.indeterminate?e.ariaControls:null,"aria-checked":e.indeterminate?"mixed":void 0,"aria-label":e.ariaLabel,onClick:p(v)},{default:$(()=>[C("span",{class:U(g.value)},[st(C("input",Tt({id:p(n),"onUpdate:modelValue":b[0]||(b[0]=_=>xn(u)?u.value=_:null),class:p(h).e("original"),type:"checkbox",indeterminate:e.indeterminate,name:e.name,tabindex:e.tabindex,disabled:p(o)},f.value,{onChange:b[1]||(b[1]=(..._)=>p(d)&&p(d)(..._)),onFocus:b[2]||(b[2]=_=>i.value=!0),onBlur:b[3]||(b[3]=_=>i.value=!1),onClick:b[4]||(b[4]=at(()=>{},["stop"]))}),null,16,Hde),[[O7,p(u)]]),C("span",{class:U(p(h).e("inner"))},null,2)],2),p(s)?(x(),O("span",{key:0,class:U(p(h).e("label"))},[ce(y.$slots,"default"),y.$slots.default?te("v-if",!0):(x(),O(Ve,{key:0},[ue(Q(e.label),1)],64))],2)):te("v-if",!0)]),_:3},8,["for","class","aria-controls","aria-checked","aria-label","onClick"]))}}),hP=Wde;const Kde=["name","tabindex","disabled"];var Ude=q({name:"ElCheckboxButton",__name:"checkbox-button",props:cP,emits:dP,setup(e){const t=e,{isFocused:n,isChecked:a,isDisabled:r,checkboxButtonSize:o,model:i,actualValue:l,handleChange:s}=vP(t,Xn()),u=D(()=>t.trueValue||t.falseValue||t.trueLabel||t.falseLabel?{"true-value":t.trueValue??t.trueLabel??!0,"false-value":t.falseValue??t.falseLabel??!1}:{value:l.value}),c=Ze(mf,void 0),d=ke("checkbox"),v=D(()=>{var m,g;const h=((m=c==null?void 0:c.fill)==null?void 0:m.value)??"";return{backgroundColor:h,borderColor:h,color:((g=c==null?void 0:c.textColor)==null?void 0:g.value)??"",boxShadow:h?`-1px 0 0 0 ${h}`:void 0}}),f=D(()=>[d.b("button"),d.bm("button",o.value),d.is("disabled",r.value),d.is("checked",a.value),d.is("focus",n.value)]);return(h,m)=>(x(),O("label",{class:U(f.value)},[st(C("input",Tt({"onUpdate:modelValue":m[0]||(m[0]=g=>xn(i)?i.value=g:null),class:p(d).be("button","original"),type:"checkbox",name:e.name,tabindex:e.tabindex,disabled:p(r)},u.value,{onChange:m[1]||(m[1]=(...g)=>p(s)&&p(s)(...g)),onFocus:m[2]||(m[2]=g=>n.value=!0),onBlur:m[3]||(m[3]=g=>n.value=!1),onClick:m[4]||(m[4]=at(()=>{},["stop"]))}),null,16,Kde),[[O7,p(i)]]),h.$slots.default||e.label?(x(),O("span",{key:0,class:U(p(d).be("button","inner")),style:et(p(a)?v.value:void 0)},[ce(h.$slots,"default",{},()=>[ue(Q(e.label),1)])],6)):te("v-if",!0)],2))}}),uw=Ude,Gde=q({name:"ElCheckboxGroup",__name:"checkbox-group",props:Rde,emits:Nde,setup(e,{emit:t}){const n=e,a=t,r=ke("checkbox"),o=$n(),{formItem:i}=Ea(),{inputId:l,isLabeledByFormItem:s}=Yr(n,{formItemContext:i}),u=async h=>{a(bt,h),await He(),a(Ot,h)},c=D({get(){return n.modelValue},set(h){u(h)}}),d=D(()=>({...fP,...n.props})),v=h=>{const{label:m,value:g,disabled:y}=d.value,b={label:h[m],value:h[g],disabled:h[y]};return{...Tc(h,[m,g,y]),...b}},f=D(()=>n.type==="button"?uw:hP);return Ft(mf,{...ji(Ta(n),["size","min","max","validateEvent","fill","textColor"]),disabled:o,modelValue:c,changeEvent:u}),ye(()=>n.modelValue,(h,m)=>{n.validateEvent&&!Rn(h,m)&&(i==null||i.validate("change").catch(kt))}),(h,m)=>{var g;return x(),ne(St(e.tag),{id:p(l),class:U(p(r).b("group")),role:"group","aria-label":p(s)?void 0:e.ariaLabel||"checkbox-group","aria-labelledby":p(s)?(g=p(i))==null?void 0:g.labelId:void 0},{default:$(()=>[ce(h.$slots,"default",{},()=>[(x(!0),O(Ve,null,dt(e.options,(y,b)=>(x(),ne(St(f.value),Tt({key:b},{ref_for:!0},v(y)),null,16))),128))])]),_:3},8,["id","class","aria-label","aria-labelledby"])}}}),pP=Gde;const Zo=wt(hP,{CheckboxButton:uw,CheckboxGroup:pP}),Yde=On(uw),cw=On(pP),mP=$e({modelValue:{type:[String,Number,Boolean],default:void 0},size:pa,disabled:{type:Boolean,default:void 0},label:{type:[String,Number,Boolean],default:void 0},value:{type:[String,Number,Boolean],default:void 0},name:{type:String,default:void 0}}),jde=$e({...mP,border:Boolean}),gP={[bt]:e=>Ye(e)||Je(e)||vn(e),[Ot]:e=>Ye(e)||Je(e)||vn(e)},yP=Symbol("radioGroupKey"),qde=$e({...mP}),bP={label:"label",value:"value",disabled:"disabled"},Xde=$e({id:{type:String,default:void 0},size:pa,disabled:{type:Boolean,default:void 0},modelValue:{type:[String,Number,Boolean],default:void 0},fill:{type:String,default:""},textColor:{type:String,default:""},name:{type:String,default:void 0},validateEvent:{type:Boolean,default:!0},options:{type:ae(Array)},props:{type:ae(Object),default:()=>bP},type:{type:String,values:["radio","button"],default:"radio"},...dr(["ariaLabel"])}),Zde=gP,_P=(e,t)=>{const n=F(),a=Ze(yP,void 0),r=D(()=>!!a),o=D(()=>Qr(e.value)?e.label:e.value),i=D({get(){return r.value?a.modelValue:e.modelValue},set(d){r.value?a.changeEvent(d):t&&t("update:modelValue",d),n.value.checked=e.modelValue===o.value}}),l=ca(D(()=>a==null?void 0:a.size)),s=$n(D(()=>a==null?void 0:a.disabled)),u=F(!1),c=D(()=>s.value||r.value&&i.value!==o.value?-1:0);return Ri({from:"label act as value",replacement:"value",version:"3.0.0",scope:"el-radio",ref:"https://element-plus.org/en-US/component/radio.html"},D(()=>r.value&&Qr(e.value))),{radioRef:n,isGroup:r,radioGroup:a,focus:u,size:l,disabled:s,tabIndex:c,modelValue:i,actualValue:o}},Jde=["value","name","disabled","checked"];var Qde=q({name:"ElRadio",__name:"radio",props:jde,emits:gP,setup(e,{emit:t}){const n=e,a=t,r=ke("radio"),{radioRef:o,radioGroup:i,focus:l,size:s,disabled:u,modelValue:c,actualValue:d}=_P(n,a);function v(){He(()=>a(Ot,c.value))}return(f,h)=>{var m;return x(),O("label",{class:U([p(r).b(),p(r).is("disabled",p(u)),p(r).is("focus",p(l)),p(r).is("bordered",e.border),p(r).is("checked",p(c)===p(d)),p(r).m(p(s))])},[C("span",{class:U([p(r).e("input"),p(r).is("disabled",p(u)),p(r).is("checked",p(c)===p(d))])},[st(C("input",{ref_key:"radioRef",ref:o,"onUpdate:modelValue":h[0]||(h[0]=g=>xn(c)?c.value=g:null),class:U(p(r).e("original")),value:p(d),name:e.name||((m=p(i))==null?void 0:m.name),disabled:p(u),checked:p(c)===p(d),type:"radio",onFocus:h[1]||(h[1]=g=>l.value=!0),onBlur:h[2]||(h[2]=g=>l.value=!1),onChange:v,onClick:h[3]||(h[3]=at(()=>{},["stop"]))},null,42,Jde),[[$7,p(c)]]),C("span",{class:U(p(r).e("inner"))},null,2)],2),C("span",{class:U(p(r).e("label")),onKeydown:h[4]||(h[4]=at(()=>{},["stop"]))},[ce(f.$slots,"default",{},()=>[ue(Q(e.label),1)])],34)],2)}}}),wP=Qde;const efe=["value","name","disabled"];var tfe=q({name:"ElRadioButton",__name:"radio-button",props:qde,setup(e){const t=e,n=ke("radio"),{radioRef:a,focus:r,size:o,disabled:i,modelValue:l,radioGroup:s,actualValue:u}=_P(t),c=D(()=>({backgroundColor:(s==null?void 0:s.fill)||"",borderColor:(s==null?void 0:s.fill)||"",boxShadow:s!=null&&s.fill?`-1px 0 0 0 ${s.fill}`:"",color:(s==null?void 0:s.textColor)||""}));return(d,v)=>{var f;return x(),O("label",{class:U([p(n).b("button"),p(n).is("active",p(l)===p(u)),p(n).is("disabled",p(i)),p(n).is("focus",p(r)),p(n).bm("button",p(o))])},[st(C("input",{ref_key:"radioRef",ref:a,"onUpdate:modelValue":v[0]||(v[0]=h=>xn(l)?l.value=h:null),class:U(p(n).be("button","original-radio")),value:p(u),type:"radio",name:e.name||((f=p(s))==null?void 0:f.name),disabled:p(i),onFocus:v[1]||(v[1]=h=>r.value=!0),onBlur:v[2]||(v[2]=h=>r.value=!1),onClick:v[3]||(v[3]=at(()=>{},["stop"]))},null,42,efe),[[$7,p(l)]]),C("span",{class:U(p(n).be("button","inner")),style:et(p(l)===p(u)?c.value:{}),onKeydown:v[4]||(v[4]=at(()=>{},["stop"]))},[ce(d.$slots,"default",{},()=>[ue(Q(e.label),1)])],38)],2)}}}),dw=tfe;const nfe=["id","aria-label","aria-labelledby"];var afe=q({name:"ElRadioGroup",__name:"radio-group",props:Xde,emits:Zde,setup(e,{emit:t}){const n=e,a=t,r=ke("radio"),o=Fa(),i=F(),{formItem:l}=Ea(),{inputId:s,isLabeledByFormItem:u}=Yr(n,{formItemContext:l}),c=m=>{a(bt,m),He(()=>a(Ot,m))};mt(()=>{const m=i.value.querySelectorAll("[type=radio]"),g=m[0];!Array.from(m).some(y=>y.checked)&&g&&(g.tabIndex=0)});const d=D(()=>n.name||o.value),v=D(()=>({...bP,...n.props})),f=m=>{const{label:g,value:y,disabled:b}=v.value,_={label:m[g],value:m[y],disabled:m[b]};return{...Tc(m,[g,y,b]),..._}},h=D(()=>n.type==="button"?dw:wP);return Ft(yP,nn({...Ta(n),changeEvent:c,name:d})),ye(()=>n.modelValue,(m,g)=>{n.validateEvent&&!Rn(m,g)&&(l==null||l.validate("change").catch(kt))}),(m,g)=>(x(),O("div",{id:p(s),ref_key:"radioGroupRef",ref:i,class:U(p(r).b("group")),role:"radiogroup","aria-label":p(u)?void 0:e.ariaLabel||"radio-group","aria-labelledby":p(u)?p(l).labelId:void 0},[ce(m.$slots,"default",{},()=>[(x(!0),O(Ve,null,dt(e.options,(y,b)=>(x(),ne(St(h.value),Tt({key:b},{ref_for:!0},f(y)),null,16))),128))])],10,nfe))}}),SP=afe;const CP=wt(wP,{RadioButton:dw,RadioGroup:SP}),rfe=On(SP),ofe=On(dw),K1=Symbol();function ife(e){return!!(Ae(e)?e.every(({type:t})=>t===oa):(e==null?void 0:e.type)===oa)}var lfe=q({name:"NodeContent",props:{node:{type:Object,required:!0}},setup(e){const t=ke("cascader-node"),{renderLabelFn:n}=Ze(K1),{node:a}=e,{data:r,label:o}=a,i=()=>{const l=n==null?void 0:n({node:a,data:r});return ife(l)?o:l??o};return()=>L("span",{class:t.e("label")},[i()])}});const sfe=["id","aria-haspopup","aria-owns","aria-expanded","tabindex"];var ufe=q({name:"ElCascaderNode",__name:"node",props:{node:{type:Object,required:!0},menuId:{type:String,required:!1}},emits:["expand"],setup(e,{emit:t}){const n=e,a=t,r=Ze(K1),o=ke("cascader-node"),i=D(()=>r.isHoverMenu),l=D(()=>r.config.multiple),s=D(()=>r.config.checkStrictly),u=D(()=>r.config.showPrefix),c=D(()=>{var E;return(E=r.checkedNodes[0])==null?void 0:E.uid}),d=D(()=>n.node.isDisabled),v=D(()=>n.node.isLeaf),f=D(()=>s.value&&!v.value||!d.value),h=D(()=>g(r.expandingNode)),m=D(()=>s.value&&r.checkedNodes.some(g)),g=E=>{var P;const{level:A,uid:I}=n.node;return((P=E==null?void 0:E.pathNodes[A-1])==null?void 0:P.uid)===I},y=()=>{h.value||r.expandNode(n.node)},b=E=>{const{node:A}=n;E!==A.checked&&r.handleCheckChange(A,E)},_=()=>{r.lazyLoad(n.node,()=>{v.value||y()})},w=E=>{i.value&&(S(),!v.value&&a("expand",E))},S=()=>{const{node:E}=n;!f.value||E.loading||(E.loaded?y():_())},k=()=>{v.value&&!d.value&&!s.value&&!l.value?M(!0):(r.config.checkOnClickNode&&(l.value||s.value)||v.value&&r.config.checkOnClickLeaf)&&!d.value?T(!n.node.checked):i.value||S()},T=E=>{s.value?(b(E),n.node.loaded&&y()):M(E)},M=E=>{n.node.loaded?(b(E),!s.value&&y()):_()};return(E,A)=>(x(),O("li",{id:`${e.menuId}-${e.node.uid}`,role:"menuitem","aria-haspopup":!v.value,"aria-owns":v.value?void 0:e.menuId,"aria-expanded":h.value,tabindex:f.value?-1:void 0,class:U([p(o).b(),p(o).is("selectable",s.value),p(o).is("active",e.node.checked),p(o).is("disabled",!f.value),h.value&&"in-active-path",m.value&&"in-checked-path"]),onMouseenter:w,onFocus:w,onClick:k},[te(" prefix "),l.value&&u.value?(x(),ne(p(Zo),{key:0,"model-value":e.node.checked,indeterminate:e.node.indeterminate,disabled:d.value,onClick:A[0]||(A[0]=at(()=>{},["stop"])),"onUpdate:modelValue":T},null,8,["model-value","indeterminate","disabled"])):s.value&&u.value?(x(),ne(p(CP),{key:1,"model-value":c.value,label:e.node.uid,disabled:d.value,"onUpdate:modelValue":T,onClick:A[1]||(A[1]=at(()=>{},["stop"]))},{default:$(()=>[te(` + Add an empty element to avoid render label, + do not use empty fragment here for https://github.com/vuejs/vue-next/pull/2485 + `),A[2]||(A[2]=C("span",null,null,-1))]),_:1},8,["model-value","label","disabled"])):v.value&&e.node.checked?(x(),ne(p(Xe),{key:2,class:U(p(o).e("prefix"))},{default:$(()=>[L(p(Os))]),_:1},8,["class"])):te("v-if",!0),te(" content "),L(p(lfe),{node:e.node},null,8,["node"]),te(" postfix "),v.value?te("v-if",!0):(x(),O(Ve,{key:3},[e.node.loading?(x(),ne(p(Xe),{key:0,class:U([p(o).is("loading"),p(o).e("postfix")])},{default:$(()=>[L(p(Mr))]),_:1},8,["class"])):(x(),ne(p(Xe),{key:1,class:U(["arrow-right",p(o).e("postfix")])},{default:$(()=>[L(p(za))]),_:1},8,["class"]))],64))],42,sfe))}}),vx=ufe,cfe=q({name:"ElCascaderMenu",__name:"menu",props:{nodes:{type:Array,required:!0},index:{type:Number,required:!0},virtualScroll:{type:Boolean,required:!1,default:!1},itemSize:{type:Number,required:!1,default:34},height:{type:Number,required:!1,default:204}},setup(e,{expose:t}){const n=e,a=Et(),r=ke("cascader-menu"),{t:o}=Vt(),i=Fa();let l,s;const u=Ze(K1),c=F(),d=F(),v=D(()=>!n.nodes.length),f=D(()=>!u.initialLoaded),h=D(()=>`${i.value}-${n.index}`),m=()=>{var M,E,A,I;let T;if(u.expandingNode){const{level:P,pathNodes:R}=u.expandingNode;n.index0&&(T=(A=(E=u.checkedNodes[0])==null?void 0:E.pathNodes[n.index])==null?void 0:A.uid)}else u.checkedNodes.length>0&&n.indexP.uid===T):-1},g=T=>T?n.nodes.findIndex(M=>`${h.value}-${M.uid}`===T):-1,y=T=>{var E;const M=Br(T,0,n.nodes.length-1);(E=d.value)==null||E.scrollToItem(M)},b=T=>{if(!n.nodes.length)return;const M=Br(T,0,n.nodes.length-1);y(M),He(()=>{var A;const E=(A=a.vnode.el)==null?void 0:A.querySelector(`#${h.value}-${n.nodes[M].uid}`);E&&_d(E)})},_=T=>{l=T.target},w=T=>{var M,E,A;if(!(!u.isHoverMenu||!l||!c.value))if(l.contains(T.target)){S();const I=a.vnode.el,{left:P}=I.getBoundingClientRect(),{offsetWidth:R,offsetHeight:G}=I,K=T.clientX-P,H=l.offsetTop,z=H+l.offsetHeight,B=n.virtualScroll?((E=(M=d.value)==null?void 0:M.states)==null?void 0:E.scrollOffset)||0:((A=I.querySelector(`.${r.e("wrap")}`))==null?void 0:A.scrollTop)||0;c.value.innerHTML=` + + + `}else s||(s=window.setTimeout(k,u.config.hoverThreshold))},S=()=>{s&&(clearTimeout(s),s=void 0)},k=()=>{c.value&&(c.value.innerHTML="",S())};return t({getActiveNodeIndex:m,getNodeIndexById:g,scrollToItem:y,focusNodeAt:b,virtualListRef:d,get $el(){return a.vnode.el}}),(T,M)=>{var E;return e.virtualScroll?(x(),O("div",{key:h.value,class:U(p(r).b()),onMousemove:w,onMouseleave:k},[L(p(cp),{ref_key:"virtualListRef",ref:d,height:e.height,"item-size":e.itemSize,data:e.nodes,total:e.nodes.length,"class-name":p(r).e("list"),"inner-element":"ul","inner-props":{role:"menu",class:p(r).is("empty",v.value)}},{default:$(({data:A,index:I,style:P})=>[(x(),ne(vx,{key:A[I].uid,node:A[I],"menu-id":h.value,style:et(P),onExpand:_},null,8,["node","menu-id","style"]))]),_:1},8,["height","item-size","data","total","class-name","inner-props"]),f.value?(x(),O("div",{key:0,class:U(p(r).e("empty-text"))},[L(p(Xe),{size:14,class:U(p(r).is("loading"))},{default:$(()=>[L(p(Mr))]),_:1},8,["class"]),ue(" "+Q(p(o)("el.cascader.loading")),1)],2)):v.value?(x(),O("div",{key:1,class:U(p(r).e("empty-text"))},[ce(T.$slots,"empty",{},()=>[ue(Q(p(o)("el.cascader.noData")),1)])],2)):(E=p(u))!=null&&E.isHoverMenu?(x(),O(Ve,{key:2},[te(" eslint-disable vue/html-self-closing "),(x(),O("svg",{ref_key:"hoverZone",ref:c,class:U(p(r).e("hover-zone"))},null,2))],2112)):te("v-if",!0),te(" eslint-enable vue/html-self-closing ")],34)):(x(),ne(p(ni),{key:h.value,tag:"ul",role:"menu",class:U(p(r).b()),"wrap-class":p(r).e("wrap"),"view-class":[p(r).e("list"),p(r).is("empty",v.value)],onMousemove:w,onMouseleave:k},{default:$(()=>{var A;return[(x(!0),O(Ve,null,dt(e.nodes,I=>(x(),ne(vx,{key:I.uid,node:I,"menu-id":h.value,onExpand:_},null,8,["node","menu-id"]))),128)),f.value?(x(),O("div",{key:0,class:U(p(r).e("empty-text"))},[L(p(Xe),{size:14,class:U(p(r).is("loading"))},{default:$(()=>[L(p(Mr))]),_:1},8,["class"]),ue(" "+Q(p(o)("el.cascader.loading")),1)],2)):v.value?(x(),O("div",{key:1,class:U(p(r).e("empty-text"))},[ce(T.$slots,"empty",{},()=>[ue(Q(p(o)("el.cascader.noData")),1)])],2)):(A=p(u))!=null&&A.isHoverMenu?(x(),O(Ve,{key:2},[te(" eslint-disable vue/html-self-closing "),(x(),O("svg",{ref_key:"hoverZone",ref:c,class:U(p(r).e("hover-zone"))},null,2))],2112)):te("v-if",!0),te(" eslint-enable vue/html-self-closing ")]}),_:3},8,["class","wrap-class","view-class"]))}}}),dfe=cfe;let ffe=0;const vfe=e=>{const t=[e];let{parent:n}=e;for(;n;)t.unshift(n),n=n.parent;return t};var G4=class Y4{constructor(t,n,a,r=!1){this.data=t,this.config=n,this.parent=a,this.root=r,this.uid=ffe++,this.checked=!1,this.indeterminate=!1,this.loading=!1;const{value:o,label:i,children:l}=n,s=t[l],u=vfe(this);this.level=r?0:a?a.level+1:1,this.value=t[o],this.label=t[i],this.pathNodes=u,this.pathValues=u.map(c=>c.value),this.pathLabels=u.map(c=>c.label),this.childrenData=s,this.children=(s||[]).map(c=>new Y4(c,n,this)),this.loaded=!n.lazy||this.isLeaf||!_r(s),this.text=""}get isDisabled(){const{data:t,parent:n,config:a}=this,{disabled:r,checkStrictly:o}=a;return(Qe(r)?r(t,this):!!t[r])||!o&&!!(n!=null&&n.isDisabled)}get isLeaf(){const{data:t,config:n,childrenData:a,loaded:r}=this,{lazy:o,leaf:i}=n,l=Qe(i)?i(t,this):t[i];return zt(l)?o&&!r?!1:!(Ae(a)&&a.length):!!l}get valueByOption(){return this.config.emitPath?this.pathValues:this.value}appendChild(t){const{childrenData:n,children:a}=this,r=new Y4(t,this.config,this);return Ae(n)?n.push(t):this.childrenData=[t],a.push(r),r}calcText(t,n){const a=t?this.pathLabels.join(n):this.label;return this.text=a,a}broadcast(t){this.children.forEach(n=>{var a;n&&(n.broadcast(t),(a=n.onParentCheck)==null||a.call(n,t))})}emit(){var n;const{parent:t}=this;t&&((n=t.onChildCheck)==null||n.call(t),t.emit())}onParentCheck(t){this.isDisabled||this.setCheckState(t)}onChildCheck(){const{children:t}=this,n=t.filter(r=>!r.isDisabled),a=n.length?n.every(r=>r.checked):!1;this.setCheckState(a)}setCheckState(t){const n=this.children.length,a=this.children.reduce((r,o)=>r+(o.checked?1:o.indeterminate?.5:0),0);this.checked=this.loaded&&this.children.filter(r=>!r.isDisabled).every(r=>r.loaded&&r.checked)&&t,this.indeterminate=this.loaded&&a!==n&&a>0}doCheck(t){if(this.checked===t)return;const{checkStrictly:n,multiple:a}=this.config;n||!a?this.checked=t:(this.broadcast(t),this.setCheckState(t),this.emit())}};const j4=(e,t)=>e.reduce((n,a)=>(a.isLeaf?n.push(a):(!t&&n.push(a),n=n.concat(j4(a.children,t))),n),[]);var hx=class{constructor(e,t){this.config=t;const n=(e||[]).map(a=>new G4(a,this.config));this.nodes=n,this.allNodes=j4(n,!1),this.leafNodes=j4(n,!0)}getNodes(){return this.nodes}getFlattedNodes(e){return e?this.leafNodes:this.allNodes}appendNode(e,t){const n=t?t.appendChild(e):new G4(e,this.config);t||this.nodes.push(n),this.appendAllNodesAndLeafNodes(n)}appendNodes(e,t){e.length>0?e.forEach(n=>this.appendNode(n,t)):t&&t.isLeaf&&this.leafNodes.push(t)}appendAllNodesAndLeafNodes(e){this.allNodes.push(e),e.isLeaf&&this.leafNodes.push(e),e.children&&e.children.forEach(t=>{this.appendAllNodesAndLeafNodes(t)})}getNodeByValue(e,t=!1){return Qr(e)?null:this.getFlattedNodes(t).find(n=>Rn(n.value,e)||Rn(n.pathValues,e))||null}getSameNode(e){return e&&this.getFlattedNodes(!1).find(({value:t,level:n})=>Rn(e.value,t)&&e.level===n)||null}};const ay=e=>{if(!e)return 0;const t=e.id.split("-");return Number(t[t.length-2])},hfe=e=>{if(!e)return;const t=e.querySelector("input");t?t.click():DA(e)&&e.click()},pfe=(e,t)=>{const n=t.slice(0),a=n.map(o=>o.uid),r=e.reduce((o,i)=>{const l=a.indexOf(i.uid);return l>-1&&(o.push(i),n.splice(l,1),a.splice(l,1)),o},[]);return r.push(...n),r};var mfe=q({name:"ElCascaderPanel",__name:"index",props:hde,emits:pde,setup(e,{expose:t,emit:n}){const a=e,r=n;let o=!1;const i=ke("cascader"),l=mde(a),s=Xn();let u;const c=F(!0),d=F(!1),v=F([]),f=F(),h=F([]),m=F(),g=F([]),y=D(()=>l.value.expandTrigger==="hover"),b=D(()=>a.renderLabel||s.default),_=D(()=>a.virtualScroll),w=D(()=>a.itemSize),S=D(()=>a.height),k=()=>{const{options:N}=a,V=l.value;o=!1,u=new hx(N,V),h.value=[u.getNodes()],V.lazy&&_r(a.options)?(c.value=!1,T(void 0,Y=>{Y&&(u=new hx(Y,V),h.value=[u.getNodes()]),c.value=!0,K(!1,!0)})):K(!1,!0)},T=(N,V)=>{const Y=l.value;N=N||new G4({},Y,void 0,!0),N.loading=!0;const X=re=>{const ie=N,le=ie.root?null:ie;ie.loading=!1,ie.loaded=!0,ie.childrenData=ie.childrenData||[],re&&(u==null||u.appendNodes(re,le)),re&&(V==null||V(re)),N.level===0&&(d.value=!0)},ee=()=>{N.loading=!1,N.loaded=!1,N.level===0&&(c.value=!0)};Y.lazyLoad(N,X,ee)},M=(N,V)=>{var re;const{level:Y}=N,X=h.value.slice(0,Y);let ee;N.isLeaf?ee=N.pathNodes[Y-2]:(ee=N,X.push(N.children)),((re=m.value)==null?void 0:re.uid)!==(ee==null?void 0:ee.uid)&&(m.value=N,h.value=X,!V&&r("expand-change",(N==null?void 0:N.pathValues)||[]))},E=(N,V,Y=!0)=>{const{checkStrictly:X,multiple:ee}=l.value,re=g.value[0];o=!0,!ee&&(re==null||re.doCheck(!1)),N.doCheck(V),G(),Y&&!ee&&!X&&r("close"),!Y&&!ee&&A(N)},A=N=>{N&&(N=N.parent,A(N),N&&M(N))},I=N=>u==null?void 0:u.getFlattedNodes(N),P=N=>{var V;return(V=I(N))==null?void 0:V.filter(({checked:Y})=>Y!==!1)},R=()=>{g.value.forEach(N=>N.doCheck(!1)),G(),h.value=h.value.slice(0,1),m.value=void 0,r("expand-change",[])},G=()=>{const{checkStrictly:N,multiple:V}=l.value,Y=g.value,X=pfe(Y,P(!N)),ee=X.map(re=>re.valueByOption);g.value=X,f.value=V?ee:ee[0]??null},K=(N=!1,V=!1)=>{const{modelValue:Y}=a,{lazy:X,multiple:ee,checkStrictly:re}=l.value,ie=!re;if(!(!c.value||o||!V&&Rn(Y,f.value)))if(X&&!N){const le=m5(VG(or(Y))).map(ve=>u==null?void 0:u.getNodeByValue(ve)).filter(ve=>!!ve&&!ve.loaded&&!ve.loading);le.length?le.forEach(ve=>{T(ve,()=>K(!1,V))}):K(!0,V)}else H(m5((ee?or(Y):[Y]).map(le=>u==null?void 0:u.getNodeByValue(le,ie))),V),f.value=Li(Y??void 0)},H=(N,V=!0)=>{const{checkStrictly:Y}=l.value,X=g.value,ee=N.filter(le=>!!le&&(Y||le.isLeaf)),re=u==null?void 0:u.getSameNode(m.value),ie=V&&re||ee[0];ie?ie.pathNodes.forEach(le=>M(le,!0)):m.value=void 0,X.forEach(le=>le.doCheck(!1)),nn(ee).forEach(le=>le.doCheck(!0)),g.value=ee,He(z)},z=()=>{qt&&v.value.forEach(N=>{var Y,X;const V=N==null?void 0:N.$el;if(V)if(_.value){const ee=(Y=N==null?void 0:N.getActiveNodeIndex)==null?void 0:Y.call(N);ee!==void 0&&ee>=0&&((X=N==null?void 0:N.scrollToItem)==null||X.call(N,ee))}else{const ee=V.querySelector(`.${i.namespace.value}-scrollbar__wrap`);let re=V.querySelector(`.${i.b("node")}.in-active-path`);if(!re){const ie=V.querySelectorAll(`.${i.b("node")}.${i.is("active")}`);re=ie[ie.length-1]}k6(ee,re)}})},B=N=>{var X,ee;const V=N.target,Y=mn(N);switch(Y){case Pe.up:case Pe.down:{N.preventDefault();const re=Y===Pe.up?-1:1;if(_.value){const ie=ay(V),le=v.value[ie];if(le){const ve=le.getNodeIndexById(V.id);if(ve>=0){const he=h.value[ie]??[],J=he.length;let Z=ve+re;for(;Z>=0&&Z{Rn(N,V)||k()},{immediate:!0}),ye(()=>a.options,k,{deep:!0}),ye(()=>a.modelValue,()=>{o=!1,K()},{deep:!0}),ye(()=>f.value,N=>{Rn(N,a.modelValue)||(r(bt,N),r(Ot,N))});const W=()=>{d.value||!c.value||k()};return q3(()=>v.value=[]),mt(()=>!_r(a.modelValue)&&K()),t({menuList:v,menus:h,checkedNodes:g,handleKeyDown:B,handleCheckChange:E,getFlattedNodes:I,getCheckedNodes:P,clearCheckedNodes:R,calculateCheckedValue:G,scrollToExpandingNode:z,loadLazyRootNodes:W}),(N,V)=>(x(),O("div",{class:U([p(i).b("panel"),p(i).is("bordered",e.border)]),onKeydown:B},[(x(!0),O(Ve,null,dt(h.value,(Y,X)=>(x(),ne(dfe,{key:X,ref_for:!0,ref:ee=>v.value[X]=ee,index:X,nodes:[...Y],"virtual-scroll":_.value,"item-size":w.value,height:S.value},{empty:$(()=>[ce(N.$slots,"empty")]),_:3},8,["index","nodes","virtual-scroll","item-size","height"]))),128))],34))}}),gfe=mfe;const xP=wt(gfe),yfe=$e({...GL,size:pa,placeholder:String,disabled:{type:Boolean,default:void 0},clearable:Boolean,clearIcon:{type:pn,default:ci},filterable:Boolean,filterMethod:{type:ae(Function),default:(e,t)=>e.text.includes(t)},separator:{type:String,default:" / "},showAllLevels:{type:Boolean,default:!0},collapseTags:Boolean,maxCollapseTags:{type:Number,default:1},collapseTagsTooltip:Boolean,maxCollapseTagsTooltipHeight:{type:[String,Number]},debounce:{type:Number,default:300},beforeFilter:{type:ae(Function),default:()=>!0},placement:{type:ae(String),values:tl,default:"bottom-start"},fallbackPlacements:{type:ae(Array),default:["bottom-start","bottom","top-start","top","right","left"]},popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,teleported:Tn.teleported,effect:{type:ae(String),default:"light"},tagType:{...Ll.type,default:"info"},tagEffect:{...Ll.effect,default:"light"},validateEvent:{type:Boolean,default:!0},persistent:{type:Boolean,default:!0},showCheckedStrategy:{type:String,values:["parent","child"],default:"child"},checkOnClickNode:Boolean,showPrefix:{type:Boolean,default:!0},fitInputWidth:{type:[Boolean,Number],default:!1},...Mc}),px=e=>!0,bfe={[bt]:px,[Ot]:px,focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0,visibleChange:e=>vn(e),expandChange:e=>!!e,removeTag:e=>!!e},_fe=["placeholder"],wfe=["onClick"],Sfe=["id","data-suggestion-index","onClick"],Cfe=34;var xfe=q({name:"ElCascader",__name:"cascader",props:yfe,emits:bfe,setup(e,{expose:t,emit:n}){const a={modifiers:[{name:"arrowPosition",enabled:!0,phase:"main",fn:({state:Ie})=>{const{modifiersData:Ke,placement:Ge}=Ie;["right","left","bottom","top"].includes(Ge)||Ke.arrow&&(Ke.arrow.x=35)},requires:["arrow"]}]},r=e,o=n,i=Nl(),l=Xn();let s=0,u=0;const c=ke("cascader"),d=ke("input"),v={small:7,default:11,large:15},{t:f}=Vt(),{formItem:h}=Ea(),m=$n(),{valueOnClear:g}=op(r),{isComposing:y,handleComposition:b}=rp({afterComposition(Ie){var Ge;const Ke=(Ge=Ie.target)==null?void 0:Ge.value;Sn(Ke)}}),_=F(),w=F(),S=F(),k=F(),T=F(),M=F(),E=F(),A=F(!1),I=F(!1),P=F(!1),R=F(""),G=F(""),K=F([]),H=F([]),z=F("100%"),B=D(()=>!!l["suggestion-item"]),W=D(()=>Br(H.value.length*r.itemSize,r.itemSize,r.height)),N=D(()=>r.props.multiple?r.collapseTags?K.value.slice(0,r.maxCollapseTags):K.value:[]),V=D(()=>r.props.multiple?r.collapseTags?K.value.slice(r.maxCollapseTags):[]:[]),Y=D(()=>i.style),X=D(()=>r.placeholder??f("el.cascader.placeholder")),ee=D(()=>G.value||K.value.length>0||y.value?"":X.value),re=ca(),ie=D(()=>re.value==="small"?"small":"default"),le=D(()=>!!r.props.multiple),ve=D(()=>!r.filterable||le.value),he=D(()=>le.value?G.value:R.value),J=D(()=>{var Ie;return((Ie=T.value)==null?void 0:Ie.checkedNodes)||[]}),{wrapperRef:Z,isFocused:j,handleBlur:oe}=Fl(S,{disabled:m,beforeBlur(Ie){var Ke,Ge;return((Ke=_.value)==null?void 0:Ke.isFocusInsideContent(Ie))||((Ge=w.value)==null?void 0:Ge.isFocusInsideContent(Ie))},afterBlur(){var Ie;r.validateEvent&&((Ie=h==null?void 0:h.validate)==null||Ie.call(h,"blur").catch(kt))}}),se=D(()=>!r.clearable||m.value||P.value||!I.value&&!j.value?!1:!!J.value.length),fe=D(()=>{const{showAllLevels:Ie,separator:Ke}=r,Ge=J.value;return Ge.length?le.value?"":Ge[0].calcText(Ie,Ke):""}),ge=D(()=>(h==null?void 0:h.validateState)||""),Se=D({get(){return Li(r.modelValue)},set(Ie){const Ke=Ie??g.value;o(bt,Ke),o(Ot,Ke),r.validateEvent&&(h==null||h.validate("change").catch(kt))}}),De=D(()=>[c.b(),c.m(re.value),c.is("disabled",m.value),i.class]),Te=D(()=>[d.e("icon"),"icon-arrow-down",c.is("reverse",A.value)]),Re=D(()=>c.is("focus",j.value)),Ce=D(()=>{var Ie,Ke;return(Ke=(Ie=_.value)==null?void 0:Ie.popperRef)==null?void 0:Ke.contentRef}),Le=Ie=>{j.value&&oe(new FocusEvent("blur",Ie)),me(!1)},me=Ie=>{var Ke,Ge;m.value||(Ie=Ie??!A.value,Ie!==A.value&&(A.value=Ie,(Ge=(Ke=S.value)==null?void 0:Ke.input)==null||Ge.setAttribute("aria-expanded",`${Ie}`),Ie?(Be(),T.value&&He(T.value.scrollToExpandingNode)):r.filterable&&ft(),o("visibleChange",Ie)))},Be=()=>{He(()=>{var Ie;(Ie=_.value)==null||Ie.updatePopper()})},ze=()=>{P.value=!1},xe=Ie=>{const{showAllLevels:Ke,separator:Ge}=r;return{node:Ie,key:Ie.uid,text:Ie.calcText(Ke,Ge),hitState:!1,closable:!m.value&&!Ie.isDisabled}},rt=Ie=>{var Ge;const Ke=Ie.node;Ke.doCheck(!1),(Ge=T.value)==null||Ge.calculateCheckedValue(),o("removeTag",Ke.valueByOption)},nt=()=>{switch(r.showCheckedStrategy){case"child":return J.value;case"parent":{const Ie=Me(!1),Ke=Ie.map(Ge=>Ge.value);return Ie.filter(Ge=>!Ge.parent||!Ke.includes(Ge.parent.value))}default:return[]}},Pt=()=>{if(!le.value)return;const Ie=nt(),Ke=[];Ie.forEach(Ge=>Ke.push(xe(Ge))),K.value=Ke},Wt=()=>{var de,it;const{filterMethod:Ie,showAllLevels:Ke,separator:Ge}=r,Ee=(it=(de=T.value)==null?void 0:de.getFlattedNodes(!r.props.checkStrictly))==null?void 0:it.filter(Dt=>Dt.isDisabled?!1:(Dt.calcText(Ke,Ge),Ie(Dt,he.value)));le.value&&K.value.forEach(Dt=>{Dt.hitState=!1}),P.value=!0,H.value=Ee,He(()=>{var Dt;r.virtualScroll&&H.value.length>0&&((Dt=E.value)==null||Dt.scrollToItem(0)),Ne()}),Be()},Jt=Ie=>{const Ke=M.value;return Ie?(Ke==null?void 0:Ke.querySelector(Ie))??void 0:Ke},Rt=()=>{var Ke;let Ie;P.value&&M.value?Ie=Jt(`.${c.e("suggestion-item")}`):Ie=(Ke=T.value)==null?void 0:Ke.$el.querySelector(`.${c.b("node")}[tabindex="-1"]`),Ie&&(Ie.focus(),!P.value&&Ie.getAttribute("aria-haspopup")==="true"&&Ie.click())},Qt=Ie=>{const Ke=Jt();if(!Ke)return;const Ge=Je(r.fitInputWidth)?`${r.fitInputWidth}px`:`${Ie}px`,Ee=it=>{r.fitInputWidth!==!1?(it.style.width=Ge,it.style.minWidth=""):(it.style.width="",it.style.minWidth=Ge)};if(Ee(Ke),r.virtualScroll){z.value=r.fitInputWidth!==!1?Ge:B.value?`${Ie}px`:`${Math.max(Ie,tt())}px`;return}const de=Jt(`.${c.e("suggestion-list")}`);de&&Ee(de)},_e=()=>{var Ge;if(!l.prefix)return 0;const Ie=(Ge=S.value)==null?void 0:Ge.$el.querySelector(`.${d.e("prefix")}`);if(!Ie)return 0;const Ke=Ie.getBoundingClientRect().width;return Ke<=0?0:Ke+v[re.value||"default"]},Ne=()=>{var Ee,de;const Ie=(Ee=S.value)==null?void 0:Ee.input,Ke=(de=S.value)==null?void 0:de.$el;if(!qt||!Ie||!Ke)return;M.value&&Qt(Ke.getBoundingClientRect().width);const Ge=k.value;if(Ge){const it=K.value.length>0?`${Math.max(Ge.offsetHeight,s)-2}px`:`${s}px`;Ie.style.height=it,Ge.style.left=`${_e()}px`,Be()}},tt=()=>{if(B.value||!H.value.length)return 0;const Ie=document.createElement("canvas").getContext("2d");if(!Ie)return 0;const Ke=Jt(`.${c.e("suggestion-item")}`);if(!Ke||!qt)return 0;const Ge=getComputedStyle(Ke),Ee=Number.parseFloat(Ge.paddingLeft)+Number.parseFloat(Ge.paddingRight);Ie.font=`bold ${Ge.fontSize} ${Ge.fontFamily}`;let de=0,it=!1;for(const Dt of H.value){const sn=Dt.text||"",gn=Ie.measureText(sn);de=Math.max(de,gn.width),Dt.checked&&!it&&(it=!0)}return de+Ee+(it?Cfe:0)},Me=Ie=>{var Ke;return(Ke=T.value)==null?void 0:Ke.getCheckedNodes(Ie)},Ue=Ie=>{Be(),o("expandChange",Ie)},Gt=Ie=>{if(!y.value)switch(mn(Ie)){case Pe.enter:case Pe.numpadEnter:me();break;case Pe.down:me(!0),He(Rt),Ie.preventDefault();break;case Pe.esc:A.value===!0&&(Ie.preventDefault(),Ie.stopPropagation(),me(!1));break;case Pe.tab:me(!1);break}},je=()=>{var Ie;(Ie=T.value)==null||Ie.clearCheckedNodes(),!A.value&&r.filterable&&ft(),me(!1),o("clear")},ft=()=>{const{value:Ie}=fe;R.value=Ie,G.value=Ie},ht=Ie=>{var Ge,Ee;const{checked:Ke}=Ie;le.value?(Ge=T.value)==null||Ge.handleCheckChange(Ie,!Ke,!1):(!Ke&&((Ee=T.value)==null||Ee.handleCheckChange(Ie,!0,!1)),me(!1))},pt=Ie=>{var Ee;const Ke=(Ee=Ie.closest("[data-suggestion-index]"))==null?void 0:Ee.dataset.suggestionIndex;if(!Ke)return-1;const Ge=Number.parseInt(Ke,10);return Number.isNaN(Ge)||Ge<0||Ge>=H.value.length?-1:Ge},It=Ie=>{const Ke=Ie.target,Ge=mn(Ie);switch(Ge){case Pe.up:case Pe.down:{Ie.preventDefault();const Ee=Ge===Pe.up?-1:1;if(r.virtualScroll&&E.value){const de=pt(Ke);if(de>=0){const it=H.value.length,Dt=(de+Ee+it)%it;E.value.scrollToItem(Dt),He(()=>{const sn=Jt(`#suggestion-${H.value[Dt].uid}`);sn&&_d(sn)});return}}_d(OA(Ke,Ee,`.${c.e("suggestion-item")}[tabindex="-1"]`));break}case Pe.enter:case Pe.numpadEnter:Ke.click();break}},Ct=()=>{const Ie=K.value[K.value.length-1];u=G.value?0:u+1,!(!Ie||!u||r.collapseTags&&K.value.length>1)&&(Ie.hitState?rt(Ie):Ie.hitState=!0)},$t=Xh(()=>{const{value:Ie}=he;if(!Ie)return;const Ke=r.beforeFilter(Ie);Ls(Ke)?Ke.then(Wt).catch(()=>{}):Ke!==!1?Wt():ze()},D(()=>r.debounce)),Sn=(Ie,Ke)=>{if(!A.value&&me(!0),!(Ke!=null&&Ke.isComposing))if(Ie)$t();else{const Ge=r.beforeFilter("");Ls(Ge)&&Ge.catch(()=>{}),ze()}},Fe=Ie=>Number.parseFloat(IY(d.cssVarName("input-height"),Ie).value)-2,qe=()=>{var Ie;(Ie=S.value)==null||Ie.focus()},ut=()=>{var Ie;(Ie=S.value)==null||Ie.blur()};return ye(P,Be),ye([J,m,()=>r.collapseTags,()=>r.maxCollapseTags],Pt),ye(K,()=>{He(()=>Ne())}),ye(re,async()=>{await He();const Ie=S.value.input;s=Fe(Ie)||s,Ne()}),ye(fe,ft,{immediate:!0}),ye(()=>A.value,Ie=>{var Ke;Ie&&r.props.lazy&&r.props.lazyLoad&&((Ke=T.value)==null||Ke.loadLazyRootNodes())}),mt(()=>{const Ie=S.value.input,Ke=S.value.$el,Ge=Fe(Ie);s=Ie.offsetHeight||Ge,Mn(Ke,Ne)}),t({getCheckedNodes:Me,cascaderPanelRef:T,togglePopperVisible:me,contentRef:Ce,presentText:fe,focus:qe,blur:ut}),(Ie,Ke)=>(x(),ne(p(Ma),{ref_key:"tooltipRef",ref:_,visible:A.value,teleported:e.teleported,"popper-class":[p(c).e("dropdown"),e.popperClass],"popper-style":e.popperStyle,"popper-options":a,"fallback-placements":e.fallbackPlacements,"stop-popper-mouse-event":!1,"gpu-acceleration":!1,placement:e.placement,transition:`${p(c).namespace.value}-zoom-in-top`,effect:e.effect,pure:"",persistent:e.persistent,onHide:ze},{default:$(()=>[st((x(),O("div",{ref_key:"wrapperRef",ref:Z,class:U(De.value),style:et(Y.value),onClick:Ke[8]||(Ke[8]=()=>me(ve.value?void 0:!0)),onKeydown:Gt,onMouseenter:Ke[9]||(Ke[9]=Ge=>I.value=!0),onMouseleave:Ke[10]||(Ke[10]=Ge=>I.value=!1)},[L(p(Ba),{ref_key:"inputRef",ref:S,modelValue:R.value,"onUpdate:modelValue":Ke[1]||(Ke[1]=Ge=>R.value=Ge),placeholder:ee.value,readonly:ve.value,disabled:p(m),"validate-event":!1,size:p(re),class:U(Re.value),tabindex:le.value&&e.filterable&&!p(m)?-1:void 0,onCompositionstart:p(b),onCompositionupdate:p(b),onCompositionend:p(b),onInput:Sn},xr({suffix:$(()=>[se.value?(x(),ne(p(Xe),{key:"clear",class:U([p(d).e("icon"),"icon-circle-close"]),onClick:at(je,["stop"])},{default:$(()=>[(x(),ne(St(e.clearIcon)))]),_:1},8,["class"])):(x(),ne(p(Xe),{key:"arrow-down",class:U(Te.value),onClick:Ke[0]||(Ke[0]=at(Ge=>me(),["stop"]))},{default:$(()=>[L(p(ui))]),_:1},8,["class"]))]),_:2},[Ie.$slots.prefix?{name:"prefix",fn:$(()=>[ce(Ie.$slots,"prefix")]),key:"0"}:void 0]),1032,["modelValue","placeholder","readonly","disabled","size","class","tabindex","onCompositionstart","onCompositionupdate","onCompositionend"]),le.value?(x(),O("div",{key:0,ref_key:"tagWrapper",ref:k,class:U([p(c).e("tags"),p(c).is("validate",!!ge.value)])},[ce(Ie.$slots,"tag",{data:K.value,deleteTag:rt},()=>[(x(!0),O(Ve,null,dt(N.value,Ge=>(x(),ne(p(Cl),{key:Ge.key,type:e.tagType,size:ie.value,effect:e.tagEffect,hit:Ge.hitState,closable:Ge.closable,"disable-transitions":"",onClose:Ee=>rt(Ge)},{default:$(()=>[C("span",null,Q(Ge.text),1)]),_:2},1032,["type","size","effect","hit","closable","onClose"]))),128))]),e.collapseTags&&K.value.length>e.maxCollapseTags?(x(),ne(p(Ma),{key:0,ref_key:"tagTooltipRef",ref:w,disabled:A.value||!e.collapseTagsTooltip,"fallback-placements":["bottom","top","right","left"],placement:"bottom","popper-class":e.popperClass,"popper-style":e.popperStyle,effect:e.effect,persistent:e.persistent},{default:$(()=>[L(p(Cl),{closable:!1,size:ie.value,type:e.tagType,effect:e.tagEffect,"disable-transitions":""},{default:$(()=>[C("span",{class:U(p(c).e("tags-text"))}," + "+Q(K.value.length-e.maxCollapseTags),3)]),_:1},8,["size","type","effect"])]),content:$(()=>[L(p(ni),{"max-height":e.maxCollapseTagsTooltipHeight},{default:$(()=>[C("div",{class:U(p(c).e("collapse-tags"))},[(x(!0),O(Ve,null,dt(V.value,(Ge,Ee)=>(x(),O("div",{key:Ee,class:U(p(c).e("collapse-tag"))},[(x(),ne(p(Cl),{key:Ge.key,class:"in-tooltip",type:e.tagType,size:ie.value,effect:e.tagEffect,hit:Ge.hitState,closable:Ge.closable,"disable-transitions":"",onClose:de=>rt(Ge)},{default:$(()=>[C("span",null,Q(Ge.text),1)]),_:2},1032,["type","size","effect","hit","closable","onClose"]))],2))),128))],2)]),_:1},8,["max-height"])]),_:1},8,["disabled","popper-class","popper-style","effect","persistent"])):te("v-if",!0),e.filterable&&!p(m)?st((x(),O("input",{key:1,"onUpdate:modelValue":Ke[2]||(Ke[2]=Ge=>G.value=Ge),type:"text",class:U(p(c).e("search-input")),placeholder:fe.value?"":X.value,onInput:Ke[3]||(Ke[3]=Ge=>Sn(G.value,Ge)),onClick:Ke[4]||(Ke[4]=at(Ge=>me(!0),["stop"])),onKeydown:kn(Ct,["delete"]),onCompositionstart:Ke[5]||(Ke[5]=(...Ge)=>p(b)&&p(b)(...Ge)),onCompositionupdate:Ke[6]||(Ke[6]=(...Ge)=>p(b)&&p(b)(...Ge)),onCompositionend:Ke[7]||(Ke[7]=(...Ge)=>p(b)&&p(b)(...Ge))},null,42,_fe)),[[D7,G.value]]):te("v-if",!0)],2)):te("v-if",!0)],38)),[[p(Ns),Le,Ce.value]])]),content:$(()=>[Ie.$slots.header?(x(),O("div",{key:0,class:U(p(c).e("header")),onClick:Ke[11]||(Ke[11]=at(()=>{},["stop"]))},[ce(Ie.$slots,"header")],2)):te("v-if",!0),st(L(p(xP),{ref_key:"cascaderPanelRef",ref:T,modelValue:Se.value,"onUpdate:modelValue":Ke[12]||(Ke[12]=Ge=>Se.value=Ge),options:e.options,props:r.props,border:!1,"render-label":Ie.$slots.default,"virtual-scroll":e.virtualScroll,"item-size":e.itemSize,height:e.height,onExpandChange:Ue,onClose:Ke[13]||(Ke[13]=Ge=>Ie.$nextTick(()=>me(!1)))},{empty:$(()=>[ce(Ie.$slots,"empty")]),_:3},8,["modelValue","options","props","render-label","virtual-scroll","item-size","height"]),[[Zt,!P.value]]),e.filterable?(x(),O(Ve,{key:1},[e.virtualScroll?st((x(),O("div",{key:1,ref_key:"suggestionPanel",ref:M,class:U(p(c).e("suggestion-panel")),onKeydown:It},[st(L(p(cp),{ref_key:"suggestionVirtualListRef",ref:E,height:W.value,"item-size":e.itemSize,data:H.value,total:H.value.length,"class-name":p(c).e("suggestion-list"),"inner-element":"ul","inner-width":z.value},{default:$(({data:Ge,index:Ee,style:de})=>[(x(),O("li",{id:`suggestion-${Ge[Ee].uid}`,key:Ge[Ee].uid,"data-suggestion-index":Ee,class:U([p(c).e("suggestion-item"),p(c).is("checked",Ge[Ee].checked)]),tabindex:-1,style:et(de),onClick:it=>ht(Ge[Ee])},[ce(Ie.$slots,"suggestion-item",{item:Ge[Ee]},()=>[C("span",null,Q(Ge[Ee].text),1),Ge[Ee].checked?(x(),ne(p(Xe),{key:0},{default:$(()=>[L(p(Os))]),_:1})):te("v-if",!0)])],14,Sfe))]),_:3},8,["height","item-size","data","total","class-name","inner-width"]),[[Zt,H.value.length]]),H.value.length?te("v-if",!0):ce(Ie.$slots,"empty",{key:0},()=>[C("ul",{class:U(p(c).e("suggestion-list"))},[C("li",{class:U(p(c).e("empty-text"))},Q(p(f)("el.cascader.noMatch")),3)],2)])],34)),[[Zt,P.value]]):st((x(),ne(p(ni),{key:0,ref:Ge=>M.value=Ge==null?void 0:Ge.$el,tag:"ul",class:U(p(c).e("suggestion-panel")),"wrap-class":p(c).e("suggestion-wrap"),"view-class":p(c).e("suggestion-list"),onKeydown:It},{default:$(()=>[H.value.length?(x(!0),O(Ve,{key:0},dt(H.value,Ge=>(x(),O("li",{key:Ge.uid,class:U([p(c).e("suggestion-item"),p(c).is("checked",Ge.checked)]),tabindex:-1,onClick:Ee=>ht(Ge)},[ce(Ie.$slots,"suggestion-item",{item:Ge},()=>[C("span",null,Q(Ge.text),1),Ge.checked?(x(),ne(p(Xe),{key:0},{default:$(()=>[L(p(Os))]),_:1})):te("v-if",!0)])],10,wfe))),128)):ce(Ie.$slots,"empty",{key:1},()=>[C("li",{class:U(p(c).e("empty-text"))},Q(p(f)("el.cascader.noMatch")),3)])]),_:3},8,["class","wrap-class","view-class"])),[[Zt,P.value]])],64)):te("v-if",!0),Ie.$slots.footer?(x(),O("div",{key:2,class:U(p(c).e("footer")),onClick:Ke[14]||(Ke[14]=at(()=>{},["stop"]))},[ce(Ie.$slots,"footer")],2)):te("v-if",!0)]),_:3},8,["visible","teleported","popper-class","popper-style","fallback-placements","placement","transition","effect","persistent"]))}}),kfe=xfe;const Tfe=wt(kfe),Mfe=$e({checked:Boolean,disabled:Boolean,type:{type:String,values:["primary","success","info","warning","danger"],default:"primary"}}),Efe={"update:checked":e=>vn(e),[Ot]:e=>vn(e)};var Afe=q({name:"ElCheckTag",__name:"check-tag",props:Mfe,emits:Efe,setup(e,{emit:t}){const n=e,a=t,r=ke("check-tag"),o=D(()=>[r.b(),r.is("checked",n.checked),r.is("disabled",n.disabled),r.m(n.type||"primary")]),i=()=>{if(n.disabled)return;const l=!n.checked;a(Ot,l),a("update:checked",l)};return(l,s)=>(x(),O("span",{class:U(o.value),onClick:i},[ce(l.$slots,"default")],2))}}),Ife=Afe;const Lfe=wt(Ife),Pfe=$e({tag:{type:String,default:"div"},span:{type:Number,default:24},offset:{type:Number,default:0},pull:{type:Number,default:0},push:{type:Number,default:0},xs:{type:ae([Number,Object]),default:()=>Nn({})},sm:{type:ae([Number,Object]),default:()=>Nn({})},md:{type:ae([Number,Object]),default:()=>Nn({})},lg:{type:ae([Number,Object]),default:()=>Nn({})},xl:{type:ae([Number,Object]),default:()=>Nn({})}}),Dfe=["start","center","end","space-around","space-between","space-evenly"],Ofe=["top","middle","bottom"],$fe=$e({tag:{type:String,default:"div"},gutter:{type:Number,default:0},justify:{type:String,values:Dfe,default:"start"},align:{type:String,values:Ofe}}),kP=Symbol("rowContextKey");var Rfe=q({name:"ElRow",__name:"row",props:$fe,setup(e){const t=e,n=ke("row");Ft(kP,{gutter:D(()=>t.gutter)});const a=D(()=>{const o={};return t.gutter&&(o.marginRight=o.marginLeft=`-${t.gutter/2}px`),o}),r=D(()=>[n.b(),n.is(`justify-${t.justify}`,t.justify!=="start"),n.is(`align-${t.align}`,!!t.align)]);return(o,i)=>(x(),ne(St(e.tag),{class:U(r.value),style:et(a.value)},{default:$(()=>[ce(o.$slots,"default")]),_:3},8,["class","style"]))}}),Nfe=Rfe;const Bfe=wt(Nfe);var Vfe=q({name:"ElCol",__name:"col",props:Pfe,setup(e){const t=e,{gutter:n}=Ze(kP,{gutter:D(()=>0)}),a=ke("col"),r=D(()=>{const i={};return n.value&&(i.paddingLeft=i.paddingRight=`${n.value/2}px`),i}),o=D(()=>{const i=[];return["span","offset","pull","push"].forEach(l=>{const s=t[l];Je(s)&&(l==="span"?i.push(a.b(`${t[l]}`)):s>0&&i.push(a.b(`${l}-${t[l]}`)))}),["xs","sm","md","lg","xl"].forEach(l=>{Je(t[l])?i.push(a.b(`${l}-${t[l]}`)):_t(t[l])&&Object.entries(t[l]).forEach(([s,u])=>{i.push(s!=="span"?a.b(`${l}-${s}-${u}`):a.b(`${l}-${u}`))})}),n.value&&i.push(a.is("guttered")),[a.b(),i]});return(i,l)=>(x(),ne(St(e.tag),{class:U(o.value),style:et(r.value)},{default:$(()=>[ce(i.$slots,"default")]),_:3},8,["class","style"]))}}),zfe=Vfe;const Ffe=wt(zfe),mx=e=>Je(e)||Ye(e)||Ae(e),Hfe=$e({accordion:Boolean,modelValue:{type:ae([Array,String,Number]),default:()=>Nn([])},expandIconPosition:{type:ae([String]),default:"right"},beforeCollapse:{type:ae(Function)}}),Wfe={[bt]:mx,[Ot]:mx},TP=Symbol("collapseContextKey"),Kfe=$e({title:{type:String,default:""},name:{type:ae([String,Number]),default:void 0},icon:{type:pn,default:za},disabled:Boolean});var Ufe=q({name:"ElCollapseTransition",__name:"collapse-transition",setup(e){const t=ke("collapse-transition"),n=r=>{r.style.maxHeight="",r.style.overflow=r.dataset.oldOverflow,r.style.paddingTop=r.dataset.oldPaddingTop,r.style.paddingBottom=r.dataset.oldPaddingBottom},a={beforeEnter(r){r.dataset||(r.dataset={}),r.dataset.oldPaddingTop=r.style.paddingTop,r.dataset.oldPaddingBottom=r.style.paddingBottom,r.style.height&&(r.dataset.elExistsHeight=r.style.height),r.style.maxHeight=0,r.style.paddingTop=0,r.style.paddingBottom=0},enter(r){requestAnimationFrame(()=>{r.dataset.oldOverflow=r.style.overflow,r.dataset.elExistsHeight?r.style.maxHeight=r.dataset.elExistsHeight:r.scrollHeight!==0?r.style.maxHeight=`${r.scrollHeight}px`:r.style.maxHeight=0,r.style.paddingTop=r.dataset.oldPaddingTop,r.style.paddingBottom=r.dataset.oldPaddingBottom,r.style.overflow="hidden"})},afterEnter(r){r.style.maxHeight="",r.style.overflow=r.dataset.oldOverflow},enterCancelled(r){n(r)},beforeLeave(r){r.dataset||(r.dataset={}),r.dataset.oldPaddingTop=r.style.paddingTop,r.dataset.oldPaddingBottom=r.style.paddingBottom,r.dataset.oldOverflow=r.style.overflow,r.style.maxHeight=`${r.scrollHeight}px`,r.style.overflow="hidden"},leave(r){r.scrollHeight!==0&&(r.style.maxHeight=0,r.style.paddingTop=0,r.style.paddingBottom=0)},afterLeave(r){n(r)},leaveCancelled(r){n(r)}};return(r,o)=>(x(),ne(Pa,Tt({name:p(t).b()},fz(a)),{default:$(()=>[ce(r.$slots,"default")]),_:3},16,["name"]))}}),Gfe=Ufe;const U1=wt(Gfe),gx="ElCollapse",Yfe=(e,t)=>{const n=F(wa(e.modelValue)),a=i=>{n.value=i;const l=e.accordion?n.value[0]:n.value;t(bt,l),t(Ot,l)},r=i=>{if(e.accordion)a([n.value[0]===i?"":i]);else{const l=[...n.value],s=l.indexOf(i);s>-1?l.splice(s,1):l.push(i),a(l)}},o=async i=>{const{beforeCollapse:l}=e;if(!l){r(i);return}const s=l(i);[Ls(s),vn(s)].includes(!0)||Dn(gx,"beforeCollapse must return type `Promise` or `boolean`"),Ls(s)?s.then(u=>{u!==!1&&r(i)}).catch(u=>{un(gx,`some error occurred: ${u}`)}):s&&r(i)};return ye(()=>e.modelValue,()=>n.value=wa(e.modelValue),{deep:!0}),Ft(TP,{activeNames:n,handleItemClick:o}),{activeNames:n,setActiveNames:a}},jfe=e=>{const t=ke("collapse");return{rootKls:D(()=>[t.b(),t.b(`icon-position-${e.expandIconPosition}`)])}};var qfe=q({name:"ElCollapse",__name:"collapse",props:Hfe,emits:Wfe,setup(e,{expose:t,emit:n}){const a=e,{activeNames:r,setActiveNames:o}=Yfe(a,n),{rootKls:i}=jfe(a);return t({activeNames:r,setActiveNames:o}),(l,s)=>(x(),O("div",{class:U(p(i))},[ce(l.$slots,"default")],2))}}),Xfe=qfe;const Zfe=e=>{const t=Ze(TP),{namespace:n}=ke("collapse"),a=F(!1),r=F(!1),o=$6(),i=D(()=>o.current++),l=D(()=>e.name??`${n.value}-id-${o.prefix}-${p(i)}`),s=D(()=>t==null?void 0:t.activeNames.value.includes(p(l)));return{focusing:a,id:i,isActive:s,handleFocus:()=>{setTimeout(()=>{r.value?r.value=!1:a.value=!0},50)},handleHeaderClick:v=>{var f;e.disabled||(f=v.target)!=null&&f.closest("input, textarea, select")||(t==null||t.handleItemClick(p(l)),a.value=!1,r.value=!0)},handleEnterClick:v=>{var f;(f=v.target)!=null&&f.closest("input, textarea, select")||(v.preventDefault(),t==null||t.handleItemClick(p(l)))}}},Jfe=(e,{focusing:t,isActive:n,id:a})=>{const r=ke("collapse"),o=D(()=>[r.b("item"),r.is("active",p(n)),r.is("disabled",e.disabled)]),i=D(()=>[r.be("item","header"),r.is("active",p(n)),{focusing:p(t)&&!e.disabled}]),l=D(()=>[r.be("item","arrow"),r.is("active",p(n))]);return{itemTitleKls:D(()=>[r.be("item","title")]),arrowKls:l,headKls:i,rootKls:o,itemWrapperKls:D(()=>r.be("item","wrap")),itemContentKls:D(()=>r.be("item","content")),scopedContentId:D(()=>r.b(`content-${p(a)}`)),scopedHeadId:D(()=>r.b(`head-${p(a)}`))}},Qfe=["id","aria-expanded","aria-controls","aria-describedby","tabindex","aria-disabled"],eve=["id","aria-hidden","aria-labelledby"];var tve=q({name:"ElCollapseItem",__name:"collapse-item",props:Kfe,setup(e,{expose:t}){const n=e,{focusing:a,id:r,isActive:o,handleFocus:i,handleHeaderClick:l,handleEnterClick:s}=Zfe(n),{arrowKls:u,headKls:c,rootKls:d,itemTitleKls:v,itemWrapperKls:f,itemContentKls:h,scopedContentId:m,scopedHeadId:g}=Jfe(n,{focusing:a,isActive:o,id:r});return t({isActive:o}),(y,b)=>(x(),O("div",{class:U(p(d))},[C("div",{id:p(g),class:U(p(c)),"aria-expanded":p(o),"aria-controls":p(m),"aria-describedby":p(m),tabindex:e.disabled?void 0:0,"aria-disabled":e.disabled,role:"button",onClick:b[0]||(b[0]=(..._)=>p(l)&&p(l)(..._)),onKeydown:b[1]||(b[1]=kn(at((..._)=>p(s)&&p(s)(..._),["stop"]),["space","enter"])),onFocus:b[2]||(b[2]=(..._)=>p(i)&&p(i)(..._)),onBlur:b[3]||(b[3]=_=>a.value=!1)},[C("span",{class:U(p(v))},[ce(y.$slots,"title",{isActive:p(o)},()=>[ue(Q(e.title),1)])],2),ce(y.$slots,"icon",{isActive:p(o)},()=>[L(p(Xe),{class:U(p(u))},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1},8,["class"])])],42,Qfe),L(p(U1),null,{default:$(()=>[st(C("div",{id:p(m),role:"region",class:U(p(f)),"aria-hidden":!p(o),"aria-labelledby":p(g)},[C("div",{class:U(p(h))},[ce(y.$slots,"default")],2)],10,eve),[[Zt,p(o)]])]),_:3})],2))}}),MP=tve;const nve=wt(Xfe,{CollapseItem:MP}),ave=On(MP),EP=$e({modelValue:{type:ae(String),default:void 0},border:{type:Boolean,default:!0},showAlpha:Boolean,colorFormat:{type:ae(String)},disabled:Boolean,predefine:{type:ae(Array)},validateEvent:{type:Boolean,default:!0},hueSliderClass:{type:ae([String,Array,Object,Boolean]),default:void 0},hueSliderStyle:{type:ae([String,Array,Object,Boolean]),default:void 0}}),rve={[bt]:e=>Ye(e)||Un(e)},AP=Symbol("colorCommonPickerKey"),IP=Symbol("colorPickerPanelContextKey"),LP=$e({color:{type:ae(Object),required:!0},vertical:Boolean,disabled:Boolean}),ove=LP,yx=e=>{let t=0,n=e;for(;n;)t+=n.offsetTop,n=n.offsetParent;return t},q4=(e,t)=>Math.abs(yx(e)-yx(t)),sg=e=>{let t,n;return e.type==="touchend"?(n=e.changedTouches[0].clientY,t=e.changedTouches[0].clientX):e.type.startsWith("touch")?(n=e.touches[0].clientY,t=e.touches[0].clientX):(n=e.clientY,t=e.clientX),{clientX:t,clientY:n}};let ry=!1;function X4(e,t){if(!qt)return;const n=function(o){var i;(i=t.drag)==null||i.call(t,o)},a=function(o){var i;document.removeEventListener("mousemove",n),document.removeEventListener("mouseup",a),document.removeEventListener("touchmove",n),document.removeEventListener("touchend",a),document.onselectstart=null,document.ondragstart=null,ry=!1,(i=t.end)==null||i.call(t,o)},r=function(o){var i;ry||(document.onselectstart=()=>!1,document.ondragstart=()=>!1,document.addEventListener("mousemove",n),document.addEventListener("mouseup",a),document.addEventListener("touchmove",n),document.addEventListener("touchend",a),ry=!0,(i=t.start)==null||i.call(t,o))};e.addEventListener("mousedown",r),e.addEventListener("touchstart",r,{passive:!1})}const PP=(e,{key:t,minValue:n,maxValue:a})=>{const r=Et(),o=Ht(),i=Ht(),l=D(()=>e.color.get(t));function s(v){var f;e.disabled||(v.target!==o.value&&u(v),(f=o.value)==null||f.focus())}function u(v){if(!i.value||!o.value||e.disabled)return;const f=r.vnode.el.getBoundingClientRect(),{clientX:h,clientY:m}=sg(v);let g;if(e.vertical){let y=m-f.top;y=Math.max(o.value.offsetHeight/2,y),y=Math.min(y,f.height-o.value.offsetHeight/2),g=Math.round((y-o.value.offsetHeight/2)/(f.height-o.value.offsetHeight)*a)}else{let y=h-f.left;y=Math.max(o.value.offsetWidth/2,y),y=Math.min(y,f.width-o.value.offsetWidth/2),g=Math.round((y-o.value.offsetWidth/2)/(f.width-o.value.offsetWidth)*a)}e.color.set(t,g)}function c(v){if(e.disabled)return;const{shiftKey:f}=v,h=mn(v),m=f?10:1,g=t==="hue"?-1:1;let y=!0;switch(h){case Pe.left:case Pe.down:d(-m*g);break;case Pe.right:case Pe.up:d(m*g);break;case Pe.home:e.color.set(t,t==="hue"?a:n);break;case Pe.end:e.color.set(t,t==="hue"?n:a);break;case Pe.pageDown:d(-4*g);break;case Pe.pageUp:d(4*g);break;default:y=!1;break}y&&v.preventDefault()}function d(v){let f=l.value+v;f=fa?a:f,e.color.set(t,f)}return{thumb:o,bar:i,currentValue:l,handleDrag:u,handleClick:s,handleKeydown:c}},DP=(e,{namespace:t,maxValue:n,bar:a,thumb:r,currentValue:o,handleDrag:i,getBackground:l})=>{const s=Et(),u=ke(t),c=F(0),d=F(0),v=F();function f(){if(!r.value||e.vertical)return 0;const _=s.vnode.el,w=o.value;return _?Math.round(w*(_.offsetWidth-r.value.offsetWidth/2)/n):0}function h(){if(!r.value)return 0;const _=s.vnode.el;if(!e.vertical)return 0;const w=o.value;return _?Math.round(w*(_.offsetHeight-r.value.offsetHeight/2)/n):0}function m(){c.value=f(),d.value=h(),v.value=l==null?void 0:l()}mt(()=>{if(!a.value||!r.value)return;const _={drag:w=>{i(w)},end:w=>{i(w)}};X4(a.value,_),X4(r.value,_),m()}),ye(o,()=>m()),ye(()=>e.color.value,()=>m());const g=D(()=>[u.b(),u.is("vertical",e.vertical),u.is("disabled",e.disabled)]),y=D(()=>u.e("bar")),b=D(()=>u.e("thumb"));return{rootKls:g,barKls:y,barStyle:D(()=>({background:v.value})),thumbKls:b,thumbStyle:D(()=>({left:Vn(c.value),top:Vn(d.value)})),thumbLeft:c,thumbTop:d,update:m}},ive=["aria-label","aria-valuenow","aria-valuetext","aria-orientation","tabindex","aria-disabled"],bx=0,oy=100;var lve=q({name:"ElColorAlphaSlider",__name:"alpha-slider",props:LP,setup(e,{expose:t}){const n=e,{currentValue:a,bar:r,thumb:o,handleDrag:i,handleClick:l,handleKeydown:s}=PP(n,{key:"alpha",minValue:bx,maxValue:oy}),{rootKls:u,barKls:c,barStyle:d,thumbKls:v,thumbStyle:f,update:h}=DP(n,{namespace:"color-alpha-slider",maxValue:oy,currentValue:a,bar:r,thumb:o,handleDrag:i,getBackground:b}),{t:m}=Vt(),g=D(()=>m("el.colorpicker.alphaLabel")),y=D(()=>m("el.colorpicker.alphaDescription",{alpha:a.value,color:n.color.value}));function b(){if(n.color&&n.color.value){const{r:_,g:w,b:S}=n.color.toRgb();return`linear-gradient(to right, rgba(${_}, ${w}, ${S}, 0) 0%, rgba(${_}, ${w}, ${S}, 1) 100%)`}return""}return t({update:h,bar:r,thumb:o}),(_,w)=>(x(),O("div",{class:U(p(u))},[C("div",{ref_key:"bar",ref:r,class:U(p(c)),style:et(p(d)),onClick:w[0]||(w[0]=(...S)=>p(l)&&p(l)(...S))},null,6),C("div",{ref_key:"thumb",ref:o,class:U(p(v)),style:et(p(f)),"aria-label":g.value,"aria-valuenow":p(a),"aria-valuetext":y.value,"aria-orientation":e.vertical?"vertical":"horizontal","aria-valuemin":bx,"aria-valuemax":oy,role:"slider",tabindex:e.disabled?void 0:0,"aria-disabled":e.disabled,onKeydown:w[1]||(w[1]=(...S)=>p(s)&&p(s)(...S))},null,46,ive)],2))}}),sve=lve;const uve=["aria-label","aria-valuenow","aria-valuetext","aria-orientation","tabindex","aria-disabled"],_x=0,iy=360;var cve=q({name:"ElColorHueSlider",__name:"hue-slider",props:ove,setup(e,{expose:t}){const n=e,{currentValue:a,bar:r,thumb:o,handleDrag:i,handleClick:l,handleKeydown:s}=PP(n,{key:"hue",minValue:_x,maxValue:iy}),{rootKls:u,barKls:c,thumbKls:d,thumbStyle:v,thumbTop:f,update:h}=DP(n,{namespace:"color-hue-slider",maxValue:iy,currentValue:a,bar:r,thumb:o,handleDrag:i}),{t:m}=Vt(),g=D(()=>m("el.colorpicker.hueLabel")),y=D(()=>m("el.colorpicker.hueDescription",{hue:a.value,color:n.color.value}));return t({bar:r,thumb:o,thumbTop:f,update:h}),(b,_)=>(x(),O("div",{class:U(p(u))},[C("div",{ref_key:"bar",ref:r,class:U(p(c)),onClick:_[0]||(_[0]=(...w)=>p(l)&&p(l)(...w))},null,2),C("div",{ref_key:"thumb",ref:o,class:U(p(d)),style:et(p(v)),"aria-label":g.value,"aria-valuenow":p(a),"aria-valuetext":y.value,"aria-orientation":e.vertical?"vertical":"horizontal","aria-valuemin":_x,"aria-valuemax":iy,role:"slider",tabindex:e.disabled?void 0:0,"aria-disabled":e.disabled,onKeydown:_[1]||(_[1]=(...w)=>p(s)&&p(s)(...w))},null,46,uve)],2))}}),dve=cve;const fve=$e({colors:{type:ae(Array),required:!0},color:{type:ae(Object),required:!0},enableAlpha:{type:Boolean,required:!0},disabled:Boolean});var ug=class{constructor(e={}){this._hue=0,this._saturation=100,this._value=100,this._alpha=100,this._tiny=new ea,this._isValid=!1,this.enableAlpha=!1,this.format="",this.value="";for(const t in e)en(e,t)&&(this[t]=e[t]);e.value?this.fromString(e.value):this.doOnChange()}set(e,t){if(arguments.length===1&&typeof e=="object"){for(const n in e)en(e,n)&&this.set(n,e[n]);return}this[`_${e}`]=t,this._isValid=!0,this.doOnChange()}get(e){return["hue","saturation","value","alpha"].includes(e)?Math.round(this[`_${e}`]):this[`_${e}`]}toRgb(){return this._isValid?this._tiny.toRgb():{r:255,g:255,b:255,a:0}}fromString(e){const t=new ea(e);if(this._isValid=t.isValid,t.isValid){const{h:n,s:a,v:r,a:o}=t.toHsv();this._hue=n,this._saturation=a*100,this._value=r*100,this._alpha=o*100}else this._hue=0,this._saturation=100,this._value=100,this._alpha=100;this.doOnChange()}clear(){this._isValid=!1,this.value="",this._hue=0,this._saturation=100,this._value=100,this._alpha=100}compare(e){const t=new ea({h:e._hue,s:e._saturation/100,v:e._value/100,a:e._alpha/100});return this._tiny.equals(t)}doOnChange(){const{_hue:e,_saturation:t,_value:n,_alpha:a,format:r,enableAlpha:o}=this;let i=r||(o?"rgb":"hex");r==="hex"&&o&&(i="hex8"),this._tiny=new ea({h:e,s:t/100,v:n/100,a:a/100}),this.value=this._isValid?this._tiny.toString(i):""}};const vve=e=>{const{currentColor:t}=Ze(IP),n=F(r(e.colors,e.color));ye(()=>t.value,o=>{const i=new ug({value:o,enableAlpha:e.enableAlpha});n.value.forEach(l=>{l.selected=i.compare(l)})}),ur(()=>{n.value=r(e.colors,e.color)});function a(o){e.color.fromString(e.colors[o])}function r(o,i){return o.map(l=>{const s=new ug({value:l,enableAlpha:e.enableAlpha});return s.selected=s.compare(i),s})}return{rgbaColors:n,handleSelect:a}},hve=e=>{const t=ke("color-predefine"),n=D(()=>[t.b(),t.is("disabled",e.disabled)]),a=D(()=>t.e("colors"));function r(o){return[t.e("color-selector"),t.is("alpha",o.get("alpha")<100),{selected:o.selected}]}return{rootKls:n,colorsKls:a,colorSelectorKls:r}},pve=["disabled","aria-label","onClick"];var mve=q({name:"ElColorPredefine",__name:"predefine",props:fve,setup(e){const t=e,{rgbaColors:n,handleSelect:a}=vve(t),{rootKls:r,colorsKls:o,colorSelectorKls:i}=hve(t),{t:l}=Vt(),s=u=>l("el.colorpicker.predefineDescription",{value:u});return(u,c)=>(x(),O("div",{class:U(p(r))},[C("div",{class:U(p(o))},[(x(!0),O(Ve,null,dt(p(n),(d,v)=>(x(),O("button",{key:e.colors[v],type:"button",disabled:e.disabled,"aria-label":s(d.value),class:U(p(i)(d)),onClick:f=>p(a)(v)},[C("div",{style:et({backgroundColor:d.value})},null,4)],10,pve))),128))],2)],2))}}),gve=mve;const yve=$e({color:{type:ae(Object),required:!0},disabled:Boolean}),bve=e=>{const t=Et(),n=F(),a=F(0),r=F(0),o=F("hsl(0, 100%, 50%)"),i=D(()=>e.color.get("saturation")),l=D(()=>e.color.get("value")),s=D(()=>e.color.get("hue"));function u(h){var m;e.disabled||(h.target!==n.value&&c(h),(m=n.value)==null||m.focus({preventScroll:!0}))}function c(h){if(e.disabled)return;const m=t.vnode.el.getBoundingClientRect(),{clientX:g,clientY:y}=sg(h);let b=g-m.left,_=y-m.top;b=Math.max(0,b),b=Math.min(b,m.width),_=Math.max(0,_),_=Math.min(_,m.height),r.value=b,a.value=_,e.color.set({saturation:b/m.width*100,value:100-_/m.height*100})}function d(h){if(e.disabled)return;const{shiftKey:m}=h,g=mn(h),y=m?10:1;let b=!0;switch(g){case Pe.left:v(-y);break;case Pe.right:v(y);break;case Pe.up:f(y);break;case Pe.down:f(-y);break;default:b=!1;break}b&&h.preventDefault()}function v(h){let m=i.value+h;m=m<0?0:m>100?100:m,e.color.set("saturation",m)}function f(h){let m=l.value+h;m=m<0?0:m>100?100:m,e.color.set("value",m)}return{cursorRef:n,cursorTop:a,cursorLeft:r,background:o,saturation:i,brightness:l,hue:s,handleClick:u,handleDrag:c,handleKeydown:d}},_ve=(e,{cursorTop:t,cursorLeft:n,background:a,handleDrag:r})=>{const o=Et(),i=ke("color-svpanel");function l(){const s=e.color.get("saturation"),u=e.color.get("value"),{clientWidth:c,clientHeight:d}=o.vnode.el;n.value=s*c/100,t.value=(100-u)*d/100,a.value=`hsl(${e.color.get("hue")}, 100%, 50%)`}return mt(()=>{X4(o.vnode.el,{drag:s=>{r(s)},end:s=>{r(s)}}),l()}),ye([()=>e.color.get("hue"),()=>e.color.get("value"),()=>e.color.value],()=>l()),{rootKls:D(()=>i.b()),cursorKls:D(()=>i.e("cursor")),rootStyle:D(()=>({backgroundColor:a.value})),cursorStyle:D(()=>({top:Vn(t.value),left:Vn(n.value)})),update:l}},wve=["tabindex","aria-disabled","aria-label","aria-valuenow","aria-valuetext"];var Sve=q({name:"ElSvPanel",__name:"sv-panel",props:yve,setup(e,{expose:t}){const n=e,{cursorRef:a,cursorTop:r,cursorLeft:o,background:i,saturation:l,brightness:s,handleClick:u,handleDrag:c,handleKeydown:d}=bve(n),{rootKls:v,cursorKls:f,rootStyle:h,cursorStyle:m,update:g}=_ve(n,{cursorTop:r,cursorLeft:o,background:i,handleDrag:c}),{t:y}=Vt(),b=D(()=>y("el.colorpicker.svLabel")),_=D(()=>y("el.colorpicker.svDescription",{saturation:l.value,brightness:s.value,color:n.color.value}));return t({update:g}),(w,S)=>(x(),O("div",{class:U(p(v)),style:et(p(h)),onClick:S[1]||(S[1]=(...k)=>p(u)&&p(u)(...k))},[C("div",{ref_key:"cursorRef",ref:a,class:U(p(f)),style:et(p(m)),tabindex:e.disabled?void 0:0,"aria-disabled":e.disabled,role:"slider","aria-valuemin":"0,0","aria-valuemax":"100,100","aria-label":b.value,"aria-valuenow":`${p(l)},${p(s)}`,"aria-valuetext":_.value,onKeydown:S[0]||(S[0]=(...k)=>p(d)&&p(d)(...k))},null,46,wve)],6))}}),Cve=Sve;const OP=(e,t)=>{const n=nn(new ug({enableAlpha:e.showAlpha,format:e.colorFormat||"",value:e.modelValue}));return ye(()=>[e.colorFormat,e.showAlpha],()=>{n.enableAlpha=e.showAlpha,n.format=e.colorFormat||n.format,n.doOnChange(),t(bt,n.value)}),{color:n}};var xve=q({name:"ElColorPickerPanel",__name:"color-picker-panel",props:EP,emits:rve,setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("color-picker-panel"),{formItem:i}=Ea(),l=$n(),s=F(),u=F(),c=F(),d=F(),v=F(""),{color:f}=Ze(AP,()=>OP(a,r),!0);function h(){f.fromString(v.value),f.value!==v.value&&(v.value=f.value)}function m(){var y;a.validateEvent&&((y=i==null?void 0:i.validate)==null||y.call(i,"blur").catch(kt))}function g(){var y,b,_;(y=s.value)==null||y.update(),(b=u.value)==null||b.update(),(_=c.value)==null||_.update()}return mt(()=>{a.modelValue&&(v.value=f.value),He(g)}),ye(()=>a.modelValue,y=>{y!==f.value&&(y?f.fromString(y):f.clear())}),ye(()=>f.value,y=>{r(bt,y),v.value=y,a.validateEvent&&(i==null||i.validate("change").catch(kt))}),Ft(IP,{currentColor:D(()=>f.value)}),t({color:f,inputRef:d,update:g}),(y,b)=>(x(),O("div",{class:U([p(o).b(),p(o).is("disabled",p(l)),p(o).is("border",e.border)]),onFocusout:m},[C("div",{class:U(p(o).e("wrapper"))},[L(dve,{ref_key:"hueRef",ref:s,color:p(f),vertical:"",disabled:p(l),class:U(["hue-slider",e.hueSliderClass]),style:et(e.hueSliderStyle)},null,8,["color","disabled","class","style"]),L(Cve,{ref_key:"svRef",ref:u,color:p(f),disabled:p(l)},null,8,["color","disabled"])],2),e.showAlpha?(x(),ne(sve,{key:0,ref_key:"alphaRef",ref:c,color:p(f),disabled:p(l)},null,8,["color","disabled"])):te("v-if",!0),e.predefine?(x(),ne(gve,{key:1,ref:"predefine","enable-alpha":e.showAlpha,color:p(f),colors:e.predefine,disabled:p(l)},null,8,["enable-alpha","color","colors","disabled"])):te("v-if",!0),C("div",{class:U(p(o).e("footer"))},[L(p(Ba),{ref_key:"inputRef",ref:d,modelValue:v.value,"onUpdate:modelValue":b[0]||(b[0]=_=>v.value=_),"validate-event":!1,size:"small",disabled:p(l),onChange:h},null,8,["modelValue","disabled"]),ce(y.$slots,"footer")],2)],34))}}),kve=xve;const $P=wt(kve),Tve=$e({persistent:{type:Boolean,default:!0},modelValue:{type:ae(String),default:void 0},id:String,showAlpha:Boolean,colorFormat:{type:ae(String)},disabled:{type:Boolean,default:void 0},clearable:{type:Boolean,default:!0},size:pa,popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,tabindex:{type:[String,Number],default:0},teleported:Tn.teleported,appendTo:Tn.appendTo,predefine:{type:ae(Array)},validateEvent:{type:Boolean,default:!0},...Mc,...dr(["ariaLabel"])}),Mve={[bt]:e=>Ye(e)||Un(e),[Ot]:e=>Ye(e)||Un(e),activeChange:e=>Ye(e)||Un(e),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0},Eve=["id","aria-label","aria-labelledby","aria-description","aria-disabled","tabindex"];var Ave=q({name:"ElColorPicker",__name:"color-picker",props:Tve,emits:Mve,setup(e,{expose:t,emit:n}){const a=e,r=n,{t:o}=Vt(),i=ke("color"),{formItem:l}=Ea(),s=ca(),u=$n(),{valueOnClear:c,isEmptyValue:d}=op(a,null),v=OP(a,r),{inputId:f,isLabeledByFormItem:h}=Yr(a,{formItemContext:l}),m=F(),g=F(),y=F(),b=F(!1),_=F(!1);let w=!0;const{isFocused:S,handleFocus:k,handleBlur:T}=Fl(g,{disabled:u,beforeBlur(J){var Z;return(Z=m.value)==null?void 0:Z.isFocusInsideContent(J)},afterBlur(){var J;H(!1),N(),a.validateEvent&&((J=l==null?void 0:l.validate)==null||J.call(l,"blur").catch(kt))}}),M=yY(()=>{var J;return((J=y.value)==null?void 0:J.color)??v.color}),E=D(()=>ji(a,Object.keys(EP))),A=D(()=>!a.modelValue&&!_.value?"transparent":K(M,a.showAlpha)),I=D(()=>!a.modelValue&&!_.value?"":M.value),P=D(()=>h.value?void 0:a.ariaLabel||o("el.colorpicker.defaultLabel")),R=D(()=>h.value?l==null?void 0:l.labelId:void 0),G=D(()=>[i.b("picker"),i.is("disabled",u.value),i.bm("picker",s.value),i.is("focused",S.value)]);function K(J,Z){const{r:j,g:oe,b:se,a:fe}=J.toRgb();return Z?`rgba(${j}, ${oe}, ${se}, ${fe})`:`rgb(${j}, ${oe}, ${se})`}function H(J){b.value=J}const z=Yi(H,100,{leading:!0});function B(){u.value||H(!0)}function W(){z(!1),N()}function N(){He(()=>{a.modelValue?M.fromString(a.modelValue):(M.value="",He(()=>{_.value=!1}))})}function V(){u.value||(b.value&&N(),z(!b.value))}function Y(){const J=d(M.value)?c.value:M.value;r(bt,J),r(Ot,J),a.validateEvent&&(l==null||l.validate("change").catch(kt)),z(!1),He(()=>{const Z=new ug({enableAlpha:a.showAlpha,format:a.colorFormat||"",value:a.modelValue});M.compare(Z)||N()})}function X(){z(!1),r(bt,c.value),r(Ot,c.value),a.modelValue!==c.value&&a.validateEvent&&(l==null||l.validate("change").catch(kt)),N(),r("clear")}function ee(){var J,Z;(Z=(J=y==null?void 0:y.value)==null?void 0:J.inputRef)==null||Z.focus()}function re(){b.value&&(W(),S.value&&ve())}function ie(J){J.preventDefault(),J.stopPropagation(),H(!1),N()}function le(J){switch(mn(J)){case Pe.enter:case Pe.numpadEnter:case Pe.space:J.preventDefault(),J.stopPropagation(),B();break;case Pe.esc:ie(J);break}}function ve(){g.value.focus()}function he(){g.value.blur()}return ye(()=>I.value,J=>{w&&r("activeChange",J),w=!0}),ye(()=>M.value,()=>{!a.modelValue&&!_.value&&(_.value=!0)}),ye(()=>a.modelValue,J=>{J?J&&J!==M.value&&(w=!1,M.fromString(J)):_.value=!1}),ye(()=>b.value,()=>{y.value&&He(y.value.update)}),Ft(AP,v),t({color:M,show:B,hide:W,focus:ve,blur:he}),(J,Z)=>(x(),ne(p(Ma),{ref_key:"popper",ref:m,visible:b.value,"show-arrow":!1,"fallback-placements":["bottom","top","right","left"],offset:0,"gpu-acceleration":!1,"popper-class":[p(i).be("picker","panel"),e.popperClass],"popper-style":e.popperStyle,"stop-popper-mouse-event":!1,pure:"",loop:"",role:"dialog",effect:"light",trigger:"click",teleported:e.teleported,transition:`${p(i).namespace.value}-zoom-in-top`,persistent:e.persistent,"append-to":e.appendTo,onShow:ee,onHide:Z[2]||(Z[2]=j=>H(!1))},{content:$(()=>[st((x(),ne(p($P),Tt({ref_key:"pickerPanelRef",ref:y},E.value,{border:!1,"validate-event":!1,onKeydown:kn(ie,["esc"])}),{footer:$(()=>[C("div",null,[e.clearable?(x(),ne(p(xa),{key:0,class:U(p(i).be("footer","link-btn")),text:"",size:"small",onClick:X},{default:$(()=>[ue(Q(p(o)("el.colorpicker.clear")),1)]),_:1},8,["class"])):te("v-if",!0),L(p(xa),{plain:"",size:"small",class:U(p(i).be("footer","btn")),onClick:Y},{default:$(()=>[ue(Q(p(o)("el.colorpicker.confirm")),1)]),_:1},8,["class"])])]),_:1},16)),[[p(Ns),re,g.value]])]),default:$(()=>[C("div",Tt({id:p(f),ref_key:"triggerRef",ref:g},J.$attrs,{class:G.value,role:"button","aria-label":P.value,"aria-labelledby":R.value,"aria-description":p(o)("el.colorpicker.description",{color:e.modelValue||""}),"aria-disabled":p(u),tabindex:p(u)?void 0:e.tabindex,onKeydown:le,onFocus:Z[0]||(Z[0]=(...j)=>p(k)&&p(k)(...j)),onBlur:Z[1]||(Z[1]=(...j)=>p(T)&&p(T)(...j))}),[C("div",{class:U(p(i).be("picker","trigger")),onClick:V},[C("span",{class:U([p(i).be("picker","color"),p(i).is("alpha",e.showAlpha)])},[C("span",{class:U(p(i).be("picker","color-inner")),style:et({backgroundColor:A.value})},[st(L(p(Xe),{class:U([p(i).be("picker","icon"),p(i).is("icon-arrow-down")])},{default:$(()=>[L(p(ui))]),_:1},8,["class"]),[[Zt,e.modelValue||_.value]]),st(L(p(Xe),{class:U([p(i).be("picker","empty"),p(i).is("icon-close")])},{default:$(()=>[L(p(so))]),_:1},8,["class"]),[[Zt,!e.modelValue&&!_.value]])],6)],2)],2)],16,Eve)]),_:1},8,["visible","popper-class","popper-style","teleported","transition","persistent","append-to"]))}}),Ive=Ave;const Lve=wt(Ive);var Pve=q({name:"ElContainer",__name:"container",props:{direction:{type:String,required:!1}},setup(e){const t=e,n=Xn(),a=ke("container"),r=D(()=>t.direction==="vertical"?!0:t.direction==="horizontal"?!1:n&&n.default?n.default().some(o=>{const i=o.type.name;return i==="ElHeader"||i==="ElFooter"}):!1);return(o,i)=>(x(),O("section",{class:U([p(a).b(),p(a).is("vertical",r.value)])},[ce(o.$slots,"default")],2))}}),Dve=Pve,Ove=q({name:"ElAside",__name:"aside",props:{width:{type:[String,null],required:!1,default:null}},setup(e){const t=e,n=ke("aside"),a=D(()=>t.width?n.cssVarBlock({width:t.width}):{});return(r,o)=>(x(),O("aside",{class:U(p(n).b()),style:et(a.value)},[ce(r.$slots,"default")],6))}}),RP=Ove,$ve=q({name:"ElFooter",__name:"footer",props:{height:{type:[String,null],required:!1,default:null}},setup(e){const t=e,n=ke("footer"),a=D(()=>t.height?n.cssVarBlock({height:t.height}):{});return(r,o)=>(x(),O("footer",{class:U(p(n).b()),style:et(a.value)},[ce(r.$slots,"default")],6))}}),NP=$ve,Rve=q({name:"ElHeader",__name:"header",props:{height:{type:[String,null],required:!1,default:null}},setup(e){const t=e,n=ke("header"),a=D(()=>t.height?n.cssVarBlock({height:t.height}):{});return(r,o)=>(x(),O("header",{class:U(p(n).b()),style:et(a.value)},[ce(r.$slots,"default")],6))}}),BP=Rve,Nve=q({name:"ElMain",__name:"main",setup(e){const t=ke("main");return(n,a)=>(x(),O("main",{class:U(p(t).b())},[ce(n.$slots,"default")],2))}}),VP=Nve;const Bve=wt(Dve,{Aside:RP,Footer:NP,Header:BP,Main:VP}),Vve=On(RP),zve=On(NP),Fve=On(BP),Hve=On(VP),Wve=$e({format:{type:String,default:"HH:mm:ss"},prefix:String,suffix:String,title:String,value:{type:ae([Number,Object]),default:0},valueStyle:{type:ae([String,Object,Array,Boolean]),default:void 0}}),Kve={finish:()=>!0,[Ot]:e=>Je(e)},Uve=$e({decimalSeparator:{type:String,default:"."},groupSeparator:{type:String,default:","},precision:{type:Number,default:0},formatter:Function,value:{type:ae([Number,Object]),default:0},prefix:String,suffix:String,title:String,valueStyle:{type:ae([String,Object,Array,Boolean]),default:void 0}});var Gve=q({name:"ElStatistic",__name:"statistic",props:Uve,setup(e,{expose:t}){const n=e,a=ke("statistic"),r=D(()=>{const{value:o,formatter:i,precision:l,decimalSeparator:s,groupSeparator:u}=n;if(Qe(i))return i(o);if(!Je(o)||Number.isNaN(o))return o;let[c,d=""]=String(o).split(".");return d=d.padEnd(l,"0").slice(0,l>0?l:0),c=c.replace(/\B(?=(\d{3})+(?!\d))/g,u),[c,d].join(d?s:"")});return t({displayValue:r}),(o,i)=>(x(),O("div",{class:U(p(a).b())},[o.$slots.title||e.title?(x(),O("div",{key:0,class:U(p(a).e("head"))},[ce(o.$slots,"title",{},()=>[ue(Q(e.title),1)])],2)):te("v-if",!0),C("div",{class:U(p(a).e("content"))},[o.$slots.prefix||e.prefix?(x(),O("div",{key:0,class:U(p(a).e("prefix"))},[ce(o.$slots,"prefix",{},()=>[C("span",null,Q(e.prefix),1)])],2)):te("v-if",!0),C("span",{class:U(p(a).e("number")),style:et(e.valueStyle)},Q(r.value),7),o.$slots.suffix||e.suffix?(x(),O("div",{key:1,class:U(p(a).e("suffix"))},[ce(o.$slots,"suffix",{},()=>[C("span",null,Q(e.suffix),1)])],2)):te("v-if",!0)],2)],2))}}),Yve=Gve;const zP=wt(Yve),jve=[["Y",1e3*60*60*24*365],["M",1e3*60*60*24*30],["D",1e3*60*60*24],["H",1e3*60*60],["m",1e3*60],["s",1e3],["S",1]],Sx=e=>Je(e)?new Date(e).getTime():e.valueOf(),Cx=(e,t)=>{let n=e;return jve.reduce((a,[r,o])=>{const i=new RegExp(`${r}+(?![^\\[\\]]*\\])`,"g");if(i.test(a)){const l=Math.floor(n/o);return n-=l*o,a.replace(i,s=>String(l).padStart(s.length,"0"))}return a},t).replace(/\[([^\]]*)]/g,"$1")};var qve=q({name:"ElCountdown",__name:"countdown",props:Wve,emits:Kve,setup(e,{expose:t,emit:n}){const a=e,r=n;let o;const i=F(0),l=D(()=>Cx(i.value,a.format)),s=d=>Cx(d,a.format),u=()=>{o&&(ti(o),o=void 0)},c=()=>{const d=Sx(a.value),v=()=>{let f=d-Date.now();r(Ot,f),f<=0?(f=0,u(),r("finish")):o=Wr(v),i.value=f};o=Wr(v)};return mt(()=>{i.value=Sx(a.value)-Date.now(),ye(()=>[a.value,a.format],()=>{u(),c()},{immediate:!0})}),rn(()=>{u()}),t({displayValue:l}),(d,v)=>(x(),ne(p(zP),{value:i.value,title:e.title,prefix:e.prefix,suffix:e.suffix,"value-style":e.valueStyle,formatter:s},xr({_:2},[dt(d.$slots,(f,h)=>({name:h,fn:$(()=>[ce(d.$slots,h)])}))]),1032,["value","title","prefix","suffix","value-style"]))}}),Xve=qve;const Zve=wt(Xve),Jve=$e({valueFormat:String,dateFormat:String,timeFormat:String,disabled:{type:Boolean,default:void 0},modelValue:{type:ae([Date,Array,String,Number]),default:""},defaultValue:{type:ae([Date,Array])},defaultTime:{type:ae([Date,Array])},isRange:Boolean,...nw,disabledDate:{type:Function},cellClassName:{type:Function},shortcuts:{type:Array,default:()=>[]},arrowControl:Boolean,unlinkPanels:Boolean,showNow:{type:Boolean,default:!0},showConfirm:Boolean,showFooter:Boolean,showWeekNumber:Boolean,type:{type:ae(String),default:"date"},clearable:{type:Boolean,default:!0},border:{type:Boolean,default:!0},editable:{type:Boolean,default:!0}}),fw=Symbol("rootPickerContextKey"),Qve=["date","dates","year","years","month","months","week","range"],vw=$e({cellClassName:{type:ae(Function)},disabledDate:{type:ae(Function)},date:{type:ae(Object),required:!0},minDate:{type:ae(Object)},maxDate:{type:ae(Object)},parsedValue:{type:ae([Object,Array])},rangeState:{type:ae(Object),default:()=>({endDate:null,selecting:!1})},disabled:Boolean}),FP=$e({type:{type:ae(String),required:!0,values:PF},dateFormat:String,timeFormat:String,showNow:{type:Boolean,default:!0},showConfirm:Boolean,showFooter:{type:Boolean,default:!0},showWeekNumber:Boolean,border:Boolean,disabled:Boolean,editable:{type:Boolean,default:!0}}),hw=$e({unlinkPanels:Boolean,visible:{type:Boolean,default:!0},showConfirm:Boolean,showFooter:{type:Boolean,default:!0},border:Boolean,disabled:Boolean,parsedValue:{type:ae(Array)},singlePanel:Boolean}),pw=e=>({type:String,values:Qve,default:e}),ehe=$e({...FP,parsedValue:{type:ae([Object,Array])},visible:{type:Boolean,default:!0},format:{type:String,default:""}}),bh=e=>{if(!Ae(e))return!1;const[t,n]=e;return gt.isDayjs(t)&>.isDayjs(n)&>(t).isValid()&>(n).isValid()&&t.isSameOrBefore(n)},G1=(e,{lang:t,step:n=1,unit:a,unlinkPanels:r})=>{let o;if(Ae(e)){let[i,l]=e.map(s=>gt(s).locale(t));return r||(l=i.add(n,a)),[i,l]}else e?o=gt(e):o=gt();return o=o.locale(t),[o,o.add(n,a)]},the=(e,t,{columnIndexOffset:n,startDate:a,nextEndDate:r,now:o,unit:i,relativeDateGetter:l,setCellMetadata:s,setRowMetadata:u})=>{for(let c=0;c{const r=gt().locale(a).startOf("month").month(n).year(t).hour(e.hour()).minute(e.minute()).second(e.second());return xs(r.daysInMonth()).map(o=>r.add(o,"day").toDate())},Gd=(e,t,n,a,r)=>{const o=gt().year(t).month(n).startOf("month").hour(e.hour()).minute(e.minute()).second(e.second()),i=cg(e,t,n,a).find(l=>!(r!=null&&r(l)));return i?gt(i).locale(a):o.locale(a)},dg=(e,t,n)=>{const a=e.year();if(!(n!=null&&n(e.toDate())))return e.locale(t);const r=e.month();if(!cg(e,a,r,t).every(n))return Gd(e,a,r,t,n);for(let o=0;o<12;o++)if(!cg(e,a,o,t).every(n))return Gd(e,a,o,t,n);return e},Yd=(e,t,n)=>{if(Ae(e))return e.map(a=>Yd(a,t,n));if(Ye(e)){let a=gt(e,t);if(a.isValid()||(a=gt(e)),!a.isValid())return a;const r=a.format(t);return gt(r,t,!0).isValid()?gt(r,t).locale(n):gt("")}return gt(e,t).locale(n)},nhe=$e({...vw,showWeekNumber:Boolean,selectionMode:pw("date")}),ahe=["changerange","pick","select"],fg=(e="")=>["normal","today"].includes(e),rhe=(e,t)=>{const{lang:n}=Vt(),a=F(),r=F(),o=F(),i=F(),l=F([[],[],[],[],[],[]]);let s=!1;const u=e.date.$locale().weekStart||7,c=e.date.locale("en").localeData().weekdaysShort().map(W=>W.toLowerCase()),d=D(()=>u>3?7-u:-u),v=D(()=>{const W=e.date.startOf("month");return W.subtract(W.day()||7,"day")}),f=D(()=>c.concat(c).slice(u,u+7)),h=D(()=>Wm(p(w)).some(W=>W.isCurrent)),m=D(()=>{const W=e.date.startOf("month");return{startOfMonthDay:W.day()||7,dateCountOfMonth:W.daysInMonth(),dateCountOfLastMonth:W.subtract(1,"month").daysInMonth()}}),g=D(()=>e.selectionMode==="dates"?or(e.parsedValue):[]),y=(W,{count:N,rowIndex:V,columnIndex:Y})=>{const{startOfMonthDay:X,dateCountOfMonth:ee,dateCountOfLastMonth:re}=p(m),ie=p(d);if(V>=0&&V<=1){const le=X+ie<0?7+X+ie:X+ie;if(Y+V*7>=le)return W.text=N,!0;W.text=re-(le-Y%7)+1+V*7,W.type="prev-month"}else return N<=ee?W.text=N:(W.text=N-ee,W.type="next-month"),!0;return!1},b=(W,{columnIndex:N,rowIndex:V},Y)=>{const{disabledDate:X,cellClassName:ee}=e,re=p(g),ie=y(W,{count:Y,rowIndex:V,columnIndex:N}),le=W.dayjs.toDate();return W.selected=re.find(ve=>ve.isSame(W.dayjs,"day")),W.isSelected=!!W.selected,W.isCurrent=k(W),W.disabled=X==null?void 0:X(le),W.customClass=ee==null?void 0:ee(le),ie},_=W=>{if(e.selectionMode==="week"){const[N,V]=e.showWeekNumber?[1,7]:[0,6],Y=B(W[N+1]);W[N].inRange=Y,W[N].start=Y,W[V].inRange=Y,W[V].end=Y}},w=D(()=>{const{minDate:W,maxDate:N,rangeState:V,showWeekNumber:Y}=e,X=p(d),ee=p(l),re="day";let ie=1;if(the({row:6,column:7},ee,{startDate:W,columnIndexOffset:Y?1:0,nextEndDate:V.endDate||N||V.selecting&&W||null,now:gt().locale(p(n)).startOf(re),unit:re,relativeDateGetter:le=>p(v).add(le-X,re),setCellMetadata:(...le)=>{b(...le,ie)&&(ie+=1)},setRowMetadata:_}),Y)for(let le=0;le<6;le++)ee[le][1].dayjs&&(ee[le][0]={type:"week",text:ee[le][1].dayjs.week()});return ee});ye(()=>e.date,async()=>{var W;(W=p(a))!=null&&W.contains(document.activeElement)&&(await He(),await S())});const S=async()=>{var W;return(W=p(r))==null?void 0:W.focus()},k=W=>e.selectionMode==="date"&&fg(W.type)&&T(W,e.parsedValue),T=(W,N)=>N?gt(N).locale(p(n)).isSame(e.date.date(Number(W.text)),"day"):!1,M=(W,N)=>{const V=p(m).startOfMonthDay,Y=p(d),X=V+Y<0?7+V+Y:V+Y,ee=W*7+(N-(e.showWeekNumber?1:0));return e.date.startOf("month").subtract(X,"day").add(ee,"day")},E=W=>{var X;if(!e.rangeState.selecting)return;let N=W.target;if(N.tagName==="SPAN"&&(N=(X=N.parentNode)==null?void 0:X.parentNode),N.tagName==="DIV"&&(N=N.parentNode),N.tagName!=="TD")return;const V=N.parentNode.rowIndex-1,Y=N.cellIndex;p(w)[V][Y].disabled||(V!==p(o)||Y!==p(i))&&(o.value=V,i.value=Y,t("changerange",{selecting:!0,endDate:M(V,Y)}))},A=W=>!p(h)&&(W==null?void 0:W.text)===1&&fg(W.type)||W.isCurrent,I=W=>{s||p(h)||e.selectionMode!=="date"||z(W,!0)},P=W=>{W.target.closest("td")&&(s=!0)},R=W=>{W.target.closest("td")&&(s=!1)},G=W=>{!e.rangeState.selecting||!e.minDate?(t("pick",{minDate:W,maxDate:null}),t("select",!0)):(W>=e.minDate?t("pick",{minDate:e.minDate,maxDate:W}):t("pick",{minDate:W,maxDate:e.minDate}),t("select",!1))},K=W=>{const N=W.week(),V=`${W.year()}w${N}`;t("pick",{year:W.year(),week:N,value:V,date:W.startOf("week")})},H=(W,N)=>{t("pick",N?or(e.parsedValue).filter(V=>(V==null?void 0:V.valueOf())!==W.valueOf()):or(e.parsedValue).concat([W]))},z=(W,N=!1)=>{if(e.disabled)return;const V=W.target.closest("td");if(!V)return;const Y=V.parentNode.rowIndex-1,X=V.cellIndex,ee=p(w)[Y][X];if(ee.disabled||ee.type==="week")return;const re=M(Y,X);switch(e.selectionMode){case"range":G(re);break;case"date":t("pick",re,N);break;case"week":K(re);break;case"dates":H(re,!!ee.selected);break}},B=W=>{if(e.selectionMode!=="week")return!1;let N=e.date.startOf("day");if(W.type==="prev-month"&&(N=N.subtract(1,"month")),W.type==="next-month"&&(N=N.add(1,"month")),N=N.date(Number.parseInt(W.text,10)),e.parsedValue&&!Ae(e.parsedValue)){const V=(e.parsedValue.day()-u+7)%7-1;return e.parsedValue.subtract(V,"day").isSame(N,"day")}return!1};return{WEEKS:f,rows:w,tbodyRef:a,currentCellRef:r,focus:S,isCurrent:k,isWeekActive:B,isSelectedCell:A,handlePickDate:z,handleMouseUp:R,handleMouseDown:P,handleMouseMove:E,handleFocus:I}},ohe=(e,{isCurrent:t,isWeekActive:n})=>{const a=ke("date-table"),{t:r}=Vt(),o=D(()=>[a.b(),a.is("week-mode",e.selectionMode==="week"&&!e.disabled)]),i=D(()=>r("el.datepicker.dateTablePrompt")),l=u=>{const c=[];return fg(u.type)&&!u.disabled?(c.push("available"),u.type==="today"&&c.push("today")):c.push(u.type),t(u)&&c.push("current"),u.inRange&&(fg(u.type)||e.selectionMode==="week")&&(c.push("in-range"),u.start&&c.push("start-date"),u.end&&c.push("end-date")),(u.disabled||e.disabled)&&c.push("disabled"),u.selected&&c.push("selected"),u.customClass&&c.push(u.customClass),c.join(" ")},s=u=>[a.e("row"),{current:n(u)}];return{tableKls:o,tableLabel:i,weekHeaderClass:a.e("week-header"),getCellClasses:l,getRowKls:s,t:r}},ihe=$e({cell:{type:ae(Object)}});var mw=q({name:"ElDatePickerCell",props:ihe,setup(e){const t=ke("date-table-cell"),{slots:n}=Ze(fw);return()=>{const{cell:a}=e;return ce(n,"default",{...a},()=>[L("div",{class:t.b()},[L("span",{class:t.e("text")},[(a==null?void 0:a.renderText)??(a==null?void 0:a.text)])])])}}});const lhe=["aria-label"],she=["aria-label"],uhe=["aria-current","aria-selected","tabindex","aria-disabled"];var che=q({__name:"basic-date-table",props:nhe,emits:ahe,setup(e,{expose:t,emit:n}){const a=e,{WEEKS:r,rows:o,tbodyRef:i,currentCellRef:l,focus:s,isCurrent:u,isWeekActive:c,isSelectedCell:d,handlePickDate:v,handleMouseUp:f,handleMouseDown:h,handleMouseMove:m,handleFocus:g}=rhe(a,n),{tableLabel:y,tableKls:b,getCellClasses:_,getRowKls:w,weekHeaderClass:S,t:k}=ohe(a,{isCurrent:u,isWeekActive:c});let T=!1;return rn(()=>{T=!0}),t({focus:s}),(M,E)=>(x(),O("table",{"aria-label":p(y),class:U(p(b)),cellspacing:"0",cellpadding:"0",role:"grid",onClick:E[1]||(E[1]=(...A)=>p(v)&&p(v)(...A)),onMousemove:E[2]||(E[2]=(...A)=>p(m)&&p(m)(...A)),onMousedown:E[3]||(E[3]=(...A)=>p(h)&&p(h)(...A)),onMouseup:E[4]||(E[4]=(...A)=>p(f)&&p(f)(...A))},[C("tbody",{ref_key:"tbodyRef",ref:i},[C("tr",null,[M.showWeekNumber?(x(),O("th",{key:0,scope:"col",class:U(p(S))},null,2)):te("v-if",!0),(x(!0),O(Ve,null,dt(p(r),(A,I)=>(x(),O("th",{key:I,"aria-label":p(k)("el.datepicker.weeksFull."+A),scope:"col"},Q(p(k)("el.datepicker.weeks."+A)),9,she))),128))]),(x(!0),O(Ve,null,dt(p(o),(A,I)=>(x(),O("tr",{key:I,class:U(p(w)(M.showWeekNumber?A[2]:A[1]))},[(x(!0),O(Ve,null,dt(A,(P,R)=>(x(),O("td",{key:`${I}.${R}`,ref_for:!0,ref:G=>!p(T)&&p(d)(P)&&(l.value=G),class:U(p(_)(P)),"aria-current":P.isCurrent?"date":void 0,"aria-selected":P.isCurrent,tabindex:M.disabled?void 0:p(d)(P)?0:-1,"aria-disabled":M.disabled,onFocus:E[0]||(E[0]=(...G)=>p(g)&&p(g)(...G))},[L(p(mw),{cell:P},null,8,["cell"])],42,uhe))),128))],2))),128))],512)],42,lhe))}}),Z4=che;const dhe=$e({...vw,selectionMode:pw("month")}),fhe=["aria-label"],vhe=["aria-selected","aria-label","tabindex","onKeydown"];var hhe=q({__name:"basic-month-table",props:dhe,emits:["changerange","pick","select"],setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("month-table"),{t:i,lang:l}=Vt(),s=F(),u=F(),c=F(a.date.locale("en").localeData().monthsShort().map(w=>w.toLowerCase())),d=F([[],[],[]]),v=F(),f=F(),h=D(()=>{var k,T,M;const w=d.value,S=gt().locale(l.value).startOf("month");for(let E=0;E<3;E++){const A=w[E];for(let I=0;I<4;I++){const P=A[I]||(A[I]={row:E,column:I,type:"normal",inRange:!1,start:!1,end:!1,text:-1,disabled:!1,isSelected:!1,customClass:void 0,date:void 0,dayjs:void 0,isCurrent:void 0,selected:void 0,renderText:void 0,timestamp:void 0});P.type="normal";const R=E*4+I,G=a.date.startOf("year").month(R),K=a.rangeState.endDate||a.maxDate||a.rangeState.selecting&&a.minDate||null;P.inRange=!!(a.minDate&&G.isSameOrAfter(a.minDate,"month")&&K&&G.isSameOrBefore(K,"month"))||!!(a.minDate&&G.isSameOrBefore(a.minDate,"month")&&K&&G.isSameOrAfter(K,"month")),(k=a.minDate)!=null&&k.isSameOrAfter(K)?(P.start=!!(K&&G.isSame(K,"month")),P.end=a.minDate&&G.isSame(a.minDate,"month")):(P.start=!!(a.minDate&&G.isSame(a.minDate,"month")),P.end=!!(K&&G.isSame(K,"month"))),S.isSame(G)&&(P.type="today");const H=G.toDate();P.text=R,P.disabled=((T=a.disabledDate)==null?void 0:T.call(a,H))||!1,P.date=H,P.customClass=(M=a.cellClassName)==null?void 0:M.call(a,H),P.dayjs=G,P.timestamp=G.valueOf(),P.isSelected=y(P)}}return w}),m=()=>{var w;(w=u.value)==null||w.focus()},g=w=>{const S={},k=a.date.year(),T=new Date,M=w.text;return S.disabled=a.disabled||(a.disabledDate?cg(a.date,k,M,l.value).every(a.disabledDate):!1),S.current=or(a.parsedValue).some(E=>gt.isDayjs(E)&&E.year()===k&&E.month()===M),S.today=T.getFullYear()===k&&T.getMonth()===M,w.customClass&&(S[w.customClass]=!0),w.inRange&&(S["in-range"]=!0,w.start&&(S["start-date"]=!0),w.end&&(S["end-date"]=!0)),S},y=w=>{const S=a.date.year(),k=w.text;return or(a.date).some(T=>T.year()===S&&T.month()===k)},b=w=>{var M;if(!a.rangeState.selecting)return;let S=w.target;if(S.tagName==="SPAN"&&(S=(M=S.parentNode)==null?void 0:M.parentNode),S.tagName==="DIV"&&(S=S.parentNode),S.tagName!=="TD")return;const k=S.parentNode.rowIndex,T=S.cellIndex;h.value[k][T].disabled||(k!==v.value||T!==f.value)&&(v.value=k,f.value=T,r("changerange",{selecting:!0,endDate:a.date.startOf("year").month(k*4+T)}))},_=w=>{var E;if(a.disabled)return;const S=(E=w.target)==null?void 0:E.closest("td");if((S==null?void 0:S.tagName)!=="TD"||Ni(S,"disabled"))return;const k=S.cellIndex,T=S.parentNode.rowIndex*4+k,M=a.date.startOf("year").month(T);if(a.selectionMode==="months"){if(w.type==="keydown"){r("pick",or(a.parsedValue),!1);return}const A=Gd(a.date,a.date.year(),T,l.value,a.disabledDate);r("pick",Ni(S,"current")?or(a.parsedValue).filter(I=>(I==null?void 0:I.year())!==A.year()||(I==null?void 0:I.month())!==A.month()):or(a.parsedValue).concat([gt(A)]))}else a.selectionMode==="range"?a.rangeState.selecting?(a.minDate&&M>=a.minDate?r("pick",{minDate:a.minDate,maxDate:M}):r("pick",{minDate:M,maxDate:a.minDate}),r("select",!1)):(r("pick",{minDate:M,maxDate:null}),r("select",!0)):r("pick",T)};return ye(()=>a.date,async()=>{var w,S;(w=s.value)!=null&&w.contains(document.activeElement)&&(await He(),(S=u.value)==null||S.focus())}),t({focus:m}),(w,S)=>(x(),O("table",{role:"grid","aria-label":p(i)("el.datepicker.monthTablePrompt"),class:U(p(o).b()),onClick:_,onMousemove:b},[C("tbody",{ref_key:"tbodyRef",ref:s},[(x(!0),O(Ve,null,dt(h.value,(k,T)=>(x(),O("tr",{key:T},[(x(!0),O(Ve,null,dt(k,(M,E)=>(x(),O("td",{key:E,ref_for:!0,ref:A=>M.isSelected&&(u.value=A),class:U(g(M)),"aria-selected":!!M.isSelected,"aria-label":p(i)(`el.datepicker.month${+M.text+1}`),tabindex:M.isSelected?0:-1,onKeydown:[kn(at(_,["prevent","stop"]),["space"]),kn(at(_,["prevent","stop"]),["enter"])]},[L(p(mw),{cell:{...M,renderText:p(i)("el.datepicker.months."+c.value[M.text])}},null,8,["cell"])],42,vhe))),128))]))),128))],512)],42,fhe))}}),_h=hhe;const phe=$e({...vw,selectionMode:pw("year")}),mhe=["aria-label"],ghe=["aria-selected","aria-label","tabindex","onKeydown"];var yhe=q({__name:"basic-year-table",props:phe,emits:["changerange","pick","select"],setup(e,{expose:t,emit:n}){const a=(S,k)=>{const T=gt(String(S)).locale(k).startOf("year");return xs(T.endOf("year").dayOfYear()).map(M=>T.add(M,"day").toDate())},r=e,o=n,i=ke("year-table"),{t:l,lang:s}=Vt(),u=F(),c=F(),d=D(()=>Math.floor(r.date.year()/10)*10),v=F([[],[],[]]),f=F(),h=F(),m=D(()=>{var T,M,E;const S=v.value,k=gt().locale(s.value).startOf("year");for(let A=0;A<3;A++){const I=S[A];for(let P=0;P<4&&!(A*4+P>=10);P++){let R=I[P];R||(R={row:A,column:P,type:"normal",inRange:!1,start:!1,end:!1,text:-1,disabled:!1,isSelected:!1,customClass:void 0,date:void 0,dayjs:void 0,isCurrent:void 0,selected:void 0,renderText:void 0,timestamp:void 0}),R.type="normal";const G=A*4+P+d.value,K=gt().year(G),H=r.rangeState.endDate||r.maxDate||r.rangeState.selecting&&r.minDate||null;R.inRange=!!(r.minDate&&K.isSameOrAfter(r.minDate,"year")&&H&&K.isSameOrBefore(H,"year"))||!!(r.minDate&&K.isSameOrBefore(r.minDate,"year")&&H&&K.isSameOrAfter(H,"year")),(T=r.minDate)!=null&&T.isSameOrAfter(H)?(R.start=!!(H&&K.isSame(H,"year")),R.end=!!(r.minDate&&K.isSame(r.minDate,"year"))):(R.start=!!(r.minDate&&K.isSame(r.minDate,"year")),R.end=!!(H&&K.isSame(H,"year"))),k.isSame(K)&&(R.type="today"),R.text=G;const z=K.toDate();R.disabled=((M=r.disabledDate)==null?void 0:M.call(r,z))||!1,R.date=z,R.customClass=(E=r.cellClassName)==null?void 0:E.call(r,z),R.dayjs=K,R.timestamp=K.valueOf(),R.isSelected=b(R),I[P]=R}}return S}),g=()=>{var S;(S=c.value)==null||S.focus()},y=S=>{const k={},T=gt().locale(s.value),M=S.text;return k.disabled=r.disabled||(r.disabledDate?a(M,s.value).every(r.disabledDate):!1),k.today=T.year()===M,k.current=or(r.parsedValue).some(E=>E.year()===M),S.customClass&&(k[S.customClass]=!0),S.inRange&&(k["in-range"]=!0,S.start&&(k["start-date"]=!0),S.end&&(k["end-date"]=!0)),k},b=S=>{const k=S.text;return or(r.date).some(T=>T.year()===k)},_=S=>{var A;if(r.disabled)return;const k=(A=S.target)==null?void 0:A.closest("td");if(!k||!k.textContent||Ni(k,"disabled"))return;const T=k.cellIndex,M=k.parentNode.rowIndex*4+T+d.value,E=gt().year(M);if(r.selectionMode==="range")r.rangeState.selecting?(r.minDate&&E>=r.minDate?o("pick",{minDate:r.minDate,maxDate:E}):o("pick",{minDate:E,maxDate:r.minDate}),o("select",!1)):(o("pick",{minDate:E,maxDate:null}),o("select",!0));else if(r.selectionMode==="years"){if(S.type==="keydown"){o("pick",or(r.parsedValue),!1);return}const I=dg(E.startOf("year"),s.value,r.disabledDate);o("pick",Ni(k,"current")?or(r.parsedValue).filter(P=>(P==null?void 0:P.year())!==M):or(r.parsedValue).concat([I]))}else o("pick",M)},w=S=>{var E;if(!r.rangeState.selecting)return;const k=(E=S.target)==null?void 0:E.closest("td");if(!k)return;const T=k.parentNode.rowIndex,M=k.cellIndex;m.value[T][M].disabled||(T!==f.value||M!==h.value)&&(f.value=T,h.value=M,o("changerange",{selecting:!0,endDate:gt().year(d.value).add(T*4+M,"year")}))};return ye(()=>r.date,async()=>{var S,k;(S=u.value)!=null&&S.contains(document.activeElement)&&(await He(),(k=c.value)==null||k.focus())}),t({focus:g}),(S,k)=>(x(),O("table",{role:"grid","aria-label":p(l)("el.datepicker.yearTablePrompt"),class:U(p(i).b()),onClick:_,onMousemove:w},[C("tbody",{ref_key:"tbodyRef",ref:u},[(x(!0),O(Ve,null,dt(m.value,(T,M)=>(x(),O("tr",{key:M},[(x(!0),O(Ve,null,dt(T,(E,A)=>(x(),O("td",{key:`${M}_${A}`,ref_for:!0,ref:I=>E.isSelected&&(c.value=I),class:U(["available",y(E)]),"aria-selected":E.isSelected,"aria-label":String(E.text),tabindex:E.isSelected?0:-1,onKeydown:[kn(at(_,["prevent","stop"]),["space"]),kn(at(_,["prevent","stop"]),["enter"])]},[L(p(mw),{cell:E},null,8,["cell"])],42,ghe))),128))]))),128))],512)],42,mhe))}}),wh=yhe;const bhe=["disabled","onClick"],_he=["aria-label","disabled"],whe=["aria-label","disabled"],She=["tabindex","aria-disabled"],Che=["tabindex","aria-disabled"],xhe=["aria-label","disabled"],khe=["aria-label","disabled"];var The=q({__name:"panel-date-pick",props:ehe,emits:["pick","set-picker-option","panel-change"],setup(e,{emit:t}){const n=(_e,Ne,tt)=>!0,a=e,r=t,o=ke("picker-panel"),i=ke("date-picker"),l=Nl(),s=Xn(),{t:u,lang:c}=Vt(),d=Ze(nl),{shortcuts:v,disabledDate:f,cellClassName:h,defaultTime:m}=d.props,g=cn(d.props,"defaultValue"),y=F(),b=F(gt().locale(c.value)),_=F(!1);let w=!1;const S=D(()=>gt(m).locale(c.value)),k=D(()=>b.value.month()),T=D(()=>b.value.year()),M=F([]),E=F(null),A=F(null),I=_e=>M.value.length>0?n(_e,M.value,a.format||"HH:mm:ss"):!0,P=_e=>m&&!Se.value&&!_.value&&!w?S.value.year(_e.year()).month(_e.month()).date(_e.date()):ve.value?_e.millisecond(0):_e.startOf("day"),R=(_e,...Ne)=>{_e?Ae(_e)?r("pick",_e.map(P),...Ne):r("pick",P(_e),...Ne):r("pick",_e,...Ne),E.value=null,A.value=null,_.value=!1,w=!1},G=async(_e,Ne)=>{if(N.value==="date"&>.isDayjs(_e)){const tt=Pf(a.parsedValue);let Me=tt?tt.year(_e.year()).month(_e.month()).date(_e.date()):_e;I(Me),b.value=Me,R(Me,ve.value||Ne)}else N.value==="week"?R(_e.date):N.value==="dates"&&R(_e,!0)},K=_e=>{const Ne=_e?"add":"subtract";b.value=b.value[Ne](1,"month"),Qt("month")},H=_e=>{const Ne=b.value,tt=_e?"add":"subtract";b.value=z.value==="year"?Ne[tt](10,"year"):Ne[tt](1,"year"),Qt("year")},z=F("date"),B=D(()=>{const _e=u("el.datepicker.year");if(z.value==="year"){const Ne=Math.floor(T.value/10)*10;return _e?`${Ne} ${_e} - ${Ne+9} ${_e}`:`${Ne} - ${Ne+9}`}return`${T.value} ${_e}`}),W=_e=>{const Ne=Qe(_e.value)?_e.value():_e.value;if(Ne){w=!0,R(gt(Ne).locale(c.value));return}_e.onClick&&_e.onClick({attrs:l,slots:s,emit:r})},N=D(()=>{const{type:_e}=a;return["week","month","months","year","years","dates"].includes(_e)?_e:"date"}),V=D(()=>N.value==="dates"||N.value==="months"||N.value==="years"),Y=D(()=>N.value==="date"?z.value:N.value),X=D(()=>!!v.length),ee=async(_e,Ne)=>{N.value==="month"?(b.value=Gd(b.value,b.value.year(),_e,c.value,f),R(b.value,!1)):N.value==="months"?R(_e,Ne??!0):(b.value=Gd(b.value,b.value.year(),_e,c.value,f),z.value="date",["month","year","date","week"].includes(N.value)&&(R(b.value,!0),await He(),Pt())),Qt("month")},re=async(_e,Ne)=>{N.value==="year"?(b.value=dg(b.value.startOf("year").year(_e),c.value,f),R(b.value,!1)):N.value==="years"?R(_e,Ne??!0):(b.value=dg(b.value.year(_e),c.value,f),z.value="month",["month","year","date","week"].includes(N.value)&&(R(b.value,!0),await He(),Pt())),Qt("year")},ie=$n(),le=async _e=>{ie.value||(z.value=_e,await He(),Pt())},ve=D(()=>a.type==="datetime"||a.type==="datetimerange"),he=D(()=>{const _e=ve.value||N.value==="dates",Ne=N.value==="years",tt=N.value==="months",Me=z.value==="date",Ue=z.value==="year",Gt=z.value==="month";return _e&&Me||Ne&&Ue||tt&&Gt}),J=D(()=>!V.value&&a.showNow||a.showConfirm),Z=D(()=>f?a.parsedValue?Ae(a.parsedValue)?f(a.parsedValue[0].toDate()):f(a.parsedValue.toDate()):!0:!1),j=()=>{if(V.value)R(a.parsedValue);else{let _e=Pf(a.parsedValue);if(!_e){const Ne=gt(m).locale(c.value),tt=nt();_e=Ne.year(tt.year()).month(tt.month()).date(tt.date())}b.value=_e,R(_e)}},oe=D(()=>f?f(gt().locale(c.value).toDate()):!1),se=()=>{const _e=gt().locale(c.value).toDate();_.value=!0,(!f||!f(_e))&&I(_e)&&(b.value=gt().locale(c.value),R(b.value))},fe=D(()=>a.timeFormat||SL(a.format)||"HH:mm:ss"),ge=D(()=>a.dateFormat||wL(a.format)||"YYYY-MM-DD"),Se=D(()=>{if(A.value)return A.value;if(!(!a.parsedValue&&!g.value))return(Pf(a.parsedValue)||b.value).format(fe.value)}),De=D(()=>{if(E.value)return E.value;if(!(!a.parsedValue&&!g.value))return(Pf(a.parsedValue)||b.value).format(ge.value)}),Te=F(!1),Re=()=>{Te.value=!0},Ce=()=>{Te.value=!1},Le=_e=>({hour:_e.hour(),minute:_e.minute(),second:_e.second(),year:_e.year(),month:_e.month(),date:_e.date()}),me=(_e,Ne,tt)=>{const{hour:Me,minute:Ue,second:Gt}=Le(_e),je=Pf(a.parsedValue);b.value=je?je.hour(Me).minute(Ue).second(Gt):_e,R(b.value,!0),tt||(Te.value=Ne)},Be=_e=>{const Ne=gt(_e,fe.value).locale(c.value);if(Ne.isValid()&&I(Ne)){const{year:tt,month:Me,date:Ue}=Le(b.value);b.value=Ne.year(tt).month(Me).date(Ue),A.value=null,Te.value=!1,R(b.value,!0)}},ze=_e=>{const Ne=Yd(_e,ge.value,c.value);if(Ne.isValid()){if(f&&f(Ne.toDate()))return;const{hour:tt,minute:Me,second:Ue}=Le(b.value);b.value=Ne.hour(tt).minute(Me).second(Ue),E.value=null,R(b.value,!0)}},xe=_e=>gt.isDayjs(_e)&&_e.isValid()&&(f?!f(_e.toDate()):!0),rt=_e=>Yd(_e,a.format,c.value),nt=()=>{const _e=gt(g.value).locale(c.value);if(!g.value){const Ne=S.value;return gt().hour(Ne.hour()).minute(Ne.minute()).second(Ne.second()).locale(c.value)}return _e},Pt=()=>{var _e;["week","month","year","date"].includes(N.value)&&((_e=y.value)==null||_e.focus())},Wt=()=>{Pt(),N.value==="week"&&Rt(Pe.down)},Jt=_e=>{const Ne=mn(_e);[Pe.up,Pe.down,Pe.left,Pe.right,Pe.home,Pe.end,Pe.pageUp,Pe.pageDown].includes(Ne)&&(Rt(Ne),_e.stopPropagation(),_e.preventDefault()),[Pe.enter,Pe.space,Pe.numpadEnter].includes(Ne)&&E.value===null&&A.value===null&&(_e.preventDefault(),R(b.value,!1))},Rt=_e=>{const{up:Ne,down:tt,left:Me,right:Ue,home:Gt,end:je,pageUp:ft,pageDown:ht}=Pe,pt={year:{[Ne]:-4,[tt]:4,[Me]:-1,[Ue]:1,offset:(Ct,$t)=>Ct.setFullYear(Ct.getFullYear()+$t)},month:{[Ne]:-4,[tt]:4,[Me]:-1,[Ue]:1,offset:(Ct,$t)=>Ct.setMonth(Ct.getMonth()+$t)},week:{[Ne]:-1,[tt]:1,[Me]:-1,[Ue]:1,offset:(Ct,$t)=>Ct.setDate(Ct.getDate()+$t*7)},date:{[Ne]:-7,[tt]:7,[Me]:-1,[Ue]:1,[Gt]:Ct=>-Ct.getDay(),[je]:Ct=>-Ct.getDay()+6,[ft]:Ct=>-new Date(Ct.getFullYear(),Ct.getMonth(),0).getDate(),[ht]:Ct=>new Date(Ct.getFullYear(),Ct.getMonth()+1,0).getDate(),offset:(Ct,$t)=>Ct.setDate(Ct.getDate()+$t)}},It=b.value.toDate();for(;Math.abs(b.value.diff(It,"year",!0))<1;){const Ct=pt[Y.value];if(!Ct)return;if(Ct.offset(It,Qe(Ct[_e])?Ct[_e](It):Ct[_e]??0),f&&f(It))break;const $t=gt(It).locale(c.value);b.value=$t,r("pick",$t,!0);break}},Qt=_e=>{r("panel-change",b.value.toDate(),_e,z.value)};return ye(()=>N.value,_e=>{if(["month","year"].includes(_e)){z.value=_e;return}else if(_e==="years"){z.value="year";return}else if(_e==="months"){z.value="month";return}z.value="date"},{immediate:!0}),ye(()=>g.value,_e=>{_e&&(b.value=nt())},{immediate:!0}),ye(()=>a.parsedValue,_e=>{if(_e){if(V.value||Ae(_e))return;b.value=_e}else b.value=nt()},{immediate:!0}),r("set-picker-option",["isValidValue",xe]),r("set-picker-option",["parseUserInput",rt]),r("set-picker-option",["handleFocusPicker",Wt]),(_e,Ne)=>(x(),O("div",{class:U([p(o).b(),p(i).b(),p(o).is("border",_e.border),p(o).is("disabled",p(ie)),{"has-sidebar":_e.$slots.sidebar||X.value,"has-time":ve.value}])},[C("div",{class:U(p(o).e("body-wrapper"))},[ce(_e.$slots,"sidebar",{class:U(p(o).e("sidebar"))}),X.value?(x(),O("div",{key:0,class:U(p(o).e("sidebar"))},[(x(!0),O(Ve,null,dt(p(v),(tt,Me)=>(x(),O("button",{key:Me,type:"button",disabled:p(ie),class:U(p(o).e("shortcut")),onClick:Ue=>W(tt)},Q(tt.text),11,bhe))),128))],2)):te("v-if",!0),C("div",{class:U(p(o).e("body"))},[ve.value?(x(),O("div",{key:0,class:U(p(i).e("time-header"))},[C("span",{class:U(p(i).e("editor-wrap"))},[L(p(Ba),{placeholder:p(u)("el.datepicker.selectDate"),"model-value":De.value,size:"small","validate-event":!1,disabled:p(ie),readonly:!_e.editable,onInput:Ne[0]||(Ne[0]=tt=>E.value=tt),onChange:ze},null,8,["placeholder","model-value","disabled","readonly"])],2),st((x(),O("span",{class:U(p(i).e("editor-wrap"))},[L(p(Ba),{placeholder:p(u)("el.datepicker.selectTime"),"model-value":Se.value,size:"small","validate-event":!1,disabled:p(ie),readonly:!_e.editable,onFocus:Re,onInput:Ne[1]||(Ne[1]=tt=>A.value=tt),onChange:Be},null,8,["placeholder","model-value","disabled","readonly"]),L(p(Jm),{visible:Te.value,format:fe.value,"parsed-value":b.value,onPick:me},null,8,["visible","format","parsed-value"])],2)),[[p(Ns),Ce]])],2)):te("v-if",!0),st(C("div",{class:U([p(i).e("header"),(z.value==="year"||z.value==="month")&&p(i).em("header","bordered")])},[C("span",{class:U(p(i).e("prev-btn"))},[C("button",{type:"button","aria-label":p(u)("el.datepicker.prevYear"),class:U(["d-arrow-left",p(o).e("icon-btn")]),disabled:p(ie),onClick:Ne[2]||(Ne[2]=tt=>H(!1))},[ce(_e.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,_he),st(C("button",{type:"button","aria-label":p(u)("el.datepicker.prevMonth"),class:U([p(o).e("icon-btn"),"arrow-left"]),disabled:p(ie),onClick:Ne[3]||(Ne[3]=tt=>K(!1))},[ce(_e.$slots,"prev-month",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Ji))]),_:1})])],10,whe),[[Zt,z.value==="date"]])],2),C("span",{role:"button",class:U(p(i).e("header-label")),"aria-live":"polite",tabindex:_e.disabled?void 0:0,"aria-disabled":_e.disabled,onKeydown:Ne[4]||(Ne[4]=kn(tt=>le("year"),["enter"])),onClick:Ne[5]||(Ne[5]=tt=>le("year"))},Q(B.value),43,She),st(C("span",{role:"button","aria-live":"polite",tabindex:_e.disabled?void 0:0,"aria-disabled":_e.disabled,class:U([p(i).e("header-label"),{active:z.value==="month"}]),onKeydown:Ne[6]||(Ne[6]=kn(tt=>le("month"),["enter"])),onClick:Ne[7]||(Ne[7]=tt=>le("month"))},Q(p(u)(`el.datepicker.month${k.value+1}`)),43,Che),[[Zt,z.value==="date"]]),C("span",{class:U(p(i).e("next-btn"))},[st(C("button",{type:"button","aria-label":p(u)("el.datepicker.nextMonth"),class:U([p(o).e("icon-btn"),"arrow-right"]),disabled:p(ie),onClick:Ne[8]||(Ne[8]=tt=>K(!0))},[ce(_e.$slots,"next-month",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})])],10,xhe),[[Zt,z.value==="date"]]),C("button",{type:"button","aria-label":p(u)("el.datepicker.nextYear"),class:U([p(o).e("icon-btn"),"d-arrow-right"]),disabled:p(ie),onClick:Ne[9]||(Ne[9]=tt=>H(!0))},[ce(_e.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,khe)],2)],2),[[Zt,z.value!=="time"]]),C("div",{class:U(p(o).e("content")),onKeydown:Jt},[z.value==="date"?(x(),ne(Z4,{key:0,ref_key:"currentViewRef",ref:y,"selection-mode":N.value,date:b.value,"parsed-value":_e.parsedValue,"disabled-date":p(f),disabled:p(ie),"cell-class-name":p(h),"show-week-number":_e.showWeekNumber,onPick:G},null,8,["selection-mode","date","parsed-value","disabled-date","disabled","cell-class-name","show-week-number"])):te("v-if",!0),z.value==="year"?(x(),ne(wh,{key:1,ref_key:"currentViewRef",ref:y,"selection-mode":N.value,date:b.value,"disabled-date":p(f),disabled:p(ie),"parsed-value":_e.parsedValue,"cell-class-name":p(h),onPick:re},null,8,["selection-mode","date","disabled-date","disabled","parsed-value","cell-class-name"])):te("v-if",!0),z.value==="month"?(x(),ne(_h,{key:2,ref_key:"currentViewRef",ref:y,"selection-mode":N.value,date:b.value,"parsed-value":_e.parsedValue,"disabled-date":p(f),disabled:p(ie),"cell-class-name":p(h),onPick:ee},null,8,["selection-mode","date","parsed-value","disabled-date","disabled","cell-class-name"])):te("v-if",!0)],34)],2)],2),_e.showFooter&&he.value&&J.value?(x(),O("div",{key:0,class:U(p(o).e("footer"))},[st(L(p(xa),{text:"",size:"small",class:U(p(o).e("link-btn")),disabled:oe.value,onClick:se},{default:$(()=>[ue(Q(p(u)("el.datepicker.now")),1)]),_:1},8,["class","disabled"]),[[Zt,!V.value&&_e.showNow]]),_e.showConfirm?(x(),ne(p(xa),{key:0,plain:"",size:"small",class:U(p(o).e("link-btn")),disabled:Z.value,onClick:j},{default:$(()=>[ue(Q(p(u)("el.datepicker.confirm")),1)]),_:1},8,["class","disabled"])):te("v-if",!0)],2)):te("v-if",!0)],2))}}),Mhe=The;const Ehe=$e({...FP,...hw}),Ahe=e=>{const{emit:t}=Et(),n=Nl(),a=Xn();return o=>{const i=Qe(o.value)?o.value():o.value;if(i){t("pick",[gt(i[0]).locale(e.value),gt(i[1]).locale(e.value)]);return}o.onClick&&o.onClick({attrs:n,slots:a,emit:t})}},gw=(e,{defaultValue:t,defaultTime:n,leftDate:a,rightDate:r,step:o,unit:i,sortDates:l})=>{const{emit:s}=Et(),{pickerNs:u}=Ze(fw),c=ke("date-range-picker"),{t:d,lang:v}=Vt(),f=Ahe(v),h=F(),m=F(),g=F({endDate:null,selecting:!1}),y=k=>{g.value=k},b=(k=!1)=>{const T=p(h),M=p(m);bh([T,M])&&s("pick",[T,M],k)},_=k=>{g.value.selecting=k,k||(g.value.endDate=null)},w=k=>{if(Ae(k)&&k.length===2){const[T,M]=k;h.value=T,a.value=T,m.value=M,l(p(h),p(m))}else S()},S=()=>{let[k,T]=G1(p(t),{lang:p(v),step:o,unit:i,unlinkPanels:e.unlinkPanels});const M=A=>A.diff(A.startOf("d"),"ms"),E=p(n);if(E){let A=0,I=0;if(Ae(E)){const[P,R]=E.map(gt);A=M(P),I=M(R)}else{const P=M(gt(E));A=P,I=P}k=k.startOf("d").add(A,"ms"),T=T.startOf("d").add(I,"ms")}h.value=void 0,m.value=void 0,a.value=k,r.value=T};return ye(t,k=>{k&&S()},{immediate:!0}),ye(()=>e.parsedValue,k=>{(!(k!=null&&k.length)||!Rn(k,[h.value,m.value]))&&w(k)},{immediate:!0}),ye(()=>e.visible,()=>{e.visible&&w(e.parsedValue)},{immediate:!0}),{minDate:h,maxDate:m,rangeState:g,lang:v,ppNs:u,drpNs:c,handleChangeRange:y,handleRangeConfirm:b,handleShortcutClick:f,onSelect:_,parseValue:w,t:d}},Ihe=(e,t,n,a)=>{const r=F("date"),o=F(),i=F("date"),l=F(),{disabledDate:s}=Ze(nl).props,{t:u,lang:c}=Vt(),d=D(()=>n.value.year()),v=D(()=>n.value.month()),f=D(()=>a.value.year()),h=D(()=>a.value.month());function m(S,k){const T=u("el.datepicker.year");if(S.value==="year"){const M=Math.floor(k.value/10)*10;return T?`${M} ${T} - ${M+9} ${T}`:`${M} - ${M+9}`}return`${k.value} ${T}`}function g(S){S==null||S.focus()}async function y(S,k){if(e.disabled)return;const T=S==="left"?r:i,M=S==="left"?o:l;T.value=k,await He(),g(M.value)}async function b(S,k,T){if(e.disabled)return;const M=k==="left",E=M?n:a,A=M?a:n,I=M?r:i,P=M?o:l;S==="year"&&(E.value=dg(E.value.year(T),c.value,s)),S==="month"&&(E.value=Gd(E.value,E.value.year(),T,c.value,s)),e.unlinkPanels||(A.value=k==="left"?E.value.add(1,"month"):E.value.subtract(1,"month")),I.value=S==="year"?"month":"date",await He(),g(P.value),_(S)}function _(S){t("panel-change",[n.value.toDate(),a.value.toDate()],S)}function w(S,k,T){const M=T?"add":"subtract";return S==="year"?k[M](10,"year"):k[M](1,"year")}return{leftCurrentView:r,rightCurrentView:i,leftCurrentViewRef:o,rightCurrentViewRef:l,leftYear:d,rightYear:f,leftMonth:v,rightMonth:h,leftYearLabel:D(()=>m(r,d)),rightYearLabel:D(()=>m(i,f)),showLeftPicker:S=>y("left",S),showRightPicker:S=>y("right",S),handleLeftYearPick:S=>b("year","left",S),handleRightYearPick:S=>b("year","right",S),handleLeftMonthPick:S=>b("month","left",S),handleRightMonthPick:S=>b("month","right",S),handlePanelChange:_,adjustDateByView:w}},Lhe=["disabled","onClick"],Phe=["aria-label","disabled"],Dhe=["aria-label","disabled"],Ohe=["disabled","aria-label"],$he=["disabled","aria-label"],Rhe=["tabindex","aria-disabled"],Nhe=["tabindex","aria-disabled"],Bhe=["disabled","aria-label"],Vhe=["disabled","aria-label"],zhe=["aria-label","disabled"],Fhe=["disabled","aria-label"],Hhe=["tabindex","aria-disabled"],Whe=["tabindex","aria-disabled"],Up="month";var Khe=q({__name:"panel-date-range",props:Ehe,emits:["pick","set-picker-option","calendar-change","panel-change","clear"],setup(e,{emit:t}){const n=e,a=t,r=Ze(nl),{disabledDate:o,cellClassName:i,defaultTime:l,clearable:s}=r.props,u=cn(r.props,"format"),c=cn(r.props,"shortcuts"),d=cn(r.props,"defaultValue"),{lang:v}=Vt(),f=F(gt().locale(v.value)),h=F(gt().locale(v.value).add(1,Up)),{minDate:m,maxDate:g,rangeState:y,ppNs:b,drpNs:_,handleChangeRange:w,handleRangeConfirm:S,handleShortcutClick:k,onSelect:T,parseValue:M,t:E}=gw(n,{defaultValue:d,defaultTime:l,leftDate:f,rightDate:h,unit:Up,sortDates:Sn});ye(()=>n.visible,Fe=>{!Fe&&y.value.selecting&&(M(n.parsedValue),T(!1))});const A=F({min:null,max:null}),I=F({min:null,max:null}),{leftCurrentView:P,rightCurrentView:R,leftCurrentViewRef:G,rightCurrentViewRef:K,leftYear:H,rightYear:z,leftMonth:B,rightMonth:W,leftYearLabel:N,rightYearLabel:V,showLeftPicker:Y,showRightPicker:X,handleLeftYearPick:ee,handleRightYearPick:re,handleLeftMonthPick:ie,handleRightMonthPick:le,handlePanelChange:ve,adjustDateByView:he}=Ihe(n,a,f,h),J=D(()=>!!c.value.length),Z=D(()=>A.value.min!==null?A.value.min:m.value?m.value.format(ge.value):""),j=D(()=>A.value.max!==null?A.value.max:g.value||m.value?(g.value||m.value).format(ge.value):""),oe=D(()=>I.value.min!==null?I.value.min:m.value?m.value.format(fe.value):""),se=D(()=>I.value.max!==null?I.value.max:g.value||m.value?(g.value||m.value).format(fe.value):""),fe=D(()=>n.timeFormat||SL(u.value||"")||"HH:mm:ss"),ge=D(()=>n.dateFormat||wL(u.value||"")||"YYYY-MM-DD"),Se=Fe=>bh(Fe)&&(o?!o(Fe[0].toDate())&&!o(Fe[1].toDate()):!0),De=()=>{f.value=he(P.value,f.value,!1),n.unlinkPanels||(h.value=f.value.add(1,"month")),ve("year")},Te=()=>{f.value=f.value.subtract(1,"month"),n.unlinkPanels||(h.value=f.value.add(1,"month")),ve("month")},Re=()=>{n.unlinkPanels?h.value=he(R.value,h.value,!0):(f.value=he(R.value,f.value,!0),h.value=f.value.add(1,"month")),ve("year")},Ce=()=>{n.unlinkPanels?h.value=h.value.add(1,"month"):(f.value=f.value.add(1,"month"),h.value=f.value.add(1,"month")),ve("month")},Le=()=>{f.value=he(P.value,f.value,!0),ve("year")},me=()=>{f.value=f.value.add(1,"month"),ve("month")},Be=()=>{h.value=he(R.value,h.value,!1),ve("year")},ze=()=>{h.value=h.value.subtract(1,"month"),ve("month")},xe=D(()=>{const Fe=(B.value+1)%12,qe=B.value+1>=12?1:0;return n.singlePanel||n.unlinkPanels&&new Date(H.value+qe,Fe)n.singlePanel||n.unlinkPanels&&z.value*12+W.value-(H.value*12+B.value+1)>=12),nt=$n(),Pt=D(()=>!(m.value&&g.value&&!y.value.selecting&&bh([m.value,g.value])&&!nt.value)),Wt=D(()=>n.type==="datetime"||n.type==="datetimerange"),Jt=(Fe,qe)=>{if(Fe)return l?gt(l[qe]||l).locale(v.value).year(Fe.year()).month(Fe.month()).date(Fe.date()):Fe},Rt=(Fe,qe=!0)=>{const ut=Fe.minDate,Ie=Fe.maxDate,Ke=Jt(ut,0),Ge=Jt(Ie,1);g.value===Ge&&m.value===Ke||(a("calendar-change",[ut.toDate(),Ie&&Ie.toDate()]),g.value=Ge,m.value=Ke,!Wt.value&&qe&&(qe=!Ke||!Ge),S(qe))},Qt=F(!1),_e=F(!1),Ne=()=>{Qt.value=!1},tt=()=>{_e.value=!1},Me=(Fe,qe)=>{if(!o||!o(Fe.toDate()))return Fe;const ut=Fe.isBefore(qe);let Ie=Fe;for(;ut?Ie.isBefore(qe):Ie.isAfter(qe);)if(Ie=ut?Ie.add(1,"day"):Ie.subtract(1,"day"),!o(Ie.toDate()))return Ie;return Fe},Ue=(Fe,qe)=>{A.value[qe]=Fe;const ut=gt(Fe,ge.value).locale(v.value);if(ut.isValid()){if(o&&o(ut.toDate()))return;if(qe==="min"){if(f.value=ut,m.value=(m.value||f.value).year(ut.year()).month(ut.month()).date(ut.date()),!n.unlinkPanels&&!g.value){const Ie=Me(m.value.add(1,"month"),m.value);h.value=Ie,g.value=Ie}}else if(h.value=ut,g.value=(g.value||h.value).year(ut.year()).month(ut.month()).date(ut.date()),!n.unlinkPanels&&!m.value){const Ie=Me(g.value.subtract(1,"month"),g.value);f.value=Ie,m.value=Ie}Sn(m.value,g.value),S(!0)}},Gt=(Fe,qe)=>{if(A.value[qe]=null,qe==="min"){if(!n.unlinkPanels&&g.value&&m.value&&g.value.isBefore(m.value)){const ut=Me(m.value.add(1,"month"),m.value);h.value=ut,g.value=ut}}else if(!n.unlinkPanels&&m.value&&g.value&&m.value.isAfter(g.value)){const ut=Me(g.value.subtract(1,"month"),g.value);f.value=ut,m.value=ut}Sn(m.value,g.value),S(!0)},je=(Fe,qe)=>{I.value[qe]=Fe;const ut=gt(Fe,fe.value).locale(v.value);ut.isValid()&&(qe==="min"?(Qt.value=!0,m.value=(m.value||f.value).hour(ut.hour()).minute(ut.minute()).second(ut.second()),f.value=m.value):(_e.value=!0,g.value=(g.value||h.value).hour(ut.hour()).minute(ut.minute()).second(ut.second()),h.value=g.value))},ft=(Fe,qe)=>{I.value[qe]=null,qe==="min"?(f.value=m.value,Qt.value=!1,(!g.value||g.value.isBefore(m.value))&&(g.value=m.value)):(h.value=g.value,_e.value=!1,g.value&&g.value.isBefore(m.value)&&(m.value=g.value)),S(!0)},ht=(Fe,qe,ut)=>{I.value.min||(Fe&&(m.value=(m.value||f.value).hour(Fe.hour()).minute(Fe.minute()).second(Fe.second())),ut||(Qt.value=qe),(!g.value||g.value.isBefore(m.value))&&(g.value=m.value,h.value=Fe,He(()=>{M(n.parsedValue)})),S(!0))},pt=(Fe,qe,ut)=>{I.value.max||(Fe&&(g.value=(g.value||h.value).hour(Fe.hour()).minute(Fe.minute()).second(Fe.second())),ut||(_e.value=qe),g.value&&g.value.isBefore(m.value)&&(m.value=g.value),S(!0))},It=()=>{Ct(),a("clear")},Ct=()=>{let Fe=null;r!=null&&r.emptyValues&&(Fe=r.emptyValues.valueOnClear.value),f.value=G1(p(d),{lang:p(v),unit:"month",unlinkPanels:n.unlinkPanels})[0],h.value=f.value.add(1,"month"),g.value=void 0,m.value=void 0,S(!0),a("pick",Fe)},$t=Fe=>Yd(Fe,u.value||"",v.value);function Sn(Fe,qe){if(n.unlinkPanels&&qe){const ut=(Fe==null?void 0:Fe.year())||0,Ie=(Fe==null?void 0:Fe.month())||0,Ke=qe.year(),Ge=qe.month();h.value=ut===Ke&&Ie===Ge?qe.add(1,Up):qe}else h.value=f.value.add(1,Up),qe&&(h.value=h.value.hour(qe.hour()).minute(qe.minute()).second(qe.second()))}return a("set-picker-option",["isValidValue",Se]),a("set-picker-option",["parseUserInput",$t]),a("set-picker-option",["handleClear",Ct]),(Fe,qe)=>(x(),O("div",{class:U([p(b).b(),p(_).b(),p(b).is("border",Fe.border),p(b).is("disabled",p(nt)),{"has-sidebar":Fe.$slots.sidebar||J.value,"has-time":Wt.value,"single-panel":Fe.singlePanel}])},[C("div",{class:U(p(b).e("body-wrapper"))},[ce(Fe.$slots,"sidebar",{class:U(p(b).e("sidebar"))}),J.value?(x(),O("div",{key:0,class:U(p(b).e("sidebar"))},[(x(!0),O(Ve,null,dt(c.value,(ut,Ie)=>(x(),O("button",{key:Ie,type:"button",disabled:p(nt),class:U(p(b).e("shortcut")),onClick:Ke=>p(k)(ut)},Q(ut.text),11,Lhe))),128))],2)):te("v-if",!0),C("div",{class:U(p(b).e("body"))},[Wt.value?(x(),O("div",{key:0,class:U(p(_).e("time-header"))},[C("span",{class:U(p(_).e("editors-wrap"))},[C("span",{class:U(p(_).e("time-picker-wrap"))},[L(p(Ba),{size:"small",disabled:p(y).selecting||p(nt),placeholder:p(E)("el.datepicker.startDate"),class:U(p(_).e("editor")),"model-value":Z.value,"validate-event":!1,readonly:!Fe.editable,onInput:qe[0]||(qe[0]=ut=>Ue(ut,"min")),onChange:qe[1]||(qe[1]=ut=>Gt(ut,"min"))},null,8,["disabled","placeholder","class","model-value","readonly"])],2),st((x(),O("span",{class:U(p(_).e("time-picker-wrap"))},[L(p(Ba),{size:"small",class:U(p(_).e("editor")),disabled:p(y).selecting||p(nt),placeholder:p(E)("el.datepicker.startTime"),"model-value":oe.value,"validate-event":!1,readonly:!Fe.editable,onFocus:qe[2]||(qe[2]=ut=>Qt.value=!0),onInput:qe[3]||(qe[3]=ut=>je(ut,"min")),onChange:qe[4]||(qe[4]=ut=>ft(ut,"min"))},null,8,["class","disabled","placeholder","model-value","readonly"]),L(p(Jm),{visible:Qt.value,format:fe.value,"datetime-role":"start","parsed-value":p(m)||f.value,onPick:ht},null,8,["visible","format","parsed-value"])],2)),[[p(Ns),Ne]])],2),C("span",null,[L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})]),C("span",{class:U([p(_).e("editors-wrap"),"is-right"])},[C("span",{class:U(p(_).e("time-picker-wrap"))},[L(p(Ba),{size:"small",class:U(p(_).e("editor")),disabled:p(y).selecting||p(nt),placeholder:p(E)("el.datepicker.endDate"),"model-value":j.value,readonly:!p(m)||!Fe.editable,"validate-event":!1,onInput:qe[5]||(qe[5]=ut=>Ue(ut,"max")),onChange:qe[6]||(qe[6]=ut=>Gt(ut,"max"))},null,8,["class","disabled","placeholder","model-value","readonly"])],2),st((x(),O("span",{class:U(p(_).e("time-picker-wrap"))},[L(p(Ba),{size:"small",class:U(p(_).e("editor")),disabled:p(y).selecting||p(nt),placeholder:p(E)("el.datepicker.endTime"),"model-value":se.value,readonly:!p(m)||!Fe.editable,"validate-event":!1,onFocus:qe[7]||(qe[7]=ut=>p(m)&&(_e.value=!0)),onInput:qe[8]||(qe[8]=ut=>je(ut,"max")),onChange:qe[9]||(qe[9]=ut=>ft(ut,"max"))},null,8,["class","disabled","placeholder","model-value","readonly"]),L(p(Jm),{"datetime-role":"end",visible:_e.value,format:fe.value,"parsed-value":p(g)||h.value,onPick:pt},null,8,["visible","format","parsed-value"])],2)),[[p(Ns),tt]])],2)],2)):te("v-if",!0),C("div",{class:U([p(b).e("content"),p(_).e("content"),p(_).is("left",!Fe.singlePanel)])},[C("div",{class:U(p(_).e("header"))},[C("button",{type:"button",class:U([p(b).e("icon-btn"),"d-arrow-left"]),"aria-label":p(E)("el.datepicker.prevYear"),disabled:p(nt),onClick:De},[ce(Fe.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,Phe),st(C("button",{type:"button",class:U([p(b).e("icon-btn"),"arrow-left"]),"aria-label":p(E)("el.datepicker.prevMonth"),disabled:p(nt),onClick:Te},[ce(Fe.$slots,"prev-month",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Ji))]),_:1})])],10,Dhe),[[Zt,p(P)==="date"]]),Fe.unlinkPanels||Fe.singlePanel?(x(),O("button",{key:0,type:"button",disabled:!rt.value||p(nt),class:U([[p(b).e("icon-btn"),p(b).is("disabled",!rt.value||p(nt))],"d-arrow-right"]),"aria-label":p(E)("el.datepicker.nextYear"),onClick:Le},[ce(Fe.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,Ohe)):te("v-if",!0),Fe.unlinkPanels&&p(P)==="date"||Fe.singlePanel?(x(),O("button",{key:1,type:"button",disabled:!xe.value||p(nt),class:U([[p(b).e("icon-btn"),p(b).is("disabled",!xe.value||p(nt))],"arrow-right"]),"aria-label":p(E)("el.datepicker.nextMonth"),onClick:me},[ce(Fe.$slots,"next-month",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})])],10,$he)):te("v-if",!0),C("div",null,[C("span",{role:"button",class:U(p(_).e("header-label")),"aria-live":"polite",tabindex:Fe.disabled?void 0:0,"aria-disabled":Fe.disabled,onKeydown:qe[10]||(qe[10]=kn(ut=>p(Y)("year"),["enter"])),onClick:qe[11]||(qe[11]=ut=>p(Y)("year"))},Q(p(N)),43,Rhe),st(C("span",{role:"button","aria-live":"polite",tabindex:Fe.disabled?void 0:0,"aria-disabled":Fe.disabled,class:U([p(_).e("header-label"),{active:p(P)==="month"}]),onKeydown:qe[12]||(qe[12]=kn(ut=>p(Y)("month"),["enter"])),onClick:qe[13]||(qe[13]=ut=>p(Y)("month"))},Q(p(E)(`el.datepicker.month${f.value.month()+1}`)),43,Nhe),[[Zt,p(P)==="date"]])])],2),p(P)==="date"?(x(),ne(Z4,{key:0,ref_key:"leftCurrentViewRef",ref:G,"selection-mode":"range",date:f.value,"min-date":p(m),"max-date":p(g),"range-state":p(y),"disabled-date":p(o),"cell-class-name":p(i),"show-week-number":Fe.showWeekNumber,disabled:p(nt),onChangerange:p(w),onPick:Rt,onSelect:p(T)},null,8,["date","min-date","max-date","range-state","disabled-date","cell-class-name","show-week-number","disabled","onChangerange","onSelect"])):te("v-if",!0),p(P)==="year"?(x(),ne(wh,{key:1,ref_key:"leftCurrentViewRef",ref:G,"selection-mode":"year",date:f.value,"disabled-date":p(o),"parsed-value":Fe.parsedValue,disabled:p(nt),onPick:p(ee)},null,8,["date","disabled-date","parsed-value","disabled","onPick"])):te("v-if",!0),p(P)==="month"?(x(),ne(_h,{key:2,ref_key:"leftCurrentViewRef",ref:G,"selection-mode":"month",date:f.value,"parsed-value":Fe.parsedValue,"disabled-date":p(o),disabled:p(nt),onPick:p(ie)},null,8,["date","parsed-value","disabled-date","disabled","onPick"])):te("v-if",!0)],2),Fe.singlePanel?te("v-if",!0):(x(),O("div",{key:1,class:U([[p(b).e("content"),p(_).e("content")],"is-right"])},[C("div",{class:U(p(_).e("header"))},[Fe.unlinkPanels?(x(),O("button",{key:0,type:"button",disabled:!rt.value||p(nt),class:U([[p(b).e("icon-btn"),p(b).is("disabled",!rt.value||p(nt))],"d-arrow-left"]),"aria-label":p(E)("el.datepicker.prevYear"),onClick:Be},[ce(Fe.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,Bhe)):te("v-if",!0),Fe.unlinkPanels&&p(R)==="date"?(x(),O("button",{key:1,type:"button",disabled:!xe.value||p(nt),class:U([[p(b).e("icon-btn"),p(b).is("disabled",!xe.value||p(nt))],"arrow-left"]),"aria-label":p(E)("el.datepicker.prevMonth"),onClick:ze},[ce(Fe.$slots,"prev-month",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Ji))]),_:1})])],10,Vhe)):te("v-if",!0),C("button",{type:"button","aria-label":p(E)("el.datepicker.nextYear"),class:U([p(b).e("icon-btn"),"d-arrow-right"]),disabled:p(nt),onClick:Re},[ce(Fe.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,zhe),st(C("button",{type:"button",class:U([p(b).e("icon-btn"),"arrow-right"]),disabled:p(nt),"aria-label":p(E)("el.datepicker.nextMonth"),onClick:Ce},[ce(Fe.$slots,"next-month",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})])],10,Fhe),[[Zt,p(R)==="date"]]),C("div",null,[C("span",{role:"button",class:U(p(_).e("header-label")),"aria-live":"polite",tabindex:Fe.disabled?void 0:0,"aria-disabled":Fe.disabled,onKeydown:qe[14]||(qe[14]=kn(ut=>p(X)("year"),["enter"])),onClick:qe[15]||(qe[15]=ut=>p(X)("year"))},Q(p(V)),43,Hhe),st(C("span",{role:"button","aria-live":"polite",tabindex:Fe.disabled?void 0:0,"aria-disabled":Fe.disabled,class:U([p(_).e("header-label"),{active:p(R)==="month"}]),onKeydown:qe[16]||(qe[16]=kn(ut=>p(X)("month"),["enter"])),onClick:qe[17]||(qe[17]=ut=>p(X)("month"))},Q(p(E)(`el.datepicker.month${h.value.month()+1}`)),43,Whe),[[Zt,p(R)==="date"]])])],2),p(R)==="date"?(x(),ne(Z4,{key:0,ref_key:"rightCurrentViewRef",ref:K,"selection-mode":"range",date:h.value,"min-date":p(m),"max-date":p(g),"range-state":p(y),"disabled-date":p(o),"cell-class-name":p(i),"show-week-number":Fe.showWeekNumber,disabled:p(nt),onChangerange:p(w),onPick:Rt,onSelect:p(T)},null,8,["date","min-date","max-date","range-state","disabled-date","cell-class-name","show-week-number","disabled","onChangerange","onSelect"])):te("v-if",!0),p(R)==="year"?(x(),ne(wh,{key:1,ref_key:"rightCurrentViewRef",ref:K,"selection-mode":"year",date:h.value,"disabled-date":p(o),"parsed-value":Fe.parsedValue,disabled:p(nt),onPick:p(re)},null,8,["date","disabled-date","parsed-value","disabled","onPick"])):te("v-if",!0),p(R)==="month"?(x(),ne(_h,{key:2,ref_key:"rightCurrentViewRef",ref:K,"selection-mode":"month",date:h.value,"parsed-value":Fe.parsedValue,"disabled-date":p(o),disabled:p(nt),onPick:p(le)},null,8,["date","parsed-value","disabled-date","disabled","onPick"])):te("v-if",!0)],2))],2)],2),Fe.showFooter&&Wt.value&&(Fe.showConfirm||p(s))?(x(),O("div",{key:0,class:U(p(b).e("footer"))},[p(s)?(x(),ne(p(xa),{key:0,text:"",size:"small",class:U(p(b).e("link-btn")),onClick:It},{default:$(()=>[ue(Q(p(E)("el.datepicker.clear")),1)]),_:1},8,["class"])):te("v-if",!0),Fe.showConfirm?(x(),ne(p(xa),{key:1,plain:"",size:"small",class:U(p(b).e("link-btn")),disabled:Pt.value,onClick:qe[18]||(qe[18]=ut=>p(S)(!1))},{default:$(()=>[ue(Q(p(E)("el.datepicker.confirm")),1)]),_:1},8,["class","disabled"])):te("v-if",!0)],2)):te("v-if",!0)],2))}}),Uhe=Khe;const Ghe=$e({...hw}),Yhe=["pick","set-picker-option","calendar-change"],jhe=({unlinkPanels:e,leftDate:t,rightDate:n})=>{const{t:a}=Vt();return{leftPrevYear:()=>{t.value=t.value.subtract(1,"year"),e.value||(n.value=n.value.subtract(1,"year"))},rightNextYear:()=>{e.value||(t.value=t.value.add(1,"year")),n.value=n.value.add(1,"year")},leftNextYear:()=>{t.value=t.value.add(1,"year")},rightPrevYear:()=>{n.value=n.value.subtract(1,"year")},leftLabel:D(()=>`${t.value.year()} ${a("el.datepicker.year")}`),rightLabel:D(()=>`${n.value.year()} ${a("el.datepicker.year")}`),leftYear:D(()=>t.value.year()),rightYear:D(()=>n.value.year()===t.value.year()?t.value.year()+1:n.value.year())}},qhe=["disabled","onClick"],Xhe=["disabled"],Zhe=["disabled"],Jhe=["disabled"],Qhe=["disabled"],Gp="year";var epe=q({name:"DatePickerMonthRange",__name:"panel-month-range",props:Ghe,emits:Yhe,setup(e,{emit:t}){const n=e,a=t,{lang:r}=Vt(),o=Ze(nl),{shortcuts:i,disabledDate:l,cellClassName:s}=o.props,u=cn(o.props,"format"),c=cn(o.props,"defaultValue"),d=F(gt().locale(r.value)),v=F(gt().locale(r.value).add(1,Gp)),{minDate:f,maxDate:h,rangeState:m,ppNs:g,drpNs:y,handleChangeRange:b,handleRangeConfirm:_,handleShortcutClick:w,onSelect:S,parseValue:k}=gw(n,{defaultValue:c,leftDate:d,rightDate:v,unit:Gp,sortDates:N}),T=D(()=>!!i.length),{leftPrevYear:M,rightNextYear:E,leftNextYear:A,rightPrevYear:I,leftLabel:P,rightLabel:R,leftYear:G,rightYear:K}=jhe({unlinkPanels:cn(n,"unlinkPanels"),leftDate:d,rightDate:v}),H=D(()=>n.singlePanel||n.unlinkPanels&&K.value>G.value+1),z=(Y,X=!0)=>{const ee=Y.minDate,re=Y.maxDate;h.value===re&&f.value===ee||(a("calendar-change",[ee.toDate(),re&&re.toDate()]),h.value=re,f.value=ee,X&&_())},B=()=>{let Y=null;o!=null&&o.emptyValues&&(Y=o.emptyValues.valueOnClear.value),d.value=G1(p(c),{lang:p(r),unit:"year",unlinkPanels:n.unlinkPanels})[0],v.value=d.value.add(1,"year"),a("pick",Y)},W=Y=>Yd(Y,u.value,r.value);function N(Y,X){n.unlinkPanels&&X?v.value=((Y==null?void 0:Y.year())||0)===X.year()?X.add(1,Gp):X:v.value=d.value.add(1,Gp)}const V=$n();return ye(()=>n.visible,Y=>{!Y&&m.value.selecting&&(k(n.parsedValue),S(!1))}),a("set-picker-option",["isValidValue",bh]),a("set-picker-option",["parseUserInput",W]),a("set-picker-option",["handleClear",B]),(Y,X)=>(x(),O("div",{class:U([p(g).b(),p(y).b(),p(g).is("border",Y.border),p(g).is("disabled",p(V)),{"has-sidebar":!!Y.$slots.sidebar||T.value,"single-panel":Y.singlePanel}])},[C("div",{class:U(p(g).e("body-wrapper"))},[ce(Y.$slots,"sidebar",{class:U(p(g).e("sidebar"))}),T.value?(x(),O("div",{key:0,class:U(p(g).e("sidebar"))},[(x(!0),O(Ve,null,dt(p(i),(ee,re)=>(x(),O("button",{key:re,type:"button",class:U(p(g).e("shortcut")),disabled:p(V),onClick:ie=>p(w)(ee)},Q(ee.text),11,qhe))),128))],2)):te("v-if",!0),C("div",{class:U(p(g).e("body"))},[C("div",{class:U([p(g).e("content"),p(y).e("content"),p(y).is("left",!Y.singlePanel)])},[C("div",{class:U(p(y).e("header"))},[C("button",{type:"button",class:U([p(g).e("icon-btn"),"d-arrow-left"]),disabled:p(V),onClick:X[0]||(X[0]=(...ee)=>p(M)&&p(M)(...ee))},[ce(Y.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,Xhe),Y.unlinkPanels||Y.singlePanel?(x(),O("button",{key:0,type:"button",disabled:!H.value||p(V),class:U([[p(g).e("icon-btn"),p(g).is("disabled",!H.value||p(V))],"d-arrow-right"]),onClick:X[1]||(X[1]=(...ee)=>p(A)&&p(A)(...ee))},[ce(Y.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,Zhe)):te("v-if",!0),C("div",null,Q(p(P)),1)],2),L(_h,{"selection-mode":"range",date:d.value,"min-date":p(f),"max-date":p(h),"range-state":p(m),"disabled-date":p(l),disabled:p(V),"cell-class-name":p(s),onChangerange:p(b),onPick:z,onSelect:p(S)},null,8,["date","min-date","max-date","range-state","disabled-date","disabled","cell-class-name","onChangerange","onSelect"])],2),Y.singlePanel?te("v-if",!0):(x(),O("div",{key:0,class:U([[p(g).e("content"),p(y).e("content")],"is-right"])},[C("div",{class:U(p(y).e("header"))},[Y.unlinkPanels?(x(),O("button",{key:0,type:"button",disabled:!H.value||p(V),class:U([[p(g).e("icon-btn"),p(g).is("disabled",!H.value||p(V))],"d-arrow-left"]),onClick:X[2]||(X[2]=(...ee)=>p(I)&&p(I)(...ee))},[ce(Y.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,Jhe)):te("v-if",!0),C("button",{type:"button",class:U([p(g).e("icon-btn"),"d-arrow-right"]),disabled:p(V),onClick:X[3]||(X[3]=(...ee)=>p(E)&&p(E)(...ee))},[ce(Y.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,Qhe),C("div",null,Q(p(R)),1)],2),L(_h,{"selection-mode":"range",date:v.value,"min-date":p(f),"max-date":p(h),"range-state":p(m),"disabled-date":p(l),disabled:p(V),"cell-class-name":p(s),onChangerange:p(b),onPick:z,onSelect:p(S)},null,8,["date","min-date","max-date","range-state","disabled-date","disabled","cell-class-name","onChangerange","onSelect"])],2))],2)],2)],2))}}),tpe=epe;const npe=$e({...hw}),ape=["pick","set-picker-option","calendar-change"],rpe=({unlinkPanels:e,leftDate:t,rightDate:n})=>({leftPrevYear:()=>{t.value=t.value.subtract(10,"year"),e.value||(n.value=n.value.subtract(10,"year"))},rightNextYear:()=>{e.value||(t.value=t.value.add(10,"year")),n.value=n.value.add(10,"year")},leftNextYear:()=>{t.value=t.value.add(10,"year")},rightPrevYear:()=>{n.value=n.value.subtract(10,"year")},leftLabel:D(()=>{const l=Math.floor(t.value.year()/10)*10;return`${l}-${l+9}`}),rightLabel:D(()=>{const l=Math.floor(n.value.year()/10)*10;return`${l}-${l+9}`}),leftYear:D(()=>Math.floor(t.value.year()/10)*10+9),rightYear:D(()=>Math.floor(n.value.year()/10)*10)}),ope=["disabled","onClick"],ipe=["disabled"],lpe=["disabled"],spe=["disabled"],upe=["disabled"],Nc=10,Df="year";var cpe=q({name:"DatePickerYearRange",__name:"panel-year-range",props:npe,emits:ape,setup(e,{emit:t}){const n=e,a=t,{lang:r}=Vt(),o=F(gt().locale(r.value)),i=F(gt().locale(r.value).add(Nc,Df)),l=Ze(nl),{shortcuts:s,disabledDate:u,cellClassName:c}=l.props,d=cn(l.props,"format"),v=cn(l.props,"defaultValue"),{minDate:f,maxDate:h,rangeState:m,ppNs:g,drpNs:y,handleChangeRange:b,handleRangeConfirm:_,handleShortcutClick:w,onSelect:S,parseValue:k}=gw(n,{defaultValue:v,leftDate:o,rightDate:i,step:Nc,unit:Df,sortDates:re}),{leftPrevYear:T,rightNextYear:M,leftNextYear:E,rightPrevYear:A,leftLabel:I,rightLabel:P,leftYear:R,rightYear:G}=rpe({unlinkPanels:cn(n,"unlinkPanels"),leftDate:o,rightDate:i}),K=$n(),H=D(()=>!!s.length),z=D(()=>[g.b(),y.b(),g.is("border",n.border),g.is("disabled",K.value),{"has-sidebar":!!Xn().sidebar||H.value,"single-panel":n.singlePanel}]),B=D(()=>({content:[g.e("content"),y.e("content"),y.is("left",!n.singlePanel)],arrowLeftBtn:[g.e("icon-btn"),"d-arrow-left"],arrowRightBtn:[g.e("icon-btn"),g.is("disabled",!N.value||K.value),"d-arrow-right"]})),W=D(()=>({content:[g.e("content"),y.e("content"),"is-right"],arrowLeftBtn:[g.e("icon-btn"),g.is("disabled",!N.value||K.value),"d-arrow-left"],arrowRightBtn:[g.e("icon-btn"),"d-arrow-right"]})),N=D(()=>n.singlePanel||n.unlinkPanels&&G.value>R.value+1),V=(ie,le=!0)=>{const ve=ie.minDate,he=ie.maxDate;h.value===he&&f.value===ve||(a("calendar-change",[ve.toDate(),he&&he.toDate()]),h.value=he,f.value=ve,le&&_())},Y=ie=>Yd(ie,d.value,r.value),X=ie=>bh(ie)&&(u?!u(ie[0].toDate())&&!u(ie[1].toDate()):!0),ee=()=>{let ie=null;l!=null&&l.emptyValues&&(ie=l.emptyValues.valueOnClear.value);const le=G1(p(v),{lang:p(r),step:Nc,unit:Df,unlinkPanels:n.unlinkPanels});o.value=le[0],i.value=le[1],a("pick",ie)};function re(ie,le){if(n.unlinkPanels&&le){const ve=(ie==null?void 0:ie.year())||0,he=le.year();i.value=ve+Nc>he?le.add(Nc,Df):le}else i.value=o.value.add(Nc,Df)}return ye(()=>n.visible,ie=>{!ie&&m.value.selecting&&(k(n.parsedValue),S(!1))}),a("set-picker-option",["isValidValue",X]),a("set-picker-option",["parseUserInput",Y]),a("set-picker-option",["handleClear",ee]),(ie,le)=>(x(),O("div",{class:U(z.value)},[C("div",{class:U(p(g).e("body-wrapper"))},[ce(ie.$slots,"sidebar",{class:U(p(g).e("sidebar"))}),H.value?(x(),O("div",{key:0,class:U(p(g).e("sidebar"))},[(x(!0),O(Ve,null,dt(p(s),(ve,he)=>(x(),O("button",{key:he,type:"button",class:U(p(g).e("shortcut")),disabled:p(K),onClick:J=>p(w)(ve)},Q(ve.text),11,ope))),128))],2)):te("v-if",!0),C("div",{class:U(p(g).e("body"))},[C("div",{class:U(B.value.content)},[C("div",{class:U(p(y).e("header"))},[C("button",{type:"button",class:U(B.value.arrowLeftBtn),disabled:p(K),onClick:le[0]||(le[0]=(...ve)=>p(T)&&p(T)(...ve))},[ce(ie.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,ipe),ie.unlinkPanels||ie.singlePanel?(x(),O("button",{key:0,type:"button",disabled:!N.value||p(K),class:U(B.value.arrowRightBtn),onClick:le[1]||(le[1]=(...ve)=>p(E)&&p(E)(...ve))},[ce(ie.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,lpe)):te("v-if",!0),C("div",null,Q(p(I)),1)],2),L(wh,{"selection-mode":"range",date:o.value,"min-date":p(f),"max-date":p(h),"range-state":p(m),"disabled-date":p(u),disabled:p(K),"cell-class-name":p(c),onChangerange:p(b),onPick:V,onSelect:p(S)},null,8,["date","min-date","max-date","range-state","disabled-date","disabled","cell-class-name","onChangerange","onSelect"])],2),ie.singlePanel?te("v-if",!0):(x(),O("div",{key:0,class:U(W.value.content)},[C("div",{class:U(p(y).e("header"))},[ie.unlinkPanels?(x(),O("button",{key:0,type:"button",disabled:!N.value||p(K),class:U(W.value.arrowLeftBtn),onClick:le[2]||(le[2]=(...ve)=>p(A)&&p(A)(...ve))},[ce(ie.$slots,"prev-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Al))]),_:1})])],10,spe)):te("v-if",!0),C("button",{type:"button",class:U(W.value.arrowRightBtn),disabled:p(K),onClick:le[3]||(le[3]=(...ve)=>p(M)&&p(M)(...ve))},[ce(ie.$slots,"next-year",{},()=>[L(p(Xe),null,{default:$(()=>[L(p(Il))]),_:1})])],10,upe),C("div",null,Q(p(P)),1)],2),L(wh,{"selection-mode":"range",date:i.value,"min-date":p(f),"max-date":p(h),"range-state":p(m),"disabled-date":p(u),disabled:p(K),"cell-class-name":p(c),onChangerange:p(b),onPick:V,onSelect:p(S)},null,8,["date","min-date","max-date","range-state","disabled-date","disabled","cell-class-name","onChangerange","onSelect"])],2))],2)],2)],2))}}),dpe=cpe;const fpe=function(e){switch(e){case"daterange":case"datetimerange":return Uhe;case"monthrange":return tpe;case"yearrange":return dpe;default:return Mhe}};var HP={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){return function(n,a){var r=a.prototype,o=r.format;r.format=function(i){var l=this,s=this.$locale();if(!this.isValid())return o.bind(this)(i);var u=this.$utils(),c=(i||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,function(d){switch(d){case"Q":return Math.ceil((l.$M+1)/3);case"Do":return s.ordinal(l.$D);case"gggg":return l.weekYear();case"GGGG":return l.isoWeekYear();case"wo":return s.ordinal(l.week(),"W");case"w":case"ww":return u.s(l.week(),d==="w"?1:2,"0");case"W":case"WW":return u.s(l.isoWeek(),d==="W"?1:2,"0");case"k":case"kk":return u.s(String(l.$H===0?24:l.$H),d==="k"?1:2,"0");case"X":return Math.floor(l.$d.getTime()/1e3);case"x":return l.$d.getTime();case"z":return"["+l.offsetName()+"]";case"zzz":return"["+l.offsetName("long")+"]";default:return d}});return o.bind(this)(c)}}})})(HP);var vpe=HP.exports;const hpe=Kl(vpe);var WP={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){var n="week",a="year";return function(r,o,i){var l=o.prototype;l.week=function(s){if(s===void 0&&(s=null),s!==null)return this.add(7*(s-this.week()),"day");var u=this.$locale().yearStart||1;if(this.month()===11&&this.date()>25){var c=i(this).startOf(a).add(1,a).date(u),d=i(this).endOf(n);if(c.isBefore(d))return 1}var v=i(this).startOf(a).date(u).startOf(n).subtract(1,"millisecond"),f=this.diff(v,n,!0);return f<0?i(this).startOf("week").week():Math.ceil(f)},l.weeks=function(s){return s===void 0&&(s=null),this.week(s)}}})})(WP);var ppe=WP.exports;const mpe=Kl(ppe);var KP={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){return function(n,a){a.prototype.weekYear=function(){var r=this.month(),o=this.week(),i=this.year();return o===1&&r===11?i+1:r===0&&o>=52?i-1:i}}})})(KP);var gpe=KP.exports;const ype=Kl(gpe);var UP={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){return function(n,a,r){a.prototype.dayOfYear=function(o){var i=Math.round((r(this).startOf("day")-r(this).startOf("year"))/864e5)+1;return o==null?i:this.add(o-i,"day")}}})})(UP);var bpe=UP.exports;const _pe=Kl(bpe);var GP={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){return function(n,a){a.prototype.isSameOrAfter=function(r,o){return this.isSame(r,o)||this.isAfter(r,o)}}})})(GP);var wpe=GP.exports;const Spe=Kl(wpe);var YP={exports:{}};(function(e,t){(function(n,a){e.exports=a()})(Wl,function(){return function(n,a){a.prototype.isSameOrBefore=function(r,o){return this.isSame(r,o)||this.isBefore(r,o)}}})})(YP);var Cpe=YP.exports;const xpe=Kl(Cpe);function kpe(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}gt.extend(WL);gt.extend(hpe);gt.extend(rw);gt.extend(mpe);gt.extend(ype);gt.extend(_pe);gt.extend(Spe);gt.extend(xpe);var Tpe=q({name:"ElDatePickerPanel",install:null,inheritAttrs:!1,props:Jve,emits:[bt,"calendar-change","panel-change","visible-change","clear"],setup(e,{slots:t,emit:n,attrs:a}){const r=ke("picker-panel");zt(Ze("EP_PICKER_BASE",void 0))&&Ft(nl,{props:nn({...Ta(e)})}),Ft(fw,{slots:t,pickerNs:r});const{parsedValue:o,onCalendarChange:i,onPanelChange:l,onSetPickerOption:s,onPick:u}=Ze(bL,()=>kL(e,n),!0);return()=>L(fpe(e.type),Tt(Tc(a,"onPick"),e,{parsedValue:o.value,"onSet-picker-option":s,"onCalendar-change":i,"onPanel-change":l,onClear:()=>n("clear"),onPick:u}),kpe(t)?t:{default:()=>[t]})}});const jP=wt(Tpe),Mpe=$e({...aw,type:{type:ae(String),default:"date"}});function Epe(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}var Ape=q({name:"ElDatePicker",install:null,props:Mpe,emits:[bt],setup(e,{expose:t,emit:n,slots:a}){Ft(tw,nn(cn(e,"popperOptions")));const r=F();t({focus:()=>{var i;(i=r.value)==null||i.focus()},blur:()=>{var i;(i=r.value)==null||i.blur()},handleOpen:()=>{var i;(i=r.value)==null||i.handleOpen()},handleClose:()=>{var i;(i=r.value)==null||i.handleClose()}});const o=i=>{n(bt,i)};return()=>L(TL,Tt(e,{format:e.format??(Tue[e.type]||"YYYY-MM-DD"),type:e.type,ref:r,"onUpdate:modelValue":o}),{default:i=>L(jP,Tt({disabled:e.disabled,editable:e.editable,border:!1},i),Epe(a)?a:{default:()=>[a]}),"range-separator":a["range-separator"]})}});const Ipe=wt(Ape),Lpe=$e({border:Boolean,column:{type:Number,default:3},direction:{type:String,values:["horizontal","vertical"],default:"horizontal"},size:pa,title:{type:String,default:""},extra:{type:String,default:""},labelWidth:{type:[String,Number]}}),qP="ElDescriptionsItem",Ppe=$e({label:{type:String,default:""},span:{type:Number,default:1},rowspan:{type:Number,default:1},width:{type:[String,Number],default:""},minWidth:{type:[String,Number],default:""},labelWidth:{type:[String,Number]},align:{type:String,values:cC,default:"left"},labelAlign:{type:String,values:cC},className:{type:String,default:""},labelClassName:{type:String,default:""}}),XP=q({name:qP,props:Ppe}),yw=Symbol("elDescriptions"),Dpe=$e({row:{type:ae(Array),default:()=>[]}});var Of=q({name:"ElDescriptionsCell",props:{cell:{type:Object},tag:{type:String,default:"td"},type:{type:String}},setup(){return{descriptions:Ze(yw,{})}},render(){var m;const e=tX(this.cell),t=(((m=this.cell)==null?void 0:m.dirs)||[]).map(g=>{const{dir:y,arg:b,modifiers:_,value:w}=g;return[y,w,b,_]}),{border:n,direction:a}=this.descriptions,r=a==="vertical",o=()=>{var g,y,b;return((b=(y=(g=this.cell)==null?void 0:g.children)==null?void 0:y.label)==null?void 0:b.call(y))||e.label},i=()=>{var g,y,b;return(b=(y=(g=this.cell)==null?void 0:g.children)==null?void 0:y.default)==null?void 0:b.call(y)},l=e.span,s=e.rowspan,u=e.align?`is-${e.align}`:"",c=e.labelAlign?`is-${e.labelAlign}`:u,d=e.className,v=e.labelClassName,f={width:Vn(this.type==="label"?e.labelWidth??this.descriptions.labelWidth??e.width:e.width),minWidth:Vn(e.minWidth)},h=ke("descriptions");switch(this.type){case"label":return st(ot(this.tag,{style:f,class:[h.e("cell"),h.e("label"),h.is("bordered-label",n),h.is("vertical-label",r),c,v],colSpan:r?l:1,rowspan:r?1:s},o()),t);case"content":return st(ot(this.tag,{style:f,class:[h.e("cell"),h.e("content"),h.is("bordered-content",n),h.is("vertical-content",r),u,d],colSpan:r?l:l*2-1,rowspan:r?s*2-1:s},i()),t);default:{const g=o(),y={},b=Vn(e.labelWidth??this.descriptions.labelWidth);return b&&(y.width=b,y.display="inline-block"),st(ot("td",{style:f,class:[h.e("cell"),u],colSpan:l,rowspan:s},[Un(g)?void 0:ot("span",{style:y,class:[h.e("label"),v]},g),ot("span",{class:[h.e("content"),d]},i())]),t)}}}});const Ope={key:1};var $pe=q({name:"ElDescriptionsRow",__name:"descriptions-row",props:Dpe,setup(e){const t=Ze(yw,{});return(n,a)=>p(t).direction==="vertical"?(x(),O(Ve,{key:0},[C("tr",null,[(x(!0),O(Ve,null,dt(e.row,(r,o)=>(x(),ne(p(Of),{key:`tr1-${o}`,cell:r,tag:"th",type:"label"},null,8,["cell"]))),128))]),C("tr",null,[(x(!0),O(Ve,null,dt(e.row,(r,o)=>(x(),ne(p(Of),{key:`tr2-${o}`,cell:r,tag:"td",type:"content"},null,8,["cell"]))),128))])],64)):(x(),O("tr",Ope,[(x(!0),O(Ve,null,dt(e.row,(r,o)=>(x(),O(Ve,{key:`tr3-${o}`},[p(t).border?(x(),O(Ve,{key:0},[L(p(Of),{cell:r,tag:"td",type:"label"},null,8,["cell"]),L(p(Of),{cell:r,tag:"td",type:"content"},null,8,["cell"])],64)):(x(),ne(p(Of),{key:1,cell:r,tag:"td",type:"both"},null,8,["cell"]))],64))),128))]))}}),Rpe=$pe,Npe=q({name:"ElDescriptions",__name:"description",props:Lpe,setup(e){const t=e,n=ke("descriptions"),a=ca(),r=Xn();Ft(yw,t);const o=D(()=>[n.b(),n.m(a.value)]),i=(s,u,c,d=!1)=>(s.props||(s.props={}),u>c&&(s.props.span=c),d&&(s.props.span=u),s),l=()=>{if(!r.default)return[];const s=eo(r.default()).filter(h=>{var m;return((m=h==null?void 0:h.type)==null?void 0:m.name)===qP}),u=[];let c=[],d=t.column,v=0;const f=[];return s.forEach((h,m)=>{var _,w,S;const g=((_=h.props)==null?void 0:_.span)||1,y=((w=h.props)==null?void 0:w.rowspan)||1,b=u.length;if(f[b]||(f[b]=0),y>1)for(let k=1;k0&&(d-=f[b],f[b]=0),md?d:g),m===s.length-1){const k=t.column-v%t.column;c.push(i(h,k,d,!0)),u.push(c);return}g(x(),O("div",{class:U(o.value)},[e.title||e.extra||s.$slots.title||s.$slots.extra?(x(),O("div",{key:0,class:U(p(n).e("header"))},[C("div",{class:U(p(n).e("title"))},[ce(s.$slots,"title",{},()=>[ue(Q(e.title),1)])],2),C("div",{class:U(p(n).e("extra"))},[ce(s.$slots,"extra",{},()=>[ue(Q(e.extra),1)])],2)],2)):te("v-if",!0),C("div",{class:U(p(n).e("body"))},[C("table",{class:U([p(n).e("table"),p(n).is("bordered",e.border)])},[C("tbody",null,[(x(!0),O(Ve,null,dt(l(),(c,d)=>(x(),ne(Rpe,{key:d,row:c},null,8,["row"]))),128))])],2)],2)],2))}}),Bpe=Npe;const Vpe=wt(Bpe,{DescriptionsItem:XP}),zpe=On(XP),ZP=$e({center:Boolean,alignCenter:{type:Boolean,default:void 0},closeIcon:{type:pn},draggable:{type:Boolean,default:void 0},overflow:{type:Boolean,default:void 0},fullscreen:Boolean,headerClass:{type:ae([String,Array,Object,Boolean]),default:void 0},bodyClass:{type:ae([String,Array,Object,Boolean]),default:void 0},footerClass:{type:ae([String,Array,Object,Boolean]),default:void 0},showClose:{type:Boolean,default:!0},title:{type:String,default:""},ariaLevel:{type:String,default:"2"}}),Fpe={close:()=>!0},JP=$e({...ZP,appendToBody:Boolean,appendTo:{type:ae([String,Object]),default:"body"},beforeClose:{type:ae(Function)},destroyOnClose:Boolean,closeOnClickModal:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},lockScroll:{type:Boolean,default:!0},modal:{type:Boolean,default:!0},modalPenetrable:Boolean,openDelay:{type:Number,default:0},closeDelay:{type:Number,default:0},top:{type:String},modelValue:Boolean,modalClass:{type:ae([String,Array,Object,Boolean]),default:void 0},width:{type:[String,Number]},zIndex:{type:Number},trapFocus:Boolean,headerAriaLevel:{type:String,default:"2"},transition:{type:ae([String,Object]),default:void 0}}),QP={open:()=>!0,opened:()=>!0,close:()=>!0,closed:()=>!0,[bt]:e=>vn(e),openAutoFocus:()=>!0,closeAutoFocus:()=>!0},Hpe=$e({mask:{type:Boolean,default:!0},customMaskEvent:Boolean,overlayClass:{type:ae([String,Array,Object,Boolean]),default:void 0},zIndex:{type:ae([String,Number])}}),Wpe={click:e=>e instanceof MouseEvent},Kpe="overlay";var Upe=q({name:"ElOverlay",props:Hpe,emits:Wpe,setup(e,{slots:t,emit:n}){const a=ke(Kpe),r=s=>{n("click",s)},{onClick:o,onMousedown:i,onMouseup:l}=O6(e.customMaskEvent?void 0:r);return()=>e.mask?L("div",{class:[a.b(),e.overlayClass],style:{zIndex:e.zIndex},onClick:o,onMousedown:i,onMouseup:l},[ce(t,"default")],14,["onClick","onMouseup","onMousedown"]):ot("div",{class:e.overlayClass,style:{zIndex:e.zIndex,position:"fixed",top:"0px",right:"0px",bottom:"0px",left:"0px"}},[ce(t,"default")])}});const bw=Upe,eD=Symbol("dialogInjectionKey"),xx="dialog-fade",Gpe="ElDialog",tD=(e,t)=>{const n=Et().emit,{nextZIndex:a}=tp();let r="";const o=Fa(),i=Fa(),l=F(!1),s=F(!1),u=F(!1),c=F(e.zIndex??a()),d=F(!1);let v,f;const h=Hl(),m=D(()=>{var Y;return((Y=h.value)==null?void 0:Y.namespace)??"el"}),g=D(()=>{var Y;return(Y=h.value)==null?void 0:Y.dialog}),y=D(()=>{const Y={},X=`--${m.value}-dialog`;if(!e.fullscreen){e.top&&(Y[`${X}-margin-top`]=e.top);const ee=Vn(e.width);ee&&(Y[`${X}-width`]=ee)}return Y}),b=D(()=>{var Y;return(e.draggable??((Y=g.value)==null?void 0:Y.draggable)??!1)&&!e.fullscreen}),_=D(()=>{var Y;return e.alignCenter??((Y=g.value)==null?void 0:Y.alignCenter)??!1}),w=D(()=>{var Y;return e.overflow??((Y=g.value)==null?void 0:Y.overflow)??!1}),S=D(()=>e.modalPenetrable&&!e.modal&&!e.fullscreen),k=D(()=>_.value?{display:"flex"}:{}),T=D(()=>{var ee;const Y=e.transition??((ee=g.value)==null?void 0:ee.transition)??"dialog-fade",X={name:Y,onAfterEnter:M,onBeforeLeave:A,onAfterLeave:E};if(_t(Y)){const re={...Y},ie=(le,ve)=>he=>{Ae(le)?le.forEach(J=>{Qe(J)&&J(he)}):Qe(le)&&le(he),ve()};return re.onAfterEnter=ie(re.onAfterEnter,M),re.onBeforeLeave=ie(re.onBeforeLeave,A),re.onAfterLeave=ie(re.onAfterLeave,E),re.name||(re.name=xx,un(Gpe,`transition.name is missing when using object syntax, fallback to '${xx}'`)),re}return X});function M(){n("opened")}function E(){n("closed"),n(bt,!1),e.destroyOnClose&&(u.value=!1),d.value=!1}function A(){d.value=!0,n("close")}function I(){f==null||f(),v==null||v(),e.openDelay&&e.openDelay>0?{stop:v}=Bd(()=>K(),e.openDelay):K()}function P(){v==null||v(),f==null||f(),e.closeDelay&&e.closeDelay>0?{stop:f}=Bd(()=>H(),e.closeDelay):H()}function R(){function Y(X){X||(s.value=!0,l.value=!1)}e.beforeClose?e.beforeClose(Y):P()}function G(){e.closeOnClickModal&&R()}function K(){qt&&(l.value=!0)}function H(){l.value=!1}function z(){n("openAutoFocus")}function B(){n("closeAutoFocus")}function W(Y){var X;((X=Y.detail)==null?void 0:X.focusReason)==="pointer"&&Y.preventDefault()}e.lockScroll&&D1(l);function N(){e.closeOnPressEscape&&R()}function V(){!l.value||!S.value||e.zIndex!==void 0||(c.value=a())}return ye(()=>e.zIndex,()=>{c.value=e.zIndex??a()}),ye(()=>e.modelValue,Y=>{Y?(s.value=!1,d.value=!1,I(),u.value=!0,c.value=e.zIndex??a(),He(()=>{n("open"),t.value&&(t.value.parentElement.scrollTop=0,t.value.parentElement.scrollLeft=0,t.value.scrollTop=0)})):l.value&&P()}),ye(()=>e.fullscreen,Y=>{t.value&&(Y?(r=t.value.style.transform,t.value.style.transform=""):t.value.style.transform=r)}),mt(()=>{e.modelValue&&(l.value=!0,u.value=!0,I())}),{afterEnter:M,afterLeave:E,beforeLeave:A,handleClose:R,onModalClick:G,close:P,doClose:H,onOpenAutoFocus:z,onCloseAutoFocus:B,onCloseRequested:N,onFocusoutPrevented:W,bringToFront:V,titleId:o,bodyId:i,closed:s,style:y,overlayDialogStyle:k,rendered:u,visible:l,zIndex:c,transitionConfig:T,_draggable:b,_alignCenter:_,_overflow:w,closing:d,penetrable:S}},_w=(...e)=>t=>{e.forEach(n=>{n.value=t})},Ype=["aria-level"],jpe=["aria-label"],qpe=["id"];var Xpe=q({name:"ElDialogContent",__name:"dialog-content",props:ZP,emits:Fpe,setup(e,{expose:t}){const{t:n}=Vt(),{Close:a}=YI,r=e,{dialogRef:o,headerRef:i,bodyId:l,ns:s,style:u}=Ze(eD),{focusTrapRef:c}=Ze(rL),d=_w(c,o),v=D(()=>!!r.draggable),{resetPosition:f,updatePosition:h,isDragging:m}=BA(o,i,v,D(()=>!!r.overflow)),g=D(()=>[s.b(),s.is("fullscreen",r.fullscreen),s.is("draggable",v.value),s.is("dragging",m.value),s.is("align-center",!!r.alignCenter),{[s.m("center")]:r.center}]);return t({resetPosition:f,updatePosition:h}),(y,b)=>(x(),O("div",{ref:p(d),class:U(g.value),style:et(p(u)),tabindex:"-1"},[C("header",{ref_key:"headerRef",ref:i,class:U([p(s).e("header"),e.headerClass,{"show-close":e.showClose}])},[ce(y.$slots,"header",{},()=>[C("span",{role:"heading","aria-level":e.ariaLevel,class:U(p(s).e("title"))},Q(e.title),11,Ype)]),e.showClose?(x(),O("button",{key:0,"aria-label":p(n)("el.dialog.close"),class:U(p(s).e("headerbtn")),type:"button",onClick:b[0]||(b[0]=_=>y.$emit("close"))},[L(p(Xe),{class:U(p(s).e("close"))},{default:$(()=>[(x(),ne(St(e.closeIcon||p(a))))]),_:1},8,["class"])],10,jpe)):te("v-if",!0)],2),C("div",{id:p(l),class:U([p(s).e("body"),e.bodyClass])},[ce(y.$slots,"default")],10,qpe),y.$slots.footer?(x(),O("footer",{key:0,class:U([p(s).e("footer"),e.footerClass])},[ce(y.$slots,"footer")],2)):te("v-if",!0)],6))}}),Zpe=Xpe;const Jpe=["aria-label","aria-labelledby","aria-describedby"];var Qpe=q({name:"ElDialog",inheritAttrs:!1,__name:"dialog",props:JP,emits:QP,setup(e,{expose:t}){const n=e,a=Xn();Ri({scope:"el-dialog",from:"the title slot",replacement:"the header slot",version:"3.0.0",ref:"https://element-plus.org/en-US/component/dialog.html#slots"},D(()=>!!a.title));const r=ke("dialog"),o=F(),i=F(),l=F(),{visible:s,titleId:u,bodyId:c,style:d,overlayDialogStyle:v,rendered:f,transitionConfig:h,zIndex:m,_draggable:g,_alignCenter:y,_overflow:b,penetrable:_,handleClose:w,onModalClick:S,onOpenAutoFocus:k,onCloseAutoFocus:T,onCloseRequested:M,onFocusoutPrevented:E,bringToFront:A,closing:I}=tD(n,o);Ft(eD,{dialogRef:o,headerRef:i,bodyId:c,ns:r,rendered:f,style:d});const P=O6(S);return t({visible:s,dialogContentRef:l,resetPosition:()=>{var G;(G=l.value)==null||G.resetPosition()},handleClose:w}),(G,K)=>(x(),ne(sf,{to:e.appendTo,disabled:e.appendTo!=="body"?!1:!e.appendToBody},[L(Pa,Tt(p(h),{persisted:""}),{default:$(()=>[st(L(p(bw),{"custom-mask-event":"",mask:e.modal,"overlay-class":[e.modalClass??"",`${p(r).namespace.value}-modal-dialog`,p(r).is("penetrable",p(_))],"z-index":p(m)},{default:$(()=>[C("div",{role:"dialog","aria-modal":"true","aria-label":e.title||void 0,"aria-labelledby":e.title?void 0:p(u),"aria-describedby":p(c),class:U([`${p(r).namespace.value}-overlay-dialog`,p(r).is("closing",p(I))]),style:et(p(v)),onClick:K[0]||(K[0]=(...H)=>p(P).onClick&&p(P).onClick(...H)),onMousedown:K[1]||(K[1]=(...H)=>p(P).onMousedown&&p(P).onMousedown(...H)),onMouseup:K[2]||(K[2]=(...H)=>p(P).onMouseup&&p(P).onMouseup(...H))},[L(p(pf),{loop:"",trapped:p(s),"focus-start-el":"container",onFocusAfterTrapped:p(k),onFocusAfterReleased:p(T),onFocusoutPrevented:p(E),onReleaseRequested:p(M)},{default:$(()=>[p(f)?(x(),ne(Zpe,Tt({key:0,ref_key:"dialogContentRef",ref:l},G.$attrs,{center:e.center,"align-center":p(y),"close-icon":e.closeIcon,draggable:p(g),overflow:p(b),fullscreen:e.fullscreen,"header-class":e.headerClass,"body-class":e.bodyClass,"footer-class":e.footerClass,"show-close":e.showClose,title:e.title,"aria-level":e.headerAriaLevel,onClose:p(w),onMousedown:p(A)}),xr({header:$(()=>[G.$slots.title?ce(G.$slots,"title",{key:1}):ce(G.$slots,"header",{key:0,close:p(w),titleId:p(u),titleClass:p(r).e("title")})]),default:$(()=>[ce(G.$slots,"default")]),_:2},[G.$slots.footer?{name:"footer",fn:$(()=>[ce(G.$slots,"footer")]),key:"0"}:void 0]),1040,["center","align-center","close-icon","draggable","overflow","fullscreen","header-class","body-class","footer-class","show-close","title","aria-level","onClose","onMousedown"])):te("v-if",!0)]),_:3},8,["trapped","onFocusAfterTrapped","onFocusAfterReleased","onFocusoutPrevented","onReleaseRequested"])],46,Jpe)]),_:3},8,["mask","overlay-class","z-index"]),[[Zt,p(s)]])]),_:3},16)],8,["to","disabled"]))}}),e0e=Qpe;const t0e=wt(e0e),n0e=$e({direction:{type:String,values:["horizontal","vertical"],default:"horizontal"},contentPosition:{type:String,values:["left","center","right"],default:"center"},borderStyle:{type:ae(String),default:"solid"}});var a0e=q({name:"ElDivider",__name:"divider",props:n0e,setup(e){const t=e,n=ke("divider"),a=D(()=>n.cssVar({"border-style":t.borderStyle}));return(r,o)=>(x(),O("div",{class:U([p(n).b(),p(n).m(e.direction)]),style:et(a.value),role:"separator"},[r.$slots.default&&e.direction!=="vertical"?(x(),O("div",{key:0,class:U([p(n).e("text"),p(n).is(e.contentPosition)])},[ce(r.$slots,"default")],2)):te("v-if",!0)],6))}}),r0e=a0e;const nD=wt(r0e),o0e=$e({...JP,direction:{type:String,default:"rtl",values:["ltr","rtl","ttb","btt"]},resizable:Boolean,size:{type:[String,Number],default:"30%"},withHeader:{type:Boolean,default:!0},modalFade:{type:Boolean,default:!0},headerAriaLevel:{type:String,default:"2"}}),i0e={...QP,"resize-start":(e,t)=>e instanceof MouseEvent&&typeof t=="number",resize:(e,t)=>e instanceof MouseEvent&&typeof t=="number","resize-end":(e,t)=>e instanceof MouseEvent&&typeof t=="number"};function l0e(e,t,n){const{width:a,height:r}=_6(),o=D(()=>["ltr","rtl"].includes(e.direction)),i=D(()=>["ltr","ttb"].includes(e.direction)?1:-1),l=D(()=>o.value?a.value:r.value),s=D(()=>I1(u.value+i.value*c.value,4,l.value)),u=F(0),c=F(0),d=F(!1),v=F(!1);let f=[],h=[];const m=()=>{var S;const w=(S=t.value)==null?void 0:S.closest('[aria-modal="true"]');return w?o.value?w.offsetWidth:w.offsetHeight:100};ye(()=>[e.size,e.resizable],()=>{v.value=!1,u.value=0,c.value=0,b()});const g=w=>{e.resizable&&(v.value||(u.value=m(),v.value=!0),f=[w.pageX,w.pageY],d.value=!0,n("resize-start",w,u.value),h.push(on(window,"mouseup",b),on(window,"mousemove",y)))},y=w=>{const{pageX:S,pageY:k}=w,T=S-f[0],M=k-f[1];c.value=o.value?T:M,n("resize",w,s.value)},b=w=>{d.value&&(f=[],u.value=s.value,c.value=0,d.value=!1,h.forEach(S=>S==null?void 0:S()),h=[],w&&n("resize-end",w,u.value))},_=on(t,"mousedown",g);return rn(()=>{_(),b()}),{size:D(()=>v.value?`${s.value}px`:Vn(e.size)),isResizing:d,isHorizontal:o}}const s0e=["aria-label","aria-labelledby","aria-describedby"],u0e=["id","aria-level"],c0e=["aria-label"],d0e=["id"];var f0e=q({name:"ElDrawer",inheritAttrs:!1,__name:"drawer",props:o0e,emits:i0e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Xn();Ri({scope:"el-drawer",from:"the title slot",replacement:"the header slot",version:"3.0.0",ref:"https://element-plus.org/en-US/component/drawer.html#slots"},D(()=>!!o.title));const i=F(),l=F(),s=F(),u=ke("drawer"),{t:c}=Vt(),{afterEnter:d,afterLeave:v,beforeLeave:f,visible:h,rendered:m,titleId:g,bodyId:y,zIndex:b,onModalClick:_,onOpenAutoFocus:w,onCloseAutoFocus:S,onFocusoutPrevented:k,onCloseRequested:T,handleClose:M}=tD(a,i),{isHorizontal:E,size:A,isResizing:I}=l0e(a,s,r),P=D(()=>a.modalPenetrable&&!a.modal);return t({handleClose:M}),(R,G)=>(x(),ne(sf,{to:e.appendTo,disabled:e.appendTo!=="body"?!1:!e.appendToBody},[L(Pa,{name:p(u).b("fade"),onAfterEnter:p(d),onAfterLeave:p(v),onBeforeLeave:p(f),persisted:""},{default:$(()=>[st(L(p(bw),{mask:e.modal,"overlay-class":[p(u).is("drawer"),e.modalClass??"",`${p(u).namespace.value}-modal-drawer`,p(u).is("penetrable",P.value)],"z-index":p(b),onClick:p(_)},{default:$(()=>[L(p(pf),{loop:"",trapped:p(h),"focus-trap-el":i.value,"focus-start-el":l.value,onFocusAfterTrapped:p(w),onFocusAfterReleased:p(S),onFocusoutPrevented:p(k),onReleaseRequested:p(T)},{default:$(()=>[C("div",Tt({ref_key:"drawerRef",ref:i,"aria-modal":"true","aria-label":e.title||void 0,"aria-labelledby":e.title?void 0:p(g),"aria-describedby":p(y)},R.$attrs,{class:[p(u).b(),e.direction,p(h)&&"open",p(u).is("dragging",p(I))],style:{[p(E)?"width":"height"]:p(A)},role:"dialog",onClick:G[1]||(G[1]=at(()=>{},["stop"]))}),[C("span",{ref_key:"focusStartRef",ref:l,class:U(p(u).e("sr-focus")),tabindex:"-1"},null,2),e.withHeader?(x(),O("header",{key:0,class:U([p(u).e("header"),e.headerClass])},[R.$slots.title?ce(R.$slots,"title",{key:1},()=>[te(" DEPRECATED SLOT ")]):ce(R.$slots,"header",{key:0,close:p(M),titleId:p(g),titleClass:p(u).e("title")},()=>[C("span",{id:p(g),role:"heading","aria-level":e.headerAriaLevel,class:U(p(u).e("title"))},Q(e.title),11,u0e)]),e.showClose?(x(),O("button",{key:2,"aria-label":p(c)("el.drawer.close"),class:U(p(u).e("close-btn")),type:"button",onClick:G[0]||(G[0]=(...K)=>p(M)&&p(M)(...K))},[L(p(Xe),{class:U(p(u).e("close"))},{default:$(()=>[L(p(so))]),_:1},8,["class"])],10,c0e)):te("v-if",!0)],2)):te("v-if",!0),p(m)?(x(),O("div",{key:1,id:p(y),class:U([p(u).e("body"),e.bodyClass])},[ce(R.$slots,"default")],10,d0e)):te("v-if",!0),R.$slots.footer?(x(),O("div",{key:2,class:U([p(u).e("footer"),e.footerClass])},[ce(R.$slots,"footer")],2)):te("v-if",!0),e.resizable?(x(),O("div",{key:3,ref_key:"draggerRef",ref:s,style:et({zIndex:p(b)}),class:U(p(u).e("dragger"))},null,6)):te("v-if",!0)],16,s0e)]),_:3},8,["trapped","focus-trap-el","focus-start-el","onFocusAfterTrapped","onFocusAfterReleased","onFocusoutPrevented","onReleaseRequested"])]),_:3},8,["mask","overlay-class","z-index","onClick"]),[[Zt,p(h)]])]),_:3},8,["name","onAfterEnter","onAfterLeave","onBeforeLeave"])],8,["to","disabled"]))}}),v0e=f0e;const h0e=wt(v0e),J4=$e({trigger:{...qu.trigger,type:ae([String,Array])},triggerKeys:{type:ae(Array),default:()=>[Pe.enter,Pe.numpadEnter,Pe.space,Pe.down]},virtualTriggering:qu.virtualTriggering,virtualRef:qu.virtualRef,effect:{...Tn.effect,default:"light"},type:{type:ae(String)},placement:{type:ae(String),default:"bottom"},popperOptions:{type:ae(Object),default:()=>({})},id:String,size:{type:String,default:""},splitButton:Boolean,hideOnClick:{type:Boolean,default:!0},loop:{type:Boolean,default:!0},showArrow:{type:Boolean,default:!0},showTimeout:{type:Number,default:150},hideTimeout:{type:Number,default:150},tabindex:{type:ae([Number,String]),default:0},maxHeight:{type:ae([Number,String]),default:""},popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,disabled:Boolean,role:{type:String,values:jI,default:"menu"},buttonProps:{type:ae(Object)},teleported:Tn.teleported,appendTo:Tn.appendTo,persistent:{type:Boolean,default:!0}}),aD=$e({command:{type:[Object,String,Number],default:()=>({})},disabled:Boolean,divided:Boolean,textValue:String,icon:{type:pn}}),p0e=$e({onKeydown:{type:ae(Function)}}),Y1=Symbol("elDropdown"),rD="elDropdown";var m0e=q({inheritAttrs:!1});function g0e(e,t,n,a,r,o){return ce(e.$slots,"default")}var y0e=ya(m0e,[["render",g0e]]),b0e=q({name:"ElCollectionItem",inheritAttrs:!1});function _0e(e,t,n,a,r,o){return ce(e.$slots,"default")}var w0e=ya(b0e,[["render",_0e]]);const oD="data-el-collection-item",S0e=e=>{const t=`El${e}Collection`,n=`${t}Item`,a=Symbol(t),r=Symbol(n);return{COLLECTION_INJECTION_KEY:a,COLLECTION_ITEM_INJECTION_KEY:r,ElCollection:Object.assign({},y0e,{name:t,setup(){const o=F(),i=new Map;Ft(a,{itemMap:i,getItems:()=>{const s=p(o);if(!s)return[];const u=Array.from(s.querySelectorAll(`[${oD}]`));return[...i.values()].sort((c,d)=>u.indexOf(c.ref)-u.indexOf(d.ref))},collectionRef:o})}}),ElCollectionItem:Object.assign({},w0e,{name:n,setup(o,{attrs:i}){const l=F(),s=Ze(a,void 0);Ft(r,{collectionItemRef:l}),mt(()=>{const u=p(l);u&&s.itemMap.set(u,{ref:u,...i})}),rn(()=>{const u=p(l);s.itemMap.delete(u)})}})}},C0e=$e({style:{type:ae([String,Array,Object,Boolean]),default:void 0},currentTabId:{type:ae(String)},defaultCurrentTabId:String,loop:Boolean,dir:{type:String,values:["ltr","rtl"],default:"ltr"},orientation:{type:ae(String)},onBlur:Function,onFocus:Function,onMousedown:Function}),{ElCollection:x0e,ElCollectionItem:k0e,COLLECTION_INJECTION_KEY:iD,COLLECTION_ITEM_INJECTION_KEY:T0e}=S0e("RovingFocusGroup"),ww=Symbol("elRovingFocusGroup"),lD=Symbol("elRovingFocusGroupItem"),M0e={ArrowLeft:"prev",ArrowUp:"prev",ArrowRight:"next",ArrowDown:"next",PageUp:"first",Home:"first",PageDown:"last",End:"last"},E0e=(e,t)=>e,A0e=(e,t,n)=>{const a=E0e(mn(e));return M0e[a]},I0e=(e,t)=>e.map((n,a)=>e[(a+t)%e.length]),kx=e=>{const{activeElement:t}=document;for(const n of e)if(n===t||(n.focus(),t!==document.activeElement))return},Tx="currentTabIdChange",Mx="rovingFocusGroup.entryFocus",L0e={bubbles:!1,cancelable:!0};var P0e=q({name:"ElRovingFocusGroupImpl",inheritAttrs:!1,props:C0e,emits:[Tx,"entryFocus"],setup(e,{emit:t}){const n=F((e.currentTabId||e.defaultCurrentTabId)??null),a=F(!1),r=F(!1),o=F(),{getItems:i}=Ze(iD,void 0),l=D(()=>[{outline:"none"},e.style]),s=m=>{t(Tx,m)},u=()=>{a.value=!0},c=_a(m=>{var g;(g=e.onMousedown)==null||g.call(e,m)},()=>{r.value=!0}),d=_a(m=>{var g;(g=e.onFocus)==null||g.call(e,m)},m=>{const g=!p(r),{target:y,currentTarget:b}=m;if(y===b&&g&&!p(a)){const _=new Event(Mx,L0e);if(b==null||b.dispatchEvent(_),!_.defaultPrevented){const w=i().filter(S=>S.focusable);kx([w.find(S=>S.active),w.find(S=>S.id===p(n)),...w].filter(Boolean).map(S=>S.ref))}}r.value=!1}),v=_a(m=>{var g;(g=e.onBlur)==null||g.call(e,m)},()=>{a.value=!1}),f=(...m)=>{t("entryFocus",...m)},h=m=>{const g=A0e(m);if(g){m.preventDefault();let y=i().filter(b=>b.focusable).map(b=>b.ref);switch(g){case"last":y.reverse();break;case"prev":case"next":{g==="prev"&&y.reverse();const b=y.indexOf(m.currentTarget);y=e.loop?I0e(y,b+1):y.slice(b+1);break}}He(()=>{kx(y)})}};Ft(ww,{currentTabbedId:Ld(n),loop:cn(e,"loop"),tabIndex:D(()=>p(a)?-1:0),rovingFocusGroupRef:o,rovingFocusGroupRootStyle:l,orientation:cn(e,"orientation"),dir:cn(e,"dir"),onItemFocus:s,onItemShiftTab:u,onBlur:v,onFocus:d,onMousedown:c,onKeydown:h}),ye(()=>e.currentTabId,m=>{n.value=m??null}),on(o,Mx,f)}});function D0e(e,t,n,a,r,o){return ce(e.$slots,"default")}var O0e=ya(P0e,[["render",D0e]]),$0e=q({name:"ElRovingFocusGroup",components:{ElFocusGroupCollection:x0e,ElRovingFocusGroupImpl:O0e}});function R0e(e,t,n,a,r,o){const i=be("el-roving-focus-group-impl"),l=be("el-focus-group-collection");return x(),ne(l,null,{default:$(()=>[L(i,bl(wl(e.$attrs)),{default:$(()=>[ce(e.$slots,"default")]),_:3},16)]),_:3})}var N0e=ya($0e,[["render",R0e]]),B0e=q({components:{ElRovingFocusCollectionItem:k0e},props:{focusable:{type:Boolean,default:!0},active:Boolean},emits:["mousedown","focus","keydown"],setup(e,{emit:t}){const{currentTabbedId:n,onItemFocus:a,onItemShiftTab:r,onKeydown:o}=Ze(ww,void 0),i=Fa(),l=F(),s=_a(v=>{t("mousedown",v)},v=>{e.focusable?a(p(i)):v.preventDefault()}),u=_a(v=>{t("focus",v)},()=>{a(p(i))}),c=_a(v=>{t("keydown",v)},v=>{const{shiftKey:f,target:h,currentTarget:m}=v;if(mn(v)===Pe.tab&&f){r();return}h===m&&o(v)}),d=D(()=>n.value===p(i));return Ft(lD,{rovingFocusGroupItemRef:l,tabIndex:D(()=>p(d)?0:-1),handleMousedown:s,handleFocus:u,handleKeydown:c}),{id:i,handleKeydown:c,handleFocus:u,handleMousedown:s}}});function V0e(e,t,n,a,r,o){const i=be("el-roving-focus-collection-item");return x(),ne(i,{id:e.id,focusable:e.focusable,active:e.active},{default:$(()=>[ce(e.$slots,"default")]),_:3},8,["id","focusable","active"])}var z0e=ya(B0e,[["render",V0e]]),F0e=N0e;const{ButtonGroup:H0e}=xa;var W0e=q({name:"ElDropdown",components:{ElButton:xa,ElButtonGroup:H0e,ElScrollbar:ni,ElTooltip:Ma,ElRovingFocusGroup:F0e,ElOnlyChild:JI,ElIcon:Xe,ArrowDown:ui},props:J4,emits:["visible-change","click","command"],setup(e,{emit:t}){const n=Et(),a=ke("dropdown"),{t:r}=Vt(),o=F(),i=F(),l=F(),s=F(),u=F(null),c=F(null),d=F(!1),v=D(()=>({maxHeight:Vn(e.maxHeight)})),f=D(()=>[a.m(w.value)]),h=D(()=>wa(e.trigger)),m=Fa().value,g=D(()=>e.id||m);function y(){var R;(R=l.value)==null||R.onClose(void 0,0)}function b(){var R;(R=l.value)==null||R.onClose()}function _(){var R;(R=l.value)==null||R.onOpen()}const w=ca();function S(...R){t("command",...R)}function k(){}function T(){const R=p(s);h.value.includes("hover")&&(R==null||R.focus({preventScroll:!0})),c.value=null}function M(R){c.value=R}function E(){t("visible-change",!0)}function A(R){var G;d.value=(R==null?void 0:R.type)==="keydown",(G=s.value)==null||G.focus()}function I(){t("visible-change",!1)}return Ft(Y1,{contentRef:s,role:D(()=>e.role),triggerId:g,isUsingKeyboard:d,onItemEnter:k,onItemLeave:T,handleClose:b}),Ft(rD,{instance:n,dropdownSize:w,handleClick:y,commandHandler:S,trigger:cn(e,"trigger"),hideOnClick:cn(e,"hideOnClick")}),{t:r,ns:a,scrollbar:u,wrapStyle:v,dropdownTriggerKls:f,dropdownSize:w,triggerId:g,currentTabId:c,handleCurrentTabIdChange:M,handlerMainButtonClick:R=>{t("click",R)},handleClose:b,handleOpen:_,handleBeforeShowTooltip:E,handleShowTooltip:A,handleBeforeHideTooltip:I,popperRef:l,contentRef:s,triggeringElementRef:o,referenceElementRef:i}}});function K0e(e,t,n,a,r,o){const i=be("el-roving-focus-group"),l=be("el-scrollbar"),s=be("el-only-child"),u=be("el-tooltip"),c=be("el-button"),d=be("arrow-down"),v=be("el-icon"),f=be("el-button-group");return x(),O("div",{class:U([e.ns.b(),e.ns.is("disabled",e.disabled)])},[L(u,{ref:"popperRef",role:e.role,effect:e.effect,"fallback-placements":["bottom","top"],"popper-options":e.popperOptions,"gpu-acceleration":!1,placement:e.placement,"popper-class":[e.ns.e("popper"),e.popperClass],"popper-style":e.popperStyle,trigger:e.trigger,"trigger-keys":e.triggerKeys,"trigger-target-el":e.contentRef,"show-arrow":e.showArrow,"show-after":e.trigger==="hover"?e.showTimeout:0,"hide-after":e.trigger==="hover"?e.hideTimeout:0,"virtual-ref":e.virtualRef??e.triggeringElementRef,"virtual-triggering":e.virtualTriggering||e.splitButton,disabled:e.disabled,transition:`${e.ns.namespace.value}-zoom-in-top`,teleported:e.teleported,"append-to":e.appendTo,pure:"","focus-on-target":"",persistent:e.persistent,onBeforeShow:e.handleBeforeShowTooltip,onShow:e.handleShowTooltip,onBeforeHide:e.handleBeforeHideTooltip},xr({content:$(()=>[L(l,{ref:"scrollbar","wrap-style":e.wrapStyle,tag:"div","view-class":e.ns.e("list")},{default:$(()=>[L(i,{loop:e.loop,"current-tab-id":e.currentTabId,orientation:"horizontal",onCurrentTabIdChange:e.handleCurrentTabIdChange},{default:$(()=>[ce(e.$slots,"dropdown")]),_:3},8,["loop","current-tab-id","onCurrentTabIdChange"])]),_:3},8,["wrap-style","view-class"])]),_:2},[e.splitButton?void 0:{name:"default",fn:$(()=>[L(s,{id:e.triggerId,ref:"triggeringElementRef",role:"button",tabindex:e.tabindex},{default:$(()=>[ce(e.$slots,"default")]),_:3},8,["id","tabindex"])]),key:"0"}]),1032,["role","effect","popper-options","placement","popper-class","popper-style","trigger","trigger-keys","trigger-target-el","show-arrow","show-after","hide-after","virtual-ref","virtual-triggering","disabled","transition","teleported","append-to","persistent","onBeforeShow","onShow","onBeforeHide"]),e.splitButton?(x(),ne(f,{key:0},{default:$(()=>[L(c,Tt({ref:"referenceElementRef"},e.buttonProps,{size:e.dropdownSize,type:e.type,disabled:e.disabled,tabindex:e.tabindex,onClick:e.handlerMainButtonClick}),{default:$(()=>[ce(e.$slots,"default")]),_:3},16,["size","type","disabled","tabindex","onClick"]),L(c,Tt({id:e.triggerId,ref:"triggeringElementRef"},e.buttonProps,{role:"button",size:e.dropdownSize,type:e.type,class:e.ns.e("caret-button"),disabled:e.disabled,tabindex:e.tabindex,"aria-label":e.t("el.dropdown.toggleDropdown")}),{default:$(()=>[L(v,{class:U(e.ns.e("icon"))},{default:$(()=>[L(d)]),_:1},8,["class"])]),_:1},16,["id","size","type","class","disabled","tabindex","aria-label"])]),_:3})):te("v-if",!0)],2)}var U0e=ya(W0e,[["render",K0e]]),G0e=q({name:"DropdownItemImpl",components:{ElIcon:Xe},props:aD,emits:["pointermove","pointerleave","click","clickimpl"],setup(e,{emit:t}){const n=ke("dropdown"),{role:a}=Ze(Y1,void 0),{collectionItemRef:r}=Ze(T0e,void 0),{rovingFocusGroupItemRef:o,tabIndex:i,handleFocus:l,handleKeydown:s,handleMousedown:u}=Ze(lD,void 0),c=_w(r,o),d=D(()=>a.value==="menu"?"menuitem":a.value==="navigation"?"link":"button"),v=_a(f=>{const h=mn(f);if([Pe.enter,Pe.numpadEnter,Pe.space].includes(h))return f.preventDefault(),f.stopImmediatePropagation(),t("clickimpl",f),!0},s);return{ns:n,itemRef:c,dataset:{[oD]:""},role:d,tabIndex:i,handleFocus:l,handleKeydown:v,handleMousedown:u}}});const Y0e=["aria-disabled","tabindex","role"];function j0e(e,t,n,a,r,o){const i=be("el-icon");return x(),O(Ve,null,[e.divided?(x(),O("li",{key:0,role:"separator",class:U(e.ns.bem("menu","item","divided"))},null,2)):te("v-if",!0),C("li",Tt({ref:e.itemRef},{...e.dataset,...e.$attrs},{"aria-disabled":e.disabled,class:[e.ns.be("menu","item"),e.ns.is("disabled",e.disabled)],tabindex:e.tabIndex,role:e.role,onClick:t[0]||(t[0]=l=>e.$emit("clickimpl",l)),onFocus:t[1]||(t[1]=(...l)=>e.handleFocus&&e.handleFocus(...l)),onKeydown:t[2]||(t[2]=at((...l)=>e.handleKeydown&&e.handleKeydown(...l),["self"])),onMousedown:t[3]||(t[3]=(...l)=>e.handleMousedown&&e.handleMousedown(...l)),onPointermove:t[4]||(t[4]=l=>e.$emit("pointermove",l)),onPointerleave:t[5]||(t[5]=l=>e.$emit("pointerleave",l))}),[e.icon||e.$slots.icon?(x(),ne(i,{key:0},{default:$(()=>[ce(e.$slots,"icon",{},()=>[(x(),ne(St(e.icon)))])]),_:3})):te("v-if",!0),ce(e.$slots,"default")],16,Y0e)],64)}var q0e=ya(G0e,[["render",j0e]]);const sD=()=>{const e=Ze(rD,{});return{elDropdown:e,_elDropdownSize:D(()=>e==null?void 0:e.dropdownSize)}};var X0e=q({name:"ElDropdownItem",components:{ElRovingFocusItem:z0e,ElDropdownItemImpl:q0e},inheritAttrs:!1,props:aD,emits:["pointermove","pointerleave","click"],setup(e,{emit:t,attrs:n}){const{elDropdown:a}=sD(),r=Et(),{onItemEnter:o,onItemLeave:i}=Ze(Y1,void 0),l=_a(u=>(t("pointermove",u),u.defaultPrevented),YC(u=>{if(e.disabled){i(u);return}const c=u.currentTarget;c===document.activeElement||c.contains(document.activeElement)||(o(u),u.defaultPrevented||c==null||c.focus({preventScroll:!0}))})),s=_a(u=>(t("pointerleave",u),u.defaultPrevented),YC(i));return{handleClick:_a(u=>{if(!e.disabled)return t("click",u),u.type!=="keydown"&&u.defaultPrevented},u=>{var c,d,v;if(e.disabled){u.stopImmediatePropagation();return}(c=a==null?void 0:a.hideOnClick)!=null&&c.value&&((d=a.handleClick)==null||d.call(a)),(v=a.commandHandler)==null||v.call(a,e.command,r,u)}),handlePointerMove:l,handlePointerLeave:s,propsAndAttrs:D(()=>({...e,...n}))}}});function Z0e(e,t,n,a,r,o){const i=be("el-dropdown-item-impl"),l=be("el-roving-focus-item");return x(),ne(l,{focusable:!e.disabled},{default:$(()=>[L(i,Tt(e.propsAndAttrs,{onPointerleave:e.handlePointerLeave,onPointermove:e.handlePointerMove,onClickimpl:e.handleClick}),xr({default:$(()=>[ce(e.$slots,"default")]),_:2},[e.$slots.icon?{name:"icon",fn:$(()=>[ce(e.$slots,"icon")]),key:"0"}:void 0]),1040,["onPointerleave","onPointermove","onClickimpl"])]),_:3},8,["focusable"])}var uD=ya(X0e,[["render",Z0e]]),J0e=q({name:"ElDropdownMenu",props:p0e,setup(e){const t=ke("dropdown"),{_elDropdownSize:n}=sD(),a=n.value,{contentRef:r,role:o,triggerId:i,isUsingKeyboard:l,handleClose:s}=Ze(Y1,void 0),{rovingFocusGroupRef:u,rovingFocusGroupRootStyle:c,onBlur:d,onFocus:v,onKeydown:f,onMousedown:h}=Ze(ww,void 0),{collectionRef:m}=Ze(iD,void 0),g=D(()=>[t.b("menu"),t.bm("menu",a==null?void 0:a.value)]),y=_w(r,u,m),b=_a(w=>{var S;(S=e.onKeydown)==null||S.call(e,w)},w=>{const{currentTarget:S,target:k}=w,T=mn(w);if(S.contains(k),Pe.tab===T)return s();f(w)});function _(w){l.value&&v(w)}return{size:a,rovingFocusGroupRootStyle:c,dropdownKls:g,role:o,triggerId:i,dropdownListWrapperRef:y,handleKeydown:b,onBlur:d,handleFocus:_,onMousedown:h}}});const Q0e=["role","aria-labelledby"];function eme(e,t,n,a,r,o){return x(),O("ul",{ref:e.dropdownListWrapperRef,class:U(e.dropdownKls),style:et(e.rovingFocusGroupRootStyle),tabindex:-1,role:e.role,"aria-labelledby":e.triggerId,onFocusin:t[0]||(t[0]=(...i)=>e.handleFocus&&e.handleFocus(...i)),onFocusout:t[1]||(t[1]=(...i)=>e.onBlur&&e.onBlur(...i)),onKeydown:t[2]||(t[2]=at((...i)=>e.handleKeydown&&e.handleKeydown(...i),["self"])),onMousedown:t[3]||(t[3]=at((...i)=>e.onMousedown&&e.onMousedown(...i),["self"]))},[ce(e.$slots,"default")],46,Q0e)}var cD=ya(J0e,[["render",eme]]);const tme=wt(U0e,{DropdownItem:uD,DropdownMenu:cD}),nme=On(uD),ame=On(cD),rme=$e({image:{type:String,default:""},imageSize:Number,description:{type:String,default:""}}),ome={viewBox:"0 0 79 86",version:"1.1",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink"},ime=["id"],lme=["stop-color"],sme=["stop-color"],ume=["id"],cme=["stop-color"],dme=["stop-color"],fme=["id"],vme={stroke:"none","stroke-width":"1",fill:"none","fill-rule":"evenodd"},hme={transform:"translate(-1268.000000, -535.000000)"},pme={transform:"translate(1268.000000, 535.000000)"},mme=["fill"],gme=["fill"],yme={transform:"translate(34.500000, 31.500000) scale(-1, 1) rotate(-25.000000) translate(-34.500000, -31.500000) translate(7.000000, 10.000000)"},bme=["fill"],_me=["fill"],wme=["fill"],Sme=["fill"],Cme=["fill"],xme={transform:"translate(53.000000, 45.000000)"},kme=["fill","xlink:href"],Tme=["fill","mask"],Mme=["fill"];var Eme=q({name:"ImgEmpty",__name:"img-empty",setup(e){const t=ke("empty"),n=Fa();return(a,r)=>(x(),O("svg",ome,[C("defs",null,[C("linearGradient",{id:`linearGradient-1-${p(n)}`,x1:"38.8503086%",y1:"0%",x2:"61.1496914%",y2:"100%"},[C("stop",{"stop-color":`var(${p(t).cssVarBlockName("fill-color-1")})`,offset:"0%"},null,8,lme),C("stop",{"stop-color":`var(${p(t).cssVarBlockName("fill-color-4")})`,offset:"100%"},null,8,sme)],8,ime),C("linearGradient",{id:`linearGradient-2-${p(n)}`,x1:"0%",y1:"9.5%",x2:"100%",y2:"90.5%"},[C("stop",{"stop-color":`var(${p(t).cssVarBlockName("fill-color-1")})`,offset:"0%"},null,8,cme),C("stop",{"stop-color":`var(${p(t).cssVarBlockName("fill-color-6")})`,offset:"100%"},null,8,dme)],8,ume),C("rect",{id:`path-3-${p(n)}`,x:"0",y:"0",width:"17",height:"36"},null,8,fme)]),C("g",vme,[C("g",hme,[C("g",pme,[C("path",{d:"M39.5,86 C61.3152476,86 79,83.9106622 79,81.3333333 C79,78.7560045 57.3152476,78 35.5,78 C13.6847524,78 0,78.7560045 0,81.3333333 C0,83.9106622 17.6847524,86 39.5,86 Z",fill:`var(${p(t).cssVarBlockName("fill-color-3")})`},null,8,mme),C("polygon",{fill:`var(${p(t).cssVarBlockName("fill-color-7")})`,transform:"translate(27.500000, 51.500000) scale(1, -1) translate(-27.500000, -51.500000) ",points:"13 58 53 58 42 45 2 45"},null,8,gme),C("g",yme,[C("polygon",{fill:`var(${p(t).cssVarBlockName("fill-color-7")})`,transform:"translate(11.500000, 5.000000) scale(1, -1) translate(-11.500000, -5.000000) ",points:"2.84078316e-14 3 18 3 23 7 5 7"},null,8,bme),C("polygon",{fill:`var(${p(t).cssVarBlockName("fill-color-5")})`,points:"-3.69149156e-15 7 38 7 38 43 -3.69149156e-15 43"},null,8,_me),C("rect",{fill:`url(#linearGradient-1-${p(n)})`,transform:"translate(46.500000, 25.000000) scale(-1, 1) translate(-46.500000, -25.000000) ",x:"38",y:"7",width:"17",height:"36"},null,8,wme),C("polygon",{fill:`var(${p(t).cssVarBlockName("fill-color-2")})`,transform:"translate(39.500000, 3.500000) scale(-1, 1) translate(-39.500000, -3.500000) ",points:"24 7 41 7 55 -3.63806207e-12 38 -3.63806207e-12"},null,8,Sme)]),C("rect",{fill:`url(#linearGradient-2-${p(n)})`,x:"13",y:"45",width:"40",height:"36"},null,8,Cme),C("g",xme,[C("use",{fill:`var(${p(t).cssVarBlockName("fill-color-8")})`,transform:"translate(8.500000, 18.000000) scale(-1, 1) translate(-8.500000, -18.000000) ","xlink:href":`#path-3-${p(n)}`},null,8,kme),C("polygon",{fill:`var(${p(t).cssVarBlockName("fill-color-9")})`,mask:`url(#mask-4-${p(n)})`,transform:"translate(12.000000, 9.000000) scale(-1, 1) translate(-12.000000, -9.000000) ",points:"7 0 24 0 20 18 7 16.5"},null,8,Tme)]),C("polygon",{fill:`var(${p(t).cssVarBlockName("fill-color-2")})`,transform:"translate(66.000000, 51.500000) scale(-1, 1) translate(-66.000000, -51.500000) ",points:"62 45 79 45 70 58 53 58"},null,8,Mme)])])])]))}}),Ame=Eme;const Ime=["src"],Lme={key:1};var Pme=q({name:"ElEmpty",__name:"empty",props:rme,setup(e){const t=e,{t:n}=Vt(),a=ke("empty"),r=D(()=>t.description||n("el.table.emptyText")),o=D(()=>({width:Vn(t.imageSize)}));return(i,l)=>(x(),O("div",{class:U(p(a).b())},[C("div",{class:U(p(a).e("image")),style:et(o.value)},[e.image?(x(),O("img",{key:0,src:e.image,ondragstart:"return false"},null,8,Ime)):ce(i.$slots,"image",{key:1},()=>[L(Ame)])],6),C("div",{class:U(p(a).e("description"))},[i.$slots.description?ce(i.$slots,"description",{key:0}):(x(),O("p",Lme,Q(r.value),1))],2),i.$slots.default?(x(),O("div",{key:0,class:U(p(a).e("bottom"))},[ce(i.$slots,"default")],2)):te("v-if",!0)],2))}}),Dme=Pme;const dD=wt(Dme),Ome=$e({hideOnClickModal:Boolean,src:{type:String,default:""},fit:{type:String,values:["","contain","cover","fill","none","scale-down"],default:""},loading:{type:String,values:["eager","lazy"]},lazy:Boolean,scrollContainer:{type:ae([String,Object])},previewSrcList:{type:ae(Array),default:()=>Nn([])},previewTeleported:Boolean,zIndex:{type:Number},initialIndex:{type:Number,default:0},infinite:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},zoomRate:{type:Number,default:1.2},scale:{type:Number,default:1},minScale:{type:Number,default:.2},maxScale:{type:Number,default:7},showProgress:Boolean,crossorigin:{type:ae(String)}}),$me={load:e=>e instanceof Event,error:e=>e instanceof Event,switch:e=>Je(e),close:()=>!0,show:()=>!0},Rme=$e({urlList:{type:ae(Array),default:()=>Nn([])},zIndex:{type:Number},initialIndex:{type:Number,default:0},infinite:{type:Boolean,default:!0},hideOnClickModal:Boolean,teleported:Boolean,closeOnPressEscape:{type:Boolean,default:!0},zoomRate:{type:Number,default:1.2},scale:{type:Number,default:1},minScale:{type:Number,default:.2},maxScale:{type:Number,default:7},showProgress:Boolean,crossorigin:{type:ae(String)}}),Nme={close:()=>!0,error:e=>e instanceof Event,switch:e=>Je(e),rotate:e=>Je(e)},Bme=["src","crossorigin"];var Vme=q({name:"ElImageViewer",__name:"image-viewer",props:Rme,emits:Nme,setup(e,{expose:t,emit:n}){const a={CONTAIN:{name:"contain",icon:$i(II)},ORIGINAL:{name:"original",icon:$i(FI)}},r=e,o=n;let i;const{t:l}=Vt(),s=ke("image-viewer"),{nextZIndex:u}=tp(),c=F(),d=F(),v=SV(),f=D(()=>{const{scale:he,minScale:J,maxScale:Z}=r;return I1(he,J,Z)}),h=F(!0),m=F(!1),g=F(!1),y=F(r.initialIndex),b=Ht(a.CONTAIN),_=F({scale:f.value,deg:0,offsetX:0,offsetY:0,enableTransition:!1}),w=F(r.zIndex??u());D1(g,{ns:s});const S=D(()=>{const{urlList:he}=r;return he.length<=1}),k=D(()=>y.value===0),T=D(()=>y.value===r.urlList.length-1),M=D(()=>r.urlList[y.value]),E=D(()=>[s.e("btn"),s.e("prev"),s.is("disabled",!r.infinite&&k.value)]),A=D(()=>[s.e("btn"),s.e("next"),s.is("disabled",!r.infinite&&T.value)]),I=D(()=>{const{scale:he,deg:J,offsetX:Z,offsetY:j,enableTransition:oe}=_.value;let se=Z/he,fe=j/he;const ge=J*Math.PI/180,Se=Math.cos(ge),De=Math.sin(ge);se=se*Se+fe*De,fe=fe*Se-Z/he*De;const Te={transform:`scale(${he}) rotate(${J}deg) translate(${se}px, ${fe}px)`,transition:oe?"transform .3s":""};return b.value.name===a.CONTAIN.name&&(Te.maxWidth=Te.maxHeight="100%"),Te}),P=D(()=>`${y.value+1} / ${r.urlList.length}`);function R(){K(),i==null||i(),g.value=!1,o("close")}function G(){const he=hs(Z=>{switch(mn(Z)){case Pe.esc:r.closeOnPressEscape&&R();break;case Pe.space:V();break;case Pe.left:X();break;case Pe.up:re("zoomIn");break;case Pe.right:ee();break;case Pe.down:re("zoomOut");break}}),J=hs(Z=>{re((Z.deltaY||Z.deltaX)<0?"zoomIn":"zoomOut",{zoomRate:r.zoomRate,enableTransition:!1})});v.run(()=>{on(document,"keydown",he),on(c,"wheel",J)})}function K(){v.stop()}function H(){h.value=!1}function z(he){m.value=!0,h.value=!1,o("error",he),he.target.alt=l("el.image.error")}function B(he){if(h.value||he.button!==0||!c.value)return;_.value.enableTransition=!1;const{offsetX:J,offsetY:Z}=_.value,j=he.pageX,oe=he.pageY,se=hs(Se=>{_.value={..._.value,offsetX:J+Se.pageX-j,offsetY:Z+Se.pageY-oe}}),fe=on(document,"mousemove",se),ge=on(document,"mouseup",()=>{fe(),ge()});he.preventDefault()}function W(he){if(h.value||!c.value||he.touches.length!==1)return;_.value.enableTransition=!1;const{offsetX:J,offsetY:Z}=_.value,{pageX:j,pageY:oe}=he.touches[0],se=hs(Se=>{const De=Se.touches[0];_.value={..._.value,offsetX:J+De.pageX-j,offsetY:Z+De.pageY-oe}}),fe=on(document,"touchmove",se),ge=on(document,"touchend",()=>{fe(),ge()});he.preventDefault()}function N(){_.value={scale:f.value,deg:0,offsetX:0,offsetY:0,enableTransition:!1}}function V(){if(h.value||m.value)return;const he=sh(a),J=Object.values(a),Z=b.value.name;b.value=a[he[(J.findIndex(j=>j.name===Z)+1)%he.length]],N()}function Y(he){m.value=!1;const J=r.urlList.length;y.value=(he+J)%J}function X(){k.value&&!r.infinite||Y(y.value-1)}function ee(){T.value&&!r.infinite||Y(y.value+1)}function re(he,J={}){if(h.value||m.value)return;const{minScale:Z,maxScale:j}=r,{zoomRate:oe,rotateDeg:se,enableTransition:fe}={zoomRate:r.zoomRate,rotateDeg:90,enableTransition:!0,...J};switch(he){case"zoomOut":_.value.scale>Z&&(_.value.scale=Number.parseFloat((_.value.scale/oe).toFixed(3)));break;case"zoomIn":_.value.scale0)return he.preventDefault(),!1}}return ye(()=>f.value,he=>{_.value.scale=he}),ye(M,()=>{He(()=>{var he;(he=d.value)!=null&&he.complete||(h.value=!0)})}),ye(y,he=>{N(),o("switch",he)}),mt(()=>{g.value=!0,G(),i=on("wheel",ve,{passive:!1})}),t({setActiveItem:Y}),(he,J)=>(x(),ne(sf,{to:"body",disabled:!e.teleported},[L(Pa,{name:"viewer-fade",appear:""},{default:$(()=>[C("div",{ref_key:"wrapper",ref:c,tabindex:-1,class:U(p(s).e("wrapper")),style:et({zIndex:w.value})},[L(p(pf),{loop:"",trapped:"","focus-trap-el":c.value,"focus-start-el":"container",onFocusoutPrevented:ie,onReleaseRequested:le},{default:$(()=>[C("div",{class:U(p(s).e("mask")),onClick:J[0]||(J[0]=at(Z=>e.hideOnClickModal&&R(),["self"]))},null,2),te(" CLOSE "),C("span",{class:U([p(s).e("btn"),p(s).e("close")]),onClick:R},[L(p(Xe),null,{default:$(()=>[L(p(so))]),_:1})],2),te(" ARROW "),S.value?te("v-if",!0):(x(),O(Ve,{key:0},[C("span",{class:U(E.value),onClick:X},[L(p(Xe),null,{default:$(()=>[L(p(Ji))]),_:1})],2),C("span",{class:U(A.value),onClick:ee},[L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})],2)],64)),he.$slots.progress||e.showProgress?(x(),O("div",{key:1,class:U([p(s).e("btn"),p(s).e("progress")])},[ce(he.$slots,"progress",{activeIndex:y.value,total:e.urlList.length},()=>[ue(Q(P.value),1)])],2)):te("v-if",!0),te(" ACTIONS "),C("div",{class:U([p(s).e("btn"),p(s).e("actions")])},[C("div",{class:U(p(s).e("actions__inner"))},[ce(he.$slots,"toolbar",{actions:re,prev:X,next:ee,reset:V,activeIndex:y.value,setActiveItem:Y},()=>[L(p(Xe),{onClick:J[1]||(J[1]=Z=>re("zoomOut"))},{default:$(()=>[L(p(GI))]),_:1}),L(p(Xe),{onClick:J[2]||(J[2]=Z=>re("zoomIn"))},{default:$(()=>[L(p(G6))]),_:1}),C("i",{class:U(p(s).e("actions__divider"))},null,2),L(p(Xe),{onClick:V},{default:$(()=>[(x(),ne(St(b.value.icon)))]),_:1}),C("i",{class:U(p(s).e("actions__divider"))},null,2),L(p(Xe),{onClick:J[3]||(J[3]=Z=>re("anticlockwise"))},{default:$(()=>[L(p(VI))]),_:1}),L(p(Xe),{onClick:J[4]||(J[4]=Z=>re("clockwise"))},{default:$(()=>[L(p(zI))]),_:1})])],2)],2),te(" CANVAS "),C("div",{class:U(p(s).e("canvas"))},[m.value&&he.$slots["viewer-error"]?ce(he.$slots,"viewer-error",{key:0,activeIndex:y.value,src:M.value}):(x(),O("img",{ref_key:"imgRef",ref:d,key:M.value,src:M.value,style:et(I.value),class:U(p(s).e("img")),crossorigin:e.crossorigin,onLoad:H,onError:z,onMousedown:B,onTouchstart:W},null,46,Bme))],2),ce(he.$slots,"default")]),_:3},8,["focus-trap-el"])],6)]),_:3})],8,["disabled"]))}}),zme=Vme;const fD=wt(zme),Fme=["src","loading","crossorigin"],Hme={key:0};var Wme=q({name:"ElImage",inheritAttrs:!1,__name:"image",props:Ome,emits:$me,setup(e,{expose:t,emit:n}){const a=e,r=n,{t:o}=Vt(),i=ke("image"),l=Nl(),s=D(()=>Rd(Object.entries(l).filter(([z])=>/^(data-|on[A-Z])/i.test(z)||["id","style"].includes(z)))),u=A1({excludeListeners:!0,excludeKeys:D(()=>Object.keys(s.value))}),c=F(),d=F(!1),v=F(!0),f=F(!1),h=F(),m=F(),g=qt&&"loading"in HTMLImageElement.prototype;let y;const b=D(()=>[i.e("inner"),w.value&&i.e("preview"),v.value&&i.is("loading")]),_=D(()=>{const{fit:z}=a;return qt&&z?{objectFit:z}:{}}),w=D(()=>{const{previewSrcList:z}=a;return Ae(z)&&z.length>0}),S=D(()=>{const{previewSrcList:z,initialIndex:B}=a;let W=B;return B>z.length-1&&(W=0),W}),k=D(()=>a.loading==="eager"?!1:!g&&a.loading==="lazy"||a.lazy),T=()=>{qt&&(v.value=!0,d.value=!1,c.value=a.src)};function M(z){v.value=!1,d.value=!1,r("load",z)}function E(z){v.value=!1,d.value=!0,r("error",z)}function A(z){z&&(T(),R())}const I=LA(A,200,!0);async function P(){if(!qt)return;await He();const{scrollContainer:z}=a;if(Vr(z))m.value=z;else if(Ye(z)&&z!=="")m.value=document.querySelector(z)??void 0;else if(h.value){const W=x6(h.value);m.value=qh(W)?void 0:W}const{stop:B}=PY(h,([W])=>{I(W.isIntersecting)},{root:m});y=B}function R(){!qt||!I||(y==null||y(),m.value=void 0,y=void 0)}function G(){w.value&&(f.value=!0,r("show"))}function K(){f.value=!1,r("close")}function H(z){r("switch",z)}return ye(()=>a.src,()=>{k.value?(v.value=!0,d.value=!1,R(),P()):T()}),mt(()=>{k.value?P():T()}),t({showPreview:G}),(z,B)=>(x(),O("div",Tt({ref_key:"container",ref:h},s.value,{class:[p(i).b(),z.$attrs.class]}),[d.value?ce(z.$slots,"error",{key:0},()=>[C("div",{class:U(p(i).e("error"))},Q(p(o)("el.image.error")),3)]):(x(),O(Ve,{key:1},[c.value!==void 0?(x(),O("img",Tt({key:0},p(u),{src:c.value,loading:e.loading,style:_.value,class:b.value,crossorigin:e.crossorigin,onClick:G,onLoad:M,onError:E}),null,16,Fme)):te("v-if",!0),v.value?(x(),O("div",{key:1,class:U(p(i).e("wrapper"))},[ce(z.$slots,"placeholder",{},()=>[C("div",{class:U(p(i).e("placeholder"))},null,2)])],2)):te("v-if",!0)],64)),w.value?(x(),O(Ve,{key:2},[f.value?(x(),ne(p(fD),{key:0,"z-index":e.zIndex,"initial-index":S.value,infinite:e.infinite,"zoom-rate":e.zoomRate,"min-scale":e.minScale,"max-scale":e.maxScale,"show-progress":e.showProgress,"url-list":e.previewSrcList,scale:e.scale,crossorigin:e.crossorigin,"hide-on-click-modal":e.hideOnClickModal,teleported:e.previewTeleported,"close-on-press-escape":e.closeOnPressEscape,onClose:K,onSwitch:H},xr({toolbar:$(W=>[ce(z.$slots,"toolbar",bl(wl(W)))]),default:$(()=>[z.$slots.viewer?(x(),O("div",Hme,[ce(z.$slots,"viewer")])):te("v-if",!0)]),_:2},[z.$slots.progress?{name:"progress",fn:$(W=>[ce(z.$slots,"progress",bl(wl(W)))]),key:"0"}:void 0,z.$slots["viewer-error"]?{name:"viewer-error",fn:$(W=>[ce(z.$slots,"viewer-error",bl(wl(W)))]),key:"1"}:void 0]),1032,["z-index","initial-index","infinite","zoom-rate","min-scale","max-scale","show-progress","url-list","scale","crossorigin","hide-on-click-modal","teleported","close-on-press-escape"])):te("v-if",!0)],64)):te("v-if",!0)],16))}}),Kme=Wme;const Ume=wt(Kme),Gme=$e({id:{type:String,default:void 0},step:{type:Number,default:1},stepStrictly:Boolean,max:{type:Number,default:Number.MAX_SAFE_INTEGER},min:{type:Number,default:Number.MIN_SAFE_INTEGER},modelValue:{type:[Number,null]},readonly:Boolean,disabled:{type:Boolean,default:void 0},size:pa,controls:{type:Boolean,default:!0},controlsPosition:{type:String,default:"",values:["","right"]},valueOnClear:{type:ae([String,Number,null]),validator:e=>e===null||Je(e)||["min","max"].includes(e),default:null},name:String,placeholder:String,precision:{type:Number,validator:e=>e>=0&&e===Number.parseInt(`${e}`,10)},validateEvent:{type:Boolean,default:!0},...dr(["ariaLabel"]),inputmode:{type:ae(String),default:void 0},align:{type:ae(String),default:"center"},disabledScientific:Boolean,formatter:{type:Function},parser:{type:Function},tabindex:{type:[String,Number],default:0}}),Yme={[Ot]:(e,t)=>t!==e,blur:e=>e instanceof FocusEvent,focus:e=>e instanceof FocusEvent,[ua]:e=>Je(e)||Un(e),[bt]:e=>Je(e)||Un(e)},jme=["aria-label"],qme=["aria-label"];var Xme=q({name:"ElInputNumber",__name:"input-number",props:Gme,emits:Yme,setup(e,{expose:t,emit:n}){const a=e,r=n,{t:o}=Vt(),i=ke("input-number"),l=F(),s=nn({currentValue:a.modelValue,userInput:null}),{formItem:u}=Ea(),c=D(()=>Je(a.modelValue)&&a.modelValue<=a.min),d=D(()=>Je(a.modelValue)&&a.modelValue>=a.max),v=D(()=>{const z=b(a.step);return zt(a.precision)?Math.max(b(a.modelValue),z):(z>a.precision&&un("InputNumber","precision should not be less than the decimal places of step"),a.precision)}),f=D(()=>a.controls&&a.controlsPosition==="right"),h=ca(),m=$n(),g=D(()=>{if(s.userInput!==null)return s.userInput;let z=s.currentValue;if(Un(z))return"";if(Je(z)){if(Number.isNaN(z))return"";zt(a.precision)||(z=z.toFixed(a.precision))}return z}),y=(z,B)=>{if(zt(B)&&(B=v.value),B===0)return Math.round(z);let W=String(z);const N=W.indexOf(".");if(N===-1||!W.replace(".","").split("")[N+B])return z;const V=W.length;return W.charAt(V-1)==="5"&&(W=`${W.slice(0,Math.max(0,V-1))}6`),Number.parseFloat(Number(W).toFixed(B))},b=z=>{if(Un(z))return 0;const B=z.toString(),W=B.indexOf(".");let N=0;return W!==-1&&(N=B.length-W-1),N},_=(z,B=1)=>Je(z)?z>=Number.MAX_SAFE_INTEGER&&B===1?(un("InputNumber","The value has reached the maximum safe integer limit."),z):z<=Number.MIN_SAFE_INTEGER&&B===-1?(un("InputNumber","The value has reached the minimum safe integer limit."),z):y(z+a.step*B):s.currentValue,w=z=>{const B=mn(z),W=HA(z);if(a.disabledScientific&&["e","E"].includes(W)){z.preventDefault();return}switch(B){case Pe.up:z.preventDefault(),S();break;case Pe.down:z.preventDefault(),k();break}},S=()=>{a.readonly||m.value||d.value||(M(_(Number(g.value)||0)),r(ua,s.currentValue),K())},k=()=>{a.readonly||m.value||c.value||(M(_(Number(g.value)||0,-1)),r(ua,s.currentValue),K())},T=(z,B)=>{const{max:W,min:N,step:V,precision:Y,stepStrictly:X,valueOnClear:ee}=a;WW||reW?W:N,B&&r("update:modelValue",re)),re},M=(z,B=!0)=>{var V;const W=s.currentValue,N=T(z);if(!B){r(bt,N);return}s.userInput=null,!(W===N&&z)&&(r(bt,N),W!==N&&r(Ot,N,W),a.validateEvent&&((V=u==null?void 0:u.validate)==null||V.call(u,"change").catch(kt)),s.currentValue=N)},E=z=>{s.userInput=z;let B=z===""?null:Number.parseFloat(z);Number.isNaN(B)&&(B=null),r(ua,B),M(B,!1)},A=z=>{const B=z!==""?Number.parseFloat(z):"";(Je(B)&&!Number.isNaN(B)||a.formatter&&Number.isNaN(B)||B==="")&&M(B),K(),s.userInput=null},I=()=>{var z,B;(B=(z=l.value)==null?void 0:z.focus)==null||B.call(z)},P=()=>{var z,B;(B=(z=l.value)==null?void 0:z.blur)==null||B.call(z)},R=z=>{r("focus",z)},G=z=>{var B,W,N;s.userInput=null,s.currentValue===null&&((B=l.value)!=null&&B.input)&&(l.value.input.value=((W=a.formatter)==null?void 0:W.call(a,""))??""),r("blur",z),a.validateEvent&&((N=u==null?void 0:u.validate)==null||N.call(u,"blur").catch(kt))},K=()=>{s.currentValue!==a.modelValue&&(s.currentValue=a.modelValue)},H=z=>{document.activeElement===z.target&&z.preventDefault()};return ye(()=>a.modelValue,(z,B)=>{const W=T(z,!0);s.userInput===null&&W!==B&&(s.currentValue=W)},{immediate:!0}),ye(()=>a.precision,()=>{s.currentValue=T(a.modelValue)}),mt(()=>{var V;const{min:z,max:B,modelValue:W}=a,N=(V=l.value)==null?void 0:V.input;if(N.setAttribute("role","spinbutton"),Number.isFinite(B)?N.setAttribute("aria-valuemax",String(B)):N.removeAttribute("aria-valuemax"),Number.isFinite(z)?N.setAttribute("aria-valuemin",String(z)):N.removeAttribute("aria-valuemin"),N.setAttribute("aria-valuenow",s.currentValue||s.currentValue===0?String(s.currentValue):""),N.setAttribute("aria-disabled",String(m.value)),!Je(W)&&W!=null){let Y=Number(W);Number.isNaN(Y)&&(Y=null),r(bt,Y)}N.addEventListener("wheel",H,{passive:!1})}),oi(()=>{var z,B;(B=(z=l.value)==null?void 0:z.input)==null||B.setAttribute("aria-valuenow",`${s.currentValue??""}`)}),t({focus:I,blur:P}),(z,B)=>(x(),O("div",{class:U([p(i).b(),p(i).m(p(h)),p(i).is("disabled",p(m)),p(i).is("without-controls",!e.controls),p(i).is("controls-right",f.value),p(i).is(e.align,!!e.align)]),onDragstart:B[0]||(B[0]=at(()=>{},["prevent"]))},[e.controls?st((x(),O("span",{key:0,role:"button","aria-label":p(o)("el.inputNumber.decrease"),class:U([p(i).e("decrease"),p(i).is("disabled",c.value)]),onKeydown:kn(k,["enter"])},[ce(z.$slots,"decrease-icon",{},()=>[L(p(Xe),null,{default:$(()=>[f.value?(x(),ne(p(ui),{key:0})):(x(),ne(p(OI),{key:1}))]),_:1})])],42,jme)),[[p(Zm),k]]):te("v-if",!0),e.controls?st((x(),O("span",{key:1,role:"button","aria-label":p(o)("el.inputNumber.increase"),class:U([p(i).e("increase"),p(i).is("disabled",d.value)]),onKeydown:kn(S,["enter"])},[ce(z.$slots,"increase-icon",{},()=>[L(p(Xe),null,{default:$(()=>[f.value?(x(),ne(p(ip),{key:0})):(x(),ne(p(hf),{key:1}))]),_:1})])],42,qme)),[[p(Zm),S]]):te("v-if",!0),L(p(Ba),{id:e.id,ref_key:"input",ref:l,type:e.formatter?"text":"number",step:e.step,"model-value":g.value,placeholder:e.placeholder,readonly:e.readonly,disabled:p(m),size:p(h),max:e.max,min:e.min,name:e.name,"aria-label":e.ariaLabel,"validate-event":!1,inputmode:e.inputmode,formatter:e.formatter,parser:e.parser,tabindex:e.tabindex,onKeydown:w,onBlur:G,onFocus:R,onInput:E,onChange:A},xr({_:2},[z.$slots.prefix?{name:"prefix",fn:$(()=>[ce(z.$slots,"prefix")]),key:"0"}:void 0,z.$slots.suffix?{name:"suffix",fn:$(()=>[ce(z.$slots,"suffix")]),key:"1"}:void 0]),1032,["id","type","step","model-value","placeholder","readonly","disabled","size","max","min","name","aria-label","inputmode","formatter","parser","tabindex"])],34))}}),Zme=Xme;const vD=wt(Zme),Jme=$e({modelValue:{type:ae(Array)},max:Number,tagType:{...Ll.type,default:"info"},tagEffect:Ll.effect,effect:{type:ae(String),default:"light"},trigger:{type:ae(String),default:Pe.enter},draggable:Boolean,delimiter:{type:[String,RegExp],default:""},size:pa,clearable:Boolean,clearIcon:{type:pn,default:ci},disabled:{type:Boolean,default:void 0},validateEvent:{type:Boolean,default:!0},readonly:Boolean,autofocus:Boolean,id:{type:String,default:void 0},tabindex:{type:[String,Number],default:0},maxlength:{type:[String,Number]},minlength:{type:[String,Number]},placeholder:String,autocomplete:{type:ae(String),default:"off"},saveOnBlur:{type:Boolean,default:!0},collapseTags:Boolean,collapseTagsTooltip:Boolean,maxCollapseTags:{type:Number,default:1},ariaLabel:String}),Qme={[bt]:e=>Ae(e)||zt(e),[Ot]:e=>Ae(e)||zt(e),[ua]:e=>Ye(e),"add-tag":e=>Ye(e)||Ae(e),"remove-tag":(e,t)=>Ye(e)&&Je(t),"drag-tag":(e,t,n)=>Je(e)&&Je(t)&&Ye(n),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0};function ege({wrapperRef:e,handleDragged:t,afterDragged:n}){const a=ke("input-tag"),r=Ht(),o=F(!1);let i,l,s,u;function c(h){return`.${a.e("inner")} .${a.namespace.value}-tag:nth-child(${h+1})`}function d(h,m){i=m,l=e.value.querySelector(c(m)),l&&(l.style.opacity="0.5"),h.dataTransfer.effectAllowed="move"}function v(h,m){if(s=m,h.preventDefault(),h.dataTransfer.dropEffect="move",zt(i)||i===m){o.value=!1;return}const g=e.value.querySelector(c(m)).getBoundingClientRect(),y=i+1!==m,b=i-1!==m,_=h.clientX-g.left,w=y?b?.5:1:-1,S=b?y?.5:0:1;_<=g.width*w?u="before":_>g.width*S?u="after":u=void 0;const k=e.value.querySelector(`.${a.e("inner")}`),T=k.getBoundingClientRect(),M=Number.parseFloat(ml(k,"gap"))/2,E=g.top-T.top;let A=-9999;if(u==="before")A=Math.max(g.left-T.left-M,Math.floor(-M/2));else if(u==="after"){const I=g.right-T.left;A=I+(T.width===I?Math.floor(M/2):M)}NA(r.value,{top:`${E}px`,left:`${A}px`}),o.value=!!u}function f(h){h.preventDefault(),l&&(l.style.opacity=""),u&&!zt(i)&&!zt(s)&&i!==s&&t(i,s,u),o.value=!1,i=void 0,l=null,s=void 0,u=void 0,n==null||n()}return{dropIndicatorRef:r,showDropIndicator:o,handleDragStart:d,handleDragOver:v,handleDragEnd:f}}function tge(){const e=F(!1);return{hovering:e,handleMouseEnter:()=>{e.value=!0},handleMouseLeave:()=>{e.value=!1}}}function nge({props:e,emit:t,formItem:n}){const a=$n(),r=ca(),o=Ht(),i=F(),l=F(),s=D(()=>["small"].includes(r.value)?"small":"default"),u=D(()=>{var H;return(H=e.modelValue)!=null&&H.length?void 0:e.placeholder}),c=D(()=>!(e.readonly||a.value)),d=D(()=>{var H;return zt(e.max)?!1:(((H=e.modelValue)==null?void 0:H.length)??0)>=e.max}),v=D(()=>{var H;return e.collapseTags?(H=e.modelValue)==null?void 0:H.slice(0,e.maxCollapseTags):e.modelValue}),f=D(()=>{var H;return e.collapseTags?(H=e.modelValue)==null?void 0:H.slice(e.maxCollapseTags):[]}),h=H=>{const z=[...e.modelValue??[],...wa(H)];t(bt,z),t(Ot,z),t("add-tag",H),i.value=void 0},m=H=>{var W;const z=H.split(e.delimiter),B=z.length>1?z.map(N=>N.trim()).filter(Boolean):[];if(e.max){const N=e.max-(((W=e.modelValue)==null?void 0:W.length)??0);B.splice(N)}return B.length===1?B[0]:B},g=H=>{var X;const z=(X=H.clipboardData)==null?void 0:X.getData("text");if(e.readonly||d.value||!e.delimiter||!z)return;const{selectionStart:B=0,selectionEnd:W=0,value:N}=H.target,V=N.slice(0,B)+z+N.slice(W),Y=m(V);Y.length&&(h(Y),t(ua,V),H.preventDefault())},y=H=>{if(d.value){i.value=void 0;return}if(!P.value){if(e.delimiter&&i.value){const z=m(i.value);z.length&&h(z)}t(ua,H.target.value)}},b=H=>{var z;if(!P.value)switch(mn(H)){case e.trigger:H.preventDefault(),H.stopPropagation(),w();break;case Pe.numpadEnter:e.trigger===Pe.enter&&(H.preventDefault(),H.stopPropagation(),w());break;case Pe.backspace:!i.value&&((z=e.modelValue)!=null&&z.length)&&(H.preventDefault(),H.stopPropagation(),S(e.modelValue.length-1));break}},_=H=>{if(!(P.value||!$A()))switch(mn(H)){case Pe.space:e.trigger===Pe.space&&(H.preventDefault(),H.stopPropagation(),w());break}},w=()=>{var z;const H=(z=i.value)==null?void 0:z.trim();!H||d.value||h(H)},S=H=>{const z=(e.modelValue??[]).slice(),[B]=z.splice(H,1);t(bt,z),t(Ot,z),t("remove-tag",B,H)},k=()=>{i.value=void 0,t(bt,void 0),t(Ot,void 0),t("clear")},T=(H,z,B)=>{const W=(e.modelValue??[]).slice(),[N]=W.splice(H,1),V=z>H&&B==="before"?-1:z{var H;(H=o.value)==null||H.focus()},E=()=>{var H;(H=o.value)==null||H.blur()},{wrapperRef:A,isFocused:I}=Fl(o,{disabled:a,beforeBlur(H){var z;return(z=l.value)==null?void 0:z.isFocusInsideContent(H)},afterBlur(){var H;e.saveOnBlur?w():i.value=void 0,e.validateEvent&&((H=n==null?void 0:n.validate)==null||H.call(n,"blur").catch(kt))}}),{isComposing:P,handleCompositionStart:R,handleCompositionUpdate:G,handleCompositionEnd:K}=rp({afterComposition:y});return ye(()=>e.modelValue,()=>{var H;e.validateEvent&&((H=n==null?void 0:n.validate)==null||H.call(n,Ot).catch(kt))}),{inputRef:o,wrapperRef:A,tagTooltipRef:l,isFocused:I,isComposing:P,inputValue:i,size:r,tagSize:s,placeholder:u,closable:c,disabled:a,inputLimit:d,showTagList:v,collapseTagList:f,handleDragged:T,handlePaste:g,handleInput:y,handleKeydown:b,handleKeyup:_,handleAddTag:w,handleRemoveTag:S,handleClear:k,handleCompositionStart:R,handleCompositionUpdate:G,handleCompositionEnd:K,focus:M,blur:E}}function age({props:e,isFocused:t,hovering:n,disabled:a,inputValue:r,size:o,validateState:i,validateIcon:l,needStatusIcon:s}){const u=Nl(),c=Xn(),d=ke("input-tag"),v=ke("input"),f=F(),h=F(),m=D(()=>[d.b(),d.is("focused",t.value),d.is("hovering",n.value),d.is("disabled",a.value),d.m(o.value),d.e("wrapper"),u.class]),g=D(()=>[u.style]),y=D(()=>{var E,A;return[d.e("inner"),d.is("draggable",e.draggable),d.is("left-space",!((E=e.modelValue)!=null&&E.length)&&!c.prefix),d.is("right-space",!((A=e.modelValue)!=null&&A.length)&&!_.value)]}),b=D(()=>{var E;return e.clearable&&!a.value&&!e.readonly&&(((E=e.modelValue)==null?void 0:E.length)||r.value)&&(t.value||n.value)}),_=D(()=>c.suffix||b.value||i.value&&l.value&&s.value),w=nn({innerWidth:0,collapseItemWidth:0}),S=()=>{if(!h.value)return 0;const E=window.getComputedStyle(h.value);return Number.parseFloat(E.gap||"6px")},k=()=>{w.innerWidth=Number.parseFloat(window.getComputedStyle(h.value).width)},T=()=>{w.collapseItemWidth=f.value.getBoundingClientRect().width},M=D(()=>{if(!e.collapseTags)return{};const E=S(),A=E+11,I=f.value&&e.maxCollapseTags===1?w.innerWidth-w.collapseItemWidth-E-A:w.innerWidth-A;return{maxWidth:`${Math.max(I,0)}px`}});return Mn(h,k),Mn(f,T),{ns:d,nsInput:v,containerKls:m,containerStyle:g,innerKls:y,showClear:b,showSuffix:_,tagStyle:M,collapseItemRef:f,innerRef:h}}const rge=["id","minlength","maxlength","disabled","readonly","autocomplete","tabindex","placeholder","autofocus","ariaLabel"],oge=["textContent"];var ige=q({name:"ElInputTag",inheritAttrs:!1,__name:"input-tag",props:Jme,emits:Qme,setup(e,{expose:t,emit:n}){const a=e,r=n,o=A1(),i=Xn(),{form:l,formItem:s}=Ea(),{inputId:u}=Yr(a,{formItemContext:s}),c=D(()=>(l==null?void 0:l.statusIcon)??!1),d=D(()=>(s==null?void 0:s.validateState)||""),v=D(()=>d.value&&F1[d.value]),{inputRef:f,wrapperRef:h,tagTooltipRef:m,isFocused:g,inputValue:y,size:b,tagSize:_,placeholder:w,closable:S,disabled:k,showTagList:T,collapseTagList:M,handleDragged:E,handlePaste:A,handleInput:I,handleKeydown:P,handleKeyup:R,handleRemoveTag:G,handleClear:K,handleCompositionStart:H,handleCompositionUpdate:z,handleCompositionEnd:B,focus:W,blur:N}=nge({props:a,emit:r,formItem:s}),{hovering:V,handleMouseEnter:Y,handleMouseLeave:X}=tge(),{calculatorRef:ee,inputStyle:re}=w6(),{dropIndicatorRef:ie,showDropIndicator:le,handleDragStart:ve,handleDragOver:he,handleDragEnd:J}=ege({wrapperRef:h,handleDragged:E,afterDragged:W}),{ns:Z,nsInput:j,containerKls:oe,containerStyle:se,innerKls:fe,showClear:ge,showSuffix:Se,tagStyle:De,collapseItemRef:Te,innerRef:Re}=age({props:a,hovering:V,isFocused:g,inputValue:y,disabled:k,size:b,validateState:d,validateIcon:v,needStatusIcon:c});return t({focus:W,blur:N}),(Ce,Le)=>(x(),O("div",{ref_key:"wrapperRef",ref:h,class:U(p(oe)),style:et(p(se)),onMouseenter:Le[9]||(Le[9]=(...me)=>p(Y)&&p(Y)(...me)),onMouseleave:Le[10]||(Le[10]=(...me)=>p(X)&&p(X)(...me))},[p(i).prefix?(x(),O("div",{key:0,class:U(p(Z).e("prefix"))},[ce(Ce.$slots,"prefix")],2)):te("v-if",!0),C("div",{ref_key:"innerRef",ref:Re,class:U(p(fe))},[(x(!0),O(Ve,null,dt(p(T),(me,Be)=>(x(),ne(p(Cl),{key:Be,size:p(_),closable:p(S),type:e.tagType,effect:e.tagEffect,draggable:p(S)&&e.draggable,style:et(p(De)),"disable-transitions":"",onClose:ze=>p(G)(Be),onDragstart:ze=>p(ve)(ze,Be),onDragover:ze=>p(he)(ze,Be),onDragend:p(J),onDrop:Le[0]||(Le[0]=at(()=>{},["stop"]))},{default:$(()=>[ce(Ce.$slots,"tag",{value:me,index:Be},()=>[ue(Q(me),1)])]),_:2},1032,["size","closable","type","effect","draggable","style","onClose","onDragstart","onDragover","onDragend"]))),128)),e.collapseTags&&e.modelValue&&e.modelValue.length>e.maxCollapseTags?(x(),ne(p(Ma),{key:0,ref_key:"tagTooltipRef",ref:m,disabled:!e.collapseTagsTooltip,"fallback-placements":["bottom","top","right","left"],effect:e.effect,placement:"bottom"},{default:$(()=>[C("div",{ref_key:"collapseItemRef",ref:Te,class:U(p(Z).e("collapse-tag"))},[L(p(Cl),{closable:!1,size:p(_),type:e.tagType,effect:e.tagEffect,"disable-transitions":""},{default:$(()=>[ue(" + "+Q(e.modelValue.length-e.maxCollapseTags),1)]),_:1},8,["size","type","effect"])],2)]),content:$(()=>[C("div",{class:U(p(Z).e("input-tag-list"))},[(x(!0),O(Ve,null,dt(p(M),(me,Be)=>(x(),ne(p(Cl),{key:Be,size:p(_),closable:p(S),type:e.tagType,effect:e.tagEffect,"disable-transitions":"",onClose:ze=>p(G)(Be+e.maxCollapseTags)},{default:$(()=>[ce(Ce.$slots,"tag",{value:me,index:Be+e.maxCollapseTags},()=>[ue(Q(me),1)])]),_:2},1032,["size","closable","type","effect","onClose"]))),128))],2)]),_:3},8,["disabled","effect"])):te("v-if",!0),C("div",{class:U(p(Z).e("input-wrapper"))},[st(C("input",Tt({id:p(u),ref_key:"inputRef",ref:f,"onUpdate:modelValue":Le[1]||(Le[1]=me=>xn(y)?y.value=me:null)},p(o),{type:"text",minlength:e.minlength,maxlength:e.maxlength,disabled:p(k),readonly:e.readonly,autocomplete:e.autocomplete,tabindex:e.tabindex,placeholder:p(w),autofocus:e.autofocus,ariaLabel:e.ariaLabel,class:p(Z).e("input"),style:p(re),onCompositionstart:Le[2]||(Le[2]=(...me)=>p(H)&&p(H)(...me)),onCompositionupdate:Le[3]||(Le[3]=(...me)=>p(z)&&p(z)(...me)),onCompositionend:Le[4]||(Le[4]=(...me)=>p(B)&&p(B)(...me)),onPaste:Le[5]||(Le[5]=(...me)=>p(A)&&p(A)(...me)),onInput:Le[6]||(Le[6]=(...me)=>p(I)&&p(I)(...me)),onKeydown:Le[7]||(Le[7]=(...me)=>p(P)&&p(P)(...me)),onKeyup:Le[8]||(Le[8]=(...me)=>p(R)&&p(R)(...me))}),null,16,rge),[[D7,p(y)]]),C("span",{ref_key:"calculatorRef",ref:ee,"aria-hidden":"true",class:U(p(Z).e("input-calculator")),textContent:Q(p(y))},null,10,oge)],2),st(C("div",{ref_key:"dropIndicatorRef",ref:ie,class:U(p(Z).e("drop-indicator"))},null,2),[[Zt,p(le)]])],2),p(Se)?(x(),O("div",{key:1,class:U(p(Z).e("suffix"))},[ce(Ce.$slots,"suffix"),p(ge)?(x(),ne(p(Xe),{key:0,class:U([p(Z).e("icon"),p(Z).e("clear")]),onMousedown:at(p(kt),["prevent"]),onClick:p(K)},{default:$(()=>[(x(),ne(St(e.clearIcon)))]),_:1},8,["class","onMousedown","onClick"])):te("v-if",!0),d.value&&v.value&&c.value?(x(),ne(p(Xe),{key:1,class:U([p(j).e("icon"),p(j).e("validateIcon"),p(j).is("loading",d.value==="validating")])},{default:$(()=>[(x(),ne(St(v.value)))]),_:1},8,["class"])):te("v-if",!0)],2)):te("v-if",!0)],38))}}),lge=ige;const sge=wt(lge),uge={[bt]:e=>Ye(e),[Ot]:e=>Ye(e),finish:e=>Ye(e),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent},cge=["id","aria-label","aria-labelledby"],dge=["value","type","disabled","readonly","inputmode","aria-label","onClick","onKeydown","onInput"];var fge=q({name:"ElInputOtp",__name:"input-otp",props:{modelValue:{type:[String,Number],required:!1},length:{type:Number,required:!1,default:6},validator:{type:Function,required:!1,default:()=>!0},inputmode:{type:null,required:!1},type:{type:String,required:!1,default:"outlined"},size:{type:null,required:!1},mask:{type:Boolean,required:!1},disabled:{type:Boolean,required:!1,default:void 0},readonly:{type:Boolean,required:!1},id:{type:String,required:!1},validateEvent:{type:Boolean,required:!1,default:!0},separator:{type:[String,Object,Function],required:!1},ariaLabel:{type:String,required:!1}},emits:uge,setup(e,{expose:t,emit:n}){const a=e,r=n,o=D(()=>{const I=String(a.modelValue??"");return Array.from({length:a.length},(P,R)=>I.charAt(R))}),i=D(()=>{const{separator:I}=a,P=Qe(I)?I:()=>I;return Array.from({length:a.length-1},(R,G)=>P(G))}),l=F(o.value),s=F(!1),u=F([]),c=ke("input-otp"),{t:d}=Vt(),{formItem:v}=Ea(),{inputId:f,isLabeledByFormItem:h}=Yr(a,{formItemContext:v}),m=ca(),g=$n();let y=a.modelValue;const b=I=>{const P=l.value.findIndex((R,G)=>!R&&G<=I);return P===-1?I:P},_=I=>{var P;(P=u.value)!=null&&P.includes(I.relatedTarget)||(s.value=!0,r("focus",I))},w=I=>{var P,R;(P=u.value)!=null&&P.includes(I.relatedTarget)||(s.value=!1,r("blur",I),a.validateEvent&&((R=v==null?void 0:v.validate)==null||R.call(v,"blur").catch(kt)))},S=(I=!0)=>{const P=l.value.join("").slice(0,a.length);P!==a.modelValue&&(r(bt,P),I&&P.length===a.length&&r("finish",P))},k=(I,P)=>{const R=mn(I);let G=!0;switch(R){case Pe.backspace:if(a.readonly)break;l.value[P]="",E(P-1),S();break;case Pe.delete:if(a.readonly)break;l.value[P]="",E(P),S();break;case Pe.up:case Pe.left:E(P-1);break;case Pe.down:case Pe.right:E(P+1);break;default:G=!1}G&&I.preventDefault()},T=(I,P)=>{const R=I.target,G=b(P);let K=G+1,H=R.value;if(H.length>1){const z=M(H,G);R.value=l.value[P]??"",z.forEach((B,W)=>l.value[G+W]=B),E(G+z.length),S();return}a.validator(H,G)||(R.value=l.value[P]??"",H=R.value,K=G),l.value[G]=H,G!==P&&(R.value=l.value[P]??""),E(K),S()},M=(I,P=0)=>{const R=`${I??""}`.split(""),G=[];for(const K of R){if(G.length+P>=a.length)break;a.validator(K,G.length+P)&&G.push(K)}return G},E=(I=0)=>{var G;const P=I1(I,0,a.length-1),R=(G=u.value)==null?void 0:G[P];document.activeElement!==R&&(R==null||R.focus()),Wr(()=>{!a.readonly&&document.activeElement===R&&(R==null||R.select())})},A=()=>{var I,P;(P=(I=u.value)==null?void 0:I.find(R=>document.activeElement===R))==null||P.blur()};return ye(()=>a.modelValue,()=>{var I;l.value=o.value,a.validateEvent&&((I=v==null?void 0:v.validate)==null||I.call(v,"change").catch(kt))}),ye(()=>a.length,()=>{l.value=o.value,S(!1)}),ye(s,I=>{if(I){y=a.modelValue;return}y!==a.modelValue&&r(Ot,a.modelValue)}),t({inputRefs:u,focus:E,blur:A}),(I,P)=>{var R;return x(),O("div",{id:p(f),class:U([p(c).b(),p(c).m(p(m)),p(c).m(e.type),p(c).is("disabled",p(g))]),role:"group","aria-label":p(h)?void 0:e.ariaLabel||p(d)("el.inputOTP.groupLabel"),"aria-labelledby":p(h)?(R=p(v))==null?void 0:R.labelId:void 0},[(x(!0),O(Ve,null,dt(e.length,(G,K)=>(x(),O(Ve,{key:K},[C("label",{class:U(p(c).e("input-field"))},[C("input",{ref_for:!0,ref_key:"inputRefs",ref:u,value:l.value[K],class:U(p(c).e("input")),type:e.mask?"password":"text",disabled:p(g),readonly:e.readonly,inputmode:e.inputmode,autocomplete:"one-time-code","aria-label":p(d)("el.inputOTP.defaultLabel",{index:K+1}),onFocus:_,onBlur:w,onClick:H=>E(K),onKeydown:H=>k(H,K),onInput:H=>T(H,K)},null,42,dge)],2),(I.$slots.separator||i.value[K])&&K[(x(),ne(St(()=>i.value[K])))]):te("v-if",!0)],64))),128))],10,cge)}}}),vge=fge;const hge=wt(vge),pge=$e({type:{type:String,values:["primary","success","warning","info","danger","default"],default:void 0},underline:{type:[Boolean,String],values:[!0,!1,"always","never","hover"],default:void 0},disabled:Boolean,href:{type:String,default:""},target:{type:String,default:"_self"},icon:{type:pn}}),mge={click:e=>e instanceof MouseEvent},gge=["href","target"];var yge=q({name:"ElLink",__name:"link",props:pge,emits:mge,setup(e,{emit:t}){const n=e,a=t,r=Hl("link");Ri({scope:"el-link",from:"The underline option (boolean)",replacement:"'always' | 'hover' | 'never'",version:"3.0.0",ref:"https://element-plus.org/en-US/component/link.html#underline"},D(()=>vn(n.underline)));const o=ke("link"),i=D(()=>{var u;return[o.b(),o.m(n.type??((u=r.value)==null?void 0:u.type)??"default"),o.is("disabled",n.disabled),o.is("underline",l.value==="always"),o.is("hover-underline",l.value==="hover"&&!n.disabled)]}),l=D(()=>{var u;return vn(n.underline)?n.underline?"hover":"never":n.underline??((u=r.value)==null?void 0:u.underline)??"hover"});function s(u){n.disabled||a("click",u)}return(u,c)=>(x(),O("a",{class:U(i.value),href:e.disabled||!e.href?void 0:e.href,target:e.disabled||!e.href?void 0:e.target,onClick:s},[e.icon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1})):te("v-if",!0),u.$slots.default?(x(),O("span",{key:1,class:U(p(o).e("inner"))},[ce(u.$slots,"default")],2)):te("v-if",!0),u.$slots.icon?ce(u.$slots,"icon",{key:2}):te("v-if",!0)],10,gge))}}),bge=yge;const _ge=wt(bge),Sw="rootMenu",vg="subMenu:";function hD(e,t){const n=D(()=>{let a=e.parent;const r=[t.value];for(;a.type.name!=="ElMenu";)a.props.index&&r.unshift(a.props.index),a=a.parent;return r});return{parentMenu:D(()=>{let a=e.parent;for(;a&&!["ElMenu","ElSubMenu"].includes(a.type.name);)a=a.parent;return a}),indexPath:n}}function wge(e){return D(()=>{const t=e.backgroundColor;return t?new ea(t).shade(20).toString():""})}const pD=(e,t)=>{const n=ke("menu");return D(()=>n.cssVarBlock({"text-color":e.textColor||"","hover-text-color":e.textColor||"","bg-color":e.backgroundColor||"","hover-bg-color":wge(e).value||"","active-color":e.activeTextColor||"",level:`${t}`}))},Sge=$e({index:{type:String,required:!0},showTimeout:Number,hideTimeout:Number,popperClass:{type:ae([String,Array,Object,Boolean]),default:void 0},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},disabled:Boolean,teleported:{type:Boolean,default:void 0},popperOffset:Number,expandCloseIcon:{type:pn},expandOpenIcon:{type:pn},collapseCloseIcon:{type:pn},collapseOpenIcon:{type:pn}}),ly="ElSubMenu";var Cw=q({name:ly,props:Sge,setup(e,{slots:t,expose:n}){const a=Et(),{indexPath:r,parentMenu:o}=hD(a,D(()=>e.index)),i=ke("menu"),l=ke("sub-menu"),s=Ze(Sw);s||Dn(ly,"can not inject root menu");const u=Ze(`${vg}${o.value.uid}`);u||Dn(ly,"can not inject sub menu");const c=F({}),d=F({});let v;const f=F(!1),h=F(),m=F(),g=D(()=>u.level===0),y=D(()=>M.value==="horizontal"&&g.value?"bottom-start":"right-start"),b=D(()=>M.value==="horizontal"&&g.value||M.value==="vertical"&&!s.props.collapse?e.expandCloseIcon&&e.expandOpenIcon?k.value?e.expandOpenIcon:e.expandCloseIcon:ui:e.collapseCloseIcon&&e.collapseOpenIcon?k.value?e.collapseOpenIcon:e.collapseCloseIcon:za),_=D(()=>{const Y=e.teleported;return zt(Y)?g.value:Y}),w=D(()=>s.props.collapse?`${i.namespace.value}-zoom-in-left`:`${i.namespace.value}-zoom-in-top`),S=D(()=>M.value==="horizontal"&&g.value?["bottom-start","bottom-end","top-start","top-end","right-start","left-start"]:["right-start","right","right-end","left-start","bottom-start","bottom-end","top-start","top-end"]),k=D(()=>s.openedMenus.includes(e.index)),T=D(()=>[...Object.values(c.value),...Object.values(d.value)].some(({active:Y})=>Y)),M=D(()=>s.props.mode),E=D(()=>s.props.persistent),A=nn({index:e.index,indexPath:r,active:T}),I=pD(s.props,u.level+1),P=D(()=>e.popperOffset??s.props.popperOffset),R=D(()=>e.popperClass??s.props.popperClass),G=D(()=>e.popperStyle??s.props.popperStyle),K=D(()=>e.showTimeout??s.props.showTimeout),H=D(()=>e.hideTimeout??s.props.hideTimeout),z=()=>{var Y,X,ee;return(ee=(X=(Y=m.value)==null?void 0:Y.popperRef)==null?void 0:X.popperInstanceRef)==null?void 0:ee.destroy()},B=Y=>{Y||z()},W=()=>{s.props.menuTrigger==="hover"&&s.props.mode==="horizontal"||s.props.collapse&&s.props.mode==="vertical"||e.disabled||s.handleSubMenuClick({index:e.index,indexPath:r.value,active:T.value})},N=(Y,X=K.value)=>{var ee;if(Y.type!=="focus"){if(s.props.menuTrigger==="click"&&s.props.mode==="horizontal"||!s.props.collapse&&s.props.mode==="vertical"||e.disabled){u.mouseInChild.value=!0;return}u.mouseInChild.value=!0,v==null||v(),{stop:v}=Bd(()=>{s.openMenu(e.index,r.value)},X),_.value&&((ee=o.value.vnode.el)==null||ee.dispatchEvent(new MouseEvent("mouseenter"))),Y.type==="mouseenter"&&Y.target&&He(()=>{Jh(Y.target,{preventScroll:!0})})}},V=(Y=!1)=>{var X;if(s.props.menuTrigger==="click"&&s.props.mode==="horizontal"||!s.props.collapse&&s.props.mode==="vertical"){u.mouseInChild.value=!1;return}v==null||v(),u.mouseInChild.value=!1,{stop:v}=Bd(()=>!f.value&&s.closeMenu(e.index,r.value),H.value),_.value&&Y&&((X=u.handleMouseleave)==null||X.call(u,!0))};ye(()=>s.props.collapse,Y=>B(!!Y));{const Y=ee=>{d.value[ee.index]=ee},X=ee=>{delete d.value[ee.index]};Ft(`${vg}${a.uid}`,{addSubMenu:Y,removeSubMenu:X,handleMouseleave:V,mouseInChild:f,level:u.level+1})}return n({opened:k}),mt(()=>{s.addSubMenu(A),u.addSubMenu(A)}),rn(()=>{u.removeSubMenu(A),s.removeSubMenu(A)}),()=>{var ee;const Y=[(ee=t.title)==null?void 0:ee.call(t),ot(Xe,{class:l.e("icon-arrow"),style:{transform:k.value?e.expandCloseIcon&&e.expandOpenIcon||e.collapseCloseIcon&&e.collapseOpenIcon&&s.props.collapse?"none":"rotateZ(180deg)":"none"}},{default:()=>Ye(b.value)?ot(a.appContext.components[b.value]):ot(b.value)})],X=s.isMenuPopup?ot(Ma,{ref:m,visible:k.value,effect:"light",pure:!0,offset:P.value,showArrow:!1,persistent:E.value,popperClass:R.value,popperStyle:G.value,placement:y.value,teleported:_.value,fallbackPlacements:S.value,transition:w.value,gpuAcceleration:!1},{content:()=>{var re;return ot("div",{class:[i.m(M.value),i.m("popup-container"),R.value],onMouseenter:ie=>N(ie,100),onMouseleave:()=>V(!0),onFocus:ie=>N(ie,100)},[ot("ul",{class:[i.b(),i.m("popup"),i.m(`popup-${y.value}`)],style:I.value},[(re=t.default)==null?void 0:re.call(t)])])},default:()=>ot("div",{class:l.e("title"),onClick:W},Y)}):ot(Ve,{},[ot("div",{class:l.e("title"),ref:h,onClick:W},Y),ot(U1,{},{default:()=>{var re;return st(ot("ul",{role:"menu",class:[i.b(),i.m("inline")],style:I.value},[(re=t.default)==null?void 0:re.call(t)]),[[Zt,k.value]])}})]);return ot("li",{class:[l.b(),l.is("active",T.value),l.is("opened",k.value),l.is("disabled",e.disabled)],role:"menuitem",ariaHaspopup:!0,ariaExpanded:k.value,onMouseenter:N,onMouseleave:()=>V(),onFocus:N},[X])}}}),Cge=class{constructor(e,t){this.parent=e,this.domNode=t,this.subIndex=0,this.subMenuItems=this.domNode.querySelectorAll("li"),this.addListeners()}gotoSubIndex(e){e===this.subMenuItems.length?e=0:e<0&&(e=this.subMenuItems.length-1),this.subMenuItems[e].focus(),this.subIndex=e}addListeners(){const e=this.parent.domNode;this.subMenuItems.forEach(t=>{t.addEventListener("keydown",n=>{const a=mn(n);let r=!1;switch(a){case Pe.down:this.gotoSubIndex(this.subIndex+1),r=!0;break;case Pe.up:this.gotoSubIndex(this.subIndex-1),r=!0;break;case Pe.tab:X0(e,"mouseleave");break;case Pe.enter:case Pe.numpadEnter:case Pe.space:r=!0,n.currentTarget.click();break}return r&&(n.preventDefault(),n.stopPropagation()),!1})})}},xge=class{constructor(e,t){this.domNode=e,this.submenu=null,this.init(t)}init(e){this.domNode.setAttribute("tabindex","0");const t=this.domNode.querySelector(`.${e}-menu`);t&&(this.submenu=new Cge(this,t)),this.addListeners()}addListeners(){this.domNode.addEventListener("keydown",e=>{var a,r;const t=mn(e);let n=!1;switch(t){case Pe.down:X0(e.currentTarget,"mouseenter"),(a=this.submenu)==null||a.gotoSubIndex(0),n=!0;break;case Pe.up:X0(e.currentTarget,"mouseenter"),(r=this.submenu)==null||r.gotoSubIndex(this.submenu.subMenuItems.length-1),n=!0;break;case Pe.tab:X0(e.currentTarget,"mouseleave");break;case Pe.enter:case Pe.numpadEnter:case Pe.space:n=!0,e.currentTarget.click();break}n&&e.preventDefault()})}},kge=class{constructor(e,t){this.domNode=e,this.init(t)}init(e){const t=this.domNode.childNodes;Array.from(t).forEach(n=>{n.nodeType===1&&new xge(n,e)})}},Tge=q({name:"ElMenuCollapseTransition",__name:"menu-collapse-transition",setup(e){const t=ke("menu"),n={onBeforeEnter:a=>a.style.opacity="0.2",onEnter(a,r){Eo(a,`${t.namespace.value}-opacity-transition`),a.style.opacity="1",r()},onAfterEnter(a){ir(a,`${t.namespace.value}-opacity-transition`),a.style.opacity=""},onBeforeLeave(a){a.dataset||(a.dataset={}),Ni(a,t.m("collapse"))?(ir(a,t.m("collapse")),a.dataset.oldOverflow=a.style.overflow,a.dataset.scrollWidth=a.clientWidth.toString(),Eo(a,t.m("collapse"))):(Eo(a,t.m("collapse")),a.dataset.oldOverflow=a.style.overflow,a.dataset.scrollWidth=a.clientWidth.toString(),ir(a,t.m("collapse"))),a.style.width=`${a.scrollWidth}px`,a.style.overflow="hidden"},onLeave(a){Eo(a,"horizontal-collapse-transition"),a.style.width=`${a.dataset.scrollWidth}px`}};return(a,r)=>(x(),ne(Pa,Tt({mode:"out-in"},n),{default:$(()=>[ce(a.$slots,"default")]),_:3},16))}}),Mge=Tge;const Ege=$e({mode:{type:String,values:["horizontal","vertical"],default:"vertical"},defaultActive:{type:String,default:""},defaultOpeneds:{type:ae(Array),default:()=>Nn([])},uniqueOpened:Boolean,router:Boolean,menuTrigger:{type:String,values:["hover","click"],default:"hover"},collapse:Boolean,backgroundColor:String,textColor:String,activeTextColor:String,closeOnClickOutside:Boolean,collapseTransition:{type:Boolean,default:!0},ellipsis:{type:Boolean,default:!0},popperOffset:{type:Number,default:6},ellipsisIcon:{type:pn,default:()=>$I},popperEffect:{type:ae(String),default:"dark"},popperClass:{type:ae([String,Array,Object,Boolean]),default:void 0},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},showTimeout:{type:Number,default:300},hideTimeout:{type:Number,default:300},persistent:{type:Boolean,default:!0}}),sy=e=>Ae(e)&&e.every(t=>Ye(t)),Age={close:(e,t)=>Ye(e)&&sy(t),open:(e,t)=>Ye(e)&&sy(t),select:(e,t,n,a)=>Ye(e)&&sy(t)&&_t(n)&&(zt(a)||a instanceof Promise)},Ex=64;var Ige=q({name:"ElMenu",props:Ege,emits:Age,setup(e,{emit:t,slots:n,expose:a}){const r=Et(),o=r.appContext.config.globalProperties.$router,i=F(),l=F(),s=ke("menu"),u=ke("sub-menu");let c=Ex;const d=F(-1),v=F(e.defaultOpeneds&&!e.collapse?e.defaultOpeneds.slice(0):[]),f=F(e.defaultActive),h=F({}),m=F({}),g=D(()=>e.mode==="horizontal"||e.mode==="vertical"&&e.collapse),y=()=>{const z=f.value&&h.value[f.value];!z||e.mode==="horizontal"||e.collapse||z.indexPath.forEach(B=>{const W=m.value[B];W&&b(B,W.indexPath)})},b=(z,B)=>{v.value.includes(z)||(e.uniqueOpened&&(v.value=v.value.filter(W=>B.includes(W))),v.value.push(z),t("open",z,B))},_=z=>{const B=v.value.indexOf(z);B!==-1&&v.value.splice(B,1)},w=(z,B)=>{_(z),t("close",z,B)},S=({index:z,indexPath:B})=>{v.value.includes(z)?w(z,B):b(z,B)},k=z=>{(e.mode==="horizontal"||e.collapse)&&(v.value=[]);const{index:B,indexPath:W}=z;if(!(Un(B)||Un(W)))if(e.router&&o){const N=z.route||B,V=o.push(N).then(Y=>(Y||(f.value=B),Y));t("select",B,W,{index:B,indexPath:W,route:N},V)}else f.value=B,t("select",B,W,{index:B,indexPath:W})},T=z=>{var W;const B=h.value;f.value=((W=B[z]||f.value&&B[f.value]||B[e.defaultActive])==null?void 0:W.index)??z},M=z=>{const B=getComputedStyle(z),W=Number.parseInt(B.marginLeft,10),N=Number.parseInt(B.marginRight,10);return z.offsetWidth+W+N||0},E=()=>{if(!i.value)return-1;const z=Array.from(i.value.childNodes).filter(ee=>ee.nodeName!=="#comment"&&(ee.nodeName!=="#text"||ee.nodeValue)),B=getComputedStyle(i.value),W=Number.parseInt(B.paddingLeft,10),N=Number.parseInt(B.paddingRight,10),V=i.value.clientWidth-W-N;let Y=0,X=0;return z.forEach((ee,re)=>{Y+=M(ee),Y<=V-c&&(X=re+1)}),X===z.length?-1:X},A=z=>m.value[z].indexPath,I=(z,B=33.34)=>{let W;return()=>{W&&clearTimeout(W),W=setTimeout(()=>{z()},B)}};let P=!0;const R=()=>{const z=da(l);if(z&&(c=M(z)||Ex),d.value===E())return;const B=()=>{d.value=-1,He(()=>{d.value=E()})};P?B():I(B)(),P=!1};ye(()=>e.defaultActive,z=>{h.value[z]||(f.value=""),T(z)}),ye(()=>e.collapse,z=>{z&&(v.value=[])}),ye(h.value,y);let G;ur(()=>{e.mode==="horizontal"&&e.ellipsis?G=Mn(i,R).stop:G==null||G()});const K=F(!1);{const z=V=>{m.value[V.index]=V},B=V=>{delete m.value[V.index]};Ft(Sw,nn({props:e,openedMenus:v,items:h,subMenus:m,activeIndex:f,isMenuPopup:g,addMenuItem:V=>{h.value[V.index]=V},removeMenuItem:V=>{delete h.value[V.index]},addSubMenu:z,removeSubMenu:B,openMenu:b,closeMenu:w,handleMenuItemClick:k,handleSubMenuClick:S})),Ft(`${vg}${r.uid}`,{addSubMenu:z,removeSubMenu:B,mouseInChild:K,level:0})}mt(()=>{e.mode==="horizontal"&&new kge(r.vnode.el,s.namespace.value)}),a({open:B=>{const{indexPath:W}=m.value[B];W.forEach(N=>b(N,W))},close:_,updateActiveIndex:T,handleResize:R});const H=pD(e,0);return()=>{var V;let z=((V=n.default)==null?void 0:V.call(n))??[];const B=[];if(e.mode==="horizontal"&&i.value){const Y=eo(z).filter(re=>(re==null?void 0:re.shapeFlag)!==8),X=d.value===-1?Y:Y.slice(0,d.value),ee=d.value===-1?[]:Y.slice(d.value);ee!=null&&ee.length&&e.ellipsis&&(z=X,B.push(ot(Cw,{ref:l,index:"sub-menu-more",class:u.e("hide-arrow"),popperOffset:e.popperOffset},{title:()=>ot(Xe,{class:u.e("icon-more")},{default:()=>ot(e.ellipsisIcon)}),default:()=>ee})))}const W=e.closeOnClickOutside?[[Ns,()=>{v.value.length&&(K.value||(v.value.forEach(Y=>t("close",Y,A(Y))),v.value=[]))}]]:[],N=st(ot("ul",{key:String(e.collapse),role:"menubar",ref:i,style:H.value,class:{[s.b()]:!0,[s.m(e.mode)]:!0,[s.m("collapse")]:e.collapse}},[...z,...B]),W);return e.collapseTransition&&e.mode==="vertical"?ot(Mge,()=>N):N}}});const Lge=$e({index:{type:String,required:!0},route:{type:ae([String,Object])},disabled:Boolean}),Pge={click:e=>Ye(e.index)&&Ae(e.indexPath)},Dge={title:String},uy="ElMenuItem";var Oge=q({name:uy,__name:"menu-item",props:Lge,emits:Pge,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Et(),i=Ze(Sw),l=ke("menu"),s=ke("menu-item");i||Dn(uy,"can not inject root menu");const{parentMenu:u,indexPath:c}=hD(o,cn(a,"index")),d=Ze(`${vg}${u.value.uid}`);d||Dn(uy,"can not inject sub menu");const v=D(()=>a.index===i.activeIndex),f=nn({index:a.index,indexPath:c,active:v}),h=()=>{a.disabled||(i.handleMenuItemClick({index:a.index,indexPath:c.value,route:a.route}),r("click",f))};return mt(()=>{d.addSubMenu(f),i.addMenuItem(f)}),rn(()=>{d.removeSubMenu(f),i.removeMenuItem(f)}),t({parentMenu:u,rootMenu:i,active:v,nsMenu:l,nsMenuItem:s,handleClick:h}),(m,g)=>(x(),O("li",{class:U([p(s).b(),p(s).is("active",v.value),p(s).is("disabled",e.disabled)]),role:"menuitem",tabindex:"-1",onClick:h},[p(u).type.name==="ElMenu"&&p(i).props.collapse&&m.$slots.title?(x(),ne(p(Ma),{key:0,effect:p(i).props.popperEffect,placement:"right","fallback-placements":["left"],"popper-class":p(i).props.popperClass,"popper-style":p(i).props.popperStyle,persistent:p(i).props.persistent,"focus-on-target":""},{content:$(()=>[ce(m.$slots,"title")]),default:$(()=>[C("div",{class:U(p(l).be("tooltip","trigger"))},[ce(m.$slots,"default")],2)]),_:3},8,["effect","popper-class","popper-style","persistent"])):(x(),O(Ve,{key:1},[ce(m.$slots,"default"),ce(m.$slots,"title")],64))],2))}}),mD=Oge,$ge=q({name:"ElMenuItemGroup",__name:"menu-item-group",props:Dge,setup(e){const t=ke("menu-item-group");return(n,a)=>(x(),O("li",{class:U(p(t).b())},[C("div",{class:U(p(t).e("title"))},[n.$slots.title?ce(n.$slots,"title",{key:1}):(x(),O(Ve,{key:0},[ue(Q(e.title),1)],64))],2),C("ul",null,[ce(n.$slots,"default")])],2))}}),gD=$ge;const Rge=wt(Ige,{MenuItem:mD,MenuItemGroup:gD,SubMenu:Cw}),Nge=On(mD),Bge=On(gD),Vge=On(Cw),zge=$e({icon:{type:pn,default:()=>CI},title:String,content:{type:String,default:""}}),Fge={back:()=>!0},Hge=["aria-label"];var Wge=q({name:"ElPageHeader",__name:"page-header",props:zge,emits:Fge,setup(e,{emit:t}){const n=t,{t:a}=Vt(),r=ke("page-header");function o(){n("back")}return(i,l)=>(x(),O("div",{class:U([p(r).b(),p(r).is("contentful",!!i.$slots.default),{[p(r).m("has-breadcrumb")]:!!i.$slots.breadcrumb,[p(r).m("has-extra")]:!!i.$slots.extra}])},[i.$slots.breadcrumb?(x(),O("div",{key:0,class:U(p(r).e("breadcrumb"))},[ce(i.$slots,"breadcrumb")],2)):te("v-if",!0),C("div",{class:U(p(r).e("header"))},[C("div",{class:U(p(r).e("left"))},[C("div",{class:U(p(r).e("back")),role:"button",tabindex:"0",onClick:o},[e.icon||i.$slots.icon?(x(),O("div",{key:0,"aria-label":e.title||p(a)("el.pageHeader.title"),class:U(p(r).e("icon"))},[ce(i.$slots,"icon",{},()=>[e.icon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1})):te("v-if",!0)])],10,Hge)):te("v-if",!0),C("div",{class:U(p(r).e("title"))},[ce(i.$slots,"title",{},()=>[ue(Q(e.title||p(a)("el.pageHeader.title")),1)])],2)],2),L(p(nD),{direction:"vertical"}),C("div",{class:U(p(r).e("content"))},[ce(i.$slots,"content",{},()=>[ue(Q(e.content),1)])],2)],2),i.$slots.extra?(x(),O("div",{key:0,class:U(p(r).e("extra"))},[ce(i.$slots,"extra")],2)):te("v-if",!0)],2),i.$slots.default?(x(),O("div",{key:1,class:U(p(r).e("main"))},[ce(i.$slots,"default")],2)):te("v-if",!0)],2))}}),Kge=Wge;const Uge=wt(Kge),yD=Symbol("elPaginationKey"),Gge=$e({disabled:Boolean,currentPage:{type:Number,default:1},prevText:{type:String},prevIcon:{type:pn}}),Yge={click:e=>e instanceof MouseEvent},jge=["disabled","aria-label","aria-disabled"],qge={key:0};var Xge=q({name:"ElPaginationPrev",__name:"prev",props:Gge,emits:Yge,setup(e){const t=e,{t:n}=Vt(),a=D(()=>t.disabled||t.currentPage<=1);return(r,o)=>(x(),O("button",{type:"button",class:"btn-prev",disabled:a.value,"aria-label":r.prevText||p(n)("el.pagination.prev"),"aria-disabled":a.value,onClick:o[0]||(o[0]=i=>r.$emit("click",i))},[r.prevText?(x(),O("span",qge,Q(r.prevText),1)):(x(),ne(p(Xe),{key:1},{default:$(()=>[(x(),ne(St(r.prevIcon)))]),_:1}))],8,jge))}}),Zge=Xge;const Jge=$e({disabled:Boolean,currentPage:{type:Number,default:1},pageCount:{type:Number,default:50},nextText:{type:String},nextIcon:{type:pn}}),Qge=["disabled","aria-label","aria-disabled"],e1e={key:0};var t1e=q({name:"ElPaginationNext",__name:"next",props:Jge,emits:["click"],setup(e){const t=e,{t:n}=Vt(),a=D(()=>t.disabled||t.currentPage===t.pageCount||t.pageCount===0);return(r,o)=>(x(),O("button",{type:"button",class:"btn-next",disabled:a.value,"aria-label":r.nextText||p(n)("el.pagination.next"),"aria-disabled":a.value,onClick:o[0]||(o[0]=i=>r.$emit("click",i))},[r.nextText?(x(),O("span",e1e,Q(r.nextText),1)):(x(),ne(p(Xe),{key:1},{default:$(()=>[(x(),ne(St(r.nextIcon)))]),_:1}))],8,Qge))}}),n1e=t1e;const xw=()=>Ze(yD,{}),a1e=$e({pageSize:{type:Number,required:!0},pageSizes:{type:ae(Array),default:()=>Nn([10,20,30,40,50,100])},popperClass:{type:ae([String,Array,Object,Boolean]),default:void 0},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},disabled:Boolean,teleported:Boolean,size:{type:String,values:ii},appendSizeTo:String});var r1e=q({name:"ElPaginationSizes",__name:"sizes",props:a1e,emits:["page-size-change"],setup(e,{emit:t}){const n=e,a=t,{t:r}=Vt(),o=ke("pagination"),i=xw(),l=F(n.pageSize);ye(()=>n.pageSizes,(c,d)=>{Rn(c,d)||Ae(c)&&a("page-size-change",c.includes(n.pageSize)?n.pageSize:n.pageSizes[0])}),ye(()=>n.pageSize,c=>{l.value=c});const s=D(()=>n.pageSizes);function u(c){var d;c!==l.value&&(l.value=c,(d=i.handleSizeChange)==null||d.call(i,Number(c)))}return(c,d)=>(x(),O("span",{class:U(p(o).e("sizes"))},[L(p(Bs),{"model-value":l.value,disabled:c.disabled,"popper-class":c.popperClass,"popper-style":c.popperStyle,size:c.size,teleported:c.teleported,"validate-event":!1,"append-to":c.appendSizeTo,onChange:u},{default:$(()=>[(x(!0),O(Ve,null,dt(s.value,v=>(x(),ne(p(eg),{key:v,value:v,label:v+p(r)("el.pagination.pagesize")},null,8,["value","label"]))),128))]),_:1},8,["model-value","disabled","popper-class","popper-style","size","teleported","append-to"])],2))}}),o1e=r1e;const i1e=$e({size:{type:String,values:ii}}),l1e=["disabled"];var s1e=q({name:"ElPaginationJumper",__name:"jumper",props:i1e,setup(e){const{t}=Vt(),n=ke("pagination"),{pageCount:a,disabled:r,currentPage:o,changeEvent:i}=xw(),l=F(),s=D(()=>l.value??(o==null?void 0:o.value));function u(d){l.value=d?+d:""}function c(d){d=Math.trunc(+d),i==null||i(d),l.value=void 0}return(d,v)=>(x(),O("span",{class:U(p(n).e("jump")),disabled:p(r)},[C("span",{class:U([p(n).e("goto")])},Q(p(t)("el.pagination.goto")),3),L(p(Ba),{size:d.size,class:U([p(n).e("editor"),p(n).is("in-pagination")]),min:1,max:p(a),disabled:p(r),"model-value":s.value,"validate-event":!1,"aria-label":p(t)("el.pagination.page"),type:"number","onUpdate:modelValue":u,onChange:c},null,8,["size","class","max","disabled","model-value","aria-label"]),C("span",{class:U([p(n).e("classifier")])},Q(p(t)("el.pagination.pageClassifier")),3)],10,l1e))}}),u1e=s1e;const c1e=$e({total:{type:Number,default:1e3}}),d1e=["disabled"];var f1e=q({name:"ElPaginationTotal",__name:"total",props:c1e,setup(e){const{t}=Vt(),n=ke("pagination"),{disabled:a}=xw();return(r,o)=>(x(),O("span",{class:U(p(n).e("total")),disabled:p(a)},Q(p(t)("el.pagination.total",{total:r.total})),11,d1e))}}),v1e=f1e;const h1e=$e({currentPage:{type:Number,default:1},pageCount:{type:Number,required:!0},pagerCount:{type:Number,default:7},disabled:Boolean}),p1e=["aria-current","aria-label","tabindex"],m1e=["tabindex","aria-label"],g1e=["aria-current","aria-label","tabindex"],y1e=["tabindex","aria-label"],b1e=["aria-current","aria-label","tabindex"];var _1e=q({name:"ElPaginationPager",__name:"pager",props:h1e,emits:[Ot],setup(e,{emit:t}){const n=e,a=t,r=ke("pager"),o=ke("icon"),{t:i}=Vt(),l=F(!1),s=F(!1),u=F(!1),c=F(!1),d=F(!1),v=F(!1),f=D(()=>{const S=n.pagerCount,k=(S-1)/2,T=Number(n.currentPage),M=Number(n.pageCount);let E=!1,A=!1;M>S&&(T>S-k&&(E=!0),T["more","btn-quickprev",o.b(),r.is("disabled",n.disabled)]),m=D(()=>["more","btn-quicknext",o.b(),r.is("disabled",n.disabled)]),g=D(()=>n.disabled?-1:0);ye(()=>[n.pageCount,n.pagerCount,n.currentPage],([S,k,T])=>{const M=(k-1)/2;let E=!1,A=!1;S>k&&(E=T>k-M,A=TM&&(T=M)),T!==E&&a(Ot,T)}return(S,k)=>(x(),O("ul",{class:U(p(r).b()),onClick:w,onKeyup:kn(_,["enter"])},[S.pageCount>0?(x(),O("li",{key:0,class:U([[p(r).is("active",S.currentPage===1),p(r).is("disabled",S.disabled)],"number"]),"aria-current":S.currentPage===1,"aria-label":p(i)("el.pagination.currentPage",{pager:1}),tabindex:g.value}," 1 ",10,p1e)):te("v-if",!0),l.value?(x(),O("li",{key:1,class:U(h.value),tabindex:g.value,"aria-label":p(i)("el.pagination.prevPages",{pager:S.pagerCount-2}),onMouseenter:k[0]||(k[0]=T=>y(!0)),onMouseleave:k[1]||(k[1]=T=>u.value=!1),onFocus:k[2]||(k[2]=T=>b(!0)),onBlur:k[3]||(k[3]=T=>d.value=!1)},[(u.value||d.value)&&!S.disabled?(x(),ne(p(Al),{key:0})):(x(),ne(p(m4),{key:1}))],42,m1e)):te("v-if",!0),(x(!0),O(Ve,null,dt(f.value,T=>(x(),O("li",{key:T,class:U([[p(r).is("active",S.currentPage===T),p(r).is("disabled",S.disabled)],"number"]),"aria-current":S.currentPage===T,"aria-label":p(i)("el.pagination.currentPage",{pager:T}),tabindex:g.value},Q(T),11,g1e))),128)),s.value?(x(),O("li",{key:2,class:U(m.value),tabindex:g.value,"aria-label":p(i)("el.pagination.nextPages",{pager:S.pagerCount-2}),onMouseenter:k[4]||(k[4]=T=>y()),onMouseleave:k[5]||(k[5]=T=>c.value=!1),onFocus:k[6]||(k[6]=T=>b()),onBlur:k[7]||(k[7]=T=>v.value=!1)},[(c.value||v.value)&&!S.disabled?(x(),ne(p(Il),{key:0})):(x(),ne(p(m4),{key:1}))],42,y1e)):te("v-if",!0),S.pageCount>1?(x(),O("li",{key:3,class:U([[p(r).is("active",S.currentPage===S.pageCount),p(r).is("disabled",S.disabled)],"number"]),"aria-current":S.currentPage===S.pageCount,"aria-label":p(i)("el.pagination.currentPage",{pager:S.pageCount}),tabindex:g.value},Q(S.pageCount),11,b1e)):te("v-if",!0)],34))}}),w1e=_1e;const hr=e=>typeof e!="number",S1e=$e({pageSize:Number,defaultPageSize:Number,total:Number,pageCount:Number,pagerCount:{type:Number,validator:e=>Je(e)&&Math.trunc(e)===e&&e>4&&e<22&&e%2===1,default:7},currentPage:Number,defaultCurrentPage:Number,layout:{type:String,default:["prev","pager","next","jumper","->","total"].join(", ")},pageSizes:{type:ae(Array),default:()=>Nn([10,20,30,40,50,100])},popperClass:{type:ae([String,Array,Object,Boolean]),default:""},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},prevText:{type:String,default:""},prevIcon:{type:pn,default:()=>Ji},nextText:{type:String,default:""},nextIcon:{type:pn,default:()=>za},teleported:{type:Boolean,default:!0},small:Boolean,size:pa,background:Boolean,disabled:Boolean,hideOnSinglePage:Boolean,appendSizeTo:String}),C1e={"update:current-page":e=>Je(e),"update:page-size":e=>Je(e),"size-change":e=>Je(e),change:(e,t)=>Je(e)&&Je(t),"current-change":e=>Je(e),"prev-click":e=>Je(e),"next-click":e=>Je(e)},Ax="ElPagination";var x1e=q({name:Ax,props:S1e,emits:C1e,setup(e,{emit:t,slots:n}){const{t:a}=Vt(),r=ke("pagination"),o=Et().vnode.props||{},i=bI(),l=D(()=>e.small?"small":e.size??i.value);Ri({from:"small",replacement:"size",version:"3.0.0",scope:"el-pagination",ref:"https://element-plus.org/zh-CN/component/pagination.html"},D(()=>!!e.small));const s="onUpdate:currentPage"in o||"onUpdate:current-page"in o||"onCurrentChange"in o,u="onUpdate:pageSize"in o||"onUpdate:page-size"in o||"onSizeChange"in o,c=D(()=>{if(hr(e.total)&&hr(e.pageCount)||!hr(e.currentPage)&&!s)return!1;if(e.layout.includes("sizes")){if(hr(e.pageCount)){if(!hr(e.total)&&!hr(e.pageSize)&&!u)return!1}else if(!u)return!1}return!0}),d=F(hr(e.defaultPageSize)?10:e.defaultPageSize),v=F(hr(e.defaultCurrentPage)?1:e.defaultCurrentPage),f=D({get(){return hr(e.pageSize)?d.value:e.pageSize},set(S){hr(e.pageSize)&&(d.value=S),u&&(t("update:page-size",S),t("size-change",S))}}),h=D(()=>{let S=0;return hr(e.pageCount)?hr(e.total)||(S=Math.max(1,Math.ceil(e.total/f.value))):S=e.pageCount,S}),m=D({get(){return hr(e.currentPage)?v.value:e.currentPage},set(S){let k=S;S<1?k=1:S>h.value&&(k=h.value),hr(e.currentPage)&&(v.value=k),s&&(t("update:current-page",k),t("current-change",k))}});ye(h,S=>{m.value>S&&(m.value=S)}),ye([m,f],S=>{t(Ot,...S)},{flush:"post"});function g(S){m.value=S}function y(S){f.value=S;const k=h.value;m.value>k&&(m.value=k)}function b(){e.disabled||(m.value-=1,t("prev-click",m.value))}function _(){e.disabled||(m.value+=1,t("next-click",m.value))}function w(S,k){S&&(S.props||(S.props={}),S.props.class=[S.props.class,k].join(" "))}return Ft(yD,{pageCount:h,disabled:D(()=>e.disabled),currentPage:m,changeEvent:g,handleSizeChange:y}),()=>{var I;if(!c.value)return un(Ax,a("el.pagination.deprecationWarning")),null;if(!e.layout||e.hideOnSinglePage&&h.value<=1)return null;const S=[],k=[],T=ot("div",{class:r.e("rightwrapper")},k),M={prev:ot(Zge,{disabled:e.disabled,currentPage:m.value,prevText:e.prevText,prevIcon:e.prevIcon,onClick:b}),jumper:ot(u1e,{size:l.value}),pager:ot(w1e,{currentPage:m.value,pageCount:h.value,pagerCount:e.pagerCount,onChange:g,disabled:e.disabled}),next:ot(n1e,{disabled:e.disabled,currentPage:m.value,pageCount:h.value,nextText:e.nextText,nextIcon:e.nextIcon,onClick:_}),sizes:ot(o1e,{pageSize:f.value,pageSizes:e.pageSizes,popperClass:e.popperClass,popperStyle:e.popperStyle,disabled:e.disabled,teleported:e.teleported,size:l.value,appendSizeTo:e.appendSizeTo}),slot:((I=n==null?void 0:n.default)==null?void 0:I.call(n))??null,total:ot(v1e,{total:hr(e.total)?0:e.total})},E=e.layout.split(",").map(P=>P.trim());let A=!1;return E.forEach(P=>{if(P==="->"){A=!0;return}A?k.push(M[P]):S.push(M[P])}),w(S[0],r.is("first")),w(S[S.length-1],r.is("last")),A&&k.length>0&&(w(k[0],r.is("first")),w(k[k.length-1],r.is("last")),S.push(T)),ot("div",{class:[r.b(),r.is("background",e.background),r.m(l.value)]},S)}}});const k1e=wt(x1e),T1e=$e({title:String,confirmButtonText:String,cancelButtonText:String,confirmButtonType:{type:String,values:x4,default:"primary"},cancelButtonType:{type:String,values:x4,default:"text"},icon:{type:pn,default:()=>BI},iconColor:{type:String,default:"#f90"},hideIcon:Boolean,hideAfter:{type:Number,default:200},effect:{...Tn.effect,default:"light"},teleported:Tn.teleported,persistent:Tn.persistent,width:{type:[String,Number],default:150},virtualTriggering:qu.virtualTriggering,virtualRef:qu.virtualRef}),M1e={confirm:e=>e instanceof MouseEvent,cancel:e=>e instanceof MouseEvent};var E1e=q({name:"ElPopconfirm",__name:"popconfirm",props:T1e,emits:M1e,setup(e,{expose:t,emit:n}){const a=e,r=n,{t:o}=Vt(),i=ke("popconfirm"),l=F(),s=F(),u=D(()=>{var y;return(y=p(l))==null?void 0:y.popperRef}),c=()=>{var y,b;(b=(y=s.value)==null?void 0:y.focus)==null||b.call(y)},d=()=>{var y,b;(b=(y=l.value)==null?void 0:y.onClose)==null||b.call(y)},v=D(()=>({width:Vn(a.width)})),f=y=>{r("confirm",y),d()},h=y=>{r("cancel",y),d()},m=D(()=>a.confirmButtonText||o("el.popconfirm.confirmButtonText")),g=D(()=>a.cancelButtonText||o("el.popconfirm.cancelButtonText"));return t({popperRef:u,hide:d}),(y,b)=>(x(),ne(p(Ma),Tt({ref_key:"tooltipRef",ref:l,trigger:"click",effect:e.effect},y.$attrs,{"virtual-triggering":e.virtualTriggering,"virtual-ref":e.virtualRef,"popper-class":`${p(i).namespace.value}-popover`,"popper-style":v.value,teleported:e.teleported,"fallback-placements":["bottom","top","right","left"],"hide-after":e.hideAfter,persistent:e.persistent,loop:"",onShow:c}),{content:$(()=>[C("div",{ref_key:"rootRef",ref:s,tabindex:"-1",class:U(p(i).b())},[C("div",{class:U(p(i).e("main"))},[!e.hideIcon&&e.icon?(x(),ne(p(Xe),{key:0,class:U(p(i).e("icon")),style:et({color:e.iconColor})},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1},8,["class","style"])):te("v-if",!0),ue(" "+Q(e.title),1)],2),C("div",{class:U(p(i).e("action"))},[ce(y.$slots,"actions",{confirm:f,cancel:h},()=>[L(p(xa),{size:"small",type:e.cancelButtonType==="text"?"":e.cancelButtonType,text:e.cancelButtonType==="text",onClick:h},{default:$(()=>[ue(Q(g.value),1)]),_:1},8,["type","text"]),L(p(xa),{size:"small",type:e.confirmButtonType==="text"?"":e.confirmButtonType,text:e.confirmButtonType==="text",onClick:f},{default:$(()=>[ue(Q(m.value),1)]),_:1},8,["type","text"])])],2)],2)]),default:$(()=>[y.$slots.reference?ce(y.$slots,"reference",{key:0}):te("v-if",!0)]),_:3},16,["effect","virtual-triggering","virtual-ref","popper-class","popper-style","teleported","hide-after","persistent"]))}}),A1e=E1e;const I1e=wt(A1e),L1e=$e({type:{type:String,default:"line",values:["line","circle","dashboard"]},percentage:{type:Number,default:0,validator:e=>e>=0&&e<=100},status:{type:String,default:"",values:["","success","exception","warning"]},indeterminate:Boolean,duration:{type:Number,default:3},strokeWidth:{type:Number,default:6},strokeLinecap:{type:ae(String),default:"round"},textInside:Boolean,width:{type:Number,default:126},showText:{type:Boolean,default:!0},color:{type:ae([String,Array,Function]),default:""},striped:Boolean,stripedFlow:Boolean,format:{type:ae(Function),default:e=>`${e}%`}}),P1e=["aria-valuenow"],D1e={viewBox:"0 0 100 100"},O1e=["d","stroke","stroke-linecap","stroke-width"],$1e=["d","stroke","opacity","stroke-linecap","stroke-width"],R1e={key:0};var N1e=q({name:"ElProgress",__name:"progress",props:L1e,setup(e){const t={success:"#13ce66",exception:"#ff4949",warning:"#e6a23c",default:"#20a0ff"},n=e,a=ke("progress"),r=D(()=>{const _={width:`${n.percentage}%`,animationDuration:`${n.duration}s`},w=b(n.percentage);return w.includes("gradient")?_.background=w:_.backgroundColor=w,_}),o=D(()=>(n.strokeWidth/n.width*100).toFixed(1)),i=D(()=>["circle","dashboard"].includes(n.type)?Number.parseInt(`${50-Number.parseFloat(o.value)/2}`,10):0),l=D(()=>{const _=i.value,w=n.type==="dashboard";return` + M 50 50 + m 0 ${w?"":"-"}${_} + a ${_} ${_} 0 1 1 0 ${w?"-":""}${_*2} + a ${_} ${_} 0 1 1 0 ${w?"":"-"}${_*2} + `}),s=D(()=>2*Math.PI*i.value),u=D(()=>n.type==="dashboard"?.75:1),c=D(()=>`${-1*s.value*(1-u.value)/2}px`),d=D(()=>({strokeDasharray:`${s.value*u.value}px, ${s.value}px`,strokeDashoffset:c.value})),v=D(()=>({strokeDasharray:`${s.value*u.value*(n.percentage/100)}px, ${s.value}px`,strokeDashoffset:c.value,transition:"stroke-dasharray 0.6s ease 0s, stroke 0.6s ease, opacity ease 0.6s"})),f=D(()=>{let _;return n.color?_=b(n.percentage):_=t[n.status]||t.default,_}),h=D(()=>n.status==="warning"?$s:n.type==="line"?n.status==="success"?Ao:ci:n.status==="success"?Os:so),m=D(()=>n.type==="line"?12+n.strokeWidth*.4:n.width*.111111+2),g=D(()=>n.format(n.percentage));function y(_){const w=100/_.length;return _.map((S,k)=>Ye(S)?{color:S,percentage:(k+1)*w}:S).sort((S,k)=>S.percentage-k.percentage)}const b=_=>{var S;const{color:w}=n;if(Qe(w))return w(_);if(Ye(w))return w;{const k=y(w);for(const T of k)if(T.percentage>_)return T.color;return(S=k[k.length-1])==null?void 0:S.color}};return(_,w)=>(x(),O("div",{class:U([p(a).b(),p(a).m(e.type),p(a).is(e.status),{[p(a).m("without-text")]:!e.showText,[p(a).m("text-inside")]:e.textInside}]),role:"progressbar","aria-valuenow":e.percentage,"aria-valuemin":"0","aria-valuemax":"100"},[e.type==="line"?(x(),O("div",{key:0,class:U(p(a).b("bar"))},[C("div",{class:U(p(a).be("bar","outer")),style:et({height:`${e.strokeWidth}px`})},[C("div",{class:U([p(a).be("bar","inner"),{[p(a).bem("bar","inner","indeterminate")]:e.indeterminate},{[p(a).bem("bar","inner","striped")]:e.striped},{[p(a).bem("bar","inner","striped-flow")]:e.stripedFlow}]),style:et(r.value)},[(e.showText||_.$slots.default)&&e.textInside?(x(),O("div",{key:0,class:U(p(a).be("bar","innerText"))},[ce(_.$slots,"default",{percentage:e.percentage},()=>[C("span",null,Q(g.value),1)])],2)):te("v-if",!0)],6)],6)],2)):(x(),O("div",{key:1,class:U(p(a).b("circle")),style:et({height:`${e.width}px`,width:`${e.width}px`})},[(x(),O("svg",D1e,[C("path",{class:U(p(a).be("circle","track")),d:l.value,stroke:`var(${p(a).cssVarName("fill-color-light")}, #e5e9f2)`,"stroke-linecap":e.strokeLinecap,"stroke-width":o.value,fill:"none",style:et(d.value)},null,14,O1e),C("path",{class:U(p(a).be("circle","path")),d:l.value,stroke:f.value,fill:"none",opacity:e.percentage?1:0,"stroke-linecap":e.strokeLinecap,"stroke-width":o.value,style:et(v.value)},null,14,$1e)]))],6)),(e.showText||_.$slots.default)&&!e.textInside?(x(),O("div",{key:2,class:U(p(a).e("text")),style:et({fontSize:`${m.value}px`})},[ce(_.$slots,"default",{percentage:e.percentage},()=>[e.status?(x(),ne(p(Xe),{key:1},{default:$(()=>[(x(),ne(St(h.value)))]),_:1})):(x(),O("span",R1e,Q(g.value),1))])],6)):te("v-if",!0)],10,P1e))}}),B1e=N1e;const kw=wt(B1e),V1e=$e({modelValue:{type:Number,default:0},id:{type:String,default:void 0},lowThreshold:{type:Number,default:2},highThreshold:{type:Number,default:4},max:{type:Number,default:5},colors:{type:ae([Array,Object]),default:()=>Nn(["","",""])},voidColor:{type:String,default:""},disabledVoidColor:{type:String,default:""},icons:{type:ae([Array,Object]),default:()=>[rv,rv,rv]},voidIcon:{type:pn,default:()=>UI},disabledVoidIcon:{type:pn,default:()=>rv},disabled:{type:Boolean,default:void 0},allowHalf:Boolean,showText:Boolean,showScore:Boolean,textColor:{type:String,default:""},texts:{type:ae(Array),default:()=>Nn(["Extremely bad","Disappointed","Fair","Satisfied","Surprise"])},scoreTemplate:{type:String,default:"{value}"},size:pa,clearable:Boolean,...dr(["ariaLabel"])}),z1e={[Ot]:e=>Je(e),[bt]:e=>Je(e)},F1e=["id","aria-label","aria-labelledby","aria-valuenow","aria-valuetext","aria-valuemax","tabindex","aria-disabled"],H1e=["onMousemove","onClick"];var W1e=q({name:"ElRate",__name:"rate",props:V1e,emits:z1e,setup(e,{expose:t,emit:n}){function a(B,W){const N=Y=>_t(Y),V=W[Object.keys(W).map(Y=>+Y).filter(Y=>{const X=W[Y];return N(X)&&X.excluded?BY-X)[0]];return N(V)&&V.value||V}const r=e,o=n,i=Ze(Qi,void 0),l=ca(),s=ke("rate"),{inputId:u,isLabeledByFormItem:c}=Yr(r,{formItemContext:i}),d=F(Br(r.modelValue,0,r.max)),v=F(-1),f=F(!0),h=F([]),m=D(()=>h.value.map(B=>B.$el.clientWidth)),g=D(()=>[s.b(),s.m(l.value)]),y=$n(),b=D(()=>s.cssVarBlock({"void-color":r.voidColor,"disabled-void-color":r.disabledVoidColor,"fill-color":k.value})),_=D(()=>{let B="";return r.showScore?B=r.scoreTemplate.replace(/\{\s*value\s*\}/,y.value?`${r.modelValue}`:`${d.value}`):r.showText&&(B=r.texts[Math.ceil(d.value)-1]),B}),w=D(()=>r.modelValue*100-Math.floor(r.modelValue)*100),S=D(()=>Ae(r.colors)?{[r.lowThreshold]:r.colors[0],[r.highThreshold]:{value:r.colors[1],excluded:!0},[r.max]:r.colors[2]}:r.colors),k=D(()=>{const B=a(d.value,S.value);return _t(B)?"":B}),T=D(()=>{let B="";return y.value?B=`${w.value}%`:r.allowHalf&&(B="50%"),{color:k.value,width:B}}),M=D(()=>{let B=Ae(r.icons)?[...r.icons]:{...r.icons};return B=$i(B),Ae(B)?{[r.lowThreshold]:B[0],[r.highThreshold]:{value:B[1],excluded:!0},[r.max]:B[2]}:B}),E=D(()=>a(r.modelValue,M.value)),A=D(()=>y.value?Ye(r.disabledVoidIcon)?r.disabledVoidIcon:$i(r.disabledVoidIcon):Ye(r.voidIcon)?r.voidIcon:$i(r.voidIcon)),I=D(()=>a(d.value,M.value));function P(B){const W=y.value&&w.value>0&&B-1r.modelValue,N=r.allowHalf&&f.value&&B-.5<=d.value&&B>d.value;return W||N}function R(B){r.clearable&&B===r.modelValue&&(B=0),o(bt,B),r.modelValue!==B&&o(Ot,B)}function G(B){y.value||(r.allowHalf&&f.value?R(d.value):R(B))}function K(B){if(y.value)return;const W=mn(B),N=r.allowHalf?.5:1;let V=d.value;switch(W){case Pe.up:case Pe.right:V+=N;break;case Pe.left:case Pe.down:V-=N;break}if(V=Br(V,0,r.max),V!==d.value)return B.stopPropagation(),B.preventDefault(),o(bt,V),o(Ot,V),V}function H(B,W){y.value||(r.allowHalf&&W?(f.value=W.offsetX*2<=m.value[B-1],d.value=f.value?B-.5:B):d.value=B,v.value=B)}function z(){y.value||(r.allowHalf&&(f.value=r.modelValue!==Math.floor(r.modelValue)),d.value=Br(r.modelValue,0,r.max),v.value=-1)}return ye(()=>r.modelValue,B=>{d.value=Br(B,0,r.max),f.value=r.modelValue!==Math.floor(r.modelValue)}),r.modelValue||o(bt,0),t({setCurrentValue:H,resetCurrentValue:z}),(B,W)=>{var N;return x(),O("div",{id:p(u),class:U([g.value,p(s).is("disabled",p(y))]),role:"slider","aria-label":p(c)?void 0:e.ariaLabel||"rating","aria-labelledby":p(c)?(N=p(i))==null?void 0:N.labelId:void 0,"aria-valuenow":d.value,"aria-valuetext":_.value||void 0,"aria-valuemin":"0","aria-valuemax":e.max,style:et(b.value),tabindex:p(y)?void 0:0,"aria-disabled":p(y),onKeydown:K},[(x(!0),O(Ve,null,dt(e.max,(V,Y)=>(x(),O("span",{key:Y,class:U(p(s).e("item")),onMousemove:X=>H(V,X),onMouseleave:z,onClick:X=>G(V)},[L(p(Xe),{ref_for:!0,ref_key:"iconRefs",ref:h,class:U([p(s).e("icon"),{hover:v.value===V},p(s).is("active",V<=d.value),p(s).is("focus-visible",V===Math.ceil(d.value||1))])},{default:$(()=>[st((x(),ne(St(I.value),null,null,512)),[[Zt,!P(V)&&V<=d.value]]),st((x(),ne(St(A.value),null,null,512)),[[Zt,!P(V)&&V>d.value]]),st((x(),ne(St(A.value),{class:U([p(s).em("decimal","box")])},null,8,["class"])),[[Zt,P(V)]]),st(L(p(Xe),{style:et(T.value),class:U([p(s).e("icon"),p(s).e("decimal")])},{default:$(()=>[(x(),ne(St(E.value)))]),_:1},8,["style","class"]),[[Zt,P(V)]])]),_:2},1032,["class"])],42,H1e))),128)),e.showText||e.showScore?(x(),O("span",{key:0,class:U(p(s).e("text")),style:et({color:e.textColor})},Q(_.value),7)):te("v-if",!0)],46,F1e)}}}),K1e=W1e;const U1e=wt(K1e),Pu={primary:"icon-primary",success:"icon-success",warning:"icon-warning",error:"icon-error",info:"icon-info"},Ix={[Pu.primary]:Ud,[Pu.success]:MI,[Pu.warning]:$s,[Pu.error]:fc,[Pu.info]:Ud},G1e=$e({title:{type:String,default:""},subTitle:{type:String,default:""},icon:{type:String,values:["primary","success","warning","info","error"],default:"info"}});var Y1e=q({name:"ElResult",__name:"result",props:G1e,setup(e){const t=e,n=ke("result"),a=D(()=>{const r=t.icon,o=r&&Pu[r]?Pu[r]:"icon-info";return{class:o,component:Ix[o]||Ix["icon-info"]}});return(r,o)=>(x(),O("div",{class:U(p(n).b())},[C("div",{class:U(p(n).e("icon"))},[ce(r.$slots,"icon",{},()=>[a.value.component?(x(),ne(St(a.value.component),{key:0,class:U(a.value.class)},null,8,["class"])):te("v-if",!0)])],2),e.title||r.$slots.title?(x(),O("div",{key:0,class:U(p(n).e("title"))},[ce(r.$slots,"title",{},()=>[C("p",null,Q(e.title),1)])],2)):te("v-if",!0),e.subTitle||r.$slots["sub-title"]?(x(),O("div",{key:1,class:U(p(n).e("subtitle"))},[ce(r.$slots,"sub-title",{},()=>[C("p",null,Q(e.subTitle),1)])],2)):te("v-if",!0),r.$slots.extra?(x(),O("div",{key:2,class:U(p(n).e("extra"))},[ce(r.$slots,"extra")],2)):te("v-if",!0)],2))}}),j1e=Y1e;const q1e=wt(j1e),Tw=Symbol("ElSelectV2Injection"),X1e=$e({allowCreate:Boolean,autocomplete:{type:ae(String),default:"none"},automaticDropdown:Boolean,clearable:Boolean,clearIcon:{type:pn,default:ci},effect:{type:ae(String),default:"light"},collapseTags:Boolean,collapseTagsTooltip:Boolean,tagTooltip:{type:ae(Object),default:()=>({})},maxCollapseTags:{type:Number,default:1},defaultFirstOption:Boolean,disabled:{type:Boolean,default:void 0},estimatedOptionHeight:{type:Number,default:void 0},filterable:Boolean,filterMethod:{type:ae(Function)},height:{type:Number,default:274},itemHeight:{type:Number,default:34},id:String,loading:Boolean,loadingText:String,modelValue:{type:ae([Array,String,Number,Boolean,Object]),default:void 0},multiple:Boolean,multipleLimit:{type:Number,default:0},name:String,noDataText:String,noMatchText:String,remoteMethod:{type:ae(Function)},reserveKeyword:{type:Boolean,default:!0},options:{type:ae(Array),required:!0},placeholder:{type:String},teleported:Tn.teleported,persistent:{type:Boolean,default:!0},popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,popperOptions:{type:ae(Object),default:()=>({})},remote:Boolean,debounce:{type:Number,default:300},size:pa,props:{type:ae(Object),default:()=>Qm},valueKey:{type:String,default:"value"},scrollbarAlwaysOn:Boolean,validateEvent:{type:Boolean,default:!0},offset:{type:Number,default:12},remoteShowSuffix:Boolean,showArrow:{type:Boolean,default:!0},placement:{type:ae(String),values:tl,default:"bottom-start"},fallbackPlacements:{type:ae(Array),default:["bottom-start","top-start","right","left"]},tagType:{...Ll.type,default:"info"},tagEffect:{...Ll.effect,default:"light"},tabindex:{type:[String,Number],default:0},appendTo:Tn.appendTo,fitInputWidth:{type:[Boolean,Number],default:!0,validator(e){return vn(e)||Je(e)}},suffixIcon:{type:pn,default:ui},...Mc,...dr(["ariaLabel"])}),Z1e=$e({data:Array,disabled:Boolean,hovering:Boolean,item:{type:ae(Object),required:!0},index:Number,style:Object,selected:Boolean,created:Boolean}),J1e={[bt]:e=>!0,[Ot]:e=>!0,"end-reached":hh["end-reached"],"remove-tag":e=>!0,"visible-change":e=>!0,focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0},Q1e={hover:e=>Je(e),select:(e,t)=>!0};var e2e=q({props:{item:{type:Object,required:!0},style:{type:Object},height:Number},setup(){return{ns:ke("select")}}});function t2e(e,t,n,a,r,o){return x(),O("div",{class:U(e.ns.be("group","title")),style:et({...e.style,lineHeight:`${e.height}px`})},Q(e.item.label),7)}var n2e=ya(e2e,[["render",t2e]]);function a2e(e,{emit:t}){return{hoverItem:()=>{e.disabled||t("hover",e.index)},selectOptionClick:()=>{e.disabled||t("select",e.item,e.index)}}}var r2e=q({props:Z1e,emits:Q1e,setup(e,{emit:t}){const n=Ze(Tw),a=ke("select"),r=Nd?null:"mousemove",{hoverItem:o,selectOptionClick:i}=a2e(e,{emit:t}),{getLabel:l}=up(n.props),s=n.contentId;return{ns:a,contentId:s,mouseMoveEventName:r,hoverItem:o,handleMousedown:c=>{let d=c.target;const v=c.currentTarget;for(;d&&d!==v;){if(sc(d))return;d=d.parentElement}c.preventDefault()},selectOptionClick:i,getLabel:l}}});const o2e=["id","aria-selected","aria-disabled"];function i2e(e,t,n,a,r,o){return x(),O("li",Tt({id:`${e.contentId}-${e.index}`,role:"option","aria-selected":e.selected,"aria-disabled":e.disabled||void 0,style:e.style,class:[e.ns.be("dropdown","item"),e.ns.is("selected",e.selected),e.ns.is("disabled",e.disabled),e.ns.is("created",e.created),e.ns.is("hovering",e.hovering)]},{[Wu(e.mouseMoveEventName)]:t[0]||(t[0]=(...i)=>e.hoverItem&&e.hoverItem(...i))},{onMousedown:t[1]||(t[1]=(...i)=>e.handleMousedown&&e.handleMousedown(...i)),onClick:t[2]||(t[2]=at((...i)=>e.selectOptionClick&&e.selectOptionClick(...i),["stop"]))}),[ce(e.$slots,"default",{item:e.item,index:e.index,disabled:e.disabled},()=>[C("span",null,Q(e.getLabel(e.item)),1)])],16,o2e)}var l2e=ya(r2e,[["render",i2e]]),s2e=q({name:"ElSelectDropdown",props:{loading:Boolean,data:{type:Array,required:!0},hoveringIndex:Number,width:Number,id:String,ariaLabel:String},emits:{"end-reached":hh["end-reached"]},setup(e,{slots:t,expose:n,emit:a}){const r=Ze(Tw),o=ke("select"),{getLabel:i,getValue:l,getDisabled:s}=up(r.props),u=F([]),c=F(),d=D(()=>e.data.length);ye(()=>d.value,()=>{var P,R;(R=(P=r.tooltipRef.value)==null?void 0:P.updatePopper)==null||R.call(P)});const v=D(()=>zt(r.props.estimatedOptionHeight)),f=D(()=>v.value?{itemSize:r.props.itemHeight}:{estimatedSize:r.props.estimatedOptionHeight,itemSize:P=>u.value[P]}),h=(P=[],R)=>{const{props:{valueKey:G}}=r;return _t(R)?P&&P.some(K=>wn(na(K,G))===na(R,G)):P.includes(R)},m=(P,R)=>{if(_t(R)){const{valueKey:G}=r.props;return na(P,G)===na(R,G)}else return P===R},g=(P,R)=>r.props.multiple?h(P,l(R)):m(P,l(R)),y=(P,R)=>{const{disabled:G,multiple:K,multipleLimit:H}=r.props;return G||!R&&(K?H>0&&P.length>=H:!1)},b=P=>e.hoveringIndex===P;n({listRef:c,isSized:v,isItemDisabled:y,isItemHovering:b,isItemSelected:g,scrollToItem:P=>{const R=c.value;R&&R.scrollToItem(P)},resetScrollTop:()=>{const P=c.value;P&&P.resetScrollTop()}});const S=P=>{const{index:R,data:G,style:K}=P,H=p(v),{itemSize:z,estimatedSize:B}=p(f),{modelValue:W}=r.props,{onSelect:N,onHover:V}=r,Y=G[R];if(Y.type==="Group")return L(n2e,{item:Y,style:K,height:H?z:B},null);const X=g(W,Y),ee=y(W,X),re=b(R);return L(l2e,Tt(P,{selected:X,disabled:s(Y)||ee,created:!!Y.created,hovering:re,item:Y,onSelect:N,onHover:V}),{default:ie=>{var le;return((le=t.default)==null?void 0:le.call(t,ie))||L("span",null,[i(Y)])}})},{onKeyboardNavigate:k,onKeyboardSelect:T}=r,M=()=>{k("forward")},E=()=>{k("backward")},A=P=>{const R=mn(P),{tab:G,esc:K,down:H,up:z,enter:B,numpadEnter:W}=Pe;switch([K,H,z,B,W].includes(R)&&(P.preventDefault(),P.stopPropagation()),R){case G:case K:break;case H:M();break;case z:E();break;case B:case W:T();break}},I=P=>{a("end-reached",P)};return()=>{var W,N,V,Y;const{data:P,width:R}=e,{height:G,multiple:K,scrollbarAlwaysOn:H}=r.props,z=Nd?!0:H,B=p(v)?cp:Ede;return L("div",{class:[o.b("dropdown"),o.is("multiple",K)],style:{width:`${R}px`}},[(W=t.header)==null?void 0:W.call(t),((N=t.loading)==null?void 0:N.call(t))||((V=t.empty)==null?void 0:V.call(t))||L(B,Tt({ref:c},p(f),{className:o.be("dropdown","list"),scrollbarAlwaysOn:z,data:P,height:G,width:R,total:P.length,innerElement:"ul",innerProps:{id:e.id,role:"listbox","aria-label":e.ariaLabel,"aria-orientation":"vertical"},onEndReached:I,onKeydown:A}),{default:X=>L(S,X,null)}),(Y=t.footer)==null?void 0:Y.call(t)])}}});function u2e(e,t){const{aliasProps:n,getLabel:a,getValue:r}=up(e),o=F(0),i=F(),l=D(()=>e.allowCreate&&e.filterable);ye(()=>e.options,f=>{const h=new Set(f.map(m=>a(m)));t.createdOptions=t.createdOptions.filter(m=>!h.has(a(m)))});function s(f){const h=m=>a(m)===f;return e.options&&e.options.some(h)||t.createdOptions.some(h)}function u(f){l.value&&(e.multiple&&f.created?o.value++:i.value=f)}function c(f){if(l.value)if(f&&f.length>0){if(s(f)){t.createdOptions=t.createdOptions.filter(m=>a(m)!==t.previousQuery);return}const h={[n.value.value]:f,[n.value.label]:f,created:!0,[n.value.disabled]:!1};t.createdOptions.length>=o.value?t.createdOptions[o.value]=h:t.createdOptions.push(h)}else if(e.multiple)t.createdOptions.length=o.value;else{const h=i.value;t.createdOptions.length=0,h&&h.created&&t.createdOptions.push(h)}}function d(f){if(!l.value||!f||!f.created||f.created&&e.reserveKeyword&&t.inputValue===a(f))return;const h=t.createdOptions.findIndex(m=>r(m)===r(f));~h&&(t.createdOptions.splice(h,1),o.value--)}function v(){l.value&&(t.createdOptions.length=0,o.value=0)}return{createNewOption:c,removeNewOption:d,selectNewOption:u,clearAllNewOption:v}}const c2e=(e,t)=>{const{t:n}=Vt(),a=Xn(),r=ke("select"),o=ke("input"),{form:i,formItem:l}=Ea(),{inputId:s}=Yr(e,{formItemContext:l}),{aliasProps:u,getLabel:c,getValue:d,getDisabled:v,getOptions:f}=up(e),{valueOnClear:h,isEmptyValue:m}=op(e),g=nn({inputValue:"",cachedOptions:[],createdOptions:[],hoveringIndex:-1,inputHovering:!1,selectionWidth:0,collapseItemWidth:0,previousQuery:null,previousValue:void 0,selectedLabel:"",menuVisibleOnFocus:!1,isBeforeHide:!1}),y=F(-1),b=F(!1),_=F(),w=F(),S=F(),k=F(),T=F(),M=F(),E=F(),A=F(),I=F(),P=F(),{isComposing:R,handleCompositionStart:G,handleCompositionEnd:K,handleCompositionUpdate:H}=rp({afterComposition:pe=>_n(pe)}),z=$n(),{wrapperRef:B,isFocused:W,handleBlur:N}=Fl(T,{disabled:z,afterFocus(){e.automaticDropdown&&!ee.value&&(ee.value=!0,g.menuVisibleOnFocus=!0)},beforeBlur(pe){var ct,Nt;return((ct=S.value)==null?void 0:ct.isFocusInsideContent(pe))||((Nt=k.value)==null?void 0:Nt.isFocusInsideContent(pe))},afterBlur(){var pe;ee.value=!1,g.menuVisibleOnFocus=!1,e.validateEvent&&((pe=l==null?void 0:l.validate)==null||pe.call(l,"blur").catch(kt))}}),V=D(()=>De("")),Y=D(()=>e.loading?!1:e.options.length>0||g.createdOptions.length>0),X=F([]),ee=F(!1),re=D(()=>(i==null?void 0:i.statusIcon)??!1),ie=D(()=>{const pe=X.value.length*e.itemHeight;return pe>e.height?e.height:pe}),le=D(()=>e.multiple?Ae(e.modelValue)&&e.modelValue.length>0:!m(e.modelValue)),ve=D(()=>e.clearable&&!z.value&&le.value&&(W.value||g.inputHovering)),he=D(()=>e.remote&&e.filterable&&!e.remoteShowSuffix?"":e.suffixIcon),J=D(()=>he.value&&r.is("reverse",ee.value)),Z=D(()=>(l==null?void 0:l.validateState)||""),j=D(()=>{if(Z.value)return F1[Z.value]}),oe=D(()=>e.remote?e.debounce:0),se=D(()=>e.remote&&!g.inputValue&&!Y.value),fe=D(()=>e.loading?e.loadingText||n("el.select.loading"):e.filterable&&g.inputValue&&Y.value&&X.value.length===0?e.noMatchText||n("el.select.noMatch"):Y.value?null:e.noDataText||n("el.select.noData")),ge=D(()=>e.filterable&&Qe(e.filterMethod)),Se=D(()=>e.filterable&&e.remote&&Qe(e.remoteMethod)),De=pe=>{const ct=new RegExp(S6(pe),"i"),Nt=Kt=>ge.value||Se.value?!0:pe?ct.test(c(Kt)||""):!0;return e.loading?[]:[...g.createdOptions,...e.options].reduce((Kt,$a)=>{const Ir=f($a);if(Ae(Ir)){const kf=Ir.filter(Nt);kf.length>0&&Kt.push({label:c($a),type:"Group"},...kf)}else(e.remote||Nt($a))&&Kt.push($a);return Kt},[])},Te=()=>{X.value=De(g.inputValue)},Re=D(()=>{const pe=new Map;return V.value.forEach((ct,Nt)=>{pe.set(qe(d(ct)),{option:ct,index:Nt})}),pe}),Ce=D(()=>{const pe=new Map;return X.value.forEach((ct,Nt)=>{pe.set(qe(d(ct)),{option:ct,index:Nt})}),pe}),Le=D(()=>X.value.every(pe=>v(pe))),me=ca(),Be=D(()=>me.value==="small"?"small":"default"),ze=()=>{var ct;if(Je(e.fitInputWidth)){y.value=e.fitInputWidth;return}const pe=((ct=_.value)==null?void 0:ct.offsetWidth)||200;!e.fitInputWidth&&Y.value?He(()=>{y.value=Math.max(pe,xe())}):y.value=pe},xe=()=>{var Ir,kf;const pe=document.createElement("canvas").getContext("2d"),ct=r.be("dropdown","item"),Nt=(((kf=(Ir=A.value)==null?void 0:Ir.listRef)==null?void 0:kf.innerRef)||document).querySelector(`.${ct}`);if(Nt===null||pe===null)return 0;const Kt=getComputedStyle(Nt),$a=Number.parseFloat(Kt.paddingLeft)+Number.parseFloat(Kt.paddingRight);return pe.font=`bold ${Kt.font.replace(new RegExp(`\\b${Kt.fontWeight}\\b`),"")}`,X.value.reduce((iV,lV)=>{const sV=pe.measureText(c(lV));return Math.max(sV.width,iV)},0)+$a},rt=()=>{if(!w.value)return 0;const pe=window.getComputedStyle(w.value);return Number.parseFloat(pe.gap||"6px")},nt=D(()=>{const pe=rt(),ct=e.filterable?pe+11:0;return{maxWidth:`${P.value&&e.maxCollapseTags===1?g.selectionWidth-g.collapseItemWidth-pe-ct:g.selectionWidth-ct}px`}}),Pt=D(()=>({maxWidth:`${g.selectionWidth}px`})),Wt=D(()=>Ae(e.modelValue)?e.modelValue.length===0&&!g.inputValue:e.filterable?!g.inputValue:!0),Jt=D(()=>{const pe=e.placeholder??n("el.select.placeholder");return e.multiple||!le.value?pe:g.selectedLabel}),Rt=D(()=>{var pe,ct;return(ct=(pe=S.value)==null?void 0:pe.popperRef)==null?void 0:ct.contentRef}),Qt=D(()=>{if(e.multiple){const pe=e.modelValue.length;if(pe>0&&Ce.value.has(e.modelValue[pe-1])){const{index:ct}=Ce.value.get(e.modelValue[pe-1]);return ct}}else if(!m(e.modelValue)&&Ce.value.has(e.modelValue)){const{index:pe}=Ce.value.get(e.modelValue);return pe}return-1}),_e=D({get(){return ee.value&&(e.loading||!se.value||e.remote&&!!a.empty)&&(!b.value||!_r(g.previousQuery)||Y.value)},set(pe){ee.value=pe}}),Ne=D(()=>e.multiple?e.collapseTags?g.cachedOptions.slice(0,e.maxCollapseTags):g.cachedOptions:[]),tt=D(()=>e.multiple?e.collapseTags?g.cachedOptions.slice(e.maxCollapseTags):[]:[]),{createNewOption:Me,removeNewOption:Ue,selectNewOption:Gt,clearAllNewOption:je}=u2e(e,g),ft=pe=>{var ct;z.value||e.filterable&&ee.value&&pe&&!((ct=E.value)!=null&&ct.contains(pe.target))||(g.menuVisibleOnFocus?g.menuVisibleOnFocus=!1:ee.value=!ee.value)},ht=()=>{g.inputValue.length>0&&!ee.value&&(ee.value=!0),Me(g.inputValue),He(()=>{It(g.inputValue)})},pt=Xh(()=>{ht(),b.value=!1},oe),It=pe=>{g.previousQuery===pe||R.value||(g.previousQuery=pe,e.filterable&&Qe(e.filterMethod)?e.filterMethod(pe):e.filterable&&e.remote&&Qe(e.remoteMethod)&&e.remoteMethod(pe),e.defaultFirstOption&&(e.filterable||e.remote)&&X.value.length?He(Ct):He(Bt))},Ct=()=>{const pe=X.value.filter(Kt=>!Kt.disabled&&Kt.type!=="Group"),ct=pe.find(Kt=>Kt.created),Nt=pe[0];g.hoveringIndex=Fe(X.value,ct||Nt)},$t=pe=>{Rn(e.modelValue,pe)||t(Ot,pe)},Sn=pe=>{t(bt,pe),$t(pe),g.previousValue=e.multiple?String(pe):pe,He(()=>{if(e.multiple&&Ae(e.modelValue)){const ct=g.cachedOptions.slice(),Nt=e.modelValue.map(Kt=>Gl(Kt,ct));Rn(g.cachedOptions,Nt)||(g.cachedOptions=Nt)}else Yl(!0)})},Fe=(pe=[],ct)=>{if(!_t(ct))return pe.indexOf(ct);const Nt=e.valueKey;let Kt=-1;return pe.some(($a,Ir)=>na($a,Nt)===na(ct,Nt)?(Kt=Ir,!0):!1),Kt},qe=pe=>_t(pe)?na(pe,e.valueKey):pe,ut=()=>{ze()},Ie=pe=>{t("end-reached",pe)},Ke=()=>{g.selectionWidth=Number.parseFloat(window.getComputedStyle(w.value).width)},Ge=()=>{g.collapseItemWidth=P.value.getBoundingClientRect().width},Ee=()=>{var pe,ct;(ct=(pe=S.value)==null?void 0:pe.updatePopper)==null||ct.call(pe)},de=()=>{var pe,ct;(ct=(pe=k.value)==null?void 0:pe.updatePopper)==null||ct.call(pe)},it=pe=>{const ct=d(pe);if(e.multiple){let Nt=e.modelValue.slice();const Kt=Fe(Nt,ct);Kt>-1?(Nt=[...Nt.slice(0,Kt),...Nt.slice(Kt+1)],g.cachedOptions.splice(Kt,1),Ue(pe)):(e.multipleLimit<=0||Nt.length{let Nt=e.modelValue.slice();const Kt=Fe(Nt,d(ct));Kt>-1&&!z.value&&(Nt=[...e.modelValue.slice(0,Kt),...e.modelValue.slice(Kt+1)],g.cachedOptions.splice(Kt,1),Sn(Nt),t("remove-tag",d(ct)),Ue(ct)),pe.stopPropagation(),sn()},sn=()=>{var pe;(pe=T.value)==null||pe.focus()},gn=()=>{var pe;if(ee.value){ee.value=!1,He(()=>{var ct;return(ct=T.value)==null?void 0:ct.blur()});return}(pe=T.value)==null||pe.blur()},Aa=()=>{g.inputValue.length>0?g.inputValue="":ee.value=!1},jr=pe=>wA(pe,ct=>!g.cachedOptions.some(Nt=>d(Nt)===ct&&v(Nt))),vr=pe=>{const ct=mn(pe);if(e.multiple&&ct!==Pe.delete&&g.inputValue.length===0){pe.preventDefault();const Nt=e.modelValue.slice(),Kt=jr(Nt);if(Kt<0)return;const $a=Nt[Kt];Nt.splice(Kt,1);const Ir=g.cachedOptions[Kt];g.cachedOptions.splice(Kt,1),Ue(Ir),Sn(Nt),t("remove-tag",$a)}},Ia=()=>{let pe;Ae(e.modelValue)?pe=[]:pe=h.value,g.selectedLabel="",ee.value=!1,Sn(pe),t("clear"),je(),sn()},Oa=(pe,ct=void 0)=>{const Nt=X.value;if(!["forward","backward"].includes(pe)||z.value||Nt.length<=0||Le.value||R.value)return;if(!ee.value)return ft();zt(ct)&&(ct=g.hoveringIndex);let Kt=-1;pe==="forward"?(Kt=ct+1,Kt>=Nt.length&&(Kt=0)):pe==="backward"&&(Kt=ct-1,(Kt<0||Kt>=Nt.length)&&(Kt=Nt.length-1));const $a=Nt[Kt];if(v($a)||$a.type==="Group")return Oa(pe,Kt);g.hoveringIndex=Kt,ja(Kt)},Oe=()=>{if(ee.value)~g.hoveringIndex&&X.value[g.hoveringIndex]&&it(X.value[g.hoveringIndex]);else return ft()},lt=pe=>{g.hoveringIndex=pe??-1},Bt=()=>{if(!e.multiple)g.hoveringIndex=X.value.findIndex(pe=>qe(d(pe))===qe(e.modelValue));else{const pe=e.modelValue.length;if(pe>0){const ct=e.modelValue[pe-1];g.hoveringIndex=X.value.findIndex(Nt=>qe(ct)===qe(d(Nt)))}else g.hoveringIndex=-1}},_n=pe=>{if(g.inputValue=pe.target.value,e.remote)b.value=!0,pt();else return ht()},Yn=pe=>{ee.value=!1,W.value&&N(new FocusEvent("blur",pe))},Zn=()=>(g.isBeforeHide=!1,He(()=>{~Qt.value&&ja(Qt.value)})),ja=pe=>{A.value.scrollToItem(pe)},Gl=(pe,ct)=>{const Nt=qe(pe);if(Re.value.has(Nt)){const{option:Kt}=Re.value.get(Nt);return Kt}if(ct&&ct.length){const Kt=ct.find($a=>qe(d($a))===Nt);if(Kt)return Kt}return{[u.value.value]:pe,[u.value.label]:pe}},co=pe=>{var ct;return((ct=Re.value.get(d(pe)))==null?void 0:ct.index)??-1},Yl=(pe=!1)=>{if(e.multiple)if(e.modelValue.length>0){const ct=g.cachedOptions.slice();g.cachedOptions.length=0,g.previousValue=e.modelValue.toString();for(const Nt of e.modelValue){const Kt=Gl(Nt,ct);g.cachedOptions.push(Kt)}}else g.cachedOptions=[],g.previousValue=void 0;else if(le.value){g.previousValue=e.modelValue;const ct=X.value,Nt=ct.findIndex(Kt=>qe(d(Kt))===qe(e.modelValue));~Nt?g.selectedLabel=c(ct[Nt]):(!g.selectedLabel||pe)&&(g.selectedLabel=qe(e.modelValue))}else g.selectedLabel="",g.previousValue=void 0;je(),ze()};ye(()=>e.fitInputWidth,()=>{ze()}),ye(ee,pe=>{pe?(e.persistent||ze(),It("")):(g.inputValue="",g.previousQuery=null,g.isBeforeHide=!0,g.menuVisibleOnFocus=!1,Me(""))}),ye(()=>e.modelValue,(pe,ct)=>{var Nt;(!pe||Ae(pe)&&pe.length===0||e.multiple&&!Rn(pe.toString(),g.previousValue)||!e.multiple&&qe(pe)!==qe(g.previousValue))&&Yl(!0),!Rn(pe,ct)&&e.validateEvent&&((Nt=l==null?void 0:l.validate)==null||Nt.call(l,"change").catch(kt))},{deep:!0}),ye(()=>e.options,()=>{const pe=T.value;(!pe||pe&&document.activeElement!==pe)&&Yl()},{deep:!0,flush:"post"}),ye(()=>X.value,()=>(ze(),A.value&&He(A.value.resetScrollTop))),ur(()=>{g.isBeforeHide||Te()}),ur(()=>{const{valueKey:pe,options:ct}=e,Nt=new Map;for(const Kt of ct){const $a=d(Kt);let Ir=$a;if(_t(Ir)&&(Ir=na($a,pe)),Nt.get(Ir)){un("ElSelectV2","The option values you provided seem to be duplicated, which may cause some problems, please check.");break}else Nt.set(Ir,!0)}}),mt(()=>{Yl()}),Mn(_,ut),Mn(w,Ke),Mn(B,Ee),Mn(I,de),Mn(P,Ge);let Xs;return ye(()=>_e.value,pe=>{pe?Xs=Mn(A,Ee).stop:(Xs==null||Xs(),Xs=void 0),t("visible-change",pe)}),{inputId:s,collapseTagSize:Be,currentPlaceholder:Jt,expanded:ee,emptyText:fe,popupHeight:ie,debounce:oe,allOptions:V,allOptionsValueMap:Re,filteredOptions:X,iconComponent:he,iconReverse:J,tagStyle:nt,collapseTagStyle:Pt,popperSize:y,dropdownMenuVisible:_e,hasModelValue:le,shouldShowPlaceholder:Wt,selectDisabled:z,selectSize:me,needStatusIcon:re,showClearBtn:ve,states:g,isFocused:W,nsSelect:r,nsInput:o,inputRef:T,menuRef:A,tagMenuRef:I,tooltipRef:S,tagTooltipRef:k,selectRef:_,wrapperRef:B,selectionRef:w,prefixRef:M,suffixRef:E,collapseItemRef:P,popperRef:Rt,validateState:Z,validateIcon:j,showTagList:Ne,collapseTagList:tt,debouncedOnInputChange:pt,deleteTag:Dt,getLabel:c,getValue:d,getDisabled:v,getValueKey:qe,getIndex:co,handleClear:Ia,handleClickOutside:Yn,handleDel:vr,handleEsc:Aa,focus:sn,blur:gn,handleMenuEnter:Zn,handleResize:ut,resetSelectionWidth:Ke,updateTooltip:Ee,updateTagTooltip:de,updateOptions:Te,toggleMenu:ft,scrollTo:ja,onInput:_n,onKeyboardNavigate:Oa,onKeyboardSelect:Oe,onEndReached:Ie,onSelect:it,onHover:lt,handleCompositionStart:G,handleCompositionEnd:K,handleCompositionUpdate:H}};var d2e=q({name:"ElSelectV2",components:{ElSelectMenu:s2e,ElTag:Cl,ElTooltip:Ma,ElIcon:Xe},directives:{ClickOutside:Ns},props:X1e,emits:J1e,setup(e,{emit:t}){const n=D(()=>{const{modelValue:s,multiple:u}=e,c=u?[]:void 0;return Ae(s)?u?s:c:u?c:s}),a=c2e(nn({...Ta(e),modelValue:n}),t),{calculatorRef:r,inputStyle:o}=w6(),i=Fa();Ft(Tw,{props:nn({...Ta(e),height:a.popupHeight,modelValue:n}),expanded:a.expanded,tooltipRef:a.tooltipRef,contentId:i,onSelect:a.onSelect,onHover:a.onHover,onKeyboardNavigate:a.onKeyboardNavigate,onKeyboardSelect:a.onKeyboardSelect});const l=D(()=>e.multiple?a.states.cachedOptions.map(s=>a.getLabel(s)):a.states.selectedLabel);return{...a,modelValue:n,selectedLabel:l,calculatorRef:r,inputStyle:o,contentId:i,BORDER_HORIZONTAL_WIDTH:2}}});const f2e=["id","value","autocomplete","tabindex","aria-expanded","aria-label","disabled","aria-controls","aria-activedescendant","readonly","name"],v2e=["textContent"],h2e={key:1};function p2e(e,t,n,a,r,o){const i=be("el-tag"),l=be("el-tooltip"),s=be("el-icon"),u=be("el-select-menu"),c=Gr("click-outside");return st((x(),O("div",{ref:"selectRef",class:U([e.nsSelect.b(),e.nsSelect.m(e.selectSize)]),onMouseenter:t[15]||(t[15]=d=>e.states.inputHovering=!0),onMouseleave:t[16]||(t[16]=d=>e.states.inputHovering=!1)},[L(l,{ref:"tooltipRef",visible:e.dropdownMenuVisible,teleported:e.teleported,"popper-class":[e.nsSelect.e("popper"),e.popperClass],"popper-style":e.popperStyle,"gpu-acceleration":!1,"stop-popper-mouse-event":!1,"popper-options":e.popperOptions,"fallback-placements":e.fallbackPlacements,effect:e.effect,placement:e.placement,pure:"",transition:`${e.nsSelect.namespace.value}-zoom-in-top`,trigger:"click",persistent:e.persistent,"append-to":e.appendTo,"show-arrow":e.showArrow,offset:e.offset,onBeforeShow:e.handleMenuEnter,onHide:t[14]||(t[14]=d=>e.states.isBeforeHide=!1)},{default:$(()=>{var d;return[C("div",{ref:"wrapperRef",class:U([e.nsSelect.e("wrapper"),e.nsSelect.is("focused",e.isFocused),e.nsSelect.is("hovering",e.states.inputHovering),e.nsSelect.is("filterable",e.filterable),e.nsSelect.is("disabled",e.selectDisabled)]),onClick:t[11]||(t[11]=at((...v)=>e.toggleMenu&&e.toggleMenu(...v),["prevent"]))},[e.$slots.prefix?(x(),O("div",{key:0,ref:"prefixRef",class:U(e.nsSelect.e("prefix"))},[ce(e.$slots,"prefix")],2)):te("v-if",!0),C("div",{ref:"selectionRef",class:U([e.nsSelect.e("selection"),e.nsSelect.is("near",e.multiple&&!e.$slots.prefix&&!!e.modelValue.length)])},[e.multiple?ce(e.$slots,"tag",{key:0,data:e.states.cachedOptions,deleteTag:e.deleteTag,selectDisabled:e.selectDisabled},()=>{var v,f,h,m,g,y,b,_,w,S,k,T,M;return[(x(!0),O(Ve,null,dt(e.showTagList,E=>(x(),O("div",{key:e.getValueKey(e.getValue(E)),class:U(e.nsSelect.e("selected-item"))},[L(i,{closable:!e.selectDisabled&&!e.getDisabled(E),size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",style:et(e.tagStyle),onClose:A=>e.deleteTag(A,E)},{default:$(()=>[C("span",{class:U(e.nsSelect.e("tags-text"))},[ce(e.$slots,"label",{index:e.getIndex(E),label:e.getLabel(E),value:e.getValue(E)},()=>[ue(Q(e.getLabel(E)),1)])],2)]),_:2},1032,["closable","size","type","effect","style","onClose"])],2))),128)),e.collapseTags&&e.states.cachedOptions.length>e.maxCollapseTags?(x(),ne(l,{key:0,ref:"tagTooltipRef",disabled:e.dropdownMenuVisible||!e.collapseTagsTooltip,"fallback-placements":((v=e.tagTooltip)==null?void 0:v.fallbackPlacements)??["bottom","top","right","left"],effect:((f=e.tagTooltip)==null?void 0:f.effect)??e.effect,placement:((h=e.tagTooltip)==null?void 0:h.placement)??"bottom","popper-class":((m=e.tagTooltip)==null?void 0:m.popperClass)??e.popperClass,"popper-style":((g=e.tagTooltip)==null?void 0:g.popperStyle)??e.popperStyle,teleported:((y=e.tagTooltip)==null?void 0:y.teleported)??e.teleported,"append-to":((b=e.tagTooltip)==null?void 0:b.appendTo)??e.appendTo,"popper-options":((_=e.tagTooltip)==null?void 0:_.popperOptions)??e.popperOptions,transition:(w=e.tagTooltip)==null?void 0:w.transition,"show-after":(S=e.tagTooltip)==null?void 0:S.showAfter,"hide-after":(k=e.tagTooltip)==null?void 0:k.hideAfter,"auto-close":(T=e.tagTooltip)==null?void 0:T.autoClose,offset:(M=e.tagTooltip)==null?void 0:M.offset},{default:$(()=>[C("div",{ref:"collapseItemRef",class:U(e.nsSelect.e("selected-item"))},[L(i,{closable:!1,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,style:et(e.collapseTagStyle),"disable-transitions":""},{default:$(()=>[C("span",{class:U(e.nsSelect.e("tags-text"))}," + "+Q(e.states.cachedOptions.length-e.maxCollapseTags),3)]),_:1},8,["size","type","effect","style"])],2)]),content:$(()=>[C("div",{ref:"tagMenuRef",class:U(e.nsSelect.e("selection"))},[(x(!0),O(Ve,null,dt(e.collapseTagList,E=>(x(),O("div",{key:e.getValueKey(e.getValue(E)),class:U(e.nsSelect.e("selected-item"))},[L(i,{class:"in-tooltip",closable:!e.selectDisabled&&!e.getDisabled(E),size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",onClose:A=>e.deleteTag(A,E)},{default:$(()=>[C("span",{class:U(e.nsSelect.e("tags-text"))},[ce(e.$slots,"label",{index:e.getIndex(E),label:e.getLabel(E),value:e.getValue(E)},()=>[ue(Q(e.getLabel(E)),1)])],2)]),_:2},1032,["closable","size","type","effect","onClose"])],2))),128))],2)]),_:3},8,["disabled","fallback-placements","effect","placement","popper-class","popper-style","teleported","append-to","popper-options","transition","show-after","hide-after","auto-close","offset"])):te("v-if",!0)]}):te("v-if",!0),C("div",{class:U([e.nsSelect.e("selected-item"),e.nsSelect.e("input-wrapper"),e.nsSelect.is("hidden",!e.filterable||e.selectDisabled||e.multiple&&!e.states.inputValue&&!e.isFocused)])},[C("input",{id:e.inputId,ref:"inputRef",value:e.states.inputValue,style:et(e.inputStyle),autocomplete:e.autocomplete,tabindex:e.tabindex,"aria-autocomplete":"none","aria-haspopup":"listbox",autocapitalize:"off","aria-expanded":e.expanded,"aria-label":e.ariaLabel,class:U([e.nsSelect.e("input"),e.nsSelect.is(e.selectSize)]),disabled:e.selectDisabled,role:"combobox","aria-controls":e.contentId,"aria-activedescendant":e.states.hoveringIndex>=0?`${e.contentId}-${e.states.hoveringIndex}`:"",readonly:!e.filterable,spellcheck:"false",type:"text",name:e.name,onInput:t[0]||(t[0]=(...v)=>e.onInput&&e.onInput(...v)),onChange:t[1]||(t[1]=at(()=>{},["stop"])),onCompositionstart:t[2]||(t[2]=(...v)=>e.handleCompositionStart&&e.handleCompositionStart(...v)),onCompositionupdate:t[3]||(t[3]=(...v)=>e.handleCompositionUpdate&&e.handleCompositionUpdate(...v)),onCompositionend:t[4]||(t[4]=(...v)=>e.handleCompositionEnd&&e.handleCompositionEnd(...v)),onKeydown:[t[5]||(t[5]=kn(at(v=>e.onKeyboardNavigate("backward"),["stop","prevent"]),["up"])),t[6]||(t[6]=kn(at(v=>e.onKeyboardNavigate("forward"),["stop","prevent"]),["down"])),t[7]||(t[7]=kn(at((...v)=>e.onKeyboardSelect&&e.onKeyboardSelect(...v),["stop","prevent"]),["enter"])),t[8]||(t[8]=kn(at((...v)=>e.handleEsc&&e.handleEsc(...v),["stop","prevent"]),["esc"])),t[9]||(t[9]=kn(at((...v)=>e.handleDel&&e.handleDel(...v),["stop"]),["delete"]))],onClick:t[10]||(t[10]=at((...v)=>e.toggleMenu&&e.toggleMenu(...v),["stop"]))},null,46,f2e),e.filterable?(x(),O("span",{key:0,ref:"calculatorRef","aria-hidden":"true",class:U(e.nsSelect.e("input-calculator")),textContent:Q(e.states.inputValue)},null,10,v2e)):te("v-if",!0)],2),e.shouldShowPlaceholder?(x(),O("div",{key:1,class:U([e.nsSelect.e("selected-item"),e.nsSelect.e("placeholder"),e.nsSelect.is("transparent",!e.hasModelValue||e.expanded&&!e.states.inputValue)])},[e.hasModelValue?ce(e.$slots,"label",{key:0,index:((d=e.allOptionsValueMap.get(e.modelValue))==null?void 0:d.index)??-1,label:e.currentPlaceholder,value:e.modelValue},()=>[C("span",null,Q(e.currentPlaceholder),1)]):(x(),O("span",h2e,Q(e.currentPlaceholder),1))],2)):te("v-if",!0)],2),C("div",{ref:"suffixRef",class:U(e.nsSelect.e("suffix"))},[e.iconComponent?st((x(),ne(s,{key:0,class:U([e.nsSelect.e("caret"),e.nsInput.e("icon"),e.iconReverse])},{default:$(()=>[(x(),ne(St(e.iconComponent)))]),_:1},8,["class"])),[[Zt,!e.showClearBtn]]):te("v-if",!0),e.showClearBtn&&e.clearIcon?(x(),ne(s,{key:1,class:U([e.nsSelect.e("caret"),e.nsInput.e("icon"),e.nsSelect.e("clear")]),onClick:at(e.handleClear,["prevent","stop"])},{default:$(()=>[(x(),ne(St(e.clearIcon)))]),_:1},8,["class","onClick"])):te("v-if",!0),e.validateState&&e.validateIcon&&e.needStatusIcon?(x(),ne(s,{key:2,class:U([e.nsInput.e("icon"),e.nsInput.e("validateIcon"),e.nsInput.is("loading",e.validateState==="validating")])},{default:$(()=>[(x(),ne(St(e.validateIcon)))]),_:1},8,["class"])):te("v-if",!0)],2)],2)]}),content:$(()=>[L(u,{id:e.contentId,ref:"menuRef",data:e.filteredOptions,width:e.popperSize-e.BORDER_HORIZONTAL_WIDTH,"hovering-index":e.states.hoveringIndex,"scrollbar-always-on":e.scrollbarAlwaysOn,"aria-label":e.ariaLabel,onEndReached:e.onEndReached},xr({default:$(d=>[ce(e.$slots,"default",bl(wl(d)))]),_:2},[e.$slots.header?{name:"header",fn:$(()=>[C("div",{class:U(e.nsSelect.be("dropdown","header")),onClick:t[12]||(t[12]=at(()=>{},["stop"]))},[ce(e.$slots,"header")],2)]),key:"0"}:void 0,e.$slots.loading&&e.loading?{name:"loading",fn:$(()=>[C("div",{class:U(e.nsSelect.be("dropdown","loading"))},[ce(e.$slots,"loading")],2)]),key:"1"}:e.loading||e.filteredOptions.length===0?{name:"empty",fn:$(()=>[C("div",{class:U(e.nsSelect.be("dropdown","empty"))},[ce(e.$slots,"empty",{},()=>[C("span",null,Q(e.emptyText),1)])],2)]),key:"2"}:void 0,e.$slots.footer?{name:"footer",fn:$(()=>[C("div",{class:U(e.nsSelect.be("dropdown","footer")),onClick:t[13]||(t[13]=at(()=>{},["stop"]))},[ce(e.$slots,"footer")],2)]),key:"3"}:void 0]),1032,["id","data","width","hovering-index","scrollbar-always-on","aria-label","onEndReached"])]),_:3},8,["visible","teleported","popper-class","popper-style","popper-options","fallback-placements","effect","placement","transition","persistent","append-to","show-arrow","offset","onBeforeShow"])],34)),[[c,e.handleClickOutside,e.popperRef]])}var m2e=ya(d2e,[["render",p2e]]);const g2e=wt(m2e),y2e=$e({animated:Boolean,count:{type:Number,default:1},rows:{type:Number,default:3},loading:{type:Boolean,default:!0},throttle:{type:ae([Number,Object])}}),b2e=$e({variant:{type:String,values:["circle","rect","h1","h3","text","caption","p","image","button"],default:"text"}});var _2e=q({name:"ElSkeletonItem",__name:"skeleton-item",props:b2e,setup(e){const t=ke("skeleton");return(n,a)=>(x(),O("div",{class:U([p(t).e("item"),p(t).e(e.variant)])},[e.variant==="image"?(x(),ne(p(RI),{key:0})):te("v-if",!0)],2))}}),hg=_2e,w2e=q({name:"ElSkeleton",__name:"skeleton",props:y2e,setup(e,{expose:t}){const n=e,a=ke("skeleton"),r=Qj(cn(n,"loading"),n.throttle);return t({uiLoading:r}),(o,i)=>p(r)?(x(),O("div",Tt({key:0,class:[p(a).b(),p(a).is("animated",e.animated)]},o.$attrs),[(x(!0),O(Ve,null,dt(e.count,l=>(x(),O(Ve,{key:l},[p(r)?ce(o.$slots,"template",{key:l},()=>[L(hg,{class:U(p(a).is("first")),variant:"p"},null,8,["class"]),(x(!0),O(Ve,null,dt(e.rows,s=>(x(),ne(hg,{key:s,class:U([p(a).e("paragraph"),p(a).is("last",s===e.rows&&e.rows>1)]),variant:"p"},null,8,["class"]))),128))]):te("v-if",!0)],64))),128))],16)):ce(o.$slots,"default",bl(Tt({key:1},o.$attrs)))}}),S2e=w2e;const C2e=wt(S2e,{SkeletonItem:hg}),x2e=On(hg),bD=Symbol("sliderContextKey"),k2e=$e({modelValue:{type:ae([Number,Array]),default:0},id:{type:String,default:void 0},min:{type:Number,default:0},max:{type:Number,default:100},step:{type:ae([Number,String]),default:1},showInput:Boolean,showInputControls:{type:Boolean,default:!0},size:pa,inputSize:pa,showStops:Boolean,showTooltip:{type:Boolean,default:!0},formatTooltip:{type:ae(Function),default:void 0},disabled:{type:Boolean,default:void 0},range:Boolean,vertical:Boolean,height:String,rangeStartLabel:{type:String,default:void 0},rangeEndLabel:{type:String,default:void 0},formatValueText:{type:ae(Function),default:void 0},tooltipClass:{type:ae([String,Array,Object,Boolean]),default:void 0},placement:{type:String,values:tl,default:"top"},marks:{type:ae(Object)},validateEvent:{type:Boolean,default:!0},persistent:{type:Boolean,default:!0},...dr(["ariaLabel"])}),cy=e=>Je(e)||Ae(e)&&e.every(Je),T2e={[bt]:cy,[ua]:cy,[Ot]:cy},M2e=(e,t,n)=>{const a=F();return mt(async()=>{e.range?(Ae(e.modelValue)?(t.firstValue=Math.max(e.min,e.modelValue[0]),t.secondValue=Math.min(e.max,e.modelValue[1])):(t.firstValue=e.min,t.secondValue=e.max),t.oldValue=[t.firstValue,t.secondValue]):(!Je(e.modelValue)||Number.isNaN(e.modelValue)?t.firstValue=e.min:t.firstValue=Math.min(e.max,Math.max(e.min,e.modelValue)),t.oldValue=t.firstValue),on(window,"resize",n),await He(),n()}),{sliderWrapper:a}},E2e=e=>{const t=D(()=>e.marks?Object.keys(e.marks).map(Number.parseFloat).sort((n,a)=>n-a).filter(n=>n<=e.max&&n>=e.min).map(n=>({point:n,position:(n-e.min)*100/(e.max-e.min),mark:e.marks[n]})):[]);return ur(()=>{if(e.step==="mark"&&!e.marks&&un("ElSlider","marks prop must be provided when step is mark"),e.marks){const n=Object.keys(e.marks),a=t.value.map(o=>o.point),r=n.filter(o=>{const i=Number.parseFloat(o);return Number.isNaN(i)||!a.includes(i)});r.length>0&&un("ElSlider",`Some marks keys are invalid (not a number or out of [min, max]): [${r.map(o=>`'${o}'`).join(", ")}] and will be ignored.`)}}),t},A2e=(e,t,n)=>{const{formItem:a}=Ea(),r=Ht(),o=F(),i=F(),l={firstButton:o,secondButton:i},s=$n(),u=D(()=>Math.min(t.firstValue,t.secondValue)),c=D(()=>Math.max(t.firstValue,t.secondValue)),d=D(()=>e.range?`${100*(c.value-u.value)/(e.max-e.min)}%`:`${100*(t.firstValue-e.min)/(e.max-e.min)}%`),v=D(()=>e.range?`${100*(u.value-e.min)/(e.max-e.min)}%`:"0%"),f=D(()=>e.vertical?{height:e.height}:{}),h=D(()=>e.vertical?{height:d.value,bottom:v.value}:{width:d.value,left:v.value}),m=()=>{r.value&&(t.sliderSize=r.value.getBoundingClientRect()[e.vertical?"height":"width"])},g=I=>{const P=e.min+I*(e.max-e.min)/100;if(!e.range)return o;let R;return Math.abs(u.value-P)t.secondValue?"firstButton":"secondButton",l[R]},y=I=>{const P=g(I);return P.value.setPosition(I),P},b=I=>{t.firstValue=I??e.min,w(e.range?[u.value,c.value]:I??e.min)},_=I=>{t.secondValue=I,e.range&&w([u.value,c.value])},w=I=>{n(bt,I),n(ua,I)},S=async()=>{await He(),n(Ot,e.range?[u.value,c.value]:e.modelValue)},k=I=>{var R,G,K,H;if(s.value||t.dragging)return;m();let P=0;if(e.vertical){const z=((G=(R=I.touches)==null?void 0:R.item(0))==null?void 0:G.clientY)??I.clientY;P=(r.value.getBoundingClientRect().bottom-z)/t.sliderSize*100}else P=((((H=(K=I.touches)==null?void 0:K.item(0))==null?void 0:H.clientX)??I.clientX)-r.value.getBoundingClientRect().left)/t.sliderSize*100;if(!(P<0||P>100))return y(P)};return{elFormItem:a,slider:r,firstButton:o,secondButton:i,sliderDisabled:s,minValue:u,maxValue:c,runwayStyle:f,barStyle:h,resetSize:m,setPosition:y,emitChange:S,onSliderWrapperPrevent:I=>{var P,R;((P=l.firstButton.value)!=null&&P.dragging||(R=l.secondButton.value)!=null&&R.dragging)&&I.preventDefault()},onSliderClick:I=>{k(I)&&S()},onSliderDown:async I=>{const P=k(I);P&&(await He(),P.value.onButtonDown(I))},onSliderMarkerDown:I=>{s.value||t.dragging||y(I)&&S()},setFirstValue:b,setSecondValue:_}},I2e=(e,t,n,a)=>({stops:D(()=>{if(!e.showStops||e.min>e.max)return[];if(e.step==="mark"||e.step===0)return e.step===0&&un("ElSlider","step should not be 0."),[];const i=Math.ceil((e.max-e.min)/e.step),l=100*e.step/(e.max-e.min),s=Array.from({length:i-1}).map((u,c)=>(c+1)*l);return e.range?s.filter(u=>u<100*(n.value-e.min)/(e.max-e.min)||u>100*(a.value-e.min)/(e.max-e.min)):s.filter(u=>u>100*(t.firstValue-e.min)/(e.max-e.min))}),getStopStyle:i=>e.vertical?{bottom:`${i}%`}:{left:`${i}%`}}),L2e=(e,t,n,a,r,o)=>{const i=u=>{r(bt,u),r(ua,u)},l=()=>e.range?![n.value,a.value].every((u,c)=>u===t.oldValue[c]):e.modelValue!==t.oldValue,s=()=>{var c,d;e.min>e.max&&Dn("Slider","min should not be greater than max.");const u=e.modelValue;e.range&&Ae(u)?u[1]e.max?i([e.max,e.max]):u[0]e.max?i([u[0],e.max]):(t.firstValue=u[0],t.secondValue=u[1],l()&&(e.validateEvent&&((c=o==null?void 0:o.validate)==null||c.call(o,"change").catch(kt)),t.oldValue=u.slice())):!e.range&&Je(u)&&!Number.isNaN(u)&&(ue.max?i(e.max):(t.firstValue=u,l()&&(e.validateEvent&&((d=o==null?void 0:o.validate)==null||d.call(o,"change").catch(kt)),t.oldValue=u)))};s(),ye(()=>t.dragging,u=>{u||s()}),ye(()=>e.modelValue,(u,c)=>{t.dragging||Ae(u)&&Ae(c)&&u.every((d,v)=>d===c[v])&&t.firstValue===u[0]&&t.secondValue===u[1]||s()},{deep:!0}),ye(()=>[e.min,e.max],()=>{s()})},P2e=(e,t,n)=>{const a=F(),r=F(!1),o=D(()=>t.value instanceof Function);return{tooltip:a,tooltipVisible:r,formatValue:D(()=>o.value&&t.value(e.modelValue)||e.modelValue),displayTooltip:Yi(()=>{n.value&&(r.value=!0)},50),hideTooltip:Yi(()=>{n.value&&(r.value=!1)},50)}},D2e=(e,t,n)=>{const{disabled:a,min:r,max:o,step:i,showTooltip:l,persistent:s,precision:u,sliderSize:c,formatTooltip:d,emitChange:v,resetSize:f,updateDragging:h,markList:m}=Ze(bD),{tooltip:g,tooltipVisible:y,formatValue:b,displayTooltip:_,hideTooltip:w}=P2e(e,d,l),S=F(),k=D(()=>`${(e.modelValue-r.value)/(o.value-r.value)*100}%`),T=D(()=>e.vertical?{bottom:k.value}:{left:k.value}),M=D(()=>i.value==="mark"&&m.value.length>0),E=()=>{t.hovering=!0,_()},A=()=>{t.hovering=!1,t.dragging||w()},I=ie=>{a.value||(ie.preventDefault(),Y(ie),window.addEventListener("mousemove",X),window.addEventListener("touchmove",X),window.addEventListener("mouseup",ee),window.addEventListener("touchend",ee),window.addEventListener("contextmenu",ee),S.value.focus())},P=ie=>{a.value||(t.newPosition=Number.parseFloat(k.value)+ie/(o.value-r.value)*100,re(t.newPosition),v())},R=ie=>{if(a.value||!m.value.length)return;const le=e.modelValue,ve=Number.EPSILON,he=Math.abs(ie);let J;if(ie>0){const Z=m.value.findIndex(j=>j.point>le+ve);if(Z!==-1){const j=Math.min(Z+he-1,m.value.length-1);J=m.value[j].point}}else{let Z=-1;for(let j=m.value.length-1;j>=0;j--)if(m.value[j].point{M.value?R(-1):Je(i.value)&&P(-i.value)},K=()=>{M.value?R(1):Je(i.value)&&P(i.value)},H=()=>{M.value?R(-4):Je(i.value)&&P(-i.value*4)},z=()=>{M.value?R(4):Je(i.value)&&P(i.value*4)},B=()=>{a.value||(re(0),v())},W=()=>{a.value||(re(100),v())},N=ie=>{const le=mn(ie);let ve=!0;switch(le){case Pe.left:case Pe.down:G();break;case Pe.right:case Pe.up:K();break;case Pe.home:B();break;case Pe.end:W();break;case Pe.pageDown:H();break;case Pe.pageUp:z();break;default:ve=!1;break}ve&&ie.preventDefault()},V=ie=>{let le,ve;return ie.type.startsWith("touch")?(ve=ie.touches[0].clientY,le=ie.touches[0].clientX):(ve=ie.clientY,le=ie.clientX),{clientX:le,clientY:ve}},Y=ie=>{t.dragging=!0,t.isClick=!0;const{clientX:le,clientY:ve}=V(ie);e.vertical?t.startY=ve:t.startX=le,t.startPosition=Number.parseFloat(k.value),t.newPosition=t.startPosition},X=ie=>{if(t.dragging){t.isClick=!1,_(),f();let le;const{clientX:ve,clientY:he}=V(ie);e.vertical?(t.currentY=he,le=(t.startY-t.currentY)/c.value*100):(t.currentX=ve,le=(t.currentX-t.startX)/c.value*100),t.newPosition=t.startPosition+le,re(t.newPosition)}},ee=()=>{t.dragging&&(setTimeout(()=>{t.dragging=!1,t.hovering||w(),t.isClick||re(t.newPosition),v()},0),window.removeEventListener("mousemove",X),window.removeEventListener("touchmove",X),window.removeEventListener("mouseup",ee),window.removeEventListener("touchend",ee),window.removeEventListener("contextmenu",ee))},re=async ie=>{if(ie===null||Number.isNaN(+ie))return;ie=Br(ie,0,100);let le;if(i.value==="mark")m.value.length===0?le=ie<=50?r.value:o.value:le=m.value.reduce((ve,he)=>Math.abs(he.position-ie)t.dragging,ie=>{h(ie)}),on(S,"touchstart",I,{passive:!1}),{disabled:a,button:S,tooltip:g,tooltipVisible:y,showTooltip:l,persistent:s,wrapperStyle:T,formatValue:b,handleMouseEnter:E,handleMouseLeave:A,onButtonDown:I,onKeyDown:N,setPosition:re}},O2e=$e({modelValue:{type:Number,default:0},vertical:Boolean,tooltipClass:{type:ae([String,Array,Object,Boolean]),default:void 0},placement:{type:String,values:tl,default:"top"}}),$2e={[bt]:e=>Je(e)},R2e=["tabindex"];var N2e=q({name:"ElSliderButton",__name:"button",props:O2e,emits:$2e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("slider"),i=nn({hovering:!1,dragging:!1,isClick:!1,startX:0,currentX:0,startY:0,currentY:0,startPosition:0,newPosition:0,oldValue:a.modelValue}),l=D(()=>d.value?v.value:!1),{disabled:s,button:u,tooltip:c,showTooltip:d,persistent:v,tooltipVisible:f,wrapperStyle:h,formatValue:m,handleMouseEnter:g,handleMouseLeave:y,onButtonDown:b,onKeyDown:_,setPosition:w}=D2e(a,i,r),{hovering:S,dragging:k}=Ta(i);return t({onButtonDown:b,onKeyDown:_,setPosition:w,hovering:S,dragging:k}),(T,M)=>(x(),O("div",{ref_key:"button",ref:u,class:U([p(o).e("button-wrapper"),{hover:p(S),dragging:p(k)}]),style:et(p(h)),tabindex:p(s)?void 0:0,onMouseenter:M[0]||(M[0]=(...E)=>p(g)&&p(g)(...E)),onMouseleave:M[1]||(M[1]=(...E)=>p(y)&&p(y)(...E)),onMousedown:M[2]||(M[2]=(...E)=>p(b)&&p(b)(...E)),onFocus:M[3]||(M[3]=(...E)=>p(g)&&p(g)(...E)),onBlur:M[4]||(M[4]=(...E)=>p(y)&&p(y)(...E)),onKeydown:M[5]||(M[5]=(...E)=>p(_)&&p(_)(...E))},[L(p(Ma),{ref_key:"tooltip",ref:c,visible:p(f),placement:T.placement,"fallback-placements":["top","bottom","right","left"],"stop-popper-mouse-event":!1,"popper-class":T.tooltipClass,disabled:!p(d),persistent:l.value},{content:$(()=>[C("span",null,Q(p(m)),1)]),default:$(()=>[C("div",{class:U([p(o).e("button"),{hover:p(S),dragging:p(k)}])},null,2)]),_:1},8,["visible","placement","popper-class","disabled","persistent"])],46,R2e))}}),Lx=N2e;const B2e=$e({mark:{type:ae([String,Object]),default:void 0}});var V2e=q({name:"ElSliderMarker",props:B2e,setup(e){const t=ke("slider"),n=D(()=>Ye(e.mark)?e.mark:e.mark.label),a=D(()=>Ye(e.mark)?void 0:e.mark.style);return()=>ot("div",{class:t.e("marks-text"),style:a.value},n.value)}});const z2e=["id","role","aria-label","aria-labelledby"],F2e={key:1};var H2e=q({name:"ElSlider",__name:"slider",props:k2e,emits:T2e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("slider"),{t:i}=Vt(),l=nn({firstValue:0,secondValue:0,oldValue:0,dragging:!1,sliderSize:1}),{elFormItem:s,slider:u,firstButton:c,secondButton:d,sliderDisabled:v,minValue:f,maxValue:h,runwayStyle:m,barStyle:g,resetSize:y,emitChange:b,onSliderWrapperPrevent:_,onSliderClick:w,onSliderDown:S,onSliderMarkerDown:k,setFirstValue:T,setSecondValue:M}=A2e(a,l,r),{stops:E,getStopStyle:A}=I2e(a,l,f,h),{inputId:I,isLabeledByFormItem:P}=Yr(a,{formItemContext:s}),R=ca(),G=D(()=>a.inputSize||R.value),K=D(()=>a.showInput&&!a.range&&a.step!=="mark"),H=D(()=>a.ariaLabel||i("el.slider.defaultLabel",{min:a.min,max:a.max})),z=D(()=>a.range?a.rangeStartLabel||i("el.slider.defaultRangeStartLabel"):H.value),B=D(()=>a.formatValueText?a.formatValueText(ie.value):`${ie.value}`),W=D(()=>a.rangeEndLabel||i("el.slider.defaultRangeEndLabel")),N=D(()=>a.formatValueText?a.formatValueText(le.value):`${le.value}`),V=D(()=>[o.b(),o.m(R.value),o.is("vertical",a.vertical),{[o.m("with-input")]:K.value}]),Y=E2e(a);L2e(a,l,f,h,r,s);const X=D(()=>Je(a.step)?a.step:1),ee=D(()=>{const J=Je(a.step)?a.step:1,Z=[a.min,a.max,J].map(j=>{const oe=`${j}`.split(".")[1];return oe?oe.length:0});return Math.max.apply(null,Z)}),{sliderWrapper:re}=M2e(a,l,y),{firstValue:ie,secondValue:le,sliderSize:ve}=Ta(l),he=J=>{l.dragging=J};return on(re,"touchstart",_,{passive:!1}),on(re,"touchmove",_,{passive:!1}),Ft(bD,{...Ta(a),sliderSize:ve,disabled:v,precision:ee,markList:Y,emitChange:b,resetSize:y,updateDragging:he}),t({onSliderClick:w}),(J,Z)=>{var j,oe;return x(),O("div",{id:J.range?p(I):void 0,ref_key:"sliderWrapper",ref:re,class:U(V.value),role:J.range?"group":void 0,"aria-label":J.range&&!p(P)?H.value:void 0,"aria-labelledby":J.range&&p(P)?(j=p(s))==null?void 0:j.labelId:void 0},[C("div",{ref_key:"slider",ref:u,class:U([p(o).e("runway"),{"show-input":K.value},p(o).is("disabled",p(v))]),style:et(p(m)),onMousedown:Z[0]||(Z[0]=(...se)=>p(S)&&p(S)(...se)),onTouchstartPassive:Z[1]||(Z[1]=(...se)=>p(S)&&p(S)(...se))},[C("div",{class:U(p(o).e("bar")),style:et(p(g))},null,6),L(Lx,{id:J.range?void 0:p(I),ref_key:"firstButton",ref:c,"model-value":p(ie),vertical:J.vertical,"tooltip-class":J.tooltipClass,placement:J.placement,role:"slider","aria-label":J.range||!p(P)?z.value:void 0,"aria-labelledby":!J.range&&p(P)?(oe=p(s))==null?void 0:oe.labelId:void 0,"aria-valuemin":J.min,"aria-valuemax":J.range?p(le):J.max,"aria-valuenow":p(ie),"aria-valuetext":B.value,"aria-orientation":J.vertical?"vertical":"horizontal","aria-disabled":p(v),"onUpdate:modelValue":p(T)},null,8,["id","model-value","vertical","tooltip-class","placement","aria-label","aria-labelledby","aria-valuemin","aria-valuemax","aria-valuenow","aria-valuetext","aria-orientation","aria-disabled","onUpdate:modelValue"]),J.range?(x(),ne(Lx,{key:0,ref_key:"secondButton",ref:d,"model-value":p(le),vertical:J.vertical,"tooltip-class":J.tooltipClass,placement:J.placement,role:"slider","aria-label":W.value,"aria-valuemin":p(ie),"aria-valuemax":J.max,"aria-valuenow":p(le),"aria-valuetext":N.value,"aria-orientation":J.vertical?"vertical":"horizontal","aria-disabled":p(v),"onUpdate:modelValue":p(M)},null,8,["model-value","vertical","tooltip-class","placement","aria-label","aria-valuemin","aria-valuemax","aria-valuenow","aria-valuetext","aria-orientation","aria-disabled","onUpdate:modelValue"])):te("v-if",!0),J.showStops?(x(),O("div",F2e,[(x(!0),O(Ve,null,dt(p(E),(se,fe)=>(x(),O("div",{key:fe,class:U(p(o).e("stop")),style:et(p(A)(se))},null,6))),128))])):te("v-if",!0),p(Y).length>0?(x(),O(Ve,{key:2},[C("div",null,[(x(!0),O(Ve,null,dt(p(Y),(se,fe)=>(x(),O("div",{key:fe,style:et(p(A)(se.position)),class:U([p(o).e("stop"),p(o).e("marks-stop")])},null,6))),128))]),C("div",{class:U(p(o).e("marks"))},[(x(!0),O(Ve,null,dt(p(Y),(se,fe)=>(x(),ne(p(V2e),{key:fe,mark:se.mark,style:et(p(A)(se.position)),onMousedown:at(ge=>p(k)(se.position),["stop"])},null,8,["mark","style","onMousedown"]))),128))],2)],64)):te("v-if",!0)],38),K.value?(x(),ne(p(vD),{key:0,ref:"input","model-value":p(ie),class:U(p(o).e("input")),step:X.value,disabled:p(v),controls:J.showInputControls,min:J.min,max:J.max,precision:ee.value,size:G.value,"onUpdate:modelValue":p(T),onChange:p(b)},null,8,["model-value","class","step","disabled","controls","min","max","precision","size","onUpdate:modelValue","onChange"])):te("v-if",!0)],10,z2e)}}}),W2e=H2e;const K2e=wt(W2e),U2e=$e({prefixCls:{type:String}}),Px=q({name:"ElSpaceItem",props:U2e,setup(e,{slots:t}){const n=ke("space"),a=D(()=>`${e.prefixCls||n.b()}__item`);return()=>ot("div",{class:a.value},ce(t,"default"))}}),Dx={small:8,default:12,large:16};function G2e(e){const t=ke("space"),n=D(()=>[t.b(),t.m(e.direction),e.class]),a=F(0),r=F(0),o=D(()=>[e.wrap||e.fill?{flexWrap:"wrap"}:{},{alignItems:e.alignment},{rowGap:`${r.value}px`,columnGap:`${a.value}px`},e.style]),i=D(()=>e.fill?{flexGrow:1,minWidth:`${e.fillRatio}%`}:{});return ur(()=>{const{size:l="small",wrap:s,direction:u,fill:c}=e;if(Ae(l)){const[d=0,v=0]=l;a.value=d,r.value=v}else{let d;Je(l)?d=l:d=Dx[l||"small"]||Dx.small,(s||c)&&u==="horizontal"?a.value=r.value=d:u==="horizontal"?(a.value=d,r.value=0):(r.value=d,a.value=0)}}),{classes:n,containerStyle:o,itemStyle:i}}const Y2e=$e({direction:{type:String,values:["horizontal","vertical"],default:"horizontal"},class:{type:ae([String,Object,Array,Boolean]),default:""},style:{type:ae([String,Array,Object,Boolean]),default:""},alignment:{type:ae(String),default:"center"},prefixCls:{type:String},spacer:{type:ae([Object,String,Number,Array]),default:null,validator:e=>bn(e)||Je(e)||Ye(e)},wrap:Boolean,fill:Boolean,fillRatio:{type:Number,default:100},size:{type:[String,Array,Number],values:ii,validator:e=>Je(e)||Ae(e)&&e.length===2&&e.every(Je)}}),j2e=q({name:"ElSpace",props:Y2e,setup(e,{slots:t}){const{classes:n,containerStyle:a,itemStyle:r}=G2e(e);function o(i,l="",s=[]){const{prefixCls:u}=e;return i.forEach((c,d)=>{h4(c)?Ae(c.children)&&c.children.forEach((v,f)=>{h4(v)&&Ae(v.children)?o(v.children,`${l+f}-`,s):bn(v)&&(v==null?void 0:v.type)===oa?s.push(v):s.push(L(Px,{style:r.value,prefixCls:u,key:`nested-${l+f}`},{default:()=>[v]},12,["style","prefixCls"]))}):eX(c)&&s.push(L(Px,{style:r.value,prefixCls:u,key:`LoopKey${l+d}`},{default:()=>[c]},12,["style","prefixCls"]))}),s}return()=>{const{spacer:i,direction:l}=e,s=ce(t,"default",{key:0},()=>[]);if((s.children??[]).length===0)return null;if(Ae(s.children)){let u=o(s.children);if(i){const c=u.length-1;u=u.reduce((d,v,f)=>{const h=[...d,v];return f!==c&&h.push(L("span",{style:[r.value,l==="vertical"?"width: 100%":null],key:f},[bn(i)?ei(i):ue(i,1)],4)),h},[])}return L("div",{class:n.value,style:a.value},u,6)}return s.children}}}),q2e=wt(j2e),X2e=$e({space:{type:[Number,String],default:""},active:{type:Number,default:0},direction:{type:String,default:"horizontal",values:["horizontal","vertical"]},alignCenter:{type:Boolean},simple:{type:Boolean},finishStatus:{type:String,values:["wait","process","finish","error","success"],default:"finish"},processStatus:{type:String,values:["wait","process","finish","error","success"],default:"process"}}),Z2e={[Ot]:(e,t)=>[e,t].every(Je)},_D="ElSteps",J2e=$e({title:{type:String,default:""},icon:{type:pn},description:{type:String,default:""},status:{type:String,values:["","wait","process","finish","error","success"],default:""}});var Q2e=q({name:"ElSteps",__name:"steps",props:X2e,emits:Z2e,setup(e,{emit:t}){const n=e,a=t,r=ke("steps"),{children:o,addChild:i,removeChild:l,ChildrenSorter:s}=z1(Et(),"ElStep");return ye(o,()=>{o.value.forEach((u,c)=>{u.setIndex(c)})}),Ft(_D,{props:n,steps:o,addStep:i,removeStep:l}),ye(()=>n.active,(u,c)=>{a(Ot,u,c)}),(u,c)=>(x(),O("div",{class:U([p(r).b(),p(r).m(e.simple?"simple":e.direction)])},[ce(u.$slots,"default"),L(p(s))],2))}}),eye=Q2e,tye=q({name:"ElStep",__name:"item",props:J2e,setup(e){const t=e,n=ke("step"),a=F(-1),r=F({}),o=F(""),i=Ze(_D),l=Et();let s=0,u=0;mt(()=>{ye([()=>i.props.active,()=>i.props.processStatus,()=>i.props.finishStatus],([M],[E])=>{u=E||0,s=M-u,k(M)},{immediate:!0})});const c=D(()=>t.status||o.value),d=D(()=>{const M=i.steps.value[a.value-1];return M?M.internalStatus.value:"wait"}),v=D(()=>i.props.alignCenter),f=D(()=>i.props.direction==="vertical"),h=D(()=>i.props.simple),m=D(()=>i.steps.value.length),g=D(()=>{var M;return((M=i.steps.value[m.value-1])==null?void 0:M.uid)===l.uid}),y=D(()=>h.value?"":i.props.space),b=D(()=>[n.b(),n.is(h.value?"simple":i.props.direction),n.is("flex",g.value&&!y.value&&!v.value),n.is("center",v.value&&!f.value&&!h.value)]),_=D(()=>{const M={flexBasis:Je(y.value)?`${y.value}px`:y.value?y.value:`${100/(m.value-(v.value?0:1))}%`};return f.value||g.value&&(M.maxWidth=`${100/m.value}%`),M}),w=M=>{a.value=M},S=M=>{const E=M==="wait",A={transitionDelay:`${Math.abs(s)===1?0:s>0?(a.value+1-u)*150:-(a.value+1-i.props.active)*150}ms`},I=M===i.props.processStatus||E?0:100;A.borderWidth=I&&!h.value?"1px":0,A[i.props.direction==="vertical"?"height":"width"]=`${I}%`,r.value=A},k=M=>{M>a.value?o.value=i.props.finishStatus:M===a.value&&d.value!=="error"?o.value=i.props.processStatus:o.value="wait";const E=i.steps.value[a.value-1];E&&E.calcProgress(o.value)},T={uid:l.uid,getVnode:()=>l.vnode,currentStatus:c,internalStatus:o,setIndex:w,calcProgress:S};return i.addStep(T),rn(()=>{i.removeStep(T)}),(M,E)=>(x(),O("div",{style:et(_.value),class:U(b.value)},[te(" icon & line "),C("div",{class:U([p(n).e("head"),p(n).is(c.value)])},[h.value?te("v-if",!0):(x(),O("div",{key:0,class:U(p(n).e("line"))},[C("i",{class:U(p(n).e("line-inner")),style:et(r.value)},null,6)],2)),C("div",{class:U([p(n).e("icon"),p(n).is(e.icon||M.$slots.icon?"icon":"text")])},[ce(M.$slots,"icon",{},()=>[e.icon?(x(),ne(p(Xe),{key:0,class:U(p(n).e("icon-inner"))},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1},8,["class"])):c.value==="success"?(x(),ne(p(Xe),{key:1,class:U([p(n).e("icon-inner"),p(n).is("status")])},{default:$(()=>[L(p(Os))]),_:1},8,["class"])):c.value==="error"?(x(),ne(p(Xe),{key:2,class:U([p(n).e("icon-inner"),p(n).is("status")])},{default:$(()=>[L(p(so))]),_:1},8,["class"])):h.value?te("v-if",!0):(x(),O("div",{key:3,class:U(p(n).e("icon-inner"))},Q(a.value+1),3))])],2)],2),te(" title & description "),C("div",{class:U(p(n).e("main"))},[C("div",{class:U([p(n).e("title"),p(n).is(c.value)])},[ce(M.$slots,"title",{},()=>[ue(Q(e.title),1)])],2),h.value?(x(),O("div",{key:0,class:U(p(n).e("arrow"))},null,2)):(x(),O("div",{key:1,class:U([p(n).e("description"),p(n).is(c.value)])},[ce(M.$slots,"description",{},()=>[ue(Q(e.description),1)])],2))],2)],6))}}),wD=tye;const nye=wt(eye,{Step:wD}),aye=On(wD),SD=e=>["",...ii].includes(e),rye=$e({modelValue:{type:[Boolean,String,Number],default:!1},disabled:{type:Boolean,default:void 0},loading:Boolean,size:{type:String,validator:SD},width:{type:[String,Number],default:""},inlinePrompt:Boolean,inactiveActionIcon:{type:pn},activeActionIcon:{type:pn},activeIcon:{type:pn},inactiveIcon:{type:pn},activeText:{type:String,default:""},inactiveText:{type:String,default:""},activeValue:{type:[Boolean,String,Number],default:!0},inactiveValue:{type:[Boolean,String,Number],default:!1},name:{type:String,default:""},validateEvent:{type:Boolean,default:!0},beforeChange:{type:ae(Function)},id:String,tabindex:{type:[String,Number]},...dr(["ariaLabel"])}),oye={[bt]:e=>vn(e)||Ye(e)||Je(e),[Ot]:e=>vn(e)||Ye(e)||Je(e),[ua]:e=>vn(e)||Ye(e)||Je(e)},iye=["id","aria-checked","aria-disabled","aria-label","name","true-value","false-value","disabled","tabindex"],lye=["aria-hidden"],sye={key:1},uye={key:1},cye=["aria-hidden"],Yp="ElSwitch";var dye=q({name:Yp,__name:"switch",props:rye,emits:oye,setup(e,{expose:t,emit:n}){const a=e,r=n,{formItem:o}=Ea(),i=ca(),l=ke("switch"),{inputId:s}=Yr(a,{formItemContext:o}),u=$n(D(()=>{if(a.loading)return!0})),c=F(a.modelValue!==!1),d=Ht(),v=D(()=>[l.b(),l.m(i.value),l.is("disabled",u.value),l.is("checked",y.value)]),f=D(()=>[l.e("label"),l.em("label","left"),l.is("active",!y.value)]),h=D(()=>[l.e("label"),l.em("label","right"),l.is("active",y.value)]),m=D(()=>({width:Vn(a.width)}));ye(()=>a.modelValue,()=>{c.value=!0});const g=D(()=>c.value?a.modelValue:!1),y=D(()=>g.value===a.activeValue);[a.activeValue,a.inactiveValue].includes(g.value)||(un(Yp,"model-value must be active-value or inactive-value"),r(bt,a.inactiveValue),r(Ot,a.inactiveValue),r(ua,a.inactiveValue)),ye(y,S=>{var k;d.value.checked=S,a.validateEvent&&((k=o==null?void 0:o.validate)==null||k.call(o,"change").catch(kt))});const b=()=>{const S=y.value?a.inactiveValue:a.activeValue;r(bt,S),r(Ot,S),r(ua,S),He(()=>{d.value.checked=y.value})},_=()=>{if(u.value)return;const{beforeChange:S}=a;if(!S){b();return}const k=S();[Ls(k),vn(k)].includes(!0)||Dn(Yp,"beforeChange must return type `Promise` or `boolean`"),Ls(k)?k.then(T=>{T&&b()}).catch(T=>{un(Yp,`some error occurred: ${T}`)}):k&&b()},w=()=>{var S,k;(k=(S=d.value)==null?void 0:S.focus)==null||k.call(S)};return mt(()=>{d.value.checked=y.value}),t({focus:w,checked:y}),(S,k)=>(x(),O("div",{class:U(v.value),onClick:at(_,["prevent"])},[C("input",{id:p(s),ref_key:"input",ref:d,class:U(p(l).e("input")),type:"checkbox",role:"switch","aria-checked":y.value,"aria-disabled":p(u),"aria-label":e.ariaLabel,name:e.name,"true-value":e.activeValue,"false-value":e.inactiveValue,disabled:p(u),tabindex:e.tabindex,onChange:b,onKeydown:kn(_,["enter"])},null,42,iye),!e.inlinePrompt&&(e.inactiveIcon||e.inactiveText||S.$slots.inactive)?(x(),O("span",{key:0,class:U(f.value)},[ce(S.$slots,"inactive",{},()=>[e.inactiveIcon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.inactiveIcon)))]),_:1})):te("v-if",!0),!e.inactiveIcon&&e.inactiveText?(x(),O("span",{key:1,"aria-hidden":y.value},Q(e.inactiveText),9,lye)):te("v-if",!0)])],2)):te("v-if",!0),C("span",{class:U(p(l).e("core")),style:et(m.value)},[e.inlinePrompt?(x(),O("div",{key:0,class:U(p(l).e("inner"))},[y.value?(x(),O("div",{key:1,class:U(p(l).e("inner-wrapper"))},[ce(S.$slots,"active",{},()=>[e.activeIcon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.activeIcon)))]),_:1})):te("v-if",!0),!e.activeIcon&&e.activeText?(x(),O("span",uye,Q(e.activeText),1)):te("v-if",!0)])],2)):(x(),O("div",{key:0,class:U(p(l).e("inner-wrapper"))},[ce(S.$slots,"inactive",{},()=>[e.inactiveIcon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.inactiveIcon)))]),_:1})):te("v-if",!0),!e.inactiveIcon&&e.inactiveText?(x(),O("span",sye,Q(e.inactiveText),1)):te("v-if",!0)])],2))],2)):te("v-if",!0),C("div",{class:U(p(l).e("action"))},[e.loading?(x(),ne(p(Xe),{key:0,class:U(p(l).is("loading"))},{default:$(()=>[L(p(Mr))]),_:1},8,["class"])):y.value?ce(S.$slots,"active-action",{key:1},()=>[e.activeActionIcon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.activeActionIcon)))]),_:1})):te("v-if",!0)]):y.value?te("v-if",!0):ce(S.$slots,"inactive-action",{key:2},()=>[e.inactiveActionIcon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.inactiveActionIcon)))]),_:1})):te("v-if",!0)])],2)],6),!e.inlinePrompt&&(e.activeIcon||e.activeText||S.$slots.active)?(x(),O("span",{key:1,class:U(h.value)},[ce(S.$slots,"active",{},()=>[e.activeIcon?(x(),ne(p(Xe),{key:0},{default:$(()=>[(x(),ne(St(e.activeIcon)))]),_:1})):te("v-if",!0),!e.activeIcon&&e.activeText?(x(),O("span",{key:1,"aria-hidden":!y.value},Q(e.activeText),9,cye)):te("v-if",!0)])],2)):te("v-if",!0)],2))}}),fye=dye;const vye=wt(fye),hye={data:{type:Array,default:()=>[]},size:pa,width:[String,Number],height:[String,Number],maxHeight:[String,Number],fit:{type:Boolean,default:!0},stripe:Boolean,border:Boolean,rowKey:[String,Function],showHeader:{type:Boolean,default:!0},showSummary:Boolean,sumText:String,summaryMethod:Function,rowClassName:[String,Function],rowStyle:[Object,Function],cellClassName:[String,Function],cellStyle:[Object,Function],headerRowClassName:[String,Function],headerRowStyle:[Object,Function],headerCellClassName:[String,Function],headerCellStyle:[Object,Function],highlightCurrentRow:Boolean,currentRowKey:[String,Number],emptyText:String,expandRowKeys:Array,defaultExpandAll:Boolean,rowExpandable:{type:Function},defaultSort:Object,tooltipEffect:String,tooltipOptions:Object,spanMethod:Function,selectOnIndeterminate:{type:Boolean,default:!0},indent:{type:Number,default:16},treeProps:{type:Object,default:()=>({hasChildren:"hasChildren",children:"children",checkStrictly:!1})},lazy:Boolean,load:Function,style:{type:[String,Object,Array,Boolean],default:()=>({})},className:{type:String,default:""},tableLayout:{type:String,default:"fixed"},scrollbarAlwaysOn:Boolean,flexible:Boolean,showOverflowTooltip:{type:[Boolean,Object],default:void 0},tooltipFormatter:Function,appendFilterPanelTo:String,scrollbarTabindex:{type:[Number,String],default:void 0},allowDragLastColumn:{type:Boolean,default:!0},preserveExpandedContent:Boolean,nativeScrollbar:Boolean};let pye=1,mye=1;const gye=e=>`${e}-table_${pye++}`,yye=e=>`${e}_column_${mye++}`,dy=function(e){var t;return(t=e.target)==null?void 0:t.closest("td")},bye=function(e,t,n,a,r){if(!t&&!a&&(!r||Ae(r)&&!r.length))return e;Ye(n)?n=n==="descending"?-1:1:n=n&&n<0?-1:1;const o=a?null:function(l,s){return r?SA(wa(r),u=>Ye(u)?na(l,u):u(l,s,e)):(t!=="$key"&&_t(l)&&"$value"in l&&(l=l.$value),[_t(l)?t?na(l,t):null:l])},i=function(l,s){var u,c,d,v,f;if(a)return a(l.value,s.value);for(let h=0,m=((u=l.key)==null?void 0:u.length)??0;h((f=s.key)==null?void 0:f[h]))return 1}return 0};return e.map((l,s)=>({value:l,index:s,key:o?o(l,s):null})).sort((l,s)=>{let u=i(l,s);return u||(u=l.index-s.index),u*+n}).map(l=>l.value)},CD=function(e,t){let n=null;return e.columns.forEach(a=>{a.id===t&&(n=a)}),n},_ye=function(e,t){let n=null;for(let a=0;a{if(!e)throw new Error("Row is required when get row identity");if(Ye(t)){if(!t.includes("."))return`${e[t]}`;const n=t.split(".");let a=e;for(const r of n)a=a[r];return`${a}`}else if(Qe(t))return t.call(null,e);return""},Sd=function(e,t,n=!1,a="children"){const r=e||[],o={};return r.forEach((i,l)=>{if(o[ra(i,t)]={row:i,index:l},n){const s=i[a];Ae(s)&&Object.assign(o,Sd(s,t,!0,a))}}),o};function wye(e,t){const n={};let a;for(a in e)n[a]=e[a];for(a in t)if(en(t,a)){const r=t[a];zt(r)||(n[a]=r)}return n}function Mw(e){return e===""||zt(e)||(e=Number.parseInt(e,10),Number.isNaN(e)&&(e="")),e}function xD(e){return e===""||zt(e)||(e=Mw(e),Number.isNaN(e)&&(e=80)),e}function Sye(e){return Je(e)?e:Ye(e)?/^\d+(?:px)?$/.test(e)?Number.parseInt(e,10):e:null}function Cye(...e){return e.length===0?t=>t:e.length===1?e[0]:e.reduce((t,n)=>(...a)=>t(n(...a)))}function ld(e,t,n,a,r,o,i){let l=o??0,s=!1;const c=(()=>{if(!i)return e.indexOf(t);const m=ra(t,i);return e.findIndex(g=>ra(g,i)===m)})(),d=c!==-1,v=r==null?void 0:r.call(null,t,l),f=m=>{m==="add"?e.push(t):e.splice(c,1),s=!0},h=m=>{let g=0;const y=(a==null?void 0:a.children)&&m[a.children];return y&&Ae(y)&&(g+=y.length,y.forEach(b=>{g+=h(b)})),g};return(!r||v)&&(vn(n)?n&&!d?f("add"):!n&&d&&f("remove"):f(d?"remove":"add")),!(a!=null&&a.checkStrictly)&&(a!=null&&a.children)&&Ae(t[a.children])&&t[a.children].forEach(m=>{const g=ld(e,m,n??!d,a,r,l+1,i);l+=h(m)+1,g&&(s=g)}),s}function xye(e,t,n="children",a="hasChildren",r=!1){const o=l=>!(Ae(l)&&l.length);function i(l,s,u){t(l,s,u),s.forEach(c=>{if(c[a]&&r){t(c,null,u+1);return}const d=c[n];o(d)||i(c,d,u+1)})}e.forEach(l=>{if(l[a]&&r){t(l,null,0);return}const s=l[n];o(s)||i(l,s,0)})}const kye=(e,t,n,a)=>{const r=Qe(a==null?void 0:a.tooltipFormatter)?a.tooltipFormatter({row:n,column:a,cellValue:Cs(n,a.property).value}):void 0;return bn(r)?{slotContent:r,content:null,strategy:"fixed",...e}:{slotContent:null,content:r??t,strategy:"fixed",...e}};let Wn=null;function Tye(e,t,n,a,r,o){var f;const i=kye(e,t,n,a),l={...i,slotContent:void 0};if((Wn==null?void 0:Wn.trigger)===r){const h=(f=Wn.vm)==null?void 0:f.component;xA(h==null?void 0:h.props,l),h&&i.slotContent&&(h.slots.content=()=>[i.slotContent]);return}Wn==null||Wn();const s=o==null?void 0:o.refs.tableWrapper,u=s==null?void 0:s.dataset.prefix,c=L(Ma,{virtualTriggering:!0,virtualRef:r,appendTo:s,placement:"top",transition:"none",offset:0,hideAfter:0,...l},i.slotContent?{content:()=>i.slotContent}:void 0);c.appContext={...o.appContext,...o};const d=document.createElement("div");Ps(c,d),c.component.exposed.onOpen();const v=s==null?void 0:s.querySelector(`.${u}-scrollbar__wrap`);Wn=()=>{var m,g;(g=(m=c.component)==null?void 0:m.exposed)!=null&&g.onClose&&c.component.exposed.onClose(),Ps(null,d);const h=Wn;v==null||v.removeEventListener("scroll",h),h.trigger=void 0,h.vm=void 0,Wn=null},Wn.trigger=r??void 0,Wn.vm=c,v==null||v.addEventListener("scroll",Wn)}function kD(e){return e.children?SA(e.children,kD):[e]}function $x(e,t){return e+t.colSpan}const TD=(e,t,n,a)=>{let r=0,o=e;const i=n.states.columns.value;if(a){const s=kD(a[e]);r=i.slice(0,i.indexOf(s[0])).reduce($x,0),o=r+s.reduce($x,0)-1}else r=e;let l;switch(t){case"left":o=i.length-n.states.rightFixedLeafColumnsLength.value&&(l="right");break;default:o=i.length-n.states.rightFixedLeafColumnsLength.value&&(l="right")}return l?{direction:l,start:r,after:o}:{}},Ew=(e,t,n,a,r,o=0)=>{const i=[],{direction:l,start:s,after:u}=TD(t,n,a,r);if(l){const c=l==="left";i.push(`${e}-fixed-column--${l}`),c&&u+o===a.states.fixedLeafColumnsLength.value-1?i.push("is-last-column"):!c&&s-o===a.states.columns.value.length-a.states.rightFixedLeafColumnsLength.value&&i.push("is-first-column")}return i};function Rx(e,t){return e+(E1(t.realWidth)||Number.isNaN(t.realWidth)?Number(t.width):t.realWidth)}const Aw=(e,t,n,a)=>{const{direction:r,start:o=0,after:i=0}=TD(e,t,n,a);if(!r)return;const l={},s=r==="left",u=n.states.columns.value;return s?l.left=u.slice(0,o).reduce(Rx,0):l.right=u.slice(i+1).reverse().reduce(Rx,0),l},jd=(e,t)=>{e&&(Number.isNaN(e[t])||(e[t]=`${e[t]}px`))};function MD(e){return e.some(t=>bn(t)?!(t.type===oa||t.type===Ve&&!MD(t.children)):!0)?e:null}function Mye(e){const t=Et(),n=F(!1),a=F([]),r=(u,c)=>{const d=t.store.states.rowExpandable.value;return(d==null?void 0:d(u,c))??!0};return{updateExpandRows:()=>{const u=e.data.value||[],c=e.rowKey.value;if(n.value)a.value=t.store.states.rowExpandable.value?u.filter(r):u.slice();else if(c){const d=Sd(a.value,c);a.value=u.filter((v,f)=>!!d[ra(v,c)]&&r(v,f))}else a.value=[]},toggleRowExpansion:(u,c)=>{const d=(e.data.value||[]).indexOf(u);d>-1&&!r(u,d)||ld(a.value,u,c,void 0,void 0,void 0,e.rowKey.value)&&t.emit("expand-change",u,a.value.slice())},setExpandRowKeys:u=>{t.store.assertRowKey();const c=e.data.value||[],d=e.rowKey.value,v=Sd(c,d);a.value=u.reduce((f,h)=>{const m=v[h];return m&&r(m.row,m.index)&&f.push(m.row),f},[])},isRowExpanded:u=>{const c=e.rowKey.value;return c?!!Sd(a.value,c)[ra(u,c)]:a.value.includes(u)},states:{expandRows:a,defaultExpandAll:n}}}function Eye(e){const t=Et(),n=F(null),a=F(null),r=u=>{t.store.assertRowKey(),n.value=u,i(u)},o=()=>{n.value=null},i=u=>{const{data:c,rowKey:d}=e,v=a.value;let f=null;d.value&&(f=(p(c)||[]).find(h=>ra(h,d.value)===u)??null),a.value=f??null,t.emit("current-change",a.value,v)};return{setCurrentRowKey:r,restoreCurrentRowKey:o,setCurrentRowByKey:i,updateCurrentRow:u=>{const c=a.value;if(u&&u!==c){a.value=u,t.emit("current-change",a.value,c);return}!u&&c&&(a.value=null,t.emit("current-change",null,c))},updateCurrentRowData:()=>{const u=e.rowKey.value,c=e.data.value||[],d=a.value;d&&!c.includes(d)?u?i(ra(d,u)):(a.value=null,t.emit("current-change",null,d)):n.value&&(i(n.value),o())},states:{_currentRowKey:n,currentRow:a}}}function Aye(e){const t=F([]),n=F({}),a=F(16),r=F(!1),o=F({}),i=F("hasChildren"),l=F("children"),s=F(!1),u=Et(),c=D(()=>e.rowKey.value?v(e.data.value||[]):{}),d=D(()=>{const w=e.rowKey.value,S=Object.keys(o.value),k={};return S.length&&S.forEach(T=>{if(o.value[T].length){const M={children:[]};o.value[T].forEach(E=>{const A=ra(E,w);M.children.push(A),E[i.value]&&!k[A]&&(k[A]={children:[]})}),k[T]=M}}),k}),v=w=>{const S=e.rowKey.value,k={};return xye(w,(T,M,E)=>{const A=ra(T,S);Ae(M)?k[A]={children:M.map(I=>ra(I,S)),level:E}:r.value&&(k[A]={children:[],lazy:!0,level:E})},l.value,i.value,r.value),k},f=(w=!1,S)=>{var A,I;S||(S=(A=u.store)==null?void 0:A.states.defaultExpandAll.value);const k=c.value,T=d.value,M=Object.keys(k),E={};if(M.length){const P=p(n),R=[],G=(H,z)=>{if(w)return t.value?S||t.value.includes(z):!!(S||H!=null&&H.expanded);{const B=S||t.value&&t.value.includes(z);return!!(H!=null&&H.expanded||B)}};M.forEach(H=>{const z=P[H],B={...k[H]};if(B.expanded=G(z,H),B.lazy){const{loaded:W=!1,loading:N=!1}=z||{};B.loaded=!!W,B.loading=!!N,R.push(H)}E[H]=B});const K=Object.keys(T);r.value&&K.length&&R.length&&K.forEach(H=>{var W;const z=P[H],B=T[H].children;if(R.includes(H)){if(((W=E[H].children)==null?void 0:W.length)!==0)throw new Error("[ElTable]children must be an empty array.");E[H].children=B}else{const{loaded:N=!1,loading:V=!1}=z||{};E[H]={lazy:!0,loaded:!!N,loading:!!V,expanded:G(z,H),children:B,level:void 0}}})}n.value=E,(I=u.store)==null||I.updateTableScrollY()};ye(()=>t.value,()=>{f(!0)},{deep:!0}),ye(()=>c.value,()=>{f()}),ye(()=>d.value,()=>{f()});const h=w=>{t.value=w,f()},m=w=>r.value&&w&&"loaded"in w&&!w.loaded,g=(w,S)=>{u.store.assertRowKey();const k=e.rowKey.value,T=ra(w,k),M=T&&n.value[T];if(T&&M&&"expanded"in M){const E=M.expanded;S=zt(S)?!M.expanded:S,n.value[T].expanded=S,E!==S&&u.emit("expand-change",w,S),S&&m(M)&&b(w,T,M),u.store.updateTableScrollY()}},y=w=>{u.store.assertRowKey();const S=e.rowKey.value,k=ra(w,S),T=n.value[k];m(T)?b(w,k,T):g(w,void 0)},b=(w,S,k)=>{const{load:T}=u.props;T&&!n.value[S].loaded&&(n.value[S].loading=!0,T(w,k,M=>{if(!Ae(M))throw new TypeError("[ElTable] data must be an array");n.value[S].loading=!1,n.value[S].loaded=!0,n.value[S].expanded=!0,M.length&&(o.value={...o.value,[S]:M}),u.emit("expand-change",w,!0)}))};return{loadData:b,loadOrToggle:y,toggleTreeExpansion:g,updateTreeExpandKeys:h,updateTreeData:f,updateKeyChildren:(w,S)=>{const{lazy:k,rowKey:T}=u.props;if(k){if(!T)throw new Error("[Table] rowKey is required in updateKeyChild");o.value[w]&&(o.value={...o.value,[w]:S})}},normalize:v,states:{expandRowKeys:t,treeData:n,indent:a,lazy:r,lazyTreeNodeMap:o,lazyColumnIdentifier:i,childrenColumnName:l,checkStrictly:s}}}const Iye=(e,t)=>{const n=t.sortingColumn;return!n||Ye(n.sortable)?e:bye(e,t.sortProp,t.sortOrder,n.sortMethod,n.sortBy)},am=e=>{const t=[];return e.forEach(n=>{n.children&&n.children.length>0?t.push.apply(t,am(n.children)):t.push(n)}),t};function Lye(){var Gt;const e=Et(),{size:t}=Ta((Gt=e.proxy)==null?void 0:Gt.$props),n=F(null),a=F([]),r=F([]),o=F(!1),i=F([]),l=F([]),s=F([]),u=F([]),c=F([]),d=F([]),v=F([]),f=F([]),h=[],m=F(0),g=F(0),y=F(0),b=F(!1),_=F([]),w=F({}),S=F(!1),k=F(!1),T=F(null),M=F(null),E=F({}),A=F(null),I=F(null),P=F(null),R=F(null),G=F(null);let K=[];const H=D(()=>n.value?Sd(_.value,n.value):void 0),z=je=>{var Ct;const{childrenColumnName:ft,lazyTreeNodeMap:ht}=e.store.states,pt=je[ft.value]??[];if(!n.value)return pt;const It=ra(je,n.value);return[...((Ct=ht.value)==null?void 0:Ct[It])??[],...pt]};ye(a,()=>{var je;e.state&&(V(!1),e.props.tableLayout==="auto"&&((je=e.refs.tableHeaderRef)==null||je.updateFixedColumnStyle()))},{deep:!0});const B=()=>{if(!n.value)throw new Error("[ElTable] prop row-key is required")},W=je=>{var ft;(ft=je.children)==null||ft.forEach(ht=>{ht.fixed=je.fixed,W(ht)})},N=()=>{i.value.forEach($t=>{W($t)}),u.value=i.value.filter($t=>[!0,"left"].includes($t.fixed));const je=i.value.find($t=>$t.type==="selection");let ft;je&&je.fixed!=="right"&&!u.value.includes(je)&&i.value.indexOf(je)===0&&u.value.length&&(u.value.unshift(je),ft=!0),c.value=i.value.filter($t=>$t.fixed==="right");const ht=i.value.filter($t=>(ft?$t.type!=="selection":!0)&&!$t.fixed);l.value=Array.from(u.value).concat(ht).concat(c.value);const pt=am(ht),It=am(u.value),Ct=am(c.value);m.value=pt.length,g.value=It.length,y.value=Ct.length,s.value=Array.from(It).concat(pt).concat(Ct),o.value=u.value.length>0||c.value.length>0},V=(je,ft=!1)=>{je&&N(),ft?e.state.doLayout():e.state.debouncedUpdateLayout()},Y=je=>H.value?!!H.value[ra(je,n.value)]:_.value.includes(je),X=D(()=>{const je=new Map;if(!n.value||!T.value)return je;let ft=0;const ht=pt=>{Ae(pt)&&pt.forEach(It=>{const Ct=ra(It,n.value);je.set(Ct,ft),ft+=1;const $t=z(It);$t.length&&ht($t)})};return ht(a.value||[]),je}),ee=(je={})=>{const{emitChange:ft=!0}=je;if(Rt.checkStrictly.value||!n.value){w.value={},K=[];return}const ht=n.value,pt=je.rowIndexMap??X.value,It=T.value,Ct=new WeakMap,$t=Ee=>{const de=Ct.get(Ee);if(de)return de;const it=ra(Ee,ht);return Ct.set(Ee,it),it},Sn={},Fe=[],qe=new Set(_.value.map(Ee=>$t(Ee))),ut=[];let Ie=!1;const Ke=(Ee,de,it)=>{const Dt=qe.has(de);it&&!Dt?(ut.push(Ee),qe.add(de),Ie=!0):!it&&Dt&&(qe.delete(de),Ie=!0)},Ge=Ee=>{let de=0,it=0;return Ae(Ee)?(Ee.forEach(Dt=>{const sn=$t(Dt),gn=z(Dt);let Aa=0,jr=0;if(gn.length){const Ia=Ge(gn);Aa=Ia.selectedCount,jr=Ia.selectableCount}const vr=It?It.call(null,Dt,pt.get(sn)??0):!0;if(vr&&jr>0){const Ia=Aa===jr;!Ia&&Aa!==0&&(Sn[sn]=!0,Fe.push(Dt)),Ke(Dt,sn,Ia)}vr&&(it+=1,qe.has(sn)&&(de+=1)),de+=Aa,it+=jr}),{selectedCount:de,selectableCount:it}):{selectedCount:de,selectableCount:it}};if(Ge(a.value||[]),Ie){const Ee=_.value.filter(de=>qe.has($t(de)));ut.forEach(de=>{qe.has($t(de))&&Ee.push(de)}),_.value=Ee}w.value=Sn,K=Fe,Ie&&ft&&e.emit("selection-change",_.value?_.value.slice():[])},re=()=>{b.value=!1;const je=_.value;_.value=[],w.value={},K=[],je.length&&e.emit("selection-change",[])},ie=()=>{var ft,ht;let je;if(n.value){je=[];const pt=(ht=(ft=e==null?void 0:e.store)==null?void 0:ft.states)==null?void 0:ht.childrenColumnName.value,It=Sd(a.value,n.value,!0,pt),{lazyTreeNodeMap:Ct}=e.store.states;Ct.value&&Object.entries(Ct.value).forEach(([$t,Sn])=>{It[$t]&&Sn.forEach(Fe=>{const qe=ra(Fe,n.value);It[qe]||(It[qe]={row:Fe,index:-1})})});for(const $t in H.value)en(H.value,$t)&&!It[$t]&&je.push(H.value[$t].row)}else je=_.value.filter(pt=>!a.value.includes(pt));if(je.length){const pt=_.value.filter(It=>!je.includes(It));_.value=pt,ee({emitChange:!1}),e.emit("selection-change",[...pt])}},le=()=>(_.value||[]).slice(),ve=()=>K.slice(),he=(je,ft,ht)=>{var qe;if(!n.value||Rt.checkStrictly.value||!Rt.lazy.value)return;const{lazyTreeNodeMap:pt,childrenColumnName:It}=e.store.states,Ct=ra(je,n.value),$t=((qe=pt.value)==null?void 0:qe[Ct])??[],Sn=je[It.value]??[],Fe={children:It.value,checkStrictly:!1};for(const ut of $t){const Ie=ht.get(ra(ut,n.value))??0;ld(_.value,ut,ft,Fe,T.value,Ie,n.value),he(ut,ft,ht)}for(const ut of Sn)he(ut,ft,ht)},J=(je,ft,ht=!0,pt=!1)=>{var Ct,$t,Sn,Fe;const It={children:($t=(Ct=e==null?void 0:e.store)==null?void 0:Ct.states)==null?void 0:$t.childrenColumnName.value,checkStrictly:(Fe=(Sn=e==null?void 0:e.store)==null?void 0:Sn.states)==null?void 0:Fe.checkStrictly.value};if(ld(_.value,je,ft,It,pt?void 0:T.value,a.value.indexOf(je),n.value)){Rt.lazy.value&&!Rt.checkStrictly.value?(he(je,ft??Y(je),X.value),ee({emitChange:!1,rowIndexMap:X.value})):ee({emitChange:!1});const qe=(_.value||[]).slice();ht&&e.emit("select",qe,je),e.emit("selection-change",qe)}},Z=()=>{var Sn,Fe;const je=k.value?!b.value:!(b.value||_.value.length);b.value=je;let ft=!1,ht=0;const pt=(Fe=(Sn=e==null?void 0:e.store)==null?void 0:Sn.states)==null?void 0:Fe.rowKey.value,{childrenColumnName:It}=e.store.states,Ct={children:It.value,checkStrictly:!1};a.value.forEach((qe,ut)=>{const Ie=ut+ht;ld(_.value,qe,je,Ct,T.value,Ie,pt)&&(ft=!0),ht+=se(ra(qe,pt))});const $t=X.value;if(Rt.lazy.value&&!Rt.checkStrictly.value&&pt)for(const qe of Object.values(Rt.lazyTreeNodeMap.value))for(const ut of qe){const Ie=$t.get(ra(ut,pt))??0;ld(_.value,ut,je,Ct,T.value,Ie,pt)&&(ft=!0),he(ut,je,$t)}ee({emitChange:!1,rowIndexMap:$t}),ft&&e.emit("selection-change",_.value?[..._.value]:[]),e.emit("select-all",(_.value||[]).slice())},j=()=>{var It;if(((It=a.value)==null?void 0:It.length)===0){b.value=!1;return}let je=0,ft=0;const ht=Ct=>{for(const $t of Ct){const Sn=T.value&&T.value.call(null,$t,je);if(Y($t))ft++;else if(!T.value||Sn)return!1;je++;const Fe=z($t);if(Fe.length&&!ht(Fe))return!1}return!0},pt=ht(a.value||[]);b.value=ft===0?!1:pt},oe=je=>{if(!n.value)return!1;const ft=ra(je,n.value);return!!w.value[ft]},se=je=>{var It;if(!e||!e.store)return 0;const{treeData:ft}=e.store.states;let ht=0;const pt=(It=ft.value[je])==null?void 0:It.children;return pt&&(ht+=pt.length,pt.forEach(Ct=>{ht+=se(Ct)})),ht},fe=(je,ft)=>{const ht={};return wa(je).forEach(pt=>{E.value[pt.id]=ft,ht[pt.columnKey||pt.id]=ft}),ht},ge=(je,ft,ht)=>{I.value&&I.value!==je&&(I.value.order=null),I.value=je,P.value=ft,R.value=ht},Se=()=>{let je=p(r);Object.keys(E.value).forEach(ft=>{const ht=E.value[ft];if(!ht||ht.length===0)return;const pt=CD({columns:s.value},ft);pt&&pt.filterMethod&&(je=je.filter(It=>ht.some(Ct=>pt.filterMethod.call(null,Ct,It,pt))))}),A.value=je},De=()=>{a.value=Iye(A.value??[],{sortingColumn:I.value,sortProp:P.value,sortOrder:R.value})},Te=(je=void 0)=>{je!=null&&je.filter||Se(),De()},Re=je=>{const{tableHeaderRef:ft}=e.refs;if(!ft)return;const ht=Object.assign({},ft.filterPanels),pt=Object.keys(ht);if(pt.length)if(Ye(je)&&(je=[je]),Ae(je)){const It=je.map(Ct=>_ye({columns:s.value},Ct));pt.forEach(Ct=>{const $t=It.find(Sn=>Sn.id===Ct);$t&&($t.filteredValue=[])}),e.store.commit("filterChange",{column:It,values:[],silent:!0,multi:!0})}else pt.forEach(It=>{const Ct=s.value.find($t=>$t.id===It);Ct&&(Ct.filteredValue=[])}),E.value={},e.store.commit("filterChange",{column:{},values:[],silent:!0})},Ce=()=>{I.value&&(ge(null,null,null),e.store.commit("changeSortCondition",{silent:!0}))},{setExpandRowKeys:Le,toggleRowExpansion:me,updateExpandRows:Be,states:ze,isRowExpanded:xe}=Mye({data:a,rowKey:n}),{updateTreeExpandKeys:rt,toggleTreeExpansion:nt,updateTreeData:Pt,updateKeyChildren:Wt,loadOrToggle:Jt,states:Rt}=Aye({data:a,rowKey:n}),{updateCurrentRowData:Qt,updateCurrentRow:_e,setCurrentRowKey:Ne,states:tt}=Eye({data:a,rowKey:n}),Me=je=>{Le(je),rt(je)},Ue=(je,ft)=>{s.value.some(({type:ht})=>ht==="expand")?me(je,ft):nt(je,ft)};return ye(()=>Rt.checkStrictly.value,je=>{je?(w.value={},K=[]):ee({emitChange:!1}),j()}),ye(()=>Rt.lazyTreeNodeMap.value,()=>{var pt;if(!Rt.lazy.value||Rt.checkStrictly.value||!n.value)return;const je=X.value,ft=_.value.length;for(const It of Object.keys(Rt.lazyTreeNodeMap.value))(pt=H.value)!=null&&pt[It]&&he(H.value[It].row,!0,je);const ht=_.value.length!==ft;ee({emitChange:!ht,rowIndexMap:je}),j(),ht&&e.emit("selection-change",[..._.value])}),{assertRowKey:B,updateColumns:N,scheduleLayout:V,isSelected:Y,clearSelection:re,cleanSelection:ie,getSelectionRows:le,getHalfSelectionRows:ve,toggleRowSelection:J,_toggleAllSelection:Z,toggleAllSelection:null,updateAllSelected:j,updateSelectionByChildren:ee,getRowIndeterminate:oe,updateFilters:fe,updateCurrentRow:_e,updateSort:ge,execFilter:Se,execSort:De,execQuery:Te,clearFilter:Re,clearSort:Ce,toggleRowExpansion:me,setExpandRowKeysAdapter:Me,setCurrentRowKey:Ne,toggleRowExpansionAdapter:Ue,isRowExpanded:xe,updateExpandRows:Be,updateCurrentRowData:Qt,loadOrToggle:Jt,updateTreeData:Pt,updateKeyChildren:Wt,states:{tableSize:t,rowKey:n,data:a,_data:r,isComplex:o,_columns:i,originColumns:l,columns:s,fixedColumns:u,rightFixedColumns:c,leafColumns:d,fixedLeafColumns:v,rightFixedLeafColumns:f,updateOrderFns:h,leafColumnsLength:m,fixedLeafColumnsLength:g,rightFixedLeafColumnsLength:y,isAllSelected:b,selection:_,selectionIndeterminate:w,reserveSelection:S,selectOnIndeterminate:k,selectable:T,rowExpandable:M,filters:E,filteredData:A,sortingColumn:I,sortProp:P,sortOrder:R,hoverRow:G,...ze,...Rt,...tt}}}function Q4(e,t){return e.map(n=>{var a;return n.id===t.id?t:((a=n.children)!=null&&a.length&&(n.children=Q4(n.children,t)),n)})}function e_(e){e.forEach(t=>{var n,a;t.no=(n=t.getColumnIndex)==null?void 0:n.call(t),(a=t.children)!=null&&a.length&&e_(t.children)}),e.sort((t,n)=>t.no-n.no)}function Pye(){const e=Et(),t=Lye(),n=ke("table"),{t:a}=Vt();return{ns:n,t:a,...t,mutations:{setData(l,s){const u=p(l._data)!==s;l.data.value=s,l._data.value=s,e.store.execQuery(),e.store.updateCurrentRowData(),e.store.updateExpandRows(),e.store.updateTreeData(e.store.states.defaultExpandAll.value),p(l.reserveSelection)?e.store.assertRowKey():u?e.store.clearSelection():e.store.cleanSelection(),e.store.updateSelectionByChildren({emitChange:!1}),e.store.updateAllSelected(),e.$ready&&e.store.scheduleLayout()},insertColumn(l,s,u,c){var f;const d=p(l._columns);let v=[];u?(u&&!u.children&&(u.children=[]),(f=u.children)==null||f.push(s),v=Q4(d,u)):(d.push(s),v=d),e_(v),l._columns.value=v,l.updateOrderFns.push(c),s.type==="selection"&&(l.selectable.value=s.selectable,l.reserveSelection.value=s.reserveSelection),e.$ready&&(e.store.updateColumns(),e.store.scheduleLayout())},updateColumnOrder(l,s){var u;((u=s.getColumnIndex)==null?void 0:u.call(s))!==s.no&&(e_(l._columns.value),e.$ready&&e.store.updateColumns())},removeColumn(l,s,u,c){var f;const d=p(l._columns)||[];if(u)(f=u.children)==null||f.splice(u.children.findIndex(h=>h.id===s.id),1),He(()=>{var h;((h=u.children)==null?void 0:h.length)===0&&delete u.children}),l._columns.value=Q4(d,u);else{const h=d.indexOf(s);h>-1&&(d.splice(h,1),l._columns.value=d)}const v=l.updateOrderFns.indexOf(c);v>-1&&l.updateOrderFns.splice(v,1),e.$ready&&(e.store.updateColumns(),e.store.scheduleLayout())},sort(l,s){const{prop:u,order:c,init:d}=s;if(u){const v=p(l.columns).find(f=>f.property===u);v&&(v.order=c,e.store.updateSort(v,u,c),e.store.commit("changeSortCondition",{init:d}))}},changeSortCondition(l,s){const{sortingColumn:u,sortProp:c,sortOrder:d}=l,v=p(u),f=p(c),h=p(d);E1(h)&&(l.sortingColumn.value=null,l.sortProp.value=null),e.store.execQuery({filter:!0}),(!s||!(s.silent||s.init))&&e.emit("sort-change",{column:v,prop:f,order:h}),e.store.updateTableScrollY()},filterChange(l,s){const{column:u,values:c,silent:d}=s,v=e.store.updateFilters(u,c);e.store.execQuery(),d||e.emit("filter-change",v),e.store.updateTableScrollY()},toggleAllSelection(){var l,s;(s=(l=e.store).toggleAllSelection)==null||s.call(l)},rowSelectedChanged(l,s){e.store.toggleRowSelection(s),e.store.updateAllSelected()},setHoverRow(l,s){l.hoverRow.value=s},setCurrentRow(l,s){e.store.updateCurrentRow(s)}},commit:function(l,...s){const u=e.store.mutations;if(u[l])u[l].apply(e,[e.store.states,...s]);else throw new Error(`Action not found: ${l}`)},updateTableScrollY:function(){He(()=>e.layout.updateScrollY.apply(e.layout))}}}const Iw={rowKey:"rowKey",defaultExpandAll:"defaultExpandAll",rowExpandable:"rowExpandable",selectOnIndeterminate:"selectOnIndeterminate",indent:"indent",lazy:"lazy","treeProps.hasChildren":{key:"lazyColumnIdentifier",default:"hasChildren"},"treeProps.children":{key:"childrenColumnName",default:"children"},"treeProps.checkStrictly":{key:"checkStrictly",default:!1}};function Dye(e,t){if(!e)throw new Error("Table is required.");const n=Pye();return n.toggleAllSelection=Yi(n._toggleAllSelection,10),Object.keys(Iw).forEach(a=>{ED(AD(t,a),a,n)}),Oye(n,t),n}function Oye(e,t){Object.keys(Iw).forEach(n=>{ye(()=>AD(t,n),a=>{ED(a,n,e)})})}function ED(e,t,n){let a=e,r=Iw[t];_t(r)&&(a=a||r.default,r=r.key),n.states[r].value=a}function AD(e,t){if(t.includes(".")){const n=t.split(".");let a=e;return n.forEach(r=>{a=a[r]}),a}else return e[t]}var $ye=class{constructor(e){this.observers=[],this.table=null,this.store=null,this.columns=[],this.fit=!0,this.showHeader=!0,this.heightMap={},this.height=F(null),this.scrollX=F(!1),this.scrollY=F(!1),this.bodyWidth=F(null),this.fixedWidth=F(null),this.rightFixedWidth=F(null),this.gutterWidth=0;for(const t in e)en(e,t)&&(xn(this[t])?this[t].value=e[t]:this[t]=e[t]);if(!this.table)throw new Error("Table is required for Table Layout");if(!this.store)throw new Error("Store is required for Table Layout")}updateScrollY(){const e=this.height.value;if(E1(e))return!1;const t=this.table.refs.scrollBarRef;if(this.table.vnode.el&&(t!=null&&t.wrapRef)){let n=!0;const a=this.scrollY.value;return n=t.wrapRef.scrollHeight>t.wrapRef.clientHeight,this.scrollY.value=n,a!==n}return!1}setHeight(e,t="height"){if(!qt)return;const n=this.table.vnode.el;if(e=Sye(e),this.height.value=Number(e),this.heightMap[t]=e,!n&&(e||e===0)){He(()=>{this.heightMap[t]===e&&this.setHeight(e,t)});return}n&&Je(e)?(n.style[t]=`${e}px`,this.updateElsHeight()):n&&Ye(e)&&(n.style[t]=e,this.updateElsHeight())}setMaxHeight(e){this.setHeight(e,"max-height")}getFlattenColumns(){const e=[];return this.table.store.states.columns.value.forEach(t=>{t.isColumnGroup?e.push.apply(e,t.columns):e.push(t)}),e}updateElsHeight(){this.updateScrollY(),this.notifyObservers("scrollable")}headerDisplayNone(e){if(!e)return!0;let t=e;for(;t.tagName!=="DIV";){if(getComputedStyle(t).display==="none")return!0;t=t.parentElement}return!1}updateColumnsWidth(){var l;if(!qt)return;const e=this.fit,t=(l=this.table.vnode.el)==null?void 0:l.clientWidth;let n=0;const a=this.getFlattenColumns(),r=a.filter(s=>!Je(s.width));if(a.forEach(s=>{Je(s.width)&&s.realWidth&&(s.realWidth=null)}),r.length>0&&e){if(a.forEach(s=>{n+=Number(s.width||s.minWidth||80)}),n<=t){this.scrollX.value=!1;const s=t-n;if(r.length===1)r[0].realWidth=Number(r[0].minWidth||80)+s;else{const u=s/r.reduce((d,v)=>d+Number(v.minWidth||80),0);let c=0;r.forEach((d,v)=>{if(v===0)return;const f=Math.floor(Number(d.minWidth||80)*u);c+=f,d.realWidth=Number(d.minWidth||80)+f}),r[0].realWidth=Number(r[0].minWidth||80)+s-c}}else this.scrollX.value=!0,r.forEach(s=>{s.realWidth=Number(s.minWidth)});this.bodyWidth.value=Math.max(n,t),this.table.state.resizeState.value.width=this.bodyWidth.value}else a.forEach(s=>{!s.width&&!s.minWidth?s.realWidth=80:s.realWidth=Number(s.width||s.minWidth),n+=s.realWidth}),this.scrollX.value=n>t,this.bodyWidth.value=n;const o=this.store.states.fixedColumns.value;if(o.length>0){let s=0;o.forEach(u=>{s+=Number(u.realWidth||u.width)}),this.fixedWidth.value=s}const i=this.store.states.rightFixedColumns.value;if(i.length>0){let s=0;i.forEach(u=>{s+=Number(u.realWidth||u.width)}),this.rightFixedWidth.value=s}this.notifyObservers("columns")}addObserver(e){this.observers.push(e)}removeObserver(e){const t=this.observers.indexOf(e);t!==-1&&this.observers.splice(t,1)}notifyObservers(e){this.observers.forEach(t=>{var n,a;switch(e){case"columns":(n=t.state)==null||n.onColumnsChange(this);break;case"scrollable":(a=t.state)==null||a.onScrollableChange(this);break;default:throw new Error(`Table Layout don't have event ${e}.`)}})}};const di=Symbol("ElTable"),ID=e=>{const t=[];return e.forEach(n=>{n.children?(t.push(n),t.push.apply(t,ID(n.children))):t.push(n)}),t},LD=e=>{let t=1;const n=(r,o)=>{if(o&&(r.level=o.level+1,t{n(l,r),i+=l.colSpan}),r.colSpan=i}else r.colSpan=1};e.forEach(r=>{r.level=1,n(r,void 0)});const a=[];for(let r=0;r{r.children?(r.rowSpan=1,r.children.forEach(o=>o.isSubColumn=!0)):r.rowSpan=t-r.level+1,a[r.level-1].push(r)}),a};function Rye(e){const t=Ze(di),n=D(()=>LD(e.store.states.originColumns.value));return{isGroup:D(()=>{const o=n.value.length>1;return o&&t&&(t.state.isGroup.value=!0),o}),toggleAllSelection:o=>{o.stopPropagation(),t==null||t.store.commit("toggleAllSelection")},columnRows:n}}var Nye=q({name:"ElTableFilterPanel",components:{ElCheckbox:Zo,ElCheckboxGroup:cw,ElScrollbar:ni,ElTooltip:Ma,ElIcon:Xe,ArrowDown:ui,ArrowUp:ip},props:{placement:{type:String,default:"bottom-start"},store:{type:Object},column:{type:Object},upDataColumn:{type:Function},appendTo:Tn.appendTo},setup(e){const t=Et(),{t:n}=Vt(),a=ke("table-filter"),r=t==null?void 0:t.parent;e.column&&!r.filterPanels.value[e.column.id]&&(r.filterPanels.value[e.column.id]=t);const o=F(null),i=F(null),l=F(0),s=D(()=>e.column&&e.column.filters),u=D(()=>e.column&&e.column.filterClassName?`${a.b()} ${e.column.filterClassName}`:a.b()),c=D({get:()=>{var T;return(((T=e.column)==null?void 0:T.filteredValue)||[])[0]},set:T=>{var M;if(e.column){const E=[...e.column.filteredValue??[]];Qr(T)?E.splice(0,1):E.splice(0,1,T),(M=e.upDataColumn)==null||M.call(e,"filteredValue",E)}}}),d=D({get(){return e.column?e.column.filteredValue||[]:[]},set(T){var M;e.column&&((M=e.upDataColumn)==null||M.call(e,"filteredValue",T))}}),v=D(()=>e.column?e.column.filterMultiple:!0),f=T=>T.value===c.value,h=()=>{var T;(T=o.value)==null||T.onClose()},m=()=>{b(d.value),h()},g=()=>{d.value=[],b(d.value),h()},y=(T,M)=>{c.value=T,l.value=M,Qr(T)?b([]):b(d.value),h()},b=T=>{var M,E;(M=e.store)==null||M.commit("filterChange",{column:e.column,values:T}),(E=e.store)==null||E.updateAllSelected()},_=()=>{var T,M;(T=i.value)==null||T.focus(),!v.value&&S(),e.column&&((M=e.upDataColumn)==null||M.call(e,"filterOpened",!0))},w=()=>{var T;e.column&&((T=e.upDataColumn)==null||T.call(e,"filterOpened",!1))},S=()=>{if(Qr(c)){l.value=0;return}const T=(s.value||[]).findIndex(M=>M.value===c.value);l.value=T>=0?T+1:0};return{multiple:v,filterClassName:u,filteredValue:d,filterValue:c,filters:s,handleConfirm:m,handleReset:g,handleSelect:y,isPropAbsent:Qr,isActive:f,t:n,ns:a,tooltipRef:o,rootRef:i,checkedIndex:l,handleShowTooltip:_,handleHideTooltip:w,handleKeydown:T=>{var P,R;const M=mn(T),E=(s.value?s.value.length:0)+1;let A=l.value,I=!0;switch(M){case Pe.down:case Pe.right:A=(A+1)%E;break;case Pe.up:case Pe.left:A=(A-1+E)%E;break;case Pe.tab:h(),I=!1;break;case Pe.enter:case Pe.space:if(A===0)y(null,0);else{const G=(s.value||[])[A-1];G.value&&y(G.value,A)}break;default:I=!1;break}I&&T.preventDefault(),l.value=A,(R=(P=i.value)==null?void 0:P.querySelector(`.${a.e("list-item")}:nth-child(${A+1})`))==null||R.focus()}}}});const Bye=["disabled"],Vye=["tabindex","aria-checked"],zye=["tabindex","aria-checked","onClick"],Fye=["aria-label"];function Hye(e,t,n,a,r,o){const i=be("el-checkbox"),l=be("el-checkbox-group"),s=be("el-scrollbar"),u=be("arrow-up"),c=be("arrow-down"),d=be("el-icon"),v=be("el-tooltip");return x(),ne(v,{ref:"tooltipRef",offset:0,placement:e.placement,"show-arrow":!1,trigger:"click",role:"dialog",teleported:"",effect:"light",pure:"",loop:"","popper-class":e.filterClassName,persistent:"","append-to":e.appendTo,onShow:e.handleShowTooltip,onHide:e.handleHideTooltip},{content:$(()=>[e.multiple?(x(),O("div",{key:0,ref:"rootRef",tabindex:"-1",class:U(e.ns.e("multiple"))},[C("div",{class:U(e.ns.e("content"))},[L(s,{"wrap-class":e.ns.e("wrap")},{default:$(()=>[L(l,{modelValue:e.filteredValue,"onUpdate:modelValue":t[0]||(t[0]=f=>e.filteredValue=f),class:U(e.ns.e("checkbox-group"))},{default:$(()=>[(x(!0),O(Ve,null,dt(e.filters,f=>(x(),ne(i,{key:f.value,value:f.value},{default:$(()=>[ue(Q(f.text),1)]),_:2},1032,["value"]))),128))]),_:1},8,["modelValue","class"])]),_:1},8,["wrap-class"])],2),C("div",{class:U(e.ns.e("bottom"))},[C("button",{class:U(e.ns.is("disabled",e.filteredValue.length===0)),disabled:e.filteredValue.length===0,type:"button",onClick:t[1]||(t[1]=(...f)=>e.handleConfirm&&e.handleConfirm(...f))},Q(e.t("el.table.confirmFilter")),11,Bye),C("button",{type:"button",onClick:t[2]||(t[2]=(...f)=>e.handleReset&&e.handleReset(...f))},Q(e.t("el.table.resetFilter")),1)],2)],2)):(x(),O("ul",{key:1,ref:"rootRef",tabindex:"-1",role:"radiogroup",class:U(e.ns.e("list")),onKeydown:t[4]||(t[4]=(...f)=>e.handleKeydown&&e.handleKeydown(...f))},[C("li",{role:"radio",class:U([e.ns.e("list-item"),e.ns.is("active",e.isPropAbsent(e.filterValue))]),tabindex:e.checkedIndex===0?0:-1,"aria-checked":e.isPropAbsent(e.filterValue),onClick:t[3]||(t[3]=f=>e.handleSelect(null,0))},Q(e.t("el.table.clearFilter")),11,Vye),(x(!0),O(Ve,null,dt(e.filters,(f,h)=>(x(),O("li",{key:f.value,role:"radio",class:U([e.ns.e("list-item"),e.ns.is("active",e.isActive(f))]),tabindex:e.checkedIndex===h+1?0:-1,"aria-checked":e.isActive(f),onClick:m=>e.handleSelect(f.value,h+1)},Q(f.text),11,zye))),128))],34))]),default:$(()=>{var f;return[C("button",{type:"button",class:U(`${e.ns.namespace.value}-table__column-filter-trigger`),"aria-label":e.t("el.table.filterLabel",{column:((f=e.column)==null?void 0:f.label)||""})},[L(d,null,{default:$(()=>[ce(e.$slots,"filter-icon",{},()=>{var h;return[(h=e.column)!=null&&h.filterOpened?(x(),ne(u,{key:0})):(x(),ne(c,{key:1}))]})]),_:3})],10,Fye)]}),_:3},8,["placement","popper-class","append-to","onShow","onHide"])}var Wye=ya(Nye,[["render",Hye]]);function Lw(e){const t=Et();g1(()=>{n.value.addObserver(t)}),mt(()=>{a(n.value),r(n.value)}),oi(()=>{a(n.value),r(n.value)}),Us(()=>{n.value.removeObserver(t)});const n=D(()=>{const o=e.layout;if(!o)throw new Error("Can not find table layout.");return o}),a=o=>{var u;const i=((u=e.vnode.el)==null?void 0:u.querySelectorAll("colgroup > col"))||[];if(!i.length)return;const l=o.getFlattenColumns(),s={};l.forEach(c=>{s[c.id]=c});for(let c=0,d=i.length;c{var s,u;const i=((s=e.vnode.el)==null?void 0:s.querySelectorAll("colgroup > col[name=gutter]"))||[];for(let c=0,d=i.length;c{m.stopPropagation()},o=(m,g)=>{!g.filters&&g.sortable?h(m,g,!1):g.filterable&&!g.sortable&&r(m),a==null||a.emit("header-click",g,m)},i=(m,g)=>{a==null||a.emit("header-contextmenu",g,m)},l=F(null),s=F(!1),u=F(),c=(m,g)=>{var y,b,_;if(qt&&!(g.children&&g.children.length>0)&&l.value&&e.border&&l.value.id===g.id){s.value=!0;const w=a;t("set-drag-visible",!0);const S=(y=w==null?void 0:w.vnode.el)==null?void 0:y.getBoundingClientRect().left,k=(_=(b=n==null?void 0:n.vnode)==null?void 0:b.el)==null?void 0:_.querySelector(`th.${g.id}`),T=k.getBoundingClientRect(),M=T.left-S+30;Eo(k,"noclick"),u.value={startMouseLeft:m.clientX,startLeft:T.right-S,startColumnLeft:T.left-S,tableLeft:S};const E=w==null?void 0:w.refs.resizeProxy;E.style.left=`${u.value.startLeft}px`,document.onselectstart=function(){return!1},document.ondragstart=function(){return!1};const A=P=>{const R=P.clientX-u.value.startMouseLeft,G=u.value.startLeft+R;E.style.left=`${Math.max(M,G)}px`},I=()=>{if(s.value){const{startColumnLeft:P,startLeft:R}=u.value;g.width=g.realWidth=Number.parseInt(E.style.left,10)-P,w==null||w.emit("header-dragend",g.width,R-P,g,m),requestAnimationFrame(()=>{e.store.scheduleLayout(!1,!0)}),document.body.style.cursor="",s.value=!1,l.value=null,u.value=void 0,t("set-drag-visible",!1)}document.removeEventListener("mousemove",A),document.removeEventListener("mouseup",I),document.onselectstart=null,document.ondragstart=null,setTimeout(()=>{ir(k,"noclick")},0)};document.addEventListener("mousemove",A),document.addEventListener("mouseup",I)}},d=(m,g)=>{var E;if(!e.border||g.children&&g.children.length>0)return;const y=m.target,b=Vr(y)?y.closest("th"):null;if(!b)return;const _=Ni(b,"is-sortable");if(_){const A=s.value?"col-resize":"";b.style.cursor=A;const I=b.querySelector(".caret-wrapper");I&&(I.style.cursor=A)}if(!g.resizable||s.value){l.value=null;return}const w=b.getBoundingClientRect(),S=((E=b.parentNode)==null?void 0:E.lastElementChild)===b,k=e.allowDragLastColumn||!S,T=w.width>12&&w.right-m.clientX<8&&k,M=T?"col-resize":"";document.body.style.cursor=M,l.value=T?g:null,_&&(b.style.cursor=M)},v=()=>{!qt||s.value||(document.body.style.cursor="")},f=({order:m,sortOrders:g})=>{if(m==="")return g[0];const y=g.indexOf(m||null);return g[y>g.length-2?0:y+1]},h=(m,g,y)=>{var E;m.stopPropagation();const b=g.order===y?null:y||f(g),_=(E=m.target)==null?void 0:E.closest("th");if(_&&Ni(_,"noclick")){ir(_,"noclick");return}if(!g.sortable)return;const w=m.currentTarget;if(["ascending","descending"].some(A=>Ni(w,A)&&!g.sortOrders.includes(A)))return;const S=e.store.states;let k=S.sortProp.value,T;const M=S.sortingColumn.value;(M!==g||M===g&&E1(M.order))&&(M&&(M.order=null),S.sortingColumn.value=g,k=g.property??null),b?T=g.order=b:T=g.order=null,S.sortProp.value=k,S.sortOrder.value=T,a==null||a.store.commit("changeSortCondition")};return{handleHeaderClick:o,handleHeaderContextMenu:i,handleMouseDown:c,handleMouseMove:d,handleMouseOut:v,handleSortClick:h,handleFilterClick:r}}function Uye(e){const t=Ze(di),n=ke("table");return{getHeaderRowStyle:l=>{const s=t==null?void 0:t.props.headerRowStyle;return Qe(s)?s.call(null,{rowIndex:l}):s},getHeaderRowClass:l=>{const s=[],u=t==null?void 0:t.props.headerRowClassName;return Ye(u)?s.push(u):Qe(u)&&s.push(u.call(null,{rowIndex:l})),s.join(" ")},getHeaderCellStyle:(l,s,u,c)=>{let d=(t==null?void 0:t.props.headerCellStyle)??{};Qe(d)&&(d=d.call(null,{rowIndex:l,columnIndex:s,row:u,column:c}));const v=Aw(s,c.fixed,e.store,u);return jd(v,"left"),jd(v,"right"),Object.assign({},d,v)},getHeaderCellClass:(l,s,u,c)=>{const d=Ew(n.b(),s,c.fixed,e.store,u),v=[c.id,c.order,c.headerAlign,c.className,c.labelClassName,...d];c.children||v.push("is-leaf"),c.sortable&&v.push("is-sortable");const f=t==null?void 0:t.props.headerCellClassName;return Ye(f)?v.push(f):Qe(f)&&v.push(f.call(null,{rowIndex:l,columnIndex:s,row:u,column:c})),v.push(n.e("cell")),v.filter(h=>!!h).join(" ")}}}var Nx=q({name:"ElTableHeader",components:{ElCheckbox:Zo},props:{fixed:{type:String,default:""},store:{required:!0,type:Object},border:Boolean,defaultSort:{type:Object,default:()=>({prop:"",order:""})},appendFilterPanelTo:{type:String},allowDragLastColumn:{type:Boolean}},setup(e,{emit:t}){const n=Et(),a=Ze(di),r=ke("table"),o=F({}),{onColumnsChange:i,onScrollableChange:l}=Lw(a),s=(a==null?void 0:a.props.tableLayout)==="auto",u=nn(new Map),c=F();let d;const v=()=>{d&&(clearTimeout(d),d=void 0),d=setTimeout(()=>{d=void 0;const P=c.value;P&&u.size>0&&(u.forEach((R,G)=>{const K=P.querySelector(`.${G.replace(/\s/g,".")}`);K&&(R.width=K.getBoundingClientRect().width||R.width)}),u.clear())})};ye(u,v),rn(()=>{d&&(clearTimeout(d),d=void 0)}),mt(async()=>{await He(),await He();const{prop:P,order:R}=e.defaultSort;a==null||a.store.commit("sort",{prop:P,order:R,init:!0}),v()});const{handleHeaderClick:f,handleHeaderContextMenu:h,handleMouseDown:m,handleMouseMove:g,handleMouseOut:y,handleSortClick:b,handleFilterClick:_}=Kye(e,t),{getHeaderRowStyle:w,getHeaderRowClass:S,getHeaderCellStyle:k,getHeaderCellClass:T}=Uye(e),{isGroup:M,toggleAllSelection:E,columnRows:A}=Rye(e),{t:I}=Vt();return n.state={onColumnsChange:i,onScrollableChange:l},n.filterPanels=o,{ns:r,t:I,filterPanels:o,onColumnsChange:i,onScrollableChange:l,columnRows:A,getHeaderRowClass:S,getHeaderRowStyle:w,getHeaderCellClass:T,getHeaderCellStyle:k,handleHeaderClick:f,handleHeaderContextMenu:h,handleMouseDown:m,handleMouseMove:g,handleMouseOut:y,handleSortClick:b,handleFilterClick:_,isGroup:M,toggleAllSelection:E,saveIndexSelection:u,isTableLayoutAuto:s,theadRef:c,updateFixedColumnStyle:v}},render(){const{ns:e,t,isGroup:n,columnRows:a,getHeaderCellStyle:r,getHeaderCellClass:o,getHeaderRowClass:i,getHeaderRowStyle:l,handleHeaderClick:s,handleHeaderContextMenu:u,handleMouseDown:c,handleMouseMove:d,handleSortClick:v,handleMouseOut:f,store:h,$parent:m,saveIndexSelection:g,isTableLayoutAuto:y}=this;let b=1;return ot("thead",{ref:"theadRef",class:e.is("group",n)},a.map((_,w)=>ot("tr",{class:i(w),key:w,style:l(w)},_.map((S,k)=>{S.rowSpan>b&&(b=S.rowSpan);const T=o(w,k,_,S);return y&&S.fixed&&g.set(T,S),ot("th",{class:T,colspan:S.colSpan,key:`${S.id}-thead`,rowspan:S.rowSpan,scope:S.colSpan>1?"colgroup":"col",ariaSort:S.sortable?S.order:void 0,style:r(w,k,_,S),onClick:M=>{var E;(E=M.currentTarget)!=null&&E.classList.contains("noclick")||s(M,S)},onContextmenu:M=>u(M,S),onMousedown:M=>c(M,S),onMousemove:M=>d(M,S),onMouseout:f},[ot("div",{class:["cell",S.filteredValue&&S.filteredValue.length>0?"highlight":""]},[S.renderHeader?S.renderHeader({column:S,$index:k,store:h,_self:m}):S.label,S.sortable&&ot("button",{type:"button",class:"caret-wrapper","aria-label":t("el.table.sortLabel",{column:S.label||""}),onClick:M=>v(M,S)},[ot("i",{onClick:M=>v(M,S,"ascending"),class:"sort-caret ascending"}),ot("i",{onClick:M=>v(M,S,"descending"),class:"sort-caret descending"})]),S.filterable&&ot(Wye,{store:h,placement:S.filterPlacement||"bottom-start",appendTo:m==null?void 0:m.appendFilterPanelTo,column:S,upDataColumn:(M,E)=>{S[M]=E}},{"filter-icon":()=>S.renderFilterIcon?S.renderFilterIcon({filterOpened:S.filterOpened}):null})])])}))))}});function Gye(e){const t=Ze(di),n=F(""),a=F(ot("div")),r=(h,m,g)=>{var S,k;const y=t,b=dy(h);let _=null;const w=(S=y==null?void 0:y.vnode.el)==null?void 0:S.dataset.prefix;b&&(_=Ox({columns:((k=e.store)==null?void 0:k.states.columns.value)??[]},b,w),_&&(y==null||y.emit(`cell-${g}`,m,_,b,h))),y==null||y.emit(`row-${g}`,m,_,h)},o=(h,m)=>{r(h,m,"dblclick")},i=(h,m)=>{var g;(g=e.store)==null||g.commit("setCurrentRow",m),r(h,m,"click")},l=(h,m)=>{r(h,m,"contextmenu")},s=Yi(h=>{var m;(m=e.store)==null||m.commit("setHoverRow",h)},30),u=Yi(()=>{var h;(h=e.store)==null||h.commit("setHoverRow",null)},30),c=h=>{const m=window.getComputedStyle(h,null);return{left:Number.parseInt(m.paddingLeft,10)||0,right:Number.parseInt(m.paddingRight,10)||0,top:Number.parseInt(m.paddingTop,10)||0,bottom:Number.parseInt(m.paddingBottom,10)||0}},d=(h,m,g)=>{var b;let y=(b=m==null?void 0:m.target)==null?void 0:b.parentNode;for(;h>1&&(y=y==null?void 0:y.nextSibling,!(!y||y.nodeName!=="TR"));)g(y,"hover-row hover-fixed-row"),h--};return{handleDoubleClick:o,handleClick:i,handleContextMenu:l,handleMouseEnter:s,handleMouseLeave:u,handleCellMouseEnter:(h,m,g)=>{var z,B,W;if(!t)return;const y=t,b=dy(h),_=(z=y==null?void 0:y.vnode.el)==null?void 0:z.dataset.prefix;let w=null;if(b){if(w=Ox({columns:((B=e.store)==null?void 0:B.states.columns.value)??[]},b,_),!w)return;b.rowSpan>1&&d(b.rowSpan,h,Eo);const N=y.hoverState={cell:b,column:w,row:m};y==null||y.emit("cell-mouse-enter",N.row,N.column,N.cell,h)}if(!g){(Wn==null?void 0:Wn.trigger)===b&&(Wn==null||Wn());return}const S=h.target.querySelector(".cell");if(!(Ni(S,`${_}-tooltip`)&&S.childNodes.length&&((W=S.textContent)!=null&&W.trim())))return;const k=document.createRange();k.setStart(S,0),k.setEnd(S,S.childNodes.length);const{width:T,height:M}=k.getBoundingClientRect(),{width:E,height:A}=S.getBoundingClientRect(),{top:I,left:P,right:R,bottom:G}=c(S),K=P+R,H=I+G;Vi(T+K,E)||Vi(M+H,A)||Vi(S.scrollWidth,E)?Tye(g,((b==null?void 0:b.innerText)||(b==null?void 0:b.textContent))??"",m,w,b,y):(Wn==null?void 0:Wn.trigger)===b&&(Wn==null||Wn())},handleCellMouseLeave:h=>{const m=dy(h);if(!m)return;m.rowSpan>1&&d(m.rowSpan,h,ir);const g=t==null?void 0:t.hoverState;t==null||t.emit("cell-mouse-leave",g==null?void 0:g.row,g==null?void 0:g.column,g==null?void 0:g.cell,h)},tooltipContent:n,tooltipTrigger:a}}function Yye(e){const t=Ze(di),n=ke("table");return{getRowStyle:(u,c)=>{const d=t==null?void 0:t.props.rowStyle;return Qe(d)?d.call(null,{row:u,rowIndex:c}):d||null},getRowClass:(u,c,d)=>{var h;const v=[n.e("row")];t!=null&&t.props.highlightCurrentRow&&u===((h=e.store)==null?void 0:h.states.currentRow.value)&&v.push("current-row"),e.stripe&&d%2===1&&v.push(n.em("row","striped"));const f=t==null?void 0:t.props.rowClassName;return Ye(f)?v.push(f):Qe(f)&&v.push(f.call(null,{row:u,rowIndex:c})),v},getCellStyle:(u,c,d,v)=>{const f=t==null?void 0:t.props.cellStyle;let h=f??{};Qe(f)&&(h=f.call(null,{rowIndex:u,columnIndex:c,row:d,column:v}));const m=Aw(c,e==null?void 0:e.fixed,e.store);return jd(m,"left"),jd(m,"right"),Object.assign({},h,m)},getCellClass:(u,c,d,v,f)=>{const h=Ew(n.b(),c,e==null?void 0:e.fixed,e.store,void 0,f),m=[v.id,v.align,v.className,...h],g=t==null?void 0:t.props.cellClassName;return Ye(g)?m.push(g):Qe(g)&&m.push(g.call(null,{rowIndex:u,columnIndex:c,row:d,column:v})),m.push(n.e("cell")),m.filter(y=>!!y).join(" ")},getSpan:(u,c,d,v)=>{let f=1,h=1;const m=t==null?void 0:t.props.spanMethod;if(Qe(m)){const g=m({row:u,column:c,rowIndex:d,columnIndex:v});Ae(g)?(f=g[0],h=g[1]):_t(g)&&(f=g.rowspan,h=g.colspan)}return{rowspan:f,colspan:h}},getColspanRealWidth:(u,c,d)=>{if(c<1)return u[d].realWidth;const v=u.map(({realWidth:f,width:h})=>f||h).slice(d,d+c);return Number(v.reduce((f,h)=>Number(f)+Number(h),-1))}}}const jye=["colspan","rowspan"];var qye=q({name:"TableTdWrapper",__name:"td-wrapper",props:{colspan:{type:Number,default:1},rowspan:{type:Number,default:1}},setup(e){return(t,n)=>(x(),O("td",{colspan:e.colspan,rowspan:e.rowspan},[ce(t.$slots,"default")],8,jye))}}),Xye=qye;function Zye(e){const t=Ze(di),n=ke("table"),{handleDoubleClick:a,handleClick:r,handleContextMenu:o,handleMouseEnter:i,handleMouseLeave:l,handleCellMouseEnter:s,handleCellMouseLeave:u,tooltipContent:c,tooltipTrigger:d}=Gye(e),{getRowStyle:v,getRowClass:f,getCellStyle:h,getCellClass:m,getSpan:g,getColspanRealWidth:y}=Yye(e);let b=-1;const _=D(()=>{var M;return(M=e.store)==null?void 0:M.states.columns.value.findIndex(({type:E})=>E==="default")}),w=(M,E)=>{var I;const A=(I=t==null?void 0:t.props)==null?void 0:I.rowKey;return A?ra(M,A):E},S=(M,E,A,I=!1)=>{const{tooltipEffect:P,tooltipOptions:R,store:G}=e,{indent:K,columns:H}=G.states,z=[];let B=!0;return A&&(z.push(n.em("row",`level-${A.level}`)),B=!!A.display),E===0&&(b=-1),e.stripe&&B&&b++,z.push(...f(M,E,b)),ot("tr",{style:[B?null:{display:"none"},v(M,E)],class:z,key:w(M,E),onDblclick:W=>a(W,M),onClick:W=>r(W,M),onContextmenu:W=>o(W,M),onMouseenter:()=>i(E),onMouseleave:l},H.value.map((W,N)=>{const{rowspan:V,colspan:Y}=g(M,W,E,N);if(!V||!Y)return null;const X=Object.assign({},W);X.realWidth=y(H.value,Y,N);const ee={store:G,_self:e.context||t,column:X,row:M,$index:E,cellIndex:N,expanded:I};N===_.value&&A&&(ee.treeNode={indent:A.level&&A.level*K.value,level:A.level},vn(A.expanded)&&(ee.treeNode.expanded=A.expanded,"loading"in A&&(ee.treeNode.loading=A.loading),"noLazyChildren"in A&&(ee.treeNode.noLazyChildren=A.noLazyChildren)));const re=`${w(M,E)},${N}`,ie=X.columnKey||X.rawColumnKey||"",le=W.showOverflowTooltip&&xA({effect:P},R,W.showOverflowTooltip);return ot(Xye,{style:h(E,N,M,W),class:m(E,N,M,W,Y-1),key:`${ie}${re}`,rowspan:V,colspan:Y,onMouseenter:ve=>s(ve,M,le),onMouseleave:u},{default:()=>k(N,W,ee)})}))},k=(M,E,A)=>E.renderCell(A);return{wrappedRowRender:(M,E)=>{const A=e.store,{isRowExpanded:I,assertRowKey:P}=A,{treeData:R,lazyTreeNodeMap:G,childrenColumnName:K,rowKey:H}=A.states,z=A.states.columns.value;if(z.some(({type:B})=>B==="expand")){const B=I(M),W=S(M,E,void 0,B),N=t==null?void 0:t.renderExpanded;if(!N)return console.error("[Element Error]renderExpanded is required."),W;const V=[[W]];return(t.props.preserveExpandedContent||B)&&V[0].push(ot("tr",{key:`expanded-row__${W.key}`,style:{display:B?"":"none"}},[ot("td",{colspan:z.length,class:`${n.e("cell")} ${n.e("expanded-cell")}`},[N({row:M,$index:E,store:A,expanded:B})])])),V}else if(Object.keys(R.value).length){P();const B=ra(M,H.value);let W=R.value[B],N=null;W&&(N={expanded:W.expanded,level:W.level,display:!0,noLazyChildren:void 0,loading:void 0},vn(W.lazy)&&(N&&vn(W.loaded)&&W.loaded&&(N.noLazyChildren=!(W.children&&W.children.length)),N.loading=W.loading));const V=[S(M,E,N??void 0)];if(W){let Y=0;const X=(ee,re)=>{ee&&ee.length&&re&&ee.forEach(ie=>{const le={display:re.display&&re.expanded,level:re.level+1,expanded:!1,noLazyChildren:!1,loading:!1},ve=ra(ie,H.value);if(Qr(ve))throw new Error("For nested data item, row-key is required.");W={...R.value[ve]},W&&(le.expanded=W.expanded,W.level=W.level||le.level,W.display=!!(W.expanded&&le.display),vn(W.lazy)&&(vn(W.loaded)&&W.loaded&&(le.noLazyChildren=!(W.children&&W.children.length)),le.loading=W.loading)),Y++,V.push(S(ie,E+Y,le)),W&&X(G.value[ve]||ie[K.value],W)})};W.display=!0,X(G.value[B]||M[K.value],W)}return V}else return S(M,E,void 0)},tooltipContent:c,tooltipTrigger:d}}const Jye={store:{required:!0,type:Object},stripe:Boolean,tooltipEffect:String,tooltipOptions:{type:Object},context:{default:()=>({}),type:Object},rowClassName:[String,Function],rowStyle:[Object,Function],fixed:{type:String,default:""},highlight:Boolean};var Qye=q({name:"ElTableBody",props:Jye,setup(e){var c;const t=Et(),n=Ze(di),a=ke("table"),{wrappedRowRender:r,tooltipContent:o,tooltipTrigger:i}=Zye(e),{onColumnsChange:l,onScrollableChange:s}=Lw(n),u=[];return ye((c=e.store)==null?void 0:c.states.hoverRow,(d,v)=>{var y,b;const f=t==null?void 0:t.vnode.el,h=Array.from((f==null?void 0:f.children)||[]).filter(_=>_==null?void 0:_.classList.contains(`${a.e("row")}`));let m=d;const g=(y=h[m])==null?void 0:y.childNodes;if(g!=null&&g.length){let _=0;Array.from(g).reduce((w,S,k)=>{var T,M;return((T=g[k])==null?void 0:T.colSpan)>1&&(_=(M=g[k])==null?void 0:M.colSpan),S.nodeName!=="TD"&&_===0&&w.push(k),_>0&&_--,w},[]).forEach(w=>{var S;for(m=d;m>0;){const k=(S=h[m-1])==null?void 0:S.childNodes;if(k[w]&&k[w].nodeName==="TD"&&k[w].rowSpan>1){Eo(k[w],"hover-cell"),u.push(k[w]);break}m--}})}else u.forEach(_=>ir(_,"hover-cell")),u.length=0;!((b=e.store)!=null&&b.states.isComplex.value)||!qt||Wr(()=>{const _=h[v],w=h[d];_&&!_.classList.contains("hover-fixed-row")&&ir(_,"hover-row"),w&&Eo(w,"hover-row")})}),Us(()=>{Wn==null||Wn()}),{ns:a,onColumnsChange:l,onScrollableChange:s,wrappedRowRender:r,tooltipContent:o,tooltipTrigger:i}},render(){const{wrappedRowRender:e,store:t}=this;return ot("tbody",{tabIndex:-1},[((t==null?void 0:t.states.data.value)||[]).reduce((n,a)=>n.concat(e(a,n.length)),[])])}});function ebe(){var t;const e=(t=Ze(di))==null?void 0:t.store;return{leftFixedLeafCount:D(()=>(e==null?void 0:e.states.fixedLeafColumnsLength.value)??0),rightFixedLeafCount:D(()=>(e==null?void 0:e.states.rightFixedColumns.value.length)??0),columnsCount:D(()=>(e==null?void 0:e.states.columns.value.length)??0),leftFixedCount:D(()=>(e==null?void 0:e.states.fixedColumns.value.length)??0),rightFixedCount:D(()=>(e==null?void 0:e.states.rightFixedColumns.value.length)??0),columns:D(()=>(e==null?void 0:e.states.columns.value)??[])}}function tbe(e){const{columns:t}=ebe(),n=ke("table");return{getCellClasses:(o,i)=>{const l=o[i],s=[n.e("cell"),l.id,l.align,l.labelClassName,...Ew(n.b(),i,l.fixed,e.store)];return l.className&&s.push(l.className),l.children||s.push(n.is("leaf")),s},getCellStyles:(o,i)=>{const l=Aw(i,o.fixed,e.store);return jd(l,"left"),jd(l,"right"),l},columns:t}}var Bx=q({name:"ElTableFooter",props:{fixed:{type:String,default:""},store:{required:!0,type:Object},summaryMethod:Function,sumText:String,border:Boolean,defaultSort:{type:Object,default:()=>({prop:"",order:""})}},setup(e){const t=Ze(di),n=ke("table"),{getCellClasses:a,getCellStyles:r,columns:o}=tbe(e),{onScrollableChange:i,onColumnsChange:l}=Lw(t);return{ns:n,onScrollableChange:i,onColumnsChange:l,getCellClasses:a,getCellStyles:r,columns:o}},render(){const{columns:e,getCellStyles:t,getCellClasses:n,summaryMethod:a,sumText:r}=this,o=this.store.states.data.value;let i=[];return a?i=a({columns:e,data:o}):e.forEach((l,s)=>{if(s===0){i[s]=r;return}const u=o.map(f=>Number(f[l.property])),c=[];let d=!0;u.forEach(f=>{if(!Number.isNaN(+f)){d=!1;const h=`${f}`.split(".")[1];c.push(h?h.length:0)}});const v=Math.max.apply(null,c);d?i[s]="":i[s]=u.reduce((f,h)=>{const m=Number(h);return Number.isNaN(+m)?f:Number.parseFloat((f+h).toFixed(Math.min(v,20)))},0)}),ot(ot("tfoot",[ot("tr",{},[...e.map((l,s)=>ot("td",{key:s,colspan:l.colSpan,rowspan:l.rowSpan,class:n(e,s),style:t(l,s)},[ot("div",{class:["cell",l.labelClassName]},[i[s]])]))])]))}});function nbe(e){return{setCurrentRow:v=>{e.commit("setCurrentRow",v)},getSelectionRows:()=>e.getSelectionRows(),getHalfSelectionRows:()=>e.getHalfSelectionRows(),toggleRowSelection:(v,f,h=!0)=>{e.toggleRowSelection(v,f,!1,h),e.updateAllSelected()},clearSelection:()=>{e.clearSelection()},clearFilter:v=>{e.clearFilter(v)},toggleAllSelection:()=>{e.commit("toggleAllSelection")},toggleRowExpansion:(v,f)=>{e.toggleRowExpansionAdapter(v,f)},clearSort:()=>{e.clearSort()},sort:(v,f)=>{e.commit("sort",{prop:v,order:f})},updateKeyChildren:(v,f)=>{e.updateKeyChildren(v,f)}}}function abe(e,t,n,a){const r=F(!1),o=F(null),i=F(!1),l=K=>{i.value=K},s=F({width:null,height:null,headerHeight:null}),u=F(!1),c={display:"inline-block",verticalAlign:"middle"},d=F(),v=F(0),f=F(0),h=F(0),m=F(0),g=F(0);ye(()=>e.height,K=>{t.setHeight(K??null)},{immediate:!0}),ye(()=>e.maxHeight,K=>{t.setMaxHeight(K??null)},{immediate:!0}),ye(()=>[e.currentRowKey,n.states.rowKey],([K,H])=>{!p(H)||!p(K)||n.setCurrentRowKey(`${K}`)},{immediate:!0}),ye(()=>e.data,K=>{a.store.commit("setData",K)},{immediate:!0,deep:!0}),ur(()=>{e.expandRowKeys&&n.setExpandRowKeysAdapter(e.expandRowKeys)});const y=()=>{a.store.commit("setHoverRow",null),a.hoverState&&(a.hoverState=null)},b=(K,H)=>{const{pixelX:z,pixelY:B}=H;Math.abs(z)>=Math.abs(B)&&(a.refs.bodyWrapper.scrollLeft+=H.pixelX/5)},_=D(()=>e.height||e.maxHeight||n.states.fixedColumns.value.length>0||n.states.rightFixedColumns.value.length>0),w=D(()=>({width:t.bodyWidth.value?`${t.bodyWidth.value}px`:""})),S=()=>{_.value&&t.updateElsHeight(),t.updateColumnsWidth(),!(typeof window>"u")&&requestAnimationFrame(E)};mt(async()=>{await He(),n.updateColumns(),A(),requestAnimationFrame(S);const K=a.vnode.el,H=a.refs.headerWrapper;e.flexible&&K&&K.parentElement&&(K.parentElement.style.minWidth="0"),s.value={width:d.value=K.offsetWidth,height:K.offsetHeight,headerHeight:e.showHeader&&H?H.offsetHeight:null},n.states.columns.value.forEach(z=>{z.filteredValue&&z.filteredValue.length&&a.store.commit("filterChange",{column:z,values:z.filteredValue,silent:!0})}),a.$ready=!0});const k=(K,H)=>{if(!K)return;const z=Array.from(K.classList).filter(B=>!B.startsWith("is-scrolling-"));z.push(t.scrollX.value?H:"is-scrolling-none"),K.className=z.join(" ")},T=K=>{const{tableWrapper:H}=a.refs;k(H,K)},M=K=>{const{tableWrapper:H}=a.refs;return!!(H&&H.classList.contains(K))},E=function(){if(!a.refs.scrollBarRef)return;if(!t.scrollX.value){const V="is-scrolling-none";M(V)||T(V);return}const K=a.refs.scrollBarRef.wrapRef;if(!K)return;const{scrollLeft:H,offsetWidth:z,scrollWidth:B}=K,{headerWrapper:W,footerWrapper:N}=a.refs;W&&(W.scrollLeft=H),N&&(N.scrollLeft=H),H>=B-z-1?T("is-scrolling-right"):T(H===0?"is-scrolling-left":"is-scrolling-middle")},A=()=>{a.refs.scrollBarRef&&(a.refs.scrollBarRef.wrapRef&&on(a.refs.scrollBarRef.wrapRef,"scroll",E,{passive:!0}),e.fit?Mn(a.vnode.el,I):on(window,"resize",I),Mn(a.refs.tableInnerWrapper,()=>{var K,H;I(),(H=(K=a.refs)==null?void 0:K.scrollBarRef)==null||H.update()}))},I=()=>{var X,ee,re,ie;const K=a.vnode.el;if(!a.$ready||!K)return;let H=!1;const{width:z,height:B,headerHeight:W}=s.value,N=d.value=K.offsetWidth;z!==N&&(H=!0);const V=K.offsetHeight;(e.height||_.value)&&B!==V&&(H=!0);const Y=e.tableLayout==="fixed"?a.refs.headerWrapper:(X=a.refs.tableHeaderRef)==null?void 0:X.$el;e.showHeader&&(Y==null?void 0:Y.offsetHeight)!==W&&(H=!0),v.value=((ee=a.refs.tableWrapper)==null?void 0:ee.scrollHeight)||0,h.value=(Y==null?void 0:Y.scrollHeight)||0,m.value=((re=a.refs.footerWrapper)==null?void 0:re.offsetHeight)||0,g.value=((ie=a.refs.appendWrapper)==null?void 0:ie.offsetHeight)||0,f.value=v.value-h.value-m.value-g.value,H&&(s.value={width:N,height:V,headerHeight:e.showHeader&&(Y==null?void 0:Y.offsetHeight)||0},S())},P=ca(),R=D(()=>{const{bodyWidth:K,scrollY:H,gutterWidth:z}=t;return K.value?`${K.value-(H.value?z:0)}px`:""}),G=D(()=>e.maxHeight?"fixed":e.tableLayout);return{isHidden:r,renderExpanded:o,setDragVisible:l,isGroup:u,handleMouseLeave:y,handleHeaderFooterMousewheel:b,tableSize:P,emptyBlockStyle:D(()=>{if(e.data&&e.data.length)return;let K="100%";e.height&&f.value&&(K=`${f.value}px`);const H=d.value;return{width:H?`${H}px`:"",height:K}}),resizeProxyVisible:i,bodyWidth:R,resizeState:s,doLayout:S,tableBodyStyles:w,tableLayout:G,scrollbarViewStyle:c,scrollbarStyle:D(()=>e.height?{height:"100%"}:e.maxHeight?Number.isNaN(Number(e.maxHeight))?{maxHeight:`calc(${e.maxHeight} - ${h.value+m.value}px)`}:{maxHeight:`${+e.maxHeight-h.value-m.value}px`}:{})}}function rbe(e){let t;const n=()=>{const a=e.vnode.el.querySelector(".hidden-columns"),r={childList:!0,subtree:!0},o=e.store.states.updateOrderFns;t=new MutationObserver(()=>{o.forEach(i=>i())}),t.observe(a,r)};mt(()=>{n()}),Us(()=>{t==null||t.disconnect()})}function rm(e){const t=e.tableLayout==="auto";let n=e.columns||[];t&&n.every(({width:r})=>zt(r))&&(n=[]);const a=r=>{const o={key:`${e.tableLayout}_${r.id}`,style:{},name:void 0};return t?o.style={width:`${r.width}px`}:o.name=r.id,o};return ot("colgroup",{},n.map(r=>ot("col",a(r))))}rm.props=["columns","tableLayout"];const obe=()=>{const e=F(),t=(o,i)=>{const l=e.value;l&&l.scrollTo(o,i)},n=(o,i)=>{const l=e.value;l&&Je(i)&&["Top","Left"].includes(o)&&l[`setScroll${o}`](i)};return{scrollBarRef:e,scrollTo:t,setScrollTop:o=>n("Top",o),setScrollLeft:o=>n("Left",o)}},ibe=["data-prefix"],lbe={ref:"hiddenColumns",class:"hidden-columns"};var sbe=q({name:"ElTable",__name:"table",props:hye,emits:["select","select-all","selection-change","cell-mouse-enter","cell-mouse-leave","cell-dblclick","cell-contextmenu","cell-click","row-dblclick","row-click","row-contextmenu","header-click","header-contextmenu","sort-change","filter-change","current-change","header-dragend","expand-change","scroll"],setup(e,{expose:t}){const n=e,{t:a}=Vt(),r=ke("table"),o=Hl("table"),i=Et();Ft(di,i);const l=Dye(i,n);i.store=l;const s=new $ye({store:i.store,table:i,fit:n.fit,showHeader:n.showHeader});i.layout=s;const u=D(()=>(l.states.data.value||[]).length===0),{setCurrentRow:c,getSelectionRows:d,getHalfSelectionRows:v,toggleRowSelection:f,clearSelection:h,clearFilter:m,toggleAllSelection:g,toggleRowExpansion:y,clearSort:b,sort:_,updateKeyChildren:w}=nbe(l),{isHidden:S,renderExpanded:k,setDragVisible:T,isGroup:M,handleMouseLeave:E,handleHeaderFooterMousewheel:A,tableSize:I,emptyBlockStyle:P,resizeProxyVisible:R,bodyWidth:G,resizeState:K,doLayout:H,tableBodyStyles:z,tableLayout:B,scrollbarViewStyle:W,scrollbarStyle:N}=abe(n,s,l,i),{scrollBarRef:V,scrollTo:Y,setScrollLeft:X,setScrollTop:ee}=obe(),re=Yi(H,50),ie=gye(r.namespace.value),le=i;i.tableId=ie,i.state={isGroup:M,resizeState:K,doLayout:H,debouncedUpdateLayout:re};const ve=D(()=>n.sumText??a("el.table.sumText")),he=D(()=>n.emptyText??a("el.table.emptyText")),J=D(()=>{var oe;return n.tooltipEffect??((oe=o.value)==null?void 0:oe.tooltipEffect)}),Z=D(()=>{var oe;return n.tooltipOptions??((oe=o.value)==null?void 0:oe.tooltipOptions)}),j=D(()=>LD(l.states.originColumns.value)[0]);return rbe(i),rn(()=>{re.cancel()}),t({ns:r,layout:s,store:l,columns:j,handleHeaderFooterMousewheel:A,handleMouseLeave:E,tableId:ie,tableSize:I,isHidden:S,isEmpty:u,renderExpanded:k,resizeProxyVisible:R,resizeState:K,isGroup:M,bodyWidth:G,tableBodyStyles:z,emptyBlockStyle:P,debouncedUpdateLayout:re,setCurrentRow:c,getSelectionRows:d,getHalfSelectionRows:v,toggleRowSelection:f,clearSelection:h,clearFilter:m,toggleAllSelection:g,toggleRowExpansion:y,clearSort:b,doLayout:H,sort:_,updateKeyChildren:w,t:a,setDragVisible:T,context:le,computedSumText:ve,computedEmptyText:he,computedTooltipEffect:J,computedTooltipOptions:Z,tableLayout:B,scrollbarViewStyle:W,scrollbarStyle:N,scrollBarRef:V,scrollTo:Y,setScrollLeft:X,setScrollTop:ee,allowDragLastColumn:n.allowDragLastColumn}),(oe,se)=>(x(),O("div",{ref:"tableWrapper",class:U([{[p(r).m("fit")]:e.fit,[p(r).m("striped")]:e.stripe,[p(r).m("border")]:e.border||p(M),[p(r).m("hidden")]:p(S),[p(r).m("group")]:p(M),[p(r).m("fluid-height")]:e.maxHeight,[p(r).m("scrollable-x")]:p(s).scrollX.value,[p(r).m("scrollable-y")]:p(s).scrollY.value,[p(r).m("enable-row-hover")]:!p(l).states.isComplex.value,[p(r).m("enable-row-transition")]:(p(l).states.data.value||[]).length!==0&&(p(l).states.data.value||[]).length<100,"has-footer":e.showSummary},p(r).m(p(I)),e.className,p(r).b(),p(r).m(`layout-${p(B)}`)]),style:et(e.style),"data-prefix":p(r).namespace.value,onMouseleave:se[1]||(se[1]=(...fe)=>p(E)&&p(E)(...fe))},[C("div",{ref:"tableInnerWrapper",class:U(p(r).e("inner-wrapper"))},[C("div",lbe,[ce(oe.$slots,"default")],512),e.showHeader&&p(B)==="fixed"?st((x(),O("div",{key:0,ref:"headerWrapper",class:U(p(r).e("header-wrapper"))},[C("table",{ref:"tableHeader",class:U(p(r).e("header")),style:et(p(z)),border:"0",cellpadding:"0",cellspacing:"0"},[L(p(rm),{columns:p(l).states.columns.value,"table-layout":p(B)},null,8,["columns","table-layout"]),L(p(Nx),{ref:"tableHeaderRef",border:e.border,"default-sort":e.defaultSort,store:p(l),"append-filter-panel-to":e.appendFilterPanelTo,"allow-drag-last-column":e.allowDragLastColumn,onSetDragVisible:p(T)},null,8,["border","default-sort","store","append-filter-panel-to","allow-drag-last-column","onSetDragVisible"])],6)],2)),[[p(Q5),p(A)]]):te("v-if",!0),C("div",{ref:"bodyWrapper",class:U(p(r).e("body-wrapper"))},[L(p(ni),{ref_key:"scrollBarRef",ref:V,"view-style":p(W),"wrap-style":p(N),always:e.scrollbarAlwaysOn,tabindex:e.scrollbarTabindex,native:e.nativeScrollbar,onScroll:se[0]||(se[0]=fe=>oe.$emit("scroll",fe))},{default:$(()=>[C("table",{ref:"tableBody",class:U(p(r).e("body")),cellspacing:"0",cellpadding:"0",border:"0",style:et({width:p(G),tableLayout:p(B)})},[L(p(rm),{columns:p(l).states.columns.value,"table-layout":p(B)},null,8,["columns","table-layout"]),e.showHeader&&p(B)==="auto"?(x(),ne(p(Nx),{key:0,ref:"tableHeaderRef",class:U(p(r).e("body-header")),border:e.border,"default-sort":e.defaultSort,store:p(l),"append-filter-panel-to":e.appendFilterPanelTo,onSetDragVisible:p(T)},null,8,["class","border","default-sort","store","append-filter-panel-to","onSetDragVisible"])):te("v-if",!0),L(p(Qye),{context:p(le),highlight:e.highlightCurrentRow,"row-class-name":e.rowClassName,"tooltip-effect":J.value,"tooltip-options":Z.value,"row-style":e.rowStyle,store:p(l),stripe:e.stripe},null,8,["context","highlight","row-class-name","tooltip-effect","tooltip-options","row-style","store","stripe"]),e.showSummary&&p(B)==="auto"?(x(),ne(p(Bx),{key:1,class:U(p(r).e("body-footer")),border:e.border,"default-sort":e.defaultSort,store:p(l),"sum-text":ve.value,"summary-method":e.summaryMethod},null,8,["class","border","default-sort","store","sum-text","summary-method"])):te("v-if",!0)],6),u.value?(x(),O("div",{key:0,ref:"emptyBlock",style:et(p(P)),class:U(p(r).e("empty-block"))},[C("span",{class:U(p(r).e("empty-text"))},[ce(oe.$slots,"empty",{},()=>[ue(Q(he.value),1)])],2)],6)):te("v-if",!0),oe.$slots.append?(x(),O("div",{key:1,ref:"appendWrapper",class:U(p(r).e("append-wrapper"))},[ce(oe.$slots,"append")],2)):te("v-if",!0)]),_:3},8,["view-style","wrap-style","always","tabindex","native"])],2),e.showSummary&&p(B)==="fixed"?st((x(),O("div",{key:1,ref:"footerWrapper",class:U(p(r).e("footer-wrapper"))},[C("table",{class:U(p(r).e("footer")),cellspacing:"0",cellpadding:"0",border:"0",style:et(p(z))},[L(p(rm),{columns:p(l).states.columns.value,"table-layout":p(B)},null,8,["columns","table-layout"]),L(p(Bx),{border:e.border,"default-sort":e.defaultSort,store:p(l),"sum-text":ve.value,"summary-method":e.summaryMethod},null,8,["border","default-sort","store","sum-text","summary-method"])],6)],2)),[[Zt,!u.value],[p(Q5),p(A)]]):te("v-if",!0),e.border||p(M)?(x(),O("div",{key:2,class:U(p(r).e("border-left-patch"))},null,2)):te("v-if",!0)],2),st(C("div",{ref:"resizeProxy",class:U(p(r).e("column-resize-proxy"))},null,2),[[Zt,p(R)]])],46,ibe))}}),ube=sbe;const cbe={type:{type:String,default:"default"},label:String,className:String,labelClassName:String,property:String,prop:String,width:{type:[String,Number],default:""},minWidth:{type:[String,Number],default:""},renderHeader:Function,sortable:{type:[Boolean,String],default:!1},sortMethod:Function,sortBy:[String,Function,Array],resizable:{type:Boolean,default:!0},columnKey:String,align:String,headerAlign:String,showOverflowTooltip:{type:[Boolean,Object],default:void 0},tooltipFormatter:Function,fixed:[Boolean,String],formatter:Function,selectable:Function,reserveSelection:Boolean,filterMethod:Function,filteredValue:Array,filters:Array,filterPlacement:String,filterMultiple:{type:Boolean,default:!0},filterClassName:String,index:[Number,Function],sortOrders:{type:Array,default:()=>["ascending","descending",null],validator:e=>e.every(t=>["ascending","descending",null].includes(t))}},dbe={selection:"table-column--selection",expand:"table__expand-column"},fbe={default:{order:""},selection:{width:48,minWidth:48,realWidth:48,order:""},expand:{width:48,minWidth:48,realWidth:48,order:""},index:{width:48,minWidth:48,realWidth:48,order:""}},vbe=e=>dbe[e]||"",hbe={selection:{renderHeader({store:e}){function t(){return e.states.data.value&&e.states.data.value.length===0}return ot(Zo,{disabled:t(),size:e.states.tableSize.value,indeterminate:e.states.selection.value.length>0&&!e.states.isAllSelected.value,"onUpdate:modelValue":e.toggleAllSelection??void 0,modelValue:e.states.isAllSelected.value,ariaLabel:e.t("el.table.selectAllLabel")})},renderCell({row:e,column:t,store:n,$index:a}){return ot(Zo,{disabled:t.selectable?!t.selectable.call(null,e,a):!1,size:n.states.tableSize.value,onChange:()=>{n.commit("rowSelectedChanged",e)},onClick:r=>r.stopPropagation(),modelValue:n.isSelected(e),indeterminate:n.getRowIndeterminate(e),ariaLabel:n.t("el.table.selectRowLabel")})},sortable:!1,resizable:!1},index:{renderHeader({column:e}){return e.label||"#"},renderCell({column:e,$index:t}){let n=t+1;const a=e.index;return Je(a)?n=t+a:Qe(a)&&(n=a(t)),ot("div",{},[n])},sortable:!1},expand:{renderHeader({column:e}){return e.label||""},renderCell({column:e,row:t,store:n,expanded:a,$index:r}){var u,c;const{ns:o}=n,i=[o.e("expand-icon")];!e.renderExpand&&a&&i.push(o.em("expand-icon","expanded"));const l=function(d){d.stopPropagation(),n.toggleRowExpansion(t)},s=((c=(u=n.states.rowExpandable).value)==null?void 0:c.call(u,t,r))??!0;return s||i.push(o.is("disabled")),ot("button",{type:"button",disabled:!s,"aria-label":n.t(a?"el.table.collapseRowLabel":"el.table.expandRowLabel"),"aria-expanded":a,class:i,onClick:l},{default:()=>e.renderExpand?[e.renderExpand({expanded:a,expandable:s})]:[ot(Xe,null,{default:()=>[ot(za)]})]})},sortable:!1,resizable:!1}};function pbe({row:e,column:t,$index:n}){var o;const a=t.property,r=a&&Cs(e,a).value;return t&&t.formatter?t.formatter(e,t,r,n):((o=r==null?void 0:r.toString)==null?void 0:o.call(r))||""}function mbe({row:e,treeNode:t,store:n},a=!1){const{ns:r}=n;if(!t)return a?[ot("span",{class:r.e("placeholder")})]:null;const o=[],i=function(l){l.stopPropagation(),!t.loading&&n.loadOrToggle(e)};if(t.indent&&o.push(ot("span",{class:r.e("indent"),style:{"padding-left":`${t.indent}px`}})),vn(t.expanded)&&!t.noLazyChildren){const l=[r.e("expand-icon"),t.expanded?r.em("expand-icon","expanded"):""];let s=za;t.loading&&(s=Mr),o.push(ot("button",{type:"button","aria-label":n.t(t.expanded?"el.table.collapseRowLabel":"el.table.expandRowLabel"),"aria-expanded":t.expanded,class:l,onClick:i},{default:()=>[ot(Xe,{class:r.is("loading",t.loading)},{default:()=>[ot(s)]})]}))}else o.push(ot("span",{class:r.e("placeholder")}));return o}function Vx(e,t){return e.reduce((n,a)=>(n[a]=a,n),t)}function gbe(e,t){const n=Et();return{registerComplexWatchers:()=>{const o=["fixed"],i={realWidth:"width",realMinWidth:"minWidth"},l=Vx(o,i);Object.keys(l).forEach(s=>{const u=i[s];en(t,u)&&ye(()=>t[u],c=>{let d=c;u==="width"&&s==="realWidth"&&(d=Mw(c)),u==="minWidth"&&s==="realMinWidth"&&(d=xD(c)),n.columnConfig.value[u]=d,n.columnConfig.value[s]=d;const v=u==="fixed";e.value.store.scheduleLayout(v)})})},registerNormalWatchers:()=>{const o=["label","filters","filterMultiple","filteredValue","sortable","index","formatter","className","labelClassName","filterClassName","showOverflowTooltip","tooltipFormatter","resizable"],i=["showOverflowTooltip"],l={property:"prop",align:"realAlign",headerAlign:"realHeaderAlign"},s=Vx(o,l);Object.keys(s).forEach(c=>{const d=l[c];en(t,d)&&ye(()=>t[d],v=>{n.columnConfig.value[c]=v,(c==="filters"||c==="filterMethod")&&(n.columnConfig.value.filterable=!!(n.columnConfig.value.filters||n.columnConfig.value.filterMethod))})}),i.forEach(c=>{en(e.value.props,c)&&ye(()=>e.value.props[c],d=>{n.columnConfig.value.type!=="selection"&&zt(t[c])&&(n.columnConfig.value[c]=d)})});const u=Hl("table");u.value&&en(u.value,"showOverflowTooltip")&&ye(()=>{var c;return(c=u.value)==null?void 0:c.showOverflowTooltip},c=>{n.columnConfig.value.type!=="selection"&&(!zt(t.showOverflowTooltip)||!zt(e.value.props.showOverflowTooltip)||(n.columnConfig.value.showOverflowTooltip=c))})}}}function ybe(e,t,n){const a=Et(),r=F(""),o=F(!1),i=F(),l=F(),s=ke("table");ur(()=>{i.value=e.align?`is-${e.align}`:null,i.value}),ur(()=>{l.value=e.headerAlign?`is-${e.headerAlign}`:i.value,l.value});const u=D(()=>{let w=a.vnode.vParent||a.parent;for(;w&&!w.tableId&&!w.columnId;)w=w.vnode.vParent||w.parent;return w}),c=D(()=>{const{store:w}=a.parent;if(!w)return!1;const{treeData:S}=w.states,k=S.value;return k&&Object.keys(k).length>0}),d=F(Mw(e.width)),v=F(xD(e.minWidth)),f=w=>(d.value&&(w.width=d.value),v.value&&(w.minWidth=v.value),!d.value&&v.value&&(w.width=void 0),w.minWidth||(w.minWidth=80),w.realWidth=Number(zt(w.width)?w.minWidth:w.width),w),h=w=>{const S=w.type,k=hbe[S]||{};Object.keys(k).forEach(M=>{const E=k[M];M!=="className"&&!zt(E)&&(w[M]=E)});const T=vbe(S);if(T){const M=`${p(s.namespace)}-${T}`;w.className=w.className?`${w.className} ${M}`:M}return w},m=w=>{Ae(w)?w.forEach(k=>S(k)):S(w);function S(k){var T;((T=k==null?void 0:k.type)==null?void 0:T.name)==="ElTableColumn"&&(k.vParent=a)}};return{columnId:r,realAlign:i,isSubColumn:o,realHeaderAlign:l,columnOrTableParent:u,setColumnWidth:f,setColumnForcedProps:h,setColumnRenders:w=>{e.renderHeader?un("TableColumn","Comparing to render-header, scoped-slot header is easier to use. We recommend users to use scoped-slot header."):w.type!=="selection"&&(w.renderHeader=k=>{if(a.columnConfig.value.label,t.header){const T=t.header(k);if(MD(T))return ot(Ve,T)}return ue(w.label)}),t["filter-icon"]&&(w.renderFilterIcon=k=>ce(t,"filter-icon",k)),t.expand&&(w.renderExpand=k=>ce(t,"expand",k));let S=w.renderCell;return w.type==="expand"?(w.renderCell=k=>ot("div",{class:"cell"},[S(k)]),n.value.renderExpanded=k=>t.default?t.default(k):t.default):(S=S||pbe,w.renderCell=k=>{let T=null;if(t.default){const P=t.default(k);T=P.some(R=>R.type!==oa)?P:S(k)}else T=S(k);const{columns:M}=n.value.store.states,E=M.value.findIndex(P=>P.type==="default"),A=mbe(k,c.value&&k.cellIndex===E),I={class:"cell",style:{}};return w.showOverflowTooltip&&(I.class=`${I.class} ${p(s.namespace)}-tooltip`,I.style={width:`${(k.column.realWidth||Number(k.column.width))-1}px`}),m(T),ot("div",I,[A,T])}),w},getPropsData:(...w)=>w.reduce((S,k)=>(Ae(k)&&k.forEach(T=>{S[T]=e[T]}),S),{}),getColumnElIndex:(w,S)=>Array.prototype.indexOf.call(w,S),updateColumnOrder:()=>{n.value.store.commit("updateColumnOrder",a.columnConfig.value)}}}var bbe=q({name:"ElTableColumn",__name:"index",props:cbe,setup(e){const t=e,n=Xn(),a=Et(),r=Hl("table"),o=F({}),i=D(()=>{let k=a.parent;for(;k&&!k.tableId;)k=k.parent;return k}),{registerNormalWatchers:l,registerComplexWatchers:s}=gbe(i,t),{columnId:u,isSubColumn:c,realHeaderAlign:d,columnOrTableParent:v,setColumnWidth:f,setColumnForcedProps:h,setColumnRenders:m,getPropsData:g,getColumnElIndex:y,realAlign:b,updateColumnOrder:_}=ybe(t,n,i),w=v.value;u.value=yye("tableId"in w?w.tableId:w.columnId),g1(()=>{var P,R;c.value=i.value!==w;const k=t.type||"default",T=t.sortable===""?!0:t.sortable,M=k==="selection"?!1:zt(t.showOverflowTooltip)?w.props.showOverflowTooltip??((P=r.value)==null?void 0:P.showOverflowTooltip):t.showOverflowTooltip,E=zt(t.tooltipFormatter)?w.props.tooltipFormatter??((R=r.value)==null?void 0:R.tooltipFormatter):t.tooltipFormatter,A={...fbe[k],id:u.value,type:k,property:t.prop||t.property,align:b,headerAlign:d,showOverflowTooltip:M,tooltipFormatter:E,filterable:t.filters||t.filterMethod,filteredValue:[],filterPlacement:"",filterClassName:"",isColumnGroup:!1,isSubColumn:!1,filterOpened:!1,sortable:T,index:t.index,rawColumnKey:a.vnode.key};let I=g(["columnKey","label","className","labelClassName","type","renderHeader","formatter","fixed","resizable"],["sortMethod","sortBy","sortOrders"],["selectable","reserveSelection"],["filterMethod","filters","filterMultiple","filterOpened","filteredValue","filterPlacement","filterClassName"]);I=wye(A,I),I=Cye(m,f,h)(I),o.value=I,l(),s()}),mt(()=>{var E,A;const k=v.value,T=c.value?(E=k.vnode.el)==null?void 0:E.children:(A=k.refs.hiddenColumns)==null?void 0:A.children,M=()=>y(T||[],a.vnode.el);o.value.getColumnIndex=M,M()>-1&&i.value.store.commit("insertColumn",o.value,c.value?"columnConfig"in k&&k.columnConfig.value:null,_)}),rn(()=>{const k=o.value.getColumnIndex;(k?k():-1)>-1&&i.value.store.commit("removeColumn",o.value,c.value?"columnConfig"in w&&w.columnConfig.value:null,_)}),a.columnId=u.value,a.columnConfig=o;const S=()=>{var k,T;try{const M=(k=n.default)==null?void 0:k.call(n,{row:{},column:{},$index:-1}),E=[];if(Ae(M))for(const A of M)((T=A.type)==null?void 0:T.name)==="ElTableColumn"||A.shapeFlag&2?E.push(A):A.type===Ve&&Ae(A.children)&&A.children.forEach(I=>{(I==null?void 0:I.patchFlag)!==1024&&!Ye(I==null?void 0:I.children)&&E.push(I)});return ot("div",E)}catch{return ot("div",[])}};return(k,T)=>(x(),ne(S))}}),_be=bbe,wbe=_be,PD=wbe;const Sbe=wt(ube,{TableColumn:PD}),Cbe=On(PD),t_={asc:"desc",desc:"asc"},Sh=Symbol("placeholder"),DD=String,dp={type:ae(Array),required:!0},Pw={type:ae(Array)},OD={...Pw,required:!0},xbe=String,zx={type:ae(Array),default:()=>Nn([])},Du={type:Number,required:!0},$D={type:ae([String,Number,Symbol]),default:"id"},Fx={type:ae(Object)},Bu=$e({class:String,columns:dp,columnsStyles:{type:ae(Object),required:!0},depth:Number,expandColumnKey:xbe,estimatedRowHeight:{...Xu.estimatedRowHeight,default:void 0},isScrolling:Boolean,onRowExpand:{type:ae(Function)},onRowHover:{type:ae(Function)},onRowHeightChange:{type:ae(Function)},rowData:{type:ae(Object),required:!0},rowEventHandlers:{type:ae(Object)},rowIndex:{type:Number,required:!0},rowKey:$D,style:{type:ae(Object)}}),fy={type:Number,required:!0},Dw=$e({class:String,columns:dp,fixedHeaderData:{type:ae(Array)},headerData:{type:ae(Array),required:!0},headerHeight:{type:ae([Number,Array]),default:50},rowWidth:fy,rowHeight:{type:Number,default:50},height:fy,width:fy}),om=$e({columns:dp,data:OD,fixedData:Pw,estimatedRowHeight:Bu.estimatedRowHeight,width:Du,height:Du,headerWidth:Du,headerHeight:Dw.headerHeight,bodyWidth:Du,rowHeight:Du,cache:XL.cache,useIsScrolling:Boolean,scrollbarAlwaysOn:Xu.scrollbarAlwaysOn,scrollbarStartGap:Xu.scrollbarStartGap,scrollbarEndGap:Xu.scrollbarEndGap,class:DD,style:Fx,containerStyle:Fx,getRowHeight:{type:ae(Function),required:!0},rowKey:Bu.rowKey,onRowsRendered:{type:ae(Function)},onScroll:{type:ae(Function)}}),kbe=$e({cache:om.cache,estimatedRowHeight:Bu.estimatedRowHeight,rowKey:$D,headerClass:{type:ae([String,Function])},headerProps:{type:ae([Object,Function])},headerCellProps:{type:ae([Object,Function])},headerHeight:Dw.headerHeight,footerHeight:{type:Number,default:0},rowClass:{type:ae([String,Function])},rowProps:{type:ae([Object,Function])},rowHeight:{type:Number,default:50},cellProps:{type:ae([Object,Function])},columns:dp,data:OD,dataGetter:{type:ae(Function)},fixedData:Pw,expandColumnKey:Bu.expandColumnKey,expandedRowKeys:zx,defaultExpandedRowKeys:zx,class:DD,fixed:Boolean,style:{type:ae(Object)},width:Du,height:Du,maxHeight:Number,useIsScrolling:Boolean,indentSize:{type:Number,default:12},iconSize:{type:Number,default:12},hScrollbarSize:Xu.hScrollbarSize,vScrollbarSize:Xu.vScrollbarSize,scrollbarAlwaysOn:QL.alwaysOn,sortBy:{type:ae(Object),default:()=>({})},sortState:{type:ae(Object),default:void 0},onColumnSort:{type:ae(Function)},onExpandedRowsChange:{type:ae(Function)},onEndReached:{type:ae(Function)},onRowExpand:Bu.onRowExpand,onScroll:om.onScroll,onRowsRendered:om.onRowsRendered,rowEventHandlers:Bu.rowEventHandlers}),Tbe=(e,t,n)=>{const a={flexGrow:0,flexShrink:0,...n?{}:{flexGrow:e.flexGrow??0,flexShrink:e.flexShrink??1}},r={...e.style??{},...a,flexBasis:"auto",width:e.width};return t||(e.maxWidth&&(r.maxWidth=e.maxWidth),e.minWidth&&(r.minWidth=e.minWidth)),r};function Mbe(e,t,n){const a=D(()=>p(t).map((g,y)=>({...g,key:g.key??g.dataKey??y}))),r=D(()=>p(a).filter(g=>!g.hidden)),o=D(()=>p(r).filter(g=>g.fixed==="left"||g.fixed===!0)),i=D(()=>p(r).filter(g=>g.fixed==="right")),l=D(()=>p(r).filter(g=>!g.fixed)),s=D(()=>{const g=[];return p(o).forEach(y=>{g.push({...y,placeholderSign:Sh})}),p(l).forEach(y=>{g.push(y)}),p(i).forEach(y=>{g.push({...y,placeholderSign:Sh})}),g}),u=D(()=>p(o).length||p(i).length),c=D(()=>p(a).reduce((g,y)=>(g[y.key]=Tbe(y,p(n),e.fixed),g),{})),d=D(()=>p(r).reduce((g,y)=>g+y.width,0)),v=g=>p(a).find(y=>y.key===g),f=g=>p(c)[g],h=(g,y)=>{g.width=y};function m(g){var S;const{key:y}=g.currentTarget.dataset;if(!y)return;const{sortState:b,sortBy:_}=e;let w="asc";_t(b)?w=t_[b[y]]:w=t_[_.order],(S=e.onColumnSort)==null||S.call(e,{column:v(y),key:y,order:w})}return{columns:a,columnsStyles:c,columnsTotalWidth:d,fixedColumnsOnLeft:o,fixedColumnsOnRight:i,hasFixedColumns:u,mainColumns:s,normalColumns:l,visibleColumns:r,getColumn:v,getColumnStyle:f,updateColumnWidth:h,onColumnSorted:m}}const Ebe=(e,{mainTableRef:t,leftTableRef:n,rightTableRef:a,onMaybeEndReached:r})=>{const o=F({scrollLeft:0,scrollTop:0});function i(f){var m,g,y;const{scrollTop:h}=f;(m=t.value)==null||m.scrollTo(f),(g=n.value)==null||g.scrollToTop(h),(y=a.value)==null||y.scrollToTop(h)}function l(f){o.value=f,i(f)}function s(f){o.value.scrollTop=f,i(p(o))}function u(f){var h,m;o.value.scrollLeft=f,(m=(h=t.value)==null?void 0:h.scrollTo)==null||m.call(h,p(o))}function c(f){var h;l(f),(h=e.onScroll)==null||h.call(e,f)}function d({scrollTop:f}){const{scrollTop:h}=p(o);f!==h&&s(f)}function v(f,h="auto"){var m;(m=t.value)==null||m.scrollToRow(f,h)}return ye(()=>p(o).scrollTop,(f,h)=>{f>h&&r()}),{scrollPos:o,scrollTo:l,scrollToLeft:u,scrollToTop:s,scrollToRow:v,onScroll:c,onVerticalScroll:d}},Abe=(e,{mainTableRef:t,leftTableRef:n,rightTableRef:a,tableInstance:r,ns:o,isScrolling:i})=>{const l=Et(),{emit:s}=l,u=Ht(!1),c=F(e.defaultExpandedRowKeys||[]),d=F(-1),v=Ht(null),f=F({}),h=F({}),m=Ht({}),g=Ht({}),y=Ht({}),b=D(()=>Je(e.estimatedRowHeight));function _(A){var I;(I=e.onRowsRendered)==null||I.call(e,A),A.rowCacheEnd>p(d)&&(d.value=A.rowCacheEnd)}function w({hovered:A,rowKey:I}){i.value||r.vnode.el.querySelectorAll(`[rowkey="${String(I)}"]`).forEach(P=>{A?P.classList.add(o.is("hovered")):P.classList.remove(o.is("hovered"))})}function S({expanded:A,rowData:I,rowIndex:P,rowKey:R}){var H,z;const G=[...p(c)],K=G.indexOf(R);A?K===-1&&G.push(R):K>-1&&G.splice(K,1),c.value=G,s("update:expandedRowKeys",G),(H=e.onRowExpand)==null||H.call(e,{expanded:A,rowData:I,rowIndex:P,rowKey:R}),(z=e.onExpandedRowsChange)==null||z.call(e,G),r.vnode.el.querySelector(`.${o.is("hovered")}[rowkey="${String(R)}"]`)&&He(()=>w({hovered:!0,rowKey:R}))}const k=Yi(()=>{var A,I,P,R;u.value=!0,f.value={...p(f),...p(h)},T(p(v),!1),h.value={},v.value=null,(A=t.value)==null||A.forceUpdate(),(I=n.value)==null||I.forceUpdate(),(P=a.value)==null||P.forceUpdate(),(R=l.proxy)==null||R.$forceUpdate(),u.value=!1},0);function T(A,I=!1){p(b)&&[t,n,a].forEach(P=>{const R=p(P);R&&R.resetAfterRowIndex(A,I)})}function M(A,I,P){const R=p(v);(R===null||R>P)&&(v.value=P),h.value[A]=I}function E({rowKey:A,height:I,rowIndex:P},R){R?R==="right"?y.value[A]=I:m.value[A]=I:g.value[A]=I;const G=Math.max(...[m,y,g].map(K=>K.value[A]||0));p(f)[A]!==G&&(M(A,G,P),k())}return{expandedRowKeys:c,lastRenderedRowIndex:d,isDynamic:b,isResetting:u,rowHeights:f,resetAfterIndex:T,onRowExpanded:S,onRowHovered:w,onRowsRendered:_,onRowHeightChange:E}},Ibe=(e,{expandedRowKeys:t,lastRenderedRowIndex:n,resetAfterIndex:a})=>{const r=F({}),o=D(()=>{const l={},{data:s,rowKey:u}=e,c=p(t);if(!c||!c.length)return s;const d=[],v=new Set;c.forEach(h=>v.add(h));let f=s.slice();for(f.forEach(h=>l[h[u]]=0);f.length>0;){const h=f.shift();d.push(h),v.has(h[u])&&Ae(h.children)&&h.children.length>0&&(f=[...h.children,...f],h.children.forEach(m=>l[m[u]]=l[h[u]]+1))}return r.value=l,d}),i=D(()=>{const{data:l,expandColumnKey:s}=e;return s?p(o):l});return ye(i,(l,s)=>{l!==s&&(n.value=-1,a(0,!0))}),{data:i,depthMap:r}},Lbe=(e,t)=>e+t,im=e=>Ae(e)?e.reduce(Lbe,0):e,vc=(e,t,n={})=>Qe(e)?e(t):e??n,gs=e=>(["width","maxWidth","minWidth","height"].forEach(t=>{e[t]=Vn(e[t])}),e),RD=e=>bn(e)?t=>ot(e,t):e,Pbe=(e,{columnsTotalWidth:t,rowsHeight:n,fixedColumnsOnLeft:a,fixedColumnsOnRight:r})=>{const o=D(()=>{const{fixed:m,width:g,vScrollbarSize:y}=e,b=g-y;return m?Math.max(Math.round(p(t)),b):b}),i=D(()=>{const{height:m=0,maxHeight:g=0,footerHeight:y,hScrollbarSize:b}=e;if(g>0){const _=p(v),w=p(n),S=p(d)+_+w+b;return Math.min(S,g-y)}return m-y}),l=D(()=>{const{maxHeight:m}=e,g=p(i);if(Je(m)&&m>0)return g;const y=p(n)+p(d)+p(v);return Math.min(g,y)}),s=m=>m.width,u=D(()=>im(p(a).map(s))),c=D(()=>im(p(r).map(s))),d=D(()=>im(e.headerHeight)),v=D(()=>{var m;return(((m=e.fixedData)==null?void 0:m.length)||0)*e.rowHeight}),f=D(()=>p(i)-p(d)-p(v)),h=D(()=>{const{style:m={},height:g,width:y}=e;return gs({...m,height:g,width:y})});return{bodyWidth:o,fixedTableHeight:l,mainTableHeight:i,leftTableWidth:u,rightTableWidth:c,windowHeight:f,footerHeight:D(()=>gs({height:e.footerHeight})),emptyStyle:D(()=>({top:Vn(p(d)),bottom:Vn(e.footerHeight),width:Vn(e.width)})),rootStyle:h,headerHeight:d}};function Dbe(e){const t=F(),n=F(),a=F(),{columns:r,columnsStyles:o,columnsTotalWidth:i,fixedColumnsOnLeft:l,fixedColumnsOnRight:s,hasFixedColumns:u,mainColumns:c,onColumnSorted:d}=Mbe(e,cn(e,"columns"),cn(e,"fixed")),{scrollTo:v,scrollToLeft:f,scrollToTop:h,scrollToRow:m,onScroll:g,onVerticalScroll:y,scrollPos:b}=Ebe(e,{mainTableRef:t,leftTableRef:n,rightTableRef:a,onMaybeEndReached:oe}),_=ke("table-v2"),w=Et(),S=Ht(!1),{expandedRowKeys:k,lastRenderedRowIndex:T,isDynamic:M,isResetting:E,rowHeights:A,resetAfterIndex:I,onRowExpanded:P,onRowHeightChange:R,onRowHovered:G,onRowsRendered:K}=Abe(e,{mainTableRef:t,leftTableRef:n,rightTableRef:a,tableInstance:w,ns:_,isScrolling:S}),{data:H,depthMap:z}=Ibe(e,{expandedRowKeys:k,lastRenderedRowIndex:T,resetAfterIndex:I}),B=D(()=>{const{estimatedRowHeight:se,rowHeight:fe}=e,ge=p(H);return Je(se)?Object.values(p(A)).reduce((Se,De)=>Se+De,0):ge.length*fe}),{bodyWidth:W,fixedTableHeight:N,mainTableHeight:V,leftTableWidth:Y,rightTableWidth:X,windowHeight:ee,footerHeight:re,emptyStyle:ie,rootStyle:le,headerHeight:ve}=Pbe(e,{columnsTotalWidth:i,fixedColumnsOnLeft:l,fixedColumnsOnRight:s,rowsHeight:B}),he=F(),J=D(()=>{const se=p(H).length===0;return Ae(e.fixedData)?e.fixedData.length===0&&se:se});function Z(se){const{estimatedRowHeight:fe,rowHeight:ge,rowKey:Se}=e;return fe?p(A)[p(H)[se][Se]]||fe:ge}const j=F(!1);function oe(){const{onEndReached:se}=e;if(!se)return;const{scrollTop:fe}=p(b),ge=p(B),Se=ge-(fe+p(ee))+e.hScrollbarSize;!j.value&&p(T)>=0&&ge<=fe+p(V)-p(ve)?(j.value=!0,se(Se)):j.value=!1}return ye(()=>p(B),()=>j.value=!1),ye(()=>e.expandedRowKeys,se=>k.value=se,{deep:!0}),{columns:r,containerRef:he,mainTableRef:t,leftTableRef:n,rightTableRef:a,isDynamic:M,isResetting:E,isScrolling:S,hasFixedColumns:u,columnsStyles:o,columnsTotalWidth:i,data:H,expandedRowKeys:k,depthMap:z,fixedColumnsOnLeft:l,fixedColumnsOnRight:s,mainColumns:c,bodyWidth:W,emptyStyle:ie,rootStyle:le,footerHeight:re,mainTableHeight:V,fixedTableHeight:N,leftTableWidth:Y,rightTableWidth:X,showEmpty:J,getRowHeight:Z,onColumnSorted:d,onRowHovered:G,onRowExpanded:P,onRowsRendered:K,onRowHeightChange:R,scrollTo:v,scrollToLeft:f,scrollToTop:h,scrollToRow:m,onScroll:g,onVerticalScroll:y}}const Ow=Symbol("tableV2"),ND="tableV2GridScrollLeft",Obe=q({name:"ElTableV2Header",props:Dw,setup(e,{slots:t,expose:n}){const a=ke("table-v2"),r=Ze(ND),o=F(),i=D(()=>gs({width:e.width,height:e.height})),l=D(()=>gs({width:e.rowWidth,height:e.height})),s=D(()=>wa(p(e.headerHeight))),u=v=>{const f=p(o);He(()=>{f!=null&&f.scroll&&f.scroll({left:v})})},c=()=>{const v=a.e("fixed-header-row"),{columns:f,fixedHeaderData:h,rowHeight:m}=e;return h==null?void 0:h.map((g,y)=>{var _;const b=gs({height:m,width:"100%"});return(_=t.fixed)==null?void 0:_.call(t,{class:v,columns:f,rowData:g,rowIndex:-(y+1),style:b})})},d=()=>{const v=a.e("dynamic-header-row"),{columns:f}=e;return p(s).map((h,m)=>{var y;const g=gs({width:"100%",height:h});return(y=t.dynamic)==null?void 0:y.call(t,{class:v,columns:f,headerIndex:m,style:g})})};return oi(()=>{r!=null&&r.value&&u(r.value)}),n({scrollToLeft:u}),()=>{if(!(e.height<=0))return L("div",{ref:o,class:e.class,style:p(i),role:"rowgroup"},[L("div",{style:p(l),class:a.e("header")},[d(),c()])])}}}),$be="ElTableV2Grid",Rbe=e=>{const t=F(),n=F(),a=F(0),r=D(()=>{const{data:g,rowHeight:y,estimatedRowHeight:b}=e;if(!b)return g.length*y}),o=D(()=>{const{fixedData:g,rowHeight:y}=e;return((g==null?void 0:g.length)||0)*y}),i=D(()=>im(e.headerHeight)),l=D(()=>{const{height:g}=e;return Math.max(0,g-p(i)-p(o))}),s=D(()=>p(i)+p(o)>0),u=({data:g,rowIndex:y})=>g[y][e.rowKey];function c({rowCacheStart:g,rowCacheEnd:y,rowVisibleStart:b,rowVisibleEnd:_}){var w;(w=e.onRowsRendered)==null||w.call(e,{rowCacheStart:g,rowCacheEnd:y,rowVisibleStart:b,rowVisibleEnd:_})}function d(g,y){var b;(b=n.value)==null||b.resetAfterRowIndex(g,y)}function v(g,y){const b=p(t),_=p(n);_t(g)?(b==null||b.scrollToLeft(g.scrollLeft),a.value=g.scrollLeft,_==null||_.scrollTo(g)):(b==null||b.scrollToLeft(g),a.value=g,_==null||_.scrollTo({scrollLeft:g,scrollTop:y}))}function f(g){var y;(y=p(n))==null||y.scrollTo({scrollTop:g})}function h(g,y){const b=p(n);if(!b)return;const _=a.value;b.scrollToItem(g,0,y),_&&v({scrollLeft:_})}function m(){var g,y;(g=p(n))==null||g.$forceUpdate(),(y=p(t))==null||y.$forceUpdate()}return ye(()=>e.bodyWidth,()=>{var g;Je(e.estimatedRowHeight)&&((g=n.value)==null||g.resetAfter({columnIndex:0},!1))}),{bodyRef:n,forceUpdate:m,fixedRowHeight:o,gridHeight:l,hasHeader:s,headerHeight:i,headerRef:t,totalHeight:r,itemKey:u,onItemRendered:c,resetAfterRowIndex:d,scrollTo:v,scrollToTop:f,scrollToRow:h,scrollLeft:a}},$w=q({name:$be,props:om,setup(e,{slots:t,expose:n}){const{ns:a}=Ze(Ow),{bodyRef:r,fixedRowHeight:o,gridHeight:i,hasHeader:l,headerRef:s,headerHeight:u,totalHeight:c,forceUpdate:d,itemKey:v,onItemRendered:f,resetAfterRowIndex:h,scrollTo:m,scrollToTop:g,scrollToRow:y,scrollLeft:b}=Rbe(e);Ft(ND,b),Wh(async()=>{var S;await He();const w=(S=r.value)==null?void 0:S.states.scrollTop;w&&g(Math.round(w)+1)}),n({forceUpdate:d,totalHeight:c,scrollTo:m,scrollToTop:g,scrollToRow:y,resetAfterRowIndex:h});const _=()=>e.bodyWidth;return()=>{const{cache:w,columns:S,data:k,fixedData:T,useIsScrolling:M,scrollbarAlwaysOn:E,scrollbarEndGap:A,scrollbarStartGap:I,style:P,rowHeight:R,bodyWidth:G,estimatedRowHeight:K,headerWidth:H,height:z,width:B,getRowHeight:W,onScroll:N}=e,V=Je(K),Y=V?$de:Lde,X=p(u);return L("div",{role:"table",class:[a.e("table"),e.class],style:P},[L(Y,{ref:r,data:k,useIsScrolling:M,itemKey:v,columnCache:0,columnWidth:V?_:G,totalColumn:1,totalRow:k.length,rowCache:w,rowHeight:V?W:R,width:B,height:p(i),class:a.e("body"),role:"rowgroup",scrollbarStartGap:I,scrollbarEndGap:A,scrollbarAlwaysOn:E,onScroll:N,onItemRendered:f,perfMode:!1},{default:ee=>{var ie;const re=k[ee.rowIndex];return(ie=t.row)==null?void 0:ie.call(t,{...ee,columns:S,rowData:re})}}),p(l)&&L(Obe,{ref:s,class:a.e("header-wrapper"),columns:S,headerData:k,headerHeight:e.headerHeight,fixedHeaderData:T,rowWidth:H,rowHeight:R,width:B,height:Math.min(X+p(o),z)},{dynamic:t.header,fixed:t.row})])}}});function Nbe(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}const Bbe=(e,{slots:t})=>{const{mainTableRef:n,...a}=e;return L($w,Tt({ref:n},a),Nbe(t)?t:{default:()=>[t]})};function Vbe(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}const zbe=(e,{slots:t})=>{if(!e.columns.length)return;const{leftTableRef:n,...a}=e;return L($w,Tt({ref:n},a),Vbe(t)?t:{default:()=>[t]})};function Fbe(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}const Hbe=(e,{slots:t})=>{if(!e.columns.length)return;const{rightTableRef:n,...a}=e;return L($w,Tt({ref:n},a),Fbe(t)?t:{default:()=>[t]})},Wbe=e=>{const{isScrolling:t}=Ze(Ow),n=F(!1),a=F(),r=D(()=>Je(e.estimatedRowHeight)&&e.rowIndex>=0),o=(s=!1)=>{const u=p(a);if(!u)return;const{columns:c,onRowHeightChange:d,rowKey:v,rowIndex:f,style:h}=e,{height:m}=u.getBoundingClientRect();n.value=!0,He(()=>{if(s||m!==Number.parseInt(h.height)){const g=c[0],y=(g==null?void 0:g.placeholderSign)===Sh;d==null||d({rowKey:v,height:m,rowIndex:f},g&&!y&&g.fixed)}})},i=D(()=>{const{rowData:s,rowIndex:u,rowKey:c,onRowHover:d}=e,v=e.rowEventHandlers||{},f={};return Object.entries(v).forEach(([h,m])=>{Qe(m)&&(f[h]=g=>{m({event:g,rowData:s,rowIndex:u,rowKey:c})})}),d&&[{name:"onMouseleave",hovered:!1},{name:"onMouseenter",hovered:!0}].forEach(({name:h,hovered:m})=>{const g=f[h];f[h]=y=>{d({event:y,hovered:m,rowData:s,rowIndex:u,rowKey:c}),g==null||g(y)}}),f}),l=s=>{const{onRowExpand:u,rowData:c,rowIndex:d,rowKey:v}=e;u==null||u({expanded:s,rowData:c,rowIndex:d,rowKey:v})};return mt(()=>{p(r)&&o(!0)}),{isScrolling:t,measurable:r,measured:n,rowRef:a,eventHandlers:i,onExpand:l}},Kbe=q({name:"ElTableV2TableRow",props:Bu,setup(e,{expose:t,slots:n,attrs:a}){const{eventHandlers:r,isScrolling:o,measurable:i,measured:l,rowRef:s,onExpand:u}=Wbe(e);return t({onExpand:u}),()=>{const{columns:c,columnsStyles:d,expandColumnKey:v,depth:f,rowData:h,rowIndex:m,style:g}=e;let y=c.map((b,_)=>{const w=Ae(h.children)&&h.children.length>0&&b.key===v;return n.cell({column:b,columns:c,columnIndex:_,depth:f,style:d[b.key],rowData:h,rowIndex:m,isScrolling:p(o),expandIconProps:w?{rowData:h,rowIndex:m,onExpand:u}:void 0})});if(n.row&&(y=n.row({cells:y.map(b=>Ae(b)&&b.length===1?b[0]:b),style:g,columns:c,depth:f,rowData:h,rowIndex:m,isScrolling:p(o)})),p(i)){const{height:b,..._}=g||{},w=p(l);return L("div",Tt({ref:s,class:e.class,style:w?g:_,role:"row"},a,p(r)),[y])}return L("div",Tt(a,{ref:s,class:e.class,style:g,role:"row"},p(r)),[y])}}});function Ube(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}const Gbe=(e,{slots:t})=>{const{columns:n,columnsStyles:a,depthMap:r,expandColumnKey:o,expandedRowKeys:i,estimatedRowHeight:l,hasFixedColumns:s,rowData:u,rowIndex:c,style:d,isScrolling:v,rowProps:f,rowClass:h,rowKey:m,rowEventHandlers:g,ns:y,onRowHovered:b,onRowExpanded:_}=e,w=vc(h,{columns:n,rowData:u,rowIndex:c},""),S=vc(f,{columns:n,rowData:u,rowIndex:c}),k=u[m],T=r[k]||0,M=!!o,E=c<0,A=[y.e("row"),w,y.is("expanded",M&&i.includes(k)),y.is("fixed",!T&&E),y.is("customized",!!t.row),{[y.e(`row-depth-${T}`)]:M&&c>=0}],I=s?b:void 0,P={...S,columns:n,columnsStyles:a,class:A,depth:T,expandColumnKey:o,estimatedRowHeight:E?void 0:l,isScrolling:v,rowIndex:c,rowData:u,rowKey:k,rowEventHandlers:g,style:d};return L(Kbe,Tt(P,{onRowExpand:_,onMouseenter:K=>{I==null||I({hovered:!0,rowKey:k,event:K,rowData:u,rowIndex:c})},onMouseleave:K=>{I==null||I({hovered:!1,rowKey:k,event:K,rowData:u,rowIndex:c})},rowkey:k}),Ube(t)?t:{default:()=>[t]})},Rw=(e,{slots:t})=>{var i;const{cellData:n,style:a}=e,r=((i=n==null?void 0:n.toString)==null?void 0:i.call(n))||"",o=ce(t,"default",e,()=>[r]);return L("div",{class:e.class,title:r,style:a},[o])};Rw.displayName="ElTableV2Cell";Rw.inheritAttrs=!1;const BD=e=>{const{expanded:t,expandable:n,onExpand:a,style:r,size:o,ariaLabel:i}=e;return L("button",Tt({onClick:n?()=>a(!t):void 0,ariaLabel:i,ariaExpanded:t,class:e.class},{type:"button"}),[L(Xe,{size:o,style:r},{default:()=>[L(za,null,null)]})])};BD.inheritAttrs=!1;const n_=({columns:e,column:t,columnIndex:n,depth:a,expandIconProps:r,isScrolling:o,rowData:i,rowIndex:l,style:s,expandedRowKeys:u,ns:c,t:d,cellProps:v,expandColumnKey:f,indentSize:h,iconSize:m,rowKey:g},{slots:y})=>{const b=gs(s);if(t.placeholderSign===Sh)return L("div",{class:c.em("row-cell","placeholder"),style:b},null);const{cellRenderer:_,dataKey:w,dataGetter:S}=t,k=Qe(S)?S({columns:e,column:t,columnIndex:n,rowData:i,rowIndex:l}):na(i,w??""),T=vc(v,{cellData:k,columns:e,column:t,columnIndex:n,rowIndex:l,rowData:i}),M={class:c.e("cell-text"),columns:e,column:t,columnIndex:n,cellData:k,isScrolling:o,rowData:i,rowIndex:l},E=RD(_),A=E?E(M):ce(y,"default",M,()=>[L(Rw,M,null)]),I=[c.e("row-cell"),t.class,t.align==="center"&&c.is("align-center"),t.align==="right"&&c.is("align-right")],P=l>=0&&f&&t.key===f,R=l>=0&&u.includes(i[g]);let G;const K=`margin-inline-start: ${a*h}px;`;return P&&(_t(r)?G=L(BD,Tt(r,{class:[c.e("expand-icon"),c.is("expanded",R)],size:m,expanded:R,ariaLabel:d(R?"el.table.collapseRowLabel":"el.table.expandRowLabel"),style:K,expandable:!0}),null):G=L("div",{style:[K,`width: ${m}px; height: ${m}px;`].join(" ")},null)),L("div",Tt({class:I,style:b},T,{role:"cell"}),[G,A])};n_.inheritAttrs=!1;const Ybe=$e({class:String,columns:dp,columnsStyles:{type:ae(Object),required:!0},headerIndex:Number,style:{type:ae(Object)}}),jbe=q({name:"ElTableV2HeaderRow",props:Ybe,setup(e,{slots:t}){return()=>{const{columns:n,columnsStyles:a,headerIndex:r,style:o}=e;let i=n.map((l,s)=>t.cell({columns:n,column:l,columnIndex:s,headerIndex:r,style:a[l.key]}));return t.header&&(i=t.header({cells:i.map(l=>Ae(l)&&l.length===1?l[0]:l),columns:n,headerIndex:r})),L("div",{class:e.class,style:o,role:"row"},[i])}}});function qbe(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}const Xbe=({columns:e,columnsStyles:t,headerIndex:n,style:a,headerClass:r,headerProps:o,ns:i},{slots:l})=>{const s={columns:e,headerIndex:n},u=[i.e("header-row"),vc(r,s,""),i.is("customized",!!l.header)];return L(jbe,{...vc(o,s),columnsStyles:t,class:u,columns:e,headerIndex:n,style:a},qbe(l)?l:{default:()=>[l]})},Nw=(e,{slots:t})=>ce(t,"default",e,()=>{var n,a;return[L("div",{class:e.class,title:(n=e.column)==null?void 0:n.title},[(a=e.column)==null?void 0:a.title])]});Nw.displayName="ElTableV2HeaderCell";Nw.inheritAttrs=!1;const Zbe=e=>{const{sortOrder:t}=e;return L("button",{type:"button","aria-label":e.ariaLabel,class:e.class},[L(Xe,{size:14},{default:()=>[L(t==="asc"?KI:WI,null,null)]})])},Hx=(e,{slots:t})=>{const{column:n,ns:a,t:r,style:o,onColumnSorted:i}=e,l=gs(o);if(n.placeholderSign===Sh)return L("div",{class:a.em("header-row-cell","placeholder"),style:l},null);const{headerCellRenderer:s,headerClass:u,sortable:c}=n,d={...e,class:a.e("header-cell-text")},v=RD(s),f=v?v(d):ce(t,"default",d,()=>[L(Nw,d,null)]),{sortBy:h,sortState:m,headerCellProps:g}=e;let y,b,_;if(m){const S=m[n.key];y=!!t_[S],b=y?S:"asc"}else y=n.key===h.key,b=y?h.order:"asc";b==="asc"?_="ascending":b==="desc"?_="descending":_=void 0;const w=[a.e("header-cell"),vc(u,e,""),n.align==="center"&&a.is("align-center"),n.align==="right"&&a.is("align-right"),c&&a.is("sortable")];return L("div",Tt({...vc(g,e),onClick:n.sortable?i:void 0,ariaSort:c?_:void 0,class:w,style:l,"data-key":n.key},{role:"columnheader"}),[f,c&&L(Zbe,{class:[a.e("sort-icon"),y&&a.is("sorting")],sortOrder:b,ariaLabel:r("el.table.sortLabel",{column:n.title||""})},null)])},VD=(e,{slots:t})=>{var n;return L("div",{class:e.class,style:e.style},[(n=t.default)==null?void 0:n.call(t)])};VD.displayName="ElTableV2Footer";const zD=(e,{slots:t})=>{const n=ce(t,"default",{},()=>[L(dD,null,null)]);return L("div",{class:e.class,style:e.style},[n])};zD.displayName="ElTableV2Empty";const FD=(e,{slots:t})=>{var n;return L("div",{class:e.class,style:e.style},[(n=t.default)==null?void 0:n.call(t)])};FD.displayName="ElTableV2Overlay";function $f(e){return typeof e=="function"||Object.prototype.toString.call(e)==="[object Object]"&&!bn(e)}const Jbe=q({name:"ElTableV2",props:kbe,slots:Object,setup(e,{slots:t,expose:n}){const a=ke("table-v2"),{t:r}=Vt(),{columnsStyles:o,fixedColumnsOnLeft:i,fixedColumnsOnRight:l,mainColumns:s,mainTableHeight:u,fixedTableHeight:c,leftTableWidth:d,rightTableWidth:v,data:f,depthMap:h,expandedRowKeys:m,hasFixedColumns:g,mainTableRef:y,leftTableRef:b,rightTableRef:_,isDynamic:w,isResetting:S,isScrolling:k,bodyWidth:T,emptyStyle:M,rootStyle:E,footerHeight:A,showEmpty:I,scrollTo:P,scrollToLeft:R,scrollToTop:G,scrollToRow:K,getRowHeight:H,onColumnSorted:z,onRowHeightChange:B,onRowHovered:W,onRowExpanded:N,onRowsRendered:V,onScroll:Y,onVerticalScroll:X}=Dbe(e);return n({scrollTo:P,scrollToLeft:R,scrollToTop:G,scrollToRow:K}),Ft(Ow,{ns:a,isResetting:S,isScrolling:k}),()=>{const{cache:ee,cellProps:re,estimatedRowHeight:ie,expandColumnKey:le,fixedData:ve,headerHeight:he,headerClass:J,headerProps:Z,headerCellProps:j,sortBy:oe,sortState:se,rowHeight:fe,rowClass:ge,rowEventHandlers:Se,rowKey:De,rowProps:Te,scrollbarAlwaysOn:Re,indentSize:Ce,iconSize:Le,useIsScrolling:me,vScrollbarSize:Be,width:ze}=e,xe=p(f),rt={cache:ee,class:a.e("main"),columns:p(s),data:xe,fixedData:ve,estimatedRowHeight:ie,bodyWidth:p(T),headerHeight:he,headerWidth:p(T),height:p(u),mainTableRef:y,rowKey:De,rowHeight:fe,scrollbarAlwaysOn:Re,scrollbarStartGap:2,scrollbarEndGap:Be,useIsScrolling:me,width:ze,getRowHeight:H,onRowsRendered:V,onScroll:Y},nt=p(d),Pt=p(c),Wt={cache:ee,class:a.e("left"),columns:p(i),data:xe,fixedData:ve,estimatedRowHeight:ie,leftTableRef:b,rowHeight:fe,bodyWidth:nt,headerWidth:nt,headerHeight:he,height:Pt,rowKey:De,scrollbarAlwaysOn:Re,scrollbarStartGap:2,scrollbarEndGap:Be,useIsScrolling:me,width:nt,getRowHeight:H,onScroll:X},Jt=p(v),Rt={cache:ee,class:a.e("right"),columns:p(l),data:xe,fixedData:ve,estimatedRowHeight:ie,rightTableRef:_,rowHeight:fe,bodyWidth:Jt,headerWidth:Jt,headerHeight:he,height:Pt,rowKey:De,scrollbarAlwaysOn:Re,scrollbarStartGap:2,scrollbarEndGap:Be,width:Jt,style:`${a.cssVarName("table-scrollbar-size")}: ${Be}px`,useIsScrolling:me,getRowHeight:H,onScroll:X},Qt=p(o),_e={ns:a,depthMap:p(h),columnsStyles:Qt,expandColumnKey:le,expandedRowKeys:p(m),estimatedRowHeight:ie,hasFixedColumns:p(g),rowProps:Te,rowClass:ge,rowKey:De,rowEventHandlers:Se,onRowHovered:W,onRowExpanded:N,onRowHeightChange:B},Ne={cellProps:re,expandColumnKey:le,indentSize:Ce,iconSize:Le,rowKey:De,expandedRowKeys:p(m),ns:a,t:r},tt={ns:a,headerClass:J,headerProps:Z,columnsStyles:Qt},Me={ns:a,t:r,sortBy:oe,sortState:se,headerCellProps:j,onColumnSorted:z},Ue={row:ft=>L(Gbe,Tt(ft,_e),{row:t.row,cell:ht=>{let pt;return t.cell?L(n_,Tt(ht,Ne,{style:Qt[ht.column.key]}),$f(pt=t.cell(ht))?pt:{default:()=>[pt]}):L(n_,Tt(ht,Ne,{style:Qt[ht.column.key]}),null)}}),header:ft=>L(Xbe,Tt(ft,tt),{header:t.header,cell:ht=>{let pt;return t["header-cell"]?L(Hx,Tt(ht,Me,{style:Qt[ht.column.key]}),$f(pt=t["header-cell"](ht))?pt:{default:()=>[pt]}):L(Hx,Tt(ht,Me,{style:Qt[ht.column.key]}),null)}})},Gt=[e.class,a.b(),a.e("root"),a.is("dynamic",p(w))],je={class:a.e("footer"),style:p(A)};return L("div",{class:Gt,style:p(E)},[L(Bbe,rt,$f(Ue)?Ue:{default:()=>[Ue]}),L(zbe,Wt,$f(Ue)?Ue:{default:()=>[Ue]}),L(Hbe,Rt,$f(Ue)?Ue:{default:()=>[Ue]}),t.footer&&L(VD,je,{default:t.footer}),p(I)&&L(zD,{class:a.e("empty"),style:p(M)},{default:t.empty}),t.overlay&&L(FD,{class:a.e("overlay")},{default:t.overlay})])}}}),Qbe=$e({disableWidth:Boolean,disableHeight:Boolean,onResize:{type:ae(Function)}}),e4e=e=>{const t=F(),n=F(0),a=F(0);let r;return mt(()=>{r=Mn(t,([o])=>{const{width:i,height:l}=o.contentRect,{paddingLeft:s,paddingRight:u,paddingTop:c,paddingBottom:d}=getComputedStyle(o.target),v=Number.parseInt(s)||0,f=Number.parseInt(u)||0,h=Number.parseInt(c)||0,m=Number.parseInt(d)||0;n.value=i-v-f,a.value=l-h-m}).stop}),rn(()=>{r==null||r()}),ye([n,a],([o,i])=>{var l;(l=e.onResize)==null||l.call(e,{width:o,height:i})}),{sizer:t,width:n,height:a}},t4e=q({name:"ElAutoResizer",props:Qbe,setup(e,{slots:t}){const n=ke("auto-resizer"),{height:a,width:r,sizer:o}=e4e(e),i={width:"100%",height:"100%"};return()=>{var l;return L("div",{ref:o,class:n.b(),style:i},[(l=t.default)==null?void 0:l.call(t,{height:a.value,width:r.value})])}}}),n4e=wt(Jbe),a4e=wt(t4e),j1=Symbol("tabsRootContextKey"),r4e=$e({tabs:{type:ae(Array),default:()=>Nn([])},tabRefs:{type:ae(Object),default:()=>Nn({})}}),Wx="ElTabBar";var o4e=q({name:Wx,__name:"tab-bar",props:r4e,setup(e,{expose:t}){const n=e,a=Ze(j1);a||Dn(Wx,"");const r=ke("tabs"),o=F(),i=F(),l=D(()=>{var f;return zt(a.props.defaultValue)||!!((f=i.value)!=null&&f.transform)}),s=()=>{let f=0,h=0;const m=["top","bottom"].includes(a.props.tabPosition)?"width":"height",g=m==="width"?"x":"y",y=g==="x"?"left":"top";return n.tabs.every(b=>{if(zt(b.paneName))return!1;const _=n.tabRefs[b.paneName];if(!_)return!1;if(!b.active)return!0;f=_[`offset${W2(y)}`],h=_[`client${W2(m)}`];const w=window.getComputedStyle(_);return m==="width"&&(h-=Number.parseFloat(w.paddingLeft)+Number.parseFloat(w.paddingRight),f+=Number.parseFloat(w.paddingLeft)),!1}),{[m]:`${h}px`,transform:`translate${W2(g)}(${f}px)`}},u=()=>i.value=s(),c=[],d=()=>{c.forEach(f=>f.stop()),c.length=0,Object.values(n.tabRefs).forEach(f=>{c.push(Mn(f,u))})};ye(()=>n.tabs,async()=>{await He(),u(),d()},{immediate:!0});const v=Mn(o,()=>u());return rn(()=>{c.forEach(f=>f.stop()),c.length=0,v.stop()}),t({ref:o,update:u}),(f,h)=>l.value?(x(),O("div",{key:0,ref_key:"barRef",ref:o,class:U([p(r).e("active-bar"),p(r).is(p(a).props.tabPosition)]),style:et(i.value)},null,6)):te("v-if",!0)}}),i4e=o4e;const l4e=5,s4e=({scrollable:e,navOffset:t,navSize:n,navContainerSize:a,isHorizontal:r})=>{const o=F(!1),i=D(()=>Math.max(n.value-a.value,0));let l,s;const u=v=>{if(!e.value||v.touches.length!==1)return;const{clientX:f,clientY:h}=sg(v);l={startX:f,startY:h,startOffset:t.value},s=void 0},c=v=>{if(!l||!e.value)return;if(v.touches.length!==1){d();return}const{clientX:f,clientY:h}=sg(v),m=l.startX-f,g=l.startY-h,y=r.value?m:g,b=r.value?g:m,_=Math.abs(y),w=Math.abs(b);if(CA(s)){if(Math.max(_,w)<=l4e)return;s=_>w}if(!s)return;const S=Br(l.startOffset+y,0,i.value);i.value<=0||S===t.value||!v.cancelable||(v.preventDefault(),o.value=!0,t.value=S)},d=()=>{l=void 0,s=void 0,o.value=!1};return{isTouchScrolling:o,handleTouchStart:u,handleTouchMove:c,handleTouchEnd:d}},u4e=$e({panes:{type:ae(Array),default:()=>Nn([])},currentName:{type:[String,Number],default:""},editable:Boolean,type:{type:String,values:["card","border-card",""],default:""},stretch:Boolean,tabindex:{type:[String,Number],default:void 0}}),c4e={tabClick:(e,t,n)=>n instanceof Event,tabRemove:(e,t)=>t instanceof Event},Kx="ElTabNav",d4e=q({name:Kx,props:u4e,emits:c4e,setup(e,{expose:t,emit:n}){const a=Ze(j1);a||Dn(Kx,"");const r=ke("tabs"),o=LY(),i=OY(),l=F(),s=F(),u=F(),c=F({}),d=F(),v=F(!1),f=F(0),h=F(!1),m=F(!0),g=F(!1),y=Ht(),b=D(()=>["top","bottom"].includes(a.props.tabPosition)),_=D(()=>b.value?"width":"height"),w=D(()=>{const ie=_.value==="width"?"X":"Y";return{transition:g.value||R.value?"none":void 0,transform:`translate${ie}(-${f.value}px)`}}),{width:S,height:k}=Um(l),{width:T,height:M}=Um(s,{width:0,height:0},{box:"border-box"}),E=D(()=>b.value?S.value:k.value),A=D(()=>b.value?T.value:M.value),{onWheel:I}=tP({atStartEdge:D(()=>f.value<=0),atEndEdge:D(()=>A.value-f.value<=E.value),layout:D(()=>b.value?"horizontal":"vertical")},ie=>{f.value=Br(f.value+ie,0,A.value-E.value)}),P=ie=>{g.value=!0,I(ie),Wr(()=>{g.value=!1})},{isTouchScrolling:R,handleTouchStart:G,handleTouchMove:K,handleTouchEnd:H}=s4e({scrollable:v,navOffset:f,navSize:A,navContainerSize:E,isHorizontal:b}),z=()=>{if(!l.value)return;const ie=l.value.getBoundingClientRect()[_.value],le=f.value;le&&(f.value=le>ie?le-ie:0)},B=()=>{if(!l.value||!s.value)return;const ie=s.value.getBoundingClientRect()[_.value],le=l.value.getBoundingClientRect()[_.value],ve=f.value;Vi(ie-ve,le)&&(f.value=ie-ve>le*2?ve+le:ie-le)},W=async()=>{const ie=s.value;if(!v.value||!u.value||!l.value||!ie)return;await He();const le=c.value[e.currentName];if(!le)return;const ve=l.value,he=le.getBoundingClientRect(),J=ve.getBoundingClientRect(),Z=J.left+1,j=J.right-1,oe=ie.getBoundingClientRect(),se=b.value?oe.width-J.width:oe.height-J.height,fe=f.value;let ge=fe;b.value?(he.leftj&&(ge=fe+he.right-j)):(he.topJ.bottom&&(ge=fe+(he.bottom-J.bottom))),ge=Math.max(ge,0),f.value=Math.min(ge,se)},N=()=>{var he;if(!s.value||!l.value)return;e.stretch&&((he=d.value)==null||he.update());const ie=s.value.getBoundingClientRect()[_.value],le=l.value.getBoundingClientRect()[_.value],ve=f.value;le0&&(f.value=0))},V=ie=>{const le=mn(ie);let ve=0;switch(le){case Pe.left:case Pe.up:ve=-1;break;case Pe.right:case Pe.down:ve=1;break;default:return}const he=Array.from(ie.currentTarget.querySelectorAll("[role=tab]:not(.is-disabled)"));let J=he.indexOf(ie.target)+ve;J<0?J=he.length-1:J>=he.length&&(J=0),he[J].focus({preventScroll:!0}),he[J].click(),Y()},Y=()=>{m.value&&(h.value=!0)},X=()=>h.value=!1,ee=(ie,le)=>{c.value[le]=ie},re=async()=>{var ie;await He(),(ie=c.value[e.currentName])==null||ie.focus({preventScroll:!0})};return ye(o,ie=>{ie==="hidden"?m.value=!1:ie==="visible"&&setTimeout(()=>m.value=!0,50)}),ye(i,ie=>{ie?setTimeout(()=>m.value=!0,50):m.value=!1}),Mn(u,()=>{Wr(N)}),mt(()=>setTimeout(()=>W(),0)),oi(()=>N()),t({scrollToActiveTab:W,removeFocus:X,focusActiveTab:re,tabListRef:s,tabBarRef:d,scheduleRender:()=>Y0(y)}),()=>{const ie=v.value?[L("span",{class:[r.e("nav-prev"),r.is("disabled",!v.value.prev)],onClick:z},[L(Xe,null,{default:()=>[L(Ji,null,null)]})]),L("span",{class:[r.e("nav-next"),r.is("disabled",!v.value.next)],onClick:B},[L(Xe,null,{default:()=>[L(za,null,null)]})])]:null,le=e.panes.map((ve,he)=>{var Se,De;const J=ve.uid,Z=ve.props.disabled,j=ve.props.name??ve.index??`${he}`,oe=!Z&&(ve.isClosable||ve.props.closable!==!1&&e.editable);ve.index=`${he}`;const se=oe?L(Xe,{class:"is-icon-close",onClick:Te=>n("tabRemove",ve,Te)},{default:()=>[L(so,null,null)]}):null,fe=((De=(Se=ve.slots).label)==null?void 0:De.call(Se))||ve.props.label,ge=!Z&&ve.active?e.tabindex??a.props.tabindex:-1;return L("div",{ref:Te=>ee(Te,j),class:[r.e("item"),r.is(a.props.tabPosition),r.is("active",ve.active),r.is("disabled",Z),r.is("closable",oe),r.is("focus",h.value)],id:`tab-${j}`,key:`tab-${J}`,"aria-controls":`pane-${j}`,role:"tab","aria-selected":ve.active,tabindex:ge,onFocus:()=>Y(),onBlur:()=>X(),onClick:Te=>{X(),n("tabClick",ve,j,Te)},onKeydown:Te=>{const Re=mn(Te);oe&&(Re===Pe.delete||Re===Pe.backspace)&&n("tabRemove",ve,Te)}},[fe,se])});return y.value,L("div",{ref:u,class:[r.e("nav-wrap"),r.is("scrollable",!!v.value),r.is(a.props.tabPosition)]},[ie,L("div",{class:r.e("nav-scroll"),ref:l},[e.panes.length>0?L("div",{class:[r.e("nav"),r.is(a.props.tabPosition),r.is("stretch",e.stretch&&["top","bottom"].includes(a.props.tabPosition))],ref:s,style:w.value,role:"tablist",onKeydown:V,onWheel:P,onTouchstart:G,onTouchmove:K,onTouchend:H,onTouchcancel:H},[e.type?null:L(i4e,{ref:d,tabs:[...e.panes],tabRefs:c.value},null),le]):null])])}}}),f4e=$e({type:{type:String,values:["card","border-card",""],default:""},closable:Boolean,addable:Boolean,modelValue:{type:[String,Number]},defaultValue:{type:[String,Number]},editable:Boolean,tabPosition:{type:String,values:["top","right","bottom","left"],default:"top"},beforeLeave:{type:ae(Function),default:()=>!0},stretch:Boolean,tabindex:{type:[String,Number],default:0}}),vy=e=>Ye(e)||Je(e),v4e={[bt]:e=>vy(e),tabClick:(e,t)=>t instanceof Event,tabChange:e=>vy(e),edit:(e,t)=>["remove","add"].includes(t),tabRemove:e=>vy(e),tabAdd:()=>!0},h4e=q({name:"ElTabs",props:f4e,emits:v4e,setup(e,{emit:t,slots:n,expose:a}){const r=ke("tabs"),o=D(()=>["left","right"].includes(e.tabPosition)),{children:i,addChild:l,removeChild:s,ChildrenSorter:u}=z1(Et(),"ElTabPane"),c=F(),d=F((zt(e.modelValue)?e.defaultValue:e.modelValue)??"0"),v=async(b,_=!1)=>{var w,S,k,T;if(!(d.value===b||zt(b)))try{let M;if(e.beforeLeave){const E=e.beforeLeave(b,d.value);M=E instanceof Promise?await E:E}else M=!0;if(M!==!1){const E=(w=i.value.find(A=>A.paneName===d.value))==null?void 0:w.isFocusInsidePane();d.value=b,_&&(t(bt,b),t("tabChange",b)),(k=(S=c.value)==null?void 0:S.removeFocus)==null||k.call(S),E&&((T=c.value)==null||T.focusActiveTab())}}catch{}},f=(b,_,w)=>{b.props.disabled||(t("tabClick",b,w),v(_,!0))},h=(b,_)=>{b.props.disabled||zt(b.props.name)||(_.stopPropagation(),t("edit",b.props.name,"remove"),t("tabRemove",b.props.name))},m=()=>{t("edit",void 0,"add"),t("tabAdd")},g=b=>{const _=mn(b);[Pe.enter,Pe.numpadEnter].includes(_)&&m()},y=b=>{const _=b.el.firstChild,w=["bottom","right"].includes(e.tabPosition)?b.children[0].el:b.children[1].el;_!==w&&_.before(w)};return ye(()=>e.modelValue,b=>v(b)),ye(d,async()=>{var b;await He(),(b=c.value)==null||b.scrollToActiveTab()}),Ft(j1,{props:e,currentName:d,registerPane:l,unregisterPane:s,nav$:c}),a({currentName:d,get tabNavRef(){return Tc(c.value,["scheduleRender"])}}),()=>{const b=n["add-icon"],_=e.editable||e.addable?L("div",{class:[r.e("new-tab"),o.value&&r.e("new-tab-vertical")],tabindex:e.tabindex,onClick:m,onKeydown:g},[b?ce(n,"add-icon"):L(Xe,{class:r.is("icon-plus")},{default:()=>[L(hf,null,null)]})]):null,w=()=>L(d4e,{ref:c,currentName:d.value,editable:e.editable,type:e.type,panes:i.value,stretch:e.stretch,onTabClick:f,onTabRemove:h},null),S=L("div",{class:[r.e("header"),o.value&&r.e("header-vertical"),r.is(e.tabPosition)]},[L(u,null,{default:w,$stable:!0}),_]),k=L("div",{class:r.e("content")},[ce(n,"default")]);return L("div",{class:[r.b(),r.m(e.tabPosition),{[r.m("card")]:e.type==="card",[r.m("border-card")]:e.type==="border-card"}],onVnodeMounted:y,onVnodeUpdated:y},[k,S])}}}),p4e=$e({label:{type:String,default:""},name:{type:[String,Number]},closable:{type:Boolean,default:void 0},disabled:Boolean,lazy:Boolean}),m4e=["id","aria-hidden","aria-labelledby"],Ux="ElTabPane";var g4e=q({name:Ux,__name:"tab-pane",props:p4e,setup(e){const t=e,n=Et(),a=Xn(),r=Ze(j1);r||Dn(Ux,"usage: ");const o=ke("tab-pane"),i=F(),l=F(),s=D(()=>t.closable??r.props.closable),u=D(()=>r.currentName.value===(t.name??l.value)),c=F(u.value),d=D(()=>t.name??l.value),v=D(()=>!t.lazy||c.value||u.value),f=()=>{var m;return(m=i.value)==null?void 0:m.contains(document.activeElement)};ye(u,m=>{m&&(c.value=!0)});const h=nn({uid:n.uid,getVnode:()=>n.vnode,slots:a,props:t,paneName:d,active:u,index:l,isClosable:s,isFocusInsidePane:f});return r.registerPane(h),rn(()=>{r.unregisterPane(h)}),q3(()=>{var m;a.label&&((m=r.nav$.value)==null||m.scheduleRender())}),(m,g)=>v.value?st((x(),O("div",{key:0,id:`pane-${d.value}`,ref_key:"paneRef",ref:i,class:U(p(o).b()),role:"tabpanel","aria-hidden":!u.value,"aria-labelledby":`tab-${d.value}`},[ce(m.$slots,"default")],10,m4e)),[[Zt,u.value]]):te("v-if",!0)}}),HD=g4e;const y4e=wt(h4e,{TabPane:HD}),b4e=On(HD),_4e=$e({type:{type:String,values:["primary","success","info","warning","danger",""],default:""},size:{type:String,values:ii,default:""},truncated:Boolean,lineClamp:{type:[String,Number]},tag:{type:String,default:"span"}});var w4e=q({name:"ElText",__name:"text",props:_4e,setup(e){const t=e,n=Nl(),a=F(),r=ca(),o=ke("text"),i=D(()=>[o.b(),o.m(t.type),o.m(r.value),o.is("truncated",t.truncated),o.is("line-clamp",!zt(t.lineClamp))]),l=async()=>{var c,d,v,f,h,m,g;if(await He(),n.title)return;let s=!1;const u=((c=a.value)==null?void 0:c.textContent)||"";if(t.truncated){const y=(d=a.value)==null?void 0:d.offsetWidth,b=(v=a.value)==null?void 0:v.scrollWidth;y&&b&&b>y&&(s=!0)}else if(!zt(t.lineClamp)){const y=(f=a.value)==null?void 0:f.offsetHeight,b=(h=a.value)==null?void 0:h.scrollHeight;y&&b&&b>y&&(s=!0)}s?(m=a.value)==null||m.setAttribute("title",u):(g=a.value)==null||g.removeAttribute("title")};return mt(l),oi(l),(s,u)=>(x(),ne(St(e.tag),{ref_key:"textRef",ref:a,class:U(i.value),style:et({"-webkit-line-clamp":e.lineClamp})},{default:$(()=>[ce(s.$slots,"default")]),_:3},8,["class","style"]))}}),S4e=w4e;const Bw=wt(S4e),WD="09:00",KD="18:00",UD="00:30",C4e=$e({format:{type:String,default:"HH:mm"},modelValue:{type:ae(String)},disabled:{type:Boolean,default:void 0},editable:{type:Boolean,default:!0},effect:{type:ae(String),default:"light"},clearable:{type:Boolean,default:!0},size:pa,placeholder:String,start:{type:String,default:WD},end:{type:String,default:KD},step:{type:String,default:UD},minTime:{type:ae(String)},maxTime:{type:ae(String)},includeEndTime:Boolean,name:String,prefixIcon:{type:ae([String,Object]),default:()=>H6},clearIcon:{type:ae([String,Object]),default:()=>ci},popperClass:{type:ae([String,Array,Object,Boolean]),default:""},popperStyle:{type:ae([String,Array,Object,Boolean]),default:void 0},...Mc}),Zu=e=>{const t=(e||"").split(":");if(t.length>=2){let n=Number.parseInt(t[0],10);const a=Number.parseInt(t[1],10);if(Number.isNaN(n)||Number.isNaN(a))return null;const r=e.toUpperCase();return r.includes("AM")&&n===12?n=0:r.includes("PM")&&n!==12&&(n+=12),{hours:n,minutes:a}}return null},x4e=e=>!!e&&e.hours>=0&&e.minutes>=0&&!Number.isNaN(e.hours)&&!Number.isNaN(e.minutes),hy=(e,t)=>{const n=Zu(e);if(!n)return-1;const a=Zu(t);if(!a)return-1;const r=n.minutes+n.hours*60,o=a.minutes+a.hours*60;return r===o?0:r>o?1:-1},Gx=e=>`${e}`.padStart(2,"0"),lm=e=>`${Gx(e.hours)}:${Gx(e.minutes)}`,k4e=(e,t)=>{const n=Zu(e);if(!n)return"";const a=Zu(t);if(!a)return"";const r={hours:n.hours,minutes:n.minutes};return r.minutes+=a.minutes,r.hours+=a.hours,r.hours+=Math.floor(r.minutes/60),r.minutes=r.minutes%60,lm(r)};var T4e=q({name:"ElTimeSelect",__name:"time-select",props:C4e,emits:[Ot,"blur","focus","clear",bt],setup(e,{expose:t}){gt.extend(rw);const{Option:n}=Bs,a=e,r=ke("input"),o=F(),i=$n(),{lang:l}=Vt(),s=(b,_,w,S=!0)=>{const k=Zu(b);return!x4e(k)||!S&&k.hours===0&&k.minutes===0?(un("ElTimeSelect",`invalid ${_}, fallback to default ${_} (${w}).`),w):lm(k)},u=D(()=>a.modelValue),c=D(()=>s(a.start,"start",WD)),d=D(()=>s(a.end,"end",KD)),v=D(()=>{const b=Zu(a.minTime||"");return b?lm(b):null}),f=D(()=>{const b=Zu(a.maxTime||"");return b?lm(b):null}),h=D(()=>s(a.step,"step",UD,!1)),m=D(()=>{var S;const b=[],_=(k,T)=>{b.push({value:k,rawValue:T,disabled:hy(T,v.value||"-1:-1")<=0||hy(T,f.value||"100:100")>=0})};let w=c.value;for(;hy(w,d.value)<=0;)_(gt(w,"HH:mm").locale(l.value).format(a.format),w),w=k4e(w,h.value);return a.includeEndTime&&((S=b[b.length-1])==null?void 0:S.rawValue)!==d.value&&_(gt(d.value,"HH:mm").locale(l.value).format(a.format),d.value),b});return t({blur:()=>{var b,_;(_=(b=o.value)==null?void 0:b.blur)==null||_.call(b)},focus:()=>{var b,_;(_=(b=o.value)==null?void 0:b.focus)==null||_.call(b)}}),(b,_)=>(x(),ne(p(Bs),{ref_key:"select",ref:o,name:e.name,"model-value":u.value,disabled:p(i),clearable:e.clearable,"clear-icon":e.clearIcon,size:e.size,effect:e.effect,placeholder:e.placeholder,"default-first-option":"",filterable:e.editable,"empty-values":e.emptyValues,"value-on-clear":e.valueOnClear,"popper-class":e.popperClass,"popper-style":e.popperStyle,"onUpdate:modelValue":_[0]||(_[0]=w=>b.$emit(p("update:modelValue"),w)),onChange:_[1]||(_[1]=w=>b.$emit(p("change"),w)),onBlur:_[2]||(_[2]=w=>b.$emit("blur",w)),onFocus:_[3]||(_[3]=w=>b.$emit("focus",w)),onClear:_[4]||(_[4]=()=>b.$emit("clear"))},{prefix:$(()=>[e.prefixIcon?(x(),ne(p(Xe),{key:0,class:U(p(r).e("prefix-icon"))},{default:$(()=>[(x(),ne(St(e.prefixIcon)))]),_:1},8,["class"])):te("v-if",!0)]),default:$(()=>[(x(!0),O(Ve,null,dt(m.value,w=>(x(),ne(p(n),{key:w.value,label:w.value,value:w.value,disabled:w.disabled},null,8,["label","value","disabled"]))),128))]),_:1},8,["name","model-value","disabled","clearable","clear-icon","size","effect","placeholder","filterable","empty-values","value-on-clear","popper-class","popper-style"]))}}),M4e=T4e;const E4e=wt(M4e),GD="timeline",A4e=$e({mode:{type:String,values:["start","alternate","alternate-reverse","end"],default:"start"},reverse:Boolean}),I4e=q({name:"ElTimeline",props:A4e,setup(e,{slots:t}){const n=ke("timeline");Ft(GD,{props:e,slots:t});const a=D(()=>[n.b(),n.is(e.mode)]);return()=>{var o;const r=eo(((o=t.default)==null?void 0:o.call(t))??[]);return ot("ul",{class:a.value},e.reverse?r.reverse():r)}}}),L4e=$e({timestamp:{type:String,default:""},hideTimestamp:Boolean,center:Boolean,placement:{type:String,values:["top","bottom"],default:"bottom"},type:{type:String,values:["primary","success","warning","danger","info"],default:""},color:{type:String,default:""},size:{type:String,values:["normal","large"],default:"normal"},icon:{type:pn},hollow:Boolean});var P4e=q({name:"ElTimelineItem",__name:"timeline-item",props:L4e,setup(e){const t=e,{props:n}=Ze(GD),a=ke("timeline-item"),r=D(()=>[a.e("node"),a.em("node",t.size||""),a.em("node",t.type||""),a.is("hollow",t.hollow)]),o=D(()=>[a.b(),{[a.e("center")]:t.center},a.is(n.mode)]);return(i,l)=>(x(),O("li",{class:U(o.value)},[C("div",{class:U(p(a).e("tail"))},null,2),i.$slots.dot?te("v-if",!0):(x(),O("div",{key:0,class:U(r.value),style:et({backgroundColor:e.color})},[e.icon?(x(),ne(p(Xe),{key:0,class:U(p(a).e("icon"))},{default:$(()=>[(x(),ne(St(e.icon)))]),_:1},8,["class"])):te("v-if",!0)],6)),i.$slots.dot?(x(),O("div",{key:1,class:U(p(a).e("dot"))},[ce(i.$slots,"dot")],2)):te("v-if",!0),C("div",{class:U(p(a).e("wrapper"))},[!e.hideTimestamp&&e.placement==="top"?(x(),O("div",{key:0,class:U([p(a).e("timestamp"),p(a).is("top")])},Q(e.timestamp),3)):te("v-if",!0),C("div",{class:U(p(a).e("content"))},[ce(i.$slots,"default")],2),!e.hideTimestamp&&e.placement==="bottom"?(x(),O("div",{key:1,class:U([p(a).e("timestamp"),p(a).is("bottom")])},Q(e.timestamp),3)):te("v-if",!0)],2)],2))}}),YD=P4e;const D4e=wt(I4e,{TimelineItem:YD}),O4e=On(YD),jD="left-check-change",qD="right-check-change",ls=$e({data:{type:ae(Array),default:()=>[]},titles:{type:ae(Array),default:()=>[]},buttonTexts:{type:ae(Array),default:()=>[]},filterPlaceholder:String,filterMethod:{type:ae(Function)},leftDefaultChecked:{type:ae(Array),default:()=>[]},rightDefaultChecked:{type:ae(Array),default:()=>[]},renderContent:{type:ae(Function)},modelValue:{type:ae(Array),default:()=>[]},format:{type:ae(Object),default:()=>({})},filterable:Boolean,props:{type:ae(Object),default:()=>Nn({label:"label",key:"key",disabled:"disabled"})},targetOrder:{type:String,values:["original","push","unshift"],default:"original"},validateEvent:{type:Boolean,default:!0},virtualScroll:Boolean,itemSize:{type:Number,default:30}}),a_=(e,t)=>[e,t].every(Ae)||Ae(e)&&Un(t),$4e={[Ot]:(e,t,n)=>[e,n].every(Ae)&&["left","right"].includes(t),[bt]:e=>Ae(e),[jD]:a_,[qD]:a_},fp=e=>{const t={label:"label",key:"key",disabled:"disabled"};return D(()=>({...t,...e.props}))},R4e=(e,t)=>({onSourceCheckedChange:(r,o)=>{e.leftChecked=r,o&&t(jD,r,o)},onTargetCheckedChange:(r,o)=>{e.rightChecked=r,o&&t(qD,r,o)}}),N4e=e=>{const t=fp(e),n=D(()=>e.data.reduce((a,r)=>(a[r[t.value.key]]=r,a),{}));return{sourceData:D(()=>e.data.filter(a=>!e.modelValue.includes(a[t.value.key]))),targetData:D(()=>e.targetOrder==="original"?e.data.filter(a=>e.modelValue.includes(a[t.value.key])):e.modelValue.reduce((a,r)=>{const o=n.value[r];return o&&a.push(o),a},[]))}},B4e=(e,t,n)=>{const a=fp(e),r=(l,s,u)=>{n(bt,l),n(Ot,l,s,u)};return{addToLeft:()=>{const l=e.modelValue.slice();t.rightChecked.forEach(s=>{const u=l.indexOf(s);u>-1&&l.splice(u,1)}),r(l,"left",t.rightChecked)},addToRight:()=>{let l=e.modelValue.slice();const s=e.data.filter(u=>{const c=u[a.value.key];return t.leftChecked.includes(c)&&!e.modelValue.includes(c)}).map(u=>u[a.value.key]);l=e.targetOrder==="unshift"?s.concat(l):l.concat(s),e.targetOrder==="original"&&(l=e.data.filter(u=>l.includes(u[a.value.key])).map(u=>u[a.value.key])),r(l,"right",t.leftChecked)}}},r_="checked-change",V4e=$e({data:ls.data,optionRender:{type:ae(Function)},placeholder:String,title:String,filterable:Boolean,format:ls.format,filterMethod:ls.filterMethod,defaultChecked:ls.leftDefaultChecked,props:ls.props,virtualScroll:ls.virtualScroll,itemSize:ls.itemSize}),z4e={[r_]:a_},F4e=(e,t,n)=>{const a=fp(e),r=D(()=>e.data.filter(f=>Qe(e.filterMethod)?e.filterMethod(t.query,f):String(f[a.value.label]||f[a.value.key]).toLowerCase().includes(t.query.toLowerCase()))),o=D(()=>r.value.filter(f=>!f[a.value.disabled])),i=D(()=>{const f=t.checked.length,h=e.data.length,{noChecked:m,hasChecked:g}=e.format;return m&&g?f>0?g.replace(/\${checked}/g,f.toString()).replace(/\${total}/g,h.toString()):m.replace(/\${total}/g,h.toString()):`${f}/${h}`}),l=D(()=>{const f=t.checked.length;return f>0&&f{const f=o.value.map(h=>h[a.value.key]);t.allChecked=f.length>0&&f.every(h=>t.checked.includes(h))},u=f=>{t.checked=f?o.value.map(h=>h[a.value.key]):[]},c=F(),{height:d}=Um(D(()=>{var f;return(f=c.value)==null?void 0:f.$el})),v=F();return ye(()=>t.checked,(f,h)=>{s(),t.checkChangeByUser?n(r_,f,f.concat(h).filter(m=>!f.includes(m)||!h.includes(m))):(n(r_,f),t.checkChangeByUser=!0)}),ye(o,()=>{s()}),ye(()=>e.data,()=>{const f=[],h=r.value.map(m=>m[a.value.key]);t.checked.forEach(m=>{h.includes(m)&&f.push(m)}),t.checkChangeByUser=!1,t.checked=f}),ye(()=>e.defaultChecked,(f,h)=>{if(h&&f.length===h.length&&f.every(y=>h.includes(y)))return;const m=[],g=o.value.map(y=>y[a.value.key]);f.forEach(y=>{g.includes(y)&&m.push(y)}),t.checkChangeByUser=!1,t.checked=m},{immediate:!0}),ye(()=>t.query,()=>{var f;e.virtualScroll&&((f=v.value)==null||f.scrollToItem(0))},{flush:"post"}),{filteredData:r,checkableData:o,checkedSummary:i,virtualListRef:v,isIndeterminate:l,checkboxGroupRef:c,virtualListHeight:d,updateAllChecked:s,handleAllCheckedChange:u}};var H4e=q({name:"ElTransferPanel",__name:"transfer-panel",props:V4e,emits:z4e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Xn(),i=({option:T})=>T,{t:l}=Vt(),s=ke("transfer"),u=nn({checked:[],allChecked:!1,query:"",checkChangeByUser:!0}),c=fp(a),{filteredData:d,checkedSummary:v,virtualListRef:f,isIndeterminate:h,checkboxGroupRef:m,virtualListHeight:g,handleAllCheckedChange:y}=F4e(a,u,r),b=D(()=>!_r(u.query)&&_r(d.value)),_=D(()=>!_r(o.default()[0].children)),{checked:w,allChecked:S,query:k}=Ta(u);return t({query:k}),(T,M)=>(x(),O("div",{class:U(p(s).b("panel"))},[C("p",{class:U(p(s).be("panel","header"))},[L(p(Zo),{modelValue:p(S),"onUpdate:modelValue":M[0]||(M[0]=E=>xn(S)?S.value=E:null),indeterminate:p(h),"validate-event":!1,onChange:p(y)},{default:$(()=>[C("span",{class:U(p(s).be("panel","header-title"))},Q(e.title),3),C("span",{class:U(p(s).be("panel","header-count"))},Q(p(v)),3)]),_:1},8,["modelValue","indeterminate","onChange"])],2),C("div",{class:U([p(s).be("panel","body"),p(s).is("with-footer",_.value)])},[e.filterable?(x(),ne(p(Ba),{key:0,modelValue:p(k),"onUpdate:modelValue":M[1]||(M[1]=E=>xn(k)?k.value=E:null),class:U(p(s).be("panel","filter")),size:"default",placeholder:e.placeholder,"prefix-icon":p(HI),clearable:"","validate-event":!1},null,8,["modelValue","class","placeholder","prefix-icon"])):te("v-if",!0),st(L(p(cw),{ref_key:"checkboxGroupRef",ref:m,modelValue:p(w),"onUpdate:modelValue":M[2]||(M[2]=E=>xn(w)?w.value=E:null),"validate-event":!1,class:U([p(s).is("filterable",e.filterable),p(s).be("panel","list")])},{default:$(()=>[e.virtualScroll?(x(),ne(p(cp),{key:1,ref_key:"virtualListRef",ref:f,data:p(d),total:p(d).length,"item-size":e.itemSize,height:p(g)},{default:$(({data:E,index:A,style:I})=>[(x(),O("div",{key:E[A][p(c).key],style:et(I)},[L(p(Zo),{class:U(p(s).be("panel","item")),value:E[A][p(c).key],disabled:E[A][p(c).disabled],"validate-event":!1},{default:$(()=>{var P;return[L(i,{option:(P=e.optionRender)==null?void 0:P.call(e,E[A])},null,8,["option"])]}),_:2},1032,["class","value","disabled"])],4))]),_:1},8,["data","total","item-size","height"])):(x(!0),O(Ve,{key:0},dt(p(d),E=>(x(),ne(p(Zo),{key:E[p(c).key],class:U(p(s).be("panel","item")),value:E[p(c).key],disabled:E[p(c).disabled],"validate-event":!1},{default:$(()=>{var A;return[L(i,{option:(A=e.optionRender)==null?void 0:A.call(e,E)},null,8,["option"])]}),_:2},1032,["class","value","disabled"]))),128))]),_:1},8,["modelValue","class"]),[[Zt,!b.value&&!p(_r)(e.data)]]),st(C("div",{class:U(p(s).be("panel","empty"))},[ce(T.$slots,"empty",{},()=>[ue(Q(b.value?p(l)("el.transfer.noMatch"):p(l)("el.transfer.noData")),1)])],2),[[Zt,b.value||p(_r)(e.data)]])],2),_.value?(x(),O("p",{key:0,class:U(p(s).be("panel","footer"))},[ce(T.$slots,"default")],2)):te("v-if",!0)],2))}}),Yx=H4e;const W4e={key:0},K4e={key:0};var U4e=q({name:"ElTransfer",__name:"transfer",props:ls,emits:$4e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Xn(),{t:i}=Vt(),l=ke("transfer"),{formItem:s}=Ea(),u=nn({leftChecked:[],rightChecked:[]}),c=fp(a),{sourceData:d,targetData:v}=N4e(a),{onSourceCheckedChange:f,onTargetCheckedChange:h}=R4e(u,r),{addToLeft:m,addToRight:g}=B4e(a,u,r),y=F(),b=F(),_=E=>{switch(E){case"left":y.value.query="";break;case"right":b.value.query="";break}},w=D(()=>a.buttonTexts.length===2),S=D(()=>a.titles[0]||i("el.transfer.titles.0")),k=D(()=>a.titles[1]||i("el.transfer.titles.1")),T=D(()=>a.filterPlaceholder||i("el.transfer.filterPlaceholder"));ye(()=>a.modelValue,()=>{var E;a.validateEvent&&((E=s==null?void 0:s.validate)==null||E.call(s,"change").catch(kt))});const M=D(()=>E=>{var I;if(a.renderContent)return a.renderContent(ot,E);const A=(((I=o.default)==null?void 0:I.call(o,{option:E}))||[]).filter(P=>P.type!==oa);return A.length?A:ot("span",E[c.value.label]||E[c.value.key])});return t({clearQuery:_,leftPanel:y,rightPanel:b}),(E,A)=>(x(),O("div",{class:U(p(l).b())},[L(Yx,{ref_key:"leftPanel",ref:y,data:p(d),"option-render":M.value,placeholder:T.value,title:S.value,filterable:e.filterable,format:e.format,"filter-method":e.filterMethod,"default-checked":e.leftDefaultChecked,props:a.props,"virtual-scroll":e.virtualScroll,"item-size":e.itemSize,onCheckedChange:p(f)},{empty:$(()=>[ce(E.$slots,"left-empty")]),default:$(()=>[ce(E.$slots,"left-footer")]),_:3},8,["data","option-render","placeholder","title","filterable","format","filter-method","default-checked","props","virtual-scroll","item-size","onCheckedChange"]),C("div",{class:U(p(l).e("buttons"))},[L(p(xa),{type:"primary",class:U([p(l).e("button"),p(l).is("with-texts",w.value)]),disabled:p(_r)(u.rightChecked),onClick:p(m)},{default:$(()=>[L(p(Xe),null,{default:$(()=>[L(p(Ji))]),_:1}),p(zt)(e.buttonTexts[0])?te("v-if",!0):(x(),O("span",W4e,Q(e.buttonTexts[0]),1))]),_:1},8,["class","disabled","onClick"]),L(p(xa),{type:"primary",class:U([p(l).e("button"),p(l).is("with-texts",w.value)]),disabled:p(_r)(u.leftChecked),onClick:p(g)},{default:$(()=>[p(zt)(e.buttonTexts[1])?te("v-if",!0):(x(),O("span",K4e,Q(e.buttonTexts[1]),1)),L(p(Xe),null,{default:$(()=>[L(p(za))]),_:1})]),_:1},8,["class","disabled","onClick"])],2),L(Yx,{ref_key:"rightPanel",ref:b,data:p(v),"option-render":M.value,placeholder:T.value,filterable:e.filterable,format:e.format,"filter-method":e.filterMethod,title:k.value,"default-checked":e.rightDefaultChecked,props:a.props,"virtual-scroll":e.virtualScroll,"item-size":e.itemSize,onCheckedChange:p(h)},{empty:$(()=>[ce(E.$slots,"right-empty")]),default:$(()=>[ce(E.$slots,"right-footer")]),_:3},8,["data","option-render","placeholder","filterable","format","filter-method","title","default-checked","props","virtual-scroll","item-size","onCheckedChange"])],2))}}),G4e=U4e;const Y4e=wt(G4e),Vw="RootTree",XD="NodeInstance",jx="TreeNodeMap",ZD=$e({data:{type:ae(Array),default:()=>[]},emptyText:{type:String},renderAfterExpand:{type:Boolean,default:!0},nodeKey:String,checkStrictly:Boolean,defaultExpandAll:Boolean,expandOnClickNode:{type:Boolean,default:!0},checkOnClickNode:Boolean,checkOnClickLeaf:{type:Boolean,default:!0},checkDescendants:Boolean,autoExpandParent:{type:Boolean,default:!0},defaultCheckedKeys:{type:Array},defaultExpandedKeys:{type:Array},currentNodeKey:{type:[String,Number]},renderContent:{type:ae(Function)},showCheckbox:Boolean,draggable:Boolean,allowDrag:{type:ae(Function)},allowDrop:{type:ae(Function)},props:{type:Object,default:()=>({children:"children",label:"label",disabled:"disabled"})},lazy:Boolean,highlightCurrent:Boolean,load:{type:Function},filterNodeMethod:{type:Function},accordion:Boolean,indent:{type:Number,default:18},icon:{type:pn}}),j4e={"check-change":(e,t,n)=>e&&vn(t)&&vn(n),"current-change":(e,t)=>!0,"node-click":(e,t,n,a)=>e&&t&&a instanceof Event,"node-contextmenu":(e,t,n,a)=>e instanceof Event&&t&&n,"node-collapse":(e,t,n)=>e&&t,"node-expand":(e,t,n)=>e&&t,check:(e,t)=>e&&t,"node-drag-start":(e,t)=>e&&t,"node-drag-end":(e,t,n,a)=>e&&a,"node-drop":(e,t,n,a)=>e&&t&&a,"node-drag-leave":(e,t,n)=>e&&t&&n,"node-drag-enter":(e,t,n)=>e&&t&&n,"node-drag-over":(e,t,n)=>e&&t&&n},JD="$treeNodeId",qx=function(e,t){!t||t.$treeNodeId||Object.defineProperty(t,JD,{value:e.id,enumerable:!1,configurable:!1,writable:!1})},zw=(e,t)=>t==null?void 0:t[e||"$treeNodeId"],o_=(e,t,n)=>{const a=e.value.currentNode;n();const r=e.value.currentNode;a!==r&&t("current-change",r?r.data:null,r)},QD=e=>{let t=!0,n=!0,a=!0,r=!0;for(let o=0,i=e.length;o{n.canFocus=t,i_(n.childNodes,t)})};let q4e=0;var l_=class sm{constructor(t){this.isLeafByUser=void 0,this.isLeaf=void 0,this.isEffectivelyChecked=!1,this.id=q4e++,this.text=null,this.checked=!1,this.indeterminate=!1,this.data=null,this.expanded=!1,this.parent=null,this.visible=!0,this.isCurrent=!1,this.canFocus=!1;for(const n in t)en(t,n)&&(this[n]=t[n]);this.level=0,this.loaded=!1,this.childNodes=[],this.loading=!1,this.parent&&(this.level=this.parent.level+1)}initialize(){var o;const t=this.store;if(!t)throw new Error("[Node]store is required!");t.registerNode(this);const n=t.props;if(n&&typeof n.isLeaf<"u"){const i=jp(this,"isLeaf");vn(i)&&(this.isLeafByUser=i)}if(t.lazy!==!0&&this.data?(this.setData(this.data),t.defaultExpandAll&&(this.expanded=!0,this.canFocus=!0)):this.level>0&&t.lazy&&t.defaultExpandAll&&!this.isLeafByUser&&this.expand(),Ae(this.data)||qx(this,this.data),!this.data)return;const a=t.defaultExpandedKeys,r=t.key;r&&!Un(this.key)&&a&&a.includes(this.key)&&this.expand(null,t.autoExpandParent),r&&t.currentNodeKey!==void 0&&this.key===t.currentNodeKey&&(t.currentNode&&(t.currentNode.isCurrent=!1),t.currentNode=this,t.currentNode.isCurrent=!0),t.lazy&&t._initDefaultCheckedNode(this),this.updateLeafState(),(this.level===1||((o=this.parent)==null?void 0:o.expanded)===!0)&&(this.canFocus=!0)}setData(t){Ae(t)||qx(this,t),this.data=t,this.childNodes=[];let n;this.level===0&&Ae(this.data)?n=this.data:n=jp(this,"children")||[];for(let a=0,r=n.length;a-1)return t.childNodes[n+1]}return null}get previousSibling(){const t=this.parent;if(t){const n=t.childNodes.indexOf(this);if(n>-1)return n>0?t.childNodes[n-1]:null}return null}contains(t,n=!0){return(this.childNodes||[]).some(a=>a===t||n&&a.contains(t))}remove(){const t=this.parent;t&&t.removeChild(this)}insertChild(t,n,a){if(!t)throw new Error("InsertChild error: child is required.");if(!(t instanceof sm)){if(!a){const r=this.getChildren(!0);r!=null&&r.includes(t.data)||(zt(n)||n<0?r==null||r.push(t.data):r==null||r.splice(n,0,t.data))}Object.assign(t,{parent:this,store:this.store}),t=nn(new sm(t)),t instanceof sm&&t.initialize()}t.level=this.level+1,zt(n)||n<0?this.childNodes.push(t):this.childNodes.splice(n,0,t),this.updateLeafState()}insertBefore(t,n){let a;n&&(a=this.childNodes.indexOf(n)),this.insertChild(t,a)}insertAfter(t,n){let a;n&&(a=this.childNodes.indexOf(n),a!==-1&&(a+=1)),this.insertChild(t,a)}removeChild(t){const n=this.getChildren()||[],a=n.indexOf(t.data);a>-1&&n.splice(a,1);const r=this.childNodes.indexOf(t);r>-1&&(this.store&&this.store.deregisterNode(t),t.parent=null,this.childNodes.splice(r,1)),this.updateLeafState()}removeChildByData(t){const n=this.childNodes.find(a=>a.data===t);n&&this.removeChild(n)}expand(t,n){const a=()=>{if(n){let r=this.parent;for(;r&&r.level>0;)r.expanded=!0,r=r.parent}this.expanded=!0,t&&t(),i_(this.childNodes,!0)};this.shouldLoadData()?this.loadData(r=>{Ae(r)&&(this.store.checkStrictly||(this.checked?this.setChecked(!0,!0):sv(this)),a())}):a()}doCreateChildren(t,n={}){t.forEach(a=>{this.insertChild(Object.assign({data:a},n),void 0,!0)})}collapse(){this.expanded=!1,i_(this.childNodes,!1)}shouldLoadData(){return!!(this.store.lazy===!0&&this.store.load&&!this.loaded)}updateLeafState(){if(this.store.lazy===!0&&this.loaded!==!0&&typeof this.isLeafByUser<"u"){this.isLeaf=this.isLeafByUser,this.isEffectivelyChecked=this.isLeaf&&this.disabled;return}const t=this.childNodes;if(!this.store.lazy||this.store.lazy===!0&&this.loaded===!0){this.isLeaf=!t||t.length===0,this.isEffectivelyChecked=this.isLeaf&&this.disabled;return}this.isLeaf=!1}setChecked(t,n,a,r){if(this.indeterminate=t==="half",this.checked=t===!0,this.isEffectivelyChecked=!this.childNodes.length&&(this.disabled||this.checked),this.store.checkStrictly&&!n)return;if(!(this.shouldLoadData()&&!this.store.checkDescendants)){const i=()=>{if(n){const l=this.childNodes;for(let d=0,v=l.length;d{i(),sv(this)},{checked:t!==!1});return}else i()}const o=this.parent;!o||o.level===0||!a&&!this.store.checkStrictly&&sv(o)}getChildren(t=!1){if(this.level===0)return this.data;const n=this.data;if(!n)return null;const a=this.store.props;let r="children";return a&&(r=a.children||"children"),zt(n[r])&&(n[r]=null),t&&!n[r]&&(n[r]=[]),n[r]}updateChildren(){const t=this.getChildren()||[],n=this.childNodes.map(o=>o.data),a={},r=[];t.forEach((o,i)=>{const l=o[JD];l&&n.some(s=>(s==null?void 0:s.$treeNodeId)===l)?a[l]={index:i,data:o}:r.push({index:i,data:o})}),this.store.lazy||n.forEach(o=>{a[o==null?void 0:o.$treeNodeId]||this.removeChildByData(o)}),r.forEach(({index:o,data:i})=>{this.insertChild({data:i},o)}),this.updateLeafState()}loadData(t,n={}){if(this.store.lazy===!0&&this.store.load&&!this.loaded&&(!this.loading||Object.keys(n).length)){this.loading=!0;const a=o=>{this.childNodes=[],this.doCreateChildren(o,n),this.loaded=!0,this.loading=!1,this.updateLeafState(),t&&t.call(this,o)},r=()=>{this.loading=!1};this.store.load(this,a,r)}else t&&t.call(this)}eachNode(t){const n=[this];for(;n.length;){const a=n.shift();n.unshift(...a.childNodes),t(a)}}reInitChecked(){this.store.checkStrictly||sv(this)}},X4e=class{constructor(e){this.lazy=!1,this.checkStrictly=!1,this.autoExpandParent=!1,this.defaultExpandAll=!1,this.checkDescendants=!1,this.currentNode=null,this.currentNodeKey=null;for(const t in e)en(e,t)&&(this[t]=e[t]);this.nodesMap={}}initialize(){if(this.root=new l_({data:this.data,store:this}),this.root.initialize(),this.lazy&&this.load){const e=this.load;e(this.root,t=>{this.root.doCreateChildren(t),this._initDefaultCheckedNodes()},kt)}else this._initDefaultCheckedNodes()}filter(e){const t=this.filterNodeMethod,n=this.lazy,a=async function(r){const o=r.root?r.root.childNodes:r.childNodes;for(const[i,l]of o.entries())l.visible=!!(t!=null&&t.call(l,e,l.data,l)),i%80===0&&i>0&&await He(),await a(l);if(!r.visible&&o.length){let i=!0;i=!o.some(l=>l.visible),r.root?r.root.visible=i===!1:r.visible=i===!1}e&&r.visible&&!r.isLeaf&&(!n||r.loaded)&&r.expand()};a(this)}setData(e){e!==this.root.data?(this.nodesMap={},this.root.setData(e),this._initDefaultCheckedNodes(),this.setCurrentNodeKey(this.currentNodeKey)):this.root.updateChildren()}getNode(e){if(e instanceof l_)return e;const t=_t(e)?zw(this.key,e):e;return this.nodesMap[t]||null}insertBefore(e,t){var a;const n=this.getNode(t);(a=n.parent)==null||a.insertBefore({data:e},n)}insertAfter(e,t){var a;const n=this.getNode(t);(a=n.parent)==null||a.insertAfter({data:e},n)}remove(e){const t=this.getNode(e);t&&t.parent&&(t===this.currentNode&&(this.currentNode=null),t.parent.removeChild(t))}append(e,t){const n=Qr(t)?this.root:this.getNode(t);n&&n.insertChild({data:e})}_initDefaultCheckedNodes(){const e=this.defaultCheckedKeys||[],t=this.nodesMap;e.forEach(n=>{const a=t[n];a&&a.setChecked(!0,!this.checkStrictly)})}_initDefaultCheckedNode(e){const t=this.defaultCheckedKeys||[];!Un(e.key)&&t.includes(e.key)&&e.setChecked(!0,!this.checkStrictly)}setDefaultCheckedKey(e){e!==this.defaultCheckedKeys&&(this.defaultCheckedKeys=e,this._initDefaultCheckedNodes())}registerNode(e){const t=this.key;if(!(!e||!e.data))if(!t)this.nodesMap[e.id]=e;else{const n=e.key;Un(n)||(this.nodesMap[n]=e)}}deregisterNode(e){!this.key||!e||!e.data||(e.childNodes.forEach(t=>{this.deregisterNode(t)}),delete this.nodesMap[e.key])}getCheckedNodes(e=!1,t=!1){const n=[],a=function(r){(r.root?r.root.childNodes:r.childNodes).forEach(o=>{(o.checked||t&&o.indeterminate)&&(!e||e&&o.isLeaf)&&n.push(o.data),a(o)})};return a(this),n}getCheckedKeys(e=!1){return this.getCheckedNodes(e).map(t=>(t||{})[this.key])}getHalfCheckedNodes(){const e=[],t=function(n){(n.root?n.root.childNodes:n.childNodes).forEach(a=>{a.indeterminate&&e.push(a.data),t(a)})};return t(this),e}getHalfCheckedKeys(){return this.getHalfCheckedNodes().map(e=>(e||{})[this.key])}_getAllNodes(){const e=[],t=this.nodesMap;for(const n in t)en(t,n)&&e.push(t[n]);return e}updateChildren(e,t){const n=this.nodesMap[e];if(!n)return;const a=n.childNodes;for(let r=a.length-1;r>=0;r--){const o=a[r];this.remove(o.data)}for(let r=0,o=t.length;rl.level-s.level),r=Object.create(null),o=Object.keys(n);a.forEach(l=>l.setChecked(!1,!1));const i=l=>{l.childNodes.forEach(s=>{var u;r[s.data[e]]=!0,(u=s.childNodes)!=null&&u.length&&i(s)})};for(let l=0,s=a.length;l{f.isLeaf||f.setChecked(!1,!1,!0),d(f)}),v.reInitChecked()};d(u)}}}setCheckedNodes(e,t=!1){const n=this.key,a={};e.forEach(r=>{a[(r||{})[n]]=!0}),this._setCheckedKeys(n,t,a)}setCheckedKeys(e,t=!1){this.defaultCheckedKeys=e;const n=this.key,a={};e.forEach(r=>{a[r]=!0}),this._setCheckedKeys(n,t,a)}setDefaultExpandedKeys(e){e=e||[],this.defaultExpandedKeys=e,e.forEach(t=>{const n=this.getNode(t);n&&n.expand(null,this.autoExpandParent)})}setChecked(e,t,n){const a=this.getNode(e);a&&a.setChecked(!!t,n)}getCurrentNode(){return this.currentNode}setCurrentNode(e){const t=this.currentNode;t&&(t.isCurrent=!1),this.currentNode=e,this.currentNode.isCurrent=!0}setUserCurrentNode(e,t=!0){var r;const n=e[this.key],a=this.nodesMap[n];this.setCurrentNode(a),t&&this.currentNode&&this.currentNode.level>1&&((r=this.currentNode.parent)==null||r.expand(null,!0))}setCurrentNodeKey(e,t=!0){var a;if(this.currentNodeKey=e,Qr(e)){this.currentNode&&(this.currentNode.isCurrent=!1),this.currentNode=null;return}const n=this.getNode(e);n&&(this.setCurrentNode(n),t&&this.currentNode&&this.currentNode.level>1&&((a=this.currentNode.parent)==null||a.expand(null,!0)))}};function eO(e){const t=Ze(jx,null);let n={treeNodeExpand:a=>{var r;e.node!==a&&((r=e.node)==null||r.collapse())},children:new Set};return t&&t.children.add(n),rn(()=>{t&&t.children.delete(n),n=null}),Ft(jx,n),{broadcastExpanded:a=>{if(e.accordion)for(const r of n.children)r.treeNodeExpand(a)}}}const tO=Symbol("dragEvents");function Z4e({props:e,ctx:t,el$:n,dropIndicator$:a,store:r}){const o=ke("tree"),i=F({showDropIndicator:!1,draggingNode:null,dropNode:null,allowDrop:!0,dropType:null});return Ft(tO,{treeNodeDragStart:({event:c,treeNode:d})=>{if(c.dataTransfer){if(Qe(e.allowDrag)&&!e.allowDrag(d.node))return c.preventDefault(),!1;c.dataTransfer.effectAllowed="move";try{c.dataTransfer.setData("text/plain","")}catch{}i.value.draggingNode=d,t.emit("node-drag-start",d.node,c)}},treeNodeDragOver:({event:c,treeNode:d})=>{if(!c.dataTransfer)return;const v=d,f=i.value.dropNode;f&&f.node.id!==v.node.id&&ir(f.$el,o.is("drop-inner"));const h=i.value.draggingNode;if(!h||!v)return;let m=!0,g=!0,y=!0,b=!0;Qe(e.allowDrop)&&(m=e.allowDrop(h.node,v.node,"prev"),b=g=e.allowDrop(h.node,v.node,"inner"),y=e.allowDrop(h.node,v.node,"next")),c.dataTransfer.dropEffect=g||m||y?"move":"none",(m||g||y)&&(f==null?void 0:f.node.id)!==v.node.id&&(f&&t.emit("node-drag-leave",h.node,f.node,c),t.emit("node-drag-enter",h.node,v.node,c)),m||g||y?i.value.dropNode=v:i.value.dropNode=null,v.node.nextSibling===h.node&&(y=!1),v.node.previousSibling===h.node&&(m=!1),v.node.contains(h.node,!1)&&(g=!1),(h.node===v.node||h.node.contains(v.node))&&(m=!1,g=!1,y=!1);const _=v.$el,w=_.querySelector(`.${o.be("node","content")}`).getBoundingClientRect(),S=n.value.getBoundingClientRect(),k=n.value.scrollTop;let T;const M=m?g?.25:y?.45:1:Number.NEGATIVE_INFINITY,E=y?g?.75:m?.55:0:Number.POSITIVE_INFINITY;let A=-9999;const I=c.clientY-w.top;Iw.height*E?T="after":g?T="inner":T="none";const P=_.querySelector(`.${o.be("node","expand-icon")}`).getBoundingClientRect(),R=a.value;T==="before"?A=P.top-S.top+k:T==="after"&&(A=P.bottom-S.top+k),R.style.top=`${A}px`,R.style.left=`${P.right-S.left}px`,T==="inner"?Eo(_,o.is("drop-inner")):ir(_,o.is("drop-inner")),i.value.showDropIndicator=T==="before"||T==="after",i.value.allowDrop=i.value.showDropIndicator||b,i.value.dropType=T,t.emit("node-drag-over",h.node,v.node,c)},treeNodeDragEnd:c=>{var h,m;const{draggingNode:d,dropType:v,dropNode:f}=i.value;if(c.preventDefault(),c.dataTransfer&&(c.dataTransfer.dropEffect="move"),d!=null&&d.node.data&&f){const g={data:d.node.data};v!=="none"&&d.node.remove(),v==="before"?(h=f.node.parent)==null||h.insertBefore(g,f.node):v==="after"?(m=f.node.parent)==null||m.insertAfter(g,f.node):v==="inner"&&f.node.insertChild(g),v!=="none"&&(r.value.registerNode(g),r.value.key&&d.node.eachNode(y=>{var b;(b=r.value.nodesMap[y.data[r.value.key]])==null||b.setChecked(y.checked,!r.value.checkStrictly)})),ir(f.$el,o.is("drop-inner")),t.emit("node-drag-end",d.node,f.node,v,c),v!=="none"&&t.emit("node-drop",d.node,f.node,v,c)}d&&!f&&t.emit("node-drag-end",d.node,null,v,c),i.value.showDropIndicator=!1,i.value.draggingNode=null,i.value.dropNode=null,i.value.allowDrop=!0}}),{dragState:i}}var J4e=q({name:"ElTreeNodeContent",props:{node:{type:Object,required:!0},renderContent:Function},setup(e){const t=ke("tree"),n=Ze(XD),a=Ze(Vw);return()=>{const r=e.node,{data:o,store:i}=r;return e.renderContent?e.renderContent(ot,{_self:n,node:r,data:o,store:i}):ce(a.ctx.slots,"default",{node:r,data:o},()=>[ot(Bw,{tag:"span",truncated:!0,class:t.be("node","label")},()=>[r.label])])}}}),Q4e=J4e,e_e=q({name:"ElTreeNode",components:{ElCollapseTransition:U1,ElCheckbox:Zo,NodeContent:Q4e,ElIcon:Xe,Loading:Mr},props:{node:{type:l_,default:()=>({})},props:{type:Object,default:()=>({})},accordion:Boolean,renderContent:Function,renderAfterExpand:Boolean,showCheckbox:Boolean},emits:["node-expand"],setup(e,t){const n=ke("tree"),{broadcastExpanded:a}=eO(e),r=Ze(Vw),o=F(!1),i=F(!1),l=F(),s=F(),u=F(),c=Ze(tO),d=Et();Ft(XD,d),r||un("Tree","Can not find node's tree."),e.node.expanded&&(o.value=!0,i.value=!0);const v=r.props.props.children||"children";ye(()=>{var A;const E=(A=e.node.data)==null?void 0:A[v];return E&&[...E]},()=>{e.node.updateChildren()}),ye(()=>e.node.indeterminate,E=>{m(e.node.checked,E)}),ye(()=>e.node.checked,E=>{m(E,e.node.indeterminate)}),ye(()=>e.node.childNodes.length,()=>e.node.reInitChecked()),ye(()=>e.node.expanded,E=>{He(()=>o.value=E),E&&(i.value=!0)});const f=E=>r.props.nodeKey?zw(r.props.nodeKey,E.data):E.id,h=E=>{const A=e.props.class;if(!A)return{};let I;if(Qe(A)){const{data:P}=E;I=A(P,E)}else I=A;return Ye(I)?{[I]:!0}:I},m=(E,A)=>{(l.value!==E||s.value!==A)&&r.ctx.emit("check-change",e.node.data,E,A),l.value=E,s.value=A},g=E=>{o_(r.store,r.ctx.emit,()=>{var A;if((A=r==null?void 0:r.props)!=null&&A.nodeKey){const I=f(e.node);r.store.value.setCurrentNodeKey(I)}else r.store.value.setCurrentNode(e.node)}),r.currentNode.value=e.node,r.props.expandOnClickNode&&b(),(r.props.checkOnClickNode||e.node.isLeaf&&r.props.checkOnClickLeaf&&e.showCheckbox)&&!e.node.disabled&&_(!e.node.checked),r.ctx.emit("node-click",e.node.data,e.node,d,E)},y=E=>{var A;(A=r.instance.vnode.props)!=null&&A.onNodeContextmenu&&(E.stopPropagation(),E.preventDefault()),r.ctx.emit("node-contextmenu",E,e.node.data,e.node,d)},b=()=>{e.node.isLeaf||(o.value?(r.ctx.emit("node-collapse",e.node.data,e.node,d),e.node.collapse()):e.node.expand(()=>{t.emit("node-expand",e.node.data,e.node,d)}))},_=E=>{const A=r==null?void 0:r.props.checkStrictly,I=e.node.childNodes;!A&&I.length&&(E=I.some(P=>!P.isEffectivelyChecked)),e.node.setChecked(E,!A),He(()=>{const P=r.store.value;r.ctx.emit("check",e.node.data,{checkedNodes:P.getCheckedNodes(),checkedKeys:P.getCheckedKeys(),halfCheckedNodes:P.getHalfCheckedNodes(),halfCheckedKeys:P.getHalfCheckedKeys()})})};return{ns:n,node$:u,tree:r,expanded:o,childNodeRendered:i,oldChecked:l,oldIndeterminate:s,getNodeKey:f,getNodeClass:h,handleSelectChange:m,handleClick:g,handleContextMenu:y,handleExpandIconClick:b,handleCheckChange:_,handleChildNodeExpand:(E,A,I)=>{a(A),r.ctx.emit("node-expand",E,A,I)},handleDragStart:E=>{r.props.draggable&&c.treeNodeDragStart({event:E,treeNode:e})},handleDragOver:E=>{E.preventDefault(),r.props.draggable&&c.treeNodeDragOver({event:E,treeNode:{$el:u.value,node:e.node}})},handleDrop:E=>{E.preventDefault()},handleDragEnd:E=>{r.props.draggable&&c.treeNodeDragEnd(E)},CaretRight:F6}}});const t_e=["aria-expanded","aria-disabled","aria-checked","draggable","data-key"],n_e=["aria-expanded"];function a_e(e,t,n,a,r,o){const i=be("el-icon"),l=be("el-checkbox"),s=be("loading"),u=be("node-content"),c=be("el-tree-node"),d=be("el-collapse-transition");return st((x(),O("div",{ref:"node$",class:U([e.ns.b("node"),e.ns.is("expanded",e.expanded),e.ns.is("current",e.node.isCurrent),e.ns.is("hidden",!e.node.visible),e.ns.is("focusable",!e.node.disabled),e.ns.is("checked",!e.node.disabled&&e.node.checked),e.getNodeClass(e.node)]),role:"treeitem",tabindex:"-1","aria-expanded":e.expanded,"aria-disabled":e.node.disabled,"aria-checked":e.node.checked,draggable:e.tree.props.draggable,"data-key":e.getNodeKey(e.node),onClick:t[2]||(t[2]=at((...v)=>e.handleClick&&e.handleClick(...v),["stop"])),onContextmenu:t[3]||(t[3]=(...v)=>e.handleContextMenu&&e.handleContextMenu(...v)),onDragstart:t[4]||(t[4]=at((...v)=>e.handleDragStart&&e.handleDragStart(...v),["stop"])),onDragover:t[5]||(t[5]=at((...v)=>e.handleDragOver&&e.handleDragOver(...v),["stop"])),onDragend:t[6]||(t[6]=at((...v)=>e.handleDragEnd&&e.handleDragEnd(...v),["stop"])),onDrop:t[7]||(t[7]=at((...v)=>e.handleDrop&&e.handleDrop(...v),["stop"]))},[C("div",{class:U(e.ns.be("node","content")),style:et({paddingLeft:(e.node.level-1)*e.tree.props.indent+"px"})},[e.tree.props.icon||e.CaretRight?(x(),ne(i,{key:0,class:U([e.ns.be("node","expand-icon"),e.ns.is("leaf",e.node.isLeaf),{expanded:!e.node.isLeaf&&e.expanded}]),onClick:at(e.handleExpandIconClick,["stop"])},{default:$(()=>[(x(),ne(St(e.tree.props.icon||e.CaretRight)))]),_:1},8,["class","onClick"])):te("v-if",!0),e.showCheckbox?(x(),ne(l,{key:1,"model-value":e.node.checked,indeterminate:e.node.indeterminate,disabled:!!e.node.disabled,onClick:t[0]||(t[0]=at(()=>{},["stop"])),onChange:e.handleCheckChange},null,8,["model-value","indeterminate","disabled","onChange"])):te("v-if",!0),e.node.loading?(x(),ne(i,{key:2,class:U([e.ns.be("node","loading-icon"),e.ns.is("loading")])},{default:$(()=>[L(s)]),_:1},8,["class"])):te("v-if",!0),L(u,{node:e.node,"render-content":e.renderContent},null,8,["node","render-content"])],6),L(d,null,{default:$(()=>[!e.renderAfterExpand||e.childNodeRendered?st((x(),O("div",{key:0,class:U(e.ns.be("node","children")),role:"group","aria-expanded":e.expanded,onClick:t[1]||(t[1]=at(()=>{},["stop"]))},[(x(!0),O(Ve,null,dt(e.node.childNodes,v=>(x(),ne(c,{key:e.getNodeKey(v),"render-content":e.renderContent,"render-after-expand":e.renderAfterExpand,"show-checkbox":e.showCheckbox,node:v,accordion:e.accordion,props:e.props,onNodeExpand:e.handleChildNodeExpand},null,8,["render-content","render-after-expand","show-checkbox","node","accordion","props","onNodeExpand"]))),128))],10,n_e)),[[Zt,e.expanded]]):te("v-if",!0)]),_:1})],42,t_e)),[[Zt,e.node.visible]])}var r_e=ya(e_e,[["render",a_e]]);function o_e({el$:e},t){const n=ke("tree");mt(()=>{o()}),oi(()=>{var i;(i=e.value)==null||i.querySelectorAll("input[type=checkbox]").forEach(l=>{l.setAttribute("tabindex","-1")})});function a(i,l){var u,c;const s=t.value.getNode(i[l].dataset.key);return s.canFocus&&s.visible&&(((u=s.parent)==null?void 0:u.expanded)||((c=s.parent)==null?void 0:c.level)===0)}on(e,"keydown",i=>{const l=i.target;if(!l.className.includes(n.b("node")))return;const s=mn(i),u=Array.from(e.value.querySelectorAll(`.${n.is("focusable")}[role=treeitem]`)),c=u.indexOf(l);let d;if([Pe.up,Pe.down].includes(s)){if(i.preventDefault(),s===Pe.up){d=c===-1?0:c!==0?c-1:u.length-1;const f=d;for(;!a(u,d);){if(d--,d===f){d=-1;break}d<0&&(d=u.length-1)}}else{d=c===-1?0:c=u.length&&(d=0)}}d!==-1&&u[d].focus()}[Pe.left,Pe.right].includes(s)&&(i.preventDefault(),l.click());const v=l.querySelector('[type="checkbox"]');[Pe.enter,Pe.numpadEnter,Pe.space].includes(s)&&v&&(i.preventDefault(),v.click())});const o=()=>{var s;if(!e.value)return;const i=Array.from(e.value.querySelectorAll(`.${n.is("focusable")}[role=treeitem]`));Array.from(e.value.querySelectorAll("input[type=checkbox]")).forEach(u=>{u.setAttribute("tabindex","-1")});const l=e.value.querySelectorAll(`.${n.is("checked")}[role=treeitem]`);if(l.length){l[0].setAttribute("tabindex","0");return}(s=i[0])==null||s.setAttribute("tabindex","0")}}var i_e=q({name:"ElTree",components:{ElTreeNode:r_e},props:ZD,emits:j4e,setup(e,t){const{t:n}=Vt(),a=ke("tree"),r=F(new X4e({key:e.nodeKey,data:e.data,lazy:e.lazy,props:e.props,load:e.load,currentNodeKey:e.currentNodeKey,checkStrictly:e.checkStrictly,checkDescendants:e.checkDescendants,defaultCheckedKeys:e.defaultCheckedKeys,defaultExpandedKeys:e.defaultExpandedKeys,autoExpandParent:e.autoExpandParent,defaultExpandAll:e.defaultExpandAll,filterNodeMethod:e.filterNodeMethod}));r.value.initialize();const o=F(r.value.root),i=F(null),l=F(null),s=F(null),{broadcastExpanded:u}=eO(e),{dragState:c}=Z4e({props:e,ctx:t,el$:l,dropIndicator$:s,store:r});o_e({el$:l},r);const d=Et(),v=D(()=>{let N=d==null?void 0:d.parent;for(;N;){if(N.type.name==="ElTreeSelect")return!0;N=N.parent}return!1}),f=D(()=>{const{childNodes:N}=o.value;return(!N||N.length===0||N.every(({visible:V})=>!V))&&!v.value});ye(()=>e.currentNodeKey,N=>{r.value.setCurrentNodeKey(N??null)}),ye(()=>e.defaultCheckedKeys,(N,V)=>{Rn(N,V)||r.value.setDefaultCheckedKey(N??[])}),ye(()=>e.defaultExpandedKeys,N=>{r.value.setDefaultExpandedKeys(N??[])}),ye(()=>e.data,N=>{r.value.setData(N)},{deep:!0}),ye(()=>e.checkStrictly,N=>{r.value.checkStrictly=N});const h=N=>{if(!e.filterNodeMethod)throw new Error("[Tree] filterNodeMethod is required when filter");r.value.filter(N)},m=N=>e.nodeKey?zw(e.nodeKey,N.data):N.id,g=N=>{if(!e.nodeKey)throw new Error(`[Tree] nodeKey is required in ${N}`)},y=N=>{g("getNodePath");const V=r.value.getNode(N);if(!V)return[];const Y=[V.data];let X=V.parent;for(;X&&X!==o.value;)Y.push(X.data),X=X.parent;return Y.reverse()},b=(N,V)=>r.value.getCheckedNodes(N,V),_=N=>r.value.getCheckedKeys(N),w=()=>{const N=r.value.getCurrentNode();return N?N.data:null},S=()=>{g("getCurrentKey");const N=w();return N?N[e.nodeKey]:null},k=(N,V)=>{g("setCheckedNodes"),r.value.setCheckedNodes(N,V)},T=(N,V)=>{g("setCheckedKeys"),r.value.setCheckedKeys(N,V)},M=(N,V,Y=!1)=>{r.value.setChecked(N,V,Y)},E=()=>r.value.getHalfCheckedNodes(),A=()=>r.value.getHalfCheckedKeys(),I=(N,V=!0)=>{g("setCurrentNode"),o_(r,t.emit,()=>{u(N),r.value.setUserCurrentNode(N,V)})},P=(N=null,V=!0)=>{g("setCurrentKey"),o_(r,t.emit,()=>{u(),r.value.setCurrentNodeKey(N,V)})},R=N=>r.value.getNode(N),G=N=>{r.value.remove(N)},K=(N,V)=>{r.value.append(N,V)},H=(N,V)=>{r.value.insertBefore(N,V)},z=(N,V)=>{r.value.insertAfter(N,V)},B=(N,V,Y)=>{u(V),t.emit("node-expand",N,V,Y)},W=(N,V)=>{g("updateKeyChildren"),r.value.updateChildren(N,V)};return Ft(Vw,{ctx:t,props:e,store:r,root:o,currentNode:i,instance:d}),Ft(Qi,void 0),{ns:a,store:r,root:o,currentNode:i,dragState:c,el$:l,dropIndicator$:s,isEmpty:f,filter:h,getNodeKey:m,getNodePath:y,getCheckedNodes:b,getCheckedKeys:_,getCurrentNode:w,getCurrentKey:S,setCheckedNodes:k,setCheckedKeys:T,setChecked:M,getHalfCheckedNodes:E,getHalfCheckedKeys:A,setCurrentNode:I,setCurrentKey:P,t:n,getNode:R,remove:G,append:K,insertBefore:H,insertAfter:z,handleNodeExpand:B,updateKeyChildren:W}}});function l_e(e,t,n,a,r,o){const i=be("el-tree-node");return x(),O("div",{ref:"el$",class:U([e.ns.b(),e.ns.is("dragging",!!e.dragState.draggingNode),e.ns.is("drop-not-allow",!e.dragState.allowDrop),e.ns.is("drop-inner",e.dragState.dropType==="inner"),{[e.ns.m("highlight-current")]:e.highlightCurrent}]),role:"tree"},[(x(!0),O(Ve,null,dt(e.root.childNodes,l=>(x(),ne(i,{key:e.getNodeKey(l),node:l,props:e.props,accordion:e.accordion,"render-after-expand":e.renderAfterExpand,"show-checkbox":e.showCheckbox,"render-content":e.renderContent,onNodeExpand:e.handleNodeExpand},null,8,["node","props","accordion","render-after-expand","show-checkbox","render-content","onNodeExpand"]))),128)),e.isEmpty?(x(),O("div",{key:0,class:U(e.ns.e("empty-block"))},[ce(e.$slots,"empty",{},()=>[C("span",{class:U(e.ns.e("empty-text"))},Q(e.emptyText??e.t("el.tree.emptyText")),3)])],2)):te("v-if",!0),st(C("div",{ref:"dropIndicator$",class:U(e.ns.e("drop-indicator"))},null,2),[[Zt,e.dragState.showDropIndicator]])],2)}var s_e=ya(i_e,[["render",l_e]]);const Fw=wt(s_e),u_e=(e,{attrs:t,emit:n},{select:a,tree:r,key:o})=>{const i=ke("tree-select");ye(()=>e.data,()=>{e.filterable&&He(()=>{var s,u;(u=r.value)==null||u.filter((s=a.value)==null?void 0:s.states.inputValue)})},{flush:"post"});const l=s=>{var c,d;const u=s.at(-1);if(u.expanded&&u.childNodes.at(-1))l([u.childNodes.at(-1)]);else{(d=(c=r.value.el$)==null?void 0:c.querySelector(`[data-key="${s.at(-1).key}"]`))==null||d.focus({preventScroll:!0});return}};return mt(()=>{on(()=>{var s;return(s=a.value)==null?void 0:s.$el},"keydown",async s=>{const u=mn(s),{dropdownMenuVisible:c}=a.value;[Pe.down,Pe.up].includes(u)&&c&&(await He(),setTimeout(()=>{var d,v,f;if(Pe.up===u){const h=r.value.store.root.childNodes;l(h);return}(f=(v=(d=a.value.optionsArray[a.value.states.hoveringIndex].$el)==null?void 0:d.parentNode)==null?void 0:v.parentNode)==null||f.focus({preventScroll:!0})}))},{capture:!0})}),{...ji(Ta(e),Object.keys(Bs.props)),...t,class:D(()=>t.class),style:D(()=>t.style),"onUpdate:modelValue":s=>n(bt,s),valueKey:o,popperClass:D(()=>[i.e("popper"),e.popperClass]),filterMethod:(s="")=>{var u;e.filterMethod?e.filterMethod(s):e.remoteMethod?e.remoteMethod(s):(u=r.value)==null||u.filter(s)}}},c_e=q({extends:eg,setup(e,t){const n=eg.setup(e,t);delete n.selectOptionClick;const a=Et().proxy;return He(()=>{n.select.states.cachedOptions.get(a.value)||n.select.onOptionCreate(a)}),ye(()=>t.attrs.visible,r=>{He(()=>{n.states.visible=r})},{immediate:!0}),n},methods:{selectOptionClick(){this.$el.parentElement.click()}}});function s_(e){return e||e===0}function Hw(e){return Ae(e)&&e.length}function Bc(e){return Ae(e)?e:s_(e)?[e]:[]}function um(e,t,n,a,r){for(let o=0;o{ye([()=>e.modelValue,o],()=>{e.showCheckbox&&He(()=>{const h=o.value;h&&!Rn(h.getCheckedKeys(),Bc(e.modelValue))&&h.setCheckedKeys(Bc(e.modelValue))})},{immediate:!0,deep:!0});const l=D(()=>({value:i.value,label:"label",children:"children",disabled:"disabled",isLeaf:"isLeaf",...e.props})),s=(h,m)=>{var y;const g=l.value[h];return Qe(g)?g(m,(y=o.value)==null?void 0:y.getNode(s("value",m))):m[g]},u=Bc(e.modelValue).map(h=>um(e.data||[],m=>s("value",m)===h,m=>s("children",m),(m,g,y,b)=>b&&s("value",b))).filter(h=>s_(h)),c=D(()=>{if(!e.renderAfterExpand&&!e.lazy)return[];const h=[];return cm(e.data.concat(e.cacheData),m=>{const g=s("value",m);h.push({value:g,currentLabel:s("label",m),isDisabled:s("disabled",m)})},m=>s("children",m)),h}),d=()=>{var h;return(h=o.value)==null?void 0:h.getCheckedKeys().filter(m=>{var y;const g=(y=o.value)==null?void 0:y.getNode(m);return!Un(g)&&_r(g.childNodes)})},v=h=>{Rn(e.modelValue,h)||a(Ot,h)};function f(h){a(bt,h),v(h)}return{...ji(Ta(e),Object.keys(Fw.props)),...t,nodeKey:i,expandOnClickNode:D(()=>!e.checkStrictly&&e.expandOnClickNode),defaultExpandedKeys:D(()=>e.defaultExpandedKeys?e.defaultExpandedKeys.concat(u):u),renderContent:(h,{node:m,data:g,store:y})=>h(c_e,{value:s("value",g),label:s("label",g),disabled:s("disabled",g),visible:m.visible},e.renderContent?()=>e.renderContent(h,{node:m,data:g,store:y}):n.default?()=>n.default({node:m,data:g,store:y}):void 0),filterNodeMethod:(h,m,g)=>e.filterNodeMethod?e.filterNodeMethod(h,m,g):h?new RegExp(S6(h),"i").test(s("label",m)||""):!0,onNodeClick:(h,m,g)=>{var y,b,_;if((y=t.onNodeClick)==null||y.call(t,h,m,g),!(e.showCheckbox&&e.checkOnClickNode))if(!e.showCheckbox&&(e.checkStrictly||m.isLeaf)){if(!s("disabled",h)){const w=(b=r.value)==null?void 0:b.states.options.get(s("value",h));(_=r.value)==null||_.handleOptionSelect(w)}}else e.expandOnClickNode&&g.proxy.handleExpandIconClick()},onCheck:(h,m)=>{var S;if(!e.showCheckbox)return;const g=s("value",h),y={};cm([o.value.store.root],k=>y[k.key]=k,k=>k.childNodes);const b=m.checkedKeys,_=e.multiple?Bc(e.modelValue).filter(k=>!(k in y)&&!b.includes(k)):[],w=_.concat(b);if(e.checkStrictly)f(e.multiple?w:w.includes(g)?g:void 0);else if(e.multiple){const k=d();f(_.concat(k))}else{const k=um([h],E=>!Hw(s("children",E))&&!s("disabled",E),E=>s("children",E)),T=k?s("value",k):void 0,M=s_(e.modelValue)&&!!um([h],E=>s("value",E)===e.modelValue,E=>s("children",E));f(T===e.modelValue||M?void 0:T)}He(()=>{var T;const k=Bc(e.modelValue);o.value.setCheckedKeys(k),(T=t.onCheck)==null||T.call(t,h,{checkedKeys:o.value.getCheckedKeys(),checkedNodes:o.value.getCheckedNodes(),halfCheckedKeys:o.value.getHalfCheckedKeys(),halfCheckedNodes:o.value.getHalfCheckedNodes()})}),(S=r.value)==null||S.focus()},onNodeExpand:(h,m,g)=>{var y;(y=t.onNodeExpand)==null||y.call(t,h,m,g),He(()=>{if(!e.checkStrictly&&e.lazy&&e.multiple&&m.checked){const b={},_=o.value.getCheckedKeys();cm([o.value.store.root],k=>b[k.key]=k,k=>k.childNodes);const w=Bc(e.modelValue).filter(k=>!(k in b)&&!_.includes(k)),S=d();f(w.concat(S))}})},cacheOptions:c}};var f_e=q({props:{data:{type:Array,default:()=>[]}},setup(e){const t=Ze(sp);return ye(()=>e.data,()=>{var a;e.data.forEach(r=>{t.states.cachedOptions.has(r.value)||t.states.cachedOptions.set(r.value,r)});const n=((a=t.selectRef)==null?void 0:a.querySelectorAll("input"))||[];qt&&!Array.from(n).includes(document.activeElement)&&t.setSelected()},{flush:"post",immediate:!0}),()=>{}}}),v_e=q({name:"ElTreeSelect",inheritAttrs:!1,props:{...FL,...ZD,cacheData:{type:Array,default:()=>[]}},setup(e,t){const{slots:n,expose:a,emit:r,attrs:o}=t,i={...o,onChange:void 0},l=F(),s=F(),u=D(()=>e.nodeKey||e.valueKey||"value"),c=u_e(e,{attrs:o,emit:r},{select:l,tree:s,key:u}),{cacheOptions:d,...v}=d_e(e,{attrs:i,slots:n,emit:r},{select:l,tree:s,key:u}),f=nn({});return a(f),mt(()=>{Object.assign(f,{...ji(s.value,["filter","updateKeyChildren","getCheckedNodes","setCheckedNodes","getCheckedKeys","setCheckedKeys","setChecked","getHalfCheckedNodes","getHalfCheckedKeys","getCurrentKey","getCurrentNode","setCurrentKey","setCurrentNode","getNode","remove","append","insertBefore","insertAfter"]),...ji(l.value,["focus","blur","selectedLabel"]),treeRef:s.value,selectRef:l.value})}),()=>ot(Bs,nn({...c,ref:h=>l.value=h}),{...n,default:()=>[ot(f_e,{data:d.value}),ot(Fw,nn({...v,ref:h=>s.value=h}))]})}}),h_e=v_e;const p_e=wt(h_e),Ww=Symbol(),m_e={key:-1,level:-1,data:{}},nO={type:Number,default:26},g_e=$e({data:{type:ae(Array),default:()=>Nn([])},emptyText:{type:String},height:{type:Number,default:200},props:{type:ae(Object),default:()=>Nn({children:"children",label:"label",disabled:"disabled",value:"id",class:""})},highlightCurrent:Boolean,showCheckbox:Boolean,defaultCheckedKeys:{type:ae(Array),default:()=>Nn([])},checkStrictly:Boolean,defaultExpandedKeys:{type:ae(Array),default:()=>Nn([])},indent:{type:Number,default:16},itemSize:nO,icon:{type:pn},expandOnClickNode:{type:Boolean,default:!0},checkOnClickNode:Boolean,checkOnClickLeaf:{type:Boolean,default:!0},currentNodeKey:{type:ae([String,Number])},accordion:Boolean,filterMethod:{type:ae(Function)},perfMode:{type:Boolean,default:!0},scrollbarAlwaysOn:Boolean}),y_e=$e({node:{type:ae(Object),default:()=>Nn(m_e)},expanded:Boolean,checked:Boolean,indeterminate:Boolean,showCheckbox:Boolean,disabled:Boolean,current:Boolean,hiddenExpandIcon:Boolean,itemSize:nO}),b_e=$e({node:{type:ae(Object),required:!0}}),aO="node-click",rO="node-drop",oO="node-expand",iO="node-collapse",lO="current-change",sO="check",uO="check-change",cO="node-contextmenu",__e={[aO]:(e,t,n)=>e&&t&&n,[rO]:(e,t,n)=>e&&t&&n,[oO]:(e,t)=>e&&t,[iO]:(e,t)=>e&&t,[lO]:(e,t)=>e&&t,[sO]:(e,t)=>e&&t,[uO]:(e,t)=>e&&vn(t),[cO]:(e,t,n)=>e&&t&&n},w_e={click:(e,t)=>!!(e&&t),drop:(e,t)=>!!(e&&t),toggle:e=>!!e,check:(e,t)=>e&&vn(t)};function S_e(e,t){const n=F(new Set),a=F(new Set),{emit:r}=Et();ye([()=>t.value,()=>e.defaultCheckedKeys],()=>He(()=>{b(e.defaultCheckedKeys)}),{immediate:!0});const o=(_=!1)=>{if(!t.value||!e.showCheckbox||e.checkStrictly&&!_)return;const{levelTreeNodeMap:w,maxLevel:S}=t.value,k=n.value,T=new Set;for(let M=S;M>=1;--M){const E=w.get(M);E&&E.forEach(A=>{const I=A.children;let P=!A.isLeaf||A.disabled||k.has(A.key);if(I){let R=!0,G=!1;for(const K of I){const H=K.key;if(K.isEffectivelyChecked||(P=!1),k.has(H))G=!0;else if(T.has(H)){R=!1,G=!0;break}else R=!1}R?k.add(A.key):G?(T.add(A.key),k.delete(A.key)):(k.delete(A.key),T.delete(A.key))}A.isEffectivelyChecked=P})}a.value=T},i=_=>n.value.has(_.key),l=_=>a.value.has(_.key),s=(_,w,S=!0,k=!0,T=!1)=>{const M=n.value,E=_.children;(!e.checkStrictly||T)&&S&&(E!=null&&E.length)&&(w=E.some(I=>!I.isEffectivelyChecked));const A=(I,P)=>{M[P?"add":"delete"](I.key);const R=I.children;(!e.checkStrictly||T)&&R&&R.forEach(G=>{(!G.disabled||G.children)&&A(G,P)})};A(_,w),k&&o(),S&&u(_,w)},u=(_,w)=>{const{checkedNodes:S,checkedKeys:k}=h(),{halfCheckedNodes:T,halfCheckedKeys:M}=m();r(sO,_.data,{checkedKeys:k,checkedNodes:S,halfCheckedKeys:M,halfCheckedNodes:T}),r(uO,_.data,w)};function c(_=!1){return h(_).checkedKeys}function d(_=!1){return h(_).checkedNodes}function v(){return m().halfCheckedKeys}function f(){return m().halfCheckedNodes}function h(_=!1){const w=[],S=[];if(t!=null&&t.value&&e.showCheckbox){const{treeNodeMap:k}=t.value;n.value.forEach(T=>{const M=k.get(T);M&&(!_||_&&M.isLeaf)&&(S.push(T),w.push(M.data))})}return{checkedKeys:S,checkedNodes:w}}function m(){const _=[],w=[];if(t!=null&&t.value&&e.showCheckbox){const{treeNodeMap:S}=t.value;a.value.forEach(k=>{const T=S.get(k);T&&(w.push(k),_.push(T.data))})}return{halfCheckedNodes:_,halfCheckedKeys:w}}function g(_){n.value.clear(),a.value.clear(),He(()=>{b(_)})}function y(_,w,S){if(t!=null&&t.value&&e.showCheckbox){const k=t.value.treeNodeMap.get(_);k&&s(k,w,!1,void 0,S)}}function b(_){if(t!=null&&t.value){const{treeNodeMap:w}=t.value;if(e.showCheckbox&&w&&(_==null?void 0:_.length)>0){for(const S of _){const k=w.get(S);k&&!i(k)&&s(k,!0,!1,!1)}o()}}}return{updateCheckedKeys:o,toggleCheckbox:s,isChecked:i,isIndeterminate:l,getCheckedKeys:c,getCheckedNodes:d,getHalfCheckedKeys:v,getHalfCheckedNodes:f,setChecked:y,setCheckedKeys:g}}function C_e(e,t){const n=F(new Set([])),a=F(new Set([])),r=D(()=>Qe(e.filterMethod));function o(l){var m;if(!r.value)return;const s=new Set,u=a.value,c=n.value,d=[],v=((m=t.value)==null?void 0:m.treeNodes)||[],f=e.filterMethod;c.clear();function h(g){g.forEach(y=>{d.push(y),f!=null&&f(l,y.data,y)?d.forEach(_=>{s.add(_.key),_.expanded=!0}):(y.expanded=!1,y.isLeaf&&c.add(y.key));const b=y.children;if(b&&h(b),!y.isLeaf){if(!s.has(y.key))c.add(y.key);else if(b){let _=!0;for(const w of b)if(!c.has(w.key)){_=!1;break}_?u.add(y.key):u.delete(y.key)}}d.pop()})}return h(v),s}function i(l){return a.value.has(l.key)}return{hiddenExpandIconKeySet:a,hiddenNodeKeySet:n,doFilter:o,isForceHiddenExpandIcon:i}}function x_e(e,t){const n=F(new Set),a=F(),r=Ht(),o=F(),{isIndeterminate:i,isChecked:l,toggleCheckbox:s,getCheckedKeys:u,getCheckedNodes:c,getHalfCheckedKeys:d,getHalfCheckedNodes:v,setChecked:f,setCheckedKeys:h}=S_e(e,r),{doFilter:m,hiddenNodeKeySet:g,isForceHiddenExpandIcon:y}=C_e(e,r),b=D(()=>{var Z;return((Z=e.props)==null?void 0:Z.value)||"id"}),_=D(()=>{var Z;return((Z=e.props)==null?void 0:Z.children)||"children"}),w=D(()=>{var Z;return((Z=e.props)==null?void 0:Z.disabled)||"disabled"}),S=D(()=>{var Z;return((Z=e.props)==null?void 0:Z.label)||"label"}),k=D(()=>{var ge;const Z=n.value,j=g.value,oe=[],se=((ge=r.value)==null?void 0:ge.treeNodes)||[],fe=[];for(let Se=se.length-1;Se>=0;--Se)fe.push(se[Se]);for(;fe.length;){const Se=fe.pop();if(!j.has(Se.key)&&(oe.push(Se),Se.children&&Z.has(Se.key)))for(let De=Se.children.length-1;De>=0;--De)fe.push(Se.children[De])}return oe}),T=D(()=>k.value.length>0);function M(Z){const j=new Map,oe=new Map;let se=1;function fe(Se,De=1,Te=void 0){var Ce;const Re=[];for(const Le of Se){const me=I(Le),Be={level:De,key:me,data:Le};Be.label=R(Le),Be.parent=Te;const ze=A(Le);Be.disabled=P(Le),Be.isLeaf=!ze||ze.length===0,Be.expanded=n.value.has(me),ze&&ze.length&&(Be.children=fe(ze,De+1,Be)),Re.push(Be),j.set(me,Be),oe.has(De)||oe.set(De,[]),(Ce=oe.get(De))==null||Ce.push(Be)}return De>se&&(se=De),Re}const ge=fe(Z);return{treeNodeMap:j,levelTreeNodeMap:oe,maxLevel:se,treeNodes:ge}}function E(Z){const j=m(Z);j&&(n.value=j)}function A(Z){return Z[_.value]}function I(Z){return Z?Z[b.value]:""}function P(Z){return Z[w.value]}function R(Z){return Z[S.value]}function G(Z){n.value.has(Z.key)?V(Z):N(Z)}function K(Z){const j=new Set,oe=r.value.treeNodeMap;n.value.forEach(se=>{const fe=oe.get(se);fe&&(fe.expanded=!1)}),Z.forEach(se=>{let fe=oe.get(se);for(;fe&&!j.has(fe.key);)j.add(fe.key),fe.expanded=!0,fe=fe.parent}),n.value=j}function H(Z,j){B(Z),t(aO,Z.data,Z,j),e.expandOnClickNode&&G(Z),e.showCheckbox&&(e.checkOnClickNode||Z.isLeaf&&e.checkOnClickLeaf)&&!Z.disabled&&s(Z,!l(Z),!0)}function z(Z,j){t(rO,Z.data,Z,j)}function B(Z){X(Z)||(a.value=Z.key,t(lO,Z.data,Z))}function W(Z,j){s(Z,j)}function N(Z){const j=n.value;if(r.value&&e.accordion){const{treeNodeMap:se}=r.value;j.forEach(fe=>{const ge=se.get(fe);Z&&Z.level===(ge==null?void 0:ge.level)&&(j.delete(fe),ge.expanded=!1)})}j.add(Z.key);const oe=ve(Z.key);oe&&(oe.expanded=!0,t(oO,oe.data,oe))}function V(Z){n.value.delete(Z.key);const j=ve(Z.key);j&&(j.expanded=!1,t(iO,j.data,j))}function Y(Z){return!!Z.disabled}function X(Z){const j=a.value;return j!==void 0&&j===Z.key}function ee(){var Z,j;if(a.value!==void 0)return(j=(Z=r.value)==null?void 0:Z.treeNodeMap.get(a.value))==null?void 0:j.data}function re(){return a.value}function ie(Z){a.value=Z}function le(Z){r.value=M(Z)}function ve(Z){var oe;const j=_t(Z)?I(Z):Z;return(oe=r.value)==null?void 0:oe.treeNodeMap.get(j)}function he(Z,j="auto"){const oe=ve(Z);oe&&o.value&&o.value.scrollToItem(k.value.indexOf(oe),j)}function J(Z){var j;(j=o.value)==null||j.scrollTo(Z)}return ye(()=>e.currentNodeKey,Z=>{a.value=Z},{immediate:!0}),ye(()=>e.defaultExpandedKeys,Z=>{K(Z||[])}),ye(()=>e.data,Z=>{le(Z),K(e.defaultExpandedKeys||[])},{immediate:!0}),{tree:r,flattenTree:k,isNotEmpty:T,listRef:o,getKey:I,getChildren:A,toggleExpand:G,toggleCheckbox:s,isChecked:l,isIndeterminate:i,isDisabled:Y,isCurrent:X,isForceHiddenExpandIcon:y,handleNodeClick:H,handleNodeDrop:z,handleNodeCheck:W,getCurrentNode:ee,getCurrentKey:re,setCurrentKey:ie,getCheckedKeys:u,getCheckedNodes:c,getHalfCheckedKeys:d,getHalfCheckedNodes:v,setChecked:f,setCheckedKeys:h,filter:E,setData:le,getNode:ve,expandNode:N,collapseNode:V,setExpandedKeys:K,scrollToNode:he,scrollTo:J}}var k_e=q({name:"ElTreeNodeContent",props:b_e,setup(e){const t=Ze(Ww),n=ke("tree");return()=>{const a=e.node,{data:r}=a;return t!=null&&t.ctx.slots.default?t.ctx.slots.default({node:a,data:r}):ot(Bw,{tag:"span",truncated:!0,class:n.be("node","label")},()=>[a==null?void 0:a.label])}}});const T_e=["aria-expanded","aria-disabled","aria-checked","data-key"];var M_e=q({name:"ElTreeNode",__name:"tree-node",props:y_e,emits:w_e,setup(e,{emit:t}){const n=e,a=t,r=Ze(Ww),o=ke("tree"),i=D(()=>(r==null?void 0:r.props.indent)??16),l=D(()=>(r==null?void 0:r.props.icon)??F6),s=h=>{var y;const m=(y=r==null?void 0:r.props.props)==null?void 0:y.class;if(!m)return{};let g;if(Qe(m)){const{data:b}=h;g=m(b,h)}else g=m;return Ye(g)?{[g]:!0}:g},u=h=>{a("click",n.node,h)},c=h=>{a("drop",n.node,h)},d=()=>{a("toggle",n.node)},v=h=>{a("check",n.node,h)},f=h=>{var m,g,y,b;(y=(g=(m=r==null?void 0:r.instance)==null?void 0:m.vnode)==null?void 0:g.props)!=null&&y.onNodeContextmenu&&(h.stopPropagation(),h.preventDefault()),r==null||r.ctx.emit(cO,h,(b=n.node)==null?void 0:b.data,n.node)};return(h,m)=>{var g,y,b;return x(),O("div",{ref:"node$",class:U([p(o).b("node"),p(o).is("expanded",e.expanded),p(o).is("current",e.current),p(o).is("focusable",!e.disabled),p(o).is("checked",!e.disabled&&e.checked),s(e.node)]),role:"treeitem",tabindex:"-1","aria-expanded":e.expanded,"aria-disabled":e.disabled,"aria-checked":e.checked,"data-key":(g=e.node)==null?void 0:g.key,onClick:at(u,["stop"]),onContextmenu:f,onDragover:m[1]||(m[1]=at(()=>{},["prevent"])),onDragenter:m[2]||(m[2]=at(()=>{},["prevent"])),onDrop:at(c,["stop"])},[C("div",{class:U(p(o).be("node","content")),style:et({paddingLeft:`${(e.node.level-1)*i.value}px`,height:e.itemSize+"px"})},[l.value?(x(),ne(p(Xe),{key:0,class:U([p(o).is("leaf",!!((y=e.node)!=null&&y.isLeaf)),p(o).is("hidden",e.hiddenExpandIcon),{expanded:!((b=e.node)!=null&&b.isLeaf)&&e.expanded},p(o).be("node","expand-icon")]),onClick:at(d,["stop"])},{default:$(()=>[(x(),ne(St(l.value)))]),_:1},8,["class"])):te("v-if",!0),e.showCheckbox?(x(),ne(p(Zo),{key:1,"model-value":e.checked,indeterminate:e.indeterminate,disabled:e.disabled,onChange:v,onClick:m[0]||(m[0]=at(()=>{},["stop"]))},null,8,["model-value","indeterminate","disabled"])):te("v-if",!0),L(p(k_e),{node:{...e.node,expanded:e.expanded}},null,8,["node"])],6)],42,T_e)}}}),E_e=M_e,A_e=q({name:"ElTreeV2",__name:"tree",props:g_e,emits:__e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Xn(),i=D(()=>a.itemSize);Ft(Ww,{ctx:{emit:r,slots:o},props:a,instance:Et()}),Ft(Qi,void 0);const{t:l}=Vt(),s=ke("tree"),{flattenTree:u,isNotEmpty:c,listRef:d,toggleExpand:v,isIndeterminate:f,isChecked:h,isDisabled:m,isCurrent:g,isForceHiddenExpandIcon:y,handleNodeClick:b,handleNodeDrop:_,handleNodeCheck:w,toggleCheckbox:S,getCurrentNode:k,getCurrentKey:T,setCurrentKey:M,getCheckedKeys:E,getCheckedNodes:A,getHalfCheckedKeys:I,getHalfCheckedNodes:P,setChecked:R,setCheckedKeys:G,filter:K,setData:H,getNode:z,expandNode:B,collapseNode:W,setExpandedKeys:N,scrollToNode:V,scrollTo:Y}=x_e(a,r);return t({toggleCheckbox:S,getCurrentNode:k,getCurrentKey:T,setCurrentKey:M,getCheckedKeys:E,getCheckedNodes:A,getHalfCheckedKeys:I,getHalfCheckedNodes:P,setChecked:R,setCheckedKeys:G,filter:K,setData:H,getNode:z,expandNode:B,collapseNode:W,setExpandedKeys:N,scrollToNode:V,scrollTo:Y}),(X,ee)=>(x(),O("div",{class:U([p(s).b(),{[p(s).m("highlight-current")]:e.highlightCurrent}]),role:"tree"},[p(c)?(x(),ne(p(cp),{key:0,ref_key:"listRef",ref:d,"class-name":p(s).b("virtual-list"),data:p(u),total:p(u).length,height:e.height,"item-size":i.value,"perf-mode":e.perfMode,"scrollbar-always-on":e.scrollbarAlwaysOn},{default:$(({data:re,index:ie,style:le})=>[(x(),ne(E_e,{key:re[ie].key,style:et(le),node:re[ie],expanded:re[ie].expanded,"show-checkbox":e.showCheckbox,checked:p(h)(re[ie]),indeterminate:p(f)(re[ie]),"item-size":i.value,disabled:p(m)(re[ie]),current:p(g)(re[ie]),"hidden-expand-icon":p(y)(re[ie]),onClick:p(b),onToggle:p(v),onCheck:p(w),onDrop:p(_)},null,8,["style","node","expanded","show-checkbox","checked","indeterminate","item-size","disabled","current","hidden-expand-icon","onClick","onToggle","onCheck","onDrop"]))]),_:1},8,["class-name","data","total","height","item-size","perf-mode","scrollbar-always-on"])):(x(),O("div",{key:1,class:U(p(s).e("empty-block"))},[ce(X.$slots,"empty",{},()=>[C("span",{class:U(p(s).e("empty-text"))},Q(e.emptyText??p(l)("el.tree.emptyText")),3)])],2))],2))}}),I_e=A_e;const L_e=wt(I_e),P_e="ElUpload";var D_e=class extends Error{constructor(e,t,n,a){super(e),this.name="UploadAjaxError",this.status=t,this.method=n,this.url=a}};function Xx(e,t,n){let a;return n.response?a=`${n.response.error||n.response}`:n.responseText?a=`${n.responseText}`:a=`fail to ${t.method} ${e} ${n.status}`,new D_e(a,n.status,t.method,e)}function O_e(e){const t=e.responseText||e.response;if(!t)return t;try{return JSON.parse(t)}catch{return t}}const $_e=e=>{typeof XMLHttpRequest>"u"&&Dn(P_e,"XMLHttpRequest is undefined");const t=new XMLHttpRequest,n=e.action;t.upload&&t.upload.addEventListener("progress",o=>{const i=o;i.percent=o.total>0?o.loaded/o.total*100:0,e.onProgress(i)});const a=new FormData;if(e.data)for(const[o,i]of Object.entries(e.data))Ae(i)?i.length===2&&i[0]instanceof Blob&&Ye(i[1])?a.append(o,i[0],i[1]):i.forEach(l=>{a.append(o,l)}):a.append(o,i);a.append(e.filename,e.file,e.file.name),t.addEventListener("error",()=>{e.onError(Xx(n,e,t))}),t.addEventListener("load",()=>{if(t.status<200||t.status>=300)return e.onError(Xx(n,e,t));e.onSuccess(O_e(t))}),t.open(e.method,n,!0),e.withCredentials&&"withCredentials"in t&&(t.withCredentials=!0);const r=e.headers||{};if(r instanceof Headers)r.forEach((o,i)=>t.setRequestHeader(i,o));else for(const[o,i]of Object.entries(r))Un(i)||t.setRequestHeader(o,String(i));return t.send(a),t},dO=["text","picture","picture-card"];let R_e=1;const u_=()=>Date.now()+R_e++,fO=$e({action:{type:String,default:"#"},headers:{type:ae(Object)},method:{type:String,default:"post"},data:{type:ae([Object,Function,Promise]),default:()=>Nn({})},multiple:Boolean,name:{type:String,default:"file"},drag:Boolean,withCredentials:Boolean,showFileList:{type:Boolean,default:!0},accept:{type:String,default:""},fileList:{type:ae(Array),default:()=>Nn([])},autoUpload:{type:Boolean,default:!0},listType:{type:String,values:dO,default:"text"},httpRequest:{type:ae(Function),default:$_e},disabled:{type:Boolean,default:void 0},limit:Number,directory:Boolean}),N_e=$e({...fO,beforeUpload:{type:ae(Function),default:kt},beforeRemove:{type:ae(Function)},onRemove:{type:ae(Function),default:kt},onChange:{type:ae(Function),default:kt},onPreview:{type:ae(Function),default:kt},onSuccess:{type:ae(Function),default:kt},onProgress:{type:ae(Function),default:kt},onError:{type:ae(Function),default:kt},onExceed:{type:ae(Function),default:kt},crossorigin:{type:ae(String)}}),vO=Symbol("uploadContextKey"),B_e=$e({files:{type:ae(Array),default:()=>Nn([])},disabled:{type:Boolean,default:void 0},handlePreview:{type:ae(Function),default:kt},listType:{type:String,values:dO,default:"text"},crossorigin:{type:ae(String)}}),V_e={remove:e=>!!e},z_e=$e({...fO,beforeUpload:{type:ae(Function),default:kt},onRemove:{type:ae(Function),default:kt},onStart:{type:ae(Function),default:kt},onSuccess:{type:ae(Function),default:kt},onProgress:{type:ae(Function),default:kt},onError:{type:ae(Function),default:kt},onExceed:{type:ae(Function),default:kt}}),F_e=$e({disabled:{type:Boolean,default:void 0},directory:Boolean}),H_e={file:e=>Ae(e)},W_e=["tabindex","aria-disabled","onKeydown"],K_e=["src","crossorigin"],U_e=["onClick"],G_e=["title"],Y_e=["onClick"],j_e=["onClick"];var q_e=q({name:"ElUploadList",__name:"upload-list",props:B_e,emits:V_e,setup(e,{emit:t}){const n=e,a=t,{t:r}=Vt(),o=ke("upload"),i=ke("icon"),l=ke("list"),s=$n(),u=F(!1),c=D(()=>[o.b("list"),o.bm("list",n.listType),o.is("disabled",s.value)]),d=async f=>{const h=f.relatedTarget;await He(),h!=null&&h.classList.contains(o.be("list","item"))||(u.value=!1)},v=f=>{a("remove",f)};return(f,h)=>(x(),ne(L7,{tag:"ul",class:U(c.value),name:p(l).b()},{default:$(()=>[(x(!0),O(Ve,null,dt(e.files,(m,g)=>(x(),O("li",{key:m.uid||m.name,class:U([p(o).be("list","item"),p(o).is(m.status),{focusing:u.value}]),tabindex:p(s)?void 0:0,"aria-disabled":p(s),role:"button",onKeydown:kn(y=>!p(s)&&v(m),["delete"]),onFocus:h[0]||(h[0]=y=>u.value=!0),onBlur:d,onClick:h[1]||(h[1]=y=>u.value=!1)},[ce(f.$slots,"default",{file:m,index:g},()=>[e.listType==="picture"||m.status!=="uploading"&&e.listType==="picture-card"?(x(),O("img",{key:0,class:U(p(o).be("list","item-thumbnail")),src:m.url,crossorigin:e.crossorigin,alt:""},null,10,K_e)):te("v-if",!0),m.status==="uploading"||e.listType!=="picture-card"?(x(),O("div",{key:1,class:U(p(o).be("list","item-info"))},[C("a",{class:U(p(o).be("list","item-name")),onClick:at(y=>e.handlePreview(m),["prevent"])},[L(p(Xe),{class:U(p(i).m("document"))},{default:$(()=>[L(p(EI))]),_:1},8,["class"]),C("span",{class:U(p(o).be("list","item-file-name")),title:m.name},Q(m.name),11,G_e)],10,U_e),m.status==="uploading"?(x(),ne(p(kw),{key:0,type:e.listType==="picture-card"?"circle":"line","stroke-width":e.listType==="picture-card"?6:2,percentage:Number(m.percentage),style:et(e.listType==="picture-card"?"":"margin-top: 0.5rem")},null,8,["type","stroke-width","percentage","style"])):te("v-if",!0)],2)):te("v-if",!0),C("label",{class:U(p(o).be("list","item-status-label"))},[e.listType==="text"?(x(),ne(p(Xe),{key:0,class:U([p(i).m("upload-success"),p(i).m("circle-check")])},{default:$(()=>[L(p(Ao))]),_:1},8,["class"])):["picture-card","picture"].includes(e.listType)?(x(),ne(p(Xe),{key:1,class:U([p(i).m("upload-success"),p(i).m("check")])},{default:$(()=>[L(p(Os))]),_:1},8,["class"])):te("v-if",!0)],2),p(s)?te("v-if",!0):(x(),ne(p(Xe),{key:2,class:U(p(i).m("close")),"aria-label":p(r)("el.upload.delete"),role:"button",tabindex:"0",onClick:y=>v(m),onKeydown:kn(at(y=>v(m),["prevent"]),["enter","space"])},{default:$(()=>[L(p(so))]),_:1},8,["class","aria-label","onClick","onKeydown"])),p(s)?te("v-if",!0):(x(),O("i",{key:3,class:U(p(i).m("close-tip"))},Q(p(r)("el.upload.deleteTip")),3)),e.listType==="picture-card"?(x(),O("span",{key:4,class:U(p(o).be("list","item-actions"))},[C("span",{class:U(p(o).be("list","item-preview")),onClick:y=>e.handlePreview(m)},[L(p(Xe),{class:U(p(i).m("zoom-in"))},{default:$(()=>[L(p(G6))]),_:1},8,["class"])],10,Y_e),p(s)?te("v-if",!0):(x(),O("span",{key:0,class:U(p(o).be("list","item-delete")),onClick:y=>v(m)},[L(p(Xe),{class:U(p(i).m("delete"))},{default:$(()=>[L(p(W6))]),_:1},8,["class"])],10,j_e))],2)):te("v-if",!0)])],42,W_e))),128)),ce(f.$slots,"append")]),_:3},8,["class","name"]))}}),Zx=q_e;const Jx="ElUploadDrag";var X_e=q({name:Jx,__name:"upload-dragger",props:F_e,emits:H_e,setup(e,{emit:t}){const n=e,a=t;Ze(vO)||Dn(Jx,"usage: ");const r=ke("upload"),o=F(!1),i=$n(),l=v=>new Promise((f,h)=>v.file(f,h)),s=async v=>{try{if(v.isFile){const f=await l(v);return f.isDirectory=!1,[f]}if(v.isDirectory){const f=v.createReader(),h=()=>new Promise((b,_)=>f.readEntries(b,_)),m=[];let g=await h();for(;g.length>0;)m.push(...g),g=await h();const y=m.map(b=>s(b).catch(()=>[]));return Wm(await Promise.all(y))}}catch{return[]}return[]},u=async v=>{if(i.value)return;o.value=!1,v.stopPropagation();const f=Array.from(v.dataTransfer.files),h=v.dataTransfer.items||[];if(n.directory){const m=Array.from(h).map(g=>{var y;return(y=g==null?void 0:g.webkitGetAsEntry)==null?void 0:y.call(g)}).filter(g=>g);a("file",Wm(await Promise.all(m.map(s))));return}f.forEach((m,g)=>{var b,_;const y=(_=(b=h[g])==null?void 0:b.webkitGetAsEntry)==null?void 0:_.call(b);y&&(m.isDirectory=y.isDirectory)}),a("file",f)},c=()=>{i.value||(o.value=!0)},d=v=>{v.currentTarget.contains(v.relatedTarget)||(o.value=!1)};return(v,f)=>(x(),O("div",{class:U([p(r).b("dragger"),p(r).is("dragover",o.value)]),onDrop:at(u,["prevent"]),onDragover:at(c,["prevent"]),onDragleave:at(d,["prevent"])},[ce(v.$slots,"default")],34))}}),Z_e=X_e;const J_e=["tabindex","aria-disabled","onKeydown"],Q_e=["name","disabled","multiple","accept","webkitdirectory"];var e3e=q({name:"ElUploadContent",inheritAttrs:!1,__name:"upload-content",props:z_e,setup(e,{expose:t}){const n=e,a=ke("upload"),r=$n(),o=Ht({}),i=Ht(),l=m=>{if(m.length===0)return;const{autoUpload:g,limit:y,fileList:b,multiple:_,onStart:w,onExceed:S}=n;if(y&&b.length+m.length>y){S(m,b);return}_||(m=m.slice(0,1));for(const k of m){const T=k;T.uid=u_(),w(T),g&&s(T)}},s=async m=>{if(i.value.value="",!n.beforeUpload)return c(m);let g,y={};try{const _=n.data,w=n.beforeUpload(m);y=qv(n.data)?Li(n.data):n.data,g=await w,qv(n.data)&&Rn(_,y)&&(y=Li(n.data))}catch{g=!1}if(g===!1){n.onRemove(m);return}let b=m;g instanceof Blob&&(g instanceof File?b=g:b=new File([g],m.name,{type:m.type})),c(Object.assign(b,{uid:m.uid}),y)},u=async(m,g)=>Qe(m)?m(g):m,c=async(m,g)=>{const{headers:y,data:b,method:_,withCredentials:w,name:S,action:k,onProgress:T,onSuccess:M,onError:E,httpRequest:A}=n;try{g=await u(g??b,m)}catch{n.onRemove(m);return}const{uid:I}=m,P={headers:y||{},withCredentials:w,file:m,data:g,method:_,filename:S,action:k,onProgress:G=>{T(G,m)},onSuccess:G=>{M(G,m),delete o.value[I]},onError:G=>{E(G,m),delete o.value[I]}},R=A(P);o.value[I]=R,R instanceof Promise&&R.then(P.onSuccess,P.onError)},d=m=>{const g=m.target.files;g&&l(Array.from(g))},v=()=>{r.value||(i.value.value="",i.value.click())},f=()=>{v()};return t({abort:m=>{TA(o.value).filter(m?([g])=>String(m.uid)===g:()=>!0).forEach(([g,y])=>{y instanceof XMLHttpRequest&&y.abort(),delete o.value[g]})},upload:s}),(m,g)=>(x(),O("div",{class:U([p(a).b(),p(a).m(e.listType),p(a).is("drag",e.drag),p(a).is("disabled",p(r))]),tabindex:p(r)?void 0:0,"aria-disabled":p(r),role:"button",onClick:v,onKeydown:kn(at(f,["self"]),["enter","space"])},[e.drag?(x(),ne(Z_e,{key:0,disabled:p(r),directory:e.directory,onFile:l},{default:$(()=>[ce(m.$slots,"default")]),_:3},8,["disabled","directory"])):ce(m.$slots,"default",{key:1}),C("input",{ref_key:"inputRef",ref:i,class:U(p(a).e("input")),name:e.name,disabled:p(r),multiple:e.multiple,accept:e.accept,webkitdirectory:e.directory||void 0,type:"file",onChange:d,onClick:g[0]||(g[0]=at(()=>{},["stop"]))},null,42,Q_e)],42,J_e))}}),Qx=e3e;const ek="ElUpload",tk=e=>{var t;(t=e.url)!=null&&t.startsWith("blob:")&&URL.revokeObjectURL(e.url)},t3e=(e,t)=>{const n=DY(e,"fileList",void 0,{passive:!0}),a=h=>n.value.find(m=>m.uid===h.uid);function r(h){var m;(m=t.value)==null||m.abort(h)}function o(h=["ready","uploading","success","fail"]){n.value=n.value.filter(m=>!h.includes(m.status))}function i(h){n.value=n.value.filter(m=>m.uid!==h.uid)}const l=h=>{He(()=>e.onChange(h,n.value))},s=(h,m)=>{const g=a(m);g&&(console.error(h),g.status="fail",i(g),e.onError(h,g,n.value),l(g))},u=(h,m)=>{const g=a(m);g&&(e.onProgress(h,g,n.value),g.status="uploading",g.percentage=Math.round(h.percent))},c=(h,m)=>{const g=a(m);g&&(g.status="success",g.response=h,e.onSuccess(h,g,n.value),l(g))},d=h=>{Un(h.uid)&&(h.uid=u_());const m={name:h.name,percentage:0,status:"ready",size:h.size,raw:h,uid:h.uid};if(e.listType==="picture-card"||e.listType==="picture")try{m.url=URL.createObjectURL(h)}catch(g){un(ek,g.message),e.onError(g,m,n.value)}n.value=[...n.value,m],l(m)},v=async h=>{const m=h instanceof File?a(h):h;m||Dn(ek,"file to be removed not found");const g=y=>{r(y),i(y),e.onRemove(y,n.value),tk(y)};e.beforeRemove?await e.beforeRemove(m,n.value)!==!1&&g(m):g(m)};function f(){n.value.filter(({status:h})=>h==="ready").forEach(({raw:h})=>{var m;return h&&((m=t.value)==null?void 0:m.upload(h))})}return ye(()=>e.listType,h=>{h!=="picture-card"&&h!=="picture"||(n.value=n.value.map(m=>{const{raw:g,url:y}=m;if(!y&&g)try{m.url=URL.createObjectURL(g)}catch(b){e.onError(b,m,n.value)}return m}))}),ye(n,h=>{for(const m of h)m.uid||(m.uid=u_()),m.status||(m.status="success")},{immediate:!0,deep:!0}),{uploadFiles:n,abort:r,clearFiles:o,handleError:s,handleProgress:u,handleStart:d,handleSuccess:c,handleRemove:v,submit:f,revokeFileObjectURL:tk}};var n3e=q({name:"ElUpload",__name:"upload",props:N_e,setup(e,{expose:t}){const n=e,a=$n(),r=Ht(),{abort:o,submit:i,clearFiles:l,uploadFiles:s,handleStart:u,handleError:c,handleRemove:d,handleSuccess:v,handleProgress:f,revokeFileObjectURL:h}=t3e(n,r),m=D(()=>n.listType==="picture-card"),g=D(()=>({...n,fileList:s.value,onStart:u,onProgress:f,onSuccess:v,onError:c,onRemove:d}));return rn(()=>{s.value.forEach(h)}),Ft(vO,{accept:cn(n,"accept")}),t({abort:o,submit:i,clearFiles:l,handleStart:u,handleRemove:d}),(y,b)=>(x(),O("div",null,[m.value&&e.showFileList?(x(),ne(Zx,{key:0,disabled:p(a),"list-type":e.listType,files:p(s),crossorigin:e.crossorigin,"handle-preview":e.onPreview,onRemove:p(d)},xr({append:$(()=>[L(Qx,Tt({ref_key:"uploadRef",ref:r},g.value),{default:$(()=>[y.$slots.trigger?ce(y.$slots,"trigger",{key:0}):te("v-if",!0),!y.$slots.trigger&&y.$slots.default?ce(y.$slots,"default",{key:1}):te("v-if",!0)]),_:3},16)]),_:2},[y.$slots.file?{name:"default",fn:$(({file:_,index:w})=>[ce(y.$slots,"file",{file:_,index:w})]),key:"0"}:void 0]),1032,["disabled","list-type","files","crossorigin","handle-preview","onRemove"])):te("v-if",!0),!m.value||m.value&&!e.showFileList?(x(),ne(Qx,Tt({key:1,ref_key:"uploadRef",ref:r},g.value),{default:$(()=>[y.$slots.trigger?ce(y.$slots,"trigger",{key:0}):te("v-if",!0),!y.$slots.trigger&&y.$slots.default?ce(y.$slots,"default",{key:1}):te("v-if",!0)]),_:3},16)):te("v-if",!0),y.$slots.trigger?ce(y.$slots,"default",{key:2}):te("v-if",!0),ce(y.$slots,"tip"),!m.value&&e.showFileList?(x(),ne(Zx,{key:3,disabled:p(a),"list-type":e.listType,files:p(s),crossorigin:e.crossorigin,"handle-preview":e.onPreview,onRemove:p(d)},xr({_:2},[y.$slots.file?{name:"default",fn:$(({file:_,index:w})=>[ce(y.$slots,"file",{file:_,index:w})]),key:"0"}:void 0]),1032,["disabled","list-type","files","crossorigin","handle-preview","onRemove"])):te("v-if",!0)]))}}),a3e=n3e;const r3e=wt(a3e),o3e=$e({zIndex:{type:Number,default:9},rotate:{type:Number,default:-22},width:Number,height:Number,image:String,content:{type:ae([String,Array]),default:"Element Plus"},font:{type:ae(Object)},gap:{type:ae(Array),default:()=>[100,100]},offset:{type:ae(Array)}});function i3e(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}function l3e(e){return Object.keys(e).map(t=>`${i3e(t)}: ${e[t]};`).join(" ")}function s3e(){return window.devicePixelRatio||1}const u3e=(e,t)=>{let n=!1;return e.removedNodes.length&&t&&(n=Array.from(e.removedNodes).includes(t)),e.type==="attributes"&&e.target===t&&(n=!0),n},c3e={left:[0,.5],start:[0,.5],center:[.5,0],right:[1,-.5],end:[1,-.5]};function py(e,t,n=1){const a=document.createElement("canvas"),r=a.getContext("2d"),o=e*n,i=t*n;return a.setAttribute("width",`${o}px`),a.setAttribute("height",`${i}px`),r.save(),[r,a,o,i]}function d3e(){function e(t,n,a,r,o,i,l,s,u){const[c,d,v,f]=py(r,o,a);let h=0;if(t instanceof HTMLImageElement)c.drawImage(t,0,0,v,f);else{const{color:Y,fontSize:X,fontStyle:ee,fontWeight:re,fontFamily:ie,textAlign:le,textBaseline:ve}=i,he=Number(X)*a;c.font=`${ee} normal ${re} ${he}px/${o}px ${ie}`,c.fillStyle=Y,c.textAlign=le,c.textBaseline=ve;const J=Ae(t)?t:[t];if(ve!=="top"&&J[0]){const Z=c.measureText(J[0]);c.textBaseline="top";const j=c.measureText(J[0]);h=Z.actualBoundingBoxAscent-j.actualBoundingBoxAscent}J==null||J.forEach((Z,j)=>{const[oe,se]=c3e[le];c.fillText(Z??"",v*oe+u*se,j*(he+i.fontGap*a))})}const m=Math.PI/180*Number(n),g=Math.max(r,o),[y,b,_]=py(g,g,a);y.translate(_/2,_/2),y.rotate(m),v>0&&f>0&&y.drawImage(d,-v/2,-f/2);function w(Y,X){return[Y*Math.cos(m)-X*Math.sin(m),Y*Math.sin(m)+X*Math.cos(m)]}let S=0,k=0,T=0,M=0;const E=v/2,A=f/2;[[0-E,0-A],[0+E,0-A],[0+E,0+A],[0-E,0+A]].forEach(([Y,X])=>{const[ee,re]=w(Y,X);S=Math.min(S,ee),k=Math.max(k,ee),T=Math.min(T,re),M=Math.max(M,re)});const I=S+_/2,P=T+_/2,R=k-S,G=M-T,K=l*a,H=s*a,z=(R+K)*2,B=G+H,[W,N]=py(z,B);function V(Y=0,X=0){W.drawImage(b,I,P,R,G,Y,X+h,R,G)}return V(),V(R+K,-G/2-H/2),V(R+K,+G/2+H/2),[N.toDataURL(),z/a,B/a]}return e}var f3e=q({name:"ElWatermark",__name:"watermark",props:o3e,setup(e){const t={position:"relative"},n=e,a=D(()=>{var I;return((I=n.font)==null?void 0:I.fontGap)??3}),r=D(()=>{var I;return((I=n.font)==null?void 0:I.color)??"rgba(0,0,0,.15)"}),o=D(()=>{var I;return((I=n.font)==null?void 0:I.fontSize)??16}),i=D(()=>{var I;return((I=n.font)==null?void 0:I.fontWeight)??"normal"}),l=D(()=>{var I;return((I=n.font)==null?void 0:I.fontStyle)??"normal"}),s=D(()=>{var I;return((I=n.font)==null?void 0:I.fontFamily)??"sans-serif"}),u=D(()=>{var I;return((I=n.font)==null?void 0:I.textAlign)??"center"}),c=D(()=>{var I;return((I=n.font)==null?void 0:I.textBaseline)??"hanging"}),d=D(()=>n.gap[0]),v=D(()=>n.gap[1]),f=D(()=>d.value/2),h=D(()=>v.value/2),m=D(()=>{var I;return((I=n.offset)==null?void 0:I[0])??f.value}),g=D(()=>{var I;return((I=n.offset)==null?void 0:I[1])??h.value}),y=()=>{const I={zIndex:n.zIndex,position:"absolute",left:0,top:0,width:"100%",height:"100%",pointerEvents:"none",backgroundRepeat:"repeat"};let P=m.value-f.value,R=g.value-h.value;return P>0&&(I.left=`${P}px`,I.width=`calc(100% - ${P}px)`,P=0),R>0&&(I.top=`${R}px`,I.height=`calc(100% - ${R}px)`,R=0),I.backgroundPosition=`${P}px ${R}px`,I},b=Ht(null),_=Ht(),w=F(!1),S=()=>{_.value&&(_.value.remove(),_.value=void 0)},k=(I,P)=>{var R;b.value&&_.value&&(w.value=!0,_.value.setAttribute("style",l3e({...y(),backgroundImage:`url('${I}')`,backgroundSize:`${Math.floor(P)}px`})),(R=b.value)==null||R.append(_.value),setTimeout(()=>{w.value=!1}))},T=I=>{let P=120,R=64,G=0;const{image:K,content:H,width:z,height:B,rotate:W}=n;if(!K&&I.measureText){I.font=`${Number(o.value)}px ${s.value}`;const N=Ae(H)?H:[H];let V=0,Y=0;N.forEach(ee=>{const{width:re,fontBoundingBoxAscent:ie,fontBoundingBoxDescent:le,actualBoundingBoxAscent:ve,actualBoundingBoxDescent:he}=I.measureText(ee),J=zt(ie)?ve+he:ie+le;re>V&&(V=Math.ceil(re)),J>Y&&(Y=Math.ceil(J))}),P=V,R=Y*N.length+(N.length-1)*a.value;const X=Math.PI/180*Number(W);G=Math.ceil(Math.abs(Math.sin(X)*R)/2),P+=G}return[z??P,B??R,G]},M=d3e(),E=()=>{const I=document.createElement("canvas").getContext("2d"),P=n.image,R=n.content,G=n.rotate;if(I){_.value||(_.value=document.createElement("div"));const K=s3e(),[H,z,B]=T(I),W=N=>{const[V,Y]=M(N||"",G,K,H,z,{color:r.value,fontSize:o.value,fontStyle:l.value,fontWeight:i.value,fontFamily:s.value,fontGap:a.value,textAlign:u.value,textBaseline:c.value},d.value,v.value,B);k(V,Y)};if(P){const N=new Image;N.onload=()=>{W(N)},N.onerror=()=>{W(R)},N.crossOrigin="anonymous",N.referrerPolicy="no-referrer",N.src=P}else W(R)}};return mt(()=>{E()}),ye(()=>n,()=>{E()},{deep:!0,flush:"post"}),rn(()=>{S()}),Zh(b,I=>{w.value||I.forEach(P=>{u3e(P,_.value)&&(S(),E())})},{attributes:!0,subtree:!0,childList:!0}),(I,P)=>(x(),O("div",{ref_key:"containerRef",ref:b,style:et([t])},[ce(I.$slots,"default")],4))}}),v3e=f3e;const h3e=wt(v3e),p3e=["absolute","fixed"],m3e=["top-start","top-end","top","bottom-start","bottom-end","bottom","left-start","left-end","left","right-start","right-end","right"],Kw=$e({placement:{type:ae(String),values:m3e,default:"bottom"},reference:{type:ae(Object),default:null},strategy:{type:ae(String),values:p3e,default:"absolute"},offset:{type:Number,default:10},showArrow:Boolean,zIndex:{type:Number,default:2001}}),g3e={close:()=>!0},y3e=$e({modelValue:Boolean,current:{type:Number},showArrow:{type:Boolean,default:!0},showClose:{type:Boolean,default:!0},closeIcon:{type:pn},placement:Kw.placement,contentStyle:{type:ae([Object])},mask:{type:ae([Boolean,Object]),default:!0},gap:{type:ae(Object),default:()=>({offset:6,radius:2})},zIndex:{type:Number},scrollIntoViewOptions:{type:ae([Boolean,Object]),default:()=>({block:"center"})},type:{type:ae(String)},appendTo:{type:ae([String,Object]),default:"body"},closeOnPressEscape:{type:Boolean,default:!0},targetAreaClickable:{type:Boolean,default:!0}}),b3e={[bt]:e=>vn(e),"update:current":e=>Je(e),close:e=>Je(e),finish:()=>!0,change:e=>Je(e)},_3e=$e({target:{type:ae([String,Object,Function])},title:String,description:String,showClose:{type:Boolean,default:void 0},closeIcon:{type:pn},showArrow:{type:Boolean,default:void 0},placement:Kw.placement,mask:{type:ae([Boolean,Object]),default:void 0},contentStyle:{type:ae([Object])},prevButtonProps:{type:ae(Object)},nextButtonProps:{type:ae(Object)},scrollIntoViewOptions:{type:ae([Boolean,Object]),default:void 0},type:{type:ae(String)}}),w3e={close:()=>!0},S3e=(e,t,n,a,r)=>{const o=F(null),i=()=>{let c;return Ye(e.value)?c=document.querySelector(e.value):Qe(e.value)?c=e.value():c=e.value,c},l=()=>{const c=i();if(!c||!t.value){o.value=null;return}C3e(c)||c.scrollIntoView(r.value);const{left:d,top:v,width:f,height:h}=c.getBoundingClientRect();o.value={left:d,top:v,width:f,height:h,radius:0}};mt(()=>{ye([t,e],()=>{l()},{immediate:!0}),window.addEventListener("resize",l)}),rn(()=>{window.removeEventListener("resize",l)});const s=c=>(Ae(n.value.offset)?n.value.offset[c]:n.value.offset)??6,u=D(()=>{var f;if(!o.value)return o.value;const c=s(0),d=s(1),v=((f=n.value)==null?void 0:f.radius)||2;return{left:o.value.left-c,top:o.value.top-d,width:o.value.width+c*2,height:o.value.height+d*2,radius:v}});return{mergedPosInfo:u,triggerTarget:D(()=>{const c=i();return!a.value||!c||!window.DOMRect?c||void 0:{getBoundingClientRect(){var d,v,f,h;return window.DOMRect.fromRect({width:((d=u.value)==null?void 0:d.width)||0,height:((v=u.value)==null?void 0:v.height)||0,x:((f=u.value)==null?void 0:f.left)||0,y:((h=u.value)==null?void 0:h.top)||0})}}})}},q1=Symbol("ElTour");function C3e(e){const t=window.innerWidth||document.documentElement.clientWidth,n=window.innerHeight||document.documentElement.clientHeight,{top:a,right:r,bottom:o,left:i}=e.getBoundingClientRect();return a>=0&&i>=0&&r<=t&&o<=n}const x3e=(e,t,n,a,r,o,i,l)=>{const s=F(),u=F(),c=F({}),d={x:s,y:u,placement:a,strategy:r,middlewareData:c},v=D(()=>{const y=[Yq(p(o)),qq(),jq(),k3e()];return p(l)&&p(n)&&y.push(Xq({element:p(n)})),y}),f=async()=>{if(!qt)return;const y=p(e),b=p(t);if(!y||!b)return;const _=await Zq(y,b,{placement:p(a),strategy:p(r),middleware:p(v)});sh(d).forEach(w=>{d[w].value=_[w]})},h=D(()=>{if(!p(e))return{position:"fixed",top:"50%",left:"50%",transform:"translate3d(-50%, -50%, 0)",maxWidth:"100vw",zIndex:p(i)};const{overflow:y}=p(c);return{position:p(r),zIndex:p(i),top:p(u)!=null?`${p(u)}px`:"",left:p(s)!=null?`${p(s)}px`:"",maxWidth:y!=null&&y.maxWidth?`${y==null?void 0:y.maxWidth}px`:""}}),m=D(()=>{if(!p(l))return{};const{arrow:y}=p(c);return{left:(y==null?void 0:y.x)!=null?`${y==null?void 0:y.x}px`:"",top:(y==null?void 0:y.y)!=null?`${y==null?void 0:y.y}px`:""}});let g;return mt(()=>{const y=p(e),b=p(t);y&&b&&(g=Uq(y,b,f)),ur(()=>{f()})}),rn(()=>{g&&g()}),{update:f,contentStyle:h,arrowStyle:m}},k3e=()=>({name:"overflow",async fn(e){const t=await Gq(e);let n=0;return t.left>0&&(n=t.left),t.right>0&&(n=t.right),{data:{maxWidth:e.rects.floating.width-n}}}}),T3e=$e({zIndex:{type:Number,default:1001},visible:Boolean,fill:{type:String,default:"rgba(0,0,0,0.5)"},pos:{type:ae(Object)},targetAreaClickable:{type:Boolean,default:!0}}),M3e={style:{width:"100%",height:"100%"}},E3e=["d"];var A3e=q({name:"ElTourMask",inheritAttrs:!1,__name:"mask",props:T3e,setup(e){const t=e,{ns:n}=Ze(q1),a=D(()=>{var c;return((c=t.pos)==null?void 0:c.radius)??2}),r=D(()=>{const c=a.value,d=`a${c},${c} 0 0 1`;return{topRight:`${d} ${c},${c}`,bottomRight:`${d} ${-c},${c}`,bottomLeft:`${d} ${-c},${-c}`,topLeft:`${d} ${c},${-c}`}}),{width:o,height:i}=_6(),l=D(()=>{const c=o.value,d=i.value,v=r.value,f=`M${c},0 L0,0 L0,${d} L${c},${d} L${c},0 Z`,h=a.value;return t.pos?`${f} M${t.pos.left+h},${t.pos.top} h${t.pos.width-h*2} ${v.topRight} v${t.pos.height-h*2} ${v.bottomRight} h${-t.pos.width+h*2} ${v.bottomLeft} v${-t.pos.height+h*2} ${v.topLeft} z`:f}),s=D(()=>({position:"fixed",left:0,right:0,top:0,bottom:0,zIndex:t.zIndex,pointerEvents:t.pos&&t.targetAreaClickable?"none":"auto"})),u=D(()=>({fill:t.fill,pointerEvents:"auto",cursor:"auto"}));return D1(cn(t,"visible"),{ns:n}),(c,d)=>e.visible?(x(),O("div",Tt({key:0,class:p(n).e("mask"),style:s.value},c.$attrs),[(x(),O("svg",M3e,[C("path",{class:U(p(n).e("hollow")),style:et(u.value),d:l.value},null,14,E3e)]))],16)):te("v-if",!0)}}),I3e=A3e;const L3e=["data-side"];var P3e=q({name:"ElTourContent",__name:"content",props:Kw,emits:g3e,setup(e,{emit:t}){const n=e,a=t,r=F(n.placement),o=F(n.strategy),i=F(null),l=F(null);ye(()=>n.placement,()=>{r.value=n.placement});const{contentStyle:s,arrowStyle:u}=x3e(cn(n,"reference"),i,l,r,o,cn(n,"offset"),cn(n,"zIndex"),cn(n,"showArrow")),c=D(()=>r.value.split("-")[0]),{ns:d}=Ze(q1),v=()=>{a("close")},f=h=>{h.detail.focusReason==="pointer"&&h.preventDefault()};return(h,m)=>(x(),O("div",{ref_key:"contentRef",ref:i,style:et(p(s)),class:U(p(d).e("content")),"data-side":c.value,tabindex:"-1"},[L(p(pf),{loop:"",trapped:"","focus-start-el":"container","focus-trap-el":i.value||void 0,onReleaseRequested:v,onFocusoutPrevented:f},{default:$(()=>[ce(h.$slots,"default")]),_:3},8,["focus-trap-el"]),e.showArrow?(x(),O("span",{key:0,ref_key:"arrowRef",ref:l,style:et(p(u)),class:U(p(d).e("arrow"))},null,6)):te("v-if",!0)],14,L3e))}}),D3e=P3e,O3e=q({name:"ElTourSteps",props:{current:{type:Number,default:0}},emits:["update-total"],setup(e,{slots:t,emit:n}){let a=0;return()=>{var s,u;const r=(s=t.default)==null?void 0:s.call(t),o=[];let i=0;function l(c){Ae(c)&&c.forEach(d=>{var v;((v=(d==null?void 0:d.type)||{})==null?void 0:v.name)==="ElTourStep"&&(o.push(d),i+=1)})}return r.length&&l(eo((u=r[0])==null?void 0:u.children)),a!==i&&(a=i,n("update-total",i)),o.length?o[e.current]:null}}}),$3e=q({name:"ElTour",inheritAttrs:!1,__name:"tour",props:y3e,emits:b3e,setup(e,{emit:t}){const n=e,a=t,r=ke("tour"),o=F(0),i=F(),l=D(()=>!zt(n.current)),s=F(n.current??0),u=D({get(){return zt(n.current)?s.value:n.current},set(P){(l.value?n.current:s.value)!==P&&(l.value||(s.value=P),a("update:current",P))}}),c=D(()=>{var P;return(P=i.value)==null?void 0:P.target}),d=D(()=>[r.b(),_.value==="primary"?r.m("primary"):""]),v=D(()=>{var P;return((P=i.value)==null?void 0:P.placement)||n.placement}),f=D(()=>{var P;return((P=i.value)==null?void 0:P.contentStyle)??n.contentStyle}),h=D(()=>{var P;return((P=i.value)==null?void 0:P.mask)??n.mask}),m=D(()=>!!h.value&&n.modelValue),g=D(()=>vn(h.value)?void 0:h.value),y=D(()=>{var P;return!!c.value&&(((P=i.value)==null?void 0:P.showArrow)??n.showArrow)}),b=D(()=>{var P;return((P=i.value)==null?void 0:P.scrollIntoViewOptions)??n.scrollIntoViewOptions}),_=D(()=>{var P;return((P=i.value)==null?void 0:P.type)??n.type}),{nextZIndex:w}=tp(),S=w(),k=D(()=>n.zIndex??S),{mergedPosInfo:T,triggerTarget:M}=S3e(c,cn(n,"modelValue"),cn(n,"gap"),h,b);ye(()=>n.current,P=>!zt(P)&&(s.value=P)),ye(u,(P,R)=>{!n.modelValue||P===R||a(Ot,P)},{flush:"post"}),ye(()=>n.modelValue,P=>{!P&&u.value!==0&&(u.value=0)});const E=()=>{n.closeOnPressEscape&&(a(bt,!1),a("close",u.value))},A=P=>{o.value=P},I=Xn();return Ft(q1,{currentStep:i,current:u,total:o,showClose:cn(n,"showClose"),closeIcon:cn(n,"closeIcon"),mergedType:_,ns:r,slots:I,updateModelValue(P){a(bt,P)},onClose(){a("close",u.value)},onFinish(){a("finish")}}),(P,R)=>{var G,K;return x(),O(Ve,null,[(x(),ne(sf,{to:e.appendTo},[C("div",Tt({class:d.value},P.$attrs),[L(I3e,{visible:m.value,fill:(G=g.value)==null?void 0:G.color,style:et((K=g.value)==null?void 0:K.style),pos:p(T),"z-index":k.value,"target-area-clickable":e.targetAreaClickable},null,8,["visible","fill","style","pos","z-index","target-area-clickable"]),e.modelValue?(x(),ne(D3e,{key:u.value,reference:p(M),placement:v.value,"show-arrow":y.value,"z-index":k.value,style:et(f.value),onClose:E},{default:$(()=>[L(p(O3e),{current:u.value,onUpdateTotal:A},{default:$(()=>[ce(P.$slots,"default")]),_:3},8,["current"])]),_:3},8,["reference","placement","show-arrow","z-index","style"])):te("v-if",!0)],16)],8,["to"])),te(" just for IDE "),te("v-if",!0)],64)}}}),R3e=$3e;const N3e=["aria-label"];var B3e=q({name:"ElTourStep",__name:"step",props:_3e,emits:w3e,setup(e,{emit:t}){const n=e,a=t,{Close:r}=YI,{t:o}=Vt(),{currentStep:i,current:l,total:s,showClose:u,closeIcon:c,mergedType:d,ns:v,slots:f,updateModelValue:h,onClose:m,onFinish:g}=Ze(q1);ye(n,E=>{i.value=E},{immediate:!0});const y=D(()=>n.showClose??u.value),b=D(()=>n.closeIcon??c.value??r),_=E=>{if(E)return Tc(E,["children","onClick"])},w=()=>{var E,A;l.value-=1,(E=n.prevButtonProps)!=null&&E.onClick&&((A=n.prevButtonProps)==null||A.onClick())},S=()=>{var E;l.value>=s.value-1?k():l.value+=1,(E=n.nextButtonProps)!=null&&E.onClick&&n.nextButtonProps.onClick()},k=()=>{T(),g()},T=()=>{h(!1),m(),a("close")},M=E=>{var A;if(!((A=E.target)!=null&&A.isContentEditable))switch(mn(E)){case Pe.left:E.preventDefault(),l.value>0&&w();break;case Pe.right:E.preventDefault(),S();break}};return mt(()=>{window.addEventListener("keydown",M)}),rn(()=>{window.removeEventListener("keydown",M)}),(E,A)=>(x(),O(Ve,null,[y.value?(x(),O("button",{key:0,"aria-label":p(o)("el.tour.close"),class:U(p(v).e("closebtn")),type:"button",onClick:T},[L(p(Xe),{class:U(p(v).e("close"))},{default:$(()=>[(x(),ne(St(b.value)))]),_:1},8,["class"])],10,N3e)):te("v-if",!0),C("header",{class:U([p(v).e("header"),{"show-close":p(u)}])},[ce(E.$slots,"header",{},()=>[C("span",{role:"heading",class:U(p(v).e("title"))},Q(e.title),3)])],2),C("div",{class:U(p(v).e("body"))},[ce(E.$slots,"default",{},()=>[C("span",null,Q(e.description),1)])],2),C("footer",{class:U(p(v).e("footer"))},[C("div",{class:U(p(v).b("indicators"))},[p(f).indicators?(x(),ne(St(p(f).indicators),{key:0,current:p(l),total:p(s)},null,8,["current","total"])):(x(!0),O(Ve,{key:1},dt(p(s),(I,P)=>(x(),O("span",{key:I,class:U([p(v).b("indicator"),p(v).is("active",P===p(l))])},null,2))),128))],2),C("div",{class:U(p(v).b("buttons"))},[p(l)>0?(x(),ne(p(xa),Tt({key:0,size:"small",type:p(d)},_(e.prevButtonProps),{onClick:w}),{default:$(()=>{var I;return[ue(Q(((I=e.prevButtonProps)==null?void 0:I.children)??p(o)("el.tour.previous")),1)]}),_:1},16,["type"])):te("v-if",!0),p(l)<=p(s)-1?(x(),ne(p(xa),Tt({key:1,size:"small",type:p(d)==="primary"?"default":"primary"},_(e.nextButtonProps),{onClick:S}),{default:$(()=>{var I;return[ue(Q(((I=e.nextButtonProps)==null?void 0:I.children)??(p(l)===p(s)-1?p(o)("el.tour.finish"):p(o)("el.tour.next"))),1)]}),_:1},16,["type"])):te("v-if",!0)],2)],2)],64))}}),hO=B3e;const V3e=wt(R3e,{TourStep:hO}),z3e=On(hO),F3e=$e({container:{type:ae([String,Object])},offset:{type:Number,default:0},bound:{type:Number,default:15},duration:{type:Number,default:300},marker:{type:Boolean,default:!0},type:{type:ae(String),default:"default"},direction:{type:ae(String),default:"vertical"},selectScrollTop:Boolean}),H3e={change:e=>Ye(e),click:(e,t)=>e instanceof MouseEvent&&(Ye(t)||zt(t))},qp=e=>{if(!qt||e==="")return null;if(Ye(e))try{return document.querySelector(e)}catch{return null}return e};function W3e(e){let t=0;const n=(...a)=>{t&&ti(t),t=Wr(()=>{e(...a),t=0})};return n.cancel=()=>{ti(t),t=0},n}const pO=Symbol("anchor");var K3e=q({name:"ElAnchor",__name:"anchor",props:F3e,emits:H3e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=Xn(),i=F(""),l=F({}),s=F(null),u=F(null),c=F(),d={};let v=!1,f=0;const h=ke("anchor"),m=D(()=>[h.b(),a.type==="underline"?h.m("underline"):"",h.m(a.direction)]),g=P=>{d[P.href]=P.el},y=P=>{delete d[P]},b=P=>{i.value!==P&&(i.value=P,r(Ot,P))};let _=null,w="";const S=P=>{if(!c.value)return;const R=qp(P);if(!R)return;if(_){if(w===P)return;_()}w=P,v=!0;const G=UC(R,c.value),K=q4(R,G),H=G.scrollHeight-G.clientHeight,z=Math.min(K-a.offset,H);_=UY(c.value,f,z,a.duration,()=>{setTimeout(()=>{v=!1,w=""},20)})},k=P=>{P&&(b(P),S(P))},T=(P,R)=>{r("click",P,R),k(R)},M=W3e(()=>{c.value&&(f=GC(c.value));const P=E();v||zt(P)||b(P)}),E=()=>{if(!c.value)return;const P=GC(c.value),R=[];for(const G of Object.keys(d)){const K=qp(G);if(!K)continue;const H=q4(K,UC(K,c.value));R.push({top:H-a.offset-a.bound,href:G})}R.sort((G,K)=>G.top-K.top);for(let G=0;GP))return K.href}},A=()=>{const P=qp(a.container);!P||qh(P)?c.value=window:c.value=P};on(c,"scroll",M);const I=()=>{He(()=>{if(!s.value||!u.value||!i.value){l.value={};return}const P=d[i.value];if(!P){l.value={};return}const R=s.value.getBoundingClientRect(),G=u.value.getBoundingClientRect(),K=P.getBoundingClientRect();a.direction==="horizontal"?l.value={left:`${K.left-R.left}px`,width:`${K.width}px`,opacity:1}:l.value={top:`${K.top-R.top+(K.height-G.height)/2}px`,opacity:1}})};return ye(i,I),ye(()=>{var P;return(P=o.default)==null?void 0:P.call(o)},I),mt(()=>{A();const P=decodeURIComponent(window.location.hash);qp(P)?k(P):M()}),ye(()=>a.container,()=>{A()}),Ft(pO,{ns:h,direction:a.direction,currentAnchor:i,addLink:g,removeLink:y,handleClick:T}),t({scrollTo:k}),(P,R)=>(x(),O("div",{ref_key:"anchorRef",ref:s,class:U(m.value)},[e.marker?(x(),O("div",{key:0,ref_key:"markerRef",ref:u,class:U(p(h).e("marker")),style:et(l.value)},null,6)):te("v-if",!0),C("div",{class:U(p(h).e("list"))},[ce(P.$slots,"default")],2)],2))}}),U3e=K3e;const G3e=$e({title:String,href:String}),Y3e=["href"];var j3e=q({name:"ElAnchorLink",__name:"anchor-link",props:G3e,setup(e){const t=e,n=F(null),{ns:a,direction:r,currentAnchor:o,addLink:i,removeLink:l,handleClick:s}=Ze(pO),u=D(()=>[a.e("link"),a.is("active",o.value===t.href)]),c=d=>{s(d,t.href)};return ye(()=>t.href,(d,v)=>{He(()=>{v&&l(v),d&&i({href:d,el:n.value})})}),mt(()=>{const{href:d}=t;d&&i({href:d,el:n.value})}),rn(()=>{const{href:d}=t;d&&l(d)}),(d,v)=>(x(),O("div",{class:U(p(a).e("item"))},[C("a",{ref_key:"linkRef",ref:n,class:U(u.value),href:e.href,onClick:c},[ce(d.$slots,"default",{},()=>[ue(Q(e.title),1)])],10,Y3e),d.$slots["sub-link"]&&p(r)==="vertical"?(x(),O("div",{key:0,class:U(p(a).e("list"))},[ce(d.$slots,"sub-link")],2)):te("v-if",!0)],2))}}),mO=j3e;const q3e=wt(U3e,{AnchorLink:mO}),X3e=On(mO),gO={label:"label",value:"value",disabled:"disabled"},Z3e=$e({direction:{type:ae(String),default:"horizontal"},options:{type:ae(Array),default:()=>[]},modelValue:{type:[String,Number,Boolean],default:void 0},props:{type:ae(Object),default:()=>gO},block:Boolean,size:pa,disabled:{type:Boolean,default:void 0},validateEvent:{type:Boolean,default:!0},id:String,name:String,...dr(["ariaLabel"])}),J3e={[bt]:e=>Ye(e)||Je(e)||vn(e),[Ot]:e=>Ye(e)||Je(e)||vn(e)},Q3e=["id","aria-label","aria-labelledby"],e6e=["name","disabled","checked","onChange"];var t6e=q({name:"ElSegmented",__name:"segmented",props:Z3e,emits:J3e,setup(e,{emit:t}){const n=e,a=t,r=ke("segmented"),o=Fa(),i=ca(),l=$n(),{formItem:s}=Ea(),{inputId:u,isLabeledByFormItem:c}=Yr(n,{formItemContext:s}),d=F(null),v=kY(),f=nn({isInit:!1,width:0,height:0,translateX:0,translateY:0,focusVisible:!1}),h=(I,P)=>{const R=g(P);a(bt,R),a(Ot,R),I.target.checked=R===n.modelValue},m=D(()=>({...gO,...n.props})),g=I=>_t(I)?I[m.value.value]:I,y=I=>_t(I)?I[m.value.label]:I,b=I=>!!(l.value||_t(I)&&I[m.value.disabled]),_=I=>n.modelValue===g(I),w=I=>n.options.find(P=>g(P)===I),S=I=>[r.e("item"),r.is("selected",_(I)),r.is("disabled",b(I))],k=()=>{if(!d.value)return;const I=d.value.querySelector(".is-selected"),P=d.value.querySelector(".is-selected input");if(!I||!P){f.width=0,f.height=0,f.translateX=0,f.translateY=0,f.focusVisible=!1;return}f.isInit=!0,n.direction==="vertical"?(f.height=I.offsetHeight,f.translateY=I.offsetTop):(f.width=I.offsetWidth,f.translateX=I.offsetLeft);try{f.focusVisible=P.matches(":focus-visible")}catch{}},T=D(()=>[r.b(),r.m(i.value),r.is("block",n.block)]),M=D(()=>({width:n.direction==="vertical"?"100%":`${f.width}px`,height:n.direction==="vertical"?`${f.height}px`:"100%",transform:n.direction==="vertical"?`translateY(${f.translateY}px)`:`translateX(${f.translateX}px)`,display:f.isInit?"block":"none"})),E=D(()=>[r.e("item-selected"),r.is("disabled",b(w(n.modelValue))),r.is("focus-visible",f.focusVisible)]),A=D(()=>n.name||o.value);return Mn(d,k),ye(v,k),ye(()=>n.options,k,{deep:!0,flush:"post"}),ye(()=>n.modelValue,()=>{var I;k(),n.validateEvent&&((I=s==null?void 0:s.validate)==null||I.call(s,"change").catch(kt))},{flush:"post"}),(I,P)=>e.options.length?(x(),O("div",{key:0,id:p(u),ref_key:"segmentedRef",ref:d,class:U(T.value),role:"radiogroup","aria-label":p(c)?void 0:e.ariaLabel||"segmented","aria-labelledby":p(c)?p(s).labelId:void 0},[C("div",{class:U([p(r).e("group"),p(r).m(e.direction)])},[C("div",{style:et(M.value),class:U(E.value)},null,6),(x(!0),O(Ve,null,dt(e.options,(R,G)=>(x(),O("label",{key:G,class:U(S(R))},[C("input",{class:U(p(r).e("item-input")),type:"radio",name:A.value,disabled:b(R),checked:_(R),onChange:K=>h(K,R)},null,42,e6e),C("div",{class:U(p(r).e("item-label"))},[ce(I.$slots,"default",{item:R},()=>[ue(Q(y(R)),1)])],2)],2))),128))],2)],10,Q3e)):te("v-if",!0)}}),n6e=t6e;const a6e=wt(n6e),r6e=(e,t)=>{const n=e.toLowerCase();return(t.label||t.value||"").toLowerCase().includes(n)},o6e=(e,t,n)=>{const{selectionEnd:a}=e;if(a===null)return;const r=e.value,o=wa(t);let i=-1,l;for(let s=a-1;s>=0;--s){const u=r[s];if(i===-1&&(u===n||u===` +`||u==="\r")){i=s;continue}if(o.includes(u)){const c=i===-1?a:i;l={pattern:r.slice(s+1,c),start:s+1,end:c,prefix:u,prefixIndex:s,splitIndex:i,selectionEnd:a};break}}return l},i6e=(e,t={debug:!1,useSelectionEnd:!1})=>{const n=e.selectionStart!==null?e.selectionStart:0,a=e.selectionEnd!==null?e.selectionEnd:0,r=t.useSelectionEnd?a:n,o=["direction","boxSizing","width","height","overflowX","overflowY","borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth","borderStyle","paddingTop","paddingRight","paddingBottom","paddingLeft","fontStyle","fontVariant","fontWeight","fontStretch","fontSize","fontSizeAdjust","lineHeight","fontFamily","textAlign","textTransform","textIndent","textDecoration","letterSpacing","wordSpacing","tabSize","MozTabSize"];if(t.debug){const v=document.querySelector("#input-textarea-caret-position-mirror-div");v!=null&&v.parentNode&&v.parentNode.removeChild(v)}const i=document.createElement("div");i.id="input-textarea-caret-position-mirror-div",document.body.appendChild(i);const l=i.style,s=window.getComputedStyle(e),u=e.nodeName==="INPUT";l.whiteSpace=u?"nowrap":"pre-wrap",u||(l.wordWrap="break-word"),l.position="absolute",t.debug||(l.visibility="hidden"),o.forEach(v=>{if(u&&v==="lineHeight")if(s.boxSizing==="border-box"){const f=Number.parseInt(s.height),h=Number.parseInt(s.paddingTop)+Number.parseInt(s.paddingBottom)+Number.parseInt(s.borderTopWidth)+Number.parseInt(s.borderBottomWidth),m=h+Number.parseInt(s.lineHeight);f>m?l.lineHeight=`${f-h}px`:f===m?l.lineHeight=s.lineHeight:l.lineHeight="0"}else l.lineHeight=s.height;else l[v]=s[v]}),P1()?e.scrollHeight>Number.parseInt(s.height)&&(l.overflowY="scroll"):l.overflow="hidden",i.textContent=e.value.slice(0,Math.max(0,r)),u&&i.textContent&&(i.textContent=i.textContent.replace(/\s/g," "));const c=document.createElement("span");c.textContent=e.value.slice(Math.max(0,r))||".",c.style.position="relative",c.style.left=`${-e.scrollLeft}px`,c.style.top=`${-e.scrollTop}px`,i.appendChild(c);const d={top:c.offsetTop+Number.parseInt(s.borderTopWidth),left:c.offsetLeft+Number.parseInt(s.borderLeftWidth),height:Number.parseInt(s.fontSize)*1.5};return t.debug?c.style.backgroundColor="#aaa":document.body.removeChild(i),d.left>=e.clientWidth&&(d.left=e.clientWidth),d},l6e=$e({...J6,options:{type:ae(Array),default:()=>[]},prefix:{type:ae([String,Array]),default:"@",validator:e=>Ye(e)?e.length===1:e.every(t=>Ye(t)&&t.length===1)},split:{type:String,default:" ",validator:e=>e.length===1},filterOption:{type:ae([Boolean,Function]),default:()=>r6e,validator:e=>e===!1?!0:Qe(e)},placement:{type:ae(String),default:"bottom"},showArrow:Boolean,offset:{type:Number,default:0},whole:Boolean,checkIsWhole:{type:ae(Function)},modelValue:String,loading:Boolean,popperClass:Tn.popperClass,popperStyle:Tn.popperStyle,popperOptions:{type:ae(Object),default:()=>({})},props:{type:ae(Object),default:()=>yO}}),s6e={[bt]:e=>Ye(e),"whole-remove":(e,t)=>Ye(e)&&Ye(t),input:e=>Ye(e),search:(e,t)=>Ye(e)&&Ye(t),select:(e,t)=>_t(e)&&Ye(t),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent},yO={value:"value",label:"label",disabled:"disabled"},u6e=$e({options:{type:ae(Array),default:()=>[]},loading:Boolean,disabled:Boolean,contentId:String,ariaLabel:String}),c6e={select:e=>Ye(e.value)},d6e=["id","aria-disabled","aria-selected","onMousemove","onClick"];var f6e=q({name:"ElMentionDropdown",__name:"mention-dropdown",props:u6e,emits:c6e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=ke("mention"),{t:i}=Vt(),l=F(-1),s=F(),u=F(),c=F(),d=(w,S)=>[o.be("dropdown","item"),o.is("hovering",l.value===S),o.is("disabled",w.disabled||a.disabled)],v=w=>{w.disabled||a.disabled||r("select",w)},f=w=>{l.value=w},h=D(()=>a.disabled||a.options.every(w=>w.disabled)),m=D(()=>a.options[l.value]),g=()=>{!m.value||m.value.disabled||a.disabled||r("select",m.value)},y=w=>{const{options:S}=a;if(S.length===0||h.value)return;w==="next"?(l.value++,l.value===S.length&&(l.value=0)):w==="prev"&&(l.value--,l.value<0&&(l.value=S.length-1));const k=S[l.value];if(k.disabled){y(w);return}He(()=>b(k))},b=w=>{var M,E,A,I;const{options:S}=a,k=S.findIndex(P=>P.value===w.value),T=(M=u.value)==null?void 0:M[k];if(T){const P=(A=(E=c.value)==null?void 0:E.querySelector)==null?void 0:A.call(E,`.${o.be("dropdown","wrap")}`);P&&k6(P,T)}(I=s.value)==null||I.handleScroll()};return ye(()=>a.options,()=>{h.value||a.options.length===0?l.value=-1:l.value=a.options.findIndex(w=>!w.disabled)},{immediate:!0}),t({hoveringIndex:l,navigateOptions:y,selectHoverOption:g,hoverOption:m}),(w,S)=>(x(),O("div",{ref_key:"dropdownRef",ref:c,class:U(p(o).b("dropdown"))},[w.$slots.header?(x(),O("div",{key:0,class:U(p(o).be("dropdown","header"))},[ce(w.$slots,"header")],2)):te("v-if",!0),st(L(p(ni),{id:e.contentId,ref_key:"scrollbarRef",ref:s,tag:"ul","wrap-class":p(o).be("dropdown","wrap"),"view-class":p(o).be("dropdown","list"),role:"listbox","aria-label":e.ariaLabel,"aria-orientation":"vertical"},{default:$(()=>[(x(!0),O(Ve,null,dt(e.options,(k,T)=>(x(),O("li",{id:`${e.contentId}-${T}`,ref_for:!0,ref_key:"optionRefs",ref:u,key:T,class:U(d(k,T)),role:"option","aria-disabled":k.disabled||e.disabled||void 0,"aria-selected":l.value===T,onMousemove:M=>f(T),onClick:at(M=>v(k),["stop"])},[ce(w.$slots,"label",{item:k,index:T},()=>[C("span",null,Q(k.label??k.value),1)])],42,d6e))),128))]),_:3},8,["id","wrap-class","view-class","aria-label"]),[[Zt,e.options.length>0&&!e.loading]]),e.loading?(x(),O("div",{key:1,class:U(p(o).be("dropdown","loading"))},[ce(w.$slots,"loading",{},()=>[ue(Q(p(i)("el.mention.loading")),1)])],2)):te("v-if",!0),w.$slots.footer?(x(),O("div",{key:2,class:U(p(o).be("dropdown","footer"))},[ce(w.$slots,"footer")],2)):te("v-if",!0)],2))}}),v6e=f6e,h6e=q({name:"ElMention",inheritAttrs:!1,__name:"mention",props:l6e,emits:s6e,setup(e,{expose:t,emit:n}){const a=e,r=n,o=D(()=>{const z=Ba.props??[];return ji(a,Ae(z)?z:Object.keys(z))}),i=ke("mention"),l=$n(),s=Fa(),u=F(),c=F(),d=F(),v=F(!1),f=F(),h=F(),m=D(()=>a.showArrow?a.placement:`${a.placement}-start`),g=D(()=>a.showArrow?["bottom","top"]:["bottom-start","top-start"]),y=D(()=>({...yO,...a.props})),b=z=>{const B={label:z[y.value.label],value:z[y.value.value],disabled:z[y.value.disabled]};return{...z,...B}},_=D(()=>a.options.map(b)),w=D(()=>{const{filterOption:z}=a;return!h.value||!z?_.value:_.value.filter(B=>z(h.value.pattern,B))}),S=D(()=>v.value&&(!!w.value.length||a.loading)),k=D(()=>{var z;return`${s.value}-${(z=d.value)==null?void 0:z.hoveringIndex}`}),T=z=>{r(bt,z),r(ua,z),G()},M=z=>{var W,N,V,Y;if((W=u.value)!=null&&W.isComposing)return;const B=mn(z);switch(B){case Pe.left:case Pe.right:G();break;case Pe.up:case Pe.down:if(!v.value)return;z.preventDefault(),(N=d.value)==null||N.navigateOptions(B===Pe.up?"prev":"next");break;case Pe.enter:case Pe.numpadEnter:if(!v.value){a.type!=="textarea"&&G();return}z.preventDefault(),(V=d.value)!=null&&V.hoverOption?(Y=d.value)==null||Y.selectHoverOption():v.value=!1;break;case Pe.esc:if(!v.value)return;z.preventDefault(),v.value=!1;break;case Pe.backspace:if(a.whole&&h.value){const{splitIndex:X,selectionEnd:ee,pattern:re,prefixIndex:ie,prefix:le}=h.value,ve=R();if(!ve)return;const he=ve.value,J=_.value.find(Z=>Z.value===re);if((Qe(a.checkIsWhole)?a.checkIsWhole(re,le):J)&&X!==-1&&X+1===ee){z.preventDefault();const Z=he.slice(0,ie)+he.slice(X+1);r(bt,Z),r(ua,Z),r("whole-remove",re,le);const j=ie;He(()=>{ve.selectionStart=j,ve.selectionEnd=j,H()})}}}},{wrapperRef:E}=Fl(u,{disabled:l,afterFocus(){G()},beforeBlur(z){var B;return(B=c.value)==null?void 0:B.isFocusInsideContent(z)},afterBlur(){v.value=!1}}),A=()=>{G()},I=z=>a.options.find(B=>z.value===B[y.value.value]),P=z=>{if(!h.value)return;const B=R();if(!B)return;const W=B.value,{split:N}=a,V=W.slice(h.value.end),Y=V.startsWith(N),X=`${z.value}${Y?"":N}`,ee=W.slice(0,h.value.start)+X+V;r(bt,ee),r(ua,ee),r("select",I(z),h.value.prefix);const re=h.value.start+X.length+(Y?1:0);He(()=>{B.selectionStart=re,B.selectionEnd=re,B.focus(),H()})},R=()=>{var z,B;return a.type==="textarea"?(z=u.value)==null?void 0:z.textarea:(B=u.value)==null?void 0:B.input},G=()=>{setTimeout(()=>{K(),H(),He(()=>{var z;return(z=c.value)==null?void 0:z.updatePopper()})},0)},K=()=>{const z=R();if(!z)return;const B=i6e(z),W=z.getBoundingClientRect(),N=E.value.getBoundingClientRect();f.value={position:"absolute",width:0,height:`${B.height}px`,left:`${B.left+W.left-N.left}px`,top:`${B.top+W.top-N.top}px`}},H=()=>{const z=R();if(document.activeElement!==z){v.value=!1;return}const{prefix:B,split:W}=a;if(h.value=o6e(z,B,W),h.value&&h.value.splitIndex===-1){v.value=!0,r("search",h.value.pattern,h.value.prefix);return}v.value=!1};return t({input:u,tooltip:c,dropdownVisible:S}),(z,B)=>(x(),O("div",{ref_key:"wrapperRef",ref:E,class:U(p(i).b())},[L(p(Ba),Tt(Tt(o.value,z.$attrs),{ref_key:"elInputRef",ref:u,"model-value":e.modelValue,disabled:p(l),role:S.value?"combobox":void 0,"aria-activedescendant":S.value?k.value||"":void 0,"aria-controls":S.value?p(s):void 0,"aria-expanded":S.value||void 0,"aria-label":e.ariaLabel,"aria-autocomplete":S.value?"none":void 0,"aria-haspopup":S.value?"listbox":void 0,onInput:T,onKeydown:M,onMousedown:A}),xr({_:2},[dt(z.$slots,(W,N)=>({name:N,fn:$(V=>[ce(z.$slots,N,bl(wl(V)))])}))]),1040,["model-value","disabled","role","aria-activedescendant","aria-controls","aria-expanded","aria-label","aria-autocomplete","aria-haspopup"]),L(p(Ma),{ref_key:"tooltipRef",ref:c,visible:S.value,"popper-class":[p(i).e("popper"),e.popperClass],"popper-style":e.popperStyle,"popper-options":e.popperOptions,placement:m.value,"fallback-placements":g.value,effect:"light",pure:"",offset:e.offset,"show-arrow":e.showArrow},{default:$(()=>[C("div",{style:et(f.value)},null,4)]),content:$(()=>[L(v6e,{ref_key:"dropdownRef",ref:d,options:w.value,disabled:p(l),loading:e.loading,"content-id":p(s),"aria-label":e.ariaLabel,onSelect:P,onClick:B[0]||(B[0]=at(W=>{var N;return(N=u.value)==null?void 0:N.focus()},["stop"]))},xr({_:2},[dt(z.$slots,(W,N)=>({name:N,fn:$(V=>[ce(z.$slots,N,bl(wl(V)))])}))]),1032,["options","disabled","loading","content-id","aria-label"])]),_:3},8,["visible","popper-class","popper-style","popper-options","placement","fallback-placements","offset","show-arrow"])],2))}}),p6e=h6e;const m6e=wt(p6e),g6e=$e({layout:{type:String,default:"horizontal",values:["horizontal","vertical"]},lazy:Boolean}),y6e={resizeStart:(e,t)=>!0,resize:(e,t)=>!0,resizeEnd:(e,t)=>!0,collapse:(e,t,n)=>!0},b6e=$e({min:{type:[String,Number]},max:{type:[String,Number]},size:{type:[String,Number]},resizable:{type:Boolean,default:!0},collapsible:Boolean}),_6e={"update:size":e=>typeof e=="number"||typeof e=="string"};function w6e(e){const t=F(),{width:n,height:a}=Um(t);return{containerEl:t,containerSize:D(()=>e.value==="horizontal"?n.value:a.value)}}function Uw(e){return Number(e.slice(0,-1))/100}function Gw(e){return Number(e.slice(0,-2))}function Yw(e){return Ye(e)&&e.endsWith("%")}function jw(e){return Ye(e)&&e.endsWith("px")}function S6e(e,t){const n=D(()=>e.value.map(i=>i.size)),a=D(()=>e.value.length),r=F([]);ye([n,a,t],()=>{var u;let i=[],l=0;for(let c=0;cc+(d||0),0);if(s>1||!l){const c=1/s;i=i.map(d=>d===void 0?0:d*c)}else{const c=(1-s)/l;i=i.map(d=>d===void 0?c:d)}r.value=i});const o=i=>i*t.value;return{percentSizes:r,pxSizes:D(()=>r.value.map(o))}}function C6e(e,t,n,a){const r=g=>g*t.value||0;function o(g,y){return Yw(g)?r(Uw(g)):jw(g)?Gw(g):g??y}const i=F(0),l=F(null);let s=[],u=kt;const c=D(()=>e.value.map(g=>[g.min,g.max]));ye(a,()=>{if(i.value){const g=new MouseEvent("mouseup",{bubbles:!0});window.dispatchEvent(g)}});const d=g=>{i.value=0,l.value={index:g,confirmed:!1},s=n.value},v=(g,y)=>{var I;let b=null;if((!l.value||!l.value.confirmed)&&y!==0){if(y>0)b=g,l.value={index:g,confirmed:!0};else for(let P=g;P>=0;P-=1)if(s[P]>0){b=P,l.value={index:P,confirmed:!0};break}}const _=b??((I=l.value)==null?void 0:I.index)??g,w=[...s],S=_+1,k=o(c.value[_][0],0),T=o(c.value[S][0],0),M=o(c.value[_][1],t.value||0),E=o(c.value[S][1],t.value||0);let A=y;w[_]+AM&&(A=M-w[_]),w[S]-A>E&&(A=w[S]-E),w[_]+=A,w[S]-=A,i.value=A,u=()=>{e.value.forEach((P,R)=>{P.size=w[R]}),u=kt},a.value||u()},f=()=>{a.value&&u(),i.value=0,l.value=null,s=[]},h=[];return{lazyOffset:i,onMoveStart:d,onMoving:v,onMoveEnd:f,movingIndex:l,onCollapse:(g,y)=>{h.length||h.push(...n.value.map((T,M)=>{var E;return T<=0?o((E=c.value[M])==null?void 0:E[0],0):T}));const b=n.value,_=y==="start"?g:g+1,w=y==="start"?g+1:g,S=b[_],k=b[w];if(S!==0&&k!==0)b[_]=0,b[w]+=S,h[g]=S;else{const T=S+k,M=Br(h[g],0,T),E=T-M;b[w]=M,b[_]=E}e.value.forEach((T,M)=>{T.size=b[M]})}}}const bO=Symbol("splitterRootContextKey");var x6e=q({name:"ElSplitter",__name:"splitter",props:g6e,emits:y6e,setup(e,{emit:t}){const n=ke("splitter"),a=t,r=e,o=cn(r,"layout"),i=cn(r,"lazy"),{containerEl:l,containerSize:s}=w6e(o),{removeChild:u,children:c,addChild:d,ChildrenSorter:v}=z1(Et(),"ElSplitterPanel");ye(c,()=>{g.value=null,c.value.forEach((A,I)=>{A.setIndex(I)})});const{percentSizes:f,pxSizes:h}=S6e(c,s),{lazyOffset:m,movingIndex:g,onMoveStart:y,onMoving:b,onMoveEnd:_,onCollapse:w}=C6e(c,s,h,i),S=D(()=>({[n.cssVarBlockName("bar-offset")]:i.value?`${m.value}px`:void 0}));return Ft(bO,nn({panels:c,percentSizes:f,pxSizes:h,layout:o,lazy:i,movingIndex:g,containerSize:s,onMoveStart:A=>{y(A),a("resizeStart",A,h.value)},onMoving:(A,I)=>{b(A,I),i.value||a("resize",A,h.value)},onMoveEnd:async A=>{_(),await He(),a("resizeEnd",A,h.value)},onCollapse:(A,I)=>{w(A,I),a("collapse",A,I,h.value)},registerPanel:d,unregisterPanel:u})),(A,I)=>(x(),O("div",{ref_key:"containerEl",ref:l,class:U([p(n).b(),p(n).e(o.value)]),style:et(S.value)},[ce(A.$slots,"default"),L(p(v)),te(" Prevent iframe touch events from breaking "),p(g)?(x(),O("div",{key:0,class:U([p(n).e("mask"),p(n).e(`mask-${o.value}`)])},null,2)):te("v-if",!0)],6))}}),k6e=x6e;function T6e(e){return e&&_t(e)?e:{start:!!e,end:!!e}}function nk(e,t,n,a){return!!(e!=null&&e.collapsible.end&&t>0||n!=null&&n.collapsible.start&&a===0&&t>0)}var M6e=q({name:"ElSplitterBar",__name:"split-bar",props:{index:{type:Number,required:!0},layout:{type:String,values:["horizontal","vertical"],default:"horizontal"},resizable:{type:Boolean,default:!0},lazy:Boolean,startCollapsible:Boolean,endCollapsible:Boolean},emits:["moveStart","moving","moveEnd","collapse"],setup(e,{emit:t}){const n=ke("splitter-bar"),a=e,r=t,o=D(()=>a.layout==="horizontal"),i=D(()=>o.value?{width:0}:{height:0}),l=D(()=>({width:o.value?"16px":"100%",height:o.value?"100%":"16px",cursor:a.resizable?o.value?"ew-resize":"ns-resize":"auto",touchAction:"none"})),s=D(()=>{const b=n.e("dragger");return{[`${b}-horizontal`]:o.value,[`${b}-vertical`]:!o.value,[`${b}-active`]:!!u.value}}),u=F(null),c=b=>{a.resizable&&(u.value=[b.pageX,b.pageY],r("moveStart",a.index),window.addEventListener("mouseup",h),window.addEventListener("mousemove",v))},d=b=>{if(a.resizable&&b.touches.length===1){b.preventDefault();const _=b.touches[0];u.value=[_.pageX,_.pageY],r("moveStart",a.index),window.addEventListener("touchend",m),window.addEventListener("touchmove",f)}},v=b=>{const{pageX:_,pageY:w}=b,S=_-u.value[0],k=w-u.value[1],T=o.value?S:k;r("moving",a.index,T)},f=b=>{if(b.touches.length===1){b.preventDefault();const _=b.touches[0],w=_.pageX-u.value[0],S=_.pageY-u.value[1],k=o.value?w:S;r("moving",a.index,k)}},h=()=>{u.value=null,window.removeEventListener("mouseup",h),window.removeEventListener("mousemove",v),r("moveEnd",a.index)},m=()=>{u.value=null,window.removeEventListener("touchend",m),window.removeEventListener("touchmove",f),r("moveEnd",a.index)},g=D(()=>o.value?Ji:ip),y=D(()=>o.value?za:ui);return(b,_)=>(x(),O("div",{class:U([p(n).b()]),style:et(i.value)},[e.startCollapsible?(x(),O("div",{key:0,class:U([p(n).e("collapse-icon"),p(n).e(`${e.layout}-collapse-icon-start`)]),onClick:_[0]||(_[0]=w=>r("collapse",e.index,"start"))},[ce(b.$slots,"start-collapsible",{},()=>[(x(),ne(St(g.value),{style:{width:"12px",height:"12px"}}))])],2)):te("v-if",!0),C("div",{class:U([p(n).e("dragger"),s.value,p(n).is("disabled",!e.resizable),p(n).is("lazy",e.resizable&&e.lazy)]),style:et(l.value),onMousedown:c,onTouchstart:d},null,38),e.endCollapsible?(x(),O("div",{key:1,class:U([p(n).e("collapse-icon"),p(n).e(`${e.layout}-collapse-icon-end`)]),onClick:_[1]||(_[1]=w=>r("collapse",e.index,"end"))},[ce(b.$slots,"end-collapsible",{},()=>[(x(),ne(St(y.value),{style:{width:"12px",height:"12px"}}))])],2)):te("v-if",!0)],6))}}),E6e=M6e;const ak="ElSplitterPanel";var A6e=q({name:ak,__name:"split-panel",props:b6e,emits:_6e,setup(e,{expose:t,emit:n}){const a=ke("splitter-panel"),r=e,o=n,i=Ze(bO);i||Dn(ak,"usage: ");const{panels:l,layout:s,lazy:u,containerSize:c,pxSizes:d}=Ta(i),{registerPanel:v,unregisterPanel:f,onCollapse:h,onMoveEnd:m,onMoveStart:g,onMoving:y}=i,b=F(),_=Et(),w=_.uid,S=F(0),k=D(()=>l.value[S.value]),T=B=>{S.value=B},M=D(()=>k.value?d.value[S.value]??0:0),E=D(()=>k.value?d.value[S.value+1]??0:0),A=D(()=>k.value?l.value[S.value+1]:null),I=D(()=>{var B;return A.value?r.resizable&&((B=A.value)==null?void 0:B.resizable)&&(M.value!==0||!r.min)&&(E.value!==0||!A.value.min):!1}),P=D(()=>k.value?S.value!==l.value.length-1:!1),R=D(()=>nk(k.value,M.value,A.value,E.value)),G=D(()=>nk(A.value,E.value,k.value,M.value));function K(B){return Yw(B)?Uw(B)*c.value||0:jw(B)?Gw(B):B??0}let H=!1;ye(()=>r.size,()=>{if(!H&&k.value){if(!c.value){k.value.size=r.size;return}const B=K(r.size),W=K(r.max),N=K(r.min),V=Math.min(Math.max(B,N||0),W||B);V!==B&&o("update:size",V),k.value.size=V}}),ye(()=>{var B;return(B=k.value)==null?void 0:B.size},B=>{B!==r.size&&(H=!0,o("update:size",B),He(()=>H=!1))}),ye(()=>r.resizable,B=>{k.value&&(k.value.resizable=B)});const z=nn({uid:w,getVnode:()=>_.vnode,setIndex:T,...r,collapsible:D(()=>T6e(r.collapsible))});return v(z),rn(()=>f(z)),t({splitterPanelRef:b}),(B,W)=>(x(),O(Ve,null,[C("div",Tt({ref_key:"panelEl",ref:b,class:[p(a).b()],style:{flexBasis:`${M.value}px`}},B.$attrs),[ce(B.$slots,"default")],16),P.value?(x(),ne(E6e,{key:0,index:S.value,layout:p(s),lazy:p(u),resizable:I.value,"start-collapsible":R.value,"end-collapsible":G.value,onMoveStart:p(g),onMoving:p(y),onMoveEnd:p(m),onCollapse:p(h)},{"start-collapsible":$(()=>[ce(B.$slots,"start-collapsible")]),"end-collapsible":$(()=>[ce(B.$slots,"end-collapsible")]),_:3},8,["index","layout","lazy","resizable","start-collapsible","end-collapsible","onMoveStart","onMoving","onMoveEnd","onCollapse"])):te("v-if",!0)],64))}}),_O=A6e;const I6e=wt(k6e,{SplitPanel:_O}),L6e=On(_O),jo="ElInfiniteScroll",P6e={delay:{type:Number,default:200},distance:{type:Number,default:0},disabled:{type:Boolean,default:!1},immediate:{type:Boolean,default:!0}},qw=(e,t)=>Object.entries(P6e).reduce((n,[a,r])=>{const{type:o,default:i}=r,l=e.getAttribute(`infinite-scroll-${a}`);let s=t[l]??l??i;return s=s==="false"?!1:s,s=o(s),n[a]=Number.isNaN(s)?i:s,n},{}),wO=e=>{const{observer:t}=e[jo];t&&(t.disconnect(),delete e[jo].observer)},D6e=(e,t)=>{const{container:n,containerEl:a,instance:r,observer:o,lastScrollTop:i}=e[jo],{disabled:l,distance:s}=qw(e,r),{clientHeight:u,scrollHeight:c,scrollTop:d}=a,v=d-i;if(e[jo].lastScrollTop=d,o||l||v<0)return;let f=!1;if(n===e)f=c-(u+d)<=s;else{const{clientTop:h,scrollHeight:m}=e,g=q4(e,a);f=d+u>=g+h+m-s}f&&t.call(r)};function my(e,t){const{containerEl:n,instance:a}=e[jo],{disabled:r}=qw(e,a);r||n.clientHeight===0||(n.scrollHeight<=n.clientHeight?t.call(a):wO(e))}const O6e={async mounted(e,t){const{instance:n,value:a}=t;Ri({scope:jo,from:"the directive v-infinite-scroll",replacement:"the el-scrollbar infinite scroll",version:"3.0.0",ref:"https://element-plus.org/en-US/component/scrollbar#infinite-scroll"},!0),Qe(a)||Dn(jo,"'v-infinite-scroll' binding value must be a function"),await He();const{delay:r,immediate:o}=qw(e,n),i=x6(e,!0),l=i===window?document.documentElement:i,s=hs(D6e.bind(null,e,a),r);if(i){if(e[jo]={instance:n,container:i,containerEl:l,delay:r,cb:a,onScroll:s,lastScrollTop:l.scrollTop},o){const u=new MutationObserver(hs(my.bind(null,e,a),50));e[jo].observer=u,u.observe(e,{childList:!0,subtree:!0}),my(e,a)}i.addEventListener("scroll",s)}},unmounted(e){if(!e.ElInfiniteScroll)return;const{container:t,onScroll:n}=e[jo];t==null||t.removeEventListener("scroll",n),wO(e)},async updated(e){if(!e.ElInfiniteScroll)await He();else{const{containerEl:t,cb:n,observer:a}=e[jo];t.clientHeight&&a&&my(e,n)}}},c_=O6e;c_.install=e=>{e.directive("InfiniteScroll",c_)};const $6e=c_;function R6e(e,t){let n;const a=F(!1),r=nn({...e,originalPosition:"",originalOverflow:"",visible:!1});function o(v){r.text=v}function i(){const v=r.parent,f=d.ns;if(!v.vLoadingAddClassList){let h=v.getAttribute("loading-number");h=Number.parseInt(h)-1,h?v.setAttribute("loading-number",h.toString()):(ir(v,f.bm("parent","relative")),v.removeAttribute("loading-number")),ir(v,f.bm("parent","hidden"))}l(),c.unmount()}function l(){var v,f;(f=(v=d.$el)==null?void 0:v.parentNode)==null||f.removeChild(d.$el)}function s(){var v;e.beforeClose&&!e.beforeClose()||(a.value=!0,clearTimeout(n),n=setTimeout(u,400),r.visible=!1,(v=e.closed)==null||v.call(e))}function u(){if(!a.value)return;const v=r.parent;a.value=!1,v.vLoadingAddClassList=void 0,i()}const c=V7(q({name:"ElLoading",setup(v,{expose:f}){const{ns:h,zIndex:m}=W1("loading");return f({ns:h,zIndex:m}),()=>{const g=r.spinner||r.svg,y=ot("svg",{class:"circular",viewBox:r.svgViewBox?r.svgViewBox:"0 0 50 50",...g?{innerHTML:g}:{}},g?void 0:[ot("circle",{class:"path",cx:"25",cy:"25",r:"20",fill:"none"})]),b=r.text?ot("p",{class:h.b("text")},[r.text]):void 0;return ot(Pa,{name:h.b("fade"),onAfterLeave:u},{default:$(()=>[st(L("div",{style:{backgroundColor:r.background||""},class:[h.b("mask"),r.customClass,h.is("fullscreen",r.fullscreen)]},[ot("div",{class:h.b("spinner")},[y,b])]),[[Zt,r.visible]])])})}}}));Object.assign(c._context,t??{});const d=c.mount(document.createElement("div"));return{...Ta(r),setText:o,removeElLoadingChild:l,close:s,handleAfterLeave:u,vm:d,get $el(){return d.$el}}}let Xp;const Cd=function(e={},t){if(!qt)return;const n=N6e(e);if(n.fullscreen&&Xp)return Xp;const a=R6e({...n,closed:()=>{var o;(o=n.closed)==null||o.call(n),n.fullscreen&&(Xp=void 0)}},t??Cd._context);B6e(n,n.parent,a),rk(n,n.parent,a),n.parent.vLoadingAddClassList=()=>rk(n,n.parent,a);let r=n.parent.getAttribute("loading-number");return r?r=`${Number.parseInt(r)+1}`:r="1",n.parent.setAttribute("loading-number",r),n.parent.appendChild(a.$el),He(()=>a.visible.value=n.visible),n.fullscreen&&(Xp=a),a},N6e=e=>{let t;return Ye(e.target)?t=document.querySelector(e.target)??document.body:t=e.target||document.body,{parent:t===document.body||e.body?document.body:t,background:e.background||"",svg:e.svg||"",svgViewBox:e.svgViewBox||"",spinner:e.spinner||!1,text:e.text||"",fullscreen:t===document.body&&(e.fullscreen??!0),lock:e.lock??!1,customClass:e.customClass||"",visible:e.visible??!0,beforeClose:e.beforeClose,closed:e.closed,target:t}},B6e=async(e,t,n)=>{const{nextZIndex:a}=n.vm.zIndex||n.vm._.exposed.zIndex,r={};if(e.fullscreen)n.originalPosition.value=ml(document.body,"position"),n.originalOverflow.value=ml(document.body,"overflow"),r.zIndex=a();else if(e.parent===document.body){n.originalPosition.value=ml(document.body,"position"),await He();for(const o of["top","left"]){const i=o==="top"?"scrollTop":"scrollLeft";r[o]=`${e.target.getBoundingClientRect()[o]+document.body[i]+document.documentElement[i]-Number.parseInt(ml(document.body,`margin-${o}`),10)}px`}for(const o of["height","width"])r[o]=`${e.target.getBoundingClientRect()[o]}px`}else n.originalPosition.value=ml(t,"position");for(const[o,i]of Object.entries(r))n.$el.style[o]=i},rk=(e,t,n)=>{const a=n.vm.ns||n.vm._.exposed.ns;["absolute","fixed","sticky"].includes(n.originalPosition.value)?ir(t,a.bm("parent","relative")):Eo(t,a.bm("parent","relative")),e.fullscreen&&e.lock?Eo(t,a.bm("parent","hidden")):ir(t,a.bm("parent","hidden"))};Cd._context=null;const uv=Symbol("ElLoading"),Eu=e=>`element-loading-${$l(e)}`,ok=(e,t)=>{const n=t.instance,a=u=>_t(t.value)?t.value[u]:void 0,r=u=>F(Ye(u)&&(n==null?void 0:n[u])||u),o=u=>r(a(u)||e.getAttribute(Eu(u))),i=a("fullscreen")??t.modifiers.fullscreen,l={text:o("text"),svg:o("svg"),svgViewBox:o("svgViewBox"),spinner:o("spinner"),background:o("background"),customClass:o("customClass"),fullscreen:i,target:a("target")??(i?void 0:e),body:a("body")??t.modifiers.body,lock:a("lock")??t.modifiers.lock},s=Cd(l);s._context=Dv._context,e[uv]={options:l,instance:s}},V6e=(e,t)=>{for(const n of Object.keys(e))xn(e[n])&&(e[n].value=t[n])},Dv={mounted(e,t){t.value&&ok(e,t)},updated(e,t){const n=e[uv];if(!t.value){n==null||n.instance.close(),e[uv]=null;return}n?V6e(n.options,_t(t.value)?t.value:{text:e.getAttribute(Eu("text")),svg:e.getAttribute(Eu("svg")),svgViewBox:e.getAttribute(Eu("svgViewBox")),spinner:e.getAttribute(Eu("spinner")),background:e.getAttribute(Eu("background")),customClass:e.getAttribute(Eu("customClass"))}):ok(e,t)},unmounted(e){var t;(t=e[uv])==null||t.instance.close(),e[uv]=null}};Dv._context=null;const z6e={install(e){Cd._context=e._context,Dv._context=e._context,e.directive("loading",Dv),e.config.globalProperties.$loading=Cd},directive:Dv,service:Cd},SO=["primary","success","info","warning","error"],CO=["top","top-left","top-right","bottom","bottom-left","bottom-right"],Ja=Nn({customClass:"",dangerouslyUseHTMLString:!1,duration:3e3,icon:void 0,id:"",message:"",onClose:void 0,showClose:!1,type:"info",plain:!1,offset:16,placement:void 0,zIndex:0,grouping:!1,repeatNum:1,appendTo:qt?document.body:void 0}),F6e=$e({customClass:{type:ae([String,Array,Object,Boolean]),default:Ja.customClass},dangerouslyUseHTMLString:{type:Boolean,default:Ja.dangerouslyUseHTMLString},duration:{type:Number,default:Ja.duration},icon:{type:pn,default:Ja.icon},id:{type:String,default:Ja.id},message:{type:ae([String,Object,Function]),default:Ja.message},onClose:{type:ae(Function),default:Ja.onClose},showClose:{type:Boolean,default:Ja.showClose},type:{type:String,values:SO,default:Ja.type},plain:{type:Boolean,default:Ja.plain},offset:{type:Number,default:Ja.offset},placement:{type:String,values:CO,default:Ja.placement},zIndex:{type:Number,default:Ja.zIndex},grouping:{type:Boolean,default:Ja.grouping},repeatNum:{type:Number,default:Ja.repeatNum}}),H6e={destroy:()=>!0},Jo=W3({}),W6e=e=>(Jo[e]||(Jo[e]=W3([])),Jo[e]),K6e=(e,t)=>{const n=Jo[t]||[],a=n.findIndex(i=>i.id===e),r=n[a];let o;return a>0&&(o=n[a-1]),{current:r,prev:o}},U6e=(e,t)=>{const{prev:n}=K6e(e,t);return n?n.vm.exposed.bottom.value:0},G6e=(e,t,n)=>(Jo[n]||[]).findIndex(a=>a.id===e)>0?16:t,Y6e=["id"],j6e=["innerHTML"];var q6e=q({name:"ElMessage",__name:"message",props:F6e,emits:H6e,setup(e,{expose:t,emit:n}){const{Close:a}=Y6,r=e,o=n,i=F(!1),{ns:l,zIndex:s}=W1("message"),{currentZIndex:u,nextZIndex:c}=s,d=F(),v=F(!1),f=F(0);let h;const m=D(()=>r.type?r.type==="error"?"danger":r.type:"info"),g=D(()=>{const R=r.type;return{[l.bm("icon",R)]:R&&Rs[R]}}),y=D(()=>r.icon||Rs[r.type]||""),b=D(()=>r.placement||"top"),_=D(()=>U6e(r.id,b.value)),w=D(()=>Math.max(G6e(r.id,r.offset,b.value)+_.value,r.offset)),S=D(()=>f.value+w.value),k=D(()=>b.value.includes("left")?l.is("left"):b.value.includes("right")?l.is("right"):l.is("center")),T=D(()=>b.value.startsWith("top")?"top":"bottom"),M=D(()=>({[T.value]:`${w.value}px`,zIndex:u.value}));function E(){r.duration!==0&&({stop:h}=Bd(()=>{I()},r.duration))}function A(){h==null||h()}function I(){v.value=!1,He(()=>{var R;i.value||((R=r.onClose)==null||R.call(r),o("destroy"))})}function P(R){mn(R)===Pe.esc&&I()}return mt(()=>{E(),c(),v.value=!0}),ye(()=>r.repeatNum,()=>{A(),E()}),on(document,"keydown",P),Mn(d,()=>{f.value=d.value.getBoundingClientRect().height}),t({visible:v,bottom:S,close:I}),(R,G)=>(x(),ne(Pa,{name:p(l).b("fade"),onBeforeEnter:G[0]||(G[0]=K=>i.value=!0),onBeforeLeave:e.onClose,onAfterLeave:G[1]||(G[1]=K=>R.$emit("destroy")),persisted:""},{default:$(()=>[st(C("div",{id:e.id,ref_key:"messageRef",ref:d,class:U([p(l).b(),{[p(l).m(e.type)]:e.type},p(l).is("closable",e.showClose),p(l).is("plain",e.plain),p(l).is("bottom",T.value==="bottom"),k.value,e.customClass]),style:et(M.value),role:"alert",onMouseenter:A,onMouseleave:E},[e.repeatNum>1?(x(),ne(p(dL),{key:0,value:e.repeatNum,type:m.value,class:U(p(l).e("badge"))},null,8,["value","type","class"])):te("v-if",!0),y.value?(x(),ne(p(Xe),{key:1,class:U([p(l).e("icon"),g.value])},{default:$(()=>[(x(),ne(St(y.value)))]),_:1},8,["class"])):te("v-if",!0),!e.dangerouslyUseHTMLString||R.$slots.default?(x(),O("p",{key:2,class:U(p(l).e("content"))},[ce(R.$slots,"default",{},()=>[ue(Q(e.message),1)])],2)):(x(),O(Ve,{key:3},[te(" Caution here, message could've been compromised, never use user's input as message "),C("p",{class:U(p(l).e("content")),innerHTML:e.message},null,10,j6e)],2112)),e.showClose?(x(),ne(p(Xe),{key:4,class:U(p(l).e("closeBtn")),onClick:at(I,["stop"])},{default:$(()=>[L(p(a))]),_:1},8,["class"])):te("v-if",!0)],46,Y6e),[[Zt,v.value]])]),_:3},8,["name","onBeforeLeave"]))}}),X6e=q6e;let Z6e=1;const J6e=e=>{if(!e.appendTo)e.appendTo=document.body;else if(Ye(e.appendTo)){let t=document.querySelector(e.appendTo);Vr(t)||(un("ElMessage","the appendTo option is not an HTMLElement. Falling back to document.body."),t=document.body),e.appendTo=t}},Q6e=e=>{!e.placement&&Ye(Qa.placement)&&Qa.placement&&(e.placement=Qa.placement),e.placement||(e.placement="top"),CO.includes(e.placement)||(un("ElMessage",`Invalid placement: ${e.placement}. Falling back to 'top'.`),e.placement="top")},xO=e=>{const t=!e||Ye(e)||bn(e)||Qe(e)?{message:e}:e,n={...Ja,...t};return J6e(n),Q6e(n),vn(Qa.grouping)&&!n.grouping&&(n.grouping=Qa.grouping),Je(Qa.duration)&&n.duration===3e3&&(n.duration=Qa.duration),Je(Qa.offset)&&n.offset===16&&(n.offset=Qa.offset),vn(Qa.showClose)&&!n.showClose&&(n.showClose=Qa.showClose),vn(Qa.plain)&&!n.plain&&(n.plain=Qa.plain),n},ewe=e=>{const t=Jo[e.props.placement||"top"],n=t.indexOf(e);if(n===-1)return;t.splice(n,1);const{handler:a}=e;a.close()},twe=({appendTo:e,...t},n)=>{const a=`message_${Z6e++}`,r=t.onClose,o=document.createElement("div"),i={...t,id:a,onClose:()=>{r==null||r(),ewe(u)},onDestroy:()=>{Ps(null,o)}},l=L(X6e,i,Qe(i.message)||bn(i.message)?{default:Qe(i.message)?i.message:()=>i.message}:null);l.appContext=n||hc._context,Ps(l,o),e.appendChild(o.firstElementChild);const s=l.component,u={id:a,vnode:l,vm:s,handler:{close:()=>{s.exposed.close()}},props:l.component.props};return u},hc=(e={},t)=>{if(!qt)return{close:()=>{}};const n=xO(e),a=W6e(n.placement||"top");if(n.grouping&&a.length){const o=a.find(({vnode:i})=>{var l;return((l=i.props)==null?void 0:l.message)===n.message});if(o)return o.props.repeatNum+=1,o.props.type=n.type,o.handler}if(Je(Qa.max)&&a.length>=Qa.max)return{close:()=>{}};const r=twe(n,t);return a.push(r),r.handler};SO.forEach(e=>{hc[e]=(t={},n)=>hc({...xO(t),type:e},n)});function nwe(e){for(const t in Jo)if(en(Jo,t)){const n=[...Jo[t]];for(const a of n)(!e||e===a.props.type)&&a.handler.close()}}function awe(e){Jo[e]&&[...Jo[e]].forEach(t=>t.handler.close())}hc.closeAll=nwe;hc.closeAllByPlacement=awe;hc._context=null;const We=SI(hc,"$message");var rwe=q({name:"ElMessageBox",directives:{TrapFocus:Oue},components:{ElButton:xa,ElFocusTrap:pf,ElInput:Ba,ElOverlay:bw,ElIcon:Xe,...Y6},inheritAttrs:!1,props:{buttonSize:{type:String,validator:SD},modal:{type:Boolean,default:!0},lockScroll:{type:Boolean,default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},closeOnHashChange:{type:Boolean,default:!0},center:Boolean,draggable:Boolean,overflow:Boolean,roundButton:Boolean,container:{type:String,default:"body"},boxType:{type:String,default:""}},emits:["vanish","action"],setup(e,{emit:t}){const{locale:n,zIndex:a,ns:r,size:o}=W1("message-box",D(()=>e.buttonSize)),{t:i}=n,{nextZIndex:l}=a,s=F(!1),u=nn({autofocus:!0,beforeClose:null,callback:null,cancelButtonText:"",cancelButtonClass:"",confirmButtonText:"",confirmButtonClass:"",cancelButtonType:"",confirmButtonType:"primary",customClass:"",customStyle:{},dangerouslyUseHTMLString:!1,distinguishCancelAndClose:!1,icon:"",closeIcon:"",inputPattern:null,inputPlaceholder:"",inputType:"text",inputValue:"",inputValidator:void 0,inputErrorMessage:"",message:"",modalFade:!0,modalClass:"",showCancelButton:!1,showConfirmButton:!0,type:"",title:void 0,showInput:!1,action:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonLoadingIcon:$i(Mr),cancelButtonLoadingIcon:$i(Mr),confirmButtonDisabled:!1,editorErrorMessage:"",validateError:!1,zIndex:l()}),c=D(()=>{const K=u.type;return{[r.bm("icon",K)]:K&&Rs[K]}}),d=Fa(),v=Fa(),f=D(()=>{const K=u.type;return u.icon||K&&Rs[K]||""}),h=D(()=>!!u.message),m=F(),g=F(),y=F(),b=F(),_=F(),w=D(()=>u.confirmButtonClass);ye(()=>u.inputValue,async K=>{await He(),e.boxType==="prompt"&&K&&I()},{immediate:!0}),ye(()=>s.value,K=>{var H;K&&(e.boxType!=="prompt"&&(u.autofocus?y.value=((H=_.value)==null?void 0:H.$el)??m.value:y.value=m.value),u.zIndex=l()),e.boxType==="prompt"&&(K?He().then(()=>{b.value&&b.value.$el&&(u.autofocus?y.value=P()??m.value:y.value=m.value)}):(u.editorErrorMessage="",u.validateError=!1))});const{isDragging:S}=BA(m,g,D(()=>e.draggable),D(()=>e.overflow));mt(async()=>{await He(),e.closeOnHashChange&&window.addEventListener("hashchange",k)}),rn(()=>{e.closeOnHashChange&&window.removeEventListener("hashchange",k)});function k(){s.value&&(s.value=!1,He(()=>{u.action&&t("action",u.action)}))}const T=()=>{e.closeOnClickModal&&A(u.distinguishCancelAndClose?"close":"cancel")},M=O6(T),E=K=>{var H;if(u.inputType!=="textarea"&&!((H=b.value)!=null&&H.isComposing))return K.preventDefault(),A("confirm")},A=K=>{var H;e.boxType==="prompt"&&K==="confirm"&&!I()||(u.action=K,u.beforeClose?(H=u.beforeClose)==null||H.call(u,K,u,k):k())},I=()=>{if(e.boxType==="prompt"){const K=u.inputPattern;if(K&&!K.test(u.inputValue||""))return u.editorErrorMessage=u.inputErrorMessage||i("el.messagebox.error"),u.validateError=!0,!1;const H=u.inputValidator;if(Qe(H)){const z=H(u.inputValue);if(z===!1)return u.editorErrorMessage=u.inputErrorMessage||i("el.messagebox.error"),u.validateError=!0,!1;if(Ye(z))return u.editorErrorMessage=z,u.validateError=!0,!1}}return u.editorErrorMessage="",u.validateError=!1,!0},P=()=>{var H;const K=(H=b.value)==null?void 0:H.$refs;return(K==null?void 0:K.input)??(K==null?void 0:K.textarea)},R=()=>{A("close")},G=()=>{e.closeOnPressEscape&&R()};return e.lockScroll&&D1(s,{ns:r}),{...Ta(u),ns:r,overlayEvent:M,visible:s,hasMessage:h,typeClass:c,contentId:d,inputId:v,btnSize:o,iconComponent:f,confirmButtonClasses:w,rootRef:m,focusStartRef:y,headerRef:g,inputRef:b,isDragging:S,confirmRef:_,doClose:k,handleClose:R,onCloseRequested:G,handleWrapperClick:T,handleInputEnter:E,handleAction:A,t:i}}});const owe=["aria-label","aria-describedby"],iwe=["aria-label"],lwe=["id"];function swe(e,t,n,a,r,o){const i=be("el-icon"),l=be("el-input"),s=be("el-button"),u=be("el-focus-trap"),c=be("el-overlay");return x(),ne(Pa,{name:"fade-in-linear",onAfterLeave:t[11]||(t[11]=d=>e.$emit("vanish")),persisted:""},{default:$(()=>[st(L(c,{"z-index":e.zIndex,"overlay-class":[e.ns.is("message-box"),e.modalClass],mask:e.modal},{default:$(()=>[C("div",{role:"dialog","aria-label":e.title,"aria-modal":"true","aria-describedby":e.showInput?void 0:e.contentId,class:U(`${e.ns.namespace.value}-overlay-message-box`),onClick:t[8]||(t[8]=(...d)=>e.overlayEvent.onClick&&e.overlayEvent.onClick(...d)),onMousedown:t[9]||(t[9]=(...d)=>e.overlayEvent.onMousedown&&e.overlayEvent.onMousedown(...d)),onMouseup:t[10]||(t[10]=(...d)=>e.overlayEvent.onMouseup&&e.overlayEvent.onMouseup(...d))},[L(u,{loop:"",trapped:e.visible,"focus-trap-el":e.rootRef,"focus-start-el":e.focusStartRef,onReleaseRequested:e.onCloseRequested},{default:$(()=>[C("div",{ref:"rootRef",class:U([e.ns.b(),e.customClass,e.ns.is("draggable",e.draggable),e.ns.is("dragging",e.isDragging),{[e.ns.m("center")]:e.center}]),style:et(e.customStyle),tabindex:"-1",onClick:t[7]||(t[7]=at(()=>{},["stop"]))},[e.title!==null&&e.title!==void 0?(x(),O("div",{key:0,ref:"headerRef",class:U([e.ns.e("header"),{"show-close":e.showClose}])},[C("div",{class:U(e.ns.e("title"))},[e.iconComponent&&e.center?(x(),ne(i,{key:0,class:U([e.ns.e("status"),e.typeClass])},{default:$(()=>[(x(),ne(St(e.iconComponent)))]),_:1},8,["class"])):te("v-if",!0),C("span",null,Q(e.title),1)],2),e.showClose?(x(),O("button",{key:0,type:"button",class:U(e.ns.e("headerbtn")),"aria-label":e.t("el.messagebox.close"),onClick:t[0]||(t[0]=d=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel")),onKeydown:t[1]||(t[1]=kn(at(d=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel"),["prevent"]),["enter"]))},[L(i,{class:U(e.ns.e("close"))},{default:$(()=>[(x(),ne(St(e.closeIcon||"close")))]),_:1},8,["class"])],42,iwe)):te("v-if",!0)],2)):te("v-if",!0),C("div",{id:e.contentId,class:U(e.ns.e("content"))},[C("div",{class:U(e.ns.e("container"))},[e.iconComponent&&!e.center&&e.hasMessage?(x(),ne(i,{key:0,class:U([e.ns.e("status"),e.typeClass])},{default:$(()=>[(x(),ne(St(e.iconComponent)))]),_:1},8,["class"])):te("v-if",!0),e.hasMessage?(x(),O("div",{key:1,class:U(e.ns.e("message"))},[ce(e.$slots,"default",{},()=>[e.dangerouslyUseHTMLString?(x(),ne(St(e.showInput?"label":"p"),{key:1,for:e.showInput?e.inputId:void 0,innerHTML:e.message},null,8,["for","innerHTML"])):(x(),ne(St(e.showInput?"label":"p"),{key:0,for:e.showInput?e.inputId:void 0,textContent:Q(e.message)},null,8,["for","textContent"]))])],2)):te("v-if",!0)],2),st(C("div",{class:U(e.ns.e("input"))},[L(l,{id:e.inputId,ref:"inputRef",modelValue:e.inputValue,"onUpdate:modelValue":t[2]||(t[2]=d=>e.inputValue=d),type:e.inputType,placeholder:e.inputPlaceholder,"aria-invalid":e.validateError,class:U({invalid:e.validateError}),onKeydown:kn(e.handleInputEnter,["enter"])},null,8,["id","modelValue","type","placeholder","aria-invalid","class","onKeydown"]),C("div",{class:U(e.ns.e("errormsg")),style:et({visibility:e.editorErrorMessage?"visible":"hidden"})},Q(e.editorErrorMessage),7)],2),[[Zt,e.showInput]])],10,lwe),e.showCancelButton||e.showConfirmButton?(x(),O("div",{key:1,class:U(e.ns.e("btns"))},[e.showCancelButton?(x(),ne(s,{key:0,type:e.cancelButtonType==="text"?"":e.cancelButtonType,text:e.cancelButtonType==="text",loading:e.cancelButtonLoading,"loading-icon":e.cancelButtonLoadingIcon,class:U([e.cancelButtonClass]),round:e.roundButton,size:e.btnSize,onClick:t[3]||(t[3]=d=>e.handleAction("cancel")),onKeydown:t[4]||(t[4]=kn(at(d=>e.handleAction("cancel"),["prevent"]),["enter"]))},{default:$(()=>[ue(Q(e.cancelButtonText||e.t("el.messagebox.cancel")),1)]),_:1},8,["type","text","loading","loading-icon","class","round","size"])):te("v-if",!0),st(L(s,{ref:"confirmRef",type:e.confirmButtonType==="text"?"":e.confirmButtonType,text:e.confirmButtonType==="text",loading:e.confirmButtonLoading,"loading-icon":e.confirmButtonLoadingIcon,class:U([e.confirmButtonClasses]),round:e.roundButton,disabled:e.confirmButtonDisabled,size:e.btnSize,onClick:t[5]||(t[5]=d=>e.handleAction("confirm")),onKeydown:t[6]||(t[6]=kn(at(d=>e.handleAction("confirm"),["prevent"]),["enter"]))},{default:$(()=>[ue(Q(e.confirmButtonText||e.t("el.messagebox.confirm")),1)]),_:1},8,["type","text","loading","loading-icon","class","round","disabled","size"]),[[Zt,e.showConfirmButton]])],2)):te("v-if",!0)],6)]),_:3},8,["trapped","focus-trap-el","focus-start-el","onReleaseRequested"])],42,owe)]),_:3},8,["z-index","overlay-class","mask"]),[[Zt,e.visible]])]),_:3})}var uwe=ya(rwe,[["render",swe]]);const Ch=new Map,cwe=e=>{let t=document.body;return e.appendTo&&(Ye(e.appendTo)&&(t=document.querySelector(e.appendTo)),Vr(e.appendTo)&&(t=e.appendTo),Vr(t)||(un("ElMessageBox","the appendTo option is not an HTMLElement. Falling back to document.body."),t=document.body)),t},gy=(e,t)=>{var a;const n=(a=e.component)==null?void 0:a.proxy;return()=>n.handleAction(t)},dwe=(e,t,n=null)=>{const a=L(uwe,e,Qe(e.message)||bn(e.message)?{default:Qe(e.message)?()=>e.message({confirm:gy(a,"confirm"),cancel:gy(a,"cancel"),close:gy(a,"close")}):()=>e.message}:null);return a.appContext=n,Ps(a,t),cwe(e).appendChild(t.firstElementChild),a.component},fwe=()=>document.createElement("div"),vwe=(e,t)=>{const n=fwe();e.onVanish=()=>{Ps(null,n),Ch.delete(r)},e.onAction=o=>{const i=Ch.get(r);let l;e.showInput?l={value:r.inputValue,action:o}:l=o,e.callback?e.callback(l,a.proxy):o==="cancel"||o==="close"?e.distinguishCancelAndClose&&o!=="cancel"?i.reject("close"):i.reject("cancel"):i.resolve(l)};const a=dwe(e,n,t),r=a.proxy;for(const o in e)en(e,o)&&!en(r.$props,o)&&(o==="closeIcon"&&_t(e[o])?r[o]=$i(e[o]):r[o]=e[o]);return r.visible=!0,r};function gf(e,t=null){if(!qt)return Promise.reject();let n;return Ye(e)||bn(e)?e={message:e}:n=e.callback,new Promise((a,r)=>{const o=vwe(e,t??gf._context);Ch.set(o,{options:e,callback:n,resolve:a,reject:r})})}const hwe=["alert","confirm","prompt"],pwe={alert:{closeOnPressEscape:!1,closeOnClickModal:!1},confirm:{showCancelButton:!0},prompt:{showCancelButton:!0,showInput:!0}};hwe.forEach(e=>{gf[e]=mwe(e)});function mwe(e){return(t,n,a,r)=>{let o="";return _t(n)?(a=n,o=""):zt(n)?o="":o=n,gf(Object.assign({title:o,message:t,type:"",...pwe[e]},a,{boxType:e}),r)}}gf.close=()=>{Ch.forEach((e,t)=>{t.doClose()}),Ch.clear()};gf._context=null;const ss=gf;ss.install=e=>{ss._context=e._context,e.config.globalProperties.$msgbox=ss,e.config.globalProperties.$messageBox=ss,e.config.globalProperties.$alert=ss.alert,e.config.globalProperties.$confirm=ss.confirm,e.config.globalProperties.$prompt=ss.prompt};const er=ss,kO=["primary","success","info","warning","error"],gwe=$e({customClass:{type:ae([String,Array,Object,Boolean]),default:""},dangerouslyUseHTMLString:Boolean,duration:{type:Number,default:4500},icon:{type:pn},id:{type:String,default:""},message:{type:ae([String,Object,Function]),default:""},offset:{type:Number,default:0},onClick:{type:ae(Function),default:()=>{}},onClose:{type:ae(Function),required:!0},position:{type:String,values:["top-right","top-left","bottom-right","bottom-left"],default:"top-right"},showClose:{type:Boolean,default:!0},title:{type:String,default:""},type:{type:String,values:[...kO,""],default:""},zIndex:Number,closeIcon:{type:pn,default:so},progress:{type:ae([Boolean,Object]),default:!1},pauseOnHover:{type:Boolean,default:!0}}),ywe={destroy:()=>!0},bwe=["id"],_we=["textContent"],wwe={key:0},Swe=["innerHTML"];var Cwe=q({name:"ElNotification",__name:"notification",props:gwe,emits:ywe,setup(e,{expose:t}){const n=e,{ns:a,zIndex:r}=W1("notification"),{nextZIndex:o,currentZIndex:i}=r,l=F(!1);let s;const u=F(100);let c=0,d=0;const v=D(()=>{const R=n.type;return R&&Rs[n.type]?a.m(R):""}),f=D(()=>!!n.progress&&n.duration>0),h=D(()=>{switch(n.type){case"success":return"success";case"warning":return"warning";case"error":return"exception";default:return}}),m=D(()=>({status:h.value,strokeWidth:3,showText:!1,...typeof n.progress=="object"?n.progress:void 0})),g=D(()=>n.type&&Rs[n.type]||n.icon),y=D(()=>n.position.endsWith("right")?"right":"left"),b=D(()=>n.position.startsWith("top")?"top":"bottom"),_=D(()=>({[b.value]:`${n.offset}px`,zIndex:n.zIndex??i.value})),{pause:w,resume:S}=_Y(()=>{u.value=Math.max(0,100-(c+Date.now()-d)/n.duration*100)},100,{immediate:!1});function k(){if(n.duration<=0)return;s==null||s(),w(),d=Date.now();const R=Math.max(0,n.duration-c);({stop:s}=Bd(()=>{l.value&&I()},R)),f.value&&(u.value=R/n.duration*100,S())}function T(){s==null||s(),w(),d>0&&(c+=Date.now()-d,d=0)}let M=!1;function E(){M=!0,n.pauseOnHover&&T()}function A(){M=!1,n.pauseOnHover&&k()}function I(){l.value=!1,T()}function P(R){switch(mn(R)){case Pe.delete:case Pe.backspace:T();break;case Pe.esc:l.value&&I();break;default:if(M&&n.pauseOnHover)break;T(),k();break}}return mt(()=>{k(),o(),l.value=!0}),on(document,"keydown",P),t({visible:l,close:I}),(R,G)=>(x(),ne(Pa,{name:p(a).b("fade"),onBeforeLeave:e.onClose,onAfterLeave:G[1]||(G[1]=K=>R.$emit("destroy")),persisted:""},{default:$(()=>[st(C("div",{id:e.id,class:U([p(a).b(),e.customClass,y.value]),style:et(_.value),role:"alert",onMouseenter:E,onMouseleave:A,onClick:G[0]||(G[0]=(...K)=>e.onClick&&e.onClick(...K))},[g.value?(x(),ne(p(Xe),{key:0,class:U([p(a).e("icon"),v.value])},{default:$(()=>[(x(),ne(St(g.value)))]),_:1},8,["class"])):te("v-if",!0),C("div",{class:U(p(a).e("group"))},[C("h2",{class:U(p(a).e("title")),textContent:Q(e.title)},null,10,_we),st(C("div",{class:U(p(a).e("content")),style:et(e.title?void 0:{margin:0})},[ce(R.$slots,"default",{},()=>[e.dangerouslyUseHTMLString?(x(),O(Ve,{key:1},[te(" Caution here, message could've been compromised, never use user's input as message "),C("p",{innerHTML:e.message},null,8,Swe)],2112)):(x(),O("p",wwe,Q(e.message),1))])],6),[[Zt,e.message]]),e.showClose?(x(),ne(p(Xe),{key:0,class:U(p(a).e("closeBtn")),onClick:at(I,["stop"])},{default:$(()=>[(x(),ne(St(e.closeIcon)))]),_:1},8,["class"])):te("v-if",!0)],2),f.value?(x(),O("div",{key:1,class:U(p(a).e("progress")),"aria-hidden":"true"},[L(p(kw),Tt(m.value,{percentage:u.value}),null,16,["percentage"])],2)):te("v-if",!0)],46,bwe),[[Zt,l.value]])]),_:3},8,["name","onBeforeLeave"]))}}),xwe=Cwe;const qd={"top-left":[],"top-right":[],"bottom-left":[],"bottom-right":[]},pg=16;let kwe=1;const pc=function(e={},t){if(!qt)return{close:()=>{}};(Ye(e)||bn(e))&&(e={message:e});const n=e.position||"top-right";let a=e.offset||0;qd[n].forEach(({vm:c})=>{var d;a+=(((d=c.el)==null?void 0:d.offsetHeight)||0)+pg}),a+=pg;const r=`notification_${kwe++}`,o=e.onClose,i={...e,offset:a,id:r,onClose:()=>{Twe(r,n,o)}};let l=document.body;Vr(e.appendTo)?l=e.appendTo:Ye(e.appendTo)&&(l=document.querySelector(e.appendTo)),Vr(l)||(un("ElNotification","the appendTo option is not an HTMLElement. Falling back to document.body."),l=document.body);const s=document.createElement("div"),u=L(xwe,i,Qe(i.message)?i.message:bn(i.message)?()=>i.message:null);return u.appContext=zt(t)?pc._context:t,u.props.onDestroy=()=>{Ps(null,s)},Ps(u,s),qd[n].push({vm:u}),l.appendChild(s.firstElementChild),{close:()=>{u.component.exposed.close()}}};kO.forEach(e=>{pc[e]=(t={},n)=>((Ye(t)||bn(t))&&(t={message:t}),pc({...t,type:e},n))});function Twe(e,t,n){const a=qd[t],r=a.findIndex(({vm:u})=>{var c;return((c=u.component)==null?void 0:c.props.id)===e});if(r===-1)return;const{vm:o}=a[r];if(!o)return;n==null||n(o);const i=o.el.offsetHeight,l=t.split("-")[0];a.splice(r,1);const s=a.length;if(!(s<1))for(let u=r;u{t.component.exposed.close()})}function Ewe(e="top-right"){var n,a,r,o;let t=((r=(a=(n=qd[e][0])==null?void 0:n.vm.component)==null?void 0:a.props)==null?void 0:r.offset)||0;for(const{vm:i}of qd[e])i.component.props.offset=t,t+=(((o=i.el)==null?void 0:o.offsetHeight)||0)+pg}pc.closeAll=Mwe;pc.updateOffsets=Ewe;pc._context=null;const Awe=SI(pc,"$notify"),Iwe=$e({...Tc(iv,["ariaLabel","gpuAcceleration","rawContent"]),placement:J4.placement,tabindex:J4.tabindex,effect:{...iv.effect,default:"light"},title:String,width:{type:[String,Number],default:150},offset:{...iv.offset,default:void 0},persistent:{...iv.persistent,default:!0}}),Lwe={"update:visible":e=>vn(e),"before-enter":()=>!0,"before-leave":()=>!0,"after-enter":()=>!0,"after-leave":()=>!0};({...Tc(Hle,["gpuAcceleration"])});var Pwe=q({name:"ElPopover",__name:"popover",props:Iwe,emits:Lwe,setup(e,{expose:t,emit:n}){const a=e,r=n,o=D(()=>{const y=Ma.props;return ji(a,Ae(y)?y:Object.keys(y))}),i=ke("popover"),l=F(),s=D(()=>{var y;return(y=p(l))==null?void 0:y.popperRef}),u=D(()=>[{width:Vn(a.width)},a.popperStyle]),c=D(()=>[i.b(),a.popperClass,{[i.m("plain")]:!!a.content}]),d=D(()=>a.transition===`${i.namespace.value}-fade-in-linear`),v=()=>{var y;(y=l.value)==null||y.hide()},f=()=>{r("before-enter")},h=()=>{r("before-leave")},m=()=>{r("after-enter")},g=()=>{r("update:visible",!1),r("after-leave")};return t({popperRef:s,hide:v}),(y,b)=>(x(),ne(p(Ma),Tt({ref_key:"tooltipRef",ref:l},o.value,{"aria-label":e.title,"popper-class":c.value,"popper-style":u.value,"gpu-acceleration":d.value,onBeforeShow:f,onBeforeHide:h,onShow:m,onHide:g}),{content:$(()=>[e.title?(x(),O("div",{key:0,class:U(p(i).e("title")),role:"title"},Q(e.title),3)):te("v-if",!0),ce(y.$slots,"default",{hide:v},()=>[ue(Q(e.content),1)])]),default:$(()=>[y.$slots.reference?ce(y.$slots,"reference",{key:0}):te("v-if",!0)]),_:3},16,["aria-label","popper-class","popper-style","gpu-acceleration"]))}}),Dwe=Pwe;const ik=(e,t)=>{var a;const n=(a=t.arg||t.value)==null?void 0:a.popperRef;n&&(n.triggerRef=e)};var Owe={mounted(e,t){ik(e,t)},updated(e,t){ik(e,t)}};const $we="popover",TO=iX(Owe,$we),Rwe=wt(Dwe,{directive:TO}),Nwe="2.14.4",Bwe=(e=[])=>({version:Nwe,install:(n,a)=>{n[uC]||(n[uC]=!0,e.forEach(r=>n.use(r)),a&&ew(a,n,!0))}});var Vwe=[uX,mie,Mse,a4e,Dse,Ose,zse,dL,Xse,Zse,xa,yL,Gce,Xce,dde,fde,Tfe,xP,Lfe,Zo,Yde,cw,Ffe,nve,ave,U1,$P,Lve,aue,Bve,Vve,zve,Fve,Hve,Ipe,jP,Vpe,zpe,t0e,nD,h0e,tme,nme,ame,dD,mle,gle,Xe,Ume,fD,Ba,vD,sge,hge,_ge,Rge,Nge,Bge,Vge,Uge,k1e,I1e,Rwe,iL,kw,CP,ofe,rfe,U1e,q1e,Bfe,ni,Bs,eg,Tce,g2e,C2e,x2e,K2e,q2e,zP,Zve,nye,aye,vye,Sbe,Cbe,n4e,y4e,b4e,Cl,Bw,Que,E4e,D4e,O4e,Ma,Y4e,Fw,p_e,L_e,r3e,h3e,V3e,z3e,q3e,X3e,a6e,m6e,I6e,L6e],zwe=[$6e,z6e,We,er,Awe,TO],Xw=Bwe([...Vwe,...zwe]);Xw.install;Xw.version;var Fwe=Xw,Hwe={name:"zh-cn",el:{breadcrumb:{label:"面包屑"},colorpicker:{confirm:"确定",clear:"清空",defaultLabel:"颜色选择器",description:"当前颜色 {color},按 Enter 键选择新颜色",alphaLabel:"选择透明度的值",alphaDescription:"透明度 {alpha}, 当前颜色 {color}",hueLabel:"选择色相值",hueDescription:"色相 {hue}, 当前颜色 {color}",svLabel:"选择饱和度与明度的值",svDescription:"饱和度 {saturation}, 明度 {brightness}, 当前颜色 {color}",predefineDescription:"选择 {value} 作为颜色"},datepicker:{now:"此刻",today:"今天",cancel:"取消",clear:"清空",confirm:"确定",dateTablePrompt:"使用方向键与 Enter 键可选择日期",monthTablePrompt:"使用方向键与 Enter 键可选择月份",yearTablePrompt:"使用方向键与 Enter 键可选择年份",selectedDate:"已选日期",selectDate:"选择日期",selectTime:"选择时间",startDate:"开始日期",startTime:"开始时间",endDate:"结束日期",endTime:"结束时间",prevYear:"前一年",nextYear:"后一年",prevMonth:"上个月",nextMonth:"下个月",year:"年",month1:"1 月",month2:"2 月",month3:"3 月",month4:"4 月",month5:"5 月",month6:"6 月",month7:"7 月",month8:"8 月",month9:"9 月",month10:"10 月",month11:"11 月",month12:"12 月",weeks:{sun:"日",mon:"一",tue:"二",wed:"三",thu:"四",fri:"五",sat:"六"},weeksFull:{sun:"星期日",mon:"星期一",tue:"星期二",wed:"星期三",thu:"星期四",fri:"星期五",sat:"星期六"},months:{jan:"一月",feb:"二月",mar:"三月",apr:"四月",may:"五月",jun:"六月",jul:"七月",aug:"八月",sep:"九月",oct:"十月",nov:"十一月",dec:"十二月"}},inputNumber:{decrease:"减少数值",increase:"增加数值"},select:{loading:"加载中",noMatch:"无匹配数据",noData:"无数据",placeholder:"请选择"},mention:{loading:"加载中"},dropdown:{toggleDropdown:"切换下拉选项"},cascader:{noMatch:"无匹配数据",loading:"加载中",placeholder:"请选择",noData:"暂无数据"},pagination:{goto:"前往",pagesize:"条/页",total:"共 {total} 条",pageClassifier:"页",page:"页",prev:"上一页",next:"下一页",currentPage:"第 {pager} 页",prevPages:"向前 {pager} 页",nextPages:"向后 {pager} 页",deprecationWarning:"你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档"},dialog:{close:"关闭此对话框"},drawer:{close:"关闭此对话框"},messagebox:{title:"提示",confirm:"确定",cancel:"取消",error:"输入的数据不合法!",close:"关闭此对话框"},upload:{deleteTip:"按 Delete 键可删除",delete:"删除",preview:"查看图片",continue:"继续上传"},slider:{defaultLabel:"滑块介于 {min} 至 {max}",defaultRangeStartLabel:"选择起始值",defaultRangeEndLabel:"选择结束值"},table:{emptyText:"暂无数据",confirmFilter:"筛选",resetFilter:"重置",clearFilter:"全部",sumText:"合计",selectAllLabel:"选择所有行",selectRowLabel:"选择当前行",expandRowLabel:"展开当前行",collapseRowLabel:"收起当前行",sortLabel:"按 {column} 排序",filterLabel:"按 {column} 过滤"},tag:{close:"关闭此标签"},tour:{next:"下一步",previous:"上一步",finish:"结束导览",close:"关闭此对话框"},tree:{emptyText:"暂无数据"},transfer:{noMatch:"无匹配数据",noData:"无数据",titles:["列表 1","列表 2"],filterPlaceholder:"请输入搜索内容",noCheckedFormat:"共 {total} 项",hasCheckedFormat:"已选 {checked}/{total} 项"},image:{error:"加载失败"},pageHeader:{title:"返回"},popconfirm:{confirmButtonText:"确定",cancelButtonText:"取消"},carousel:{leftArrow:"上一张幻灯片",rightArrow:"下一张幻灯片",indicator:"幻灯片切换至索引 {index}"},inputOTP:{groupLabel:"一次性密码输入框",defaultLabel:"请输入第 {index} 位 OTP 字符"}}};const fr=(e,t)=>{const n=e.__vccOpts||e;for(const[a,r]of t)n[a]=r;return n},Wwe={class:"login-page"},Kwe={class:"login-card"},Uwe={class:"login-header"},Gwe={class:"login-sub"},Ywe={key:0,class:"login-error"},jwe={key:0,class:"login-error"},qwe={__name:"Login",emits:["login-success"],setup(e,{emit:t}){const n=t,a=nn({username:"",password:""}),r=F(!1),o=F(""),i=F(!1);mt(async()=>{var u;try{const d=await(await fetch("/api/auth/setup-status")).json();i.value=((u=d.data)==null?void 0:u.setup_needed)??!1}catch{}});async function l(){if(!a.username||!a.password){o.value="请输入用户名和密码";return}r.value=!0,o.value="";try{const u=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)}),c=await u.json();if(!u.ok){o.value=c.error||"登录失败";return}sessionStorage.setItem("admin_token",c.data.token),sessionStorage.setItem("admin_username",a.username),We.success("登录成功"),n("login-success")}catch(u){o.value="网络错误: "+u.message}finally{r.value=!1}}async function s(){if(!a.username||a.username.length<3){o.value="用户名至少 3 位";return}if(!a.password||a.password.length<8){o.value="密码至少 8 位";return}r.value=!0,o.value="";try{const u=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:a.username,password:a.password})}),c=await u.json();if(!u.ok){o.value=c.error||"注册失败";return}We.success("管理员创建成功"),i.value=!1,r.value=!1,await l()}catch(u){o.value="网络错误: "+u.message}finally{r.value=!1}}return(u,c)=>{const d=be("el-alert"),v=be("el-input"),f=be("el-form-item"),h=be("el-button"),m=be("el-icon"),g=be("el-form");return x(),O("div",Wwe,[C("div",Kwe,[C("div",Uwe,[c[4]||(c[4]=_7('

PayGateway

',2)),C("p",Gwe,Q(i.value?"首次启动 · 初始化管理员":"支付网关管理控制台"),1)]),i.value?(x(),ne(g,{key:0,onSubmit:at(s,["prevent"]),model:a,size:"large"},{default:$(()=>[L(d,{type:"info",closable:!1,class:"mb12",style:{"font-size":"12px"},title:"首次启动:设置管理员账号与密码。凭据经 bcrypt 哈希加密后存入数据库,登录后台后可随时管理。"}),L(f,null,{default:$(()=>[L(v,{modelValue:a.username,"onUpdate:modelValue":c[0]||(c[0]=y=>a.username=y),placeholder:"管理员用户名(3-32 位)","prefix-icon":"User",onKeyup:kn(s,["enter"]),size:"large",class:"login-input"},null,8,["modelValue"])]),_:1}),L(f,null,{default:$(()=>[L(v,{modelValue:a.password,"onUpdate:modelValue":c[1]||(c[1]=y=>a.password=y),type:"password",placeholder:"管理员密码(至少 8 位)","prefix-icon":"Lock","show-password":"",onKeyup:kn(s,["enter"]),size:"large",class:"login-input"},null,8,["modelValue"])]),_:1}),L(f,{style:{"margin-bottom":"0","margin-top":"4px"}},{default:$(()=>[L(h,{type:"primary",loading:r.value,class:"login-btn",onClick:s,style:{width:"100%"},size:"large"},{default:$(()=>[ue(Q(r.value?"初始化中...":"创建管理员并登录"),1)]),_:1},8,["loading"])]),_:1}),o.value?(x(),O("div",Ywe,[L(m,{size:13},{default:$(()=>[L(p(fc))]),_:1}),ue(" "+Q(o.value),1)])):te("",!0)]),_:1},8,["model"])):(x(),ne(g,{key:1,onSubmit:at(l,["prevent"]),model:a,size:"large"},{default:$(()=>[L(f,null,{default:$(()=>[L(v,{modelValue:a.username,"onUpdate:modelValue":c[2]||(c[2]=y=>a.username=y),placeholder:"用户名","prefix-icon":"User",onKeyup:kn(l,["enter"]),size:"large",class:"login-input"},null,8,["modelValue"])]),_:1}),L(f,null,{default:$(()=>[L(v,{modelValue:a.password,"onUpdate:modelValue":c[3]||(c[3]=y=>a.password=y),type:"password",placeholder:"密码","prefix-icon":"Lock","show-password":"",onKeyup:kn(l,["enter"]),size:"large",class:"login-input"},null,8,["modelValue"])]),_:1}),L(f,{style:{"margin-bottom":"0","margin-top":"4px"}},{default:$(()=>[L(h,{type:"primary",loading:r.value,class:"login-btn",onClick:l,style:{width:"100%"},size:"large"},{default:$(()=>[ue(Q(r.value?"登录中...":"登 录"),1)]),_:1},8,["loading"])]),_:1}),o.value?(x(),O("div",jwe,[L(m,{size:13},{default:$(()=>[L(p(fc))]),_:1}),ue(" "+Q(o.value),1)])):te("",!0)]),_:1},8,["model"]))])])}}},Xwe=fr(qwe,[["__scopeId","data-v-40e3134b"]]);/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var d_=function(e,t){return d_=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,a){n.__proto__=a}||function(n,a){for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(n[r]=a[r])},d_(e,t)};function At(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");d_(e,t);function n(){this.constructor=e}e.prototype=t===null?Object.create(t):(n.prototype=t.prototype,new n)}var Zwe=function(){function e(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1}return e}(),Jwe=function(){function e(){this.browser=new Zwe,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow=typeof window<"u"}return e}(),hn=new Jwe;typeof wx=="object"&&typeof wx.getSystemInfoSync=="function"?(hn.wxa=!0,hn.touchEventsSupported=!0):typeof document>"u"&&typeof self<"u"?hn.worker=!0:!hn.hasGlobalWindow||"Deno"in window||typeof navigator<"u"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Node.js")>-1?(hn.node=!0,hn.svgSupported=!0):Qwe(navigator.userAgent,hn);function Qwe(e,t){var n=t.browser,a=e.match(/Firefox\/([\d.]+)/),r=e.match(/MSIE\s([\d.]+)/)||e.match(/Trident\/.+?rv:(([\d.]+))/),o=e.match(/Edge?\/([\d.]+)/),i=/micromessenger/i.test(e);a&&(n.firefox=!0,n.version=a[1]),r&&(n.ie=!0,n.version=r[1]),o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18),i&&(n.weChat=!0),t.svgSupported=typeof SVGRect<"u",t.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,t.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11);var l=t.domSupported=typeof document<"u";if(l){var s=document.documentElement.style;t.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),t.transformSupported=t.transform3dSupported||n.ie&&+n.version>=9}}var Zw=12,e8e="sans-serif",Vs=Zw+"px "+e8e,t8e=20,n8e=100,a8e="007LLmW'55;N0500LLLLLLLLLL00NNNLzWW\\\\WQb\\0FWLg\\bWb\\WQ\\WrWWQ000CL5LLFLL0LL**F*gLLLL5F0LF\\FFF5.5N";function r8e(e){var t={};if(typeof JSON>"u")return t;for(var n=0;n=0)l=i*n.length;else for(var s=0;s=u8e&&(yy=0),yy++}function e8(){for(var e=[],t=0;t>1)%2;l.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",a[s]+":0",r[u]+":0",a[1-s]+":auto",r[1-u]+":auto",""].join("!important;"),e.appendChild(i),n.push(i)}return t.clearMarkers=function(){we(n,function(c){c.parentNode&&c.parentNode.removeChild(c)})},n}function L8e(e,t,n){for(var a=n?"invTrans":"trans",r=t[a],o=t.srcCoords,i=[],l=[],s=!0,u=0;u<4;u++){var c=e[u].getBoundingClientRect(),d=2*u,v=c.left,f=c.top;i.push(v,f),s=s&&o&&v===o[d]&&f===o[d+1],l.push(e[u].offsetLeft,e[u].offsetTop)}return s&&r?r:(t.srcCoords=i,t[a]=n?ck(l,i):ck(i,l))}function DO(e){return e.nodeName.toUpperCase()==="CANVAS"}var P8e=/([&<>"'])/g,D8e={"&":"&","<":"<",">":">",'"':""","'":"'"};function Or(e){return e==null?"":(e+"").replace(P8e,function(t,n){return D8e[n]})}var O8e=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Cy=[],$8e=hn.browser.firefox&&+hn.browser.version.split(".")[0]<39;function y_(e,t,n,a){return n=n||{},a?dk(e,t,n):$8e&&t.layerX!=null&&t.layerX!==t.offsetX?(n.zrX=t.layerX,n.zrY=t.layerY):t.offsetX!=null?(n.zrX=t.offsetX,n.zrY=t.offsetY):dk(e,t,n),n}function dk(e,t,n){if(hn.domSupported&&e.getBoundingClientRect){var a=t.clientX,r=t.clientY;if(DO(e)){var o=e.getBoundingClientRect();n.zrX=a-o.left,n.zrY=r-o.top;return}else if(g_(Cy,e,a,r)){n.zrX=Cy[0],n.zrY=Cy[1];return}}n.zrX=n.zrY=0}function a8(e){return e||window.event}function go(e,t,n){if(t=a8(t),t.zrX!=null)return t;var a=t.type,r=a&&a.indexOf("touch")>=0;if(r){var i=a!=="touchend"?t.targetTouches[0]:t.changedTouches[0];i&&y_(e,i,t,n)}else{y_(e,t,t,n);var o=R8e(t);t.zrDelta=o?o/120:-(t.detail||0)/3}var l=t.button;return t.which==null&&l!==void 0&&O8e.test(t.type)&&(t.which=l&1?1:l&2?3:l&4?2:0),t}function R8e(e){var t=e.wheelDelta;if(t)return t;var n=e.deltaX,a=e.deltaY;if(n==null||a==null)return t;var r=Math.abs(a!==0?a:n),o=a>0?-1:a<0?1:n>0?-1:1;return 3*r*o}function N8e(e,t,n,a){e.addEventListener(t,n,a)}function B8e(e,t,n,a){e.removeEventListener(t,n,a)}var OO=function(e){e.preventDefault(),e.stopPropagation(),e.cancelBubble=!0},V8e=function(){function e(){this._track=[]}return e.prototype.recognize=function(t,n,a){return this._doTrack(t,n,a),this._recognize(t)},e.prototype.clear=function(){return this._track.length=0,this},e.prototype._doTrack=function(t,n,a){var r=t.touches;if(r){for(var o={points:[],touches:[],target:n,event:t},i=0,l=r.length;i1&&a&&a.length>1){var o=fk(a)/fk(r);!isFinite(o)&&(o=1),t.pinchScale=o;var i=z8e(a);return t.pinchX=i[0],t.pinchY=i[1],{type:"pinch",target:e[0].target,event:t}}}}};function zi(){return[1,0,0,1,0,0]}function hp(e){return e[0]=1,e[1]=0,e[2]=0,e[3]=1,e[4]=0,e[5]=0,e}function r8(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4],e[5]=t[5],e}function $v(e,t,n){var a=t[0]*n[0]+t[2]*n[1],r=t[1]*n[0]+t[3]*n[1],o=t[0]*n[2]+t[2]*n[3],i=t[1]*n[2]+t[3]*n[3],l=t[0]*n[4]+t[2]*n[5]+t[4],s=t[1]*n[4]+t[3]*n[5]+t[5];return e[0]=a,e[1]=r,e[2]=o,e[3]=i,e[4]=l,e[5]=s,e}function b_(e,t,n){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e[4]=t[4]+n[0],e[5]=t[5]+n[1],e}function o8(e,t,n,a){a===void 0&&(a=[0,0]);var r=t[0],o=t[2],i=t[4],l=t[1],s=t[3],u=t[5],c=Math.sin(n),d=Math.cos(n);return e[0]=r*d+l*c,e[1]=-r*c+l*d,e[2]=o*d+s*c,e[3]=-o*c+d*s,e[4]=d*(i-a[0])+c*(u-a[1])+a[0],e[5]=d*(u-a[1])-c*(i-a[0])+a[1],e}function F8e(e,t,n){var a=n[0],r=n[1];return e[0]=t[0]*a,e[1]=t[1]*r,e[2]=t[2]*a,e[3]=t[3]*r,e[4]=t[4]*a,e[5]=t[5]*r,e}function pp(e,t){var n=t[0],a=t[2],r=t[4],o=t[1],i=t[3],l=t[5],s=n*i-o*a;return s?(s=1/s,e[0]=i*s,e[1]=-o*s,e[2]=-a*s,e[3]=n*s,e[4]=(a*l-i*r)*s,e[5]=(o*r-n*l)*s,e):null}var fn=function(){function e(t,n){this.x=t||0,this.y=n||0}return e.prototype.copy=function(t){return this.x=t.x,this.y=t.y,this},e.prototype.clone=function(){return new e(this.x,this.y)},e.prototype.set=function(t,n){return this.x=t,this.y=n,this},e.prototype.equal=function(t){return t.x===this.x&&t.y===this.y},e.prototype.add=function(t){return this.x+=t.x,this.y+=t.y,this},e.prototype.scale=function(t){this.x*=t,this.y*=t},e.prototype.scaleAndAdd=function(t,n){this.x+=t.x*n,this.y+=t.y*n},e.prototype.sub=function(t){return this.x-=t.x,this.y-=t.y,this},e.prototype.dot=function(t){return this.x*t.x+this.y*t.y},e.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},e.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},e.prototype.normalize=function(){var t=this.len();return this.x/=t,this.y/=t,this},e.prototype.distance=function(t){var n=this.x-t.x,a=this.y-t.y;return Math.sqrt(n*n+a*a)},e.prototype.distanceSquare=function(t){var n=this.x-t.x,a=this.y-t.y;return n*n+a*a},e.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},e.prototype.transform=function(t){if(t){var n=this.x,a=this.y;return this.x=t[0]*n+t[2]*a+t[4],this.y=t[1]*n+t[3]*a+t[5],this}},e.prototype.toArray=function(t){return t[0]=this.x,t[1]=this.y,t},e.prototype.fromArray=function(t){this.x=t[0],this.y=t[1]},e.set=function(t,n,a){t.x=n,t.y=a},e.copy=function(t,n){t.x=n.x,t.y=n.y},e.len=function(t){return Math.sqrt(t.x*t.x+t.y*t.y)},e.lenSquare=function(t){return t.x*t.x+t.y*t.y},e.dot=function(t,n){return t.x*n.x+t.y*n.y},e.add=function(t,n,a){t.x=n.x+a.x,t.y=n.y+a.y},e.sub=function(t,n,a){t.x=n.x-a.x,t.y=n.y-a.y},e.scale=function(t,n,a){t.x=n.x*a,t.y=n.y*a},e.scaleAndAdd=function(t,n,a,r){t.x=n.x+a.x*r,t.y=n.y+a.y*r},e.lerp=function(t,n,a,r){var o=1-r;t.x=o*n.x+r*a.x,t.y=o*n.y+r*a.y},e}(),Vu=Math.min,cd=Math.max,__=Math.abs,vk=["x","y"],H8e=["width","height"],nu=new fn,au=new fn,ru=new fn,ou=new fn,Zr=$O(),cv=Zr.minTv,w_=Zr.maxTv,Rv=[0,0],ln=function(){function e(t,n,a,r){ky(this,t,n,a,r)}return e.set=function(t,n,a,r,o){return r<0&&(n=n+r,r=-r),o<0&&(a=a+o,o=-o),t.x=n,t.y=a,t.width=r,t.height=o,t},e.prototype.union=function(t){var n=Vu(t.x,this.x),a=Vu(t.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=cd(t.x+t.width,this.x+this.width)-n:this.width=t.width,isFinite(this.y)&&isFinite(this.height)?this.height=cd(t.y+t.height,this.y+this.height)-a:this.height=t.height,this.x=n,this.y=a},e.prototype.applyTransform=function(t){e.applyTransform(this,this,t)},e.prototype.calculateTransform=function(t){return W8e(zi(),this,t)},e.prototype.intersect=function(t,n,a){return e.intersect(this,t,n,a)},e.intersect=function(t,n,a,r){a&&fn.set(a,0,0);var o=r&&r.outIntersectRect||null,i=r&&r.clamp;if(o&&(o.x=o.y=o.width=o.height=NaN),!t||!n)return!1;t instanceof e||(t=ky(K8e,t.x,t.y,t.width,t.height)),n instanceof e||(n=ky(U8e,n.x,n.y,n.width,n.height));var l=!!a;Zr.reset(r,l);var s=Zr.touchThreshold,u=t.x+s,c=t.x+t.width-s,d=t.y+s,v=t.y+t.height-s,f=n.x+s,h=n.x+n.width-s,m=n.y+s,g=n.y+n.height-s;if(u>c||d>v||f>h||m>g)return!1;var y=!(c=t.x&&n<=t.x+t.width&&a>=t.y&&a<=t.y+t.height},e.prototype.contain=function(t,n){return e.contain(this,t,n)},e.prototype.clone=function(){return new e(this.x,this.y,this.width,this.height)},e.prototype.copy=function(t){hk(this,t)},e.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},e.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},e.prototype.isZero=function(){return this.width===0||this.height===0},e.create=function(t){return new e(t?t.x:0,t?t.y:0,t?t.width:0,t?t.height:0)},e.copy=function(t,n){return t.x=n.x,t.y=n.y,t.width=n.width,t.height=n.height,t},e.applyTransform=function(t,n,a){if(!a){t!==n&&hk(t,n);return}if(a[1]<1e-5&&a[1]>-1e-5&&a[2]<1e-5&&a[2]>-1e-5){var r=a[0],o=a[3],i=a[4],l=a[5];t.x=n.x*r+i,t.y=n.y*o+l,t.width=n.width*r,t.height=n.height*o,t.width<0&&(t.x+=t.width,t.width=-t.width),t.height<0&&(t.y+=t.height,t.height=-t.height);return}nu.x=ru.x=n.x,nu.y=ou.y=n.y,au.x=ou.x=n.x+n.width,au.y=ru.y=n.y+n.height,nu.transform(a),ou.transform(a),au.transform(a),ru.transform(a),t.x=Vu(nu.x,au.x,ru.x,ou.x),t.y=Vu(nu.y,au.y,ru.y,ou.y);var s=cd(nu.x,au.x,ru.x,ou.x),u=cd(nu.y,au.y,ru.y,ou.y);t.width=s-t.x,t.height=u-t.y},e.calculateTransform=function(t,n,a){var r=a.width/n.width,o=a.height/n.height;return t=hp(t||[]),b_(t,t,by(Ty,-n.x,-n.y)),F8e(t,t,by(Ty,r,o)),b_(t,t,by(Ty,a.x,a.y)),t},e}();ln.create;var ky=ln.set,hk=ln.copy,W8e=ln.calculateTransform;ln.applyTransform;ln.contain;var K8e=new ln(0,0,0,0),U8e=new ln(0,0,0,0),Ty=[];function pk(e,t,n,a,r,o,i,l){var s=__(t-n),u=__(a-e),c=Vu(s,u),d=vk[r],v=vk[1-r],f=H8e[r];t=u||!Zr.bidirectional)&&(cv[d]=-u,cv[v]=0,Zr.useDir&&Zr.calcDirMTV())))}function $O(){var e=0,t=new fn,n=new fn,a={minTv:new fn,maxTv:new fn,useDir:!1,dirMinTv:new fn,touchThreshold:0,bidirectional:!0,negativeSize:!1,reset:function(o,i){a.touchThreshold=0,o&&o.touchThreshold!=null&&(a.touchThreshold=cd(0,o.touchThreshold)),a.negativeSize=!1,i&&(a.minTv.set(1/0,1/0),a.maxTv.set(0,0),a.useDir=!1,o&&o.direction!=null&&(a.useDir=!0,a.dirMinTv.copy(a.minTv),n.copy(a.minTv),e=o.direction,a.bidirectional=o.bidirectional==null||!!o.bidirectional,a.bidirectional||t.set(Math.cos(e),Math.sin(e))))},calcDirMTV:function(){var o=a.minTv,i=a.dirMinTv,l=o.y*o.y+o.x*o.x,s=Math.sin(e),u=Math.cos(e),c=s*o.y+u*o.x;if(r(c)){r(o.x)&&r(o.y)&&i.set(0,0);return}if(n.x=l*u/c,n.y=l*s/c,r(n.x)&&r(n.y)){i.set(0,0);return}(a.bidirectional||t.dot(n)>0)&&n.len()=0;d--){var v=o[d];v!==r&&!v.ignore&&!v.ignoreCoarsePointer&&(!v.parent||!v.parent.ignoreCoarsePointer)&&(My.copy(v.getBoundingRect()),v.transform&&My.applyTransform(v.transform),My.intersect(c)&&l.push(v))}if(l.length)for(var f=4,h=Math.PI/12,m=Math.PI*2,g=0;g4)return;this._downPoint=null}this.dispatchToElement(o,e,t)}});function X8e(e,t,n){if(e[e.rectHover?"rectContain":"contain"](t,n)){for(var a=e,r=void 0,o=!1;a;){if(a.ignoreClip&&(o=!0),!o){var i=a.getClipPath();if(i&&!i.contain(t,n))return!1}a.silent&&(r=!0);var l=a.__hostTarget;a=l?a.ignoreHostSilent?null:l:a.parent}return r?RO:!0}return!1}function mk(e,t,n,a,r){for(var o=e.length-1;o>=0;o--){var i=e[o],l=void 0;if(i!==r&&!i.ignore&&(l=X8e(i,n,a))&&(!t.topTarget&&(t.topTarget=i),l!==RO)){t.target=i;break}}}function BO(e,t,n){var a=e.painter;return t<0||t>a.getWidth()||n<0||n>a.getHeight()}var VO=32,Nf=7;function Z8e(e){for(var t=0;e>=VO;)t|=e&1,e>>=1;return e+t}function gk(e,t,n,a){var r=t+1;if(r===n)return 1;if(a(e[r++],e[t])<0){for(;r=0;)r++;return r-t}function J8e(e,t,n){for(n--;t>>1,r(o,e[s])<0?l=s:i=s+1;var u=a-i;switch(u){case 3:e[i+3]=e[i+2];case 2:e[i+2]=e[i+1];case 1:e[i+1]=e[i];break;default:for(;u>0;)e[i+u]=e[i+u-1],u--}e[i]=o}}function Ey(e,t,n,a,r,o){var i=0,l=0,s=1;if(o(e,t[n+r])>0){for(l=a-r;s0;)i=s,s=(s<<1)+1,s<=0&&(s=l);s>l&&(s=l),i+=r,s+=r}else{for(l=r+1;sl&&(s=l);var u=i;i=r-s,s=r-u}for(i++;i>>1);o(e,t[n+c])>0?i=c+1:s=c}return s}function Ay(e,t,n,a,r,o){var i=0,l=0,s=1;if(o(e,t[n+r])<0){for(l=r+1;sl&&(s=l);var u=i;i=r-s,s=r-u}else{for(l=a-r;s=0;)i=s,s=(s<<1)+1,s<=0&&(s=l);s>l&&(s=l),i+=r,s+=r}for(i++;i>>1);o(e,t[n+c])<0?s=c:i=c+1}return s}function Q8e(e,t){var n=Nf,a,r,o=0,i=[];a=[],r=[];function l(f,h){a[o]=f,r[o]=h,o+=1}function s(){for(;o>1;){var f=o-2;if(f>=1&&r[f-1]<=r[f]+r[f+1]||f>=2&&r[f-2]<=r[f]+r[f-1])r[f-1]r[f+1])break;c(f)}}function u(){for(;o>1;){var f=o-2;f>0&&r[f-1]=Nf||T>=Nf);if(M)break;S<0&&(S=0),S+=2}if(n=S,n<1&&(n=1),h===1){for(y=0;y=0;y--)e[k+y]=e[S+y];e[w]=i[_];return}for(var T=n;;){var M=0,E=0,A=!1;do if(t(i[_],e[b])<0){if(e[w--]=e[b--],M++,E=0,--h===0){A=!0;break}}else if(e[w--]=i[_--],E++,M=0,--g===1){A=!0;break}while((M|E)=0;y--)e[k+y]=e[S+y];if(h===0){A=!0;break}}if(e[w--]=i[_--],--g===1){A=!0;break}if(E=g-Ey(e[b],i,0,g,g-1,t),E!==0){for(w-=E,_-=E,g-=E,k=w+1,S=_+1,y=0;y=Nf||E>=Nf);if(A)break;T<0&&(T=0),T+=2}if(n=T,n<1&&(n=1),g===1){for(w-=h,b-=h,k=w+1,S=b+1,y=h-1;y>=0;y--)e[k+y]=e[S+y];e[w]=i[_]}else{if(g===0)throw new Error;for(S=w-(g-1),y=0;yl&&(s=l),yk(e,n,n+s,n+o,t),o=s}i.pushRun(n,o),i.mergeRuns(),r-=o,n+=o}while(r!==0);i.forceMergeRuns()}}var Nr=1,dv=2,rd=4,bk=!1;function Iy(){bk||(bk=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function _k(e,t){return e.zlevel===t.zlevel?e.z===t.z?e.z2-t.z2:e.z-t.z:e.zlevel-t.zlevel}var eSe=function(){function e(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=_k}return e.prototype.traverse=function(t,n){for(var a=0;a=0&&this._roots.splice(r,1)},e.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},e.prototype.getRoots=function(){return this._roots},e.prototype.dispose=function(){this._displayList=null,this._roots=null},e}(),yg;yg=hn.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(e){return setTimeout(e,16)};var Nv={linear:function(e){return e},quadraticIn:function(e){return e*e},quadraticOut:function(e){return e*(2-e)},quadraticInOut:function(e){return(e*=2)<1?.5*e*e:-.5*(--e*(e-2)-1)},cubicIn:function(e){return e*e*e},cubicOut:function(e){return--e*e*e+1},cubicInOut:function(e){return(e*=2)<1?.5*e*e*e:.5*((e-=2)*e*e+2)},quarticIn:function(e){return e*e*e*e},quarticOut:function(e){return 1- --e*e*e*e},quarticInOut:function(e){return(e*=2)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2)},quinticIn:function(e){return e*e*e*e*e},quinticOut:function(e){return--e*e*e*e*e+1},quinticInOut:function(e){return(e*=2)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2)},sinusoidalIn:function(e){return 1-Math.cos(e*Math.PI/2)},sinusoidalOut:function(e){return Math.sin(e*Math.PI/2)},sinusoidalInOut:function(e){return .5*(1-Math.cos(Math.PI*e))},exponentialIn:function(e){return e===0?0:Math.pow(1024,e-1)},exponentialOut:function(e){return e===1?1:1-Math.pow(2,-10*e)},exponentialInOut:function(e){return e===0?0:e===1?1:(e*=2)<1?.5*Math.pow(1024,e-1):.5*(-Math.pow(2,-10*(e-1))+2)},circularIn:function(e){return 1-Math.sqrt(1-e*e)},circularOut:function(e){return Math.sqrt(1- --e*e)},circularInOut:function(e){return(e*=2)<1?-.5*(Math.sqrt(1-e*e)-1):.5*(Math.sqrt(1-(e-=2)*e)+1)},elasticIn:function(e){var t,n=.1,a=.4;return e===0?0:e===1?1:(!n||n<1?(n=1,t=a/4):t=a*Math.asin(1/n)/(2*Math.PI),-(n*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/a)))},elasticOut:function(e){var t,n=.1,a=.4;return e===0?0:e===1?1:(!n||n<1?(n=1,t=a/4):t=a*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*e)*Math.sin((e-t)*(2*Math.PI)/a)+1)},elasticInOut:function(e){var t,n=.1,a=.4;return e===0?0:e===1?1:(!n||n<1?(n=1,t=a/4):t=a*Math.asin(1/n)/(2*Math.PI),(e*=2)<1?-.5*(n*Math.pow(2,10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/a)):n*Math.pow(2,-10*(e-=1))*Math.sin((e-t)*(2*Math.PI)/a)*.5+1)},backIn:function(e){var t=1.70158;return e*e*((t+1)*e-t)},backOut:function(e){var t=1.70158;return--e*e*((t+1)*e+t)+1},backInOut:function(e){var t=2.5949095;return(e*=2)<1?.5*(e*e*((t+1)*e-t)):.5*((e-=2)*e*((t+1)*e+t)+2)},bounceIn:function(e){return 1-Nv.bounceOut(1-e)},bounceOut:function(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375},bounceInOut:function(e){return e<.5?Nv.bounceIn(e*2)*.5:Nv.bounceOut(e*2-1)*.5+.5}},Jp=Math.pow,ks=Math.sqrt,bg=1e-8,zO=1e-4,wk=ks(3),Qp=1/3,Ei=yf(),wo=yf(),kd=yf();function ys(e){return e>-bg&&ebg||e<-bg}function Ua(e,t,n,a,r){var o=1-r;return o*o*(o*e+3*r*t)+r*r*(r*a+3*o*n)}function Sk(e,t,n,a,r){var o=1-r;return 3*(((t-e)*o+2*(n-t)*r)*o+(a-n)*r*r)}function _g(e,t,n,a,r,o){var i=a+3*(t-n)-e,l=3*(n-t*2+e),s=3*(t-e),u=e-r,c=l*l-3*i*s,d=l*s-9*i*u,v=s*s-3*l*u,f=0;if(ys(c)&&ys(d))if(ys(l))o[0]=0;else{var h=-s/l;h>=0&&h<=1&&(o[f++]=h)}else{var m=d*d-4*c*v;if(ys(m)){var g=d/c,h=-l/i+g,y=-g/2;h>=0&&h<=1&&(o[f++]=h),y>=0&&y<=1&&(o[f++]=y)}else if(m>0){var b=ks(m),_=c*l+1.5*i*(-d+b),w=c*l+1.5*i*(-d-b);_<0?_=-Jp(-_,Qp):_=Jp(_,Qp),w<0?w=-Jp(-w,Qp):w=Jp(w,Qp);var h=(-l-(_+w))/(3*i);h>=0&&h<=1&&(o[f++]=h)}else{var S=(2*c*l-3*i*d)/(2*ks(c*c*c)),k=Math.acos(S)/3,T=ks(c),M=Math.cos(k),h=(-l-2*T*M)/(3*i),y=(-l+T*(M+wk*Math.sin(k)))/(3*i),E=(-l+T*(M-wk*Math.sin(k)))/(3*i);h>=0&&h<=1&&(o[f++]=h),y>=0&&y<=1&&(o[f++]=y),E>=0&&E<=1&&(o[f++]=E)}}return f}function HO(e,t,n,a,r){var o=6*n-12*t+6*e,i=9*t+3*a-3*e-9*n,l=3*t-3*e,s=0;if(ys(i)){if(FO(o)){var u=-l/o;u>=0&&u<=1&&(r[s++]=u)}}else{var c=o*o-4*i*l;if(ys(c))r[0]=-o/(2*i);else if(c>0){var d=ks(c),u=(-o+d)/(2*i),v=(-o-d)/(2*i);u>=0&&u<=1&&(r[s++]=u),v>=0&&v<=1&&(r[s++]=v)}}return s}function wg(e,t,n,a,r,o){var i=(t-e)*r+e,l=(n-t)*r+t,s=(a-n)*r+n,u=(l-i)*r+i,c=(s-l)*r+l,d=(c-u)*r+u;o[0]=e,o[1]=i,o[2]=u,o[3]=d,o[4]=d,o[5]=c,o[6]=s,o[7]=a}function tSe(e,t,n,a,r,o,i,l,s,u,c){var d,v=.005,f=1/0,h,m,g,y;Ei[0]=s,Ei[1]=u;for(var b=0;b<1;b+=.05)wo[0]=Ua(e,n,r,i,b),wo[1]=Ua(t,a,o,l,b),g=xd(Ei,wo),g=0&&g=0&&u<=1&&(r[s++]=u)}}else{var c=i*i-4*o*l;if(ys(c)){var u=-i/(2*o);u>=0&&u<=1&&(r[s++]=u)}else if(c>0){var d=ks(c),u=(-i+d)/(2*o),v=(-i-d)/(2*o);u>=0&&u<=1&&(r[s++]=u),v>=0&&v<=1&&(r[s++]=v)}}return s}function WO(e,t,n){var a=e+n-2*t;return a===0?.5:(e-t)/a}function Sg(e,t,n,a,r){var o=(t-e)*a+e,i=(n-t)*a+t,l=(i-o)*a+o;r[0]=e,r[1]=o,r[2]=l,r[3]=l,r[4]=i,r[5]=n}function rSe(e,t,n,a,r,o,i,l,s){var u,c=.005,d=1/0;Ei[0]=i,Ei[1]=l;for(var v=0;v<1;v+=.05){wo[0]=$r(e,n,r,v),wo[1]=$r(t,a,o,v);var f=xd(Ei,wo);f=0&&f=1?1:_g(0,a,o,1,s,l)&&Ua(0,r,i,1,l[0])}}}var lSe=function(){function e(t){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=t.life||1e3,this._delay=t.delay||0,this.loop=t.loop||!1,this.onframe=t.onframe||lr,this.ondestroy=t.ondestroy||lr,this.onrestart=t.onrestart||lr,t.easing&&this.setEasing(t.easing)}return e.prototype.step=function(t,n){if(this._inited||(this._startTime=t+this._delay,this._inited=!0),this._paused){this._pausedTime+=n;return}var a=this._life,r=t-this._startTime-this._pausedTime,o=r/a;o<0&&(o=0),o=Math.min(o,1);var i=this.easingFunc,l=i?i(o):o;if(this.onframe(l),o===1)if(this.loop){var s=r%a;this._startTime=t-s,this._pausedTime=0,this.onrestart()}else return!0;return!1},e.prototype.pause=function(){this._paused=!0},e.prototype.resume=function(){this._paused=!1},e.prototype.setEasing=function(t){this.easing=t,this.easingFunc=dn(t)?t:Nv[t]||KO(t)},e}(),UO=function(){function e(t){this.value=t}return e}(),sSe=function(){function e(){this._len=0}return e.prototype.insert=function(t){var n=new UO(t);return this.insertEntry(n),n},e.prototype.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},e.prototype.remove=function(t){var n=t.prev,a=t.next;n?n.next=a:this.head=a,a?a.prev=n:this.tail=n,t.next=t.prev=null,this._len--},e.prototype.len=function(){return this._len},e.prototype.clear=function(){this.head=this.tail=null,this._len=0},e}(),Xd=function(){function e(t){this._list=new sSe,this._maxSize=10,this._map={},this._maxSize=t}return e.prototype.put=function(t,n){var a=this._list,r=this._map,o=null;if(r[t]==null){var i=a.len(),l=this._lastRemovedEntry;if(i>=this._maxSize&&i>0){var s=a.head;a.remove(s),delete r[s.key],o=s.value,this._lastRemovedEntry=s}l?l.value=n:l=new UO(n),l.key=t,a.insertEntry(l),r[t]=l}return o},e.prototype.get=function(t){var n=this._map[t],a=this._list;if(n!=null)return n!==a.tail&&(a.remove(n),a.insertEntry(n)),n.value},e.prototype.clear=function(){this._list.clear(),this._map={}},e.prototype.len=function(){return this._list.len()},e}(),xk={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function Ts(e){return e=Math.round(e),e<0?0:e>255?255:e}function S_(e){return e<0?0:e>1?1:e}function Ly(e){var t=e;return t.length&&t.charAt(t.length-1)==="%"?Ts(parseFloat(t)/100*255):Ts(parseInt(t,10))}function Qu(e){var t=e;return t.length&&t.charAt(t.length-1)==="%"?S_(parseFloat(t)/100):S_(parseFloat(t))}function Py(e,t,n){return n<0?n+=1:n>1&&(n-=1),n*6<1?e+(t-e)*n*6:n*2<1?t:n*3<2?e+(t-e)*(2/3-n)*6:e}function e0(e,t,n){return e+(t-e)*n}function po(e,t,n,a,r){return e[0]=t,e[1]=n,e[2]=a,e[3]=r,e}function C_(e,t){return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e}var GO=new Xd(20),t0=null;function zc(e,t){t0&&C_(t0,t),t0=GO.put(e,t0||t.slice())}function Fi(e,t){if(e){t=t||[];var n=GO.get(e);if(n)return C_(t,n);e=e+"";var a=e.replace(/ /g,"").toLowerCase();if(a in xk)return C_(t,xk[a]),zc(e,t),t;var r=a.length;if(a.charAt(0)==="#"){if(r===4||r===5){var o=parseInt(a.slice(1,4),16);if(!(o>=0&&o<=4095)){po(t,0,0,0,1);return}return po(t,(o&3840)>>4|(o&3840)>>8,o&240|(o&240)>>4,o&15|(o&15)<<4,r===5?parseInt(a.slice(4),16)/15:1),zc(e,t),t}else if(r===7||r===9){var o=parseInt(a.slice(1,7),16);if(!(o>=0&&o<=16777215)){po(t,0,0,0,1);return}return po(t,(o&16711680)>>16,(o&65280)>>8,o&255,r===9?parseInt(a.slice(7),16)/255:1),zc(e,t),t}return}var i=a.indexOf("("),l=a.indexOf(")");if(i!==-1&&l+1===r){var s=a.substr(0,i),u=a.substr(i+1,l-(i+1)).split(","),c=1;switch(s){case"rgba":if(u.length!==4)return u.length===3?po(t,+u[0],+u[1],+u[2],1):po(t,0,0,0,1);c=Qu(u.pop());case"rgb":if(u.length>=3)return po(t,Ly(u[0]),Ly(u[1]),Ly(u[2]),u.length===3?c:Qu(u[3])),zc(e,t),t;po(t,0,0,0,1);return;case"hsla":if(u.length!==4){po(t,0,0,0,1);return}return u[3]=Qu(u[3]),x_(u,t),zc(e,t),t;case"hsl":if(u.length!==3){po(t,0,0,0,1);return}return x_(u,t),zc(e,t),t;default:return}}po(t,0,0,0,1)}}function x_(e,t){var n=(parseFloat(e[0])%360+360)%360/360,a=Qu(e[1]),r=Qu(e[2]),o=r<=.5?r*(a+1):r+a-r*a,i=r*2-o;return t=t||[],po(t,Ts(Py(i,o,n+1/3)*255),Ts(Py(i,o,n)*255),Ts(Py(i,o,n-1/3)*255),1),e.length===4&&(t[3]=e[3]),t}function uSe(e){if(e){var t=e[0]/255,n=e[1]/255,a=e[2]/255,r=Math.min(t,n,a),o=Math.max(t,n,a),i=o-r,l=(o+r)/2,s,u;if(i===0)s=0,u=0;else{l<.5?u=i/(o+r):u=i/(2-o-r);var c=((o-t)/6+i/2)/i,d=((o-n)/6+i/2)/i,v=((o-a)/6+i/2)/i;t===o?s=v-d:n===o?s=1/3+c-v:a===o&&(s=2/3+d-c),s<0&&(s+=1),s>1&&(s-=1)}var f=[s*360,u,l];return e[3]!=null&&f.push(e[3]),f}}function kk(e,t){var n=Fi(e);if(n){for(var a=0;a<3;a++)n[a]=n[a]*(1-t)|0,n[a]>255?n[a]=255:n[a]<0&&(n[a]=0);return mp(n,n.length===4?"rgba":"rgb")}}function cSe(e,t,n){if(!(!(t&&t.length)||!(e>=0&&e<=1))){var a=e*(t.length-1),r=Math.floor(a),o=Math.ceil(a),i=Fi(t[r]),l=Fi(t[o]),s=a-r,u=mp([Ts(e0(i[0],l[0],s)),Ts(e0(i[1],l[1],s)),Ts(e0(i[2],l[2],s)),S_(e0(i[3],l[3],s))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:a}:u}}function k_(e,t,n,a){var r=Fi(e);if(e)return r=uSe(r),n!=null&&(r[1]=Qu(dn(n)?n(r[1]):n)),a!=null&&(r[2]=Qu(dn(a)?a(r[2]):a)),mp(x_(r),"rgba")}function mp(e,t){if(!(!e||!e.length)){var n=e[0]+","+e[1]+","+e[2];return(t==="rgba"||t==="hsva"||t==="hsla")&&(n+=","+e[3]),t+"("+n+")"}}function Cg(e,t){var n=Fi(e);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*t:0}var Tk=new Xd(100);function Mk(e){if(Ut(e)){var t=Tk.get(e);return t||(t=kk(e,-.1),Tk.put(e,t)),t}else if(Z1(e)){var n=vt({},e);return n.colorStops=Xt(e.colorStops,function(a){return{offset:a.offset,color:kk(a.color,-.1)}}),n}return e}function dSe(e){return e.type==="linear"}function fSe(e){return e.type==="radial"}(function(){return typeof Buffer<"u"&&typeof Buffer.from=="function"?function(e){return Buffer.from(e).toString("base64")}:typeof btoa=="function"&&typeof unescape=="function"&&typeof encodeURIComponent=="function"?function(e){return btoa(unescape(encodeURIComponent(e)))}:function(e){return null}})();var T_=Array.prototype.slice;function dl(e,t,n){return(t-e)*n+e}function Dy(e,t,n,a){for(var r=t.length,o=0;oa?t:e,o=Math.min(n,a),i=r[o-1]||{color:[0,0,0,0],offset:0},l=o;li;if(l)a.length=i;else for(var s=o;s=1},e.prototype.getAdditiveTrack=function(){return this._additiveTrack},e.prototype.addKeyframe=function(t,n,a){this._needsSort=!0;var r=this.keyframes,o=r.length,i=!1,l=Ak,s=n;if(Kr(n)){var u=mSe(n);l=u,(u===1&&!Gn(n[0])||u===2&&!Gn(n[0][0]))&&(i=!0)}else if(Gn(n)&&!kh(n))l=a0;else if(Ut(n))if(!isNaN(+n))l=a0;else{var c=Fi(n);c&&(s=c,l=fv)}else if(Z1(n)){var d=vt({},s);d.colorStops=Xt(n.colorStops,function(f){return{offset:f.offset,color:Fi(f.color)}}),dSe(n)?l=M_:fSe(n)&&(l=E_),s=d}o===0?this.valType=l:(l!==this.valType||l===Ak)&&(i=!0),this.discrete=this.discrete||i;var v={time:t,value:s,rawValue:n,percent:0};return a&&(v.easing=a,v.easingFunc=dn(a)?a:Nv[a]||KO(a)),r.push(v),v},e.prototype.prepare=function(t,n){var a=this.keyframes;this._needsSort&&a.sort(function(m,g){return m.time-g.time});for(var r=this.valType,o=a.length,i=a[o-1],l=this.discrete,s=r0(r),u=Ik(r),c=0;c=0&&!(i[c].percent<=n);c--);c=v(c,l-2)}else{for(c=d;cn);c++);c=v(c-1,l-2)}h=i[c+1],f=i[c]}if(f&&h){this._lastFr=c,this._lastFrP=n;var g=h.percent-f.percent,y=g===0?1:v((n-f.percent)/g,1);h.easingFunc&&(y=h.easingFunc(y));var b=a?this._additiveValue:u?Bf:t[s];if((r0(o)||u)&&!b&&(b=this._additiveValue=[]),this.discrete)t[s]=y<1?f.rawValue:h.rawValue;else if(r0(o))o===hm?Dy(b,f[r],h[r],y):vSe(b,f[r],h[r],y);else if(Ik(o)){var _=f[r],w=h[r],S=o===M_;t[s]={type:S?"linear":"radial",x:dl(_.x,w.x,y),y:dl(_.y,w.y,y),colorStops:Xt(_.colorStops,function(T,M){var E=w.colorStops[M];return{offset:dl(T.offset,E.offset,y),color:vm(Dy([],T.color,E.color,y))}}),global:w.global},S?(t[s].x2=dl(_.x2,w.x2,y),t[s].y2=dl(_.y2,w.y2,y)):t[s].r=dl(_.r,w.r,y)}else if(u)Dy(b,f[r],h[r],y),a||(t[s]=vm(b));else{var k=dl(f[r],h[r],y);a?this._additiveValue=k:t[s]=k}a&&this._addToTarget(t)}}},e.prototype._addToTarget=function(t){var n=this.valType,a=this.propName,r=this._additiveValue;n===a0?t[a]=t[a]+r:n===fv?(Fi(t[a],Bf),n0(Bf,Bf,r,1),t[a]=vm(Bf)):n===hm?n0(t[a],t[a],r,1):n===YO&&Ek(t[a],t[a],r,1)},e}(),i8=function(){function e(t,n,a,r){if(this._tracks={},this._trackKeys=[],this._maxTime=0,this._started=0,this._clip=null,this._target=t,this._loop=n,n&&r){e8("Can' use additive animation on looped animation.");return}this._additiveAnimators=r,this._allowDiscrete=a}return e.prototype.getMaxTime=function(){return this._maxTime},e.prototype.getDelay=function(){return this._delay},e.prototype.getLoop=function(){return this._loop},e.prototype.getTarget=function(){return this._target},e.prototype.changeTarget=function(t){this._target=t},e.prototype.when=function(t,n,a){return this.whenWithKeys(t,n,la(n),a)},e.prototype.whenWithKeys=function(t,n,a,r){for(var o=this._tracks,i=0;i0&&s.addKeyframe(0,fm(u),r),this._trackKeys.push(l)}s.addKeyframe(t,fm(n[l]),r)}return this._maxTime=Math.max(this._maxTime,t),this},e.prototype.pause=function(){this._clip.pause(),this._paused=!0},e.prototype.resume=function(){this._clip.resume(),this._paused=!1},e.prototype.isPaused=function(){return!!this._paused},e.prototype.duration=function(t){return this._maxTime=t,this._force=!0,this},e.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var t=this._doneCbs;if(t)for(var n=t.length,a=0;a0)){this._started=1;for(var n=this,a=[],r=this._maxTime||0,o=0;o1){var l=i.pop();o.addKeyframe(l.time,t[r]),o.prepare(this._maxTime,o.getAdditiveTrack())}}}},e}();function dd(){return new Date().getTime()}var ySe=function(e){At(t,e);function t(n){var a=e.call(this)||this;return a._running=!1,a._time=0,a._pausedTime=0,a._pauseStart=0,a._paused=!1,n=n||{},a.stage=n.stage||{},a}return t.prototype.addClip=function(n){n.animation&&this.removeClip(n),this._head?(this._tail.next=n,n.prev=this._tail,n.next=null,this._tail=n):this._head=this._tail=n,n.animation=this},t.prototype.addAnimator=function(n){n.animation=this;var a=n.getClip();a&&this.addClip(a)},t.prototype.removeClip=function(n){if(n.animation){var a=n.prev,r=n.next;a?a.next=r:this._head=r,r?r.prev=a:this._tail=a,n.next=n.prev=n.animation=null}},t.prototype.removeAnimator=function(n){var a=n.getClip();a&&this.removeClip(a),n.animation=null},t.prototype.update=function(n){for(var a=dd()-this._pausedTime,r=a-this._time,o=this._head;o;){var i=o.next,l=o.step(a,r);l&&(o.ondestroy(),this.removeClip(o)),o=i}this._time=a,n||(this.trigger("frame",r),this.stage.update&&this.stage.update())},t.prototype._startLoop=function(){var n=this;this._running=!0;function a(){n._running&&(yg(a),!n._paused&&n.update())}yg(a)},t.prototype.start=function(){this._running||(this._time=dd(),this._pausedTime=0,this._startLoop())},t.prototype.stop=function(){this._running=!1},t.prototype.pause=function(){this._paused||(this._pauseStart=dd(),this._paused=!0)},t.prototype.resume=function(){this._paused&&(this._pausedTime+=dd()-this._pauseStart,this._paused=!1)},t.prototype.clear=function(){for(var n=this._head;n;){var a=n.next;n.prev=n.next=n.animation=null,n=a}this._head=this._tail=null},t.prototype.isFinished=function(){return this._head==null},t.prototype.animate=function(n,a){a=a||{},this.start();var r=new i8(n,a.loop);return this.addAnimator(r),r},t}(rl),bSe=300,Oy=hn.domSupported,$y=function(){var e=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],t=["touchstart","touchend","touchmove"],n={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},a=Xt(e,function(r){var o=r.replace("mouse","pointer");return n.hasOwnProperty(o)?o:r});return{mouse:e,touch:t,pointer:a}}(),Lk={mouse:["mousemove","mouseup"],pointer:["pointermove","pointerup"]},Pk=!1;function A_(e){var t=e.pointerType;return t==="pen"||t==="touch"}function _Se(e){e.touching=!0,e.touchTimer!=null&&(clearTimeout(e.touchTimer),e.touchTimer=null),e.touchTimer=setTimeout(function(){e.touching=!1,e.touchTimer=null},700)}function Ry(e){e&&(e.zrByTouch=!0)}function wSe(e,t){return go(e.dom,new SSe(e,t),!0)}function jO(e,t){for(var n=t,a=!1;n&&n.nodeType!==9&&!(a=n.domBelongToZr||n!==t&&n===e.painterRoot);)n=n.parentNode;return a}var SSe=function(){function e(t,n){this.stopPropagation=lr,this.stopImmediatePropagation=lr,this.preventDefault=lr,this.type=n.type,this.target=this.currentTarget=t.dom,this.pointerType=n.pointerType,this.clientX=n.clientX,this.clientY=n.clientY}return e}(),Go={mousedown:function(e){e=go(this.dom,e),this.__mayPointerCapture=[e.zrX,e.zrY],this.trigger("mousedown",e)},mousemove:function(e){e=go(this.dom,e);var t=this.__mayPointerCapture;t&&(e.zrX!==t[0]||e.zrY!==t[1])&&this.__togglePointerCapture(!0),this.trigger("mousemove",e)},mouseup:function(e){e=go(this.dom,e),this.__togglePointerCapture(!1),this.trigger("mouseup",e)},mouseout:function(e){e=go(this.dom,e);var t=e.toElement||e.relatedTarget;jO(this,t)||(this.__pointerCapturing&&(e.zrEventControl="no_globalout"),this.trigger("mouseout",e))},wheel:function(e){Pk=!0,e=go(this.dom,e),this.trigger("mousewheel",e)},mousewheel:function(e){Pk||(e=go(this.dom,e),this.trigger("mousewheel",e))},touchstart:function(e){e=go(this.dom,e),Ry(e),this.__lastTouchMoment=new Date,this.handler.processGesture(e,"start"),Go.mousemove.call(this,e),Go.mousedown.call(this,e)},touchmove:function(e){e=go(this.dom,e),Ry(e),this.handler.processGesture(e,"change"),Go.mousemove.call(this,e)},touchend:function(e){e=go(this.dom,e),Ry(e),this.handler.processGesture(e,"end"),Go.mouseup.call(this,e),+new Date-+this.__lastTouchMoment$k||e<-$k}var lu=[],Fc=[],By=zi(),Vy=Math.abs,gp=function(){function e(){}return e.prototype.getLocalTransform=function(t){return MSe(this,t)},e.prototype.setPosition=function(t){this.x=t[0],this.y=t[1]},e.prototype.setScale=function(t){this.scaleX=t[0],this.scaleY=t[1]},e.prototype.setSkew=function(t){this.skewX=t[0],this.skewY=t[1]},e.prototype.setOrigin=function(t){this.originX=t[0],this.originY=t[1]},e.prototype.needLocalTransform=function(){return iu(this.rotation)||iu(this.x)||iu(this.y)||iu(this.scaleX-1)||iu(this.scaleY-1)||iu(this.skewX)||iu(this.skewY)},e.prototype.updateTransform=function(){var t=this.parent&&this.parent.transform,n=this.needLocalTransform(),a=this.transform;if(!(n||t)){a&&(Ok(a),this.invTransform=null);return}a=a||zi(),n?this.getLocalTransform(a):Ok(a),t&&(n?$v(a,t,a):r8(a,t)),this.transform=a,this._resolveGlobalScaleRatio(a),this.invTransform=this.invTransform||zi(),pp(this.invTransform,a)},e.prototype._resolveGlobalScaleRatio=function(t){var n=this.globalScaleRatio;if(n!=null&&n!==1){this.getGlobalScale(lu);var a=lu[0]<0?-1:1,r=lu[1]<0?-1:1,o=((lu[0]-a)*n+a)/lu[0]||0,i=((lu[1]-r)*n+r)/lu[1]||0;t[0]*=o,t[1]*=o,t[2]*=i,t[3]*=i}},e.prototype.getComputedTransform=function(){for(var t=this,n=[];t;)n.push(t),t=t.parent;for(;t=n.pop();)t.updateTransform();return this.transform},e.prototype.setLocalTransform=function(t){if(t){var n=t[0]*t[0]+t[1]*t[1],a=t[2]*t[2]+t[3]*t[3],r=Math.atan2(t[1],t[0]),o=Math.PI/2+r-Math.atan2(t[3],t[2]);a=Math.sqrt(a)*Math.cos(o),n=Math.sqrt(n),this.skewX=o,this.skewY=0,this.rotation=-r,this.x=+t[4],this.y=+t[5],this.scaleX=n,this.scaleY=a,this.originX=0,this.originY=0}},e.prototype.decomposeTransform=function(){if(this.transform){var t=this.parent,n=this.transform;t&&t.transform&&(t.invTransform=t.invTransform||zi(),$v(Fc,t.invTransform,n),n=Fc);var a=this.originX,r=this.originY;(a||r)&&(By[4]=a,By[5]=r,$v(Fc,n,By),Fc[4]-=a,Fc[5]-=r,n=Fc),this.setLocalTransform(n)}},e.prototype.getGlobalScale=function(t){var n=this.transform;return t=t||[],n?(t[0]=Math.sqrt(n[0]*n[0]+n[1]*n[1]),t[1]=Math.sqrt(n[2]*n[2]+n[3]*n[3]),n[0]<0&&(t[0]=-t[0]),n[3]<0&&(t[1]=-t[1]),t):(t[0]=1,t[1]=1,t)},e.prototype.transformCoordToLocal=function(t,n){var a=[t,n],r=this.invTransform;return r&&Po(a,a,r),a},e.prototype.transformCoordToGlobal=function(t,n){var a=[t,n],r=this.transform;return r&&Po(a,a,r),a},e.prototype.getLineScale=function(){var t=this.transform;return t&&Vy(t[0]-1)>1e-10&&Vy(t[3]-1)>1e-10?Math.sqrt(Vy(t[0]*t[3]-t[2]*t[1])):1},e.prototype.copyTransform=function(t){kg(this,t)},e.getLocalTransform=function(t,n){n=n||[];var a=t.originX||0,r=t.originY||0,o=t.scaleX,i=t.scaleY,l=t.anchorX,s=t.anchorY,u=t.rotation||0,c=t.x,d=t.y,v=t.skewX?Math.tan(t.skewX):0,f=t.skewY?Math.tan(-t.skewY):0;if(a||r||l||s){var h=a+l,m=r+s;n[4]=-h*o-v*m*i,n[5]=-m*i-f*h*o}else n[4]=n[5]=0;return n[0]=o,n[3]=i,n[1]=f*o,n[2]=v*i,u&&o8(n,n,u),n[4]+=a+c,n[5]+=r+d,n},e.initDefaultProps=function(){var t=e.prototype;t.scaleX=t.scaleY=t.globalScaleRatio=1,t.x=t.y=t.originX=t.originY=t.skewX=t.skewY=t.rotation=t.anchorX=t.anchorY=0}(),e}(),MSe=gp.getLocalTransform,Q1=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function kg(e,t){return c8e(e,t,Q1)}function Hi(e){o0||(o0=new Xd(100)),e=e||Vs;var t=o0.get(e);return t||(t={font:e,strWidthCache:new Xd(500),asciiWidthMap:null,asciiWidthMapTried:!1,stWideCharWidth:oo.measureText("国",e).width,asciiCharWidth:oo.measureText("a",e).width},o0.put(e,t)),t}var o0;function ESe(e){if(!(zy>=Rk)){e=e||Vs;for(var t=[],n=+new Date,a=0;a<=127;a++)t[a]=oo.measureText(String.fromCharCode(a),e).width;var r=+new Date-n;return r>16?zy=Rk:r>2&&zy++,t}}var zy=0,Rk=5;function XO(e,t){return e.asciiWidthMapTried||(e.asciiWidthMap=ESe(e.font),e.asciiWidthMapTried=!0),0<=t&&t<=127?e.asciiWidthMap!=null?e.asciiWidthMap[t]:e.asciiCharWidth:e.stWideCharWidth}function Wi(e,t){var n=e.strWidthCache,a=n.get(t);return a==null&&(a=oo.measureText(t,e.font).width,n.put(t,a)),a}function Nk(e,t,n,a){var r=Wi(Hi(t),e),o=e2(t),i=Zd(0,r,n),l=ec(0,o,a),s=new ln(i,l,r,o);return s}function ZO(e,t,n,a){var r=((e||"")+"").split(` +`),o=r.length;if(o===1)return Nk(r[0],t,n,a);for(var i=new ln(0,0,0,0),l=0;l=0?parseFloat(e)/100*t:parseFloat(e):e}function Tg(e,t,n){var a=t.position||"inside",r=t.distance!=null?t.distance:5,o=n.height,i=n.width,l=o/2,s=n.x,u=n.y,c="left",d="top";if(a instanceof Array)s+=mc(a[0],n.width),u+=mc(a[1],n.height),c=null,d=null;else switch(a){case"left":s-=r,u+=l,c="right",d="middle";break;case"right":s+=r+i,u+=l,d="middle";break;case"top":s+=i/2,u-=r,c="center",d="bottom";break;case"bottom":s+=i/2,u+=o+r,c="center";break;case"inside":s+=i/2,u+=l,c="center",d="middle";break;case"insideLeft":s+=r,u+=l,d="middle";break;case"insideRight":s+=i-r,u+=l,c="right",d="middle";break;case"insideTop":s+=i/2,u+=r,c="center";break;case"insideBottom":s+=i/2,u+=o-r,c="center",d="bottom";break;case"insideTopLeft":s+=r,u+=r;break;case"insideTopRight":s+=i-r,u+=r,c="right";break;case"insideBottomLeft":s+=r,u+=o-r,d="bottom";break;case"insideBottomRight":s+=i-r,u+=o-r,c="right",d="bottom";break}return e=e||{},e.x=s,e.y=u,e.align=c,e.verticalAlign=d,e}var Fy="__zr_normal__",Hy=Q1.concat(["ignore"]),ASe=zs(Q1,function(e,t){return e[t]=!0,e},{ignore:!1}),Hc={},ISe=new ln(0,0,0,0),i0=[],mm=0,t2=1,n2=function(){function e(t){this.id=AO(),this.animators=[],this.currentStates=[],this.states={},this._init(t)}return e.prototype._init=function(t){this.attr(t)},e.prototype.drift=function(t,n,a){switch(this.draggable){case"horizontal":n=0;break;case"vertical":t=0;break}var r=this.transform;r||(r=this.transform=[1,0,0,1,0,0]),r[4]+=t,r[5]+=n,this.decomposeTransform(),this.markRedraw()},e.prototype.beforeUpdate=function(){},e.prototype.afterUpdate=function(){},e.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},e.prototype.updateInnerText=function(t){var n=this._textContent;if(n&&(!n.ignore||t)){this.textConfig||(this.textConfig={});var a=this.textConfig,r=a.local,o=n.innerTransformable,i=void 0,l=void 0,s=!1;o.parent=r?this:null;var u=!1;o.copyTransform(n);var c=a.position!=null,d=a.autoOverflowArea,v=void 0;if((d||c)&&(v=ISe,a.layoutRect?v.copy(a.layoutRect):v.copy(this.getBoundingRect()),r||v.applyTransform(this.transform)),c){this.calculateTextPosition?this.calculateTextPosition(Hc,a,v):Tg(Hc,a,v),o.x=Hc.x,o.y=Hc.y,i=Hc.align,l=Hc.verticalAlign;var f=a.origin;if(f&&a.rotation!=null){var h=void 0,m=void 0;f==="center"?(h=v.width*.5,m=v.height*.5):(h=mc(f[0],v.width),m=mc(f[1],v.height)),u=!0,o.originX=-o.x+h+(r?0:v.x),o.originY=-o.y+m+(r?0:v.y)}}a.rotation!=null&&(o.rotation=a.rotation);var g=a.offset;g&&(o.x+=g[0],o.y+=g[1],u||(o.originX=-g[0],o.originY=-g[1]));var y=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={});if(d){var b=y.overflowRect=y.overflowRect||new ln(0,0,0,0);o.getLocalTransform(i0),pp(i0,i0),ln.copy(b,v),b.applyTransform(i0)}else y.overflowRect=null;var _=a.inside==null?typeof a.position=="string"&&a.position.indexOf("inside")>=0:a.inside,w=void 0,S=void 0,k=void 0;_&&this.canBeInsideText()?(w=a.insideFill,S=a.insideStroke,(w==null||w==="auto")&&(w=this.getInsideTextFill()),(S==null||S==="auto")&&(S=this.getInsideTextStroke(w),k=!0)):(w=a.outsideFill,S=a.outsideStroke,(w==null||w==="auto")&&(w=this.getOutsideFill()),(S==null||S==="auto")&&(S=this.getOutsideStroke(w),k=!0)),w=w||"#000",(w!==y.fill||S!==y.stroke||k!==y.autoStroke||i!==y.align||l!==y.verticalAlign)&&(s=!0,y.fill=w,y.stroke=S,y.autoStroke=k,y.align=i,y.verticalAlign=l,n.setDefaultTextStyle(y)),n.__dirty|=Nr,s&&n.dirtyStyle(!0)}},e.prototype.canBeInsideText=function(){return!0},e.prototype.getInsideTextFill=function(){return"#fff"},e.prototype.getInsideTextStroke=function(t){return"#000"},e.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?D_:P_},e.prototype.getOutsideStroke=function(t){var n=this.__zr&&this.__zr.getBackgroundColor(),a=typeof n=="string"&&Fi(n);a||(a=[255,255,255,1]);for(var r=a[3],o=this.__zr.isDarkMode(),i=0;i<3;i++)a[i]=a[i]*r+(o?0:255)*(1-r);return a[3]=1,mp(a,"rgba")},e.prototype.traverse=function(t,n){},e.prototype.attrKV=function(t,n){t==="textConfig"?this.setTextConfig(n):t==="textContent"?this.setTextContent(n):t==="clipPath"?this.setClipPath(n):t==="extra"?(this.extra=this.extra||{},vt(this.extra,n)):this[t]=n},e.prototype.hide=function(){this.ignore=!0,this.markRedraw()},e.prototype.show=function(){this.ignore=!1,this.markRedraw()},e.prototype.attr=function(t,n){if(typeof t=="string")this.attrKV(t,n);else if(Yt(t))for(var a=t,r=la(a),o=0;o0},e.prototype.getState=function(t){return this.states[t]},e.prototype.ensureState=function(t){var n=this.states;return n[t]||(n[t]={}),n[t]},e.prototype.clearStates=function(t){this.useState(Fy,!1,t)},e.prototype.useState=function(t,n,a,r){var o=t===Fy,i=this.hasState();if(!(!i&&o)){var l=this.currentStates,s=this.stateTransition;if(!(Pn(l,t)>=0&&(n||l.length===1))){var u;if(this.stateProxy&&!o&&(u=this.stateProxy(t)),u||(u=this.states&&this.states[t]),!u&&!o){e8("State "+t+" not exists.");return}o||this.saveCurrentToNormalState(u);var c=this._textContent,d=Bk(this,c,u,r);d&&!this.__inHover&&(this.__inHover=d),this._applyStateObj(t,u,this._normalState,n,zk(this,a,s),s);var v=this._textGuide;return c&&c.useState(t,n,a,!!d),v&&v.useState(t,n,a,!!d),o?(this.currentStates=[],this._normalState={}):n?this.currentStates.push(t):this.currentStates=[t],this._updateAnimationTargets(),this.markRedraw(),!d&&this.__inHover&&(this.__inHover=mm,this.__dirty&=~Nr),u}}},e.prototype.useStates=function(t,n,a){if(!t.length)this.clearStates();else{var r=[],o=this.currentStates,i=t.length,l=i===o.length;if(l){for(var s=0;s=0){var a=this.currentStates.slice();a.splice(n,1),this.useStates(a)}},e.prototype.replaceState=function(t,n,a){var r=this.currentStates.slice(),o=Pn(r,t),i=Pn(r,n)>=0;o>=0?i?r.splice(o,1):r[o]=n:a&&!i&&r.push(n),this.useStates(r)},e.prototype.toggleState=function(t,n){n?this.useState(t,!0):this.removeState(t)},e.prototype._mergeStates=function(t){for(var n={},a,r=0;r=0&&o.splice(i,1)}),this.animators.push(t),a&&a.animation.addAnimator(t),a&&a.wakeUp()},e.prototype.updateDuringAnimation=function(t){this.markRedraw()},e.prototype.stopAnimation=function(t,n){for(var a=this.animators,r=a.length,o=[],i=0;i0&&n.during&&o[0].during(function(h,m){n.during(m)});for(var v=0;v0||r.force&&!i.length){var M=void 0,E=void 0,A=void 0;if(l){E={},v&&(M={});for(var w=0;w<_;w++){var y=m[w];E[y]=n[y],v?M[y]=a[y]:n[y]=a[y]}}else if(v){A={};for(var w=0;w<_;w++){var y=m[w];A[y]=fm(n[y]),PSe(n,a,y)}}var S=new i8(n,!1,!1,d?ar(h,function(P){return P.targetName===t}):null);S.targetName=t,r.scope&&(S.scope=r.scope),v&&M&&S.whenWithKeys(0,M,m),A&&S.whenWithKeys(0,A,m),S.whenWithKeys(u??500,l?E:a,m).delay(c||0),e.addAnimator(S,t),i.push(S)}}function Bk(e,t,n,a){return!(n&&n.hoverLayer||a)||Vk(e)||t&&Vk(t)?mm:t2}function Vk(e){return e.type==="text"||e.type==="tspan"}function zk(e,t,n){return!t&&!e.__inHover&&n&&n.duration>0}var Ha=function(e){At(t,e);function t(n){var a=e.call(this)||this;return a.isGroup=!0,a._children=[],a.attr(n),a}return t.prototype.childrenRef=function(){return this._children},t.prototype.children=function(){return this._children.slice()},t.prototype.childAt=function(n){return this._children[n]},t.prototype.childOfName=function(n){for(var a=this._children,r=0;r=0&&(r.splice(o,0,n),this._doAdd(n))}return this},t.prototype.replace=function(n,a){var r=Pn(this._children,n);return r>=0&&this.replaceAt(a,r),this},t.prototype.replaceAt=function(n,a){var r=this._children,o=r[a];if(n&&n!==this&&n.parent!==this&&n!==o){r[a]=n,o.parent=null;var i=this.__zr;i&&o.removeSelfFromZr(i),this._doAdd(n)}return this},t.prototype._doAdd=function(n){n.parent&&n.parent.remove(n),n.parent=this;var a=this.__zr;a&&a!==n.__zr&&n.addSelfToZr(a),a&&a.refresh()},t.prototype.remove=function(n){var a=this.__zr,r=this._children,o=Pn(r,n);return o<0?this:(r.splice(o,1),n.parent=null,a&&n.removeSelfFromZr(a),a&&a.refresh(),this)},t.prototype.removeAll=function(){for(var n=this._children,a=this.__zr,r=0;r0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},e.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},e.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},e.prototype.refreshHover=function(){this._needsRefreshHover=!0},e.prototype.refreshHoverImmediately=function(){this._disposed||this._refresh({animUpdate:!1,refresh:!1,refreshHover:!0})},e.prototype.resize=function(t){this._disposed||(t=t||{},this.painter.resize(t.width,t.height),this.handler.resize())},e.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},e.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},e.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},e.prototype.setCursorStyle=function(t){this._disposed||this.handler.setCursorStyle(t)},e.prototype.findHover=function(t,n){if(!this._disposed)return this.handler.findHover(t,n)},e.prototype.on=function(t,n,a){return this._disposed||this.handler.on(t,n,a),this},e.prototype.off=function(t,n){this._disposed||this.handler.off(t,n)},e.prototype.trigger=function(t,n){this._disposed||this.handler.trigger(t,n)},e.prototype.clear=function(){if(!this._disposed){for(var t=this.storage.getRoots(),n=0;n0){if(e<=r)return i;if(e>=o)return l}else{if(e>=r)return i;if(e<=o)return l}else{if(e===r)return i;if(e===o)return l}return(e-r)/s*u+i}var sa=HSe;function HSe(e,t,n){switch(e){case"center":case"middle":e="50%";break;case"left":case"top":e="0%";break;case"right":case"bottom":e="100%";break}return R_(e,t,n)}function R_(e,t,n){return Ut(e)?WSe(e)?parseFloat(e)/100*t+(n||0):parseFloat(e):e==null?NaN:+e}function WSe(e){return!!VSe(e).match(/%$/)}function En(e,t,n){return isNaN(t)?n?""+e:+e:(t=sr(An(0,t),e$),e=(+e).toFixed(t),n?e:+e)}function l8(e){return e.sort(function(t,n){return t-n}),e}function bs(e){if(e=+e,isNaN(e))return 0;if(e>1e-14){for(var t=1,n=0;n<15;n++,t*=10)if(Dl(e*t)/t===e)return n}return KSe(e)}function KSe(e){var t=e.toString().toLowerCase(),n=t.indexOf("e"),a=n>0?+t.slice(n+1):0,r=n>0?n:t.length,o=t.indexOf("."),i=o<0?0:r-1-o;return An(0,i-a)}function USe(e,t,n){var a=Ca(e[1]-e[0]);if(!isFinite(a)||a===0)return NaN;var r=Th(2*Ca(n||1)*Ca(a))/O_,o=Th(Ca(t))/O_,i=An(0,yp(-r+o));return isFinite(i)||(i=NaN),i}function GSe(e,t){var n=zs(e,function(f,h){return f+(isNaN(h)?0:h)},0);if(n===0)return[];for(var a=Ac(10,t),r=Xt(e,function(f){return(isNaN(f)?0:f)/n*a*100}),o=a*100,i=Xt(r,function(f){return gc(f)}),l=zs(i,function(f,h){return f+h},0),s=Xt(r,function(f,h){return f-i[h]});lu&&(u=s[d],c=d);++i[c],s[c]=0,++l}return Xt(i,function(f){return f/a})}function YSe(e,t){var n=An(bs(e),bs(t)),a=e+t;return n>e$?a:En(a,n)}var Wk=Ac(2,53)-1;function t$(e){var t=zSe*2;return(e%t+t)%t}function Mg(e){return e>-Hk&&e=10&&t++,t}var a$=2;function u8(e,t){var n=s8(e),a=Ac(10,n),r=e/a,o;return t===a$?o=1:t?r<1.5?o=1:r<2.5?o=2:r<4?o=3:r<7?o=5:o=10:r<1?o=1:r<2?o=2:r<3?o=3:r<5?o=5:o=10,e=o*a,En(e,-n)}function Eg(e){var t=parseFloat(e);return t==e&&(t!==0||!Ut(e)||e.indexOf("x")<=0)?t:NaN}function qSe(e){return!isNaN(Eg(e))}function c8(){return Dl(FSe()*9)}function r$(e,t){return t===0?e:r$(t,e%t)}function Kk(e,t){return e==null?t:t==null?e:e*t/r$(e,t)}function zo(e){return e!=null&&isFinite(e)}var XSe="[ECharts] ",ZSe=typeof console<"u"&&console.warn&&console.log;function JSe(e,t,n){ZSe&&console[e](XSe+t)}function o$(e,t){JSe("error",e)}function Rr(e){throw new Error(e)}function Uk(e,t,n){return(t-e)*n+e}var i$="series\0",QSe="\0_ec_\0";function Cr(e){return e instanceof Array?e:e==null?[]:[e]}function N_(e,t,n){if(e){e[t]=e[t]||{},e.emphasis=e.emphasis||{},e.emphasis[t]=e.emphasis[t]||{};for(var a=0,r=n.length;ae[1]&&(e[1]=t))}function u$(e,t){Ol(t)&&te[1]&&(e[1]=t)}function pCe(e,t){Jd(t[0],t[1])&&(t[0]e[1]&&(e[1]=t[1]))}function Ol(e){return e!=null&&isFinite(e)}function Jd(e,t){return Ol(e)&&Ol(t)&&e<=t}function mCe(e){var t=e[1]-e[0];return isFinite(t)&&t>=0}function gCe(e){Jd(e[0],e[1])&&e[0]>e[1]&&(e[0]=e[1])}function d$(){var e="__ec_once_"+yCe++;return function(t,n){Sr(t,e)||(t[e]=1,n())}}var yCe=c8();function v8(e,t,n){var a=an(),r=0;we(e,function(o){var i=t(o),l=a.get(i)||0;n&&n(o,l),!l&&!n&&(e[r++]=o),a.set(i,l+1)}),n||(e.length=r)}function bCe(e){return e.value+""}function _Ce(e){return e+""}function wCe(e,t){return jt(t,!0)?e.seriesIndex+2:0}function f$(e,t,n){var a=e.getData().count();return{progressiveRender:n.progressiveEnabled&&t.incrementalPrepareRender&&a>=n.threshold,large:e.get("large")&&a>=e.get("largeThreshold"),modDataCount:e.get("progressiveChunkMode")==="mod"?e.getData().count():null}}function SCe(e,t){return{seriesType:e,overallReset:t}}function h8(e){return{overallReset:e}}var CCe=".",su="___EC__COMPONENT__CONTAINER___",v$="___EC__EXTENDED_CLASS___";function Oi(e){var t={main:"",sub:""};if(e){var n=e.split(CCe);t.main=n[0]||"",t.sub=n[1]||""}return t}function xCe(e){Pl(/^[a-zA-Z0-9_]+([.][a-zA-Z0-9_]+)?$/.test(e),'componentType "'+e+'" illegal')}function kCe(e){return!!(e&&e[v$])}function p8(e,t){e.$constructor=e,e.extend=function(n){var a=this,r;return TCe(a)?r=function(o){At(i,o);function i(){return o.apply(this,arguments)||this}return i}(a):(r=function(){(n.$constructor||a).apply(this,arguments)},d8e(r,this)),vt(r.prototype,n),r[v$]=!0,r.extend=this.extend,r.superCall=ACe,r.superApply=ICe,r.superClass=a,r}}function TCe(e){return dn(e)&&/^class\s/.test(Function.prototype.toString.call(e))}function h$(e,t){e.extend=t.extend}var MCe=Math.round(Math.random()*10);function ECe(e){var t=["__\0is_clz",MCe++].join("_");e.prototype[t]=!0,e.isInstance=function(n){return!!(n&&n[t])}}function ACe(e,t){for(var n=[],a=2;a=0||o&&Pn(o,s)<0)){var u=a.getShallow(s,t);u!=null&&(i[e[l][0]]=u)}}return i}}var LCe=[["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]],PCe=Eh(LCe),DCe=function(){function e(){}return e.prototype.getAreaStyle=function(t,n){return PCe(this,t,n)},e}(),V_=new Xd(50);function OCe(e){if(typeof e=="string"){var t=V_.get(e);return t&&t.image}else return e}function p$(e,t,n,a,r){if(e)if(typeof e=="string"){if(t&&t.__zrImageSrc===e||!n)return t;var o=V_.get(e),i={hostEl:n,cb:a,cbPayload:r};return o?(t=o.image,!r2(t)&&o.pending.push(i)):(t=oo.loadImage(e,Yk,Yk),t.__zrImageSrc=e,V_.put(e,t.__cachedImgObj={image:t,pending:[i]})),t}else return e;else return t}function Yk(){var e=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var t=0;t=l;u++)s-=l;var c=Wi(i,n);return c>s&&(n="",c=0),s=e-c,r.ellipsis=n,r.ellipsisWidth=c,r.contentWidth=s,r.containerWidth=e,r}function g$(e,t,n){var a=n.containerWidth,r=n.contentWidth,o=n.fontMeasureInfo;if(!a){e.textLine="",e.isTruncated=!1;return}var i=Wi(o,t);if(i<=a){e.textLine=t,e.isTruncated=!1;return}for(var l=0;;l++){if(i<=r||l>=n.maxIterations){t+=n.ellipsis;break}var s=l===0?RCe(t,r,o):i>0?Math.floor(t.length*r/i):0;t=t.substr(0,s),i=Wi(o,t)}t===""&&(t=n.placeholder),e.textLine=t,e.isTruncated=!0}function RCe(e,t,n){for(var a=0,r=0,o=e.length;rg&&f){var _=Math.floor(g/v);h=h||y.length>_,y=y.slice(0,_),b=y.length*v}if(r&&c&&m!=null)for(var w=m$(m,u,t.ellipsis,{minChar:t.truncateMinChar,placeholder:t.placeholder}),S={},k=0;kh&&Yy(o,i.substring(h,g),t,f),Yy(o,m[2],t,f,m[1]),h=Gy.lastIndex}hd){var W=o.lines.length;P>0?(E.tokens=E.tokens.slice(0,P),T(E,I,A),o.lines=o.lines.slice(0,M+1)):o.lines=o.lines.slice(0,M),o.isTruncated=o.isTruncated||o.lines.length0&&h+a.accumWidth>a.width&&(c=t.split(` +`),u=!0),a.accumWidth=h}else{var m=y$(t,s,a.width,a.breakAll,a.accumWidth);a.accumWidth=m.accumWidth+f,d=m.linesWidths,c=m.lines}}c||(c=t.split(` +`));for(var g=Hi(s),y=0;y=32&&t<=591||t>=880&&t<=4351||t>=4608&&t<=5119||t>=7680&&t<=8303}var HCe=zs(",&?/;] ".split(""),function(e,t){return e[t]=!0,e},{});function WCe(e){return FCe(e)?!!HCe[e]:!0}function y$(e,t,n,a,r){for(var o=[],i=[],l="",s="",u=0,c=0,d=Hi(t),v=0;vn:r+c+h>n){c?(l||s)&&(m?(l||(l=s,s="",u=0,c=u),o.push(l),i.push(c-u),s+=f,u+=h,l="",c=u):(s&&(l+=s,s="",u=0),o.push(l),i.push(c),l=f,c=h)):m?(o.push(s),i.push(u),s=f,u=h):(o.push(f),i.push(h));continue}c+=h,m?(s+=f,u+=h):(s&&(l+=s,s="",u=0),l+=f)}return s&&(l+=s),l&&(o.push(l),i.push(c)),o.length===1&&(c+=r),{accumWidth:c,lines:o,linesWidths:i}}function qk(e,t,n,a,r,o){if(e.baseX=n,e.baseY=a,e.outerWidth=e.outerHeight=null,!!t){var i=t.width*2,l=t.height*2;ln.set(Xk,Zd(n,i,r),ec(a,l,o),i,l),ln.intersect(t,Xk,null,Zk);var s=Zk.outIntersectRect;e.outerWidth=s.width,e.outerHeight=s.height,e.baseX=Zd(s.x,s.width,r,!0),e.baseY=ec(s.y,s.height,o,!0)}}var Xk=new ln(0,0,0,0),Zk={outIntersectRect:{},clamp:!0};function m8(e){return e!=null?e+="":e=""}function KCe(e){var t=m8(e.text),n=e.font,a=Wi(Hi(n),t),r=e2(n);return z_(e,a,r,null)}function z_(e,t,n,a){var r=new ln(Zd(e.x||0,t,e.textAlign),ec(e.y||0,n,e.textBaseline),t,n),o=a??(b$(e)?e.lineWidth:0);return o>0&&(r.x-=o/2,r.y-=o/2,r.width+=o,r.height+=o),r}function b$(e){var t=e.stroke;return t!=null&&t!=="none"&&e.lineWidth>0}var F_="__zr_style_"+Math.round(Math.random()*10),tc={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},o2={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};tc[F_]=!0;var Jk=["z","z2","invisible"],UCe=["invisible"],wp=function(e){At(t,e);function t(n){return e.call(this,n)||this}return t.prototype._init=function(n){for(var a=la(n),r=0;r1e-4){l[0]=e-n,l[1]=t-a,s[0]=e+n,s[1]=t+a;return}if(l0[0]=Zy(r)*n+e,l0[1]=Xy(r)*a+t,s0[0]=Zy(o)*n+e,s0[1]=Xy(o)*a+t,u(l,l0,s0),c(s,l0,s0),r=r%uu,r<0&&(r=r+uu),o=o%uu,o<0&&(o=o+uu),r>o&&!i?o+=uu:rr&&(u0[0]=Zy(f)*n+e,u0[1]=Xy(f)*a+t,u(l,u0,l),c(s,u0,s))}var Ln={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},cu=[],du=[],yi=[],ql=[],bi=[],_i=[],Jy=Math.min,Qy=Math.max,fu=Math.cos,vu=Math.sin,il=Math.abs,H_=Math.PI,us=H_*2,eb=typeof Float32Array<"u",Vf=[];function tb(e){var t=Math.round(e/H_*1e8)/1e8;return t%2*H_}function _$(e,t){var n=tb(e[0]);n<0&&(n+=us);var a=n-e[0],r=e[1];r+=a,!t&&r-n>=us?r=n+us:t&&n-r>=us?r=n-us:!t&&n>r?r=n+(us-tb(n-r)):t&&n0&&(this._ux=il(a/xg/t)||0,this._uy=il(a/xg/n)||0)},e.prototype.setDPR=function(t){this.dpr=t},e.prototype.setContext=function(t){this._ctx=t},e.prototype.getContext=function(){return this._ctx},e.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},e.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},e.prototype.moveTo=function(t,n){return this._drawPendingPt(),this.addData(Ln.M,t,n),this._ctx&&this._ctx.moveTo(t,n),this._x0=t,this._y0=n,this._xi=t,this._yi=n,this},e.prototype.lineTo=function(t,n){var a=il(t-this._xi),r=il(n-this._yi),o=a>this._ux||r>this._uy;if(this.addData(Ln.L,t,n),this._ctx&&o&&this._ctx.lineTo(t,n),o)this._xi=t,this._yi=n,this._pendingPtDist=0;else{var i=a*a+r*r;i>this._pendingPtDist&&(this._pendingPtX=t,this._pendingPtY=n,this._pendingPtDist=i)}return this},e.prototype.bezierCurveTo=function(t,n,a,r,o,i){return this._drawPendingPt(),this.addData(Ln.C,t,n,a,r,o,i),this._ctx&&this._ctx.bezierCurveTo(t,n,a,r,o,i),this._xi=o,this._yi=i,this},e.prototype.quadraticCurveTo=function(t,n,a,r){return this._drawPendingPt(),this.addData(Ln.Q,t,n,a,r),this._ctx&&this._ctx.quadraticCurveTo(t,n,a,r),this._xi=a,this._yi=r,this},e.prototype.arc=function(t,n,a,r,o,i){this._drawPendingPt(),Vf[0]=r,Vf[1]=o,_$(Vf,i),r=Vf[0],o=Vf[1];var l=o-r;return this.addData(Ln.A,t,n,a,a,r,l,0,i?0:1),this._ctx&&this._ctx.arc(t,n,a,r,o,i),this._xi=fu(o)*a+t,this._yi=vu(o)*a+n,this},e.prototype.arcTo=function(t,n,a,r,o){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(t,n,a,r,o),this},e.prototype.rect=function(t,n,a,r){return this._drawPendingPt(),this._ctx&&this._ctx.rect(t,n,a,r),this.addData(Ln.R,t,n,a,r),this},e.prototype.closePath=function(){this._drawPendingPt(),this.addData(Ln.Z);var t=this._ctx,n=this._x0,a=this._y0;return t&&t.closePath(),this._xi=n,this._yi=a,this},e.prototype.fill=function(t){t&&t.fill(),this.toStatic()},e.prototype.stroke=function(t){t&&t.stroke(),this.toStatic()},e.prototype.len=function(){return this._len},e.prototype.setData=function(t){if(this._saveData){var n=t.length;!(this.data&&this.data.length===n)&&eb&&(this.data=new Float32Array(n));for(var a=0;a0&&i))for(var l=0;lc.length&&(this._expandData(),c=this.data);for(var d=0;d0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},e.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var t=[],n=0;n11&&(this.data=new Float32Array(t)))}},e.prototype.getBoundingRect=function(){yi[0]=yi[1]=bi[0]=bi[1]=Number.MAX_VALUE,ql[0]=ql[1]=_i[0]=_i[1]=-Number.MAX_VALUE;var t=this.data,n=0,a=0,r=0,o=0,i;for(i=0;ia||il(_)>r||v===n-1)&&(m=Math.sqrt(b*b+_*_),o=g,i=y);break}case Ln.C:{var w=t[v++],S=t[v++],g=t[v++],y=t[v++],k=t[v++],T=t[v++];m=nSe(o,i,w,S,g,y,k,T,10),o=k,i=T;break}case Ln.Q:{var w=t[v++],S=t[v++],g=t[v++],y=t[v++];m=oSe(o,i,w,S,g,y,10),o=g,i=y;break}case Ln.A:var M=t[v++],E=t[v++],A=t[v++],I=t[v++],P=t[v++],R=t[v++],G=R+P;v+=1,h&&(l=fu(P)*A+M,s=vu(P)*I+E),m=Qy(A,I)*Jy(us,Math.abs(R)),o=fu(G)*A+M,i=vu(G)*I+E;break;case Ln.R:{l=o=t[v++],s=i=t[v++];var K=t[v++],H=t[v++];m=K*2+H*2;break}case Ln.Z:{var b=l-o,_=s-i;m=Math.sqrt(b*b+_*_),o=l,i=s;break}}m>=0&&(u[d++]=m,c+=m)}return this._pathLen=c,c},e.prototype.rebuildPath=function(t,n){var a=this.data,r=this._ux,o=this._uy,i=this._len,l,s,u,c,d,v,f=n<1,h,m,g=0,y=0,b,_=0,w,S;if(!(f&&(this._pathSegLen||this._calculateLength(),h=this._pathSegLen,m=this._pathLen,b=n*m,!b)))e:for(var k=0;k0&&(t.lineTo(w,S),_=0),T){case Ln.M:l=u=a[k++],s=c=a[k++],t.moveTo(u,c);break;case Ln.L:{d=a[k++],v=a[k++];var E=il(d-u),A=il(v-c);if(E>r||A>o){if(f){var I=h[y++];if(g+I>b){var P=(b-g)/I;t.lineTo(u*(1-P)+d*P,c*(1-P)+v*P);break e}g+=I}t.lineTo(d,v),u=d,c=v,_=0}else{var R=E*E+A*A;R>_&&(w=d,S=v,_=R)}break}case Ln.C:{var G=a[k++],K=a[k++],H=a[k++],z=a[k++],B=a[k++],W=a[k++];if(f){var I=h[y++];if(g+I>b){var P=(b-g)/I;wg(u,G,H,B,P,cu),wg(c,K,z,W,P,du),t.bezierCurveTo(cu[1],du[1],cu[2],du[2],cu[3],du[3]);break e}g+=I}t.bezierCurveTo(G,K,H,z,B,W),u=B,c=W;break}case Ln.Q:{var G=a[k++],K=a[k++],H=a[k++],z=a[k++];if(f){var I=h[y++];if(g+I>b){var P=(b-g)/I;Sg(u,G,H,P,cu),Sg(c,K,z,P,du),t.quadraticCurveTo(cu[1],du[1],cu[2],du[2]);break e}g+=I}t.quadraticCurveTo(G,K,H,z),u=H,c=z;break}case Ln.A:var N=a[k++],V=a[k++],Y=a[k++],X=a[k++],ee=a[k++],re=a[k++],ie=a[k++],le=!a[k++],ve=Y>X?Y:X,he=il(Y-X)>.001,J=ee+re,Z=!1;if(f){var I=h[y++];g+I>b&&(J=ee+re*(b-g)/I,Z=!0),g+=I}if(he&&t.ellipse?t.ellipse(N,V,Y,X,ie,ee,J,le):t.arc(N,V,ve,ee,J,le),Z)break e;M&&(l=fu(ee)*Y+N,s=vu(ee)*X+V),u=fu(J)*Y+N,c=vu(J)*X+V;break;case Ln.R:l=u=a[k],s=c=a[k+1],d=a[k++],v=a[k++];var j=a[k++],oe=a[k++];if(f){var I=h[y++];if(g+I>b){var se=b-g;t.moveTo(d,v),t.lineTo(d+Jy(se,j),v),se-=j,se>0&&t.lineTo(d+j,v+Jy(se,oe)),se-=oe,se>0&&t.lineTo(d+Qy(j-se,0),v+oe),se-=j,se>0&&t.lineTo(d,v+Qy(oe-se,0));break e}g+=I}t.rect(d,v,j,oe);break;case Ln.Z:if(f){var I=h[y++];if(g+I>b){var P=(b-g)/I;t.lineTo(u*(1-P)+l*P,c*(1-P)+s*P);break e}g+=I}t.closePath(),u=l,c=s}}},e.prototype.clone=function(){var t=new e,n=this.data;return t.data=n.slice?n.slice():Array.prototype.slice.call(n),t._len=this._len,t},e.prototype.canSave=function(){return!!this._saveData},e.CMD=Ln,e.initDefaultProps=function(){var t=e.prototype;t._saveData=!0,t._ux=0,t._uy=0,t._pendingPtDist=0,t._version=0}(),e}();function Wc(e,t,n,a,r,o,i){if(r===0)return!1;var l=r,s=0,u=e;if(i>t+l&&i>a+l||ie+l&&o>n+l||ot+d&&c>a+d&&c>o+d&&c>l+d||ce+d&&u>n+d&&u>r+d&&u>i+d||ut+u&&s>a+u&&s>o+u||se+u&&l>n+u&&l>r+u||ln||c+ur&&(r+=zf);var v=Math.atan2(s,l);return v<0&&(v+=zf),v>=a&&v<=r||v+zf>=a&&v+zf<=r}function hu(e,t,n,a,r,o){if(o>t&&o>a||or?l:0}var Xl=bc.CMD,pu=Math.PI*2,QCe=1e-4;function e5e(e,t){return Math.abs(e-t)t&&u>a&&u>o&&u>l||u1&&t5e(),f=Ua(t,a,o,l,bo[0]),v>1&&(h=Ua(t,a,o,l,bo[1]))),v===2?gt&&l>a&&l>o||l=0&&u<=1){for(var c=0,d=$r(t,a,o,u),v=0;vn||l<-n)return 0;var s=Math.sqrt(n*n-l*l);gr[0]=-s,gr[1]=s;var u=Math.abs(a-r);if(u<1e-4)return 0;if(u>=pu-1e-4){a=0,r=pu;var c=o?1:-1;return i>=gr[0]+e&&i<=gr[1]+e?c:0}if(a>r){var d=a;a=r,r=d}a<0&&(a+=pu,r+=pu);for(var v=0,f=0;f<2;f++){var h=gr[f];if(h+e>i){var m=Math.atan2(l,h),c=o?1:-1;m<0&&(m=pu+m),(m>=a&&m<=r||m+pu>=a&&m+pu<=r)&&(m>Math.PI/2&&m1&&(n||(l+=hu(s,u,c,d,a,r))),g&&(s=o[h],u=o[h+1],c=s,d=u),m){case Xl.M:c=o[h++],d=o[h++],s=c,u=d;break;case Xl.L:if(n){if(Wc(s,u,o[h],o[h+1],t,a,r))return!0}else l+=hu(s,u,o[h],o[h+1],a,r)||0;s=o[h++],u=o[h++];break;case Xl.C:if(n){if(XCe(s,u,o[h++],o[h++],o[h++],o[h++],o[h],o[h+1],t,a,r))return!0}else l+=n5e(s,u,o[h++],o[h++],o[h++],o[h++],o[h],o[h+1],a,r)||0;s=o[h++],u=o[h++];break;case Xl.Q:if(n){if(ZCe(s,u,o[h++],o[h++],o[h],o[h+1],t,a,r))return!0}else l+=a5e(s,u,o[h++],o[h++],o[h],o[h+1],a,r)||0;s=o[h++],u=o[h++];break;case Xl.A:var y=o[h++],b=o[h++],_=o[h++],w=o[h++],S=o[h++],k=o[h++];h+=1;var T=!!(1-o[h++]);v=Math.cos(S)*_+y,f=Math.sin(S)*w+b,g?(c=v,d=f):l+=hu(s,u,v,f,a,r);var M=(a-y)*w/_+y;if(n){if(JCe(y,b,w,S,S+k,T,t,M,r))return!0}else l+=r5e(y,b,w,S,S+k,T,M,r);s=Math.cos(S+k)*_+y,u=Math.sin(S+k)*w+b;break;case Xl.R:c=s=o[h++],d=u=o[h++];var E=o[h++],A=o[h++];if(v=c+E,f=d+A,n){if(Wc(c,d,v,d,t,a,r)||Wc(v,d,v,f,t,a,r)||Wc(v,f,c,f,t,a,r)||Wc(c,f,c,d,t,a,r))return!0}else l+=hu(v,d,v,f,a,r),l+=hu(c,f,c,d,a,r);break;case Xl.Z:if(n){if(Wc(s,u,c,d,t,a,r))return!0}else l+=hu(s,u,c,d,a,r);s=c,u=d;break}}return!n&&!e5e(u,d)&&(l+=hu(s,u,c,d,a,r)||0),l!==0}function o5e(e,t,n){return w$(e,0,!1,t,n)}function i5e(e,t,n,a){return w$(e,t,!0,n,a)}var S$=zn({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},tc),l5e={style:zn({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},o2.style)},nb=Q1.concat(["invisible","culling","z","z2","zlevel","parent"]),Fn=function(e){At(t,e);function t(n){return e.call(this,n)||this}return t.prototype.update=function(){var n=this;e.prototype.update.call(this);var a=this.style;if(a.decal){var r=this._decalEl=this._decalEl||new t;r.buildPath===t.prototype.buildPath&&(r.buildPath=function(s){n.buildPath(s,n.shape)}),r.silent=!0;var o=r.style;for(var i in a)o[i]!==a[i]&&(o[i]=a[i]);o.fill=a.fill?a.decal:null,o.decal=null,o.shadowColor=null,a.strokeFirst&&(o.stroke=null);for(var l=0;l.5?P_:a>.2?TSe:D_}else if(n)return D_}return P_},t.prototype.getInsideTextStroke=function(n){var a=this.style.fill;if(Ut(a)){var r=this.__zr,o=!!(r&&r.isDarkMode()),i=Cg(n,0)0))},t.prototype.hasFill=function(){var n=this.style,a=n.fill;return a!=null&&a!=="none"},t.prototype.getBoundingRect=function(){var n=this._rect,a=this.style,r=!n;if(r){var o=!1;this.path||(o=!0,this.createPathProxy());var i=this.path;(o||this.__dirty&rd)&&(i.beginPath(),this.buildPath(i,this.shape,!1),this.pathUpdated()),n=i.getBoundingRect()}if(this._rect=n,this.hasStroke()&&this.path&&this.path.len()>0){var l=this._rectStroke||(this._rectStroke=n.clone());if(this.__dirty||r){l.copy(n);var s=a.strokeNoScale?this.getLineScale():1,u=a.lineWidth;if(!this.hasFill()){var c=this.strokeContainThreshold;u=Math.max(u,c??4)}s>1e-10&&(l.width+=u/s,l.height+=u/s,l.x-=u/s/2,l.y-=u/s/2)}return l}return n},t.prototype.contain=function(n,a){var r=this.transformCoordToLocal(n,a),o=this.getBoundingRect(),i=this.style;if(n=r[0],a=r[1],o.contain(n,a)){var l=this.path;if(this.hasStroke()){var s=i.lineWidth,u=i.strokeNoScale?this.getLineScale():1;if(u>1e-10&&(this.hasFill()||(s=Math.max(s,this.strokeContainThreshold)),i5e(l,s/u,n,a)))return!0}if(this.hasFill())return o5e(l,n,a)}return!1},t.prototype.dirtyShape=function(){this.__dirty|=rd,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},t.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},t.prototype.animateShape=function(n){return this.animate("shape",n)},t.prototype.updateDuringAnimation=function(n){n==="style"?this.dirtyStyle():n==="shape"?this.dirtyShape():this.markRedraw()},t.prototype.attrKV=function(n,a){n==="shape"?this.setShape(a):e.prototype.attrKV.call(this,n,a)},t.prototype.setShape=function(n,a){var r=this.shape;return r||(r=this.shape={}),typeof n=="string"?r[n]=a:vt(r,n),this.dirtyShape(),this},t.prototype.shapeChanged=function(){return!!(this.__dirty&rd)},t.prototype.createStyle=function(n){return J1(S$,n)},t.prototype._innerSaveToNormal=function(n){e.prototype._innerSaveToNormal.call(this,n);var a=this._normalState;n.shape&&!a.shape&&(a.shape=vt({},this.shape))},t.prototype._applyStateObj=function(n,a,r,o,i,l){if(e.prototype._applyStateObj.call(this,n,a,r,o,i,l),this.__inHover!==t2){var s=!(a&&o),u;if(a&&a.shape?i?o?u=a.shape:(u=vt({},r.shape),vt(u,a.shape)):(u=vt({},o?this.shape:r.shape),vt(u,a.shape)):s&&(u=r.shape),u)if(i){this.shape=vt({},this.shape);for(var c={},d=la(u),v=0;vr&&(d=l+s,l*=r/d,s*=r/d),u+c>r&&(d=u+c,u*=r/d,c*=r/d),s+u>o&&(d=s+u,s*=o/d,u*=o/d),l+c>o&&(d=l+c,l*=o/d,c*=o/d),e.moveTo(n+l,a),e.lineTo(n+r-s,a),s!==0&&e.arc(n+r-s,a+s,s,-Math.PI/2,0),e.lineTo(n+r,a+o-u),u!==0&&e.arc(n+r-u,a+o-u,u,0,Math.PI/2),e.lineTo(n+c,a+o),c!==0&&e.arc(n+c,a+o-c,c,Math.PI/2,Math.PI),e.lineTo(n,a+l),l!==0&&e.arc(n+l,a+l,l,Math.PI,Math.PI*1.5),e.closePath()}var fd=Math.round;function C$(e,t,n){if(t){var a=t.x1,r=t.x2,o=t.y1,i=t.y2;e.x1=a,e.x2=r,e.y1=o,e.y2=i;var l=n&&n.lineWidth;return l&&(fd(a*2)===fd(r*2)&&(e.x1=e.x2=zu(a,l,!0)),fd(o*2)===fd(i*2)&&(e.y1=e.y2=zu(o,l,!0))),e}}function x$(e,t,n){if(t){var a=t.x,r=t.y,o=t.width,i=t.height;e.x=a,e.y=r,e.width=o,e.height=i;var l=n&&n.lineWidth;return l&&(e.x=zu(a,l,!0),e.y=zu(r,l,!0),e.width=Math.max(zu(a+o,l,!1)-e.x,o===0?0:1),e.height=Math.max(zu(r+i,l,!1)-e.y,i===0?0:1)),e}}function zu(e,t,n){if(!t)return e;var a=fd(e*2);return(a+fd(t))%2===0?a/2:(a+(n?1:-1))/2}var v5e=function(){function e(){this.x=0,this.y=0,this.width=0,this.height=0}return e}(),h5e={},ga=function(e){At(t,e);function t(n){return e.call(this,n)||this}return t.prototype.getDefaultShape=function(){return new v5e},t.prototype.buildPath=function(n,a){var r,o,i,l;if(this.subPixelOptimize){var s=x$(h5e,a,this.style);r=s.x,o=s.y,i=s.width,l=s.height,s.r=a.r,a=s}else r=a.x,o=a.y,i=a.width,l=a.height;a.r?f5e(n,a):n.rect(r,o,i,l)},t.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},t}(Fn);ga.prototype.type="rect";var a9={fill:"#000"},r9=2,wi={},p5e={style:zn({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},o2.style)},Er=function(e){At(t,e);function t(n){var a=e.call(this)||this;return a.type="text",a._children=[],a._defaultStyle=a9,a.attr(n),a}return t.prototype.childrenRef=function(){return this._children},t.prototype.update=function(){e.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var n=0;n0,P=0;P=0&&(G=k[R],G.align==="right");)this._placeToken(G,n,M,y,P,"right",_),E-=G.width,P-=G.width,R--;for(I+=(c-(I-g)-(b-P)-E)/2;A<=R;)G=k[A],this._placeToken(G,n,M,y,I+G.width/2,"center",_),I+=G.width,A++;y+=M}},t.prototype._placeToken=function(n,a,r,o,i,l,s){var u=a.rich[n.styleName]||{};u.text=n.text;var c=n.verticalAlign,d=o+r/2;c==="top"?d=o+n.height/2:c==="bottom"&&(d=o+r-n.height/2);var v=!n.isLineHolder&&ab(u);v&&this._renderBackground(u,a,l==="right"?i-n.width:l==="center"?i-n.width/2:i,d-n.height/2,n.width,n.height);var f=!!u.backgroundColor,h=n.textPadding;h&&(i=c9(i,l,h),d-=n.height/2-h[0]-n.innerHeight/2);var m=this._getOrCreateChild(Ag),g=m.createStyle();m.useStyle(g);var y=this._defaultStyle,b=!1,_=0,w=!1,S=u9("fill"in u?u.fill:"fill"in a?a.fill:(b=!0,y.fill)),k=s9("stroke"in u?u.stroke:"stroke"in a?a.stroke:!f&&!s&&(!y.autoStroke||b)?(_=r9,w=!0,y.stroke):null),T=u.textShadowBlur>0||a.textShadowBlur>0;g.text=n.text,g.x=i,g.y=d,T&&(g.shadowBlur=u.textShadowBlur||a.textShadowBlur||0,g.shadowColor=u.textShadowColor||a.textShadowColor||"transparent",g.shadowOffsetX=u.textShadowOffsetX||a.textShadowOffsetX||0,g.shadowOffsetY=u.textShadowOffsetY||a.textShadowOffsetY||0),g.textAlign=l,g.textBaseline="middle",g.font=n.font||Vs,g.opacity=Ju(u.opacity,a.opacity,1),i9(g,u),k&&(g.lineWidth=Ju(u.lineWidth,a.lineWidth,_),g.lineDash=jt(u.lineDash,a.lineDash),g.lineDashOffset=a.lineDashOffset||0,g.stroke=k),S&&(g.fill=S),m.setBoundingRect(z_(g,n.contentWidth,n.contentHeight,w?0:null))},t.prototype._renderBackground=function(n,a,r,o,i,l){var s=n.backgroundColor,u=n.borderWidth,c=n.borderColor,d=s&&s.image,v=s&&!d,f=n.borderRadius,h=this,m,g;if(v||n.lineHeight||u&&c){m=this._getOrCreateChild(ga),m.useStyle(m.createStyle()),m.style.fill=null;var y=m.shape;y.x=r,y.y=o,y.width=i,y.height=l,y.r=f,m.dirtyShape()}if(v){var b=m.style;b.fill=s||null,b.fillOpacity=jt(n.fillOpacity,1)}else if(d){g=this._getOrCreateChild(js),g.onload=function(){h.dirtyStyle()};var _=g.style;_.image=s.image,_.x=r,_.y=o,_.width=i,_.height=l}if(u&&c){var b=m.style;b.lineWidth=u,b.stroke=c,b.strokeOpacity=jt(n.strokeOpacity,1),b.lineDash=n.borderDash,b.lineDashOffset=n.borderDashOffset||0,m.strokeContainThreshold=0,m.hasFill()&&m.hasStroke()&&(b.strokeFirst=!0,b.lineWidth*=2)}var w=(m||g).style;w.shadowBlur=n.shadowBlur||0,w.shadowColor=n.shadowColor||"transparent",w.shadowOffsetX=n.shadowOffsetX||0,w.shadowOffsetY=n.shadowOffsetY||0,w.opacity=Ju(n.opacity,a.opacity,1)},t.makeFont=function(n){var a="";return b5e(n)&&(a=[n.fontStyle,n.fontWeight,y5e(n.fontSize),n.fontFamily||"sans-serif"].join(" ")),a&&Di(a)||n.textFont||n.font},t}(wp),m5e={left:!0,right:1,center:1},g5e={top:1,bottom:1,middle:1},o9=["fontStyle","fontWeight","fontSize","fontFamily"];function y5e(e){return typeof e=="string"&&(e.indexOf("px")!==-1||e.indexOf("rem")!==-1||e.indexOf("em")!==-1)?e:isNaN(+e)?Zw+"px":e+"px"}function i9(e,t){for(var n=0;n=0,o=!1;if(e instanceof Fn){var i=A$(e),l=r&&i.selectFill||i.normalFill,s=r&&i.selectStroke||i.normalStroke;if(Kc(l)||Kc(s)){a=a||{};var u=a.style||{};u.fill==="inherit"?(o=!0,a=vt({},a),u=vt({},u),u.fill=l):!Kc(u.fill)&&Kc(l)?(o=!0,a=vt({},a),u=vt({},u),u.fill=Mk(l)):!Kc(u.stroke)&&Kc(s)&&(o||(a=vt({},a),u=vt({},u)),u.stroke=Mk(s)),a.style=u}}if(a&&a.z2==null){o||(a=vt({},a));var c=e.z2EmphasisLift;a.z2=e.z2+(c??x5e)}return a}function I5e(e,t,n){if(n&&n.z2==null){n=vt({},n);var a=e.z2SelectLift;n.z2=e.z2+(a??k5e)}return n}function L5e(e,t,n){var a=Pn(e.currentStates,t)>=0,r=e.style.opacity,o=a?null:E5e(e,["opacity"],t,{opacity:1});n=n||{};var i=n.style||{};return i.opacity==null&&(n=vt({},n),i=vt({opacity:a?r:o.opacity*.1},i),n.style=i),n}function rb(e,t){var n=this.states[e];if(this.style){if(e==="emphasis")return A5e(this,e,t,n);if(e==="blur")return L5e(this,e,n);if(e==="select")return I5e(this,e,n)}return n}function P5e(e){e.stateProxy=rb;var t=e.getTextContent(),n=e.getTextGuideLine();t&&(t.stateProxy=rb),n&&(n.stateProxy=rb)}function m9(e,t){!R$(e,t)&&!e.__highByOuter&&Ul(e,I$)}function g9(e,t){!R$(e,t)&&!e.__highByOuter&&Ul(e,L$)}function Dg(e,t){e.__highByOuter|=1<<(t||0),Ul(e,I$)}function Og(e,t){!(e.__highByOuter&=~(1<<(t||0)))&&Ul(e,L$)}function D5e(e){Ul(e,_8)}function D$(e){Ul(e,P$)}function O$(e){Ul(e,T5e)}function $$(e){Ul(e,M5e)}function R$(e,t){return e.__highDownSilentOnTouch&&t.zrByTouch}function N$(e){var t=e.getModel(),n=[],a=[];t.eachComponent(function(r,o){var i=g8(o),l=C5e(e,o),s=r==="series";!s&&a.push(l),i.isBlured&&(l.group.traverse(function(u){P$(u)}),s&&n.push(o)),i.isBlured=!1}),we(a,function(r){r&&r.toggleBlurSeries&&r.toggleBlurSeries(n,!1,t)})}function K_(e,t,n,a){var r=a.getModel();n=n||"coordinateSystem";function o(u,c){for(var d=0;d0){var l={dataIndex:i,seriesIndex:n.seriesIndex};o!=null&&(l.dataType=o),t.push(l)}})}),t}function G_(e,t,n){B$(e,!0),Ul(e,P5e),z5e(e,t,n)}function V5e(e){B$(e,!1)}function Ah(e,t,n,a){a?V5e(e):G_(e,t,n)}function z5e(e,t,n){var a=In(e);t!=null?(a.focus=t,a.blurScope=n):a.focus&&(a.focus=null)}var b9=["emphasis","blur","select"],F5e={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function $g(e,t,n,a){n=n||"itemStyle";for(var r=0;r1&&(i*=ob(h),l*=ob(h));var m=(r===o?-1:1)*ob((i*i*(l*l)-i*i*(f*f)-l*l*(v*v))/(i*i*(f*f)+l*l*(v*v)))||0,g=m*i*f/l,y=m*-l*v/i,b=(e+n)/2+f0(d)*g-d0(d)*y,_=(t+a)/2+d0(d)*g+f0(d)*y,w=C9([1,0],[(v-g)/i,(f-y)/l]),S=[(v-g)/i,(f-y)/l],k=[(-1*v-g)/i,(-1*f-y)/l],T=C9(S,k);if(q_(S,k)<=-1&&(T=Ff),q_(S,k)>=1&&(T=0),T<0){var M=Math.round(T/Ff*1e6)/1e6;T=Ff*2+M%2*Ff}c.addData(u,b,_,i,l,w,T,d,o)}var Y5e=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig,j5e=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;function q5e(e){var t=new bc;if(!e)return t;var n=0,a=0,r=n,o=a,i,l=bc.CMD,s=e.match(Y5e);if(!s)return t;for(var u=0;uG*G+K*K&&(M=A,E=I),{cx:M,cy:E,x0:-c,y0:-d,x1:M*(r/S-1),y1:E*(r/S-1)}}function axe(e){var t;if(Lt(e)){var n=e.length;if(!n)return e;n===1?t=[e[0],e[0],0,0]:n===2?t=[e[0],e[0],e[1],e[1]]:n===3?t=e.concat(e[2]):t=e}else t=[e,e,e,e];return t}function rxe(e,t){var n,a=vv(t.r,0),r=vv(t.r0||0,0),o=a>0,i=r>0;if(!(!o&&!i)){if(o||(a=r,r=0),r>a){var l=a;a=r,r=l}var s=t.startAngle,u=t.endAngle;if(!(isNaN(s)||isNaN(u))){var c=t.cx,d=t.cy,v=!!t.clockwise,f=k9(u-s),h=f>ib&&f%ib;if(h>Uo&&(f=h),!(a>Uo))e.moveTo(c,d);else if(f>ib-Uo)e.moveTo(c+a*Gc(s),d+a*mu(s)),e.arc(c,d,a,s,u,!v),r>Uo&&(e.moveTo(c+r*Gc(u),d+r*mu(u)),e.arc(c,d,r,u,s,v));else{var m=void 0,g=void 0,y=void 0,b=void 0,_=void 0,w=void 0,S=void 0,k=void 0,T=void 0,M=void 0,E=void 0,A=void 0,I=void 0,P=void 0,R=void 0,G=void 0,K=a*Gc(s),H=a*mu(s),z=r*Gc(u),B=r*mu(u),W=f>Uo;if(W){var N=t.cornerRadius;N&&(n=axe(N),m=n[0],g=n[1],y=n[2],b=n[3]);var V=k9(a-r)/2;if(_=Si(V,y),w=Si(V,b),S=Si(V,m),k=Si(V,g),E=T=vv(_,w),A=M=vv(S,k),(T>Uo||M>Uo)&&(I=a*Gc(u),P=a*mu(u),R=r*Gc(s),G=r*mu(s),fUo){var he=Si(y,E),J=Si(b,E),Z=v0(R,G,K,H,a,he,v),j=v0(I,P,z,B,a,J,v);e.moveTo(c+Z.cx+Z.x0,d+Z.cy+Z.y0),E0&&e.arc(c+Z.cx,d+Z.cy,he,Xa(Z.y0,Z.x0),Xa(Z.y1,Z.x1),!v),e.arc(c,d,a,Xa(Z.cy+Z.y1,Z.cx+Z.x1),Xa(j.cy+j.y1,j.cx+j.x1),!v),J>0&&e.arc(c+j.cx,d+j.cy,J,Xa(j.y1,j.x1),Xa(j.y0,j.x0),!v))}else e.moveTo(c+K,d+H),e.arc(c,d,a,s,u,!v);if(!(r>Uo)||!W)e.lineTo(c+z,d+B);else if(A>Uo){var he=Si(m,A),J=Si(g,A),Z=v0(z,B,I,P,r,-J,v),j=v0(K,H,R,G,r,-he,v);e.lineTo(c+Z.cx+Z.x0,d+Z.cy+Z.y0),A0&&e.arc(c+Z.cx,d+Z.cy,J,Xa(Z.y0,Z.x0),Xa(Z.y1,Z.x1),!v),e.arc(c,d,r,Xa(Z.cy+Z.y1,Z.cx+Z.x1),Xa(j.cy+j.y1,j.cx+j.x1),v),he>0&&e.arc(c+j.cx,d+j.cy,he,Xa(j.y1,j.x1),Xa(j.y0,j.x0),!v))}else e.lineTo(c+z,d+B),e.arc(c,d,r,u,s,v)}e.closePath()}}}var oxe=function(){function e(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=Math.PI*2,this.clockwise=!0,this.cornerRadius=0}return e}(),qs=function(e){At(t,e);function t(n){return e.call(this,n)||this}return t.prototype.getDefaultShape=function(){return new oxe},t.prototype.buildPath=function(n,a){rxe(n,a)},t.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},t}(Fn);qs.prototype.type="sector";var ixe=function(){function e(){this.cx=0,this.cy=0,this.r=0,this.r0=0}return e}(),C8=function(e){At(t,e);function t(n){return e.call(this,n)||this}return t.prototype.getDefaultShape=function(){return new ixe},t.prototype.buildPath=function(n,a){var r=a.cx,o=a.cy,i=Math.PI*2;n.moveTo(r+a.r,o),n.arc(r,o,a.r,0,i,!1),n.moveTo(r+a.r0,o),n.arc(r,o,a.r0,0,i,!0)},t}(Fn);C8.prototype.type="ring";function lxe(e,t,n,a){var r=[],o=[],i=[],l=[],s,u,c,d;if(a){c=[1/0,1/0],d=[-1/0,-1/0];for(var v=0,f=e.length;v=2){if(a){var o=lxe(r,a,n,t.smoothConstraint);e.moveTo(r[0][0],r[0][1]);for(var i=r.length,l=0;l<(n?i:i-1);l++){var s=o[l*2],u=o[l*2+1],c=r[(l+1)%i];e.bezierCurveTo(s[0],s[1],u[0],u[1],c[0],c[1])}}else{e.moveTo(r[0][0],r[0][1]);for(var l=1,d=r.length;lyu[1]){if(o=!1,Na.negativeSize||a)return o;var s=h0(yu[0]-gu[1]),u=h0(gu[0]-yu[1]);lb(s,u)>m0.len()&&(s=u||!Na.bidirectional)&&(fn.scale(p0,l,-u*r),Na.useDir&&Na.calcDirMTV()))}}return o},e.prototype._getProjMinMaxOnAxis=function(t,n,a){for(var r=this._axes[t],o=this._origin,i=n[0].dot(r)+o[t],l=i,s=i,u=1;u0){var d=c.duration,v=c.delay,f=c.easing,h={duration:d,delay:v||0,easing:f,done:o,force:!!o||!!i,setToFinal:!u,scope:e,during:i};l?t.animateFrom(n,h):t.animateTo(n,h)}else t.stopAnimation(),!l&&t.attr(n),i&&i(1),o&&o()}function Ar(e,t,n,a,r,o){T8("update",e,t,n,a,r,o)}function Do(e,t,n,a,r,o){T8("enter",e,t,n,a,r,o)}function zv(e){if(!e.__zr)return!0;for(var t=0;tCa(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function E9(e){return!e.isGroup}function Dxe(e){return e.shape!=null}function Q$(e,t,n){if(!e||!t)return;function a(i){var l={};return i.traverse(function(s){E9(s)&&s.anid&&(l[s.anid]=s)}),l}function r(i){var l={x:i.x,y:i.y,rotation:i.rotation};return Dxe(i)&&(l.shape=yn(i.shape)),l}var o=a(e);t.traverse(function(i){if(E9(i)&&i.anid){var l=o[i.anid];if(l){var s=r(i);i.attr(r(l)),Ar(i,s,n,In(i).dataIndex)}}})}function Oxe(e,t){return Xt(e,function(n){var a=n[0];a=An(a,t.x),a=sr(a,t.x+t.width);var r=n[1];return r=An(r,t.y),r=sr(r,t.y+t.height),[a,r]})}function $xe(e,t){var n=An(e.x,t.x),a=sr(e.x+e.width,t.x+t.width),r=An(e.y,t.y),o=sr(e.y+e.height,t.y+t.height);if(a>=n&&o>=r)return{x:n,y:r,width:a-n,height:o-r}}function L8(e,t,n){var a=vt({rectHover:!0},t),r=a.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},e)return e.indexOf("image://")===0?(r.image=e.slice(8),zn(r,n),new js(a)):A8(e.replace("path://",""),a,n,"center")}function Rxe(e,t,n,a,r){for(var o=0,i=r[r.length-1];o1)return!1;var g=sb(f,h,c,d)/v;return!(g<0||g>1)}function sb(e,t,n,a){return e*a-n*t}function Nxe(e){return e<=1e-6&&e>=-1e-6}function Ng(e,t,n,a,r){return t==null||(Gn(t)?Jn[0]=Jn[1]=Jn[2]=Jn[3]=t:(Jn[0]=t[0],Jn[1]=t[1],Jn[2]=t[2],Jn[3]=t[3]),a&&(Jn[0]=An(0,Jn[0]),Jn[1]=An(0,Jn[1]),Jn[2]=An(0,Jn[2]),Jn[3]=An(0,Jn[3])),n&&(Jn[0]=-Jn[0],Jn[1]=-Jn[1],Jn[2]=-Jn[2],Jn[3]=-Jn[3]),A9(e,Jn,"x","width",3,1,r&&r[0]||0),A9(e,Jn,"y","height",0,2,r&&r[1]||0)),e}var Jn=[0,0,0,0];function A9(e,t,n,a,r,o,i){var l=t[o]+t[r],s=e[a];e[a]+=l,i=An(0,sr(i,s)),e[a]=0?-t[r]:t[o]>=0?s+t[o]:Ca(l)>1e-8?(s-i)*t[r]/l:0):e[n]-=t[r]}function d2(e){var t=e.itemTooltipOption,n=e.componentModel,a=e.itemName,r=Ut(t)?{formatter:t}:t,o=n.mainType,i=n.componentIndex,l={componentType:o,name:a,$vars:["name"]};l[o+"Index"]=i;var s=e.formatterParamsExtra;s&&we(la(s),function(c){Sr(l,c)||(l[c]=s[c],l.$vars.push(c))});var u=In(e.el);u.componentMainType=o,u.componentIndex=i,u.tooltipConfig={name:a,option:zn({content:a,encodeHTMLContent:!0,formatterParams:l},r)}}function Z_(e,t){var n;e.isGroup&&(n=t(e)),n||e.traverse(t)}function f2(e,t){if(e)if(Lt(e))for(var n=0;nt&&(t=i),it&&(n=t=0),{min:n,max:t}}function nR(e,t,n){aR(e,t,n,-1/0)}function aR(e,t,n,a){if(e.ignoreModelZ)return a;var r=e.getTextContent(),o=e.getTextGuideLine(),i=e.isGroup;if(i)for(var l=e.childrenRef(),s=0;s=0&&l.push(s)}),l}}function iR(e,t){return Bn(Bn({},e,!0),t,!0)}const ake={time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst",custom:"Custom chart",chart:"Chart"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}},rke={time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图",custom:"自定义图表",chart:"图表"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}};var Bg="ZH",O8="EN",Td=O8,_m={},$8={},lR=hn.domSupported?function(){var e=(document.documentElement.lang||navigator.language||navigator.browserLanguage||Td).toUpperCase();return e.indexOf(Bg)>-1?Bg:Td}():Td;function sR(e,t){e=e.toUpperCase(),$8[e]=new fa(t),_m[e]=t}function oke(e){if(Ut(e)){var t=_m[e.toUpperCase()]||{};return e===Bg||e===O8?yn(t):Bn(yn(t),yn(_m[Td]),!1)}else return Bn(yn(e),yn(_m[Td]),!1)}function ike(e){return $8[e]}function lke(){return $8[Td]}sR(O8,ake);sR(Bg,rke);var ske=null;function m2(){return ske}function uR(e,t){t.breakOption;var n=t.breakParsed;return n}function R8(e){var t=e.brk;return t?t.breaks:[]}function Vg(e){var t=e.brk;return t?t.hasBreaks():!1}var N8=1e3,B8=N8*60,Hv=B8*60,To=Hv*24,R9=To*365,uke={year:/({yyyy}|{yy})/,month:/({MMMM}|{MMM}|{MM}|{M})/,day:/({dd}|{d})/,hour:/({HH}|{H}|{hh}|{h})/,minute:/({mm}|{m})/,second:/({ss}|{s})/,millisecond:/({SSS}|{S})/},wm={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}"},cke="{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}",y0="{yyyy}-{MM}-{dd}",N9={year:"{yyyy}",month:"{yyyy}-{MM}",day:y0,hour:y0+" "+wm.hour,minute:y0+" "+wm.minute,second:y0+" "+wm.second,millisecond:cke},ac=["year","month","day","hour","minute","second","millisecond"],dke=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function fke(e){return!Ut(e)&&!dn(e)?vke(e):e}function vke(e){e=e||{};var t={},n=!0;return we(ac,function(a){n&&(n=e[a]==null)}),we(ac,function(a,r){var o=e[a];t[a]={};for(var i=null,l=r;l>=0;l--){var s=ac[l],u=Yt(o)&&!Lt(o)?o[s]:o,c=void 0;Lt(u)?(c=u.slice(),i=c[0]||""):Ut(u)?(i=u,c=[i]):(i==null?i=wm[a]:uke[s].test(i)||(i=t[s][s][0]+" "+i),c=[i],n&&(c[1]="{primary|"+i+"}")),t[a][s]=c}}),t}function Zl(e,t){return e+="","0000".substr(0,t-e.length)+e}function Wv(e){switch(e){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return e}}function hke(e){return e===Wv(e)}function pke(e){switch(e){case"year":case"month":return"day";case"millisecond":return"millisecond";default:return"second"}}function g2(e,t,n,a){var r=bf(e),o=r[cR(n)](),i=r[V8(n)]()+1,l=Math.floor((i-1)/3)+1,s=r[z8(n)](),u=r["get"+(n?"UTC":"")+"Day"](),c=r[F8(n)](),d=(c-1)%12+1,v=r[H8(n)](),f=r[W8(n)](),h=r[K8(n)](),m=c>=12?"pm":"am",g=m.toUpperCase(),y=a instanceof fa?a:ike(a||lR)||lke(),b=y.getModel("time"),_=b.get("month"),w=b.get("monthAbbr"),S=b.get("dayOfWeek"),k=b.get("dayOfWeekAbbr");return(t||"").replace(/{a}/g,m+"").replace(/{A}/g,g+"").replace(/{yyyy}/g,o+"").replace(/{yy}/g,Zl(o%100+"",2)).replace(/{Q}/g,l+"").replace(/{MMMM}/g,_[i-1]).replace(/{MMM}/g,w[i-1]).replace(/{MM}/g,Zl(i,2)).replace(/{M}/g,i+"").replace(/{dd}/g,Zl(s,2)).replace(/{d}/g,s+"").replace(/{eeee}/g,S[u]).replace(/{ee}/g,k[u]).replace(/{e}/g,u+"").replace(/{HH}/g,Zl(c,2)).replace(/{H}/g,c+"").replace(/{hh}/g,Zl(d+"",2)).replace(/{h}/g,d+"").replace(/{mm}/g,Zl(v,2)).replace(/{m}/g,v+"").replace(/{ss}/g,Zl(f,2)).replace(/{s}/g,f+"").replace(/{SSS}/g,Zl(h,3)).replace(/{S}/g,h+"")}function mke(e,t,n,a,r){var o=null;if(Ut(n))o=n;else if(dn(n)){var i={time:e.time,level:e.time?e.time.level:0},l=m2();l&&l.makeAxisLabelFormatterParamBreak(i,e.break),o=n(e.value,t,i)}else{var s=e.time;if(s){var u=n[s.lowerTimeUnit][s.upperTimeUnit];o=u[Math.min(s.level,u.length-1)]||""}else{var c=Sm(e.value,r);o=n[c][c][0]}}return g2(new Date(e.value),o,r,a)}function Sm(e,t){var n=bf(e),a=n[V8(t)]()+1,r=n[z8(t)](),o=n[F8(t)](),i=n[H8(t)](),l=n[W8(t)](),s=n[K8(t)](),u=s===0,c=u&&l===0,d=c&&i===0,v=d&&o===0,f=v&&r===1,h=f&&a===1;return h?"year":f?"month":v?"day":d?"hour":c?"minute":u?"second":"millisecond"}function J_(e,t,n){switch(t){case"year":e[dR(n)](0);case"month":e[fR(n)](1);case"day":e[vR(n)](0);case"hour":e[hR(n)](0);case"minute":e[pR(n)](0);case"second":e[mR(n)](0)}return e}function cR(e){return e?"getUTCFullYear":"getFullYear"}function V8(e){return e?"getUTCMonth":"getMonth"}function z8(e){return e?"getUTCDate":"getDate"}function F8(e){return e?"getUTCHours":"getHours"}function H8(e){return e?"getUTCMinutes":"getMinutes"}function W8(e){return e?"getUTCSeconds":"getSeconds"}function K8(e){return e?"getUTCMilliseconds":"getMilliseconds"}function gke(e){return e?"setUTCFullYear":"setFullYear"}function dR(e){return e?"setUTCMonth":"setMonth"}function fR(e){return e?"setUTCDate":"setDate"}function vR(e){return e?"setUTCHours":"setHours"}function hR(e){return e?"setUTCMinutes":"setMinutes"}function pR(e){return e?"setUTCSeconds":"setSeconds"}function mR(e){return e?"setUTCMilliseconds":"setMilliseconds"}function gR(e){if(!qSe(e))return Ut(e)?e:"-";var t=(e+"").split(".");return t[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(t.length>1?"."+t[1]:"")}function yR(e,t){return e=(e||"").toLowerCase().replace(/-(.)/g,function(n,a){return a.toUpperCase()}),t&&e&&(e=e.charAt(0).toUpperCase()+e.slice(1)),e}var y2=n8;function Q_(e,t,n){var a="{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}";function r(c){return c&&Di(c)?c:"-"}function o(c){return zo(c)}var i=t==="time",l=e instanceof Date;if(i||l){var s=i?bf(e):e;if(isNaN(+s)){if(l)return"-"}else return g2(s,a,n)}if(t==="ordinal")return f_(e)?r(e):Gn(e)&&o(e)?e+"":"-";var u=Eg(e);return o(u)?gR(u):f_(e)?r(e):typeof e=="boolean"?e+"":"-"}var B9=["a","b","c","d","e","f","g"],db=function(e,t){return"{"+e+(t??"")+"}"};function bR(e,t,n){Lt(t)||(t=[t]);var a=t.length;if(!a)return"";for(var r=t[0].$vars||[],o=0;o':'';var i=n.markerId||"markerX";return{renderMode:o,content:"{"+i+"|} ",style:r==="subItem"?{width:4,height:4,borderRadius:2,backgroundColor:a}:{width:10,height:10,borderRadius:5,backgroundColor:a}}}function _c(e,t){return t=t||"transparent",Ut(e)?e:Yt(e)&&e.colorStops&&(e.colorStops[0]||{}).color||t}var Cm={},fb={},U8=function(){function e(){this._normalMasterList=[],this._nonSeriesBoxMasterList=[]}return e.prototype.create=function(t,n){this._nonSeriesBoxMasterList=a(Cm),this._normalMasterList=a(fb);function a(r,o){var i=[];return we(r,function(l,s){var u=l.create(t,n);i=i.concat(u||[])}),i}},e.prototype.update=function(t,n){we(this._normalMasterList,function(a){a.update&&a.update(t,n)})},e.prototype.getCoordinateSystems=function(){return this._normalMasterList.concat(this._nonSeriesBoxMasterList)},e.register=function(t,n){if(t==="matrix"||t==="calendar"){Cm[t]=n;return}fb[t]=n},e.get=function(t){return fb[t]||Cm[t]},e}();function bke(e){return!!Cm[e]}var _ke=1,_R=2;function wke(e){wR.set(e.fullType,{getCoord2:void 0}).getCoord2=e.getCoord2}var wR=an();function SR(e){var t=e.getShallow("coord",!0),n=_ke;if(t==null){var a=wR.get(e.type);a&&a.getCoord2&&(n=_R,t=a.getCoord2(e))}return{coord:t,from:n}}var Md=0,xm=1,Ske=2;function Cke(e,t){var n=e.getShallow("coordinateSystem"),a=e.getShallow("coordinateSystemUsage",!0),r=Md;if(n){var o=e.mainType==="series";a==null&&(a=o?"data":"box"),a==="data"?(r=xm,o||(r=Md)):a==="box"&&(r=Ske,!o&&!bke(n)&&(r=Md))}return{coordSysType:n,kind:r}}function xke(e){var t=e.targetModel,n=e.coordSysType,a=e.coordSysProvider,r=e.isDefaultDataCoordSys,o=Cke(t),i=o.kind,l=o.coordSysType;if(r&&i!==xm&&(i=xm,l=n),i===Md||l!==n)return Md;var s=a(n,t);return s?(i===xm?t.coordinateSystem=s:t.boxCoordinateSystem=s,i):Md}var km=we,kke=["left","right","top","bottom","width","height"],b0=[["width","left","right"],["height","top","bottom"]];function G8(e,t,n,a,r){var o=0,i=0;a==null&&(a=1/0),r==null&&(r=1/0);var l=0;t.eachChild(function(s,u){var c=s.getBoundingRect(),d=t.childAt(u+1),v=d&&d.getBoundingRect(),f,h;if(e==="horizontal"){var m=c.width+(v?-v.x+c.x:0);f=o+m,f>a||s.newline?(o=0,f=m,i+=l+n,l=c.height):l=Math.max(l,c.height)}else{var g=c.height+(v?-v.y+c.y:0);h=i+g,h>r||s.newline?(o+=l+n,i=0,h=g,l=c.width):l=Math.max(l,c.width)}s.newline||(s.x=o,s.y=i,s.markRedraw(),e==="horizontal"?o=f+n:i=h+n)})}var Kv=G8;Sa(G8,"vertical");Sa(G8,"horizontal");function Tke(e,t){return{left:e.getShallow("left",t),top:e.getShallow("top",t),right:e.getShallow("right",t),bottom:e.getShallow("bottom",t),width:e.getShallow("width",t),height:e.getShallow("height",t)}}function Mke(e,t){var n=Y8(e,t,{enableLayoutOnlyByCenter:!0}),a=e.getBoxLayoutParams(),r,o;if(n.type===pv.point)o=n.refPoint,r=wc(a,{width:t.getWidth(),height:t.getHeight()});else{var i=e.get("center"),l=Lt(i)?i:[i,i];r=wc(a,n.refContainer),o=n.boxCoordFrom===_R?n.refPoint:[sa(l[0],r.width)+r.x,sa(l[1],r.height)+r.y]}return{viewRect:r,center:o}}function Eke(e,t){var n=Mke(e,t),a=n.viewRect,r=n.center,o=e.get("radius");Lt(o)||(o=[0,o]);var i=sa(a.width,t.getWidth()),l=sa(a.height,t.getHeight()),s=Math.min(i,l),u=sa(o[0],s/2),c=sa(o[1],s/2);return{cx:r[0],cy:r[1],r0:u,r:c,viewRect:a}}function wc(e,t,n){n=y2(n||0);var a=t.width,r=t.height,o=sa(e.left,a),i=sa(e.top,r),l=sa(e.right,a),s=sa(e.bottom,r),u=sa(e.width,a),c=sa(e.height,r),d=n[2]+n[0],v=n[1]+n[3],f=e.aspect;switch(isNaN(u)&&(u=a-l-v-o),isNaN(c)&&(c=r-s-d-i),f!=null&&(isNaN(u)&&isNaN(c)&&(f>a/r?u=a*.8:c=r*.8),isNaN(u)&&(u=f*c),isNaN(c)&&(c=u/f)),isNaN(o)&&(o=a-l-u-v),isNaN(i)&&(i=r-s-c-d),e.left||e.right){case"center":o=a/2-u/2-n[3];break;case"right":o=a-u-v;break}switch(e.top||e.bottom){case"middle":case"center":i=r/2-c/2-n[0];break;case"bottom":i=r-c-d;break}o=o||0,i=i||0,isNaN(u)&&(u=a-v-o-(l||0)),isNaN(c)&&(c=r-d-i-(s||0));var h=new ln((t.x||0)+o+n[3],(t.y||0)+i+n[0],u,c);return h.margin=n,h}var pv={rect:1,point:2};function Y8(e,t,n){var a,r,o,i=e.boxCoordinateSystem,l;if(i){var s=SR(e),u=s.coord,c=s.from;if(i.dataToLayout){o=pv.rect,l=c;var d=i.dataToLayout(u);a=d.contentRect||d.rect}else n&&n.enableLayoutOnlyByCenter&&i.dataToPoint&&(o=pv.point,l=c,r=i.dataToPoint(u))}return o==null&&(o=pv.rect),o===pv.rect&&(a||(a={x:0,y:0,width:t.getWidth(),height:t.getHeight()}),r=[a.x+a.width/2,a.y+a.height/2]),{type:o,refContainer:a,refPoint:r,boxCoordFrom:l}}function Ph(e){var t=e.layoutMode||e.constructor.layoutMode;return Yt(t)?t:t?{type:t}:null}function Hs(e,t,n){var a=n&&n.ignoreSize;!Lt(a)&&(a=[a,a]);var r=i(b0[0],0),o=i(b0[1],1);s(b0[0],e,r),s(b0[1],e,o);function i(u,c){var d={},v=0,f={},h=0,m=2;if(km(u,function(b){f[b]=e[b]}),km(u,function(b){Sr(t,b)&&(d[b]=f[b]=t[b]),l(d,b)&&v++,l(f,b)&&h++}),a[c])return l(t,u[1])?f[u[2]]=null:l(t,u[2])&&(f[u[1]]=null),f;if(h===m||!v)return f;if(v>=m)return d;for(var g=0;g=0;s--)l=Bn(l,r[s],!0);a.defaultOption=l}return a.defaultOption},t.prototype.getReferringComponents=function(n,a){var r=n+"Index",o=n+"Id";return _p(this.ecModel,n,{index:this.get(r,!0),id:this.get(o,!0)},a)},t.prototype.getBoxLayoutParams=function(){return Tke(this,!1)},t.prototype.getZLevelKey=function(){return""},t.prototype.setZLevel=function(n){this.option.zlevel=n},t.protoInitialize=function(){var n=t.prototype;n.type="component",n.id="",n.name="",n.mainType="",n.subType="",n.componentIndex=0}(),t}(fa);h$(Kn,fa);a2(Kn);tke(Kn);nke(Kn,Lke);function Lke(e){var t=[];return we(Kn.getClassesByMainType(e),function(n){t=t.concat(n.dependencies||n.prototype.dependencies||[])}),t=Xt(t,function(n){return Oi(n).main}),e!=="dataset"&&Pn(t,"dataset")<=0&&t.unshift("dataset"),t}var tn={color:{},darkColor:{},size:{}},va=tn.color={theme:["#5070dd","#b6d634","#505372","#ff994d","#0ca8df","#ffd10a","#fb628b","#785db0","#3fbe95"],neutral00:"#fff",neutral05:"#f4f7fd",neutral10:"#e8ebf0",neutral15:"#dbdee4",neutral20:"#cfd2d7",neutral25:"#c3c5cb",neutral30:"#b7b9be",neutral35:"#aaacb2",neutral40:"#9ea0a5",neutral45:"#929399",neutral50:"#86878c",neutral55:"#797b7f",neutral60:"#6d6e73",neutral65:"#616266",neutral70:"#54555a",neutral75:"#48494d",neutral80:"#3c3c41",neutral85:"#303034",neutral90:"#232328",neutral95:"#17171b",neutral99:"#000",accent05:"#eff1f9",accent10:"#e0e4f2",accent15:"#d0d6ec",accent20:"#c0c9e6",accent25:"#b1bbdf",accent30:"#a1aed9",accent35:"#91a0d3",accent40:"#8292cc",accent45:"#7285c6",accent50:"#6578ba",accent55:"#5c6da9",accent60:"#536298",accent65:"#4a5787",accent70:"#404c76",accent75:"#374165",accent80:"#2e3654",accent85:"#252b43",accent90:"#1b2032",accent95:"#121521",transparent:"rgba(0,0,0,0)",highlight:"rgba(255,231,130,0.8)"};vt(va,{primary:va.neutral80,secondary:va.neutral70,tertiary:va.neutral60,quaternary:va.neutral50,disabled:va.neutral20,border:va.neutral30,borderTint:va.neutral20,borderShade:va.neutral40,background:va.neutral05,backgroundTint:"rgba(234,237,245,0.5)",backgroundTransparent:"rgba(255,255,255,0)",backgroundShade:va.neutral10,shadow:"rgba(0,0,0,0.2)",shadowTint:"rgba(129,130,136,0.2)",axisLine:va.neutral70,axisLineTint:va.neutral40,axisTick:va.neutral70,axisTickMinor:va.neutral60,axisLabel:va.neutral70,axisSplitLine:va.neutral15,axisMinorSplitLine:va.neutral05});for(var bu in va)if(va.hasOwnProperty(bu)){var V9=va[bu];bu==="theme"?tn.darkColor.theme=va.theme.slice():bu==="highlight"?tn.darkColor.highlight="rgba(255,231,130,0.4)":bu.indexOf("accent")===0?tn.darkColor[bu]=k_(V9,null,function(e){return e*.5},function(e){return Math.min(1,1.3-e)}):tn.darkColor[bu]=k_(V9,null,function(e){return e*.9},function(e){return 1-Math.pow(e,1.5)})}tn.size={xxs:2,xs:5,s:10,m:15,l:20,xl:30,xxl:40,xxxl:50};var CR="";typeof navigator<"u"&&(CR=navigator.platform||"");var Yc="rgba(0, 0, 0, 0.2)",xR=tn.color.theme[0],Pke=k_(xR,null,null,.9);const kR={darkMode:"auto",colorBy:"series",color:tn.color.theme,gradientColor:[Pke,xR],aria:{decal:{decals:[{color:Yc,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:Yc,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:Yc,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:Yc,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:Yc,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:Yc,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:CR.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1};var Wa={Must:1,Might:2,Not:3},TR=Hn();function Dke(e){TR(e).datasetMap=an()}function Oke(e,t,n){var a={},r=j8(t);if(!r||!e)return a;var o=[],i=[],l=t.ecModel,s=TR(l).datasetMap,u=r.uid+"_"+n.seriesLayoutBy,c,d;e=e.slice(),we(e,function(m,g){var y=Yt(m)?m:e[g]={name:m};y.type==="ordinal"&&c==null&&(c=g,d=h(y)),a[y.name]=[]});var v=s.get(u)||s.set(u,{categoryWayDim:d,valueWayDim:0});we(e,function(m,g){var y=m.name,b=h(m);if(c==null){var _=v.valueWayDim;f(a[y],_,b),f(i,_,b),v.valueWayDim+=b}else if(c===g)f(a[y],0,b),f(o,0,b);else{var _=v.categoryWayDim;f(a[y],_,b),f(i,_,b),v.categoryWayDim+=b}});function f(m,g,y){for(var b=0;bt)return e[a];return e[n-1]}function zke(e,t,n,a,r,o,i){o=o||e;var l=t(o),s=l.paletteIdx||0,u=l.paletteNameMap=l.paletteNameMap||{};if(u.hasOwnProperty(r))return u[r];var c=i==null||!a?n:Vke(a,i);if(c=c||n,!(!c||!c.length)){var d=c[s];return r&&(u[r]=d),l.paletteIdx=(s+1)%c.length,d}}function Fke(e,t){t(e).paletteIdx=0,t(e).paletteNameMap={}}var _0,Hf,F9,H9="\0_ec_inner",Hke=1,X8=function(e){At(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.init=function(n,a,r,o,i,l){o=o||{},this.option=null,this._theme=new fa(o),this._locale=new fa(i),this._optionManager=l},t.prototype.setOption=function(n,a,r){var o=U9(a);this._optionManager.setOption(n,r,o),this._resetOption(null,o)},t.prototype.resetOption=function(n,a){return this._resetOption(n,U9(a))},t.prototype._resetOption=function(n,a){var r=!1,o=this._optionManager;if(!n||n==="recreate"){var i=o.mountOption(n==="recreate");!this.option||n==="recreate"?F9(this,i):(this.restoreData(),this._mergeOption(i,a)),r=!0}if((n==="timeline"||n==="media")&&this.restoreData(),!n||n==="recreate"||n==="timeline"){var l=o.getTimelineOption(this);l&&(r=!0,this._mergeOption(l,a))}if(!n||n==="recreate"||n==="media"){var s=o.getMediaOption(this);s.length&&we(s,function(u){r=!0,this._mergeOption(u,a)},this)}return r},t.prototype.mergeOption=function(n){this._mergeOption(n,null)},t.prototype._mergeOption=function(n,a){var r=this.option,o=this._componentsMap,i=this._componentsCount,l=[],s=an(),u=a&&a.replaceMergeMainTypeMap;Dke(this),we(n,function(d,v){d!=null&&(Kn.hasClass(v)?v&&(l.push(v),s.set(v,!0)):r[v]=r[v]==null?yn(d):Bn(r[v],d,!0))}),u&&u.each(function(d,v){Kn.hasClass(v)&&!s.get(v)&&(l.push(v),s.set(v,!0))}),Kn.topologicalTravel(l,Kn.getAllClassMainTypes(),c,this);function c(d){var v=Bke(this,d,Cr(n[d])),f=o.get(d),h=f?u&&u.get(d)?"replaceMerge":"normalMerge":"replaceAll",m=tCe(f,v,h);sCe(m,d,Kn),r[d]=null,o.set(d,null),i.set(d,0);var g=[],y=[],b=0,_;we(m,function(w,S){var k=w.existing,T=w.newOption;if(!T)k&&(k.mergeOption({},this),k.optionUpdated({},!1));else{var M=d==="series",E=Kn.getClass(d,w.keyInfo.subType,!M);if(!E)return;if(d==="tooltip"){if(_)return;_=!0}if(k&&k.constructor===E)k.name=w.keyInfo.name,k.mergeOption(T,this),k.optionUpdated(T,!1);else{var A=vt({componentIndex:S},w.keyInfo);k=new E(T,this,this,A),vt(k,A),w.brandNew&&(k.__requireNewView=!0),k.init(T,this,this),k.optionUpdated(null,!0)}}k?(g.push(k.option),y.push(k),b++):(g.push(void 0),y.push(void 0))},this),r[d]=g,o.set(d,y),i.set(d,b),d==="series"&&_0(this)}this._seriesIndices||_0(this)},t.prototype.getOption=function(){var n=yn(this.option);return we(n,function(a,r){if(Kn.hasClass(r)){for(var o=Cr(a),i=o.length,l=!1,s=i-1;s>=0;s--)o[s]&&!Mh(o[s])?l=!0:(o[s]=null,!l&&i--);o.length=i,n[r]=o}}),delete n[H9],n},t.prototype.setTheme=function(n){this._theme=new fa(n),this._resetOption("recreate",null)},t.prototype.getTheme=function(){return this._theme},t.prototype.getLocaleModel=function(){return this._locale},t.prototype.setUpdatePayload=function(n){this._payload=n},t.prototype.getUpdatePayload=function(){return this._payload},t.prototype.getComponent=function(n,a){var r=this._componentsMap.get(n);if(r){var o=r[a||0];if(o)return o;if(a==null){for(var i=0;i=t:n==="max"?e<=t:e===t}function Xke(e,t){return e.join(",")===t.join(",")}var Ko=we,Dh=Yt,G9=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function vb(e){var t=e&&e.itemStyle;if(t)for(var n=0,a=G9.length;n0?n[i-1].seriesModel:null)}),i9e(n)}})}function i9e(e){we(e,function(t,n){var a=[],r=[NaN,NaN],o=[t.stackResultDimension,t.stackedOverDimension],i=t.data,l=t.isStackedByIndex,s=t.seriesModel.get("stackStrategy")||"samesign";i.modify(o,function(u,c,d){var v=i.get(t.stackedDimension,d);if(isNaN(v))return r;var f,h;l?h=i.getRawIndex(d):f=i.get(t.stackedByDimension,d);for(var m=NaN,g=n-1;g>=0;g--){var y=e[g];if(l||(h=y.data.rawIndexOf(y.stackedByDimension,f)),h>=0){var b=y.data.getByRawIndex(y.stackResultDimension,h);if(s==="all"||s==="positive"&&b>0||s==="negative"&&b<0||s==="samesign"&&v>=0&&b>0||s==="samesign"&&v<=0&&b<0){v=YSe(v,b),m=b;break}}}return a[0]=v,a[1]=m,a})})}var b2=function(){function e(t){this.data=t.data||(t.sourceFormat===fi?{}:[]),this.sourceFormat=t.sourceFormat||M$,this.seriesLayoutBy=t.seriesLayoutBy||Ui,this.startIndex=t.startIndex||0,this.dimensionsDetectedCount=t.dimensionsDetectedCount,this.metaRawOption=t.metaRawOption;var n=this.dimensionsDefine=t.dimensionsDefine;if(n)for(var a=0;am&&(m=_)}f[0]=h,f[1]=m}},r=function(){return this._data?this._data.length/this._dimSize:0};Q9=(t={},t[cr+"_"+Ui]={pure:!0,appendData:o},t[cr+"_"+Ic]={pure:!0,appendData:function(){throw new Error('Do not support appendData when set seriesLayoutBy: "row".')}},t[Wo]={pure:!0,appendData:o},t[fi]={pure:!0,appendData:function(i){var l=this._data;we(i,function(s,u){for(var c=l[u]||(l[u]=[]),d=0;d<(s||[]).length;d++)c.push(s[d])})}},t[uo]={appendData:o},t[Ms]={persistent:!1,pure:!0,appendData:function(i){this._data=i},clean:function(){this._offset+=this.count(),this._data=null}},t);function o(i){for(var l=0;l=0&&(m=i.interpolatedValue[g])}return m!=null?m+"":""})}},e.prototype.getRawValue=function(t,n){return tf(this.getData(n),t)},e.prototype.formatTooltip=function(t,n,a){},e}();function aT(e){var t,n;return Yt(e)?e.type&&(n=e):t=e,{text:t,frag:n}}function Uv(e){return new p9e(e)}var p9e=function(){function e(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0}return e.prototype.perform=function(t){var n=this._upstream,a=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this);var o;this._plan&&!a&&(o=this._plan(this.context));var i=c(this._modBy),l=this._modDataCount||0,s=c(t&&t.modBy),u=t&&t.modDataCount||0;(i!==s||l!==u)&&(o="reset");function c(b){return!(b>=1)&&(b=1),b}var d;(this._dirty||o==="reset")&&(this._dirty=!1,d=this._doReset(a)),this._modBy=s,this._modDataCount=u;var v=t&&t.step;if(n?this._dueEnd=n._outputDueEnd:this._dueEnd=this._count?this._count(this.context):1/0,this._progress){var f=this._dueIndex,h=Math.min(v!=null?this._dueIndex+v:1/0,this._dueEnd);if(!a&&(d||f1&&a>0?l:i}};return o;function i(){return t=e?null:sr?-this._resultLT:0},e}();function BR(e){var t="",n=-1/0,a=-1/0,r=1/0,o=1/0;return e&&(e.g!=null&&(t+="G"+e.g,n=e.g),e.ge!=null&&(t+="GE"+e.ge,a=e.ge),e.l!=null&&(t+="L"+e.l,r=e.l),e.le!=null&&(t+="LE"+e.le,o=e.le)),{key:t,g:n,ge:a,l:r,le:o}}function VR(e,t){return t>e.g&&t>=e.ge&&t65535?T9e:M9e}function E9e(e){var t=e.constructor;return t===Array?e.slice():new t(e)}function iT(e,t,n,a,r){var o=HR[n||"float"];if(r){var i=e[t],l=i&&i.length;if(l!==a){for(var s=new o(a),u=0;ug[1]&&(g[1]=m)}return this._rawCount=this._count=s,{start:l,end:s}},e.prototype._initDataFromProvider=function(t,n,a){for(var r=this._provider,o=this._chunks,i=this._dimensions,l=i.length,s=this._rawExtent,u=Xt(i,function(b){return b.property}),c=0;cy[1]&&(y[1]=g)}}!r.persistent&&r.clean&&r.clean(),this._rawCount=this._count=n,this._extent=[]},e.prototype.count=function(){return this._count},e.prototype.get=function(t,n){if(!(n>=0&&n=0&&n=this._rawCount||t<0)return-1;if(!this._indices)return t;var n=this._indices,a=n[t];if(a!=null&&at)o=i-1;else return i}return-1},e.prototype.getIndices=function(){var t,n=this._indices;if(n){var a=n.constructor,r=this._count;if(a===Array){t=new a(r);for(var o=0;o=d&&b<=v||isNaN(b))&&(s[u++]=m),m++}h=!0}else if(o===2){for(var g=f[r[0]],_=f[r[1]],w=t[r[1]][0],S=t[r[1]][1],y=0;y=d&&b<=v||isNaN(b))&&(k>=w&&k<=S||isNaN(k))&&(s[u++]=m),m++}h=!0}}if(!h)if(o===1)for(var y=0;y=d&&b<=v||isNaN(b))&&(s[u++]=T)}else for(var y=0;yt[A][1])&&(M=!1)}M&&(s[u++]=n.getRawIndex(y))}return uy[1]&&(y[1]=g)}}}},e.prototype.lttbDownSample=function(t,n){var a=this.clone([t],!0),r=a._chunks,o=r[t],i=this.count(),l=0,s=Math.floor(1/n),u=this.getRawIndex(0),c,d,v,f=new(jc(this._rawCount))(Math.min((Math.ceil(i/s)+2)*2,i));f[l++]=u;for(var h=1;hc&&(c=d,v=w)}I>0&&Il&&(m=l-c);for(var g=0;gh&&(h=b,f=c+g)}var _=this.getRawIndex(d),w=this.getRawIndex(f);dc-h&&(s=c-h,l.length=s);for(var m=0;md[1]&&(d[1]=y),v[f++]=b}return o._count=f,o._indices=v,o._updateGetRawIdx(),o},e.prototype.each=function(t,n){if(this._count)for(var a=t.length,r=this._chunks,o=0,i=this.count();of&&(f=g))}return s[c]=[v,f]},e.prototype.getRawDataItem=function(t){var n=this.getRawIndex(t);if(this._provider.persistent)return this._provider.getItem(n);for(var a=[],r=this._chunks,o=0;o=0?this._indices[t]:-1},e.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},e.internalField=function(){function t(n,a,r,o){return Tm(n[o],this._dimensions[o])}mb={arrayRows:t,objectRows:function(n,a,r,o){return Tm(n[a],this._dimensions[o])},keyedColumns:t,original:function(n,a,r,o){var i=n&&(n.value==null?n:n.value);return Tm(i instanceof Array?i[o]:i,this._dimensions[o])},typedArray:function(n,a,r,o){return n[o]}}}(),e}(),A9e=function(){function e(t){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=t}return e.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},e.prototype._setLocalSource=function(t,n){this._sourceList=t,this._upstreamSignList=n,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},e.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},e.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},e.prototype._createSource=function(){this._setLocalSource([],[]);var t=this._sourceHost,n=this._getUpstreamSourceManagers(),a=!!n.length,r,o;if(S0(t)){var i=t,l=void 0,s=void 0,u=void 0;if(a){var c=n[0];c.prepareSource(),u=c.getSource(),l=u.data,s=u.sourceFormat,o=[c._getVersionSign()]}else l=i.get("data",!0),s=Ur(l)?Ms:uo,o=[];var d=this._getSourceMetaRawOption()||{},v=u&&u.metaRawOption||{},f=jt(d.seriesLayoutBy,v.seriesLayoutBy)||null,h=jt(d.sourceHeader,v.sourceHeader),m=jt(d.dimensions,v.dimensions),g=f!==v.seriesLayoutBy||!!h!=!!v.sourceHeader||m;r=g?[e3(l,{seriesLayoutBy:f,sourceHeader:h,dimensions:m},s)]:[]}else{var y=t;if(a){var b=this._applyTransform(n);r=b.sourceList,o=b.upstreamSignList}else{var _=y.get("source",!0);r=[e3(_,this._getSourceMetaRawOption(),null)],o=[]}}this._setLocalSource(r,o)},e.prototype._applyTransform=function(t){var n=this._sourceHost,a=n.get("transform",!0),r=n.get("fromTransformResult",!0);if(r!=null){var o="";t.length!==1&&lT(o)}var i,l=[],s=[];return we(t,function(u){u.prepareSource();var c=u.getSource(r||0),d="";r!=null&&!c&&lT(d),l.push(c),s.push(u._getVersionSign())}),a?i=x9e(a,l,{datasetIndex:n.componentIndex}):r!=null&&(i=[l9e(l[0])]),{sourceList:i,upstreamSignList:s}},e.prototype._isDirty=function(){if(this._dirty)return!0;for(var t=this._getUpstreamSourceManagers(),n=0;n1||n>0&&!e.noHeader;return we(e.blocks,function(r){var o=GR(r);o>=t&&(t=o+ +(a&&(!o||n3(r)&&!r.noHeader)))}),t}return 0}function D9e(e,t,n,a){var r=t.noHeader,o=$9e(GR(t)),i=[],l=t.blocks||[];Pl(!l||Lt(l)),l=l||[];var s=e.orderMode;if(t.sortBlocks&&s){l=l.slice();var u={valueAsc:"asc",valueDesc:"desc"};if(Sr(u,s)){var c=new m9e(u[s],null);l.sort(function(m,g){return c.evaluate(m.sortParam,g.sortParam)})}else s==="seriesDesc"&&l.reverse()}we(l,function(m,g){var y=t.valueFormatter,b=UR(m)(y?vt(vt({},e),{valueFormatter:y}):e,m,g>0?o.html:0,a);b!=null&&i.push(b)});var d=e.renderMode==="richText"?i.join(o.richText):a3(a,i.join(""),r?n:o.html);if(r)return d;var v=Q_(t.header,"ordinal",e.useUTC),f=KR(a,e.renderMode).nameStyle,h=WR(a);return e.renderMode==="richText"?YR(e,v,f)+o.richText+d:a3(a,'
'+Or(v)+"
"+d,n)}function O9e(e,t,n,a){var r=e.renderMode,o=t.noName,i=t.noValue,l=!t.markerType,s=t.name,u=e.useUTC,c=t.valueFormatter||e.valueFormatter||function(w){return w=Lt(w)?w:[w],Xt(w,function(S,k){return Q_(S,Lt(f)?f[k]:f,u)})};if(!(o&&i)){var d=l?"":e.markupStyleCreator.makeTooltipMarker(t.markerType,t.markerColor||tn.color.secondary,r),v=o?"":Q_(s,"ordinal",u),f=t.valueType,h=i?[]:c(t.value,t.rawDataIndex),m=!l||!o,g=!l&&o,y=KR(a,r),b=y.nameStyle,_=y.valueStyle;return r==="richText"?(l?"":d)+(o?"":YR(e,v,b))+(i?"":B9e(e,h,m,g,_)):a3(a,(l?"":d)+(o?"":R9e(v,!l,b))+(i?"":N9e(h,m,g,_)),n)}}function sT(e,t,n,a,r,o){if(e){var i=UR(e),l={useUTC:r,renderMode:n,orderMode:a,markupStyleCreator:t,valueFormatter:e.valueFormatter};return i(l,e,0,o)}}function $9e(e){return{html:L9e[e],richText:P9e[e]}}function a3(e,t,n){var a='
',r="margin: "+n+"px 0 0",o=WR(e);return'
'+t+a+"
"}function R9e(e,t,n){var a=t?"margin-left:2px":"";return''+Or(e)+""}function N9e(e,t,n,a){var r=n?"10px":"20px",o=t?"float:right;margin-left:"+r:"";return e=Lt(e)?e:[e],''+Xt(e,function(i){return Or(i)}).join("  ")+""}function YR(e,t,n){return e.markupStyleCreator.wrapRichTextStyle(t,n)}function B9e(e,t,n,a,r){var o=[r],i=a?10:20;return n&&o.push({padding:[0,0,0,i],align:"right"}),e.markupStyleCreator.wrapRichTextStyle(Lt(t)?t.join(" "):t,o)}function V9e(e,t){var n=e.getData().getItemVisual(t,"style"),a=n[e.visualDrawType];return _c(a)}function jR(e,t){var n=e.get("padding");return n??(t==="richText"?[8,10]:10)}var gb=function(){function e(){this.richTextStyles={},this._nextStyleNameId=c8()}return e.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},e.prototype.makeTooltipMarker=function(t,n,a){var r=a==="richText"?this._generateStyleName():null,o=yke({color:n,type:t,renderMode:a,markerId:r});return Ut(o)?o:(this.richTextStyles[r]=o.style,o.content)},e.prototype.wrapRichTextStyle=function(t,n){var a={};Lt(n)?we(n,function(o){return vt(a,o)}):vt(a,n);var r=this._generateStyleName();return this.richTextStyles[r]=a,"{"+r+"|"+t+"}"},e}();function z9e(e){var t=e.series,n=e.dataIndex,a=e.multipleSeries,r=t.getData(),o=r.mapDimensionsAll("defaultedTooltip"),i=o.length,l=t.getRawValue(n),s=Lt(l),u=V9e(t,n),c,d,v,f;if(i>1||s&&!i){var h=F9e(l,t,n,o,u);c=h.inlineValues,d=h.inlineValueTypes,v=h.blocks,f=h.inlineValues[0]}else if(i){var m=r.getDimensionInfo(o[0]);f=c=tf(r,n,o[0]),d=m.type}else f=c=s?l[0]:l;var g=d8(t),y=g&&t.name||"",b=r.getName(n),_=a?y:b;return Oh("section",{header:y,noHeader:a||!g,sortParam:f,blocks:[Oh("nameValue",{markerType:"item",markerColor:u,name:_,noName:!Di(_),value:c,valueType:d,rawDataIndex:r.getRawIndex(n)})].concat(v||[])})}function F9e(e,t,n,a,r){var o=t.getData(),i=zs(e,function(d,v,f){var h=o.getDimensionInfo(f);return d=d||h&&h.tooltip!==!1&&h.displayName!=null},!1),l=[],s=[],u=[];a.length?we(a,function(d){c(tf(o,n,d),d)}):we(e,c);function c(d,v){var f=o.getDimensionInfo(v);!f||f.otherDims.tooltip===!1||(i?u.push(Oh("nameValue",{markerType:"subItem",markerColor:r,name:f.displayName,value:d,valueType:f.type})):(l.push(d),s.push(f.type)))}return{inlineValues:l,inlineValueTypes:s,blocks:u}}var Jl=Hn();function C0(e,t){return e.getName(t)||e.getId(t)}var H9e="__universalTransitionEnabled",ai=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n._selectedDataIndicesMap={},n}return t.prototype.init=function(n,a,r){this.seriesIndex=this.componentIndex,this.dataTask=Uv({count:K9e,reset:U9e}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(n,r);var o=Jl(this).sourceManager=new A9e(this);o.prepareSource();var i=this.getInitialData(n,r);cT(i,this),this.dataTask.context.data=i,Jl(this).dataBeforeProcessed=i,uT(this),this._initSelectedMapFromData(i)},t.prototype.mergeDefaultAndTheme=function(n,a){var r=Ph(this),o=r?kp(n):{},i=this.subType;Kn.hasClass(i)&&(i+="Series"),Bn(n,a.getTheme().get(this.subType)),Bn(n,this.getDefaultOption()),N_(n,"label",["show"]),this.fillDataTextStyle(n.data),r&&Hs(n,o,r)},t.prototype.mergeOption=function(n,a){n=Bn(this.option,n,!0),this.fillDataTextStyle(n.data);var r=Ph(this);r&&Hs(this.option,n,r);var o=Jl(this).sourceManager;o.dirty(),o.prepareSource();var i=this.getInitialData(n,a);cT(i,this),this.dataTask.dirty(),this.dataTask.context.data=i,Jl(this).dataBeforeProcessed=i,uT(this),this._initSelectedMapFromData(i)},t.prototype.fillDataTextStyle=function(n){if(n&&!Ur(n))for(var a=["show"],r=0;r=0&&v<0)&&(d=S,v=w,f=0),w===v&&(c[f++]=g))}return c.length=f,c},t.prototype.formatTooltip=function(n,a,r){return z9e({series:this,dataIndex:n,multipleSeries:a})},t.prototype.isAnimationEnabled=function(){var n=this.ecModel;if(hn.node&&!(n&&n.ssr))return!1;var a=this.getShallow("animation");return a&&this.getData().count()>this.getShallow("animationThreshold")&&(a=!1),!!a},t.prototype.restoreData=function(){this.dataTask.dirty()},t.prototype.getColorFromPalette=function(n,a,r){var o=this.ecModel,i=q8.prototype.getColorFromPalette.call(this,n,a,r);return i||(i=o.getColorFromPalette(n,a,r)),i},t.prototype.coordDimToDataDim=function(n){return this.getRawData().mapDimensionsAll(n)},t.prototype.getProgressive=function(){return this.get("progressive")},t.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},t.prototype.select=function(n,a){this._innerSelect(this.getData(a),n)},t.prototype.unselect=function(n,a){var r=this.option.selectedMap;if(r){var o=this.option.selectedMode,i=this.getData(a);if(o==="series"||r==="all"){this.option.selectedMap={},this._selectedDataIndicesMap={};return}for(var l=0;l=0&&r.push(i)}return r},t.prototype.isSelected=function(n,a){var r=this.option.selectedMap;if(!r)return!1;var o=this.getData(a);return(r==="all"||r[C0(o,n)])&&!o.getItemModel(n).get(["select","disabled"])},t.prototype.isUniversalTransitionEnabled=function(){if(this[H9e])return!0;var n=this.option.universalTransition;return n?n===!0?!0:n&&n.enabled:!1},t.prototype._innerSelect=function(n,a){var r,o,i=this.option,l=i.selectedMode,s=a.length;if(!(!l||!s)){if(l==="series")i.selectedMap="all";else if(l==="multiple"){Yt(i.selectedMap)||(i.selectedMap={});for(var u=i.selectedMap,c=0;c0&&this._innerSelect(n,a)}},t.registerClass=function(n){return Kn.registerClass(n)},t.protoInitialize=function(){var n=t.prototype;n.type="series.__base__",n.seriesIndex=0,n.ignoreStyleOnData=!1,n.hasSymbolVisual=!1,n.defaultSymbol="circle",n.visualStyleAccessPath="itemStyle",n.visualDrawType="fill"}(),t}(Kn);al(ai,h9e);al(ai,q8);h$(ai,Kn);function uT(e){var t=e.name;d8(e)||(e.name=W9e(e)||t)}function W9e(e){var t=e.getRawData(),n=t.mapDimensionsAll("seriesName"),a=[];return we(n,function(r){var o=t.getDimensionInfo(r);o.displayName&&a.push(o.displayName)}),a.join(" ")}function K9e(e){return e.model.getRawData().count()}function U9e(e){var t=e.model;return t.setData(t.getRawData().cloneShallow()),G9e}function G9e(e,t){t.outputData&&e.end>t.outputData.count()&&t.model.getRawData().cloneShallow(t.outputData)}function cT(e,t){we(y8e(e.CHANGABLE_METHODS,e.DOWNSAMPLE_METHODS),function(n){e.wrapMethod(n,Sa(Y9e,t))})}function Y9e(e,t){var n=r3(e);return n&&n.setOutputEnd((t||this).count()),t}function r3(e){var t=(e.ecModel||{}).scheduler,n=t&&t.getPipeline(e.uid);if(n){var a=n.currentTask;if(a){var r=a.agentStubMap;r&&(a=r.get(e.uid))}return a}}var el=function(){function e(){this.group=new Ha,this.uid=p2("viewComponent")}return e.prototype.init=function(t,n){},e.prototype.render=function(t,n,a,r){},e.prototype.dispose=function(t,n){},e.prototype.updateView=function(t,n,a,r){},e.prototype.updateLayout=function(t,n,a,r){},e.prototype.updateVisual=function(t,n,a,r){},e.prototype.toggleBlurSeries=function(t,n,a){},e.prototype.eachRendered=function(t){var n=this.group;n&&n.traverse(t)},e}();p8(el);a2(el);function eS(){var e=Hn();return function(t){var n=e(t),a=t.pipelineContext,r=!!n.large,o=!!n.progressiveRender,i=n.large=!!(a&&a.large),l=n.progressiveRender=!!(a&&a.progressiveRender);return(r!==i||o!==l)&&"reset"}}var qR=Hn(),j9e=eS(),Oo=function(){function e(){this.group=new Ha,this.uid=p2("viewChart"),this.renderTask=Uv({plan:q9e,reset:X9e}),this.renderTask.context={view:this}}return e.prototype.init=function(t,n){},e.prototype.render=function(t,n,a,r){},e.prototype.highlight=function(t,n,a,r){var o=t.getData(r&&r.dataType);o&&fT(o,r,"emphasis")},e.prototype.downplay=function(t,n,a,r){var o=t.getData(r&&r.dataType);o&&fT(o,r,"normal")},e.prototype.remove=function(t,n){this.group.removeAll()},e.prototype.dispose=function(t,n){},e.prototype.updateView=function(t,n,a,r){this.render(t,n,a,r)},e.prototype.updateVisual=function(t,n,a,r){this.render(t,n,a,r)},e.prototype.eachRendered=function(t){f2(this.group,t)},e.markUpdateMethod=function(t,n){qR(t).updateMethod=n},e.protoInitialize=function(){var t=e.prototype;t.type="chart"}(),e}();function dT(e,t,n){e&&Y_(e)&&(t==="emphasis"?Dg:Og)(e,n)}function fT(e,t,n){var a=yc(e,t),r=t&&t.highlightKey!=null?H5e(t.highlightKey):null;a!=null?we(Cr(a),function(o){dT(e.getItemGraphicEl(o),n,r)}):e.eachItemGraphicEl(function(o){dT(o,n,r)})}p8(Oo);a2(Oo);function q9e(e){return j9e(e.model)}function X9e(e){var t=e.model,n=e.ecModel,a=e.api,r=e.payload,o=t.pipelineContext.progressiveRender,i=e.view,l=r&&qR(r).updateMethod,s=o?"incrementalPrepareRender":l&&i[l]?l:"render";return s!=="render"&&i[s](t,n,a,r),Z9e[s]}var Z9e={incrementalPrepareRender:{progress:function(e,t){t.view.incrementalRender(e,t.model,t.ecModel,t.api,t.payload)}},render:{forceFirstProgress:!0,progress:function(e,t){t.view.render(t.model,t.ecModel,t.api,t.payload)}}},zg="\0__throttleOriginMethod",vT="\0__throttleRate",hT="\0__throttleType";function tS(e,t,n){var a,r=0,o=0,i=null,l,s,u,c;t=t||0;function d(){o=new Date().getTime(),i=null,e.apply(s,u||[])}var v=function(){for(var f=[],h=0;h=0?d():i=setTimeout(d,-l),r=a};return v.clear=function(){i&&(clearTimeout(i),i=null)},v.debounceNextCall=function(f){c=f},v}function XR(e,t,n,a){var r=e[t];if(r){var o=r[zg]||r,i=r[hT],l=r[vT];if(l!==n||i!==a){if(n==null||!a)return e[t]=o;r=e[t]=tS(o,n,a==="debounce"),r[zg]=o,r[hT]=a,r[vT]=n}return r}}function o3(e,t){var n=e[t];n&&n[zg]&&(n.clear&&n.clear(),e[t]=n[zg])}var pT=Hn(),mT={itemStyle:Eh(oR,!0),lineStyle:Eh(rR,!0)},J9e={lineStyle:"stroke",itemStyle:"fill"};function ZR(e,t){var n=e.visualStyleMapper||mT[t];return n||(console.warn("Unknown style type '"+t+"'."),mT.itemStyle)}function JR(e,t){var n=e.visualDrawType||J9e[t];return n||(console.warn("Unknown style type '"+t+"'."),"fill")}var Q9e={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){var n=e.getData(),a=e.visualStyleAccessPath||"itemStyle",r=e.getModel(a),o=ZR(e,a),i=o(r),l=r.getShallow("decal");l&&(n.setVisual("decal",l),l.dirty=!0);var s=JR(e,a),u=i[s],c=dn(u)?u:null,d=i.fill==="auto"||i.stroke==="auto";if(!i[s]||c||d){var v=e.getColorFromPalette(e.name,null,t.getSeriesCount());i[s]||(i[s]=v,n.setVisual("colorFromPalette",!0)),i.fill=i.fill==="auto"||dn(i.fill)?v:i.fill,i.stroke=i.stroke==="auto"||dn(i.stroke)?v:i.stroke}if(n.setVisual("style",i),n.setVisual("drawType",s),!t.isSeriesFiltered(e)&&c)return n.setVisual("colorFromPalette",!1),{dataEach:function(f,h){var m=e.getDataParams(h),g=vt({},i);g[s]=c(m),f.setItemVisual(h,"style",g)}}}},Kf=new fa,eTe={createOnAllSeries:!0,reset:function(e,t){if(!e.ignoreStyleOnData){var n=e.getData(),a=e.visualStyleAccessPath||"itemStyle",r=ZR(e,a),o=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(i,l){var s=i.getRawDataItem(l);if(s&&s[a]){Kf.option=s[a];var u=r(Kf),c=i.ensureUniqueItemVisual(l,"style");vt(c,u),Kf.option.decal&&(i.setItemVisual(l,"decal",Kf.option.decal),Kf.option.decal.dirty=!0),o in u&&i.setItemVisual(l,"colorFromPalette",!1)}}:null}}}},tTe={performRawSeries:!0,overallReset:function(e){var t=an();e.eachSeries(function(n){if(!n.isColorBySeries()){var a=n.type+"-"+n.getColorBy();pT(n).scope=t.get(a)||t.set(a,{})}}),e.eachSeries(function(n){if(!n.isColorBySeries()){var a=n.getRawData(),r={},o=n.getData(),i=pT(n).scope,l=n.visualStyleAccessPath||"itemStyle",s=JR(n,l);o.each(function(u){var c=o.getRawIndex(u);r[c]=u}),a.each(function(u){var c=r[u],d=o.getItemVisual(c,"colorFromPalette");if(d){var v=o.ensureUniqueItemVisual(c,"style"),f=a.getName(u)||u+"",h=a.count();v[s]=n.getColorFromPalette(f,i,h)}})}})}},x0=Math.PI;function nTe(e,t){t=t||{},zn(t,{text:"loading",textColor:tn.color.primary,fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255,255,255,0.8)",showSpinner:!0,color:tn.color.theme[0],spinnerRadius:10,lineWidth:5,zlevel:0});var n=new Ha,a=new ga({style:{fill:t.maskColor},zlevel:t.zlevel,z:1e4});n.add(a);var r=new Er({style:{text:t.text,fill:t.textColor,fontSize:t.fontSize,fontWeight:t.fontWeight,fontStyle:t.fontStyle,fontFamily:t.fontFamily},zlevel:t.zlevel,z:10001}),o=new ga({style:{fill:"none"},textContent:r,textConfig:{position:"right",distance:10},zlevel:t.zlevel,z:10001});n.add(o);var i;return t.showSpinner&&(i=new c2({shape:{startAngle:-x0/2,endAngle:-x0/2+.1,r:t.spinnerRadius},style:{stroke:t.color,lineCap:"round",lineWidth:t.lineWidth},zlevel:t.zlevel,z:10001}),i.animateShape(!0).when(1e3,{endAngle:x0*3/2}).start("circularInOut"),i.animateShape(!0).when(1e3,{startAngle:x0*3/2}).delay(300).start("circularInOut"),n.add(i)),n.resize=function(){var l=r.getBoundingRect().width,s=t.showSpinner?t.spinnerRadius:0,u=(e.getWidth()-s*2-(t.showSpinner&&l?10:0)-l)/2-(t.showSpinner&&l?0:5+l/2)+(t.showSpinner?0:l/2)+(l?0:s),c=e.getHeight()/2;t.showSpinner&&i.setShape({cx:u,cy:c}),o.setShape({x:u-s,y:c-s,width:s*2,height:s*2}),a.setShape({x:0,y:0,width:e.getWidth(),height:e.getHeight()})},n.resize(),n}var QR=function(){function e(t,n,a,r){this._stageTaskMap=an(),this.ecInstance=t,this.api=n,a=this._dataProcessorHandlers=a.slice(),r=this._visualHandlers=r.slice(),this._allHandlers=a.concat(r)}return e.prototype.restoreData=function(t,n){t.restoreData(n),this._stageTaskMap.each(function(a){var r=a.overallTask;r&&r.dirty()})},e.prototype.getPerformArgs=function(t,n){if(t.__pipeline){var a=this._pipelineMap.get(t.__pipeline.id),r=a.context,o=!n&&a.progressiveEnabled&&(!r||r.progressiveRender)&&t.__idxInPipeline>a.blockIndex,i=o?a.step:null,l=r&&r.modDataCount,s=l!=null?Math.ceil(l/i):null;return{step:i,modBy:s,modDataCount:l}}},e.prototype.getPipeline=function(t){return this._pipelineMap.get(t)},e.prototype.updateStreamModes=function(t,n){var a=this._pipelineMap.get(t.uid),r=t.__preparePipelineContext?t.__preparePipelineContext(n,a):f$(t,n,a);t.pipelineContext=a.context=r},e.prototype.restorePipelines=function(t,n){var a=this,r=a._pipelineMap=an();n.eachSeries(function(o){var i=t.painter.type==="canvas"&&o.getProgressive(),l=o.uid;r.set(l,{id:l,head:null,tail:null,threshold:o.getProgressiveThreshold(),progressiveEnabled:i&&!(o.preventIncremental&&o.preventIncremental()),blockIndex:-1,step:Math.round(i||700),count:0}),a._pipe(o,o.dataTask)})},e.prototype.prepareStageTasks=function(){var t=this._stageTaskMap,n=this.api.getModel(),a=this.api;we(this._allHandlers,function(r){var o=t.get(r.uid)||t.set(r.uid,{}),i="";Pl(!(r.reset&&r.overallReset),i),r.reset&&this._createSeriesStageTask(r,o,n,a),r.overallReset&&this._createOverallStageTask(r,o,n,a)},this)},e.prototype.prepareView=function(t,n,a,r){var o=t.renderTask,i=o.context;i.model=n,i.ecModel=a,i.api=r,o.__block=!t.incrementalPrepareRender,this._pipe(n,o)},e.prototype.performDataProcessorTasks=function(t,n){this._performStageTasks(this._dataProcessorHandlers,t,n,{block:!0})},e.prototype.performVisualTasks=function(t,n,a){this._performStageTasks(this._visualHandlers,t,n,a)},e.prototype._performStageTasks=function(t,n,a,r){r=r||{};var o=!1,i=this;we(t,function(s,u){if(!(r.visualType&&r.visualType!==s.visualType)){var c=i._stageTaskMap.get(s.uid),d=c.seriesTaskMap,v=c.overallTask;if(v){var f,h=v.agentStubMap;h.each(function(g){l(r,g)&&(g.dirty(),f=!0)}),f&&v.dirty(),i.updatePayload(v,a);var m=i.getPerformArgs(v,r.block);h.each(function(g){g.perform(m)}),v.perform(m)&&(o=!0)}else d&&d.each(function(g,y){l(r,g)&&g.dirty();var b=i.getPerformArgs(g,r.block);b.skip=!s.performRawSeries&&n.isSeriesFiltered(g.context.model),i.updatePayload(g,a),g.perform(b)&&(o=!0)})}});function l(s,u){return s.setDirty&&(!s.dirtyMap||s.dirtyMap.get(u.__pipeline.id))}this.unfinished=o||this.unfinished},e.prototype.performSeriesTasks=function(t){var n;t.eachSeries(function(a){n=a.dataTask.perform()||n}),this.unfinished=n||this.unfinished},e.prototype.plan=function(){this._pipelineMap.each(function(t){var n=t.tail;do{if(n.__block){t.blockIndex=n.__idxInPipeline;break}n=n.getUpstream()}while(n)})},e.prototype.updatePayload=function(t,n){n!=="remain"&&(t.context.payload=n)},e.prototype._createSeriesStageTask=function(t,n,a,r){var o=this,i=n.seriesTaskMap,l=n.seriesTaskMap=an(),s=t.seriesType,u=t.getTargetSeries;t.createOnAllSeries?a.eachRawSeries(c):s?a.eachRawSeriesByType(s,c):u&&u(a,r).each(c);function c(d){var v=d.uid,f=l.set(v,i&&i.get(v)||Uv({plan:lTe,reset:sTe,count:cTe}));f.context={model:d,ecModel:a,api:r,useClearVisual:t.isVisual&&!t.isLayout,plan:t.plan,reset:t.reset,scheduler:o},o._pipe(d,f)}},e.prototype._createOverallStageTask=function(t,n,a,r){var o=this,i=n.overallTask=n.overallTask||Uv({reset:aTe});i.context={ecModel:a,api:r,overallReset:t.overallReset,scheduler:o};var l=i.agentStubMap,s=i.agentStubMap=an(),u=t.seriesType,c=t.getTargetSeries,d=t.dirtyOnOverallProgress,v=!1,f="";Pl(!t.createOnAllSeries,f),u?a.eachRawSeriesByType(u,h):c?c(a,r).each(h):we(a.getSeries(),h);function h(m){var g=m.uid,y=s.set(g,l&&l.get(g)||(v=!0,Uv({reset:rTe,onDirty:iTe})));y.context={model:m,dirtyOnOverallProgress:d},y.agent=i,y.__block=d,o._pipe(m,y)}v&&i.dirty()},e.prototype._pipe=function(t,n){var a=t.uid,r=this._pipelineMap.get(a);!r.head&&(r.head=n),r.tail&&r.tail.pipe(n),r.tail=n,n.__idxInPipeline=r.count++,n.__pipeline=r},e.wrapStageHandler=function(t,n){return dn(t)&&(t={overallReset:t,seriesType:dTe(t)}),t.uid=p2("stageHandler"),n&&(t.visualType=n),t},e}();function aTe(e){e.overallReset(e.ecModel,e.api,e.payload)}function rTe(e){return e.dirtyOnOverallProgress&&oTe}function oTe(){this.agent.dirty(),this.getDownstream().dirty()}function iTe(){this.agent&&this.agent.dirty()}function lTe(e){return e.plan?e.plan(e.model,e.ecModel,e.api,e.payload):null}function sTe(e){e.useClearVisual&&e.data.clearAllVisual();var t=e.resetDefines=Cr(e.reset(e.model,e.ecModel,e.api,e.payload));return t.length>1?Xt(t,function(n,a){return eN(a)}):uTe}var uTe=eN(0);function eN(e){return function(t,n){var a=n.data,r=n.resetDefines[e];if(r&&r.dataEach)for(var o=t.start;o0&&f===u.length-v.length){var h=u.slice(0,f);h!=="data"&&(n.mainType=h,n[v.toLowerCase()]=s,c=!0)}}l.hasOwnProperty(u)&&(a[u]=s,c=!0),c||(r[u]=s)})}return{cptQuery:n,dataQuery:a,otherQuery:r}},e.prototype.filter=function(t,n){var a=this.eventInfo;if(!a)return!0;var r=a.targetEl,o=a.packedEvent,i=a.model,l=a.view;if(!i||!l)return!0;var s=n.cptQuery,u=n.dataQuery;return c(s,i,"mainType")&&c(s,i,"subType")&&c(s,i,"index","componentIndex")&&c(s,i,"name")&&c(s,i,"id")&&c(u,o,"name")&&c(u,o,"dataIndex")&&c(u,o,"dataType")&&(!l.filterForExposedEvent||l.filterForExposedEvent(t,n.otherQuery,r,o));function c(d,v,f,h){return d[f]==null||v[h||f]===d[f]}},e.prototype.afterTrigger=function(){this.eventInfo=null},e}(),i3=["symbol","symbolSize","symbolRotate","symbolOffset"],bT=i3.concat(["symbolKeepAspect"]),vTe={createOnAllSeries:!0,performRawSeries:!0,reset:function(e,t){var n=e.getData();if(e.legendIcon&&n.setVisual("legendIcon",e.legendIcon),!e.hasSymbolVisual)return;for(var a={},r={},o=!1,i=0;i=0&&Fu(s)?s:.5;var u=e.createRadialGradient(i,l,0,i,l,s);return u}function s3(e,t,n){for(var a=t.type==="radial"?NTe(e,t,n):RTe(e,t,n),r=t.colorStops,o=0;o0)?null:e==="dashed"?[4*t,2*t]:e==="dotted"?[t]:Gn(e)?[e]:Lt(e)?e:null}function iN(e){var t=e.style,n=t.lineDash&&t.lineWidth>0&&VTe(t.lineDash,t.lineWidth),a=t.lineDashOffset;if(n){var r=t.strokeNoScale&&e.getLineScale?e.getLineScale():1;r&&r!==1&&(n=Xt(n,function(o){return o/r}),a/=r)}return[n,a]}var zTe=new bc(!0);function Hg(e){var t=e.stroke;return!(t==null||t==="none"||!(e.lineWidth>0))}function _T(e){return typeof e=="string"&&e!=="none"}function Wg(e){var t=e.fill;return t!=null&&t!=="none"}function wT(e,t){if(t.fillOpacity!=null&&t.fillOpacity!==1){var n=e.globalAlpha;e.globalAlpha=t.fillOpacity*t.opacity,e.fill(),e.globalAlpha=n}else e.fill()}function ST(e,t){if(t.strokeOpacity!=null&&t.strokeOpacity!==1){var n=e.globalAlpha;e.globalAlpha=t.strokeOpacity*t.opacity,e.stroke(),e.globalAlpha=n}else e.stroke()}function u3(e,t,n){var a=p$(t.image,t.__image,n);if(r2(a)){var r=e.createPattern(a,t.repeat||"repeat");if(typeof DOMMatrix=="function"&&r&&r.setTransform){var o=new DOMMatrix;o.translateSelf(t.x||0,t.y||0),o.rotateSelf(0,0,(t.rotation||0)*b8e),o.scaleSelf(t.scaleX||1,t.scaleY||1),r.setTransform(o)}return r}}function FTe(e,t,n,a,r){var o,i=Hg(n),l=Wg(n),s=n.strokePercent,u=s<1,c=!t.path;(!t.silent||u)&&c&&t.createPathProxy();var d=t.path||zTe,v=t.__dirty;if(!a){var f=n.fill,h=n.stroke,m=l&&!!f.colorStops,g=i&&!!h.colorStops,y=l&&!!f.image,b=i&&!!h.image,_=void 0,w=void 0,S=void 0,k=void 0,T=void 0;(m||g)&&(T=t.getBoundingRect()),m&&(_=v?s3(e,f,T):t.__canvasFillGradient,t.__canvasFillGradient=_),g&&(w=v?s3(e,h,T):t.__canvasStrokeGradient,t.__canvasStrokeGradient=w),y&&(S=v||!t.__canvasFillPattern?u3(e,f,t):t.__canvasFillPattern,t.__canvasFillPattern=S),b&&(k=v||!t.__canvasStrokePattern?u3(e,h,t):t.__canvasStrokePattern,t.__canvasStrokePattern=k),m?e.fillStyle=_:y&&(S?e.fillStyle=S:l=!1),g?e.strokeStyle=w:b&&(k?e.strokeStyle=k:i=!1)}var M=t.getGlobalScale();d.setScale(M[0],M[1],t.segmentIgnoreThreshold);var E,A;e.setLineDash&&n.lineDash&&(o=iN(t),E=o[0],A=o[1]);var I=!0;(c||v&rd)&&(d.setDPR(e.dpr),u?d.setContext(null):(d.setContext(e),I=!1),d.reset(),t.buildPath(d,t.shape,a),d.toStatic(),t.pathUpdated()),I&&d.rebuildPath(e,u?s:1),E&&(e.setLineDash(E),e.lineDashOffset=A),a?(r.batchFill=l,r.batchStroke=i):n.strokeFirst?(i&&ST(e,n),l&&wT(e,n)):(l&&wT(e,n),i&&ST(e,n)),E&&e.setLineDash([])}function HTe(e,t,n){var a=t.__image=p$(n.image,t.__image,t,t.onload);if(!(!a||!r2(a))){var r=n.x||0,o=n.y||0,i=t.getWidth(),l=t.getHeight(),s=a.width/a.height;if(i==null&&l!=null?i=l*s:l==null&&i!=null?l=i/s:i==null&&l==null&&(i=a.width,l=a.height),n.sWidth&&n.sHeight){var u=n.sx||0,c=n.sy||0;e.drawImage(a,u,c,n.sWidth,n.sHeight,r,o,i,l)}else if(n.sx&&n.sy){var u=n.sx,c=n.sy,d=i-u,v=l-c;e.drawImage(a,u,c,d,v,r,o,i,l)}else e.drawImage(a,r,o,i,l)}}function WTe(e,t,n){var a,r=n.text;if(r!=null&&(r+=""),r){e.font=n.font||Vs,e.textAlign=n.textAlign,e.textBaseline=n.textBaseline;var o=void 0,i=void 0;e.setLineDash&&n.lineDash&&(a=iN(t),o=a[0],i=a[1]),o&&(e.setLineDash(o),e.lineDashOffset=i),n.strokeFirst?(Hg(n)&&e.strokeText(r,n.x,n.y),Wg(n)&&e.fillText(r,n.x,n.y)):(Wg(n)&&e.fillText(r,n.x,n.y),Hg(n)&&e.strokeText(r,n.x,n.y)),o&&e.setLineDash([])}}var CT=["shadowBlur","shadowOffsetX","shadowOffsetY"],xT=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function lN(e,t,n,a,r){var o=!1;if(!a&&(n=n||{},t===n))return!1;if(a||t.opacity!==n.opacity){wr(e,r),o=!0;var i=Math.max(Math.min(t.opacity,1),0);e.globalAlpha=isNaN(i)?tc.opacity:i}(a||t.blend!==n.blend)&&(o||(wr(e,r),o=!0),e.globalCompositeOperation=t.blend||tc.blend);for(var l=0;l0&&n.unfinished);n.unfinished||this._zr.flush()}}},t.prototype.getDom=function(){return this._dom},t.prototype.getId=function(){return this.id},t.prototype.getZr=function(){return this._zr},t.prototype.isSSR=function(){return this._ssr},t.prototype.setOption=function(n,a,r){if(!this[ba]){if(this._disposed){this.id;return}var o,i,l;if(Yt(a)&&(r=a.lazyUpdate,o=a.silent,i=a.replaceMerge,l=a.transition,a=a.notMerge),this[ba]=!0,Jc(this),!this._model||a){var s=new Gke(this._api),u=this._theme,c=this._model=new X8;c.scheduler=this._scheduler,c.ssr=this._ssr,c.init(null,null,null,u,this._locale,s)}this._model.setOption(n,{replaceMerge:i},h3);var d={seriesTransition:l,optionChanged:!0};if(r)this[Ra]={silent:o,updateParams:d},this[ba]=!1,this.getZr().wakeUp();else{try{xu(this),sl.update.call(this,null,d)}catch(v){throw this[Ra]=null,this[ba]=!1,v}this._ssr||this._zr.flush(),this[Ra]=null,this[ba]=!1,Xc.call(this,o),Zc.call(this,o)}}},t.prototype.setTheme=function(n,a){if(!this[ba]){if(this._disposed){this.id;return}var r=this._model;if(r){var o=a&&a.silent,i=null;this[Ra]&&(o==null&&(o=this[Ra].silent),i=this[Ra].updateParams,this[Ra]=null),this[ba]=!0,Jc(this);try{this._updateTheme(n),r.setTheme(this._theme),xu(this),sl.update.call(this,{type:"setTheme"},i)}catch(l){throw this[ba]=!1,l}this[ba]=!1,Xc.call(this,o),Zc.call(this,o)}}},t.prototype._updateTheme=function(n){Ut(n)&&(n=CN[n]),n&&(n=yn(n),n&&IR(n,!0),this._theme=n)},t.prototype.getModel=function(){return this._model},t.prototype.getOption=function(){return this._model&&this._model.getOption()},t.prototype.getWidth=function(){return this._zr.getWidth()},t.prototype.getHeight=function(){return this._zr.getHeight()},t.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||hn.hasGlobalWindow&&window.devicePixelRatio||1},t.prototype.getRenderedCanvas=function(n){return this.renderToCanvas(n)},t.prototype.renderToCanvas=function(n){n=n||{};var a=this._zr.painter;return a.getRenderedCanvas({backgroundColor:n.backgroundColor||this._model.get("backgroundColor"),pixelRatio:n.pixelRatio||this.getDevicePixelRatio()})},t.prototype.renderToSVGString=function(n){n=n||{};var a=this._zr.painter;return a.renderToString({useViewBox:n.useViewBox})},t.prototype.getSvgDataURL=function(){var n=this._zr,a=n.storage.getDisplayList();return we(a,function(r){r.stopAnimation(null,!0)}),n.painter.toDataURL()},t.prototype.getDataURL=function(n){if(this._disposed){this.id;return}n=n||{};var a=n.excludeComponents,r=this._model,o=[],i=this;we(a,function(s){r.eachComponent({mainType:s},function(u){var c=i._componentsMap[u.__viewId];c.group.ignore||(o.push(c),c.group.ignore=!0)})});var l=this._zr.painter.getType()==="svg"?this.getSvgDataURL():this.renderToCanvas(n).toDataURL("image/"+(n&&n.type||"png"));return we(o,function(s){s.group.ignore=!1}),l},t.prototype.getConnectedDataURL=function(n){if(this._disposed){this.id;return}var a=n.type==="svg",r=this.group,o=Math.min,i=Math.max,l=1/0;if(zT[r]){var s=l,u=l,c=-l,d=-l,v=[],f=n&&n.pixelRatio||this.getDevicePixelRatio();we(Gv,function(_,w){if(_.group===r){var S=a?_.getZr().painter.getSvgDom().innerHTML:_.renderToCanvas(yn(n)),k=_.getDom().getBoundingClientRect();s=o(k.left,s),u=o(k.top,u),c=i(k.right,c),d=i(k.bottom,d),v.push({dom:S,left:k.left,top:k.top})}}),s*=f,u*=f,c*=f,d*=f;var h=c-s,m=d-u,g=oo.createCanvas(),y=Fk(g,{renderer:a?"svg":"canvas"});if(y.resize({width:h,height:m}),a){var b="";return we(v,function(_){var w=_.left-s,S=_.top-u;b+=''+_.dom+""}),y.painter.getSvgRoot().innerHTML=b,n.connectedBackgroundColor&&y.painter.setBackgroundColor(n.connectedBackgroundColor),y.refreshImmediately(),y.painter.toDataURL()}else return n.connectedBackgroundColor&&y.add(new ga({shape:{x:0,y:0,width:h,height:m},style:{fill:n.connectedBackgroundColor}})),we(v,function(_){var w=new js({style:{x:_.left*f-s,y:_.top*f-u,image:_.dom}});y.add(w)}),y.refreshImmediately(),g.toDataURL("image/"+(n&&n.type||"png"))}else return this.getDataURL(n)},t.prototype.convertToPixel=function(n,a,r){return A0(this,"convertToPixel",n,a,r)},t.prototype.convertToLayout=function(n,a,r){return A0(this,"convertToLayout",n,a,r)},t.prototype.convertFromPixel=function(n,a,r){return A0(this,"convertFromPixel",n,a,r)},t.prototype.containPixel=function(n,a){if(this._disposed){this.id;return}var r=this._model,o,i=Uy(r,n);return we(i,function(l,s){s.indexOf("Models")>=0&&we(l,function(u){var c=u.coordinateSystem;if(c&&c.containPoint)o=o||!!c.containPoint(a);else if(s==="seriesModels"){var d=this._chartsMap[u.__viewId];d&&d.containPoint&&(o=o||d.containPoint(a,u))}},this)},this),!!o},t.prototype.getVisual=function(n,a){var r=this._model,o=Uy(r,n,{defaultMainType:"series"}),i=o.seriesModel,l=i.getData(),s=o.hasOwnProperty("dataIndexInside")?o.dataIndexInside:o.hasOwnProperty("dataIndex")?l.indexOfRawIndex(o.dataIndex):null;return s!=null?pTe(l,s,a):mTe(l,a)},t.prototype.getViewOfComponentModel=function(n){return this._componentsMap[n.__viewId]},t.prototype.getViewOfSeriesModel=function(n){return this._chartsMap[n.__viewId]},t.prototype._initEvents=function(){var n=this;we(vMe,function(r){var o=function(i){var l=n.getModel(),s=i.target,u,c=r==="globalout";if(c?u={}:s&&gv(s,function(m){var g=In(m);if(g&&g.dataIndex!=null){var y=g.dataModel||l.getSeriesByIndex(g.seriesIndex);return u=y&&y.getDataParams(g.dataIndex,g.dataType,s)||{},!0}else if(g.eventData)return u=vt({},g.eventData),!0},!0),u){var d=u.componentType,v=u.componentIndex;(d==="markLine"||d==="markPoint"||d==="markArea")&&(d="series",v=u.seriesIndex);var f=d&&v!=null&&l.getComponent(d,v),h=f&&n[f.mainType==="series"?"_chartsMap":"_componentsMap"][f.__viewId];u.event=i,u.type=r,n._$eventProcessor.eventInfo={targetEl:s,packedEvent:u,model:f,view:h},n.trigger(r,u)}};o.zrEventfulCallAtLast=!0,n._zr.on(r,o,n)});var a=this._messageCenter;we(f3,function(r,o){a.on(o,function(i){n.trigger(o,i)})}),yTe(a,this,this._api)},t.prototype.isDisposed=function(){return this._disposed},t.prototype.clear=function(){if(this._disposed){this.id;return}this.setOption({series:[]},!0)},t.prototype.dispose=function(){if(this._disposed){this.id;return}this._disposed=!0;var n=this.getDom();n&&s$(this.getDom(),rS,"");var a=this,r=a._api,o=a._model;we(a._componentsViews,function(i){i.dispose(o,r)}),we(a._chartsViews,function(i){i.dispose(o,r)}),a._zr.dispose(),a._dom=a._model=a._chartsMap=a._componentsMap=a._chartsViews=a._componentsViews=a._scheduler=a._api=a._zr=a._throttledZrFlush=a._theme=a._coordSysMgr=a._messageCenter=null,delete Gv[a.id]},t.prototype.resize=function(n){if(!this[ba]){if(this._disposed){this.id;return}this._zr.resize(n);var a=this._model;if(this._loadingFX&&this._loadingFX.resize(),!!a){var r=a.resetOption("media"),o=n&&n.silent;this[Ra]&&(o==null&&(o=this[Ra].silent),r=!0,this[Ra]=null),this[ba]=!0,Jc(this);try{r&&xu(this),sl.update.call(this,{type:"resize",animation:vt({duration:0},n&&n.animation)})}catch(i){throw this[ba]=!1,i}this[ba]=!1,Xc.call(this,o),Zc.call(this,o)}}},t.prototype.showLoading=function(n,a){if(this._disposed){this.id;return}if(Yt(n)&&(a=n,n=""),n=n||"default",this.hideLoading(),!!p3[n]){var r=p3[n](this._api,a),o=this._zr;this._loadingFX=r,o.add(r)}},t.prototype.hideLoading=function(){if(this._disposed){this.id;return}this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null},t.prototype.makeActionFromEvent=function(n){var a=vt({},n);return a.type=d3[n.type],a},t.prototype.dispatchAction=function(n,a){if(this._disposed){this.id;return}if(Yt(a)||(a={silent:!!a}),!!Kg[n.type]&&this._model){if(this[ba]){this._pendingActions.push(n);return}var r=a.silent;Cb.call(this,n,r);var o=a.flush;o?this._zr.flush():o!==!1&&hn.browser.weChat&&this._throttledZrFlush(),Xc.call(this,r),Zc.call(this,r)}},t.prototype.updateLabelLayout=function(){mo.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},t.prototype.appendData=function(n){if(this._disposed){this.id;return}var a=n.seriesIndex,r=this.getModel(),o=r.getSeriesByIndex(a);o.appendData(n),this._scheduler.unfinished=!0,this.getZr().wakeUp()},t.internalField=function(){xu=function(d){wTe(d._model);var v=d._scheduler;v.restorePipelines(d._zr,d._model),v.prepareStageTasks(),wb(d,!0),wb(d,!1),v.plan()},wb=function(d,v){for(var f=d._model,h=d._scheduler,m=v?d._componentsViews:d._chartsViews,g=v?d._componentsMap:d._chartsMap,y=d._zr,b=d._api,_=0;_jt(v.get("hoverLayerThreshold"),kR.hoverLayerThreshold)&&!hn.node&&!hn.worker;(d._usingTHL||g)&&(v.eachSeries(function(y){if(!y.preventUsingHoverLayer){var b=d._chartsMap[y.__viewId];b.__alive&&b.eachRendered(function(_){var w=_.states.emphasis;w&&w.hoverLayer!==E8&&(w.hoverLayer=g?q$:j$)})}}),d._usingTHL=g)}}function l(d,v){var f=d.get("blendMode")||null;v.eachRendered(function(h){h.isGroup||(h.style.blend=f)})}function s(d,v){if(!d.preventAutoZ){var f=tR(d);v.eachRendered(function(h){return nR(h,f.z,f.zlevel),!0})}}function u(d,v){v.eachRendered(function(f){if(!zv(f)){var h=f.getTextContent(),m=f.getTextGuideLine();f.stateTransition&&(f.stateTransition=null),h&&h.stateTransition&&(h.stateTransition=null),m&&m.stateTransition&&(m.stateTransition=null),f.hasState()?(f.prevStates=f.currentStates,f.clearStates()):f.prevStates&&(f.prevStates=null)}})}function c(d,v){var f=d.getModel("stateAnimation"),h=d.isAnimationEnabled(),m=f.get("duration"),g=m>0?{duration:m,delay:f.get("delay"),easing:f.get("easing")}:null;v.eachRendered(function(y){if(y.states&&y.states.emphasis){if(zv(y))return;if(y instanceof Fn&&W5e(y),y.__dirty){var b=y.prevStates;b&&y.useStates(b)}if(h){y.stateTransition=g;var _=y.getTextContent(),w=y.getTextGuideLine();_&&(_.stateTransition=g),w&&(w.stateTransition=g)}y.__dirty&&o(y)}})}BT=function(d){return new(function(v){At(f,v);function f(){return v!==null&&v.apply(this,arguments)||this}return f.prototype.getCoordinateSystems=function(){return d._coordSysMgr.getCoordinateSystems()},f.prototype.getComponentByElement=function(h){for(;h;){var m=h.__ecComponentInfo;if(m!=null)return d._model.getComponent(m.mainType,m.index);h=h.parent}},f.prototype.enterEmphasis=function(h,m){Dg(h,m),vo(d)},f.prototype.leaveEmphasis=function(h,m){Og(h,m),vo(d)},f.prototype.enterBlur=function(h){D5e(h),vo(d)},f.prototype.leaveBlur=function(h){D$(h),vo(d)},f.prototype.enterSelect=function(h){O$(h),vo(d)},f.prototype.leaveSelect=function(h){$$(h),vo(d)},f.prototype.getModel=function(){return d.getModel()},f.prototype.getViewOfComponentModel=function(h){return d.getViewOfComponentModel(h)},f.prototype.getViewOfSeriesModel=function(h){return d.getViewOfSeriesModel(h)},f.prototype.getECUpdateCycleVersion=function(){return d[M0]},f.prototype.usingTHL=function(){return d._usingTHL},f}(E$))(d)},wN=function(d){function v(f,h){for(var m=0;m=0)){FT.push(n);var i=QR.wrapStageHandler(n,r);i.__prio=t,i.__raw=n,e.push(i)}}function MN(e,t){p3[e]=t}function bMe(e,t,n){var a=_Te("registerMap");a&&a(e,t,n)}var _Me=C9e;Lc(nS,Q9e);Lc(w2,eTe);Lc(w2,tTe);Lc(nS,vTe);Lc(w2,hTe);Lc(pN,ZTe);kN(IR);TN(tMe,r9e);MN("default",nTe);Sf({type:nc,event:nc,update:nc},lr);Sf({type:ym,event:ym,update:ym},lr);Sf({type:Lg,event:b8,update:Lg,action:lr,refineEvent:lS,publishNonRefinedEvent:!0});Sf({type:W_,event:b8,update:W_,action:lr,refineEvent:lS,publishNonRefinedEvent:!0});Sf({type:Pg,event:b8,update:Pg,action:lr,refineEvent:lS,publishNonRefinedEvent:!0});function lS(e,t,n,a){return{eventContent:{selected:B5e(n),isFromClick:t.isFromClick||!1}}}xN("default",{});xN("dark",aN);function Gf(e){return e==null?0:e.length||1}function HT(e){return e}var wMe=function(){function e(t,n,a,r,o,i){this._old=t,this._new=n,this._oldKeyGetter=a||HT,this._newKeyGetter=r||HT,this.context=o,this._diffModeMultiple=i==="multiple"}return e.prototype.add=function(t){return this._add=t,this},e.prototype.update=function(t){return this._update=t,this},e.prototype.updateManyToOne=function(t){return this._updateManyToOne=t,this},e.prototype.updateOneToMany=function(t){return this._updateOneToMany=t,this},e.prototype.updateManyToMany=function(t){return this._updateManyToMany=t,this},e.prototype.remove=function(t){return this._remove=t,this},e.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},e.prototype._executeOneToOne=function(){var t=this._old,n=this._new,a={},r=new Array(t.length),o=new Array(n.length);this._initIndexMap(t,null,r,"_oldKeyGetter"),this._initIndexMap(n,a,o,"_newKeyGetter");for(var i=0;i1){var c=s.shift();s.length===1&&(a[l]=s[0]),this._update&&this._update(c,i)}else u===1?(a[l]=null,this._update&&this._update(s,i)):this._remove&&this._remove(i)}this._performRestAdd(o,a)},e.prototype._executeMultiple=function(){var t=this._old,n=this._new,a={},r={},o=[],i=[];this._initIndexMap(t,a,o,"_oldKeyGetter"),this._initIndexMap(n,r,i,"_newKeyGetter");for(var l=0;l1&&v===1)this._updateManyToOne&&this._updateManyToOne(c,u),r[s]=null;else if(d===1&&v>1)this._updateOneToMany&&this._updateOneToMany(c,u),r[s]=null;else if(d===1&&v===1)this._update&&this._update(c,u),r[s]=null;else if(d>1&&v>1)this._updateManyToMany&&this._updateManyToMany(c,u),r[s]=null;else if(d>1)for(var f=0;f1)for(var l=0;l30}var Yf=Yt,Ql=Xt,EMe=typeof Int32Array>"u"?Array:Int32Array,AMe="e\0\0",WT=-1,IMe=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],LMe=["_approximateExtent"],KT,P0,jf,qf,Tb,Xf,Mb,PN=function(){function e(t,n){this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","minmaxDownSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","minmaxDownSample","lttbDownSample"];var a,r=!1;AN(t)?(a=t.dimensions,this._dimOmitted=t.isDimensionOmitted(),this._schema=t):(r=!0,a=t),a=a||["x","y"];for(var o={},i=[],l={},s=!1,u={},c=0;c=n)){var a=this._store,r=a.getProvider();this._updateOrdinalMeta();var o=this._nameList,i=this._idList,l=r.getSource().sourceFormat,s=l===uo;if(s&&!r.pure)for(var u=[],c=t;c0},e.prototype.ensureUniqueItemVisual=function(t,n){var a=this._itemVisuals,r=a[t];r||(r=a[t]={});var o=r[n];return o==null&&(o=this.getVisual(n),Lt(o)?o=o.slice():Yf(o)&&(o=vt({},o)),r[n]=o),o},e.prototype.setItemVisual=function(t,n,a){var r=this._itemVisuals[t]||{};this._itemVisuals[t]=r,Yf(n)?vt(r,n):r[n]=a},e.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},e.prototype.setLayout=function(t,n){Yf(t)?vt(this._layout,t):this._layout[t]=n},e.prototype.getLayout=function(t){return this._layout[t]},e.prototype.getItemLayout=function(t){return this._itemLayouts[t]},e.prototype.setItemLayout=function(t,n,a){this._itemLayouts[t]=a?vt(this._itemLayouts[t]||{},n):n},e.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},e.prototype.setItemGraphicEl=function(t,n){var a=this.hostModel&&this.hostModel.seriesIndex;w5e(a,this.dataType,t,n),this._graphicEls[t]=n},e.prototype.getItemGraphicEl=function(t){return this._graphicEls[t]},e.prototype.eachItemGraphicEl=function(t,n){we(this._graphicEls,function(a,r){a&&t&&t.call(n,a,r)})},e.prototype.cloneShallow=function(t){return t||(t=new e(this._schema?this._schema:Ql(this.dimensions,this._getDimInfo,this),this.hostModel)),Tb(t,this),t._store=this._store,t},e.prototype.wrapMethod=function(t,n){var a=this[t];dn(a)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var r=a.apply(this,arguments);return n.apply(this,[r].concat(t8(arguments)))})},e.internalField=function(){KT=function(t){var n=t._invertedIndicesMap;we(n,function(a,r){var o=t._dimInfos[r],i=o.ordinalMeta,l=t._store;if(i){a=n[r]=new EMe(i.categories.length);for(var s=0;s1&&(s+="__ec__"+c),r[n]=s}}}(),e}();function DN(e,t){Z8(e)||(e=LR(e)),t=t||{};var n=t.coordDimensions||[],a=t.dimensionsDefine||e.dimensionsDefine||[],r=an(),o=[],i=PMe(e,n,a,t.dimensionsCount),l=t.canOmitUnusedDimensions&&LN(i),s=a===e.dimensionsDefine,u=s?IN(e):sS(a),c=t.encodeDefine;!c&&t.encodeDefaulter&&(c=t.encodeDefaulter(e,i));for(var d=an(c),v=new FR(i),f=0;f0&&(E.name=E.name+(A-1))}),new EN({source:e,dimensions:o,fullDimensionCount:i,dimensionOmitted:l})}function PMe(e,t,n,a){var r=Math.max(e.dimensionsDetectedCount||1,t.length,n.length,a||0);return we(t,function(o){var i;Yt(o)&&(i=o.dimsDef)&&(r=Math.max(r,i.length))}),r}function DMe(e,t,n){if(n||t.hasKey(e)){for(var a=0;t.hasKey(e+a);)a++;e+=a}return t.set(e,!0),e}var OMe=function(){function e(t){this.coordSysDims=[],this.axisMap=an(),this.categoryAxisMap=an(),this.coordSysName=t}return e}();function $Me(e){var t=e.get("coordinateSystem"),n=new OMe(t),a=RMe[t];if(a)return a(e,n,n.axisMap,n.categoryAxisMap),n}var RMe={cartesian2d:function(e,t,n,a){var r=e.getReferringComponents("xAxis",ko).models[0],o=e.getReferringComponents("yAxis",ko).models[0];t.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),Qc(r)&&(a.set("x",r),t.firstCategoryDimIndex=0),Qc(o)&&(a.set("y",o),t.firstCategoryDimIndex==null&&(t.firstCategoryDimIndex=1))},singleAxis:function(e,t,n,a){var r=e.getReferringComponents("singleAxis",ko).models[0];t.coordSysDims=["single"],n.set("single",r),Qc(r)&&(a.set("single",r),t.firstCategoryDimIndex=0)},polar:function(e,t,n,a){var r=e.getReferringComponents("polar",ko).models[0],o=r.findAxisModel("radiusAxis"),i=r.findAxisModel("angleAxis");t.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",i),Qc(o)&&(a.set("radius",o),t.firstCategoryDimIndex=0),Qc(i)&&(a.set("angle",i),t.firstCategoryDimIndex==null&&(t.firstCategoryDimIndex=1))},geo:function(e,t,n,a){t.coordSysDims=["lng","lat"]},parallel:function(e,t,n,a){var r=e.ecModel,o=r.getComponent("parallel",e.get("parallelIndex")),i=t.coordSysDims=o.dimensions.slice();we(o.parallelAxisIndex,function(l,s){var u=r.getComponent("parallelAxis",l),c=i[s];n.set(c,u),Qc(u)&&(a.set(c,u),t.firstCategoryDimIndex==null&&(t.firstCategoryDimIndex=s))})},matrix:function(e,t,n,a){var r=e.getReferringComponents("matrix",ko).models[0];t.coordSysDims=["x","y"];var o=r.getDimensionModel("x"),i=r.getDimensionModel("y");n.set("x",o),n.set("y",i),a.set("x",o),a.set("y",i)}};function Qc(e){return e.get("type")==="category"}function NMe(e,t,n){n=n||{};var a=n.byIndex,r=n.stackedCoordDimension,o,i,l;BMe(t)?o=t:(i=t.schema,o=i.dimensions,l=t.store);var s=!!(e&&e.get("stack")),u,c,d,v,f=!0;function h(w){return w.type!=="ordinal"&&w.type!=="time"}if(we(o,function(w,S){Ut(w)&&(o[S]=w={name:w}),h(w)||(f=!1)}),we(o,function(w,S){s&&!w.isExtraCoord&&(!a&&!u&&w.ordinalMeta&&(u=w),!c&&h(w)&&(!f||w.coordDim!=="x"&&w.coordDim!=="angle")&&(!r||r===w.coordDim)&&(c=w))}),c&&!a&&!u&&(a=!0),c){d="__\0ecstackresult_"+e.id,v="__\0ecstackedover_"+e.id,u&&(u.createInvertedIndices=!0);var m=c.coordDim,g=c.type,y=0;we(o,function(w){w.coordDim===m&&y++});var b={name:d,coordDim:m,coordDimIndex:y,type:g,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:o.length},_={name:v,coordDim:v,coordDimIndex:y+1,type:g,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:o.length+1};i?(l&&(b.storeDimIndex=l.ensureCalculationDimension(v,g),_.storeDimIndex=l.ensureCalculationDimension(d,g)),i.appendCalculationDimension(b),i.appendCalculationDimension(_)):(o.push(b),o.push(_))}return{stackedDimension:c&&c.name,stackedByDimension:u&&u.name,isStackedByIndex:a,stackedOverDimension:v,stackResultDimension:d}}function BMe(e){return!AN(e.schema)}function af(e,t){return!!t&&t===e.getCalculationInfo("stackedDimension")}function VMe(e,t){return af(e,t)?e.getCalculationInfo("stackResultDimension"):t}function zMe(e,t){var n=e.get("coordinateSystem"),a=U8.get(n),r;return t&&t.coordSysDims&&(r=Xt(t.coordSysDims,function(o){var i={name:o},l=t.axisMap.get(o);if(l){var s=l.get("type");i.type=xMe(s)}return i})),r||(r=a&&(a.getDimensionsInfo?a.getDimensionsInfo():a.dimensions.slice())||["x","y"]),r}function FMe(e,t,n){var a,r;return n&&we(e,function(o,i){var l=o.coordDim,s=n.categoryAxisMap.get(l);s&&(a==null&&(a=i),o.ordinalMeta=s.getOrdinalMeta(),t&&(o.createInvertedIndices=!0)),o.otherDims.itemName!=null&&(r=!0)}),!r&&a!=null&&(e[a].otherDims.itemName=0),a}function uS(e,t,n){n=n||{};var a=t.getSourceManager(),r,o=!1;r=a.getSource(),o=r.sourceFormat===uo;var i=$Me(t),l=zMe(t,i),s=n.useEncodeDefaulter,u=dn(s)?s:s?Sa(Oke,l,t):null,c={coordDimensions:l,generateCoord:n.generateCoord,encodeDefine:t.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!o},d=DN(r,c),v=FMe(d.dimensions,n.createInvertedIndices,i),f=o?null:a.getSharedDataStore(d),h=NMe(t,{schema:d,store:f}),m=new PN(d,t);m.setCalculationInfo(h);var g=v!=null&&HMe(r)?function(y,b,_,w){return w===v?_:this.defaultDimValueGetter(y,b,_,w)}:null;return m.hasItemOption=!1,m.initData(o?r:f,null,g),m}function HMe(e){if(e.sourceFormat===uo){var t=WMe(e.data||[]);return!Lt(bp(t))}}function WMe(e){for(var t=0;t=t[0]&&e<=t[1]},getExtent:function(){return this._extents[Mo].slice()},getExtentUnsafe:function(e){return this._extents[e]},setExtent:function(e,t){UT(this._extents,Mo,e,t)},setExtent2:function(e,t,n){var a=this._extents;a[e]||(a[e]=a[Mo].slice()),UT(a,e,t,n)},freeze:function(){}};function UT(e,t,n,a){Jd(n,a)&&(e[t][0]=n,e[t][1]=a)}function NN(e){return jg(e)||rf(e)}function jg(e){return e.type==="interval"}function dS(e){return e.type==="time"}function rf(e){return e.type==="log"}function pi(e){return e.type==="ordinal"}function ZMe(e){var t=s8(e),n=Ac(10,t),a=Dl(e/n);return a?a===2?a=3:a===3?a=5:a*=2:a=1,En(a*n,-t)}function Sc(e){return bs(e)+2}function D0(e,t){return Th(e)/Th(t)}function Eb(e,t,n){var a=n&&n.lookup;if(a){for(var r=0;r1&&o/i>2&&(r=Math.round(Math.ceil(r/i)*i)),r!==a[0]&&s(a[0],!0,!0);for(var l=r;l<=a[1];l+=i)s(l,!1,l===a[0]||l===a[1]);l-i!==a[1]&&s(a[1],!0,!0);function s(u,c,d){n({value:u,offInterval:c},d)}}var zN=function(e){At(t,e);function t(n){var a=e.call(this)||this;a.type="ordinal",a.parse=t.parse,$N(a,t.decoratedMethods);var r=n.ordinalMeta;r||(r=new m3({})),Lt(r)&&(r=new m3({categories:Xt(r,function(i){return Yt(i)?i.value:i})})),a._ordinalMeta=r;var o=cS(null,null,n.extent||[0,r.categories.length-1]);return a._mapper=o.mapper,RN(a),a}return t.parse=function(n){return n==null?n=NaN:Ut(n)?(n=this._ordinalMeta.getOrdinal(n),n==null&&(n=NaN)):n=Dl(n),n},t.prototype.getTicks=function(){var n=[];return VN(this,0,function(a){n.push(a)}),n},t.prototype.getMinorTicks=function(n){},t.prototype.setSortInfo=function(n){if(n==null){this._ordinalNumbersByTick=this._ticksByOrdinalNumber=null;return}for(var a=n.ordinalNumbers,r=this._ordinalNumbersByTick=[],o=this._ticksByOrdinalNumber=[],i=0,l=this._ordinalMeta.categories.length,s=sr(l,a.length);i=0&&n=0&&n=0&&ni[0]&&mi[1]||!isFinite(m)||!isFinite(i[1]))break}else{if(g>h)break;m=sr(m,i[1]),g===h&&(m=i[1])}if(d.push({value:m}),m=En(m+r,l),u){var y=u.calcNiceTickMultiple(m,f);y>=0&&(m=En(m+y*r,l))}if(d.length>0&&m===d[d.length-1].value)break;if(d.length>v)return[]}var b=d.length?d[d.length-1].value:i[1];return o[1]>b&&d.push({value:n.expandToNicedExtent?En(b+r,l):o[1]}),d},t.prototype.getMinorTicks=function(n){return vS(this,n,R8(this),this._cfg.interval)},t.prototype.getLabel=function(n,a){if(n==null)return"";var r=a&&a.precision;r==null?r=bs(n.value)||0:r==="auto"&&(r=this._cfg.intervalPrecision);var o=En(n.value,r,!0);return gR(o)},t.type="interval",t}(hi);hi.registerClass(Ad);var QMe=function(e,t,n,a){for(;n>>1;e[r][1]16?16:e>7.5?7:e>3.5?4:e>1.5?2:1}function nEe(e){var t=30*To;return e/=t,e>6?6:e>3?3:e>2?2:1}function aEe(e){return e/=Hv,e>12?12:e>6?6:e>3.5?4:e>2?2:1}function GT(e,t){return e/=t?B8:N8,e>30?30:e>20?20:e>15?15:e>10?10:e>5?5:e>2?2:1}function rEe(e){return An(u8(e,!0),1)}function oEe(e,t,n){var a=Math.max(0,Pn(ac,t)-1);return J_(new Date(e),ac[a],n).getTime()}function iEe(e,t){var n=new Date(0);n[e](1);var a=n.getTime();n[e](1+t);var r=n.getTime()-a;return function(o,i){return Math.max(0,Math.round((i-o)/r))}}function lEe(e,t,n,a,r,o){var i=3e3,l=dke,s=0;function u(H,z,B,W,N,V,Y){for(var X=iEe(N,H),ee=z,re=new Date(ee);eei));)if(re[N](re[W]()+H),ee=re.getTime(),o){var ie=o.calcNiceTickMultiple(ee,X);ie>0&&(re[N](re[W]()+ie*H),ee=re.getTime())}Y.push({value:ee,notAdd:ee>a[1]})}function c(H,z,B){var W=[],N=!z.length;if(!eEe(Wv(H),a[0],a[1],n)){N&&(z=[{value:oEe(a[0],H,n)},{value:a[1]}]);for(var V=0;V=a[0]&&Y<=a[1]&&u(ee,Y,X,re,ie,le,W),H==="year"&&B.length>1&&V===0&&B.unshift({value:B[0].value-ee})}}for(var V=0;V=a[0]&&w<=a[1]&&f++)}var S=r/t;if(f>S*1.5&&h>S/1.5||(d.push(b),f>S||e===l[m]))break}v=[]}}}for(var k=ar(Xt(d,function(H){return ar(H,function(z){return z.value>=a[0]&&z.value<=a[1]&&!z.notAdd})}),function(H){return H.length>0}),T=k.length-1,M=[],m=0;ma[0])&&M.unshift({value:a[0],time:{level:0,upperTimeUnit:G,lowerTimeUnit:G},notNice:!0}),(!R||R.valuel&&(o=l);var s=O0.length,u=Math.min(QMe(O0,o,0,s),s-1),c=O0[u][1],d=O0[Math.max(u-1,0)][0];e.setTimeInterval({approxInterval:o,interval:c,minLevelUnit:d})};hi.registerClass(FN);var $0=0,R0=1,HN=function(e){At(t,e);function t(n){var a=e.call(this)||this;a.type="log",a.parse=Ad.parse,a.base=n.logBase||10;var r=[],o=[];a._lookup={from:r,to:o},r[$0]=r[R0]=o[$0]=o[R0]=NaN,$N(a,t.mapperMethods),n.breakOption;var i={};return a.powStub=new Ad({breakParsed:i.original}),a.intervalStub=new Ad({breakParsed:i.transformed}),RN(a,a.intervalStub),a}return t.prototype.getTicks=function(n){var a=this.base,r=this.powStub,o=this.intervalStub,i=o.getExtent(),l=r.getExtent(),s={lookup:{from:i,to:l}};return Xt(o.getTicks(n||{}),function(u){var c=u.value,d=Eb(c,a,s),v;return{value:d,break:v}},this)},t.prototype.getMinorTicks=function(n){return vS(this,n,R8(this.powStub),this.intervalStub.getConfig().interval)},t.prototype.getLabel=function(n,a){return this.intervalStub.getLabel(n,a)},t.type="log",t.mapperMethods={needTransform:function(){return!0},normalize:function(n){return this.intervalStub.normalize(D0(n,this.base))},scale:function(n){return Eb(this.intervalStub.scale(n),this.base,null)},transformIn:function(n,a){return n=D0(n,this.base),a&&a.depth===Gg?n:this.intervalStub.transformIn(n,a)},transformOut:function(n,a){var r=a?a.depth:null;return YT.depth=r,jT.lookup=this._lookup,Eb(r===Gg?n:this.intervalStub.transformOut(n,YT),this.base,jT)},contain:function(n){return this.powStub.contain(n)},setExtent:function(n,a){this.setExtent2(Mo,n,a)},setExtent2:function(n,a,r){if(!(!Jd(a,r)||a<=0||r<=0)){var o=qT,i=qT;if(n===Mo){var l=this._lookup;o=l.to,i=l.from}this.powStub.setExtent2(n,o[$0]=a,o[R0]=r);var s=this.base;this.intervalStub.setExtent2(n,i[$0]=D0(a,s),i[R0]=D0(r,s))}},getFilter:function(){return{g:0}},sanitize:function(n,a){return Jd(a[0],a[1])&&zo(n)&&n<=0&&(n=a[0]),n},getDefaultStartValue:function(){return 1},getExtent:function(){return this.powStub.getExtent()},getExtentUnsafe:function(n,a){return a===null?this.powStub.getExtentUnsafe(n,null):this.intervalStub.getExtentUnsafe(n,a)}},t}(hi);hi.registerClass(HN);var YT={},jT={},qT=[],WN={value:1,category:1,time:1,log:1},KN=Hn();function uEe(e){var t=e.get("type");return(t==null||!Sr(WN,t)&&!hi.getClass(t))&&(t="value"),t}function cEe(e,t,n){var a;switch(t){case"category":return new zN({ordinalMeta:e.getOrdinalMeta?e.getOrdinalMeta():e.getCategories(),extent:So()});case"time":return new FN({locale:e.ecModel.getLocaleModel(),useUTC:e.ecModel.get("useUTC"),breakOption:a});case"log":return new HN({logBase:e.get("logBase"),breakOption:a});case"value":return new Ad({breakOption:a});default:return new(hi.getClass(t)||Ad)({})}}function dEe(e,t,n){var a=e.getExtentUnsafe(Mo,null),r=a[0],o=a[1];return Jd(r,o)?r===t||o===t?vEe:rt?fEe:g3:g3}var fEe=1,vEe=2,g3=3;function hEe(e){KN(e).noOnMyZero=!0}function pEe(e){return KN(e).noOnMyZero}function C2(e){var t=e.getLabelModel().get("formatter");if(e.type==="time"){var n=fke(t);return function(r,o){return e.scale.getFormattedLabel(r,o,n)}}else{if(Ut(t))return function(r){var o=e.scale.getLabel(r),i=t.replace("{value}",o??"");return i};if(dn(t)){if(e.type==="category")return function(r,o){return t(qg(e,r),r.value-e.scale.getExtent()[0],null)};var a=m2();return function(r,o){var i=null;return a&&(i=a.makeAxisLabelFormatterParamBreak(i,r.break)),t(qg(e,r),o,i)}}else return function(r){return e.scale.getLabel(r)}}}function qg(e,t){var n=e.scale;return pi(n)?n.getLabel(t):t.value}function hS(e){var t=e.get("interval");return t??"auto"}function mEe(e){return e.type==="category"&&hS(e.getLabelModel())===0}function gEe(e,t){var n={};return we(e.mapDimensionsAll(t),function(a){n[VMe(e,a)]=!0}),la(n)}function of(e){return e==="middle"||e==="center"}function Bh(e){return e.getShallow("show")}function yEe(e,t,n){var a=e.get("breaks",!0);a==null}function UN(e,t,n,a,r,o){var i=rf(e),l=i?e.intervalStub:e;if(l.setExtent(a[0],a[1]),i){var s=e.powStub,u={depth:Gg},c=e.transformOut(a[0],u),d=e.transformOut(a[1],u),v=JMe(n,a);t[0]&&!v[0]&&(c=r[0]),t[1]&&!v[1]&&(d=r[1]),s.setExtent(c,d)}l.setConfig(o)}function Tp(e,t){return pi(e)?e.getRawOrdinalNumber(t.value):t.value}function GN(e,t){return pi(e)&&!!t.get("boundaryGap")}var bEe=function(){function e(){}return e.prototype.needIncludeZero=function(){return!this.option.scale},e.prototype.getCoordSysModel=function(){},e}(),_Ee=d$(),y3="|&",Cf=Hn(),YN=-2,wEe=-1,SEe=Hn();function jN(e,t){var n=e.model,a=Cf(wf(n.ecModel)).keyed,r=a&&a.get(t);return r&&r.get(n.uid)}function CEe(e,t){return XN(jN(e,t))}function xEe(e,t){var n=[];return qN(e.model.ecModel,function(a){for(var r=0;r0&&d[1]>0&&!v[0]&&(d[0]=0),d[0]<0&&d[1]<0&&!v[1]&&(d[1]=0));var w=!1;d[0]>d[1]&&(d.reverse(),w=!0);var S=Zf(t,n.get("startValue",!0)),k=S!=null;!zo(S)&&r&&(S=t.getDefaultStartValue?t.getDefaultStartValue():0),zo(S)&&(k||!b||_)&&(Sd[1]&&!v[1]&&(d[1]=S,v[1]=!0));var T=this._i={scale:t,dataMM:c,noZoomEffMM:d,zoomMM:[],fixMM:v,zoomFixMM:[!1,!1],startValue:S,isBlank:y,incl0:_,tggAxInv:w,ctnShp:o};ZT(T,d)}return e.prototype.makeNoZoom=function(){return this._i.noZoomEffMM.slice()},e.prototype.makeFinal=function(){var t=this._i,n=t.zoomMM,a=t.noZoomEffMM,r=t.zoomFixMM,o=t.fixMM,i={fixMM:o,zoomFixMM:r,isBlank:t.isBlank,incl0:t.incl0,tggAxInv:t.tggAxInv,ctnShp:t.ctnShp,effMM:a.slice()},l=i.effMM;return n[0]!=null&&(l[0]=n[0],o[0]=r[0]=!0),n[1]!=null&&(l[1]=n[1],o[1]=r[1]=!0),ZT(t,l),i},e.prototype.makeRenderInfo=function(){return{startValue:this._i.startValue}},e.prototype.setZoomMM=function(t,n){this._i.zoomMM[t]=n},e}();function ZT(e,t){var n=e.scale,a=e.dataMM;n.sanitize&&(t[0]=n.sanitize(t[0],a),t[1]=n.sanitize(t[1],a),gCe(t))}function Zf(e,t){return t==null?null:kh(t)?NaN:e.parse(t)}function DEe(e,t){var n;if(pi(e))n=[0,0];else{var a=t.get("boundaryGap");typeof a=="boolean"&&(a=null),n=Lt(a)?a:[a,a]}return[JT(n[0]),JT(n[1])]}function JT(e){return mc(typeof e=="boolean"?0:e,1)||0}function tB(e){var t=IEe(e.scale);return t.extent||(t.extent=So()),t}function OEe(e,t){tB(e).dimIdxInCoord=t.get(e.dim)}function $Ee(e,t){var n=e.scale,a=e.model,r=e.dim;n.rawExtentInfo||REe(n,e,r,a,t)}function REe(e,t,n,a,r){var o=tB(t),i=o.extent,l=!1;kEe(t,function(c){if(c.boxCoordinateSystem){var d=SR(c).coord,v=o.dimIdxInCoord;if(v>=0){if(Lt(d)){var f=d[v];f!=null&&!Lt(f)&&B_(i,e.parse(f))}}}else if(c.coordinateSystem){var h=c.getData();if(h){var m=e.getFilter?e.getFilter():null;we(gEe(h,n),function(g){pCe(i,h.getApproximateExtent(g,m))})}c.__requireStartValue&&c.__requireStartValue(t)&&(l=!0)}});var s=VEe(e,t,a),u=new eB(e,a,i,l,s);nB(e,u,r),o.extent=null}function NEe(e,t){var n=e.scale;nB(n,new eB(n,e.model,t,!1,!1),PEe)}function nB(e,t,n){e.rawExtentInfo=t,t.from=n}function BEe(e,t){mS.set(e,t)}var mS=an();function aB(e,t,n,a,r){e.rawExtentInfo||NEe({scale:e,model:t},So());var o=e.rawExtentInfo.makeFinal(),i=o.effMM;return e.setExtent(i[0],i[1]),e.setBlank(o.isBlank),a&&o.tggAxInv&&n&&!n.get("legacyMinMaxDontInverseAxis")&&(a.inverse=!a.inverse),o}function VEe(e,t,n){var a=GN(e,n),r=n.get("containShape",!0);if(r==null&&!a&&(r=!0),!r)return!1;var o=!1;return JN(t,function(i){o=!!mS.get(i)||o}),o}function zEe(e,t,n,a){if(n.ctnShp){var r;if(JN(e,function(l){var s=mS.get(l);if(s){var u=s(e,a);u&&(r=r||[0,0],u$(r,u[0]),c$(r,u[1]),hEe(e))}}),!!r){var o=t.getExtent();if(pi(t))e.onBand||t.setExtent2(Rh,sr(o[0],o[0]+r[0]),An(o[1],o[1]+r[1]));else{var i=o.slice();n.zoomFixMM[0]||(i[0]=sr(i[0],t.transformOut(t.transformIn(i[0],null)+r[0],null))),n.zoomFixMM[1]||(i[1]=An(i[1],t.transformOut(t.transformIn(i[1],null)+r[1],null))),(i[0]o[1])&&t.setExtent2(Rh,i[0],i[1])}}}}function QT(e,t){var n=rf(e),a=n?e.intervalStub:e,r=t.fixMinMax||[],o=n?e.getExtent():null,i=a.getExtent(),l=BN(i,r,t.rawExtentResult);a.setExtent(l[0],l[1]),l=a.getExtent();var s=n?HEe(a,t):FEe(a,t),u=s.intervalPrecision,c=s.interval,d=t.userInterval;d!=null&&(s.interval=d,s.intervalPrecision=Sc(d)),r[0]||(l[0]=En(gc(l[0]/c)*c,u)),r[1]||(l[1]=En(yp(l[1]/c)*c,u)),d!=null&&(s.niceExtent=l.slice()),UN(e,r,i,l,o,s)}function FEe(e,t){var n=fS(t.splitNumber,5),a=S2(e),r=t.minInterval,o=t.maxInterval,i=u8(a/n,!0);r!=null&&io&&(i=o);var l=Sc(i),s=e.getExtent(),u=[En(yp(s[0]/i)*i,l),En(gc(s[1]/i)*i,l)];return{interval:i,intervalPrecision:l,niceExtent:u}}function HEe(e,t){var n=fS(t.splitNumber,10),a=e.getExtent(),r=S2(e),o=An(n$(r),1),i=n/r*o;i<=.5&&(o*=10);var l=Sc(o),s=[En(yp(a[0]/o)*o,l),En(gc(a[1]/o)*o,l)];return{intervalPrecision:l,interval:o,niceExtent:s}}function eM(e){var t=e.scale,n=e.model,a=n.axis,r=n.ecModel;WEe(t,n,a,r)}function WEe(e,t,n,a,r){var o=aB(e,t,a,n),i=jg(e)||dS(e);KEe(e,{splitNumber:t.get("splitNumber"),fixMinMax:o.fixMM,userInterval:t.get("interval"),minInterval:i?t.get("minInterval"):null,maxInterval:i?t.get("maxInterval"):null,rawExtentResult:o}),n&&a&&zEe(n,e,o,a)}function KEe(e,t){UEe[e.type](e,t)}var UEe={interval:QT,log:QT,time:sEe,ordinal:lr},tM=[],GEe={registerPreprocessor:kN,registerProcessor:TN,registerPostInit:pMe,registerPostUpdate:mMe,registerUpdateLifecycle:oS,registerAction:Sf,registerCoordinateSystem:gMe,registerLayout:yMe,registerVisual:Lc,registerTransform:_Me,registerLoading:MN,registerMap:bMe,registerImpl:bTe,PRIORITY:cMe,ComponentModel:Kn,ComponentView:el,SeriesModel:ai,ChartView:Oo,registerComponentModel:function(e){Kn.registerClass(e)},registerComponentView:function(e){el.registerClass(e)},registerSeriesModel:function(e){ai.registerClass(e)},registerChartView:function(e){Oo.registerClass(e)},registerCustomSeries:function(e,t){},registerSubTypeDefaulter:function(e,t){Kn.registerSubTypeDefaulter(e,t)},registerPainter:function(e,t){BSe(e,t)}};function Ws(e){if(Lt(e)){we(e,function(t){Ws(t)});return}Pn(tM,e)>=0||(tM.push(e),dn(e)&&(e={install:e}),e.install(GEe))}var YEe=Hn(),Yv=Hn(),ri={estimate:1,determine:2};function Xg(e){return{out:{noPxChangeTryDetermine:[]},kind:e}}function jEe(e,t){var n=e.getLabelModel().get("customValues");if(n){var a=e.scale;return{labels:Xt(rB(n,a),function(r,o){return{formattedLabel:C2(e)(r,o),rawLabel:a.getLabel(r),tick:r}})}}return e.type==="category"?XEe(e,t):JEe(e)}function qEe(e,t,n){var a=e.scale,r=e.getTickModel().get("customValues");return r?{ticks:rB(r,a)}:e.type==="category"?ZEe(e,t):{ticks:a.getTicks(n)}}function rB(e,t){var n=t.getExtent(),a=[];return we(e,function(r){r=t.parse(r),r>=n[0]&&r<=n[1]&&a.push(r)}),v8(a,_Ce,null),l8(a),Xt(a,function(r){return{value:r}})}function XEe(e,t){var n=e.getLabelModel(),a=oB(e,n,t);return!n.get("show")||e.scale.isBlank()?{labels:[]}:a}function oB(e,t,n){var a=e7e(e),r=hS(t),o=n.kind===ri.estimate;if(!o){var i=lB(a,r);if(i)return i}var l,s;dn(r)?l=Zg(e,r,!1):(s=r==="auto"?t7e(e,n):r,l=Zg(e,s,!1));var u={labels:l,labelCategoryInterval:s};return o?n.out.noPxChangeTryDetermine.push(function(){return w3(a,r,u),!0}):w3(a,r,u),u}function ZEe(e,t){var n=QEe(e),a=hS(t),r=lB(n,a);if(r)return r;var o,i;if((!t.get("show")||e.scale.isBlank())&&(o=[]),dn(a))o=Zg(e,a,!0);else if(a==="auto"){var l=oB(e,e.getLabelModel(),Xg(ri.determine));i=l.labelCategoryInterval,o=Xt(l.labels,function(s){return s.tick})}else i=a,o=Zg(e,i,!0);return w3(n,a,{ticks:o,tickCategoryInterval:i})}function JEe(e){var t=e.scale.getTicks(),n=C2(e);return{labels:Xt(t,function(a,r){return{formattedLabel:n(a,r),rawLabel:e.scale.getLabel(a),tick:a}})}}var QEe=iB("axisTick"),e7e=iB("axisLabel");function iB(e){return function(n){return Yv(n)[e]||(Yv(n)[e]={list:[]})}}function lB(e,t){for(var n=0;nc&&(u=Math.max(1,Math.floor(s/c)));for(var d=l[0],v=e.dataToCoord(d+1)-e.dataToCoord(d),f=Math.abs(v*Math.cos(o)),h=Math.abs(v*Math.sin(o)),m=0,g=0;d<=l[1];d+=u){var y=0,b=0,_=ZO(r({value:d}),a.font,"center","top");y=_.width*1.3,b=_.height*1.3,m=Math.max(m,y,7),g=Math.max(g,b,7)}var w=m/f,S=g/h;isNaN(w)&&(w=1/0),isNaN(S)&&(S=1/0);var k=Math.max(0,Math.floor(Math.min(w,S)));if(n===ri.estimate)return t.out.noPxChangeTryDetermine.push(qn(a7e,null,e,k,s)),k;var T=sB(e,k,s);return T??k}function a7e(e,t,n){return sB(e,t,n)==null}function sB(e,t,n){var a=YEe(e.model),r=e.getExtent(),o=a.lastAutoInterval,i=a.lastTickCount;if(o!=null&&i!=null&&Math.abs(o-t)<=1&&Math.abs(i-n)<=1&&o>t&&a.axisExtent0===r[0]&&a.axisExtent1===r[1])return o;a.lastTickCount=n,a.lastAutoInterval=t,a.axisExtent0=r[0],a.axisExtent1=r[1]}function r7e(e){var t=e.getLabelModel();return{axisRotate:e.getRotate?e.getRotate():e.isHorizontal&&!e.isHorizontal()?90:0,labelRotate:t.get("rotate")||0,font:t.getFont()}}function Zg(e,t,n){var a=C2(e),r=e.scale,o=[],i=dn(t);return VN(r,i?0:t,function(l,s){var u=r.getLabel(l);if(i){var c=!!t(l.value,u);if(l.offInterval=!c,!c&&!s)return}o.push(n?l:{formattedLabel:a(l),rawLabel:u,tick:l})}),o}var o7e=.8;function xf(e,t){t=t||{};var n={w:NaN,w2:NaN},a=e.scale,r=t.fromStat,o=t.min,i=jMe(a);zo(i)||(i=NaN);var l=e.getExtent(),s=Ca(l[1]-l[0]);return pi(a)?i7e(n,e,i,s):r&&l7e(n,e,i,s,r),o!=null&&(n.w=zo(n.w)?An(o,n.w):o),n}function i7e(e,t,n,a){var r=t.onBand,o=n+(r?1:0);o===0&&(o=1),e.w=a/o,!r&&n&&a&&(e.w2=e.w*n/a)}function l7e(e,t,n,a,r){var o=!1,i=-1/0;we(r.key?[CEe(t,r.key)]:xEe(t,r.sers||[]),function(l){var s=l.liPosMinGap;s!=null&&(s>0?(s>i&&(i=s),o=!1):s===YN&&(o=!0))}),zo(n)&&n>0&&zo(i)?(e.w=a/n*i,e.w2=i):o&&(e.w=a*o7e,e.w2=e.w*n/a)}var nM=[0,1],s7e=function(){function e(t,n,a){this.onBand=!1,this.inverse=!1,this.dim=t,this.scale=n,this._extent=a||[0,0]}return e.prototype.contain=function(t){var n=this._extent,a=Math.min(n[0],n[1]),r=Math.max(n[0],n[1]);return t>=a&&t<=r},e.prototype.containData=function(t){return this.scale.contain(this.scale.parse(t))},e.prototype.getExtent=function(){return this._extent.slice()},e.prototype.setExtent=function(t,n){var a=this._extent;a[0]=t,a[1]=n},e.prototype.dataToCoord=function(t,n){var a=this.scale;return t=a.normalize(a.parse(t)),$_(t,nM,aM(this),n)},e.prototype.coordToData=function(t,n){var a=$_(t,aM(this),nM,n);return this.scale.scale(a)},e.prototype.pointToData=function(t,n){},e.prototype.getTicksCoords=function(t){t=t||{};var n=t.tickModel||this.getTickModel(),a=qEe(this,n,{breakTicks:t.breakTicks,pruneByBreak:t.pruneByBreak}),r=Xt(a.ticks,function(l){return{coord:this.dataToCoord(Tp(this.scale,l)),tick:l}},this),o=n.get("alignWithLabel"),i=u7e(this,r,o);return Xt(r,function(l){return{coord:l.coord,tickValue:l.tick.value,onBand:i}})},e.prototype.getMinorTicksCoords=function(){if(pi(this.scale))return[];var t=this.model.getModel("minorTick"),n=t.get("splitNumber");n>0&&n<100||(n=5);var a=this.scale.getMinorTicks(n),r=Xt(a,function(o){return Xt(o,function(i){return{coord:this.dataToCoord(i),tickValue:i}},this)},this);return r},e.prototype.getViewLabels=function(t){return t=t||Xg(ri.determine),jEe(this,t).labels},e.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},e.prototype.getTickModel=function(){return this.model.getModel("axisTick")},e.prototype.getBandWidth=function(){return xf(this,{min:1}).w},e.prototype.calculateCategoryInterval=function(t){return t=t||Xg(ri.determine),n7e(this,t)},e}();function aM(e){var t=e.getExtent();if(e.onBand){var n=t[1]-t[0],a=n/e.scale.count()/2;t[0]+=a,t[1]-=a}return t}function u7e(e,t,n){var a=t.length;if(!e.onBand||n||!a)return!1;var r=xf(e).w;if(!r)return!1;we(t,function(l){l.coord-=r/2});var o=e.scale.getExtent(),i=t[a-1];return i.tick.offInterval&&t.pop(),t.push({coord:i.coord+r,tick:{value:o[1]+1}}),!0}function uB(e,t,n,a,r,o,i,l){var s=r-e,u=o-t,c=n-e,d=a-t,v=Math.sqrt(c*c+d*d);c/=v,d/=v;var f=s*c+u*d,h=f/v;h*=v;var m=i[0]=e+h*c,g=i[1]=t+h*d;return Math.sqrt((m-r)*(m-r)+(g-o)*(g-o))}var _s=new fn,ha=new fn,Ka=new fn,ws=new fn,Ai=new fn,Jg=[],yr=new fn;function c7e(e,t){if(t<=180&&t>0){t=t/180*Math.PI,_s.fromArray(e[0]),ha.fromArray(e[1]),Ka.fromArray(e[2]),fn.sub(ws,_s,ha),fn.sub(Ai,Ka,ha);var n=ws.len(),a=Ai.len();if(!(n<.001||a<.001)){ws.scale(1/n),Ai.scale(1/a);var r=ws.dot(Ai),o=Math.cos(t);if(o1&&fn.copy(yr,Ka),yr.toArray(e[1])}}}}function d7e(e,t,n){if(n<=180&&n>0){n=n/180*Math.PI,_s.fromArray(e[0]),ha.fromArray(e[1]),Ka.fromArray(e[2]),fn.sub(ws,ha,_s),fn.sub(Ai,Ka,ha);var a=ws.len(),r=Ai.len();if(!(a<.001||r<.001)){ws.scale(1/a),Ai.scale(1/r);var o=ws.dot(t),i=Math.cos(n);if(o=s)fn.copy(yr,Ka);else{yr.scaleAndAdd(Ai,l/Math.tan(Math.PI/2-c));var d=Ka.x!==ha.x?(yr.x-ha.x)/(Ka.x-ha.x):(yr.y-ha.y)/(Ka.y-ha.y);if(isNaN(d))return;d<0?fn.copy(yr,ha):d>1&&fn.copy(yr,Ka)}yr.toArray(e[1])}}}}function Ab(e,t,n,a){var r=n==="normal",o=r?e:e.ensureState(n);o.ignore=t;var i=a.get("smooth");i=i===!0?.3:Math.max(+i,0)||0,o.shape=o.shape||{},o.shape.smooth=i;var l=a.getModel("lineStyle").getLineStyle();r?e.useStyle(l):o.style=l}function f7e(e,t){var n=t.smooth,a=t.points;if(a)if(e.moveTo(a[0][0],a[0][1]),n>0&&a.length>=3){var r=p_(a[0],a[1]),o=p_(a[1],a[2]);if(!r||!o){e.lineTo(a[1][0],a[1][1]),e.lineTo(a[2][0],a[2][1]);return}var i=Math.min(r,o)*n,l=wy([],a[1],a[0],i/r),s=wy([],a[1],a[2],i/o),u=wy([],l,s,.5);e.bezierCurveTo(l[0],l[1],l[0],l[1],u[0],u[1]),e.bezierCurveTo(s[0],s[1],s[0],s[1],a[2][0],a[2][1])}else for(var c=1;c0){w(A*E,0,o);var I=A+T;I<0&&S(-I*E,1)}else S(-T*E,1)}}function w(T,M,E){T!==0&&(c=!0);for(var A=M;A0)for(var I=0;I0;I--){var K=E[I-1]*G;w(-K,I,o)}}}function k(T){var M=T<0?-1:1;T=Math.abs(T);for(var E=Math.ceil(T/(o-1)),A=0;A0?w(E,0,A+1):w(-E,o-A-1,o),T-=E,T<=0)return}return c}function b7e(e){var t=[];e.sort(function(u,c){return(c.suggestIgnore?1:0)-(u.suggestIgnore?1:0)||c.priority-u.priority});function n(u){if(!u.ignore){var c=u.ensureState("emphasis");c.ignore==null&&(c.ignore=!1)}u.ignore=!0}for(var a=0;a-1&&(u.style.stroke=u.style.fill,u.style.fill=tn.color.neutral00,u.style.lineWidth=2),a},t.type="series.line",t.dependencies=["grid","polar"],t.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:6,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1,triggerEvent:!1},t}(ai);function yS(e,t){var n=e.mapDimensionsAll("defaultedLabel"),a=n.length;if(a===1){var r=tf(e,t,n[0]);return r!=null?r+"":null}else if(a){for(var o=[],i=0;i=0&&a.push(t[o])}return a.join(" ")}var bS=function(e){At(t,e);function t(n,a,r,o){var i=e.call(this)||this;return i.updateData(n,a,r,o),i}return t.prototype._createSymbol=function(n,a,r,o,i,l){this.removeAll();var s=nf(n,-1,-1,2,2,null,l);s.attr({z2:jt(i,100),culling:!0,scaleX:o[0]/2,scaleY:o[1]/2}),s.drift=w7e,this._symbolType=n,this.add(s)},t.prototype.stopSymbolAnimation=function(n){this.childAt(0).stopAnimation(null,n)},t.prototype.getSymbolType=function(){return this._symbolType},t.prototype.getSymbolPath=function(){return this.childAt(0)},t.prototype.highlight=function(){Dg(this.childAt(0))},t.prototype.downplay=function(){Og(this.childAt(0))},t.prototype.setZ=function(n,a){var r=this.childAt(0);r.zlevel=n,r.z=a},t.prototype.setDraggable=function(n,a){var r=this.childAt(0);r.draggable=n,r.cursor=!a&&n?"move":r.cursor},t.prototype.updateData=function(n,a,r,o){this.silent=!1;var i=n.getItemVisual(a,"symbol")||"circle",l=n.hostModel,s=t.getSymbolSize(n,a),u=t.getSymbolZ2(n,a),c=i!==this._symbolType,d=o&&o.disableAnimation;if(c){var v=n.getItemVisual(a,"symbolKeepAspect");this._createSymbol(i,n,a,s,u,v)}else{var f=this.childAt(0);f.silent=!1;var h={scaleX:s[0]/2,scaleY:s[1]/2};d?f.attr(h):Ar(f,h,l,a),M8(f)}if(this._updateCommon(n,a,s,r,o),c){var f=this.childAt(0);if(!d){var h={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:f.style.opacity}};f.scaleX=f.scaleY=0,f.style.opacity=0,Do(f,h,l,a)}}d&&this.childAt(0).stopAnimation("leave")},t.prototype._updateCommon=function(n,a,r,o,i){var l=this.childAt(0),s=n.hostModel,u,c,d,v,f,h,m,g,y;if(o&&(u=o.emphasisItemStyle,c=o.blurItemStyle,d=o.selectItemStyle,v=o.focus,f=o.blurScope,m=o.labelStatesModels,g=o.hoverScale,y=o.cursorStyle,h=o.emphasisDisabled),!o||n.hasItemOption){var b=o&&o.itemModel?o.itemModel:n.getItemModel(a),_=b.getModel("emphasis");u=_.getModel("itemStyle").getItemStyle(),d=b.getModel(["select","itemStyle"]).getItemStyle(),c=b.getModel(["blur","itemStyle"]).getItemStyle(),v=_.get("focus"),f=_.get("blurScope"),h=_.get("disabled"),m=xp(b),g=_.getShallow("scale"),y=b.getShallow("cursor")}var w=n.getItemVisual(a,"symbolRotate");l.attr("rotation",(w||0)*Math.PI/180||0);var S=oN(n.getItemVisual(a,"symbolOffset"),r);S&&(l.x=S[0],l.y=S[1]),y&&l.attr("cursor",y);var k=n.getItemVisual(a,"style"),T=k.fill;if(l instanceof js){var M=l.style;l.useStyle(vt({image:M.image,x:M.x,y:M.y,width:M.width,height:M.height},k))}else l.__isEmptyBrush?l.useStyle(vt({},k)):l.useStyle(k),l.style.decal=null,l.setColor(T,i&&i.symbolInnerColor),l.style.strokeNoScale=!0;var E=n.getItemVisual(a,"liftZ"),A=this._z2;E!=null?A==null&&(this._z2=l.z2,l.z2+=E):A!=null&&(l.z2=A,this._z2=null);var I=i&&i.useNameLabel;Cp(l,m,{labelFetcher:s,labelDataIndex:a,defaultText:P,inheritColor:T,defaultOpacity:k.opacity});function P(K){return I?n.getName(K):yS(n,K)}this._sizeX=r[0]/2,this._sizeY=r[1]/2;var R=l.ensureState("emphasis");R.style=u,l.ensureState("select").style=d,l.ensureState("blur").style=c;var G=g==null||g===!0?Math.max(1.1,3/this._sizeY):isFinite(g)&&g>0?+g:1;R.scaleX=this._sizeX*G,R.scaleY=this._sizeY*G,this.setSymbolScale(1),Ah(this,v,f,h)},t.prototype.setSymbolScale=function(n){this.scaleX=this.scaleY=n},t.prototype.fadeOut=function(n,a,r){var o=this.childAt(0),i=In(this).dataIndex,l=r&&r.animation;if(this.silent=o.silent=!0,r&&r.fadeLabel){var s=o.getTextContent();s&&Rg(s,{style:{opacity:0}},a,{dataIndex:i,removeOpt:l,cb:function(){o.removeTextContent()}})}else o.removeTextContent();Rg(o,{style:{opacity:0},scaleX:0,scaleY:0},a,{dataIndex:i,cb:n,removeOpt:l})},t.getSymbolSize=function(n,a){return $Te(n.getItemVisual(a,"symbolSize"))},t.getSymbolZ2=function(n,a){return n.getItemVisual(a,"z2")},t}(Ha);function w7e(e,t){this.parent.drift(e,t)}function N0(e,t,n,a){return t&&!isNaN(t[0])&&!isNaN(t[1])&&!(a&&a.isIgnore&&a.isIgnore(n))&&!(a&&a.clipShape&&!a.clipShape.contain(t[0],t[1]))&&e.getItemVisual(n,"symbol")!=="none"}function lM(e){return e!=null&&!Yt(e)&&(e={isIgnore:e}),e||{}}function sM(e){var t=e.hostModel,n=t.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:t.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:t.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:xp(t),cursorStyle:t.get("cursor")}}function uM(e,t,n,a,r,o,i){var l=new e(t,n,a,r);return l.setPosition(o),t.setItemGraphicEl(n,l),i.add(l),l}var S7e=function(){function e(t){this.group=new Ha,this._SymbolCtor=t||bS}return e.prototype.updateData=function(t,n){this._progressiveEls=null,n=lM(n);var a=this.group,r=t.hostModel,o=this._data,i=this._SymbolCtor,l=n.disableAnimation,s=this._seriesScope=sM(t),u={disableAnimation:l},c=n.getSymbolPoint||function(d){return t.getItemLayout(d)};o||a.removeAll(),t.diff(o).add(function(d){var v=c(d);N0(t,v,d,n)&&uM(i,t,d,s,u,v,a)}).update(function(d,v){var f=o.getItemGraphicEl(v),h=c(d);if(!N0(t,h,d,n)){a.remove(f);return}var m=t.getItemVisual(d,"symbol")||"circle",g=f&&f.getSymbolType&&f.getSymbolType();if(!f||g&&g!==m)a.remove(f),f=new i(t,d,s,u),f.setPosition(h);else{f.updateData(t,d,s,u);var y={x:h[0],y:h[1]};l?f.attr(y):Ar(f,y,r)}a.add(f),t.setItemGraphicEl(d,f)}).remove(function(d){var v=o.getItemGraphicEl(d);v&&v.fadeOut(function(){a.remove(v)},r)}).execute(),this._getSymbolPoint=c,this._data=t},e.prototype.updateLayout=function(t){var n=this._data;if(n)for(var a=this,r=n.getStore(),o=0,i=r.count();o0?n=a[0]:a[1]<0&&(n=a[1]),n}function pB(e,t,n,a){var r=NaN;e.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),a)),isNaN(r)&&(r=e.valueStart);var o=e.baseDataOffset,i=[];return i[o]=n.get(e.baseDim,a),i[1-o]=r,t.dataToPoint(i)}function $o(e,t){return!isFinite(e)||!isFinite(t)}var x7e=typeof Float32Array!==_f?Float32Array:void 0,k7e=typeof Float64Array!==_f?Float64Array:void 0;function gl(e){return _S({ctor:x7e},e).arr}function _S(e,t){var n=e.arr,a=e.ctor;if(t>Wk&&(t=Wk),!n||e.typed&&n.length=r||m<0)break;if($o(y,b)){if(s){m+=o;continue}break}if(m===n)e[o>0?"moveTo":"lineTo"](y,b),d=y,v=b;else{var _=y-u,w=b-c;if(_*_+w*w<.5){m+=o;continue}if(i>0){for(var S=m+o,k=t[S*2],T=t[S*2+1];k===y&&T===b&&g=a||$o(k,T))f=y,h=b;else{A=k-u,I=T-c;var G=y-u,K=k-y,H=b-c,z=T-b,B=void 0,W=void 0;if(l==="x"){B=Math.abs(G),W=Math.abs(K);var N=A>0?1:-1;f=y-N*B*i,h=b,P=y+N*W*i,R=b}else if(l==="y"){B=Math.abs(H),W=Math.abs(z);var V=I>0?1:-1;f=y,h=b-V*B*i,P=y,R=b+V*W*i}else B=Math.sqrt(G*G+H*H),W=Math.sqrt(K*K+z*z),E=W/(W+B),f=y-A*i*(1-E),h=b-I*i*(1-E),P=y+A*i*E,R=b+I*i*E,P=es(P,ts(k,y)),R=es(R,ts(T,b)),P=ts(P,es(k,y)),R=ts(R,es(T,b)),A=P-y,I=R-b,f=y-A*B/W,h=b-I*B/W,f=es(f,ts(u,y)),h=es(h,ts(c,b)),f=ts(f,es(u,y)),h=ts(h,es(c,b)),A=y-f,I=b-h,P=y+A*W/B,R=b+I*W/B}e.bezierCurveTo(d,v,f,h,y,b),d=P,v=R}else e.lineTo(y,b)}u=y,c=b,m+=o}return g}var mB=function(){function e(){this.smooth=0,this.smoothConstraint=!0}return e}(),E7e=function(e){At(t,e);function t(n){var a=e.call(this,n)||this;return a.type="ec-polyline",a}return t.prototype.getDefaultStyle=function(){return{stroke:tn.color.neutral99,fill:null}},t.prototype.getDefaultShape=function(){return new mB},t.prototype.buildPath=function(n,a){var r=a.points,o=0,i=r.length/2;if(a.connectNulls){for(;i>0&&$o(r[i*2-2],r[i*2-1]);i--);for(;o=0){var w=u?(h-s)*_+s:(f-l)*_+l;return u?[n,w]:[w,n]}l=f,s=h;break;case i.C:f=o[d++],h=o[d++],m=o[d++],g=o[d++],y=o[d++],b=o[d++];var S=u?_g(l,f,m,y,n,c):_g(s,h,g,b,n,c);if(S>0)for(var k=0;k=0){var w=u?Ua(s,h,g,b,T):Ua(l,f,m,y,T);return u?[n,w]:[w,n]}}l=y,s=b;break}}},t}(Fn),A7e=function(e){At(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(mB),I7e=function(e){At(t,e);function t(n){var a=e.call(this,n)||this;return a.type="ec-polygon",a}return t.prototype.getDefaultShape=function(){return new A7e},t.prototype.buildPath=function(n,a){var r=a.points,o=a.stackedOnPoints,i=0,l=r.length/2,s=a.smoothMonotone;if(a.connectNulls){for(;l>0&&$o(r[l*2-2],r[l*2-1]);l--);for(;it){o?n.push(i(o,s,t)):r&&n.push(i(r,s,0),i(r,s,t));break}else r&&(n.push(i(r,s,0)),r=null),n.push(s),o=s}return n}function O7e(e,t,n){var a=e.getVisual("visualMeta");if(!(!a||!a.length||!e.count())&&t.type==="cartesian2d"){for(var r,o,i=a.length-1;i>=0;i--){var l=e.getDimensionInfo(a[i].dimension);if(r=l&&l.coordDim,r==="x"||r==="y"){o=a[i];break}}if(o){var s=t.getAxis(r),u=Xt(o.stops,function(_){return{coord:s.toGlobalCoord(s.dataToCoord(_.value)),color:_.color}}),c=u.length,d=o.outerColors.slice();c&&u[0].coord>u[c-1].coord&&(u.reverse(),d.reverse());var v=D7e(u,r==="x"?n.getWidth():n.getHeight()),f=v.length;if(!f&&c)return u[0].coord<0?d[1]?d[1]:u[c-1].color:d[0]?d[0]:u[0].color;var h=10,m=v[0].coord-h,g=v[f-1].coord+h,y=g-m;if(y<.001)return"transparent";we(v,function(_){_.offset=(_.coord-m)/y}),v.push({offset:f?v[f-1].offset:.5,color:d[1]||"transparent"}),v.unshift({offset:f?v[0].offset:.5,color:d[0]||"transparent"});var b=new U$(0,0,0,0,v,!0);return b[r]=m,b[r+"2"]=g,b}}}function $7e(e,t,n){var a=e.get("showAllSymbol"),r=a==="auto";if(!(a&&!r)){var o=n.getAxesByScale("ordinal")[0];if(o&&!(r&&R7e(o,t))){var i=t.mapDimension(o.dim),l={};return we(o.getViewLabels(),function(s){s.tick.offInterval||(l[Tp(o.scale,s.tick)]=1)}),function(s){return!l.hasOwnProperty(t.get(i,s))}}}}function R7e(e,t){var n=e.getExtent(),a=Math.abs(n[1]-n[0])/e.scale.count();isNaN(a)&&(a=0);for(var r=t.count(),o=Math.max(1,Math.round(r/5)),i=0;ia)return!1;return!0}function N7e(e){for(var t=e.length/2;t>0&&$o(e[t*2-2],e[t*2-1]);t--);return t-1}function hM(e,t){return[e[t*2],e[t*2+1]]}function B7e(e,t,n){for(var a=e.length/2,r=n==="x"?0:1,o,i,l=0,s=-1,u=0;u=t||o>=t&&i<=t){s=u;break}l=u,o=i}return{range:[l,s],t:(t-o)/(i-o)}}function _B(e){if(e.get(["endLabel","show"]))return!0;for(var t=0;t0&&n.get(["emphasis","lineStyle","width"])==="bolder"){var W=h.getState("emphasis").style;W.lineWidth=+h.style.lineWidth+1}In(h).seriesIndex=n.seriesIndex,Ah(h,H,z,B);var N=vM(n.get("smooth")),V=n.get("smoothMonotone");if(h.setShape({smooth:N,smoothMonotone:V,connectNulls:T}),m){var Y=l.getCalculationInfo("stackedOnSeries"),X=0;m.useStyle(zn(u.getAreaStyle(),{fill:P,opacity:.7,lineJoin:"bevel",decal:l.getVisual("style").decal})),Y&&(X=vM(Y.get("smooth"))),m.setShape({smooth:N,stackedOnSmooth:X,smoothMonotone:V,connectNulls:T}),$g(m,n,"areaStyle"),In(m).seriesIndex=n.seriesIndex,Ah(m,H,z,B)}var ee=this._changePolyState;l.eachItemGraphicEl(function(he){he&&(he.onHoverStateChange=ee)}),this._polyline.onHoverStateChange=ee,this._data=l,this._coordSys=o,this._stackedOnPoints=S,this._points=c,this._step=A,this._valueOrigin=_;var re=n.get("triggerEvent"),ie=n.get("triggerLineEvent"),le=ie===!0||re===!0||re==="line",ve=ie===!0||re===!0||re==="area";this.packEventData(n,h,le),m&&this.packEventData(n,m,ve)},t.prototype.packEventData=function(n,a,r){In(a).eventData=r?{componentType:"series",componentSubType:"line",componentIndex:n.componentIndex,seriesIndex:n.seriesIndex,seriesName:n.name,seriesType:"line",selfType:a===this._polygon?"area":"line"}:null},t.prototype.highlight=function(n,a,r,o){var i=n.getData(),l=yc(i,o);if(this._changePolyState("emphasis"),!(l instanceof Array)&&l!=null&&l>=0){var s=i.getLayout("points"),u=i.getItemGraphicEl(l);if(!u){var c=s[l*2],d=s[l*2+1];if($o(c,d)||this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(c,d))return;var v=n.get("zlevel")||0,f=n.get("z")||0;u=new bS(i,l),u.x=c,u.y=d,u.setZ(v,f);var h=u.getSymbolPath().getTextContent();h&&(h.zlevel=v,h.z=f,h.z2=this._polyline.z2+1),u.__temp=!0,i.setItemGraphicEl(l,u),u.stopSymbolAnimation(!0),this.group.add(u)}u.highlight()}else Oo.prototype.highlight.call(this,n,a,r,o)},t.prototype.downplay=function(n,a,r,o){var i=n.getData(),l=yc(i,o);if(this._changePolyState("normal"),l!=null&&l>=0){var s=i.getItemGraphicEl(l);s&&(s.__temp?(i.setItemGraphicEl(l,null),this.group.remove(s)):s.downplay())}else Oo.prototype.downplay.call(this,n,a,r,o)},t.prototype._changePolyState=function(n){var a=this._polygon;p9(this._polyline,n),a&&p9(a,n)},t.prototype._newPolyline=function(n){var a=this._polyline;return a&&this._lineGroup.remove(a),a=new E7e({shape:{points:n},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(a),this._polyline=a,a},t.prototype._newPolygon=function(n,a){var r=this._polygon;return r&&this._lineGroup.remove(r),r=new I7e({shape:{points:n,stackedOnPoints:a},segmentIgnoreThreshold:2}),this._lineGroup.add(r),this._polygon=r,r},t.prototype._initSymbolLabelAnimation=function(n,a,r){var o,i,l=a.getBaseAxis(),s=l.inverse;a.type==="cartesian2d"?(o=l.isHorizontal(),i=!1):a.type==="polar"&&(o=l.dim==="angle",i=!0);var u=n.hostModel,c=u.get("animationDuration");dn(c)&&(c=c(null));var d=u.get("animationDelay")||0,v=dn(d)?d(null):d;n.eachItemGraphicEl(function(f,h){var m=f;if(m){var g=[f.x,f.y],y=void 0,b=void 0,_=void 0;if(r)if(i){var w=r,S=a.pointToCoord(g);o?(y=w.startAngle,b=w.endAngle,_=-S[1]/180*Math.PI):(y=w.r0,b=w.r,_=S[0])}else{var k=r;o?(y=k.x,b=k.x+k.width,_=f.x):(y=k.y+k.height,b=k.y,_=f.y)}var T=b===y?0:(_-y)/(b-y);s&&(T=1-T);var M=dn(d)?d(h):c*T+v,E=m.getSymbolPath(),A=E.getTextContent();m.attr({scaleX:0,scaleY:0}),m.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:M}),A&&A.animateFrom({style:{opacity:0}},{duration:300,delay:M}),E.disableLabelAnimation=!0}})},t.prototype._initOrUpdateEndLabel=function(n,a,r){var o=n.getModel("endLabel");if(_B(n)){var i=n.getData(),l=this._polyline,s=i.getLayout("points");if(!s){l.removeTextContent(),this._endLabel=null;return}var u=this._endLabel;u||(u=this._endLabel=new Er({z2:200}),u.ignoreClip=!0,l.setTextContent(this._endLabel),l.disableLabelAnimation=!0);var c=N7e(s);c>=0&&(Cp(l,xp(n,"endLabel"),{inheritColor:r,labelFetcher:n,labelDataIndex:c,defaultText:function(d,v,f){return f!=null?vB(i,f):yS(i,d)},enableTextSetter:!0},V7e(o,a)),l.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},t.prototype._endLabelOnDuring=function(n,a,r,o,i,l,s){var u=this._endLabel,c=this._polyline;if(u){n<1&&o.originalX==null&&(o.originalX=u.x,o.originalY=u.y);var d=r.getLayout("points"),v=r.hostModel,f=v.get("connectNulls"),h=l.get("precision"),m=l.get("distance")||0,g=s.getBaseAxis(),y=g.isHorizontal(),b=g.inverse,_=a.shape,w=b?y?_.x:_.y+_.height:y?_.x+_.width:_.y,S=(y?m:0)*(b?-1:1),k=(y?0:-m)*(b?-1:1),T=y?"x":"y",M=B7e(d,w,T),E=M.range,A=E[1]-E[0],I=void 0;if(A>=1){if(A>1&&!f){var P=hM(d,E[0]);u.attr({x:P[0]+S,y:P[1]+k}),i&&(I=v.getRawValue(E[0]))}else{var P=c.getPointOn(w,T);P&&u.attr({x:P[0]+S,y:P[1]+k});var R=v.getRawValue(E[0]),G=v.getRawValue(E[1]);i&&(I=hCe(r,h,R,G,M.t))}o.lastFrameIndex=E[0]}else{var K=n===1||o.lastFrameIndex>0?E[0]:0,P=hM(d,K);i&&(I=v.getRawValue(K)),u.attr({x:P[0]+S,y:P[1]+k})}if(i){var H=h2(u);typeof H.setLabelText=="function"&&H.setLabelText(I)}}},t.prototype._doUpdateAnimation=function(n,a,r,o,i,l,s){var u=this._polyline,c=this._polygon,d=n.hostModel,v=M7e(this._data,n,this._stackedOnPoints,a,this._coordSys,r,this._valueOrigin),f=v.current,h=v.stackedOnCurrent,m=v.next,g=v.stackedOnNext;if(i&&(h=ns(v.stackedOnCurrent,v.current,r,i,s),f=ns(v.current,null,r,i,s),g=ns(v.stackedOnNext,v.next,r,i,s),m=ns(v.next,null,r,i,s)),fM(f,m)>3e3||c&&fM(h,g)>3e3){u.stopAnimation(),u.setShape({points:m}),c&&(c.stopAnimation(),c.setShape({points:m,stackedOnPoints:g}));return}u.shape.__points=v.current,u.shape.points=f;var y={shape:{points:m}};v.current!==f&&(y.shape.__points=v.next),u.stopAnimation(),Ar(u,y,d),c&&(c.setShape({points:f,stackedOnPoints:h}),c.stopAnimation(),Ar(c,{shape:{stackedOnPoints:g}},d),u.shape.points!==c.shape.points&&(c.shape.points=u.shape.points));for(var b=[],_=v.status,w=0;w<_.length;w++){var S=_[w].cmd;if(S==="="){var k=n.getItemGraphicEl(_[w].idx1);k&&b.push({el:k,ptIdx:w})}}u.animators&&u.animators.length&&u.animators[0].during(function(){c&&c.dirtyShape();for(var T=u.shape.__points,M=0;Mt&&(t=e[n]);return isFinite(t)?t:NaN},min:function(e){for(var t=1/0,n=0;n10&&i.type==="cartesian2d"&&o){var s=i.getBaseAxis(),u=i.getOtherAxis(s),c=s.getExtent(),d=a.getDevicePixelRatio(),v=Math.abs(c[1]-c[0])*(d||1),f=Math.round(l/v);if(isFinite(f)&&f>1){o==="lttb"?t.setData(r.lttbDownSample(r.mapDimension(u.dim),1/f)):o==="minmax"&&t.setData(r.minmaxDownSample(r.mapDimension(u.dim),1/f));var h=void 0;Ut(o)?h=H7e[o]:dn(o)&&(h=o),h&&t.setData(r.downSample(r.mapDimension(u.dim),1/f,h,W7e))}}}}}function K7e(e){e.registerChartView(z7e),e.registerSeriesModel(_7e),e.registerLayout(F7e("line")),e.registerVisual({seriesType:"line",reset:function(t){var n=t.getData(),a=t.getModel("lineStyle").getLineStyle();a&&!a.stroke&&(a.stroke=n.getVisual("style").fill),n.setVisual("legendLineStyle",a)}}),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,wB("line"))}var U7e=function(e){At(t,e);function t(n,a,r,o,i){var l=e.call(this,n,a,r)||this;return l.index=0,l.type=o||"value",l.position=i||"bottom",l}return t.prototype.isHorizontal=function(){var n=this.position;return n==="top"||n==="bottom"},t.prototype.getGlobalExtent=function(n){var a=this.getExtent();return a[0]=this.toGlobalCoord(a[0]),a[1]=this.toGlobalCoord(a[1]),n&&a[0]>a[1]&&a.reverse(),a},t.prototype.pointToData=function(n,a){return this.coordToData(this.toLocalCoord(n[this.dim==="x"?0:1]),a)},t.prototype.setCategorySortInfo=function(n){if(this.type!=="category")return!1;this.model.option.categorySortInfo=n,this.scale.setSortInfo(n)},t}(s7e),G7e=null;function Y7e(){return G7e}var j7e="expandAxisBreak",Ss=Math.PI,q7e=[[1,2,1,2],[5,3,5,3],[8,3,8,3]],X7e=[[0,1,0,1],[0,3,0,3],[0,3,0,3]],lf=Hn(),SB=Hn(),CB=function(){function e(t){this.recordMap={},this.resolveAxisNameOverlap=t}return e.prototype.ensureRecord=function(t){var n=t.axis.dim,a=t.componentIndex,r=this.recordMap,o=r[n]||(r[n]=[]);return o[a]||(o[a]={ready:{}})},e}();function Z7e(e,t,n,a){var r=n.axis,o=t.ensureRecord(n),i=[],l,s=wS(e.axisName)&&of(e.nameLocation);we(a,function(h){var m=Ks(h);if(!(!m||m.label.ignore)){i.push(m);var g=o.transGroup;s&&(g.transform?pp(Jf,g.transform):hp(Jf),m.transform&&$v(Jf,Jf,m.transform),ln.copy(B0,m.localRect),B0.applyTransform(Jf),l?l.union(B0):ln.copy(l=new ln(0,0,0,0),B0))}});var u=Math.abs(o.dirVec.x)>.1?"x":"y",c=o.transGroup[u];if(i.sort(function(h,m){return Math.abs(h.label[u]-c)-Math.abs(m.label[u]-c)}),s&&l){var d=r.getExtent(),v=Math.min(d[0],d[1]),f=Math.max(d[0],d[1])-v;l.union(new ln(v,0,f,1))}o.stOccupiedRect=l,o.labelInfoList=i}var Jf=zi(),B0=new ln(0,0,0,0),xB=function(e,t,n,a,r,o){if(of(e.nameLocation)){var i=o.stOccupiedRect;i&&kB(m7e({},i,o.transGroup.transform),a,r)}else TB(o.labelInfoList,o.dirVec,a,r)};function kB(e,t,n){var a=new fn;gS(e,t,a,{direction:Math.atan2(n.y,n.x),bidirectional:!1,touchThreshold:.05})&&g7e(t,a)}function TB(e,t,n,a){for(var r=fn.dot(a,t)>=0,o=0,i=e.length;o0?"top":"bottom",o="center"):Mg(r-Ss)?(i=a>0?"bottom":"top",o="center"):(i="middle",r>0&&r0?"right":"left":o=a>0?"left":"right"),{rotation:r,textAlign:o,textVerticalAlign:i}},e.makeAxisEventDataBase=function(t){var n={componentType:t.mainType,componentIndex:t.componentIndex};return n[t.mainType+"Index"]=t.componentIndex,n},e.isLabelSilent=function(t){var n=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||n&&n.show)},e}(),J7e=["axisLine","axisTickLabelEstimate","axisTickLabelDetermine","axisName"],Q7e={axisLine:function(e,t,n,a,r,o,i){var l=a.get(["axisLine","show"]);if(l==="auto"&&(l=!0,e.raw.axisLineAutoShow!=null&&(l=!!e.raw.axisLineAutoShow)),!!l){var s=a.axis.getExtent(),u=o.transform,c=[s[0],0],d=[s[1],0],v=c[0]>d[0];u&&(Po(c,c,u),Po(d,d,u));var f=vt({lineCap:"round"},a.getModel(["axisLine","lineStyle"]).getLineStyle()),h={strokeContainThreshold:e.raw.strokeContainThreshold||5,silent:!0,z2:1,style:f};if(a.get(["axisLine","breakLine"])&&Vg(a.axis.scale))Y7e().buildAxisBreakLine(a,r,o,h);else{var m=new Fs(vt({shape:{x1:c[0],y1:c[1],x2:d[0],y2:d[1]}},h));Ih(m.shape,m.style.lineWidth),m.anid="line",r.add(m)}var g=a.get(["axisLine","symbol"]);if(g!=null){var y=a.get(["axisLine","symbolSize"]);Ut(g)&&(g=[g,g]),(Ut(y)||Gn(y))&&(y=[y,y]);var b=oN(a.get(["axisLine","symbolOffset"])||0,y),_=y[0],w=y[1];we([{rotate:e.rotation+Math.PI/2,offset:b[0],r:0},{rotate:e.rotation-Math.PI/2,offset:b[1],r:Math.sqrt((c[0]-d[0])*(c[0]-d[0])+(c[1]-d[1])*(c[1]-d[1]))}],function(S,k){if(g[k]!=="none"&&g[k]!=null){var T=nf(g[k],-_/2,-w/2,_,w,f.stroke,!0),M=S.r+S.offset,E=v?d:c;T.attr({rotation:S.rotate,x:E[0]+M*Math.cos(e.rotation),y:E[1]-M*Math.sin(e.rotation),silent:!0,z2:11}),r.add(T)}})}}},axisTickLabelEstimate:function(e,t,n,a,r,o,i,l){var s=mM(t,r,l);s&&pM(e,t,n,a,r,o,i,ri.estimate)},axisTickLabelDetermine:function(e,t,n,a,r,o,i,l){var s=mM(t,r,l);s&&pM(e,t,n,a,r,o,i,ri.determine);var u=aAe(e,r,o,a);nAe(e,t.labelLayoutList,u),rAe(e,r,o,a,e.tickDirection)},axisName:function(e,t,n,a,r,o,i,l){var s=n.ensureRecord(a);t.nameEl&&(r.remove(t.nameEl),t.nameEl=s.nameLayout=s.nameLocation=null);var u=e.axisName;if(wS(u)){var c=e.nameLocation,d=e.nameDirection,v=a.getModel("nameTextStyle"),f=a.get("nameGap")||0,h=a.axis.getExtent(),m=a.axis.inverse?-1:1,g=new fn(0,0),y=new fn(0,0);c==="start"?(g.x=h[0]-m*f,y.x=-m):c==="end"?(g.x=h[1]+m*f,y.x=m):(g.x=(h[0]+h[1])/2,g.y=e.labelOffset+d*f,y.y=d);var b=zi();y.transform(o8(b,b,e.rotation));var _=a.get("nameRotate");_!=null&&(_=_*Ss/180);var w,S;of(c)?w=Es.innerTextLayout(e.rotation,_??e.rotation,d):(w=eAe(e.rotation,c,_||0,h),S=e.raw.axisNameAvailableWidth,S!=null&&(S=Math.abs(S/Math.sin(w.rotation)),!isFinite(S)&&(S=null)));var k=v.getFont(),T=a.get("nameTruncate",!0)||{},M=T.ellipsis,E=mg(e.raw.nameTruncateMaxWidth,T.maxWidth,S),A=l.nameMarginLevel||0,I=new Er({x:g.x,y:g.y,rotation:w.rotation,silent:Es.isLabelSilent(a),style:ef(v,{text:u,font:k,overflow:"truncate",width:E,ellipsis:M,fill:v.getTextColor()||a.get(["axisLine","lineStyle","color"]),align:v.get("align")||w.textAlign,verticalAlign:v.get("verticalAlign")||w.textVerticalAlign}),z2:1});if(d2({el:I,componentModel:a,itemName:u}),I.__fullText=u,I.anid="name",a.get("triggerEvent")){var P=Es.makeAxisEventDataBase(a);P.targetType="axisName",P.name=u,In(I).eventData=P}o.add(I),I.updateTransform(),t.nameEl=I;var R=s.nameLayout=Ks({label:I,priority:I.z2,defaultAttr:{ignore:I.ignore},marginDefault:of(c)?q7e[A]:X7e[A]});if(s.nameLocation=c,r.add(I),I.decomposeTransform(),e.shouldNameMoveOverlap&&R){var G=n.ensureRecord(a);n.resolveAxisNameOverlap(e,n,a,R,y,G)}}}};function pM(e,t,n,a,r,o,i,l){EB(t)||oAe(e,t,r,l,a,i);var s=t.labelLayoutList;iAe(e,a,s,o),e.rotation;var u=e.optionHideOverlap;tAe(a,s,u),u&&b7e(ar(s,function(c){return c&&!c.label.ignore})),Z7e(e,n,a,s)}function eAe(e,t,n,a){var r=t$(n-e),o,i,l=a[0]>a[1],s=t==="start"&&!l||t!=="start"&&l;return Mg(r-Ss/2)?(i=s?"bottom":"top",o="center"):Mg(r-Ss*1.5)?(i=s?"top":"bottom",o="center"):(i="middle",rSs/2?o=s?"left":"right":o=s?"right":"left"),{rotation:r,textAlign:o,textVerticalAlign:i}}function tAe(e,t,n){var a=e.axis,r=e.get(["axisLabel","customValues"]);if(mEe(a))return;function o(u,c,d){var v=Ks(t[c]),f=Ks(t[d]),h=a.scale;if(!(!v||!f)){if(u==null){if(!n&&r)return;var m=lf(v.label).labelInfo.tick;if(dS(h)&&m.notNice||pi(h)&&m.offInterval){od(v.label);return}}if(u===!1||v.suggestIgnore){od(v.label);return}if(f.suggestIgnore){od(f.label);return}var g=.1;if(!n){var y=[0,0,0,0];v=oM({marginForce:y},v),f=oM({marginForce:y},f)}gS(v,f,null,{touchThreshold:g})&&od(u?f.label:v.label)}}var i=e.get(["axisLabel","showMinLabel"]),l=e.get(["axisLabel","showMaxLabel"]),s=t.length;o(i,0,1),o(l,s-1,s-2)}function nAe(e,t,n){e.showMinorTicks||we(t,function(a){if(a&&a.label.ignore)for(var r=0;r=0&&_(k,w,S.getStore())})}var f=0;if(v(function(_,w,S){a.set(w.uid,1),(!r||!r.hasKey(w.uid))&&(i=!0),f+=S.count()}),(!r||r.keys().length!==a.keys().length)&&(i=!0),!i&&o!=null){t.liPosMinGap=o;return}_S(ku,f);var h=0;v(function(_,w,S){for(var k=0,T=S.count();k0&&b0?YN:wEe,n.serUids=a}var ku=_S({ctor:k7e},50);function pAe(e){return function(t,n){var a=xf(t,{fromStat:{key:e}});if(zo(a.w2))return[-a.w2/2,a.w2/2]}}function x2(e,t){return e+y3+t}function mAe(e){return vAe(),{liPosMinGap:!pi(e.scale)}}var Id="bar";function gAe(e,t,n,a){AEe(e,{key:t,seriesType:n,coordSysType:a,getMetrics:mAe})}function yAe(e){var t=e.scale.rawExtentInfo.makeRenderInfo().startValue;return t}var AB={left:0,right:0,top:0,bottom:0},n1=["25%","25%"],xl="cartesian2d",bAe=function(e){At(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.mergeDefaultAndTheme=function(n,a){var r=kp(n.outerBounds);e.prototype.mergeDefaultAndTheme.apply(this,arguments),r&&n.outerBounds&&Hs(n.outerBounds,r)},t.prototype.mergeOption=function(n,a){e.prototype.mergeOption.apply(this,arguments),this.option.outerBounds&&n.outerBounds&&Hs(this.option.outerBounds,n.outerBounds)},t.type="grid",t.dependencies=["xAxis","yAxis"],t.layoutMode="box",t.defaultOption={show:!1,z:0,left:"15%",top:65,right:"10%",bottom:80,containLabel:!1,outerBoundsMode:"auto",outerBounds:AB,outerBoundsContain:"all",outerBoundsClampWidth:n1[0],outerBoundsClampHeight:n1[1],backgroundColor:tn.color.transparent,borderWidth:1,borderColor:tn.color.neutral30},t}(Kn),_Ae=d$(),wAe="__ec_stack_";function IB(e){return e.get("stack")||wAe+e.seriesIndex}function SAe(e,t){var n=CAe(e,t);return n.columnMap=xAe(n),n}function CAe(e,t){var n=x2(t,xl),a=[],r=xf(e,{fromStat:{key:n},min:1});return ZN(e,n,function(o){a.push({barWidth:sa(o.get("barWidth"),r.w),barMaxWidth:sa(o.get("barMaxWidth"),r.w),barMinWidth:sa(o.get("barMinWidth")||(LB(o)?.5:1),r.w),barGap:o.get("barGap"),barCategoryGap:o.get("barCategoryGap"),defaultBarGap:o.get("defaultBarGap"),stackId:IB(o)})}),{bandWidthResult:r,seriesInfo:a}}function xAe(e){var t=e.bandWidthResult.w,n=t,a=0,r,o,i=[],l={};we(e.seriesInfo,function(m,g){g||(o=m.defaultBarGap||0);var y=m.stackId;Sr(l,y)||a++;var b=l[y];b||(b=l[y]={width:0,maxWidth:0},i.push(y));var _=m.barWidth;_&&!b.width&&(b.width=_,_=sr(n,_),n-=_);var w=m.barMaxWidth;w&&(b.maxWidth=w);var S=m.barMinWidth;S&&(b.minWidth=S);var k=m.barGap;k!=null&&(o=k);var T=m.barCategoryGap;T!=null&&(r=T)}),r==null&&(r=An(35-i.length*4,15)+"%");var s=sa(r,t),u=sa(o,1),c=(n-s)/(a+(a-1)*u);c=An(c,0),we(i,function(m){var g=l[m],y=g.maxWidth,b=g.minWidth;if(g.width){var _=g.width;y&&(_=sr(_,y)),b&&(_=An(_,b)),g.width=_,n-=_+u*_,a--}else{var _=c;y&&y<_&&(_=sr(y,n)),b&&b>_&&(_=b),_!==c&&(g.width=_,n-=_+u*_,a--)}}),c=(n-s)/(a+(a-1)*u),c=An(c,0);var d=0,v;we(i,function(m){var g=l[m];g.width||(g.width=c),v=g,d+=g.width*(1+u)}),v&&(d-=v.width*u);var f={},h=-d/2;return we(i,function(m){var g=l[m];f[m]=f[m]||{bandWidth:t,offset:h,width:g.width},h+=g.width*(1+u)}),f}function kAe(e){return{seriesType:e,overallReset:function(t){var n=x2(e,xl);TEe(t,n,function(a){var r=SAe(a,e);ZN(a,n,function(o){var i=r.columnMap[IB(o)];o.getData().setLayout({bandWidth:i.bandWidth,offset:i.offset,size:i.width})})})}}}function TAe(e){return{seriesType:e,plan:eS(),reset:function(t){if(uAe(t)){var n=t.getData(),a=t.coordinateSystem,r=a.getBaseAxis(),o=a.getOtherAxis(r),i=n.getDimensionIndex(n.mapDimension(o.dim)),l=n.getDimensionIndex(n.mapDimension(r.dim)),s=t.get("showBackground",!0),u=n.mapDimension(o.dim),c=n.getCalculationInfo("stackResultDimension"),d=af(n,u)&&!!n.getCalculationInfo("stackedOnSeries"),v=o.isHorizontal(),f=o.toGlobalCoord(o.dataToCoord(yAe(o))),h=LB(t),m=t.get("barMinHeight")||0,g=c&&n.getDimensionIndex(c),y=n.getLayout("size"),b=n.getLayout("offset");return{progress:function(_,w){for(var S=_.count,k=h&&gl(S*3),T=h&&s&&gl(S*3),M=h&&gl(S),E=a.master.getRect(),A=v?E.width:E.height,I,P=w.getStore(),R=0;(I=_.next())!=null;){var G=P.get(d?g:i,I),K=P.get(l,I),H=f,z=void 0;d&&(z=+G-P.get(i,I));var B=void 0,W=void 0,N=void 0,V=void 0;if(v){var Y=a.dataToPoint([G,K]);d&&(H=a.dataToPoint([z,K])[0]),B=H,W=Y[1]+b,N=Y[0]-H,V=y,Ca(N)g){_=(k+b)/2;break}S===1&&(w=T-h[0].tickValue)}_==null&&(b?b&&(_=h[h.length-1].coord):_=h[0].coord),l[f]=v.toGlobalCoord(_)}});else{var s=this.getData(),u=s.getLayout("offset"),c=s.getLayout("size"),d=o.getBaseAxis().isHorizontal()?0:1;l[d]+=u+c/2}return l}return[NaN,NaN]},t.prototype.__requireStartValue=function(n){return this.getBaseAxis()!==n},t.type="series.__base_bar__",t.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod",defaultBarGap:"10%"},t}(ai);ai.registerClass(x3);var AAe=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n}return t.prototype.getInitialData=function(){return uS(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},t.prototype.getProgressive=function(){return this.get("large")?this.get("progressive"):!1},t.prototype.__preparePipelineContext=function(n,a){var r=f$(this,n,a);return r.progressiveRender&&(r.large=!0),r},t.prototype.brushSelector=function(n,a,r){return r.rect(a.getItemLayout(n))},t.type="series."+Id,t.dependencies=["grid","polar"],t.defaultOption=iR(x3.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:tn.color.primary,borderWidth:2}},realtimeSort:!1}),t}(x3),IAe=function(){function e(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=Math.PI*2,this.clockwise=!0}return e}(),gM=function(e){At(t,e);function t(n){var a=e.call(this,n)||this;return a.type="sausage",a}return t.prototype.getDefaultShape=function(){return new IAe},t.prototype.buildPath=function(n,a){var r=a.cx,o=a.cy,i=Math.max(a.r0||0,0),l=Math.max(a.r,0),s=(l-i)*.5,u=i+s,c=a.startAngle,d=a.endAngle,v=a.clockwise,f=Math.PI*2,h=v?d-cMath.PI/2&&cl)return!0;l=d}return!1},t.prototype._isOrderDifferentInView=function(n,a){for(var r=a.scale,o=r.getExtent(),i=Math.max(0,o[0]),l=Math.min(o[1],r.getOrdinalMeta().categories.length-1);i<=l;++i)if(n.ordinalNumbers[i]!==r.getRawOrdinalNumber(i))return!0},t.prototype._updateSortWithinSameData=function(n,a,r,o){if(this._isOrderChangedWithinSameData(n,a,r)){var i=this._dataSort(n,r,a);this._isOrderDifferentInView(i,r)&&(this._removeOnRenderedListener(o),o.dispatchAction({type:"changeAxisOrder",componentType:r.dim+"Axis",axisId:r.index,sortInfo:i}))}},t.prototype._dispatchInitSort=function(n,a,r){var o=a.baseAxis,i=this._dataSort(n,o,function(l){return n.get(n.mapDimension(a.otherAxis.dim),l)});r.dispatchAction({type:"changeAxisOrder",componentType:o.dim+"Axis",isInitSort:!0,axisId:o.index,sortInfo:i})},t.prototype.remove=function(n,a){this._clear(this._model),this._removeOnRenderedListener(a)},t.prototype.dispose=function(n,a){this._removeOnRenderedListener(a)},t.prototype._removeOnRenderedListener=function(n){this._onRendered&&(n.getZr().off("rendered",this._onRendered),this._onRendered=null)},t.prototype._clear=function(n){var a=this.group,r=this._data;n&&n.isAnimationEnabled()&&r&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],r.eachItemGraphicEl(function(o){Fv(o,n,In(o).dataIndex)})):a.removeAll(),this._data=null,this._isFirstFrame=!0},t.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},t.type=Id,t}(Oo),yM={cartesian2d:function(e,t){var n=t.width<0?-1:1,a=t.height<0?-1:1;n<0&&(t.x+=t.width,t.width=-t.width),a<0&&(t.y+=t.height,t.height=-t.height);var r=e.x+e.width,o=e.y+e.height,i=Pb(t.x,e.x),l=Db(t.x+t.width,r),s=Pb(t.y,e.y),u=Db(t.y+t.height,o),c=lr?l:i,t.y=d&&s>o?u:s,t.width=c?0:l-i,t.height=d?0:u-s,n<0&&(t.x+=t.width,t.width=-t.width),a<0&&(t.y+=t.height,t.height=-t.height),c||d},polar:function(e,t){var n=t.r0<=t.r?1:-1;if(n<0){var a=t.r;t.r=t.r0,t.r0=a}var r=Db(t.r,e.r),o=Pb(t.r0,e.r0);t.r=r,t.r0=o;var i=r-o<0;if(n<0){var a=t.r;t.r=t.r0,t.r0=a}return i}},bM={cartesian2d:function(e,t,n,a,r,o,i,l,s){var u=new ga({shape:vt({},a),z2:1});if(u.__dataIndex=n,u.name="item",o){var c=u.shape,d=r?"height":"width";c[d]=0}return u},polar:function(e,t,n,a,r,o,i,l,s){var u=!r&&s?gM:qs,c=new u({shape:a,z2:1});c.name="item";var d=PB(r);if(c.calculateTextPosition=LAe(d,{isRoundCap:u===gM}),o){var v=c.shape,f=r?"r":"endAngle",h={};v[f]=r?a.r0:a.startAngle,h[f]=a[f],(l?Ar:Do)(c,{shape:h},o)}return c}};function OAe(e,t){var n=e.get("realtimeSort",!0),a=t.getBaseAxis();if(n&&a.type==="category"&&t.type==="cartesian2d")return{baseAxis:a,otherAxis:t.getOtherAxis(a)}}function _M(e,t,n,a,r,o,i,l){var s,u;o?(u={x:a.x,width:a.width},s={y:a.y,height:a.height}):(u={y:a.y,height:a.height},s={x:a.x,width:a.width}),l||(i?Ar:Do)(n,{shape:s},t,r,null);var c=t?e.baseAxis.model:null;(i?Ar:Do)(n,{shape:u},c,r)}function wM(e,t){for(var n=0;n0?1:-1,i=a.height>0?1:-1;return{x:a.x+o*r/2,y:a.y+i*r/2,width:a.width-o*r,height:a.height-i*r}},polar:function(e,t,n){var a=e.getItemLayout(t);return{cx:a.cx,cy:a.cy,r0:a.r0,r:a.r,startAngle:a.startAngle,endAngle:a.endAngle,clockwise:a.clockwise}}};function NAe(e){return e.startAngle!=null&&e.endAngle!=null&&e.startAngle===e.endAngle}function PB(e){return function(t){var n=t?"Arc":"Angle";return function(a){switch(a){case"start":case"insideStart":case"end":case"insideEnd":return a+n;default:return a}}}(e)}function CM(e,t,n,a,r,o,i,l){var s=t.getItemVisual(n,"style");if(l){if(!o.get("roundCap")){var c=e.shape,d=bv(a.getModel("itemStyle"),c,!0);vt(c,d),e.setShape(c)}}else{var u=a.get(["itemStyle","borderRadius"])||0;e.setShape("r",u)}e.useStyle(s);var v=a.getShallow("cursor");v&&e.attr("cursor",v);var f=l?i?r.r>=r.r0?"endArc":"startArc":r.endAngle>=r.startAngle?"endAngle":"startAngle":i?HAe(r,o.coordinateSystem):WAe(r,o.coordinateSystem),h=xp(a);Cp(e,h,{labelFetcher:o,labelDataIndex:n,defaultText:yS(o.getData(),n),inheritColor:s.fill,defaultOpacity:s.opacity,defaultOutsidePosition:f});var m=e.getTextContent();if(l&&m){var g=a.get(["label","position"]);e.textConfig.inside=g==="middle"?!0:null,PAe(e,g==="outside"?f:g,PB(i),a.get(["label","rotate"]))}Yxe(m,h,o.getRawValue(n),function(b){return vB(t,b)});var y=a.getModel(["emphasis"]);Ah(e,y.get("focus"),y.get("blurScope"),y.get("disabled")),$g(e,a),NAe(r)&&(e.style.fill="none",e.style.stroke="none",we(e.states,function(b){b.style&&(b.style.fill=b.style.stroke="none")}))}function BAe(e,t){var n=e.get(["itemStyle","borderColor"]);if(!n||n==="none")return 0;var a=e.get(["itemStyle","borderWidth"])||0,r=isNaN(t.width)?Number.MAX_VALUE:Math.abs(t.width),o=isNaN(t.height)?Number.MAX_VALUE:Math.abs(t.height);return Math.min(a,r,o)}var VAe=function(){function e(){}return e}(),xM=function(e){At(t,e);function t(n){var a=e.call(this,n)||this;return a.type="largeBar",a}return t.prototype.getDefaultShape=function(){return new VAe},t.prototype.buildPath=function(n,a){for(var r=a.points,o=this.baseDimIdx,i=1-this.baseDimIdx,l=[],s=[],u=this.barWidth,c=0;c=0?n:null},30,!1);function zAe(e,t,n){for(var a=e.baseDimIdx,r=1-a,o=e.shape.points,i=e.largeDataIndices,l=[],s=[],u=e.barWidth,c=0,d=o.length/3;c=l[0]&&t<=l[0]+s[0]&&n>=l[1]&&n<=l[1]+s[1])return i[c]}return-1}function DB(e,t,n){if(bB(n,"cartesian2d")){var a=t,r=n.getArea();return{x:e?a.x:r.x,y:e?r.y:a.y,width:e?a.width:r.width,height:e?r.height:a.height}}else{var r=n.getArea(),o=t;return{cx:r.cx,cy:r.cy,r0:e?r.r0:o.r0,r:e?r.r:o.r,startAngle:e?o.startAngle:0,endAngle:e?o.endAngle:Math.PI*2}}}function FAe(e,t,n){var a=e.type==="polar"?qs:ga;return new a({shape:DB(t,n,e),silent:!0,z2:0})}function HAe(e,t){if(e.height===0){var n=t.getOtherAxis(t.getBaseAxis());return n.inverse?"bottom":"top"}return e.height>0?"bottom":"top"}function WAe(e,t){if(e.width===0){var n=t.getOtherAxis(t.getBaseAxis());return n.inverse?"left":"right"}return e.width>=0?"right":"left"}function KAe(e){e.registerChartView(DAe),e.registerSeriesModel(AAe),e.registerLayout(e.PRIORITY.VISUAL.LAYOUT,kAe(Id)),e.registerLayout(e.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,TAe(Id)),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,wB(Id)),e.registerAction({type:"changeAxisOrder",event:"changeAxisOrder",update:"update"},function(t,n){var a=t.componentType||"series";n.eachComponent({mainType:a,query:t},function(r){t.sortInfo&&r.axis.setCategorySortInfo(t.sortInfo)})}),EAe(e)}function UAe(e){return{seriesType:e,reset:function(t,n){var a=n.findComponents({mainType:"legend"});if(!(!a||!a.length)){var r=t.getData();r.filterSelf(function(o){for(var i=r.getName(o),l=0;l=0},e.prototype.indexOfName=function(t){var n=this._getDataWithEncodedVisual();return n.indexOfName(t)},e.prototype.getItemVisual=function(t,n){var a=this._getDataWithEncodedVisual();return a.getItemVisual(t,n)},e}(),As="pie",jAe=Hn(),OB=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n}return t.prototype.init=function(n){e.prototype.init.apply(this,arguments),this.legendVisualProvider=new YAe(qn(this.getData,this),qn(this.getRawData,this)),this._defaultLabelLine(n)},t.prototype.mergeOption=function(){e.prototype.mergeOption.apply(this,arguments)},t.prototype.getInitialData=function(){return GAe(this,{coordDimensions:["value"],encodeDefaulter:Sa($ke,this)})},t.prototype.getDataParams=function(n){var a=this.getData(),r=jAe(a),o=r.seats;if(!o){var i=[];a.each(a.mapDimension("value"),function(s){i.push(s)}),o=r.seats=GSe(i,a.hostModel.get("percentPrecision"))}var l=e.prototype.getDataParams.call(this,n);return l.percent=o[n]||0,l.$vars.push("percent"),l},t.prototype._defaultLabelLine=function(n){N_(n,"labelLine",["show"]);var a=n.labelLine,r=n.emphasis.labelLine;a.show=a.show&&n.label.show,r.show=r.show&&n.emphasis.label.show},t.type="series."+As,t.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"50%"],clockwise:!0,startAngle:90,endAngle:"auto",padAngle:0,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,coordinateSystemUsage:"box",left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:30,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},t}(ai);wke({fullType:OB.type,getCoord2:function(e){return e.getShallow("center")}});var qAe=Math.PI/180;function MM(e,t,n,a,r,o,i,l,s,u){if(e.length<2)return;function c(m){for(var g=m.rB,y=g*g,b=0;bn?y:g,S=Math.abs(_.label.y-n);if(S>=w.maxY){var k=_.label.x-t-_.len2*r,T=a+_.len,M=Math.abs(k)e.unconstrainedWidth?null:v:null;a.setStyle("width",f)}RB(o,a)}}}function RB(e,t){EM.rect=e,fB(EM,t,ZAe)}var ZAe={minMarginForce:[null,0,null,0],marginDefault:[1,0,1,0]},EM={};function Ob(e){return e.position==="center"}function JAe(e){var t=e.getData(),n=[],a,r,o=!1,i=(e.get("minShowLabelAngle")||0)*qAe,l=t.getLayout("viewRect"),s=t.getLayout("r"),u=l.width,c=l.x,d=l.y,v=l.height;function f(k){k.ignore=!0}function h(k){if(!k.ignore)return!0;for(var T in k.states)if(k.states[T].ignore===!1)return!0;return!1}t.each(function(k){var T=t.getItemGraphicEl(k),M=T.shape,E=T.getTextContent(),A=T.getTextGuideLine(),I=t.getItemModel(k),P=I.getModel("label"),R=P.get("position")||I.get(["emphasis","label","position"]),G=P.get("distanceToLabelLine"),K=P.get("alignTo"),H=sa(P.get("edgeDistance"),u),z=P.get("bleedMargin");z==null&&(z=Math.min(u,v)>200?10:2);var B=I.getModel("labelLine"),W=B.get("length");W=sa(W,u);var N=B.get("length2");if(N=sa(N,u),Math.abs(M.endAngle-M.startAngle)0?"right":"left":Y>0?"left":"right"}var fe=Math.PI,ge=0,Se=P.get("rotate");if(Gn(Se))ge=Se*(fe/180);else if(R==="center")ge=0;else if(Se==="radial"||Se===!0){var De=Y<0?-V+fe:-V;ge=De}else if(Se==="tangential"||Se==="tangential-noflip"&&R!=="outside"&&R!=="outer"){var Te=Math.atan2(Y,X);Te<0&&(Te=fe*2+Te);var Re=X>0;Re&&Se!=="tangential-noflip"&&(Te=fe+Te),ge=Te-fe}if(o=!!ge,E.x=ee,E.y=re,E.rotation=ge,E.setStyle({verticalAlign:"middle"}),ve){E.setStyle({align:le});var Le=E.states.select;Le&&(Le.x+=E.x,Le.y+=E.y)}else{var Ce=new ln(0,0,0,0);RB(Ce,E),n.push({label:E,labelLine:A,position:R,len:W,len2:N,minTurnAngle:B.get("minTurnAngle"),maxSurfaceAngle:B.get("maxSurfaceAngle"),surfaceNormal:new fn(Y,X),linePoints:ie,textAlign:le,labelDistance:G,labelAlignTo:K,edgeDistance:H,bleedMargin:z,rect:Ce,unconstrainedWidth:Ce.width,labelStyleWidth:E.style.width})}T.setTextConfig({inside:ve})}}),!o&&e.get("avoidLabelOverlap")&&XAe(n,a,r,s,u,v,c,d);for(var m=0;mB?(N=G+T*B/2,V=N):(N=G+E,V=W-E),a.setItemLayout(z,{angle:B,startAngle:N,endAngle:V,clockwise:_,cx:i,cy:l,r0:u,r:w?$_(H,k,[u,s]):s}),G=W}),P0){for(var c=i.getItemLayout(0),d=1;isNaN(c&&c.startAngle)&&d=o.r0}},t.type=As,t}(Oo);function aIe(e){return{seriesType:e,reset:function(t,n){var a=t.getData();a.filterSelf(function(r){var o=a.mapDimension("value"),i=a.get(o,r);return!(Gn(i)&&!isNaN(i)&&i<0)})}}}function rIe(e){e.registerChartView(nIe),e.registerSeriesModel(OB),gTe(As,e.registerAction),e.registerLayout(QAe),e.registerProcessor(UAe(As)),e.registerProcessor(aIe(As))}var k3=function(e){At(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",ko).models[0]},t.type="cartesian2dAxis",t}(Kn);al(k3,bEe);var BB={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:"auto",onZeroAxisIndex:null,lineStyle:{color:tn.color.axisLine,width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15],breakLine:!0},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12,color:tn.color.axisLabel,textMargin:[0,3]},splitLine:{show:!0,showMinLine:!0,showMaxLine:!0,lineStyle:{color:tn.color.axisSplitLine,width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:[tn.color.backgroundTint,tn.color.backgroundTransparent]}},breakArea:{show:!0,itemStyle:{color:tn.color.neutral00,borderColor:tn.color.border,borderWidth:1,borderType:[3,3],opacity:.6},zigzagAmplitude:4,zigzagMinSpan:4,zigzagMaxSpan:20,zigzagZ:100,expandOnClick:!0},breakLabelLayout:{moveOverlap:"auto"}},oIe=Bn({boundaryGap:!0,deduplication:null,jitter:0,jitterOverlap:!0,jitterMargin:2,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto",show:"auto"},axisLabel:{interval:"auto"}},BB),SS=Bn({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:tn.color.axisMinorSplitLine,width:1}}},BB),iIe=Bn({splitNumber:6,axisLabel:{rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},SS),lIe=zn({logBase:10},SS);const sIe={category:oIe,value:SS,time:iIe,log:lIe};function IM(e,t,n,a){we(WN,function(r,o){var i=Bn(Bn({},sIe[o],!0),a,!0),l=function(s){At(u,s);function u(){var c=s!==null&&s.apply(this,arguments)||this;return c.type=t+"Axis."+o,c}return u.prototype.mergeDefaultAndTheme=function(c,d){var v=Ph(this),f=v?kp(c):{},h=d.getTheme();Bn(c,h.get(o+"Axis")),Bn(c,this.getDefaultOption()),c.type=LM(c),v&&Hs(c,f,v)},u.prototype.optionUpdated=function(){var c=this.option;c.type==="category"&&(this.__ordinalMeta=m3.createByAxisModel(this))},u.prototype.getCategories=function(c){var d=this.option;if(d.type==="category")return c?d.data:this.__ordinalMeta.categories},u.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},u.prototype.updateAxisBreaks=function(c){return{breaks:[]}},u.type=t+"Axis."+o,u.defaultOption=i,u}(n);e.registerComponentModel(l)}),e.registerSubTypeDefaulter(t+"Axis",LM)}function LM(e){return e.type||(e.data?"category":"value")}var uIe=function(){function e(t){this.type="cartesian",this._dimList=[],this._axes={},this.name=t||""}return e.prototype.getAxis=function(t){return this._axes[t]},e.prototype.getAxes=function(){return Xt(this._dimList,function(t){return this._axes[t]},this)},e.prototype.getAxesByScale=function(t){return t=t.toLowerCase(),ar(this.getAxes(),function(n){return n.scale.type===t})},e.prototype.addAxis=function(t){var n=t.dim;this._axes[n]=t,this._dimList.push(n)},e}(),Em=["x","y"];function PM(e){return(e.type==="interval"||e.type==="time")&&!Vg(e)}var cIe=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=xl,n.dimensions=Em,n}return t.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var n=this.getAxis("x").scale,a=this.getAxis("y").scale;if(!(!PM(n)||!PM(a))){var r=Yg(n,null),o=Yg(a,null),i=this.dataToPoint([r[0],o[0]]),l=this.dataToPoint([r[1],o[1]]),s=r[1]-r[0],u=o[1]-o[0];if(!(!s||!u)){var c=(l[0]-i[0])/s,d=(l[1]-i[1])/u,v=i[0]-r[0]*c,f=i[1]-o[0]*d,h=this._transform=[c,0,0,d,v,f];this._invTransform=pp([],h)}}},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},t.prototype.containPoint=function(n){var a=this.getAxis("x"),r=this.getAxis("y");return a.contain(a.toLocalCoord(n[0]))&&r.contain(r.toLocalCoord(n[1]))},t.prototype.containData=function(n){return this.getAxis("x").containData(n[0])&&this.getAxis("y").containData(n[1])},t.prototype.containZone=function(n,a){var r=this.dataToPoint(n),o=this.dataToPoint(a),i=this.getArea(),l=new ln(r[0],r[1],o[0]-r[0],o[1]-r[1]);return i.intersect(l)},t.prototype.dataToPoint=function(n,a,r){r=r||[];var o=n[0],i=n[1];if(this._transform&&o!=null&&isFinite(o)&&i!=null&&isFinite(i))return Po(r,n,this._transform);var l=this.getAxis("x"),s=this.getAxis("y");return r[0]=l.toGlobalCoord(l.dataToCoord(o,a)),r[1]=s.toGlobalCoord(s.dataToCoord(i,a)),r},t.prototype.clampData=function(n,a){var r=this.getAxis("x").scale,o=this.getAxis("y").scale,i=r.getExtent(),l=o.getExtent(),s=r.parse(n[0]),u=o.parse(n[1]);return a=a||[],a[0]=Math.min(Math.max(Math.min(i[0],i[1]),s),Math.max(i[0],i[1])),a[1]=Math.min(Math.max(Math.min(l[0],l[1]),u),Math.max(l[0],l[1])),a},t.prototype.pointToData=function(n,a,r){if(r=r||[],this._invTransform)return Po(r,n,this._invTransform);var o=this.getAxis("x"),i=this.getAxis("y");return r[0]=o.coordToData(o.toLocalCoord(n[0]),a),r[1]=i.coordToData(i.toLocalCoord(n[1]),a),r},t.prototype.getOtherAxis=function(n){return this.getAxis(n.dim==="x"?"y":"x")},t.prototype.getArea=function(n){n=n||0;var a=this.getAxis("x").getGlobalExtent(),r=this.getAxis("y").getGlobalExtent(),o=Math.min(a[0],a[1])-n,i=Math.min(r[0],r[1])-n,l=Math.max(a[0],a[1])-o+n,s=Math.max(r[0],r[1])-i+n;return new ln(o,i,l,s)},t}(uIe);function dIe(e,t){var n=e.scale,a=e.model,r=aB(n,a,a.ecModel,e),o=rf(n),i=rf(t)?t.intervalStub:t,l=o?n.intervalStub:n,s=n.base,u=i.getTicks(),c=i.getTicks({expandToNicedExtent:!0}),d=u.length-1,v,f,h;if(d===1)v=f=0,h=1;else if(d===2){var m=Ca(u[0].value-u[1].value),g=Ca(u[1].value-u[2].value);v=f=0,m===g?h=2:(h=1,m=T[1])return!0})):w[1]?(E=T[1],G(function(){if(B(),R=En(P-A*h,I),K(),M<=T[0])return!0})):G(function(){R=En(yp(T[0]/A)*A,I),P=En(gc(T[1]/A)*A,I);var Y=Dl((P-R)/A);if(Y<=h){var X=h-Y,ee=void 0,re=r.incl0||o;if(re&&T[0]===0)ee=[0,X];else if(re&&T[1]===0)ee=[X,0];else{var ie=gc(X/2);ee=X%2===0?[ie,ie]:M+E=T[1])return!0}})}UN(n,w,k,[M,E],S,{interval:A,intervalCount:h,intervalPrecision:I,niceExtent:[R,P]})}var DM=[[3,1],[0,2]],fIe=function(){function e(t,n,a){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=Em,this._initCartesian(t,n,a),this.model=t}return e.prototype.getRect=function(){return this._rect},e.prototype.update=function(t,n){var a=this._axesMap;we(this._axesList,function(i){$Ee(i,LEe);var l=i.scale;pi(l)&&l.setSortInfo(i.model.get("categorySortInfo"))});function r(i){for(var l=la(i),s=[],u=l.length-1;u>=0;u--){var c=i[+l[u]];c.__alignTo?s.push(c):eM(c)}we(s,function(d){hIe(d,d.__alignTo)?eM(d):dIe(d,d.__alignTo.scale)})}r(a.x),r(a.y);var o={};we(a.x,function(i){OM(a,"y",i,o)}),we(a.y,function(i){OM(a,"x",i,o)}),this.resize(this.model,n)},e.prototype.resize=function(t,n,a){var r=Y8(t,n),o=this._rect=wc(t.getBoxLayoutParams(),r.refContainer),i=this._axesMap,l=this._coordsList,s=t.get("containLabel");if(VB(i,o),!a){var u=mIe(o,l,i,s,n),c=void 0;if(s)c=BM(o.clone(),"axisLabel",null,o,i,u,r);else{var d=gIe(t,o,r),v=d.outerBoundsRect,f=d.parsedOuterBoundsContain,h=d.outerBoundsClamp;v&&(c=BM(v,f,h,o,i,u,r))}zB(o,i,ri.determine,null,c,r),we(this._coordsList,function(m){m.calcAffineTransform()})}},e.prototype.getAxis=function(t,n){var a=this._axesMap[t];if(a!=null)return a[n||0]},e.prototype.getAxes=function(){return this._axesList.slice()},e.prototype.getCartesian=function(t,n){if(t!=null&&n!=null){var a="x"+t+"y"+n;return this._coordsMap[a]}Yt(t)&&(n=t.yAxisIndex,t=t.xAxisIndex);for(var r=0,o=this._coordsList;r=0;r--){var o=e[+t[r]];NN(o.scale)&&yEe(o.model,o.type)==null&&(o.model.get("alignTicks")&&o.model.get("interval")==null?a.push(o):n=o)}n||(n=a.pop()),n&&we(a,function(i){i.__alignTo=n})}function hIe(e,t){return Vg(e.scale)||Vg(t.scale)||t.scale.getTicks().length<2}function pIe(e,t){var n=e.getExtent(),a=n[0]+n[1];e.toGlobalCoord=e.dim==="x"?function(r){return r+t}:function(r){return a-r+t},e.toLocalCoord=e.dim==="x"?function(r){return r-t}:function(r){return a-r+t}}function VB(e,t){we(e.x,function(n){return NM(n,t.x,t.width)}),we(e.y,function(n){return NM(n,t.y,t.height)})}function NM(e,t,n){var a=[0,n],r=e.inverse?1:0;e.setExtent(a[r],a[1-r]),pIe(e,t)}function BM(e,t,n,a,r,o,i){zB(a,r,ri.estimate,t,!1,i);var l=[0,0,0,0];u(0),u(1),c(a,0,NaN),c(a,1,NaN);var s=f8e(l,function(v){return v>0})==null;return Ng(a,l,!0,!0,n),VB(r,a),s;function u(v){we(r[vs[v]],function(f){if(Bh(f.model)){var h=o.ensureRecord(f.model),m=h.labelInfoList;if(m)for(var g=0;g0&&!kh(f)&&f>1e-4&&(v/=f),v}}function mIe(e,t,n,a,r){var o=new CB(yIe);return we(n,function(i){return we(i,function(l){if(Bh(l.model)){var s=!a;l.axisBuilder=dAe(e,t,l.model,r,o,s)}})}),o}function zB(e,t,n,a,r,o){var i=n===ri.determine;we(t,function(u){return we(u,function(c){Bh(c.model)&&(fAe(c.axisBuilder,e,c.model),c.axisBuilder.build(i?{axisTickLabelDetermine:!0}:{axisTickLabelEstimate:!0},{noPxChange:r}))})});var l={x:0,y:0};s(0),s(1);function s(u){l[vs[1-u]]=e[Qd[u]]<=o.refContainer[Qd[u]]*.5?0:1-u===1?2:1}we(t,function(u,c){return we(u,function(d){Bh(d.model)&&((a==="all"||i)&&d.axisBuilder.build({axisName:!0},{nameMarginLevel:l[c]}),i&&d.axisBuilder.build({axisLine:!0}))})})}function gIe(e,t,n){var a,r=e.get("outerBoundsMode",!0);r==="same"?a=t.clone():(r==null||r==="auto")&&(a=wc(e.get("outerBounds",!0)||AB,n.refContainer));var o=e.get("outerBoundsContain",!0),i;o==null||o==="auto"||Pn(["all","axisLabel"],o)<0?i="all":i=o;var l=[R_(jt(e.get("outerBoundsClampWidth",!0),n1[0]),t.width),R_(jt(e.get("outerBoundsClampHeight",!0),n1[1]),t.height)];return{outerBoundsRect:a,parsedOuterBoundsContain:i,outerBoundsClamp:l}}var yIe=function(e,t,n,a,r,o){var i=n.axis.dim==="x"?"y":"x";xB(e,t,n,a,r,o),of(e.nameLocation)||we(t.recordMap[i],function(l){l&&l.labelInfoList&&l.dirVec&&TB(l.labelInfoList,l.dirVec,a,r)})};function bIe(e,t){var n={axesInfo:{},seriesInvolved:!1,coordSysAxesInfo:{},coordSysMap:{}};return _Ie(n,e,t),n.seriesInvolved&&SIe(n,e),n}function _Ie(e,t,n){var a=t.getComponent("tooltip"),r=t.getComponent("axisPointer"),o=r.get("link",!0)||[],i=[];we(n.getCoordinateSystems(),function(l){if(!l.axisPointerEnabled)return;var s=Vh(l.model),u=e.coordSysAxesInfo[s]={};e.coordSysMap[s]=l;var c=l.model,d=c.getModel("tooltip",a);if(we(l.getAxes(),Sa(m,!1,null)),l.getTooltipAxes&&a&&d.get("show")){var v=d.get("trigger")==="axis",f=d.get(["axisPointer","type"])==="cross",h=l.getTooltipAxes(d.get(["axisPointer","axis"]));(v||f)&&we(h.baseAxes,Sa(m,f?"cross":!0,v)),f&&we(h.otherAxes,Sa(m,"cross",!1))}function m(g,y,b){var _=b.model.getModel("axisPointer",r),w=_.get("show");if(!(!w||w==="auto"&&!g&&!T3(_))){y==null&&(y=_.get("triggerTooltip")),_=g?wIe(b,d,r,t,g,y):_;var S=_.get("snap"),k=_.get("triggerEmphasis"),T=Vh(b.model),M=y||S||b.type==="category",E=e.axesInfo[T]={key:T,axis:b,coordSys:l,axisPointerModel:_,triggerTooltip:y,triggerEmphasis:k,involveSeries:M,snap:S,useHandle:T3(_),seriesModels:[],linkGroup:null};u[T]=E,e.seriesInvolved=e.seriesInvolved||M;var A=CIe(o,b);if(A!=null){var I=i[A]||(i[A]={axesInfo:{}});I.axesInfo[T]=E,I.mapper=o[A].mapper,E.linkGroup=I}}}})}function wIe(e,t,n,a,r,o){var i=t.getModel("axisPointer"),l=["type","snap","lineStyle","shadowStyle","label","animation","animationDurationUpdate","animationEasingUpdate","z"],s={};we(l,function(v){s[v]=yn(i.get(v))}),s.snap=e.type!=="category"&&!!o,i.get("type")==="cross"&&(s.type="line");var u=s.label||(s.label={});if(u.show==null&&(u.show=!1),r==="cross"){var c=i.get(["label","show"]);if(u.show=c??!0,!o){var d=s.lineStyle=i.get("crossStyle");d&&zn(u,d.textStyle)}}return e.model.getModel("axisPointer",new fa(s,n,a))}function SIe(e,t){t.eachSeries(function(n){var a=n.coordinateSystem,r=n.get(["tooltip","trigger"],!0),o=n.get(["tooltip","show"],!0);!a||!a.model||r==="none"||r===!1||r==="item"||o===!1||n.get(["axisPointer","show"],!0)===!1||we(e.coordSysAxesInfo[Vh(a.model)],function(i){var l=i.axis;a.getAxis(l.dim)===l&&(i.seriesModels.push(n),i.seriesDataCount==null&&(i.seriesDataCount=0),i.seriesDataCount+=n.getData().count())})})}function CIe(e,t){for(var n=t.model,a=t.dim,r=0;r=0||e===t}function xIe(e){var t=CS(e);if(t){var n=t.axisPointerModel,a=t.axis.scale,r=n.option,o=n.get("status"),i=n.get("value");i!=null&&(i=a.parse(i));var l=T3(n);o==null&&(r.status=l?"show":"hide");var s=a.getExtent();(i==null||i>s[1])&&(i=s[1]),il)return!0;if(i){var s=CS(t).seriesDataCount,u=r.getExtent();return Math.abs(u[0]-u[1])/s>l}return!1}return a===!0},e.prototype.makeElOption=function(t,n,a,r,o){},e.prototype.createPointerEl=function(t,n,a,r){var o=n.pointer;if(o){var i=Ou(t).pointerEl=new Hxe[o.type](FM(n.pointer));t.add(i)}},e.prototype.createLabelEl=function(t,n,a,r){if(n.label){var o=Ou(t).labelEl=new Er(FM(n.label));t.add(o),WM(o,r)}},e.prototype.updatePointerEl=function(t,n,a){var r=Ou(t).pointerEl;r&&n.pointer&&(r.setStyle(n.pointer.style),a(r,{shape:n.pointer.shape}))},e.prototype.updateLabelEl=function(t,n,a,r){var o=Ou(t).labelEl;o&&(o.setStyle(n.label.style),a(o,{x:n.label.x,y:n.label.y}),WM(o,r))},e.prototype._renderHandle=function(t){if(!(this._dragging||!this.updateHandleTransform)){var n=this._axisPointerModel,a=this._api.getZr(),r=this._handle,o=n.getModel("handle"),i=n.get("status");if(!o.get("show")||!i||i==="hide"){r&&a.remove(r),this._handle=null;return}var l;this._handle||(l=!0,r=this._handle=L8(o.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(u){OO(u.event)},onmousedown:Rb(this._onHandleDragMove,this,0,0),drift:Rb(this._onHandleDragMove,this),ondragend:Rb(this._onHandleDragEnd,this)}),a.add(r)),KM(r,n,!1),r.setStyle(o.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=o.get("size");Lt(s)||(s=[s,s]),r.scaleX=s[0]/2,r.scaleY=s[1]/2,XR(this,"_doDispatchAxisPointer",o.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,l)}},e.prototype._moveHandleToValue=function(t,n){HM(this._axisPointerModel,!n&&this._moveAnimation,this._handle,Nb(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},e.prototype._onHandleDragMove=function(t,n){var a=this._handle;if(a){this._dragging=!0;var r=this.updateHandleTransform(Nb(a),[t,n],this._axisModel,this._axisPointerModel);this._payloadInfo=r,a.stopAnimation(),a.attr(Nb(r)),Ou(a).lastProp=null,this._doDispatchAxisPointer()}},e.prototype._doDispatchAxisPointer=function(){var t=this._handle;if(t){var n=this._payloadInfo,a=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:n.cursorPoint[0],y:n.cursorPoint[1],tooltipOption:n.tooltipOption,axesInfo:[{axisDim:a.axis.dim,axisIndex:a.componentIndex}]})}},e.prototype._onHandleDragEnd=function(){this._dragging=!1;var t=this._handle;if(t){var n=this._axisPointerModel.get("value");this._moveHandleToValue(n),this._api.dispatchAction({type:"hideTip"})}},e.prototype.clear=function(t){this._lastValue=null,this._lastStatus=null;var n=t.getZr(),a=this._group,r=this._handle;n&&a&&(this._lastGraphicKey=null,a&&n.remove(a),r&&n.remove(r),this._group=null,this._handle=null,this._payloadInfo=null),o3(this,"_doDispatchAxisPointer")},e.prototype.doClear=function(){},e.prototype.buildLabel=function(t,n,a){return a=a||0,{x:t[a],y:t[1-a],width:n[a],height:n[1-a]}},e}();function HM(e,t,n,a){KB(Ou(n).lastProp,a)||(Ou(n).lastProp=a,t?Ar(n,a,e):(n.stopAnimation(),n.attr(a)))}function KB(e,t){if(Yt(e)&&Yt(t)){var n=!0;return we(t,function(a,r){n=n&&KB(e[r],a)}),!!n}else return e===t}function WM(e,t){e[t.get(["label","show"])?"show":"hide"]()}function Nb(e){return{x:e.x||0,y:e.y||0,rotation:e.rotation||0}}function KM(e,t,n){var a=t.get("z"),r=t.get("zlevel");e&&e.traverse(function(o){o.type!=="group"&&(a!=null&&(o.z=a),r!=null&&(o.zlevel=r),o.silent=n)})}function OIe(e){var t=e.get("type"),n=e.getModel(t+"Style"),a;return t==="line"?(a=n.getLineStyle(),a.fill=null):t==="shadow"&&(a=n.getAreaStyle(),a.stroke=null),a}function $Ie(e,t,n,a,r){var o=n.get("value"),i=UB(o,t.axis,t.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),l=n.getModel("label"),s=y2(l.get("padding")||0),u=l.getFont(),c=ZO(i,u),d=r.position,v=c.width+s[1]+s[3],f=c.height+s[0]+s[2],h=r.align;h==="right"&&(d[0]-=v),h==="center"&&(d[0]-=v/2);var m=r.verticalAlign;m==="bottom"&&(d[1]-=f),m==="middle"&&(d[1]-=f/2),RIe(d,v,f,a);var g=l.get("backgroundColor");(!g||g==="auto")&&(g=t.get(["axisLine","lineStyle","color"])),e.label={x:d[0],y:d[1],style:ef(l,{text:i,font:u,fill:l.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function RIe(e,t,n,a){var r=a.getWidth(),o=a.getHeight();e[0]=Math.min(e[0]+t,r)-t,e[1]=Math.min(e[1]+n,o)-n,e[0]=Math.max(e[0],0),e[1]=Math.max(e[1],0)}function UB(e,t,n,a,r){e=t.scale.parse(e);var o=t.scale.getLabel({value:e},{precision:r.precision}),i=r.formatter;if(i){var l={value:qg(t,{value:e}),axisDimension:t.dim,axisIndex:t.index,seriesData:[]};we(a,function(s){var u=n.getSeriesByIndex(s.seriesIndex),c=s.dataIndexInside,d=u&&u.getDataParams(c);d&&l.seriesData.push(d)}),Ut(i)?o=i.replace("{value}",o):dn(i)&&(o=i(l))}return o}function GB(e,t,n){var a=zi();return o8(a,a,n.rotation),b_(a,a,n.position),I8([e.dataToCoord(t),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],a)}function NIe(e,t,n,a,r,o){var i=Es.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get(["label","margin"]),$Ie(t,a,r,o,{position:GB(a.axis,e,n),align:i.textAlign,verticalAlign:i.textVerticalAlign})}function BIe(e,t,n){return n=n||0,{x1:e[n],y1:e[1-n],x2:t[n],y2:t[1-n]}}function VIe(e,t,n){return n=n||0,{x:e[n],y:e[1-n],width:t[n],height:t[1-n]}}function zIe(e,t,n){return xf(e,{fromStat:{sers:Xt(t,function(a){return n.getSeriesByIndex(a.seriesIndex)})},min:1}).w}function FIe(e,t,n){return[An(sr(t[0],t[1]),e-n/2),sr(e+n/2,An(t[0],t[1]))]}var HIe=function(e){At(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.makeElOption=function(n,a,r,o,i){var l=r.axis,s=l.grid,u=o.get("type"),c=l.getGlobalExtent(),d=UM(s,l).getOtherAxis(l).getGlobalExtent(),v=l.toGlobalCoord(l.dataToCoord(a,!0));if(u&&u!=="none"){var f=OIe(o),h=WIe[u](l,v,c,d,o.get("seriesDataIndices"),o.ecModel);h.style=f,n.graphicKey=h.type,n.pointer=h}var m=t1(s.getRect(),r);NIe(a,n,m,r,o,i)},t.prototype.getHandleTransform=function(n,a,r){var o=t1(a.axis.grid.getRect(),a,{labelInside:!1});o.labelMargin=r.get(["handle","margin"]);var i=GB(a.axis,n,o);return{x:i[0],y:i[1],rotation:o.rotation+(o.labelDirection<0?Math.PI:0)}},t.prototype.updateHandleTransform=function(n,a,r,o){var i=r.axis,l=i.grid,s=i.getGlobalExtent(!0),u=UM(l,i).getOtherAxis(i).getGlobalExtent(),c=i.dim==="x"?0:1,d=[n.x,n.y];d[c]+=a[c],d[c]=sr(s[1],d[c]),d[c]=An(s[0],d[c]);var v=(u[1]+u[0])/2,f=[v,v];f[c]=d[c];var h=[{verticalAlign:"middle"},{align:"center"}];return{x:d[0],y:d[1],rotation:n.rotation,cursorPoint:f,tooltipOption:h[c]}},t}(DIe);function UM(e,t){var n={};return n[t.dim+"AxisIndex"]=t.index,e.getCartesian(n)}var WIe={line:function(e,t,n,a){var r=BIe([t,a[0]],[t,a[1]],GM(e));return{type:"Line",subPixelOptimize:!0,shape:r}},shadow:function(e,t,n,a,r,o){var i=zIe(e,r,o),l=a[1]-a[0],s=FIe(t,n,i),u=s[0],c=s[1];return{type:"Rect",shape:VIe([u,a[0]],[c-u,l],GM(e))}}};function GM(e){return e.dim==="x"?0:1}var KIe=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n}return t.type="axisPointer",t.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:tn.color.border,width:1,type:"dashed"},shadowStyle:{color:tn.color.shadowTint},label:{show:!0,formatter:null,precision:"auto",margin:3,color:tn.color.neutral00,padding:[5,7,5,7],backgroundColor:tn.color.accent60,borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:tn.color.accent40,throttle:40}},t}(Kn),yl=Hn(),UIe=we;function YB(e,t,n){if(!hn.node){var a=t.getZr();yl(a).records||(yl(a).records={}),GIe(a,t);var r=yl(a).records[e]||(yl(a).records[e]={});r.handler=n}}function GIe(e,t){if(yl(e).initialized)return;yl(e).initialized=!0,n("click",Sa(Bb,"click")),n("mousemove",Sa(Bb,"mousemove")),n("mousewheel",Sa(Bb,"mousewheel")),n("globalout",jIe);function n(a,r){e.on(a,function(o){var i=qIe(t);UIe(yl(e).records,function(l){l&&r(l,o,i.dispatchAction)}),YIe(i.pendings,t)})}}function YIe(e,t){var n=e.showTip.length,a=e.hideTip.length,r;n?r=e.showTip[n-1]:a&&(r=e.hideTip[a-1]),r&&(r.dispatchAction=null,t.dispatchAction(r))}function jIe(e,t,n){e.handler("leave",null,n)}function Bb(e,t,n,a){t.handler(e,n,a)}function qIe(e){var t={showTip:[],hideTip:[]},n=function(a){var r=t[a.type];r?r.push(a):(a.dispatchAction=n,e.dispatchAction(a))};return{dispatchAction:n,pendings:t}}function E3(e,t){if(!hn.node){var n=t.getZr(),a=(yl(n).records||{})[e];a&&(yl(n).records[e]=null)}}var XIe=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n}return t.prototype.render=function(n,a,r){var o=a.getComponent("tooltip"),i=n.get("triggerOn")||o&&o.get("triggerOn")||"mousemove|click|mousewheel";YB("axisPointer",r,function(l,s,u){i!=="none"&&(l==="leave"||i.indexOf(l)>=0)&&u({type:"updateAxisPointer",currTrigger:l,x:s&&s.offsetX,y:s&&s.offsetY})})},t.prototype.remove=function(n,a){E3("axisPointer",a)},t.prototype.dispose=function(n,a){E3("axisPointer",a)},t.type="axisPointer",t}(el);function jB(e,t){var n=[],a=e.seriesIndex,r;if(a==null||!(r=t.getSeriesByIndex(a)))return{point:[]};var o=r.getData(),i=yc(o,e);if(i==null||i<0||Lt(i))return{point:[]};var l=o.getItemGraphicEl(i),s=r.coordinateSystem;if(r.getTooltipPosition)n=r.getTooltipPosition(i)||[];else if(s&&s.dataToPoint)if(e.isStacked){var u=s.getBaseAxis(),c=s.getOtherAxis(u),d=c.dim,v=u.dim,f=d==="x"||d==="radius"?1:0,h=o.mapDimension(v),m=[];m[f]=o.get(h,i),m[1-f]=o.get(o.getCalculationInfo("stackResultDimension"),i),n=s.dataToPoint(m)||[]}else n=s.dataToPoint(o.getValues(Xt(s.dimensions,function(y){return o.mapDimension(y)}),i))||[];else if(l){var g=l.getBoundingRect().clone();g.applyTransform(l.transform),n=[g.x+g.width/2,g.y+g.height/2]}return{point:n,el:l}}var YM=Hn();function ZIe(e,t,n){var a=e.currTrigger,r=[e.x,e.y],o=e,i=e.dispatchAction||qn(n.dispatchAction,n),l=t.getComponent("axisPointer").coordSysAxesInfo;if(l){Am(r)&&(r=jB({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},t).point);var s=Am(r),u=o.axesInfo,c=l.axesInfo,d=a==="leave"||Am(r),v={},f={},h={list:[],map:{}},m={showPointer:Sa(QIe,f),showTooltip:Sa(eLe,h)};we(l.coordSysMap,function(y,b){var _=s||y.containPoint(r);we(l.coordSysAxesInfo[b],function(w,S){var k=w.axis,T=rLe(u,w);if(!d&&_&&(!u||T)){var M=T&&T.value;M==null&&!s&&(M=k.pointToData(r)),M!=null&&jM(w,M,m,!1,v)}})});var g={};return we(c,function(y,b){var _=y.linkGroup;_&&!f[b]&&we(_.axesInfo,function(w,S){var k=f[S];if(w!==y&&k){var T=k.value;_.mapper&&(T=y.axis.scale.parse(_.mapper(T,qM(w),qM(y)))),g[y.key]=T}})}),we(g,function(y,b){jM(c[b],y,m,!0,v)}),tLe(f,c,v),nLe(h,r,e,i),aLe(c,i,n),v}}function jM(e,t,n,a,r){var o=e.axis;if(!(o.scale.isBlank()||!o.containData(t))){if(!e.involveSeries){n.showPointer(e,t);return}var i=JIe(t,e),l=i.payloadBatch,s=i.snapToValue;l[0]&&r.seriesIndex==null&&vt(r,l[0]),!a&&e.snap&&o.containData(s)&&s!=null&&(t=s),n.showPointer(e,t,l),n.showTooltip(e,i,s)}}function JIe(e,t){var n=t.axis,a=n.dim,r=e,o=[],i=Number.MAX_VALUE,l=-1;return we(t.seriesModels,function(s,u){var c=s.getData().mapDimensionsAll(a),d,v;if(s.getAxisTooltipData){var f=s.getAxisTooltipData(c,e,n);v=f.dataIndices,d=f.nestestValue}else{if(v=s.indicesOfNearest(a,c[0],e,n.type==="category"?.5:null),!v.length)return;d=s.getData().get(c[0],v[0])}if(zo(d)){var h=e-d,m=Math.abs(h);m<=i&&((m=0&&l<0)&&(i=m,l=h,r=d,o.length=0),we(v,function(g){o.push({seriesIndex:s.seriesIndex,dataIndexInside:g,dataIndex:s.getData().getRawIndex(g)})}))}}),{payloadBatch:o,snapToValue:r}}function QIe(e,t,n,a){e[t.key]={value:n,payloadBatch:a}}function eLe(e,t,n,a){var r=n.payloadBatch,o=t.axis,i=o.model,l=t.axisPointerModel;if(!(!t.triggerTooltip||!r.length)){var s=t.coordSys.model,u=Vh(s),c=e.map[u];c||(c=e.map[u]={coordSysId:s.id,coordSysIndex:s.componentIndex,coordSysType:s.type,coordSysMainType:s.mainType,dataByAxis:[]},e.list.push(c)),c.dataByAxis.push({axisDim:o.dim,axisIndex:i.componentIndex,axisType:i.type,axisId:i.id,value:a,valueLabelOpt:{precision:l.get(["label","precision"]),formatter:l.get(["label","formatter"])},seriesDataIndices:r.slice()})}}function tLe(e,t,n){var a=n.axesInfo=[];we(t,function(r,o){var i=r.axisPointerModel.option,l=e[o];l?(!r.useHandle&&(i.status="show"),i.value=l.value,i.seriesDataIndices=(l.payloadBatch||[]).slice()):!r.useHandle&&(i.status="hide"),i.status==="show"&&a.push({axisDim:r.axis.dim,axisIndex:r.axis.model.componentIndex,value:i.value})})}function nLe(e,t,n,a){if(Am(t)||!e.list.length){a({type:"hideTip"});return}var r=((e.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};a({type:"showTip",escapeConnect:!0,x:t[0],y:t[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:e.list})}function aLe(e,t,n){var a=n.getZr(),r="axisPointerLastHighlights",o=YM(a)[r]||{},i=YM(a)[r]={};we(e,function(c,d){var v=c.axisPointerModel.option;v.status==="show"&&c.triggerEmphasis&&we(v.seriesDataIndices,function(f){i[f.seriesIndex+"|"+f.dataIndex]=f})});var l=[],s=[];function u(c){return{seriesIndex:c.seriesIndex,dataIndex:c.dataIndex}}we(o,function(c,d){!i[d]&&s.push(u(c))}),we(i,function(c,d){!o[d]&&l.push(u(c))}),s.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:s}),l.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:l})}function rLe(e,t){for(var n=0;n<(e||[]).length;n++){var a=e[n];if(t.axis.dim===a.axisDim&&t.axis.model.componentIndex===a.axisIndex)return a}}function qM(e){var t=e.axis.model,n={},a=n.axisDim=e.axis.dim;return n.axisIndex=n[a+"AxisIndex"]=t.componentIndex,n.axisName=n[a+"AxisName"]=t.name,n.axisId=n[a+"AxisId"]=t.id,n}function Am(e){return!e||e[0]==null||isNaN(e[0])||e[1]==null||isNaN(e[1])}function qB(e){FB.registerAxisPointerClass("CartesianAxisPointer",HIe),e.registerComponentModel(KIe),e.registerComponentView(XIe),e.registerPreprocessor(function(t){if(t){(!t.axisPointer||t.axisPointer.length===0)&&(t.axisPointer={});var n=t.axisPointer.link;n&&!Lt(n)&&(t.axisPointer.link=[n])}}),e.registerProcessor(e.PRIORITY.PROCESSOR.STATISTIC,{overallReset:function(t,n){t.getComponent("axisPointer").coordSysAxesInfo=bIe(t,n)}}),e.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},ZIe)}function oLe(e){Ws(PIe),Ws(qB)}function iLe(e,t){var n=y2(t.get("padding")),a=t.getItemStyle(["color","opacity"]);a.fill=t.get("backgroundColor");var r=new ga({shape:{x:e.x-n[3],y:e.y-n[0],width:e.width+n[1]+n[3],height:e.height+n[0]+n[2],r:t.get("borderRadius")},style:a,silent:!0,z2:-1});return r}var lLe=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n}return t.type="tooltip",t.dependencies=["axisPointer"],t.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click|mousewheel",alwaysShowContent:!1,renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,displayTransition:!0,enterable:!1,backgroundColor:tn.color.neutral00,shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,defaultBorderColor:tn.color.border,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:tn.color.borderShade,width:1,type:"dashed",textStyle:{}}},textStyle:{color:tn.color.tertiary,fontSize:14}},t}(Kn);function XB(e){var t=e.get("confine");return t!=null?!!t:e.get("renderMode")==="richText"}function ZB(e){if(hn.domSupported){for(var t=document.documentElement.style,n=0,a=e.length;n-1?(l+="top:50%",s+="translateY(-50%) rotate("+(u=o==="left"?-225:-45)+"deg)"):(l+="left:50%",s+="translateX(-50%) rotate("+(u=o==="top"?225:45)+"deg)");var c=u*Math.PI/180,d=i+r,v=d*Math.abs(Math.cos(c))+d*Math.abs(Math.sin(c)),f=Math.round(((v-Math.SQRT2*r)/2+Math.SQRT2*r-(v-d)/2)*100)/100;l+=";"+o+":-"+f+"px";var h=t+" solid "+r+"px;",m=["position:absolute;width:"+i+"px;height:"+i+"px;z-index:-1;",l+";"+s+";","border-bottom:"+h,"border-right:"+h,"background-color:"+a+";"];return'
'}function hLe(e,t,n){var a="cubic-bezier(0.23,1,0.32,1)",r="",o="";return n&&(r=" "+e/2+"s "+a,o="opacity"+r+",visibility"+r),t||(r=" "+e+"s "+a,o+=(o.length?",":"")+(hn.transformSupported?""+xS+r:",left"+r+",top"+r)),cLe+":"+o}function XM(e,t,n){var a=e.toFixed(0)+"px",r=t.toFixed(0)+"px";if(!hn.transformSupported)return n?"top:"+r+";left:"+a+";":[["top",r],["left",a]];var o=hn.transform3dSupported,i="translate"+(o?"3d":"")+"("+a+","+r+(o?",0":"")+")";return n?"top:0;left:0;"+xS+":"+i+";":[["top",0],["left",0],[JB,i]]}function pLe(e){var t=[],n=e.get("fontSize"),a=e.getTextColor();a&&t.push("color:"+a),t.push("font:"+e.getFont());var r=jt(e.get("lineHeight"),Math.round(n*3/2));n&&t.push("line-height:"+r+"px");var o=e.get("textShadowColor"),i=e.get("textShadowBlur")||0,l=e.get("textShadowOffsetX")||0,s=e.get("textShadowOffsetY")||0;return o&&i&&t.push("text-shadow:"+l+"px "+s+"px "+i+"px "+o),we(["decoration","align"],function(u){var c=e.get(u);c&&t.push("text-"+u+":"+c)}),t.join(";")}function mLe(e,t,n,a){var r=[],o=e.get("transitionDuration"),i=e.get("backgroundColor"),l=e.get("shadowBlur"),s=e.get("shadowColor"),u=e.get("shadowOffsetX"),c=e.get("shadowOffsetY"),d=e.getModel("textStyle"),v=jR(e,"html"),f=u+"px "+c+"px "+l+"px "+s;return r.push("box-shadow:"+f),t&&o>0&&r.push(hLe(o,n,a)),i&&r.push("background-color:"+i),we(["width","color","radius"],function(h){var m="border-"+h,g=yR(m),y=e.get(g);y!=null&&r.push(m+":"+y+(h==="color"?"":"px"))}),r.push(pLe(d)),v!=null&&r.push("padding:"+y2(v).join("px ")+"px"),r.join(";")+";"}function ZM(e,t,n,a,r){var o=t&&t.painter;if(n){var i=o&&o.getViewportRoot();i&&E8e(e,i,n,a,r)}else{e[0]=a,e[1]=r;var l=o&&o.getViewportRootOffset();l&&(e[0]+=l.offsetLeft,e[1]+=l.offsetTop)}e[2]=e[0]/t.getWidth(),e[3]=e[1]/t.getHeight()}var gLe=function(){function e(t,n){if(this._show=!1,this._styleCoord=[0,0,0,0],this._enterable=!0,this._alwaysShowContent=!1,this._firstShow=!0,this._longHide=!0,hn.wxa)return null;var a=document.createElement("div");a.domBelongToZr=!0,this.el=a;var r=this._zr=t.getZr(),o=n.appendTo,i=o&&(Ut(o)?document.querySelector(o):xh(o)?o:dn(o)&&o(t.getDom()));ZM(this._styleCoord,r,i,t.getWidth()/2,t.getHeight()/2),(i||t.getDom()).appendChild(a),this._api=t,this._container=i;var l=this;a.onmouseenter=function(){l._enterable&&(clearTimeout(l._hideTimeout),l._show=!0),l._inContent=!0},a.onmousemove=function(s){if(s=s||window.event,!l._enterable){var u=r.handler,c=r.painter.getViewportRoot();go(c,s,!0),u.dispatch("mousemove",s)}},a.onmouseleave=function(){l._inContent=!1,l._enterable&&l._show&&l.hideLater(l._hideDelay)}}return e.prototype.update=function(t){if(!this._container){var n=this._api.getDom(),a=uLe(n,"position"),r=n.style;r.position!=="absolute"&&a!=="absolute"&&(r.position="relative")}var o=t.get("alwaysShowContent");o&&this._moveIfResized(),this._alwaysShowContent=o,this._enableDisplayTransition=t.get("displayTransition")&&t.get("transitionDuration")>0,this.el.className=t.get("className")||""},e.prototype.show=function(t,n){clearTimeout(this._hideTimeout),clearTimeout(this._longHideTimeout);var a=this.el,r=a.style,o=this._styleCoord;a.innerHTML?r.cssText=dLe+mLe(t,!this._firstShow,this._longHide,this._enableDisplayTransition)+XM(o[0],o[1],!0)+("border-color:"+_c(n)+";")+(t.get("extraCssText")||"")+(";pointer-events:"+(this._enterable?"auto":"none")):r.display="none",this._show=!0,this._firstShow=!1,this._longHide=!1},e.prototype.setContent=function(t,n,a,r,o){var i=this.el;if(t==null){i.innerHTML="";return}var l="";if(Ut(o)&&a.get("trigger")==="item"&&!XB(a)&&(l=vLe(a,r,o)),Ut(t))i.innerHTML=t+l;else if(t){i.innerHTML="",Lt(t)||(t=[t]);for(var s=0;s=0?this._tryShow(o,i):r==="leave"&&this._hide(i))},this))},t.prototype._keepShow=function(){var n=this._tooltipModel,a=this._ecModel,r=this._api,o=n.get("triggerOn");if(n.get("trigger")!=="axis"&&(this._lastDataByCoordSys=null,this._cbParamsList=null),this._lastX!=null&&this._lastY!=null&&o!=="none"&&o!=="click"){var i=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout(function(){!r.isDisposed()&&i.manuallyShowTip(n,a,r,{x:i._lastX,y:i._lastY,dataByCoordSys:i._lastDataByCoordSys})})}},t.prototype.manuallyShowTip=function(n,a,r,o){if(!(o.from===this.uid||hn.node||!r.getDom())){var i=eE(o,r);this._ticket="";var l=o.dataByCoordSys,s=xLe(o,a,r);if(s){var u=s.el.getBoundingRect().clone();u.applyTransform(s.el.transform),this._tryShow({offsetX:u.x+u.width/2,offsetY:u.y+u.height/2,target:s.el,position:o.position,positionDefault:"bottom"},i)}else if(o.tooltip&&o.x!=null&&o.y!=null){var c=bLe;c.x=o.x,c.y=o.y,c.update(),In(c).tooltipConfig={name:null,option:o.tooltip},this._tryShow({offsetX:o.x,offsetY:o.y,target:c},i)}else if(l)this._tryShow({offsetX:o.x,offsetY:o.y,position:o.position,dataByCoordSys:l,tooltipOption:o.tooltipOption},i);else if(o.seriesIndex!=null){if(this._manuallyAxisShowTip(n,a,r,o))return;var d=jB(o,a),v=d.point[0],f=d.point[1];v!=null&&f!=null&&this._tryShow({offsetX:v,offsetY:f,target:d.el,position:o.position,positionDefault:"bottom"},i)}else o.x!=null&&o.y!=null&&(r.dispatchAction({type:"updateAxisPointer",x:o.x,y:o.y}),this._tryShow({offsetX:o.x,offsetY:o.y,position:o.position,target:r.getZr().findHover(o.x,o.y).target},i))}},t.prototype.manuallyHideTip=function(n,a,r,o){var i=this._tooltipContent;this._tooltipModel&&i.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,this._cbParamsList=null,o.from!==this.uid&&this._hide(eE(o,r))},t.prototype._manuallyAxisShowTip=function(n,a,r,o){var i=o.seriesIndex,l=o.dataIndex,s=a.getComponent("axisPointer").coordSysAxesInfo;if(!(i==null||l==null||s==null)){var u=a.getSeriesByIndex(i);if(u){var c=u.getData(),d=Qf([c.getItemModel(l),u,(u.coordinateSystem||{}).model],this._tooltipModel);if(d.get("trigger")==="axis")return r.dispatchAction({type:"updateAxisPointer",seriesIndex:i,dataIndex:l,position:o.position}),!0}}},t.prototype._tryShow=function(n,a){var r=n.target,o=this._tooltipModel;if(o){this._lastX=n.offsetX,this._lastY=n.offsetY;var i=n.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,n);else if(r){var l=In(r);if(l.ssrType==="legend")return;this._lastDataByCoordSys=null,this._cbParamsList=null;var s,u;gv(r,function(c){if(c.tooltipDisabled)return s=u=null,!0;s||u||(In(c).dataIndex!=null?s=c:In(c).tooltipConfig!=null&&(u=c))},!0),s?this._showSeriesItemTooltip(n,s,a):u?this._showComponentItemTooltip(n,u,a):this._hide(a)}else this._lastDataByCoordSys=null,this._cbParamsList=null,this._hide(a)}},t.prototype._showOrMove=function(n,a){var r=n.get("showDelay");a=qn(a,this),clearTimeout(this._showTimout),r>0?this._showTimout=setTimeout(a,r):a()},t.prototype._showAxisTooltip=function(n,a){var r=this._ecModel,o=this._tooltipModel,i=[a.offsetX,a.offsetY],l=Qf([a.tooltipOption],o),s=this._renderMode,u=[],c=Oh("section",{blocks:[],noHeader:!0}),d=[],v=new gb;we(n,function(b){we(b.dataByAxis,function(_){var w=r.getComponent(_.axisDim+"Axis",_.axisIndex),S=_.value,k=w.axis,T=k.scale.parse(S);if(!(!w||S==null)){var M=UB(S,k,r,_.seriesDataIndices,_.valueLabelOpt),E=Oh("section",{header:M,noHeader:!Di(M),sortBlocks:!0,blocks:[]});c.blocks.push(E),we(_.seriesDataIndices,function(A){var I=r.getSeriesByIndex(A.seriesIndex),P=A.dataIndexInside,R=I.getDataParams(P);if(!(R.dataIndex<0)){R.axisDim=_.axisDim,R.axisIndex=_.axisIndex,R.axisType=_.axisType,R.axisId=_.axisId,R.axisValue=qg(w.axis,{value:T}),R.axisValueLabel=M,R.marker=v.makeTooltipMarker("item",_c(R.color),s);var G=aT(I.formatTooltip(P,!0,null)),K=G.frag;if(K){var H=Qf([I],o).get("valueFormatter");E.blocks.push(H?vt({valueFormatter:H},K):K)}G.text&&d.push(G.text),u.push(R)}})}})}),c.blocks.reverse(),d.reverse();var f=a.position,h=l.get("order"),m=sT(c,v,s,h,r.get("useUTC"),l.get("textStyle"));m&&d.unshift(m);var g=s==="richText"?` + +`:"
",y=d.join(g);this._showOrMove(l,function(){this._updateContentNotChangedOnAxis(n,u)?this._updatePosition(l,f,i[0],i[1],this._tooltipContent,u):this._showTooltipContent(l,y,u,Math.random()+"",i[0],i[1],f,null,v)})},t.prototype._showSeriesItemTooltip=function(n,a,r){var o=this._ecModel,i=In(a),l=i.seriesIndex,s=o.getSeriesByIndex(l),u=i.dataModel||s,c=i.dataIndex,d=i.dataType,v=u.getData(d),f=this._renderMode,h=n.positionDefault,m=Qf([v.getItemModel(c),u,s&&(s.coordinateSystem||{}).model],this._tooltipModel,h?{position:h}:null),g=m.get("trigger");if(!(g!=null&&g!=="item")){var y=u.getDataParams(c,d),b=new gb;y.marker=b.makeTooltipMarker("item",_c(y.color),f);var _=aT(u.formatTooltip(c,!1,d)),w=m.get("order"),S=m.get("valueFormatter"),k=_.frag,T=k?sT(S?vt({valueFormatter:S},k):k,b,f,w,o.get("useUTC"),m.get("textStyle")):_.text,M="item_"+u.name+"_"+c;this._showOrMove(m,function(){this._showTooltipContent(m,T,y,M,n.offsetX,n.offsetY,n.position,n.target,b)}),r({type:"showTip",dataIndexInside:c,dataIndex:v.getRawIndex(c),seriesIndex:l,from:this.uid})}},t.prototype._showComponentItemTooltip=function(n,a,r){var o=this._renderMode==="html",i=In(a),l=i.tooltipConfig,s=l.option||{},u=s.encodeHTMLContent;if(Ut(s)){var c=s;s={content:c,formatter:c},u=!0}u&&o&&s.content&&(s=yn(s),s.content=Or(s.content));var d=[s],v=this._ecModel.getComponent(i.componentMainType,i.componentIndex);v&&d.push(v),d.push({formatter:s.content});var f=n.positionDefault,h=Qf(d,this._tooltipModel,f?{position:f}:null),m=h.get("content"),g=Math.random()+"",y=new gb;this._showOrMove(h,function(){var b=yn(h.get("formatterParams")||{});this._showTooltipContent(h,m,b,g,n.offsetX,n.offsetY,n.position,a,y)}),r({type:"showTip",from:this.uid})},t.prototype._showTooltipContent=function(n,a,r,o,i,l,s,u,c){if(this._ticket="",!(!n.get("showContent")||!n.get("show"))){var d=this._tooltipContent;d.setEnterable(n.get("enterable"));var v=n.get("formatter");s=s||n.get("position");var f=a,h=this._getNearestPoint([i,l],r,n.get("trigger"),n.get("borderColor"),n.get("defaultBorderColor",!0)),m=h.color;if(v)if(Ut(v)){var g=n.ecModel.get("useUTC"),y=Lt(r)?r[0]:r,b=y&&y.axisType&&y.axisType.indexOf("time")>=0;f=v,b&&(f=g2(y.axisValue,f,g)),f=bR(f,r,!0)}else if(dn(v)){var _=qn(function(w,S){w===this._ticket&&(d.setContent(S,c,n,m,s),this._updatePosition(n,s,i,l,d,r,u))},this);this._ticket=o,f=v(r,o,_)}else f=v;d.setContent(f,c,n,m,s),d.show(n,m),this._updatePosition(n,s,i,l,d,r,u)}},t.prototype._getNearestPoint=function(n,a,r,o,i){if(r==="axis"||Lt(a))return{color:o||i};if(!Lt(a))return{color:o||a.color||a.borderColor}},t.prototype._updatePosition=function(n,a,r,o,i,l,s){var u=this._api.getWidth(),c=this._api.getHeight();a=a||n.get("position");var d=i.getSize(),v=n.get("align"),f=n.get("verticalAlign"),h=s&&s.getBoundingRect().clone();if(s&&h.applyTransform(s.transform),dn(a)&&(a=a([r,o],l,i.el,h,{viewSize:[u,c],contentSize:d.slice()})),Lt(a))r=sa(a[0],u),o=sa(a[1],c);else if(Yt(a)){var m=a;m.width=d[0],m.height=d[1];var g=wc(m,{width:u,height:c});r=g.x,o=g.y,v=null,f=null}else if(Ut(a)&&s){var y=CLe(a,h,d,n.get("borderWidth"));r=y[0],o=y[1]}else{var y=wLe(r,o,i,u,c,v?null:20,f?null:20);r=y[0],o=y[1]}if(v&&(r-=tE(v)?d[0]/2:v==="right"?d[0]:0),f&&(o-=tE(f)?d[1]/2:f==="bottom"?d[1]:0),XB(n)){var y=SLe(r,o,i,u,c);r=y[0],o=y[1]}i.moveTo(r,o)},t.prototype._updateContentNotChangedOnAxis=function(n,a){var r=this._lastDataByCoordSys,o=this._cbParamsList,i=!!r&&r.length===n.length;return i&&we(r,function(l,s){var u=l.dataByAxis||[],c=n[s]||{},d=c.dataByAxis||[];i=i&&u.length===d.length,i&&we(u,function(v,f){var h=d[f]||{},m=v.seriesDataIndices||[],g=h.seriesDataIndices||[];i=i&&v.value===h.value&&v.axisType===h.axisType&&v.axisId===h.axisId&&m.length===g.length,i&&we(m,function(y,b){var _=g[b];i=i&&y.seriesIndex===_.seriesIndex&&y.dataIndex===_.dataIndex}),o&&we(v.seriesDataIndices,function(y){var b=y.seriesIndex,_=a[b],w=o[b];_&&w&&w.data!==_.data&&(i=!1)})})}),this._lastDataByCoordSys=n,this._cbParamsList=a,!!i},t.prototype._hide=function(n){this._lastDataByCoordSys=null,this._cbParamsList=null,n({type:"hideTip",from:this.uid})},t.prototype.dispose=function(n,a){hn.node||!a.getDom()||(o3(this,"_updatePosition"),this._tooltipContent.dispose(),E3("itemTooltip",a),this._tooltipContent=null,this._tooltipModel=null,this._lastDataByCoordSys=null,this._cbParamsList=null)},t.type="tooltip",t}(el);function Qf(e,t,n){var a=t.ecModel,r;n?(r=new fa(n,a,a),r=new fa(t.option,r,a)):r=t;for(var o=e.length-1;o>=0;o--){var i=e[o];i&&(i instanceof fa&&(i=i.get("tooltip",!0)),Ut(i)&&(i={formatter:i}),i&&(r=new fa(i,r,a)))}return r}function eE(e,t){return e.dispatchAction||qn(t.dispatchAction,t)}function wLe(e,t,n,a,r,o,i){var l=n.getSize(),s=l[0],u=l[1];return o!=null&&(e+s+o+2>a?e-=s+o:e+=o),i!=null&&(t+u+i>r?t-=u+i:t+=i),[e,t]}function SLe(e,t,n,a,r){var o=n.getSize(),i=o[0],l=o[1];return e=Math.min(e+i,a)-i,t=Math.min(t+l,r)-l,e=Math.max(e,0),t=Math.max(t,0),[e,t]}function CLe(e,t,n,a){var r=n[0],o=n[1],i=Math.ceil(Math.SQRT2*a)+8,l=0,s=0,u=t.width,c=t.height;switch(e){case"inside":l=t.x+u/2-r/2,s=t.y+c/2-o/2;break;case"top":l=t.x+u/2-r/2,s=t.y-o-i;break;case"bottom":l=t.x+u/2-r/2,s=t.y+c+i;break;case"left":l=t.x-r-i,s=t.y+c/2-o/2;break;case"right":l=t.x+u+i,s=t.y+c/2-o/2}return[l,s]}function tE(e){return e==="center"||e==="middle"}function xLe(e,t,n){var a=f8(e).queryOptionMap,r=a.keys()[0];if(!(!r||r==="series")){var o=_p(t,r,a.get(r),{useDefault:!1,enableAll:!1,enableNone:!1}),i=o.models[0];if(i){var l=n.getViewOfComponentModel(i),s;if(l.group.traverse(function(u){var c=In(u).tooltipConfig;if(c&&c.name===e.name)return s=u,!0}),s)return{componentMainType:r,componentIndex:i.componentIndex,el:s}}}}function kLe(e){Ws(qB),e.registerComponentModel(lLe),e.registerComponentView(_Le),e.registerAction({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},lr),e.registerAction({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},lr)}var TLe=function(e,t){if(t==="all")return{type:"all",title:e.getLocaleModel().get(["legend","selector","all"])};if(t==="inverse")return{type:"inverse",title:e.getLocaleModel().get(["legend","selector","inverse"])}},A3=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n.layoutMode={type:"box",ignoreSize:!0},n}return t.prototype.init=function(n,a,r){this.mergeDefaultAndTheme(n,r),n.selected=n.selected||{},this._updateSelector(n)},t.prototype.mergeOption=function(n,a){e.prototype.mergeOption.call(this,n,a),this._updateSelector(n)},t.prototype._updateSelector=function(n){var a=n.selector,r=this.ecModel;a===!0&&(a=n.selector=["all","inverse"]),Lt(a)&&we(a,function(o,i){Ut(o)&&(o={type:o}),a[i]=Bn(o,TLe(r,o.type))})},t.prototype.optionUpdated=function(){this._updateData(this.ecModel);var n=this._data;if(n[0]&&this.get("selectedMode")==="single"){for(var a=!1,r=0;r=0},t.prototype.getOrient=function(){return this.get("orient")==="vertical"?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},t.type="legend.plain",t.dependencies=["series"],t.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",bottom:tn.size.m,align:"auto",backgroundColor:tn.color.transparent,borderColor:tn.color.border,borderRadius:0,borderWidth:0,padding:5,itemGap:8,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:tn.color.disabled,inactiveBorderColor:tn.color.disabled,inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:tn.color.disabled,inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:tn.color.secondary},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:tn.color.tertiary,borderWidth:1,borderColor:tn.color.border},emphasis:{selectorLabel:{show:!0,color:tn.color.quaternary}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1},triggerEvent:!1},t}(Kn),ed=Sa,I3=we,W0=Ha,eV=function(e){At(t,e);function t(){var n=e!==null&&e.apply(this,arguments)||this;return n.type=t.type,n.newlineDisabled=!1,n}return t.prototype.init=function(){this.group.add(this._contentGroup=new W0),this.group.add(this._selectorGroup=new W0),this._isFirstRender=!0},t.prototype.getContentGroup=function(){return this._contentGroup},t.prototype.getSelectorGroup=function(){return this._selectorGroup},t.prototype.render=function(n,a,r){var o=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),!!n.get("show",!0)){var i=n.get("align"),l=n.get("orient");(!i||i==="auto")&&(i=n.get("left")==="right"&&l==="vertical"?"right":"left");var s=n.get("selector",!0),u=n.get("selectorPosition",!0);s&&(!u||u==="auto")&&(u=l==="horizontal"?"end":"start"),this.renderInner(i,n,a,r,s,l,u);var c=Y8(n,r).refContainer,d=n.getBoxLayoutParams(),v=n.get("padding"),f=wc(d,c,v),h=this.layoutInner(n,i,f,o,s,u),m=wc(zn({width:h.width,height:h.height},d),c,v);this.group.x=m.x-h.x,this.group.y=m.y-h.y,this.group.markRedraw(),this.group.add(this._backgroundEl=iLe(h,n))}},t.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},t.prototype.renderInner=function(n,a,r,o,i,l,s){var u=this.getContentGroup(),c=an(),d=a.get("selectedMode"),v=a.get("triggerEvent"),f=[];r.eachRawSeries(function(h){!h.get("legendHoverLink")&&f.push(h.id)}),I3(a.getData(),function(h,m){var g=this,y=h.get("name");if(!this.newlineDisabled&&(y===""||y===` +`)){var b=new W0;b.newline=!0,u.add(b);return}var _=r.getSeriesByName(y)[0];if(!c.get(y))if(_){var w=_.getData(),S=w.getVisual("legendLineStyle")||{},k=w.getVisual("legendIcon"),T=w.getVisual("style"),M=this._createItem(_,y,m,h,a,n,S,T,k,d,o);M.on("click",ed(nE,y,null,o,f)).on("mouseover",ed(L3,_.name,null,o,f)).on("mouseout",ed(P3,_.name,null,o,f)),r.ssr&&M.eachChild(function(E){var A=In(E);A.seriesIndex=_.seriesIndex,A.dataIndex=m,A.ssrType="legend"}),v&&M.eachChild(function(E){g.packEventData(E,a,_,m,y)}),c.set(y,!0)}else r.eachRawSeries(function(E){var A=this;if(!c.get(y)&&E.legendVisualProvider){var I=E.legendVisualProvider;if(!I.containName(y))return;var P=I.indexOfName(y),R=I.getItemVisual(P,"style"),G=I.getItemVisual(P,"legendIcon"),K=Fi(R.fill);K&&K[3]===0&&(K[3]=.2,R=vt(vt({},R),{fill:mp(K,"rgba")}));var H=this._createItem(E,y,m,h,a,n,{},R,G,d,o);H.on("click",ed(nE,null,y,o,f)).on("mouseover",ed(L3,null,y,o,f)).on("mouseout",ed(P3,null,y,o,f)),r.ssr&&H.eachChild(function(z){var B=In(z);B.seriesIndex=E.seriesIndex,B.dataIndex=m,B.ssrType="legend"}),v&&H.eachChild(function(z){A.packEventData(z,a,E,m,y)}),c.set(y,!0)}},this)},this),i&&this._createSelector(i,a,o,l,s)},t.prototype.packEventData=function(n,a,r,o,i){var l={componentType:"legend",componentIndex:a.componentIndex,dataIndex:o,value:i,seriesIndex:r.seriesIndex};In(n).eventData=l},t.prototype._createSelector=function(n,a,r,o,i){var l=this.getSelectorGroup();I3(n,function(u){var c=u.type,d=new Er({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){r.dispatchAction({type:c==="all"?"legendAllSelect":"legendInverseSelect",legendId:a.id})}});l.add(d);var v=a.getModel("selectorLabel"),f=a.getModel(["emphasis","selectorLabel"]);Cp(d,{normal:v,emphasis:f},{defaultText:u.title}),G_(d)})},t.prototype._createItem=function(n,a,r,o,i,l,s,u,c,d,v){var f=n.visualDrawType,h=i.get("itemWidth"),m=i.get("itemHeight"),g=i.isSelected(a),y=o.get("symbolRotate"),b=o.get("symbolKeepAspect"),_=o.get("icon");c=_||c||"roundRect";var w=MLe(c,o,s,u,f,g,v),S=new W0,k=o.getModel("textStyle");if(dn(n.getLegendIcon)&&(!_||_==="inherit"))S.add(n.getLegendIcon({itemWidth:h,itemHeight:m,icon:c,iconRotate:y,itemStyle:w.itemStyle,lineStyle:w.lineStyle,symbolKeepAspect:b}));else{var T=_==="inherit"&&n.getData().getVisual("symbol")?y==="inherit"?n.getData().getVisual("symbolRotate"):y:0;S.add(ELe({itemWidth:h,itemHeight:m,icon:c,iconRotate:T,itemStyle:w.itemStyle,symbolKeepAspect:b}))}var M=l==="left"?h+5:-5,E=l,A=i.get("formatter"),I=a;Ut(A)&&A?I=A.replace("{name}",a??""):dn(A)&&(I=A(a));var P=g?k.getTextColor():o.get("inactiveColor");S.add(new Er({style:ef(k,{text:I,x:M,y:m/2,fill:P,align:E,verticalAlign:"middle"},{inheritColor:P})}));var R=new ga({shape:S.getBoundingRect(),style:{fill:"transparent"}}),G=o.getModel("tooltip");return G.get("show")&&d2({el:R,componentModel:i,itemName:a,itemTooltipOption:G.option}),S.add(R),S.eachChild(function(K){K.silent=!0}),R.silent=!d,this.getContentGroup().add(S),G_(S),S.__legendDataIndex=r,S},t.prototype.layoutInner=function(n,a,r,o,i,l){var s=this.getContentGroup(),u=this.getSelectorGroup();Kv(n.get("orient"),s,n.get("itemGap"),r.width,r.height);var c=s.getBoundingRect(),d=[-c.x,-c.y];if(u.markRedraw(),s.markRedraw(),i){Kv("horizontal",u,n.get("selectorItemGap",!0));var v=u.getBoundingRect(),f=[-v.x,-v.y],h=n.get("selectorButtonGap",!0),m=n.getOrient().index,g=m===0?"width":"height",y=m===0?"height":"width",b=m===0?"y":"x";l==="end"?f[m]+=c[g]+h:d[m]+=v[g]+h,f[1-m]+=c[y]/2-v[y]/2,u.x=f[0],u.y=f[1],s.x=d[0],s.y=d[1];var _={x:0,y:0};return _[g]=c[g]+h+v[g],_[y]=Math.max(c[y],v[y]),_[b]=Math.min(0,v[b]+f[1-m]),_}else return s.x=d[0],s.y=d[1],this.group.getBoundingRect()},t.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},t.type="legend.plain",t}(el);function MLe(e,t,n,a,r,o,i){function l(g,y){g.lineWidth==="auto"&&(g.lineWidth=y.lineWidth>0?2:0),I3(g,function(b,_){g[_]==="inherit"&&(g[_]=y[_])})}var s=t.getModel("itemStyle"),u=s.getItemStyle(),c=e.lastIndexOf("empty",0)===0?"fill":"stroke",d=s.getShallow("decal");u.decal=!d||d==="inherit"?a.decal:c3(d,i),u.fill==="inherit"&&(u.fill=a[r]),u.stroke==="inherit"&&(u.stroke=a[c]),u.opacity==="inherit"&&(u.opacity=(r==="fill"?a:n).opacity),l(u,a);var v=t.getModel("lineStyle"),f=v.getLineStyle();if(l(f,n),u.fill==="auto"&&(u.fill=a.fill),u.stroke==="auto"&&(u.stroke=a.fill),f.stroke==="auto"&&(f.stroke=a.fill),!o){var h=t.get("inactiveBorderWidth"),m=u[c];u.lineWidth=h==="auto"?a.lineWidth>0&&m?2:0:u.lineWidth,u.fill=t.get("inactiveColor"),u.stroke=t.get("inactiveBorderColor"),f.stroke=v.get("inactiveColor"),f.lineWidth=v.get("inactiveWidth")}return{itemStyle:u,lineStyle:f}}function ELe(e){var t=e.icon||"roundRect",n=nf(t,0,0,e.itemWidth,e.itemHeight,e.itemStyle.fill,e.symbolKeepAspect);return n.setStyle(e.itemStyle),n.rotation=(e.iconRotate||0)*Math.PI/180,n.setOrigin([e.itemWidth/2,e.itemHeight/2]),t.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill=tn.color.neutral00,n.style.lineWidth=2),n}function nE(e,t,n,a){P3(e,t,n,a),n.dispatchAction({type:"legendToggleSelect",name:e??t}),L3(e,t,n,a)}function L3(e,t,n,a){n.usingTHL()||n.dispatchAction({type:"highlight",seriesName:e,name:t,excludeSeriesId:a})}function P3(e,t,n,a){n.usingTHL()||n.dispatchAction({type:"downplay",seriesName:e,name:t,excludeSeriesId:a})}function ev(e,t,n){var a=e==="allSelect"||e==="inverseSelect",r={},o=[];n.eachComponent({mainType:"legend",query:t},function(l){a?l[e]():l[e](t.name),aE(l,r),o.push(l.componentIndex)});var i={};return n.eachComponent("legend",function(l){we(r,function(s,u){l[s?"select":"unSelect"](u)}),aE(l,i)}),a?{selected:i,legendIndex:o}:{name:t.name,selected:i}}function aE(e,t){var n=t||{};return we(e.getData(),function(a){var r=a.get("name");if(!(r===` +`||r==="")){var o=e.isSelected(r);Sr(n,r)?n[r]=n[r]&&o:n[r]=o}}),n}function ALe(e){e.registerAction("legendToggleSelect","legendselectchanged",Sa(ev,"toggleSelected")),e.registerAction("legendAllSelect","legendselectall",Sa(ev,"allSelect")),e.registerAction("legendInverseSelect","legendinverseselect",Sa(ev,"inverseSelect")),e.registerAction("legendSelect","legendselected",Sa(ev,"select")),e.registerAction("legendUnSelect","legendunselected",Sa(ev,"unSelect"))}var ILe=h8(LLe);function LLe(e){var t=e.findComponents({mainType:"legend"});t&&t.length&&e.filterSeries(function(n){for(var a=0;ar[i],g=[-f.x,-f.y];a||(g[o]=c[u]);var y=[0,0],b=[-h.x,-h.y],_=jt(n.get("pageButtonGap",!0),n.get("itemGap",!0));if(m){var w=n.get("pageButtonPosition",!0);w==="end"?b[o]+=r[i]-h[i]:y[o]+=h[i]+_}b[1-o]+=f[l]/2-h[l]/2,c.setPosition(g),d.setPosition(y),v.setPosition(b);var S={x:0,y:0};if(S[i]=m?r[i]:f[i],S[l]=Math.max(f[l],h[l]),S[s]=Math.min(0,h[s]+b[1-o]),d.__rectSize=r[i],m){var k={x:0,y:0};k[i]=Math.max(r[i]-h[i]-_,0),k[l]=S[l],d.setClipPath(new ga({shape:k})),d.__rectSize=k[i]}else v.eachChild(function(M){M.attr({invisible:!0,silent:!0})});var T=this._getPageInfo(n);return T.pageIndex!=null&&Ar(c,{x:T.contentPosition[0],y:T.contentPosition[1]},m?n:null),this._updatePageInfoView(n,T),S},t.prototype._pageGo=function(n,a,r){var o=this._getPageInfo(a)[n];o!=null&&r.dispatchAction({type:"legendScroll",scrollDataIndex:o,legendId:a.id})},t.prototype._updatePageInfoView=function(n,a){var r=this._controllerGroup;we(["pagePrev","pageNext"],function(c){var d=c+"DataIndex",v=a[d]!=null,f=r.childOfName(c);f&&(f.setStyle("fill",v?n.get("pageIconColor",!0):n.get("pageIconInactiveColor",!0)),f.cursor=v?"pointer":"default")});var o=r.childOfName("pageText"),i=n.get("pageFormatter"),l=a.pageIndex,s=l!=null?l+1:0,u=a.pageCount;o&&i&&o.setStyle("text",Ut(i)?i.replace("{current}",s==null?"":s+"").replace("{total}",u==null?"":u+""):i({current:s,total:u}))},t.prototype._getPageInfo=function(n){var a=n.get("scrollDataIndex",!0),r=this.getContentGroup(),o=this._containerGroup.__rectSize,i=n.getOrient().index,l=Vb[i],s=zb[i],u=this._findTargetItemIndex(a),c=r.children(),d=c[u],v=c.length,f=v?1:0,h={contentPosition:[r.x,r.y],pageCount:f,pageIndex:f-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!d)return h;var m=w(d);h.contentPosition[i]=-m.s;for(var g=u+1,y=m,b=m,_=null;g<=v;++g)_=w(c[g]),(!_&&b.e>y.s+o||_&&!S(_,y.s))&&(b.i>y.i?y=b:y=_,y&&(h.pageNextDataIndex==null&&(h.pageNextDataIndex=y.i),++h.pageCount)),b=_;for(var g=u-1,y=m,b=m,_=null;g>=-1;--g)_=w(c[g]),(!_||!S(b,_.s))&&y.i=T&&k.s<=T+o}},t.prototype._findTargetItemIndex=function(n){if(!this._showController)return 0;var a,r=this.getContentGroup(),o;return r.eachChild(function(i,l){var s=i.__legendDataIndex;o==null&&s!=null&&(o=l),s===n&&(a=l)}),a??o},t.type="legend.scroll",t}(eV);function OLe(e){e.registerAction("legendScroll","legendscroll",function(t,n){var a=t.scrollDataIndex;a!=null&&n.eachComponent({mainType:"legend",subType:"scroll",query:t},function(r){r.setScrollDataIndex(a)})})}function $Le(e){Ws(tV),e.registerComponentModel(PLe),e.registerComponentView(DLe),OLe(e)}function RLe(e){Ws(tV),Ws($Le)}function iE(e,t,n){var a=oo.createCanvas(),r=t.getWidth(),o=t.getHeight(),i=a.style;return i&&(i.position="absolute",i.left="0",i.top="0",i.width=r+"px",i.height=o+"px",a.setAttribute("data-zr-dom-id",e)),a.width=r*n,a.height=o*n,a}function Fb(e){return!e.__cursors.get(Y$)}function lE(e){var t=e.__cursors.get(Y$);return{startIdx:t?t.startIdx:0,endIdx:t?t.endIdx:0}}var nV=function(e){At(t,e);function t(n,a,r){var o=e.call(this)||this;o.motionBlur=!1,o.lastFrameAlpha=.7,o.dpr=1,o.virtual=!1,o.config={},o.zlevel=0,o.zlevel2=bm,o.maxRepaintRectCount=5,o.__dirty=!0,o.__firstTimePaint=!0,o.__prevIdx={startIdx:0,endIdx:0};var i;r=r||xg,typeof n=="string"?i=iE(n,a,r):Yt(n)&&(i=n,n=i.id),o.id=n,o.dom=i;var l=i.style;return l&&(PO(i),i.onselectstart=function(){return!1},l.padding="0",l.margin="0",l.borderWidth="0"),o.painter=a,o.dpr=r,o}return t.prototype.afterBrush=function(){this.__prevIdx=lE(this)},t.prototype.initContext=function(){this.ctx=this.dom.getContext("2d"),this.ctx.dpr=this.dpr},t.prototype.setUnpainted=function(){this.__firstTimePaint=!0},t.prototype.createBackBuffer=function(){var n=this.dpr;this.domBack=iE("back-"+this.id,this.painter,n),this.ctxBack=this.domBack.getContext("2d"),n!==1&&this.ctxBack.scale(n,n)},t.prototype.createRepaintRects=function(n,a,r,o){if(this.__firstTimePaint)return this.__firstTimePaint=!1,null;var i=[],l=this.maxRepaintRectCount,s=!1,u=new ln(0,0,0,0);function c(w){if(!(!w.isFinite()||w.isZero()))if(i.length===0){var S=new ln(0,0,0,0);S.copy(w),i.push(S)}else{for(var k=!1,T=1/0,M=0,E=0;E=l)}}for(var d=lE(this),v=d.startIdx;v=0)&&(i=!0)}),!(!i&&!o.__dirty)){var l=a._opts.useDirtyRect&&!Fb(o)?o.createRepaintRects(t,n,a._width,a._height):null,s=a._i.layerStack[0],u=!0;if(o.__dirty){u=!1,o.__dirty=!1;var c=o.zlevel===s.zl&&o.zlevel2===s.zl2?a._backgroundColor:null;o.clear(!1,c,l)}K0(o,function(d){var v=a._paintPerCursor(o,d,t,l,u);r=r&&v})}},U0),hn.wxa&&Za(this._i,function(o){o&&o.ctx&&o.ctx.draw&&o.ctx.draw()}),r},e.prototype._paintPerCursor=function(t,n,a,r,o){var i=t.ctx;if(r)if(!r.length)n.drawIdx=n.endIdx;else for(var l=this.dpr,s=0;s=n.endIdx},e.prototype._paintPerCursorInRect=function(t,n,a,r,o){for(var i={inHover:!1,allClipped:!1,prevEl:null,viewWidth:this._width,viewHeight:this._height,beforeBrushParam:{contentRetained:o}},l=t.ctx,s=Fb(t),u=s&&oo.getTime(),c=n.drawIdx,d=n.notClearIdx,v=d>=0?Math.min(d,c):c;v15){v++;break}}}}Ed(l,i),n.drawIdx=Math.max(v,c)},e.prototype.getLayer=function(t,n){return this._ensureLayer(t,0,n)},e.prototype._ensureLayer=function(t,n,a){n=n||0;var r=this._singleCanvas;r&&!this._needsManuallyCompositing&&(t=Tu,n=0);var o=Kb(this._i,t)[n];return o||(o=uE("zr_"+t+"."+n,this,t,n),this._layerConfig[t]&&Bn(o,this._layerConfig[t],!0),(a||r&&t!==Tu)&&(o.virtual=!0),this._insertLayer(o,t,n,!1),o.initContext()),o},e.prototype.insertLayer=function(t,n){this._insertLayer(n,t,0,!1)},e.prototype._insertLayer=function(t,n,a,r){var o=this._i,i=o.layers,l=o.layerStack,s=this._domRoot,u=null;if(!(i[n]&&i[n][a])&&BLe(t)){for(var c=l.length,d=0;d0&&(u=Kb(o,l[d-1].zl)[l[d-1].zl2]),l.splice(d,0,{zl:n,zl2:a}),Kb(o,n)[a]=t,!r&&!t.virtual)if(u){var v=u.dom;v.nextSibling?s.insertBefore(t.dom,v.nextSibling):s.appendChild(t.dom)}else s.firstChild?s.insertBefore(t.dom,s.firstChild):s.appendChild(t.dom);t.painter||(t.painter=this)}},e.prototype.eachLayer=function(t,n){return Za(this._i,function(a,r){t.call(n,a,r)})},e.prototype.eachBuiltinLayer=function(t,n){return Za(this._i,function(a,r){t.call(n,a,r)},jv)},e.prototype.eachOtherLayer=function(t,n){return Za(this._i,function(a,r){t.call(n,a,r)},D3)},e.prototype.getLayers=function(){var t={};return Za(this._i,function(n,a,r){t[n.id]=n}),t},e.prototype._updateLayerStatus=function(t,n){var a=this;if(a._singleCanvas)for(var r=1;r=0;_--){var w=b.get(y[_]);if(!w.used)g.__dirty=!0,b.removeKey(y[_]),y.splice(_,1);else{var S=w.endIdxNew;(Fb(g)?S=0;r--){var o=n[r];if(o.zl===t){var i=a[t][o.zl2];if(i.__builtin__)continue;if(n.splice(r,1),a[t][o.zl2]=void 0,!i.virtual){var l=i.dom.parentNode;l&&l.removeChild(i.dom)}}}},e.prototype.resize=function(t,n){if(this._domRoot.style){var a=this._domRoot;a.style.display="none";var r=this._opts,o=this.root;t!=null&&(r.width=t),n!=null&&(r.height=n),t=T0(o,0,r),n=T0(o,1,r),a.style.display="",(this._width!==t||n!==this._height)&&(a.style.width=t+"px",a.style.height=n+"px",Za(this._i,function(i){i.resize(t,n)}),this.refresh({paintAll:!0})),this._width=t,this._height=n}else{if(t==null||n==null)return;this._width=t,this._height=n,this._ensureLayer(Tu).resize(t,n)}return this},e.prototype.clearLayer=function(t){we(this._i.layers[t],function(n){n&&!n.__builtin__&&n.clear()})},e.prototype.dispose=function(){this.root.innerHTML="",this.root=this.storage=this._domRoot=this._i=null},e.prototype.getRenderedCanvas=function(t){if(t=t||{},this._singleCanvas&&!this._compositeManually)return this._i.layers[Tu][0].dom;var n=new nV("image",this,t.pixelRatio||this.dpr);n.initContext(),n.clear(!1,t.backgroundColor||this._backgroundColor);var a=n.ctx;if(t.pixelRatio<=this.dpr){this.refresh();var r=n.dom.width,o=n.dom.height;Za(this._i,function(d){d.__builtin__?a.drawImage(d.dom,0,0,r,o):d.renderToCanvas&&(a.save(),d.renderToCanvas(a),a.restore())})}else{for(var i={inHover:!1,viewWidth:this._width,viewHeight:this._height,beforeBrushParam:{}},l=this.storage.getDisplayList(!0),s=0,u=l.length;s({}));throw new Error(c.error||`HTTP ${s.status}`)}return(await s.json()).data}const yt={dashboard:(e={})=>Mt("/dashboard",e),epayInfo:()=>Mt("/epay-info"),channels:()=>Mt("/channels"),settings:()=>Mt("/settings"),saveSettings:e=>Mt("/settings",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json",Accept:"application/json"}}),settingsKeys:()=>Mt("/settings/keys"),resetAdmin:e=>Mt("/auth/reset",{},{method:"POST",body:JSON.stringify({password:e}),headers:{"Content-Type":"application/json"}}),toggleChannel:e=>Mt(`/settings/channel/${e}/toggle`,{},{method:"POST"}),toggleChannelMode:(e,t)=>Mt(`/settings/channel/${e}/mode/${t}/toggle`,{},{method:"POST"}),orders:e=>Mt("/orders",e),orderEvents:(e,t=20)=>Mt(`/orders/${e}/events`,{limit:t}),notifyTasks:e=>Mt("/notify-tasks",e),notifyStats:()=>Mt("/notify-stats"),logs:e=>Mt("/logs",e),ledger:e=>Mt("/ledger",e),ledgerVerify:()=>Mt("/ledger/verify",{},{method:"POST"}),ledgerSnapshots:e=>Mt("/ledger/snapshots",e),createSnapshot:()=>Mt("/ledger/snapshot",{},{method:"POST"}),ledgerPublicKey:()=>Mt("/ledger/public-key"),ledgerExport:async()=>{const e=wv(),t=await fetch(_v+"/ledger/export",{headers:{Authorization:"Bearer "+e}});if(!t.ok){const o=await t.json().catch(()=>({}));throw new Error(o.error||"导出失败")}const n=await t.blob(),a=URL.createObjectURL(n),r=document.createElement("a");r.href=a,r.download="ledger_audit_"+new Date().toISOString().slice(0,10)+".csv",document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(a)},tunnel:()=>Mt("/tunnel"),accounts:e=>Mt("/accounts",e),getAccount:e=>Mt(`/accounts/${e}`),accountSecret:e=>Mt(`/accounts/${e}/secret`),createAccount:e=>Mt("/accounts",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}}),updateAccount:(e,t)=>Mt(`/accounts/${e}`,{},{method:"PUT",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),deleteAccount:e=>Mt(`/accounts/${e}`,{},{method:"DELETE"}),freezeAccount:e=>Mt(`/accounts/${e}/freeze`,{},{method:"POST"}),unfreezeAccount:e=>Mt(`/accounts/${e}/unfreeze`,{},{method:"POST"}),enableAccount:e=>Mt(`/accounts/${e}/enable`,{},{method:"POST"}),disableAccount:e=>Mt(`/accounts/${e}/disable`,{},{method:"POST"}),accountLogs:(e,t)=>Mt(`/accounts/${e}/logs`,{limit:t}),decodeQRCode:async e=>{const t=new FormData;t.append("file",e);const n=wv(),a=await fetch(_v+"/accounts/decode-qrcode",{method:"POST",headers:{Authorization:"Bearer "+n},body:t});if(a.status===401)throw sessionStorage.removeItem("admin_token"),window.location.reload(),new Error("请重新登录");const r=await a.json();if(!a.ok)throw new Error(r.error||`HTTP ${a.status}`);return r.data.content},upstreams:()=>Mt("/upstreams"),getUpstream:e=>Mt(`/upstreams/${e}`),upstreamSecret:e=>Mt(`/upstreams/${e}/secret`),createUpstream:e=>Mt("/upstreams",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}}),updateUpstream:(e,t)=>Mt(`/upstreams/${e}`,{},{method:"PUT",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),deleteUpstream:e=>Mt(`/upstreams/${e}`,{},{method:"DELETE"}),enableUpstream:e=>Mt(`/upstreams/${e}/enable`,{},{method:"POST"}),disableUpstream:e=>Mt(`/upstreams/${e}/disable`,{},{method:"POST"}),upstreamLogs:(e,t)=>Mt(`/upstreams/${e}/logs`,{limit:t}),listExceptions:e=>Mt("/reconciler/exceptions",e),getException:e=>Mt(`/reconciler/exceptions/${e}`),requeryException:e=>Mt(`/reconciler/exceptions/${e}/requery`,{},{method:"POST"}),resolveException:(e,t)=>Mt(`/reconciler/exceptions/${e}/resolve`,{},{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),reopenException:e=>Mt(`/reconciler/exceptions/${e}/reopen`,{},{method:"POST"}),addException:e=>Mt("/reconciler/exceptions",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}}),listDeadNotify:()=>Mt("/reconciler/notify-dead"),retryDeadNotify:e=>Mt(`/reconciler/notify-dead/${e}/retry`,{},{method:"POST"}),deleteDeadNotify:e=>Mt(`/reconciler/notify-dead/${e}/delete`,{},{method:"POST"}),dailyReport:e=>Mt("/reconciler/daily-report",e),personalDevices:()=>Mt("/personal/devices"),deletePersonalDevice:e=>Mt(`/personal/devices/${e}`,{},{method:"DELETE"}),freezePersonalDevice:e=>Mt(`/personal/devices/${e}/freeze`,{},{method:"POST"}),unfreezePersonalDevice:e=>Mt(`/personal/devices/${e}/unfreeze`,{},{method:"POST"}),restartPersonalDevice:e=>Mt(`/personal/devices/${e}/restart`,{},{method:"POST"}),personalPushes:e=>Mt("/personal/pushes",e),personalUnmatched:e=>Mt("/personal/unmatched",e),resolveUnmatched:(e,t)=>Mt(`/personal/unmatched/${e}/resolve`,{},{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),personalConfigQR:async e=>{const t=wv(),n=await fetch(_v+`/personal/devices/${e}/config-qr`,{headers:{Authorization:"Bearer "+t}});if(n.status===401)throw sessionStorage.removeItem("admin_token"),window.location.reload(),new Error("请重新登录");const a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||`HTTP ${n.status}`);const r=await n.blob();return URL.createObjectURL(r)},personalScreenshot:async e=>{const t=wv(),n=await fetch(_v+`/personal/devices/${e}/screenshot`,{headers:{Authorization:"Bearer "+t}});if(n.status===401)throw sessionStorage.removeItem("admin_token"),window.location.reload(),new Error("请重新登录");const a=n.headers.get("Content-Type")||"";if(!n.ok||!a.includes("image")){const o=await n.json().catch(()=>({}));throw new Error(o.error||`HTTP ${n.status}`)}const r=await n.blob();return URL.createObjectURL(r)},personalDeviceAction:(e,t)=>Mt(`/personal/devices/${e}/action`,{},{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),personalSetAdb:(e,t)=>Mt(`/personal/devices/${e}/adb`,{},{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),personalDeviceAccounts:e=>Mt(`/personal/devices/${e}/accounts`),personalAddDeviceAccount:(e,t)=>Mt(`/personal/devices/${e}/accounts`,{},{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json"}}),personalRemoveDeviceAccount:(e,t)=>Mt(`/personal/devices/${e}/accounts/${t}`,{},{method:"DELETE"}),personalBatchRestart:e=>Mt("/personal/devices/batch/restart",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}}),personalBatchStatus:e=>Mt("/personal/devices/batch/status",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}}),personalUsbDetect:()=>Mt("/personal/usb-detect"),personalWizardStart:e=>Mt("/personal/devices/wizard-start",{},{method:"POST",body:JSON.stringify(e),headers:{"Content-Type":"application/json"}}),personalWizardWifi:e=>Mt(`/personal/devices/${e}/wizard-wifi`,{},{method:"POST"}),personalWizardVerify:e=>Mt(`/personal/devices/${e}/wizard-verify`,{},{method:"POST"}),personalWifiReconnect:e=>Mt(`/personal/devices/${e}/wifi-reconnect`,{},{method:"POST"}),personalQrcodes:()=>Mt("/personal/qrcodes"),personalDeleteQrcode:(e,t,n=0)=>Mt(`/personal/qrcodes/${e}/${t}/${n}`,{},{method:"DELETE"}),personalCaptureWechatQrcode:(e,t=0)=>Mt(`/personal/qrcodes/${e}/wechat/capture?user=${t}`,{},{method:"POST"}),personalCaptureAlipayQrcode:(e,t=999)=>Mt(`/personal/qrcodes/${e}/alipay/capture?user=${t}`,{},{method:"POST"})},KLe={class:"dash"},ULe={class:"kpi-grid"},GLe={class:"kpi-label"},YLe={class:"kpi-value font-mono"},jLe={class:"kpi-delta"},qLe={class:"chart-row"},XLe={class:"pg-card chart-card wide"},ZLe={class:"pg-card-header"},JLe={class:"trend-controls"},QLe={class:"pg-card chart-card"},ePe={class:"chart-row"},tPe={class:"pg-card chart-card wide"},nPe={class:"pg-card chart-card"},aPe={class:"status-row"},rPe={class:"pg-card status-card"},oPe={class:"pg-card-header"},iPe={class:"pg-card-body"},lPe={class:"tunnel-info"},sPe={class:"tunnel-details"},uPe={class:"tunnel-url font-mono"},cPe={key:0,class:"tunnel-cb text-muted"},dPe={class:"pg-card status-card"},fPe={class:"pg-card-body"},vPe={class:"queue-grid"},hPe={class:"queue-item"},pPe={class:"queue-num font-mono text-success"},mPe={class:"queue-item"},gPe={class:"queue-num font-mono text-warning"},yPe={class:"queue-item"},bPe={class:"queue-num font-mono text-danger"},_Pe={class:"queue-item"},wPe={class:"queue-num font-mono"},SPe={class:"rate-label text-muted"},CPe={key:0,class:"pg-card"},xPe={class:"pg-card-header"},kPe={style:{display:"flex","align-items":"center",gap:"8px"}},TPe={class:"pg-card-body"},MPe={class:"ledger-grid"},EPe={class:"ledger-item"},APe={class:"ledger-num font-mono"},IPe={class:"ledger-item"},LPe={class:"ledger-num font-mono text-muted",style:{"font-size":"12px"}},PPe={class:"ledger-item"},DPe={class:"ledger-num text-muted",style:{"font-size":"12px"}},OPe={class:"ledger-item"},$Pe={class:"pg-card"},RPe={class:"pg-card-header"},NPe={class:"font-mono"},BPe={__name:"Dashboard",props:{refreshKey:Number},emits:["tunnel","goto"],setup(e,{emit:t}){Ws([K7e,KAe,rIe,oLe,kLe,RLe,WLe]);const n=t,a=e,r=F({}),o=F(!1),i=F(14),l=F(null),s=F([]),u=F(null),c=F(!1),d=D(()=>r.value.ledger_last_hash||""),v=D(()=>d.value?d.value.slice(0,12)+"…":"—"),f=D(()=>r.value.ledger_last_time?r.value.ledger_last_time.slice(0,19).replace("T"," "):"—");async function h(){c.value=!0;try{const V=await yt.ledgerVerify();u.value=V,V.valid?We.success("账本校验通过,共 "+V.total_entries+" 条"):We.error("账本校验失败,"+(V.errors||[]).length+" 处错误")}catch(V){We.error("校验失败: "+V.message)}finally{c.value=!1}}const m=F(null),g=F(null),y=F(null),b=F(null);let _=[];const w=D(()=>{const V=r.value.trend||[],Y=new Date().toLocaleDateString("en-CA"),X=V.find(re=>re.date===Y)||{total_orders:0,success_orders:0,amount_minor:0},ee=X.total_orders>0?Math.round(X.success_orders/X.total_orders*100):0;return[{label:"今日订单",value:(X.total_orders??0).toLocaleString(),delta:"成功 "+(X.success_orders??0)+" 单"},{label:"今日交易额",value:"¥"+((X.amount_minor??0)/100).toFixed(2),delta:"成功订单金额"},{label:"成功率",value:ee+"%",delta:"今日成功率"},{label:"待支付",value:(r.value.wait_orders??0).toLocaleString(),delta:"累计待支付"},{label:"累计交易额",value:"¥"+((r.value.total_amount_minor??0)/100).toFixed(2),delta:"全部成功订单"}]}),S=D(()=>r.value.tunnel||{}),k=D(()=>S.value.running&&S.value.healthy),T=D(()=>S.value.running?S.value.healthy?"正常运行":"异常":"未连接"),M=D(()=>(r.value.notify_success??0)+(r.value.notify_pending??0)+(r.value.notify_dead??0)),E=D(()=>M.value>0?Math.round((r.value.notify_success??0)/M.value*100):100),A=D(()=>{const V=document.documentElement.classList.contains("dark");return E.value>=99?V?"#22C55E":"#16A34A":E.value>=90?V?"#D97706":"#B45309":V?"#EF4444":"#DC2626"});function I(){const V=document.documentElement.classList.contains("dark");return{accent:"#7170FF",text:V?"#C4C4C8":"#3F3F46",muted:V?"#52525B":"#A1A1AA",border:V?"rgba(255,255,255,0.07)":"rgba(0,0,0,0.06)",bg:V?"#18181B":"#F4F4F5",success:V?"#4ADE80":"#22A069",warning:V?"#FCD34D":"#C27803",danger:V?"#F87171":"#D92D20"}}function P(){const V=I(),Y=r.value.trend||[],X=Y.map(le=>le.date.slice(5)),ee=Y.map(le=>le.total_orders),re=Y.map(le=>le.success_orders),ie=Y.map(le=>(le.amount_minor/100).toFixed(2));if(m.value){const le=yv(m.value)||L0(m.value);le.setOption({tooltip:{trigger:"axis",backgroundColor:V.bg,borderColor:V.border,textStyle:{color:V.text,fontSize:12},axisPointer:{lineStyle:{color:V.border}}},legend:{data:["订单数","成功"],textStyle:{color:V.muted,fontSize:11},top:0},grid:{left:40,right:16,top:32,bottom:10,containLabel:!0},xAxis:{type:"category",data:X,axisLine:{lineStyle:{color:V.border}},axisTick:{show:!1},axisLabel:{color:V.muted,fontSize:11}},yAxis:{type:"value",splitLine:{show:!1},axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:V.muted,fontSize:11}},series:[{name:"订单数",type:"line",smooth:!0,data:ee,symbol:"circle",symbolSize:5,lineStyle:{width:2,color:V.accent},itemStyle:{color:V.accent}},{name:"成功",type:"line",smooth:!0,data:re,symbol:"circle",symbolSize:4,lineStyle:{width:1.5,color:V.muted,type:"dashed"},itemStyle:{color:V.muted}}]},!0),_.push(le)}if(g.value){const le=yv(g.value)||L0(g.value),ve=s.value.map(J=>({name:J.name,value:J.total_orders})),he=["#7170FF","#9B9BF5","#C5C5FF","#DEDEDFF","#EBEBFF"];le.setOption({tooltip:{trigger:"item",backgroundColor:V.bg,borderColor:V.border,textStyle:{color:V.text,fontSize:12},formatter:"{b}: {c} 单 ({d}%)"},legend:{bottom:4,textStyle:{color:V.muted,fontSize:11}},series:[{type:"pie",radius:["44%","66%"],center:["50%","48%"],avoidLabelOverlap:!0,itemStyle:{borderRadius:4,borderColor:V.bg,borderWidth:2},label:{show:!1},emphasis:{label:{show:!0,fontSize:13,fontWeight:590,color:V.text}},data:ve.map((J,Z)=>({...J,itemStyle:{color:he[Z%he.length]}}))}]},!0),_.push(le)}if(y.value){const le=yv(y.value)||L0(y.value);le.setOption({tooltip:{trigger:"axis",backgroundColor:V.bg,borderColor:V.border,textStyle:{color:V.text,fontSize:12},formatter:ve=>`${ve[0].axisValue}
交易额: ¥${ve[0].value}`},grid:{left:56,right:16,top:12,bottom:10,containLabel:!0},xAxis:{type:"category",data:X,axisLine:{lineStyle:{color:V.border}},axisTick:{show:!1},axisLabel:{color:V.muted,fontSize:11}},yAxis:{type:"value",splitLine:{show:!1},axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:V.muted,fontSize:11,formatter:ve=>"¥"+ve}},series:[{type:"bar",data:ie,barWidth:"50%",itemStyle:{color:V.accent,borderRadius:[3,3,0,0]}}]},!0),_.push(le)}if(b.value){const le=yv(b.value)||L0(b.value),ve=[{name:"成功",value:r.value.success_orders??0,color:V.accent},{name:"待支付",value:r.value.wait_orders??0,color:V.muted},{name:"已关闭",value:r.value.closed_orders??0,color:V.muted}];le.setOption({tooltip:{trigger:"axis",backgroundColor:V.bg,borderColor:V.border,textStyle:{color:V.text,fontSize:12}},grid:{left:12,right:40,top:8,bottom:10,containLabel:!0},xAxis:{type:"value",splitLine:{show:!1},axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:V.muted,fontSize:11}},yAxis:{type:"category",data:ve.map(he=>he.name),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:V.text,fontSize:12}},series:[{type:"bar",barWidth:"38%",data:ve.map(he=>({value:he.value,itemStyle:{color:he.color,borderRadius:[0,3,3,0]}})),label:{show:!0,position:"right",color:V.muted,fontSize:11}}]},!0),_.push(le)}}async function R(){o.value=!0;try{let V={};l.value&&l.value.length===2?V={start:l.value[0],end:l.value[1]}:V={days:i.value};const[Y,X]=await Promise.all([yt.dashboard(V),yt.channels()]);r.value=Y,s.value=X,n("tunnel",Y.tunnel),setTimeout(P,50)}catch(V){We.error("加载失败: "+V.message)}finally{o.value=!1}}function G(){l.value=null,H()}function K(V){V&&H()}function H(){_.forEach(V=>V.dispose()),_=[],R()}ye(()=>a.refreshKey,R),ye(i,G);function z(){_.forEach(V=>V.resize())}function B(V){return{WAIT_BUYER_PAY:"待支付",TRADE_SUCCESS:"成功",TRADE_CLOSED:"已关闭",CONFIRMING:"确认中",FAILED:"失败"}[V]||V}function W(V){return{WAIT_BUYER_PAY:"warning",TRADE_SUCCESS:"success",TRADE_CLOSED:"info",CONFIRMING:"warning",FAILED:"danger"}[V]||"info"}function N(V){return V?new Date(V).toLocaleString("zh-CN",{hour12:!1}):"—"}return mt(()=>{R(),window.addEventListener("resize",z)}),rn(()=>{window.removeEventListener("resize",z),_.forEach(V=>V.dispose()),_=[]}),(V,Y)=>{var se,fe,ge,Se,De;const X=be("el-radio-button"),ee=be("el-radio-group"),re=be("el-date-picker"),ie=be("Connection"),le=be("el-icon"),ve=be("el-progress"),he=be("el-tag"),J=be("el-button"),Z=be("el-table-column"),j=be("el-table"),oe=Gr("loading");return st((x(),O("div",KLe,[C("div",ULe,[(x(!0),O(Ve,null,dt(w.value,Te=>(x(),O("div",{key:Te.label,class:"kpi-card pg-card"},[C("div",GLe,Q(Te.label),1),C("div",YLe,Q(Te.value),1),C("div",jLe,Q(Te.delta),1)]))),128))]),C("div",qLe,[C("div",XLe,[C("div",ZLe,[Y[6]||(Y[6]=C("span",{class:"chart-title"},"订单趋势",-1)),C("div",JLe,[L(ee,{modelValue:i.value,"onUpdate:modelValue":Y[0]||(Y[0]=Te=>i.value=Te),size:"small",onChange:G},{default:$(()=>[L(X,{value:7},{default:$(()=>[...Y[3]||(Y[3]=[ue("7天",-1)])]),_:1}),L(X,{value:14},{default:$(()=>[...Y[4]||(Y[4]=[ue("14天",-1)])]),_:1}),L(X,{value:30},{default:$(()=>[...Y[5]||(Y[5]=[ue("30天",-1)])]),_:1})]),_:1},8,["modelValue"]),L(re,{modelValue:l.value,"onUpdate:modelValue":Y[1]||(Y[1]=Te=>l.value=Te),type:"daterange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期","value-format":"YYYY-MM-DD",size:"small",clearable:!1,"disabled-date":Te=>Te.getTime()>Date.now(),style:{width:"230px"},onChange:K},null,8,["modelValue","disabled-date"])])]),C("div",{ref_key:"trendRef",ref:m,class:"chart-box"},null,512)]),C("div",QLe,[Y[7]||(Y[7]=C("div",{class:"pg-card-header"},[C("span",{class:"chart-title"},"通道占比")],-1)),C("div",{ref_key:"channelPieRef",ref:g,class:"chart-box"},null,512)])]),C("div",ePe,[C("div",tPe,[Y[8]||(Y[8]=C("div",{class:"pg-card-header"},[C("span",{class:"chart-title"},"交易金额")],-1)),C("div",{ref_key:"amountRef",ref:y,class:"chart-box"},null,512)]),C("div",nPe,[Y[9]||(Y[9]=C("div",{class:"pg-card-header"},[C("span",{class:"chart-title"},"订单状态")],-1)),C("div",{ref_key:"statusRef",ref:b,class:"chart-box"},null,512)])]),C("div",aPe,[C("div",rPe,[C("div",oPe,[Y[11]||(Y[11]=C("span",{class:"chart-title"},"公网隧道",-1)),C("div",{class:U(["tunnel-badge",{ok:k.value}])},[Y[10]||(Y[10]=C("span",{class:"dot"},null,-1)),ue(" "+Q(T.value),1)],2)]),C("div",iPe,[C("div",lPe,[L(le,{size:18,class:"tunnel-icon"},{default:$(()=>[L(ie)]),_:1}),C("div",sPe,[C("div",uPe,Q(((se=S.value)==null?void 0:se.url)||"未连接"),1),(fe=S.value)!=null&&fe.callback?(x(),O("div",cPe,Q(S.value.callback),1)):te("",!0)])])])]),C("div",dPe,[Y[16]||(Y[16]=C("div",{class:"pg-card-header"},[C("span",{class:"chart-title"},"回调队列")],-1)),C("div",fPe,[C("div",vPe,[C("div",hPe,[C("div",pPe,Q(r.value.notify_success??0),1),Y[12]||(Y[12]=C("div",{class:"queue-label"},"成功",-1))]),C("div",mPe,[C("div",gPe,Q(r.value.notify_pending??0),1),Y[13]||(Y[13]=C("div",{class:"queue-label"},"待重试",-1))]),C("div",yPe,[C("div",bPe,Q(r.value.notify_dead??0),1),Y[14]||(Y[14]=C("div",{class:"queue-label"},"死信",-1))]),C("div",_Pe,[C("div",wPe,Q(r.value.ledger_count??0),1),Y[15]||(Y[15]=C("div",{class:"queue-label"},"账本条目",-1))])]),M.value>0?(x(),ne(ve,{key:0,percentage:E.value,"stroke-width":4,color:A.value,class:"mt14"},null,8,["percentage","color"])):te("",!0),C("div",SPe,"回调成功率 "+Q(E.value)+"%",1)])])]),d.value?(x(),O("div",CPe,[C("div",xPe,[Y[18]||(Y[18]=C("span",{class:"chart-title"},"账本状态",-1)),C("div",kPe,[L(he,{type:(ge=u.value)!=null&&ge.valid?"success":"warning",size:"small",effect:"light"},{default:$(()=>{var Te;return[ue(Q((Te=u.value)!=null&&Te.valid?"账本完整":"未校验"),1)]}),_:1},8,["type"]),L(J,{size:"small",onClick:h,loading:c.value,plain:""},{default:$(()=>[L(le,null,{default:$(()=>[L(p(Ao))]),_:1}),Y[17]||(Y[17]=ue(" 校验 ",-1))]),_:1},8,["loading"])])]),C("div",TPe,[C("div",MPe,[C("div",EPe,[C("div",APe,Q(r.value.ledger_count??0),1),Y[19]||(Y[19]=C("div",{class:"ledger-label"},"总条目",-1))]),C("div",IPe,[C("div",LPe,Q(v.value),1),Y[20]||(Y[20]=C("div",{class:"ledger-label"},"链尾哈希",-1))]),C("div",PPe,[C("div",DPe,Q(f.value),1),Y[21]||(Y[21]=C("div",{class:"ledger-label"},"最近记录",-1))]),C("div",OPe,[C("div",{class:U(["ledger-num",(Se=u.value)!=null&&Se.snapshot_ok?"text-success":"text-muted"])},Q((De=u.value)!=null&&De.snapshot_ok?"匹配":"—"),3),Y[22]||(Y[22]=C("div",{class:"ledger-label"},"快照锚点",-1))])])])])):te("",!0),C("div",$Pe,[C("div",RPe,[Y[24]||(Y[24]=C("span",{class:"chart-title"},"最近订单",-1)),C("button",{class:"link-btn",onClick:Y[2]||(Y[2]=Te=>V.$emit("goto","orders"))},[Y[23]||(Y[23]=ue(" 查看全部 ",-1)),L(le,{size:13},{default:$(()=>[L(p(za))]),_:1})])]),L(j,{data:r.value.recent_orders||[],size:"small"},{default:$(()=>[L(Z,{prop:"out_trade_no",label:"商户订单号","min-width":"150","show-overflow-tooltip":""}),L(Z,{prop:"subject",label:"商品","min-width":"100","show-overflow-tooltip":""}),L(Z,{label:"金额",width:"100"},{default:$(({row:Te})=>[C("span",NPe,"¥"+Q((Te.amount_minor/100).toFixed(2)),1)]),_:1}),L(Z,{label:"状态",width:"100"},{default:$(({row:Te})=>[L(he,{type:W(Te.status),size:"small",effect:"light"},{default:$(()=>[ue(Q(B(Te.status)),1)]),_:2},1032,["type"])]),_:1}),L(Z,{label:"时间",width:"160"},{default:$(({row:Te})=>[ue(Q(N(Te.created_at)),1)]),_:1})]),_:1},8,["data"])])])),[[oe,o.value]])}}},VPe=fr(BPe,[["__scopeId","data-v-a6ea8b68"]]),zPe={class:"channels-page"},FPe={class:"pg-card epay-info-card"},HPe={class:"pg-card-header"},WPe={class:"pg-card-body"},KPe={class:"cfg-item"},UPe={class:"cfg-value font-mono"},GPe={class:"cfg-item"},YPe={class:"cfg-value font-mono"},jPe={class:"pg-card"},qPe={class:"pg-card-header"},XPe={class:"text-muted",style:{"font-size":"12px"}},ZPe={class:"pg-card-body"},JPe={class:"channel-grid"},QPe={class:"channel-head"},eDe={class:"channel-name-wrap"},tDe={class:"channel-name"},nDe={class:"channel-id font-mono text-muted"},aDe={class:"font-mono",style:{"font-size":"12px"}},rDe={class:"font-mono",style:{"font-size":"12px"}},oDe={class:"mode-switches"},iDe={class:"font-mono text-muted",style:{"font-size":"11px","word-break":"break-all"}},lDe={class:"channel-stats"},sDe={class:"stat"},uDe={class:"stat-num font-mono"},cDe={class:"stat"},dDe={class:"stat-num font-mono"},fDe={class:"stat"},vDe={class:"stat-num font-mono"},hDe={class:"stat"},pDe={class:"stat-num font-mono"},mDe={class:"channel-foot"},gDe={class:"text-muted",style:{"font-size":"11px"}},yDe={key:0,class:"empty-card pg-card"},bDe={class:"pg-card"},_De={class:"pg-card-body"},wDe={class:"step"},SDe={class:"step-num"},CDe={class:"step-title"},xDe={class:"step-desc"},kDe={__name:"Channels",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F(!1),r=F({});ye(()=>t.refreshKey,l);function o(f){var h;(h=navigator.clipboard)==null||h.writeText(f),We.success("已复制")}const i=[{title:"进入账户池",desc:"打开「账户池管理」页面,点击「新增账户」"},{title:"填写密钥",desc:"选择通道,粘贴 AppID / 私钥 / 公钥(AES-256-GCM 加密存储)"},{title:"保存生效",desc:"保存后账户自动进入内存账户池参与调度,无需重启"},{title:"开启支付模式",desc:"在通道卡片上切换 page / wap / qrcode 模式(扫码兜底)"}];async function l(){a.value=!0;try{const[f,h]=await Promise.all([yt.channels(),yt.epayInfo()]);n.value=f,r.value=h||{}}catch(f){We.error("加载失败: "+f.message)}finally{a.value=!1}}function s(f){return{alipay:"支付宝",wechat:"微信支付",paypal:"PayPal",crypto:"加密货币"}[f]||f}function u(f){return{alipay:"CircleCheck",wechat:"CircleCheck",paypal:"CircleCheck",crypto:"CircleCheck"}[f]||"CircleCheck"}function c(f){return{page:"电脑网页",wap:"手机WAP",qrcode:"扫码",address:"链上地址"}[f]||f}const d=["page","wap","qrcode"];async function v(f,h){try{const m=await yt.toggleChannelMode(f,h);We.success(`支付模式 ${c(h)} 已${m.enabled?"开启":"关闭"}`),l()}catch(m){We.error("切换失败: "+m.message)}}return mt(l),(f,h)=>{const m=be("el-tag"),g=be("el-button"),y=be("el-col"),b=be("el-row"),_=be("el-alert"),w=be("el-icon"),S=be("el-descriptions-item"),k=be("el-switch"),T=be("el-descriptions"),M=be("el-empty"),E=Gr("loading");return x(),O("div",zPe,[C("div",FPe,[C("div",HPe,[h[2]||(h[2]=C("span",{class:"section-label"},"易支付对接信息",-1)),L(m,{type:"success",size:"small",effect:"light"},{default:$(()=>[...h[1]||(h[1]=[ue("复制给 New API 等支持易支付的项目",-1)])]),_:1})]),C("div",WPe,[L(b,{gutter:12},{default:$(()=>[L(y,{span:8},{default:$(()=>[C("div",KPe,[h[4]||(h[4]=C("div",{class:"cfg-label"},"易支付网关地址(submit)",-1)),C("div",UPe,[C("code",null,Q(r.value.submit_url),1)]),L(g,{size:"small",text:"",type:"primary",onClick:h[0]||(h[0]=A=>o(r.value.submit_url))},{default:$(()=>[...h[3]||(h[3]=[ue("复制",-1)])]),_:1})])]),_:1}),L(y,{span:8},{default:$(()=>[...h[5]||(h[5]=[C("div",{class:"cfg-item"},[C("div",{class:"cfg-label"},"商户ID (pid)"),C("div",{class:"cfg-value text-muted",style:{"font-size":"12px"}},"每个上游独立配置"),C("div",{class:"cfg-value text-muted",style:{"font-size":"12px"}},"→ 见「上游对接」页")],-1)])]),_:1}),L(y,{span:8},{default:$(()=>[...h[6]||(h[6]=[C("div",{class:"cfg-item"},[C("div",{class:"cfg-label"},"商户密钥 (key)"),C("div",{class:"cfg-value text-muted",style:{"font-size":"12px"}},"每个上游独立密钥"),C("div",{class:"cfg-value text-muted",style:{"font-size":"12px"}},"→ 见「上游对接」页")],-1)])]),_:1})]),_:1}),L(b,{gutter:12,style:{"margin-top":"12px"}},{default:$(()=>[L(y,{span:12},{default:$(()=>[...h[7]||(h[7]=[C("div",{class:"cfg-item"},[C("div",{class:"cfg-label"},"notify_url(填商户/上游自己的回调,不是这里)"),C("div",{class:"cfg-value text-muted"},"例:http://127.0.0.1:3000/api/payment/notify(仅同机部署;公网对接填商户自己的公网回调域名)")],-1)])]),_:1}),L(y,{span:12},{default:$(()=>[C("div",GPe,[h[8]||(h[8]=C("div",{class:"cfg-label"},"订单查询接口",-1)),C("div",YPe,[C("code",null,Q(r.value.query_url),1)])])]),_:1})]),_:1}),L(_,{type:"success",closable:!1,class:"mt12",title:"对接地址优先展示公网固定域名(与支付页/兜底回跳一致);本机部署新 API 可用 127.0.0.1:8080(网关固定监听,与 CF 隧道无关)。公网对接请用本页展示的公网地址。"})])]),C("div",jPe,[C("div",qPe,[h[9]||(h[9]=C("span",{class:"section-label"},"支付通道",-1)),C("span",XPe,"已接入 "+Q(n.value.length)+" 个通道",1)]),C("div",ZPe,[L(_,{type:"info",closable:!1,class:"mb16",title:"通道采用可插拔架构:新增支付方式 = 实现 PaymentChannel 接口 + 注册 + 配置。以下为当前已接入的通道。"}),st((x(),O("div",JPe,[(x(!0),O(Ve,null,dt(n.value,A=>(x(),O("div",{key:A.name,class:"channel-card pg-card"},[C("div",QPe,[C("div",{class:U(["channel-logo",A.name])},[L(w,{size:18},{default:$(()=>[(x(),ne(St(u(A.name))))]),_:2},1024)],2),C("div",eDe,[C("div",tDe,Q(s(A.name)),1),C("div",nDe,Q(A.name),1)]),L(m,{type:A.enabled?"success":"danger",size:"small",effect:"light"},{default:$(()=>[ue(Q(A.enabled?"已启用":"已停用"),1)]),_:2},1032,["type"])]),L(T,{column:1,size:"small",class:"mt10"},{default:$(()=>[L(S,{label:"环境"},{default:$(()=>[L(m,{type:A.is_prod?"danger":"warning",size:"small"},{default:$(()=>[ue(Q(A.is_prod?"生产":"沙箱测试"),1)]),_:2},1032,["type"])]),_:2},1024),L(S,{label:"APPID"},{default:$(()=>[C("span",aDe,Q(A.app_id||"—"),1)]),_:2},1024),A.mch_id?(x(),ne(S,{key:0,label:"商户号"},{default:$(()=>[C("span",rDe,Q(A.mch_id),1)]),_:2},1024)):te("",!0),L(S,{label:"支持模式"},{default:$(()=>[C("div",oDe,[(x(),O(Ve,null,dt(d,I=>C("div",{key:I,class:"mode-switch-item"},[L(m,{type:A.modes_enabled&&A.modes_enabled[I]!==!1?"success":"info",size:"small",effect:"plain",style:{"margin-right":"4px"}},{default:$(()=>[ue(Q(c(I)),1)]),_:2},1032,["type"]),A.modes_enabled?(x(),ne(k,{key:0,"model-value":A.modes_enabled[I]!==!1,size:"small",onChange:P=>v(A.name,I)},null,8,["model-value","onChange"])):te("",!0)])),64))])]),_:2},1024),L(S,{label:"回调地址"},{default:$(()=>[C("span",iDe,Q(A.notify_url||"—"),1)]),_:2},1024)]),_:2},1024),A.enabled&&!A.configured?(x(),ne(_,{key:0,type:"warning",closable:!1,class:"mt10",title:"通道已启用但未配置账户",description:"请前往「账户池管理」点击「新增账户」配置密钥,保存后即刻生效,无需重启。"})):te("",!0),C("div",lDe,[C("div",sDe,[C("div",uDe,Q(A.today_orders??0),1),h[10]||(h[10]=C("div",{class:"stat-label"},"今日订单",-1))]),C("div",cDe,[C("div",dDe,"¥"+Q(((A.today_amount??0)/100).toFixed(2)),1),h[11]||(h[11]=C("div",{class:"stat-label"},"今日交易额",-1))]),C("div",fDe,[C("div",vDe,Q(A.total_orders??0),1),h[12]||(h[12]=C("div",{class:"stat-label"},"累计订单",-1))]),C("div",hDe,[C("div",pDe,"¥"+Q(((A.total_amount??0)/100).toFixed(2)),1),h[13]||(h[13]=C("div",{class:"stat-label"},"累计交易额",-1))])]),C("div",mDe,[C("span",gDe,"类型: "+Q(A.kind==="callback"?"回调驱动":"监听驱动"),1),A.enabled?(x(),ne(m,{key:0,type:"success",size:"small"},{default:$(()=>[...h[14]||(h[14]=[ue("已启用",-1)])]),_:1})):(x(),ne(m,{key:1,type:"info",size:"small"},{default:$(()=>[...h[15]||(h[15]=[ue("已禁用",-1)])]),_:1}))])]))),128)),!n.value.length&&!a.value?(x(),O("div",yDe,[L(M,{description:"暂无已接入的通道","image-size":80})])):te("",!0)])),[[E,a.value]])])]),C("div",bDe,[h[16]||(h[16]=C("div",{class:"pg-card-header"},[C("span",{class:"section-label"},"接入新通道")],-1)),C("div",_De,[L(b,{gutter:16},{default:$(()=>[(x(),O(Ve,null,dt(i,(A,I)=>L(y,{span:6,key:I},{default:$(()=>[C("div",wDe,[C("div",SDe,Q(I+1),1),C("div",CDe,Q(A.title),1),C("div",xDe,Q(A.desc),1)])]),_:2},1024)),64))]),_:1})])])])}}},TDe=fr(kDe,[["__scopeId","data-v-77316d40"]]),MDe={class:"orders-page"},EDe={class:"pg-card"},ADe={class:"pg-card-header"},IDe={class:"text-muted",style:{"font-size":"12px"}},LDe={class:"filter-bar"},PDe={key:1,class:"text-muted"},DDe={key:0},ODe={key:1,class:"text-muted"},$De={key:2,class:"text-muted"},RDe={class:"pager mt16"},NDe={class:"detail-head"},BDe={class:"detail-amount"},VDe={class:"font-mono"},zDe={class:"font-mono"},FDe={key:0},HDe={key:1,class:"text-muted"},WDe={key:2,class:"text-muted"},KDe={class:"font-mono"},UDe={key:0,class:"text-muted",style:{"font-size":"11px","margin-left":"6px"}},GDe={class:"font-mono",style:{"font-size":"12px"}},YDe={class:"font-mono",style:{"font-size":"11px"}},jDe={class:"font-mono text-muted",style:{"font-size":"11px"}},qDe={class:"text-muted",style:{"font-size":"12px"}},XDe={key:1,class:"empty-tip"},ZDe={__name:"Orders",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F(0),r=F(!1),o=F(1),i=F(null),l=nn({status:"",channel:"",keyword:"",limit:20,offset:0}),s=F(!1),u=F(null),c=F([]),d=F(!1);async function v(){r.value=!0;try{const[T,M]=i.value||[null,null],E=await yt.orders({status:l.status,channel:l.channel,keyword:l.keyword,from:T,to:M,limit:l.limit,offset:l.offset});n.value=E.list,a.value=E.total}catch(T){We.error("加载失败: "+T.message)}finally{r.value=!1}}function f(){o.value=1,l.offset=0,v()}function h(){l.status="",l.channel="",l.keyword="",i.value=null,f()}function m(T){o.value=T,l.offset=(T-1)*l.limit,v()}ye(()=>t.refreshKey,v);async function g(T){u.value=T,s.value=!0,c.value=[],d.value=!0;try{const M=await yt.orderEvents(T.id,20);c.value=M||[]}catch(M){We.warning("流水加载失败: "+M.message),c.value=[]}finally{d.value=!1}}function y(T){return{WAIT_BUYER_PAY:"待支付",TRADE_SUCCESS:"成功",TRADE_CLOSED:"已关闭",CONFIRMING:"确认中",FAILED:"失败"}[T]||T}function b(T){return{WAIT_BUYER_PAY:"warning",TRADE_SUCCESS:"success",TRADE_CLOSED:"info",CONFIRMING:"warning",FAILED:"danger"}[T]||"info"}function _(T){return{created:"订单创建",paid:"支付成功",closed:"订单关闭",notified:"通知商户",failed:"失败"}[T]||T}function w(T){return{created:"info",paid:"success",closed:"info",notified:"primary",failed:"danger"}[T]||"info"}function S(T){return T?new Date(T).toLocaleString("zh-CN",{hour12:!1}):"—"}mt(()=>{const T=window.location.hash;if(T.includes("keyword=")){const M=T.match(/keyword=([^&]*)/);M&&M[1]&&(l.keyword=decodeURIComponent(M[1]))}v()});async function k(T){try{await navigator.clipboard.writeText(T),We.success("已复制")}catch(M){We.error("复制失败: "+M.message)}}return(T,M)=>{const E=be("Search"),A=be("el-icon"),I=be("el-input"),P=be("el-option"),R=be("el-select"),G=be("el-date-picker"),K=be("el-button"),H=be("el-table-column"),z=be("el-tag"),B=be("el-table"),W=be("el-pagination"),N=be("el-descriptions-item"),V=be("el-descriptions"),Y=be("el-timeline-item"),X=be("el-timeline"),ee=be("Document"),re=be("el-drawer"),ie=Gr("loading");return x(),O("div",MDe,[C("div",EDe,[C("div",ADe,[M[6]||(M[6]=C("span",{class:"section-label"},"订单管理",-1)),C("span",IDe,"共 "+Q(a.value)+" 笔",1)]),C("div",LDe,[L(I,{modelValue:l.keyword,"onUpdate:modelValue":M[0]||(M[0]=le=>l.keyword=le),placeholder:"搜索订单号 / 网关单号 / 交易号 / 商品",clearable:"",style:{width:"260px"},onKeyup:kn(f,["enter"]),onClear:f},{prefix:$(()=>[L(A,null,{default:$(()=>[L(E)]),_:1})]),_:1},8,["modelValue"]),L(R,{modelValue:l.status,"onUpdate:modelValue":M[1]||(M[1]=le=>l.status=le),placeholder:"全部状态",clearable:"",style:{width:"130px"},onChange:f},{default:$(()=>[L(P,{label:"待支付",value:"WAIT_BUYER_PAY"}),L(P,{label:"成功",value:"TRADE_SUCCESS"}),L(P,{label:"已关闭",value:"TRADE_CLOSED"}),L(P,{label:"失败",value:"FAILED"})]),_:1},8,["modelValue"]),L(R,{modelValue:l.channel,"onUpdate:modelValue":M[2]||(M[2]=le=>l.channel=le),placeholder:"全部通道",clearable:"",style:{width:"120px"},onChange:f},{default:$(()=>[L(P,{label:"支付宝",value:"alipay"})]),_:1},8,["modelValue"]),L(G,{modelValue:i.value,"onUpdate:modelValue":M[3]||(M[3]=le=>i.value=le),type:"daterange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期",style:{width:"260px"},"value-format":"YYYY-MM-DD",onChange:f},null,8,["modelValue"]),L(K,{type:"primary",onClick:f},{default:$(()=>[L(A,null,{default:$(()=>[L(E)]),_:1}),M[7]||(M[7]=ue(" 查询 ",-1))]),_:1}),L(K,{onClick:h},{default:$(()=>[...M[8]||(M[8]=[ue("重置",-1)])]),_:1})]),st((x(),ne(B,{data:n.value,size:"small",onRowClick:g,class:"mt10"},{default:$(()=>[L(H,{prop:"out_trade_no",label:"商户订单号","min-width":"150","show-overflow-tooltip":""}),L(H,{prop:"gateway_no",label:"网关单号","min-width":"150","show-overflow-tooltip":""}),L(H,{label:"上游",width:"130"},{default:$(({row:le})=>[le.upstream_name?(x(),ne(z,{key:0,size:"small",type:"primary",effect:"light"},{default:$(()=>[ue(Q(le.upstream_name),1)]),_:2},1024)):(x(),O("span",PDe,"—"))]),_:1}),L(H,{prop:"channel",label:"通道",width:"80"}),L(H,{label:"账户",width:"110"},{default:$(({row:le})=>[le.account_name?(x(),O("span",DDe,Q(le.account_name),1)):le.account_id?(x(),O("span",ODe,"#"+Q(le.account_id),1)):(x(),O("span",$De,"—"))]),_:1}),L(H,{prop:"subject",label:"商品","min-width":"100","show-overflow-tooltip":""}),L(H,{label:"金额",width:"95"},{default:$(({row:le})=>[C("b",null,"¥"+Q((le.amount_minor/100).toFixed(2)),1)]),_:1}),L(H,{label:"状态",width:"110"},{default:$(({row:le})=>[L(z,{type:b(le.status),size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(y(le.status)),1)]),_:2},1032,["type"])]),_:1}),L(H,{prop:"channel_tx_id",label:"通道交易号","min-width":"150","show-overflow-tooltip":""}),L(H,{label:"支付时间",width:"160"},{default:$(({row:le})=>[ue(Q(S(le.paid_at)),1)]),_:1}),L(H,{label:"创建时间",width:"160"},{default:$(({row:le})=>[ue(Q(S(le.created_at)),1)]),_:1}),L(H,{label:"",width:"70",fixed:"right"},{default:$(({row:le})=>[L(K,{size:"small",text:"",type:"primary",onClick:at(ve=>g(le),["stop"])},{default:$(()=>[...M[9]||(M[9]=[ue("详情",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[ie,r.value]]),C("div",RDe,[L(W,{background:"",layout:"total, prev, pager, next, jumper",total:a.value,"page-size":l.limit,"current-page":o.value,onCurrentChange:m},null,8,["total","page-size","current-page"])])]),L(re,{modelValue:s.value,"onUpdate:modelValue":M[5]||(M[5]=le=>s.value=le),title:"订单详情",size:"500px",class:"order-drawer"},{default:$(()=>[u.value?(x(),O(Ve,{key:0},[C("div",NDe,[C("div",BDe,"¥"+Q((u.value.amount_minor/100).toFixed(2)),1),L(z,{type:b(u.value.status),size:"small",effect:"dark"},{default:$(()=>[ue(Q(y(u.value.status)),1)]),_:1},8,["type"])]),L(V,{column:1,border:"",size:"small",class:"mt16"},{default:$(()=>[L(N,{label:"商户订单号"},{default:$(()=>[C("span",VDe,Q(u.value.out_trade_no),1)]),_:1}),L(N,{label:"网关单号"},{default:$(()=>[C("span",zDe,Q(u.value.gateway_no),1)]),_:1}),L(N,{label:"通道"},{default:$(()=>[ue(Q(u.value.channel),1)]),_:1}),L(N,{label:"处理账户"},{default:$(()=>[u.value.account_name?(x(),O("span",FDe,Q(u.value.account_name),1)):u.value.account_id?(x(),O("span",HDe,"#"+Q(u.value.account_id),1)):(x(),O("span",WDe,"—"))]),_:1}),L(N,{label:"通道交易号"},{default:$(()=>[C("span",KDe,Q(u.value.channel_tx_id||"—"),1)]),_:1}),L(N,{label:"上游"},{default:$(()=>[C("span",null,Q(u.value.upstream_name||"—"),1),u.value.pid?(x(),O("span",UDe,"(pid: "+Q(u.value.pid)+")",1)):te("",!0)]),_:1}),L(N,{label:"上游请求ID"},{default:$(()=>[C("span",GDe,Q(u.value.attach||"—"),1),u.value.attach?(x(),ne(K,{key:0,size:"small",text:"",type:"primary",onClick:M[4]||(M[4]=le=>k(u.value.attach))},{default:$(()=>[...M[10]||(M[10]=[ue("复制",-1)])]),_:1})):te("",!0)]),_:1}),L(N,{label:"商品"},{default:$(()=>[ue(Q(u.value.subject),1)]),_:1}),L(N,{label:"付款方"},{default:$(()=>[ue(Q(u.value.payer||"—"),1)]),_:1}),L(N,{label:"回调地址"},{default:$(()=>[C("span",YDe,Q(u.value.notify_url||"—"),1)]),_:1}),L(N,{label:"通知状态"},{default:$(()=>[ue(Q(u.value.notified?`已通知 ${u.value.notify_count} 次`:"未通知"),1)]),_:1}),L(N,{label:"创建时间"},{default:$(()=>[ue(Q(S(u.value.created_at)),1)]),_:1}),L(N,{label:"支付时间"},{default:$(()=>[ue(Q(S(u.value.paid_at)),1)]),_:1})]),_:1}),M[13]||(M[13]=C("h4",{class:"section-title"},"资金流水",-1)),st((x(),O("div",null,[c.value.length?(x(),ne(X,{key:0},{default:$(()=>[(x(!0),O(Ve,null,dt(c.value,le=>(x(),ne(Y,{key:le.id,timestamp:S(le.created_at),type:w(le.event_type),placement:"top"},{default:$(()=>[C("b",null,Q(_(le.event_type)),1),C("div",jDe,Q(le.channel_tx_id||""),1),C("div",qDe,"金额 ¥"+Q((le.amount_minor/100).toFixed(2)),1)]),_:2},1032,["timestamp","type"]))),128))]),_:1})):d.value?te("",!0):(x(),O("div",XDe,[L(A,{size:28,color:"var(--text-muted)"},{default:$(()=>[L(ee)]),_:1}),M[11]||(M[11]=C("div",{class:"empty-text"},"暂无资金流水",-1)),M[12]||(M[12]=C("div",{class:"empty-sub"},"该订单可能为历史数据,或支付流程未产生事件记录",-1))]))])),[[ie,d.value]])],64)):te("",!0)]),_:1},8,["modelValue"])])}}},JDe=fr(ZDe,[["__scopeId","data-v-490d8a35"]]),QDe={class:"notify-page"},eOe={class:"stat-grid"},tOe={class:"stat-card pg-card"},nOe={class:"stat-icon success"},aOe={class:"stat-num"},rOe={class:"stat-card pg-card"},oOe={class:"stat-icon warn"},iOe={class:"stat-num"},lOe={class:"stat-card pg-card"},sOe={class:"stat-icon danger"},uOe={class:"stat-num"},cOe={class:"stat-card pg-card"},dOe={class:"stat-icon info"},fOe={class:"stat-num"},vOe={class:"pg-card"},hOe={class:"pg-card-header"},pOe={style:{display:"flex",gap:"8px"}},mOe={key:1,class:"text-muted"},gOe={class:"payload-head"},yOe={class:"font-mono"},bOe={class:"payload"},_Oe={__name:"NotifyTasks",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F({}),r=F(""),o=F(!1),i=F(!1),l=F(""),s=F(null);async function u(){o.value=!0;try{const[m,g]=await Promise.all([yt.notifyTasks({status:r.value,limit:50}),yt.notifyStats()]);n.value=m,a.value=g}catch(m){We.error("加载失败: "+m.message)}finally{o.value=!1}}function c(m){s.value=m;try{l.value=JSON.stringify(JSON.parse(m.payload),null,2)}catch{l.value=m.payload}i.value=!0}async function d(){var m;if(!((m=s.value)!=null&&m.id)){We.warning("未选中任务");return}try{await yt.retryDeadNotify(s.value.id),We.success("已按最新回调地址重新入队")}catch(g){We.error("重试失败: "+g.message)}finally{i.value=!1,u()}}ye(()=>t.refreshKey,u);function v(m){return{pending:"待处理",success:"成功",dead:"死信"}[m]||m}function f(m){return{pending:"warning",success:"success",dead:"danger"}[m]||"info"}function h(m){return m?new Date(m).toLocaleString("zh-CN",{hour12:!1}):"—"}return mt(u),(m,g)=>{const y=be("CircleCheck"),b=be("el-icon"),_=be("Clock"),w=be("Warning"),S=be("List"),k=be("el-alert"),T=be("el-option"),M=be("el-select"),E=be("Refresh"),A=be("el-button"),I=be("el-table-column"),P=be("el-tag"),R=be("el-table"),G=be("el-dialog"),K=Gr("loading");return x(),O("div",QDe,[C("div",eOe,[C("div",tOe,[C("div",nOe,[L(b,{size:20},{default:$(()=>[L(y)]),_:1})]),C("div",null,[C("div",aOe,Q(a.value.success??0),1),g[3]||(g[3]=C("div",{class:"stat-label"},"成功投递",-1))])]),C("div",rOe,[C("div",oOe,[L(b,{size:20},{default:$(()=>[L(_)]),_:1})]),C("div",null,[C("div",iOe,Q(a.value.pending??0),1),g[4]||(g[4]=C("div",{class:"stat-label"},"待重试",-1))])]),C("div",lOe,[C("div",sOe,[L(b,{size:20},{default:$(()=>[L(w)]),_:1})]),C("div",null,[C("div",uOe,Q(a.value.dead??0),1),g[5]||(g[5]=C("div",{class:"stat-label"},"死信",-1))])]),C("div",cOe,[C("div",dOe,[L(b,{size:20},{default:$(()=>[L(S)]),_:1})]),C("div",null,[C("div",fOe,Q(a.value.total??0),1),g[6]||(g[6]=C("div",{class:"stat-label"},"任务总数",-1))])])]),(a.value.dead??0)>0?(x(),ne(k,{key:0,type:"error",closable:!1,class:"mt12",title:"检测到死信任务!商户回调多次失败,请检查商户服务器 notify_url 可用性。"})):te("",!0),C("div",vOe,[C("div",hOe,[g[8]||(g[8]=C("span",{class:"section-label"},"回调任务队列",-1)),C("div",pOe,[L(M,{modelValue:r.value,"onUpdate:modelValue":g[0]||(g[0]=H=>r.value=H),placeholder:"全部状态",clearable:"",size:"small",style:{width:"120px"},onChange:u},{default:$(()=>[L(T,{label:"待处理",value:"pending"}),L(T,{label:"成功",value:"success"}),L(T,{label:"死信",value:"dead"})]),_:1},8,["modelValue"]),L(A,{size:"small",type:"primary",onClick:u},{default:$(()=>[L(b,null,{default:$(()=>[L(E)]),_:1}),g[7]||(g[7]=ue(" 刷新 ",-1))]),_:1})])]),st((x(),ne(R,{data:n.value,size:"small","empty-text":"没有找到回调任务,试试清除状态筛选"},{default:$(()=>[L(I,{prop:"id",label:"ID",width:"70"}),L(I,{prop:"out_trade_no",label:"商户订单号","min-width":"150","show-overflow-tooltip":""}),L(I,{prop:"subject",label:"商品","min-width":"90","show-overflow-tooltip":""}),L(I,{label:"金额",width:"90"},{default:$(({row:H})=>[C("b",null,"¥"+Q((H.amount_minor/100).toFixed(2)),1)]),_:1}),L(I,{label:"状态",width:"90"},{default:$(({row:H})=>[L(P,{type:f(H.status),size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(v(H.status)),1)]),_:2},1032,["type"])]),_:1}),L(I,{prop:"retry_count",label:"重试次数",width:"90"},{default:$(({row:H})=>[H.retry_count>0?(x(),ne(P,{key:0,type:"warning",size:"small"},{default:$(()=>[ue(Q(H.retry_count),1)]),_:2},1024)):(x(),O("span",mOe,"0"))]),_:1}),L(I,{label:"下次重试",width:"170"},{default:$(({row:H})=>[ue(Q(h(H.next_retry_at)),1)]),_:1}),L(I,{label:"创建时间",width:"170"},{default:$(({row:H})=>[ue(Q(h(H.created_at)),1)]),_:1}),L(I,{label:"载荷",width:"80"},{default:$(({row:H})=>[L(A,{size:"small",text:"",type:"primary",onClick:z=>c(H)},{default:$(()=>[...g[9]||(g[9]=[ue("查看",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[K,o.value]])]),L(G,{modelValue:i.value,"onUpdate:modelValue":g[2]||(g[2]=H=>i.value=H),title:"回调载荷",width:"620px"},{footer:$(()=>{var H;return[L(A,{onClick:g[1]||(g[1]=z=>i.value=!1)},{default:$(()=>[...g[10]||(g[10]=[ue("关闭",-1)])]),_:1}),L(A,{type:"primary",disabled:((H=s.value)==null?void 0:H.status)==="success",onClick:d},{default:$(()=>[...g[11]||(g[11]=[ue("手动重试",-1)])]),_:1},8,["disabled"])]}),default:$(()=>{var H,z;return[C("div",gOe,[C("span",yOe,Q((H=s.value)==null?void 0:H.out_trade_no),1),L(P,{type:f((z=s.value)==null?void 0:z.status),size:"small",effect:"light"},{default:$(()=>{var B;return[ue(Q(v((B=s.value)==null?void 0:B.status)),1)]}),_:1},8,["type"])]),C("pre",bOe,Q(l.value),1)]}),_:1},8,["modelValue"])])}}},wOe=fr(_Oe,[["__scopeId","data-v-52866146"]]),SOe={class:"logs-page"},COe={class:"stat-grid"},xOe={class:"stat-card pg-card"},kOe={class:"stat-icon danger"},TOe={class:"stat-num"},MOe={class:"stat-card pg-card"},EOe={class:"stat-icon warn"},AOe={class:"stat-num"},IOe={class:"stat-card pg-card"},LOe={class:"stat-icon info"},POe={class:"stat-num"},DOe={class:"stat-card pg-card"},OOe={class:"stat-icon primary"},$Oe={class:"stat-num"},ROe={class:"pg-card"},NOe={class:"pg-card-header"},BOe={style:{display:"flex",gap:"8px","flex-wrap":"wrap"}},VOe={class:"log-stream"},zOe={class:"log-time font-mono"},FOe={class:"log-scope"},HOe={class:"log-msg"},WOe={key:1,class:"empty-tip"},KOe={class:"pager mt16"},UOe={class:"font-mono"},GOe={class:"fields"},YOe={__name:"Logs",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F(0),r=F(!1),o=F(""),i=F(""),l=F(1),s=F(!1),u=F(null),c=F({error:0,warn:0,info:0}),d=["main","epay","order","alipay","notify","ledger","tunnel","admin","logger"];async function v(){r.value=!0;try{const y=await yt.logs({level:o.value,scope:i.value,limit:50,offset:(l.value-1)*50});n.value=y&&y.list||[],a.value=y&&y.total||0}catch(y){ElMessage.error("加载失败: "+y.message),n.value=[],a.value=0}finally{r.value=!1}try{const[y,b,_]=await Promise.allSettled([yt.logs({level:"ERROR",limit:1}).then(w=>w.total),yt.logs({level:"WARN",limit:1}).then(w=>w.total),yt.logs({level:"INFO",limit:1}).then(w=>w.total)]);c.value={error:y.status==="fulfilled"?y.value:0,warn:b.status==="fulfilled"?b.value:0,info:_.status==="fulfilled"?_.value:0}}catch{}}function f(){l.value=1,v()}function h(y){l.value=y,v()}ye(()=>t.refreshKey,v);function m(y){u.value=y,s.value=!0}function g(y){return y?new Date(y).toLocaleString("zh-CN",{hour12:!1}):"—"}return mt(v),(y,b)=>{const _=be("CircleClose"),w=be("el-icon"),S=be("Warning"),k=be("InfoFilled"),T=be("Document"),M=be("el-option"),E=be("el-select"),A=be("Refresh"),I=be("el-button"),P=be("el-pagination"),R=be("el-descriptions-item"),G=be("el-descriptions"),K=be("el-dialog"),H=Gr("loading");return x(),O("div",SOe,[C("div",COe,[C("div",xOe,[C("div",kOe,[L(w,{size:18},{default:$(()=>[L(_)]),_:1})]),C("div",null,[C("div",TOe,Q(c.value.error),1),b[3]||(b[3]=C("div",{class:"stat-label"},"错误",-1))])]),C("div",MOe,[C("div",EOe,[L(w,{size:18},{default:$(()=>[L(S)]),_:1})]),C("div",null,[C("div",AOe,Q(c.value.warn),1),b[4]||(b[4]=C("div",{class:"stat-label"},"警告",-1))])]),C("div",IOe,[C("div",LOe,[L(w,{size:18},{default:$(()=>[L(k)]),_:1})]),C("div",null,[C("div",POe,Q(c.value.info),1),b[5]||(b[5]=C("div",{class:"stat-label"},"信息",-1))])]),C("div",DOe,[C("div",OOe,[L(w,{size:18},{default:$(()=>[L(T)]),_:1})]),C("div",null,[C("div",$Oe,Q(a.value),1),b[6]||(b[6]=C("div",{class:"stat-label"},"日志总数",-1))])])]),C("div",ROe,[C("div",NOe,[b[8]||(b[8]=C("span",{class:"section-label"},"日志流",-1)),C("div",BOe,[L(E,{modelValue:o.value,"onUpdate:modelValue":b[0]||(b[0]=z=>o.value=z),placeholder:"全部级别",clearable:"",size:"small",style:{width:"110px"},onChange:f},{default:$(()=>[L(M,{label:"错误",value:"ERROR"}),L(M,{label:"警告",value:"WARN"}),L(M,{label:"信息",value:"INFO"}),L(M,{label:"调试",value:"DEBUG"})]),_:1},8,["modelValue"]),L(E,{modelValue:i.value,"onUpdate:modelValue":b[1]||(b[1]=z=>i.value=z),placeholder:"全部模块",clearable:"",size:"small",style:{width:"120px"},onChange:f},{default:$(()=>[(x(),O(Ve,null,dt(d,z=>L(M,{key:z,label:z,value:z},null,8,["label","value"])),64))]),_:1},8,["modelValue"]),L(I,{size:"small",type:"primary",onClick:f},{default:$(()=>[L(w,null,{default:$(()=>[L(A)]),_:1}),b[7]||(b[7]=ue(" 刷新 ",-1))]),_:1})])]),st((x(),O("div",VOe,[n.value.length?(x(!0),O(Ve,{key:0},dt(n.value,z=>(x(),O("div",{key:z.id,class:U(["log-line","lv-"+z.level.toLowerCase()])},[C("span",zOe,Q(g(z.created_at)),1),C("span",{class:U(["log-level","lv-"+z.level.toLowerCase()])},Q(z.level),3),C("span",FOe,Q(z.scope),1),C("span",HOe,Q(z.message),1),z.fields||z.trace_id?(x(),ne(I,{key:0,size:"small",text:"",type:"primary",onClick:B=>m(z)},{default:$(()=>[...b[9]||(b[9]=[ue("详情",-1)])]),_:1},8,["onClick"])):te("",!0)],2))),128)):r.value?te("",!0):(x(),O("div",WOe,[L(w,{size:32,color:"var(--text-muted)"},{default:$(()=>[L(T)]),_:1}),b[10]||(b[10]=C("div",{class:"empty-text"},"没有找到日志",-1)),b[11]||(b[11]=C("div",{class:"empty-sub"},"试试清除级别/模块筛选,或等待新日志产生",-1))]))])),[[H,r.value]]),C("div",KOe,[L(P,{background:"",layout:"total, prev, pager, next",total:a.value,"page-size":50,"current-page":l.value,onCurrentChange:h},null,8,["total","current-page"])])]),L(K,{modelValue:s.value,"onUpdate:modelValue":b[2]||(b[2]=z=>s.value=z),title:"日志详情",width:"580px"},{default:$(()=>[u.value?(x(),ne(G,{key:0,column:1,border:"",size:"small"},{default:$(()=>[L(R,{label:"ID"},{default:$(()=>[ue(Q(u.value.id),1)]),_:1}),L(R,{label:"级别"},{default:$(()=>[ue(Q(u.value.level),1)]),_:1}),L(R,{label:"模块"},{default:$(()=>[ue(Q(u.value.scope),1)]),_:1}),L(R,{label:"消息"},{default:$(()=>[ue(Q(u.value.message),1)]),_:1}),L(R,{label:"TraceID"},{default:$(()=>[C("span",UOe,Q(u.value.trace_id||"—"),1)]),_:1}),L(R,{label:"时间"},{default:$(()=>[ue(Q(g(u.value.created_at)),1)]),_:1}),u.value.fields?(x(),ne(R,{key:0,label:"字段"},{default:$(()=>[C("pre",GOe,Q(u.value.fields),1)]),_:1})):te("",!0)]),_:1})):te("",!0)]),_:1},8,["modelValue"])])}}},jOe=fr(YOe,[["__scopeId","data-v-d4a5eb1d"]]),qOe={class:"ledger-page"},XOe={class:"action-bar pg-card"},ZOe={class:"tag-muted"},JOe={class:"pg-card"},QOe={class:"pg-card-header"},e$e={style:{display:"flex","align-items":"center",gap:"10px"}},t$e={class:"pg-card-body"},n$e={class:"chain"},a$e={class:"chain-badge"},r$e={class:"chain-body"},o$e={class:"chain-row"},i$e=["onClick"],l$e={key:1,class:"font-muted"},s$e={class:"chain-row"},u$e={class:"font-mono hash muted"},c$e={class:"chain-row"},d$e={class:"font-mono hash muted"},f$e={class:"chain-row"},v$e={class:"font-muted"},h$e={key:0,class:"chain-link"},p$e={class:"pg-card"},m$e={class:"pg-card-header"},g$e={style:{display:"flex","align-items":"center",gap:"10px"}},y$e={class:"pg-card-body"},b$e={class:"snapshot-grid"},_$e=["onClick"],w$e={class:"snapshot-date"},S$e={class:"snapshot-body"},C$e={class:"sn-row"},x$e={class:"font-mono hash muted"},k$e={class:"sn-row"},T$e={class:"font-mono hash muted"},M$e={class:"sn-row"},E$e={class:"font-muted"},A$e={key:0,class:"verify-result"},I$e={class:"verify-status-text"},L$e={key:0,class:"verify-errors mt16"},P$e={class:"verify-errors-title"},D$e={key:1,class:"verify-loading"},O$e={key:0,class:"snap-detail"},$$e={class:"font-mono",style:{"font-size":"12px","word-break":"break-all"}},R$e={class:"font-mono",style:{"font-size":"12px","word-break":"break-all",color:"var(--text-muted)"}},N$e={class:"mt16",style:{display:"flex",gap:"12px","justify-content":"center"}},B$e={class:"pubkey-container"},V$e={class:"pubkey-block"},z$e={__name:"Ledger",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F(0),r=F(!1),o=F(!1),i=F(null),l=F([]),s=F(!1),u=F(!1),c=F(!1),d=F(null),v=F(null),f=F("entries"),h=F(!1),m=F(null),g=F(!1),y=F(null),b=F(!1),_=F(""),w=F(!1),S=F(!1);async function k(){r.value=!0;try{const B=await yt.ledger({limit:30});n.value=B.list,a.value=B.total}catch(B){We.error("加载失败: "+B.message)}finally{r.value=!1}}async function T(){s.value=!0;try{const B=await yt.ledgerSnapshots({limit:30});l.value=B||[]}catch(B){We.error("加载快照失败: "+B.message)}finally{s.value=!1}}async function M(){u.value=!0,c.value=!0,d.value=null;try{const B=await yt.ledgerVerify();d.value=B,v.value=B}catch(B){d.value={valid:!1,total_entries:0,errors:["请求失败: "+B.message],snapshot_ok:!1,checked_at:new Date().toISOString()},We.error("校验失败: "+B.message)}finally{u.value=!1}}function E(B){m.value=B,h.value=!0,y.value=null}async function A(B){g.value=!0;try{const W=await yt.ledger({limit:1});W.list&&W.list.length>0?y.value=W.list[0].entry_hash===B.last_hash:y.value=!1}catch(W){We.error("核对失败: "+W.message),y.value=!1}finally{g.value=!1}}async function I(){b.value=!0;try{const B=await yt.ledgerPublicKey();_.value=B.public_key||"未获取到公钥"}catch(B){_.value="获取公钥失败: "+B.message}}async function P(){w.value=!0;try{await yt.ledgerExport(),We.success("账本已导出")}catch(B){We.error("导出失败: "+B.message)}finally{w.value=!1}}function R(B){window.location.hash="#/orders?keyword="+B,window.location.reload()}function G(B){navigator.clipboard.writeText(B).then(()=>We.success("已复制")).catch(()=>We.error("复制失败"))}async function K(){S.value=!0;try{await yt.createSnapshot(),We.success("快照创建成功"),f.value="snapshots",await T()}catch(B){We.error("创建快照失败: "+B.message)}finally{S.value=!1}}function H(B){i.value=B,o.value=!0}ye(()=>t.refreshKey,()=>{k(),T()});function z(B){return B?new Date(B).toLocaleString("zh-CN",{hour12:!1}):"—"}return mt(()=>{k(),T()}),(B,W)=>{const N=be("el-icon"),V=be("el-button"),Y=be("el-tag"),X=be("el-alert"),ee=be("el-empty"),re=be("el-tab-pane"),ie=be("el-tabs"),le=be("el-descriptions-item"),ve=be("el-descriptions"),he=be("el-dialog"),J=Gr("loading");return x(),O("div",qOe,[C("div",XOe,[L(V,{type:"primary",loading:u.value,onClick:M,plain:""},{default:$(()=>[L(N,null,{default:$(()=>[L(p(Ao))]),_:1}),W[7]||(W[7]=ue(" 校验账本 ",-1))]),_:1},8,["loading"]),L(V,{loading:S.value,onClick:K,plain:""},{default:$(()=>[L(N,null,{default:$(()=>[L(p(kI))]),_:1}),W[8]||(W[8]=ue(" 创建快照 ",-1))]),_:1},8,["loading"]),L(V,{onClick:I,plain:""},{default:$(()=>[L(N,null,{default:$(()=>[L(p(PI))]),_:1}),W[9]||(W[9]=ue(" 账本公钥 ",-1))]),_:1}),L(V,{loading:w.value,onClick:P,plain:""},{default:$(()=>[L(N,null,{default:$(()=>[L(p(AI))]),_:1}),W[10]||(W[10]=ue(" 导出CSV ",-1))]),_:1},8,["loading"]),v.value?(x(),ne(Y,{key:0,type:v.value.valid?"success":"danger",size:"small",effect:"plain",class:"verify-tag"},{default:$(()=>[ue(Q(v.value.valid?"上次校验通过":"上次校验失败")+" ",1),C("span",ZOe,"("+Q(v.value.checked_at?z(v.value.checked_at):"—")+")",1)]),_:1},8,["type"])):te("",!0)]),L(ie,{modelValue:f.value,"onUpdate:modelValue":W[0]||(W[0]=Z=>f.value=Z),class:"mt12"},{default:$(()=>[L(re,{label:"账本条目",name:"entries"},{default:$(()=>[C("div",JOe,[C("div",QOe,[W[12]||(W[12]=C("span",{class:"section-label"},"审计账本 · 哈希链",-1)),C("div",e$e,[L(Y,{type:"success",size:"small",effect:"light"},{default:$(()=>[ue("共 "+Q(a.value)+" 条",1)]),_:1}),L(V,{size:"small",onClick:k},{default:$(()=>[L(N,null,{default:$(()=>[L(p(g4))]),_:1}),W[11]||(W[11]=ue(" 刷新",-1))]),_:1})])]),C("div",t$e,[L(X,{type:"info",closable:!1,class:"mb16",title:"账本为哈希链结构:每条记录包含上一记录哈希 + ECDSA 签名。任何历史修改都会导致链条断裂。"}),st((x(),O("div",n$e,[(x(!0),O(Ve,null,dt(n.value,(Z,j)=>(x(),O(Ve,{key:Z.id},[C("div",{class:U(["chain-node",{head:j===0}])},[C("div",a$e,"#"+Q(Z.id),1),C("div",r$e,[C("div",o$e,[W[13]||(W[13]=C("span",{class:"chain-label"},"事件ID",-1)),Z.event_id?(x(),O("span",{key:0,class:"font-mono event-link",onClick:oe=>R(Z.event_id)},Q(Z.event_id),9,i$e)):(x(),O("span",l$e,"—"))]),C("div",s$e,[W[14]||(W[14]=C("span",{class:"chain-label"},"条目哈希",-1)),C("span",u$e,Q(Z.entry_hash),1)]),C("div",c$e,[W[15]||(W[15]=C("span",{class:"chain-label"},"前置哈希",-1)),C("span",d$e,Q(Z.prev_hash||"—(创世)"),1)]),C("div",f$e,[W[16]||(W[16]=C("span",{class:"chain-label"},"时间",-1)),C("span",v$e,Q(z(Z.created_at)),1)])]),L(V,{size:"small",text:"",type:"primary",onClick:oe=>H(Z)},{default:$(()=>[...W[17]||(W[17]=[ue("完整",-1)])]),_:1},8,["onClick"])],2),j[L(p(DI))]),_:1})])):te("",!0)],64))),128)),!n.value.length&&!r.value?(x(),ne(ee,{key:0,description:"账本为空","image-size":60})):te("",!0)])),[[J,r.value]])])])]),_:1}),L(re,{label:"快照管理",name:"snapshots"},{default:$(()=>[C("div",p$e,[C("div",m$e,[W[19]||(W[19]=C("span",{class:"section-label"},"每日快照",-1)),C("div",g$e,[L(Y,{size:"small",effect:"light"},{default:$(()=>[ue("共 "+Q(l.value.length)+" 条",1)]),_:1}),L(V,{size:"small",onClick:T},{default:$(()=>[L(N,null,{default:$(()=>[L(p(g4))]),_:1}),W[18]||(W[18]=ue(" 刷新",-1))]),_:1})])]),C("div",y$e,[L(X,{type:"info",closable:!1,class:"mb16",title:"快照是对账本链尾哈希的签名记录,用于锚定截至某日的账本状态。"}),st((x(),O("div",b$e,[(x(!0),O(Ve,null,dt(l.value,Z=>(x(),O("div",{key:Z.id,class:"snapshot-card",onClick:j=>E(Z),style:{cursor:"pointer"}},[C("div",w$e,Q(Z.snapshot_date),1),C("div",S$e,[C("div",C$e,[W[20]||(W[20]=C("span",{class:"sn-label"},"链尾哈希",-1)),C("span",x$e,Q(Z.last_hash),1)]),C("div",k$e,[W[21]||(W[21]=C("span",{class:"sn-label"},"签名",-1)),C("span",T$e,Q(Z.signature),1)]),C("div",M$e,[W[22]||(W[22]=C("span",{class:"sn-label"},"时间",-1)),C("span",E$e,Q(z(Z.created_at)),1)])]),L(N,{class:"detail-icon",color:"var(--accent)"},{default:$(()=>[L(p(U6))]),_:1})],8,_$e))),128)),!l.value.length&&!s.value?(x(),ne(ee,{key:0,description:"暂无快照","image-size":60})):te("",!0)])),[[J,s.value]])])])]),_:1})]),_:1},8,["modelValue"]),L(he,{modelValue:c.value,"onUpdate:modelValue":W[1]||(W[1]=Z=>c.value=Z),title:"账本校验结果",width:"600px","close-on-click-modal":!1},{default:$(()=>[d.value?(x(),O("div",A$e,[C("div",{class:U(["verify-status",d.value.valid?"pass":"fail"])},[L(N,{size:48},{default:$(()=>[d.value.valid?(x(),ne(p(Ao),{key:0})):(x(),ne(p($s),{key:1}))]),_:1}),C("div",I$e,Q(d.value.valid?"账本完整":"账本校验失败"),1)],2),L(ve,{column:2,border:"",size:"small",class:"mt16"},{default:$(()=>[L(le,{label:"总条目数"},{default:$(()=>[ue(Q(d.value.total_entries),1)]),_:1}),L(le,{label:"快照锚点"},{default:$(()=>[L(Y,{type:d.value.snapshot_ok?"success":"danger",size:"small"},{default:$(()=>[ue(Q(d.value.snapshot_ok?"匹配":"不匹配"),1)]),_:1},8,["type"])]),_:1}),L(le,{label:"耗时"},{default:$(()=>[ue(Q(d.value.duration||"—"),1)]),_:1}),L(le,{label:"检查时间"},{default:$(()=>[ue(Q(z(d.value.checked_at)),1)]),_:1})]),_:1}),d.value.errors&&d.value.errors.length?(x(),O("div",L$e,[C("div",P$e,"错误详情("+Q(d.value.errors.length)+" 处)",1),(x(!0),O(Ve,null,dt(d.value.errors,(Z,j)=>(x(),O("div",{key:j,class:"verify-error-item"},[L(N,{color:"var(--danger)"},{default:$(()=>[L(p(fc))]),_:1}),C("span",null,Q(Z),1)]))),128))])):te("",!0)])):st((x(),O("div",D$e,[...W[23]||(W[23]=[C("span",null,"正在校验账本...",-1)])])),[[J,u.value]])]),_:1},8,["modelValue"]),L(he,{modelValue:h.value,"onUpdate:modelValue":W[4]||(W[4]=Z=>h.value=Z),title:"快照详情",width:"600px"},{default:$(()=>[m.value?(x(),O("div",O$e,[L(ve,{column:1,border:"",size:"small"},{default:$(()=>[L(le,{label:"快照日期"},{default:$(()=>[ue(Q(m.value.snapshot_date),1)]),_:1}),L(le,{label:"链尾哈希"},{default:$(()=>[C("span",$$e,Q(m.value.last_hash),1)]),_:1}),L(le,{label:"签名"},{default:$(()=>[C("span",R$e,Q(m.value.signature),1)]),_:1}),L(le,{label:"创建时间"},{default:$(()=>[ue(Q(z(m.value.created_at)),1)]),_:1})]),_:1}),C("div",N$e,[L(V,{onClick:W[2]||(W[2]=Z=>A(m.value)),loading:g.value,type:"primary"},{default:$(()=>[L(N,null,{default:$(()=>[L(p(Ao))]),_:1}),W[24]||(W[24]=ue(" 核对锚点 ",-1))]),_:1},8,["loading"]),L(V,{onClick:W[3]||(W[3]=Z=>G(JSON.stringify(m.value,null,2)))},{default:$(()=>[...W[25]||(W[25]=[ue("复制快照信息",-1)])]),_:1})]),y.value!==null?(x(),O("div",{key:0,class:U(["mt16",y.value?"anchor-match":"anchor-mismatch"])},[L(N,{size:20},{default:$(()=>[y.value?(x(),ne(p(Ao),{key:0})):(x(),ne(p($s),{key:1}))]),_:1}),C("span",null,Q(y.value?"锚点匹配:链尾哈希与快照一致":"锚点不匹配:数据可能已被篡改"),1)],2)):te("",!0)])):te("",!0)]),_:1},8,["modelValue"]),L(he,{modelValue:b.value,"onUpdate:modelValue":W[6]||(W[6]=Z=>b.value=Z),title:"账本公钥",width:"700px"},{default:$(()=>[C("div",B$e,[W[27]||(W[27]=C("p",{style:{color:"var(--text-muted)","margin-bottom":"12px","font-size":"13px"}}," 使用此公钥可验证账本条目的 ECDSA P256 签名,确保数据未被篡改。 ",-1)),C("pre",V$e,Q(_.value||"加载中..."),1),L(V,{onClick:W[5]||(W[5]=Z=>G(_.value)),size:"small",class:"mt16"},{default:$(()=>[...W[26]||(W[26]=[ue("复制公钥",-1)])]),_:1})])]),_:1},8,["modelValue"])])}}},F$e=fr(z$e,[["__scopeId","data-v-05660c6d"]]),H$e={class:"accounts-page"},W$e={class:"pg-card"},K$e={class:"pg-card-header"},U$e={class:"header-actions"},G$e={class:"text-muted",style:{"font-size":"12px"}},Y$e={class:"pg-card-body"},j$e={class:"filter-bar mb16"},q$e={class:"account-grid"},X$e={class:"account-head"},Z$e={class:"account-info"},J$e={class:"account-name"},Q$e={class:"account-channel text-muted"},eRe=["title"],tRe={class:"text-muted",style:{"font-size":"12px"}},nRe={class:"font-mono",style:{"font-size":"12px"}},aRe={class:"font-mono",style:{"font-size":"12px"}},rRe={class:"font-mono text-muted",style:{"font-size":"11px"}},oRe={class:"font-mono text-muted",style:{"font-size":"11px"}},iRe={class:"account-actions"},lRe={key:0,class:"empty-card pg-card"},sRe={class:"qr-upload-row"},uRe={class:"secret-box"},cRe={key:0,class:"secret-item"},dRe={class:"secret-value"},fRe={class:"font-mono",style:{"word-break":"break-all"}},vRe={class:"secret-item"},hRe={class:"secret-value"},pRe={class:"font-mono"},mRe={key:0,class:"secret-item"},gRe={class:"secret-value"},yRe={class:"font-mono"},bRe={class:"secret-item"},_Re={class:"secret-value"},wRe={class:"font-mono text-muted"},SRe={class:"secret-item"},CRe={class:"secret-value"},xRe={class:"font-mono text-muted"},kRe={class:"log-item"},TRe={class:"log-detail"},MRe={class:"log-operator text-muted"},ERe={__name:"Accounts",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F(!1),r=F(""),o=F(!1),i=F(!1),l=F(!1),s=F(m()),u=F(!1),c=F(!1),d=F([]),v=F(!1),f=F(!1),h=F(null);ye(()=>t.refreshKey,y);function m(){return{id:0,channel:"alipay",name:"",app_id:"",mch_id:"",private_key:"",public_key:"",api_key:"",qr_content:"",private_key_configured:!1,public_key_configured:!1,api_key_configured:!1,qr_content_configured:!1,weight:50,priority:0,auto_ban:!0,dailyLimitYuan:1e3,group:"default",is_prod:!1,remark:""}}const g=D(()=>r.value?n.value.filter(N=>N.channel===r.value):n.value);async function y(){a.value=!0;try{n.value=await yt.accounts({channel:r.value||void 0})}catch(N){We.error("加载失败: "+N.message)}finally{a.value=!1}}function b(N){return{alipay:"支付宝(商家)",wechat:"微信支付(商家)","personal.alipay":"个人免签·支付宝","personal.wechat":"个人免签·微信","personal.qq":"个人免签·QQ"}[N]||N}function _(N){return{normal:"success",cooling:"warning",frozen:"danger",disabled:"info"}[N]||"info"}function w(N){return{normal:"正常",cooling:"冷却中",frozen:"已冻结",disabled:"已禁用"}[N]||N}function S(N){return{create:"success",update:"primary",delete:"danger",freeze:"warning",unfreeze:"success",enable:"success",disable:"info"}[N]||"info"}function k(N){return String(N).startsWith("personal.")}function T(){i.value=!1,s.value=m(),o.value=!0}function M(N){i.value=!0,s.value={id:N.id,channel:N.channel,name:N.name,app_id:N.app_id,mch_id:N.mch_id,private_key:"",public_key:"",api_key:"",private_key_configured:!!N.private_key,public_key_configured:!!N.public_key,api_key_configured:!!N.api_key,qr_content_configured:!!N.qr_content,weight:N.weight,priority:N.priority,auto_ban:N.auto_ban,dailyLimitYuan:N.daily_limit/100,group:N.group,is_prod:N.is_prod,remark:N.remark,qr_content:""},o.value=!0}async function E(N){try{await er.confirm(`将显示账户「${N.name}」的完整明文密钥(含私钥),请确认当前环境安全。`,"查看完整密钥",{confirmButtonText:"确认查看",cancelButtonText:"取消",type:"warning"})}catch{return}v.value=!0,f.value=!0,h.value=null;try{h.value=await yt.accountSecret(N.id)}catch(V){v.value=!1,We.error("获取完整密钥失败: "+V.message)}finally{f.value=!1}}async function A(N){try{await navigator.clipboard.writeText(N),We.success("已复制完整密钥")}catch(V){We.error("复制失败: "+V.message)}}const I=F(!1);async function P(N){I.value=!0;try{const V=await yt.decodeQRCode(N);V?(s.value.qr_content=V,We.success("收款码解析成功")):We.warning("未识别到二维码内容")}catch(V){We.error("解析失败:"+V.message)}finally{I.value=!1}return!1}async function R(){if(!s.value.channel||!s.value.name){We.warning("请填写通道和账户名称");return}if(k(s.value.channel)&&!i.value&&!s.value.qr_content.trim()){We.warning("请填写收款码内容,或上传收款码图片自动解析");return}l.value=!0;try{const N={channel:s.value.channel,name:s.value.name,app_id:s.value.app_id,mch_id:s.value.mch_id,private_key:s.value.private_key,public_key:s.value.public_key,api_key:s.value.api_key,qr_content:s.value.qr_content,weight:s.value.weight,priority:s.value.priority,auto_ban:s.value.auto_ban,daily_limit:Math.round(s.value.dailyLimitYuan*100),group:s.value.group||"default",is_prod:s.value.is_prod,remark:s.value.remark};i.value?(await yt.updateAccount(s.value.id,N),We.success("账户已更新")):(await yt.createAccount(N),We.success("账户创建成功")),o.value=!1,y()}catch(N){We.error("保存失败: "+N.message)}finally{l.value=!1}}async function G(N){try{await yt.freezeAccount(N),We.success("已冻结"),y()}catch(V){We.error(V.message)}}async function K(N){try{await yt.unfreezeAccount(N),We.success("已解冻"),y()}catch(V){We.error(V.message)}}async function H(N){try{await yt.enableAccount(N),We.success("已启用"),y()}catch(V){We.error(V.message)}}async function z(N){try{await yt.disableAccount(N),We.success("已禁用"),y()}catch(V){We.error(V.message)}}async function B(N){try{await er.confirm(`确定删除账户「${N.name}」?此操作不可恢复。`,"确认删除",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"}),await yt.deleteAccount(N.id),We.success("已删除"),y()}catch(V){V!=="cancel"&&We.error(V.message)}}async function W(N){u.value=!0,c.value=!0;try{d.value=await yt.accountLogs(N.id,30)}catch(V){We.error("加载日志失败: "+V.message)}finally{c.value=!1}}return mt(y),(N,V)=>{const Y=be("el-icon"),X=be("el-button"),ee=be("el-alert"),re=be("el-option"),ie=be("el-option-group"),le=be("el-select"),ve=be("el-tag"),he=be("el-descriptions-item"),J=be("el-descriptions"),Z=be("el-empty"),j=be("el-step"),oe=be("el-steps"),se=be("el-form-item"),fe=be("el-input"),ge=be("el-col"),Se=be("el-row"),De=be("el-upload"),Te=be("el-input-number"),Re=be("el-switch"),Ce=be("el-form"),Le=be("el-dialog"),me=be("el-timeline-item"),Be=be("el-timeline"),ze=Gr("loading");return x(),O("div",H$e,[C("div",W$e,[C("div",K$e,[V[25]||(V[25]=C("span",{class:"section-label"},"账户池管理",-1)),C("div",U$e,[C("span",G$e,"共 "+Q(n.value.length)+" 个账户",1),L(X,{type:"primary",size:"small",onClick:T},{default:$(()=>[L(Y,null,{default:$(()=>[L(p(hf))]),_:1}),V[24]||(V[24]=ue(" 新增账户 ",-1))]),_:1})])]),C("div",Y$e,[L(ee,{type:"warning",closable:!1,class:"mb16",title:"密钥加密存储:敏感字段(私钥/API密钥)使用 AES-256-GCM 加密后存入独立 accounts.db,仅管理后台可读写。"}),C("div",j$e,[L(le,{modelValue:r.value,"onUpdate:modelValue":V[0]||(V[0]=xe=>r.value=xe),placeholder:"全部通道",clearable:"",size:"small",style:{width:"180px"},onChange:y},{default:$(()=>[L(re,{label:"全部通道",value:""}),L(ie,{label:"商家通道(官方 API)"},{default:$(()=>[L(re,{label:"支付宝(商家)",value:"alipay"}),L(re,{label:"微信支付(商家)",value:"wechat"})]),_:1}),L(ie,{label:"个人免签(收款码监听)"},{default:$(()=>[L(re,{label:"个人·支付宝",value:"personal.alipay"}),L(re,{label:"个人·微信",value:"personal.wechat"})]),_:1})]),_:1},8,["modelValue"]),r.value?(x(),ne(ve,{key:0,size:"small",type:"info",class:"ml8"},{default:$(()=>[ue(Q(b(r.value)),1)]),_:1})):te("",!0)]),st((x(),O("div",q$e,[(x(!0),O(Ve,null,dt(g.value,xe=>(x(),O("div",{key:xe.id,class:"account-card pg-card"},[C("div",X$e,[C("div",{class:U(["account-logo",xe.channel])},[L(Y,{size:18},{default:$(()=>[(x(),ne(St((xe.channel.includes("wechat"),"CircleCheck"))))]),_:2},1024)],2),C("div",Z$e,[C("div",J$e,Q(xe.name),1),C("div",Q$e,Q(b(xe.channel)),1)]),L(ve,{type:_(xe.status),size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(w(xe.status)),1)]),_:2},1032,["type"])]),L(J,{column:2,size:"small",class:"mt10",border:""},{default:$(()=>[k(xe.channel)?(x(),O(Ve,{key:0},[L(he,{label:"收款码内容",span:2},{default:$(()=>[C("span",{class:"font-mono",style:{"font-size":"11px"},title:xe.qr_content},Q(xe.qr_content||"—"),9,eRe)]),_:2},1024),L(he,{label:"绑定设备",span:2},{default:$(()=>[C("span",tRe,Q(xe.remark||"未绑定"),1)]),_:2},1024)],64)):(x(),O(Ve,{key:1},[L(he,{label:"AppID",span:2},{default:$(()=>[C("span",nRe,Q(xe.app_id||"—"),1)]),_:2},1024),xe.mch_id?(x(),ne(he,{key:0,label:"商户号"},{default:$(()=>[C("span",aRe,Q(xe.mch_id),1)]),_:2},1024)):te("",!0),L(he,{label:"应用私钥"},{default:$(()=>[C("span",rRe,Q(xe.private_key||"—"),1)]),_:2},1024),L(he,{label:"公钥/API密钥"},{default:$(()=>[C("span",oRe,Q(xe.public_key||xe.api_key||"—"),1)]),_:2},1024)],64)),L(he,{label:"权重"},{default:$(()=>[L(ve,{size:"small"},{default:$(()=>[ue(Q(xe.weight),1)]),_:2},1024)]),_:2},1024),L(he,{label:"优先级"},{default:$(()=>[L(ve,{size:"small",type:"warning"},{default:$(()=>[ue(Q(xe.priority),1)]),_:2},1024)]),_:2},1024),L(he,{label:"日限额"},{default:$(()=>[C("span",{class:U({"text-danger":xe.daily_limit>0&&(xe.daily_used??0)>=xe.daily_limit})}," ¥"+Q(((xe.daily_used??0)/100).toFixed(2))+" / ¥"+Q(((xe.daily_limit??0)/100).toFixed(2)),3)]),_:2},1024),L(he,{label:"自动禁用"},{default:$(()=>[L(ve,{type:xe.auto_ban?"danger":"info",size:"small"},{default:$(()=>[ue(Q(xe.auto_ban?"开启":"关闭"),1)]),_:2},1032,["type"])]),_:2},1024),L(he,{label:"连续失败"},{default:$(()=>[C("span",{class:U({"text-danger":(xe.fail_count??0)>=3})},Q(xe.fail_count??0)+" 次",3)]),_:2},1024),L(he,{label:"分组"},{default:$(()=>[L(ve,{size:"small",type:"info"},{default:$(()=>[ue(Q(xe.group),1)]),_:2},1024)]),_:2},1024),L(he,{label:"环境"},{default:$(()=>[L(ve,{type:xe.is_prod?"danger":"warning",size:"small"},{default:$(()=>[ue(Q(xe.is_prod?"生产":"沙箱"),1)]),_:2},1032,["type"])]),_:2},1024)]),_:2},1024),C("div",iRe,[L(X,{size:"small",text:"",type:"primary",onClick:rt=>M(xe)},{default:$(()=>[...V[26]||(V[26]=[ue("编辑",-1)])]),_:1},8,["onClick"]),L(X,{size:"small",text:"",type:"primary",onClick:rt=>E(xe)},{default:$(()=>[...V[27]||(V[27]=[ue("查看/复制完整密钥",-1)])]),_:1},8,["onClick"]),xe.status==="normal"?(x(),ne(X,{key:0,size:"small",text:"",type:"warning",onClick:rt=>G(xe.id)},{default:$(()=>[...V[28]||(V[28]=[ue("冻结",-1)])]),_:1},8,["onClick"])):te("",!0),xe.status==="frozen"?(x(),ne(X,{key:1,size:"small",text:"",type:"success",onClick:rt=>K(xe.id)},{default:$(()=>[...V[29]||(V[29]=[ue("解冻",-1)])]),_:1},8,["onClick"])):te("",!0),xe.status==="disabled"?(x(),ne(X,{key:2,size:"small",text:"",type:"success",onClick:rt=>H(xe.id)},{default:$(()=>[...V[30]||(V[30]=[ue("启用",-1)])]),_:1},8,["onClick"])):te("",!0),xe.status==="normal"?(x(),ne(X,{key:3,size:"small",text:"",type:"danger",onClick:rt=>z(xe.id)},{default:$(()=>[...V[31]||(V[31]=[ue("禁用",-1)])]),_:1},8,["onClick"])):te("",!0),L(X,{size:"small",text:"",type:"danger",onClick:rt=>B(xe)},{default:$(()=>[...V[32]||(V[32]=[ue("删除",-1)])]),_:1},8,["onClick"]),L(X,{size:"small",text:"",onClick:rt=>W(xe)},{default:$(()=>[...V[33]||(V[33]=[ue("日志",-1)])]),_:1},8,["onClick"])])]))),128)),!g.value.length&&!a.value?(x(),O("div",lRe,[L(Z,{description:"账户池为空,请按以下步骤配置第一个支付账户","image-size":80}),L(oe,{active:1,"align-center":"",class:"setup-steps"},{default:$(()=>[L(j,{title:"准备密钥",description:"在支付宝开放平台/微信商户平台获取 AppID、私钥、公钥"}),L(j,{title:"点击「新增账户」",description:"选择通道并粘贴密钥,保存后 AES-256-GCM 加密入库"}),L(j,{title:"通道即刻生效",description:"无需重启,账户自动进入账户池参与下单调度"})]),_:1}),L(ee,{type:"info",closable:!1,class:"mt12",title:"已移除 .env 密钥迁移:支付密钥只通过本界面配置,加密存储于独立 accounts.db,不再依赖环境变量。"})])):te("",!0)])),[[ze,a.value]])])]),L(Le,{modelValue:o.value,"onUpdate:modelValue":V[16]||(V[16]=xe=>o.value=xe),title:i.value?"编辑账户":"新增账户",width:"600px","close-on-click-modal":!1},{footer:$(()=>[L(X,{onClick:V[15]||(V[15]=xe=>o.value=!1)},{default:$(()=>[...V[39]||(V[39]=[ue("取消",-1)])]),_:1}),L(X,{type:"primary",loading:l.value,onClick:R},{default:$(()=>[ue(Q(i.value?"保存":"创建"),1)]),_:1},8,["loading"])]),default:$(()=>[L(Ce,{model:s.value,"label-width":"100px",size:"small"},{default:$(()=>[L(se,{label:"通道",required:""},{default:$(()=>[L(le,{modelValue:s.value.channel,"onUpdate:modelValue":V[1]||(V[1]=xe=>s.value.channel=xe),placeholder:"选择通道",style:{width:"100%"}},{default:$(()=>[L(re,{label:"支付宝(商家)",value:"alipay"}),L(re,{label:"微信支付(商家)",value:"wechat"}),L(re,{label:"个人免签·支付宝",value:"personal.alipay"}),L(re,{label:"个人免签·微信",value:"personal.wechat"})]),_:1},8,["modelValue"])]),_:1}),L(se,{label:"账户名称",required:""},{default:$(()=>[L(fe,{modelValue:s.value.name,"onUpdate:modelValue":V[2]||(V[2]=xe=>s.value.name=xe),placeholder:"例:支付宝-主账户 / 个人码-支付宝-测试1"},null,8,["modelValue"])]),_:1}),k(s.value.channel)?te("",!0):(x(),O(Ve,{key:0},[L(Se,{gutter:12},{default:$(()=>[L(ge,{span:12},{default:$(()=>[L(se,{label:"AppID",required:""},{default:$(()=>[L(fe,{modelValue:s.value.app_id,"onUpdate:modelValue":V[3]||(V[3]=xe=>s.value.app_id=xe),placeholder:"支付宝 APPID / 微信 AppID"},null,8,["modelValue"])]),_:1})]),_:1}),L(ge,{span:12},{default:$(()=>[L(se,{label:"商户号"},{default:$(()=>[L(fe,{modelValue:s.value.mch_id,"onUpdate:modelValue":V[4]||(V[4]=xe=>s.value.mch_id=xe),placeholder:"微信商户号"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1}),L(se,{label:"应用私钥",required:""},{default:$(()=>[L(fe,{modelValue:s.value.private_key,"onUpdate:modelValue":V[5]||(V[5]=xe=>s.value.private_key=xe),type:"textarea",rows:3,placeholder:i.value&&s.value.private_key_configured?"已配置(留空保持不变)":"粘贴应用私钥(AES-256-GCM 加密存储)"},null,8,["modelValue","placeholder"]),i.value&&s.value.private_key_configured?(x(),ne(ve,{key:0,size:"small",type:"success"},{default:$(()=>[...V[34]||(V[34]=[ue("已配置",-1)])]),_:1})):te("",!0)]),_:1}),L(se,{label:"公钥/API密钥"},{default:$(()=>[L(fe,{modelValue:s.value.public_key,"onUpdate:modelValue":V[6]||(V[6]=xe=>s.value.public_key=xe),type:"textarea",rows:2,placeholder:i.value&&(s.value.public_key_configured||s.value.api_key_configured)?"已配置(留空保持不变)":"支付宝公钥 或 微信 API 密钥(加密存储)"},null,8,["modelValue","placeholder"]),i.value&&(s.value.public_key_configured||s.value.api_key_configured)?(x(),ne(ve,{key:0,size:"small",type:"success"},{default:$(()=>[...V[35]||(V[35]=[ue("已配置",-1)])]),_:1})):te("",!0)]),_:1})],64)),k(s.value.channel)?(x(),ne(se,{key:1,label:"收款码内容",required:!i.value},{default:$(()=>[L(fe,{modelValue:s.value.qr_content,"onUpdate:modelValue":V[7]||(V[7]=xe=>s.value.qr_content=xe),type:"textarea",rows:2,placeholder:i.value&&s.value.qr_content_configured?"已配置(留空保持不变)":"收款码内容,如 https://qr.alipay.com/xxx"},null,8,["modelValue","placeholder"]),i.value&&s.value.qr_content_configured?(x(),ne(ve,{key:0,size:"small",type:"success"},{default:$(()=>[...V[36]||(V[36]=[ue("已配置",-1)])]),_:1})):te("",!0),C("div",sRe,[L(De,{"show-file-list":!1,"before-upload":P,accept:"image/png,image/jpeg,image/gif",disabled:I.value},{default:$(()=>[L(X,{type:"primary",plain:"",size:"small",loading:I.value},{default:$(()=>[L(Y,null,{default:$(()=>[L(p(NI))]),_:1}),V[37]||(V[37]=ue(" 上传收款码图片自动解析 ",-1))]),_:1},8,["loading"])]),_:1},8,["disabled"]),V[38]||(V[38]=C("span",{class:"text-muted",style:{"font-size":"12px"}},"从支付宝/微信 App「保存收款码」导出图片,上传后自动识别",-1))])]),_:1},8,["required"])):te("",!0),L(Se,{gutter:12},{default:$(()=>[L(ge,{span:8},{default:$(()=>[L(se,{label:"权重"},{default:$(()=>[L(Te,{modelValue:s.value.weight,"onUpdate:modelValue":V[8]||(V[8]=xe=>s.value.weight=xe),min:1,max:999,style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1}),L(ge,{span:8},{default:$(()=>[L(se,{label:"优先级"},{default:$(()=>[L(Te,{modelValue:s.value.priority,"onUpdate:modelValue":V[9]||(V[9]=xe=>s.value.priority=xe),min:0,max:99,style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1}),L(ge,{span:8},{default:$(()=>[L(se,{label:"日限额(元)"},{default:$(()=>[L(Te,{modelValue:s.value.dailyLimitYuan,"onUpdate:modelValue":V[10]||(V[10]=xe=>s.value.dailyLimitYuan=xe),min:0,step:100,style:{width:"100%"}},null,8,["modelValue"])]),_:1})]),_:1})]),_:1}),L(Se,{gutter:12},{default:$(()=>[L(ge,{span:8},{default:$(()=>[L(se,{label:"自动禁用"},{default:$(()=>[L(Re,{modelValue:s.value.auto_ban,"onUpdate:modelValue":V[11]||(V[11]=xe=>s.value.auto_ban=xe)},null,8,["modelValue"])]),_:1})]),_:1}),L(ge,{span:8},{default:$(()=>[L(se,{label:"生产环境"},{default:$(()=>[L(Re,{modelValue:s.value.is_prod,"onUpdate:modelValue":V[12]||(V[12]=xe=>s.value.is_prod=xe)},null,8,["modelValue"])]),_:1})]),_:1}),L(ge,{span:8},{default:$(()=>[L(se,{label:"分组"},{default:$(()=>[L(fe,{modelValue:s.value.group,"onUpdate:modelValue":V[13]||(V[13]=xe=>s.value.group=xe),placeholder:"default"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1}),L(se,{label:"备注"},{default:$(()=>[L(fe,{modelValue:s.value.remark,"onUpdate:modelValue":V[14]||(V[14]=xe=>s.value.remark=xe),type:"textarea",rows:2},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["modelValue","title"]),L(Le,{modelValue:v.value,"onUpdate:modelValue":V[22]||(V[22]=xe=>v.value=xe),title:"完整密钥",width:"640px"},{default:$(()=>[L(ee,{type:"warning",closable:!1,class:"mb16",title:"以下为明文密钥,请勿泄露或截图分享。复制后请及时关闭。"}),st((x(),O("div",uRe,[h.value?(x(),O(Ve,{key:0},[k(h.value.channel)?(x(),O("div",cRe,[V[41]||(V[41]=C("div",{class:"secret-label"},"收款码内容",-1)),C("div",dRe,[C("span",fRe,Q(h.value.qr_content||"—"),1),h.value.qr_content?(x(),ne(X,{key:0,size:"small",text:"",type:"primary",onClick:V[17]||(V[17]=xe=>A(h.value.qr_content))},{default:$(()=>[...V[40]||(V[40]=[ue("复制",-1)])]),_:1})):te("",!0)])])):(x(),O(Ve,{key:1},[C("div",vRe,[V[43]||(V[43]=C("div",{class:"secret-label"},"AppID",-1)),C("div",hRe,[C("span",pRe,Q(h.value.app_id||"—"),1),h.value.app_id?(x(),ne(X,{key:0,size:"small",text:"",type:"primary",onClick:V[18]||(V[18]=xe=>A(h.value.app_id))},{default:$(()=>[...V[42]||(V[42]=[ue("复制",-1)])]),_:1})):te("",!0)])]),h.value.mch_id?(x(),O("div",mRe,[V[45]||(V[45]=C("div",{class:"secret-label"},"商户号",-1)),C("div",gRe,[C("span",yRe,Q(h.value.mch_id),1),L(X,{size:"small",text:"",type:"primary",onClick:V[19]||(V[19]=xe=>A(h.value.mch_id))},{default:$(()=>[...V[44]||(V[44]=[ue("复制",-1)])]),_:1})])])):te("",!0),C("div",bRe,[V[47]||(V[47]=C("div",{class:"secret-label"},"应用私钥",-1)),C("div",_Re,[C("span",wRe,Q(h.value.private_key||"(未配置)"),1),h.value.private_key?(x(),ne(X,{key:0,size:"small",text:"",type:"primary",onClick:V[20]||(V[20]=xe=>A(h.value.private_key))},{default:$(()=>[...V[46]||(V[46]=[ue("复制",-1)])]),_:1})):te("",!0)])]),C("div",SRe,[V[49]||(V[49]=C("div",{class:"secret-label"},"公钥/API密钥",-1)),C("div",CRe,[C("span",xRe,Q(h.value.public_key||h.value.api_key||"(未配置)"),1),h.value.public_key||h.value.api_key?(x(),ne(X,{key:0,size:"small",text:"",type:"primary",onClick:V[21]||(V[21]=xe=>A(h.value.public_key||h.value.api_key))},{default:$(()=>[...V[48]||(V[48]=[ue("复制",-1)])]),_:1})):te("",!0)])])],64))],64)):te("",!0)])),[[ze,f.value]])]),_:1},8,["modelValue"]),L(Le,{modelValue:u.value,"onUpdate:modelValue":V[23]||(V[23]=xe=>u.value=xe),title:"账户操作日志",width:"600px"},{default:$(()=>[st((x(),O("div",null,[d.value.length?(x(),ne(Be,{key:0},{default:$(()=>[(x(!0),O(Ve,null,dt(d.value,xe=>(x(),ne(me,{key:xe.id,timestamp:xe.created_at,placement:"top"},{default:$(()=>[C("div",kRe,[L(ve,{type:S(xe.action),size:"small"},{default:$(()=>[ue(Q(xe.action),1)]),_:2},1032,["type"]),C("span",TRe,Q(xe.detail),1),C("span",MRe,"— "+Q(xe.operator||"系统"),1)])]),_:2},1032,["timestamp"]))),128))]),_:1})):(x(),ne(Z,{key:1,description:"暂无操作日志","image-size":60}))])),[[ze,c.value]])]),_:1},8,["modelValue"])])}}},ARe=fr(ERe,[["__scopeId","data-v-e8a4a312"]]),IRe={class:"upstreams-page"},LRe={class:"pg-card"},PRe={class:"pg-card-header"},DRe={class:"header-actions"},ORe={class:"text-muted",style:{"font-size":"12px"}},$Re={class:"pg-card-body"},RRe={class:"upstream-grid"},NRe={class:"upstream-head"},BRe={class:"upstream-logo"},VRe={class:"upstream-info"},zRe={class:"upstream-name"},FRe={class:"upstream-pid font-mono text-muted"},HRe={class:"font-mono",style:{"font-size":"12px"}},WRe={class:"font-mono text-muted",style:{"font-size":"12px"}},KRe={class:"font-mono text-muted",style:{"font-size":"12px"}},URe={style:{"font-size":"12px"}},GRe={class:"upstream-actions"},YRe={key:0,class:"empty-card pg-card"},jRe={key:0,class:"guide-box"},qRe={class:"guide-steps mb16"},XRe={class:"guide-step"},ZRe={class:"guide-step-body"},JRe={class:"guide-step-desc"},QRe={class:"guide-step"},eNe={class:"guide-step-body"},tNe={class:"guide-step-desc"},nNe={class:"guide-callback font-mono"},aNe={class:"font-mono"},rNe={class:"font-mono",style:{"word-break":"break-all"}},oNe={class:"font-mono",style:{"font-size":"12px"}},iNe={class:"log-item"},lNe={class:"log-detail"},sNe={class:"log-operator text-muted"},uNe={__name:"Upstreams",props:{refreshKey:Number},setup(e){const t=e,n=F([]),a=F(!1),r=F(!1),o=F(!1),i=F(!1),l=F(g()),s=F(!1),u=F(!1),c=F(""),d=F(""),v=F(null),f=F(!1),h=F(!1),m=F([]);ye(()=>t.refreshKey,y);function g(){return{id:0,name:"",pid:"",key:"",base_url:"",notify_domains:"",remark:""}}async function y(){a.value=!0;try{n.value=await yt.upstreams()}catch(I){We.error("加载失败: "+I.message)}finally{a.value=!1}}function b(I){return{create:"success",update:"primary",delete:"danger",enable:"success",disable:"info"}[I]||"info"}function _(){o.value=!1,l.value=g(),r.value=!0}function w(I){o.value=!0,l.value={id:I.id,name:I.name,pid:I.pid,key:"",base_url:I.base_url,notify_domains:I.notify_domains,remark:I.remark},r.value=!0}async function S(){if(!l.value.name||!l.value.pid){We.warning("请填写名称和商户ID (pid)");return}i.value=!0;try{const I={name:l.value.name,pid:l.value.pid,key:l.value.key,base_url:l.value.base_url,notify_domains:l.value.notify_domains,remark:l.value.remark};o.value?(await yt.updateUpstream(l.value.id,I),We.success("上游已更新")):(await yt.createUpstream(I),We.success("上游创建成功")),r.value=!1,y()}catch(I){We.error("保存失败: "+I.message)}finally{i.value=!1}}async function k(I){try{await yt.enableUpstream(I),We.success("已启用"),y()}catch(P){We.error(P.message)}}async function T(I){try{await yt.disableUpstream(I),We.success("已禁用"),y()}catch(P){We.error(P.message)}}async function M(I){try{await er.confirm(`确定删除上游「${I.name}」?`,"确认删除",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"}),await yt.deleteUpstream(I.id),We.success("已删除"),y()}catch(P){P!=="cancel"&&We.error(P.message)}}function E(I){er.confirm(`将显示上游「${I.name}」的完整明文密钥,请确认当前环境安全。`,"查看完整密钥",{confirmButtonText:"确认查看",cancelButtonText:"取消",type:"warning"}).then(async()=>{v.value=I,c.value="",d.value="",s.value=!0,u.value=!0;try{const P=await yt.upstreamSecret(I.id);c.value=P.key||"";const R=await yt.epayInfo();d.value=((R==null?void 0:R.submit_url)||"").replace("/submit.php","/alipay/notify")}catch(P){We.error("获取完整密钥失败: "+P.message)}finally{u.value=!1}}).catch(()=>{})}async function A(){if(!v.value)return;const I=c.value||v.value.key,P=`商户ID (pid): ${v.value.pid} +商户密钥: ${I} +网关地址: ${v.value.base_url||""} +下单接口: ${(v.value.base_url||"")+"/submit.php"} +查询接口: ${(v.value.base_url||"")+"/api.php"} +回调端点 notify_url: ${d.value||""}`;try{await navigator.clipboard.writeText(P),We.success("已复制")}catch(R){We.error("复制失败: "+R.message)}}return y(),(I,P)=>{const R=be("el-icon"),G=be("el-button"),K=be("el-alert"),H=be("el-tag"),z=be("el-descriptions-item"),B=be("el-descriptions"),W=be("el-empty"),N=be("el-step"),V=be("el-steps"),Y=be("el-input"),X=be("el-form-item"),ee=be("el-form"),re=be("el-dialog"),ie=be("el-divider"),le=be("el-timeline-item"),ve=be("el-timeline"),he=Gr("loading");return x(),O("div",IRe,[C("div",LRe,[C("div",PRe,[P[11]||(P[11]=C("span",{class:"section-label"},"上游对接方管理(多商户)",-1)),C("div",DRe,[C("span",ORe,"共 "+Q(n.value.length)+" 个上游",1),L(G,{type:"primary",size:"small",onClick:_},{default:$(()=>[L(R,null,{default:$(()=>[L(p(hf))]),_:1}),P[10]||(P[10]=ue(" 新增上游 ",-1))]),_:1})])]),C("div",$Re,[L(K,{type:"info",closable:!1,class:"mb16",title:"每个上游拥有独立的易支付 pid + 密钥:下单验签、回调签名均按 pid 路由,互不伪造。"}),st((x(),O("div",RRe,[(x(!0),O(Ve,null,dt(n.value,J=>(x(),O("div",{key:J.id,class:"upstream-card pg-card"},[C("div",NRe,[C("div",BRe,Q(J.name.charAt(0).toUpperCase()),1),C("div",VRe,[C("div",zRe,Q(J.name),1),C("div",FRe,"pid: "+Q(J.pid),1)]),L(H,{type:J.status==="enabled"?"success":"info",size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(J.status==="enabled"?"启用":"禁用"),1)]),_:2},1032,["type"])]),L(B,{column:1,size:"small",class:"mt10",border:""},{default:$(()=>[L(z,{label:"服务地址"},{default:$(()=>[C("span",HRe,Q(J.base_url||"—"),1)]),_:2},1024),L(z,{label:"商户密钥"},{default:$(()=>[C("span",WRe,Q(J.key||"—"),1)]),_:2},1024),L(z,{label:"回调域名白名单"},{default:$(()=>[C("span",KRe,Q(J.notify_domains||"不限"),1)]),_:2},1024),L(z,{label:"备注"},{default:$(()=>[C("span",URe,Q(J.remark||"—"),1)]),_:2},1024)]),_:2},1024),C("div",GRe,[L(G,{size:"small",text:"",type:"primary",onClick:Z=>w(J)},{default:$(()=>[...P[12]||(P[12]=[ue("编辑",-1)])]),_:1},8,["onClick"]),J.status==="disabled"?(x(),ne(G,{key:0,size:"small",text:"",type:"success",onClick:Z=>k(J.id)},{default:$(()=>[...P[13]||(P[13]=[ue("启用",-1)])]),_:1},8,["onClick"])):te("",!0),J.status==="enabled"?(x(),ne(G,{key:1,size:"small",text:"",type:"warning",onClick:Z=>T(J.id)},{default:$(()=>[...P[14]||(P[14]=[ue("禁用",-1)])]),_:1},8,["onClick"])):te("",!0),L(G,{size:"small",text:"",type:"danger",onClick:Z=>M(J)},{default:$(()=>[...P[15]||(P[15]=[ue("删除",-1)])]),_:1},8,["onClick"]),L(G,{size:"small",text:"",onClick:Z=>E(J)},{default:$(()=>[...P[16]||(P[16]=[ue("对接指引",-1)])]),_:1},8,["onClick"])])]))),128)),!n.value.length&&!a.value?(x(),O("div",YRe,[L(W,{description:"暂无上游配置,系统已自动创建 default","image-size":80}),L(V,{active:1,"align-center":"",class:"setup-steps"},{default:$(()=>[L(N,{title:"新增上游",description:"为每个对接项目创建独立条目(如 new-api)"}),L(N,{title:"分配 pid + 密钥",description:"保存后立即生效,无需重启;密钥加密入库"}),L(N,{title:"告知上游",description:"把对接信息配置到上游项目的易支付设置"})]),_:1})])):te("",!0)])),[[he,a.value]])])]),L(re,{modelValue:r.value,"onUpdate:modelValue":P[7]||(P[7]=J=>r.value=J),title:o.value?"编辑上游":"新增上游",width:"560px","close-on-click-modal":!1},{footer:$(()=>[L(G,{onClick:P[6]||(P[6]=J=>r.value=!1)},{default:$(()=>[...P[18]||(P[18]=[ue("取消",-1)])]),_:1}),L(G,{type:"primary",loading:i.value,onClick:S},{default:$(()=>[ue(Q(o.value?"保存":"创建"),1)]),_:1},8,["loading"])]),default:$(()=>[L(ee,{model:l.value,"label-width":"120px",size:"small"},{default:$(()=>[L(X,{label:"名称",required:""},{default:$(()=>[L(Y,{modelValue:l.value.name,"onUpdate:modelValue":P[0]||(P[0]=J=>l.value.name=J),placeholder:"例:new-api"},null,8,["modelValue"])]),_:1}),L(X,{label:"商户ID (pid)",required:""},{default:$(()=>[L(Y,{modelValue:l.value.pid,"onUpdate:modelValue":P[1]||(P[1]=J=>l.value.pid=J),placeholder:"易支付商户ID,唯一"},null,8,["modelValue"])]),_:1}),L(X,{label:"易支付密钥"},{default:$(()=>[L(Y,{modelValue:l.value.key,"onUpdate:modelValue":P[2]||(P[2]=J=>l.value.key=J),type:"textarea",rows:2,placeholder:o.value?"留空表示不修改":"粘贴易支付密钥(AES-256-GCM 加密存储)","show-password":""},null,8,["modelValue","placeholder"])]),_:1}),L(X,{label:"服务地址"},{default:$(()=>[L(Y,{modelValue:l.value.base_url,"onUpdate:modelValue":P[3]||(P[3]=J=>l.value.base_url=J),placeholder:"https://new-api.example.com"},null,8,["modelValue"])]),_:1}),L(X,{"label-width":"0",style:{"margin-top":"-10px"}},{default:$(()=>[L(K,{type:"warning",closable:!1,"show-icon":"",title:"该地址必须是这个上游商户自己的公网域名!"}),P[17]||(P[17]=C("div",{class:"help-text",style:{"margin-top":"6px"}},[ue(" 填"),C("b",null,"该商户站点"),ue("(如 new-api)用户实际打开的公网地址——支付完成后浏览器会 "),C("b",null,"跳回这里"),ue("(网关在商户传来的 return_url 是内网地址时用它兜底重写)。 例:new-api 部署在 "),C("code",null,"https://new-api.example.com"),ue(",就填它。 不要填网关自己的域名(paygateway 的),也不要填 "),C("code",null,"localhost"),ue("/内网。 ")],-1))]),_:1}),L(X,{label:"回调域名白名单"},{default:$(()=>[L(Y,{modelValue:l.value.notify_domains,"onUpdate:modelValue":P[4]||(P[4]=J=>l.value.notify_domains=J),placeholder:"可选,逗号分隔:api.example.com,example.org"},null,8,["modelValue"])]),_:1}),L(X,{label:"备注"},{default:$(()=>[L(Y,{modelValue:l.value.remark,"onUpdate:modelValue":P[5]||(P[5]=J=>l.value.remark=J),type:"textarea",rows:2,placeholder:"例:New API 充值+订阅"},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["modelValue","title"]),L(re,{modelValue:s.value,"onUpdate:modelValue":P[8]||(P[8]=J=>s.value=J),title:"对接指引",width:"620px"},{default:$(()=>[v.value?st((x(),O("div",jRe,[L(K,{type:"info",closable:!1,class:"mb16","show-icon":"",title:"对接逻辑:pid 和密钥都由「上游项目」配置,paygateway 这里只是登记相同的值用于验签;只有回调端点是上游需要填的。"}),C("div",qRe,[C("div",XRe,[P[24]||(P[24]=C("div",{class:"guide-step-num"},"①",-1)),C("div",ZRe,[P[23]||(P[23]=C("div",{class:"guide-step-title"},"在上游项目(如 New API)配置易支付",-1)),C("div",JRe,[P[21]||(P[21]=C("div",null,[ue("· pid、密钥:"),C("b",null,"由上游项目自己配置/生成"),ue(",把这两个值记下来")],-1)),C("div",null,[P[19]||(P[19]=ue("· 网关地址 / 下单接口:填 ",-1)),C("code",null,Q((v.value.base_url||"http://:8080")+"/submit.php"),1)]),C("div",null,[P[20]||(P[20]=ue("· 查询接口:填 ",-1)),C("code",null,Q((v.value.base_url||"http://:8080")+"/api.php"),1)]),P[22]||(P[22]=C("div",{class:"guide-note"},[ue(" ⚠️ 上述网关地址必须用"),C("b",null,"公网域名"),ue("(如 "),C("code",null,"https://paygateway.de5.net"),ue("), 不能填 "),C("code",null,"localhost"),ue("/"),C("code",null,"127.0.0.1"),ue("/内网 IP——否则支付界面在其他机器上打不开。 ")],-1))])])]),P[28]||(P[28]=C("div",{class:"guide-step"},[C("div",{class:"guide-step-num"},"②"),C("div",{class:"guide-step-body"},[C("div",{class:"guide-step-title"},"把上游的 pid 和密钥登记到这里(本页上游配置)"),C("div",{class:"guide-step-desc"},[ue(" 编辑本上游,把上一步记下的 "),C("b",null,"pid"),ue(" 和 "),C("b",null,"密钥"),ue(" 填进来。两边的值必须"),C("b",null,"完全一致"),ue(",paygateway 才能验签通过。 ")])])],-1)),C("div",QRe,[P[27]||(P[27]=C("div",{class:"guide-step-num"},"③",-1)),C("div",eNe,[P[26]||(P[26]=C("div",{class:"guide-step-title"},"上游项目填回调端点 notify_url",-1)),C("div",tNe,[P[25]||(P[25]=C("div",null,[ue("这是"),C("b",null,"上游需要配置"),ue("的唯一回调项,指向 paygateway 的回调地址:")],-1)),C("div",nNe,Q(d.value||"(启动后自动生成,见「支付通道」页)"),1)])])])]),L(ie,null,{default:$(()=>[...P[29]||(P[29]=[ue("当前登记的 pid / 密钥",-1)])]),_:1}),u.value?te("",!0):(x(),ne(K,{key:0,type:"warning",closable:!1,class:"mb16",title:"密钥为明文展示,复制后请勿泄露。"})),L(B,{column:1,size:"small",border:""},{default:$(()=>[L(z,{label:"商户ID (pid)"},{default:$(()=>[C("span",aNe,Q(v.value.pid),1)]),_:1}),L(z,{label:"商户密钥"},{default:$(()=>[C("span",rNe,Q(c.value||v.value.key),1),L(G,{size:"small",text:"",type:"primary",onClick:A},{default:$(()=>[...P[30]||(P[30]=[ue("复制",-1)])]),_:1})]),_:1}),L(z,{label:"网关地址"},{default:$(()=>[C("span",oNe,Q(v.value.base_url||"(未配置)"),1)]),_:1})]),_:1})])),[[he,u.value]]):te("",!0)]),_:1},8,["modelValue"]),L(re,{modelValue:f.value,"onUpdate:modelValue":P[9]||(P[9]=J=>f.value=J),title:"上游操作日志",width:"600px"},{default:$(()=>[st((x(),O("div",null,[m.value.length?(x(),ne(ve,{key:0},{default:$(()=>[(x(!0),O(Ve,null,dt(m.value,J=>(x(),ne(le,{key:J.id,timestamp:J.created_at,placement:"top"},{default:$(()=>[C("div",iNe,[L(H,{type:b(J.action),size:"small"},{default:$(()=>[ue(Q(J.action),1)]),_:2},1032,["type"]),C("span",lNe,Q(J.detail),1),C("span",sNe,"— "+Q(J.operator||"系统"),1)])]),_:2},1032,["timestamp"]))),128))]),_:1})):(x(),ne(W,{key:1,description:"暂无操作日志","image-size":60}))])),[[he,h.value]])]),_:1},8,["modelValue"])])}}},cNe=fr(uNe,[["__scopeId","data-v-cefda71d"]]),dNe={class:"reconciler-page"},fNe={class:"filter-bar"},vNe={class:"text-muted",style:{"margin-left":"auto","font-size":"12px"}},hNe={key:0,class:"pager"},pNe={class:"daily-report"},mNe={class:"report-head"},gNe={key:0,class:"mt16"},yNe={class:"report-stats"},bNe={class:"stat-box pg-card"},_Ne={class:"stat-val"},wNe={class:"stat-box pg-card"},SNe={class:"stat-val text-success"},CNe={class:"stat-box pg-card"},xNe={class:"stat-val text-warning"},kNe={class:"stat-box pg-card"},TNe={class:"stat-box pg-card"},MNe={key:1,class:"empty-text"},ENe={class:"font-mono"},ANe={class:"font-mono"},INe={class:"evidence-box"},LNe={key:0,class:"text-muted",style:{"font-size":"12px"}},PNe={key:1,class:"text-muted"},Ub=20,DNe={__name:"Reconciler",setup(e){const t=F("exceptions"),n=F(!1),a=F([]),r=F(0),o=F(1),i=F({phase:"pending",reason:""}),l=F(!1),s=F(null),u=F([]),c=F(!1),d=F({action:"settle",paid_amount:0,channel_tx_id:"",payer:"",note:""}),v=F(!1),f=F(null),h=F(!1),m=F({out_trade_no:"",note:""}),g=F(!1),y=F([]),b=F(new Date().toISOString().slice(0,10)),_=F(null),w=F(!1);async function S(){w.value=!0;try{_.value=await yt.dailyReport({date:b.value})}catch(W){We.error("生成报告失败:"+W.message)}finally{w.value=!1}}function k(W){return{query_failed:"danger",amount_mismatch:"warning",state_conflict:"warning",close_uncertain:"danger",notify_dead:"info",manual:""}[W]||""}function T(W){return{query_failed:"查单失败",amount_mismatch:"金额不一致",state_conflict:"状态矛盾",close_uncertain:"关单存疑",notify_dead:"通知dead",manual:"手动加入"}[W]||W}async function M(){n.value=!0;try{const W=await yt.listExceptions({phase:i.value.phase,reason:i.value.reason,page:o.value,page_size:Ub});a.value=W.list||[],r.value=W.total||0}catch(W){We.error("加载失败: "+W.message)}finally{n.value=!1}}async function E(W){try{const N=await yt.getException(W.id);s.value=N.exception,u.value=N.logs||[],l.value=!0}catch(N){We.error("加载详情失败: "+N.message)}}async function A(W){var N;try{await yt.requeryException(W),We.success("查单完成"),((N=s.value)==null?void 0:N.id)===W&&E({id:W})}catch(V){We.error("查单失败: "+V.message)}}function I(W){f.value=W,d.value={action:"settle",paid_amount:W.amount_minor/100,channel_tx_id:W.channel_tx_id||"",payer:"",note:""},c.value=!0}async function P(){v.value=!0;try{const W={action:d.value.action,paid_amount:Math.round(d.value.paid_amount*100),channel_tx_id:d.value.channel_tx_id,payer:d.value.payer,note:d.value.note};await yt.resolveException(f.value.id,W),We.success("处理成功"),c.value=!1,M()}catch(W){We.error("处理失败: "+W.message)}finally{v.value=!1}}async function R(W){try{await yt.reopenException(W),We.success("已重新打开"),l.value=!1,M()}catch(N){We.error("操作失败: "+N.message)}}function G(){m.value={out_trade_no:"",note:""},h.value=!0}async function K(){try{await yt.addException(m.value),We.success("已加入异常池"),h.value=!1,M()}catch(W){We.error("操作失败: "+W.message)}}async function H(){g.value=!0;try{const W=await yt.listDeadNotify();y.value=(W.list||[]).map(N=>{let V="";try{N.payload&&(V=JSON.parse(N.payload).out_trade_no||"")}catch{}return{...N,out_trade_no:V}})}catch(W){We.error("加载失败: "+W.message)}finally{g.value=!1}}async function z(W){try{await yt.retryDeadNotify(W),We.success("已按最新回调地址重新入队"),H()}catch(N){We.error("重发失败: "+N.message)}}async function B(W){try{await er.confirm("彻底删除该死信通知任务?若该订单已支付但未通知到商户,删除后将无法自动补发。","确认删除",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{await yt.deleteDeadNotify(W),We.success("已删除"),H()}catch(N){We.error("删除失败: "+N.message)}}return mt(()=>{M(),S()}),(W,N)=>{const V=be("el-option"),Y=be("el-select"),X=be("el-button"),ee=be("el-table-column"),re=be("el-tag"),ie=be("el-table"),le=be("el-pagination"),ve=be("el-tab-pane"),he=be("el-date-picker"),J=be("el-tabs"),Z=be("el-descriptions-item"),j=be("el-descriptions"),oe=be("el-timeline-item"),se=be("el-timeline"),fe=be("el-dialog"),ge=be("el-radio"),Se=be("el-radio-group"),De=be("el-form-item"),Te=be("el-input-number"),Re=be("el-input"),Ce=be("el-form"),Le=Gr("loading");return x(),O("div",dNe,[L(J,{modelValue:t.value,"onUpdate:modelValue":N[4]||(N[4]=me=>t.value=me)},{default:$(()=>[L(ve,{label:"异常订单池",name:"exceptions"},{default:$(()=>[C("div",fNe,[L(Y,{modelValue:i.value.phase,"onUpdate:modelValue":N[0]||(N[0]=me=>i.value.phase=me),placeholder:"阶段",clearable:"",style:{width:"120px"},onChange:M},{default:$(()=>[L(V,{label:"待处理",value:"pending"}),L(V,{label:"处理中",value:"processing"}),L(V,{label:"已处理",value:"resolved"})]),_:1},8,["modelValue"]),L(Y,{modelValue:i.value.reason,"onUpdate:modelValue":N[1]||(N[1]=me=>i.value.reason=me),placeholder:"原因",clearable:"",style:{width:"140px"},onChange:M},{default:$(()=>[L(V,{label:"查单失败",value:"query_failed"}),L(V,{label:"金额不一致",value:"amount_mismatch"}),L(V,{label:"状态矛盾",value:"state_conflict"}),L(V,{label:"关单存疑",value:"close_uncertain"}),L(V,{label:"通知dead",value:"notify_dead"}),L(V,{label:"手动加入",value:"manual"})]),_:1},8,["modelValue"]),L(X,{type:"primary",onClick:G},{default:$(()=>[...N[21]||(N[21]=[ue("手动加入",-1)])]),_:1}),C("span",vNe,"共 "+Q(r.value)+" 条",1)]),st((x(),ne(ie,{data:a.value,size:"small",class:"mt10"},{default:$(()=>[L(ee,{prop:"out_trade_no",label:"订单号","min-width":"160","show-overflow-tooltip":""}),L(ee,{prop:"upstream_name",label:"上游",width:"120"}),L(ee,{prop:"reason",label:"原因",width:"110"},{default:$(({row:me})=>[L(re,{type:k(me.reason),size:"small"},{default:$(()=>[ue(Q(T(me.reason)),1)]),_:2},1032,["type"])]),_:1}),L(ee,{prop:"channel_status",label:"通道状态",width:"110"}),L(ee,{label:"阶段",width:"90"},{default:$(({row:me})=>[L(re,{type:me.phase==="resolved"?"success":me.phase==="processing"?"warning":"danger",size:"small",effect:"light"},{default:$(()=>[ue(Q(me.phase==="pending"?"待处理":me.phase==="processing"?"处理中":"已处理"),1)]),_:2},1032,["type"])]),_:1}),L(ee,{label:"金额",width:"90"},{default:$(({row:me})=>[C("b",null,"¥"+Q((me.amount_minor/100).toFixed(2)),1)]),_:1}),L(ee,{prop:"created_at",label:"创建时间",width:"160"}),L(ee,{label:"",width:"140",fixed:"right"},{default:$(({row:me})=>[L(X,{size:"small",text:"",type:"primary",onClick:at(Be=>E(me),["stop"])},{default:$(()=>[...N[22]||(N[22]=[ue("详情",-1)])]),_:1},8,["onClick"]),me.phase!=="resolved"?(x(),ne(X,{key:0,size:"small",text:"",type:"primary",onClick:at(Be=>I(me),["stop"])},{default:$(()=>[...N[23]||(N[23]=[ue("处理",-1)])]),_:1},8,["onClick"])):te("",!0)]),_:1})]),_:1},8,["data"])),[[Le,n.value]]),r.value>Ub?(x(),O("div",hNe,[L(le,{background:"",layout:"prev,pager,next",total:r.value,"page-size":Ub,"current-page":o.value,"onUpdate:currentPage":N[2]||(N[2]=me=>o.value=me),onCurrentChange:M},null,8,["total","current-page"])])):te("",!0)]),_:1}),L(ve,{label:"Dead 通知",name:"dead"},{default:$(()=>[st((x(),ne(ie,{data:y.value,size:"small",class:"mt10"},{default:$(()=>[L(ee,{prop:"id",label:"任务ID",width:"80"}),L(ee,{prop:"out_trade_no",label:"商户订单号","min-width":"160","show-overflow-tooltip":""}),L(ee,{prop:"retry_count",label:"重试次数",width:"90"}),L(ee,{prop:"created_at",label:"创建时间",width:"160"}),L(ee,{label:"",width:"150"},{default:$(({row:me})=>[L(X,{size:"small",type:"primary",onClick:Be=>z(me.id)},{default:$(()=>[...N[24]||(N[24]=[ue("重发",-1)])]),_:1},8,["onClick"]),L(X,{size:"small",type:"danger",onClick:Be=>B(me.id)},{default:$(()=>[...N[25]||(N[25]=[ue("删除",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[Le,g.value]])]),_:1}),L(ve,{label:"日终对账",name:"daily"},{default:$(()=>[C("div",pNe,[C("div",mNe,[L(he,{modelValue:b.value,"onUpdate:modelValue":N[3]||(N[3]=me=>b.value=me),type:"date","value-format":"YYYY-MM-DD",size:"small",placeholder:"选择日期"},null,8,["modelValue"]),L(X,{size:"small",type:"primary",loading:w.value,onClick:S},{default:$(()=>[...N[26]||(N[26]=[ue("生成报告",-1)])]),_:1},8,["loading"]),_.value?(x(),ne(re,{key:0,type:_.value.conclusion.startsWith("通过")?"success":"warning",size:"small"},{default:$(()=>[ue(Q(_.value.conclusion),1)]),_:1},8,["type"])):te("",!0)]),_.value?st((x(),O("div",gNe,[C("div",yNe,[C("div",bNe,[C("div",_Ne,Q(_.value.orders_total),1),N[27]||(N[27]=C("div",{class:"stat-label"},"当日订单",-1))]),C("div",wNe,[C("div",SNe,Q(_.value.orders_success),1),N[28]||(N[28]=C("div",{class:"stat-label"},"成功订单",-1))]),C("div",CNe,[C("div",xNe,"¥"+Q((_.value.success_amount/100).toFixed(2)),1),N[29]||(N[29]=C("div",{class:"stat-label"},"成功金额",-1))]),C("div",kNe,[C("div",{class:U(["stat-val",{"text-danger":_.value.notify_dead>0}])},Q(_.value.notify_dead),3),N[30]||(N[30]=C("div",{class:"stat-label"},"回调 Dead",-1))]),C("div",TNe,[C("div",{class:U(["stat-val",{"text-danger":_.value.exceptions_pending>0}])},Q(_.value.exceptions_pending),3),N[31]||(N[31]=C("div",{class:"stat-label"},"异常待处理",-1))])]),L(ie,{data:_.value.by_channel,stripe:"",size:"small",class:"mt16"},{default:$(()=>[L(ee,{prop:"channel",label:"通道","min-width":"160"}),L(ee,{prop:"total_orders",label:"订单",width:"80"}),L(ee,{prop:"success_orders",label:"成功",width:"80"}),L(ee,{label:"成功金额",width:"110"},{default:$(({row:me})=>[ue("¥"+Q((me.amount_minor/100).toFixed(2)),1)]),_:1}),L(ee,{prop:"closed_orders",label:"关闭",width:"80"}),L(ee,{label:"关闭金额",width:"110"},{default:$(({row:me})=>[ue("¥"+Q((me.amount_closed/100).toFixed(2)),1)]),_:1})]),_:1},8,["data"])])),[[Le,w.value]]):(x(),O("div",MNe,"选择日期生成日终对账报告"))])]),_:1})]),_:1},8,["modelValue"]),L(fe,{modelValue:l.value,"onUpdate:modelValue":N[9]||(N[9]=me=>l.value=me),title:"异常单详情",width:"700px"},{footer:$(()=>[L(X,{onClick:N[5]||(N[5]=me=>l.value=!1)},{default:$(()=>[...N[34]||(N[34]=[ue("关闭",-1)])]),_:1}),s.value&&s.value.phase!=="resolved"?(x(),ne(X,{key:0,type:"primary",onClick:N[6]||(N[6]=me=>{l.value=!1,I(s.value)})},{default:$(()=>[...N[35]||(N[35]=[ue("处理",-1)])]),_:1})):te("",!0),s.value&&s.value.phase==="resolved"?(x(),ne(X,{key:1,text:"",onClick:N[7]||(N[7]=me=>R(s.value.id))},{default:$(()=>[...N[36]||(N[36]=[ue("重新打开",-1)])]),_:1})):te("",!0),L(X,{text:"",onClick:N[8]||(N[8]=me=>{var Be;return A((Be=s.value)==null?void 0:Be.id)})},{default:$(()=>[...N[37]||(N[37]=[ue("再次查单",-1)])]),_:1})]),default:$(()=>[s.value?(x(),O(Ve,{key:0},[L(j,{column:2,border:"",size:"small"},{default:$(()=>[L(Z,{label:"订单号",span:2},{default:$(()=>[C("span",ENe,Q(s.value.out_trade_no),1)]),_:1}),L(Z,{label:"上游"},{default:$(()=>[ue(Q(s.value.upstream_name||"—"),1)]),_:1}),L(Z,{label:"通道"},{default:$(()=>[ue(Q(s.value.channel),1)]),_:1}),L(Z,{label:"金额"},{default:$(()=>[ue("¥"+Q((s.value.amount_minor/100).toFixed(2)),1)]),_:1}),L(Z,{label:"通道状态"},{default:$(()=>[ue(Q(s.value.channel_status),1)]),_:1}),L(Z,{label:"交易号"},{default:$(()=>[C("span",ANe,Q(s.value.channel_tx_id||"—"),1)]),_:1}),L(Z,{label:"原因",span:2},{default:$(()=>[ue(Q(T(s.value.reason)),1)]),_:1}),L(Z,{label:"建议",span:2},{default:$(()=>[ue(Q(s.value.suggestion||"—"),1)]),_:1}),L(Z,{label:"处理人"},{default:$(()=>[ue(Q(s.value.handler||"—"),1)]),_:1}),L(Z,{label:"处理动作"},{default:$(()=>[ue(Q(s.value.resolved_action||"—"),1)]),_:1}),L(Z,{label:"备注",span:2},{default:$(()=>[ue(Q(s.value.handler_note||"—"),1)]),_:1})]),_:1}),N[32]||(N[32]=C("h4",{class:"section-title"},"证据时间线",-1)),C("pre",INe,Q(s.value.evidence||"(无)"),1),N[33]||(N[33]=C("h4",{class:"section-title"},"操作日志",-1)),u.value&&u.value.length?(x(),ne(se,{key:0},{default:$(()=>[(x(!0),O(Ve,null,dt(u.value,me=>(x(),ne(oe,{key:me.id,timestamp:me.created_at,placement:"top"},{default:$(()=>[C("p",null,[C("b",null,Q(me.action),1),ue(" — "+Q(me.operator),1)]),me.detail?(x(),O("p",LNe,Q(me.detail),1)):te("",!0)]),_:2},1032,["timestamp"]))),128))]),_:1})):(x(),O("p",PNe,"无操作记录"))],64)):te("",!0)]),_:1},8,["modelValue"]),L(fe,{modelValue:c.value,"onUpdate:modelValue":N[16]||(N[16]=me=>c.value=me),title:"人工处理",width:"500px"},{footer:$(()=>[L(X,{onClick:N[15]||(N[15]=me=>c.value=!1)},{default:$(()=>[...N[42]||(N[42]=[ue("取消",-1)])]),_:1}),L(X,{type:"primary",onClick:P,loading:v.value},{default:$(()=>[...N[43]||(N[43]=[ue("确认处理",-1)])]),_:1},8,["loading"])]),default:$(()=>[L(Ce,{model:d.value,"label-width":"100px"},{default:$(()=>[L(De,{label:"动作"},{default:$(()=>[L(Se,{modelValue:d.value.action,"onUpdate:modelValue":N[10]||(N[10]=me=>d.value.action=me)},{default:$(()=>[L(ge,{value:"settle"},{default:$(()=>[...N[38]||(N[38]=[ue("确认入账",-1)])]),_:1}),L(ge,{value:"close"},{default:$(()=>[...N[39]||(N[39]=[ue("确认未支付,关单",-1)])]),_:1}),L(ge,{value:"noop"},{default:$(()=>[...N[40]||(N[40]=[ue("无需处理",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.value.action==="settle"?(x(),ne(De,{key:0,label:"实际金额"},{default:$(()=>[L(Te,{modelValue:d.value.paid_amount,"onUpdate:modelValue":N[11]||(N[11]=me=>d.value.paid_amount=me),min:1,precision:2},null,8,["modelValue"]),N[41]||(N[41]=C("span",{class:"text-muted",style:{"margin-left":"8px"}},"元",-1))]),_:1})):te("",!0),d.value.action==="settle"?(x(),ne(De,{key:1,label:"通道交易号"},{default:$(()=>[L(Re,{modelValue:d.value.channel_tx_id,"onUpdate:modelValue":N[12]||(N[12]=me=>d.value.channel_tx_id=me),placeholder:"从支付宝账单核对后填入"},null,8,["modelValue"])]),_:1})):te("",!0),d.value.action==="settle"?(x(),ne(De,{key:2,label:"付款方"},{default:$(()=>[L(Re,{modelValue:d.value.payer,"onUpdate:modelValue":N[13]||(N[13]=me=>d.value.payer=me),placeholder:"可选"},null,8,["modelValue"])]),_:1})):te("",!0),L(De,{label:"备注"},{default:$(()=>[L(Re,{modelValue:d.value.note,"onUpdate:modelValue":N[14]||(N[14]=me=>d.value.note=me),type:"textarea",rows:2,placeholder:"处理说明"},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["modelValue"]),L(fe,{modelValue:h.value,"onUpdate:modelValue":N[20]||(N[20]=me=>h.value=me),title:"手动加入异常池",width:"400px"},{footer:$(()=>[L(X,{onClick:N[19]||(N[19]=me=>h.value=!1)},{default:$(()=>[...N[44]||(N[44]=[ue("取消",-1)])]),_:1}),L(X,{type:"primary",onClick:K},{default:$(()=>[...N[45]||(N[45]=[ue("加入",-1)])]),_:1})]),default:$(()=>[L(Ce,{model:m.value,"label-width":"100px"},{default:$(()=>[L(De,{label:"订单号"},{default:$(()=>[L(Re,{modelValue:m.value.out_trade_no,"onUpdate:modelValue":N[17]||(N[17]=me=>m.value.out_trade_no=me),placeholder:"输入订单号"},null,8,["modelValue"])]),_:1}),L(De,{label:"说明"},{default:$(()=>[L(Re,{modelValue:m.value.note,"onUpdate:modelValue":N[18]||(N[18]=me=>m.value.note=me),type:"textarea",rows:2},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["modelValue"])])}}},ONe=fr(DNe,[["__scopeId","data-v-e25116cd"]]),oV=F(0);function O3(){oV.value++}const $Ne={class:"device-stream"},RNe={class:"stream-wrap"},NNe={class:"stream-stage"},BNe={key:0,class:"stream-overlay"},VNe={class:"stream-msg"},zNe={__name:"DeviceStream",props:{serials:{type:Array,default:()=>[]},deviceId:{type:Number,default:0}},setup(e){const t=e,n=F([...t.serials]);ye(()=>t.serials,K=>{n.value=[...K]});const a=F(null),r=F(!1),o=F(!1),i=F("连接中…"),l=F("");let s=null,u=null,c=null,d={width:0,height:0},v=null,f=window.devicePixelRatio||1,h=0,m=!1,g=0;function y(){const K=n.value[h]||"",H=location.protocol==="https:"?"wss":"ws",z=sessionStorage.getItem("admin_token")||"";return`${H}://${location.host}/api/personal/devices/stream?serial=${encodeURIComponent(K)}&token=${encodeURIComponent(z)}`}function b(){if(!r.value){if(!n.value.length){w("设备未配置 adb 通道");return}r.value=!0,m=!1,l.value="",i.value=`连接中…(${h+1}/${n.value.length})`;try{s=new WebSocket(y())}catch(K){w("WebSocket 创建失败: "+K.message);return}s.binaryType="arraybuffer",s.onopen=()=>{o.value=!0,i.value="已连接",g=0},s.onmessage=S,s.onclose=()=>{o.value=!1,r.value=!1,M(),!m&&(h=3){w("连接已断开(多次重建失败,请检查设备网络/USB)");return}g++,r.value=!0,i.value=`adb 通道离线,正在重建连接(${g}/3)…`;try{const K=await yt.personalWifiReconnect(t.deviceId),H=K&&K.serial;if(!H)throw new Error("无可用通道");n.value.includes(H)||n.value.unshift(H),h=0,l.value="",setTimeout(b,300)}catch(K){i.value="连接已断开(等待重试)",l.value=K.message||"连接已断开",setTimeout(_,3e3)}}function w(K){r.value=!1,o.value=!1,l.value=K,i.value=K}function S(K){if(typeof K.data=="string"){try{const W=JSON.parse(K.data);W.type==="meta"&&(d=W,E())}catch{}return}const H=new Uint8Array(K.data);if(H.length<1)return;const z=H[0],B=H.slice(1);if(u||k(),!!u)try{const W=new EncodedVideoChunk({type:z===1||z===2?"key":"delta",timestamp:0,data:B});u.decode(W)}catch{}}function k(){if(!u){if(typeof VideoDecoder>"u"){l.value="浏览器不支持 WebCodecs(请使用 Chrome/Edge 94+)",i.value=l.value;return}u=new VideoDecoder({output:K=>{T(K),K.close()},error:()=>{u=null}});try{u.configure({codec:"avc1.42E01E",avc:{format:"annexb"},optimizeForLatency:!0}),i.value="解码就绪"}catch(K){l.value="解码器配置失败: "+K.message,i.value=l.value,u=null}}}function T(K){const H=a.value;if(!H)return;c=c||H.getContext("2d");const z=H.clientWidth,B=H.clientHeight,W=Math.min(z/K.displayWidth,B/K.displayHeight),N=K.displayWidth*W,V=K.displayHeight*W,Y=(z-N)/2,X=(B-V)/2;c.clearRect(0,0,H.width,H.height),c.drawImage(K,Y,X,N,V)}function M(){if(u){try{u.close()}catch{}u=null}c=null}function E(){const K=a.value;!K||!d.width||(K.width=Math.round(K.clientWidth*f),K.height=Math.round(K.clientHeight*f),c=K.getContext("2d"),c.setTransform(f,0,0,f,0,0))}function A(K){s&&s.readyState===WebSocket.OPEN&&s.send(JSON.stringify(K))}function I(K){const H=a.value;if(!H||!d.width||!d.height)return null;const z=H.getBoundingClientRect(),B=Math.min(z.width/d.width,z.height/d.height),W=d.width*B,N=d.height*B,V=(z.width-W)/2,Y=(z.height-N)/2,X=K.clientX-z.left,ee=K.clientY-z.top;return XV+W||eeY+N?null:{x:Math.round((X-V)/B),y:Math.round((ee-Y)/B)}}function P(K){if(!d.width)return;const H=I(K);H&&(v=H,a.value.setPointerCapture(K.pointerId),A({type:"touch",action:0,x:H.x,y:H.y}))}function R(K){if(!v||!d.width)return;const H=I(K);H&&A({type:"touch",action:2,x:H.x,y:H.y})}function G(){v&&(A({type:"touch",action:1,x:v.x,y:v.y}),v=null)}return mt(()=>{b(),window.addEventListener("resize",E)}),rn(()=>{window.removeEventListener("resize",E),m=!0,s&&s.close(),M()}),(K,H)=>{const z=be("el-icon"),B=be("el-button");return x(),O("div",$Ne,[C("div",RNe,[C("div",NNe,[C("canvas",{ref_key:"canvasEl",ref:a,class:"stream-canvas",onPointerdown:P,onPointermove:R,onPointerup:G,onPointercancel:G,onContextmenu:H[0]||(H[0]=at(()=>{},["prevent"]))},null,544),o.value?te("",!0):(x(),O("div",BNe,[r.value?(x(),ne(z,{key:0,class:"is-loading",size:40},{default:$(()=>[L(p(Mr))]),_:1})):(x(),ne(z,{key:1,size:40},{default:$(()=>[L(p($s))]),_:1})),C("div",VNe,Q(i.value),1),!r.value&&l.value?(x(),ne(B,{key:2,size:"small",type:"primary",onClick:b},{default:$(()=>[...H[1]||(H[1]=[ue("重连",-1)])]),_:1})):te("",!0)]))])])])}}},FNe=fr(zNe,[["__scopeId","data-v-c99e0b77"]]),HNe={class:"personal-devices-page"},WNe={class:"pg-card"},KNe={class:"pg-card-header"},UNe={class:"header-actions"},GNe={key:0,class:"text-muted",style:{"font-size":"12px"}},YNe={key:1,class:"text-muted",style:{"font-size":"12px",color:"var(--el-color-warning)"}},jNe={class:"pg-card-body"},qNe={class:"guide-content"},XNe={class:"pg-card"},ZNe={class:"pg-card-body"},JNe={class:"device-grid"},QNe=["onClick"],eBe={key:0,class:"phone-checkbox"},tBe={class:"phone-frame"},nBe={class:"phone-screen"},aBe=["src"],rBe={key:1,class:"phone-placeholder"},oBe={class:"phone-placeholder-name"},iBe=["onClick"],lBe={class:"phone-info"},sBe={class:"phone-name"},uBe={class:"phone-meta"},cBe={key:6,class:"snapshot-tag"},dBe={key:0,class:"empty-state"},fBe={key:0,class:"batch-actions-bar"},vBe={class:"batch-actions-left"},hBe={class:"batch-actions-right"},pBe={key:0,class:"console-wrap"},mBe={class:"console-header"},gBe={class:"console-device-info"},yBe={class:"device-name"},bBe={class:"font-mono text-muted",style:{"font-size":"11px"}},_Be={class:"font-mono text-muted",style:{"font-size":"11px"}},wBe={style:{"margin-top":"4px"}},SBe={class:"console-header-actions"},CBe={key:0,class:"console-stream"},xBe={key:0,style:{"margin-bottom":"12px"}},kBe={class:"wizard-body"},TBe={key:0},MBe={key:0,style:{"text-align":"center",padding:"16px 0"}},EBe={key:1,style:{"text-align":"center",padding:"32px 0"}},ABe={key:2},IBe={class:"usb-device-list"},LBe=["onClick"],PBe={class:"usb-device-info"},DBe={class:"usb-device-serial font-mono"},OBe={class:"usb-device-model text-muted",style:{"font-size":"12px"}},$Be={class:"usb-device-right"},RBe={key:1},NBe={style:{"text-align":"center",padding:"24px 0"}},BBe={key:0,style:{"margin-top":"16px"}},VBe={key:1,style:{"margin-top":"16px"}},zBe={key:2,style:{"margin-top":"16px",color:"var(--text-muted)","font-size":"13px"}},FBe={key:2},HBe={key:3},WBe={key:0},KBe={style:{"text-align":"center",padding:"24px 0"}},UBe={key:1,style:{"text-align":"center",padding:"16px 0"}},GBe={style:{"margin-top":"20px"}},YBe={__name:"PersonalDevices",props:{refreshKey:{type:Number,default:0}},setup(e){const t=e,n=F([]),a=F(!1),r=F([]),o=F(!1),i=F(null),l=D(()=>{const Ee=i.value;if(!Ee)return[];const de=[];return Ee.adb_host&&de.push(Ee.adb_port?`${Ee.adb_host}:${Ee.adb_port}`:Ee.adb_host),Ee.adb_wifi_host&&Ee.adb_wifi_port&&de.push(`${Ee.adb_wifi_host}:${Ee.adb_wifi_port}`),de}),s=F(!1),u=F(new Set),c=D(()=>n.value.length>0&&u.value.size===n.value.length),d=D(()=>u.value.size),v=D(()=>n.value.filter(Ee=>Ee.status==="online").length);function f(){s.value=!s.value,s.value||u.value.clear()}function h(Ee){u.value.has(Ee)?u.value.delete(Ee):u.value.add(Ee),u.value=new Set(u.value)}function m(){c.value?u.value.clear():u.value=new Set(n.value.map(Ee=>Ee.id)),u.value=new Set(u.value)}function g(){u.value.clear(),u.value=new Set(u.value)}function y(Ee){return{online:"success",offline:"info",frozen:"danger"}[Ee]||"info"}function b(Ee){return{online:"在线",offline:"离线",frozen:"已冻结"}[Ee]||Ee}function _(Ee){return Ee?Ee.includes("wechat")?"微信":Ee.includes("alipay")?"支付宝":Ee:""}function w(Ee){const de=Ee&&Ee.control_state;return de?de==="usb"?{type:"success",label:"控制面:USB"}:de==="relay"?{type:"primary",label:"控制面:反代"}:de==="tunnel_only"?{type:"info",label:"控制面:隧道"}:{type:"info",label:"控制面:"+de}:null}async function S(){a.value=!0;try{const Ee=await yt.personalDevices(),de=new Map(n.value.map(Dt=>[Dt.id,Dt])),it=Ee.devices||[];for(const Dt of it){const sn=de.get(Dt.id);sn&&sn._cachedSnapshot&&(Dt._cachedSnapshot=sn._cachedSnapshot)}n.value=it}catch(Ee){We.error("加载设备失败:"+Ee.message)}finally{a.value=!1}}async function k(){try{const Ee=await yt.accounts({});r.value=(Ee||[]).filter(de=>String(de.channel).startsWith("personal."))}catch{}}async function T(Ee){await er.confirm(`确认冻结设备「${Ee.name||Ee.device_id}」?`,"冻结设备",{type:"warning"});try{await yt.freezePersonalDevice(Ee.id),We.success("已冻结"),S()}catch(de){We.error("冻结失败:"+de.message)}}async function M(Ee){try{await yt.unfreezePersonalDevice(Ee.id),We.success("已解冻"),S()}catch(de){We.error("解冻失败:"+de.message)}}const E=F({});async function A(Ee){await er.confirm(`确认重启设备「${Ee.name||Ee.device_id}」?重启可能需要 30-60 秒。`,"重启设备",{type:"warning"}),E.value={...E.value,[Ee.id]:!0};try{const de=await yt.restartPersonalDevice(Ee.id),Dt=(de&&de.stages||[]).map(gn=>`${gn.stage}:${gn.ok===!0?"成功":"失败"}`).join(" / ");!!de&&de.ok===!0?We.success(`重启完成${Dt?"("+Dt+")":""}`):We.error(`重启失败${Dt?"("+Dt+")":""}`),setTimeout(()=>S(),8e3)}catch(de){We.error("重启失败:"+de.message)}finally{E.value={...E.value,[Ee.id]:!1}}}const I=F({});async function P(Ee){await er.confirm(`确认预热设备「${Ee.name||Ee.device_id}」?将执行冷启动导航到支付宝账本页(约 18 秒)。`,"手动预热",{type:"info"}),I.value={...I.value,[Ee.id]:!0};try{const de=await yt.personalDeviceAction(Ee.id,{action:"warmup"});de&&de.data&&de.data.ok?We.success("预热完成:账本页已就绪,任务卡片已锁定"):We.error("预热失败")}catch(de){We.error("预热失败:"+de.message)}finally{I.value={...I.value,[Ee.id]:!1}}}async function R(Ee){await er.confirm(`确认删除设备「${Ee.name||Ee.device_id}」?`,"删除设备",{type:"error"});try{await yt.deletePersonalDevice(Ee.id),We.success("已删除"),S()}catch(de){We.error("删除失败:"+de.message)}}function G(Ee){i.value=Ee,o.value=!0,N(Ee.id)}function K(){i.value=null,H.value=[]}const H=F([]),z=F(!1),B=F(!1),W=F({account_id:null});async function N(Ee){try{const de=await yt.personalDeviceAccounts(Ee);H.value=de&&de.accounts||[];const it=i.value;it&&de&&(it._accountCount=(de.accounts||[]).length)}catch{}}const V=D(()=>{const Ee=new Set(H.value.map(de=>de.account_id));return r.value.filter(de=>Ee.has(de.id)?!1:String(de.channel).startsWith("personal."))});function Y(){W.value={account_id:null},z.value=!0,k()}async function X(){const Ee=i.value;if(!(!Ee||!W.value.account_id)){B.value=!0;try{await yt.personalAddDeviceAccount(Ee.id,{account_id:W.value.account_id}),We.success("账户已绑定(下次心跳后生效)"),z.value=!1,await N(Ee.id),S()}catch(de){We.error("绑定失败:"+de.message)}finally{B.value=!1}}}async function ee(Ee){const de=i.value;if(de){await er.confirm(`确认解绑账户「${Ee.name}」?该账户将不再由本设备服务。`,"解绑账户",{type:"warning"});try{await yt.personalRemoveDeviceAccount(de.id,Ee.account_id),We.success("已解绑"),await N(de.id),S()}catch(it){We.error("解绑失败:"+it.message)}}}const re=F(!1),ie=F(!1),le=F({platform:"alipay",user:0});function ve(){i.value&&(le.value={platform:"alipay",user:0},re.value=!0)}async function he(){const Ee=i.value;if(!(!Ee||!Ee.device_id)){ie.value=!0;try{le.value.platform==="wechat"?await yt.personalCaptureWechatQrcode(Ee.device_id,le.value.user):await yt.personalCaptureAlipayQrcode(Ee.device_id,le.value.user),We.success("收款码捕获成功"),re.value=!1,O3(),S()}catch(de){We.error("捕获失败:"+de.message)}finally{ie.value=!1}}}const J=F(!1),Z=F(!1),j=F({adb_host:"",adb_port:null,adb_wifi_host:"",adb_wifi_port:null});function oe(){const Ee=i.value;Ee&&(j.value={adb_host:Ee.adb_host||"",adb_port:Ee.adb_port||null,adb_wifi_host:Ee.adb_wifi_host||"",adb_wifi_port:Ee.adb_wifi_port||null},J.value=!0)}async function se(){const Ee=i.value;if(Ee){Z.value=!0;try{await yt.personalSetAdb(Ee.id,j.value),We.success("ADB 通道已更新,下次操作生效"),Object.assign(Ee,j.value),J.value=!1,S()}catch(de){We.error("保存失败:"+de.message)}finally{Z.value=!1}}}const fe=F(!1),ge=F(0),Se=F(!1),De=F([]),Te=F(""),Re=F(null),Ce=F(null),Le=F({name:"",account_id:null}),me=F(!1),Be=F(""),ze=F(""),xe=F(!1),rt=F(""),nt=F(!1),Pt=F(""),Wt=F(""),Jt=F("");let Rt=null;function Qt(Ee){const de=Ee.setup_progress;return!de||!de.length?!1:de.includes("usb_connected")}function _e(Ee){const de=Ee.setup_progress;return de?[...new Set(de)].length:0}function Ne(){fe.value=!0,ge.value=0,De.value=[],Te.value="",Re.value=null,Ce.value=null,Le.value={name:"",account_id:null},me.value=!1,Be.value="",ze.value="",xe.value=!1,rt.value="",nt.value=!1,Pt.value="",Wt.value="",Jt.value="",k()}function tt(){Ue(),Ce.value=null}function Me(){fe.value=!1}function Ue(){Rt!==null&&(clearInterval(Rt),Rt=null)}async function Gt(){if(fe.value){Se.value=!0;try{const Ee=await yt.personalUsbDetect(),de=Ee&&Ee.devices||[];if(De.value=de.filter(it=>!it.bound),Te.value){const it=De.value.find(Dt=>Dt.serial===Te.value)||de.find(Dt=>Dt.serial===Te.value);it&&(Re.value=it)}}catch{}finally{Se.value=!1}}}function je(Ee){Te.value=Ee.serial,Re.value=Ee}function ft(){Te.value&&(ge.value=1)}function ht(){ge.value=2}function pt(){Ue(),ge.value-=1,ge.value<=1&&(Gt(),Rt===null&&(Rt=setInterval(Gt,2500)))}function It(){if(!Le.value.name.trim())return We.warning("请填写设备名称");if(!Le.value.account_id)return We.warning("请选择收款账户");me.value=!0;const Ee={serial:Te.value,name:Le.value.name,account_id:Le.value.account_id};yt.personalWizardStart(Ee).then(de=>{Ce.value=de.id||de.device_id,Ue(),Ct(),ge.value=3}).catch(de=>{We.error("绑定设备失败:"+de.message)}).finally(()=>{me.value=!1})}function Ct(){const Ee=Ce.value;Ee&&(Be.value="running",ze.value="",yt.personalWizardWifi(Ee).then(de=>{de&&de.ok?(Be.value="done",ze.value=`${de.wifi_host}:${de.wifi_port}`):(Be.value="skipped",ze.value=de&&de.warning||"配置失败(USB 直连不受影响)")}).catch(()=>{Be.value="skipped",ze.value="配置失败(USB 直连不受影响)"}))}function $t(){Ce.value&&(xe.value=!0,rt.value="",yt.personalWizardVerify(Ce.value).then(Ee=>{Pt.value=Ee.hint||"",Wt.value=Ee.warning||"",Jt.value=Ee.keepalive_hint||"",nt.value=!0}).catch(Ee=>{rt.value="下发配置失败:"+Ee.message}).finally(()=>{xe.value=!1}))}function Sn(){fe.value=!1,S()}const Fe=F(null);function qe(){const Ee=Array.from(u.value);return Ee.length===n.value.length?{all:!0}:{ids:Ee}}function ut(Ee,de){const it=Ee.results||[],Dt=Ee.total||it.length,sn=Ee.missing_ids||[],gn=it.filter(Ia=>Ia.ok).length,Aa=it.filter(Ia=>!Ia.ok).length,jr=it.filter(Ia=>Ia.throttled);let vr=`${de}完成:共 ${Dt} 台 +成功 ${gn} 台`;if(Aa>0){vr+=` +失败 ${Aa} 台:`;const Ia=it.filter(Oa=>!Oa.ok).slice(0,5);vr+=Ia.map(Oa=>`${Oa.device_id}(${Oa.error||"未知错误"})`).join(";"),it.filter(Oa=>!Oa.ok).length>5&&(vr+="...")}return jr.length>0&&(vr+=` +距上次重启不足 5 分钟已跳过 ${jr.length} 台`),sn.length>0&&(vr+=` +不存在或其他原因跳过 ${sn.length} 台(id: ${sn.join(",")})`),vr}async function Ie(){const Ee=d.value,de=c.value;await er.confirm(de?`确认重启全部 ${n.value.length} 台设备?`:`确认重启选中的 ${Ee} 台设备?`,"批量重启",{type:"warning"}),Fe.value="restart";try{const it=qe(),Dt=await yt.personalBatchRestart(it),sn=ut(Dt,"重启");(Dt.results||[]).some(Aa=>!Aa.ok)||(Dt.missing_ids||[]).length>0?We.warning(sn):We.success(sn),S(),g(),s.value=!1}catch(it){We.error("批量重启失败:"+it.message)}finally{Fe.value=null}}async function Ke(){const Ee=d.value,de=c.value;await er.confirm(de?`确认冻结全部 ${n.value.length} 台设备?`:`确认冻结选中的 ${Ee} 台设备?`,"批量冻结",{type:"warning"}),Fe.value="freeze";try{const it=qe(),Dt=await yt.personalBatchStatus({...it,status:"frozen"}),sn=ut(Dt,"冻结");(Dt.results||[]).some(Aa=>!Aa.ok)||(Dt.missing_ids||[]).length>0?We.warning(sn):We.success(sn),S(),g(),s.value=!1}catch(it){We.error("批量冻结失败:"+it.message)}finally{Fe.value=null}}async function Ge(){const Ee=d.value,de=c.value;await er.confirm(de?`确认解冻全部 ${n.value.length} 台设备?`:`确认解冻选中的 ${Ee} 台设备?`,"批量解冻",{type:"warning"}),Fe.value="unfreeze";try{const it=qe(),Dt=await yt.personalBatchStatus({...it,status:"offline"}),sn=ut(Dt,"解冻");(Dt.results||[]).some(Aa=>!Aa.ok)||(Dt.missing_ids||[]).length>0?We.warning(sn):We.success(sn),S(),g(),s.value=!1}catch(it){We.error("批量解冻失败:"+it.message)}finally{Fe.value=null}}return mt(S),ye(()=>t.refreshKey,S),ye(()=>fe.value,Ee=>{Ee?(Gt(),Rt=setInterval(Gt,2500)):Ue()}),Us(Ue),(Ee,de)=>{const it=be("el-button"),Dt=be("el-icon"),sn=be("el-alert"),gn=be("el-tag"),Aa=be("el-tab-pane"),jr=be("el-tabs"),vr=be("el-collapse-item"),Ia=be("el-collapse"),Oa=be("el-checkbox"),Oe=be("el-dialog"),lt=be("el-input"),Bt=be("el-form-item"),_n=be("el-form"),Yn=be("el-option"),Zn=be("el-select"),ja=be("el-radio"),Gl=be("el-radio-group"),co=be("el-step"),Yl=be("el-steps"),Xs=Gr("loading");return x(),O("div",HNe,[C("div",WNe,[C("div",KNe,[de[22]||(de[22]=C("span",{class:"section-label"},"监听端设备",-1)),C("div",UNe,[s.value?(x(),O("span",YNe,"已选 "+Q(d.value)+" 台",1)):(x(),O("span",GNe,"共 "+Q(n.value.length)+" 台 · 在线 "+Q(v.value)+" 台",1)),s.value?(x(),ne(it,{key:3,size:"small",onClick:f},{default:$(()=>[...de[20]||(de[20]=[ue("退出批量",-1)])]),_:1})):(x(),ne(it,{key:2,type:"warning",size:"small",onClick:f},{default:$(()=>[...de[19]||(de[19]=[ue(" 批量操作 ",-1)])]),_:1})),L(it,{type:"success",size:"small",onClick:Ne},{default:$(()=>[L(Dt,null,{default:$(()=>[L(p(hf))]),_:1}),de[21]||(de[21]=ue(" USB 引导接入 ",-1))]),_:1})])]),C("div",jNe,[L(sn,{type:"info",closable:!1,class:"mb16",title:"ADB 真机绑定个人收款账户:设备上线由 adb 连通性判定(USB 直连 / WiFi 备用)。"}),L(Ia,{class:"integration-guide"},{default:$(()=>[L(vr,{name:"guide"},{title:$(()=>[de[24]||(de[24]=C("span",{class:"guide-title"},"集成流程(USB 真机)",-1)),L(gn,{type:"info",size:"small",effect:"plain",round:"",class:"guide-tag"},{default:$(()=>[...de[23]||(de[23]=[ue("点击展开",-1)])]),_:1})]),default:$(()=>[C("div",qNe,[L(jr,null,{default:$(()=>[L(Aa,{label:"真机 USB 直连"},{default:$(()=>[de[25]||(de[25]=C("ol",{class:"step-list"},[C("li",null,[C("b",null,"接入引导"),ue(":点击右上「USB 引导接入」,向导自动检测已连接的 USB 真机(手机插 USB + 开启开发者选项 + USB 调试)。")]),C("li",null,[C("b",null,"授权调试"),ue(":手机会弹出「允许 USB 调试」,点击允许,向导自动进入下一步。")]),C("li",null,[C("b",null,"绑定设备"),ue(":填设备名称、选择收款账户,向导自动创建设备记录。")]),C("li",null,[C("b",null,"ADB 连通即上线"),ue(":USB 直连后设备自动上线,WiFi 备用通道自动配置(断线自动切换保活)。")])],-1)),L(sn,{type:"info",closable:!1,class:"mt8",title:"历史变更(2026-08-17):redroid 容器载体方案已废弃;个人免签监听端统一走 USB 引导接入自动检测,手填「新增设备」表单已移除。"})]),_:1})]),_:1})])]),_:1})]),_:1})])]),C("div",XNe,[st((x(),O("div",ZNe,[C("div",JNe,[(x(!0),O(Ve,null,dt(n.value,pe=>(x(),O("div",{key:pe.id,class:U(["phone-card",{"phone-card-selected":u.value.has(pe.id)}]),onClick:ct=>s.value?h(pe.id):G(pe)},[s.value?(x(),O("div",eBe,[L(Oa,{"model-value":u.value.has(pe.id),onClick:de[0]||(de[0]=at(()=>{},["stop"])),onChange:ct=>h(pe.id)},null,8,["model-value","onChange"])])):te("",!0),C("div",tBe,[de[27]||(de[27]=C("div",{class:"phone-notch"},null,-1)),C("div",nBe,[pe._cachedSnapshot?(x(),O("img",{key:0,src:pe._cachedSnapshot,class:"phone-screenshot"},null,8,aBe)):(x(),O("div",rBe,[de[26]||(de[26]=C("div",{class:"phone-placeholder-icon"},"📱",-1)),C("div",oBe,Q(pe.name||"未命名"),1)]))]),C("div",{class:"phone-home",onClick:at(ct=>G(pe),["stop"])},null,8,iBe)]),C("div",lBe,[C("div",sBe,Q(pe.name||"未命名"),1),C("div",uBe,[L(gn,{type:y(pe.status),size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(b(pe.status)),1)]),_:2},1032,["type"]),L(gn,{type:pe.provider==="alipay"?"primary":"success",size:"small",effect:"plain",round:""},{default:$(()=>[ue(Q(pe.provider==="alipay"?"支付宝":"微信"),1)]),_:2},1032,["type"]),pe._accountCount>1?(x(),ne(gn,{key:0,type:"primary",size:"small",effect:"plain",round:""},{default:$(()=>[ue("+"+Q(pe._accountCount-1)+" 账户",1)]),_:2},1024)):te("",!0),pe.adb_host?(x(),ne(gn,{key:1,type:"info",size:"small",effect:"plain",round:""},{default:$(()=>[...de[28]||(de[28]=[ue("USB",-1)])]),_:1})):pe.adb_wifi_host?(x(),ne(gn,{key:2,type:"info",size:"small",effect:"plain",round:""},{default:$(()=>[...de[29]||(de[29]=[ue("WiFi",-1)])]),_:1})):te("",!0),pe.adb_host&&pe.adb_wifi_host?(x(),ne(gn,{key:3,type:"warning",size:"small",effect:"plain",round:""},{default:$(()=>[...de[30]||(de[30]=[ue("双通道",-1)])]),_:1})):te("",!0),w(pe)?(x(),ne(gn,{key:4,type:w(pe).type,size:"small",effect:"plain",round:""},{default:$(()=>[ue(Q(w(pe).label),1)]),_:2},1032,["type"])):te("",!0),pe.ts_ip?(x(),ne(gn,{key:5,type:"info",size:"small",effect:"plain",round:"",class:"font-mono"},{default:$(()=>[ue(Q(pe.ts_ip),1)]),_:2},1024)):te("",!0),pe._cachedSnapshot?(x(),O("span",cBe,"快照")):te("",!0),Qt(pe)?(x(),ne(gn,{key:7,type:"warning",size:"small",effect:"plain",round:""},{default:$(()=>[ue("配置中 "+Q(_e(pe))+"/5",1)]),_:2},1024)):te("",!0)])]),C("div",{class:"phone-actions",onClick:de[1]||(de[1]=at(()=>{},["stop"]))},[L(it,{size:"small",type:"primary",text:"",onClick:ct=>G(pe)},{default:$(()=>[...de[31]||(de[31]=[ue("控制台",-1)])]),_:1},8,["onClick"]),pe.status!=="frozen"?(x(),ne(it,{key:0,size:"small",type:"danger",text:"",onClick:ct=>T(pe)},{default:$(()=>[...de[32]||(de[32]=[ue("冻结",-1)])]),_:1},8,["onClick"])):(x(),ne(it,{key:1,size:"small",type:"success",text:"",onClick:ct=>M(pe)},{default:$(()=>[...de[33]||(de[33]=[ue("解冻",-1)])]),_:1},8,["onClick"])),L(it,{size:"small",type:"danger",text:"",onClick:ct=>R(pe)},{default:$(()=>[...de[34]||(de[34]=[ue("删除",-1)])]),_:1},8,["onClick"]),L(it,{size:"small",type:"warning",text:"",loading:E.value[pe.id],onClick:ct=>A(pe)},{default:$(()=>[...de[35]||(de[35]=[ue("重启",-1)])]),_:1},8,["loading","onClick"]),L(it,{size:"small",type:"success",text:"",loading:I.value[pe.id],onClick:ct=>P(pe)},{default:$(()=>[...de[36]||(de[36]=[ue("预热",-1)])]),_:1},8,["loading","onClick"])])],10,QNe))),128)),!a.value&&n.value.length===0?(x(),O("div",dBe,[de[38]||(de[38]=C("div",{style:{"font-size":"40px","margin-bottom":"12px"}},"📱",-1)),de[39]||(de[39]=C("div",{class:"text-muted"},"暂无监听端设备",-1)),L(it,{type:"success",plain:"",style:{"margin-top":"16px"},onClick:Ne},{default:$(()=>[...de[37]||(de[37]=[ue("USB 引导接入",-1)])]),_:1})])):te("",!0)]),s.value?(x(),O("div",fBe,[C("div",vBe,[L(Oa,{"model-value":c.value,indeterminate:d.value>0&&!c.value,onChange:m},{default:$(()=>[ue(Q(c.value?"取消全选":"全选"),1)]),_:1},8,["model-value","indeterminate"]),L(it,{size:"small",text:"",onClick:g,disabled:d.value===0},{default:$(()=>[...de[40]||(de[40]=[ue("清空",-1)])]),_:1},8,["disabled"])]),C("div",hBe,[L(it,{type:"warning",size:"small",disabled:d.value===0,loading:Fe.value==="restart",onClick:Ie},{default:$(()=>[...de[41]||(de[41]=[ue(" 批量重启 ",-1)])]),_:1},8,["disabled","loading"]),L(it,{type:"danger",size:"small",disabled:d.value===0,loading:Fe.value==="freeze",onClick:Ke},{default:$(()=>[...de[42]||(de[42]=[ue(" 批量冻结 ",-1)])]),_:1},8,["disabled","loading"]),L(it,{type:"success",size:"small",disabled:d.value===0,loading:Fe.value==="unfreeze",onClick:Ge},{default:$(()=>[...de[43]||(de[43]=[ue(" 批量解冻 ",-1)])]),_:1},8,["disabled","loading"])])])):te("",!0)])),[[Xs,a.value]])]),L(Oe,{modelValue:o.value,"onUpdate:modelValue":de[2]||(de[2]=pe=>o.value=pe),title:"设备控制台",width:"1100px",top:"4vh","destroy-on-close":"",onClosed:K},{default:$(()=>[i.value?(x(),O("div",pBe,[C("div",mBe,[C("div",gBe,[C("div",yBe,Q(i.value.name||"未命名"),1),C("div",bBe,Q(i.value.device_id),1),C("div",_Be,"USB: "+Q(i.value.adb_host||"—")+Q(i.value.adb_port?":"+i.value.adb_port:"")+" · WiFi: "+Q(i.value.adb_wifi_host?i.value.adb_wifi_host+":"+(i.value.adb_wifi_port||"—"):"未配置"),1),C("div",wBe,[L(gn,{type:y(i.value.status),size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(b(i.value.status)),1)]),_:1},8,["type"]),w(i.value)?(x(),ne(gn,{key:0,type:w(i.value).type,size:"small",effect:"plain",round:"",style:{"margin-left":"6px"}},{default:$(()=>[ue(Q(w(i.value).label),1)]),_:1},8,["type"])):te("",!0),i.value.ts_ip?(x(),ne(gn,{key:1,type:"info",size:"small",effect:"plain",round:"",class:"font-mono",style:{"margin-left":"6px"}},{default:$(()=>[ue(Q(i.value.ts_ip),1)]),_:1})):te("",!0),(x(!0),O(Ve,null,dt(H.value,pe=>(x(),ne(gn,{key:pe.account_id,type:pe.is_primary?"primary":"info",size:"small",effect:"plain",round:"",style:{"margin-left":"6px"}},{default:$(()=>[ue(Q(pe.name),1),pe.is_primary?(x(),O(Ve,{key:0},[ue("(主)")],64)):te("",!0)]),_:2},1032,["type"]))),128))])]),C("div",SBe,[L(it,{size:"small",plain:"",onClick:oe},{default:$(()=>[...de[44]||(de[44]=[ue("编辑 ADB",-1)])]),_:1}),L(it,{size:"small",type:"success",plain:"",onClick:Y},{default:$(()=>[...de[45]||(de[45]=[ue("绑定账户",-1)])]),_:1}),L(it,{size:"small",type:"warning",plain:"",onClick:ve},{default:$(()=>[...de[46]||(de[46]=[ue("捕获收款码",-1)])]),_:1})])]),l.value.length?(x(),O("div",CBe,[L(FNe,{serials:l.value,"device-id":i.value.id},null,8,["serials","device-id"])])):(x(),ne(sn,{key:1,type:"warning",closable:!1,title:"该设备未配置 ADB 地址,无法实时操作。请编辑设备填写 adb_host/adb_port。"}))])):te("",!0)]),_:1},8,["modelValue"]),L(Oe,{modelValue:J.value,"onUpdate:modelValue":de[8]||(de[8]=pe=>J.value=pe),title:"编辑 ADB 通道",width:"480px"},{footer:$(()=>[L(it,{onClick:de[7]||(de[7]=pe=>J.value=!1)},{default:$(()=>[...de[50]||(de[50]=[ue("取消",-1)])]),_:1}),L(it,{type:"primary",loading:Z.value,onClick:se},{default:$(()=>[...de[51]||(de[51]=[ue("保存",-1)])]),_:1},8,["loading"])]),default:$(()=>[L(_n,{"label-width":"90px"},{default:$(()=>[L(Bt,{label:"USB 直连"},{default:$(()=>[L(lt,{modelValue:j.value.adb_host,"onUpdate:modelValue":de[3]||(de[3]=pe=>j.value.adb_host=pe),placeholder:"真机 USB serial,如 4f2bdc30"},null,8,["modelValue"]),de[47]||(de[47]=C("div",{class:"text-muted",style:{"font-size":"11px",width:"100%"}},"真机 USB 直连填 adb serial;adb over WiFi(可选)填 host:port",-1))]),_:1}),L(Bt,{label:"TCP 端口"},{default:$(()=>[L(lt,{modelValue:j.value.adb_port,"onUpdate:modelValue":de[4]||(de[4]=pe=>j.value.adb_port=pe),modelModifiers:{number:!0},placeholder:"如 5555(USB 直连留空)"},null,8,["modelValue"])]),_:1}),L(Bt,{label:"WiFi 备用"},{default:$(()=>[L(lt,{modelValue:j.value.adb_wifi_host,"onUpdate:modelValue":de[5]||(de[5]=pe=>j.value.adb_wifi_host=pe),placeholder:"同局域网无线调试 IP(可选)",style:{width:"45%"}},null,8,["modelValue"]),de[48]||(de[48]=C("span",{style:{margin:"0 6px",color:"var(--text-muted)"}},":",-1)),L(lt,{modelValue:j.value.adb_wifi_port,"onUpdate:modelValue":de[6]||(de[6]=pe=>j.value.adb_wifi_port=pe),modelModifiers:{number:!0},placeholder:"5555",style:{width:"45%"}},null,8,["modelValue"]),de[49]||(de[49]=C("div",{class:"text-muted",style:{"font-size":"11px",width:"100%"}},"可选:USB 断线后自动切到 WiFi 通道保活",-1))]),_:1})]),_:1})]),_:1},8,["modelValue"]),L(Oe,{modelValue:z.value,"onUpdate:modelValue":de[11]||(de[11]=pe=>z.value=pe),title:"绑定更多账户(应用双开)",width:"520px"},{footer:$(()=>[L(it,{onClick:de[10]||(de[10]=pe=>z.value=!1)},{default:$(()=>[...de[53]||(de[53]=[ue("取消",-1)])]),_:1}),L(it,{type:"primary",loading:B.value,disabled:!W.value.account_id,onClick:X},{default:$(()=>[...de[54]||(de[54]=[ue(" 绑定 ",-1)])]),_:1},8,["loading","disabled"])]),default:$(()=>[L(sn,{type:"info",closable:!1,style:{"margin-bottom":"12px"},title:"一台设备(双开微信/支付宝)可服务多个收款账户。已绑定账户在设备恢复心跳/接单时会同时生效。"}),H.value.length?(x(),O("div",xBe,[de[52]||(de[52]=C("div",{style:{"font-size":"12px",color:"var(--text-muted)","margin-bottom":"6px"}},"已绑定账户:",-1)),(x(!0),O(Ve,null,dt(H.value,pe=>(x(),ne(gn,{key:pe.account_id,type:pe.is_primary?"primary":"info",size:"small",effect:"plain",round:"",style:{margin:"0 6px 6px 0"},closable:"",onClose:ct=>ee(pe)},{default:$(()=>[ue(Q(pe.name)+Q(pe.is_primary?"(主)":""),1)]),_:2},1032,["type","closable","onClose"]))),128))])):te("",!0),L(_n,{"label-width":"90px"},{default:$(()=>[L(Bt,{label:"添加账户",required:""},{default:$(()=>[L(Zn,{modelValue:W.value.account_id,"onUpdate:modelValue":de[9]||(de[9]=pe=>W.value.account_id=pe),placeholder:"选择同通道的个人收款账户",style:{width:"100%"}},{default:$(()=>[(x(!0),O(Ve,null,dt(V.value,pe=>(x(),ne(Yn,{key:pe.id,label:`#${pe.id} ${pe.name}(${_(pe.channel)})`,value:pe.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"]),L(Oe,{modelValue:re.value,"onUpdate:modelValue":de[15]||(de[15]=pe=>re.value=pe),title:"捕获收款码",width:"420px"},{footer:$(()=>[L(it,{onClick:de[14]||(de[14]=pe=>re.value=!1)},{default:$(()=>[...de[59]||(de[59]=[ue("取消",-1)])]),_:1}),L(it,{type:"warning",loading:ie.value,onClick:he},{default:$(()=>[...de[60]||(de[60]=[ue(" 开始捕获(约 30~90 秒) ",-1)])]),_:1},8,["loading"])]),default:$(()=>[L(sn,{type:"info",closable:!1,class:"mb12",title:"即使设备尚未捕获过收款码也可直接捕获。服务端 adb 会自动打开微信/支付宝并导航到收款码页,请确保设备 adb 在线。"}),L(_n,{"label-width":"90px"},{default:$(()=>[L(Bt,{label:"平台",required:""},{default:$(()=>[L(Gl,{modelValue:le.value.platform,"onUpdate:modelValue":de[12]||(de[12]=pe=>le.value.platform=pe)},{default:$(()=>[L(ja,{value:"alipay"},{default:$(()=>[...de[55]||(de[55]=[ue("支付宝",-1)])]),_:1}),L(ja,{value:"wechat"},{default:$(()=>[...de[56]||(de[56]=[ue("微信",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),L(Bt,{label:"应用空间",required:""},{default:$(()=>[L(Gl,{modelValue:le.value.user,"onUpdate:modelValue":de[13]||(de[13]=pe=>le.value.user=pe)},{default:$(()=>[L(ja,{value:0},{default:$(()=>[...de[57]||(de[57]=[ue("主空间",-1)])]),_:1}),L(ja,{value:999},{default:$(()=>[...de[58]||(de[58]=[ue("双开",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"]),L(Oe,{modelValue:fe.value,"onUpdate:modelValue":de[18]||(de[18]=pe=>fe.value=pe),title:"USB 真机引导接入",width:"720px","destroy-on-close":"",onClosed:tt},{footer:$(()=>[L(it,{onClick:Me},{default:$(()=>[...de[75]||(de[75]=[ue("关闭",-1)])]),_:1}),ge.value>0&&ge.value<3?(x(),ne(it,{key:0,onClick:pt},{default:$(()=>[...de[76]||(de[76]=[ue("上一步",-1)])]),_:1})):te("",!0),ge.value===0?(x(),ne(it,{key:1,type:"primary",disabled:!Te.value,onClick:ft},{default:$(()=>[...de[77]||(de[77]=[ue(" 下一步 ",-1)])]),_:1},8,["disabled"])):te("",!0),ge.value===1?(x(),ne(it,{key:2,type:"primary",disabled:!(Re.value&&Re.value.state==="device"),onClick:ht},{default:$(()=>[...de[78]||(de[78]=[ue(" 下一步 ",-1)])]),_:1},8,["disabled"])):te("",!0),ge.value===2?(x(),ne(it,{key:3,type:"primary",loading:me.value,onClick:It},{default:$(()=>[...de[79]||(de[79]=[ue(" 创建并继续 ",-1)])]),_:1},8,["loading"])):te("",!0)]),default:$(()=>[L(Yl,{active:ge.value,simple:"",style:{"margin-bottom":"20px"}},{default:$(()=>[L(co,{title:"连接 USB"}),L(co,{title:"授权调试"}),L(co,{title:"绑定设备"}),L(co,{title:"下发配置"})]),_:1},8,["active"]),C("div",kBe,[ge.value===0?(x(),O("div",TBe,[Se.value?(x(),O("div",MBe,[L(Dt,{class:"is-loading",style:{"font-size":"24px"}},{default:$(()=>[L(p(Mr))]),_:1}),de[61]||(de[61]=C("div",{style:{"margin-top":"8px",color:"var(--text-muted)","font-size":"13px"}},"正在检测 USB 设备...",-1))])):De.value.length===0?(x(),O("div",EBe,[...de[62]||(de[62]=[C("div",{style:{"font-size":"48px","margin-bottom":"12px"}},"🔌",-1),C("div",{style:{color:"var(--text-muted)"}},"未检测到 USB 真机,请用数据线连接手机并开启开发者选项中的 USB 调试",-1)])])):(x(),O("div",ABe,[de[68]||(de[68]=C("div",{style:{"margin-bottom":"12px",color:"var(--text-muted)","font-size":"13px"}},"请选择要接入的设备:",-1)),C("div",IBe,[(x(!0),O(Ve,null,dt(De.value,pe=>(x(),O("div",{key:pe.serial,class:U(["usb-device-item",{"usb-device-item-selected":Te.value===pe.serial}]),onClick:ct=>je(pe)},[C("div",PBe,[C("div",DBe,Q(pe.serial),1),C("div",OBe,[pe.kind==="wifi"?(x(),ne(gn,{key:0,type:"info",size:"small",effect:"plain",style:{"margin-right":"4px"}},{default:$(()=>[...de[63]||(de[63]=[ue("WiFi",-1)])]),_:1})):(x(),ne(gn,{key:1,type:"success",size:"small",effect:"plain",style:{"margin-right":"4px"}},{default:$(()=>[...de[64]||(de[64]=[ue("USB",-1)])]),_:1})),ue(" "+Q(pe.model||"未知型号"),1)])]),C("div",$Be,[pe.state==="device"?(x(),ne(gn,{key:0,type:"success",size:"small",effect:"plain"},{default:$(()=>[...de[65]||(de[65]=[ue("可下一步",-1)])]),_:1})):pe.state==="unauthorized"?(x(),ne(gn,{key:1,type:"warning",size:"small",effect:"plain"},{default:$(()=>[...de[66]||(de[66]=[ue("待授权",-1)])]),_:1})):(x(),ne(gn,{key:2,type:"info",size:"small",effect:"plain"},{default:$(()=>[...de[67]||(de[67]=[ue("连接异常",-1)])]),_:1})),Te.value===pe.serial?(x(),ne(Dt,{key:3,style:{"margin-left":"6px",color:"var(--el-color-success)"}},{default:$(()=>[L(p(Os))]),_:1})):te("",!0)])],10,LBe))),128))]),Te.value&&Re.value&&Re.value.state==="unauthorized"?(x(),ne(sn,{key:0,type:"warning",closable:!1,style:{"margin-top":"12px"},title:"请在手机弹出的对话框中点击「允许 USB 调试」"})):te("",!0)]))])):te("",!0),ge.value===1?(x(),O("div",RBe,[C("div",NBe,[L(Dt,{class:"is-loading",style:{"font-size":"32px"}},{default:$(()=>[L(p(Mr))]),_:1}),de[70]||(de[70]=C("div",{style:{"margin-top":"12px","font-size":"15px","font-weight":"500"}},"等待手机端授权...",-1)),de[71]||(de[71]=C("div",{style:{"margin-top":"8px",color:"var(--text-muted)","font-size":"13px"}}," 请在手机屏幕上点击「允许 USB 调试」 ",-1)),Re.value?(x(),O("div",BBe,[L(gn,{type:"info",size:"small",effect:"plain",class:"font-mono"},{default:$(()=>[ue(Q(Te.value),1)]),_:1}),L(gn,{type:"info",size:"small",effect:"plain",style:{"margin-left":"6px"}},{default:$(()=>[ue(Q(Re.value.model||"未知型号"),1)]),_:1})])):te("",!0),Re.value&&Re.value.state==="device"?(x(),O("div",VBe,[L(Dt,{style:{color:"var(--el-color-success)","font-size":"28px"}},{default:$(()=>[L(p(Ao))]),_:1}),de[69]||(de[69]=C("div",{style:{color:"var(--el-color-success)","margin-top":"6px","font-weight":"500"}},"授权成功,点击下方「下一步」继续",-1))])):(x(),O("div",zBe," 若手机未弹出授权框,请断开 USB 重插,或检查「开发者选项 → USB 调试」是否开启 "))])])):te("",!0),ge.value===2?(x(),O("div",FBe,[L(_n,{"label-width":"90px"},{default:$(()=>[L(Bt,{label:"设备名称",required:""},{default:$(()=>[L(lt,{modelValue:Le.value.name,"onUpdate:modelValue":de[16]||(de[16]=pe=>Le.value.name=pe),placeholder:"如:卧室安卓机-1",maxlength:"30"},null,8,["modelValue"])]),_:1}),L(Bt,{label:"收款账户",required:""},{default:$(()=>[L(Zn,{modelValue:Le.value.account_id,"onUpdate:modelValue":de[17]||(de[17]=pe=>Le.value.account_id=pe),placeholder:"选择个人通道收款账户",style:{width:"100%"}},{default:$(()=>[(x(!0),O(Ve,null,dt(r.value,pe=>(x(),ne(Yn,{key:pe.id,label:`#${pe.id} ${pe.name}`,value:pe.id},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1})]),_:1}),Ce.value?(x(),ne(sn,{key:0,type:"info",closable:!1,style:{"margin-top":"8px"},title:"该设备已绑定,将继续未完成的配置流程"})):te("",!0)])):te("",!0),ge.value===3?(x(),O("div",HBe,[nt.value?(x(),O("div",UBe,[L(Dt,{style:{color:"var(--el-color-success)","font-size":"40px"}},{default:$(()=>[L(p(Ao))]),_:1}),de[74]||(de[74]=C("div",{style:{"margin-top":"10px","font-size":"15px","font-weight":"500"}},"配置下发成功!",-1)),Pt.value?(x(),ne(sn,{key:0,type:"info",closable:!1,style:{"margin-top":"12px","text-align":"left"},title:Pt.value},null,8,["title"])):te("",!0),Wt.value?(x(),ne(sn,{key:1,type:"warning",closable:!1,style:{"margin-top":"8px","text-align":"left"},title:Wt.value},null,8,["title"])):te("",!0),Jt.value?(x(),ne(sn,{key:2,type:"warning",closable:!1,"show-icon":"",style:{"margin-top":"8px","text-align":"left"},title:Jt.value},null,8,["title"])):te("",!0),C("div",GBe,[L(it,{type:"primary",onClick:Sn},{default:$(()=>[...de[73]||(de[73]=[ue("完成",-1)])]),_:1})])])):(x(),O("div",WBe,[C("div",KBe,[L(it,{type:"primary",size:"large",loading:xe.value,onClick:$t},{default:$(()=>[...de[72]||(de[72]=[ue(" 下发并启动 ",-1)])]),_:1},8,["loading"]),rt.value?(x(),ne(sn,{key:0,type:"danger",closable:!1,style:{"margin-top":"16px"},title:rt.value},null,8,["title"])):te("",!0)])]))])):te("",!0)])]),_:1},8,["modelValue"])])}}},jBe=fr(YBe,[["__scopeId","data-v-3b83dd8d"]]),qBe={class:"unmatched-page"},XBe={class:"pg-card"},ZBe={class:"pg-card-header"},JBe={class:"header-actions"},QBe={class:"text-muted",style:{"font-size":"12px"}},eVe={class:"pg-card-body"},tVe={class:"pg-card"},nVe={class:"pg-card-body"},aVe={class:"font-mono"},rVe={class:"text-danger",style:{"font-weight":"600"}},oVe={key:0,class:"font-mono"},iVe={key:1,class:"text-muted"},lVe={key:1,class:"text-muted"},sVe={key:1,class:"text-muted"},uVe={class:"text-muted"},cVe={class:"font-mono text-muted",style:{"font-size":"12px"}},dVe=["title"],fVe={key:0,class:"text-muted",style:{"text-align":"center",padding:"40px 0"}},vVe={class:"text-danger",style:{"font-weight":"600"}},hVe={__name:"Unmatched",props:{refreshKey:{type:Number,default:0}},setup(e){const t=e,n=F([]),a=F(!1),r=F("pending"),o=F(!1),i=F(!1),l=F({}),s=F({status:"resolved",note:""});function u(g){return{pending:"warning",resolved:"success",refunded:"info"}[g]||"info"}function c(g){return{pending:"待处理",resolved:"已确认",refunded:"已退款"}[g]||g}function d(g){return{big_amount:"大额",burst:"高频",repeat_payer:"同源重复"}[g]||g}function v(g){return g?String(g).replace("T"," ").slice(0,19):"—"}async function f(){a.value=!0;try{const g=await yt.personalUnmatched({status:r.value,limit:100});n.value=g.unmatched||[]}catch(g){We.error("加载失败:"+g.message)}finally{a.value=!1}}function h(g){l.value=g,s.value={status:"resolved",note:""},o.value=!0}async function m(){i.value=!0;try{await yt.resolveUnmatched(l.value.id,{status:s.value.status,note:s.value.note}),We.success("已处理"),o.value=!1,f()}catch(g){We.error("处理失败:"+g.message)}finally{i.value=!1}}return mt(f),ye(()=>t.refreshKey,f),(g,y)=>{const b=be("el-option"),_=be("el-select"),w=be("el-alert"),S=be("el-table-column"),k=be("el-tag"),T=be("el-button"),M=be("el-table"),E=be("el-descriptions-item"),A=be("el-descriptions"),I=be("el-radio"),P=be("el-radio-group"),R=be("el-form-item"),G=be("el-input"),K=be("el-form"),H=be("el-dialog"),z=Gr("loading");return x(),O("div",qBe,[C("div",XBe,[C("div",ZBe,[y[5]||(y[5]=C("span",{class:"section-label"},"无订单收款",-1)),C("div",JBe,[L(_,{modelValue:r.value,"onUpdate:modelValue":y[0]||(y[0]=B=>r.value=B),size:"small",style:{width:"130px"},onChange:f},{default:$(()=>[L(b,{label:"全部",value:""}),L(b,{label:"待处理",value:"pending"}),L(b,{label:"已确认",value:"resolved"}),L(b,{label:"已退款",value:"refunded"})]),_:1},8,["modelValue"]),C("span",QBe,"共 "+Q(n.value.length)+" 条",1)])]),C("div",eVe,[L(w,{type:"warning",closable:!1,class:"mb16",title:"有人向收款码转账但没有匹配到订单。请人工核对后处理:已确认(资金已收到)或已退款(原路退回)。"}),L(w,{type:"error",closable:!1,class:"mb16",title:"司法提示:系统按行为模式自动标记疑似风险收款(大额/同源高频)。标记金额疑为黑钱/赃款的,请立即核实付款人身份与资金来源,确认异常的请保全本页记录并联系司法部门处理。"})])]),C("div",tVe,[st((x(),O("div",nVe,[L(M,{data:n.value,stripe:"",style:{width:"100%"}},{default:$(()=>[L(S,{label:"ID",width:"70"},{default:$(({row:B})=>[C("span",aVe,"#"+Q(B.id),1)]),_:1}),L(S,{label:"通道",width:"100"},{default:$(({row:B})=>[L(k,{type:B.provider==="alipay"?"primary":"success",size:"small"},{default:$(()=>[ue(Q(B.provider==="alipay"?"支付宝":"微信"),1)]),_:2},1032,["type"])]),_:1}),L(S,{label:"金额",width:"120"},{default:$(({row:B})=>[C("span",rVe,"¥"+Q((B.amount_minor/100).toFixed(2)),1)]),_:1}),L(S,{label:"付款人",width:"120"},{default:$(({row:B})=>[B.payer?(x(),O("span",oVe,Q(B.payer),1)):(x(),O("span",iVe,"—"))]),_:1}),L(S,{label:"风险标记",width:"180"},{default:$(({row:B})=>[B.risk_level>0?(x(),O(Ve,{key:0},[L(k,{type:"danger",size:"small",effect:"dark",style:{"margin-right":"4px"}},{default:$(()=>[ue(Q(B.risk_level===2?"高风险":"建议核实"),1)]),_:2},1024),(x(!0),O(Ve,null,dt(String(B.risk_tags||"").split(",").filter(Boolean),W=>(x(),ne(k,{key:W,type:"danger",size:"small",effect:"plain",style:{"margin-right":"2px"}},{default:$(()=>[ue(Q(d(W)),1)]),_:2},1024))),128))],64)):(x(),O("span",lVe,"—"))]),_:1}),L(S,{label:"备注",width:"110"},{default:$(({row:B})=>[B.note?(x(),ne(k,{key:0,size:"small",type:"warning"},{default:$(()=>[ue(Q(B.note),1)]),_:2},1024)):(x(),O("span",sVe,"无"))]),_:1}),L(S,{label:"状态",width:"100"},{default:$(({row:B})=>[L(k,{type:u(B.status),size:"small",effect:"light",round:""},{default:$(()=>[ue(Q(c(B.status)),1)]),_:2},1032,["type"])]),_:1}),L(S,{label:"处理人",width:"110"},{default:$(({row:B})=>[C("span",uVe,Q(B.handler||"—"),1)]),_:1}),L(S,{label:"时间",width:"170"},{default:$(({row:B})=>[C("span",cVe,Q(v(B.created_at)),1)]),_:1}),L(S,{label:"通知原文","min-width":"200"},{default:$(({row:B})=>[C("div",{class:"raw-cell",title:B.raw},Q(B.raw||"—"),9,dVe)]),_:1}),L(S,{label:"操作",width:"100",fixed:"right"},{default:$(({row:B})=>[B.status==="pending"?(x(),ne(T,{key:0,size:"small",type:"primary",text:"",onClick:W=>h(B)},{default:$(()=>[...y[6]||(y[6]=[ue("处理",-1)])]),_:1},8,["onClick"])):te("",!0)]),_:1})]),_:1},8,["data"]),!a.value&&n.value.length===0?(x(),O("div",fVe," 暂无无订单收款记录 ")):te("",!0)])),[[z,a.value]])]),L(H,{modelValue:o.value,"onUpdate:modelValue":y[4]||(y[4]=B=>o.value=B),title:"处理无订单收款",width:"500px"},{footer:$(()=>[L(T,{onClick:y[3]||(y[3]=B=>o.value=!1)},{default:$(()=>[...y[9]||(y[9]=[ue("取消",-1)])]),_:1}),L(T,{type:"primary",loading:i.value,onClick:m},{default:$(()=>[...y[10]||(y[10]=[ue("提交",-1)])]),_:1},8,["loading"])]),default:$(()=>[L(A,{column:2,size:"small",border:"",class:"mb16"},{default:$(()=>[L(E,{label:"金额"},{default:$(()=>[C("span",vVe,"¥"+Q((l.value.amount_minor/100).toFixed(2)),1)]),_:1}),L(E,{label:"通道"},{default:$(()=>[ue(Q(l.value.provider==="alipay"?"支付宝":"微信"),1)]),_:1}),L(E,{label:"备注码"},{default:$(()=>[ue(Q(l.value.note||"—"),1)]),_:1}),L(E,{label:"ID"},{default:$(()=>[ue("#"+Q(l.value.id),1)]),_:1})]),_:1}),L(K,{"label-width":"90px"},{default:$(()=>[L(R,{label:"处理结果",required:""},{default:$(()=>[L(P,{modelValue:s.value.status,"onUpdate:modelValue":y[1]||(y[1]=B=>s.value.status=B)},{default:$(()=>[L(I,{value:"resolved"},{default:$(()=>[...y[7]||(y[7]=[ue("已确认收款",-1)])]),_:1}),L(I,{value:"refunded"},{default:$(()=>[...y[8]||(y[8]=[ue("已原路退款",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),L(R,{label:"处理备注"},{default:$(()=>[L(G,{modelValue:s.value.note,"onUpdate:modelValue":y[2]||(y[2]=B=>s.value.note=B),type:"textarea",rows:3,placeholder:"填写处理说明:核对依据、退款凭证等(留痕审计)"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])])}}},pVe=fr(hVe,[["__scopeId","data-v-1df8350a"]]),mVe={class:"settings-page"},gVe={class:"pg-card"},yVe={class:"pg-card-body"},bVe={class:"channel-grid"},_Ve={class:"ch-info"},wVe={class:"ch-name"},SVe={class:"ch-status"},CVe={class:"mt16"},xVe={key:0,class:"saved-hint"},kVe={class:"admin-item"},TVe={class:"admin-info"},MVe={class:"admin-value font-mono"},EVe={class:"key-grid"},AVe={class:"key-label"},IVe={class:"key-value font-mono"},LVe={class:"key-actions"},PVe={__name:"Settings",setup(e){const t=nn({order_close_minutes:30,order_close_interval:60}),n=F([]),a=F(!1),r=F(!1),o=F({}),i=F(""),l=F(sessionStorage.getItem("admin_username")||""),s=F(!1),u=F(""),c=F(!1),d=[{key:"ledger_private_key",label:"审计账本私钥(ECDSA P-256)"},{key:"qr_token_secret",label:"套码 token 签名密钥"}];function v(){u.value="",s.value=!0}async function f(){if(!u.value){We.warning("请输入当前密码");return}c.value=!0;try{await yt.resetAdmin(u.value),We.success("管理员已重置,正在跳转到初始化页..."),sessionStorage.removeItem("admin_token"),sessionStorage.removeItem("admin_username"),setTimeout(()=>{window.location.reload()},1200)}catch(_){We.error("重置失败: "+_.message)}finally{c.value=!1}}async function h(){try{const _=await yt.settings(),w=_.settings||{};w.order_close_minutes&&(t.order_close_minutes=parseInt(w.order_close_minutes)),w.order_close_interval&&(t.order_close_interval=parseInt(w.order_close_interval)),n.value=(_.channels||[]).map(S=>{var k;return{name:S,enabled:((k=_.channel_enabled)==null?void 0:k[S])!==!1}})}catch(_){We.error("加载设置失败: "+_.message)}}async function m(_){if(i.value===_){i.value="";return}try{await er.confirm("将显示完整密钥(明文),请确认当前环境安全。查看行为会记入审计日志。","查看安全密钥",{confirmButtonText:"确认查看",cancelButtonText:"取消",type:"warning"});const w=await yt.settingsKeys();o.value=w||{},i.value=_}catch(w){w!=="cancel"&&We.error("获取密钥失败: "+(w.message||w))}}async function g(_){const w=o.value[_];if(!w){We.warning("密钥不存在");return}try{await navigator.clipboard.writeText(w),We.success("已复制")}catch(S){We.error("复制失败: "+S.message)}}async function y(){a.value=!0,r.value=!1;try{const _=[];_.push({key:"order_close_minutes",value:String(t.order_close_minutes)}),_.push({key:"order_close_interval",value:String(t.order_close_interval)}),await yt.saveSettings({settings:_}),O3(),r.value=!0,setTimeout(()=>{r.value=!1},3e3),We.success("保存成功")}catch(_){We.error("保存失败: "+_.message)}finally{a.value=!1}}async function b(_){try{const w=await yt.toggleChannel(_),S=n.value.find(k=>k.name===_);S&&S.enabled!==w.enabled&&(S.enabled=w.enabled),O3(),We.success(`通道 ${_} 已${w.enabled?"启用":"禁用"}`)}catch(w){const S=n.value.find(k=>k.name===_);S&&(S.enabled=!S.enabled),We.error("切换失败: "+w.message)}}return ye(oV,h),mt(h),(_,w)=>{const S=be("el-alert"),k=be("el-divider"),T=be("el-switch"),M=be("el-input-number"),E=be("el-form-item"),A=be("el-form"),I=be("el-button"),P=be("el-icon"),R=be("el-input"),G=be("el-dialog");return x(),O("div",mVe,[C("div",gVe,[w[15]||(w[15]=C("div",{class:"pg-card-header"},[C("span",{class:"section-label"},"系统设置")],-1)),C("div",yVe,[L(S,{type:"info",closable:!1,"show-icon":"",class:"mb16",title:"设置保存后立即生效,无需重启。"}),L(S,{type:"info",closable:!1,"show-icon":"",class:"mb16",title:"上游对接(易支付 pid/密钥/服务地址)已迁移到「上游对接」页:每个上游独立配置,AES-256-GCM 加密存于 upstreams.db。"}),L(k),w[11]||(w[11]=C("h3",{class:"sec-title"},"支付通道",-1)),C("div",bVe,[(x(!0),O(Ve,null,dt(n.value,K=>(x(),O("div",{key:K.name,class:U(["channel-switch",{disabled:!K.enabled}])},[C("div",_Ve,[C("div",wVe,Q(K.name),1),C("div",SVe,Q(K.enabled?"已启用":"已禁用"),1)]),L(T,{modelValue:K.enabled,"onUpdate:modelValue":H=>K.enabled=H,onChange:H=>b(K.name)},null,8,["modelValue","onUpdate:modelValue","onChange"])],2))),128))]),L(k),w[12]||(w[12]=C("h3",{class:"sec-title"},"系统参数",-1)),L(A,{model:t,"label-width":"180px",size:"small"},{default:$(()=>[L(E,{label:"订单超时关闭(分钟)"},{default:$(()=>[L(M,{modelValue:t.order_close_minutes,"onUpdate:modelValue":w[0]||(w[0]=K=>t.order_close_minutes=K),min:0,max:1440},null,8,["modelValue"]),w[5]||(w[5]=C("div",{class:"form-tip"},"0 = 不自动关闭",-1))]),_:1}),L(E,{label:"扫描间隔(秒)"},{default:$(()=>[L(M,{modelValue:t.order_close_interval,"onUpdate:modelValue":w[1]||(w[1]=K=>t.order_close_interval=K),min:10,max:600},null,8,["modelValue"])]),_:1})]),_:1},8,["model"]),C("div",CVe,[L(I,{type:"primary",size:"default",onClick:y,loading:a.value},{default:$(()=>[...w[6]||(w[6]=[ue("保存全部设置",-1)])]),_:1},8,["loading"]),r.value?(x(),O("span",xVe,[L(P,{size:14},{default:$(()=>[L(p(Ao))]),_:1}),w[7]||(w[7]=ue(" 已保存 ",-1))])):te("",!0)]),L(k),w[13]||(w[13]=C("h3",{class:"sec-title"},"管理员账号",-1)),C("div",kVe,[C("div",TVe,[w[8]||(w[8]=C("div",{class:"admin-label"},"当前登录账号",-1)),C("div",MVe,Q(l.value||"—"),1)]),L(I,{size:"small",type:"danger",plain:"",onClick:v},{default:$(()=>[...w[9]||(w[9]=[ue("重置管理员",-1)])]),_:1})]),L(S,{type:"warning",closable:!1,"show-icon":"",class:"mb12 mt12",title:"重置管理员会清除当前登录凭据并回到首次注册流程,需重新设置新的管理员账号(旧账号立即失效)。此操作需验证当前密码。"}),L(k),w[14]||(w[14]=C("h3",{class:"sec-title"},"安全密钥",-1)),L(S,{type:"info",closable:!1,"show-icon":"",class:"mb12",title:"账本签名私钥与套码 token 密钥已自动生成并存库(首次启动无需手动配置)。查看完整值需确认,操作会记入审计日志。"}),C("div",EVe,[(x(),O(Ve,null,dt(d,K=>C("div",{key:K.key,class:"key-item"},[C("div",AVe,Q(K.label),1),C("div",IVe,Q(i.value===K.key?o.value[K.key]||"(未生成)":"••••••••••••••••"),1),C("div",LVe,[L(I,{size:"small",text:"",type:"primary",onClick:H=>m(K.key)},{default:$(()=>[ue(Q(i.value===K.key?"隐藏":"查看"),1)]),_:2},1032,["onClick"]),L(I,{size:"small",text:"",type:"primary",disabled:i.value!==K.key,onClick:H=>g(K.key)},{default:$(()=>[...w[10]||(w[10]=[ue("复制",-1)])]),_:1},8,["disabled","onClick"])])])),64))])])]),L(G,{modelValue:s.value,"onUpdate:modelValue":w[4]||(w[4]=K=>s.value=K),title:"重置管理员账号",width:"440px","close-on-click-modal":!1},{footer:$(()=>[L(I,{onClick:w[3]||(w[3]=K=>s.value=!1)},{default:$(()=>[...w[16]||(w[16]=[ue("取消",-1)])]),_:1}),L(I,{type:"danger",loading:c.value,onClick:f},{default:$(()=>[...w[17]||(w[17]=[ue("确认重置",-1)])]),_:1},8,["loading"])]),default:$(()=>[L(S,{type:"warning",closable:!1,class:"mb16","show-icon":"",title:"重置后将回到首次注册页,需要重新设置管理员。当前账号立即失效。"}),L(A,{"label-width":"100px",size:"small"},{default:$(()=>[L(E,{label:"当前密码",required:""},{default:$(()=>[L(R,{modelValue:u.value,"onUpdate:modelValue":w[2]||(w[2]=K=>u.value=K),type:"password","show-password":"",placeholder:"验证当前管理员密码",onKeyup:kn(f,["enter"])},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])])}}},DVe=fr(PVe,[["__scopeId","data-v-49bef0e4"]]),OVe={key:1,class:"empty"},$Ve={key:2,class:"qrcode-grid"},RVe={class:"card-header"},NVe={class:"device-name"},BVe={key:0,class:"card-account"},VVe={class:"account-name"},zVe={key:1,class:"card-account"},FVe={class:"card-body"},HVe={key:0,class:"qr-area"},WVe=["src"],KVe={class:"qr-deadline"},UVe={key:1,class:"no-qr"},GVe={class:"card-footer"},YVe={__name:"Qrcodes",props:{refreshKey:Number},setup(e){const t=F(!1),n=F(""),a=F([]),r=F(""),o=F("");let i=null;async function l(){t.value=!0,n.value="";try{const f=await yt.personalQrcodes();a.value=Array.isArray(f)?f:[]}catch(f){n.value=f.message}finally{t.value=!1}}function s(f){return f?"https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl="+encodeURIComponent(f):""}function u(f){if(!f.expire_at)return 0;const h=new Date(f.expire_at).getTime(),m=Date.now();return Math.max(0,Math.floor((h-m)/1e3))}function c(f){const h=u(f);if(h<=0)return"已过期";const m=Math.floor(h/60),g=h%60;return`${m}:${String(g).padStart(2,"0")}`}async function d(f,h,m=0){const g=f+"-"+m;r.value=g;try{h==="wechat"?await yt.personalCaptureWechatQrcode(f,m):await yt.personalCaptureAlipayQrcode(f,m),await l(),n.value=""}catch(y){n.value="adb捕获失败: "+y.message}finally{r.value=""}}async function v(f){const h=f.user_id??0;try{await er.confirm(`确定移除「${f.device_name||f.device_id}」的${f.platform==="alipay"?"支付宝":"微信"}收款码(${h===999?"双开":"主空间"})?`,"移除收款码",{type:"warning",confirmButtonText:"移除",cancelButtonText:"取消"})}catch{return}o.value=f.device_id+"-"+f.platform+"-"+h;try{await yt.personalDeleteQrcode(f.device_id,f.platform,h),await l()}catch(m){n.value="移除失败: "+m.message}finally{o.value=""}}return mt(()=>{l(),i=setInterval(l,15e3)}),Us(()=>{i&&clearInterval(i)}),(f,h)=>{const m=be("el-alert"),g=be("el-empty"),y=be("el-tag"),b=be("el-icon"),_=be("el-button"),w=Gr("loading");return st((x(),O("div",null,[n.value?(x(),ne(m,{key:0,title:n.value,type:"error","show-icon":"",closable:"",class:"mb-3"},null,8,["title"])):te("",!0),!t.value&&a.value.length===0?(x(),O("div",OVe,[L(g,{description:"暂无收款码数据"})])):(x(),O("div",$Ve,[(x(!0),O(Ve,null,dt(a.value,S=>(x(),O("div",{key:S.device_id+"-"+S.platform+"-"+(S.user_id??0),class:"qrcode-card"},[C("div",RVe,[C("span",NVe,Q(S.device_name||S.device_id),1),S.platform==="alipay"?(x(),ne(y,{key:0,type:"primary",size:"small"},{default:$(()=>[...h[0]||(h[0]=[ue("支付宝",-1)])]),_:1})):S.platform==="wechat"?(x(),ne(y,{key:1,type:"success",size:"small"},{default:$(()=>[...h[1]||(h[1]=[ue("微信",-1)])]),_:1})):te("",!0),L(y,{type:S.status==="active"?"success":"info",size:"small"},{default:$(()=>[ue(Q(S.status==="active"?"有效":"已过期"),1)]),_:2},1032,["type"]),(S.user_id??0)===999?(x(),ne(y,{key:2,type:"warning",size:"small",effect:"plain"},{default:$(()=>[...h[2]||(h[2]=[ue("双开",-1)])]),_:1})):(x(),ne(y,{key:3,size:"small",effect:"plain"},{default:$(()=>[...h[3]||(h[3]=[ue("主空间",-1)])]),_:1}))]),S.account?(x(),O("div",BVe,[h[4]||(h[4]=ue(" 收款账户: ",-1)),C("span",VVe,Q(S.account),1)])):(x(),O("div",zVe,[...h[5]||(h[5]=[ue("收款账户: ",-1),C("span",{class:"account-unknown"},"未知",-1)])])),C("div",FVe,[S.qr_content?(x(),O("div",HVe,[C("img",{src:s(S.qr_content),class:"qr-img",alt:"收款码"},null,8,WVe),C("div",KVe,[h[6]||(h[6]=ue(" 过期倒计时: ",-1)),C("span",{class:U({urgent:u(S)<15})},Q(c(S)),3)])])):(x(),O("div",UVe,"暂无收款码"))]),C("div",GVe,[S.platform==="wechat"?(x(),ne(_,{key:0,type:"success",size:"small",loading:r.value===S.device_id+"-"+(S.user_id??0),onClick:k=>d(S.device_id,"wechat",S.user_id??0)},{default:$(()=>[L(b,null,{default:$(()=>[L(p(p4))]),_:1}),h[7]||(h[7]=ue(" adb捕获 ",-1))]),_:1},8,["loading","onClick"])):te("",!0),S.platform==="alipay"?(x(),ne(_,{key:1,type:"warning",size:"small",loading:r.value===S.device_id+"-"+(S.user_id??0),onClick:k=>d(S.device_id,"alipay",S.user_id??0)},{default:$(()=>[L(b,null,{default:$(()=>[L(p(p4))]),_:1}),h[8]||(h[8]=ue(" adb捕获 ",-1))]),_:1},8,["loading","onClick"])):te("",!0),L(_,{type:"danger",size:"small",plain:"",loading:o.value===S.device_id+"-"+S.platform+"-"+(S.user_id??0),onClick:k=>v(S)},{default:$(()=>[L(b,null,{default:$(()=>[L(p(W6))]),_:1}),h[9]||(h[9]=ue(" 移除 ",-1))]),_:1},8,["loading","onClick"])])]))),128))]))])),[[w,t.value]])}}},jVe=fr(YVe,[["__scopeId","data-v-9290643a"]]),qVe={class:"sidebar"},XVe={class:"nav"},ZVe=["onClick"],JVe={class:"sidebar-foot"},QVe={class:"tunnel-text"},eze={class:"main-area"},tze={class:"topbar"},nze={class:"page-title"},aze={class:"topbar-right"},rze={class:"content"},oze={__name:"App",setup(e){const t=[{id:"dashboard",label:"仪表盘",icon:"Odometer"},{id:"channels",label:"支付通道",icon:"Connection"},{id:"orders",label:"订单管理",icon:"List"},{id:"notify",label:"回调队列",icon:"Bell"},{id:"logs",label:"系统日志",icon:"Document"},{id:"accounts",label:"账户池",icon:"Wallet"},{id:"upstreams",label:"上游对接",icon:"Share"},{id:"reconciler",label:"对账中心",icon:"WarningFilled"},{id:"ledger",label:"审计账本",icon:"Lock"},{id:"devices",label:"监听端",icon:"Iphone"},{id:"unmatched",label:"无订单收款",icon:"Coin"},{id:"qrcodes",label:"收款码",icon:"Camera"},{id:"settings",label:"系统设置",icon:"Setting"}],n={dashboard:"仪表盘",channels:"支付通道",orders:"订单管理",notify:"回调队列",logs:"系统日志",accounts:"账户池",upstreams:"上游对接",reconciler:"对账中心",ledger:"审计账本",devices:"监听端",unmatched:"无订单收款",qrcodes:"动态收款码",settings:"系统设置"},a=F(!1),r=F("dashboard"),o=F(!1),i=F(!1),l=F(0);mt(()=>{sessionStorage.getItem("admin_token")&&(a.value=!0);const h=localStorage.getItem("pg-theme");o.value=h==="dark",document.documentElement.classList.toggle("dark",o.value)});const s=D(()=>n[r.value]);function u(f){r.value=f}function c(f){i.value=(f==null?void 0:f.running)&&(f==null?void 0:f.healthy)}function d(){o.value=!o.value,document.documentElement.classList.toggle("dark",o.value),localStorage.setItem("pg-theme",o.value?"dark":"light")}function v(){l.value++}return(f,h)=>{const m=be("el-icon"),g=be("Refresh"),y=be("el-tooltip"),b=be("Sunny"),_=be("Moon");return a.value?(x(),O("div",{key:1,class:U(["app-shell",{dark:o.value}])},[C("aside",qVe,[h[2]||(h[2]=_7('
PayGateway
管理控制台
',1)),C("nav",XVe,[(x(),O(Ve,null,dt(t,w=>C("button",{key:w.id,class:U(["nav-item",{active:r.value===w.id}]),onClick:S=>u(w.id)},[L(m,{size:15},{default:$(()=>[(x(),ne(St(w.icon)))]),_:2},1024),C("span",null,Q(w.label),1),w.badge?(x(),O("span",{key:0,class:U(["nav-badge",w.badgeType])},Q(w.badge),3)):te("",!0)],10,ZVe)),64))]),C("div",JVe,[C("div",{class:U(["tunnel-mini",{ok:i.value}])},[h[1]||(h[1]=C("span",{class:"dot"},null,-1)),C("span",QVe,Q(i.value?"隧道正常":"未连接"),1)],2)])]),C("div",eze,[C("header",tze,[C("div",nze,Q(s.value),1),C("div",aze,[L(y,{content:"刷新数据",placement:"bottom"},{default:$(()=>[C("button",{class:"icon-btn",onClick:v},[L(m,{size:15},{default:$(()=>[L(g)]),_:1})])]),_:1}),L(y,{content:o.value?"浅色模式":"深色模式",placement:"bottom"},{default:$(()=>[C("button",{class:"icon-btn",onClick:d},[L(m,{size:15},{default:$(()=>[o.value?(x(),ne(b,{key:0})):(x(),ne(_,{key:1}))]),_:1})])]),_:1},8,["content"]),h[3]||(h[3]=C("div",{class:"avatar"},"G",-1))])]),C("main",rze,[L(Pa,{name:"page",mode:"out-in"},{default:$(()=>[r.value==="dashboard"?(x(),ne(VPe,{key:0,"refresh-key":l.value,onTunnel:c,onGoto:u},null,8,["refresh-key"])):r.value==="channels"?(x(),ne(TDe,{key:1,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="orders"?(x(),ne(JDe,{key:2,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="notify"?(x(),ne(wOe,{key:3,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="logs"?(x(),ne(jOe,{key:4,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="accounts"?(x(),ne(ARe,{key:5,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="upstreams"?(x(),ne(cNe,{key:6,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="reconciler"?(x(),ne(ONe,{key:7,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="ledger"?(x(),ne(F$e,{key:8,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="devices"?(x(),ne(jBe,{key:9,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="unmatched"?(x(),ne(pVe,{key:10,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="qrcodes"?(x(),ne(jVe,{key:11,"refresh-key":l.value},null,8,["refresh-key"])):r.value==="settings"?(x(),ne(DVe,{key:12,"refresh-key":l.value},null,8,["refresh-key"])):te("",!0)]),_:1})])])],2)):(x(),ne(Xwe,{key:0,onLoginSuccess:h[0]||(h[0]=w=>a.value=!0)}))}}},ize=fr(oze,[["__scopeId","data-v-b117130e"]]),kS=V7(ize);kS.use(Fwe,{locale:Hwe});for(const[e,t]of Object.entries(lie))kS.component(e,t);kS.mount("#app")});export default lze(); diff --git a/internal/admin/static/index.html b/internal/admin/static/index.html new file mode 100644 index 0000000..c7b8362 --- /dev/null +++ b/internal/admin/static/index.html @@ -0,0 +1,14 @@ + + + + + + PayGateway 管理后台 + + + + + +
+ + diff --git a/internal/admin/upstreams.go b/internal/admin/upstreams.go new file mode 100755 index 0000000..4b1a206 --- /dev/null +++ b/internal/admin/upstreams.go @@ -0,0 +1,453 @@ +// Package admin 管理后台:上游对接方管理 API +// +// 安全设计: +// - 本文件是唯一访问 upstreams.db 的组件 +// - 敏感字段(易支付密钥 key)AES-256-GCM 加密后落库 +// - 解密后的上游数据推送到 UpstreamPool(内存),供 epay 验签/回调签名使用 +// - 常规 API 响应不返回明文密钥(仅 "****" + 后4位) +// - 唯一例外:GET /upstreams/:id/secret(需登录,供管理后台「查看/复制完整密钥」),返回解密后的明文 key +package admin + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/account" + "paygateway/internal/logger" + "paygateway/internal/store" + "paygateway/internal/upstream" +) + +// UpstreamHandler 上游管理处理器 +type UpstreamHandler struct { + upstreamDB *store.UpstreamDB // 唯一访问 upstreams.db 的组件 + pool *upstream.UpstreamPool + initCrypto func(string) error // 初始化加密模块(与账户共用主密钥) + encFn func([]byte) (string, error) + decFn func(string) ([]byte, error) +} + +// NewUpstreamHandler 创建上游管理处理器 +func NewUpstreamHandler(db *store.UpstreamDB, pool *upstream.UpstreamPool, masterKey string) (*UpstreamHandler, error) { + // 复用 account 包的 AES-256-GCM 加密能力(主密钥与账户池一致) + if err := account.InitCrypto(masterKey); err != nil { + return nil, fmt.Errorf("上游加密初始化失败: %w", err) + } + return &UpstreamHandler{ + upstreamDB: db, + pool: pool, + initCrypto: account.InitCrypto, + encFn: account.Encrypt, + decFn: account.Decrypt, + }, nil +} + +// InitFromDB 启动时从 upstreams.db 加载所有上游,解密后初始化 UpstreamPool +func (h *UpstreamHandler) InitFromDB(ctx context.Context) error { + encrypted, err := h.upstreamDB.ListUpstreams(ctx) + if err != nil { + return fmt.Errorf("加载上游列表失败: %w", err) + } + var decryptErrs []string + for _, e := range encrypted { + up, err := h.decryptUpstream(e) + if err != nil { + logger.Error("upstream", "解密上游失败(主密钥不一致或数据损坏)", + "id", e.ID, "name", e.Name, "err", err) + decryptErrs = append(decryptErrs, fmt.Sprintf("id=%d name=%s: %v", e.ID, e.Name, err)) + continue + } + h.pool.Add(up) + } + // P1 守卫:DB 非空但所有/部分记录解密失败 = 主密钥变更或数据损坏,禁止静默启动(否则网关下单链路全断) + if len(decryptErrs) > 0 { + return fmt.Errorf("upstreams.db 解密失败 %d/%d 条(疑似 ACCOUNT_MASTER_KEY 变更),已拒绝启动;请恢复 .env 中的 ACCOUNT_MASTER_KEY 或清空 upstreams.db 后重新对接。失败明细:\n%s", + len(decryptErrs), len(encrypted), strings.Join(decryptErrs, "\n")) + } + logger.Info("upstream", "上游池初始化完成", "count", h.pool.Count()) + return nil +} + +// RegisterRoutes 注册上游管理路由(需在 authMiddleware 组内) +func (h *UpstreamHandler) RegisterRoutes(rg *gin.RouterGroup) { + rg.GET("/upstreams", h.handleListUpstreams) + rg.GET("/upstreams/:id", h.handleGetUpstream) + rg.POST("/upstreams", h.handleCreateUpstream) + rg.PUT("/upstreams/:id", h.handleUpdateUpstream) + rg.DELETE("/upstreams/:id", h.handleDeleteUpstream) + rg.POST("/upstreams/:id/enable", h.handleEnableUpstream) + rg.POST("/upstreams/:id/disable", h.handleDisableUpstream) + rg.GET("/upstreams/:id/logs", h.handleUpstreamLogs) + rg.GET("/upstreams/:id/secret", h.handleUpstreamSecret) +} + +// ========== API Handlers ========== + +// UpstreamResponse 前端展示的上游信息(密钥脱敏) +type UpstreamResponse struct { + ID int64 `json:"id"` + Name string `json:"name"` + Pid string `json:"pid"` + Key string `json:"key"` // 脱敏显示 + BaseURL string `json:"base_url"` + NotifyDomains string `json:"notify_domains"` + Status string `json:"status"` + Remark string `json:"remark,omitempty"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// upstreamToResponse 将 Upstream 转为前端响应(脱敏密钥) +func upstreamToResponse(up *upstream.Upstream) *UpstreamResponse { + return &UpstreamResponse{ + ID: up.ID, + Name: up.Name, + Pid: up.Pid, + Key: maskKey(up.Key), + BaseURL: up.BaseURL, + NotifyDomains: up.NotifyDomains, + Status: string(up.Status), + Remark: up.Remark, + CreatedAt: up.CreatedAt.Format(time.RFC3339), + UpdatedAt: up.UpdatedAt.Format(time.RFC3339), + } +} + +// encryptUpstream 加密上游的敏感字段,返回 UpstreamEncrypted(用于落库) +func (h *UpstreamHandler) encryptUpstream(up *upstream.Upstream) (*upstream.UpstreamEncrypted, error) { + e := &upstream.UpstreamEncrypted{ + Name: up.Name, + Pid: up.Pid, + BaseURL: up.BaseURL, + NotifyDomains: up.NotifyDomains, + Status: string(up.Status), + Remark: up.Remark, + CreatedAt: up.CreatedAt, + UpdatedAt: up.UpdatedAt, + } + var err error + if up.Key != "" { + e.KeyEnc, err = h.encFn([]byte(up.Key)) + if err != nil { + return nil, fmt.Errorf("加密易支付密钥失败: %w", err) + } + } + return e, nil +} + +// decryptUpstream 解密上游的敏感字段,返回 Upstream(用于内存) +func (h *UpstreamHandler) decryptUpstream(e *upstream.UpstreamEncrypted) (*upstream.Upstream, error) { + up := &upstream.Upstream{ + ID: e.ID, + Name: e.Name, + Pid: e.Pid, + BaseURL: e.BaseURL, + NotifyDomains: e.NotifyDomains, + Status: upstream.UpstreamStatus(e.Status), + Remark: e.Remark, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + } + if e.KeyEnc != "" { + data, err := h.decFn(e.KeyEnc) + if err != nil { + return nil, fmt.Errorf("解密易支付密钥失败: %w", err) + } + up.Key = string(data) + } + return up, nil +} + +// ========== List ========== + +func (h *UpstreamHandler) handleListUpstreams(c *gin.Context) { + ups := h.pool.All() + resp := make([]*UpstreamResponse, 0, len(ups)) + for _, up := range ups { + resp = append(resp, upstreamToResponse(up)) + } + c.JSON(http.StatusOK, gin.H{"data": resp}) +} + +// ========== Get ========== + +func (h *UpstreamHandler) handleGetUpstream(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + up := h.pool.ByID(id) + if up == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "上游不存在"}) + return + } + c.JSON(http.StatusOK, gin.H{"data": upstreamToResponse(up)}) +} + +// handleUpstreamSecret 返回上游完整密钥(明文,需登录)。 +// 用途:管理后台「查看/复制完整密钥」与「对接指引」复制真实可用的密钥。 +// 安全提示:仅登录用户可调用;前端展示时应二次确认并提示勿泄露。 +func (h *UpstreamHandler) handleUpstreamSecret(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + up := h.pool.ByID(id) + if up == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "上游不存在"}) + return + } + logger.Info("upstream", "上游密钥被查看(明文)", "id", id, "name", up.Name, "operator", c.GetString("username")) + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "id": up.ID, + "name": up.Name, + "pid": up.Pid, + "key": up.Key, + }}) +} + +// ========== Create ========== + +type createUpstreamReq struct { + Name string `json:"name" binding:"required"` + Pid string `json:"pid" binding:"required"` + Key string `json:"key"` + BaseURL string `json:"base_url"` + NotifyDomains string `json:"notify_domains"` + Remark string `json:"remark"` +} + +func (h *UpstreamHandler) handleCreateUpstream(c *gin.Context) { + var req createUpstreamReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + if req.Key == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "易支付密钥不能为空"}) + return + } + + now := time.Now() + up := &upstream.Upstream{ + Name: req.Name, + Pid: req.Pid, + Key: req.Key, + BaseURL: req.BaseURL, + NotifyDomains: req.NotifyDomains, + Status: upstream.StatusEnabled, + Remark: req.Remark, + CreatedAt: now, + UpdatedAt: now, + } + + // 加密敏感字段 + enc, err := h.encryptUpstream(up) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 写入 upstreams.db(仅 Admin API 可访问) + id, err := h.upstreamDB.CreateUpstream(c.Request.Context(), enc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上游失败(pid 可能已存在): " + err.Error()}) + return + } + up.ID = id + + // 推送到内存池(其他模块立即可用) + h.pool.Add(up) + + // 审计日志 + _ = h.upstreamDB.InsertUpstreamLog(c.Request.Context(), &upstream.UpstreamLog{ + UpstreamID: id, + Action: "create", + Detail: fmt.Sprintf("创建上游: %s (pid=%s)", req.Name, req.Pid), + Operator: c.GetString("username"), + }) + + logger.Info("upstream", "上游创建成功", "id", id, "name", req.Name, "pid", req.Pid) + c.JSON(http.StatusOK, gin.H{"data": upstreamToResponse(up)}) +} + +// ========== Update ========== + +type updateUpstreamReq struct { + Name string `json:"name" binding:"required"` + Pid string `json:"pid" binding:"required"` + Key string `json:"key"` + BaseURL string `json:"base_url"` + NotifyDomains string `json:"notify_domains"` + Remark string `json:"remark"` +} + +func (h *UpstreamHandler) handleUpdateUpstream(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + + var req updateUpstreamReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()}) + return + } + + // 保留原有状态与密钥(密钥字段前端不传(空)时保留原值,避免误清空) + existing := h.pool.ByID(id) + if existing == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "上游不存在"}) + return + } + + up := &upstream.Upstream{ + ID: id, + Name: req.Name, + Pid: req.Pid, + Key: existing.Key, + BaseURL: req.BaseURL, + NotifyDomains: req.NotifyDomains, + Status: existing.Status, + Remark: req.Remark, + CreatedAt: existing.CreatedAt, + UpdatedAt: time.Now(), + } + if req.Key != "" { + up.Key = req.Key + } + + // 加密敏感字段 + enc, err := h.encryptUpstream(up) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + enc.ID = id + + // 更新 upstreams.db + if err := h.upstreamDB.UpdateUpstream(c.Request.Context(), enc); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新上游失败(pid 可能已存在): " + err.Error()}) + return + } + + // 更新内存池 + h.pool.Update(up) + + // 审计日志 + _ = h.upstreamDB.InsertUpstreamLog(c.Request.Context(), &upstream.UpstreamLog{ + UpstreamID: id, + Action: "update", + Detail: fmt.Sprintf("更新上游: %s (pid=%s)", req.Name, req.Pid), + Operator: c.GetString("username"), + }) + + logger.Info("upstream", "上游更新成功", "id", id) + c.JSON(http.StatusOK, gin.H{"data": upstreamToResponse(up)}) +} + +// ========== Delete ========== + +func (h *UpstreamHandler) handleDeleteUpstream(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + + ctx := c.Request.Context() + + // 删除数据库 + if err := h.upstreamDB.DeleteUpstream(ctx, id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除上游失败: " + err.Error()}) + return + } + + // 移除内存池 + h.pool.Remove(id) + + // 审计日志 + _ = h.upstreamDB.InsertUpstreamLog(ctx, &upstream.UpstreamLog{ + UpstreamID: id, + Action: "delete", + Detail: fmt.Sprintf("删除上游 ID: %d", id), + Operator: c.GetString("username"), + }) + + logger.Info("upstream", "上游已删除", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// ========== Enable / Disable ========== + +func (h *UpstreamHandler) handleEnableUpstream(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + ctx := c.Request.Context() + + if err := h.upstreamDB.SetUpstreamStatus(ctx, id, string(upstream.StatusEnabled)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + h.pool.SetStatus(id, upstream.StatusEnabled) + + _ = h.upstreamDB.InsertUpstreamLog(ctx, &upstream.UpstreamLog{ + UpstreamID: id, Action: "enable", Detail: "手动启用", Operator: c.GetString("username"), + }) + logger.Info("upstream", "上游已启用", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +func (h *UpstreamHandler) handleDisableUpstream(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + ctx := c.Request.Context() + + if err := h.upstreamDB.SetUpstreamStatus(ctx, id, string(upstream.StatusDisabled)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + h.pool.SetStatus(id, upstream.StatusDisabled) + + _ = h.upstreamDB.InsertUpstreamLog(ctx, &upstream.UpstreamLog{ + UpstreamID: id, Action: "disable", Detail: "手动禁用", Operator: c.GetString("username"), + }) + logger.Info("upstream", "上游已禁用", "id", id) + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} + +// ========== Upstream Logs ========== + +func (h *UpstreamHandler) handleUpstreamLogs(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效上游ID"}) + return + } + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "30")) + if limit <= 0 || limit > 100 { + limit = 30 + } + + logs, err := h.upstreamDB.ListUpstreamLogs(c.Request.Context(), id, limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": logs}) +} diff --git a/internal/admin/wizard.go b/internal/admin/wizard.go new file mode 100644 index 0000000..62933d3 --- /dev/null +++ b/internal/admin/wizard.go @@ -0,0 +1,377 @@ +// 引导向导(USB 真机集成自动化) +// +// 目标:把「插 USB → 开 USB 调试 → 绑设备」全流程从手动操作变成 web 前端逐步引导。 +// 2026-08-30 重构:监听端 APP 弃用,向导收敛为「绑定设备即完成」; +// 设备上线/账户解冻由 DeviceWatcher 按 adb 可达性驱动(不再依赖心跳/完成度门槛)。 +// +// 三步接口(全部在 authMiddleware 组内): +// - GET /personal/usb-detect 前端轮询 USB 设备插入状态 +// - POST /personal/devices/wizard-start 绑定设备(幂等,断点续传) +// - POST /personal/devices/:id/wizard-wifi 配置 WiFi 备用通道(USB 断线自动切换) +// - POST /personal/devices/:id/wizard-verify 验证 adb 连通 + 返回保活引导(wizard 收尾) +// +// 完成度推进: +// +// wizard-start → setup_progress=["usb_connected","bound"] +package admin + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + + "paygateway/internal/account" + "paygateway/internal/logger" + "paygateway/internal/personal" +) + +// handleUSBCheck 列出当前已连 USB 真机中「未绑定本平台」的设备。 +// GET /api/admin/personal/usb-detect +// +// 前端 2-3s 轮询,识别新插入的设备(state 从 unauthorized → device 表示手机端已授权)。 +// 响应:{"data":{"devices":[{"serial":"4f2bdc30","state":"device","model":"MI 8","bound":false},...]}} +func (h *PersonalHandler) handleUSBCheck(c *gin.Context) { + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "adb 管理器未配置,USB 检测不可用"}) + return + } + devs, err := h.adbMgr.ListDevices(c.Request.Context()) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "设备检测失败: " + err.Error()}) + return + } + // 已绑定 serial 集合(匹配 adb_host,USB 纯 serial 与 WiFi host:port 皆算) + bound := h.boundSerials(c.Request.Context()) + out := make([]gin.H, 0, len(devs)) + for _, d := range devs { + _, isBound := bound[d.Serial] + out = append(out, gin.H{ + "serial": d.Serial, "state": d.State, "model": d.Model, "kind": d.Kind, "bound": isBound, + }) + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"devices": out}}) +} + +// boundSerials 返回当前已绑定设备的 adb serial 集合(累加 USB 主通道 + WiFi 备用通道) +func (h *PersonalHandler) boundSerials(ctx context.Context) map[string]int64 { + out := make(map[string]int64) + devs, err := h.pdb.ListDevices(ctx) + if err != nil { + return out + } + for _, d := range devs { + if d.AdbHost != "" { + out[d.AdbHost] = d.ID + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + out[fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort)] = d.ID + } + } + return out +} + +func containsColon(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] == ':' { + return true + } + } + return false +} + +// wizardStartReq wizard-start 请求体 +type wizardStartReq struct { + Serial string `json:"serial"` // USB serial(必填) + Name string `json:"name"` // 设备备注名(必填) + AccountID int64 `json:"account_id"` // 收款账户 ID(必填) +} + +// handleWizardStart 引导第 3 步:绑定设备(创建设备记录 + 写 setup 进度)。 +// POST /api/admin/personal/devices/wizard-start +// +// 幂等 + 断点续传: +// - serial 未绑定 → 创建设备,setup_progress=["usb_connected","bound"] +// - serial 已绑定 → 返回已有设备 + 当前进度(前端从断点继续,不重复创建) +// +// 响应:{"data":{"ok":true,"id":7,"device_id":"...","setup_progress":["usb_connected","bound"]}} +func (h *PersonalHandler) handleWizardStart(c *gin.Context) { + var req wizardStartReq + if err := c.ShouldBindJSON(&req); err != nil || req.Serial == "" || req.Name == "" || req.AccountID <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误:需要 serial + name + account_id"}) + return + } + // serial 可为 USB 直连设备号(不含 ":")或 WiFi host:port(含 ":",须已 adb connect 过) + if req.Serial == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误:需要 serial"}) + return + } + acct := h.pool.GetByID(req.AccountID) + if acct == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "账户不存在"}) + return + } + if !isPersonalChannelName(acct.Channel) { + c.JSON(http.StatusBadRequest, gin.H{"error": "账户不是个人通道账户(channel=" + acct.Channel + ")"}) + return + } + if acct.QRContent == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "账户未配置收款码(qr_content),请先上传收款码"}) + return + } + // 幂等:serial 已绑定 → 断点续传 + devs, err := h.pdb.ListDevices(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + for _, d := range devs { + if d.AdbHost == req.Serial { + steps := personal.ParseSetupProgress(d.SetupProgress) + // 已绑定即视为已接入(上线判定由 DeviceWatcher 按 adb 可达性驱动) + if personal.HasSetupStep(steps, personal.SetupStepBound) { + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "ok": true, "id": d.ID, "device_id": d.DeviceID, + "setup_progress": steps, "already_complete": true, + }}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "ok": true, "id": d.ID, "device_id": d.DeviceID, + "setup_progress": steps, "resumed": true, + }}) + return + } + } + // 新建:生成设备密钥 + 加密 + 写入(绑定逻辑内联,密钥仅此处返回一次) + secret, err := personal.NewDeviceSecret() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "生成设备密钥失败: " + err.Error()}) + return + } + secretEnc, err := account.Encrypt([]byte(secret)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加密设备密钥失败: " + err.Error()}) + return + } + provider := providerFromChannelName(acct.Channel) + deviceID := personal.NewDeviceID() + steps := []string{personal.SetupStepUSBConnected, personal.SetupStepBound} + d := &personal.DeviceEncrypted{ + DeviceID: deviceID, Name: req.Name, AccountID: acct.ID, + Provider: provider, SecretEnc: secretEnc, Status: string(personal.DeviceOffline), + AdbHost: req.Serial, + // 完成度门槛:新建即标记 usb_connected + bound,账户后续由心跳/删除 handler 守恒冻结 + SetupProgress: personal.FormatSetupProgress(steps), + } + id, err := h.pdb.CreateDevice(c.Request.Context(), d) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建设备失败: " + err.Error()}) + return + } + // 账户冻结守恒:新建半配置设备 → 该账户若无其他在线设备则冻结 + // (设备 adb 可达后由 DeviceWatcher 自动解冻) + if !h.accountHasConnectedDevice(c.Request.Context(), acct.ID) { + if a := h.pool.GetByID(acct.ID); a != nil && a.Status == account.StatusNormal { + h.pool.FreezeByOffline(acct.ID) + logger.Info("admin", "账户已冻结(wizard 新建设备未完成 setup)", "account_id", acct.ID, "name", a.Name) + } + } + logger.Info("admin", "向导设备已绑定", "device_id", deviceID, "name", req.Name, "serial", req.Serial, "account", acct.ID) + c.JSON(http.StatusOK, gin.H{"data": gin.H{ + "ok": true, "id": id, "device_id": deviceID, + "setup_progress": steps, + }}) +} + +// handleWizardWifiBackup 自动配置 WiFi 备用通道(引导向导第 4 步前)。 +// POST /api/admin/personal/devices/:id/wizard-wifi +// +// 全自动,无需用户填写任何 WiFi 信息: +// 1. 设备必须 USB 直连(AdbHost 为纯 serial)——WiFi 备用通道的开启依赖 USB 先授权 +// 2. adb tcpip 5555:开启设备无线 adb(进入 TCP/IP 监听模式) +// 3. 自动获取设备 WiFi 局域网 IP(wlan0/eth0/getprop 多路尝试) +// 4. 组合 :5555 写入 adb_wifi_host/adb_wifi_port(设备 USB 断开后自动切换保活) +// +// 幂等:备用通道已配置且设备已在 TCP 监听 → 直接返回现有配置不重复执行。 +// 失败不阻塞主流程(返回 ok=false + warning),USB 直连本身仍可用。 +func (h *PersonalHandler) handleWizardWifiBackup(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "adb 管理器未配置"}) + return + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + // 仅 USB 直连设备可自动配置(依赖 USB 通道的 adb 权限) + if dev.AdbHost == "" || containsColon(dev.AdbHost) { + c.JSON(http.StatusBadRequest, gin.H{"error": "仅 USB 直连设备可自动配置 WiFi 备用通道"}) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + serial := dev.AdbHost + // 幂等:已有备用通道且设备在线(TCP 监听已开)→ 直接返回 + if dev.AdbWifiHost != "" && dev.AdbWifiPort > 0 { + wifiSerial := fmt.Sprintf("%s:%d", dev.AdbWifiHost, dev.AdbWifiPort) + if h.adbMgr.Online(ctx, wifiSerial) { + logger.Info("admin", "WiFi 备用通道已生效,跳过配置", "device", dev.DeviceID, "wifi", wifiSerial) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "wifi_host": dev.AdbWifiHost, "wifi_port": dev.AdbWifiPort}}) + return + } + } + // 1. 开启无线 adb + if err := h.adbMgr.TCPIP(ctx, serial, 5555); err != nil { + logger.Warn("admin", "开启无线 adb 失败", "device", dev.DeviceID, "err", err) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": false, "warning": "开启无线 adb 失败:" + err.Error()}}) + return + } + // adb tcpip 会重启设备端 adbd(切到 TCP 监听模式),USB adb 通道短暂中断。 + // 立即查 IP 会失败(adbd 未就绪),等待 2s 让其恢复后再获取 IP。 + time.Sleep(2 * time.Second) + // 2. 获取 WiFi IP(重试 2 次,覆盖 adbd 重启窗口) + var ip string + for attempt := 0; attempt < 3; attempt++ { + ip = h.adbMgr.WifiIP(ctx, serial) + if ip != "" { + break + } + if attempt < 2 { + time.Sleep(time.Second) + } + } + if ip == "" { + logger.Warn("admin", "获取设备 WiFi IP 失败(设备可能未连 WiFi)", "device", dev.DeviceID) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": false, "warning": "未获取到设备 WiFi IP(请确认手机已连接 WiFi)"}}) + return + } + // 3. 写入备用通道 + wifiPort := 5555 + if err := h.pdb.SetDeviceAdb(c.Request.Context(), id, dev.AdbHost, dev.AdbPort, ip, wifiPort); err != nil { + logger.Warn("admin", "写入 WiFi 备用通道失败", "err", err, "id", id) + c.JSON(http.StatusInternalServerError, gin.H{"error": "写入 WiFi 备用通道失败: " + err.Error()}) + return + } + logger.Info("admin", "WiFi 备用通道已自动配置", "device", dev.DeviceID, "serial", serial, "wifi", fmt.Sprintf("%s:%d", ip, wifiPort)) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "wifi_host": ip, "wifi_port": wifiPort}}) +} + +// handleWizardVerify 引导最后一步:验证 adb 连通性 + 返回保活引导提示。 +// POST /api/admin/personal/devices/:id/wizard-verify +// +// 2026-08-30 重构:监听端 APP 弃用,向导不再下发配置/拉起 App; +// 设备上线由 DeviceWatcher 按 adb 可达性驱动。本步骤只做 adb 连通验证, +// 并返回 MIUI 省电策略"无限制"的保活引导(墓碑冻结无 adb 入口,需 UI 设置)。 +func (h *PersonalHandler) handleWizardVerify(c *gin.Context) { + id, _ := strconv.ParseInt(c.Param("id"), 10, 64) + if id <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + if h.adbMgr == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "adb 管理器未配置"}) + return + } + dev, err := h.pdb.GetDeviceByID(c.Request.Context(), id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + serial, ok := h.pickSerialForWizard(c.Request.Context(), dev) + if !ok { + c.JSON(http.StatusBadGateway, gin.H{"error": "设备离线:USB 未连接或未授权(请确认手机已开 USB 调试并允许)"}) + return + } + resp := gin.H{"ok": true, "serial": serial} + // 保活引导提示(MIUI 墓碑冻结/杀后台需 UI 层"省电策略→无限制"才能豁免, + // 无 root 无 adb 入口——实测 2026-08-30 红米k60/MIUI14)。 + // 微信/支付宝需无限制,否则后台被系统冻结会掉线。 + resp["keepalive_hint"] = fmt.Sprintf( + "请在手机上完成保活设置(共 %d 个应用):设置→应用设置→应用管理→%s→省电策略→选择「无限制」。"+ + "不设置的话系统可能冻结后台进程导致掉线。", keepaliveHintAppsCount(dev.Provider), keepaliveHintApps(dev.Provider)) + c.JSON(http.StatusOK, gin.H{"data": resp}) +} + +// keepaliveHintApps 保活引导的应用显示名(支付通道 APP,按 provider 区分)。 +func keepaliveHintApps(provider string) string { + return "「" + keepaliveChannelApp(provider) + "」" +} + +// keepaliveHintAppsCount 保活引导的应用数量(当前恒为 1:支付通道 APP)。 +func keepaliveHintAppsCount(provider string) int { + return 1 +} + +// keepaliveChannelApp 支付通道 APP 显示名(与包名映射,用于引导文案)。 +func keepaliveChannelApp(provider string) string { + switch provider { + case "wechat": + return "微信" + case "alipay": + return "支付宝" + default: + return "" + } +} + +// pickSerialForWizard 取设备可用的 adb serial 并校验在线(向导专用)。 +// AdbHost 可为 USB 纯 serial 或 WiFi host:port;没有或不可用时回退 WiFi 备用通道。 +func (h *PersonalHandler) pickSerialForWizard(ctx context.Context, dev *personal.DeviceEncrypted) (string, bool) { + candidates := make([]string, 0, 2) + if dev.AdbHost != "" { + candidates = append(candidates, dev.AdbHost) + } + if dev.AdbWifiHost != "" && dev.AdbWifiPort > 0 { + candidates = append(candidates, fmt.Sprintf("%s:%d", dev.AdbWifiHost, dev.AdbWifiPort)) + } + for _, s := range candidates { + if h.adbMgr.Online(ctx, s) { + return s, true + } + } + return "", false +} + +// handleWifiReconnect 设备断网/断 USB 后重连 WiFi 备用通道(adb connect 重建链路)。 +// 心跳走公网隧道可能已恢复(status=online),但 scrcpy 画面流的 adb 连接会在断网时失效, +// 需要重新 adb connect 才能恢复实时画面。成功返回可用的 serial 供前端重连, +// USB 主通道若在线则优先返回 USB serial,否则尝试重连 WiFi。 +func (h *PersonalHandler) handleWifiReconnect(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "设备 id 无效"}) + return + } + ctx := c.Request.Context() + dev, err := h.pdb.GetDeviceByID(ctx, id) + if err != nil || dev == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "设备不存在"}) + return + } + + if dev.AdbHost != "" && h.adbMgr.Online(ctx, dev.AdbHost) { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"serial": dev.AdbHost, "channel": "usb"}}) + return + } + + if dev.AdbWifiHost != "" && dev.AdbWifiPort > 0 { + serial := fmt.Sprintf("%s:%d", dev.AdbWifiHost, dev.AdbWifiPort) + if err := h.adbMgr.Connect(ctx, serial); err == nil && h.adbMgr.Online(ctx, serial) { + c.JSON(http.StatusOK, gin.H{"data": gin.H{"serial": serial, "channel": "wifi"}}) + return + } + } + + c.JSON(http.StatusBadGateway, gin.H{"error": "设备离线:USB 未连接且 WiFi 重连失败,请确认设备在线并已开 USB 调试"}) +} diff --git a/internal/admin/wizard_test.go b/internal/admin/wizard_test.go new file mode 100644 index 0000000..0cbbbc9 --- /dev/null +++ b/internal/admin/wizard_test.go @@ -0,0 +1,39 @@ +package admin + +import ( + "strings" + "testing" +) + +// TestKeepaliveHintApps 保活引导文案的支付通道应用映射。 +// 2026-08-30:监听端 APP 弃用,保活引导只包含支付通道 APP。 +func TestKeepaliveHintApps(t *testing.T) { + cases := []struct { + provider string + wantApp string // 期望文案包含的支付 APP 显示名 + wantChannel string // 期望的通道显示名(空=无) + count int + }{ + {"wechat", "「微信」", "微信", 1}, + {"alipay", "「支付宝」", "支付宝", 1}, + {"unknown", "", "", 1}, + {"", "", "", 1}, + } + for _, tc := range cases { + t.Run(tc.provider, func(t *testing.T) { + apps := keepaliveHintApps(tc.provider) + if tc.wantApp != "" && !strings.Contains(apps, tc.wantApp) { + t.Fatalf("keepaliveHintApps(%q)=%q 应包含 %q", tc.provider, apps, tc.wantApp) + } + if strings.Contains(apps, "监听端") { + t.Fatalf("keepaliveHintApps(%q)=%q 不应再包含监听端 APP", tc.provider, apps) + } + if got := keepaliveHintAppsCount(tc.provider); got != tc.count { + t.Fatalf("keepaliveHintAppsCount(%q)=%d, want %d", tc.provider, got, tc.count) + } + if got := keepaliveChannelApp(tc.provider); got != tc.wantChannel { + t.Fatalf("keepaliveChannelApp(%q)=%q, want %q", tc.provider, got, tc.wantChannel) + } + }) + } +} diff --git a/internal/alert/alert.go b/internal/alert/alert.go new file mode 100755 index 0000000..74119d1 --- /dev/null +++ b/internal/alert/alert.go @@ -0,0 +1,179 @@ +// Package alert 告警系统(RESILIENCE.md P1-3) +// +// 支持 Bark(iOS 推送)与通用 Webhook(POST JSON),异步发送 + 防抖: +// - 发送失败静默重试 2 次(不阻塞业务主链路) +// - 同类型告警 1 分钟窗口内去重(防止刷屏) +// - 配置:ALERT_BARK_URL / ALERT_WEBHOOK_URL(env),留空则该通道禁用 +package alert + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "runtime/debug" + "sync" + "time" + + "paygateway/internal/logger" +) + +// Client 告警客户端 +type Client struct { + barkURL string // Bark 推送 URL(形如 https://api.day.app/) + webhookURL string // 通用 Webhook URL(POST JSON {title, body, level, fields}) + client *http.Client + + mu sync.Mutex + last map[string]time.Time // 防抖:title → 上次发送时间 +} + +// Options 告警配置 +type Options struct { + BarkURL string + WebhookURL string +} + +// New 创建告警客户端(两者均空时返回 nil,调用方跳过) +func New(opts Options) *Client { + if opts.BarkURL == "" && opts.WebhookURL == "" { + return nil + } + return &Client{ + barkURL: opts.BarkURL, + webhookURL: opts.WebhookURL, + client: &http.Client{Timeout: 8 * time.Second}, + last: make(map[string]time.Time), + } +} + +// Enabled 是否有任何告警通道 +func (a *Client) Enabled() bool { return a != nil } + +// dedup 防抖:同 title 窗口内去重 +func (a *Client) dedup(title string) bool { + a.mu.Lock() + defer a.mu.Unlock() + now := time.Now() + if t, ok := a.last[title]; ok && now.Sub(t) < time.Minute { + return false + } + a.last[title] = now + return true +} + +// Send 发送告警(异步,不阻塞) +// level: info / warn / error;title: 告警类型(防抖 key);body: 详情 +func (a *Client) Send(level, title, body string) { + if a == nil { + return + } + if !a.dedup(title) { + return // 1 分钟窗口内已发过同类告警 + } + title = fmt.Sprintf("[%s] %s", level, title) + logger.Warn("alert", "发送告警", "level", level, "title", title, "body", body) + + go func() { + defer func() { + if r := recover(); r != nil { + logger.Error("alert", "告警发送协程 panic(已恢复)", "title", title, + "panic", r, "stack", string(debug.Stack())) + } + }() + // 重试 3 次(1s/3s/9s 退避),全部失败仅记日志 + var lastErr error + for i := 0; i < 3; i++ { + if err := a.sendOnce(level, title, body); err == nil { + return + } else { + lastErr = err + } + time.Sleep(time.Duration(1<= 400 { + return fmt.Errorf("bark HTTP %d", resp.StatusCode) + } + return nil +} + +// sendWebhook 通用 Webhook(POST JSON) +func (a *Client) sendWebhook(level, title, body string) error { + payload, _ := json.Marshal(map[string]string{ + "level": level, + "title": title, + "body": body, + "time": time.Now().Format(time.RFC3339), + }) + req, err := http.NewRequest(http.MethodPost, a.webhookURL, bytes.NewReader(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + resp, err := a.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + return fmt.Errorf("webhook HTTP %d", resp.StatusCode) + } + return nil +} + +// Close 关闭空闲连接 +func (a *Client) Close() { + if a == nil { + return + } + a.client.CloseIdleConnections() +} + +var _ = context.Background diff --git a/internal/channel/alipay/alipay.go b/internal/channel/alipay/alipay.go new file mode 100755 index 0000000..a50d1c1 --- /dev/null +++ b/internal/channel/alipay/alipay.go @@ -0,0 +1,447 @@ +// Package alipay 支付宝支付通道(基于 gopay 库) +// +// 账户池集成: +// - 持有 AccountPool,下单选账户、回调反查 app_id +// - 每个账户对应一个 gopay.Client,按需创建并缓存 +// - 下单成功/失败后上报结果给 pool(熔断统计) +package alipay + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/alipay" + "paygateway/internal/account" + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/model" +) + +// Config 支付宝通道配置(仅回调地址 + 调试开关;账户密钥一律来自账户池) +type Config struct { + NotifyURL string // 异步回调地址 + Debug bool // 调试日志 +} + +// Channel 支付宝通道实现 +type Channel struct { + cfg Config + pool *account.AccountPool // 账户池(nil = 传统单账户模式) + clientCache map[int64]*alipay.Client // 账户ID → gopay client 缓存 + clientMu sync.RWMutex + modesMu sync.RWMutex // 保护 enabledModes(管理后台切换与下单并发) + enabledModes map[string]bool // 支付模式启停状态,默认全开 +} + +// defaultEnabledModes 默认开启所有模式 +func defaultEnabledModes() map[string]bool { + return map[string]bool{ + "page": true, + "wap": true, + "qrcode": true, + } +} + +// New 创建支付宝通道(账户池模式:所有账户数据从 pool 读取) +func New(cfg Config, pool *account.AccountPool) (*Channel, error) { + ch := &Channel{ + cfg: cfg, + pool: pool, + clientCache: make(map[int64]*alipay.Client), + enabledModes: defaultEnabledModes(), + } + return ch, nil +} + +// newClient 创建 gopay 支付宝客户端 +func (c *Channel) newClient(appID, privateKey string, isProd bool, notifyURL string, debug bool) (*alipay.Client, error) { + client, err := alipay.NewClient(appID, privateKey, isProd) + if err != nil { + return nil, fmt.Errorf("支付宝客户端初始化失败: %w", err) + } + client.SetCharset("utf-8"). + SetSignType(alipay.RSA2). + SetNotifyUrl(notifyURL) + client.GetHttpClient().SetTimeout(120 * time.Second) + if debug { + client.DebugSwitch = gopay.DebugOn + } + return client, nil +} + +// getOrCreateClient 获取或创建账户对应的 gopay client +func (c *Channel) getOrCreateClient(acct *account.Account) (*alipay.Client, error) { + // 先查缓存 + c.clientMu.RLock() + if cli, ok := c.clientCache[acct.ID]; ok { + c.clientMu.RUnlock() + return cli, nil + } + c.clientMu.RUnlock() + + // 创建新 client + c.clientMu.Lock() + defer c.clientMu.Unlock() + + // 双重检查 + if cli, ok := c.clientCache[acct.ID]; ok { + return cli, nil + } + + notifyURL := c.cfg.NotifyURL + debug := c.cfg.Debug + client, err := c.newClient(acct.AppID, acct.PrivateKey, acct.IsProd, notifyURL, debug) + if err != nil { + return nil, err + } + c.clientCache[acct.ID] = client + logger.Info("alipay", "账户客户端已创建", "account_id", acct.ID, "name", acct.Name) + return client, nil +} + +// SetNotifyURL 动态更新回调地址(隧道重启换域名时调用) +func (c *Channel) SetNotifyURL(url string) { + c.cfg.NotifyURL = url + // 更新所有缓存的 client + c.clientMu.Lock() + defer c.clientMu.Unlock() + for id, client := range c.clientCache { + client.SetNotifyUrl(url) + _ = id + } + logger.Info("alipay", "回调地址已更新", "notify_url", url) +} + +// RemoveClient 移除账户对应的 gopay client 缓存。 +// 账户更新(app_id / 私钥变更)后必须调用,否则缓存的旧 client 会继续使用旧参数下单。 +func (c *Channel) RemoveClient(accountID int64) { + c.clientMu.Lock() + if _, ok := c.clientCache[accountID]; ok { + delete(c.clientCache, accountID) + logger.Info("alipay", "账户客户端缓存已清除", "account_id", accountID) + } + c.clientMu.Unlock() +} + +// Name 通道标识 +func (c *Channel) Name() string { return "merchant.alipay" } + +// Type 通道大类:商家通道 +func (c *Channel) Type() channel.ChannelType { return channel.TypeMerchant } + +// Provider 支付品牌:支付宝 +func (c *Channel) Provider() channel.Provider { return channel.ProviderAlipay } + +// ConfirmAbility 确认能力:可主动查单 +func (c *Channel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByQuery } + +// NotifyURL 返回当前回调地址(管理后台展示) +func (c *Channel) NotifyURL() string { return c.cfg.NotifyURL } + +// MchID 支付宝无商户号概念 +func (c *Channel) MchID() string { return "" } + +// Kind 通道类型:回调驱动 +func (c *Channel) Kind() channel.ChannelKind { return channel.KindCallback } + +// Modes 所有支持的支付模式 +func (c *Channel) Modes() []channel.PayMode { + return []channel.PayMode{channel.ModePage, channel.ModeWap, channel.ModeQRCode} +} + +// SetModeEnabled 开启/关闭某个支付模式 +func (c *Channel) SetModeEnabled(mode string, enabled bool) { + c.modesMu.Lock() + c.enabledModes[mode] = enabled + c.modesMu.Unlock() + logger.Info("alipay", "支付模式已切换", "mode", mode, "enabled", enabled) +} + +// IsModeEnabled 检查某个支付模式是否开启 +func (c *Channel) IsModeEnabled(mode string) bool { + c.modesMu.RLock() + defer c.modesMu.RUnlock() + return c.enabledModes[mode] +} + +// EnabledModes 返回当前启用的模式列表 +func (c *Channel) EnabledModes() map[string]bool { + c.modesMu.RLock() + defer c.modesMu.RUnlock() + result := make(map[string]bool) + for k, v := range c.enabledModes { + result[k] = v + } + return result +} + +// Listener 回调型无监听器 +func (c *Channel) Listener() channel.EventListener { return nil } + +// selectAccount 从账户池选一个可用账户 +// 返回 nil 表示无可用账户或未启用账户池 +func (c *Channel) selectAccount(group string) *account.Account { + if c.pool == nil { + return nil + } + return c.pool.Select("alipay", group) +} + +// CreatePayment 下单:按 PayMode 创建对应支付宝订单 +func (c *Channel) CreatePayment(ctx context.Context, req *channel.PayRequest) (*channel.PayCredential, error) { + payMode := c.resolveMode(req) + + // 检查支付模式是否已开启(resolveMode 已降级,走到这里说明全部模式都已关闭) + if !c.IsModeEnabled(string(payMode)) { + enabled := make([]string, 0) + for m, on := range c.enabledModes { + if on { + enabled = append(enabled, m) + } + } + return nil, fmt.Errorf("支付模式 %q 未开启(当前已开启: %v)", payMode, enabled) + } + + // 从账户池选账户 + var acct *account.Account + if c.pool != nil { + acct = c.pool.Select("alipay", "default") + if acct == nil { + return nil, fmt.Errorf("支付宝暂无可用账户(所有账户已达日限额或冷却中)") + } + } + + // 获取 gopay client(账户池模式:必选账户) + var client *alipay.Client + if acct == nil { + return nil, fmt.Errorf("支付宝暂无可用账户") + } + client, err := c.getOrCreateClient(acct) + if err != nil { + return nil, fmt.Errorf("获取支付宝客户端失败: %w", err) + } + + bm := make(gopay.BodyMap) + bm.Set("subject", req.Subject). + Set("out_trade_no", req.OutTradeNo). + Set("total_amount", yuan(req.AmountMinor)). + Set("timeout_express", "30m") + + // 支付宝 API 调用超时 120 秒 + apiCtx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + var cred *channel.PayCredential + var payErr error + + switch payMode { + case channel.ModePage: + logger.Info("alipay", "模式: 电脑网站支付 (page)") + bm.Set("product_code", "FAST_INSTANT_TRADE_PAY") + payURL, err := client.TradePagePay(apiCtx, bm) + if err != nil { + payErr = fmt.Errorf("电脑网站支付下单失败: %w", err) + } else { + cred = &channel.PayCredential{PayURL: payURL, Mode: channel.ModePage, ExpireAt: time.Now().Add(30 * time.Minute)} + } + + case channel.ModeWap: + logger.Info("alipay", "模式: 手机网站支付 (wap)") + bm.Set("product_code", "QUICK_WAP_WAY") + payURL, err := client.TradeWapPay(apiCtx, bm) + if err != nil { + payErr = fmt.Errorf("手机网站支付下单失败: %w", err) + } else { + cred = &channel.PayCredential{PayURL: payURL, Mode: channel.ModeWap, ExpireAt: time.Now().Add(30 * time.Minute)} + } + + default: // ModeQRCode 扫码支付 + logger.Info("alipay", "模式: 扫码支付 (当面付)") + aliRsp, err := client.TradePrecreate(apiCtx, bm) + if err != nil { + payErr = fmt.Errorf("当面付下单失败: %w", err) + } else { + qrCode := aliRsp.Response.QrCode + cred = &channel.PayCredential{QRCode: qrCode, Mode: channel.ModeQRCode, ExpireAt: time.Now().Add(2 * time.Hour)} + // 生成备用 WAP 链接(仅当 wap 模式开启时;关闭 wap 则不再生成备用跳转) + if c.IsModeEnabled(string(channel.ModeWap)) { + if wapURL, werr := c.wapBackup(apiCtx, client, bm); werr == nil { + cred.PayURL = wapURL + } + } + } + } + + // 上报结果给账户池 + if acct != nil && c.pool != nil { + c.pool.ReportResult(acct.ID, payErr == nil) + if payErr == nil && cred != nil { + c.pool.AddDailyUsed(acct.ID, req.AmountMinor) + } + } + + if payErr != nil { + return nil, payErr + } + // 记录使用的账户ID + if cred != nil && acct != nil { + cred.AccountID = acct.ID + } + return cred, nil +} + +// resolveMode 解析支付模式(用户指定 > 设备识别 > 智能降级到已开启模式) +// 用户显式指定 pay_type 但该模式已关闭时,降级到设备匹配的已开启模式; +// 设备识别出的模式已关闭时,按降级链选择第一个已开启的模式。 +// 全部模式关闭时返回识别出的默认模式(由 CreatePayment 报错提示)。 +func (c *Channel) resolveMode(req *channel.PayRequest) channel.PayMode { + // 用户显式指定 + if req.PayMode != "" { + if c.IsModeEnabled(string(req.PayMode)) { + return channel.PayMode(req.PayMode) + } + logger.Warn("alipay", "用户指定支付模式未开启,按设备降级", "requested", req.PayMode, "device", req.Device) + } + // 设备识别 + 降级链 + return c.fallbackMode(req.Device) +} + +// fallbackMode 按设备选择第一个已开启的模式(降级链) +// pc 偏好:page → qrcode → wap(电脑端扫码兜底) +// mobile 偏好:wap → qrcode → page(手机端扫码兜底) +// 未知设备默认:qrcode → page → wap +func (c *Channel) fallbackMode(device string) channel.PayMode { + var candidates []channel.PayMode + switch device { + case "page", "pc": + candidates = []channel.PayMode{channel.ModePage, channel.ModeQRCode, channel.ModeWap} + case "wap", "mobile": + candidates = []channel.PayMode{channel.ModeWap, channel.ModeQRCode, channel.ModePage} + default: + candidates = []channel.PayMode{channel.ModeQRCode, channel.ModePage, channel.ModeWap} + } + for _, m := range candidates { + if c.IsModeEnabled(string(m)) { + return m + } + } + // 全部关闭:返回设备首选,由 CreatePayment 给出明确错误 + return candidates[0] +} + +// wapBackup 生成备用 WAP 链接 +func (c *Channel) wapBackup(ctx context.Context, client *alipay.Client, bm gopay.BodyMap) (string, error) { + wapBM := make(gopay.BodyMap) + for k, v := range bm { + wapBM[k] = v + } + wapBM.Set("product_code", "QUICK_WAP_WAY") + wapCtx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + return client.TradeWapPay(wapCtx, wapBM) +} + +// VerifyNotify 验签支付宝异步回调 +// 账户池模式下:通过 app_id 反查账户,用该账户的公钥验签 +func (c *Channel) VerifyNotify(ctx context.Context, req *http.Request) (*channel.NotifyResult, error) { + notifyReq, err := alipay.ParseNotifyToBodyMap(req) + if err != nil { + return nil, fmt.Errorf("解析支付宝回调失败: %w", err) + } + + appID := notifyReq.GetString("app_id") + + // 通过 app_id 反查账户池中的账户 + if c.pool == nil { + return nil, fmt.Errorf("账户池未初始化,无法验签") + } + acct := c.pool.LookupByAppID(appID) + if acct == nil { + logger.Warn("alipay", "回调 app_id 未匹配到账户", "app_id", appID) + return nil, fmt.Errorf("未找到 app_id=%s 对应的账户", appID) + } + // 用该账户的公钥验签 + ok, err := alipay.VerifySign(acct.PublicKey, notifyReq) + if err != nil || !ok { + return nil, fmt.Errorf("支付宝验签失败(账户: %s): %v", acct.Name, err) + } + logger.Info("alipay", "回调验签通过", "account", acct.Name, "app_id", appID) + + tradeStatus := notifyReq.GetString("trade_status") + status := model.StatusWaitPay + switch tradeStatus { + case "TRADE_SUCCESS", "TRADE_FINISHED": + status = model.StatusSuccess + case "TRADE_CLOSED": + status = model.StatusClosed + } + amountMinor, _ := model.ParseMoneyToMinor(notifyReq.GetString("total_amount")) + + acctID := int64(0) + if acct != nil { + acctID = acct.ID + } + + return &channel.NotifyResult{ + Valid: true, + OutTradeNo: notifyReq.GetString("out_trade_no"), + ChannelTxID: notifyReq.GetString("trade_no"), + Status: status, + AmountMinor: amountMinor, + Payer: notifyReq.GetString("buyer_id"), + AppID: appID, + AccountID: acctID, + RawPayload: rawJSON(notifyReq), + }, nil +} + +// QueryOrder 主动查询(对账用) +func (c *Channel) QueryOrder(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) { + // 遍历账户池中所有支付宝账户查单 + if c.pool == nil { + return nil, fmt.Errorf("账户池未初始化") + } + accts := c.pool.ListByChannel("alipay") + for _, acct := range accts { + client, err := c.getOrCreateClient(acct) + if err != nil { + continue + } + bm := make(gopay.BodyMap) + bm.Set("out_trade_no", outTradeNo) + rsp, err := client.TradeQuery(ctx, bm) + if err != nil { + continue + } + st := &channel.OrderStatus{ChannelTxID: rsp.Response.TradeNo} + switch rsp.Response.TradeStatus { + case "TRADE_SUCCESS", "TRADE_FINISHED": + st.Status = model.StatusSuccess + case "TRADE_CLOSED": + st.Status = model.StatusClosed + default: + st.Status = model.StatusWaitPay + } + // 解析实际支付金额(元字符串 → 分,字符串十进制解析,避免 float 精度问题) + if amt, err := model.ParseMoneyToMinor(rsp.Response.TotalAmount); err == nil { + st.AmountMinor = amt + } + return st, nil + } + return nil, fmt.Errorf("所有支付宝账户查询均失败") +} + +// yuan 最小单位(分) → 元字符串(支付宝协议) +func yuan(minor int64) string { + return fmt.Sprintf("%d.%02d", minor/100, minor%100) +} + +func rawJSON(v any) string { + b, _ := json.Marshal(v) + return string(b) +} diff --git a/internal/channel/alipay/alipay_test.go b/internal/channel/alipay/alipay_test.go new file mode 100755 index 0000000..2d180ac --- /dev/null +++ b/internal/channel/alipay/alipay_test.go @@ -0,0 +1,161 @@ +package alipay + +import ( + "testing" + + "github.com/go-pay/gopay/alipay" + "paygateway/internal/account" + "paygateway/internal/channel" +) + +// newTestChannel 创建测试通道(默认全开) +func newTestChannel() *Channel { + ch := &Channel{ + pool: &account.AccountPool{}, + clientCache: make(map[int64]*alipay.Client), + enabledModes: defaultEnabledModes(), + } + return ch +} + +func TestResolveMode_AllEnabled(t *testing.T) { + ch := newTestChannel() + + cases := []struct { + name string + payMode string + device string + expected channel.PayMode + }{ + {"显式指定 page", "page", "pc", channel.ModePage}, + {"显式指定 wap", "wap", "mobile", channel.ModeWap}, + {"显式指定 qrcode", "qrcode", "pc", channel.ModeQRCode}, + {"pc 设备识别", "", "pc", channel.ModePage}, + {"page 设备识别", "", "page", channel.ModePage}, + {"mobile 设备识别", "", "mobile", channel.ModeWap}, + {"wap 设备识别", "", "wap", channel.ModeWap}, + {"未知设备默认扫码", "", "unknown", channel.ModeQRCode}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := &channel.PayRequest{PayMode: channel.PayMode(tc.payMode), Device: tc.device} + got := ch.resolveMode(req) + if got != tc.expected { + t.Fatalf("resolveMode(%q, %q) = %q, want %q", tc.payMode, tc.device, got, tc.expected) + } + }) + } +} + +func TestResolveMode_OnlyQRCode(t *testing.T) { + ch := newTestChannel() + ch.SetModeEnabled("page", false) + ch.SetModeEnabled("wap", false) + // 只开 qrcode + + cases := []struct { + name string + payMode string + device string + expected channel.PayMode + }{ + {"pc 降级到 qrcode", "", "pc", channel.ModeQRCode}, + {"mobile 降级到 qrcode", "", "mobile", channel.ModeQRCode}, + {"未知设备降级到 qrcode", "", "unknown", channel.ModeQRCode}, + {"显式指定已关闭的 page 降级", "page", "pc", channel.ModeQRCode}, + {"显式指定已关闭的 wap 降级", "wap", "mobile", channel.ModeQRCode}, + {"显式指定 qrcode 保持", "qrcode", "pc", channel.ModeQRCode}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := &channel.PayRequest{PayMode: channel.PayMode(tc.payMode), Device: tc.device} + got := ch.resolveMode(req) + if got != tc.expected { + t.Fatalf("resolveMode(%q, %q) = %q, want %q", tc.payMode, tc.device, got, tc.expected) + } + }) + } +} + +func TestResolveMode_OnlyWap(t *testing.T) { + ch := newTestChannel() + ch.SetModeEnabled("page", false) + ch.SetModeEnabled("qrcode", false) + // 只开 wap + + cases := []struct { + name string + payMode string + device string + expected channel.PayMode + }{ + {"pc 降级到 wap", "", "pc", channel.ModeWap}, + {"mobile 保持 wap", "", "mobile", channel.ModeWap}, + {"显式指定已关闭的 page 降级", "page", "pc", channel.ModeWap}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := &channel.PayRequest{PayMode: channel.PayMode(tc.payMode), Device: tc.device} + got := ch.resolveMode(req) + if got != tc.expected { + t.Fatalf("resolveMode(%q, %q) = %q, want %q", tc.payMode, tc.device, got, tc.expected) + } + }) + } +} + +func TestResolveMode_OnlyPage(t *testing.T) { + ch := newTestChannel() + ch.SetModeEnabled("wap", false) + ch.SetModeEnabled("qrcode", false) + // 只开 page + + cases := []struct { + name string + payMode string + device string + expected channel.PayMode + }{ + {"pc 保持 page", "", "pc", channel.ModePage}, + {"mobile 降级到 page", "", "mobile", channel.ModePage}, + {"显式指定已关闭的 wap 降级", "wap", "mobile", channel.ModePage}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := &channel.PayRequest{PayMode: channel.PayMode(tc.payMode), Device: tc.device} + got := ch.resolveMode(req) + if got != tc.expected { + t.Fatalf("resolveMode(%q, %q) = %q, want %q", tc.payMode, tc.device, got, tc.expected) + } + }) + } +} + +func TestResolveMode_AllDisabled(t *testing.T) { + ch := newTestChannel() + ch.SetModeEnabled("page", false) + ch.SetModeEnabled("wap", false) + ch.SetModeEnabled("qrcode", false) + + // 全部关闭:fallback 返回设备首选(pc→page),由 CreatePayment 报错 + req := &channel.PayRequest{PayMode: "", Device: "pc"} + got := ch.resolveMode(req) + if got != channel.ModePage { + t.Fatalf("resolveMode(pc, all disabled) = %q, want %q", got, channel.ModePage) + } + // 全部关闭时 CreatePayment 必须报错 + if _, err := ch.CreatePayment(nil, &channel.PayRequest{Device: "pc", Subject: "t", OutTradeNo: "x", AmountMinor: 100, Currency: "CNY"}); err == nil { + t.Fatal("全模式关闭时 CreatePayment 应返回错误") + } +} + +func TestEnabledModesSnapshot(t *testing.T) { + ch := newTestChannel() + ch.SetModeEnabled("wap", false) + snapshot := ch.EnabledModes() + // 修改快照不应影响内部状态 + snapshot["wap"] = true + if ch.IsModeEnabled("wap") { + t.Fatal("修改快照不应影响内部状态") + } +} diff --git a/internal/channel/interface.go b/internal/channel/interface.go new file mode 100755 index 0000000..e64b64c --- /dev/null +++ b/internal/channel/interface.go @@ -0,0 +1,157 @@ +// Package channel 支付通道抽象:统一接口 + 注册表,通道可插拔 +package channel + +import ( + "context" + "net/http" + "time" +) + +// ChannelType 通道大类 +type ChannelType string + +const ( + // TypeMerchant 商家通道:官方 API,gopay 驱动,可主动查单 + TypeMerchant ChannelType = "merchant" + // TypePersonal 个人通道:免签收款码,监听驱动,无法主动查单 + TypePersonal ChannelType = "personal" +) + +// Provider 支付品牌(两类通道共用) +type Provider string + +const ( + ProviderAlipay Provider = "alipay" + ProviderWechat Provider = "wechat" + ProviderQQ Provider = "qq" // 预留 + ProviderUSDT Provider = "usdt" // 预留 +) + +// ChannelKind 通道类型(支付结果获取方式) +type ChannelKind int + +const ( + // KindCallback 回调驱动:支付宝/微信等(用户付款 → 通道异步回调) + KindCallback ChannelKind = iota + // KindListener 监听驱动:链上支付/免签监听(用户转账 → 监听确认) + KindListener +) + +// ConfirmAbility 确认能力:决定 ConfirmScheduler 的兜底策略 +type ConfirmAbility int + +const ( + // ConfirmByQuery 商家通道:可主动调 API 查单(回调丢失→查单补账) + ConfirmByQuery ConfirmAbility = iota + // ConfirmByListen 个人通道:靠监听端推送,无法主动查 + ConfirmByListen + // ConfirmByManual 最终兜底:监听失联/匹配失败→人工异常池 + ConfirmByManual +) + +// PayMode 支付模式 +type PayMode string + +const ( + ModePage PayMode = "page" // 电脑网站支付(跳转收银台) + ModeWap PayMode = "wap" // 手机网站支付 + ModeQRCode PayMode = "qrcode" // 扫码支付(当面付) + ModeAddress PayMode = "address" // 链上收款地址(未来 crypto) +) + +// PayRequest 下单请求(通道无关) +type PayRequest struct { + OutTradeNo string // 网关订单号 + GatewayNo string // 网关单号 + Subject string // 商品名称 + AmountMinor int64 // 金额(最小单位) + Currency string // 币种 + NotifyURL string // 通道异步回调地址 + ReturnURL string // 跳转地址 + PayMode PayMode // 用户选择的支付模式 + Device string // pc / mobile(用于默认模式判断) +} + +// PayCredential 支付凭据(下单返回) +type PayCredential struct { + PayURL string // 跳转链接(page/wap) + QRCode string // 二维码内容(qrcode/address) + Address string // 链上收款地址(未来 crypto) + AccountID int64 // 处理这笔订单的账户ID(账户池用) + Mode PayMode // 实际使用的支付模式(含降级后的结果) + ExpireAt time.Time // 过期时间 + + // 个人免签通道附加数据(由通道返回,order.Service 持久化到订单) + NoteCode string // 备注码(用户转账备注) + QrToken string // 套码 token(/mq/{token} 校验) + QRContent string // 真实收款码内容(仅服务端使用,302 跳转目标) + DeviceID string // 承接的监听端设备(匹配后回填) +} + +// NotifyResult 回调/监听事件解析结果 +type NotifyResult struct { + Valid bool // 验签/校验是否通过 + OutTradeNo string // 商户订单号(通道回调携带) + ChannelTxID string // 通道交易号(去重依据) + Status string // 与 model 状态一致:TRADE_SUCCESS / TRADE_CLOSED / FAILED + AmountMinor int64 // 实付金额(必须与订单比对!) + Payer string // 付款方标识(可选) + DeviceID string // 个人通道:承接的监听端设备ID(审计追溯用) + AppID string // 支付宝 app_id(回调反查账户用) + MchID string // 微信商户号(回调反查账户用) + AccountID int64 // 处理这笔回调的账户ID(留痕/审计) + RawPayload string // 原始数据(审计用) +} + +// OrderStatus 主动查询结果 +type OrderStatus struct { + ChannelTxID string + Status string + AmountMinor int64 +} + +// EventListener 事件监听器(链上支付用;回调型通道返回 nil) +type EventListener interface { + Start(ctx context.Context) error + Events() <-chan ChainEvent +} + +// ChainEvent 链上事件 +type ChainEvent struct { + ChannelTxID string + Address string + AmountMinor int64 + Confirmations int64 + RawPayload string +} + +// ClientCacheInvalidator 可清除账户客户端缓存的可选接口。 +// 商家通道(alipay/wechat)持有 gopay client 缓存;账户 app_id/密钥变更后必须清除, +// 否则缓存的旧 client 会继续用旧参数下单(如旧 app_id 导致 invalid-app-id)。 +type ClientCacheInvalidator interface { + RemoveClient(accountID int64) +} + +// PaymentChannel 所有支付通道必须实现的统一接口 +type PaymentChannel interface { + // Name 通道标识:merchant.alipay / personal.wechat 等 + Name() string + // Type 通道大类:merchant / personal + Type() ChannelType + // Kind 通道类型(回调驱动 / 监听驱动) + Kind() ChannelKind + // Modes 支持的支付模式 + Modes() []PayMode + // ConfirmAbility 确认能力(决定 ConfirmScheduler 的兜底策略) + ConfirmAbility() ConfirmAbility + // Provider 支付品牌:alipay / wechat / qq / usdt + Provider() Provider + // CreatePayment 下单:返回支付凭据 + CreatePayment(ctx context.Context, req *PayRequest) (*PayCredential, error) + // VerifyNotify 验签回调(KindCallback 用):解析 + 验签原始请求 + VerifyNotify(ctx context.Context, req *http.Request) (*NotifyResult, error) + // QueryOrder 主动查单(对账 + 补偿兜底) + QueryOrder(ctx context.Context, outTradeNo string) (*OrderStatus, error) + // Listener 事件监听(KindListener 必须实现;KindCallback 返回 nil) + Listener() EventListener +} diff --git a/internal/channel/personal/personal.go b/internal/channel/personal/personal.go new file mode 100755 index 0000000..604d2ec --- /dev/null +++ b/internal/channel/personal/personal.go @@ -0,0 +1,279 @@ +// Package personal 个人免签通道(收款码 + 备注码 + 监听端确认) +// +// 与商家通道(merchant.*)的本质区别: +// - 无法主动查单(ConfirmByManual):用户扫收款码转账 → 监听端推送 → 服务端匹配入账 +// - 支付凭据 = 真实收款码直发(/qr/real 动态图片接口) +// - 每笔订单分配 4 位备注码,用户转账时填写 → 精确匹配 +// - 账户池复用:每个账户 = 一个收款码(QRContent),轮换/冷却/限额复用 +// +// 实现要点(docs/MIANQIAN.md): +// - CreatePayment:选账户 → 分配备注码 → 生成二维码 token → 直发码 URL +// - 收款确认:/personal/push(监听端推送)→ HMAC 验签 → 备注码/金额匹配 → SettlePayment +// - VerifyNotify / QueryOrder 不支持(个人通道无官方 API),分别返回错误 +package personal + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "paygateway/internal/account" + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// Config 个人通道配置 +type Config struct { + BaseURL string // 网关公网基础地址(管理后台展示,隧道域名动态更新) + OrderTTL time.Duration // 订单有效期(备注码/二维码 token 过期时间) + NoteTTL time.Duration // 备注码占用有效期(默认 5 分钟) + TokenTTL time.Duration // 二维码 token 有效期(默认 30 分钟) + MaxAmount int64 // 单笔限额(分,0=不限;docs/MIANQIAN.md §10 风控) +} + +// Channel 个人免签通道实现 +type Channel struct { + cfg Config + pool *account.AccountPool + store store.Store // 业务库(订单查询/匹配) + personalDB store.PersonalStore // personal.db(设备/推送流水) + alloc *personal.NoteCodeAllocator + provider channel.Provider // alipay / wechat + baseURL string // 当前公网基础地址(SetBaseURL 动态更新) + adbOnline func(ctx context.Context, serial string) bool // 设备 adb 可达判定(main.go 注入 adbMgr().Online) + + // balancedSelect 均衡分配器(main.go 注入,nil = 退化为旧 selectServableAccount 加权随机): + // 两层分配:先选设备(最闲最健康),再选设备内账户(最闲最健康)。 + // 返回选中账户;nil = 无可用。main.go 实现持有 waitPayPool/ledgermon 等负载数据源。 + balancedSelect func(ctx context.Context) *account.Account +} + +// SetBalancedSelect 注入均衡分配器(docs/PLAN-DYNAMIC-CLOSE-ALLOC.md §5)。 +// nil = 退化为旧逻辑(SelectFrom 加权随机,不感知负载)。 +func (c *Channel) SetBalancedSelect(fn func(ctx context.Context) *account.Account) { + c.balancedSelect = fn +} + +// SetAdbOnline 注入设备 adb 可达判定函数(main.go 注入 adbMgr().Online 包装)。 +// 2026-08-29 重构:设备在线判定改为 adb 可达性优先,不再依赖监听 App 心跳。 +func (c *Channel) SetAdbOnline(fn func(ctx context.Context, serial string) bool) { + c.adbOnline = fn +} + +// New 创建个人免签通道 +// provider: channel.ProviderAlipay / channel.ProviderWechat +func New(cfg Config, provider channel.Provider, pool *account.AccountPool, st store.Store, pdb store.PersonalStore) *Channel { + if cfg.OrderTTL <= 0 { + cfg.OrderTTL = 30 * time.Minute + } + if cfg.NoteTTL <= 0 { + cfg.NoteTTL = personal.NoteCodeTTL + } + if cfg.TokenTTL <= 0 { + cfg.TokenTTL = 30 * time.Minute + } + return &Channel{ + cfg: cfg, + pool: pool, + store: st, + personalDB: pdb, + alloc: personal.NewNoteCodeAllocator(), + provider: provider, + baseURL: cfg.BaseURL, + } +} + +// SetBaseURL 动态更新公网基础地址(隧道重启换域名时调用) +func (c *Channel) SetBaseURL(url string) { + c.baseURL = strings.TrimSuffix(url, "/") + logger.Info("personal", "公网地址已更新", "provider", c.provider, "base_url", c.baseURL) +} + +// BaseURL 当前公网基础地址(管理后台展示) +func (c *Channel) BaseURL() string { return c.baseURL } + +// channelName 通道注册名 +func (c *Channel) channelName() string { return "personal." + string(c.provider) } + +// Name 通道标识 +func (c *Channel) Name() string { return c.channelName() } + +// Type 通道大类:个人通道 +func (c *Channel) Type() channel.ChannelType { return channel.TypePersonal } + +// Provider 支付品牌 +func (c *Channel) Provider() channel.Provider { return c.provider } + +// Kind 通道类型:监听驱动 +func (c *Channel) Kind() channel.ChannelKind { return channel.KindListener } + +// ConfirmAbility 确认能力:无法主动查单,最终兜底 = 人工异常池 +func (c *Channel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByManual } + +// Modes 支持的支付模式:仅扫码(个人收款码) +func (c *Channel) Modes() []channel.PayMode { + return []channel.PayMode{channel.ModeQRCode} +} + +// Listener 监听驱动但通过 HTTP 推送,无独立事件监听器 +func (c *Channel) Listener() channel.EventListener { return nil } + +// ErrNotQueryable 个人通道无法主动查单 +var ErrNotQueryable = errors.New("个人通道无法主动查单(靠监听端推送确认)") + +// formatYuan 分 → 元字符串(如 50000 → "500.00") +func formatYuan(minor int64) string { + return fmt.Sprintf("%d.%02d", minor/100, minor%100) +} + +// CreatePayment 下单(个人免签) +// +// 流程: +// 1. 账户池选一个可用收款账户(含收款码内容 QRContent) +// 2. 分配 4 位备注码(系统生成,5 分钟占用) +// 3. 生成二维码 token(HMAC 签名,防劫持) +// 4. 直发码 URL:/qr/real?token={token}(动态图片接口下发真实收款码) +// +// 返回的 PayCredential.QRCode 就是二维码内容;前端按 src 渲染。 +func (c *Channel) CreatePayment(ctx context.Context, req *channel.PayRequest) (*channel.PayCredential, error) { + if c.baseURL == "" { + return nil, fmt.Errorf("个人通道公网地址未配置(隧道未就绪)") + } + // 单笔限额风控(docs/MIANQIAN.md §10):大额拒绝下单 + 日志告警 + if c.cfg.MaxAmount > 0 && req.AmountMinor > c.cfg.MaxAmount { + logger.Warn("personal", "单笔限额风控:金额超限,拒绝下单", + "provider", c.provider, "out_trade_no", req.OutTradeNo, + "amount", req.AmountMinor, "max", c.cfg.MaxAmount) + return nil, fmt.Errorf("单笔金额超限(最多 ¥%s)", formatYuan(c.cfg.MaxAmount)) + } + // 1. 选账户(仅选有在线监听设备绑定的账户,防路由到无设备账户造成死单) + acct := c.selectServableAccount(ctx) + if acct == nil { + return nil, fmt.Errorf("%s 个人通道暂无可用收款账户(无在线监听设备绑定或已达日限额/冷却中)", c.provider) + } + if acct.QRContent == "" { + return nil, fmt.Errorf("账户「%s」未配置收款码内容(qr_content),无法接单", acct.Name) + } + if reason := account.InvalidQRContent(c.channelName(), acct.QRContent); reason != "" { + logger.Error("personal", "账户收款码内容非法,拒绝路由(脏数据防线)", + "provider", c.provider, "account", acct.Name, "account_id", acct.ID, "reason", reason) + return nil, fmt.Errorf("账户「%s」收款码内容异常(%s),已拒绝路由,请联系管理员修复", acct.Name, reason) + } + // 2. 分配备注码 + noteCode, err := c.alloc.Alloc() + if err != nil { + return nil, fmt.Errorf("备注码分配失败: %w", err) + } + // 3. 生成二维码 token(自包含签名,/qr/real 动态图片接口用) + token, err := personal.GenerateQrToken(req.OutTradeNo, time.Now().Add(c.cfg.TokenTTL)) + if err != nil { + c.alloc.Release(noteCode) + return nil, fmt.Errorf("二维码 token 生成失败: %w", err) + } + qrURL := "/qr/real?token=" + token + // 通知栏监听方案只依赖 包名+金额+时间 匹配,备注码已无提示意义;日志不再提备注码 + logger.Info("personal", "下单成功(直发码)", + "provider", c.provider, "out_trade_no", req.OutTradeNo, + "account", acct.Name, "account_id", acct.ID, + "amount", req.AmountMinor) + + // 4. 保存备注码/套码 token 到订单(order.Service 通过 cred 回调写回,这里无法直接写) + // 通过 PayCredential 附加字段透传给 order.Service 持久化 + return &channel.PayCredential{ + QRCode: qrURL, + Mode: channel.ModeQRCode, + ExpireAt: time.Now().Add(c.cfg.OrderTTL), + AccountID: acct.ID, + // 个人通道附加数据:备注码 + 套码 token + 收款码内容(用于 /mq 302 / /qr/real 图片下发) + NoteCode: noteCode, + QrToken: token, + QRContent: acct.QRContent, + }, nil +} + +// VerifyNotify 个人通道不支持 HTTP 验签回调(收款确认走 /personal/push,HMAC 验签在 handler) +func (c *Channel) VerifyNotify(ctx context.Context, req *http.Request) (*channel.NotifyResult, error) { + return nil, fmt.Errorf("个人通道不支持 HTTP 回调验签,收款确认走 /personal/push") +} + +// QueryOrder 个人通道无法主动查单 +func (c *Channel) QueryOrder(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) { + return nil, ErrNotQueryable +} + +// SelectAccount 按收款码内容选择可用账户(供 /mq 302 校验用) +func (c *Channel) SelectAccount() *account.Account { + return c.pool.Select(c.channelName(), "default") +} + +// servableAccountIDs 返回有**adb 可达**设备绑定的账户 ID 集合(设备主账户 + 附加账户)。 +// 设备可用判定:adb 可达(USB/WiFi/LAN)。 +// 不再依赖监听 App 心跳/DB status(2026-08-29 重构)。 +// 设备冻结/通道不匹配均不计入;查询失败返回 nil(调用方按无可用账户处理)。 +func (c *Channel) servableAccountIDs(ctx context.Context) map[int64]bool { + devs, err := c.personalDB.ListDevices(ctx) + if err != nil { + logger.Error("personal", "查询监听设备失败", "err", err) + return nil + } + out := make(map[int64]bool) + for _, d := range devs { + // 设备可用判定:adb 可达性。 + // 注:不做 d.Provider 过滤——一台真机同时装有支付宝+微信,设备行只存单一 provider + // 标签,按标签过滤会让另一平台永远无账户可用(老 bug,被僵尸设备数据掩盖过)。 + // 通道归属由 SelectFrom 的 channel 过滤保证。 + if serial := c.serialOf(d); serial == "" || c.adbOnline == nil || !c.adbOnline(ctx, serial) { + continue + } + for _, id := range store.DeviceAccountIDs(ctx, c.personalDB, d.ID, d.AccountID) { + out[id] = true + } + } + return out +} + +// serialOf 解析设备 adb serial(TCP host:port 或 USB 纯 serial) +func (c *Channel) serialOf(d *personal.DeviceEncrypted) string { + if d.AdbHost != "" { + if d.AdbPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort) + } + return d.AdbHost + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort) + } + return "" +} + +// selectServableAccount 设备感知选号:可用账户 ∩ 在线监听设备绑定账户。 +// 均衡分配器已注入时走两层评分(设备负载×账户负载×健康度);否则退化为加权随机。 +func (c *Channel) selectServableAccount(ctx context.Context) *account.Account { + if c.balancedSelect != nil { + return c.balancedSelect(ctx) + } + servable := c.servableAccountIDs(ctx) + if len(servable) == 0 { + logger.Warn("personal", "无在线监听设备可服务,拒绝路由", "provider", c.provider) + return nil + } + return c.pool.SelectFrom(c.channelName(), "default", servable) +} + +// GetAccountByID 按 ID 获取账户 +func (c *Channel) GetAccountByID(id int64) *account.Account { + return c.pool.GetByID(id) +} + +// ReleaseNoteCode 释放备注码(订单支付成功/关闭后调用) +// 幂等:重复释放不会产生副作用(delete 不存在 key 是 no-op) +func (c *Channel) ReleaseNoteCode(code string) { + if code != "" { + c.alloc.Release(code) + } +} diff --git a/internal/channel/registry.go b/internal/channel/registry.go new file mode 100755 index 0000000..af63f05 --- /dev/null +++ b/internal/channel/registry.go @@ -0,0 +1,139 @@ +package channel + +import ( + "fmt" + "sync" +) + +// registry 通道注册表(线程安全) +var ( + mu sync.RWMutex + registry = make(map[string]PaymentChannel) + enabled = make(map[string]bool) // 通道启用状态,默认启用 +) + +// Register 注册通道(每个通道在 init 或组装阶段调用) +func Register(c PaymentChannel) { + mu.Lock() + defer mu.Unlock() + registry[c.Name()] = c + enabled[c.Name()] = true // 默认启用 +} + +// Get 按名称获取通道(仅返回已启用的通道) +func Get(name string) (PaymentChannel, bool) { + mu.RLock() + defer mu.RUnlock() + if !enabled[name] { + return nil, false + } + c, ok := registry[name] + return c, ok +} + +// Names 返回已启用通道名列表 +func Names() []string { + mu.RLock() + defer mu.RUnlock() + out := make([]string, 0, len(registry)) + for name := range registry { + if enabled[name] { + out = append(out, name) + } + } + return out +} + +// AllNames 返回所有已注册通道名列表(含禁用,管理后台用) +func AllNames() []string { + mu.RLock() + defer mu.RUnlock() + out := make([]string, 0, len(registry)) + for name := range registry { + out = append(out, name) + } + return out +} + +// IsEnabled 检查通道是否启用 +func IsEnabled(name string) bool { + mu.RLock() + defer mu.RUnlock() + return enabled[name] +} + +// SetEnabled 设置通道启用/禁用状态 +func SetEnabled(name string, on bool) { + mu.Lock() + defer mu.Unlock() + enabled[name] = on +} + +// Lookup 按名称查找通道(不检查启用状态,管理后台用) +func Lookup(name string) (PaymentChannel, bool) { + mu.RLock() + defer mu.RUnlock() + c, ok := registry[name] + return c, ok +} + +// EpayTypeToChannel 易支付协议类型 → 通道注册名映射 +// epay 协议 type 参数使用 "alipay"/"wxpay",需要转为通道注册名 +var EpayTypeToChannel = map[string]string{ + "alipay": "merchant.alipay", + "wxpay": "merchant.wechat", + "qqpay": "merchant.qq", + "usdt": "merchant.usdt", + // 个人免签通道(显式选择,商户传 type=personal_alipay / personal_wxpay) + "personal_alipay": "personal.alipay", + "personal_wxpay": "personal.wechat", +} + +// EpayTypeToPersonalChannel 易支付类型 → 个人免签通道名(优先路由到个人通道) +// 当对应个人通道已注册且配置了收款账户时,用 ResolveEpayType 优先选择个人通道 +var EpayTypeToPersonalChannel = map[string]string{ + "alipay": "personal.alipay", + "wxpay": "personal.wechat", + "qqpay": "personal.qq", +} + +// ResolveEpayType 解析易支付 type 参数为实际通道名 +// 路由规则(个人免签优先): +// 1. 若对应个人通道已注册且已启用 → 优先使用个人通道(个人免签 0 费率) +// 2. 否则回退到商家通道(merchant.*) +// 3. 都不存在 → 返回原映射(调用方报"不支持的支付类型") +func ResolveEpayType(epayType string) string { + // 个人通道优先(仅当注册且启用) + if pName, ok := EpayTypeToPersonalChannel[epayType]; ok { + if c, ok := Get(pName); ok && c != nil { + return pName + } + } + // 回退商家通道 + if mName, ok := EpayTypeToChannel[epayType]; ok { + return mName + } + return "" +} + +// ProviderFromName 从通道名提取 Provider(如 "merchant.alipay" → "alipay") +func ProviderFromName(name string) string { + for i := len(name) - 1; i >= 0; i-- { + if name[i] == '.' { + return name[i+1:] + } + } + return name +} + +// ErrUnsupported 不支持的支付类型 +var ErrUnsupported = fmt.Errorf("不支持的支付类型") + +// MustGet 获取通道,不存在则 panic(组装阶段校验配置) +func MustGet(name string) PaymentChannel { + c, ok := Get(name) + if !ok { + panic(fmt.Sprintf("通道未注册: %s", name)) + } + return c +} diff --git a/internal/channel/wechat/wechat.go b/internal/channel/wechat/wechat.go new file mode 100755 index 0000000..20d515b --- /dev/null +++ b/internal/channel/wechat/wechat.go @@ -0,0 +1,353 @@ +// Package wechat 微信支付通道(基于 gopay 库,V2 协议) +// +// 账户池集成: +// - 持有 AccountPool,下单选账户、回调反查 mch_id +// - 每个账户对应一个 gopay.Client,按需创建并缓存 +package wechat + +import ( + "context" + "crypto/md5" + "encoding/hex" + "encoding/xml" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/wechat" + "paygateway/internal/account" + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/model" +) + +// Config 微信通道配置(仅回调地址 + 调试开关;账户密钥一律来自账户池) +type Config struct { + NotifyURL string // 异步回调地址 + Debug bool // 调试日志 +} + +// Channel 微信通道实现 +type Channel struct { + cfg Config + pool *account.AccountPool + clientCache map[int64]*wechat.Client + clientMu sync.RWMutex +} + +// New 创建微信通道(账户池模式:所有账户数据从 pool 读取) +func New(cfg Config, pool *account.AccountPool) (*Channel, error) { + ch := &Channel{ + cfg: cfg, + pool: pool, + clientCache: make(map[int64]*wechat.Client), + } + return ch, nil +} + +// getOrCreateClient 获取或创建账户对应的 gopay client +func (c *Channel) getOrCreateClient(acct *account.Account) (*wechat.Client, error) { + c.clientMu.RLock() + if cli, ok := c.clientCache[acct.ID]; ok { + c.clientMu.RUnlock() + return cli, nil + } + c.clientMu.RUnlock() + + c.clientMu.Lock() + defer c.clientMu.Unlock() + + if cli, ok := c.clientCache[acct.ID]; ok { + return cli, nil + } + + if acct.AppID == "" || acct.MchID == "" || acct.ApiKey == "" { + return nil, fmt.Errorf("微信账户配置不完整: AppID/MchID/ApiKey 必填") + } + client := wechat.NewClient(acct.AppID, acct.MchID, acct.ApiKey, acct.IsProd) + if c.cfg.Debug { + client.DebugSwitch = gopay.DebugOn + } + c.clientCache[acct.ID] = client + logger.Info("wechat", "账户客户端已创建", "account_id", acct.ID, "name", acct.Name) + return client, nil +} + +// SetNotifyURL 动态更新回调地址 +func (c *Channel) SetNotifyURL(url string) { + c.cfg.NotifyURL = url + logger.Info("wechat", "回调地址已更新", "notify_url", url) +} + +// RemoveClient 移除账户对应的 gopay client 缓存(账户 app_id/密钥变更后必须调用) +func (c *Channel) RemoveClient(accountID int64) { + c.clientMu.Lock() + if _, ok := c.clientCache[accountID]; ok { + delete(c.clientCache, accountID) + logger.Info("wechat", "账户客户端缓存已清除", "account_id", accountID) + } + c.clientMu.Unlock() +} + +// Name 通道标识 +func (c *Channel) Name() string { return "merchant.wechat" } + +// Type 通道大类:商家通道 +func (c *Channel) Type() channel.ChannelType { return channel.TypeMerchant } + +// Provider 支付品牌:微信 +func (c *Channel) Provider() channel.Provider { return channel.ProviderWechat } + +// ConfirmAbility 确认能力:可主动查单 +func (c *Channel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByQuery } + +// NotifyURL 返回当前回调地址 +func (c *Channel) NotifyURL() string { return c.cfg.NotifyURL } + +// Kind 通道类型:回调驱动 +func (c *Channel) Kind() channel.ChannelKind { return channel.KindCallback } + +// Modes 支持的支付模式 +func (c *Channel) Modes() []channel.PayMode { + return []channel.PayMode{channel.ModeQRCode, channel.ModeWap, channel.ModePage} +} + +// Listener 回调型无监听器 +func (c *Channel) Listener() channel.EventListener { return nil } + +// selectAccount 从账户池选一个可用账户 +func (c *Channel) selectAccount(group string) *account.Account { + if c.pool == nil { + return nil + } + return c.pool.Select("wechat", group) +} + +// CreatePayment 下单 +func (c *Channel) CreatePayment(ctx context.Context, req *channel.PayRequest) (*channel.PayCredential, error) { + // 从账户池选账户 + var acct *account.Account + if c.pool != nil { + acct = c.pool.Select("wechat", "default") + if acct == nil { + return nil, fmt.Errorf("微信暂无可用账户(所有账户已达日限额或冷却中)") + } + } + + // 获取 gopay client(账户池模式:必选账户) + var client *wechat.Client + if acct == nil { + return nil, fmt.Errorf("微信暂无可用账户") + } + client, err := c.getOrCreateClient(acct) + if err != nil { + return nil, fmt.Errorf("获取微信客户端失败: %w", err) + } + + bm := make(gopay.BodyMap) + bm.Set("nonce_str", randomStr(32)). + Set("body", truncate(req.Subject, 30)). + Set("out_trade_no", req.OutTradeNo). + Set("total_fee", req.AmountMinor). + Set("spbill_create_ip", "127.0.0.1"). + Set("notify_url", c.cfg.NotifyURL) + + payMode := c.resolveMode(req) + apiCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var cred *channel.PayCredential + var payErr error + + switch payMode { + case channel.ModeWap: + logger.Info("wechat", "模式: H5支付 (MWEB)") + bm.Set("trade_type", wechat.TradeType_H5). + Set("scene_info", `{"h5_info":{"type":"Wap","wap_url":"http://localhost:8080","wap_name":"PayGateway"}}`) + wxRsp, err := client.UnifiedOrder(apiCtx, bm) + if err != nil { + payErr = fmt.Errorf("H5下单失败: %w", err) + } else if wxRsp.ReturnCode != "SUCCESS" || wxRsp.ResultCode != "SUCCESS" { + payErr = fmt.Errorf("H5下单失败: return=%s(%s) result=%s(%s)", + wxRsp.ReturnCode, wxRsp.ReturnMsg, wxRsp.ResultCode, wxRsp.ErrCodeDes) + } else { + cred = &channel.PayCredential{PayURL: wxRsp.MwebUrl, Mode: channel.ModeWap, ExpireAt: time.Now().Add(2 * time.Hour)} + } + + default: + logger.Info("wechat", "模式: Native扫码支付") + bm.Set("trade_type", wechat.TradeType_Native) + wxRsp, err := client.UnifiedOrder(apiCtx, bm) + if err != nil { + payErr = fmt.Errorf("Native下单失败: %w", err) + } else if wxRsp.ReturnCode != "SUCCESS" || wxRsp.ResultCode != "SUCCESS" { + payErr = fmt.Errorf("Native下单失败: return=%s(%s) result=%s(%s)", + wxRsp.ReturnCode, wxRsp.ReturnMsg, wxRsp.ResultCode, wxRsp.ErrCodeDes) + } else { + cred = &channel.PayCredential{QRCode: wxRsp.CodeUrl, Mode: channel.ModeQRCode, ExpireAt: time.Now().Add(2 * time.Hour)} + } + } + + // 上报结果给账户池 + if acct != nil && c.pool != nil { + c.pool.ReportResult(acct.ID, payErr == nil) + if payErr == nil && cred != nil { + c.pool.AddDailyUsed(acct.ID, req.AmountMinor) + } + } + + // 记录使用的账户ID + if payErr == nil && cred != nil && acct != nil { + cred.AccountID = acct.ID + } + + if payErr != nil { + return nil, payErr + } + return cred, nil +} + +// resolveMode 解析支付模式 +func (c *Channel) resolveMode(req *channel.PayRequest) channel.PayMode { + switch { + case req.PayMode == channel.ModeWap || req.Device == "wap": + return channel.ModeWap + case req.PayMode == channel.ModePage || req.Device == "page": + return channel.ModeQRCode + case req.Device == "mobile": + return channel.ModeWap + default: + return channel.ModeQRCode + } +} + +// VerifyNotify 验签微信异步回调 +// 通过 mch_id 反查账户池中的账户,用该账户的 API 密钥验签 +func (c *Channel) VerifyNotify(ctx context.Context, req *http.Request) (*channel.NotifyResult, error) { + bm, err := wechat.ParseNotifyToBodyMap(req) + if err != nil { + return nil, fmt.Errorf("解析微信回调失败: %w", err) + } + + mchID := bm.GetString("mch_id") + + if c.pool == nil { + return nil, fmt.Errorf("账户池未初始化,无法验签") + } + acct := c.pool.LookupByMchID(mchID) + if acct == nil { + logger.Warn("wechat", "回调 mch_id 未匹配到账户", "mch_id", mchID) + return nil, fmt.Errorf("未找到 mch_id=%s 对应的账户", mchID) + } + ok, err := wechat.VerifySign(acct.ApiKey, wechat.SignType_MD5, bm) + if err != nil || !ok { + return nil, fmt.Errorf("微信验签失败(账户: %s): %v", acct.Name, err) + } + logger.Info("wechat", "回调验签通过", "account", acct.Name, "mch_id", mchID) + + acctID := int64(0) + if acct != nil { + acctID = acct.ID + } + + if bm.GetString("return_code") != "SUCCESS" || bm.GetString("result_code") != "SUCCESS" { + return &channel.NotifyResult{ + Valid: true, OutTradeNo: bm.GetString("out_trade_no"), + Status: model.StatusFailed, MchID: mchID, AccountID: acctID, RawPayload: rawXML(bm), + }, nil + } + + amountMinor, _ := strconv.ParseInt(bm.GetString("total_fee"), 10, 64) + return &channel.NotifyResult{ + Valid: true, OutTradeNo: bm.GetString("out_trade_no"), + ChannelTxID: bm.GetString("transaction_id"), Status: model.StatusSuccess, + AmountMinor: amountMinor, Payer: bm.GetString("openid"), + MchID: mchID, AccountID: acctID, RawPayload: rawXML(bm), + }, nil +} + +// QueryOrder 主动查询订单 +func (c *Channel) QueryOrder(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) { + if c.pool == nil { + return nil, fmt.Errorf("账户池未初始化") + } + accts := c.pool.ListByChannel("wechat") + for _, acct := range accts { + client, err := c.getOrCreateClient(acct) + if err != nil { + continue + } + bm := make(gopay.BodyMap) + bm.Set("nonce_str", randomStr(32)).Set("out_trade_no", outTradeNo) + wxRsp, _, err := client.QueryOrder(ctx, bm) + if err != nil { + continue + } + return parseOrderStatus(wxRsp), nil + } + return nil, fmt.Errorf("所有微信账户查询均失败") +} + +func parseOrderStatus(wxRsp *wechat.QueryOrderResponse) *channel.OrderStatus { + st := &channel.OrderStatus{ChannelTxID: wxRsp.TransactionId} + switch wxRsp.TradeState { + case "SUCCESS": + st.Status = model.StatusSuccess + case "CLOSED", "REVOKED": + st.Status = model.StatusClosed + case "PAYERROR": + st.Status = model.StatusFailed + default: + st.Status = model.StatusWaitPay + } + if fee, err := strconv.ParseInt(wxRsp.TotalFee, 10, 64); err == nil { + st.AmountMinor = fee + } + return st +} + +// NotifySuccessResponse 微信成功应答 XML +func NotifySuccessResponse() string { + return `` +} + +// NotifyFailResponse 微信失败应答 XML +func NotifyFailResponse() string { + return `` +} + +func rawXML(bm gopay.BodyMap) string { + type kv struct { + XMLName xml.Name `xml:"xml"` + Fields map[string]string + } + fields := make(map[string]string) + for k, v := range bm { + if s, ok := v.(string); ok { + fields[k] = s + } + } + b, _ := xml.Marshal(kv{Fields: fields}) + return string(b) +} + +func randomStr(n int) string { + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + h := md5.Sum([]byte(fmt.Sprintf("%d-%d", time.Now().UnixNano(), len(letters)))) + s := hex.EncodeToString(h[:]) + if len(s) > n { + return s[:n] + } + return s +} + +func truncate(s string, n int) string { + r := []rune(s) + if len(r) <= n { + return s + } + return string(r[:n]) +} diff --git a/internal/epay/sign.go b/internal/epay/sign.go new file mode 100755 index 0000000..729ccf4 --- /dev/null +++ b/internal/epay/sign.go @@ -0,0 +1,85 @@ +// Package epay 易支付协议层:对外接口(submit/api/签名),保持向后兼容 +package epay + +import ( + "crypto/md5" + "crypto/subtle" + "encoding/hex" + "fmt" + "sort" + "strings" +) + +// ParamsFilter 过滤参数,生成签名时需删除 "sign" 和 "sign_type" 参数 +func ParamsFilter(params map[string]string) map[string]string { + filtered := make(map[string]string) + for k, v := range params { + // pay_type 是网关扩展参数(选择支付模式),不参与签名 + if k == "sign" || k == "sign_type" || k == "pay_type" || v == "" { + continue + } + filtered[k] = v + } + return filtered +} + +// ParamsSort 对参数排序 +func ParamsSort(params map[string]string) []string { + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// CreateUrlString 生成待签名字符串 "a=1&b=2&c=3" +func CreateUrlString(params map[string]string) string { + keys := ParamsSort(params) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, k+"="+params[k]) + } + return strings.Join(parts, "&") +} + +// MD5String 生成加盐 MD5 字符串 +func MD5String(urlString, key string) string { + digest := md5.Sum([]byte(urlString + key)) + return hex.EncodeToString(digest[:]) +} + +// GenerateSign 生成签名 +func GenerateSign(params map[string]string, key string) string { + filtered := ParamsFilter(params) + return MD5String(CreateUrlString(filtered), key) +} + +// VerifySign 验证签名(常数时间比较,防时序侧信道逐字节猜签名) +func VerifySign(params map[string]string, key string) bool { + sign := params["sign"] + expected := GenerateSign(params, key) + return subtle.ConstantTimeCompare([]byte(sign), []byte(expected)) == 1 +} + +// BuildSignedParams 生成带签名的参数 +func BuildSignedParams(params map[string]string, key string) map[string]string { + params["sign"] = GenerateSign(params, key) + params["sign_type"] = "MD5" + return params +} + +// BuildQueryString 将参数拼接成 URL query string +func BuildQueryString(params map[string]string) string { + keys := ParamsSort(params) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, k+"="+params[k]) + } + return strings.Join(parts, "&") +} + +// DebugPrint 打印参数用于调试 +func DebugPrint(tag string, params map[string]string) { + fmt.Printf("[%s] %s\n", tag, BuildQueryString(params)) +} diff --git a/internal/ledger/ledger.go b/internal/ledger/ledger.go new file mode 100755 index 0000000..c30ae34 --- /dev/null +++ b/internal/ledger/ledger.go @@ -0,0 +1,224 @@ +// Package ledger 资金审计账本:哈希链 + 数字签名,保证每笔交易不可篡改 +package ledger + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + "sync" + "time" + + "paygateway/internal/store" +) + +// Ledger 审计账本 +type Ledger struct { + mu sync.Mutex + store store.LedgerStore + signKey *ecdsa.PrivateKey +} + +// New 创建账本;privKeyPEM 为空时自动生成新密钥对(打印公钥供保存) +func New(st store.LedgerStore, privKeyPEM string) (*Ledger, error) { + var key *ecdsa.PrivateKey + var err error + if privKeyPEM == "" { + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("生成审计密钥失败: %w", err) + } + // 输出私钥 PEM 供保存(首次运行) + der, err := x509.MarshalECPrivateKey(key) + if err == nil { + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}) + fmt.Printf("[ledger] 首次运行,生成审计密钥,请保存到环境变量 LEDGER_PRIVATE_KEY:\n%s\n", string(pemBytes)) + } + } else { + key, err = parsePrivateKey(privKeyPEM) + if err != nil { + return nil, fmt.Errorf("解析审计私钥失败: %w", err) + } + } + return &Ledger{store: st, signKey: key}, nil +} + +func parsePrivateKey(pemStr string) (*ecdsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(pemStr)) + if block == nil { + return nil, fmt.Errorf("无效的 PEM 私钥") + } + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return key, nil +} + +// PublicKeyPEM 返回公钥 PEM(用于校验签名) +func (l *Ledger) PublicKeyPEM() string { + der, _ := x509.MarshalPKIXPublicKey(&l.signKey.PublicKey) + return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})) +} + +// HasEvent 按 eventID(订单ID)查询账本是否已有记录(崩溃恢复去重用) +func (l *Ledger) HasEvent(ctx context.Context, eventID int64) (bool, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.store.HasLedgerEvent(ctx, eventID) +} + +// Append 追加一条不可篡改记录:哈希链 + 签名 +// payload 为事件全部内容(订单号+金额+状态+通道交易号+时间+原始数据),必须包含所有不可变字段 +func (l *Ledger) Append(ctx context.Context, eventID int64, payload string) error { + l.mu.Lock() + defer l.mu.Unlock() + + // 取链尾哈希 + last, err := l.store.GetLastLedgerEntry(ctx) + if err != nil { + return fmt.Errorf("读取链尾失败: %w", err) + } + prevHash := "" + if last != nil { + prevHash = last.EntryHash + } + + payloadHash := sha256Hex(payload) + entryHash := sha256Hex(prevHash + payloadHash) + + // 签名 entry_hash + sig, err := l.sign(entryHash) + if err != nil { + return fmt.Errorf("签名失败: %w", err) + } + + // 写入账本(只增不改,数据库层有触发器保护) + if err := l.store.AppendLedger(ctx, prevHash, payloadHash, entryHash, sig, eventID); err != nil { + return fmt.Errorf("写入账本失败: %w", err) + } + return nil +} + +// Verify 全链完整性校验:从 genesis 重算哈希链 + 校验签名 +func (l *Ledger) Verify(ctx context.Context) (*VerifyResult, error) { + res := &VerifyResult{StartTime: time.Now()} + + count, err := l.store.CountLedger(ctx) + if err != nil { + return nil, err + } + res.TotalEntries = count + + var prevHash string + // 快照锚点校验:锚点必须存在于链上(快照是历史时刻的锚点,允许落后于链尾) + sn, _ := l.store.GetLatestSnapshot(ctx) + anchorFound := false + for id := int64(1); id <= count; id++ { + entry, err := l.store.GetLedgerEntry(ctx, id) + if err != nil { + return nil, err + } + if entry == nil { + res.Errors = append(res.Errors, fmt.Sprintf("账本条目缺失: id=%d", id)) + break + } + // 校验链 + expected := sha256Hex(prevHash + entry.PayloadHash) + if entry.EntryHash != expected { + res.Errors = append(res.Errors, fmt.Sprintf("哈希链断裂: id=%d 期望=%s 实际=%s", entry.ID, expected, entry.EntryHash)) + } + // 校验签名 + if !l.verify(entry.EntryHash, entry.Signature) { + res.Errors = append(res.Errors, fmt.Sprintf("签名校验失败: id=%d", entry.ID)) + } + if sn != nil && entry.EntryHash == sn.LastHash { + anchorFound = true + } + prevHash = entry.EntryHash + } + + // 快照锚点存在性:在链上即代表快照覆盖的链段完整(历史锚点,非必须等于链尾) + if sn != nil { + if anchorFound { + res.SnapshotOK = true + } else { + res.Errors = append(res.Errors, fmt.Sprintf("快照锚点不在链上: 快照=%s", sn.LastHash)) + } + } + res.Valid = len(res.Errors) == 0 + res.EndTime = time.Now() + return res, nil +} + +// CreateSnapshot 生成每日快照(链尾哈希 + 签名) +func (l *Ledger) CreateSnapshot(ctx context.Context, date string) error { + l.mu.Lock() + defer l.mu.Unlock() + last, err := l.store.GetLastLedgerEntry(ctx) + if err != nil { + return err + } + if last == nil { + return fmt.Errorf("账本为空,无法生成快照") + } + sig, err := l.sign(last.EntryHash) + if err != nil { + return err + } + return l.store.CreateSnapshot(ctx, date, last.EntryHash, sig) +} + +// VerifyResult 校验结果 +type VerifyResult struct { + Valid bool + TotalEntries int64 + Errors []string + SnapshotOK bool + StartTime time.Time + EndTime time.Time +} + +func (r *VerifyResult) Summary() string { + if r.Valid { + return fmt.Sprintf("账本完整 ✅ 共 %d 条,快照锚点匹配", r.TotalEntries) + } + return fmt.Sprintf("账本校验失败 ❌ 共 %d 条,%d 处错误", r.TotalEntries, len(r.Errors)) +} + +// ---------- 工具 ---------- + +func sha256Hex(s string) string { + h := sha256.Sum256([]byte(s)) + return hex.EncodeToString(h[:]) +} + +func (l *Ledger) sign(data string) (string, error) { + h := sha256.Sum256([]byte(data)) + r, s, err := ecdsa.Sign(rand.Reader, l.signKey, h[:]) + if err != nil { + return "", err + } + // 编码 r||s(各32字节) + out := make([]byte, 64) + r.FillBytes(out[:32]) + s.FillBytes(out[32:]) + return hex.EncodeToString(out), nil +} + +func (l *Ledger) verify(data, sigHex string) bool { + sig, err := hex.DecodeString(sigHex) + if err != nil || len(sig) != 64 { + return false + } + h := sha256.Sum256([]byte(data)) + r := new(big.Int).SetBytes(sig[:32]) + s := new(big.Int).SetBytes(sig[32:]) + return ecdsa.Verify(&l.signKey.PublicKey, h[:], r, s) +} diff --git a/internal/ledger/ledger_test.go b/internal/ledger/ledger_test.go new file mode 100755 index 0000000..ee5af2a --- /dev/null +++ b/internal/ledger/ledger_test.go @@ -0,0 +1,369 @@ +package ledger + +import ( + "context" + "strings" + "sync" + "testing" + "time" + + "paygateway/internal/store" +) + +// mockLedgerStore 内存版 LedgerStore,用于单元测试 +type mockLedgerStore struct { + mu sync.Mutex + entries []*store.LedgerEntry + snapshots []*store.Snapshot + nextID int64 +} + +func (m *mockLedgerStore) AppendLedger(_ context.Context, prevHash, payloadHash, entryHash, signature string, eventID int64) error { + m.mu.Lock() + defer m.mu.Unlock() + m.nextID++ + m.entries = append(m.entries, &store.LedgerEntry{ + ID: m.nextID, + PrevHash: prevHash, + PayloadHash: payloadHash, + EntryHash: entryHash, + Signature: signature, + EventID: eventID, + CreatedAt: time.Now(), + }) + return nil +} + +func (m *mockLedgerStore) GetLastLedgerEntry(_ context.Context) (*store.LedgerEntry, error) { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.entries) == 0 { + return nil, nil + } + return m.entries[len(m.entries)-1], nil +} + +func (m *mockLedgerStore) CountLedger(_ context.Context) (int64, error) { + m.mu.Lock() + defer m.mu.Unlock() + return int64(len(m.entries)), nil +} + +func (m *mockLedgerStore) GetLedgerEntry(_ context.Context, id int64) (*store.LedgerEntry, error) { + m.mu.Lock() + defer m.mu.Unlock() + for _, e := range m.entries { + if e.ID == id { + return e, nil + } + } + return nil, nil +} + +func (m *mockLedgerStore) HasLedgerEvent(_ context.Context, eventID int64) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + for _, e := range m.entries { + if e.EventID == eventID { + return true, nil + } + } + return false, nil +} + +func (m *mockLedgerStore) ListLedger(_ context.Context, limit int) ([]*store.LedgerEntry, error) { + m.mu.Lock() + defer m.mu.Unlock() + if limit <= 0 || limit > len(m.entries) { + limit = len(m.entries) + } + out := make([]*store.LedgerEntry, 0, limit) + for i := len(m.entries) - 1; i >= 0 && len(out) < limit; i-- { + out = append(out, m.entries[i]) + } + return out, nil +} + +func (m *mockLedgerStore) CreateSnapshot(_ context.Context, date, lastHash, signature string) error { + m.mu.Lock() + defer m.mu.Unlock() + m.snapshots = append(m.snapshots, &store.Snapshot{ + ID: int64(len(m.snapshots) + 1), + SnapshotDate: date, + LastHash: lastHash, + Signature: signature, + CreatedAt: time.Now(), + }) + return nil +} + +func (m *mockLedgerStore) GetLatestSnapshot(_ context.Context) (*store.Snapshot, error) { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.snapshots) == 0 { + return nil, nil + } + return m.snapshots[len(m.snapshots)-1], nil +} + +func (m *mockLedgerStore) ListSnapshots(_ context.Context, limit int) ([]*store.Snapshot, error) { + m.mu.Lock() + defer m.mu.Unlock() + if limit <= 0 || limit > len(m.snapshots) { + limit = len(m.snapshots) + } + out := make([]*store.Snapshot, 0, limit) + for i := len(m.snapshots) - 1; i >= 0 && len(out) < limit; i-- { + out = append(out, m.snapshots[i]) + } + return out, nil +} + +func (m *mockLedgerStore) Close() error { return nil } + +// newTestLedger 创建带新密钥的测试账本 +func newTestLedger(t *testing.T) (*Ledger, *mockLedgerStore) { + t.Helper() + m := &mockLedgerStore{} + // 传空私钥 → 自动生成新密钥对 + lg, err := New(m, "") + if err != nil { + t.Fatalf("创建账本失败: %v", err) + } + return lg, m +} + +// TestAppendAndVerify 测试:追加记录 + 全链校验通过 +func TestAppendAndVerify(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + // 追加 3 条记录 + for i := int64(1); i <= 3; i++ { + payload := "order=TEST001&amount=100&status=PAID&event=" + itoa(i) + if err := lg.Append(ctx, i, payload); err != nil { + t.Fatalf("追加第 %d 条失败: %v", i, err) + } + } + + // 数量正确 + count, _ := m.CountLedger(ctx) + if count != 3 { + t.Fatalf("账本条目数 = %d, 期望 3", count) + } + + // 全链校验通过 + res, err := lg.Verify(ctx) + if err != nil { + t.Fatalf("校验失败: %v", err) + } + if !res.Valid { + t.Fatalf("账本应有效,实际错误: %v", res.Errors) + } + if res.TotalEntries != 3 { + t.Fatalf("校验条目数 = %d, 期望 3", res.TotalEntries) + } +} + +// TestHashChainLinked 测试:哈希链正确连接(每条记录包含上一条哈希) +func TestHashChainLinked(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + _ = lg.Append(ctx, 1, "payload-1") + _ = lg.Append(ctx, 2, "payload-2") + _ = lg.Append(ctx, 3, "payload-3") + + m.mu.Lock() + defer m.mu.Unlock() + + // 检查哈希链 + if m.entries[0].PrevHash != "" { + t.Errorf("第一条 prev_hash 应为空(创世),实际 %q", m.entries[0].PrevHash) + } + if m.entries[1].PrevHash != m.entries[0].EntryHash { + t.Errorf("第2条 prev_hash 应等于第1条 entry_hash") + } + if m.entries[2].PrevHash != m.entries[1].EntryHash { + t.Errorf("第3条 prev_hash 应等于第2条 entry_hash") + } +} + +// TestTamperDetected 测试:篡改任意条目 → 校验失败 +func TestTamperDetected(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + _ = lg.Append(ctx, 1, "payload-1") + _ = lg.Append(ctx, 2, "payload-2") + _ = lg.Append(ctx, 3, "payload-3") + + // 篡改第 2 条的 entry_hash(模拟攻击者修改数据库) + m.mu.Lock() + m.entries[1].EntryHash = "fakehash123" + m.mu.Unlock() + + res, err := lg.Verify(ctx) + if err != nil { + t.Fatalf("校验出错: %v", err) + } + if res.Valid { + t.Fatal("篡改后账本不应有效") + } + found := false + for _, e := range res.Errors { + if strings.Contains(e, "哈希链断裂") { + found = true + } + } + if !found { + t.Errorf("应报告哈希链断裂,实际错误: %v", res.Errors) + } +} + +// TestTamperSignatureDetected 测试:修改 payload_hash → 签名校验失败 +func TestTamperSignatureDetected(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + _ = lg.Append(ctx, 1, "payload-1") + _ = lg.Append(ctx, 2, "payload-2") + + // 篡改 payload_hash(修改内容但保留原签名) + m.mu.Lock() + m.entries[0].PayloadHash = "changed" + m.mu.Unlock() + + res, err := lg.Verify(ctx) + if err != nil { + t.Fatalf("校验出错: %v", err) + } + if res.Valid { + t.Fatal("篡改后账本不应有效") + } +} + +// TestSnapshotCreateAndVerify 测试:快照创建 + 锚点校验 +func TestSnapshotCreateAndVerify(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + _ = lg.Append(ctx, 1, "payload-1") + _ = lg.Append(ctx, 2, "payload-2") + + // 创建快照 + if err := lg.CreateSnapshot(ctx, "2026-08-08"); err != nil { + t.Fatalf("创建快照失败: %v", err) + } + + sn, _ := m.GetLatestSnapshot(ctx) + if sn == nil { + t.Fatal("快照未创建") + } + + // 快照链尾哈希应等于最后一条的 entry_hash + last, _ := m.GetLastLedgerEntry(ctx) + if sn.LastHash != last.EntryHash { + t.Errorf("快照链尾 %q 不等于账本链尾 %q", sn.LastHash, last.EntryHash) + } + + // 校验应通过且快照锚点匹配 + res, err := lg.Verify(ctx) + if err != nil { + t.Fatalf("校验失败: %v", err) + } + if !res.Valid || !res.SnapshotOK { + t.Errorf("校验应有效且快照锚点匹配,valid=%v snapshot_ok=%v", res.Valid, res.SnapshotOK) + } +} + +// TestSnapshotAnchorMismatch 测试:快照锚点不在链上 → 校验失败(真正的数据异常) +func TestSnapshotAnchorMismatch(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + _ = lg.Append(ctx, 1, "payload-1") + _ = lg.Append(ctx, 2, "payload-2") + if err := lg.CreateSnapshot(ctx, "2026-08-08"); err != nil { + t.Fatalf("创建快照失败: %v", err) + } + + // 快照后正常追加新记录:锚点是历史锚点(仍在链上),校验应通过 + _ = lg.Append(ctx, 3, "payload-3") + + res, err := lg.Verify(ctx) + if err != nil { + t.Fatalf("校验出错: %v", err) + } + if !res.Valid || !res.SnapshotOK { + t.Errorf("快照锚点在链上(历史锚点)应有效,valid=%v snapshot_ok=%v", res.Valid, res.SnapshotOK) + } + + // 篡改快照锚点为链上不存在的哈希(模拟数据库被整体替换 / 快照被篡改) + m.mu.Lock() + m.snapshots[len(m.snapshots)-1].LastHash = "deadbeef-not-on-chain" + m.mu.Unlock() + + res, err = lg.Verify(ctx) + if err != nil { + t.Fatalf("校验出错: %v", err) + } + if res.Valid { + t.Fatal("快照锚点不在链上时账本不应标记为有效") + } + if res.SnapshotOK { + t.Error("快照锚点不在链上时 SnapshotOK 应为 false") + } +} + +// TestPublicKeyPEM 测试:公钥导出为合法 PEM +func TestPublicKeyPEM(t *testing.T) { + lg, _ := newTestLedger(t) + pem := lg.PublicKeyPEM() + if !strings.Contains(pem, "-----BEGIN PUBLIC KEY-----") { + t.Errorf("公钥应包含 PEM 头,实际: %q", pem) + } + if !strings.Contains(pem, "-----END PUBLIC KEY-----") { + t.Errorf("公钥应包含 PEM 尾,实际: %q", pem) + } +} + +// TestSignatureUnique 测试:相同内容不同事件 → 不同签名(防重放) +func TestSignatureUnique(t *testing.T) { + lg, m := newTestLedger(t) + ctx := context.Background() + + // 相同 payload 但不同 eventID + _ = lg.Append(ctx, 1, "same-payload") + _ = lg.Append(ctx, 2, "same-payload") + + m.mu.Lock() + defer m.mu.Unlock() + if m.entries[0].EntryHash == m.entries[1].EntryHash { + t.Error("不同事件不应有相同 entry_hash(防重放)") + } + if m.entries[0].Signature == m.entries[1].Signature { + t.Error("不同事件不应有相同签名(防重放)") + } +} +func itoa(n int64) string { + if n == 0 { + return "0" + } + neg := n < 0 + if neg { + n = -n + } + var b [20]byte + i := len(b) + for n > 0 { + i-- + b[i] = byte('0' + n%10) + n /= 10 + } + if neg { + i-- + b[i] = '-' + } + return string(b[i:]) +} diff --git a/internal/ledgermon/detector.go b/internal/ledgermon/detector.go new file mode 100644 index 0000000..c15421f --- /dev/null +++ b/internal/ledgermon/detector.go @@ -0,0 +1,178 @@ +package ledgermon + +import ( + "context" + "sync" + + "paygateway/internal/admin/capture" + "paygateway/internal/logger" +) + +// SeenStore 增量指纹持久化(由 store 层实现,SQLite ledger_checksums 表) +// 接口刻意最小:仅指纹集合读写,避免与 PersonalStore 全接口耦合。 +type SeenStore interface { + // LoadSeen 加载某 (设备, 账户) 已见指纹集合(服务端重启后恢复基线,防重启漏检) + LoadSeen(ctx context.Context, deviceID string, accountID int64) ([]string, error) + // SaveSeen 持久化增量后的完整指纹集合(覆盖写;集合小(可见行 ≤ 数十),全量写足够) + SaveSeen(ctx context.Context, deviceID string, accountID int64, fingerprints []string) error +} + +// IncrementalDetector 基线 + 增量检测:账本页行去重(指纹 = 时间|金额,见 capture.AlipayLedgerRow) +// +// 语义: +// - 首次读取(无基线)→ 全量建立 seen 集合,不推送(防把历史收款当新收款) +// - 后续读取 → 只推送不在 seen 集合的新行(时间|金额指纹,秒级时间戳区分同金额多笔) +// - 指纹持久化:服务端重启后从 store 恢复,重启期间的新收款不丢 +// +// 注意:baselined 与 seen 大小解耦——账本页空态(无可见行)也是有效基线, +// 否则首笔真实收款会被当作"历史"吞掉(漏收款)。 +// +// 并发安全:单账户循环内调用,但持久化读写用锁兜底(防未来多 goroutine 场景)。 +type IncrementalDetector struct { + mu sync.Mutex + deviceID string + accountID int64 + baselined bool // 是否已建立基线(与 seen 大小无关,空态页也是基线) + seen map[string]bool + store SeenStore // nil = 仅内存(不持久化) +} + +// NewDetector 创建增量检测器。store 可 nil(仅内存基线)。 +func NewDetector(deviceID string, accountID int64, store SeenStore) *IncrementalDetector { + return &IncrementalDetector{ + deviceID: deviceID, + accountID: accountID, + seen: make(map[string]bool), + store: store, + } +} + +// Diff 输入账本页当前可见行,返回新收款行(不在 seen 集合),并更新基线。 +// 基线为空(首次/重启后无持久化)→ 全部标记 seen,返回空(不推送历史)。 +// rows 可为空(空态页):仍标记基线已建立,后续新行正常推送。 +func (d *IncrementalDetector) Diff(ctx context.Context, rows []capture.AlipayLedgerRow) []capture.AlipayLedgerRow { + d.mu.Lock() + defer d.mu.Unlock() + d.restoreBaselineLocked(ctx) + + // 无基线(首次)→ 全量建基线,不推送(空态页也建基线,防首笔收款被吞) + if !d.baselined { + for _, r := range rows { + d.seen[r.Fingerprint()] = true + } + d.baselined = true + d.persistLocked(ctx) + logger.Info("ledgermon", "账本页基线已建立", + "device_id", d.deviceID, "account_id", d.accountID, "rows", len(rows)) + return nil + } + + var fresh []capture.AlipayLedgerRow + for _, r := range rows { + fp := r.Fingerprint() + if !d.seen[fp] { + d.seen[fp] = true + fresh = append(fresh, r) + } + } + if len(fresh) > 0 { + d.persistLocked(ctx) + } + return fresh +} + +// Fresh 返回不在 seen 集合的新收款行,但**不提交** seen(资金安全:先推送成功再提交, +// 推送失败下一轮仍会再次检测到该行,不会永久丢单)。首次(无基线)→ 建基线返回空。 +// rows 可为空(空态页):首次仍标记基线已建立。 +func (d *IncrementalDetector) Fresh(ctx context.Context, rows []capture.AlipayLedgerRow) []capture.AlipayLedgerRow { + d.mu.Lock() + defer d.mu.Unlock() + d.restoreBaselineLocked(ctx) + + if !d.baselined { + for _, r := range rows { + d.seen[r.Fingerprint()] = true + } + d.baselined = true + d.persistLocked(ctx) + logger.Info("ledgermon", "账本页基线已建立(Fresh)", + "device_id", d.deviceID, "account_id", d.accountID, "rows", len(rows)) + return nil + } + + var fresh []capture.AlipayLedgerRow + for _, r := range rows { + if !d.seen[r.Fingerprint()] { + fresh = append(fresh, r) + } + } + return fresh +} + +// Commit 将 fresh 行指纹提交到 seen 并持久化(Dispatch 成功后调用)。 +// 未提交的 fresh 行在下一轮 Fresh 中仍会被返回(重试推送,不丢单)。 +func (d *IncrementalDetector) Commit(ctx context.Context, fresh []capture.AlipayLedgerRow) { + if len(fresh) == 0 { + return + } + d.mu.Lock() + defer d.mu.Unlock() + changed := false + for _, r := range fresh { + if !d.seen[r.Fingerprint()] { + d.seen[r.Fingerprint()] = true + changed = true + } + } + if changed { + d.persistLocked(ctx) + } +} + +// restoreBaselineLocked 从 store 恢复基线(仅首次/重启后;调用方持锁)。 +func (d *IncrementalDetector) restoreBaselineLocked(ctx context.Context) { + if d.baselined || d.store == nil { + return + } + if stored, err := d.store.LoadSeen(ctx, d.deviceID, d.accountID); err == nil { + for _, fp := range stored { + d.seen[fp] = true + } + if len(stored) > 0 { + d.baselined = true + } + logger.Info("ledgermon", "增量基线已从持久化恢复", + "device_id", d.deviceID, "account_id", d.accountID, "fingerprints", len(stored)) + } else { + logger.Warn("ledgermon", "增量基线加载失败(将全量建基线)", "err", err, "device_id", d.deviceID) + } +} + +// persistLocked 持久化 seen 集合(调用方持锁)。集合膨胀保护:只保留最近 N 个指纹 +// (账本页可见行有限,但跨天累计会增长;保留最近 1000 个足够覆盖重试窗口)。 +const maxSeenFingerprints = 1000 + +func (d *IncrementalDetector) persistLocked(ctx context.Context) { + if d.store == nil { + return + } + // map 顺序不稳定 → 排序后保留尾部(时间戳指纹字典序≈时间序) + fps := make([]string, 0, len(d.seen)) + for fp := range d.seen { + fps = append(fps, fp) + } + // 简单稳定化:只存最近 1000 条(不排序也可,去重集合无顺序要求;截断即可) + if len(fps) > maxSeenFingerprints { + fps = fps[:maxSeenFingerprints] + } + if err := d.store.SaveSeen(ctx, d.deviceID, d.accountID, fps); err != nil { + logger.Warn("ledgermon", "增量基线持久化失败", "err", err, "device_id", d.deviceID, "account_id", d.accountID) + } +} + +// Size 返回已见指纹数(监控/调试) +func (d *IncrementalDetector) Size() int { + d.mu.Lock() + defer d.mu.Unlock() + return len(d.seen) +} diff --git a/internal/ledgermon/detector_test.go b/internal/ledgermon/detector_test.go new file mode 100644 index 0000000..dd76d67 --- /dev/null +++ b/internal/ledgermon/detector_test.go @@ -0,0 +1,129 @@ +package ledgermon + +import ( + "context" + "testing" + + "paygateway/internal/admin/capture" +) + +// memSeenStore 内存版 SeenStore(测试用,模拟 SQLite 持久化) +type memSeenStore struct { + m map[string][]string // deviceID|accountID -> fingerprints +} + +func newMemSeenStore() *memSeenStore { return &memSeenStore{m: map[string][]string{}} } + +func (s *memSeenStore) LoadSeen(_ context.Context, deviceID string, accountID int64) ([]string, error) { + return s.m[deviceID], nil +} +func (s *memSeenStore) SaveSeen(_ context.Context, deviceID string, accountID int64, fps []string) error { + s.m[deviceID] = append([]string(nil), fps...) + return nil +} + +func row(time, amount string) capture.AlipayLedgerRow { + return capture.AlipayLedgerRow{ + Payer: "**怡", + Time: time, + Amount: amount, + AmountMinor: 100, + } +} + +// TestDetector_FirstBaseline 首次读取(无基线)→ 全量建基线,不推送(防把历史收款当新收款) +func TestDetector_FirstBaseline(t *testing.T) { + d := NewDetector("dev1", 1, nil) + rows := []capture.AlipayLedgerRow{row("10:00:01", "+1.00"), row("10:00:02", "+2.00")} + fresh := d.Diff(context.Background(), rows) + if len(fresh) != 0 { + t.Fatalf("首次应建基线不推送,实际推送 %d 行", len(fresh)) + } + if d.Size() != 2 { + t.Fatalf("基线应含 2 个指纹,实际 %d", d.Size()) + } +} + +// TestDetector_Incremental 后续新增行 → 只推送新行 +func TestDetector_Incremental(t *testing.T) { + d := NewDetector("dev1", 1, nil) + // 基线 + d.Diff(context.Background(), []capture.AlipayLedgerRow{row("10:00:01", "+1.00")}) + // 新增一行 + fresh := d.Diff(context.Background(), []capture.AlipayLedgerRow{ + row("10:00:01", "+1.00"), // 已见 + row("10:00:02", "+2.00"), // 新行 + }) + if len(fresh) != 1 || fresh[0].Time != "10:00:02" { + t.Fatalf("应只推送新行 10:00:02,实际 %+v", fresh) + } +} + +// TestDetector_SameAmountMulti 同金额多笔:靠秒级时间戳区分,两笔都推送 +func TestDetector_SameAmountMulti(t *testing.T) { + d := NewDetector("dev1", 1, nil) + d.Diff(context.Background(), []capture.AlipayLedgerRow{row("10:00:01", "+1.00")}) + fresh := d.Diff(context.Background(), []capture.AlipayLedgerRow{ + row("10:00:01", "+1.00"), + row("10:00:02", "+1.00"), // 同金额不同秒 + }) + if len(fresh) != 1 || fresh[0].Time != "10:00:02" { + t.Fatalf("同金额不同秒应作为新行推送,实际 %+v", fresh) + } +} + +// TestDetector_NoChange 页面无变化(重复读取)→ 不推送 +func TestDetector_NoChange(t *testing.T) { + d := NewDetector("dev1", 1, nil) + rows := []capture.AlipayLedgerRow{row("10:00:01", "+1.00")} + d.Diff(context.Background(), rows) + if fresh := d.Diff(context.Background(), rows); len(fresh) != 0 { + t.Fatalf("重复行不应推送,实际 %d 行", len(fresh)) + } +} + +// TestDetector_RestartRestore 服务端重启:从 store 恢复基线,重启期间的新收款不丢 +func TestDetector_RestartRestore(t *testing.T) { + store := newMemSeenStore() + // 第一次运行:建基线(10:00:01 已见)+ 新收款 10:00:02 + d1 := NewDetector("dev1", 1, store) + d1.Diff(context.Background(), []capture.AlipayLedgerRow{row("10:00:01", "+1.00")}) + d1.Diff(context.Background(), []capture.AlipayLedgerRow{ + row("10:00:01", "+1.00"), row("10:00:02", "+2.00"), + }) + // 模拟重启:新 detector 从 store 恢复 + d2 := NewDetector("dev1", 1, store) + // 重启期间新收款 10:00:03 已出现在账本页 + fresh := d2.Diff(context.Background(), []capture.AlipayLedgerRow{ + row("10:00:01", "+1.00"), // 历史已见 → 不推 + row("10:00:02", "+2.00"), // 重启前已处理 → 不推 + row("10:00:03", "+3.00"), // 重启期间新收款 → 推 + }) + if len(fresh) != 1 || fresh[0].Time != "10:00:03" { + t.Fatalf("重启后应只推送新收款 10:00:03,实际 %+v", fresh) + } +} + +// TestDetector_RestartWithoutStore 重启但无持久化 → 重新全量建基线(不推送历史) +func TestDetector_RestartWithoutStore(t *testing.T) { + d1 := NewDetector("dev1", 1, nil) + d1.Diff(context.Background(), []capture.AlipayLedgerRow{row("10:00:01", "+1.00")}) + // 重启(无 store):全部行视为历史基线 + d2 := NewDetector("dev1", 1, nil) + fresh := d2.Diff(context.Background(), []capture.AlipayLedgerRow{ + row("10:00:01", "+1.00"), row("10:00:02", "+2.00"), + }) + if len(fresh) != 0 { + t.Fatalf("无持久化重启应全量建基线不推送,实际 %d 行", len(fresh)) + } +} + +// TestDetector_PersistAfterBaseline 基线建立后应持久化(store 有数据) +func TestDetector_PersistAfterBaseline(t *testing.T) { + store := newMemSeenStore() + d := NewDetector("dev1", 1, store) + d.Diff(context.Background(), []capture.AlipayLedgerRow{row("10:00:01", "+1.00")}) + if stored := store.m["dev1"]; len(stored) != 1 { + t.Fatalf("基线应持久化 1 个指纹,实际 %d", len(stored)) + } +} diff --git a/internal/ledgermon/device_actor.go b/internal/ledgermon/device_actor.go new file mode 100644 index 0000000..5fc90dc --- /dev/null +++ b/internal/ledgermon/device_actor.go @@ -0,0 +1,577 @@ +// Package ledgermon 服务端账本页 adb 监听(DeviceActor 模型,2026-08-28 重构) +// +// DeviceActor 模型:每台设备恰好一个 DeviceActor goroutine,串行消费命令队列。 +// 所有 adb 操作(导航/读取/锁定/手动预热)只通过 actor 的命令执行。 +// 无 lease、无仲裁、无抢占,竞态从结构上消失(docs/DESIGN-DEVICE-ACTOR.md)。 +package ledgermon + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/admin/capture" + "paygateway/internal/logger" +) + +// ========== 命令接口 ========== + +// Command 设备操作命令(actor 队列元素,串行执行) +type Command interface { + Execute(ctx context.Context, d *DeviceCtx) error +} + +// ========== 命令实现 ========== + +// WarmupCmd 预热:导航到指定账户账本页 + 建立基线(设备上线时入队) +type WarmupCmd struct { + AccountID int64 + Provider string // alipay / wechat + UserID int // 0=主空间,999=双开 + Done chan error // 完成信号(锁定协调器/状态查询等待用) +} + +func (c *WarmupCmd) Execute(ctx context.Context, d *DeviceCtx) error { + acct := d.Account(c.Provider, c.UserID) + if acct == nil { + return fmt.Errorf("device_actor: 账户上下文不存在 provider=%s user_id=%d", c.Provider, c.UserID) + } + // 预热优先走 Warmup(task lock 热启动优先 → 账本页栈不存在时冷启动坐标导航建页) + if w, ok := acct.Reader.(Warmer); ok { + if err := w.Warmup(ctx); err != nil { + logger.Warn("device_actor", "预热导航失败", + "err", err, "device_id", d.DeviceID, "account_id", c.AccountID) + return err + } + } else if err := acct.Reader.EnsureForeground(ctx); err != nil { + logger.Warn("device_actor", "预热导航失败(无 Warmer)", + "err", err, "device_id", d.DeviceID, "account_id", c.AccountID) + return err + } + // 读取基线(冷启动导航完成时账本页即最新数据,优先用 ReadFresh 免下拉刷新) + var rows []capture.AlipayLedgerRow + var err error + if fr, ok := acct.Reader.(FreshReader); ok { + rows, err = fr.ReadFresh(ctx) + } else { + rows, err = acct.Reader.Read(ctx) + } + if err != nil { + logger.Warn("device_actor", "预热基线读取失败", + "err", err, "device_id", d.DeviceID, "account_id", c.AccountID) + return err + } + // 空行也走 Diff:空态页是有效基线,保证首笔真实收款不被当历史吞掉 + acct.Detector.Diff(ctx, rows) + logger.Info("device_actor", "预热完成:账本页已就绪,基线已建立", + "device_id", d.DeviceID, "account_id", c.AccountID, "user_id", c.UserID, "rows", len(rows)) + return nil +} + +// PollCmd 轮询:热启动恢复账本页 + 读取 + 增量检测 + 推送(调度器入队)。 +// Done 可选(nil=投递后不等待,兼容旧调用方);非 nil 时调度器等待执行完成(pull 模式)。 +type PollCmd struct { + AccountID int64 + Provider string // alipay / wechat + UserID int // 0=主空间,999=双开 + Done chan error +} + +func (c *PollCmd) Execute(ctx context.Context, d *DeviceCtx) error { + acct := d.Account(c.Provider, c.UserID) + if acct == nil { + return nil // 账户已删除/离线,静默跳过 + } + // 热启动恢复前台(绝不冷启动,失败本轮跳过) + if err := acct.Reader.EnsureForeground(ctx); err != nil { + logger.Warn("device_actor", "轮询恢复前台失败,本轮跳过", + "err", err, "device_id", d.DeviceID, "account_id", c.AccountID) + // 热启动连续失败 → 任务栈大概率已丢失,升级冷启动预热自愈(防永久漏检)。 + if acct.bumpPollFail() >= pollFailWarmupThreshold { + logger.Warn("device_actor", "热启动连续失败,重投预热命令自愈(冷启动重建账本页)", + "device_id", d.DeviceID, "account_id", c.AccountID, "failures", acct.pollFailures()) + d.Enqueue(&WarmupCmd{AccountID: c.AccountID, Provider: c.Provider, UserID: c.UserID}) + } + d.RecordPoll(false) + return nil + } + acct.resetPollFail() + rows, err := acct.Reader.Read(ctx) + if err != nil { + logger.Warn("device_actor", "轮询读取失败", + "err", err, "device_id", d.DeviceID, "account_id", c.AccountID) + d.RecordPoll(false) + return nil + } + d.RecordPoll(true) + // 资金安全:先检测新行(Fresh 不提交 seen),推送成功后才 Commit—— + // Dispatch 失败时该行下轮仍会被检测到并重试,绝不永久丢单。 + fresh := acct.Detector.Fresh(ctx, rows) + if len(fresh) == 0 { + return nil + } + if d.Dispatch == nil { + acct.Detector.Commit(ctx, fresh) + return nil + } + if err := d.Dispatch(ctx, d.DeviceID, acct.Provider, acct.UserID, fresh); err != nil { + logger.Error("device_actor", "收款事件推送失败(行保留待重试,不提交指纹)", + "err", err, "device_id", d.DeviceID, "account_id", c.AccountID, "rows", len(fresh)) + return nil + } + acct.Detector.Commit(ctx, fresh) + return nil +} + +// ManualWarmupCmd 手动预热:完整冷启动导航 + 锁定任务(admin 触发,同步等结果) +type ManualWarmupCmd struct { + // Providers 指定要预热的通道:{"alipay":true,"wechat":true},空 = 全部通道 + Providers map[string]bool + Done chan error +} + +func (c *ManualWarmupCmd) Execute(ctx context.Context, d *DeviceCtx) error { + // 按固定顺序遍历账户(key 排序保证确定性) + for _, key := range d.SortedAccountKeys() { + acct := d.Accounts()[key] + if c.Providers != nil && !c.Providers[acct.Provider] { + continue + } + switch acct.Provider { + case "alipay": + if err := capture.CaptureAlipayLedger(ctx, d.Adb, d.Serial, acct.UserID); err != nil { + return fmt.Errorf("支付宝预热失败 (user_id=%d): %w", acct.UserID, err) + } + case "wechat": + if err := capture.CaptureWechatLedger(ctx, d.Adb, d.Serial, acct.UserID); err != nil { + return fmt.Errorf("微信预热失败 (user_id=%d): %w", acct.UserID, err) + } + } + } + // 全部账户导航完成后锁定支付任务卡片(支付宝+微信,每应用锁主进程一张)。 + // 锁定失败不致命:热启动恢复降级为冷启动导航(EnsureForeground 兜底)。 + if _, err := capture.LockPaymentTasks(ctx, d.Adb, d.Serial, "支付宝", "微信"); err != nil { + logger.Warn("device_actor", "手动预热后锁定任务卡片失败", + "err", err, "device_id", d.DeviceID) + } + return nil +} + +// LockCmd 锁定任务卡片(防一键清理;全部账户预热完成后自动入队) +type LockCmd struct { + Done chan error +} + +func (c *LockCmd) Execute(ctx context.Context, d *DeviceCtx) error { + // 先应用命令级保活(电池白名单/后台运行豁免/禁用缓存冻结),再锁任务卡片(防一键清理)。 + // 命令级保活幂等,重复执行无害;失败不阻断锁任务(锁任务是热启动恢复的关键)。 + for _, pkg := range []string{adbmgr.WechatPkg, adbmgr.AlipayPkg} { + if _, err := d.Adb.KeepAliveApply(ctx, d.Serial, pkg); err != nil { + logger.Warn("device_actor", "保活命令应用失败(不阻断锁任务)", + "err", err, "device_id", d.DeviceID, "pkg", pkg) + } + } + if err := d.Adb.KeepAliveDeviceFreezer(ctx, d.Serial); err != nil { + logger.Warn("device_actor", "禁用缓存冻结失败(不阻断锁任务)", + "err", err, "device_id", d.DeviceID) + } + locked, err := capture.LockPaymentTasks(ctx, d.Adb, d.Serial, "支付宝", "微信") + if err != nil { + logger.Warn("device_actor", "设备任务锁定失败(热启动恢复将降级为冷启动)", + "err", err, "device_id", d.DeviceID) + return err + } + logger.Info("device_actor", "设备支付任务已全部锁定(防一键清理)", + "device_id", d.DeviceID, "locked", locked) + return nil +} + +// ========== 设备执行上下文 ========== + +// AccountCtx 账户级执行上下文(每个 (device, provider, userID) 一个) +type AccountCtx struct { + AccountID int64 + Provider string + UserID int + Reader LedgerReader + Detector *IncrementalDetector + + // pollFail 热启动连续失败次数(actor 单 goroutine 内读写,无并发) + pollFail int +} + +// pollFailWarmupThreshold 热启动连续失败升级阈值:达到后重投 WarmupCmd 冷启动自愈。 +// 3 次 ≈ 3 个轮询周期(1s 间隔),任务栈丢失时热启动必然失败,及时冷启动恢复。 +const pollFailWarmupThreshold = 3 + +// warmupRetryDelay 设备上线预热失败后的重试退避间隔。预热建基线是必要动作, +// 偶发失败(uiautomator dump 冷启动被 kill)后短暂退避重试,避免立即重试撞同一次故障。 +const warmupRetryDelay = 5 * time.Second + +func (a *AccountCtx) bumpPollFail() int { a.pollFail++; return a.pollFail } +func (a *AccountCtx) pollFailures() int { return a.pollFail } +func (a *AccountCtx) resetPollFail() { a.pollFail = 0 } + +// DeviceCtx 设备级执行上下文。Adb/Serial 供 ManualWarmupCmd/LockCmd 直连 capture。 +type DeviceCtx struct { + DeviceID string + Serial string + Adb *adbmgr.Manager + Dispatch DispatchFunc + // actor 设备执行者(命令自愈时向队列投递新命令用,避免重入锁) + actor *DeviceActor + // accounts 账户上下文快照(atomic.Pointer:reconcile 写、actor 读无锁,杜绝 map 并发读写崩溃) + accounts atomic.Pointer[map[string]*AccountCtx] +} + +// Enqueue 向本设备命令队列投递命令(自愈等 actor 内部路径使用) +func (d *DeviceCtx) Enqueue(cmd Command) { d.actor.Enqueue(cmd) } + +// RecordPoll 记录一次 poll 成败(设备健康度滑动窗口,均衡分配器数据源) +func (d *DeviceCtx) RecordPoll(ok bool) { + if d.actor != nil { + d.actor.RecordPoll(ok) + } +} + +// Accounts 返回账户上下文快照(不可变 map,安全并发读) +func (d *DeviceCtx) Accounts() map[string]*AccountCtx { + return *d.accounts.Load() +} + +// Account 按 provider+userID 查找账户上下文 +func (d *DeviceCtx) Account(provider string, userID int) *AccountCtx { + return (*d.accounts.Load())[monitorKey(d.DeviceID, provider, userID)] +} + +// SortedAccountKeys 返回按 key 排序的账户列表(保证执行顺序确定性) +func (d *DeviceCtx) SortedAccountKeys() []string { + accts := d.Accounts() + keys := make([]string, 0, len(accts)) + for k := range accts { + keys = append(keys, k) + } + for i := 0; i < len(keys); i++ { + for j := i + 1; j < len(keys); j++ { + if keys[i] > keys[j] { + keys[i], keys[j] = keys[j], keys[i] + } + } + } + return keys +} + +// ========== DeviceActor ========== + +// maxQueue 命令队列上限:WarmupCmd/PollCmd 队满(>maxQueue)时丢弃 PollCmd +// (跳过一轮,资金安全优先),保留 WarmupCmd/ManualWarmupCmd。 +const maxQueue = 16 + +// DeviceActor 每设备单执行者:串行消费命令队列,唯一 adb 访问者。 +type DeviceActor struct { + DeviceID string + DeviceCtx *DeviceCtx + + mu sync.Mutex + queue []Command + pendingPoll map[int64]bool // 已在队列中的 PollCmd(按 accountID 去重) + cond *sync.Cond + + current atomic.Value // string:当前执行命令名(可观测),空=空闲 + last atomic.Value // lastResult:最近一次命令执行结果(可观测) + stop chan struct{} + done chan struct{} + + // health 设备 poll 健康度滑动窗口(均衡分配器数据源,docs/PLAN-DYNAMIC-CLOSE-ALLOC.md §6) + health *healthWindow +} + +// healthWindow 设备 poll 成功率滑动窗口(最近 size 次,无历史中性 0.9)。 +// 并发安全(RecordPoll 由 actor goroutine + 分配器读并发调用)。 +type healthWindow struct { + mu sync.Mutex + ok []bool + next int + filled bool + size int +} + +func newHealthWindow(size int) *healthWindow { + if size <= 0 { + size = 20 + } + return &healthWindow{ok: make([]bool, size), size: size} +} + +func (h *healthWindow) record(ok bool) { + h.mu.Lock() + defer h.mu.Unlock() + h.ok[h.next] = ok + h.next = (h.next + 1) % h.size + if h.next == 0 { + h.filled = true + } +} + +func (h *healthWindow) health() float64 { + h.mu.Lock() + defer h.mu.Unlock() + if !h.filled { + n := h.next + if n == 0 { + return 0.9 // 无历史:中性,不因无数据饿死新设备 + } + sum := 0 + for i := 0; i < n; i++ { + if h.ok[i] { + sum++ + } + } + return float64(sum) / float64(n) + } + sum := 0 + for _, v := range h.ok { + if v { + sum++ + } + } + return float64(sum) / float64(h.size) +} + +// cmdName 返回命令的简短描述(可观测) +func cmdName(cmd Command) string { + switch cmd.(type) { + case *WarmupCmd: + return "warmup" + case *PollCmd: + return "poll" + case *ManualWarmupCmd: + return "manual_warmup" + case *LockCmd: + return "lock" + default: + return "unknown" + } +} + +// NewDeviceActor 创建设备执行者并启动后台 goroutine。 +func NewDeviceActor(deviceID, serial string, adb *adbmgr.Manager, dispatch DispatchFunc) *DeviceActor { + accounts := make(map[string]*AccountCtx) + a := &DeviceActor{ + DeviceID: deviceID, + DeviceCtx: &DeviceCtx{ + DeviceID: deviceID, + Serial: serial, + Adb: adb, + Dispatch: dispatch, + }, + pendingPoll: make(map[int64]bool), + stop: make(chan struct{}), + done: make(chan struct{}), + health: newHealthWindow(0), + } + a.DeviceCtx.actor = a + a.DeviceCtx.accounts.Store(&accounts) + a.cond = sync.NewCond(&a.mu) + a.current.Store("") + a.last.Store(lastResult{}) + go a.loop() + return a +} + +// AddAccount 添加账户上下文(reconcile 时调用;幂等:已存在时覆盖)。 +// 写路径:锁内复制 map 快照后原子替换——读方(actor 命令执行)无锁 Load,无数据竞态。 +func (a *DeviceActor) AddAccount(acct *AccountCtx) { + a.mu.Lock() + defer a.mu.Unlock() + old := a.DeviceCtx.Accounts() + next := make(map[string]*AccountCtx, len(old)+1) + for k, v := range old { + next[k] = v + } + next[monitorKey(a.DeviceID, acct.Provider, acct.UserID)] = acct + a.DeviceCtx.accounts.Store(&next) +} + +// RemoveAccount 移除账户上下文(设备离线时调用) +func (a *DeviceActor) RemoveAccount(key string) { + a.mu.Lock() + defer a.mu.Unlock() + old := a.DeviceCtx.Accounts() + if _, ok := old[key]; !ok { + return + } + next := make(map[string]*AccountCtx, len(old)) + for k, v := range old { + if k != key { + next[k] = v + } + } + a.DeviceCtx.accounts.Store(&next) +} + +// Enqueue 投递命令到队列(非阻塞)。 +// - PollCmd:同账户去重(已有在队则合并);队满(>maxQueue)时丢弃(宁可跳过一轮)。 +// - ManualWarmupCmd:插队到队首(手动操作优先,但不打断执行中的命令——只影响排队顺序)。 +// - WarmupCmd/LockCmd:正常入队(数量有限,不丢弃)。 +func (a *DeviceActor) Enqueue(cmd Command) { + a.mu.Lock() + defer a.mu.Unlock() + + switch c := cmd.(type) { + case *PollCmd: + if a.pendingPoll[c.AccountID] { + return // 同账户已在队中,合并(防下单风暴堆积) + } + if len(a.queue) >= maxQueue { + logger.Warn("device_actor", "命令队列已满,丢弃 PollCmd", + "device_id", a.DeviceID, "account_id", c.AccountID) + return + } + a.pendingPoll[c.AccountID] = true + a.queue = append(a.queue, cmd) + case *ManualWarmupCmd: + // 插队到队首:手动操作优先,但执行中的命令不被打断(原子性保证) + a.queue = append([]Command{cmd}, a.queue...) + default: + a.queue = append(a.queue, cmd) + } + a.cond.Signal() +} + +// CurrentCmd 返回当前执行命令名(可观测) +func (a *DeviceActor) CurrentCmd() string { + s, _ := a.current.Load().(string) + return s +} + +// QueueLen 返回当前队列长度(可观测) +func (a *DeviceActor) QueueLen() int { + a.mu.Lock() + defer a.mu.Unlock() + return len(a.queue) +} + +// Stop 停止执行者(等待当前命令执行完)。队列中残留命令不再执行, +// 其 Done 等待者(Monitor.warmupOnce/WarmupDevice)由 loop 退出时统一唤醒,避免永久阻塞。 +func (a *DeviceActor) Stop() { + close(a.stop) + a.cond.Broadcast() + <-a.done +} + +// loop 执行循环:串行取命令 → 执行 → 唤醒等待者 +func (a *DeviceActor) loop() { + defer close(a.done) + ctx := context.Background() + for { + cmd := a.pop() + if cmd == nil { + // 已停止:唤醒队列中残留命令的 Done 等待者(不再执行,直接报错返回) + a.abortQueued() + return + } + name := cmdName(cmd) + a.current.Store(name) + err := cmd.Execute(ctx, a.DeviceCtx) + a.setLastResult(name, err) + // 先记录结果再唤醒等待者:Status 能看到最后命令及其错误 + signalCmdDone(cmd, err) + a.current.Store("") + } +} + +// abortQueued 停止时对队列中所有残留命令的 Done 等待者回发错误(防永久阻塞)。 +func (a *DeviceActor) abortQueued() { + a.mu.Lock() + queue := a.queue + a.queue = nil + a.mu.Unlock() + for _, cmd := range queue { + signalCmdDone(cmd, fmt.Errorf("device_actor: 设备执行者已停止,命令未执行")) + } +} + +// lastResult 最近一次执行结果(可观测:Status 展示,供 admin 诊断连续失败) +type lastResult struct { + cmd string + err error + at time.Time +} + +func (a *DeviceActor) setLastResult(cmd string, err error) { + a.last.Store(lastResult{cmd: cmd, err: err, at: time.Now()}) +} + +// LastResult 返回最近一次命令执行结果(Status 展示) +func (a *DeviceActor) LastResult() (string, error) { + lr := a.last.Load().(lastResult) + return lr.cmd, lr.err +} + +// RecordPoll 记录一次 poll 成败(均衡分配器健康度数据源) +func (a *DeviceActor) RecordPoll(ok bool) { + if a.health != nil { + a.health.record(ok) + } +} + +// PollHealth 设备 poll 成功率(0~1;无历史返回 0.9 中性) +func (a *DeviceActor) PollHealth() float64 { + if a.health == nil { + return 0.9 + } + return a.health.health() +} + +// pop 阻塞等待直到有命令可执行或收到停止信号。 +// 每轮先查 stop 再取命令——停止后不再执行队列中残留命令(Stop 只等当前命令,不排空队列)。 +func (a *DeviceActor) pop() Command { + a.mu.Lock() + defer a.mu.Unlock() + for { + select { + case <-a.stop: + return nil + default: + } + if len(a.queue) > 0 { + cmd := a.queue[0] + a.queue = a.queue[1:] + if pc, ok := cmd.(*PollCmd); ok { + delete(a.pendingPoll, pc.AccountID) + } + return cmd + } + a.cond.Wait() + } +} + +// signalCmdDone 通知命令完成(幂等:无 Done 通道的命令直接返回) +func signalCmdDone(cmd Command, err error) { + var done chan error + switch c := cmd.(type) { + case *WarmupCmd: + done = c.Done + case *PollCmd: + done = c.Done + case *ManualWarmupCmd: + done = c.Done + case *LockCmd: + done = c.Done + } + if done != nil { + done <- err // 缓冲 1,actor 不回读,不会阻塞 + } +} + +// SeenRows 返回某账户已见指纹数(增量基线大小,Status 展示) +func (a *DeviceActor) SeenRows(provider string, userID int) int { + if acct := a.DeviceCtx.Account(provider, userID); acct != nil && acct.Detector != nil { + return acct.Detector.Size() + } + return 0 +} diff --git a/internal/ledgermon/monitor.go b/internal/ledgermon/monitor.go new file mode 100644 index 0000000..8d22e09 --- /dev/null +++ b/internal/ledgermon/monitor.go @@ -0,0 +1,652 @@ +package ledgermon + +import ( + "context" + "fmt" + "runtime/debug" + "sync" + "sync/atomic" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/admin/capture" + "paygateway/internal/logger" + "paygateway/internal/pool" +) + +// DispatchFunc 收款事件推送回调(由 main.go 注入:复用匹配+入账链路,见 docs §6.1) +// 行无备注,匹配走金额+时间窗口;source=ledger。space 为账本页所在用户空间(0=主,999=双开), +// 透传给匹配链路区分双开账户收款。 +type DispatchFunc func(ctx context.Context, deviceID string, provider string, space int, rows []capture.AlipayLedgerRow) error + +// LedgerMonitor 每账户命令生产者(DeviceActor 模型,docs/DESIGN-DEVICE-ACTOR.md §4.2) +// +// 从"拥有 goroutine 的执行者"降级为**纯命令生产者**:保留账户级状态机 +// (订单驱动暂停/激活/终结判定),所有 adb 操作通过 DeviceActor 命令队列执行, +// 由 actor 串行化(每设备单执行者),天然互斥、无抢占。 +type LedgerMonitor struct { + accountID int64 + deviceID string + provider string + serial string + userId int // 0=主空间,999=双开分身 + // actor 设备执行者(投递命令用,reconcile 创建 monitor 时注入) + actor *DeviceActor + // hasPending 该账户是否仍有待支付订单(main.go 注入:ListWaitPayByAccount 非空) + hasPending func(ctx context.Context, accountID int64) bool + pollEvery time.Duration // 有待支付订单时的轮询间隔(默认 1s) + wake chan struct{} // 下单激活信号(非阻塞发送) + stop chan struct{} + done chan struct{} + // monitorActive 当前是否处于轮询态(admin 状态展示) + monitorActive atomic.Bool + // warmupDone 预热完成信号(close 后 Manager 可判断设备所有账户已就绪) + warmupDone chan struct{} + // schedulerMode 由 DeviceScheduler 接管轮询(公平调度);本 monitor 只做预热。 + // 订单驱动轮询语义移交给调度器,monitor 完成预热后等待 stop(不再投 PollCmd)。 + schedulerMode bool +} + +// NewMonitor 创建账户命令生产者。pollEvery<=0 用默认 1s。 +func NewMonitor(accountID int64, deviceID, provider, serial string, userId int, + actor *DeviceActor, hasPending func(ctx context.Context, accountID int64) bool, + pollEvery time.Duration) *LedgerMonitor { + if pollEvery <= 0 { + pollEvery = 1 * time.Second + } + return &LedgerMonitor{ + accountID: accountID, deviceID: deviceID, provider: provider, serial: serial, userId: userId, + actor: actor, hasPending: hasPending, pollEvery: pollEvery, + wake: make(chan struct{}, 1), stop: make(chan struct{}), done: make(chan struct{}), + warmupDone: make(chan struct{}), + } +} + +// Start 启动命令生产者(后台 goroutine,初始暂停态) +func (m *LedgerMonitor) Start() { go m.loop() } + +// Stop 停止命令生产者(等待退出) +func (m *LedgerMonitor) Stop() { + close(m.stop) + <-m.done +} + +// SetSchedulerMode 启用公平调度模式:monitor 只做预热,轮询由 DeviceScheduler 接管。 +// 必须在 Start() 之前调用。 +func (m *LedgerMonitor) SetSchedulerMode() { m.schedulerMode = true } + +// Activate 下单钩子:唤醒命令生产者(非阻塞;已在轮询则幂等)。 +// 唤醒后向 actor 周期投递 PollCmd,轮询持续到该账户待支付订单终结。 +// 公平调度模式下唤醒被忽略(轮询由 DeviceScheduler 统一决策)。 +func (m *LedgerMonitor) Activate() { + if m.schedulerMode { + return + } + select { + case m.wake <- struct{}{}: + default: // 已在唤醒队列/轮询中 + } +} + +// IsActive 是否处于轮询态(admin 状态展示) +func (m *LedgerMonitor) IsActive() bool { return m.monitorActive.Load() } + +// loop 订单驱动命令生产循环:预热 → 等待唤醒 → 有待支付订单才周期投递 PollCmd → +// 订单终结暂停。命令只入队不执行——执行在 DeviceActor 内串行完成(天然互斥)。 +func (m *LedgerMonitor) loop() { + defer close(m.done) + ctx := context.Background() + // 预热:投递 WarmupCmd 等待完成(设备上线即导航建基线,不依赖订单)。 + // actor 串行执行,多账户预热天然排队,无互相抢占。 + m.warmupOnce(ctx) + // 公平调度模式:轮询由 DeviceScheduler 统一决策(读内存池快照), + // monitor 预热完成后仅响应 stop(不投 PollCmd,避免双生产者)。 + if m.schedulerMode { + <-m.stop + return + } + for { + // 暂停态:等待下单唤醒 + select { + case <-m.stop: + return + case <-m.wake: + } + // 唤醒后确认有待支付订单;无(竞态:下单后立即终结)→ 直接回暂停 + if !m.hasPendingOrder(ctx) { + continue + } + // 轮询态:每次 poll 后检查订单,终结立即暂停(不空转) + m.monitorActive.Store(true) + for { + select { + case <-m.stop: + return + case <-time.After(m.pollEvery): + } + m.pollOnce(ctx) + if !m.hasPendingOrder(ctx) { + m.monitorActive.Store(false) + logger.Info("ledgermon", "账户待支付订单已终结,监听暂停", + "account_id", m.accountID, "device_id", m.deviceID) + break + } + } + } +} + +// warmupOnce 投递预热命令并等待完成。 +// 预热失败重试(带退避):设备上线预热建基线是必要动作,单次偶发失败(如 +// uiautomator dump 冷启动被 kill —— 真机实测 account3 一次 `signal: killed` +// 后整会话无基线,导致后续订单查询全失败)不应永久搁浅。此处是设备上线 +// 无实时回调的安全位置,冷启动重试不会干扰订单支付确认。 +// 预热命令在 actor 队列里与轮询/手动预热串行,天然无竞态。 +func (m *LedgerMonitor) warmupOnce(ctx context.Context) { + const maxRetries = 3 + var lastErr error + for attempt := 0; attempt <= maxRetries; attempt++ { + done := make(chan error, 1) + m.actor.Enqueue(&WarmupCmd{ + AccountID: m.accountID, + Provider: m.provider, + UserID: m.userId, + Done: done, + }) + lastErr = <-done // 等 actor 执行完 + if lastErr == nil { + break + } + if attempt < maxRetries { + logger.Warn("ledgermon", "预热失败,即将重试", + "account_id", m.accountID, "provider", m.provider, + "attempt", attempt+1, "err", lastErr) + select { + case <-ctx.Done(): + return + case <-time.After(warmupRetryDelay): + } + } + } + if lastErr != nil { + logger.Warn("ledgermon", "预热多次重试后仍失败", + "account_id", m.accountID, "provider", m.provider, "err", lastErr) + } + m.signalWarmupDone() +} + +// pollOnce 投递单轮轮询命令(不等待执行——actor 串行队列保证不重叠)。 +func (m *LedgerMonitor) pollOnce(ctx context.Context) { + m.actor.Enqueue(&PollCmd{AccountID: m.accountID, Provider: m.provider, UserID: m.userId}) +} + +// hasPendingOrder 该账户是否仍有待支付订单(未注入时视为有,保持轮询) +func (m *LedgerMonitor) hasPendingOrder(ctx context.Context) bool { + if m.hasPending == nil { + return true + } + return m.hasPending(ctx, m.accountID) +} + +// signalWarmupDone 标记预热完成(幂等:close 一次后再次调用无副作用)。 +func (m *LedgerMonitor) signalWarmupDone() { + select { + case <-m.warmupDone: + default: + close(m.warmupDone) + } +} + +// WarmupDone 返回预热完成信号(Manager 设备级编排用:全部账户就绪后统一锁定任务) +func (m *LedgerMonitor) WarmupDone() <-chan struct{} { return m.warmupDone } + +// ========== 编排管理器 ========== + +// MonitoredDevice 一台待监听设备(由 main.go 解析注入:设备 + 可服务账户 + adb serial) +type MonitoredDevice struct { + DeviceID string + Serial string + Provider string + AccountID int64 + UserID int // 0=主空间,999=双开 +} + +// DeviceResolver 设备解析回调(main.go 注入:从 personal.db 枚举在线设备 + 账户 + serial) +type DeviceResolver func(ctx context.Context) ([]MonitoredDevice, error) + +// ReaderFactory 按设备构造读取器(main.go 注入:封装 adb 管理器 + 通道分派) +type ReaderFactory func(d MonitoredDevice) LedgerReader + +// Manager 监听编排(DeviceActor 模型):按设备解析账户 → 创建 DeviceActor + +// 命令生产者(公平调度模式:DeviceScheduler;旧模式:LedgerMonitor)→ 状态查询。 +// 每设备一个 actor,串行执行全部 adb 操作。 +type Manager struct { + mu sync.Mutex + actors map[string]*DeviceActor // key: deviceID + monitors map[string]*LedgerMonitor // key: monitorKey(deviceID, provider, userID) + scheds map[string]*DeviceScheduler // key: deviceID(公平调度模式;pool 非 nil 时启用) + resolver DeviceResolver + factory ReaderFactory + dispatch DispatchFunc + adb *adbmgr.Manager + hasPending func(ctx context.Context, accountID int64) bool + pollEvery time.Duration + store SeenStore + // pool WAIT_PAY 内存订单池(非 nil → 公平调度模式:DeviceScheduler 接管轮询) + pool *pool.WaitPayPool + // lockArmed 每设备锁定协调是否已启动(map[deviceID]struct{},防重复启动) + lockArmed map[string]struct{} + // lockDone 每设备锁定是否已完成(map[deviceID]struct{},防重复执行) + lockDone map[string]struct{} + // warmupInFlight 每设备手动预热是否进行中(map[deviceID]bool,防重复点击堆积) + warmupInFlight map[string]bool +} + +// monitorKey 账户命令生产者唯一键:(deviceID, provider, userID)。 +// 同一设备的账本页实例 = 每 (平台, 用户空间) 一个(支付宝主空间/双开、微信主空间/双开各不同), +// 必须含 provider 维度——否则 alipay@user0 与 wechat@user0 会碰撞覆盖。 +func monitorKey(deviceID, provider string, userID int) string { + return fmt.Sprintf("%s|%s|%d", deviceID, provider, userID) +} + +// NewManager 创建编排器。store 可 nil(增量仅内存)。 +// hasPending 判断账户是否仍有待支付订单(main.go 注入;nil = 视为恒有,仅测试用)。 +// adb 用于 ManualWarmupCmd/LockCmd 直连 capture(可 nil:不启用设备级手动预热/锁定)。 +// waitPayPool 非 nil → 公平调度模式:DeviceScheduler 按内存池快照统一决策轮询(推荐); +// nil → 旧模式:LedgerMonitor 订单驱动轮询(测试/无池环境兼容)。 +func NewManager(adb *adbmgr.Manager, resolver DeviceResolver, factory ReaderFactory, + dispatch DispatchFunc, hasPending func(ctx context.Context, accountID int64) bool, + pollEvery time.Duration, store SeenStore, waitPayPool *pool.WaitPayPool) *Manager { + if pollEvery <= 0 { + pollEvery = 1 * time.Second + } + return &Manager{ + adb: adb, resolver: resolver, factory: factory, dispatch: dispatch, + hasPending: hasPending, pollEvery: pollEvery, store: store, + pool: waitPayPool, + actors: make(map[string]*DeviceActor), + monitors: make(map[string]*LedgerMonitor), + scheds: make(map[string]*DeviceScheduler), + lockArmed: make(map[string]struct{}), + lockDone: make(map[string]struct{}), + warmupInFlight: make(map[string]bool), + } +} + +// Start 启动编排:周期 reconcile(设备增删/下线自动收敛)。 +func (m *Manager) Start(ctx context.Context) { + go m.reconcileLoop(ctx) +} + +// reconcileLoop 周期扫描设备(30s):新增设备创建 actor/生产者,消失设备停止 +func (m *Manager) reconcileLoop(ctx context.Context) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + m.reconcileOnce(ctx) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + m.reconcileOnce(ctx) + } + } +} + +// reconcileOnce 按设备收敛:创建设备 DeviceActor(幂等),为每个账户创建命令生产者, +// 并向 actor 投递 WarmupCmd。设备消失 → 停止 actor 与全部生产者。 +func (m *Manager) reconcileOnce(ctx context.Context) { + if m.resolver == nil { + return + } + devs, err := m.resolver(ctx) + if err != nil { + logger.Warn("ledgermon", "设备解析失败", "err", err) + return + } + seen := make(map[string]bool) + m.mu.Lock() + defer m.mu.Unlock() + + // 按设备分组(消除 resolver 返回顺序依赖:任一账户有 reader 即建 actor,其余逐账户补齐) + byDevice := make(map[string][]MonitoredDevice) + for _, d := range devs { + byDevice[d.DeviceID] = append(byDevice[d.DeviceID], d) + } + for deviceID, list := range byDevice { + for _, d := range list { + seen[monitorKey(d.DeviceID, d.Provider, d.UserID)] = true + } + // 设备级 actor:每设备恰好一个(幂等创建)。任一账户有读取通道即建, + // 不再依赖"首个被枚举账户"的 provider 是否实现。 + actor := m.actors[deviceID] + if actor == nil { + var reader LedgerReader + for _, d := range list { + if m.factory == nil { + break + } + if reader = m.factory(d); reader != nil { + actor = NewDeviceActor(d.DeviceID, d.Serial, m.adb, m.dispatch) + actor.AddAccount(&AccountCtx{ + AccountID: d.AccountID, + Provider: d.Provider, + UserID: d.UserID, + Reader: reader, + Detector: NewDetector(d.DeviceID, d.AccountID, m.store), + }) + m.actors[d.DeviceID] = actor + logger.Info("ledgermon", "设备 DeviceActor 已创建(串行执行者)", + "device_id", d.DeviceID, "serial", d.Serial) + break + } + } + if actor == nil { + logger.Info("ledgermon", "设备全部读取通道未实现,跳过监听", + "device_id", deviceID, "accounts", len(list)) + continue + } + } + // 账户级命令生产者:每 (账户, 通道, 空间) 一个(幂等创建) + for _, d := range list { + key := monitorKey(d.DeviceID, d.Provider, d.UserID) + if _, ok := m.monitors[key]; ok { + continue + } + // 为 actor 补齐该账户的读取上下文 + if acct := actor.DeviceCtx.Account(d.Provider, d.UserID); acct == nil { + reader := m.factory(d) + if reader == nil { + continue + } + actor.AddAccount(&AccountCtx{ + AccountID: d.AccountID, + Provider: d.Provider, + UserID: d.UserID, + Reader: reader, + Detector: NewDetector(d.DeviceID, d.AccountID, m.store), + }) + } + mon := NewMonitor(d.AccountID, d.DeviceID, d.Provider, d.Serial, d.UserID, + actor, m.hasPending, m.pollEvery) + if m.pool != nil { + // 公平调度模式:monitor 只做预热,轮询由 DeviceScheduler 统一决策 + mon.SetSchedulerMode() + } + m.monitors[key] = mon + mon.Start() + logger.Info("ledgermon", "账户命令生产者已启动(初始暂停,等待下单唤醒)", + "device_id", d.DeviceID, "account_id", d.AccountID, "user_id", d.UserID, "provider", d.Provider) + m.armDeviceLock(d.DeviceID) + } + // 公平调度模式:每设备一个 DeviceScheduler(幂等创建),账户快照同步给调度器 + if m.pool != nil { + m.ensureScheduler(deviceID, actor) + } + } + // 移除消失设备(先停账户生产者,再停设备 actor) + for key, mon := range m.monitors { + if !seen[key] { + mon.Stop() + delete(m.monitors, key) + logger.Info("ledgermon", "账户命令生产者已停止(账户/设备消失)", "device_id", mon.deviceID) + } + } + // 公平调度模式:设备消失 → 停止该设备 scheduler + for deviceID, sched := range m.scheds { + if _, ok := byDevice[deviceID]; !ok { + sched.Stop() + delete(m.scheds, deviceID) + logger.Info("ledgermon", "设备 DeviceScheduler 已停止(设备消失)", "device_id", deviceID) + } + } + for deviceID, actor := range m.actors { + hasAccount := false + for key := range m.monitors { + if mon := m.monitors[key]; mon.deviceID == deviceID { + hasAccount = true + break + } + } + if !hasAccount { + actor.Stop() + delete(m.actors, deviceID) + delete(m.lockArmed, deviceID) + delete(m.lockDone, deviceID) + logger.Info("ledgermon", "设备 DeviceActor 已停止(设备消失)", "device_id", deviceID) + } + } +} + +// ensureScheduler 公平调度模式:每设备幂等创建 DeviceScheduler,并同步账户快照。 +// 账户快照从 actor 账户上下文提取(accountID → provider/userID),供调度器按池匹配。 +// 调用方持 m.mu 锁。 +func (m *Manager) ensureScheduler(deviceID string, actor *DeviceActor) { + if m.pool == nil { + return + } + sched := m.scheds[deviceID] + if sched == nil { + sched = NewDeviceScheduler(deviceID, actor, m.pool, DefaultSchedulerConfig()) + m.scheds[deviceID] = sched + sched.Start() + logger.Info("ledgermon", "设备 DeviceScheduler 已创建(公平时间占比调度)", + "device_id", deviceID) + } + sched.SetAccounts(schedulerAcctsOf(actor)) +} + +// schedulerAcctsOf 从 actor 账户快照提取调度器视图(accountID → provider/userID) +func schedulerAcctsOf(actor *DeviceActor) []schedulerAcct { + accts := actor.DeviceCtx.Accounts() + out := make([]schedulerAcct, 0, len(accts)) + for _, ac := range accts { + out = append(out, schedulerAcct{accountID: ac.AccountID, provider: ac.Provider, userID: ac.UserID}) + } + return out +} + +// armDeviceLock 为设备启动锁定协调(幂等:同设备只启动一次)。 +// 等设备全部账户 WarmupCmd 完成后,向 actor 投递 LockCmd(锁定最近任务,防一键清理)。 +// 锁定在 actor 队列内串行执行——天然不会与账户导航/轮询抢前台。 +func (m *Manager) armDeviceLock(deviceID string) { + if _, ok := m.lockDone[deviceID]; ok { + return // 本设备已锁定过,跳过 + } + if _, ok := m.lockArmed[deviceID]; ok { + return // 协调已启动,等待即可 + } + if m.adb == nil { + return // 无 adb(测试环境),锁定无意义 + } + m.lockArmed[deviceID] = struct{}{} + go func() { + defer func() { + if r := recover(); r != nil { + logger.Error("ledgermon", "设备加锁协程 panic(已恢复)", "device_id", deviceID, + "panic", r, "stack", string(debug.Stack())) + } + }() + m.waitAllWarmup(deviceID) + m.mu.Lock() + actor := m.actors[deviceID] + m.mu.Unlock() + if actor != nil { + actor.Enqueue(&LockCmd{}) + } + m.mu.Lock() + m.lockDone[deviceID] = struct{}{} + delete(m.lockArmed, deviceID) + m.mu.Unlock() + }() +} + +// waitAllWarmup 阻塞等待设备全部账户预热完成(任一账户预热失败 → 本轮不等待超时)。 +func (m *Manager) waitAllWarmup(deviceID string) { + deadline := time.Now().Add(3 * time.Minute) // 冷启动导航上限保护 + for time.Now().Before(deadline) { + m.mu.Lock() + var chans []<-chan struct{} + for _, mon := range m.monitors { + if mon.deviceID == deviceID { + chans = append(chans, mon.WarmupDone()) + } + } + m.mu.Unlock() + if len(chans) == 0 { + time.Sleep(2 * time.Second) + continue + } + allDone := true + for _, ch := range chans { + select { + case <-ch: + default: + allDone = false + } + } + if allDone { + return + } + time.Sleep(2 * time.Second) + } +} + +// ActivateAccount 下单钩子(main.go 调用):路由账户后唤醒对应命令生产者。 +// 公平调度模式:唤醒所属设备 scheduler(池空休眠时立即响应新订单); +// 旧模式:唤醒 LedgerMonitor。 +func (m *Manager) ActivateAccount(accountID int64) { + m.mu.Lock() + defer m.mu.Unlock() + for _, mon := range m.monitors { + if mon.accountID == accountID { + if sched := m.scheds[mon.deviceID]; sched != nil { + sched.Wake() + logger.Info("ledgermon", "下单触发设备调度器唤醒", + "account_id", accountID, "device_id", mon.deviceID) + return + } + mon.Activate() + logger.Info("ledgermon", "下单触发账户监听激活", + "account_id", accountID, "device_id", mon.deviceID) + return + } + } + logger.Debug("ledgermon", "账户无监听循环,忽略下单激活", "account_id", accountID) +} + +// WarmupDevice 手动预热(admin 触发):向设备 actor 投递 ManualWarmupCmd 并等待完成。 +// ManualWarmupCmd 是队列中的一个命令——与自动预热/轮询天然互斥,无特殊分支。 +// providers 指定通道({"alipay":true,"wechat":true}),空 = 全部。 +// 每设备单飞:已有手动预热在排队/执行时直接返回错误,防重复点击堆积(每个最长 240s)。 +func (m *Manager) WarmupDevice(ctx context.Context, deviceID string, providers map[string]bool) error { + m.mu.Lock() + actor := m.actors[deviceID] + if actor == nil { + m.mu.Unlock() + return fmt.Errorf("设备无 DeviceActor(设备离线或未配置监听)") + } + if m.warmupInFlight[deviceID] { + m.mu.Unlock() + return fmt.Errorf("设备手动预热进行中,请勿重复触发") + } + m.warmupInFlight[deviceID] = true + m.mu.Unlock() + + done := make(chan error, 1) + actor.Enqueue(&ManualWarmupCmd{Providers: providers, Done: done}) + var err error + select { + case err = <-done: + case <-ctx.Done(): + err = ctx.Err() + } + m.mu.Lock() + delete(m.warmupInFlight, deviceID) + m.mu.Unlock() + return err +} + +// StopAll 停止全部设备 actor 与账户生产者/调度器(服务端退出) +func (m *Manager) StopAll() { + m.mu.Lock() + defer m.mu.Unlock() + for _, mon := range m.monitors { + mon.Stop() + } + m.monitors = make(map[string]*LedgerMonitor) + for deviceID, sched := range m.scheds { + sched.Stop() + delete(m.scheds, deviceID) + } + for deviceID, actor := range m.actors { + actor.Stop() + delete(m.actors, deviceID) + } +} + +// DevicePollHealth 返回各设备 poll 成功率(deviceID → 0~1;无 actor 返回 0.9 中性)。 +// 均衡分配器健康度数据源(docs/PLAN-DYNAMIC-CLOSE-ALLOC.md §5.2)。 +func (m *Manager) DevicePollHealth() map[string]float64 { + m.mu.Lock() + defer m.mu.Unlock() + out := make(map[string]float64, len(m.actors)) + for devID, actor := range m.actors { + out[devID] = actor.PollHealth() + } + return out +} + +// MonitorState 单账户监听状态(admin 只读接口) +type MonitorState struct { + DeviceID string `json:"device_id"` + AccountID int64 `json:"account_id"` + Provider string `json:"provider"` + UserID int `json:"user_id"` + Serial string `json:"serial"` + Active bool `json:"active"` // 是否处于订单驱动轮询态(有待支付订单) + SeenRows int `json:"seen_rows"` // 已见指纹数(增量基线大小) + CurrentCmd string `json:"current_cmd"` // 设备 actor 当前执行命令(""=空闲) + QueueLen int `json:"queue_len"` // 设备 actor 等待队列长度 + LastCmd string `json:"last_cmd"` // 设备 actor 最近执行命令 + LastErr string `json:"last_err"` // 最近命令错误(""=成功,供诊断连续失败) +} + +// Status 返回全部账户监听状态(admin 只读接口) +func (m *Manager) Status() []MonitorState { + m.mu.Lock() + defer m.mu.Unlock() + out := make([]MonitorState, 0, len(m.monitors)) + for _, mon := range m.monitors { + actor := m.actors[mon.deviceID] + cur, qlen, lastCmd, lastErr := "", 0, "", "" + if actor != nil { + cur = actor.CurrentCmd() + qlen = actor.QueueLen() + if lc, lerr := actor.LastResult(); lerr != nil { + lastCmd, lastErr = lc, lerr.Error() + } + } + out = append(out, MonitorState{ + DeviceID: mon.deviceID, + AccountID: mon.accountID, + Provider: mon.provider, + UserID: mon.userId, + Serial: mon.serial, + Active: mon.IsActive(), + SeenRows: actorSeenRows(actor, mon.provider, mon.userId), + CurrentCmd: cur, + QueueLen: qlen, + LastCmd: lastCmd, + LastErr: lastErr, + }) + } + return out +} + +// actorSeenRows 从设备 actor 取账户已见指纹数(actor 为 nil 返回 0) +func actorSeenRows(actor *DeviceActor, provider string, userID int) int { + if actor == nil { + return 0 + } + return actor.SeenRows(provider, userID) +} diff --git a/internal/ledgermon/monitor_test.go b/internal/ledgermon/monitor_test.go new file mode 100644 index 0000000..45d5697 --- /dev/null +++ b/internal/ledgermon/monitor_test.go @@ -0,0 +1,278 @@ +package ledgermon + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "paygateway/internal/admin/capture" +) + +// fakeReader 测试用读取器:记录被调用的次数 +type fakeReader struct { + calls atomic.Int64 +} + +func (f *fakeReader) EnsureForeground(ctx context.Context) error { return nil } +func (f *fakeReader) Read(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + f.calls.Add(1) + return nil, nil // 无新行 +} + +// newTestActor 创建设备 actor 并注册测试账户(adb nil:测试只用 PollCmd/WarmupCmd) +func newTestActor(t *testing.T, reader LedgerReader) *DeviceActor { + t.Helper() + actor := NewDeviceActor("dev1", "s1", nil, nil) + actor.AddAccount(&AccountCtx{ + AccountID: 1, + Provider: "alipay", + UserID: 0, + Reader: reader, + Detector: NewDetector("dev1", 1, nil), + }) + t.Cleanup(actor.Stop) + return actor +} + +// TestMonitor_OrderDriven 订单驱动:无待支付订单 → 不投递 PollCmd(读取零次); +// 激活(有订单)→ 轮询;订单终结 → 自动暂停。 +func TestMonitor_OrderDriven(t *testing.T) { + pending := atomic.Bool{} + reader := &fakeReader{} + actor := newTestActor(t, reader) + mon := NewMonitor(1, "dev1", "alipay", "s1", 0, actor, + func(ctx context.Context, accountID int64) bool { return pending.Load() }, + 20*time.Millisecond) // 快间隔加速测试 + + mon.Start() + defer mon.Stop() + + // 无订单:预热 WarmupCmd 会读取一次建基线,之后不应再轮询(保持 1 次) + time.Sleep(80 * time.Millisecond) + if got := reader.calls.Load(); got != 1 { + t.Fatalf("无待支付订单时应仅预热读取 1 次(建基线),实际读取 %d 次", got) + } + if mon.IsActive() { + t.Fatal("无订单时应处于暂停态") + } + + // 下单激活:开始轮询(在预热 1 次基础上递增) + pending.Store(true) + mon.Activate() + deadline := time.Now().Add(2 * time.Second) + for reader.calls.Load() == 1 && time.Now().Before(deadline) { + time.Sleep(10 * time.Millisecond) + } + if got := reader.calls.Load(); got <= 1 { + t.Fatal("激活后应开始轮询(读取超过预热基线)") + } + if !mon.IsActive() { + t.Fatal("有订单时应处于轮询态") + } + + // 订单终结:应自动暂停(允许至多一轮已在途 poll 完成,之后不再增长) + pending.Store(false) + before := reader.calls.Load() + time.Sleep(150 * time.Millisecond) // 多个间隔窗口 + after := reader.calls.Load() + if after > before+1 { + t.Fatalf("订单终结后应暂停轮询(允许 ≤1 轮在途),之前 %d 次,之后 %d 次", before, after) + } + if mon.IsActive() { + t.Fatal("订单终结后应回到暂停态") + } +} + +// TestMonitor_ActivateIdempotent 重复激活幂等:已在轮询时再次激活不产生额外循环 +func TestMonitor_ActivateIdempotent(t *testing.T) { + pending := atomic.Bool{} + pending.Store(true) + reader := &fakeReader{} + actor := newTestActor(t, reader) + mon := NewMonitor(1, "dev1", "alipay", "s1", 0, actor, + func(ctx context.Context, accountID int64) bool { return pending.Load() }, + 10*time.Millisecond) + + mon.Start() + defer mon.Stop() + mon.Activate() + mon.Activate() // 重复激活 + time.Sleep(60 * time.Millisecond) + if !mon.IsActive() { + t.Fatal("激活后应处于轮询态") + } +} + +// TestMonitor_WakeThenPauseThenWake 暂停后可再次被唤醒(新订单) +func TestMonitor_WakeThenPauseThenWake(t *testing.T) { + pending := atomic.Bool{} + reader := &fakeReader{} + actor := newTestActor(t, reader) + mon := NewMonitor(1, "dev1", "alipay", "s1", 0, actor, + func(ctx context.Context, accountID int64) bool { return pending.Load() }, + 20*time.Millisecond) + + mon.Start() + defer mon.Stop() + + // 第一轮:激活 → 轮询 → 暂停 + pending.Store(true) + mon.Activate() + time.Sleep(60 * time.Millisecond) + pending.Store(false) + time.Sleep(60 * time.Millisecond) // 等自动暂停 + + // 第二轮:再次激活(新订单) + before := reader.calls.Load() + pending.Store(true) + mon.Activate() + deadline := time.Now().Add(2 * time.Second) + for reader.calls.Load() <= before && time.Now().Before(deadline) { + time.Sleep(10 * time.Millisecond) + } + if reader.calls.Load() <= before { + t.Fatal("暂停后应可再次被唤醒轮询") + } +} + +// TestDeviceActor_PollDedup PollCmd 同账户去重:并发投递多条同账户 PollCmd 不堆积 +// (去重只防排队堆积:正在执行的 poll 被 pop 后同账户可再入队,但队列不会因风暴膨胀)。 +func TestDeviceActor_PollDedup(t *testing.T) { + reader := &fakeReader{} + actor := newTestActor(t, reader) + for i := 0; i < 10; i++ { + actor.Enqueue(&PollCmd{AccountID: 1, Provider: "alipay", UserID: 0}) + } + // 等待全部执行完成 + deadline := time.Now().Add(2 * time.Second) + for { + qlen := actor.QueueLen() + cur := actor.CurrentCmd() + if qlen == 0 && cur == "" { + break + } + if time.Now().After(deadline) { + t.Fatalf("命令未在期限内执行完: queue=%d current=%q", qlen, cur) + } + time.Sleep(5 * time.Millisecond) + } + if got := reader.calls.Load(); got > 10 { + t.Fatalf("去重后执行次数不应超过投递次数,实际 %d 次", got) + } + if actor.QueueLen() != 0 { + t.Fatal("全部执行完后队列应为空") + } +} + +// TestDeviceActor_SerialExecution 串行执行:WarmupCmd 完成后 LockCmd 才执行 +// (命令之间无并发,天然原子)。 +func TestDeviceActor_SerialExecution(t *testing.T) { + reader := &fakeReader{} + actor := newTestActor(t, reader) + warmupDone := make(chan error, 1) + actor.Enqueue(&WarmupCmd{AccountID: 1, Provider: "alipay", UserID: 0, Done: warmupDone}) + if err := <-warmupDone; err != nil { + t.Fatalf("WarmupCmd 执行失败: %v", err) + } + if got := reader.calls.Load(); got != 1 { + t.Fatalf("WarmupCmd 应执行 1 次读取(建基线),实际 %d 次", got) + } + if actor.CurrentCmd() != "" { + t.Fatal("命令完成后 actor 应回到空闲态") + } +} + +// TestDeviceActor_DispatchFailureNoLoss 资金安全:Dispatch 失败时行不被提交 seen, +// 下轮重新检测并重试——绝不永久丢单。 +func TestDeviceActor_DispatchFailureNoLoss(t *testing.T) { + // 自定义行构造器:Fingerprint 为时间|金额 + row := func(ts, amt string) capture.AlipayLedgerRow { + return capture.AlipayLedgerRow{Time: ts, Amount: amt} + } + reader := &seqReader{rows: [][]capture.AlipayLedgerRow{ + {row("10:00:01", "1.00")}, // 首次:建基线 + {row("10:00:02", "2.00")}, // 第二次:新行,Dispatch 失败 + {row("10:00:02", "2.00")}, // 第三次:应再次检测到并成功推送 + }} + dispatchCalls := atomic.Int64{} + dispatchErr := atomic.Bool{} + dispatchErr.Store(true) // 先失败 + actor := NewDeviceActor("dev3", "s3", nil, func(ctx context.Context, deviceID string, provider string, space int, rows []capture.AlipayLedgerRow) error { + dispatchCalls.Add(1) + if dispatchErr.Load() { + return fmt.Errorf("模拟推送失败") + } + return nil + }) + acct := &AccountCtx{ + AccountID: 1, Provider: "alipay", UserID: 0, + Reader: reader, + Detector: NewDetector("dev3", 1, nil), + } + actor.AddAccount(acct) + defer actor.Stop() + + // 第一次 poll:建基线,无推送 + actor.Enqueue(&PollCmd{AccountID: 1, Provider: "alipay", UserID: 0}) + waitDone(t, actor, 1) + if dispatchCalls.Load() != 0 { + t.Fatalf("首次建基线不应推送,实际推送 %d 次", dispatchCalls.Load()) + } + // 第二次 poll:新行,Dispatch 失败 → 不提交 + actor.Enqueue(&PollCmd{AccountID: 1, Provider: "alipay", UserID: 0}) + waitDone(t, actor, 2) + if dispatchCalls.Load() != 1 { + t.Fatalf("第二次应尝试推送 1 次,实际 %d 次", dispatchCalls.Load()) + } + // 第三次 poll:应重新检测到同一行并推送成功(不丢单) + dispatchErr.Store(false) + actor.Enqueue(&PollCmd{AccountID: 1, Provider: "alipay", UserID: 0}) + waitDone(t, actor, 3) + if dispatchCalls.Load() != 2 { + t.Fatalf("Dispatch 失败后下轮应重试推送(共 2 次),实际 %d 次", dispatchCalls.Load()) + } + // 第四次 poll:已提交,不再推送 + reader.append(nil) + actor.Enqueue(&PollCmd{AccountID: 1, Provider: "alipay", UserID: 0}) + waitDone(t, actor, 4) + if dispatchCalls.Load() != 2 { + t.Fatalf("提交后不应重复推送,实际推送 %d 次", dispatchCalls.Load()) + } +} + +// seqReader 按调用次序返回预置行序列(append 与 Read 可能并发,mutex 保护) +type seqReader struct { + mu sync.Mutex + rows [][]capture.AlipayLedgerRow + idx int +} + +func (r *seqReader) EnsureForeground(ctx context.Context) error { return nil } +func (r *seqReader) Read(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.idx >= len(r.rows) { + return nil, nil + } + rows := r.rows[r.idx] + r.idx++ + return rows, nil +} +func (r *seqReader) append(rows []capture.AlipayLedgerRow) { + r.mu.Lock() + defer r.mu.Unlock() + r.rows = append(r.rows, rows) +} + +// waitDone 等待 actor 处理完第 n 次入队 +func waitDone(t *testing.T, actor *DeviceActor, n int) { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for actor.QueueLen() > 0 && time.Now().Before(deadline) { + time.Sleep(5 * time.Millisecond) + } + time.Sleep(20 * time.Millisecond) // 等当前命令执行完成 +} diff --git a/internal/ledgermon/reader_alipay.go b/internal/ledgermon/reader_alipay.go new file mode 100644 index 0000000..0200b11 --- /dev/null +++ b/internal/ledgermon/reader_alipay.go @@ -0,0 +1,233 @@ +package ledgermon + +import ( + "context" + "fmt" + "hash/fnv" + "strings" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/admin/capture" +) + +// 支付宝常量(与 capture 包一致;独立声明避免导出 capture 内部常量) +const ( + alipayPkg = "com.eg.android.AlipayGphone" + alipayLaunchAct = ".AlipayLogin" + alipayLedgerAct = "XRiverActivity$App" // 稳定的完整收款记录页(2026-08-27 真机实测) + alipayLedgerActStr = "XRiverActivity$App" +) + +// LedgerReader 账本页读取器接口(按通道分派) +type LedgerReader interface { + // EnsureForeground 确保账本页在前台(免点击恢复,失败返回错误 → 本轮跳过)。 + // 下单实时回调路径:只走热启动(task lock),绝不冷启动。 + EnsureForeground(ctx context.Context) error + // Read 读取当前前台账本页可见行。页面无变化返回 (nil, nil)(变化检测跳过解析)。 + Read(ctx context.Context) ([]capture.AlipayLedgerRow, error) +} + +// Warmer 可选接口:预热专用导航(task lock 热启动优先 → 失败走冷启动坐标导航建账本页)。 +// 预热阶段(warmupOnce)调用——账本页任务栈不存在时必须用冷启动建立; +// 实时回调(pollOnce)只用 EnsureForeground(纯热启动,绝不冷启动)。 +type Warmer interface { + Warmup(ctx context.Context) error +} + +// FreshReader 可选接口:实现方支持免下拉刷新读取。冷启动导航刚完成时账本页 +// 就是最新数据,无需下拉刷新(省 1.5s 且避免刷新动画扰动)。 +type FreshReader interface { + ReadFresh(ctx context.Context) ([]capture.AlipayLedgerRow, error) +} + +// TaskLocker 可选接口:实现方支持锁定最近任务(防一键清理移除任务栈, +// 保证热启动能恢复账本页,避免重复冷启动导航)。无此能力时预热跳过锁定。 +type TaskLocker interface { + LockTasks(ctx context.Context) (int, error) +} + +// AlipayReader 支付宝账本页 dump 文本通道读取器。 +// +// 链路:uiautomator dump(系统级通道,不受 FLAG_SECURE 限制,实测可读出账本行) +// → 复用 capture.ReadAlipayLedger 解析(付款方|HH:MM:SS|+金额)。 +// 变化检测:dump XML 文本哈希对比上一帧(页面无变化 → 不解析直接返回空)。 +// +// 关键:账本页是 WebView 容器(NebulaX),数据不自动刷新——新收款不会自动出现在 +// 页面里,必须下拉刷新(pull-to-refresh)触发数据重载后才能 dump 到新行。 +type AlipayReader struct { + adb *adbmgr.Manager + serial string + userId int // 0=主空间,999=双开分身 + lastDumpHash uint64 +} + +// 下拉刷新手势参数(实测账本页 WebView 需下拉才能重载数据): +// 从列表区中部 (540,600) 向下滑到 (540,1700),触发 pull-to-refresh。 +const ( + refreshSwipeX = 540 + refreshSwipeY0 = 600 + refreshSwipeY1 = 1700 + refreshSwipeMs = 400 + refreshLoadWait = 1500 * time.Millisecond // 刷新动画 + 数据重载等待 +) + +// NewAlipayReader 创建支付宝读取器。userId>0 时以该 user 空间启动(双开)。 +func NewAlipayReader(adb *adbmgr.Manager, serial string, userId int) *AlipayReader { + return &AlipayReader{adb: adb, serial: serial, userId: userId} +} + +// refresh 下拉刷新账本页(WebView 数据重载)。失败不致命:下一轮重试。 +func (r *AlipayReader) refresh(ctx context.Context) error { + err := r.adb.SwipeRef(ctx, r.serial, refreshSwipeX, refreshSwipeY0, refreshSwipeX, refreshSwipeY1, refreshSwipeMs) + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(refreshLoadWait): + } + return nil +} + +// EnsureForeground 确保支付宝完整收款记录页(XRiverTransActivity)在前台。 +// +// 下单实时回调路径(2026-08-28 重构):只走热启动,绝不冷启动。 +// 1. 前台检测(dumpsys 三要素:包名+Activity+用户空间,~100ms):已在账本页 → 零开销返回 +// 2. 热启动(~260ms):am task lock 直接唤醒 lite01 账本页任务栈到前台 +// ——am start .AlipayLogin 只恢复主任务栈(栈顶首页/总览),稳定账本页 +// XRiverActivity$App 在 lite01 亲和栈,am start 永远到不了它。task lock 按 +// taskId 精确唤醒账本页栈,与微信 taskLock(wechatTaskID) 同机制。 +// 3. 进程被杀/任务栈丢失/唤醒失败 → 返回错误本轮跳过(不读错页面)。 +// 冷启动导航(CaptureAlipayLedger)只在预热阶段执行(设备上线 warmupOnce), +// 绝不出现在下单实时回调——17.7s 冷启动会错过付款窗口。 +func (r *AlipayReader) EnsureForeground(ctx context.Context) error { + // ① 已在账本页(三要素确认,防双开/异应用误判) + if r.isLedgerForeground(ctx) { + return nil + } + // ② 进程被杀 → 任务栈已丢,task lock 无栈可唤醒,本轮跳过(不冷启动) + if alive, _ := r.adb.ProcessAlive(ctx, r.serial, alipayPkg); !alive { + return fmt.Errorf("支付宝进程不存在,热启动无法恢复(需重新预热)") + } + // ③ 热启动:am task lock 唤醒账本页任务栈(微信同款,~260ms) + taskID, terr := r.adb.FindLedgerTaskID(ctx, r.serial, alipayPkg, alipayLedgerActStr, r.userId) + if terr != nil { + return fmt.Errorf("账本页任务栈未找到,热启动无法恢复(需重新预热): %w", terr) + } + if lerr := r.adb.TaskLock(ctx, r.serial, taskID); lerr != nil { + return fmt.Errorf("task lock 唤醒账本页任务栈失败: %w", lerr) + } + // 唤醒成功后 PINNED 锁定前台,确认到账本页立即释放(否则拦截后续下拉刷新) + defer func() { _ = r.adb.UnpinTask(ctx, r.serial) }() + if !r.waitLedgerForeground(ctx, 2*time.Second) { + return fmt.Errorf("task lock 唤醒后未确认账本页前台") + } + return nil +} + +// Warmup 预热专用导航(Warmer 接口):task lock 热启动优先,账本页任务栈不存在 +// (进程被杀/首次启动/被清理)时走冷启动坐标导航建账本页。仅预热阶段调用。 +func (r *AlipayReader) Warmup(ctx context.Context) error { + // 已在账本页 → 零开销 + if r.isLedgerForeground(ctx) { + return nil + } + // task lock 热启动优先 + if taskID, terr := r.adb.FindLedgerTaskID(ctx, r.serial, alipayPkg, alipayLedgerActStr, r.userId); terr == nil { + if lerr := r.adb.TaskLock(ctx, r.serial, taskID); lerr == nil { + if r.waitLedgerForeground(ctx, 2*time.Second) { + _ = r.adb.UnpinTask(ctx, r.serial) + return nil + } + _ = r.adb.UnpinTask(ctx, r.serial) + } + } + // 冷启动坐标导航建账本页(仅预热阶段,绝不用于实时回调) + return capture.CaptureAlipayLedger(ctx, r.adb, r.serial, r.userId) +} + +// isLedgerOverview 前台是否为收款总览界面(XRiverActivity,非稳定页)。 +// 热启动恢复到主任务栈栈顶时是总览界面(稳定页在 lite01 栈),需据此补点进入稳定页。 +// 注意:ForegroundAppDetail 返回的活动名带 dumpsys ActivityRecord 的尾部 '}'(如 +// "...XRiverActivity}"),不能精确 == 匹配,必须子串匹配;同时排除稳定页 "$App" 后缀。 +func (r *AlipayReader) isLedgerOverview(ctx context.Context) bool { + pkg, act, uid, err := r.adb.ForegroundAppDetail(ctx, r.serial) + return err == nil && pkg == alipayPkg && + strings.Contains(act, "XRiverActivity") && + !strings.Contains(act, alipayLedgerActStr) && uid == r.userId +} + +// isLedgerForeground 前台是否已是本账户账本页(包名 + Activity + 用户空间三要素全匹配)。 +// 双开(user999)与主空间(user0)的账本页 Activity 同名,必须按 userID 区分, +// 否则 user0 的监听循环会把 user999 的账本页误判为已就绪 → 下拉刷新读到的全是错误账户数据。 +func (r *AlipayReader) isLedgerForeground(ctx context.Context) bool { + pkg, act, uid, err := r.adb.ForegroundAppDetail(ctx, r.serial) + return err == nil && pkg == alipayPkg && + strings.Contains(act, alipayLedgerActStr) && uid == r.userId +} + +// waitLedgerForeground 轮询等待账本页出现在前台(最长 timeout) +func (r *AlipayReader) waitLedgerForeground(ctx context.Context, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if r.isLedgerForeground(ctx) { + return true + } + select { + case <-ctx.Done(): + return false + case <-time.After(800 * time.Millisecond): + } + } + return false +} + +// Read dump 当前账本页并解析可见行(带下拉刷新:WebView 数据不自动更新, +// 新收款必须触发重载后才能被 dump 读到)。页面无变化 → 返回 (nil, nil)。 +func (r *AlipayReader) Read(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + if err := r.refresh(ctx); err != nil { + return nil, err + } + return r.readDump(ctx) +} + +// ReadFresh 免下拉刷新读取(FreshReader 接口实现)。冷启动导航刚完成时账本页 +// 就是最新数据,下拉刷新是多余开销(省 1.5s 且避免刷新动画扰动页面判定)。 +func (r *AlipayReader) ReadFresh(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + return r.readDump(ctx) +} + +// readDump dump 当前账本页并解析可见行,带变化检测。 +func (r *AlipayReader) readDump(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + xml, err := r.adb.DumpUI(ctx, r.serial) + if err != nil { + return nil, err + } + h := fnv.New64a() + h.Write([]byte(xml)) + sum := h.Sum64() + if sum == r.lastDumpHash { + return nil, nil // 页面无变化 + } + r.lastDumpHash = sum + rows, rerr := capture.ReadAlipayLedger(xml) + if rerr != nil { + return nil, rerr + } + // 资金安全:退化空 dump(无障碍未填充→空树)误判为'0 行'→建空基线→真实历史行 + // 后续被当 fresh 错误入账。退化空 dump 返回错误,跳过本轮不建基线(同微信 readDump)。 + if len(rows) == 0 && capture.DegenerateDump(xml) { + return nil, fmt.Errorf("账本页 dump 为空/未渲染(可能无障碍被回收或页面未就绪),拒绝建立空基线") + } + return rows, nil +} + +// LockTasks 锁定最近任务中支付应用主任务卡片(TaskLocker 接口实现)。 +// 锁定后一键清理不会移除账本页任务栈,热启动可恢复账本页(避免重复冷启动导航)。 +// 每应用只锁一张主任务栈(锁多个同应用进程会触发 MIUI 保护性杀进程)。 +// 锁定完成后退回最近任务界面,不调用账户级导航(避免误触发冷启动跳转)。 +func (r *AlipayReader) LockTasks(ctx context.Context) (int, error) { + return capture.LockPaymentTasks(ctx, r.adb, r.serial, "支付宝", "微信") +} diff --git a/internal/ledgermon/reader_wechat.go b/internal/ledgermon/reader_wechat.go new file mode 100644 index 0000000..754777e --- /dev/null +++ b/internal/ledgermon/reader_wechat.go @@ -0,0 +1,264 @@ +package ledgermon + +import ( + "context" + "fmt" + "hash/fnv" + "strings" + "time" + + "paygateway/internal/adbmgr" + "paygateway/internal/admin/capture" + "paygateway/internal/logger" +) + +// 微信常量(与 capture 包独立声明) +const ( + wechatPkg = "com.tencent.mm" + wechatLedgerAct = "AppBrandUI00" // 收款小账本小程序容器(PLAN-ADB-LEDGER.md §2.2 实测) +) + +// WechatReader 微信收款记录页(AppBrandUI00 小程序容器)读取器。 +// +// 与支付宝同逻辑(2026-08-28 实现): +// - 热启动:am task lock 唤醒账本页任务栈(微信 am start 直拉小程序被校验拒绝, +// task lock 是唯一放行路径,task id 动态需 dumpsys 解析——PLAN-ADB-LEDGER.md §2.3) +// - 冷启动:capture.CaptureWechatLedger 坐标导航(微信首页→我→服务→收付款→ +// 二维码收款→收款小账本→收款记录) +// - 任务卡片锁定:复用 capture.LockPaymentTasks("微信") +// - 读取:OCR 通道(Phase 2 接入);当前 EnsureForeground 保前台 + Read 占位空读 +type WechatReader struct { + adb *adbmgr.Manager + serial string + userId int // 0=主空间,999=双开分身 + lastDumpHash uint64 +} + +// NewWechatReader 创建微信读取器。userId>0 时以该 user 空间启动(双开)。 +func NewWechatReader(adb *adbmgr.Manager, serial string, userId int) *WechatReader { + return &WechatReader{adb: adb, serial: serial, userId: userId} +} + +// EnsureForeground 确保微信收款记录页(AppBrandUI00)在前台。 +// 下单实时回调路径:只走热启动(task lock),绝不冷启动(与支付宝 EnsureForeground 同策略)。 +// 冷启动导航(CaptureWechatLedger)只在预热阶段执行。 +func (r *WechatReader) EnsureForeground(ctx context.Context) error { + // ① 已在账本页(三要素确认) + if r.isLedgerForeground(ctx) { + return nil + } + // ② 进程被杀 → 任务栈已丢,task lock 无栈可唤醒,本轮跳过(不冷启动) + if alive, _ := r.adb.ProcessAlive(ctx, r.serial, wechatPkg); !alive { + return fmt.Errorf("微信进程不存在,热启动无法恢复(需重新预热)") + } + // ③ 热启动:am task lock 唤醒账本页任务栈(task id 动态解析) + taskID, terr := r.adb.FindLedgerTaskID(ctx, r.serial, wechatPkg, wechatLedgerAct, r.userId) + if terr != nil { + return fmt.Errorf("微信账本页任务栈未找到,热启动无法恢复(需重新预热): %w", terr) + } + if lerr := r.adb.TaskLock(ctx, r.serial, taskID); lerr != nil { + return fmt.Errorf("task lock 唤醒微信账本页任务栈失败: %w", lerr) + } + // 唤醒成功后 PINNED 锁定前台,确认到账本页立即释放(否则拦截后续读取) + defer func() { _ = r.adb.UnpinTask(ctx, r.serial) }() + if !r.waitLedgerForeground(ctx, 2*time.Second) { + return fmt.Errorf("task lock 唤醒后未确认微信账本页前台") + } + return nil +} + +// Warmup 预热专用导航(Warmer 接口):task lock 热启动优先,微信账本页任务栈 +// (AppBrandUI00)不存在(进程被杀/首次启动/被清理)时走冷启动坐标导航建账本页。 +// 与支付宝 Warmup 同逻辑。仅预热阶段调用。 +func (r *WechatReader) Warmup(ctx context.Context) error { + // 已在账本页 → 零开销 + if r.isLedgerForeground(ctx) { + return nil + } + // task lock 热启动优先 + if taskID, terr := r.adb.FindLedgerTaskID(ctx, r.serial, wechatPkg, wechatLedgerAct, r.userId); terr == nil { + if lerr := r.adb.TaskLock(ctx, r.serial, taskID); lerr == nil { + if r.waitLedgerForeground(ctx, 2*time.Second) { + _ = r.adb.UnpinTask(ctx, r.serial) + return nil + } + _ = r.adb.UnpinTask(ctx, r.serial) + } + } + // 冷启动坐标导航建账本页(仅预热阶段,绝不用于实时回调) + return capture.CaptureWechatLedger(ctx, r.adb, r.serial, r.userId) +} + +// Read 读取当前前台微信账本页可见行。 +// 链路:下拉刷新重载数据 → ensureAccessibility 确保绑定 → uiautomator dump → 解析。 +// 页面无变化返回 (nil, nil)。 +func (r *WechatReader) Read(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + if err := r.refresh(ctx); err != nil { + return nil, err + } + return r.readDump(ctx) +} + +// ReadFresh 免刷新读取(冷启动导航刚完成即最新数据)。 +// 与 Read 同路径(readDump 内 ensureAccessibility 紧贴 dump 前瞬态重绑)。 +func (r *WechatReader) ReadFresh(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + return r.readDump(ctx) +} + +// refresh 下拉刷新微信账本页重载数据(小程序 WebView 收款记录需刷新才见最新行)。 +// 刷新只操作页面,不触碰无障碍——绑定由 readDump 内 ensureAccessibility 在 dump +// 前紧贴重建(2026-09-13 真机实证:刷新→等3s→激活→立即 dump 成功读出账单行)。 +func (r *WechatReader) refresh(ctx context.Context) error { + if err := r.adb.SwipeRef(ctx, r.serial, refreshSwipeX, refreshSwipeY0, refreshSwipeX, refreshSwipeY1, refreshSwipeMs); err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(refreshLoadWait): + } + return nil +} + +// readDump 确保无障碍绑定并 dump 当前微信账本页,解析可见行,带变化检测。 +// +// 关键时序(红米 K60 真机实证):MIUI MiuiBooster 即使豁免也会在数秒内回收 +// 无障碍服务绑定(Bound 非空→空)。因此绑定必须【距 DumpUI 最近】—— +// ensureAccessibility 紧贴 DumpUI 调用;下拉刷新等页面操作排在它之前, +// 刷新结束到 dump 之间不夹任何长动作,dump 即落在绑定窗口内 +// (实证:刷新→等3s→激活→立即 dump 成功读出账单行;激活→滚动→惰待 dump 则空)。 +func (r *WechatReader) readDump(ctx context.Context) ([]capture.AlipayLedgerRow, error) { + // 紧贴 dump 前瞬态重绑 + 确认绑定(绑定被回收则重新激活,穷尽绑定窗口) + if err := r.ensureAccessibility(ctx); err != nil { + return nil, err + } + xml, err := r.adb.DumpUI(ctx, r.serial) + if err != nil { + return nil, err + } + h := fnv.New64a() + h.Write([]byte(xml)) + sum := h.Sum64() + if sum == r.lastDumpHash { + return nil, nil // 页面无变化 + } + r.lastDumpHash = sum + rows, rerr := capture.ReadWechatLedger(xml) + if rerr != nil { + return nil, rerr + } + // 资金安全:退化空 dump(无障碍未填充→空树)会被误判为'0 行'→建立空基线, + // 后续真实历史行变 fresh 错误入账。退化空 dump 返回错误,跳过本轮不建基线。 + if len(rows) == 0 && capture.DegenerateDump(xml) { + return nil, fmt.Errorf("账本页 dump 为空/未渲染(可能无障碍被回收或页面未就绪),拒绝建立空基线") + } + return rows, nil +} + +// ensureAccessibility 确保系统无障碍已开启(微信 XWeb 小程序 dump 的前置条件)。 +// +// 三项必要步骤(2026-09-11 真机平板实测根因,红米 K60 亦适用): +// 1. MIUI 省电豁免:MiuiBooster 省电守护会周期性回收 com.miui.accessibility(VoiceAccess) +// 服务进程,随后 BatterySaverPolicy 把 accessibility_enabled 清回 0(实测 ~45-50s 落回)。 +// 必须在启用前先给无障碍包授予后台运行豁免(appops RUN_IN_BACKGROUND allow + 白名单), +// 否则即便 enabled=1 也会被周期性清零。豁免幂等。 +// 2. 写有效服务:enabled_accessibility_services 必须写入**真实安装**的组件串。若沿用 +// 已被污染的旧值(系统常清成 null),再次写回会导致 AccessibilityManager 无法绑定 → +// 连带清空 enabled。此处固定用设备实测可绑定的 MIUI VoiceAccess。 +// 3. settings put secure accessibility_enabled 1 开启。 +func (r *WechatReader) ensureAccessibility(ctx context.Context) error { + if err := r.miuiBoostExempt(ctx); err != nil { + logger.Warn("ledgermon", "MIUI 省电豁免失败(无障碍仍可能被周期性清零)", + "device", r.serial, "err", err) + } + // 已真正绑定则直接复用(幂等)。不能只看 accessibility_enabled==1 提前返回—— + // 系统可能 enabled=1 但服务未绑定(污染值/MiuiBooster 清服务),此时 dump 仍退化。 + if r.accessibilityBound(ctx) { + return nil + } + // 固定写设备实测可绑定的 MIUI VoiceAccess(不用旧值,防污染值连带清空)。 + // 必须用【全路径】组件串:简写 ".voiceaccess.VoiceAccessAccessibilityService" + // 实测 AccessibilityManager 无法绑定(Bound/Enabled services 空),服务形同虚设。 + svc := "com.miui.accessibility/com.miui.accessibility.voiceaccess.VoiceAccessAccessibilityService" + if _, err := r.adb.Shell(ctx, 10*time.Second, r.serial, "settings put secure enabled_accessibility_services "+svc); err != nil { + logger.Warn("ledgermon", "设置无障碍服务失败", "device", r.serial, "err", err) + } + if _, err := r.adb.Shell(ctx, 10*time.Second, r.serial, "settings put secure accessibility_enabled 1"); err != nil { + return fmt.Errorf("开启无障碍失败: %w", err) + } + // 等系统激活并绑定无障碍服务(XWeb 节点树构建需要时间;MiuiBooster 窗口内)。 + // 轮询确认已真正绑定(Bound services 非空)——enabled=1 但未绑定时 dump 仍退化空。 + deadline := time.Now().Add(6 * time.Second) + for time.Now().Before(deadline) { + if r.accessibilityBound(ctx) { + logger.Info("ledgermon", "已开启系统无障碍(微信 dump 解锁)", "device", r.serial) + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(400 * time.Millisecond): + } + } + return fmt.Errorf("系统无障碍服务未能绑定(enabled=1 但 Bound services 空),微信 dump 将退化") +} + +// accessibilityBound 查询无障碍服务是否真正绑定(dumpsys accessibility Bound services 非空)。 +// enabled=1 只表示开关打开,必须服务进程挂上(Bound)才暴露 XWeb 节点树。 +func (r *WechatReader) accessibilityBound(ctx context.Context) bool { + out, err := r.adb.Shell(ctx, 6*time.Second, r.serial, "dumpsys accessibility") + if err != nil { + return false + } + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + // Bound services:{Service[label=语音控制,...]} —— 有实际服务实例即视为已绑定 + if strings.HasPrefix(line, "Bound services:") && strings.Contains(line, "Service[") { + return true + } + } + return false +} + +// miuiBoostExempt MIUI 省电/性能守护豁免:让无障碍服务包不被周期性回收。 +// 实测(红米 K60/平板):不打这个豁免,com.miui.accessibility 约 45-50s 被 +// MiuiBooster 回收 → accessibility_enabled 被清 0 → 微信 dump 回到空节点树。 +// 幂等:重复执行无害。豁免成功后 enabled 实测稳定保持 120s+。 +func (r *WechatReader) miuiBoostExempt(ctx context.Context) error { + const pkg = "com.miui.accessibility" + // appops 后台运行豁免(MiuiBooster 依赖 RUN_IN_BACKGROUND 判定回收) + if _, err := r.adb.Shell(ctx, 10*time.Second, r.serial, "cmd appops set "+pkg+" RUN_IN_BACKGROUND allow"); err != nil { + return err + } + // Doze 白名单(可选,防深度空闲回收) + _, _ = r.adb.Shell(ctx, 10*time.Second, r.serial, "dumpsys deviceidle whitelist +"+pkg) + return nil +} + +// LockTasks 锁定微信任务卡片(防一键清理移除任务栈,保证热启动可恢复)。 +func (r *WechatReader) LockTasks(ctx context.Context) (int, error) { + return capture.LockPaymentTasks(ctx, r.adb, r.serial, "微信") +} + +// isLedgerForeground 前台是否为微信收款记录页(AppBrandUI00 小程序容器 + 用户空间)。 +func (r *WechatReader) isLedgerForeground(ctx context.Context) bool { + pkg, act, uid, err := r.adb.ForegroundAppDetail(ctx, r.serial) + return err == nil && pkg == wechatPkg && + strings.Contains(act, wechatLedgerAct) && uid == r.userId +} + +// waitLedgerForeground 轮询等待账本页出现在前台(最长 timeout) +func (r *WechatReader) waitLedgerForeground(ctx context.Context, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if r.isLedgerForeground(ctx) { + return true + } + select { + case <-ctx.Done(): + return false + case <-time.After(800 * time.Millisecond): + } + } + return false +} diff --git a/internal/ledgermon/scheduler.go b/internal/ledgermon/scheduler.go new file mode 100644 index 0000000..61ae4eb --- /dev/null +++ b/internal/ledgermon/scheduler.go @@ -0,0 +1,423 @@ +// DeviceScheduler 设备级公平时间占比弹性调度器(docs/DESIGN-FAIR-SCHEDULER.md) +// +// 职责:每设备恰好一个调度 goroutine,是唯一 PollCmd 生产者;DeviceActor 保持 +// 被动串行执行者(命令队列不变)。调度输入为 WaitPayPool 内存订单池快照(零锁)。 +// +// 核心语义(2026-08-29 与用户确认): +// - 记账单位 = 一次完整 poll(原子不可打断,绝不按墙钟固定时间切片) +// - 顺序维度(前台处理权):等待最久的账户先做(age 降序) +// - 配额维度(时间使用权):与订单数量挂钩(q ∝ count,floor=1,cap=60%) +// - 硬紧急:minTTL < UrgentTTL 的账户无条件置顶(防关单丢单) +// - 窗口动态:窗口 = 一组配额 poll 操作的集合,墙钟长度随单次 poll 实测时长浮动 +// - 反馈:每 poll 后查池,账户池空立即提前终止该 visit;窗口结束立即重算 +package ledgermon + +import ( + "context" + "math" + "math/rand" + "sort" + "sync" + "sync/atomic" + "time" + + "paygateway/internal/logger" + "paygateway/internal/pool" +) + +// SchedulerConfig 调度器参数(默认值见 DefaultSchedulerConfig;可注入覆盖) +type SchedulerConfig struct { + // BaseQuota Q_total 基数:窗口总配额 = 活跃账户数 × BaseQuota(默认 3) + BaseQuota int + // CapPct 单账户占整轮窗口配额上限比例(默认 0.6) + CapPct float64 + // DwellMax 单账户连续 poll 上限(强制看一眼其它账户,默认 3) + DwellMax int + // UrgentTTL 硬紧急阈值:minTTL < UrgentTTL 的账户无条件置顶(默认 8s ≈ 2×单次 poll) + UrgentTTL time.Duration + // IdleSleep 池空休眠间隔(下单唤醒前的最小探测周期,默认 500ms) + IdleSleep time.Duration + // PollMinGap / PollMaxGap 相邻两次账单页 poll 之间的随机间隔(默认 1s~3s,时间驱动)。 + // 只对「有待支付订单的账户」生效(plan 已限定 Count>0 账户,无订单账户不刷); + // 随机化打破固定节奏,避免支付宝/微信账单页按恒定频率被检测为抓取轰炸。 + // PollMaxGap < PollMinGap 或 PollMaxGap<=0 时退化为无间隔(测试用,极低延迟)。 + PollMinGap time.Duration + PollMaxGap time.Duration + // AccountPoller 按 (provider,userID) 执行单次 poll 的钩子(注入 PollCmd 到 actor) + AccountPoller func(ctx context.Context, provider string, userID int) error +} + +// DefaultSchedulerConfig 默认参数(依据 DESIGN-FAIR-SCHEDULER.md §5.3 参数表) +func DefaultSchedulerConfig() SchedulerConfig { + return SchedulerConfig{ + BaseQuota: 3, + CapPct: 0.6, + DwellMax: 3, + UrgentTTL: 8 * time.Second, + IdleSleep: 500 * time.Millisecond, + PollMinGap: 1 * time.Second, + PollMaxGap: 3 * time.Second, + AccountPoller: nil, // 由 NewDeviceScheduler 注入 actor 封装 + } +} + +// schedulerAcct 调度器视图中的账户(provider/userID 从 actor 账户快照解析) +type schedulerAcct struct { + accountID int64 + provider string + userID int +} + +// DeviceScheduler 设备级调度器:窗口决策循环(决策只在 poll 边界发生) +type DeviceScheduler struct { + deviceID string + pool *pool.WaitPayPool + cfg SchedulerConfig + // accounts 本设备账户快照(reconcile 写入,调度循环读取;atomic 防竞态) + accounts atomic.Value // []schedulerAcct + // activeIDs 当前活跃账户(池快照中 count>0 的账户,按窗口重算) + // 调度循环内部使用,无需锁(单 goroutine) + + wake chan struct{} // 下单唤醒(非阻塞;池空休眠时立即响应) + stop chan struct{} + done chan struct{} + + // 可观测(admin Status) + current atomic.Value // 当前窗口描述(windowDesc) + + // roundStat 窗口耗时滑动统计(设备一轮完整轮询实际耗时,供 poll 成本校准/超限判定) + roundStat *roundStat +} + +// roundStat 窗口耗时滑动平均(最近 N 轮)。 +type roundStat struct { + mu sync.Mutex + vals []time.Duration + next int + full bool + size int +} + +func newRoundStat(size int) *roundStat { + if size <= 0 { + size = 10 + } + return &roundStat{vals: make([]time.Duration, size), size: size} +} + +func (r *roundStat) record(d time.Duration) { + r.mu.Lock() + defer r.mu.Unlock() + r.vals[r.next] = d + r.next = (r.next + 1) % r.size + if r.next == 0 { + r.full = true + } +} + +// avg 返回滑动平均耗时(无数据返回 0)。 +func (r *roundStat) avg() time.Duration { + r.mu.Lock() + defer r.mu.Unlock() + if !r.full { + n := r.next + if n == 0 { + return 0 + } + var sum time.Duration + for i := 0; i < n; i++ { + sum += r.vals[i] + } + return sum / time.Duration(n) + } + var sum time.Duration + for _, v := range r.vals { + sum += v + } + return sum / time.Duration(r.size) +} + +// windowDesc 当前窗口可观测状态 +type windowDesc struct { + Active int // 本窗口活跃账户数 + Total int // 本窗口总配额 + Seq []int64 // 本窗口执行顺序(accountID) + Quota map[int64]int + At time.Time +} + +// NewDeviceScheduler 创建设备调度器(不启动 goroutine;调用 Start()) +// poll 由 actor 封装:投递带 Done 的 PollCmd 并等待执行完成(pull 模式) +func NewDeviceScheduler(deviceID string, actor *DeviceActor, p *pool.WaitPayPool, cfg SchedulerConfig) *DeviceScheduler { + if cfg.BaseQuota <= 0 { + cfg.BaseQuota = 3 + } + if cfg.CapPct <= 0 || cfg.CapPct >= 1 { + cfg.CapPct = 0.6 + } + if cfg.DwellMax <= 0 { + cfg.DwellMax = 3 + } + if cfg.UrgentTTL <= 0 { + cfg.UrgentTTL = 8 * time.Second + } + if cfg.IdleSleep <= 0 { + cfg.IdleSleep = 500 * time.Millisecond + } + if cfg.PollMaxGap <= 0 || cfg.PollMaxGap < cfg.PollMinGap { + // 无有效间隔(测试置 0)→ 退化为不等待(极低延迟,与旧 execute 逐字节一致) + cfg.PollMinGap, cfg.PollMaxGap = 0, 0 + } else if cfg.PollMinGap <= 0 { + cfg.PollMinGap = 1 * time.Second + } + s := &DeviceScheduler{ + deviceID: deviceID, + pool: p, + cfg: cfg, + wake: make(chan struct{}, 1), + stop: make(chan struct{}), + done: make(chan struct{}), + roundStat: newRoundStat(10), + } + s.accounts.Store([]schedulerAcct{}) + s.current.Store(windowDesc{}) + // 注入默认 poller:经 actor 串行执行(Done 等待 = pull) + if cfg.AccountPoller == nil { + s.cfg.AccountPoller = func(ctx context.Context, provider string, userID int) error { + acct := actor.DeviceCtx.Account(provider, userID) + if acct == nil { + return nil // 账户已删除/离线,静默跳过 + } + done := make(chan error, 1) + actor.Enqueue(&PollCmd{AccountID: acct.AccountID, Provider: provider, UserID: userID, Done: done}) + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() + } + } + } + return s +} + +// SetAccounts 更新本设备账户快照(reconcile 调用) +func (s *DeviceScheduler) SetAccounts(accts []schedulerAcct) { + s.accounts.Store(accts) +} + +// Wake 下单唤醒(非阻塞;池空休眠时立即响应) +func (s *DeviceScheduler) Wake() { + select { + case s.wake <- struct{}{}: + default: + } +} + +// Start 启动调度 goroutine +func (s *DeviceScheduler) Start() { + go s.loop() +} + +// Stop 停止调度器(等待退出) +func (s *DeviceScheduler) Stop() { + close(s.stop) + <-s.done +} + +// loop 窗口决策循环:S1读池→S2配额→S3排序→S4执行→S5重算(池空休眠) +func (s *DeviceScheduler) loop() { + defer close(s.done) + ctx := context.Background() + for { + select { + case <-s.stop: + return + default: + } + // 设备维度:只读本设备子池快照(多设备天然隔离,互不干扰) + stat := s.pool.DeviceSnapshot(s.deviceID) + order, quotas := s.plan(stat) + if len(order) == 0 { + // 无活跃账户:休眠等待(下单唤醒或周期探测) + select { + case <-s.stop: + return + case <-s.wake: + case <-time.After(s.cfg.IdleSleep): + } + continue + } + s.publishWindow(order, quotas) + s.execute(ctx, order, quotas) + // 每笔 poll 已在 execute 内按 PollMinGap~PollMaxGap 随机间隔(默认1~3s)执行, + // 此处只需保证 stop/wake 响应,不再额外休眠固定时长(否则会稀释 1~3s 的节奏)。 + select { + case <-s.stop: + return + case <-s.wake: + case <-time.After(50 * time.Millisecond): + } + } +} + +// plan 纯调度决策:输入设备子池快照 → (执行顺序, 每账户配额) +// 配额:q_a = max(1, min(Q_total × count_a/Σcount, Q_total×CapPct)) +// 顺序:硬紧急(minTTL 0 && st.DupAmount { + active = append(active, st) + } + } + if len(active) == 0 { + return nil, nil + } + // S2 配额 + qTotal := len(active) * s.cfg.BaseQuota + totalOrders := 0 + for _, a := range active { + totalOrders += a.Count + } + capQuota := int(math.Floor(float64(qTotal) * s.cfg.CapPct)) + if capQuota < 1 { + capQuota = 1 + } + quotas := make(map[int64]int, len(active)) + for _, a := range active { + q := 1 // floor:至少 1 次 + if totalOrders > 0 { + q = int(math.Round(float64(qTotal) * float64(a.Count) / float64(totalOrders))) + } + if q < 1 { + q = 1 + } + if q > capQuota { + q = capQuota + } + quotas[a.AccountID] = q + } + // S3 排序 + sort.Slice(active, func(i, j int) bool { + ui, uj := active[i].MinTTL < s.cfg.UrgentTTL, active[j].MinTTL < s.cfg.UrgentTTL + switch { + case ui && !uj: + return true // 硬紧急置顶 + case !ui && uj: + return false + case ui && uj: + return active[i].MinTTL < active[j].MinTTL // 紧急内按 ttl 升序 + default: + if active[i].OldestAge != active[j].OldestAge { + return active[i].OldestAge > active[j].OldestAge // 等最久的先做 + } + return active[i].AccountID < active[j].AccountID // 确定性 + } + }) + order := make([]int64, 0, len(active)) + for _, a := range active { + order = append(order, a.AccountID) + } + return order, quotas +} + +// execute 按顺序执行:每账户连续 poll ≤ min(配额, DwellMax);每 poll 后查池, +// 账户池空 → 提前终止该 visit(防空转);命令执行失败不致命(下窗口重试)。 +func (s *DeviceScheduler) execute(ctx context.Context, order []int64, quotas map[int64]int) { + start := time.Now() + defer func() { s.roundStat.record(time.Since(start)) }() + accts := s.accounts.Load().([]schedulerAcct) + byID := make(map[int64]*schedulerAcct, len(accts)) + for i := range accts { + a := &accts[i] + byID[a.accountID] = a + } + for _, acctID := range order { + acct := byID[acctID] + if acct == nil { + continue + } + quota := quotas[acctID] + served := 0 + for served < quota { + if served >= s.cfg.DwellMax { + break // 连续占用上限:强制看一眼其它账户 + } + select { + case <-s.stop: + return + default: + } + // 时间驱动 + 随机间隔:每次账单页 poll 前均匀随机等待 [PollMinGap, PollMaxGap](默认1~3s), + // 打破固定节奏,避免微信/支付宝账单页被检测为抓取轰炸(风控)。仅同金额订单账户进入本循环。 + if g := s.pollGap(); g > 0 { + select { + case <-s.stop: + return + case <-time.After(g): + } + } + if err := s.cfg.AccountPoller(ctx, acct.provider, acct.userID); err != nil { + logger.Warn("ledgermon", "调度 poll 执行失败(下窗口重试)", + "device_id", s.deviceID, "account_id", acctID, "err", err) + } + served++ + // 反馈:同金额订单歧义解除(该账户不再有 ≥2 笔同金额待支付)→ 提前终止本 visit + if !s.pool.HasDupAmount(acctID) { + break + } + } + } +} + +// pollGap 返回相邻两次账单页 poll 之间的随机等待时长:均匀分布于 [PollMinGap, PollMaxGap]。 +// 随机化打破固定节奏(防微信/支付宝账单页被检测为抓取轰炸风控)。PollMaxGap<=0(测试退化)返回 0。 +func (s *DeviceScheduler) pollGap() time.Duration { + if s.cfg.PollMaxGap <= 0 { + return 0 + } + d := int64(s.cfg.PollMaxGap - s.cfg.PollMinGap) + if d <= 0 { + return s.cfg.PollMinGap + } + return s.cfg.PollMinGap + time.Duration(rand.Int63n(d)) +} + +// publishWindow 发布当前窗口可观测状态 +func (s *DeviceScheduler) publishWindow(order []int64, quotas map[int64]int) { + seq := make([]int64, len(order)) + copy(seq, order) + q := make(map[int64]int, len(quotas)) + for k, v := range quotas { + q[k] = v + } + s.current.Store(windowDesc{Active: len(order), Total: s.windowTotal(quotas), Seq: seq, Quota: q, At: time.Now()}) +} + +// windowTotal 窗口总配额 +func (s *DeviceScheduler) windowTotal(quotas map[int64]int) int { + t := 0 + for _, v := range quotas { + t += v + } + return t +} + +// CurrentWindow 当前窗口描述(admin 可观测) +func (s *DeviceScheduler) CurrentWindow() windowDesc { + d, _ := s.current.Load().(windowDesc) + return d +} + +// AvgRound 窗口平均耗时(最近 N 轮,poll 成本校准用;无数据返回 0) +func (s *DeviceScheduler) AvgRound() time.Duration { + return s.roundStat.avg() +} diff --git a/internal/ledgermon/scheduler_test.go b/internal/ledgermon/scheduler_test.go new file mode 100644 index 0000000..df36cdd --- /dev/null +++ b/internal/ledgermon/scheduler_test.go @@ -0,0 +1,233 @@ +package ledgermon + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "paygateway/internal/model" + "paygateway/internal/pool" +) + +// newTestScheduler 创建测试调度器(无 actor,用假 poller 计数)。 +// 测试置 0 poll 间隔(PollMaxGap<=0 退化不等待),避免随机 1~3s 等待拖慢单测。 +func newTestScheduler(t *testing.T, p *pool.WaitPayPool, cfg SchedulerConfig) (*DeviceScheduler, *atomic.Int64) { + t.Helper() + var calls atomic.Int64 + if cfg.BaseQuota <= 0 { + cfg = DefaultSchedulerConfig() + } + cfg.PollMinGap, cfg.PollMaxGap = 0, 0 + // 池注入设备映射:全部账户归 "devT"(调度器读本设备快照) + p.SetDeviceOf(func(accountID int64) (string, bool) { return "devT", true }) + s := NewDeviceScheduler("devT", nil, p, cfg) + s.cfg.AccountPoller = func(ctx context.Context, provider string, userID int) error { + calls.Add(1) + return nil + } + s.SetAccounts([]schedulerAcct{ + {accountID: 1, provider: "alipay", userID: 0}, + {accountID: 2, provider: "wechat", userID: 0}, + {accountID: 3, provider: "wechat", userID: 999}, + }) + return s, &calls +} + +// TestPlan_QuotaProportional 配额与订单数挂钩:订单多的账户分更多配额(floor=1,cap 约束) +func TestPlan_QuotaProportional(t *testing.T) { + p := pool.NewWaitPayPool(30) + // 账户1: 10单同金额,账户2: 5单同金额,账户3: 1单 → 仅前两者含同金额订单 + for i := 0; i < 10; i++ { + p.Add(orderWith(1, "o1_"+itos(i), 100)) + } + for i := 0; i < 5; i++ { + p.Add(orderWith(2, "o2_"+itos(i), 200)) + } + p.Add(orderWith(3, "o3_0", 300)) + + s, _ := newTestScheduler(t, p, DefaultSchedulerConfig()) + order, quotas := s.plan(p.DeviceSnapshot("devT")) + + // 账户3 仅 1 单(无同金额)→ 不再入列(仅同金额订单账户才轮询账单页) + if len(order) != 2 { + t.Fatalf("仅同金额账户应入列,实际 %d(账户3 无同金额应排除)", len(order)) + } + // 账户1 订单最多 → 配额应 ≥ 账户2 配额 + if quotas[1] < quotas[2] { + t.Fatalf("订单多的账户配额应不少于订单少的,acct1=%d acct2=%d", quotas[1], quotas[2]) + } + // cap 约束:任何账户配额 ≤ Q_total × 60% + qTotal := 2 * s.cfg.BaseQuota + capQ := int(float64(qTotal) * s.cfg.CapPct) + for _, q := range quotas { + if q > capQ { + t.Fatalf("配额 %d 超过 cap %d", q, capQ) + } + } +} + +// TestPlan_OrderByOldestAge 顺序维度:等待最久的账户先做(age 降序) +func TestPlan_OrderByOldestAge(t *testing.T) { + p := pool.NewWaitPayPool(30) + now := time.Now() + // 账户2 的订单最久(提前 10min)且同为金额100;账户1 新近且同为金额100 + p.Add(&model.Order{OutTradeNo: "old", AccountID: 2, AmountMinor: 100, CreatedAt: now.Add(-10 * time.Minute)}) + p.Add(&model.Order{OutTradeNo: "old2", AccountID: 2, AmountMinor: 100, CreatedAt: now.Add(-9 * time.Minute)}) + p.Add(&model.Order{OutTradeNo: "new", AccountID: 1, AmountMinor: 100, CreatedAt: now}) + p.Add(&model.Order{OutTradeNo: "new2", AccountID: 1, AmountMinor: 100, CreatedAt: now}) + + s, _ := newTestScheduler(t, p, DefaultSchedulerConfig()) + order, _ := s.plan(p.DeviceSnapshot("devT")) + if len(order) != 2 { + t.Fatalf("应 2 个活跃账户(各自有同金额订单),实际 %d", len(order)) + } + if order[0] != 2 { + t.Fatalf("等待最久的账户2 应先执行,实际顺序 %v", order) + } +} + +// TestPlan_UrgentPreemptsAll 硬紧急:minTTL < UrgentTTL 的账户无条件置顶(即使订单少) +func TestPlan_UrgentPreemptsAll(t *testing.T) { + p := pool.NewWaitPayPool(30) + now := time.Now() + // 账户1: 8 单同金额(含歧义);账户2: 2 单同金额但即将关单 + for i := 0; i < 8; i++ { + p.Add(&model.Order{OutTradeNo: "bulk_" + itos(i), AccountID: 1, AmountMinor: 100, CreatedAt: now.Add(-30 * time.Second)}) + } + // 需要强制账户2 MinTTL 很小:CloseAt = CreatedAt + 30min 不够紧急。改用 1min 窗口池。 + p2 := pool.NewWaitPayPool(1) + for i := 0; i < 8; i++ { + p2.Add(&model.Order{OutTradeNo: "bulk_" + itos(i), AccountID: 1, AmountMinor: 100, CreatedAt: now.Add(-20 * time.Second)}) + } + p2.Add(&model.Order{OutTradeNo: "urgent", AccountID: 2, AmountMinor: 500, CreatedAt: now.Add(-55 * time.Second)}) + p2.Add(&model.Order{OutTradeNo: "urgent2", AccountID: 2, AmountMinor: 500, CreatedAt: now.Add(-54 * time.Second)}) + + s, _ := newTestScheduler(t, p2, DefaultSchedulerConfig()) + order, _ := s.plan(p2.DeviceSnapshot("devT")) + if len(order) != 2 { + t.Fatalf("应 2 个活跃账户,实际 %d", len(order)) + } + if order[0] != 2 { + t.Fatalf("硬紧急账户2 应无条件置顶(8 单账户让位),实际顺序 %v", order) + } +} + +// TestExecute_DwellMaxCap 连续 poll 上限:单账户配额大时连续执行不超过 DwellMax +func TestExecute_DwellMaxCap(t *testing.T) { + p := pool.NewWaitPayPool(30) + // 仅账户1 大量订单 → 窗口内配额可能 > DwellMax,但连续执行被 DwellMax 限制 + for i := 0; i < 20; i++ { + p.Add(orderWith(1, "o1_"+itos(i), 100)) + } + s, calls := newTestScheduler(t, p, DefaultSchedulerConfig()) + order, quotas := s.plan(p.DeviceSnapshot("devT")) + s.execute(tctx(), order, quotas) + if got := calls.Load(); got > int64(s.cfg.DwellMax) { + t.Fatalf("单账户连续 poll 超过 DwellMax=%d,实际 %d", s.cfg.DwellMax, got) + } +} + +// TestExecute_EarlyTerminateDupResolved 提前终止:visit 中途同金额歧义解除 → 立即停止该 visit +func TestExecute_EarlyTerminateDupResolved(t *testing.T) { + p := pool.NewWaitPayPool(30) + p.Add(orderWith(1, "o1_0", 100)) + p.Add(orderWith(1, "o1_1", 100)) + p.Add(orderWith(2, "o2_0", 500)) + p.Add(orderWith(2, "o2_1", 500)) + + s, _ := newTestScheduler(t, p, DefaultSchedulerConfig()) + // 自定义 poller:统计账户1(alipay/user0)的 poll 次数;第一次 poll 后清空账户1 池(模拟同金额订单入账), + // 验证 visit 提前终止——账户1 的 poll 次数应远小于其配额。 + var acct1Calls atomic.Int64 + s.cfg.AccountPoller = func(ctx context.Context, provider string, userID int) error { + if provider == "alipay" && userID == 0 { + n := acct1Calls.Add(1) + if n == 1 { + p.Remove("o1_0") + p.Remove("o1_1") + } + } + return nil + } + order, quotas := s.plan(p.DeviceSnapshot("devT")) + s.execute(tctx(), order, quotas) + if got := acct1Calls.Load(); got >= int64(quotas[1]) && quotas[1] > 1 { + t.Fatalf("同金额歧义解除后应提前终止 visit,配额=%d 实际 poll=%d", quotas[1], got) + } +} + +// TestPlan_NoDupNoActive 账户仅有金额互异的订单 → 不触发账单页轮询(通知栏按金额唯一匹配即可) +func TestPlan_NoDupNoActive(t *testing.T) { + p := pool.NewWaitPayPool(30) + p.Add(orderWith(1, "o1_0", 100)) + p.Add(orderWith(1, "o1_1", 200)) + p.Add(orderWith(2, "o2_0", 300)) + + s, _ := newTestScheduler(t, p, DefaultSchedulerConfig()) + order, quotas := s.plan(p.DeviceSnapshot("devT")) + if len(order) != 0 || quotas != nil { + t.Fatalf("无同金额订单应不触发账单页轮询:order=%v quotas=%v", order, quotas) + } +} + +// TestPlan_DupTriggersActive 同一账户出现 ≥2 笔同金额订单 → 触发账单页轮询 +func TestPlan_DupTriggersActive(t *testing.T) { + p := pool.NewWaitPayPool(30) + // 账户1: 两笔同为100 → 触发;账户2: 一笔100一笔200 → 不触发 + p.Add(orderWith(1, "a1", 100)) + p.Add(orderWith(1, "a2", 100)) + p.Add(orderWith(2, "b1", 100)) + p.Add(orderWith(2, "b2", 200)) + + s, _ := newTestScheduler(t, p, DefaultSchedulerConfig()) + order, _ := s.plan(p.DeviceSnapshot("devT")) + if len(order) != 1 || order[0] != 1 { + t.Fatalf("仅同金额账户1 应触发,实际 order=%v", order) + } +} + +// TestPlan_NoActiveNoQuota 池空 → 无活跃账户 → 无窗口 +func TestPlan_NoActiveNoQuota(t *testing.T) { + p := pool.NewWaitPayPool(30) + s, _ := newTestScheduler(t, p, DefaultSchedulerConfig()) + order, quotas := s.plan(p.DeviceSnapshot("devT")) + if len(order) != 0 || quotas != nil { + t.Fatalf("池空应无活跃账户:order=%v quotas=%v", order, quotas) + } +} + +// ---- 测试辅助 ---- + +func pollingScheduler(gapMin, gapMax time.Duration) *DeviceScheduler { + p := pool.NewWaitPayPool(30) + s := NewDeviceScheduler("devT", nil, p, SchedulerConfig{}) + s.cfg.PollMinGap, s.cfg.PollMaxGap = gapMin, gapMax + return s +} + +func TestPollGap_Range(t *testing.T) { + s := pollingScheduler(time.Second, 3*time.Second) + for i := 0; i < 500; i++ { + if g := s.pollGap(); g < time.Second || g > 3*time.Second { + t.Fatalf("pollGap 越界: %v (应∈[1s,3s])", g) + } + } +} + +func TestPollGap_Disabled(t *testing.T) { + s := pollingScheduler(0, 0) + if g := s.pollGap(); g != 0 { + t.Fatalf("PollMaxGap<=0 应返回 0 不等待,实际 %v", g) + } +} + +func orderWith(accountID int64, outTradeNo string, amountMinor int64) *model.Order { + return &model.Order{OutTradeNo: outTradeNo, AccountID: accountID, AmountMinor: amountMinor, CreatedAt: time.Now()} +} + +func itos(n int) string { + return string(rune('0' + n)) +} + +func tctx() context.Context { return context.Background() } diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100755 index 0000000..a4db8f9 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,300 @@ +// Package logger 日志系统:slog + 文件轮转 + 异步写库 + trace_id +package logger + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "strings" + "sync" + "time" + + "gopkg.in/natefinch/lumberjack.v2" +) + +// Options 日志配置 +type Options struct { + Level string // debug / info / warn / error + LogFile string // 日志文件路径(空则不写文件);若含 "{date}" 占位符则按天分文件,如 "logs/paygateway-{date}.log" + MaxSizeMB int // 单个文件最大 MB + MaxBackups int // 保留文件数 + MaxAgeDays int // 保留天数 + EnableDB bool // 是否异步写库(管理界面查询) + DBWriter DBWriter +} + +const datePlaceholder = "{date}" + +// 按天分文件状态:当前日期 + 对应文件,跨天时切换 +var ( + dayMu sync.Mutex + curDay string + dayLogger *lumberjack.Logger +) + +// DBWriter 日志落库接口(由 store 包实现,避免循环依赖) +type DBWriter interface { + InsertLog(ctx context.Context, level, scope, message, traceID, fields string) error +} + +// LogEntry 结构化日志条目(异步队列用) +type LogEntry struct { + Level string + Scope string + Message string + TraceID string + Fields string + Time time.Time +} + +var ( + mu sync.Mutex + logger *slog.Logger + dbWriter DBWriter + logCh chan LogEntry + closeCh chan struct{} + once sync.Once + closeOnce sync.Once // Close 只允许执行一次(防双 close panic) + enabled bool +) + +// Init 初始化全局日志 +func Init(opts Options) { + once.Do(func() { + enabled = true + level := parseLevel(opts.Level) + + var writers []io.Writer + writers = append(writers, os.Stdout) + + if opts.LogFile != "" { + writers = append(writers, newFileWriter(opts)) + } + + multi := io.MultiWriter(writers...) + logger = slog.New(slog.NewJSONHandler(multi, &slog.HandlerOptions{ + Level: level, + })) + + dbWriter = opts.DBWriter + if opts.EnableDB && dbWriter != nil { + logCh = make(chan LogEntry, 2048) + closeCh = make(chan struct{}) + go dbWriterLoop() + } + }) +} + +// SetDBWriter 设置日志落库 writer(Init 之后调用;EnableDB 时生效) +func SetDBWriter(w DBWriter) { + if !enabled { + return + } + dbWriter = w + if logCh == nil && w != nil { + logCh = make(chan LogEntry, 2048) + closeCh = make(chan struct{}) + go dbWriterLoop() + } +} + +// Close 关闭日志(优雅退出时调用,flush 队列) +// 只关闭 closeCh 通知 writer 排空队列后退出;不关闭 logCh —— +// 避免后续任何 logger 调用向已关闭 channel 发送导致 panic(向关闭 channel 发送会 panic,非阻塞 select 也拦不住) +func Close() { + if !enabled || logCh == nil { + return + } + closeOnce.Do(func() { + close(closeCh) + }) +} + +// newFileWriter 构造文件写入器。LogFile 含 "{date}" 时按天分文件,否则单文件+大小轮转。 +func newFileWriter(opts Options) io.Writer { + if !strings.Contains(opts.LogFile, datePlaceholder) { + return &lumberjack.Logger{ + Filename: opts.LogFile, + MaxSize: opts.MaxSizeMB, + MaxBackups: opts.MaxBackups, + MaxAge: opts.MaxAgeDays, + Compress: true, + } + } + return &dayFileWriter{opts: opts} +} + +// dayFileWriter 按天切换文件的 io.Writer。每次 Write 检查当前日期,跨天时重建 lumberjack 指向新文件。 +type dayFileWriter struct { + opts Options +} + +func (w *dayFileWriter) Write(p []byte) (int, error) { + day := time.Now().Format("2006-01-02") + dayMu.Lock() + defer dayMu.Unlock() + if dayLogger == nil || curDay != day { + path := strings.ReplaceAll(w.opts.LogFile, datePlaceholder, day) + dayLogger = &lumberjack.Logger{ + Filename: path, + MaxSize: w.opts.MaxSizeMB, + MaxBackups: w.opts.MaxBackups, + MaxAge: w.opts.MaxAgeDays, + Compress: true, + } + curDay = day + } + return dayLogger.Write(p) +} + +// parseLevel 解析日志级别 +func parseLevel(s string) slog.Level { + switch s { + case "debug": + return slog.LevelDebug + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +// WithTrace 返回带 trace_id 的 logger(gin 中间件使用) +func WithTrace(traceID string) *slog.Logger { + if !enabled || logger == nil { + return slog.Default() + } + return logger.With("trace_id", traceID) +} + +// Debug / Info / Warn / Error 全局快捷方法 +func Debug(scope, msg string, args ...any) { + if !enabled || logger == nil { + return + } + entry := logEntry("debug", scope, msg, "", args) + logger.Debug(msg, append([]any{"scope", scope}, args...)...) + pushDB(entry) +} + +func Info(scope, msg string, args ...any) { + if !enabled || logger == nil { + return + } + logger.Info(msg, append([]any{"scope", scope}, args...)...) + pushDB(logEntry("info", scope, msg, "", args)) +} + +func Warn(scope, msg string, args ...any) { + if !enabled || logger == nil { + return + } + logger.Warn(msg, append([]any{"scope", scope}, args...)...) + pushDB(logEntry("warn", scope, msg, "", args)) +} + +func Error(scope, msg string, args ...any) { + if !enabled || logger == nil { + return + } + logger.Error(msg, append([]any{"scope", scope}, args...)...) + pushDB(logEntry("error", scope, msg, "", args)) +} + +// logEntry 构造结构化条目(trace_id 从 slog 参数中提取) +func logEntry(level, scope, msg, traceID string, args []any) LogEntry { + fields := make(map[string]any) + for i := 0; i+1 < len(args); i += 2 { + if k, ok := args[i].(string); ok { + fields[k] = args[i+1] + } + } + if tid, ok := fields["trace_id"]; ok { + traceID = fmt.Sprint(tid) + delete(fields, "trace_id") + } + fieldsJSON := "" + if len(fields) > 0 { + b, _ := jsonMarshal(fields) + fieldsJSON = string(b) + } + return LogEntry{ + Level: level, + Scope: scope, + Message: msg, + TraceID: traceID, + Fields: fieldsJSON, + Time: time.Now(), + } +} + +func jsonMarshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +// dbWriterLoop 异步批量写库:攒 50 条或 1s flush +func dbWriterLoop() { + batch := make([]LogEntry, 0, 50) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + flush := func() { + if len(batch) == 0 || dbWriter == nil { + return + } + for _, e := range batch { + if err := dbWriter.InsertLog(context.Background(), e.Level, e.Scope, e.Message, e.TraceID, e.Fields); err != nil { + // 写库失败不影响主流程,降级打印 + fmt.Fprintf(os.Stderr, "[logger] db write failed: %v\n", err) + } + } + batch = batch[:0] + } + + for { + select { + case e, ok := <-logCh: + if !ok { + flush() + return + } + batch = append(batch, e) + if len(batch) >= 50 { + flush() + } + case <-ticker.C: + flush() + case <-closeCh: + // 收到关闭信号:排空剩余日志后退出(logCh 永不关闭,避免发送方 panic) + flush() + for { + select { + case e := <-logCh: + batch = append(batch, e) + if len(batch) >= 50 { + flush() + } + default: + flush() + return + } + } + } + } +} + +func pushDB(e LogEntry) { + if logCh == nil { + return + } + select { + case logCh <- e: + default: + // 队列满,丢弃(保主流程) + } +} diff --git a/internal/model/exception.go b/internal/model/exception.go new file mode 100755 index 0000000..62866a4 --- /dev/null +++ b/internal/model/exception.go @@ -0,0 +1,63 @@ +package model + +// ExceptionOrder 人工异常处理池订单(自动化兜底失败后的最终兜底) +// 只增不改:resolved 后可 reopen,不可删除;全程审计(exception_logs) +type ExceptionOrder struct { + ID int64 `json:"id"` + OutTradeNo string `json:"out_trade_no"` + Pid string `json:"pid"` + UpstreamName string `json:"upstream_name"` + Channel string `json:"channel"` + AmountMinor int64 `json:"amount_minor"` + OrderStatus string `json:"order_status"` + ChannelStatus string `json:"channel_status"` // TRADE_SUCCESS / TRADE_CLOSED / QUERY_FAILED / UNKNOWN + ChannelTxID string `json:"channel_tx_id"` + PaidAmount int64 `json:"paid_amount"` // 人工确认的实际支付金额(默认=AmountMinor) + Payer string `json:"payer"` + Reason string `json:"reason"` + Phase string `json:"phase"` // pending / processing / resolved + ResolvedAction string `json:"resolved_action"` + Evidence string `json:"evidence"` // 证据时间线 JSON(自动收集,只读) + Suggestion string `json:"suggestion"` + Handler string `json:"handler"` + HandlerNote string `json:"handler_note"` + ResolvedAt string `json:"resolved_at"` + UpdatedAt string `json:"updated_at"` + CreatedAt string `json:"created_at"` +} + +// ExceptionLog 异常池操作审计 +type ExceptionLog struct { + ID int64 `json:"id"` + ExceptionID int64 `json:"exception_id"` + Action string `json:"action"` + Operator string `json:"operator"` + Detail string `json:"detail"` + CreatedAt string `json:"created_at"` +} + +// 异常原因(自动进池触发点) +const ( + ExReasonQueryFailed = "query_failed" // 查单连续失败,无法确认 + ExReasonAmountMismatch = "amount_mismatch" // 查单金额 ≠ 订单金额 + ExReasonStateConflict = "state_conflict" // 回调与查单状态矛盾 + ExReasonCloseUncertain = "close_uncertain" // 关单时查单失败,存疑 + ExReasonNotifyDead = "notify_dead" // 通知 dead 且重发失败 + ExReasonManual = "manual" // 管理后台手动加入 + ExReasonListenOffline = "listen_offline" // 监听端掉线,无法确认是否已支付(个人通道) + ExReasonListenTimeout = "listen_timeout" // 监听端在线但超时无推送,存疑(个人通道) +) + +// 异常订单阶段 +const ( + ExPhasePending = "pending" + ExPhaseProcessing = "processing" + ExPhaseResolved = "resolved" +) + +// 人工处理动作(resolved_action) +const ( + ExActionSettle = "settle" // 确认入账 + ExActionClose = "close" // 确认未支付,关单 + ExActionNoop = "noop" // 无需处理 +) diff --git a/internal/model/order.go b/internal/model/order.go new file mode 100755 index 0000000..7a7c244 --- /dev/null +++ b/internal/model/order.go @@ -0,0 +1,134 @@ +// Package model 领域模型:订单、状态常量、金额类型 +package model + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// 订单状态(状态机) +const ( + StatusCreated = "CREATED" // 已创建 + StatusWaitPay = "WAIT_BUYER_PAY" // 等待支付(与易支付协议保持一致) + StatusConfirming = "CONFIRMING" // 链上确认中(未来 crypto 用) + StatusSuccess = "TRADE_SUCCESS" // 支付成功(与易支付协议保持一致) + StatusClosed = "TRADE_CLOSED" // 已关闭/超时(与易支付协议保持一致) + StatusFailed = "FAILED" // 支付失败(链上重组等) +) + +// 订单状态允许的流转(防止状态回跳/非法跳转) +var allowedTransitions = map[string][]string{ + StatusCreated: {StatusWaitPay, StatusClosed}, + StatusWaitPay: {StatusSuccess, StatusClosed, StatusConfirming}, + StatusConfirming: {StatusSuccess, StatusFailed, StatusClosed}, + StatusSuccess: {}, // 终态,不可再变 + StatusClosed: {}, // 终态 + StatusFailed: {}, // 终态 +} + +// CanTransition 判断状态流转是否合法 +func CanTransition(from, to string) bool { + if from == to { + return true // 幂等 + } + for _, next := range allowedTransitions[from] { + if next == to { + return true + } + } + return false +} + +// Order 订单领域模型 +type Order struct { + ID int64 `json:"id"` + OutTradeNo string `json:"out_trade_no"` // 接入方订单号(幂等依据) + GatewayNo string `json:"gateway_no"` // 网关订单号 + Channel string `json:"channel"` // alipay / wechat / paypal / usdt + ChannelTxID string `json:"channel_tx_id"` // 通道交易号(回调去重) + AccountID int64 `json:"account_id"` // 处理这笔订单的账户ID(账户池) + Pid string `json:"pid"` // 上游易支付商户ID(多商户路由,空=default) + UpstreamName string `json:"upstream_name,omitempty"` // 上游名称(来源标识) + Attach string `json:"attach,omitempty"` // 上游请求ID(易支付 attach 字段,原样透传,用于对账) + AccountName string `json:"account_name,omitempty"` // 账户名称(展示用,查询时附加) + AmountMinor int64 `json:"amount_minor"` // 金额(最小单位:分) + Currency string `json:"currency"` // CNY / USD / USDT + Subject string `json:"subject"` // 商品名称 + Status string `json:"status"` // 状态机 + NotifyURL string `json:"notify_url"` // 回调地址(接入方) + ReturnURL string `json:"return_url"` // 跳转地址 + QrCode string `json:"qr_code"` // 二维码内容 + PayURL string `json:"pay_url"` // 支付链接/凭据 + Payer string `json:"payer"` // 付款方标识(可选) + NoteCode string `json:"note_code,omitempty"` // 个人免签:备注码 + QrToken string `json:"qr_token,omitempty"` // 个人免签:套码 token + DeviceID string `json:"device_id,omitempty"` // 个人免签:承接的监听端设备 + BuyerKey string `json:"buyer_key,omitempty"` // 下单环境唯一标识(IP+UA+设备指纹 hash,买家级串行/防刷单用) + FPKey string `json:"fp_key,omitempty"` // 浏览器指纹标识(canvas/时区/屏幕 hash,支付页 JS 上报,抗换 IP) + Notified bool `json:"notified"` // 是否已通知接入方 + CreatedAt time.Time `json:"created_at"` + SessionExpiresAt time.Time `json:"session_expires_at,omitempty"` + PaidAt *time.Time `json:"paid_at,omitempty"` + NotifiedAt *time.Time `json:"notified_at,omitempty"` + NotifyCount int `json:"notify_count"` // 通知次数(outbox 重试用) + + // 人工异常处理标记(人工池处理后永久留痕) + ExceptionID int64 `json:"exception_id"` + HandledBy string `json:"handled_by"` + HandledAction string `json:"handled_action"` // manual_settle / manual_close / noop + HandledAt string `json:"handled_at"` + HandledNote string `json:"handled_note"` +} + +// 兼容易支付协议:金额为字符串"元",需要与最小单位互转 + +// MoneyYuan 返回金额(元,字符串,易支付协议格式) +func (o *Order) MoneyYuan() string { + if o.AmountMinor <= 0 { + return "0.00" + } + return fmt.Sprintf("%d.%02d", o.AmountMinor/100, o.AmountMinor%100) +} + +// ParseMoneyToMinor 将金额字符串(元)解析为最小单位(分) +// 支持 "0.01"、"10"、"10.5"、"10.50" 等格式;非法返回 error +func ParseMoneyToMinor(money string) (int64, error) { + money = strings.TrimSpace(money) + if money == "" { + return 0, fmt.Errorf("金额为空") + } + // 拆分整数和小数部分 + parts := strings.Split(money, ".") + intPart := parts[0] + fracPart := "" + if len(parts) > 1 { + fracPart = parts[1] + if len(parts) > 2 { + return 0, fmt.Errorf("金额格式非法: %s", money) + } + } + // 整数部分 + whole, err := strconv.ParseInt(intPart, 10, 64) + if err != nil || whole < 0 { + return 0, fmt.Errorf("金额整数部分非法: %s", money) + } + // 小数部分:最多 2 位,补零到 2 位 + if len(fracPart) > 2 { + return 0, fmt.Errorf("金额最多两位小数: %s", money) + } + frac := fracPart + strings.Repeat("0", 2-len(fracPart)) + fracInt := int64(0) + if frac != "" { + fracInt, err = strconv.ParseInt(frac, 10, 64) + if err != nil { + return 0, fmt.Errorf("金额小数部分非法: %s", money) + } + } + minor := whole*100 + fracInt + if minor <= 0 { + return 0, fmt.Errorf("金额必须大于 0: %s", money) + } + return minor, nil +} diff --git a/internal/notify/urlguard.go b/internal/notify/urlguard.go new file mode 100644 index 0000000..838f340 --- /dev/null +++ b/internal/notify/urlguard.go @@ -0,0 +1,139 @@ +// Package notify 回调 URL 安全防护: +// SSRF 面在商户可控的 notify_url —— 下单时校验格式,发送时用安全拨号器 +// 在 TCP 连接层面复核解析出的 IP(防 DNS rebinding 绕过域名黑名单)。 +package notify + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// ValidateNotifyURL 校验回调地址。 +// scheme 必须为 http/https;allowPrivate=false 时拒绝解析到私网/环回/链路本地/未指定地址的目标。 +func ValidateNotifyURL(raw string, allowPrivate bool) error { + if raw == "" { + return nil // 空地址合法(worker 跳过发送) + } + u, err := url.Parse(raw) + if err != nil { + return fmt.Errorf("notify_url 无法解析: %w", err) + } + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("notify_url 协议必须是 http/https: %q", u.Scheme) + } + host := u.Hostname() + if host == "" { + return fmt.Errorf("notify_url 缺少主机名") + } + if allowPrivate { + return nil + } + ips, err := net.LookupIP(host) + if err != nil { + return fmt.Errorf("notify_url 域名解析失败: %w", err) + } + for _, ip := range ips { + if isPrivateIP(ip) { + return fmt.Errorf("notify_url 指向内网地址 %s(已拒绝)", ip) + } + } + return nil +} + +// IsPrivateURL 判断 URL 主机指向是否为内网/环回/链路本地等非公网地址 +// (return_url 兜底重写用:return_url 是浏览器跳转,不涉及 SSRF 拨号)。 +// DNS 解析失败按"非私有"处理:语法级检查已可靠拦截 localhost/环回/私网 IP 字面量, +// 纯域名解析失败(网关断网/无 resolver)不应把正常公网域名饿死出候选。 +func IsPrivateURL(raw string) bool { + u, err := url.Parse(raw) + if err != nil || u.Host == "" { + return true + } + if u.Scheme != "http" && u.Scheme != "https" { + return true + } + if ip := net.ParseIP(u.Hostname()); ip != nil { + return isPrivateIP(ip) + } + host := strings.ToLower(u.Hostname()) + if host == "localhost" || strings.HasSuffix(host, ".local") { + return true + } + ips, err := net.LookupIP(u.Hostname()) + if err != nil { + return false // 解析不可达:语法检查已过,放行(见函数注释) + } + for _, ip := range ips { + if isPrivateIP(ip) { + return true + } + } + return false +} + +func isPrivateIP(ip net.IP) bool { + privateRanges := []struct { + network *net.IPNet + }{ + {cidr("127.0.0.0/8")}, // IPv4 环回 + {cidr("10.0.0.0/8")}, // 私网 + {cidr("172.16.0.0/12")}, // 私网 + {cidr("192.168.0.0/16")}, // 私网 + {cidr("169.254.0.0/16")}, // 链路本地(云元数据 169.254.169.254 在此段) + {cidr("0.0.0.0/8")}, // 未指定 + {cidr("100.64.0.0/10")}, // 运营商级 NAT(Tailscale 等) + {cidr("::1/128")}, // IPv6 环回 + {cidr("fc00::/7")}, // IPv6 ULA + {cidr("fe80::/10")}, // IPv6 链路本地 + } + for _, r := range privateRanges { + if r.network.Contains(ip) { + return true + } + } + return false +} + +func cidr(s string) *net.IPNet { + _, n, err := net.ParseCIDR(s) + if err != nil { + panic(fmt.Sprintf("内置 CIDR 非法 %s: %v", s, err)) + } + return n +} + +// NewNotifyHTTPClient 构造带 SSRF 复核的 HTTP 客户端: +// Control 复核发生在实际 connect 的地址上,域名解析结果与连接地址不可替换 +// (DNS rebinding 无效)。allowPrivate=true 时行为与默认客户端一致。 +func NewNotifyHTTPClient(timeout time.Duration, allowPrivate bool) *http.Client { + dialer := &net.Dialer{Timeout: timeout} + transport := &http.Transport{} + if !allowPrivate { + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + host, _, err := net.SplitHostPort(addr) + if err == nil { + var ips []net.IP + if ip := net.ParseIP(host); ip != nil { + ips = []net.IP{ip} + } else if ips, err = net.LookupIP(host); err != nil { + return nil, fmt.Errorf("notify 目标解析失败: %w", err) + } + for _, ip := range ips { + if isPrivateIP(ip) { + return nil, fmt.Errorf("notify 目标指向内网地址 %s(已拒绝)", ip) + } + } + } + return dialer.DialContext(ctx, network, addr) + } + } + return &http.Client{ + Timeout: timeout, + Transport: transport, + } +} diff --git a/internal/notify/urlguard_test.go b/internal/notify/urlguard_test.go new file mode 100644 index 0000000..b12a6ed --- /dev/null +++ b/internal/notify/urlguard_test.go @@ -0,0 +1,62 @@ +package notify + +import ( + "net" + "testing" +) + +func TestValidateNotifyURL(t *testing.T) { + tests := []struct { + name string + raw string + allowPrivate bool + wantErr bool + }{ + {"空地址合法", "", true, false}, + {"空地址私网封锁下仍合法", "", false, false}, + {"公网 http(IP字面量)", "http://8.8.4.4/notify", false, false}, + {"公网 https(IP字面量)", "https://[2606:4700:4700::1111]/notify.php", false, false}, + {"ftp 协议拒绝", "ftp://example.com/x", true, true}, + {"javascript 协议拒绝", "javascript:alert(1)", true, true}, + {"缺主机名", "http:///path", true, true}, + {"环回封禁", "http://127.0.0.1:3000/api/notify", false, true}, + {"环回放行", "http://127.0.0.1:3000/api/notify", true, false}, + {"localhost 域名封禁", "http://localhost:8080/notify", false, true}, + {"内网 IP 封禁", "http://192.168.1.10:8080/notify", false, true}, + {"链路本地封禁(云元数据)", "http://169.254.169.254/latest/meta-data", false, true}, + {"Tailscale CGNAT 段封禁", "http://100.64.0.1/notify", false, true}, + {"内网 IP 放行", "http://192.168.1.10:8080/notify", true, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateNotifyURL(tt.raw, tt.allowPrivate) + if (err != nil) != tt.wantErr { + t.Fatalf("ValidateNotifyURL(%q, %v) err=%v, wantErr=%v", tt.raw, tt.allowPrivate, err, tt.wantErr) + } + }) + } +} + +func TestIsPrivateIP(t *testing.T) { + cases := map[string]bool{ + "127.0.0.1": true, + "10.1.2.3": true, + "172.16.0.1": true, + "172.31.255.255": true, + "172.32.0.1": false, + "192.168.0.1": true, + "169.254.169.254": true, + "100.64.0.1": true, + "8.8.8.8": false, + "1.1.1.1": false, + "::1": true, + "fe80::1": true, + "fd00::1": true, + "2606:4700:4700::": false, + } + for ip, want := range cases { + if got := isPrivateIP(net.ParseIP(ip)); got != want { + t.Errorf("isPrivateIP(%s) = %v, want %v", ip, got, want) + } + } +} diff --git a/internal/notify/worker.go b/internal/notify/worker.go new file mode 100755 index 0000000..1b81fe9 --- /dev/null +++ b/internal/notify/worker.go @@ -0,0 +1,181 @@ +// Package notify 可靠回调投递(Outbox):worker 扫描 pending 任务 → POST 接入方 → 重试 +package notify + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "paygateway/internal/alert" + "paygateway/internal/logger" + "paygateway/internal/store" +) + +// Worker 回调投递 worker +type Worker struct { + store store.Store + client *http.Client + interval time.Duration + // RetryDelays 重试间隔(指数退避) + RetryDelays []time.Duration + // MaxRetries 最大重试次数(超过标记 dead) + MaxRetries int + alert *alert.Client // 告警(RESILIENCE P1-3,nil=禁用) +} + +// SetAlert 注入告警客户端(可选) +func (w *Worker) SetAlert(a *alert.Client) { w.alert = a } + +// NewWorker 创建回调 worker。allowPrivateNotify=false 时启用 SSRF 复核拨号器。 +func NewWorker(st store.Store, allowPrivateNotify bool) *Worker { + return &Worker{ + store: st, + client: NewNotifyHTTPClient(15*time.Second, allowPrivateNotify), + interval: 3 * time.Second, + RetryDelays: []time.Duration{ + 1 * time.Minute, 5 * time.Minute, 15 * time.Minute, + 1 * time.Hour, 6 * time.Hour, 24 * time.Hour, + }, + MaxRetries: 6, // 总窗口 ≈ 31h,覆盖上游长时间不可用 + } +} + +// Start 启动循环(阻塞,应放 goroutine) +func (w *Worker) Start(ctx context.Context) { + logger.Info("notify", "回调 worker 启动", "interval", w.interval.String()) + ticker := time.NewTicker(w.interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + logger.Info("notify", "回调 worker 停止") + return + case <-ticker.C: + w.processBatch(ctx) + } + } +} + +// processBatch 处理一批到期任务 +func (w *Worker) processBatch(ctx context.Context) { + tasks, err := w.store.GetPendingNotifyTasks(ctx, 20) + if err != nil { + logger.Error("notify", "获取回调任务失败", "err", err) + return + } + for _, task := range tasks { + w.process(ctx, task) + } +} + +// process 处理单个任务 +func (w *Worker) process(ctx context.Context, task *store.NotifyTask) { + // 解析 payload(易支付协议参数,已含签名) + var params map[string]string + if err := json.Unmarshal([]byte(task.Payload), ¶ms); err != nil { + // payload 损坏:重试也无效,标记 dead + logger.Error("notify", "回调任务 payload 损坏,标记 dead", "task_id", task.ID, "err", err) + w.store.UpdateNotifyTask(ctx, task.ID, "dead", task.RetryCount, time.Now()) + return + } + + notifyURL := params["notify_url"] + if notifyURL == "" { + // 没有回调地址(payload 里应该有),标记成功跳过 + w.store.UpdateNotifyTask(ctx, task.ID, "success", task.RetryCount, time.Now()) + return + } + // 注意:notify_url 保留在 body 中并参与签名。 + // New API(go-epay)验签逻辑 = 用收到的全部参数重算签名比对, + // 若删除 notify_url 会导致签名不一致 → 验签失败。易支付标准签名本就包含 notify_url。 + + body := buildForm(params) + start := time.Now() + resp, err := w.client.Post(notifyURL, "application/x-www-form-urlencoded", bytes.NewReader(body)) + latency := time.Since(start).Milliseconds() + + if err != nil { + w.retryOrDead(ctx, task, "请求失败: "+err.Error()) + return + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + + logger.Info("notify", "回调接入方", + "task_id", task.ID, + "out_trade_no", params["out_trade_no"], + "pid", params["pid"], + "url", notifyURL, + "status", resp.StatusCode, + "latency_ms", latency) + + // 易支付协议:商户处理成功必须响应正文 "success"。 + // 仅凭 HTTP 200 判定会把 LB/网关的错误页、鉴权墙当成功,导致上游永远收不到有效通知。 + if resp.StatusCode == 200 && strings.EqualFold(strings.TrimSpace(string(respBody)), "success") { + logger.Info("notify", "回调成功 ✅", "task_id", task.ID) + w.store.UpdateNotifyTask(ctx, task.ID, "success", task.RetryCount, time.Now()) + // 同步更新订单通知状态(管理后台展示) + if otn := params["out_trade_no"]; otn != "" { + if err := w.store.MarkNotified(ctx, otn, task.RetryCount+1); err != nil { + logger.Warn("notify", "更新订单通知状态失败", "err", err, "out_trade_no", otn) + } + } + } else if resp.StatusCode == 200 { + logger.Warn("notify", "回调 HTTP 200 但响应非 success(视为失败重试)", + "task_id", task.ID, "body", string(respBody)) + w.retryOrDead(ctx, task, "200 但响应体非 success") + } else { + logger.Warn("notify", "回调失败", "task_id", task.ID, "status", resp.StatusCode, "body", string(respBody)) + w.retryOrDead(ctx, task, resp.Status) + } +} + +// retryOrDead 重试或标记 dead +func (w *Worker) retryOrDead(ctx context.Context, task *store.NotifyTask, reason string) { + retryCount := task.RetryCount + 1 + if retryCount > w.MaxRetries { + logger.Error("notify", "回调任务超过最大重试,标记 dead", "task_id", task.ID, "reason", reason) + w.store.UpdateNotifyTask(ctx, task.ID, "dead", retryCount, time.Now()) + // 告警:通知 dead(RESILIENCE P1-3) + if w.alert != nil { + w.alert.Send("error", "回调任务dead", + fmt.Sprintf("订单 %s 回调超过最大重试标记 dead(%d 次)\n原因: %s", taskPayloadTradeNo(task), retryCount, reason)) + } + return + } + delay := time.Minute + if retryCount-1 < len(w.RetryDelays) { + delay = w.RetryDelays[retryCount-1] + } + next := time.Now().Add(delay) + logger.Warn("notify", "回调任务将重试", "task_id", task.ID, "retry", retryCount, "next", next.Format(time.RFC3339)) + w.store.UpdateNotifyTask(ctx, task.ID, "pending", retryCount, next) +} + +// taskPayloadTradeNo 从任务 payload 提取订单号(告警信息用,失败返回空串) +func taskPayloadTradeNo(task *store.NotifyTask) string { + var params map[string]string + if err := json.Unmarshal([]byte(task.Payload), ¶ms); err != nil { + return "" + } + return params["out_trade_no"] +} + +// buildForm 构造表单 body(值做 URL 编码,兼容中文/特殊字符) +func buildForm(params map[string]string) []byte { + var buf bytes.Buffer + for k, v := range params { + buf.WriteString(url.QueryEscape(k) + "=" + url.QueryEscape(v) + "&") + } + s := buf.String() + if len(s) > 0 { + s = s[:len(s)-1] + } + return []byte(s) +} diff --git a/internal/notifywatcher/hash.go b/internal/notifywatcher/hash.go new file mode 100644 index 0000000..85cc891 --- /dev/null +++ b/internal/notifywatcher/hash.go @@ -0,0 +1,12 @@ +package notifywatcher + +import ( + "crypto/sha256" + "encoding/hex" +) + +// sha256Hex 计算字符串的 SHA-256 十六进制(指纹去重键)。 +func sha256Hex(s string) string { + h := sha256.Sum256([]byte(s)) + return hex.EncodeToString(h[:]) +} diff --git a/internal/notifywatcher/notifywatcher.go b/internal/notifywatcher/notifywatcher.go new file mode 100644 index 0000000..f5e7d2d --- /dev/null +++ b/internal/notifywatcher/notifywatcher.go @@ -0,0 +1,221 @@ +// Package notifywatcher 通知栏快速通道监听(独立于 ledgermon 的账本慢通道)。 +// +// 背景(2026-08-30 真机实测 + 用户需求确认): +// - 微信/支付宝收款到账会弹系统通知(dumpsys notification --noredact 可读全文); +// - 通知栏比账本页快 ~15s,是收款感知的快速通道; +// - 通知栏数据不清理会一直存在 → 匹配窗口必须以通知 when(收款发生时刻)为锚, +// 不能用 time.Now()(否则旧通知会错误匹配新订单); +// - 账户区分靠 uid:微信/支付宝主空间 uid 无 999 前缀,双开(XSpace) uid 有 999 前缀 +// (实测:com.tencent.mm uid=10277 主号 / uid=99910277 双开;支付宝同理 10250/99910250)。 +// +// 本包只负责:设备枚举 → 按设备轮询 dumpsys notification --noredact → 解析 → +// 指纹去重 → 回调 main.go 注入的匹配入账函数(复用 matchPersonalOrder 链路)。 +// 不直接触碰 UI/账本页,与 ledgermon 的 DeviceActor 串行队列零竞争。 +package notifywatcher + +import ( + "context" + "sync" + "time" + + "paygateway/internal/logger" +) + +// 默认轮询参数(Config 未设置时兜底)。 +const ( + defaultPollEvery = 500 * time.Millisecond // 有 WAIT_PAY 订单时的高频轮询 + defaultIdleEvery = 5 * time.Second // 池空时的低频探测 + defaultShellTimeout = 3 * time.Second // 单次 dumpsys 超时 +) + +// Device 设备级监听单元(由 DeviceResolver 枚举,main.go 注入)。 +type Device struct { + DeviceID string // personal.devices.device_id + Serial string // adb serial(host:port 或 USB 序列号) +} + +// DeviceResolver 在线设备枚举(main.go 注入:ListDevices + adb.Online 过滤)。 +type DeviceResolver func(ctx context.Context) ([]Device, error) + +// HasPending 是否仍有待支付订单(main.go 注入:waitPayPool.Snapshot().Total > 0)。 +// 池 reconcile 间隙可能短暂为空,调用方用低频探测兜底。 +type HasPending func(ctx context.Context) bool + +// Handler 收款通知回调(main.go 注入:复用 matchPersonalOrder 入账链路)。 +// 返回 error 表示处理失败(记录日志,不影响轮询继续)。 +type Handler func(ctx context.Context, ev Event) error + +// Sheller adb 命令执行抽象(生产 = *adbmgr.Manager;测试 = fake 桩)。 +// 只暴露 dumpsys 所需的 Shell 方法,与 ledgermon 的 UI 操作零交集。 +type Sheller interface { + Shell(ctx context.Context, timeout time.Duration, serial, command string) (string, error) +} + +// SeenStore 去重存储(main.go 注入:personalDB.PushExists)。 +// 指纹含设备ID+通知key+金额+when,同一通知跨轮幂等。 +type SeenStore interface { + PushExists(ctx context.Context, deviceID, rawHash string) (bool, error) +} + +// Config 轮询参数(零值使用默认)。 +type Config struct { + PollEvery time.Duration // 有 WAIT_PAY 订单轮询间隔(默认 500ms) + IdleEvery time.Duration // 池空探测间隔(默认 5s) + ShellTimeout time.Duration // 单次 dumpsys 超时(默认 3s) +} + +// Manager 通知栏快速通道监听器。 +// +// 与 ledgermon 的关系(架构决策,用户确认): +// - 完全独立:自己的 goroutine + 自己的 adbMgr() 实例(Manager.mu 独立 → 天然并行, +// 实测 dumpsys notification ∥ uiautomator dump ∥ input 互不阻塞); +// - 订单驱动:waitPayPool 有订单 → 高频轮询;池空 → 低频探测,不空转; +// - 匹配回调复用 main.go 的 matchPersonalOrder(空间账户过滤 + 金额时间窗 + 冻结守卫), +// 与账本链路共享同一套资金安全逻辑,绝不另起炉灶。 +type Manager struct { + adb Sheller + resolve DeviceResolver + hasPendFn HasPending // 订单驱动判定(nil 视为恒有空闲) + handler Handler + seen SeenStore + cfg Config + wake chan struct{} // 事件唤醒信号(新订单进池触发,容量1信号合并) + + mu sync.Mutex + lastPoll map[string]time.Time // deviceID → 上次轮询时刻(日志节流) +} + +// New 创建通知栏监听器。adb 为 nil 时禁用(设备枚举阶段直接返回)。 +func New(adb Sheller, resolve DeviceResolver, hasPend HasPending, + handler Handler, seen SeenStore, cfg Config) *Manager { + if cfg.PollEvery <= 0 { + cfg.PollEvery = defaultPollEvery + } + if cfg.IdleEvery <= 0 { + cfg.IdleEvery = defaultIdleEvery + } + if cfg.ShellTimeout <= 0 { + cfg.ShellTimeout = defaultShellTimeout + } + return &Manager{ + adb: adb, resolve: resolve, hasPendFn: hasPend, handler: handler, + seen: seen, cfg: cfg, lastPoll: make(map[string]time.Time), + wake: make(chan struct{}, 1), + } +} + +// Wake 事件唤醒:新订单进入 WAIT_PAY 池时调用(main.go 经 SetWaitPayHooks.Add 链入)。 +// 池空休眠态立即醒来轮询一轮;轮询态发送不阻塞(信号合并)。 +func (m *Manager) Wake() { + select { + case m.wake <- struct{}{}: + default: // 已有待消费信号,合并 + } +} + +// Start 启动监听循环(阻塞,调用方 go 启动)。 +// +// 事件驱动(2026-08-30 用户确认):池空 → 完全停止轮询(零空转),阻塞等待 +// Wake 事件或低频兜底定时器;新订单进池(Wake)→ 立即轮询,无间隔延迟。 +// idle 兜底:应对池快照与 DB 短暂不一致(reconcile 间隙),防漏唤醒长期休眠。 +func (m *Manager) Start(ctx context.Context) { + if m.adb == nil || m.resolve == nil || m.handler == nil { + logger.Warn("notifywatcher", "依赖缺失,通知栏监听未启动", + "adb", m.adb != nil, "resolve", m.resolve != nil, "handler", m.handler != nil) + return + } + logger.Info("notifywatcher", "通知栏快速通道监听已启动(事件驱动)", + "poll", m.cfg.PollEvery.String(), "idle", m.cfg.IdleEvery.String()) + + for { + // 订单驱动:池有订单才轮询;池空 → 阻塞等事件/兜底定时器 + if m.hasPendFn != nil && m.hasPendFn(ctx) { + m.pollOnce(ctx) + select { + case <-ctx.Done(): + logger.Info("notifywatcher", "通知栏监听已停止") + return + case <-m.wake: + // 唤醒信号:直接继续下一轮(高频路径) + case <-time.After(m.cfg.PollEvery): + } + continue + } + // 池空休眠:等新订单事件(即时唤醒)或低频兜底探测(防 reconcile 间隙漏唤醒) + select { + case <-ctx.Done(): + logger.Info("notifywatcher", "通知栏监听已停止") + return + case <-m.wake: + case <-time.After(m.cfg.IdleEvery): + } + } +} + +// pollOnce 单轮:枚举设备 → 逐台 dump 通知栏 → 解析 → 去重 → 回调。 +func (m *Manager) pollOnce(ctx context.Context) { + devs, err := m.resolve(ctx) + if err != nil { + logger.Warn("notifywatcher", "设备枚举失败(下轮重试)", "err", err) + return + } + for _, d := range devs { + if err := m.pollDevice(ctx, d); err != nil { + // 单设备失败不中断其他设备 + logger.Warn("notifywatcher", "设备通知轮询失败", + "err", err, "device_id", d.DeviceID, "serial", d.Serial) + } + } +} + +// pollDevice 单设备:dump → 解析 → 逐条去重回调。 +func (m *Manager) pollDevice(ctx context.Context, d Device) error { + if d.Serial == "" { + return nil + } + out, err := m.adb.Shell(ctx, m.cfg.ShellTimeout, d.Serial, "dumpsys notification --noredact") + if err != nil { + return err + } + evs := Parse(out, d.DeviceID) + if len(evs) == 0 { + return nil + } + for i := range evs { + ev := evs[i] + // 指纹去重:同设备同通知只处理一次(通知栏不清理会一直在) + fp := ev.Fingerprint() + if m.seen != nil { + dup, derr := m.seen.PushExists(ctx, d.DeviceID, fp) + if derr != nil { + logger.Error("notifywatcher", "去重查询失败(按未处理继续)", + "err", derr, "device_id", d.DeviceID) + } else if dup { + continue + } + } + // 节流日志:同设备每秒最多一条(防止池空时高频刷屏) + m.throttledLog(d.DeviceID, ev) + if err := m.handler(ctx, ev); err != nil { + logger.Error("notifywatcher", "收款通知处理失败", + "err", err, "device_id", d.DeviceID, + "provider", ev.Provider, "space", ev.Space, "amount", ev.AmountMinor) + continue + } + } + return nil +} + +// throttledLog 通知捕获日志(每设备 1s 节流)。 +func (m *Manager) throttledLog(deviceID string, ev Event) { + m.mu.Lock() + defer m.mu.Unlock() + now := time.Now() + if last, ok := m.lastPoll[deviceID]; ok && now.Sub(last) < time.Second { + return + } + m.lastPoll[deviceID] = now + logger.Info("notifywatcher", "捕获收款通知", + "device_id", deviceID, "provider", ev.Provider, + "space", ev.Space, "amount_minor", ev.AmountMinor, "when", ev.When) +} diff --git a/internal/notifywatcher/notifywatcher_test.go b/internal/notifywatcher/notifywatcher_test.go new file mode 100644 index 0000000..bdbb7ae --- /dev/null +++ b/internal/notifywatcher/notifywatcher_test.go @@ -0,0 +1,195 @@ +package notifywatcher + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" +) + +// fakeADB 注入 adb Shell 输出的桩(实现 Sheller 接口)。 +type fakeADB struct { + mu sync.Mutex + calls int + out string +} + +func (f *fakeADB) Shell(ctx context.Context, timeout time.Duration, serial, command string) (string, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.calls++ + return f.out, nil +} + +// fakeSeen 去重存储桩(实现 SeenStore 接口)。 +type fakeSeen struct { + dupe bool +} + +func (f *fakeSeen) PushExists(ctx context.Context, deviceID, rawHash string) (bool, error) { + return f.dupe, nil +} + +func TestNewDefaults(t *testing.T) { + m := New(nil, nil, nil, nil, nil, Config{}) + if m.cfg.PollEvery != defaultPollEvery { + t.Errorf("PollEvery = %v, 期望默认 %v", m.cfg.PollEvery, defaultPollEvery) + } + if m.cfg.IdleEvery != defaultIdleEvery { + t.Errorf("IdleEvery = %v, 期望默认 %v", m.cfg.IdleEvery, defaultIdleEvery) + } + if m.cfg.ShellTimeout != defaultShellTimeout { + t.Errorf("ShellTimeout = %v, 期望默认 %v", m.cfg.ShellTimeout, defaultShellTimeout) + } +} + +func TestPollOnce_CallsDumpAndHandler(t *testing.T) { + adb := &fakeADB{out: realWechatMain} + var mu sync.Mutex + handled := 0 + m := New(adb, func(ctx context.Context) ([]Device, error) { + return []Device{{DeviceID: "dev-01", Serial: "1.2.3.4:5555"}}, nil + }, nil, func(ctx context.Context, ev Event) error { + mu.Lock() + defer mu.Unlock() + handled++ + if ev.Provider != "wechat" || ev.AmountMinor != 10 { + t.Errorf("回调参数异常: %+v", ev) + } + return nil + }, &fakeSeen{}, Config{}) + + m.pollOnce(context.Background()) + mu.Lock() + defer mu.Unlock() + if handled != 1 { + t.Errorf("期望回调 1 次,实际 %d", handled) + } +} + +func TestPollOnce_Dedup(t *testing.T) { + adb := &fakeADB{out: realWechatMain} + handled := 0 + m := New(adb, func(ctx context.Context) ([]Device, error) { + return []Device{{DeviceID: "dev-01", Serial: "s1"}}, nil + }, nil, func(ctx context.Context, ev Event) error { + handled++ + return nil + }, &fakeSeen{dupe: true}, Config{}) + + m.pollOnce(context.Background()) + if handled != 0 { + t.Errorf("已去重通知不应回调,实际 %d 次", handled) + } +} + +func TestPollOnce_MultipleEvents(t *testing.T) { + adb := &fakeADB{out: realWechatMain + "\n" + realAlipay} + handled := make(map[string]int) + var mu sync.Mutex + m := New(adb, func(ctx context.Context) ([]Device, error) { + return []Device{{DeviceID: "dev-01", Serial: "s1"}}, nil + }, nil, func(ctx context.Context, ev Event) error { + mu.Lock() + defer mu.Unlock() + handled[ev.Provider]++ + return nil + }, &fakeSeen{}, Config{}) + + m.pollOnce(context.Background()) + mu.Lock() + defer mu.Unlock() + if handled["wechat"] != 1 || handled["alipay"] != 1 { + t.Errorf("期望微信/支付宝各 1 次,实际 %+v", handled) + } +} + +func TestPollOnce_HandlerError(t *testing.T) { + adb := &fakeADB{out: realWechatMain} + calls := 0 + m := New(adb, func(ctx context.Context) ([]Device, error) { + return []Device{{DeviceID: "dev-01", Serial: "s1"}}, nil + }, nil, func(ctx context.Context, ev Event) error { + calls++ + return context.DeadlineExceeded + }, &fakeSeen{}, Config{}) + + m.pollOnce(context.Background()) + if calls != 1 { + t.Errorf("回调应被调用 1 次,实际 %d", calls) + } +} + +func TestStart_OrderDrivenInterval(t *testing.T) { + // 有设备枚举测量轮询频率(adb.calls 反映实际 dump 次数) + adb := &fakeADB{out: ""} + var orderDriven atomic.Bool + orderDriven.Store(true) + hasPend := func(ctx context.Context) bool { return orderDriven.Load() } + + m := New(adb, func(ctx context.Context) ([]Device, error) { + return []Device{{DeviceID: "dev-01", Serial: "s1"}}, nil + }, hasPend, func(ctx context.Context, ev Event) error { return nil }, + nil, Config{PollEvery: 30 * time.Millisecond, IdleEvery: 200 * time.Millisecond}) + + ctx, cancel := context.WithCancel(context.Background()) + go m.Start(ctx) + + time.Sleep(400 * time.Millisecond) + adb.mu.Lock() + activeCalls := adb.calls + adb.mu.Unlock() + + // 转池空 → 完全停止轮询(事件驱动休眠,仅 idle 兜底 200ms 一次) + orderDriven.Store(false) + time.Sleep(400 * time.Millisecond) + adb.mu.Lock() + afterCalls := adb.calls + adb.mu.Unlock() + + // 事件唤醒:真实时序 = 订单先入池(hasPend 变 true)再 Wake → 休眠态立即醒来轮询 + orderDriven.Store(true) // 订单入池 + m.Wake() // 通知栏监听被唤醒 + time.Sleep(150 * time.Millisecond) + adb.mu.Lock() + wakeAfter := adb.calls + adb.mu.Unlock() + + cancel() + time.Sleep(50 * time.Millisecond) + + if activeCalls < 5 { + t.Errorf("有订单阶段 dump 过少: %d(期望高频 ≥5)", activeCalls) + } + // 池空 400ms:idle 兜底 200ms 一次 → 最多 2-3 次(事件驱动下基本停止) + if afterCalls-activeCalls > 4 { + t.Errorf("池空阶段 dump 过多: %d(期望休眠低频)", afterCalls-activeCalls) + } + // 订单入池 + Wake 后:150ms 内应恢复高频轮询(事件驱动即时响应) + if wakeAfter-activeCalls < 1 { + t.Errorf("Wake 后未恢复轮询: before=%d after=%d", activeCalls, wakeAfter) + } +} + +func TestStart_Cancel(t *testing.T) { + adb := &fakeADB{out: ""} + m := New(adb, func(ctx context.Context) ([]Device, error) { + return []Device{{DeviceID: "dev-01", Serial: "s1"}}, nil + }, nil, func(ctx context.Context, ev Event) error { return nil }, + nil, Config{PollEvery: 10 * time.Millisecond, IdleEvery: 10 * time.Millisecond}) + + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + m.Start(ctx) + close(done) + }() + time.Sleep(50 * time.Millisecond) + cancel() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Error("Start 未在 cancel 后退出") + } +} diff --git a/internal/notifywatcher/parse.go b/internal/notifywatcher/parse.go new file mode 100644 index 0000000..e616ba7 --- /dev/null +++ b/internal/notifywatcher/parse.go @@ -0,0 +1,226 @@ +// Package notifywatcher 通知栏快速通道监听(独立于 ledgermon 的账本慢通道)。 +// +// 背景(2026-08-30 真机实测 + 用户需求确认): +// - 微信/支付宝收款到账会弹系统通知(dumpsys notification --noredact 可读全文); +// - 通知栏比账本页快 ~15s,是收款感知的快速通道; +// - 通知栏数据不清理会一直存在 → 匹配窗口必须以通知 when(收款发生时刻)为锚, +// 不能用 time.Now()(否则旧通知会错误匹配新订单); +// - 账户区分靠 uid:微信/支付宝主空间 uid 无 999 前缀,双开(XSpace) uid 有 999 前缀 +// (实测:com.tencent.mm uid=10277 主号 / uid=99910277 双开;支付宝同理 10250/99910250)。 +// +// 本包只负责:设备枚举 → 按设备轮询 dumpsys notification --noredact → 解析 → +// 指纹去重 → 回调 main.go 注入的匹配入账函数(复用 matchPersonalOrder 链路)。 +// 不直接触碰 UI/账本页,与 ledgermon 的 DeviceActor 串行队列零竞争。 +package notifywatcher + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +// 收款通知金额提取正则(微信/支付宝实测格式): +// - 微信: "微信支付收款0.10元(朋友到店)" → 文本含 "收款0.10元" +// - 支付宝: "你已成功收款0.10元(老顾客消费)" → 标题含 "收款0.10元" +// +// 兼容 "收款 1 元"、"到账 12.5元" 等变体(贪心取收款/到账后第一个金额)。 +var amountRe = regexp.MustCompile(`(?:收款|到账)\s*([0-9]+(?:\.[0-9]{1,2})?)元`) + +// 目标包名:微信 / 支付宝(个人免签通道对应的收款 APP)。 +const ( + pkgWechat = "com.tencent.mm" + pkgAlipay = "com.eg.android.AlipayGphone" +) + +// Event 单条收款通知的解析结果(设备级:属于哪台设备、哪个空间)。 +type Event struct { + DeviceID string // 设备ID(personal.devices.device_id,枚举时携带) + Provider string // "wechat" / "alipay" + Space int // 0=主空间,999=双开(XSpace) + AmountMinor int64 // 金额(分) + When int64 // 通知创建时间(when= 毫秒时间戳,收款发生时刻) + Key string // 系统通知 key(0|pkg|id|tag|uid,同通知轮询去重基准之一) + Title string // 通知标题(审计) + Text string // 通知正文(审计) + Raw string // 指纹原文(审计;含设备ID保证跨设备隔离) +} + +// Fingerprint 通知指纹:同设备+同通知(key)+同金额+同时刻 只处理一次。 +// when 来自通知系统时间戳,同一条通知每次 dump 不变 → 幂等去重可靠。 +func (e *Event) Fingerprint() string { + raw := fmt.Sprintf("notify:%s|%s|%d|%s|%d|%d", e.DeviceID, e.Provider, e.Space, e.Key, e.AmountMinor, e.When) + return sha256Hex(raw) +} + +// Parse 解析 dumpsys notification --noredact 输出,返回该设备的所有收款通知。 +// deviceID 由调用方传入(同一份输出只会属于一台设备)。 +// 规则: +// 1. 只关心 pkg ∈ {微信, 支付宝} 的 NotificationRecord; +// 2. 从 user=UserHandle{0}/{999} 判定空间; +// 3. 从标题/正文提取金额(收款/到账 XX元),提取不到金额的跳过(非收款通知); +// 4. 同一条通知被 Android 聚合/折叠时可能产生多条记录,按 (key,when) 幂等去重。 +func Parse(out string, deviceID string) []Event { + var events []Event + seen := make(map[string]struct{}) + + // 按 NotificationRecord( 分块解析:块内收集 pkg/user/key/title/text/when + for _, block := range splitRecords(out) { + pkg := fieldOf(block, "pkg=") + if pkg != pkgWechat && pkg != pkgAlipay { + continue + } + provider := "wechat" + if pkg == pkgAlipay { + provider = "alipay" + } + // 空间:user=UserHandle{999} → 双开;user=UserHandle{0} → 主空间 + space := 0 + if m := regexp.MustCompile(`user=UserHandle\{(\d+)\}`).FindStringSubmatch(block); len(m) == 2 { + if v, err := strconv.Atoi(m[1]); err == nil { + space = v + } + } + // 通知 key:NotificationRecord(0x...: pkg=... key=0|pkg|id|tag|uid) + key := fieldOf(block, "key=") + // when 毫秒时间戳 + var when int64 + if m := regexp.MustCompile(`when=(\d+)`).FindStringSubmatch(block); len(m) == 2 { + when, _ = strconv.ParseInt(m[1], 10, 64) + } + title, text := textsOf(block) + // 金额:优先标题(支付宝在标题,微信标题也可能含),再正文 + amount := extractAmount(title) + if amount <= 0 { + amount = extractAmount(text) + } + if amount <= 0 { + continue // 非收款通知(无金额)或金额格式未知 → 不处理 + } + if when <= 0 || key == "" { + continue // 缺时间戳/缺key 的畸形记录不处理 + } + // 幂等:同一 (key,when) 只保留一条(聚合折叠场景) + uid := key + "|" + strconv.FormatInt(when, 10) + if _, ok := seen[uid]; ok { + continue + } + seen[uid] = struct{}{} + + events = append(events, Event{ + DeviceID: deviceID, + Provider: provider, + Space: space, + AmountMinor: amount, + When: when, + Key: key, + Title: title, + Text: text, + Raw: fmt.Sprintf("notify:%s|%s|%d|%s", deviceID, provider, space, key), + }) + } + return events +} + +// splitRecords 按 NotificationRecord( 拆分通知块(每块含完整通知字段)。 +func splitRecords(out string) []string { + var blocks []string + cur := strings.Builder{} + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, "NotificationRecord(") { + if cur.Len() > 0 { + blocks = append(blocks, cur.String()) + } + cur.Reset() + } + cur.WriteString(line) + cur.WriteString("\n") + } + if cur.Len() > 0 { + blocks = append(blocks, cur.String()) + } + return blocks +} + +// fieldOf 提取形如 "name=value" 的字段值(value 到下一个空格/冒号为止)。 +func fieldOf(block, prefix string) string { + i := strings.Index(block, prefix) + if i < 0 { + return "" + } + rest := block[i+len(prefix):] + end := strings.IndexAny(rest, " \n") + if end < 0 { + return rest + } + return rest[:end] +} + +// textsOf 提取通知标题与正文(android.title / android.text 的 String(...) 内容)。 +func textsOf(block string) (title, text string) { + title = quotedStringOf(block, "android.title=String (") + text = quotedStringOf(block, "android.text=String (") + // 标题缺失时用 tickerText 兜底(tickerText=微信收款助手: 微信支付收款0.10元) + if title == "" { + if t := quotedStringOf(block, "tickerText="); t != "" { + if i := strings.Index(t, ": "); i >= 0 { + title = t[:i] + if text == "" { + text = t[i+2:] + } + } else { + title = t + } + } + } + return title, text +} + +// quotedStringOf 提取 "prefix<内容>)" 形式的值(内容到最近右括号)。 +func quotedStringOf(block, prefix string) string { + i := strings.Index(block, prefix) + if i < 0 { + return "" + } + rest := block[i+len(prefix):] + j := strings.Index(rest, ")") + if j < 0 { + return rest + } + return rest[:j] +} + +// extractAmount 从通知文本提取金额(分)。"收款0.10元" → 10。 +func extractAmount(s string) int64 { + m := amountRe.FindStringSubmatch(s) + if len(m) != 2 { + return 0 + } + return amountToMinor(m[1]) +} + +// amountToMinor 金额字符串转分("0.10" → 10,"1" → 100,"1.5" → 150)。 +func amountToMinor(s string) int64 { + var major, minor int64 + if _, err := fmt.Sscanf(s, "%d.%d", &major, &minor); err != nil { + // 无小数:整数元 + if v, err := strconv.ParseInt(s, 10, 64); err == nil { + return v * 100 + } + return 0 + } + if i := strings.IndexByte(s, '.'); i >= 0 { + frac := s[i+1:] + switch len(frac) { + case 1: + minor *= 10 + case 2: + // 保持原样 + default: + minor = 0 + } + } else { + minor = 0 + } + return major*100 + minor +} diff --git a/internal/notifywatcher/parse_test.go b/internal/notifywatcher/parse_test.go new file mode 100644 index 0000000..4b76b36 --- /dev/null +++ b/internal/notifywatcher/parse_test.go @@ -0,0 +1,235 @@ +package notifywatcher + +import ( + "strings" + "testing" +) + +// 真机实测数据(2026-08-30,红米 k60 MIUI14 Android 13)。 +// 数据来源:dumpsys notification --noredact 实测输出(转账 0.10 元触发微信/支付宝收款通知)。 + +// realWechatMain 微信主号收款通知(uid=10277,文本含"(朋友到店)"场景标签)。 +const realWechatMain = ` NotificationRecord(0x0eb536c3: pkg=com.tencent.mm user=UserHandle{0} id=-124356843 tag=null importance=4 key=0|com.tencent.mm|-124356843|null|10277: Notification(channel=message_channel_new_id shortcut=null contentView=null vibrate=null sound=null tick defaults=0x0 flags=0x11 color=0x00000000 category=msg vis=PRIVATE)) + uid=10277 userId=0 + opPkg=com.tencent.mm + icon=Icon(typ=RESOURCE pkg=com.tencent.mm id=0x7f080bc8) + flags=0x11 + originalFlags=0x11 + pri=1 + key=0|com.tencent.mm|-124356843|null|10277 + seen=true + groupKey=0|com.tencent.mm|-124356843|null|10277 + notification= + fullscreenIntent=null + contentIntent=PendingIntent{7a9eb40: PendingIntentRecord{e3b9b0e com.tencent.mm startActivity (allowlist: 8cc672e:+30s0ms/0/NOTIFICATION_SERVICE/NotificationManagerService)}} + deleteIntent=null + number=700 + groupAlertBehavior=0 + when=1788083827846 + tickerText=微信收款助手: 微信支付收款0.10元(朋友到店) + contentView=null + bigContentView=null + headsUpContentView=null + color=0x00000000 + timeout=unknown + extras={ + android.title=String (微信收款助手) + android.reduced.images=Boolean (true) + android.subText=null + android.showChronometer=Boolean (false) + android.text=String (微信支付收款0.10元(朋友到店)) + android.progress=Integer (0) + android.progressMax=Integer (0) + android.appInfo=ApplicationInfo (ApplicationInfo{c86da79 com.tencent.mm}) + android.showWhen=Boolean (true) + android.largeIcon=Icon (Icon(typ=BITMAP size=130x130)) + android.infoText=null + android.progressIndeterminate=Boolean (false) + android.remoteInputHistory=null + }` + +// realWechatDual 微信双开(XSpace)收款通知(uid=99910277,聚合 [2条])。 +const realWechatDual = ` NotificationRecord(0x0e9213be: pkg=com.tencent.mm user=UserHandle{999} id=-124356843 tag=null importance=4 key=999|com.tencent.mm|-124356843|null|99910277: Notification(channel=message_channel_new_id shortcut=null contentView=null vibrate=null sound=null tick defaults=0x0 flags=0x11 color=0x00000000 category=msg vis=PRIVATE)) + uid=99910277 userId=999 + icon=Icon(typ=RESOURCE pkg=com.tencent.mm id=0x7f080bc8) + pri=1 + key=999|com.tencent.mm|-124356843|null|99910277 + when=1788083804845 + tickerText=微信收款助手: 微信支付收款0.10元 + extras={ + android.title=String (微信收款助手) + android.subText=null + android.text=String ([2条]微信收款助手: 微信支付收款0.10元) + android.showWhen=Boolean (true) + android.infoText=null + }` + +// realAlipay 支付宝收款通知(uid=10250,标题含"(老顾客消费)"场景标签)。 +const realAlipay = ` NotificationRecord(0x03b84a6f: pkg=com.eg.android.AlipayGphone user=UserHandle{0} id=1223552217 tag=null importance=4 key=0|com.eg.android.AlipayGphone|1223552217|null|10250: Notification(channel=alipay_default shortcut=null contentView=null vibrate=null sound=null tick defaults=0x4 flags=0x11 color=0x00000000 groupKey=com.eg.android.AlipayGphone.POPULAR_EVENT_GROUP vis=PRIVATE)) + uid=10250 userId=0 + when=1788083627490 + tickerText=已转入余额 发起店铺充值活动>> + extras={ + android.title=String (你已成功收款0.10元(老顾客消费)) + android.text=String (已转入余额 发起店铺充值活动>>) + android.showWhen=Boolean (true) + }` + +// realAlipayDual 支付宝双开(XSpace)收款通知(uid=99910250)。 +const realAlipayDual = ` NotificationRecord(0x0d7d01ac: pkg=com.eg.android.AlipayGphone user=UserHandle{999} id=1027977183 tag=null importance=4 key=999|com.eg.android.AlipayGphone|1027977183|null|99910250: Notification(channel=alipay_default shortcut=null contentView=null vibrate=null sound=null tick defaults=0x4 flags=0x11 color=0x00000000 groupKey=com.eg.android.AlipayGphone.POPULAR_EVENT_GROUP vis=PRIVATE)) + uid=99910250 userId=999 + when=1788083627491 + extras={ + android.title=String (你已成功收款0.10元) + android.text=String (已转入余额) + }` + +// nonPaymentAlipay 支付宝非收款通知(活动/营销,无"收款"文本,应被跳过)。 +const nonPaymentAlipay = ` NotificationRecord(0x03c8c250: pkg=com.eg.android.AlipayGphone user=UserHandle{0} id=2049664768 tag=null importance=4 key=0|com.eg.android.AlipayGphone|2049664768|null|10250: Notification(channel=alipay_default shortcut=null contentView=null vibrate=null sound=null defaults=0x0 flags=0x200 color=0x00000000 groupKey=com.eg.android.AlipayGphone.POPULAR_EVENT_GROUP vis=PRIVATE)) + uid=10250 userId=0 + when=1788083628051 + extras={ + android.title=String (活动通知) + android.text=String (限时优惠) + }` + +// unrelated 其他 APP 通知(MIUI 系统/视频),应被忽略。 +const unrelated = ` NotificationRecord(0x0015f4bf: pkg=com.android.updater user=UserHandle{0} id=0 tag=null importance=5 key=0|com.android.updater|0|null|6102: Notification(channel=updater_channel_id shortcut=null contentView=null vibrate=null sound=null tick defaults=0x4 flags=0x22 color=0x00000000 vis=PRIVATE)) + uid=6102 userId=0 + when=1788080945841 + extras={ + android.title=String (发现系统有新版本) + android.text=String (1.0.16.0.UMNCNXM) + }` + +func TestParse_WechatMain(t *testing.T) { + evs := Parse(realWechatMain, "dev-01") + if len(evs) != 1 { + t.Fatalf("期望 1 条事件,实际 %d", len(evs)) + } + e := evs[0] + if e.Provider != "wechat" { + t.Errorf("provider = %q, 期望 wechat", e.Provider) + } + if e.Space != 0 { + t.Errorf("space = %d, 期望 0(主空间)", e.Space) + } + if e.AmountMinor != 10 { + t.Errorf("amount = %d, 期望 10(0.10元)", e.AmountMinor) + } + if e.When != 1788083827846 { + t.Errorf("when = %d, 期望 1788083827846", e.When) + } + if !strings.Contains(e.Key, "com.tencent.mm") { + t.Errorf("key 异常: %q", e.Key) + } + // 指纹稳定:同一事件两次生成必须相同 + if e.Fingerprint() != evs[0].Fingerprint() { + t.Error("指纹不稳定") + } +} + +func TestParse_WechatDual(t *testing.T) { + evs := Parse(realWechatDual, "dev-01") + if len(evs) != 1 { + t.Fatalf("期望 1 条事件,实际 %d", len(evs)) + } + e := evs[0] + if e.Provider != "wechat" { + t.Errorf("provider = %q, 期望 wechat", e.Provider) + } + if e.Space != 999 { + t.Errorf("space = %d, 期望 999(双开)", e.Space) + } + if e.AmountMinor != 10 { + t.Errorf("amount = %d, 期望 10", e.AmountMinor) + } +} + +func TestParse_Alipay(t *testing.T) { + evs := Parse(realAlipay, "dev-01") + if len(evs) != 1 { + t.Fatalf("期望 1 条事件,实际 %d", len(evs)) + } + e := evs[0] + if e.Provider != "alipay" { + t.Errorf("provider = %q, 期望 alipay", e.Provider) + } + if e.Space != 0 { + t.Errorf("space = %d, 期望 0", e.Space) + } + if e.AmountMinor != 10 { + t.Errorf("amount = %d, 期望 10", e.AmountMinor) + } +} + +func TestParse_AlipayDual(t *testing.T) { + evs := Parse(realAlipayDual, "dev-01") + if len(evs) != 1 { + t.Fatalf("期望 1 条事件,实际 %d", len(evs)) + } + e := evs[0] + if e.Space != 999 { + t.Errorf("space = %d, 期望 999", e.Space) + } + if e.AmountMinor != 10 { + t.Errorf("amount = %d, 期望 10", e.AmountMinor) + } +} + +func TestParse_SkipNonPayment(t *testing.T) { + // 非收款通知(活动/营销/其他APP)→ 0 条 + evs := Parse(nonPaymentAlipay+unrelated, "dev-01") + if len(evs) != 0 { + t.Errorf("期望 0 条事件(非收款通知应跳过),实际 %d", len(evs)) + } +} + +func TestParse_MixedFullDump(t *testing.T) { + // 完整通知栏混合输出:微信主+双开、支付宝主+双开、非收款、其他APP + out := strings.Join([]string{realWechatMain, realWechatDual, realAlipay, realAlipayDual, + nonPaymentAlipay, unrelated}, "\n") + evs := Parse(out, "dev-01") + if len(evs) != 4 { + t.Fatalf("期望 4 条收款事件,实际 %d", len(evs)) + } + // 设备ID必须写入(跨设备隔离) + for _, e := range evs { + if e.DeviceID != "dev-01" { + t.Errorf("device_id 未写入: %q", e.DeviceID) + } + } +} + +func TestParse_DedupAggregated(t *testing.T) { + // 同一条通知在 dump 中重复出现(模拟轮询间隔内通知未变)→ 幂等 + out := realWechatMain + "\n" + realWechatMain + evs := Parse(out, "dev-01") + if len(evs) != 1 { + t.Errorf("重复记录应去重为 1 条,实际 %d", len(evs)) + } +} + +func TestExtractAmount(t *testing.T) { + cases := []struct { + text string + want int64 + }{ + {"微信支付收款0.10元(朋友到店)", 10}, + {"你已成功收款0.10元(老顾客消费)", 10}, + {"[2条]微信收款助手: 微信支付收款0.10元", 10}, + {"收款1元", 100}, + {"收款 12.5元", 1250}, + {"到账 100.00元", 10000}, + {"收款0.10", 0}, // 无"元"单位 → 不匹配(避免误提取) + {"已转入余额", 0}, // 无金额 + {"限时优惠", 0}, // 非收款 + {"充值10元话费", 0}, // 有"元"但非收款/到账语境 → 不匹配 + } + for _, c := range cases { + got := extractAmount(c.text) + if got != c.want { + t.Errorf("extractAmount(%q) = %d, 期望 %d", c.text, got, c.want) + } + } +} diff --git a/internal/order/service.go b/internal/order/service.go new file mode 100755 index 0000000..34a041e --- /dev/null +++ b/internal/order/service.go @@ -0,0 +1,1310 @@ +// Package order 订单编排层:状态机 / 幂等 / 金额校验 / 审计账本 +package order + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "paygateway/internal/account" + "paygateway/internal/alert" + "paygateway/internal/channel" + "paygateway/internal/epay" + "paygateway/internal/ledger" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/store" + "paygateway/internal/upstream" +) + +// Service 订单服务 +type Service struct { + store store.Store + ledger *ledger.Ledger + accountPool *account.AccountPool // 账户池(日志/展示账户名称) + upstreamPool *upstream.UpstreamPool // 上游池(多商户:按 pid 路由验签/签名) + alert *alert.Client // 告警(RESILIENCE P1-3,nil=禁用) + + // 可选:个人通道备注码释放回调(main.go 注入,订单终态时释放备注码,避免 TTL 惰性回收) + noteCodeReleaser func(channelName, noteCode string) + + // WaitPayHooks WAIT_PAY 内存订单池钩子(main.go 注入,可选): + // 个人免签通道订单进入/离开 WAIT_PAY 状态时通知池(调度器数据源),保证池与 DB 同步。 + // 回调在状态流转成功后调用(幂等),不参与资金判定——池只是索引,DB 是真相。 + waitPay *WaitPayHooks + + // 个人通道监听端检查(main.go 注入,与 reconciler.Scheduler.SetPersonalCheck 共用同一函数): + // 个人/监听通道关单前调用,监听端离线 → 进异常池人工核实,绝不误关已付款订单。 + personalCheck PersonalCheckFunc + + // 关单窗口(ORDER_CLOSE_MINUTES,0=未开启自动关单):复用订单时判断是否已超窗 + closeWindow time.Duration +} + +// WaitPayHooks WAIT_PAY 池同步钩子(注入后非 nil 才生效) +type WaitPayHooks struct { + // Add 订单进入 WAIT_PAY 且已绑定账户(下单/复活后调用,幂等覆盖) + Add func(o *model.Order) + // Upsert 订单凭据刷新/换通道(NoteCode/AccountID 可能变化,覆盖更新) + Upsert func(o *model.Order) + // Remove 订单离开 WAIT_PAY(入账/关单后调用,幂等) + Remove func(outTradeNo string) + // CloseAtOf 查询订单的动态关单时间(pool 注入,可选):返回 (closeAt, found)。 + // 动态关单窗口下,扫描器用该时间判断是否真正到期——未到期则跳过(窗口由设备负载决定)。 + CloseAtOf func(outTradeNo string) (time.Time, bool) +} + +// PersonalCheckFunc 个人通道监听端检查(ConfirmByManual 通道关单前调用)。 +// 返回 (offline, reason):offline=true 表示监听端离线/异常,订单进异常池人工核实(用户可能已付款但未监听到)。 +type PersonalCheckFunc func(ctx context.Context, o *model.Order) (offline bool, reason string) + +// SetPersonalCheck 注入个人通道监听端检查(与 reconciler.Scheduler 共用,防止两条关单路径安全检查分叉)。 +// nil 安全(不注入 = 不检查直接关,仅测试/无设备环境)。 +func (s *Service) SetPersonalCheck(fn PersonalCheckFunc) { s.personalCheck = fn } + +// SetWaitPayHooks 注入 WAIT_PAY 池钩子(main.go 调用;nil 安全) +func (s *Service) SetWaitPayHooks(h *WaitPayHooks) { s.waitPay = h } + +// SetAlert 注入告警客户端(可选) +func (s *Service) SetAlert(a *alert.Client) { s.alert = a } + +// SetCloseWindow 注入关单窗口(分钟),复用订单超窗判定用(0=不启用超窗刷新) +func (s *Service) SetCloseWindow(minutes int) { + if minutes > 0 { + s.closeWindow = time.Duration(minutes) * time.Minute + } +} + +// isExpiredWindow 订单是否已超出关单窗口(WAIT_PAY 但 CreatedAt 超窗,关单清扫器尚未执行) +func (s *Service) isExpiredWindow(o *model.Order) bool { + return s.closeWindow > 0 && !o.CreatedAt.IsZero() && time.Since(o.CreatedAt) >= s.closeWindow +} + +// SetNoteCodeReleaser 注入个人通道备注码释放回调(可选) +// 回调在订单终态(SUCCESS/CLOSED)时调用,避免备注码仅靠 TTL 惰性回收 +func (s *Service) SetNoteCodeReleaser(fn func(channelName, noteCode string)) { + s.noteCodeReleaser = fn +} + +// releaseNoteCode 订单终态时释放备注码(仅 personal.* 通道生效) +func (s *Service) releaseNoteCode(o *model.Order) { + if s.noteCodeReleaser != nil && o.NoteCode != "" && strings.HasPrefix(o.Channel, "personal.") { + s.noteCodeReleaser(o.Channel, o.NoteCode) + } +} + +// New 创建订单服务 +func New(st store.Store, lg *ledger.Ledger, pool *account.AccountPool, upPool *upstream.UpstreamPool) *Service { + return &Service{store: st, ledger: lg, accountPool: pool, upstreamPool: upPool} +} + +// signKey 按订单 pid 返回签名密钥(多商户路由,严格模式) +// pid 未登记或已禁用 → 返回空(回调签名无效,防止未登记上游的订单入账,防御) +func (s *Service) signKey(pid string) string { + if s.upstreamPool == nil || pid == "" { + return "" + } + if u := s.upstreamPool.ByPid(pid); u != nil && u.Status == upstream.StatusEnabled && u.Key != "" { + return u.Key + } + return "" +} + +// upstreamName 返回订单归属的上游名称(展示用) +func (s *Service) upstreamName(pid string) string { + if s.upstreamPool == nil || pid == "" { + return "" + } + if u := s.upstreamPool.ByPid(pid); u != nil { + return u.Name + } + return "" +} + +// accountName 返回账户池中账户名称(用于日志/展示;查不到返回空) +func (s *Service) accountName(id int64) string { + if s.accountPool == nil || id <= 0 { + return "" + } + if acct := s.accountPool.GetByID(id); acct != nil { + return acct.Name + } + return "" +} + +// queryOrder 历史遍历查单;新调用应传订单所属通道名以避免跨通道错账。 +func (s *Service) queryOrder(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) { + return s.queryOrderFor(ctx, outTradeNo, "") +} + +var ErrNotQueryable = errors.New("订单所属通道无法主动查单") + +func (s *Service) queryOrderFor(ctx context.Context, outTradeNo, channelName string) (*channel.OrderStatus, error) { + if channelName != "" { + ch, ok := channel.Lookup(channelName) + if !ok { + return nil, fmt.Errorf("通道未注册: %s", channelName) + } + if ch.ConfirmAbility() != channel.ConfirmByQuery { + return nil, ErrNotQueryable + } + if !channel.IsEnabled(channelName) { + return nil, fmt.Errorf("通道已禁用: %s", channelName) + } + return ch.QueryOrder(ctx, outTradeNo) + } + var lastErr error + for _, name := range channel.Names() { + ch, ok := channel.Get(name) + if !ok || ch.ConfirmAbility() != channel.ConfirmByQuery { + continue + } + st, err := ch.QueryOrder(ctx, outTradeNo) + if err != nil { + lastErr = err + continue + } + return st, nil + } + if lastErr == nil { + lastErr = fmt.Errorf("无可查询通道") + } + return nil, lastErr +} + +// CreateOrderResult 下单结果 +type CreateOrderResult struct { + Order *model.Order + Cred *channel.PayCredential + Reused bool // 是否复用了已有订单(幂等) + Existing *model.Order +} + +// CreateOrder 下单(幂等:同一订单号重复请求复用原订单,不重复调通道) +func (s *Service) CreateOrder(ctx context.Context, req *CreateRequest) (*CreateOrderResult, error) { + // 金额解析(字符串元 → 最小单位分):前置解析,复用路径需以本次下单金额为最新标准 + amountMinor, err := model.ParseMoneyToMinor(req.Money) + if err != nil { + return nil, fmt.Errorf("金额非法: %w", err) + } + + // 1. 幂等检查 + if existing, err := s.store.GetOrder(ctx, req.OutTradeNo); err != nil { + return nil, err + } else if existing != nil { + logger.Warn("order", "订单已存在,复用", "out_trade_no", req.OutTradeNo, "status", existing.Status) + return s.reuseOrCreateError(ctx, existing, req.Type, amountMinor) + } + + // 1.5 买家级串行:同一买家环境已有待支付订单 → 复用原订单(防海量刷单/错配) + // 双维度检查:buyer_key(IP+UA+device+buyer_id) 或 fp_key(浏览器指纹 cookie) 命中均拦截。 + // 换 IP 攻击:buyer_key 变但 fp_key(cookie) 不变 → 仍被拦截。 + if req.BuyerKey != "" && strings.HasPrefix(req.Type, "personal.") { + var pending []*model.Order + var err error + if req.FPKey != "" { + pending, err = s.store.ListWaitPayByBuyerOrFP(ctx, req.BuyerKey, req.FPKey) + } else { + pending, err = s.store.ListWaitPayByBuyer(ctx, req.BuyerKey) + } + if err != nil { + logger.Error("order", "买家待支付订单查询失败", "err", err, "buyer_key", req.BuyerKey) + } else if len(pending) > 0 { + o := pending[0] + bk := req.BuyerKey + if len(bk) > 12 { + bk = bk[:12] + } + if o.OutTradeNo != req.OutTradeNo { + // 不同订单号:上游订单号与金额为不可变主键,无法复用 OLD。 + // 关闭 OLD(CAS/幂等,释放备注码+移出池)。并发时仅 CAS 胜出者创建 NEW, + // 落败者复用获胜新单或返回安全冲突错误,绝不产生第二笔活跃 WAIT_PAY。 + if !o.SessionExpiresAt.IsZero() && !time.Now().Before(o.SessionExpiresAt) { + return nil, fmt.Errorf("订单 %s 的支付会话已过期,请重新发起充值", o.OutTradeNo) + } + req.SessionExpiresAt = s.sessionExpiryOf(o) + logger.Warn("order", "买家已有待支付订单,替换旧单创建新单", "buyer_key", bk, "pending", o.OutTradeNo, "new", req.OutTradeNo) + reuse, create, err := s.replacePendingOrder(ctx, o, req.OutTradeNo, amountMinor) + if err != nil { + return nil, err + } + if !create { + if reuse != nil { + // 并发替换者已创建获胜新单:复用之,不再重复创建。 + logger.Warn("order", "买家替换并发,复用获胜新单", "buyer_key", bk, "new", req.OutTradeNo, "winner", reuse.OutTradeNo) + return &CreateOrderResult{Order: reuse, Reused: true, Existing: reuse}, nil + } + // 并发结算胜出(用户已付 OLD):禁止再创建同买家新单,避免重复收款。 + return nil, fmt.Errorf("订单 %s 已支付,请勿重复下单", o.OutTradeNo) + } + } else { + // 同订单号:幂等复用(金额不可变,见 reuseOrCreateError 的金额校验) + if o.PayURL != "" || o.QrCode != "" { + return s.reuseOrCreateError(ctx, o, req.Type, amountMinor) + } + return nil, fmt.Errorf("您有未支付订单 %s,请先完成支付", o.OutTradeNo) + } + } + } + + // 2. 获取通道 + ch, ok := channel.Get(req.Type) + if !ok { + return nil, channel.ErrUnsupported + } + + // 4. 创建订单(数据库唯一约束兜底幂等) + // 记录上游标识:pid 来自下单参数(多商户路由),用于订单归属 + 回调签名 + now := time.Now() + sessionExpiresAt := req.SessionExpiresAt + if sessionExpiresAt.IsZero() && s.closeWindow > 0 { + sessionExpiresAt = now.Add(s.closeWindow) + } + o := &model.Order{ + OutTradeNo: req.OutTradeNo, + GatewayNo: fmt.Sprintf("GW%d", now.UnixNano()), + Channel: req.Type, + Pid: req.RawParams["pid"], + Attach: req.RawParams["attach"], + AmountMinor: amountMinor, + Currency: "CNY", + Subject: req.Name, + Status: model.StatusWaitPay, + NotifyURL: req.NotifyURL, + ReturnURL: req.ReturnURL, + BuyerKey: req.BuyerKey, + FPKey: req.FPKey, + CreatedAt: now, + SessionExpiresAt: sessionExpiresAt, + } + o.UpstreamName = s.upstreamName(o.Pid) + if err := s.store.CreateOrder(ctx, o); err != nil { + // 并发同号:另一请求刚插入同一 out_trade_no(唯一约束冲突)→ 转复用路径, + // 不让商户收到裸 DB 错误 + if existing, gerr := s.store.GetOrder(ctx, req.OutTradeNo); gerr == nil && existing != nil { + return s.reuseOrCreateError(ctx, existing, req.Type, amountMinor) + } + return nil, fmt.Errorf("创建订单失败: %w", err) + } + s.recordEvent(ctx, o, "created", "", amountMinor, req.rawJSON()) + + // 5. 调通道下单 + payReq := &channel.PayRequest{ + OutTradeNo: o.OutTradeNo, + GatewayNo: o.GatewayNo, + Subject: o.Subject, + AmountMinor: o.AmountMinor, + Currency: o.Currency, + NotifyURL: "", // 通道 notify 由网关统一配置 + ReturnURL: o.ReturnURL, + PayMode: channel.PayMode(req.PayMode), + Device: req.Device, + } + cred, err := ch.CreatePayment(ctx, payReq) + if err != nil { + logger.Error("order", "通道下单失败", "err", err, "out_trade_no", o.OutTradeNo) + // 清理无凭据订单:留着会让同号重试撞"已处理"死墙(订单号被永久烧掉)。 + // 删除后商户可用原号重试;created 事件随行删除,失败事实在日志与告警中留痕。 + if _, derr := s.store.DeleteOrder(ctx, o.OutTradeNo); derr != nil { + logger.Error("order", "清理失败订单未遂(该单号将无法重用,需人工处理)", + "err", derr, "out_trade_no", o.OutTradeNo) + } + return nil, err + } + + // 6. 保存支付凭据(条件更新,WAIT_BUYER_PAY -> WAIT_BUYER_PAY 幂等) + // 使用后台上下文,避免 HTTP 客户端断开导致数据库操作被取消 + dbCtx, dbCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer dbCancel() + if cred.PayURL != "" { + o.PayURL = cred.PayURL + } + if cred.QRCode != "" { + o.QrCode = cred.QRCode + } + if cred.AccountID > 0 { + o.AccountID = cred.AccountID + } + if _, err := s.store.UpdateOrderStatus(dbCtx, o.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + if cred.PayURL != "" { + upd.PayURL = cred.PayURL + } + if cred.QRCode != "" { + upd.QrCode = cred.QRCode + } + if cred.AccountID > 0 { + upd.AccountID = cred.AccountID + } + // 个人免签通道:持久化备注码 + 套码 token(匹配/跳转用) + if cred.NoteCode != "" { + upd.NoteCode = cred.NoteCode + } + if cred.QrToken != "" { + upd.QrToken = cred.QrToken + } + if cred.DeviceID != "" { + upd.DeviceID = cred.DeviceID + } + }); err != nil { + return nil, err + } + // 读回最新(含凭据) + latest, _ := s.store.GetOrder(dbCtx, o.OutTradeNo) + if latest != nil { + o = latest + } + + logger.Info("order", "下单成功 ✅", + "out_trade_no", o.OutTradeNo, + "pid", o.Pid, + "upstream", o.UpstreamName, + "channel", req.Type, + "amount", amountMinor, + "mode", cred.Mode, + "account_id", cred.AccountID, + "account", s.accountName(cred.AccountID)) + + // WAIT_PAY 池同步:个人免签订单已绑定账户 + 凭据,进入调度数据源 + s.notifyWaitPayAdd(o) + + return &CreateOrderResult{Order: o, Cred: cred}, nil +} + +// reuseOrCreateError 已存在订单的统一处置: +// - 待支付订单(WAIT_PAY)且有凭据: +// - 请求通道 ≠ 订单通道(均为个人免签)→ 自动迁移订单到新通道账户(保留原过期时间) +// - 否则 → 复用原订单(保留原过期时间) +// - 已关闭订单(TRADE_CLOSED)同号重提 → 复活重新计时(重新走一个支付生命周期) +// - 其他终态(TRADE_SUCCESS 等)→ 报"已处理" +// +// 僵尸订单防护语义(只针对 WAIT_PAY): +// - WAIT_PAY 订单占资源(买家串行锁/备注码/账户绑定/倒计时), +// 复用/切换均不重置 CreatedAt,到期必关 → 无法无限续命占资源 +// - TRADE_CLOSED 已关闭不占资源,同号复活只是重新支付,无危害 +func (s *Service) reuseOrCreateError(ctx context.Context, existing *model.Order, reqType string, amountMinor int64) (*CreateOrderResult, error) { + hasCred := existing.PayURL != "" || existing.QrCode != "" + if !hasCred { + return nil, fmt.Errorf("订单 %s 已处理(状态: %s)", existing.OutTradeNo, existing.Status) + } + // 金额不可变:订单号+金额均为上游不可变主键。同号重提若金额不同 → 拒单, + // 绝不覆盖已存在订单金额,也绝不以新金额刷新凭据(否则回调按订单金额校验会永不入账/错账)。 + // 不同订单号的替换路径见 replacePendingOrder(CreateOrder 买家守卫调用)。 + if amountMinor > 0 && amountMinor != existing.AmountMinor { + return nil, fmt.Errorf("订单 %s 金额不可变:原金额=%d 新金额=%d(订单号已存在,请使用新订单号提交)", + existing.OutTradeNo, existing.AmountMinor, amountMinor) + } + // 已关闭订单同号重提 → 复活(上游同号重试是易支付协议标准行为) + if existing.Status == model.StatusClosed { + revived, err := s.ReviveOrder(ctx, existing, reqType) + if err != nil { + logger.Error("order", "已关闭订单复活失败", "err", err, "out_trade_no", existing.OutTradeNo, "to", reqType) + return nil, fmt.Errorf("订单 %s 已过期,重新下单失败: %w", existing.OutTradeNo, err) + } + return &CreateOrderResult{Order: revived, Reused: true, Existing: revived}, nil + } + if existing.Status != model.StatusWaitPay { + return nil, fmt.Errorf("订单 %s 已处理(状态: %s)", existing.OutTradeNo, existing.Status) + } + // 自动切换支付方式:上游换了支付模式的入口进来(同号不同 type),订单整体迁移到新通道 + if reqType != "" && reqType != existing.Channel { + if strings.HasPrefix(existing.Channel, "personal.") && strings.HasPrefix(reqType, "personal.") { + switched, err := s.SwitchChannel(ctx, existing, reqType) + if err != nil { + logger.Warn("order", "自动切换支付方式失败,退回复用原订单", "err", err, "out_trade_no", existing.OutTradeNo, "from", existing.Channel, "to", reqType) + } else { + return &CreateOrderResult{Order: switched, Reused: true, Existing: switched}, nil + } + } + } + // 复用前超窗检查:WAIT_PAY 但已超过关单窗口(关单清扫器还没跑)→ 刷新凭据并重置倒计时, + // 避免复用出的支付页立即过期跳回过期页(不违反僵尸防护:只对"已超窗"订单重新计时)。 + if s.isExpiredWindow(existing) { + refreshed, err := s.RefreshPayment(ctx, existing) + if err != nil { + logger.Warn("order", "复用超窗订单刷新失败,按原单返回", "err", err, "out_trade_no", existing.OutTradeNo, "amount", existing.AmountMinor) + } else { + return &CreateOrderResult{Order: refreshed, Reused: true, Existing: refreshed}, nil + } + } + return &CreateOrderResult{Order: existing, Reused: true, Existing: existing}, nil +} + +// replacePendingOrder 买家已有 WAIT_PAY 待支付订单(OLD),本次 request 用了不同 out_trade_no(NEW)。 +// OLD 的订单号+金额是上游不可变主键,无法复用 → 关闭 OLD 释放资源,再由调用方为 NEW 走正常下单。 +// +// 并发安全:多个同买家请求可能同时试图替换同一个 OLD。用 UpdateOrderStatus 的 CAS 语义仲裁—— +// 仅 CAS 胜出者(replaceOrder 原子关单+写 replaced 事件)创建 NEW;CAS 落败者不得再创建自己的 NEW, +// 否则同一买家会出现多笔 WAIT_PAY。落败者依据 OLD 的终态与 replaced 事件分派: +// +// - OLD=SUCCESS(并发结算胜出)→ create=false,禁止创建新单(防重复收款)。 +// - OLD=CLOSED 且带 replaced 事件(被并发替换者关闭)→ create=false,若替换者的 NEW 已存在则复用, +// 否则返回可安全重试的冲突错误;绝不重复创建 NEW。 +// - OLD=CLOSED 且无 replaced 事件(仅被清扫器/超时关闭)→ create=true,正常为新单走创建流程。 +// +// 返回 (reuse, create, err):create=false 时若 reuse!=nil 则调用方应复用该订单,否则调用方不得创建 NEW。 +func (s *Service) replacePendingOrder(ctx context.Context, old *model.Order, newOutTradeNo string, newAmountMinor int64) (*model.Order, bool, error) { + payload := fmt.Sprintf(`{"old_out_trade_no":%q,"old_amount":%d,"new_out_trade_no":%q,"new_amount":%d,"reason":"buyer_replaced","time":%q}`, + old.OutTradeNo, old.AmountMinor, newOutTradeNo, newAmountMinor, time.Now().Format(time.RFC3339)) + closed, err := s.store.ReplaceOrder(ctx, old.OutTradeNo, old.ID, old.AmountMinor, payload) + if err != nil { + return nil, false, err + } + if closed { + // 赢得替换:释放备注码 + 移出 WAIT_PAY 池(各一次),随后调用方为 NEW 走正常下单。 + s.releaseNoteCode(old) + s.notifyWaitPayRemove(old) + logger.Warn("order", "买家待支付订单已被新订单替换关闭", "old", old.OutTradeNo, "new", newOutTradeNo, "new_amount", newAmountMinor) + return nil, true, nil + } + + latest, gerr := s.store.GetOrder(ctx, old.OutTradeNo) + if gerr == nil && latest != nil && latest.Status == model.StatusSuccess { + logger.Error("order", "买家待支付并发结算胜出,取消新单创建(防重复收款)", "old", old.OutTradeNo, "new", newOutTradeNo) + return nil, false, nil + } + + // 非结算:OLD 带 replaced 事件 ⇒ 被并发替换者关闭,不得再创建自己的 NEW。 + if s.hasReplacedRecently(ctx, old) { + return s.reuseWinnerNew(ctx, old, newOutTradeNo) + } + + // OLD 仅被清扫器/超时关闭(无 replaced 事件)→ 安全为本次请求创建全新 NEW。 + return nil, true, nil +} + +// hasReplacedRecently 判断 OLD 是否刚被「并发替换者」关闭:存在近窗口内的 replaced 事件。 +// 近窗口过滤掉「被替换后同号复活、又被清扫器关闭」的罕见历史场景,避免误判为并发替换。 +func (s *Service) hasReplacedRecently(ctx context.Context, old *model.Order) bool { + evs, err := s.store.ListEvents(ctx, old.ID, 20) + if err != nil { + return false + } + cutoff := time.Now().Add(-30 * time.Second) + for _, e := range evs { + if e.EventType == "replaced" && e.CreatedAt.After(cutoff) { + return true + } + } + return false +} + +// reuseWinnerNew 并发替换者已关闭 OLD:返回其创建的 NEW 供复用,若尚不可见则返回可安全重试的冲突错误。 +func (s *Service) reuseWinnerNew(ctx context.Context, old *model.Order, newOutTradeNo string) (*model.Order, bool, error) { + pending, err := s.store.ListWaitPayByBuyerOrFP(ctx, old.BuyerKey, old.FPKey) + if err != nil { + return nil, false, err + } + for _, p := range pending { + if p.OutTradeNo != old.OutTradeNo { + logger.Warn("order", "买家替换并发,复用获胜新单而非重复创建", "old", old.OutTradeNo, "new", newOutTradeNo, "winner", p.OutTradeNo) + return p, false, nil + } + } + return nil, false, fmt.Errorf("订单 %s 正在被并发替换为新单,请稍后重试", old.OutTradeNo) +} + +// CreateRequest 下单请求(易支付协议参数) +type CreateRequest struct { + OutTradeNo string + Type string // alipay / wxpay / paypal / usdt + Name string + Money string // 元(字符串) + NotifyURL string + ReturnURL string + PayMode string // page / wap / qrcode(pay_type) + Device string // pc / mobile + BuyerKey string // 下单环境唯一标识(IP+UA+设备指纹 hash,买家级串行/防刷单) + FPKey string // 浏览器指纹标识(cookie pg_fp,换 IP 场景串行兜底) + SessionExpiresAt time.Time // 内部替换继承的支付会话硬截止时间 + RawParams map[string]string +} + +func (s *Service) sessionExpiryOf(o *model.Order) time.Time { + if o == nil { + return time.Time{} + } + return o.SessionExpiresAt +} + +// RefreshPayment 刷新支付凭据(二维码刷新):保持原订单号和上游关联不变,重新调通道生成新二维码/备注码。 +// +// 易支付协议中 out_trade_no 是上游的业务主键,刷新二维码不允许改单号, +// 否则回调带上新单号(如 xxx-R2)时上游找不到订单 → 回调入账失败。 +func (s *Service) RefreshPayment(ctx context.Context, o *model.Order) (*model.Order, error) { + if o == nil || o.OutTradeNo == "" { + return nil, fmt.Errorf("订单为空") + } + if o.Status != model.StatusWaitPay { + return nil, fmt.Errorf("订单当前状态(%s)不可刷新", o.Status) + } + ch, ok := channel.Get(o.Channel) + if !ok { + return nil, channel.ErrUnsupported + } + // 释放旧备注码(避免 5 分钟 TTL 惰性回收期间码被占用) + if strings.HasPrefix(o.Channel, "personal.") { + s.releaseNoteCode(o) + } + // 重新调通道生成支付凭据(用原订单号) + payReq := &channel.PayRequest{ + OutTradeNo: o.OutTradeNo, + GatewayNo: o.GatewayNo, + Subject: o.Subject, + AmountMinor: o.AmountMinor, + Currency: o.Currency, + NotifyURL: "", + ReturnURL: o.ReturnURL, + PayMode: channel.ModeQRCode, + Device: "pc", + } + cred, err := ch.CreatePayment(ctx, payReq) + if err != nil { + logger.Error("order", "刷新支付凭据失败", "err", err, "out_trade_no", o.OutTradeNo) + return nil, err + } + // 条件更新订单凭据(WAIT_BUYER_PAY -> WAIT_BUYER_PAY,幂等) + dbCtx, dbCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer dbCancel() + if _, err := s.store.UpdateOrderStatus(dbCtx, o.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + // 刷新二维码 = 用户重新支付:重置倒计时给完整支付窗口。 + // 不违反僵尸防护:订单始终 WAIT_PAY 未复活已关闭订单; + // 且刷新是用户主动点击(每点一次重新计时),与"同号重提自动续命"语义不同。 + upd.CreatedAt = time.Now() + if cred.PayURL != "" { + upd.PayURL = cred.PayURL + } + if cred.QRCode != "" { + upd.QrCode = cred.QRCode + } + if cred.AccountID > 0 { + upd.AccountID = cred.AccountID + } + if cred.NoteCode != "" { + upd.NoteCode = cred.NoteCode + } + if cred.QrToken != "" { + upd.QrToken = cred.QrToken + } + if cred.DeviceID != "" { + upd.DeviceID = cred.DeviceID + } + }); err != nil { + return nil, err + } + // 读回最新订单(含新凭据) + latest, _ := s.store.GetOrder(dbCtx, o.OutTradeNo) + if latest != nil { + o = latest + } + logger.Info("order", "支付凭据已刷新 ✅", + "out_trade_no", o.OutTradeNo, "channel", o.Channel, + "pid", o.Pid, "amount", o.AmountMinor) + // WAIT_PAY 池同步:凭据/备注码已更新,覆盖池条目 + s.notifyWaitPayUpsert(o) + return o, nil +} + +// SwitchChannel 切换支付方式(个人免签通道间迁移订单) +// +// 用户下单后想换支付方式(如支付宝→微信):保持订单号和金额不变, +// 重新调目标通道选账户生成凭据,订单整体迁移到目标通道的可用账户。 +// 仅允许未过期(WAIT_PAY)订单切换;保留原 CreatedAt(过期时间复用, +// 防换支付方式无限刷新倒计时/复活 → 僵尸订单刷单漏洞)。 +// TRADE_CLOSED 订单是终态,任何重提都报"已处理"(不允许复活)。 +// +// 幂等性:目标通道与当前一致时直接返回原订单(不重复调通道)。 +func (s *Service) SwitchChannel(ctx context.Context, o *model.Order, newChannel string) (*model.Order, error) { + if o == nil || o.OutTradeNo == "" { + return nil, fmt.Errorf("订单为空") + } + if o.Channel == newChannel { + return o, nil + } + if !strings.HasPrefix(o.Channel, "personal.") || !strings.HasPrefix(newChannel, "personal.") { + return nil, fmt.Errorf("仅支持个人免签通道间切换支付方式") + } + if o.Status != model.StatusWaitPay { + return nil, fmt.Errorf("订单当前状态(%s)不可切换支付方式(仅待支付订单可切换,过期订单不可复活)", o.Status) + } + ch, ok := channel.Get(newChannel) + if !ok { + return nil, channel.ErrUnsupported + } + // 释放旧通道备注码(避免 TTL 惰性回收期间码被占用) + if strings.HasPrefix(o.Channel, "personal.") { + s.releaseNoteCode(o) + } + // 重新调目标通道生成支付凭据(用原订单号;SelectFrom 按 channel 过滤自动选对平台账户) + payReq := &channel.PayRequest{ + OutTradeNo: o.OutTradeNo, + GatewayNo: o.GatewayNo, + Subject: o.Subject, + AmountMinor: o.AmountMinor, + Currency: o.Currency, + NotifyURL: "", + ReturnURL: o.ReturnURL, + PayMode: channel.ModeQRCode, + Device: "pc", + } + cred, err := ch.CreatePayment(ctx, payReq) + if err != nil { + logger.Error("order", "切换支付方式失败(目标通道下单失败)", "err", err, "out_trade_no", o.OutTradeNo, "to", newChannel) + return nil, err + } + // WAIT_PAY→WAIT_PAY 换凭据;不重置 CreatedAt(过期时间复用,防僵尸订单刷单) + dbCtx, dbCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer dbCancel() + ok2, err := s.store.UpdateOrderStatus(dbCtx, o.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.Channel = newChannel + if cred.PayURL != "" { + upd.PayURL = cred.PayURL + } + if cred.QRCode != "" { + upd.QrCode = cred.QRCode + } + if cred.AccountID > 0 { + upd.AccountID = cred.AccountID + } + if cred.NoteCode != "" { + upd.NoteCode = cred.NoteCode + } + if cred.QrToken != "" { + upd.QrToken = cred.QrToken + } + if cred.DeviceID != "" { + upd.DeviceID = cred.DeviceID + } + }) + if err != nil { + return nil, err + } + if !ok2 { + return nil, fmt.Errorf("订单状态已变化,请刷新后重试") + } + // 读回最新订单(含新凭据) + latest, _ := s.store.GetOrder(dbCtx, o.OutTradeNo) + if latest != nil { + o = latest + } + logger.Info("order", "支付方式已切换 ✅", + "out_trade_no", o.OutTradeNo, "from", o.Channel, "to", newChannel, + "account_id", cred.AccountID, "status", o.Status, "amount", o.AmountMinor) + // WAIT_PAY 池同步:通道/账户/备注码已更新,覆盖池条目 + s.notifyWaitPayUpsert(o) + return o, nil +} + +// ReviveOrder 已关闭订单同号重提复活:TRADE_CLOSED → WAIT_PAY,重新走一个支付生命周期。 +// +// 上游固定订单号重试(易支付协议标准行为)时调用;已关闭订单不占资源 +// (买家串行锁/备注码/倒计时均已释放),复活 = 重新支付,无僵尸订单风险。 +// reqType 非空且与订单通道不同时顺带切换支付方式(SelectFrom 按 channel 选目标平台账户)。 +func (s *Service) ReviveOrder(ctx context.Context, o *model.Order, reqType string) (*model.Order, error) { + if o == nil || o.OutTradeNo == "" { + return nil, fmt.Errorf("订单为空") + } + if o.Status != model.StatusClosed { + return nil, fmt.Errorf("订单状态(%s)不可复活(仅 TRADE_CLOSED)", o.Status) + } + newChannel := o.Channel + if reqType != "" { + newChannel = reqType + } + if !strings.HasPrefix(newChannel, "personal.") { + return nil, fmt.Errorf("仅支持个人免签通道复活") + } + ch, ok := channel.Get(newChannel) + if !ok { + return nil, channel.ErrUnsupported + } + // 释放旧通道备注码(复活后重新分配,避免 TTL 惰性回收期间码被占用) + if strings.HasPrefix(o.Channel, "personal.") { + s.releaseNoteCode(o) + } + // 重新调通道生成支付凭据(用原订单号;选账户按目标通道过滤) + payReq := &channel.PayRequest{ + OutTradeNo: o.OutTradeNo, + GatewayNo: o.GatewayNo, + Subject: o.Subject, + AmountMinor: o.AmountMinor, + Currency: o.Currency, + NotifyURL: "", + ReturnURL: o.ReturnURL, + PayMode: channel.ModeQRCode, + Device: "pc", + } + cred, err := ch.CreatePayment(ctx, payReq) + if err != nil { + logger.Error("order", "复活订单失败(目标通道下单失败)", "err", err, "out_trade_no", o.OutTradeNo, "to", newChannel) + return nil, err + } + // TRADE_CLOSED → WAIT_PAY,重置 CreatedAt 重新计时,清除 paid_at + dbCtx, dbCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer dbCancel() + ok2, err := s.store.UpdateOrderStatus(dbCtx, o.OutTradeNo, model.StatusClosed, model.StatusWaitPay, func(upd *model.Order) { + upd.Channel = newChannel + upd.CreatedAt = time.Now() + upd.PaidAt = nil // from!=to 分支会 COALESCE 填 now,必须清掉 + if cred.PayURL != "" { + upd.PayURL = cred.PayURL + } + if cred.QRCode != "" { + upd.QrCode = cred.QRCode + } + if cred.AccountID > 0 { + upd.AccountID = cred.AccountID + } + if cred.NoteCode != "" { + upd.NoteCode = cred.NoteCode + } + if cred.QrToken != "" { + upd.QrToken = cred.QrToken + } + if cred.DeviceID != "" { + upd.DeviceID = cred.DeviceID + } + }) + if err != nil { + return nil, err + } + if !ok2 { + return nil, fmt.Errorf("订单状态已变化,请刷新后重试") + } + latest, _ := s.store.GetOrder(dbCtx, o.OutTradeNo) + if latest != nil { + o = latest + } + logger.Info("order", "已关闭订单已复活 ✅", + "out_trade_no", o.OutTradeNo, "from", o.Channel, "to", newChannel, + "account_id", cred.AccountID, "status", o.Status, "amount", o.AmountMinor) + // WAIT_PAY 池同步:CLOSED → WAIT_PAY 重新进入池 + s.notifyWaitPayAdd(o) + return o, nil +} + +func (r *CreateRequest) rawJSON() string { + b, _ := json.Marshal(r.RawParams) + return string(b) +} + +// ---------- 回调处理 ---------- + +// SettlePayment 支付成功结算(核心资金操作,唯一入口) +// nr: 通道验签后的通知结果(含 out_trade_no / 实付金额 / 通道交易号) +func (s *Service) SettlePayment(ctx context.Context, nr *channel.NotifyResult) (bool, error) { + outTradeNo := nr.OutTradeNo + // 1. 查订单 + o, err := s.store.GetOrder(ctx, outTradeNo) + if err != nil { + return false, err + } + if o == nil { + logger.Error("order", "订单不存在", "out_trade_no", outTradeNo) + return false, nil + } + + // 2. 金额校验(防改价攻击) + // 严格模式:实付金额缺失(0)同样拒绝——所有通道的通知/查单路径都能提供金额, + // 放行未知金额 = 允许未经验证的资金入账。拒绝后通道会重试,持续失败转人工池。 + if nr.AmountMinor != o.AmountMinor { + logger.Error("order", "金额校验失败(重大安全事件)", "out_trade_no", outTradeNo, "amount", o.AmountMinor, "paid", nr.AmountMinor) + s.recordEvent(ctx, o, "amount_mismatch", nr.ChannelTxID, nr.AmountMinor, nr.RawPayload) + return false, fmt.Errorf("金额不匹配: 订单=%d 实付=%d", o.AmountMinor, nr.AmountMinor) + } + + // 3. 账户验证:回调的账户与订单记录的账户一致 + if nr.AccountID > 0 && o.AccountID > 0 && nr.AccountID != o.AccountID { + logger.Error("order", "账户不匹配(重大安全事件)", + "out_trade_no", outTradeNo, + "order_account_id", o.AccountID, + "callback_account_id", nr.AccountID) + return false, fmt.Errorf("账户不匹配: 订单=%d 回调=%d", o.AccountID, nr.AccountID) + } + + // 4a. 幂等:已支付直接返回成功(同一通道交易号重复回调) + if o.Status == model.StatusSuccess { + // 崩溃恢复:若事件/账本在上次结算中丢失(条件更新成功后进程崩溃),此处补写 + s.ensureRecorded(ctx, o, nr) + // 备注码释放(幂等:重复释放无害) + s.releaseNoteCode(o) + // WAIT_PAY 池同步:确保已移出池(幂等;崩溃恢复场景兜底) + s.notifyWaitPayRemove(o) + logger.Info("order", "订单已支付,幂等返回", "out_trade_no", outTradeNo) + return true, nil + } + + // 4b. H9 迟到支付回调:订单已被超时关单(CLOSED),上游却在关单后送来支付成功通知。 + // Closed 为资金安全终态,禁止重开(state graph: StatusClosed={}); + // 但用户已付款真实存在,丢失证据=无法追讨/退款。落证据事件(不计入账本,避免凭空入账),告警待人工核查与上游对账。 + if o.Status == model.StatusClosed { + lateEvent := fmt.Sprintf(`{"out_trade_no":%q,"amount":%d,"status":"LATE_PAID_AFTER_CLOSED","channel_tx_id":%q,"payer":%q,"device_id":%q,"account_id":%d,"time":%q}`, + outTradeNo, o.AmountMinor, nr.ChannelTxID, nr.Payer, nr.DeviceID, nr.AccountID, time.Now().Format(time.RFC3339)) + if err := s.store.InsertEvent(ctx, o.ID, "late_paid_after_closed", nr.ChannelTxID, o.AmountMinor, lateEvent); err != nil { + logger.Error("order", "LATE_PAID_AFTER_CLOSED 事件落库失败", "err", err, "out_trade_no", outTradeNo) + return false, err + } + logger.Error("order", "H9 迟到支付:订单已关单但上游通知支付成功", + "out_trade_no", outTradeNo, "channel_tx_id", nr.ChannelTxID, + "amount", o.AmountMinor, "closed_event_id", o.ID, "notify_payload", nr.RawPayload) + if s.alert != nil { + s.alert.Send("error", "迟到支付(已关单):需人工对账", + fmt.Sprintf("订单 %s 已被网关关单,但上游 %s 通知支付成功 (channel_tx_id=%s, amount=%d)\n请核对上游商户后台并主动退款或补开,避免用户资金流失投诉", + outTradeNo, nr.ChannelTxID, nr.ChannelTxID, o.AmountMinor)) + } + // 返回 true 让通道收到 success 停止重试;资金不自动入账,等待人工对账 + return true, nil + } + + // 5. 条件更新状态 WAIT_BUYER_PAY -> TRADE_SUCCESS(互斥锁:防并发双写账本/事件) + ok, err := s.store.UpdateOrderStatus(ctx, outTradeNo, model.StatusWaitPay, model.StatusSuccess, func(upd *model.Order) { + upd.ChannelTxID = nr.ChannelTxID + upd.Payer = nr.Payer + if nr.DeviceID != "" { + upd.DeviceID = nr.DeviceID + } + }) + if err != nil { + return false, err + } + if !ok { + logger.Warn("order", "状态更新被拒绝(条件不满足)", "out_trade_no", outTradeNo, "from", o.Status) + return false, nil + } + + // 6. 资金流水 + 审计账本 + // 写入顺序说明:先事件后账本。任一失败都返回错误让通道重试回调; + // 重试时进入上面的幂等分支,由 ensureRecorded 补写缺失部分(账本按 eventID 去重,不会重复记账) + eventPayload := fmt.Sprintf(`{"out_trade_no":%q,"amount":%d,"status":"TRADE_SUCCESS","channel_tx_id":%q,"payer":%q,"device_id":%q,"account_id":%d,"time":%q}`, + outTradeNo, o.AmountMinor, nr.ChannelTxID, nr.Payer, nr.DeviceID, nr.AccountID, time.Now().Format(time.RFC3339)) + if err := s.store.InsertEvent(ctx, o.ID, "paid", nr.ChannelTxID, o.AmountMinor, nr.RawPayload); err != nil { + logger.Error("order", "资金流水写入失败(返回错误,等待通道重试)", "err", err, "out_trade_no", outTradeNo) + return false, err + } + if err := s.ledger.Append(ctx, o.ID, eventPayload); err != nil { + logger.Error("order", "账本写入失败(返回错误,等待通道重试,幂等分支将补写)", "err", err, "out_trade_no", outTradeNo) + if s.alert != nil { + s.alert.Send("error", "账本写入失败", + fmt.Sprintf("订单 %s 入账但账本写入失败(重大)\n错误: %v\n幂等分支将补写,请关注", outTradeNo, err)) + } + return false, err + } + logger.Info("order", "订单支付成功 ✅", + "out_trade_no", outTradeNo, + "pid", o.Pid, + "upstream", o.UpstreamName, + "amount", o.AmountMinor, + "tx_id", nr.ChannelTxID) + + // 7. 创建回调任务(outbox) + s.EnqueueNotify(ctx, o) + // 备注码释放(订单终态 SUCCESS,备注码不再需要) + s.releaseNoteCode(o) + // WAIT_PAY 池同步:订单已入账,移出池 + s.notifyWaitPayRemove(o) + return true, nil +} + +// ensureRecorded 崩溃恢复补写:条件更新成功后进程若崩溃,事件/账本可能缺失。 +// 在幂等分支调用:检查并按需补写,保证"资金事件必有账本记录"(账本按 eventID 去重,绝不重复记账)。 +func (s *Service) ensureRecorded(ctx context.Context, o *model.Order, nr *channel.NotifyResult) { + // 账本缺失 → 补写(eventID=订单ID 去重) + has, err := s.ledger.HasEvent(ctx, o.ID) + if err != nil { + logger.Error("order", "检查账本记录失败(崩溃恢复)", "err", err, "out_trade_no", o.OutTradeNo) + return + } + if !has { + eventPayload := fmt.Sprintf(`{"out_trade_no":%q,"amount":%d,"status":"TRADE_SUCCESS","channel_tx_id":%q,"payer":%q,"device_id":%q,"account_id":%d,"time":%q,"recovered":true}`, + o.OutTradeNo, o.AmountMinor, nr.ChannelTxID, nr.Payer, nr.DeviceID, nr.AccountID, time.Now().Format(time.RFC3339)) + if err := s.ledger.Append(ctx, o.ID, eventPayload); err != nil { + logger.Error("order", "崩溃恢复:账本补写失败(严重,需人工核查)", "err", err, "out_trade_no", o.OutTradeNo) + } else { + logger.Warn("order", "崩溃恢复:账本已补写", "out_trade_no", o.OutTradeNo, "event_id", o.ID) + } + } + // 事件缺失 → 补写(幂等:同一订单+交易号只记一条 paid 事件) + if nr.ChannelTxID != "" { + evs, err := s.store.ListEvents(ctx, o.ID, 50) + if err == nil { + found := false + for _, e := range evs { + if e.EventType == "paid" && e.ChannelTxID == nr.ChannelTxID { + found = true + break + } + } + if !found { + if err := s.store.InsertEvent(ctx, o.ID, "paid", nr.ChannelTxID, o.AmountMinor, nr.RawPayload); err != nil { + logger.Error("order", "崩溃恢复:事件补写失败", "err", err, "out_trade_no", o.OutTradeNo) + } else { + logger.Warn("order", "崩溃恢复:资金事件已补写", "out_trade_no", o.OutTradeNo) + } + } + } + } +} + +// CloseExpiredOrders 关闭超过 before 仍未支付的订单(定时任务调用) +// 关单前必查单(与 ConfirmScheduler 同策略):已支付 → 补账;查单失败 → 跳过留待调度器/人工池;确认未支付 → 才关闭 +// 返回本次关闭的订单数 +func (s *Service) CloseExpiredOrders(ctx context.Context, before time.Time) (int, error) { + expired, err := s.store.ListExpiredOrders(ctx, before) + if err != nil { + return 0, err + } + closed := 0 + // 扫描级设备在线缓存:同设备多个到期订单只做一次 adb 探测(Online 有连接开销)。 + // 仅缓存已绑定设备(DeviceID≠"")的结果;未绑定设备查任意可用设备,不缓存。 + type devProbe struct { + offline bool + reason string + } + probeCache := map[string]devProbe{} + for _, o := range expired { + // ConfirmAbility 分叉:不可主动查单的通道(个人/监听)走安全关单 + if ch, ok := channel.Lookup(o.Channel); ok && ch.ConfirmAbility() != channel.ConfirmByQuery { + // 动态关单窗口:池中记录的 CloseAt 未到 → 本次扫描跳过(窗口由设备负载决定,见 PLAN-DYNAMIC-CLOSE-ALLOC) + if s.waitPay != nil && s.waitPay.CloseAtOf != nil { + if ct, ok := s.waitPay.CloseAtOf(o.OutTradeNo); ok && time.Now().Before(ct) { + continue + } + } + // 关单前安全检查(与 confirmPersonal 同语义,防止两条关单路径安全检查分叉): + // 监听端离线 → 进异常池人工核实(用户可能已付款但未监听到),绝不直接关单。 + if s.personalCheck != nil { + offline, reason := false, "" + if o.DeviceID != "" { + if pr, ok := probeCache[o.DeviceID]; ok { + offline, reason = pr.offline, pr.reason + } else { + offline, reason = s.personalCheck(ctx, o) + probeCache[o.DeviceID] = devProbe{offline: offline, reason: reason} + } + } else { + offline, reason = s.personalCheck(ctx, o) + } + if offline { + s.EnqueueException(ctx, o, model.ExReasonListenOffline, "LISTEN_OFFLINE", "", + fmt.Sprintf(`{"action":"close_scan_offline","time":%q}`, time.Now().Format(time.RFC3339)), + "监听端掉线,无法确认用户是否已付款,需人工核实: "+reason) + continue + } + } + // 监听端在线但此前已进过 LISTEN_TIMEOUT 异常池(复查过仍无推送)→ 不进异常池重复核查, + // 说明监听端僵死风险已存在:不直接关单,交由人工核实(与 confirmPersonal 的复查语义一致)。 + if ex, _ := s.store.GetExceptionByOutTradeNo(ctx, o.OutTradeNo); ex != nil && ex.Reason == model.ExReasonListenTimeout { + s.EnqueueException(ctx, o, model.ExReasonListenTimeout, "LISTEN_TIMEOUT", "", + fmt.Sprintf(`{"action":"close_scan_stalled","time":%q}`, time.Now().Format(time.RFC3339)), + "监听端在线但长时间未收到收款推送(扫描器复查命中历史 LISTEN_TIMEOUT),需人工确认实际收款情况") + continue + } + if s.closeExpiredOrder(ctx, o, nil, "") { + closed++ + } + continue + } + // 商家通道关单前必查单;只查订单所属通道避免跨通道命中同名订单 + // 到期判定:ORDER_CLOSE_MINUTES(商家通道固定关单时间,不受个人免签动态窗口影响)。 + // 扫描器候选集可能被动态窗口放宽(max(商家,动态MAX)+裕量),此处二次过滤真正到期订单。 + if !s.isExpiredWindow(o) { + continue + } + st, qerr := s.queryOrderFor(ctx, o.OutTradeNo, o.Channel) + if qerr == nil && st != nil && st.Status == model.StatusSuccess { + if _, serr := s.SettleByQuery(ctx, o.OutTradeNo, st.ChannelTxID, st.AmountMinor, + fmt.Sprintf(`{"action":"close_scan_found_paid","time":%q}`, time.Now().Format(time.RFC3339))); serr != nil { + logger.Error("order", "扫描关单前查单发现已支付但补账失败(转人工核查)", + "err", serr, "out_trade_no", o.OutTradeNo) + } + continue + } + if qerr != nil && !errors.Is(qerr, ErrNotQueryable) { + // 查单失败绝不关单(资金安全)。但不能每轮扫描都重查+重 WARN 同一批卡单: + // 首次失败入人工池(close_uncertain),已存在则静默跳过,交人工池处理。 + if ex, gerr := s.store.GetExceptionByOutTradeNo(ctx, o.OutTradeNo); gerr == nil && ex != nil { + continue + } + s.EnqueueException(ctx, o, model.ExReasonCloseUncertain, "QUERY_FAILED", "", + fmt.Sprintf(`{"action":"close_scan_query_failed","time":%q,"err":%q}`, + time.Now().Format(time.RFC3339), qerr.Error()), + "关单扫描查单失败,无法确认是否已支付,需人工核对后处理") + continue + } + if s.closeExpiredOrder(ctx, o, st, "") { + closed++ + } + } + if closed > 0 { + logger.Info("order", "自动关闭扫描完成", "closed", closed) + } + return closed, nil +} + +// closeExpiredOrder 执行单笔超时关单:st=nil 表示无法/未查单(直接关),st != nil 表示已查过确认未支付。 +func (s *Service) closeExpiredOrder(ctx context.Context, o *model.Order, st *channel.OrderStatus, evidenceJSON string) bool { + ok, err := s.store.UpdateOrderStatus(ctx, o.OutTradeNo, model.StatusWaitPay, model.StatusClosed, nil) + if err != nil { + logger.Error("order", "关闭订单失败", "err", err, "out_trade_no", o.OutTradeNo) + return false + } + if !ok { + return false + } + rawPayload := fmt.Sprintf(`{"reason":"timeout","queried":%t%s}`, st != nil, evidenceJSON) + if err := s.store.InsertEvent(ctx, o.ID, "closed", "", o.AmountMinor, rawPayload); err != nil { + logger.Warn("order", "写关闭事件失败", "err", err, "out_trade_no", o.OutTradeNo) + } + s.releaseNoteCode(o) + // WAIT_PAY 池同步:订单已关单,移出池 + s.notifyWaitPayRemove(o) + logger.Info("order", "超时未支付订单已关闭", "out_trade_no", o.OutTradeNo, "id", o.ID) + return true +} + +// CloseOrder 单笔幂等关单(条件更新 WAIT→CLOSED,写关闭事件审计) +// 供 ConfirmScheduler / 人工池 / 查单确认"未支付"后调用 +func (s *Service) CloseOrder(ctx context.Context, outTradeNo string, reason string) (bool, error) { + o, err := s.store.GetOrder(ctx, outTradeNo) + if err != nil { + return false, err + } + if o == nil { + return false, nil + } + if o.Status == model.StatusClosed { + return true, nil // 幂等 + } + ok, err := s.store.UpdateOrderStatus(ctx, outTradeNo, model.StatusWaitPay, model.StatusClosed, nil) + if err != nil { + return false, err + } + if !ok { + return false, nil // 已被并发处理(如刚支付成功) + } + if err := s.store.InsertEvent(ctx, o.ID, "closed", "", o.AmountMinor, + fmt.Sprintf(`{"reason":%q,"time":%q}`, reason, time.Now().Format(time.RFC3339))); err != nil { + logger.Warn("order", "写关闭事件失败", "err", err, "out_trade_no", outTradeNo) + } + // 备注码释放(订单终态 CLOSED) + s.releaseNoteCode(o) + // WAIT_PAY 池同步:订单已关单,移出池 + s.notifyWaitPayRemove(o) + logger.Info("order", "订单已关闭", "out_trade_no", outTradeNo, "reason", reason) + return true, nil +} + +// SettleByQuery 查单补账:查单确认已支付后,走 SettlePayment 幂等入账 +// 金额不一致 → 进异常池(amount_mismatch),绝不自动入账 +func (s *Service) SettleByQuery(ctx context.Context, outTradeNo string, channelTxID string, paidAmount int64, evidence string) (bool, error) { + o, err := s.store.GetOrder(ctx, outTradeNo) + if err != nil { + return false, err + } + if o == nil { + return false, nil + } + // 金额校验:查单金额与订单金额不一致 → 进异常池,不自动入账 + if paidAmount != 0 && paidAmount != o.AmountMinor { + logger.Error("order", "查单金额与订单不一致(重大)", "out_trade_no", outTradeNo, + "order_amount", o.AmountMinor, "query_amount", paidAmount) + s.EnqueueException(ctx, o, model.ExReasonAmountMismatch, model.StatusSuccess, channelTxID, + evidence, fmt.Sprintf("查单金额 %d 与订单金额 %d 不一致", paidAmount, o.AmountMinor)) + return false, fmt.Errorf("查单金额不匹配: 订单=%d 查单=%d", o.AmountMinor, paidAmount) + } + // 幂等入账(走 SettlePayment:状态机 + 账本 + 通知) + return s.SettlePayment(ctx, &channel.NotifyResult{ + OutTradeNo: outTradeNo, + ChannelTxID: channelTxID, + AmountMinor: paidAmount, + RawPayload: evidence, + }) +} + +// SettleManual 人工确认入账(管理员授权操作,允许金额覆盖实际支付额) +// 与 SettlePayment 同路径:状态机 + 账本 hash 链 + 创建通知任务,全程审计 +func (s *Service) SettleManual(ctx context.Context, outTradeNo, channelTxID string, paidAmount int64, payer, handler, note string) (bool, error) { + o, err := s.store.GetOrder(ctx, outTradeNo) + if err != nil { + return false, err + } + if o == nil { + return false, nil + } + if o.Status == model.StatusSuccess { + // 崩溃恢复:若账本在上次人工入账中丢失(条件更新成功后进程崩溃),此处补写 + s.ensureRecorded(ctx, o, &channel.NotifyResult{ChannelTxID: channelTxID}) + // 备注码释放(幂等:重复释放无害) + s.releaseNoteCode(o) + logger.Info("order", "订单已支付,人工入账幂等返回", "out_trade_no", outTradeNo) + return true, nil + } + // 终态保护:已关单订单禁止人工入账重开(与 SettlePayment H9 守卫一致) + if o.Status == model.StatusClosed { + logger.Error("order", "人工入账拒绝:订单已关单", "out_trade_no", outTradeNo, "amount", paidAmount, "handler", handler) + if s.alert != nil { + s.alert.Send("error", "人工入账被拒(已关单)", + fmt.Sprintf("管理员 %s 试图人工入账已关单订单 %s (amount=%d)\n关单终态不可重开,请走 H9 迟到支付证据流程或上游对账退款", + handler, outTradeNo, paidAmount)) + } + return false, fmt.Errorf("订单 %s 已关单,禁止人工入账", outTradeNo) + } + // 互斥锁:条件更新(防并发双写账本/事件) + ok, err := s.store.UpdateOrderStatus(ctx, outTradeNo, model.StatusWaitPay, model.StatusSuccess, func(upd *model.Order) { + if channelTxID != "" { + upd.ChannelTxID = channelTxID + } + if payer != "" { + upd.Payer = payer + } + }) + if err != nil { + return false, err + } + if !ok { + return false, nil // 状态非法(如已关闭) + } + amount := paidAmount + if amount <= 0 { + amount = o.AmountMinor + } + eventPayload := fmt.Sprintf(`{"out_trade_no":%q,"amount":%d,"status":"TRADE_SUCCESS","channel_tx_id":%q,"source":"manual","handler":%q,"note":%q,"time":%q}`, + outTradeNo, amount, channelTxID, handler, note, time.Now().Format(time.RFC3339)) + // 先事件后账本;任一失败返回错误,重试时由幂等分支 ensureRecorded 补写 + if err := s.store.InsertEvent(ctx, o.ID, "paid_manual", channelTxID, amount, eventPayload); err != nil { + logger.Error("order", "人工入账流水写入失败(等待重试)", "err", err, "out_trade_no", outTradeNo) + return false, err + } + if err := s.ledger.Append(ctx, o.ID, eventPayload); err != nil { + logger.Error("order", "人工入账账本写入失败(等待重试,幂等分支将补写)", "err", err, "out_trade_no", outTradeNo) + if s.alert != nil { + s.alert.Send("error", "人工入账账本写入失败", + fmt.Sprintf("订单 %s 人工入账但账本写入失败(重大)\n错误: %v", outTradeNo, err)) + } + return false, err + } + logger.Warn("order", "人工确认入账(管理员操作)", "out_trade_no", outTradeNo, + "amount", amount, "handler", handler, "pid", o.Pid, "upstream", o.UpstreamName) + // 创建通知任务(易支付协议:通知金额用订单金额) + s.EnqueueNotify(ctx, o) + // 备注码释放(订单终态 SUCCESS) + s.releaseNoteCode(o) + // WAIT_PAY 池同步:订单已入账,移出池 + s.notifyWaitPayRemove(o) + return true, nil +} + +// EnqueueException 订单进人工异常池(幂等:同单已存在则仅补审计) +func (s *Service) EnqueueException(ctx context.Context, o *model.Order, reason, channelStatus, channelTxID, evidence, suggestion string) { + e := &model.ExceptionOrder{ + OutTradeNo: o.OutTradeNo, + Pid: o.Pid, + UpstreamName: o.UpstreamName, + Channel: o.Channel, + AmountMinor: o.AmountMinor, + OrderStatus: o.Status, + ChannelStatus: channelStatus, + ChannelTxID: channelTxID, + Reason: reason, + Evidence: evidence, + Suggestion: suggestion, + } + if err := s.store.CreateException(ctx, e); err != nil { + logger.Error("order", "创建异常单失败", "err", err, "out_trade_no", o.OutTradeNo) + return + } + logger.Warn("order", "订单进入人工异常池", "out_trade_no", o.OutTradeNo, + "reason", reason, "channel_status", channelStatus) + // 告警:进池事件(RESILIENCE P1-3) + if s.alert != nil { + s.alert.Send("error", "异常池进单", + fmt.Sprintf("订单 %s 进入人工异常池\n原因: %s\n通道状态: %s\n建议: %s", + o.OutTradeNo, reason, channelStatus, suggestion)) + } +} + +// BuildNotifyPayload 构造易支付协议回调参数并签名,返回 JSON payload。 +// 供 EnqueueNotify(下单入队)与死信手动重发(用订单最新 notify_url)复用,保证字段/签名单一来源。 +func (s *Service) BuildNotifyPayload(o *model.Order) string { + params := map[string]string{ + "trade_no": o.GatewayNo, + "out_trade_no": o.OutTradeNo, + "type": o.Channel, + "name": o.Subject, + "money": o.MoneyYuan(), + "trade_status": "TRADE_SUCCESS", + // notify_url 必须进 payload:worker 用它作为发送地址, + // 且参与签名(易支付协议标准),New API 验签时用收到的全部参数重算,保证一致 + "notify_url": o.NotifyURL, + } + // attach(上游请求ID)原样带回,参与签名,供上游对账 + if o.Attach != "" { + params["attach"] = o.Attach + } + if o.ChannelTxID != "" { + params["trade_no"] = o.ChannelTxID // 优先用通道交易号(易支付协议语义) + } + epay.BuildSignedParams(params, s.signKey(o.Pid)) + payload, _ := json.Marshal(params) + return string(payload) +} + +// EnqueueNotify 创建回调接入方任务(outbox) +func (s *Service) EnqueueNotify(ctx context.Context, o *model.Order) { + payload := s.BuildNotifyPayload(o) + taskID, err := s.store.CreateNotifyTask(ctx, o.ID, payload) + if err != nil { + logger.Error("order", "创建回调任务失败", "err", err, "out_trade_no", o.OutTradeNo) + return + } + logger.Info("order", "回调任务已入队", "task_id", taskID, "out_trade_no", o.OutTradeNo) +} + +// recordEvent 记录资金流水 +func (s *Service) recordEvent(ctx context.Context, o *model.Order, eventType, txID string, amount int64, raw string) { + if err := s.store.InsertEvent(ctx, o.ID, eventType, txID, amount, raw); err != nil { + logger.Error("order", "写入资金流水失败", "err", err, "out_trade_no", o.OutTradeNo, "event", eventType) + } +} + +// QueryOrder 查单(易支付协议 /api.php) +func (s *Service) QueryOrder(ctx context.Context, outTradeNo string) (*model.Order, error) { + return s.store.GetOrder(ctx, outTradeNo) +} + +// ---------- WAIT_PAY 池同步钩子(个人免签通道,资金判定不依赖钩子) ---------- + +// notifyWaitPayAdd 订单进入 WAIT_PAY 且已绑定账户 → 通知池(仅 personal.* 通道) +func (s *Service) notifyWaitPayAdd(o *model.Order) { + if s.waitPay == nil || s.waitPay.Add == nil || o == nil || o.AccountID <= 0 { + return + } + if !strings.HasPrefix(o.Channel, "personal.") { + return + } + s.waitPay.Add(o) +} + +// notifyWaitPayUpsert 订单凭据刷新/换通道(WAIT_PAY 内 NoteCode/AccountID 变化)→ 覆盖更新池 +func (s *Service) notifyWaitPayUpsert(o *model.Order) { + if s.waitPay == nil || s.waitPay.Upsert == nil || o == nil || o.AccountID <= 0 { + return + } + if !strings.HasPrefix(o.Channel, "personal.") { + return + } + s.waitPay.Upsert(o) +} + +// notifyWaitPayRemove 订单离开 WAIT_PAY(入账/关单终态)→ 通知池移除 +func (s *Service) notifyWaitPayRemove(o *model.Order) { + if s.waitPay == nil || s.waitPay.Remove == nil || o == nil { + return + } + if !strings.HasPrefix(o.Channel, "personal.") { + return + } + s.waitPay.Remove(o.OutTradeNo) +} diff --git a/internal/order/service_test.go b/internal/order/service_test.go new file mode 100755 index 0000000..4638625 --- /dev/null +++ b/internal/order/service_test.go @@ -0,0 +1,1497 @@ +package order + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "path/filepath" + "testing" + "time" + + "paygateway/internal/account" + "paygateway/internal/channel" + "paygateway/internal/ledger" + "paygateway/internal/model" + "paygateway/internal/store" + "paygateway/internal/upstream" +) + +// newTestService 创建真实 SQLite + 真实账本的测试服务 +func newTestService(t *testing.T) (*Service, *store.SQLiteStore, *store.LedgerDB, string) { + t.Helper() + dir := t.TempDir() + st, err := store.NewSQLite(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("创建 SQLite 失败: %v", err) + } + lgdb, err := store.NewLedgerDB(filepath.Join(dir, "ledger.db")) + if err != nil { + t.Fatalf("创建账本 DB 失败: %v", err) + } + lg, err := ledger.New(lgdb, "") // 自动生成测试密钥 + if err != nil { + t.Fatalf("创建账本失败: %v", err) + } + svc := New(st, lg, account.NewPool(), upstream.NewPool()) + return svc, st, lgdb, dir +} + +// createTestOrder 直接落库一笔待支付订单(绕过通道注册,专注测资金路径) +func createTestOrder(t *testing.T, st *store.SQLiteStore, outTradeNo string, amount int64) *model.Order { + t.Helper() + now := time.Now() + o := &model.Order{ + OutTradeNo: outTradeNo, + GatewayNo: "GW" + outTradeNo, + Channel: "alipay", + Pid: "2001", + AmountMinor: amount, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now, + } + if err := st.CreateOrder(context.Background(), o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + return o +} + +// TestSettlePayment_Normal 正常结算:状态→SUCCESS,事件+账本都有记录 +func TestSettlePayment_Normal(t *testing.T) { + svc, st, _, _ := newTestService(t) + ctx := context.Background() + o := createTestOrder(t, st, "NORMAL001", 100) // 1.00 元 + + ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{ + OutTradeNo: o.OutTradeNo, + ChannelTxID: "ALI_TX_001", + AmountMinor: 100, + }) + if err != nil || !ok { + t.Fatalf("SettlePayment = %v, %v; 期望 true, nil", ok, err) + } + + // 状态 + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status != model.StatusSuccess { + t.Fatalf("订单状态 = %s, 期望 %s", got.Status, model.StatusSuccess) + } + // 账本有记录 + has, err := svc.ledger.HasEvent(ctx, got.ID) + if err != nil || !has { + t.Fatalf("账本应有记录, has=%v err=%v", has, err) + } + // 事件有记录 + evs, _ := st.ListEvents(ctx, got.ID, 10) + if len(evs) != 1 || evs[0].EventType != "paid" { + t.Fatalf("事件记录 = %+v, 期望 1 条 paid", evs) + } +} + +// TestSettlePayment_Idempotent 幂等:重复回调不重复记账 +func TestSettlePayment_Idempotent(t *testing.T) { + svc, st, lgdb, _ := newTestService(t) + ctx := context.Background() + o := createTestOrder(t, st, "IDEM001", 100) + + // 第一次结算 + if ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: o.OutTradeNo, ChannelTxID: "ALI_TX_2", AmountMinor: 100}); err != nil || !ok { + t.Fatalf("第一次结算失败: %v %v", ok, err) + } + count1, _ := lgdb.CountLedger(ctx) + + // 重复回调(同交易号) + if ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: o.OutTradeNo, ChannelTxID: "ALI_TX_2", AmountMinor: 100}); err != nil || !ok { + t.Fatalf("幂等重试失败: %v %v", ok, err) + } + count2, _ := lgdb.CountLedger(ctx) + if count2 != count1 { + t.Fatalf("幂等重试不应重复记账: before=%d after=%d", count1, count2) + } + // 事件也应只有 1 条 paid + got, _ := st.GetOrder(ctx, o.OutTradeNo) + evs, _ := st.ListEvents(ctx, got.ID, 10) + paid := 0 + for _, e := range evs { + if e.EventType == "paid" { + paid++ + } + } + if paid != 1 { + t.Fatalf("paid 事件条数 = %d, 期望 1", paid) + } +} + +// TestSettlePayment_CrashRecovery 崩溃恢复:状态已 SUCCESS 但账本缺失(模拟条件更新后崩溃)→ 幂等分支补写 +func TestSettlePayment_CrashRecovery(t *testing.T) { + svc, st, lgdb, _ := newTestService(t) + ctx := context.Background() + o := createTestOrder(t, st, "CRASH001", 100) + + // 模拟崩溃窗口:仅条件更新状态为 SUCCESS(等价于进程在事件/账本写入前崩溃) + ok, err := st.UpdateOrderStatus(ctx, o.OutTradeNo, model.StatusWaitPay, model.StatusSuccess, nil) + if err != nil || !ok { + t.Fatalf("模拟崩溃状态更新失败: %v %v", ok, err) + } + // 确认账本确实没有记录 + got, _ := st.GetOrder(ctx, o.OutTradeNo) + has, _ := svc.ledger.HasEvent(ctx, got.ID) + if has { + t.Fatal("前置条件错误:账本不应有记录") + } + + // 回调重试 → 幂等分支 → ensureRecorded 补写账本 + if ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: o.OutTradeNo, ChannelTxID: "ALI_TX_3", AmountMinor: 100}); err != nil || !ok { + t.Fatalf("崩溃恢复结算失败: %v %v", ok, err) + } + has, err = svc.ledger.HasEvent(ctx, got.ID) + if err != nil || !has { + t.Fatalf("崩溃恢复后账本应有记录, has=%v err=%v", has, err) + } + // 不应重复记账(再触发一次幂等返回,账本条数不变) + count1, _ := lgdb.CountLedger(ctx) + svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: o.OutTradeNo, ChannelTxID: "ALI_TX_3", AmountMinor: 100}) + count2, _ := lgdb.CountLedger(ctx) + if count2 != count1 { + t.Fatalf("崩溃恢复后再结算不应重复记账: before=%d after=%d", count1, count2) + } +} + +// TestSettlePayment_AmountMismatch 金额不匹配:拒绝入账(防改价) +func TestSettlePayment_AmountMismatch(t *testing.T) { + svc, st, _, _ := newTestService(t) + ctx := context.Background() + o := createTestOrder(t, st, "AMT001", 100) + + ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{ + OutTradeNo: o.OutTradeNo, + ChannelTxID: "ALI_TX_4", + AmountMinor: 50, // 实付 0.50 ≠ 订单 1.00 + }) + if err == nil || ok { + t.Fatalf("金额不匹配应返回错误并拒绝, ok=%v err=%v", ok, err) + } + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status == model.StatusSuccess { + t.Fatal("金额不匹配时订单不应变为 SUCCESS") + } + // 应记录 amount_mismatch 事件(审计) + evs, _ := st.ListEvents(ctx, got.ID, 10) + found := false + for _, e := range evs { + if e.EventType == "amount_mismatch" { + found = true + } + } + if !found { + t.Fatal("应记录 amount_mismatch 审计事件") + } +} + +// TestSettleManual_Recovery 人工入账 + 幂等不重复记账 +func TestSettleManual_Recovery(t *testing.T) { + svc, st, lgdb, _ := newTestService(t) + ctx := context.Background() + o := createTestOrder(t, st, "MANUAL001", 100) + + ok, err := svc.SettleManual(ctx, o.OutTradeNo, "MAN_TX_1", 100, "payer1", "admin", "测试") + if err != nil || !ok { + t.Fatalf("人工入账失败: %v %v", ok, err) + } + count1, _ := lgdb.CountLedger(ctx) + + // 幂等重试不重复记账 + if ok, err := svc.SettleManual(ctx, o.OutTradeNo, "MAN_TX_1", 100, "payer1", "admin", "重试"); err != nil || !ok { + t.Fatalf("人工入账幂等重试失败: %v %v", ok, err) + } + count2, _ := lgdb.CountLedger(ctx) + if count2 != count1 { + t.Fatalf("人工入账幂等重试不应重复记账: before=%d after=%d", count1, count2) + } +} + +// TestSettlePayment_DeviceID 个人免签:SettlePayment 带 device_id → 订单 + 账本正确记录 +func TestSettlePayment_DeviceID(t *testing.T) { + svc, st, _, _ := newTestService(t) + ctx := context.Background() + o := createTestOrder(t, st, "DEVICE001", 100) + deviceID := "test-device-12345" + + // 下单时绑定收款账户(CreateOrder 路径语义:cred.AccountID → 订单 AccountID) + if ok, err := st.UpdateOrderStatus(ctx, o.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.AccountID = 2 + }); err != nil || !ok { + t.Fatalf("设置订单账户失败: %v %v", ok, err) + } + + ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{ + OutTradeNo: o.OutTradeNo, + ChannelTxID: "PSN-DEVICE-001", + AmountMinor: 100, + Payer: "personal:" + deviceID, + DeviceID: deviceID, + AccountID: 2, + }) + if err != nil || !ok { + t.Fatalf("SettlePayment = %v, %v; 期望 true, nil", ok, err) + } + + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.DeviceID != deviceID { + t.Fatalf("订单 device_id = %q, 期望 %q", got.DeviceID, deviceID) + } + if got.Payer != "personal:"+deviceID { + t.Fatalf("订单 payer = %q, 期望 personal:%s", got.Payer, deviceID) + } + if got.AccountID != 2 { + t.Fatalf("订单 account_id = %d, 期望 2", got.AccountID) + } + + has, err := svc.ledger.HasEvent(ctx, got.ID) + if err != nil || !has { + t.Fatalf("账本应有记录, has=%v err=%v", has, err) + } + res, err := svc.ledger.Verify(ctx) + if err != nil || res == nil || !res.Valid { + t.Fatalf("账本验证应通过, res=%v err=%v", res, err) + } +} + +// TestCloseExpiredOrders_PersonalOffline_NoClose 漏单防护:个人通道设备离线时绝不关单 +// (CloseExpiredOrders 走安全检查 → 离线 → 进异常池,订单保持 WAIT_PAY) +func TestCloseExpiredOrders_PersonalOffline_NoClose(t *testing.T) { + registerStubPersonal(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + // 个人通道订单(旧于关单窗口) + now := time.Now() + o := &model.Order{ + OutTradeNo: "P_OFFLINE", + GatewayNo: "GW_P_OFFLINE", + Channel: "personal.alipay", + Pid: "2001", + AmountMinor: 1000, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now.Add(-40 * time.Minute), + DeviceID: "dev-offline", + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + + // 注入 personalCheck:模拟设备离线(返回 offline=true) + svc.SetPersonalCheck(func(ctx context.Context, o *model.Order) (bool, string) { + return true, "设备 adb 不可达(测试模拟)" + }) + + closed, err := svc.CloseExpiredOrders(ctx, now.Add(-30*time.Minute)) + if err != nil { + t.Fatalf("CloseExpiredOrders err: %v", err) + } + if closed != 0 { + t.Fatalf("设备离线时不应关单:closed=%d", closed) + } + + // 订单必须保持 WAIT_PAY(未被误关) + got, err := st.GetOrder(ctx, o.OutTradeNo) + if err != nil || got == nil { + t.Fatalf("GetOrder: %v", err) + } + if got.Status != model.StatusWaitPay { + t.Fatalf("设备离线时订单应保持 WAIT_PAY,实际 %s(漏单!)", got.Status) + } + + // 必须已进人工异常池(人工核实路径) + exc, err := st.GetExceptionByOutTradeNo(ctx, o.OutTradeNo) + if err != nil || exc == nil { + t.Fatalf("离线订单应进人工异常池, exc=%v err=%v", exc, err) + } + if exc.Reason != model.ExReasonListenOffline { + t.Fatalf("异常池原因应为 listen_offline: %s", exc.Reason) + } +} + +// TestCloseExpiredOrders_PersonalOnline_Close 个人通道设备在线且已到关单窗口 → 正常关单 +func TestCloseExpiredOrders_PersonalOnline_Close(t *testing.T) { + registerStubPersonal(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + now := time.Now() + o := &model.Order{ + OutTradeNo: "P_ONLINE", + GatewayNo: "GW_P_ONLINE", + Channel: "personal.alipay", + Pid: "2001", + AmountMinor: 1000, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now.Add(-40 * time.Minute), + DeviceID: "dev-online", + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + + // 设备在线(返回 offline=false) + svc.SetPersonalCheck(func(ctx context.Context, o *model.Order) (bool, string) { + return false, "" + }) + + closed, err := svc.CloseExpiredOrders(ctx, now.Add(-30*time.Minute)) + if err != nil { + t.Fatalf("CloseExpiredOrders err: %v", err) + } + if closed != 1 { + t.Fatalf("设备在线应关单 1 笔:closed=%d", closed) + } + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status != model.StatusClosed { + t.Fatalf("设备在线应已关单:%s", got.Status) + } +} + +// TestCloseExpiredOrders_DynamicWindow_NotDue 动态关单窗口:CloseAtOf 未到期 → 跳过不关 +func TestCloseExpiredOrders_DynamicWindow_NotDue(t *testing.T) { + registerStubPersonal(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + now := time.Now() + o := &model.Order{ + OutTradeNo: "P_DYN", + GatewayNo: "GW_P_DYN", + Channel: "personal.alipay", + Pid: "2001", + AmountMinor: 1000, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now.Add(-40 * time.Minute), // 旧订单 + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + + // 注入 CloseAtOf:模拟动态窗口未到期(CloseAt 在未来)→ 跳过 + svc.SetWaitPayHooks(&WaitPayHooks{ + CloseAtOf: func(outTradeNo string) (time.Time, bool) { + if outTradeNo == "P_DYN" { + return now.Add(10 * time.Minute), true // 未到期 + } + return time.Time{}, false + }, + }) + // 设备在线(即使在线也不该关,因为窗口未到) + svc.SetPersonalCheck(func(ctx context.Context, o *model.Order) (bool, string) { + return false, "" + }) + + closed, err := svc.CloseExpiredOrders(ctx, now.Add(-30*time.Minute)) + if err != nil { + t.Fatalf("CloseExpiredOrders err: %v", err) + } + if closed != 0 { + t.Fatalf("动态窗口未到期不应关单:closed=%d", closed) + } + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status != model.StatusWaitPay { + t.Fatalf("未到期订单应保持 WAIT_PAY:%s", got.Status) + } +} + +// stubPersonalChannel 个人通道桩(测试用):注册表 Lookup 返回 ConfirmByManual, +// 让 CloseExpiredOrders 走个人安全检查分支(不实际下单/查单)。 +type stubPersonalChannel struct{} + +func (stubPersonalChannel) Name() string { return "personal.alipay" } +func (stubPersonalChannel) Type() channel.ChannelType { return channel.TypePersonal } +func (stubPersonalChannel) Kind() channel.ChannelKind { return channel.KindListener } +func (stubPersonalChannel) Modes() []channel.PayMode { return []channel.PayMode{channel.ModeQRCode} } +func (stubPersonalChannel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByManual } +func (stubPersonalChannel) Provider() channel.Provider { return channel.ProviderAlipay } +func (stubPersonalChannel) CreatePayment(context.Context, *channel.PayRequest) (*channel.PayCredential, error) { + return nil, nil +} +func (stubPersonalChannel) VerifyNotify(context.Context, *http.Request) (*channel.NotifyResult, error) { + return nil, nil +} +func (stubPersonalChannel) QueryOrder(context.Context, string) (*channel.OrderStatus, error) { + return nil, ErrNotQueryable +} +func (stubPersonalChannel) Listener() channel.EventListener { return nil } + +// registerStubPersonal 注册个人通道桩(幂等:已注册跳过) +func registerStubPersonal(t *testing.T) { + t.Helper() + if _, ok := channel.Lookup("personal.alipay"); ok { + return + } + channel.Register(stubPersonalChannel{}) +} + +// TestCloseExpiredOrders_Merchant_RespectsFixedWindow 商家通道固定关单窗口: +// 候选集被动态窗口放宽(before=now-11min)时,未到 ORDER_CLOSE_MINUTES(30min) 的商家订单不关单。 +func TestCloseExpiredOrders_Merchant_RespectsFixedWindow(t *testing.T) { + svc, st, _, _ := newTestService(t) + ctx := context.Background() + now := time.Now() + + // 商家通道订单,20 分钟前创建(早于动态候选集 11min,但未到商家固定 30min) + o := &model.Order{ + OutTradeNo: "M_FIXED", + GatewayNo: "GW_M_FIXED", + Channel: "alipay", // 商家通道(ConfirmByQuery) + Pid: "2001", + AmountMinor: 1000, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now.Add(-20 * time.Minute), + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + // 商家固定关单窗口 30min + svc.SetCloseWindow(30) + + // 候选集放宽到 11min(模拟动态窗口模式下扫描器传的 before) + closed, err := svc.CloseExpiredOrders(ctx, now.Add(-11*time.Minute)) + if err != nil { + t.Fatalf("CloseExpiredOrders err: %v", err) + } + if closed != 0 { + t.Fatalf("商家订单未到 30min 固定窗口不应关单:closed=%d", closed) + } + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status != model.StatusWaitPay { + t.Fatalf("商家订单应保持 WAIT_PAY:%s", got.Status) + } +} + +// TestCloseExpiredOrders_Merchant_OverFixedWindow 商家通道已到固定窗口 → 正常关单 +func TestCloseExpiredOrders_Merchant_OverFixedWindow(t *testing.T) { + registerStubMerchant(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + now := time.Now() + + o := &model.Order{ + OutTradeNo: "M_OVER", + GatewayNo: "GW_M_OVER", + Channel: "alipay", + Pid: "2001", + AmountMinor: 1000, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now.Add(-40 * time.Minute), // 已超 30min + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + svc.SetCloseWindow(30) + + // 商家通道无可用查单通道 → queryOrderFor 返回 ErrNotQueryable → 视为未支付关单 + closed, err := svc.CloseExpiredOrders(ctx, now.Add(-30*time.Minute)) + if err != nil { + t.Fatalf("CloseExpiredOrders err: %v", err) + } + if closed != 1 { + t.Fatalf("商家订单已到固定窗口应关单:closed=%d", closed) + } + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status != model.StatusClosed { + t.Fatalf("商家订单应已关闭:%s", got.Status) + } +} + +// stubMerchantChannel 商家通道桩(测试用):ConfirmByQuery,QueryOrder 返回未支付。 +type stubMerchantChannel struct{} + +func (stubMerchantChannel) Name() string { return "alipay" } +func (stubMerchantChannel) Type() channel.ChannelType { return channel.TypeMerchant } +func (stubMerchantChannel) Kind() channel.ChannelKind { return channel.KindCallback } +func (stubMerchantChannel) Modes() []channel.PayMode { + return []channel.PayMode{channel.ModePage, channel.ModeQRCode} +} +func (stubMerchantChannel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByQuery } +func (stubMerchantChannel) Provider() channel.Provider { return channel.ProviderAlipay } +func (stubMerchantChannel) CreatePayment(context.Context, *channel.PayRequest) (*channel.PayCredential, error) { + return nil, nil +} +func (stubMerchantChannel) VerifyNotify(context.Context, *http.Request) (*channel.NotifyResult, error) { + return nil, nil +} +func (stubMerchantChannel) QueryOrder(context.Context, string) (*channel.OrderStatus, error) { + // 模拟官方通道查单:订单仍处于"未支付"状态 + return &channel.OrderStatus{Status: model.StatusWaitPay}, nil +} +func (stubMerchantChannel) Listener() channel.EventListener { return nil } + +// registerStubMerchant 注册商家通道桩(幂等:已注册跳过) +func registerStubMerchant(t *testing.T) { + t.Helper() + if _, ok := channel.Lookup("alipay"); ok { + return + } + channel.Register(stubMerchantChannel{}) +} + +// TestCloseExpiredOrders_PersonalOnline_StalledListener 监听端在线但此前已进过 LISTEN_TIMEOUT 异常池 +// (复查过仍无推送,监听端僵死风险)→ 扫描器不直接关单,交由人工核实(P2-1 修复)。 +func TestCloseExpiredOrders_PersonalOnline_StalledListener(t *testing.T) { + registerStubPersonal(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + now := time.Now() + o := &model.Order{ + OutTradeNo: "P_STALLED", + GatewayNo: "GW_P_STALLED", + Channel: "personal.alipay", + Pid: "2001", + AmountMinor: 1000, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + CreatedAt: now.Add(-40 * time.Minute), + DeviceID: "dev-online", + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + + // 设备在线(返回 offline=false) + svc.SetPersonalCheck(func(ctx context.Context, o *model.Order) (bool, string) { + return false, "" + }) + + // 先模拟 reconcile 复查路径已将该单进过 LISTEN_TIMEOUT 异常池 + exc := &model.ExceptionOrder{ + OutTradeNo: o.OutTradeNo, + Pid: o.Pid, + Channel: o.Channel, + AmountMinor: o.AmountMinor, + OrderStatus: o.Status, + Reason: model.ExReasonListenTimeout, + Suggestion: "监听端在线但长时间未收到收款推送,需人工确认实际收款情况", + } + if err := st.CreateException(ctx, exc); err != nil { + t.Fatalf("预置异常记录失败: %v", err) + } + + closed, err := svc.CloseExpiredOrders(ctx, now.Add(-30*time.Minute)) + if err != nil { + t.Fatalf("CloseExpiredOrders err: %v", err) + } + if closed != 0 { + t.Fatalf("监听端僵死风险订单不应关单:closed=%d", closed) + } + got, _ := st.GetOrder(ctx, o.OutTradeNo) + if got.Status != model.StatusClosed { + t.Logf("订单保持 WAIT_PAY(转人工核实路径)") + } else { + t.Fatalf("监听端僵死风险订单被误关(漏单!):%s", got.Status) + } +} + +// stubQueryFailChannel 商家通道桩:关单前查单必失败(模拟沙箱/凭据异常)。 +type stubQueryFailChannel struct{ name string } + +func (c stubQueryFailChannel) Name() string { return c.name } +func (c stubQueryFailChannel) Type() channel.ChannelType { return channel.TypeMerchant } +func (c stubQueryFailChannel) Kind() channel.ChannelKind { return channel.KindCallback } +func (c stubQueryFailChannel) Modes() []channel.PayMode { return []channel.PayMode{channel.ModeQRCode} } +func (c stubQueryFailChannel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByQuery } +func (c stubQueryFailChannel) Provider() channel.Provider { return channel.ProviderAlipay } +func (c stubQueryFailChannel) CreatePayment(context.Context, *channel.PayRequest) (*channel.PayCredential, error) { + return nil, nil +} +func (c stubQueryFailChannel) VerifyNotify(context.Context, *http.Request) (*channel.NotifyResult, error) { + return nil, nil +} +func (c stubQueryFailChannel) QueryOrder(context.Context, string) (*channel.OrderStatus, error) { + return nil, fmt.Errorf("所有支付宝账户查询均失败") +} +func (c stubQueryFailChannel) Listener() channel.EventListener { return nil } + +// TestCloseExpiredOrders_QueryFail_EntersPoolOnce 查单失败风暴抑制: +// 首次扫描失败 → 入人工池(close_uncertain)一次;后续扫描静默跳过,不再重复入池。 +func TestCloseExpiredOrders_QueryFail_EntersPoolOnce(t *testing.T) { + chName := "merchant.queryfail" + if _, ok := channel.Lookup(chName); !ok { + channel.Register(stubQueryFailChannel{name: chName}) + } + svc, st, _, _ := newTestService(t) + svc.SetCloseWindow(30) + ctx := context.Background() + now := time.Now() + + o := &model.Order{ + OutTradeNo: "M_QFAIL", GatewayNo: "GW_M_QFAIL", Channel: chName, + Pid: "2001", AmountMinor: 1000, Currency: "CNY", Subject: "测试", + Status: model.StatusWaitPay, CreatedAt: now.Add(-40 * time.Minute), + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("下单失败: %v", err) + } + + // 第一次扫描:查单失败 → 入池一次,订单不关 + closed, err := svc.CloseExpiredOrders(ctx, now) + if err != nil { + t.Fatalf("扫描 err: %v", err) + } + if closed != 0 { + t.Fatalf("查单失败不应关单, closed=%d", closed) + } + got, _ := st.GetOrder(ctx, "M_QFAIL") + if got.Status != model.StatusWaitPay { + t.Fatalf("查单失败订单保持 WAIT_PAY:%s", got.Status) + } + exc, err := st.GetExceptionByOutTradeNo(ctx, "M_QFAIL") + if err != nil || exc == nil { + t.Fatalf("首次查单失败应入人工池: err=%v exc=%v", err, exc) + } + if exc.Reason != model.ExReasonCloseUncertain { + t.Fatalf("异常原因应 close_uncertain,实际 %s", exc.Reason) + } + + // 第二次扫描:已有记录 → 静默跳过,不再重复入池(风暴抑制断言) + before, err := st.CountExceptions(ctx, "") + if err != nil { + t.Fatalf("计数失败: %v", err) + } + _, err = svc.CloseExpiredOrders(ctx, now) + if err != nil { + t.Fatalf("第二次扫描 err: %v", err) + } + after, err := st.CountExceptions(ctx, "") + if err != nil { + t.Fatalf("计数失败: %v", err) + } + if after != before { + t.Fatalf("第二次扫描不应再入池:before=%d after=%d", before, after) + } +} + +// stubPaymentChannel 返回有效凭据的通道桩,用于测试复用更新金额触发凭据刷新(RefreshPayment/SwitchChannel/ReviveOrder)。 +type stubPaymentChannel struct{} + +func (stubPaymentChannel) Name() string { return "personal.test" } +func (stubPaymentChannel) Type() channel.ChannelType { return channel.TypePersonal } +func (stubPaymentChannel) Kind() channel.ChannelKind { return channel.KindListener } +func (stubPaymentChannel) Modes() []channel.PayMode { return []channel.PayMode{channel.ModeQRCode} } +func (stubPaymentChannel) ConfirmAbility() channel.ConfirmAbility { return channel.ConfirmByManual } +func (stubPaymentChannel) Provider() channel.Provider { return channel.ProviderAlipay } +func (stubPaymentChannel) CreatePayment(context.Context, *channel.PayRequest) (*channel.PayCredential, error) { + return &channel.PayCredential{QRCode: "psn://test", AccountID: 1}, nil +} +func (stubPaymentChannel) VerifyNotify(context.Context, *http.Request) (*channel.NotifyResult, error) { + return nil, nil +} +func (stubPaymentChannel) QueryOrder(context.Context, string) (*channel.OrderStatus, error) { + return nil, ErrNotQueryable +} +func (stubPaymentChannel) Listener() channel.EventListener { return nil } + +// registerStubPayment 注册返回有效凭据的通道桩(幂等:已注册跳过) +func registerStubPayment(t *testing.T) { + t.Helper() + if _, ok := channel.Lookup("personal.test"); ok { + return + } + channel.Register(stubPaymentChannel{}) +} + +// createWaitPayOrderWithCred 创建带凭据的 WAIT_PAY 订单,并通过 GetOrder 返回带 ID 的最新对象(模拟真实调用链)。 +func createWaitPayOrderWithCred(t *testing.T, st *store.SQLiteStore, outTradeNo, ch string, amount int64) *model.Order { + t.Helper() + o := &model.Order{ + OutTradeNo: outTradeNo, + GatewayNo: "GW" + outTradeNo, + Channel: ch, + Pid: "2001", + AmountMinor: amount, + Currency: "CNY", + Subject: "测试", + Status: model.StatusWaitPay, + NotifyURL: "http://example.com/notify", + QrCode: "old://qr", + CreatedAt: time.Now(), + } + if err := st.CreateOrder(context.Background(), o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + latest, err := st.GetOrder(context.Background(), outTradeNo) + if err != nil { + t.Fatalf("读回订单失败: %v", err) + } + return latest +} + +// TestReuseOrder_AmountChanged_Refresh 同订单号复用传不同金额 → 金额不可变,拒单。 +// 回归:旧行为覆盖已存在订单金额(UpdateOrderAmount)+ 以新金额刷新凭据;该行为会导致 +// 回调按订单金额严校验时实付/订单不一致而永不入账,且订单号+金额是上游不可变主键, +// 同号改金额应由上游换新单号(买家级替换,见 CreateOrder 守卫)而非覆盖复用。 +func TestReuseOrder_AmountChanged_Refresh(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + o := createWaitPayOrderWithCred(t, st, "REUSE_AMT", "personal.test", 100) + + _, err := svc.reuseOrCreateError(context.Background(), o, "personal.test", 200) + if err == nil { + t.Fatal("同号不同金额应拒单(金额不可变)") + } + + got, _ := st.GetOrder(context.Background(), o.OutTradeNo) + if got.AmountMinor != 100 { + t.Fatalf("金额不可变:DB 订单金额 = %d, 期望保持 100(未被覆盖)", got.AmountMinor) + } + if got.QrCode != "old://qr" { + t.Fatalf("金额不可变:不应刷新凭据,%q", got.QrCode) + } + if got.Status != model.StatusWaitPay { + t.Fatalf("金额不可变:订单不应被改状态,status=%s", got.Status) + } + evs, _ := st.ListEvents(context.Background(), got.ID, 10) + for _, e := range evs { + if e.EventType == "amount_updated" { + t.Fatal("不应再记录 amount_updated 事件(金额不可变)") + } + } +} + +// TestReuseOrder_AmountSame_NoRefresh 复用订单传入相同金额 → 纯复用,不刷新凭据、不改金额。 +func TestReuseOrder_AmountSame_NoRefresh(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + o := createWaitPayOrderWithCred(t, st, "REUSE_SAME", "personal.test", 100) + + res, err := svc.reuseOrCreateError(context.Background(), o, "personal.test", 100) + if err != nil { + t.Fatalf("reuseOrCreateError err: %v", err) + } + if !res.Reused { + t.Fatal("应标记为复用") + } + if res.Order.AmountMinor != 100 { + t.Fatalf("同金额复用订单金额 = %d, 期望 100", res.Order.AmountMinor) + } + got, _ := st.GetOrder(context.Background(), o.OutTradeNo) + if got.QrCode != "old://qr" { + t.Fatalf("同金额复用不应刷新凭据:%q", got.QrCode) + } +} + +// TestReuseOrder_AmountChanged_ClosedRevive 已关单订单同号复用传新金额 → 金额不可变,拒单(复活前置金额校验)。 +func TestReuseOrder_AmountChanged_ClosedRevive(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + _ = createWaitPayOrderWithCred(t, st, "REUSE_CLOSED", "personal.test", 100) + if ok, err := svc.CloseOrder(context.Background(), "REUSE_CLOSED", "timeout"); err != nil || !ok { + t.Fatalf("关单失败: ok=%v err=%v", ok, err) + } + closed, _ := st.GetOrder(context.Background(), "REUSE_CLOSED") + + _, err := svc.reuseOrCreateError(context.Background(), closed, "personal.test", 300) + if err == nil { + t.Fatal("同号不同金额应拒单(即使已关单,金额仍不可变)") + } + got, _ := st.GetOrder(context.Background(), "REUSE_CLOSED") + if got.AmountMinor != 100 { + t.Fatalf("金额不可变:DB 金额 = %d, 期望保持 100", got.AmountMinor) + } + if got.Status != model.StatusClosed { + t.Fatalf("金额不可变:拒单后订单不应被复活,status=%s", got.Status) + } +} + +// TestReuseOrder_AmountChanged_SwitchChannel 复用传新金额且换支付方式 → 金额不可变,拒单(通道切换前金额校验)。 +func TestReuseOrder_AmountChanged_SwitchChannel(t *testing.T) { + registerStubPersonal(t) // 原通道 personal.alipay(返回 nil 凭据,仅作 from 侧) + registerStubPayment(t) // 目标通道 personal.test(返回有效凭据) + svc, st, _, _ := newTestService(t) + o := createWaitPayOrderWithCred(t, st, "REUSE_SWITCH", "personal.alipay", 100) + + _, err := svc.reuseOrCreateError(context.Background(), o, "personal.test", 400) + if err == nil { + t.Fatal("同号不同金额应拒单(即使要切换通道,金额仍不可变)") + } + got, _ := st.GetOrder(context.Background(), o.OutTradeNo) + if got.Channel != "personal.alipay" { + t.Fatalf("金额不可变:拒单后通道不应被切换,channel=%s", got.Channel) + } + if got.AmountMinor != 100 { + t.Fatalf("金额不可变:DB 金额 = %d, 期望保持 100", got.AmountMinor) + } +} + +// TestCreateOrder_BuyerPendingNewTrade_ReplacesOld 买家级串行:同一 buyer_key 已有 WAIT_BUYER_PAY +// 待支付订单(OLD),又以新 out_trade_no 提交新订单(NEW) 时,应废弃旧单、返回新单。 +// Given: OLD 为 WAIT_BUYER_PAY @10.00 元、buyer_key=buyer-replace、通道 personal.test 已带凭据; +// +// NEW 为同 buyer_key、不同 out_trade_no、金额 1.00 元。 +// +// When: Service.CreateOrder(NEW) +// Then: 返回订单用 NEW(out_trade_no=NEW)、金额 100 分;OLD 已被关闭;NEW 存在于 DB。 +// 回归:当前 CreateOrder 买家守卫(service.go:205-227)错误地复用 OLD 并覆盖其金额,返回旧单。 +func TestCreateOrder_BuyerPendingNewTrade_ReplacesOld(t *testing.T) { + registerStubPayment(t) // personal.test 返回有效凭据 + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + // Given: OLD_ORDER WAIT_BUYER_PAY @10.00元,同 buyer_key,已带凭据 + old := createWaitPayOrderWithCred(t, st, "OLD_REPLACE", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.BuyerKey = "buyer-replace" + }); err != nil || !ok { + t.Fatalf("设置 OLD buyer_key 失败: ok=%v err=%v", ok, err) + } + old, _ = st.GetOrder(ctx, old.OutTradeNo) + if old.BuyerKey != "buyer-replace" { + t.Fatalf("前置条件:OLD buyer_key 未生效: %q", old.BuyerKey) + } + + // When: 以新单号提交 NEW_ORDER @1.00元 + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "NEW_REPLACE", + Type: "personal.test", + Name: "新单", + Money: "1.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-replace", + }) + if err != nil { + t.Fatalf("CreateOrder err: %v", err) + } + + // Then: 返回订单为新单,金额 100 分 + if res == nil || res.Order == nil { + t.Fatal("CreateOrder 返回空结果") + } + if res.Order.OutTradeNo != "NEW_REPLACE" { + t.Fatalf("返回订单 = %s, 期望新单 NEW_REPLACE(当前错误复用了旧单)", res.Order.OutTradeNo) + } + if res.Order.AmountMinor != 100 { + t.Fatalf("返回订单金额 = %d, 期望 100", res.Order.AmountMinor) + } + + // Then: OLD 已被关闭 + gotOld, _ := st.GetOrder(ctx, "OLD_REPLACE") + if gotOld == nil { + t.Fatal("OLD_REPLACE 应存在") + } + if gotOld.Status != model.StatusClosed { + t.Fatalf("OLD 状态 = %s, 期望 CLOSED(旧单应被关闭,而非继续复用)", gotOld.Status) + } + + // Then: NEW 存在于 DB + gotNew, err := st.GetOrder(ctx, "NEW_REPLACE") + if err != nil || gotNew == nil { + t.Fatalf("NEW_REPLACE 应存在于 DB, err=%v", err) + } +} + +// TestCreateOrder_BuyerReplacement_InheritsSessionExpiry 换金额不能重置支付会话总截止时间。 +func TestCreateOrder_BuyerReplacement_InheritsSessionExpiry(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + svc.SetCloseWindow(30) + expires := time.Now().Add(7 * time.Minute).Round(0) + old := createWaitPayOrderWithCred(t, st, "OLD_SESSION", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.BuyerKey = "buyer-session" + upd.SessionExpiresAt = expires + }); err != nil || !ok { + t.Fatalf("设置会话截止时间失败: ok=%v err=%v", ok, err) + } + + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "NEW_SESSION", + Type: "personal.test", + Name: "新单", + Money: "1.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-session", + }) + if err != nil { + t.Fatalf("替换下单失败: %v", err) + } + got, _ := st.GetOrder(ctx, res.Order.OutTradeNo) + if !got.SessionExpiresAt.Equal(expires) { + t.Fatalf("新订单会话截止时间 = %v, 期望继承 %v", got.SessionExpiresAt, expires) + } + if !got.CreatedAt.Before(got.SessionExpiresAt) { + t.Fatalf("新订单创建时间必须早于会话截止时间: created=%v expires=%v", got.CreatedAt, got.SessionExpiresAt) + } +} + +// TestCreateOrder_BuyerReplacement_ExpiredSessionRejects 换金额不能复活已过期支付会话。 +func TestCreateOrder_BuyerReplacement_ExpiredSessionRejects(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + old := createWaitPayOrderWithCred(t, st, "OLD_EXPIRED_SESSION", "personal.test", 1000) + expires := time.Now().Add(-time.Minute) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.BuyerKey = "buyer-expired-session" + upd.SessionExpiresAt = expires + }); err != nil || !ok { + t.Fatalf("设置过期会话失败: ok=%v err=%v", ok, err) + } + + _, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "NEW_EXPIRED_SESSION", + Type: "personal.test", + Name: "新单", + Money: "1.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-expired-session", + }) + if err == nil { + t.Fatal("过期支付会话不应继续替换创建新 WAIT_PAY") + } + if got, _ := st.GetOrder(ctx, "NEW_EXPIRED_SESSION"); got != nil { + t.Fatal("过期会话拒绝后不应创建新订单") + } +} + +// TestBuildNotifyPayload_UsesLatestNotifyURL 死信重发核心:BuildNotifyPayload 反映订单最新 NotifyURL。 +// 回归:死信重发此前固化任务创建时的旧地址 → 一直回调不了;此测试锁住「改对地址后重建 payload 用最新值」。 +func TestBuildNotifyPayload_UsesLatestNotifyURL(t *testing.T) { + svc, _, _, _ := newTestService(t) + o := &model.Order{ + GatewayNo: "GW123", + OutTradeNo: "OTN123", + Channel: "alipay", + Subject: "测试", + AmountMinor: 100, + NotifyURL: "http://old.example/cb", + } + var p map[string]string + parse := func(payload string) map[string]string { + m := map[string]string{} + if err := json.Unmarshal([]byte(payload), &m); err != nil { + t.Fatalf("payload 解析失败: %v", err) + } + return m + } + + p = parse(svc.BuildNotifyPayload(o)) + if p["notify_url"] != "http://old.example/cb" { + t.Fatalf("notify_url = %q, 期望旧地址", p["notify_url"]) + } + if p["sign"] == "" { + t.Fatal("payload 应含 sign") + } + if p["money"] != "1.00" { + t.Fatalf("money = %q, 期望 1.00", p["money"]) + } + + // 订单回调地址改对后,重建 payload 应反映最新地址 + o.NotifyURL = "http://new.example/cb" + p = parse(svc.BuildNotifyPayload(o)) + if p["notify_url"] != "http://new.example/cb" { + t.Fatalf("notify_url = %q, 期望最新地址 http://new.example/cb", p["notify_url"]) + } +} + +// TestCreateOrder_SameOrderNumber_DifferentAmount_Rejected 同订单号但金额不同 → 金额不可变,拒单。 +// Given: 已存在订单 SAME_NO @10.00元。 +// When: 以同订单号 + 1.00 元重提。 +// Then: 返回错误;DB 金额保持 1000;订单仍 WAIT_PAY;无 amount_updated 事件、无凭据刷新。 +func TestCreateOrder_SameOrderNumber_DifferentAmount_Rejected(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + createWaitPayOrderWithCred(t, st, "SAME_AMT", "personal.test", 1000) + + _, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "SAME_AMT", + Type: "personal.test", + Name: "重提", + Money: "1.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-same", + }) + if err == nil { + t.Fatal("同号不同金额应拒单(金额不可变)") + } + + got, _ := st.GetOrder(ctx, "SAME_AMT") + if got.AmountMinor != 1000 { + t.Fatalf("金额不可变:DB 金额 = %d, 期望保持 1000", got.AmountMinor) + } + if got.QrCode != "old://qr" { + t.Fatalf("金额不可变:不应刷新凭据,%q", got.QrCode) + } + if got.Status != model.StatusWaitPay { + t.Fatalf("金额不可变:订单应保持 WAIT_PAY,status=%s", got.Status) + } + evs, _ := st.ListEvents(ctx, got.ID, 10) + for _, e := range evs { + if e.EventType == "amount_updated" || e.EventType == "replaced" { + t.Fatalf("同号不同金额不应产生 %s 事件", e.EventType) + } + } +} + +// TestCreateOrder_SameOrderNumber_SameAmount_Reuse 同订单号且金额相同 → 幂等复用,不新建。 +// Given: 已存在订单 SAME_OK @10.00元(带凭据)。 +// When: 以同订单号 + 同金额 10.00 元重提。 +// Then: 返回 Reused=true,返回原订单(out_trade_no 相同、金额不变、凭据不变)。 +func TestCreateOrder_SameOrderNumber_SameAmount_Reuse(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + old := createWaitPayOrderWithCred(t, st, "SAME_OK", "personal.test", 1000) + + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "SAME_OK", + Type: "personal.test", + Name: "重提", + Money: "10.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-same-ok", + }) + if err != nil { + t.Fatalf("CreateOrder err: %v", err) + } + if res == nil || !res.Reused { + t.Fatalf("同号同金额应复用 Reused=true, res=%+v", res) + } + if res.Order.OutTradeNo != "SAME_OK" || res.Order.AmountMinor != 1000 { + t.Fatalf("复用应返回原单 SAME_OK@1000,got %s@%d", res.Order.OutTradeNo, res.Order.AmountMinor) + } + got, _ := st.GetOrder(ctx, "SAME_OK") + if got.AmountMinor != 1000 || got.QrCode != "old://qr" { + t.Fatalf("同号同金额复用不应改金额/凭据:amount=%d qr=%q", got.AmountMinor, got.QrCode) + } + if got.ID != old.ID { + t.Fatalf("同号同金额应复用同一订单记录,id=%d vs old=%d", got.ID, old.ID) + } +} + +// TestCreateOrder_BuyerReplacement_EventAndCleanup 买家替换旧单 → replaced 事件恰一条、备注码释放一次、池移除一次。 +// Given: OLD_REPL2 WAIT_PAY @10.00元、buyer_key、带 NoteCode。 +// When: CreateOrder(NEW_REPL2 @1.00元)。 +// Then: OLD 被关闭;写且仅写一条 replaced 事件;备注码释放一次;池移除一次;NEW 以新金额/新单号持久化。 +func TestCreateOrder_BuyerReplacement_EventAndCleanup(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + released := 0 + removed := 0 + svc.SetNoteCodeReleaser(func(ch, code string) { released++ }) + svc.SetWaitPayHooks(&WaitPayHooks{ + Remove: func(outTradeNo string) { removed++ }, + }) + + old := createWaitPayOrderWithCred(t, st, "OLD_REPL2", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.BuyerKey = "buyer-repl2" + upd.NoteCode = "NC-REPL2" + upd.AccountID = 1 + }); err != nil || !ok { + t.Fatalf("设置 OLD buyer_key/note_code 失败: %v %v", ok, err) + } + + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "NEW_REPL2", + Type: "personal.test", + Name: "新单", + Money: "1.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-repl2", + }) + if err != nil { + t.Fatalf("CreateOrder err: %v", err) + } + if res.Order.OutTradeNo != "NEW_REPL2" || res.Order.AmountMinor != 100 { + t.Fatalf("返回应为新单 NEW_REPL2@100,got %s@%d", res.Order.OutTradeNo, res.Order.AmountMinor) + } + + gotOld, _ := st.GetOrder(ctx, "OLD_REPL2") + if gotOld.Status != model.StatusClosed { + t.Fatalf("OLD 应 CLOSED,got %s", gotOld.Status) + } + evs, _ := st.ListEvents(ctx, gotOld.ID, 10) + replaced := 0 + for _, e := range evs { + if e.EventType == "replaced" { + replaced++ + if !json.Valid([]byte(e.RawPayload)) { + t.Fatalf("replaced 事件 payload 非法 JSON: %s", e.RawPayload) + } + } + } + if replaced != 1 { + t.Fatalf("replaced 事件条数 = %d, 期望恰 1", replaced) + } + if released != 1 { + t.Fatalf("备注码应释放恰 1 次,got %d", released) + } + if removed != 1 { + t.Fatalf("WAIT_PAY 池应移除恰 1 次,got %d", removed) + } + gotNew, _ := st.GetOrder(ctx, "NEW_REPL2") + if gotNew == nil || gotNew.AmountMinor != 100 { + t.Fatalf("NEW 应以新金额持久化:%+v", gotNew) + } +} + +// TestCreateOrder_BuyerReplacement_CallbackIdentity 替换后新单回调 payload 指向新单号/新金额。 +// When: 买家替换后创建 NEW。 +// Then: NEW 的 BuildNotifyPayload 的 out_trade_no/money 反映 NEW,而非 OLD。 +func TestCreateOrder_BuyerReplacement_CallbackIdentity(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + old := createWaitPayOrderWithCred(t, st, "OLD_CB", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.BuyerKey = "buyer-cb" + }); err != nil || !ok { + t.Fatalf("设置 OLD buyer_key 失败: %v %v", ok, err) + } + + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "NEW_CB", + Type: "personal.test", + Name: "新单", + Money: "0.05", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-cb", + }) + if err != nil { + t.Fatalf("CreateOrder err: %v", err) + } + + var params map[string]string + if err := json.Unmarshal([]byte(svc.BuildNotifyPayload(res.Order)), ¶ms); err != nil { + t.Fatalf("payload 解析失败: %v", err) + } + if params["out_trade_no"] != "NEW_CB" { + t.Fatalf("回调 out_trade_no = %q, 期望 NEW_CB(不能引用 OLD)", params["out_trade_no"]) + } + if params["money"] != "0.05" { + t.Fatalf("回调 money = %q, 期望 0.05(新单金额)", params["money"]) + } +} + +// TestReplacePendingOrder_AlreadyClosed_NoEvent OLD 已被并发替换关闭 → 替换幂等:不重复写 replaced 事件,仍可继续创建 NEW。 +func TestReplacePendingOrder_AlreadyClosed_NoEvent(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + old := createWaitPayOrderWithCred(t, st, "OLD_CLOSED", "personal.test", 1000) + + if ok, err := svc.CloseOrder(ctx, old.OutTradeNo, "timeout"); err != nil || !ok { + t.Fatalf("预置 OLD 为 CLOSED 失败: %v %v", ok, err) + } + + _, create, err := svc.replacePendingOrder(ctx, old, "NEW_CLOSED", 100) + if err != nil { + t.Fatalf("replacePendingOrder err: %v", err) + } + if !create { + t.Fatal("OLD 已被关闭(非结算)时仍应继续创建 NEW") + } + evs, _ := st.ListEvents(ctx, old.ID, 10) + for _, e := range evs { + if e.EventType == "replaced" { + t.Fatal("OLD 已被并发替换关闭时不应再写 replaced 事件(幂等)") + } + } +} + +// TestReplacePendingOrder_ConcurrentSettlement_NoProceed OLD 已被并发结算(SUCCESS) → 替换 CAS 失败,返回 proceed=false,禁止创建新单。 +func TestReplacePendingOrder_ConcurrentSettlement_NoProceed(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + old := createWaitPayOrderWithCred(t, st, "OLD_SETTLED", "personal.test", 1000) + + if ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: old.OutTradeNo, ChannelTxID: "TX-SETTLED", AmountMinor: 1000}); err != nil || !ok { + t.Fatalf("预置 OLD 结算失败: %v %v", ok, err) + } + + _, create, err := svc.replacePendingOrder(ctx, old, "NEW_SETTLED", 100) + if err != nil { + t.Fatalf("replacePendingOrder err: %v", err) + } + if create { + t.Fatal("OLD 已被并发结算胜出时不得创建新单(防重复收款)") + } + // CAS 失败 → 订单保持 SUCCESS,不写 replaced,不移除池/释放码 + got, _ := st.GetOrder(ctx, old.OutTradeNo) + if got.Status != model.StatusSuccess { + t.Fatalf("OLD 应保持 SUCCESS,got %s", got.Status) + } + evs, _ := st.ListEvents(ctx, old.ID, 10) + for _, e := range evs { + if e.EventType == "replaced" { + t.Fatal("结算胜出后不应写 replaced 事件") + } + } +} + +// TestCreateOrder_BuyerReplacement_AfterSettleAllowsNew OLD 已被结算(SUCCESS)后,买家可正常下新单。 +// Given: OLD 已结算入账(不再是 WAIT_PAY,不占用买家串行锁)。 +// When: CreateOrder 以同 buyer_key + 新单号提交。 +// Then: 成功创建 NEW(已支付旧单不阻塞新单;替换仅在 OLD 仍 WAIT_PAY 的竞态窗口内通过 CAS 拒绝,见单测)。 +func TestCreateOrder_BuyerReplacement_AfterSettleAllowsNew(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + old := createWaitPayOrderWithCred(t, st, "OLD_SETTLED3", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(upd *model.Order) { + upd.BuyerKey = "buyer-settled3" + }); err != nil || !ok { + t.Fatalf("设置 buyer_key 失败: %v %v", ok, err) + } + if ok, err := svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: old.OutTradeNo, ChannelTxID: "TX-S3", AmountMinor: 1000}); err != nil || !ok { + t.Fatalf("预置结算失败: %v %v", ok, err) + } + + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: "NEW_SETTLED3", + Type: "personal.test", + Name: "新单", + Money: "1.00", + NotifyURL: "http://example.com/notify", + BuyerKey: "buyer-settled3", + }) + if err != nil { + t.Fatalf("CreateOrder err(已支付旧单不阻塞新单): %v", err) + } + if res.Order.OutTradeNo != "NEW_SETTLED3" { + t.Fatalf("应创建新单 NEW_SETTLED3,got %s", res.Order.OutTradeNo) + } + got, _ := st.GetOrder(ctx, "NEW_SETTLED3") + if got == nil { + t.Fatal("NEW_SETTLED3 应存在于 DB") + } +} + +// TestReplacePendingOrder_Concurrent_Goroutine 真实 SQLite 并发:替代替换 与 结算 并行, +// 断言不变量:绝不双写 replaced、绝不双入账;OLD 终态必为 SUCCESS 或 CLOSED 之一。 +func TestReplacePendingOrder_Concurrent_Goroutine(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + old := createWaitPayOrderWithCred(t, st, "OLD_RACE", "personal.test", 1000) + + start := make(chan struct{}) + done := make(chan struct{}, 2) + + go func() { + <-start + svc.SettlePayment(ctx, &channel.NotifyResult{OutTradeNo: old.OutTradeNo, ChannelTxID: "TX-RACE", AmountMinor: 1000}) + done <- struct{}{} + }() + go func() { + <-start + svc.replacePendingOrder(ctx, old, "NEW_RACE", 100) + done <- struct{}{} + }() + close(start) + <-done + <-done + + got, _ := st.GetOrder(ctx, old.OutTradeNo) + if got.Status != model.StatusSuccess && got.Status != model.StatusClosed { + t.Fatalf("OLD 终态应为 SUCCESS 或 CLOSED,got %s", got.Status) + } + evs, _ := st.ListEvents(ctx, old.ID, 50) + replaced := 0 + paid := 0 + for _, e := range evs { + switch e.EventType { + case "replaced": + replaced++ + case "paid": + paid++ + } + } + if replaced > 1 { + t.Fatalf("replaced 事件最多写 1 条,got %d", replaced) + } + if paid > 1 { + t.Fatalf("paid 事件最多写 1 条(不重复入账),got %d", paid) + } + if got.Status == model.StatusSuccess && replaced != 0 { + t.Fatalf("结算胜出时不应写 replaced,got %d", replaced) + } +} + +// replaceLoserCreates 模拟并发替换者已原子关单 OLD(写 replaced 事件),然后把当前调用当作 CAS 落败者。 +// OLD 代码下该场景 CAS 落败后会继续放行创建 NEW(proceed=true),酿成同买家多笔 WAIT_PAY; +// 修复后落败者必须 create=false(复用获胜 NEW 或不创建)。 +func TestReplacePendingOrder_LoserNeverCreates(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + old := createWaitPayOrderWithCred(t, st, "OLD_LOSER", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(u *model.Order) { + u.BuyerKey = "buyer-loser" + }); err != nil || !ok { + t.Fatalf("设置 buyer_key 失败: %v %v", ok, err) + } + old, _ = st.GetOrder(ctx, old.OutTradeNo) + + // When: 并发替换者已先原子关闭 OLD 并写入 replaced 事件(模拟 A 在 CAS 中胜出)。 + if ok, err := svc.store.ReplaceOrder(ctx, old.OutTradeNo, old.ID, old.AmountMinor, `{"new_out_trade_no":"WINNER_LOSER"}`); err != nil || !ok { + t.Fatalf("模拟并发替换者关单失败: %v %v", ok, err) + } + + // When: 当前调用作为 CAS 落败者尝试替换同一个 OLD。 + _, create, err := svc.replacePendingOrder(ctx, old, "NEW_LOSER", 200) + if err == nil { + t.Fatal("OLD 已被并发替换者关闭且获胜 NEW 未就绪时,落败者应返回可重试冲突错误") + } + if create { + t.Fatal("CAS 落败者(OLD 已被并发替换者关闭)绝不能再创建自己的 NEW——违反单活跃 WAIT_PAY 不变量") + } +} + +// TestReplacePendingOrder_LoserReusesWinner OLD 已被并发替换者关闭且获胜 NEW 已可见时,落败者复用获胜 NEW。 +func TestReplacePendingOrder_LoserReusesWinner(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + old := createWaitPayOrderWithCred(t, st, "OLD_REUSE", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(u *model.Order) { + u.BuyerKey = "buyer-reuse" + }); err != nil || !ok { + t.Fatalf("设置 buyer_key 失败: %v %v", ok, err) + } + old, _ = st.GetOrder(ctx, old.OutTradeNo) + + // 并发替换者已关 OLD 并已创建获胜 NEW(同买家环境 WAIT_PAY)。 + if ok, err := svc.store.ReplaceOrder(ctx, old.OutTradeNo, old.ID, old.AmountMinor, `{"new_out_trade_no":"WINNER_REUSE"}`); err != nil || !ok { + t.Fatalf("模拟并发替换者关单失败: %v %v", ok, err) + } + winner := createWaitPayOrderWithCred(t, st, "WINNER_REUSE", "personal.test", 300) + if ok, err := st.UpdateOrderStatus(ctx, winner.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(u *model.Order) { + u.BuyerKey = "buyer-reuse" + }); err != nil || !ok { + t.Fatalf("设置获胜 NEW buyer_key 失败: %v %v", ok, err) + } + + reuse, create, err := svc.replacePendingOrder(ctx, old, "NEW_REUSE", 200) + if err != nil { + t.Fatalf("落败者应复用获胜 NEW 而非报错,got err: %v", err) + } + if create { + t.Fatal("落败者不得创建自己的 NEW") + } + if reuse == nil || reuse.OutTradeNo != "WINNER_REUSE" { + t.Fatalf("落败者应复用获胜 NEW WINNER_REUSE,got %+v", reuse) + } +} + +// TestReplacePendingOrder_ConcurrentLoser_Race 真实 SQLite 并发:两个请求同时替换同一 OLD。 +// 断言:至多一方 create=true(绝不出现两方都放行创建 → 同买家双 WAIT_PAY)。 +func TestReplacePendingOrder_ConcurrentLoser_Race(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + old := createWaitPayOrderWithCred(t, st, "OLD_RACE2", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(u *model.Order) { + u.BuyerKey = "buyer-race2" + }); err != nil || !ok { + t.Fatalf("设置 buyer_key 失败: %v %v", ok, err) + } + old, _ = st.GetOrder(ctx, old.OutTradeNo) + + start := make(chan struct{}) + done := make(chan bool, 2) + + for i := 0; i < 2; i++ { + go func() { + <-start + _, create, _ := svc.replacePendingOrder(ctx, old, "NEW_RACE2", int64(100*(i+1))) + done <- create + }() + } + close(start) + creates := 0 + for i := 0; i < 2; i++ { + if <-done { + creates++ + } + } + if creates != 1 { + t.Fatalf("并发替换同一 OLD 时恰一方可创建 NEW,got %d(>1 产生同买家多笔 WAIT_PAY)", creates) + } +} + +// TestCreateOrder_BuyerSequentialReplacement_NoGhost 连续多次改价(不同订单号): +// 每次替换关闭旧单、只保留一笔活跃 WAIT_PAY;历史订单为 CLOSED(允许存在),绝无活跃幽灵 WAIT_PAY。 +func TestCreateOrder_BuyerSequentialReplacement_NoGhost(t *testing.T) { + registerStubPayment(t) + svc, st, _, _ := newTestService(t) + ctx := context.Background() + + old := createWaitPayOrderWithCred(t, st, "OLD_SEQ", "personal.test", 1000) + if ok, err := st.UpdateOrderStatus(ctx, old.OutTradeNo, model.StatusWaitPay, model.StatusWaitPay, func(u *model.Order) { + u.BuyerKey = "buyer-seq" + }); err != nil || !ok { + t.Fatalf("设置 buyer_key 失败: %v %v", ok, err) + } + + order := func(no, money string) { + res, err := svc.CreateOrder(ctx, &CreateRequest{ + OutTradeNo: no, Type: "personal.test", Name: "新单", Money: money, + NotifyURL: "http://example.com/notify", BuyerKey: "buyer-seq", + }) + if err != nil { + t.Fatalf("CreateOrder(%s) err: %v", no, err) + } + if res != nil && res.Order != nil && res.Order.OutTradeNo != no { + t.Fatalf("顺序改价应生成新单 %s,got %s", no, res.Order.OutTradeNo) + } + } + order("NEW_SEQ_1", "1.00") + order("NEW_SEQ_2", "2.00") + order("NEW_SEQ_3", "3.00") + + // Then: 恰一笔活跃 WAIT_PAY(最新 NEW_SEQ_3),其余均为 CLOSED。 + pend, err := st.ListWaitPayByBuyer(ctx, "buyer-seq") + if err != nil { + t.Fatalf("ListWaitPayByBuyer err: %v", err) + } + if len(pend) != 1 { + t.Fatalf("连续改价后应仅剩 1 笔活跃 WAIT_PAY,got %d(存在活跃幽灵)", len(pend)) + } + if pend[0].OutTradeNo != "NEW_SEQ_3" { + t.Fatalf("活跃 WAIT_PAY 应为最新 NEW_SEQ_3,got %s", pend[0].OutTradeNo) + } + for _, no := range []string{"OLD_SEQ", "NEW_SEQ_1", "NEW_SEQ_2"} { + got, _ := st.GetOrder(ctx, no) + if got == nil || got.Status != model.StatusClosed { + t.Fatalf("历史订单 %s 应已 CLOSED(非活跃幽灵),got %v", no, got) + } + } +} diff --git a/internal/personal/model.go b/internal/personal/model.go new file mode 100755 index 0000000..5932596 --- /dev/null +++ b/internal/personal/model.go @@ -0,0 +1,191 @@ +// Package personal 个人免签通道核心:备注码分配、套码防劫持、匹配引擎 +// +// 设计文档:docs/MIANQIAN.md +// 核心思路:个人收款码 + 系统生成备注码 + 账本读取(adb)→ 服务端精确匹配入账。 +// 所有资金写入只走 order.Service 幂等入口(SettlePayment),本包不做任何直接状态修改。 +package personal + +import ( + "encoding/json" + "time" +) + +// DeviceStatus 设备状态(由 DeviceWatcher 按 adb 可达性维护) +type DeviceStatus string + +const ( + DeviceOnline DeviceStatus = "online" // 在线(adb 可达) + DeviceOffline DeviceStatus = "offline" // 离线(adb 不可达) + DeviceFrozen DeviceStatus = "frozen" // 手动冻结(不可接新单) +) + +// ControlState 控制面状态(服务端 adb 通路的可达性) +// - usb : USB 直连或 adb over WiFi 正常(control_state=usb) +// - tunnel_only : 仅报文面可达,adb 不通 +// +// 2026-08-29 重构:监听 App 弃用,relay(CF wss 反代隧道)状态已移除。 +const ( + ControlStateUSB = "usb" + ControlStateTunnelOnly = "tunnel_only" +) + +// Device 真机设备(personal.db personal_devices 表) +// 每个设备绑定一个收款账户(account_id),承载账本读取 +type Device struct { + ID int64 `json:"id"` + DeviceID string `json:"device_id"` // 客户端 UUID(首次启动生成) + Name string `json:"name"` // 备注名(如"卧室安卓机") + AccountID int64 `json:"account_id"` + Provider string `json:"provider"` // alipay / wechat + Secret string `json:"-"` // 通信密钥(仅内存,加密存储) + Status DeviceStatus `json:"status"` + LastPush time.Time `json:"last_push"` + IP string `json:"ip"` + // adb 连接信息(web 可视化操作设备用) + // AdbHost 为 USB 直连设备 serial(如 "4f2bdc30",无端口)或 host(如 "127.0.0.1") + AdbHost string `json:"adb_host,omitempty"` // USB serial 或 host(首选通道) + AdbPort int `json:"adb_port,omitempty"` // 如 5555;0 = USB 直连(无端口) + // WiFi 备用通道(USB 断线时自动切换;与 AdbHost 同网段) + AdbWifiHost string `json:"adb_wifi_host,omitempty"` // 如 192.168.1.119 + AdbWifiPort int `json:"adb_wifi_port,omitempty"` // 如 5555;0 = 未配置 + // 引导向导完成度(JSON 数组字符串,与 DeviceEncrypted.SetupProgress 同语义;"" 视为 "[]") + SetupProgress string `json:"setup_progress,omitempty"` + // TsIP 设备组网 IP(Tailscale 100.x.x.x,未上线为空)。P2P 控制面恢复 adb 时使用。 + TsIP string `json:"ts_ip,omitempty"` + // AdbListen adbd TCP 监听是否就绪(adb 探测,0/1) + AdbListen bool `json:"adb_listen,omitempty"` + // ControlState 控制面状态:usb / tunnel_only + ControlState string `json:"control_state,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// DeviceEncrypted personal.db 存储版本(secret 加密) +type DeviceEncrypted struct { + ID int64 `json:"id"` + DeviceID string `json:"device_id"` + Name string `json:"name"` + AccountID int64 `json:"account_id"` + Provider string `json:"provider"` + SecretEnc string `json:"secret_enc"` + Status string `json:"status"` + LastPush string `json:"last_push"` + IP string `json:"ip"` + AdbHost string `json:"adb_host,omitempty"` + AdbPort int `json:"adb_port,omitempty"` + AdbWifiHost string `json:"adb_wifi_host,omitempty"` + AdbWifiPort int `json:"adb_wifi_port,omitempty"` + // SetupProgress 引导向导完成度(JSON 数组字符串,元素取自 SetupStep* 常量)。 + // 空数组或 "[]" 表示新建未配置;含全部 4 步表示接入完成可参与接单。 + SetupProgress string `json:"setup_progress,omitempty"` + // TsIP 设备组网 IP(Tailscale 100.x.x.x,未上线为空)。P2P 控制面恢复 adb 时使用。 + TsIP string `json:"ts_ip,omitempty"` + // AdbListen adbd TCP 监听是否就绪(adb 探测,0/1) + AdbListen int `json:"adb_listen,omitempty"` + // ControlState 控制面状态:usb / tunnel_only + ControlState string `json:"control_state,omitempty"` + CreatedAt string `json:"created_at"` +} + +// 引导向导完成的阶段(SetupProgress 数组元素值)。改这些常量需同步改 wizard 接口与前端。 +// 2026-08-30 重构:监听端 APP 弃用,向导收敛为 2 步(usb_connected → bound), +// 设备上线由 DeviceWatcher 按 adb 可达性驱动,不再有 apk_installed/config_pushed/heartbeat_connected。 +const ( + SetupStepUSBConnected = "usb_connected" // 第 1 步:USB 真机连上,adb devices state=device + SetupStepBound = "bound" // 第 2 步:已绑定收款账户(personal_devices 记录建好) +) + +// Push 收款事件(personal.db personal_pushes 表,审计用) +type Push struct { + ID int64 `json:"id"` + DeviceID string `json:"device_id"` + Provider string `json:"provider"` + AmountMinor int64 `json:"amount_minor"` + Note string `json:"note"` // 备注码(可空) + RawHash string `json:"raw_hash"` + MatchedOrder string `json:"matched_order"` // 匹配到的订单号(可空) + MatchResult string `json:"match_result"` // matched / no_order / ambiguous / amount_fallback + RiskText string `json:"risk_text,omitempty"` // 风控文案(封号监控上报) + Space int `json:"space"` // 通知来源空间:0=主空间,999=双开(XSpace) + Raw string `json:"raw"` // 通知原文 + CreatedAt time.Time `json:"created_at"` +} + +// UnmatchedPayment 无订单收款(personal.db unmatched_payments 表,人工处理) +type UnmatchedPayment struct { + ID int64 `json:"id"` + Provider string `json:"provider"` + AmountMinor int64 `json:"amount_minor"` + Note string `json:"note"` + Raw string `json:"raw"` + Status string `json:"status"` // pending / resolved / refunded + Handler string `json:"handler"` + // 风控留痕(2026-09-14 需求:无订单收款不做熔断——直发码无法阻止对方转账; + // 记录身份→行为标记→告警提示,保留司法可核对记录) + Payer string `json:"payer"` // 付款人(账本行脱敏名;通知栏无则空) + DeviceID string `json:"device_id"` // 收款设备(锁定收款码归属) + RiskTags string `json:"risk_tags"` // 逗号分隔:big_amount/burst/repeat_payer + RiskLevel int `json:"risk_level"` // 0 正常 / 1 疑似核实 / 2 高风险 + CreatedAt time.Time `json:"created_at"` +} + +// DeviceQrcode 设备动态收款码(服务端 adb 捕获支付宝/微信收款码页) +type DeviceQrcode struct { + ID int64 `json:"id"` + DeviceID string `json:"device_id"` + Platform string `json:"platform"` // alipay / wechat + UserID int `json:"user_id"` // Android 用户空间:0=主空间,999=双开(XSpace) + QRContent string `json:"qr_content"` // 解码后的二维码内容(URL 或串) + Account string `json:"account"` // 收款账户标识(昵称/微信号) + ExpireIn int64 `json:"expire_in"` // 码有效期(秒) + ExpireAt string `json:"expire_at"` // 过期时间(RFC3339) + Status string `json:"status"` // active / expired + PendingFetch int64 `json:"pending_fetch"` // 服务端待下发 fetch_qrcode 指令标志(1=待触发) + CreatedAt string `json:"created_at"` +} + +// UserSpaceLabel 返回用户空间的显示标签 +func (q *DeviceQrcode) UserSpaceLabel() string { + if q.UserID == 999 { + return "双开" // 小米 XSpace 分身 + } + return "主空间" +} + +// ParseSetupProgress 把 SetupProgress 字段(DB 存的 JSON 数组字符串)解析为有序切片。// 空串或非法 JSON 视为空切片(不报错,半配置态守恒语义靠心跳 handler 不解冻账户兜底)。 +func ParseSetupProgress(s string) []string { + if s == "" || s == "[]" { + return nil + } + var out []string + if err := json.Unmarshal([]byte(s), &out); err != nil { + return nil + } + return out +} + +// FormatSetupProgress 把字符串切片格式化为 JSON 数组字符串入库。 +func FormatSetupProgress(steps []string) string { + if len(steps) == 0 { + return "[]" + } + b, _ := json.Marshal(steps) + return string(b) +} + +// HasSetupStep 返回 steps 是否包含 step。 +func HasSetupStep(steps []string, step string) bool { + for _, s := range steps { + if s == step { + return true + } + } + return false +} + +// AppendSetupStep 幂等追加 step 到 steps(已含则原样返回切片)。 +func AppendSetupStep(steps []string, step string) []string { + if HasSetupStep(steps, step) { + return steps + } + return append(steps, step) +} diff --git a/internal/personal/notecode.go b/internal/personal/notecode.go new file mode 100755 index 0000000..4f4d866 --- /dev/null +++ b/internal/personal/notecode.go @@ -0,0 +1,108 @@ +package personal + +import ( + "crypto/rand" + "math/big" + "sync" + "time" +) + +// 备注码分配器(NoteCodeAllocator) +// +// 设计(docs/MIANQIAN.md 6.x): +// - 4 位字符集:a-z 小写 + 0-9,排除易混淆 o/0/l/1 → 32 字符 +// - 组合数:32^4 = 1,048,576,5 分钟窗口内实际并发订单远小于此,几乎不会碰撞 +// - 位图 + 过期队列:高并发分配 O(1),到期自动释放 +// - 系统生成,用户输入 → 精确匹配 → 资金安全 + +// noteChars 备注码字符集(排除易混淆 o/0/l/1) +const noteChars = "abcdefghijkmnpqrstuvwxyz23456789" + +// NoteCodeLen 备注码长度 +const NoteCodeLen = 4 + +// NoteCodeTTL 备注码有效期(订单 5 分钟内未支付则备注码可复用) +const NoteCodeTTL = 5 * time.Minute + +// NoteCodeAllocator 备注码分配器(线程安全) +type NoteCodeAllocator struct { + mu sync.Mutex + used map[string]time.Time // code → 过期时间 +} + +// NewNoteCodeAllocator 创建分配器 +func NewNoteCodeAllocator() *NoteCodeAllocator { + return &NoteCodeAllocator{ + used: make(map[string]time.Time), + } +} + +// Alloc 分配一个未占用且未过期的备注码 +func (a *NoteCodeAllocator) Alloc() (string, error) { + a.mu.Lock() + defer a.mu.Unlock() + + // 先清理已过期条目(避免 map 无限增长) + now := time.Now() + for code, expire := range a.used { + if !expire.After(now) { + delete(a.used, code) + } + } + + // 随机生成直到未占用(最多尝试 N 次;全部占满时等量扩容逻辑不适用——32^4 足够大) + for i := 0; i < 100; i++ { + code, err := randomCode() + if err != nil { + return "", err + } + if _, taken := a.used[code]; !taken { + a.used[code] = now.Add(NoteCodeTTL) + return code, nil + } + } + // 极端情况:100 次随机碰撞(说明窗口内并发订单接近 100 万),退化为线性扫描 + // 扫描全部 32^4 组合找第一个空位;若真全满则清空最旧一批 + for _, c1 := range noteChars { + for _, c2 := range noteChars { + for _, c3 := range noteChars { + for _, c4 := range noteChars { + code := string([]rune{c1, c2, c3, c4}) + if _, taken := a.used[code]; !taken { + a.used[code] = now.Add(NoteCodeTTL) + return code, nil + } + } + } + } + } + // 全部占用:直接清空(实际不可能发生,兜底防死循环) + a.used = make(map[string]time.Time) + code, err := randomCode() + if err != nil { + return "", err + } + a.used[code] = now.Add(NoteCodeTTL) + return code, nil +} + +// Release 释放备注码(订单支付成功/关闭后调用,可选优化:过期自动释放已足够) +func (a *NoteCodeAllocator) Release(code string) { + a.mu.Lock() + defer a.mu.Unlock() + delete(a.used, code) +} + +// randomCode 生成随机 4 位备注码 +func randomCode() (string, error) { + chars := []rune(noteChars) + out := make([]rune, NoteCodeLen) + for i := range out { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) + if err != nil { + return "", err + } + out[i] = chars[n.Int64()] + } + return string(out), nil +} diff --git a/internal/personal/notecode_test.go b/internal/personal/notecode_test.go new file mode 100755 index 0000000..007aa45 --- /dev/null +++ b/internal/personal/notecode_test.go @@ -0,0 +1,82 @@ +package personal + +import ( + "strings" + "testing" + "time" +) + +func TestAllocFormat(t *testing.T) { + a := NewNoteCodeAllocator() + for i := 0; i < 200; i++ { + code, err := a.Alloc() + if err != nil { + t.Fatalf("Alloc: %v", err) + } + if len(code) != NoteCodeLen { + t.Fatalf("code length: got %q want %d", code, NoteCodeLen) + } + for _, ch := range code { + if !strings.ContainsRune(noteChars, ch) { + t.Fatalf("code %q contains invalid char %q", code, ch) + } + } + } +} + +func TestAllocUniqueInWindow(t *testing.T) { + a := NewNoteCodeAllocator() + seen := make(map[string]bool) + for i := 0; i < 5000; i++ { + code, err := a.Alloc() + if err != nil { + t.Fatalf("Alloc: %v", err) + } + if seen[code] { + t.Fatalf("duplicate code in window: %q", code) + } + seen[code] = true + } +} + +func TestAllocNoConfusableChars(t *testing.T) { + a := NewNoteCodeAllocator() + for i := 0; i < 1000; i++ { + code, _ := a.Alloc() + for _, ch := range code { + if ch == 'o' || ch == '0' || ch == 'l' || ch == '1' { + t.Fatalf("confusable char %q in code %q", ch, code) + } + } + } +} + +func TestAllocExpiryReuse(t *testing.T) { + a := NewNoteCodeAllocator() + code, _ := a.Alloc() + // 手动过期(模拟 5 分钟后) + a.mu.Lock() + a.used[code] = time.Now().Add(-time.Second) + a.mu.Unlock() + // 再分配应能拿到同一 code(或至少不报错) + code2, err := a.Alloc() + if err != nil { + t.Fatalf("Alloc after expiry: %v", err) + } + _ = code2 +} + +func TestAllocRelease(t *testing.T) { + a := NewNoteCodeAllocator() + code, _ := a.Alloc() + a.Release(code) + // 释放后可立即复用 + code2, _ := a.Alloc() + if code2 == code { + return // OK + } + // 不强制相同(随机),但必须成功 + if code2 == "" { + t.Fatalf("released code should be allocatable") + } +} diff --git a/internal/personal/ratelimit.go b/internal/personal/ratelimit.go new file mode 100755 index 0000000..f1ff8ce --- /dev/null +++ b/internal/personal/ratelimit.go @@ -0,0 +1,93 @@ +// Package personal 频次风控:滑动窗口限流器(个人免签推送/心跳用) +// +// 设计(docs/MIANQIAN.md §10 风控): +// - 频次风控:每分钟收款笔数上限(可配置),超限拒绝 + 告警 +// - 滑动窗口实现:按 key(device_id)维护 60s 内的时间戳列表,O(1) 摊销 +// - 内存占用受控:窗口外旧记录惰性清理,key 总数上限保护 +package personal + +import ( + "sync" + "time" +) + +// RateLimiter 滑动窗口限流器(线程安全) +type RateLimiter struct { + mu sync.Mutex + limit int // 窗口内最大次数(<=0 表示不限) + window time.Duration // 窗口时长(默认 60s) + maxKeys int // key 数上限(防内存无限增长,默认 10000) + hits map[string][]int64 // key → 窗口内时间戳(Unix 秒) +} + +// NewRateLimiter 创建限流器 +// limit: 窗口内最大次数;window: 窗口时长;maxKeys: key 数上限(0 用默认 10000) +func NewRateLimiter(limit int, window time.Duration, maxKeys int) *RateLimiter { + if window <= 0 { + window = time.Minute + } + if maxKeys <= 0 { + maxKeys = 10000 + } + return &RateLimiter{ + limit: limit, + window: window, + maxKeys: maxKeys, + hits: make(map[string][]int64), + } +} + +// Allow 判断 key 是否允许本次请求(允许则记录) +func (r *RateLimiter) Allow(key string) bool { + if r.limit <= 0 { + return true // 未配置限额 + } + r.mu.Lock() + defer r.mu.Unlock() + + now := time.Now().Unix() + cutoff := now - int64(r.window.Seconds()) + + // 惰性清理:移除窗口外记录;空列表直接删 key + hits := r.hits[key] + kept := hits[:0] + for _, t := range hits { + if t > cutoff { + kept = append(kept, t) + } + } + if len(kept) == 0 { + delete(r.hits, key) + kept = nil + } + if len(kept) >= r.limit { + r.hits[key] = kept + return false + } + // key 数上限保护(仅新增 key 时触发) + if len(kept) == 0 && len(r.hits) >= r.maxKeys { + // 清掉最久未活动的 key:简单策略,遍历删除首个空窗口 key + for k, v := range r.hits { + if len(v) > 0 && v[len(v)-1] <= cutoff { + delete(r.hits, k) + break + } + } + } + r.hits[key] = append(kept, now) + return true +} + +// Count 返回 key 当前窗口内次数(诊断用) +func (r *RateLimiter) Count(key string) int { + r.mu.Lock() + defer r.mu.Unlock() + cutoff := time.Now().Unix() - int64(r.window.Seconds()) + n := 0 + for _, t := range r.hits[key] { + if t > cutoff { + n++ + } + } + return n +} diff --git a/internal/personal/ratelimit_test.go b/internal/personal/ratelimit_test.go new file mode 100755 index 0000000..9720677 --- /dev/null +++ b/internal/personal/ratelimit_test.go @@ -0,0 +1,65 @@ +package personal + +import ( + "testing" + "time" +) + +// TestRateLimiterAllow 基础限流:超限拒绝 +func TestRateLimiterAllow(t *testing.T) { + rl := NewRateLimiter(3, time.Minute, 0) + if !rl.Allow("dev-1") { + t.Fatal("第1次应允许") + } + if !rl.Allow("dev-1") { + t.Fatal("第2次应允许") + } + if !rl.Allow("dev-1") { + t.Fatal("第3次应允许") + } + if rl.Allow("dev-1") { + t.Fatal("第4次应拒绝(窗口内已满)") + } + if rl.Count("dev-1") != 3 { + t.Fatalf("窗口内次数应为 3,实际 %d", rl.Count("dev-1")) + } +} + +// TestRateLimiterWindow 窗口滑动:旧记录过期后恢复 +func TestRateLimiterWindow(t *testing.T) { + rl := NewRateLimiter(2, time.Second, 0) // 1 秒窗口 + if !rl.Allow("dev-2") || !rl.Allow("dev-2") { + t.Fatal("前两次应允许") + } + if rl.Allow("dev-2") { + t.Fatal("第3次应拒绝") + } + time.Sleep(1100 * time.Millisecond) // 窗口过期 + if !rl.Allow("dev-2") { + t.Fatal("窗口过期后应恢复允许") + } +} + +// TestRateLimiterPerKey 不同 key 相互独立 +func TestRateLimiterPerKey(t *testing.T) { + rl := NewRateLimiter(1, time.Minute, 0) + if !rl.Allow("dev-a") { + t.Fatal("dev-a 第1次应允许") + } + if rl.Allow("dev-a") { + t.Fatal("dev-a 第2次应拒绝") + } + if !rl.Allow("dev-b") { + t.Fatal("dev-b 应独立允许") + } +} + +// TestRateLimiterNoLimit 未配置限额时全部放行 +func TestRateLimiterNoLimit(t *testing.T) { + rl := NewRateLimiter(0, time.Minute, 0) + for i := 0; i < 100; i++ { + if !rl.Allow("dev-x") { + t.Fatal("无限额时不应拒绝") + } + } +} diff --git a/internal/personal/sign.go b/internal/personal/sign.go new file mode 100755 index 0000000..a1269e7 --- /dev/null +++ b/internal/personal/sign.go @@ -0,0 +1,79 @@ +package personal + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" +) + +// 监听端通信签名(docs/MIANQIAN.md 8.1) +// +// HTTPS 强制(生产) +// sign = HMAC-SHA256(secret, method|path|timestamp|field1|field2|...) +// timestamp 防重放:±5 分钟窗口 +// device_id:客户端唯一标识(首次启动生成 UUID) +// +// 字段化签名设计(关键): +// - 签名基于"字段值按固定顺序拼接",而非对整个 JSON body 字节签名 +// - 原因:JSON 序列化字段顺序不可控(Go map 随机序),若对 body 字节签名, +// 客户端和服务端 JSON 字段顺序不一致会导致验签失败(且无法把 sign 放进 body 同时签名) +// - 客户端:构造业务字段 → 计算签名 → 把 sign 加入 JSON 发送 +// - 服务端:解析 JSON 取出字段 → 用相同顺序计算期望签名 → 常量时间比较 + +// SignWindow 签名时间戳窗口(±5 分钟) +const SignWindow = 5 * time.Minute + +// SignMsg 计算监听端请求签名 +// fields 按固定顺序传入(客户端/服务端必须一致): +// - 心跳: device_id +// - 推送: device_id, provider, amount, note, raw +func SignMsg(secret, method, path string, timestamp int64, fields ...string) string { + mac := hmac.New(sha256.New, []byte(secret)) + fmt.Fprintf(mac, "%s|%s|%d|", method, path, timestamp) + mac.Write([]byte(strings.Join(fields, "|"))) + return hex.EncodeToString(mac.Sum(nil)) +} + +// VerifySign 校验监听端请求签名 +// 校验:签名匹配 + 时间戳在 ±SignWindow 窗口内(防重放) +func VerifySign(secret, method, path string, timestamp int64, sign string, fields ...string) error { + if sign == "" { + return fmt.Errorf("缺少签名") + } + now := time.Now().Unix() + diff := now - timestamp + if diff < 0 { + diff = -diff + } + if diff > int64(SignWindow.Seconds()) { + return fmt.Errorf("时间戳超出窗口(防重放): diff=%ds", diff) + } + expect := SignMsg(secret, method, path, timestamp, fields...) + if !hmac.Equal([]byte(expect), []byte(sign)) { + return fmt.Errorf("签名不匹配") + } + return nil +} + +// NewDeviceSecret 生成设备通信密钥(32 字节 hex) +func NewDeviceSecret() (string, error) { + buf := make([]byte, 32) + if _, err := rand.Read(buf); err != nil { + return "", fmt.Errorf("生成设备密钥失败: %w", err) + } + return hex.EncodeToString(buf), nil +} + +// NewDeviceID 生成设备唯一标识(UUID v4 简化版,32 位 hex) +func NewDeviceID() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + // 兜底:rand 失败时用时间戳(几乎不可能) + return fmt.Sprintf("dev-%d", time.Now().UnixNano()) + } + return hex.EncodeToString(buf) +} diff --git a/internal/personal/sign_test.go b/internal/personal/sign_test.go new file mode 100755 index 0000000..3417951 --- /dev/null +++ b/internal/personal/sign_test.go @@ -0,0 +1,113 @@ +package personal + +import ( + "testing" + "time" +) + +func TestVerifySignValid(t *testing.T) { + secret := "test-secret-key" + method := "POST" + path := "/personal/push" + ts := time.Now().Unix() + + sign := SignMsg(secret, method, path, ts, "d1", "alipay", "10.00", "ab3k", "支付宝到账10.00元", "0") + if err := VerifySign(secret, method, path, ts, sign, "d1", "alipay", "10.00", "ab3k", "支付宝到账10.00元", "0"); err != nil { + t.Fatalf("valid sign should pass: %v", err) + } +} + +func TestVerifySignWrongSecret(t *testing.T) { + ts := time.Now().Unix() + sign := SignMsg("secret-a", "POST", "/personal/push", ts, "d1") + if err := VerifySign("secret-b", "POST", "/personal/push", ts, sign, "d1"); err == nil { + t.Fatalf("wrong secret should fail") + } +} + +func TestVerifySignTamperedField(t *testing.T) { + secret := "s" + ts := time.Now().Unix() + sign := SignMsg(secret, "POST", "/personal/push", ts, "d1", "10.00") + // 篡改金额字段(签名不变)→ 验签失败 + if err := VerifySign(secret, "POST", "/personal/push", ts, sign, "d1", "100.00"); err == nil { + t.Fatalf("tampered field should fail") + } +} + +func TestVerifySignReplayWindow(t *testing.T) { + secret := "s" + // 时间戳超出窗口(10 分钟前) + oldTS := time.Now().Add(-10 * time.Minute).Unix() + sign := SignMsg(secret, "POST", "/personal/push", oldTS, "d1") + if err := VerifySign(secret, "POST", "/personal/push", oldTS, sign, "d1"); err == nil { + t.Fatalf("out-of-window timestamp should fail (replay protection)") + } + // 窗口内(1 分钟前)应通过 + recentTS := time.Now().Add(-1 * time.Minute).Unix() + sign2 := SignMsg(secret, "POST", "/personal/push", recentTS, "d1") + if err := VerifySign(secret, "POST", "/personal/push", recentTS, sign2, "d1"); err != nil { + t.Fatalf("in-window timestamp should pass: %v", err) + } +} + +func TestVerifySignEmpty(t *testing.T) { + if err := VerifySign("s", "POST", "/personal/push", time.Now().Unix(), "", "d1"); err == nil { + t.Fatalf("empty sign should fail") + } +} + +// TestVerifySignRiskText 封号监控链路(docs/MIANQIAN.md §10): +// 监听端检测到风控文案 → 上报 risk_text(amount/note 为空串), +// risk_text 必须参与签名(客户端 SignUtil.signPush 字段序含 risk_text), +// 且空串也参与拼接 —— 模拟 APP 真实上报的字段序,验证服务端验签通过 +func TestVerifySignRiskText(t *testing.T) { + secret := "test-secret-key" + method := "POST" + path := "/personal/push" + ts := time.Now().Unix() + deviceID := "dev-1234" + provider := "alipay" + + // 模拟 APP 风控上报:amount="" note="",raw 与 risk_text 均带原文 + // 注意:Kotlin SignUtil.signPush 字段序 = [device_id, provider, amount, note, raw, risk_text, space] + raw := "您的账户存在风控提示,请及时处理" + amount, note := "", "" + riskText := "您的账户存在风控提示,请及时处理" + sign := SignMsg(secret, method, path, ts, deviceID, provider, amount, note, raw, riskText, "0") + if err := VerifySign(secret, method, path, ts, sign, deviceID, provider, amount, note, raw, riskText, "0"); err != nil { + t.Fatalf("risk-text sign should pass (fields order must match client): %v", err) + } + // 篡改 risk_text → 验签失败 + if err := VerifySign(secret, method, path, ts, sign, deviceID, provider, amount, note, raw, "伪造的风控文案", "0"); err == nil { + t.Fatalf("tampered risk_text should fail") + } +} + +// TestVerifySignRiskTextEmpty 普通收款上报(risk_text 为空串也必须参与拼接) +func TestVerifySignRiskTextEmpty(t *testing.T) { + secret := "s" + ts := time.Now().Unix() + // 客户端普通推送:risk_text 默认为 "",但 SignUtil 仍拼入字段序 + sign := SignMsg(secret, "POST", "/personal/push", ts, "d1", "alipay", "10.00", "ab3k", "支付宝到账10.00元", "", "0") + if err := VerifySign(secret, "POST", "/personal/push", ts, sign, "d1", "alipay", "10.00", "ab3k", "支付宝到账10.00元", "", "0"); err != nil { + t.Fatalf("empty risk_text must still be signed (client sends it): %v", err) + } +} + +func TestNewDeviceSecret(t *testing.T) { + s1, err := NewDeviceSecret() + if err != nil { + t.Fatalf("NewDeviceSecret: %v", err) + } + s2, err := NewDeviceSecret() + if err != nil { + t.Fatalf("NewDeviceSecret: %v", err) + } + if len(s1) != 64 { // 32 字节 hex + t.Fatalf("secret length: got %d want 64", len(s1)) + } + if s1 == s2 { + t.Fatalf("secrets should differ") + } +} diff --git a/internal/personal/token.go b/internal/personal/token.go new file mode 100755 index 0000000..848de0d --- /dev/null +++ b/internal/personal/token.go @@ -0,0 +1,127 @@ +package personal + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "strconv" + "strings" + "time" +) + +// 二维码防劫持 token(docs/MIANQIAN.md 5.3) +// +// 支付页面不内嵌真实收款码,只引用网关签发的动态图片接口: +// +// 图片地址 = /qr/real?token={token} +// 浏览器请求 → 网关校验(token 签名 + 订单存在 + WAIT_PAY + 未过期) +// → 下发真实收款码 PNG(真实码内容不进支付页 HTML) +// +// token 格式(自包含,无需数据库索引): +// +// {随机16字节 base64url}.{orderID:expireUnix}.{HMAC-SHA256(orderID|expireUnix|随机, secret) 前 22 字节 base64url} +// +// 服务端校验:解码 → 重算 HMAC → 常量时间比较 → 检查过期 → 返回 orderID +// 攻击者无法伪造(无 secret),篡改任一字段 HMAC 失配即拒绝。 + +// QrTokenSecret 套码 token 签名密钥(服务端持有,内存) +var qrTokenSecret []byte + +// InitQrToken 初始化套码 token 签名密钥(hex 32 字节) +// 未调用 InitQrToken 时自动生成并持久化由 main 负责;这里校验长度 +func InitQrToken(secret string) error { + key, err := hex.DecodeString(secret) + if err != nil { + return fmt.Errorf("套码 token 密钥格式错误: %w", err) + } + if len(key) != 32 { + return fmt.Errorf("套码 token 密钥长度错误: 需要 32 字节,实际 %d", len(key)) + } + qrTokenSecret = key + return nil +} + +// QrTokenSecretSet 是否已初始化 +func QrTokenSecretSet() bool { return len(qrTokenSecret) == 32 } + +// GenerateQrToken 生成套码 token +// key: 订单号(out_trade_no)或订单 ID 字符串 +func GenerateQrToken(key string, expireAt time.Time) (string, error) { + if !QrTokenSecretSet() { + return "", fmt.Errorf("套码 token 密钥未初始化") + } + // 随机 16 字节 + rnd := make([]byte, 16) + if _, err := rand.Read(rnd); err != nil { + return "", fmt.Errorf("生成 token 随机数失败: %w", err) + } + // payload: key|expireUnix + payload := fmt.Sprintf("%s|%d", key, expireAt.Unix()) + // HMAC over payload|random + mac := hmac.New(sha256.New, qrTokenSecret) + mac.Write([]byte(payload)) + mac.Write([]byte("|")) + mac.Write(rnd) + sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)[:22]) + + token := base64.RawURLEncoding.EncodeToString(rnd) + "." + + base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + sig + return token, nil +} + +// ParseQrToken 校验并解析 token,返回 (key, expireAt) +// 校验失败返回 error(篡改/伪造/过期) +func ParseQrToken(token string) (string, time.Time, error) { + if !QrTokenSecretSet() { + return "", time.Time{}, fmt.Errorf("套码 token 密钥未初始化") + } + parts := strings.Split(token, ".") + if len(parts) != 3 { + return "", time.Time{}, fmt.Errorf("token 格式错误") + } + rnd, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil || len(rnd) != 16 { + return "", time.Time{}, fmt.Errorf("token 随机数非法") + } + payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", time.Time{}, fmt.Errorf("token payload 非法") + } + payload := string(payloadBytes) + sig, err := base64.RawURLEncoding.DecodeString(parts[2]) + if err != nil || len(sig) == 0 { + return "", time.Time{}, fmt.Errorf("token 签名非法") + } + + // 重算 HMAC 并常量时间比较 + mac := hmac.New(sha256.New, qrTokenSecret) + mac.Write([]byte(payload)) + mac.Write([]byte("|")) + mac.Write(rnd) + expect := mac.Sum(nil)[:22] + if !hmac.Equal(expect, sig) { + return "", time.Time{}, fmt.Errorf("token 签名不匹配(篡改或伪造)") + } + + // 解析 payload: key|expireUnix + kv := strings.Split(payload, "|") + if len(kv) != 2 { + return "", time.Time{}, fmt.Errorf("token payload 格式错误") + } + key := kv[0] + if key == "" { + return "", time.Time{}, fmt.Errorf("token key 非法") + } + expireUnix, err := strconv.ParseInt(kv[1], 10, 64) + if err != nil { + return "", time.Time{}, fmt.Errorf("token expire 非法") + } + expireAt := time.Unix(expireUnix, 0) + if !expireAt.After(time.Now()) { + return "", time.Time{}, fmt.Errorf("token 已过期") + } + return key, expireAt, nil +} diff --git a/internal/personal/token_test.go b/internal/personal/token_test.go new file mode 100755 index 0000000..2d9c830 --- /dev/null +++ b/internal/personal/token_test.go @@ -0,0 +1,96 @@ +package personal + +import ( + "encoding/base64" + "strings" + "testing" + "time" +) + +// base64URL 辅助:普通 base64 编码 +func base64URL(s string) string { + return base64.RawURLEncoding.EncodeToString([]byte(s)) +} + +func TestGenerateQrTokenRoundTrip(t *testing.T) { + if err := InitQrToken(strings.Repeat("ab", 32)); err != nil { + t.Fatalf("InitQrToken: %v", err) + } + token, err := GenerateQrToken("ORDER-123", time.Now().Add(30*time.Minute)) + if err != nil { + t.Fatalf("GenerateQrToken: %v", err) + } + key, expire, err := ParseQrToken(token) + if err != nil { + t.Fatalf("ParseQrToken: %v", err) + } + if key != "ORDER-123" { + t.Fatalf("key mismatch: got %q want ORDER-123", key) + } + if !expire.After(time.Now()) { + t.Fatalf("expire should be in future") + } +} + +func TestGenerateQrTokenTampered(t *testing.T) { + if err := InitQrToken(strings.Repeat("ab", 32)); err != nil { + t.Fatalf("InitQrToken: %v", err) + } + token, err := GenerateQrToken("ORDER-123", time.Now().Add(30*time.Minute)) + if err != nil { + t.Fatalf("GenerateQrToken: %v", err) + } + // 篡改 payload(改成另一个订单号) + parts := strings.Split(token, ".") + if len(parts) != 3 { + t.Fatalf("token format wrong") + } + // 篡改 payload 部分为另一个订单号(保持时间戳格式,签名必然失配) + evilPayload := base64URL("ORDER-EVIL|9999999999") + evil := parts[0] + "." + evilPayload + "." + parts[2] + if _, _, err := ParseQrToken(evil); err == nil { + t.Fatalf("tampered token should fail") + } +} + +func TestParseQrTokenExpired(t *testing.T) { + if err := InitQrToken(strings.Repeat("ab", 32)); err != nil { + t.Fatalf("InitQrToken: %v", err) + } + token, err := GenerateQrToken("ORDER-1", time.Now().Add(-time.Minute)) // 已过期 + if err != nil { + t.Fatalf("GenerateQrToken: %v", err) + } + if _, _, err := ParseQrToken(token); err == nil { + t.Fatalf("expired token should fail") + } +} + +func TestParseQrTokenGarbage(t *testing.T) { + if err := InitQrToken(strings.Repeat("ab", 32)); err != nil { + t.Fatalf("InitQrToken: %v", err) + } + if _, _, err := ParseQrToken("garbage"); err == nil { + t.Fatalf("garbage token should fail") + } + if _, _, err := ParseQrToken(""); err == nil { + t.Fatalf("empty token should fail") + } +} + +func TestGenerateQrTokenUnique(t *testing.T) { + if err := InitQrToken(strings.Repeat("ab", 32)); err != nil { + t.Fatalf("InitQrToken: %v", err) + } + seen := make(map[string]bool) + for i := 0; i < 100; i++ { + token, err := GenerateQrToken("ORDER-1", time.Now().Add(time.Minute)) + if err != nil { + t.Fatalf("GenerateQrToken: %v", err) + } + if seen[token] { + t.Fatalf("duplicate token") + } + seen[token] = true + } +} diff --git a/internal/pool/waitpay_pool.go b/internal/pool/waitpay_pool.go new file mode 100644 index 0000000..c0b0e68 --- /dev/null +++ b/internal/pool/waitpay_pool.go @@ -0,0 +1,800 @@ +// Package pool 提供 WAIT_PAY 订单的并发安全内存池。 +// +// 设计要点: +// - 设备维度子池:devices[deviceID] 一个设备子池,多设备天然隔离、互不干扰, +// 每台设备只处理自己账户的订单(docs/PLAN-DYNAMIC-CLOSE-ALLOC.md §3); +// - 单一 sync.RWMutex 守护全部索引表(写入低频),聚合快照通过 atomic.Pointer 持有,读路径零锁; +// - 动态关单窗口:CloseAt 由注入的 winCalc(设备负载驱动)计算,单调只增(防误关); +// - 本包不依赖 store/order 等业务包,避免反向依赖循环;仅依赖 model(领域类型)和 logger(结构化告警)。 +package pool + +import ( + "context" + "sort" + "sync" + "sync/atomic" + "time" + + "paygateway/internal/logger" + "paygateway/internal/model" +) + +// 默认关单窗口:closeWindowMinutes<=0 且未注入 winCalc 时使用 +const defaultCloseWindow = 30 * time.Minute + +// Entry 池中订单条目(不可变值对象,Add/Upsert 时由调用方持有原对象;本包不会修改)。 +type Entry struct { + OutTradeNo string + AccountID int64 + DeviceID string // 所属设备(账户→设备映射解析;未知归 "") + Channel string // personal.alipay / personal.wechat + AmountMinor int64 // 分 + NoteCode string // 备注码(可空) + CreatedAt time.Time + CloseAt time.Time // 预计关单时间 = CreatedAt + 动态窗口(单调只增) + SessionExpiresAt time.Time // 支付会话硬截止时间,替换订单不得延长 +} + +// AccountStat 单账户聚合统计(快照元素,值类型不可变)。 +type AccountStat struct { + AccountID int64 + Count int + // DupAmount 同设备同账户是否存在 ≥2 笔相同金额的 WAIT_PAY 订单; + // 决定调度器是否需轮询账单页(金额互异的订单由通知栏按金额唯一匹配入账)。 + DupAmount bool + OldestAge time.Duration // 最久订单已等待时长(>0) + MinTTL time.Duration // 距最早关单剩余时间(可为负=已超时) +} + +// Snapshot 全池聚合快照(原子指针持有,不可变;notifywatcher 等全局判断用)。 +type Snapshot struct { + Accounts []AccountStat + Total int + UpdatedAt time.Time +} + +// DeviceStat 单设备聚合快照(设备调度器/均衡分配器零锁读取)。 +type DeviceStat struct { + DeviceID string + Total int // 该设备 WAIT_PAY 总数(设备负载) + Accounts []AccountStat // 该设备内按账户聚合 + ActiveAccts int // 活跃账户数(Count>0) + EstRound time.Duration // 估算一轮耗时(ActiveAccts × 配额 × pollCost,供动态窗口/超限判定) + CloseWindow time.Duration // 当前生效动态关单窗口(最新 Add 时计算) + UpdatedAt time.Time +} + +// devicePool 单设备子池:三张索引表(与旧全局结构同构,按设备隔离)。 +type devicePool struct { + buckets map[int64]map[string]*Entry + byNote map[int64]map[string]string + byAmount map[int64]map[int64]map[string]struct{} + stat atomic.Pointer[DeviceStat] +} + +// DeviceOfFunc 账户 → 设备 解析(main.go 注入:personal.db 设备绑定)。 +// ok=false 时订单归入 "" 兜底桶(无设备绑定/解析失败,Reconcile 修复)。 +type DeviceOfFunc func(accountID int64) (deviceID string, ok bool) + +// WindowCalcFunc 动态关单窗口计算器(main.go 注入;nil = 固定 fallback 窗口)。 +// 入参:该设备当前活跃账户数与待支付订单总数(Add 时点快照)。 +// 返回:订单关单窗口(实现方负责 clamp 到 [MIN, MAX])。 +type WindowCalcFunc func(activeAccounts, total int) time.Duration + +// WaitPayPool 并发安全的 WAIT_PAY 订单池。 +// +// 内部数据结构: +// - devices[deviceID].buckets[accountID][outTradeNo] = *Entry +// - devices[deviceID].byNote[accountID][noteCode] = outTradeNo(noteCode 为空时不入索引) +// - devices[deviceID].byAmount[accountID][amountMinor][outTradeNo] = struct{}{} +// - acctDev[accountID] = deviceID(账户→设备反查,匹配/Count/移除零遍历) +// +// 所有表必须在写锁内同步变更;快照由 rebuildStatsLocked 在写锁内一次性生成。 +type WaitPayPool struct { + deviceOf DeviceOfFunc + winCalc WindowCalcFunc + fallback time.Duration // winCalc 为 nil 时的固定关窗(closeWindowMinutes) + + mu sync.RWMutex + devices map[string]*devicePool + acctDev map[int64]string // accountID → deviceID + + snap atomic.Pointer[Snapshot] +} + +// NewWaitPayPool 创建池。closeWindowMinutes<=0 且未注入 winCalc 时 CloseAt 用 CreatedAt+30min 兜底。 +func NewWaitPayPool(closeWindowMinutes int) *WaitPayPool { + w := defaultCloseWindow + if closeWindowMinutes > 0 { + w = time.Duration(closeWindowMinutes) * time.Minute + } + p := &WaitPayPool{ + fallback: w, + devices: make(map[string]*devicePool), + acctDev: make(map[int64]string), + } + p.devices[""] = newDevicePool() + p.publishGlobalLocked() + return p +} + +// newDevicePool 创建空设备子池并初始化空快照。 +func newDevicePool() *devicePool { + dp := &devicePool{ + buckets: make(map[int64]map[string]*Entry), + byNote: make(map[int64]map[string]string), + byAmount: make(map[int64]map[int64]map[string]struct{}), + } + dp.stat.Store(&DeviceStat{}) + return dp +} + +// SetDeviceOf 注入账户→设备解析(main.go 在设备增删时调用;可重复注入)。 +func (p *WaitPayPool) SetDeviceOf(fn DeviceOfFunc) { + p.mu.Lock() + defer p.mu.Unlock() + p.deviceOf = fn + // 注入后全量重分桶:条目按新映射重新解析设备归属(DeviceID 先清空再走新 deviceOf) + if fn != nil { + var all []*Entry + for _, dp := range p.devices { + for _, m := range dp.buckets { + for _, e := range m { + e.DeviceID = "" + all = append(all, e) + } + } + } + p.devices = make(map[string]*devicePool) + p.acctDev = make(map[int64]string) + p.devices[""] = newDevicePool() + for _, e := range all { + p.indexLocked(e) + } + } + p.publishGlobalLocked() +} + +// SetWindowCalc 注入动态关单窗口计算器(main.go 调用;nil = 固定 fallback)。 +// 注入后对池内现有条目重新计算 CloseAt(仅延长不缩短,保持单调)。 +func (p *WaitPayPool) SetWindowCalc(fn WindowCalcFunc) { + p.mu.Lock() + defer p.mu.Unlock() + p.winCalc = fn + for _, dp := range p.devices { + for _, m := range dp.buckets { + for _, e := range m { + devID := p.deviceOfEntry(e) + nw := p.windowFor(devID) + if nw > 0 && e.CloseAt.Before(e.CreatedAt.Add(nw)) { + e.CloseAt = e.CreatedAt.Add(nw) + } + } + } + } + p.publishGlobalLocked() +} + +// deviceOfEntry 返回条目所属设备(优先 entry.DeviceID;兜底 deviceOf 解析)。 +func (p *WaitPayPool) deviceOfEntry(e *Entry) string { + if e.DeviceID != "" { + return e.DeviceID + } + if p.deviceOf != nil { + if d, ok := p.deviceOf(e.AccountID); ok && d != "" { + return d + } + } + return "" +} + +// windowFor 计算某设备当前动态关单窗口:winCalc(活跃账户数, 待支付订单数)。 +// 活跃账户数 = 该设备子池中 Count>0 的账户数(不含待添加条目本身)。 +func (p *WaitPayPool) windowFor(deviceID string) time.Duration { + if p.winCalc == nil { + return p.fallback + } + dp := p.devices[deviceID] + if dp == nil { + return p.fallback + } + active, total := 0, 0 + for _, m := range dp.buckets { + if len(m) > 0 { + active++ + total += len(m) + } + } + w := p.winCalc(active, total) + if w <= 0 { + return p.fallback + } + return w +} + +// orderToEntry 从 model.Order 构造池内 Entry。 +// CloseAt = CreatedAt + 动态窗口(Add 时点设备负载计算);单调只增(Upsert 覆盖时 CloseAt 不倒退)。 +func (p *WaitPayPool) orderToEntry(o *model.Order) *Entry { + devID := "" + if p.deviceOf != nil { + if d, ok := p.deviceOf(o.AccountID); ok { + devID = d + } + } + closeAt := o.CreatedAt.Add(p.windowFor(devID)) + if !o.SessionExpiresAt.IsZero() && o.SessionExpiresAt.Before(closeAt) { + closeAt = o.SessionExpiresAt + } + return &Entry{ + OutTradeNo: o.OutTradeNo, + AccountID: o.AccountID, + DeviceID: devID, + Channel: o.Channel, + AmountMinor: o.AmountMinor, + NoteCode: o.NoteCode, + CreatedAt: o.CreatedAt, + CloseAt: closeAt, + SessionExpiresAt: o.SessionExpiresAt, + } +} + +// Add 加入池。幂等:同 outTradeNo 已存在则覆盖更新(NoteCode/Amount/Channel 等变更会被刷新)。 +// CloseAt 单调只增:覆盖时若新窗口计算的 CloseAt 早于旧值,保持旧值(防负载下降提前关单误关)。 +func (p *WaitPayPool) Add(o *model.Order) { + if o == nil || o.OutTradeNo == "" { + return + } + + p.mu.Lock() + defer p.mu.Unlock() + + entry := p.orderToEntry(o) + + // 若已存在:先从旧索引中移除,再用新 entry 写入 + if old, ok := p.lookupLocked(o.OutTradeNo); ok { + if entry.CloseAt.Before(old.CloseAt) { + entry.CloseAt = old.CloseAt // 单调只增 + } + p.unindexLocked(old) + // 显式删除旧桶条目(unindexLocked 只清 byNote/byAmount,buckets 由调用方删)。 + // 设备映射变更时(旧 DeviceID ≠ 新 DeviceID)必须删旧桶,否则同订单残留双设备子池, + // 导致旧设备负载虚高、均衡分配失真(P2-3)。 + if od, ok := p.devices[old.DeviceID]; ok { + if m, ok := od.buckets[old.AccountID]; ok { + delete(m, old.OutTradeNo) + if len(m) == 0 { + delete(od.buckets, old.AccountID) + delete(p.acctDev, old.AccountID) + } + } + } + } + p.indexLocked(entry) + p.publishGlobalLocked() +} + +// Upsert 覆盖语义别名。Add 本身已支持幂等覆盖(refresh credential / channel 时 NoteCode 可能变化)。 +func (p *WaitPayPool) Upsert(o *model.Order) { + p.Add(o) +} + +// CloseAtOf 查询订单的动态关单时间(order.Service 注入 WaitPayHooks.CloseAtOf 用)。 +// 返回 (closeAt, found)。池中不存在 → found=false(已入账/已关单/非个人通道)。 +func (p *WaitPayPool) CloseAtOf(outTradeNo string) (time.Time, bool) { + p.mu.RLock() + defer p.mu.RUnlock() + if e, ok := p.lookupLocked(outTradeNo); ok { + return e.CloseAt, true + } + return time.Time{}, false +} + +// Remove 移出池。幂等:不存在无副作用。 +// +// 内部会扫描所有设备/账户桶以定位 outTradeNo(业务上 outTradeNo 全局唯一,正常情况仅匹配一处; +// 扫描全表可防御性清理被异常双写的同名条目,并保证索引不留孤儿)。 +func (p *WaitPayPool) Remove(outTradeNo string) { + if outTradeNo == "" { + return + } + + p.mu.Lock() + defer p.mu.Unlock() + + removed := false + for _, dp := range p.devices { + for accID, m := range dp.buckets { + if e, ok := m[outTradeNo]; ok { + p.unindexLocked(e) + delete(m, outTradeNo) + if len(m) == 0 { + delete(dp.buckets, accID) + delete(p.acctDev, accID) + } + removed = true + } + } + } + if !removed { + return + } + p.publishGlobalLocked() +} + +// Snapshot 返回当前聚合快照。零锁:通过 atomic.Pointer 读取已发布快照。 +// 返回的 *Snapshot 与内部 Accounts 切片不可变,调用方可安全并发读取。 +func (p *WaitPayPool) Snapshot() *Snapshot { + return p.snap.Load() +} + +// DeviceSnapshot 返回单设备聚合快照(零锁)。设备无子池/无订单 → 空统计(Total=0)。 +// 设备调度器与均衡分配器的数据源(PLAN-DYNAMIC-CLOSE-ALLOC §3.2)。 +func (p *WaitPayPool) DeviceSnapshot(deviceID string) *DeviceStat { + p.mu.RLock() + dp := p.devices[deviceID] + p.mu.RUnlock() + if dp == nil { + return &DeviceStat{DeviceID: deviceID} + } + st := dp.stat.Load() + if st == nil { + return &DeviceStat{DeviceID: deviceID} + } + return st +} + +// MatchByNote 备注码精确命中(在账户所属设备子池内匹配)。 +// 按 noteCode 找订单,要求 CreatedAt >= since。同码多单时返回 CreatedAt 最新者;未命中返回 nil。 +// +// 注意:本方法只做池内匹配,调用方必须另行做 DB 二次校验与账户/通道/空间校验。 +func (p *WaitPayPool) MatchByNote(accountID int64, note string, since time.Time) *Entry { + if note == "" { + return nil + } + + p.mu.RLock() + defer p.mu.RUnlock() + + devID, ok := p.acctDev[accountID] + if !ok { + return nil + } + dp := p.devices[devID] + if dp == nil { + return nil + } + noteMap, ok := dp.byNote[accountID] + if !ok { + return nil + } + outNo, ok := noteMap[note] + if !ok { + return nil + } + entryMap, ok := dp.buckets[accountID] + if !ok { + return nil + } + e, ok := entryMap[outNo] + if !ok { + return nil // 索引与桶不一致(理论上不应发生;防御性返回 nil) + } + if e.CreatedAt.Before(since) { + return nil + } + return e +} + +// MatchByAmount 金额兜底候选(在账户所属设备子池内匹配)。 +// 按 amountMinor 找订单(CreatedAt >= since),返回全部候选。 +// 多候选由调用方判歧义转人工;未命中返回 nil。 +func (p *WaitPayPool) MatchByAmount(accountID int64, amountMinor int64, since time.Time) []*Entry { + p.mu.RLock() + defer p.mu.RUnlock() + + devID, ok := p.acctDev[accountID] + if !ok { + return nil + } + dp := p.devices[devID] + if dp == nil { + return nil + } + amtMap, ok := dp.byAmount[accountID] + if !ok { + return nil + } + set, ok := amtMap[amountMinor] + if !ok || len(set) == 0 { + return nil + } + entryMap, ok := dp.buckets[accountID] + if !ok { + return nil + } + + out := make([]*Entry, 0, len(set)) + for outNo := range set { + e, ok := entryMap[outNo] + if !ok { + continue + } + if e.CreatedAt.Before(since) { + continue + } + out = append(out, e) + } + if len(out) == 0 { + return nil + } + // 稳定排序:先按 CreatedAt 升序,再按 OutTradeNo 字典序,保证测试可重复与歧义判定确定性 + sort.Slice(out, func(i, j int) bool { + if !out[i].CreatedAt.Equal(out[j].CreatedAt) { + return out[i].CreatedAt.Before(out[j].CreatedAt) + } + return out[i].OutTradeNo < out[j].OutTradeNo + }) + return out +} + +// Count 某账户待支付订单数。无该账户返回 0。 +func (p *WaitPayPool) Count(accountID int64) int { + p.mu.RLock() + defer p.mu.RUnlock() + devID, ok := p.acctDev[accountID] + if !ok { + return 0 + } + if dp := p.devices[devID]; dp != nil { + return len(dp.buckets[accountID]) + } + return 0 +} + +// HasDupAmount 某账户(同设备同账户)是否仍存在 ≥2 笔相同金额的待支付订单。 +// 调度器 execute 反馈判定用(同金额歧义解除 → 提前终止账单页轮询 visit)。 +func (p *WaitPayPool) HasDupAmount(accountID int64) bool { + p.mu.RLock() + defer p.mu.RUnlock() + devID, ok := p.acctDev[accountID] + if !ok { + return false + } + dp := p.devices[devID] + if dp == nil { + return false + } + for _, set := range dp.byAmount[accountID] { + if len(set) >= 2 { + return true + } + } + return false +} + +// Rebuild 全量重建:清空后批量 Add。启动时调用;调用方负责从 DB 加载 WAIT_PAY 订单。 +func (p *WaitPayPool) Rebuild(ctx context.Context, orders []*model.Order) { + p.mu.Lock() + defer p.mu.Unlock() + + p.devices = make(map[string]*devicePool) + p.acctDev = make(map[int64]string) + p.devices[""] = newDevicePool() + + for _, o := range orders { + if o == nil || o.OutTradeNo == "" { + continue + } + if ctx != nil { + select { + case <-ctx.Done(): + logger.Warn("pool", "Rebuild 中途取消", + "reason", ctx.Err(), + "loaded", len(p.acctDev)) + p.publishGlobalLocked() + return + default: + } + } + p.indexLocked(p.orderToEntry(o)) + } + p.publishGlobalLocked() +} + +// Reconcile 对账:入参为当前 DB 中全部 WAIT_PAY 订单(调用方查询)。 +// 与池内容 diff:DB 有池无(Add 遗漏)→ 补入;池有 DB 无(已终态未移除)→ 移除。 +// +// 返回 (added, removed, err)。当前实现无 I/O,err 恒为 nil;保留签名便于未来加 DB 校验。 +func (p *WaitPayPool) Reconcile(ctx context.Context, dbOrders []*model.Order) (added, removed int, err error) { + dbSet := make(map[string]struct{}, len(dbOrders)) + for _, o := range dbOrders { + if o != nil && o.OutTradeNo != "" { + dbSet[o.OutTradeNo] = struct{}{} + } + } + + p.mu.Lock() + defer p.mu.Unlock() + + // 池内 outTradeNo 全集 + poolSet := make(map[string]struct{}) + for _, dp := range p.devices { + for _, m := range dp.buckets { + for k := range m { + poolSet[k] = struct{}{} + } + } + } + + // DB 有池无 → Add + for _, o := range dbOrders { + if o == nil || o.OutTradeNo == "" { + continue + } + if _, exists := poolSet[o.OutTradeNo]; !exists { + p.indexLocked(p.orderToEntry(o)) + added++ + } + } + + // 池有 DB 无 → Remove + for outNo := range poolSet { + if _, exists := dbSet[outNo]; !exists { + for _, dp := range p.devices { + for accID, m := range dp.buckets { + if e, ok := m[outNo]; ok { + p.unindexLocked(e) + delete(m, outNo) + if len(m) == 0 { + delete(dp.buckets, accID) + delete(p.acctDev, accID) + } + removed++ + break + } + } + } + } + } + + if added > 0 || removed > 0 { + logger.Info("pool", "Reconcile 完成", + "added", added, "removed", removed, "total", len(dbOrders)) + } + + p.publishGlobalLocked() + return added, removed, nil +} + +// ReconcileLoop 周期对账(后台 goroutine):每 interval 从 loader 拉取 DB 全部 +// WAIT_PAY 订单并与池 diff(DB 有池无→补入;池有 DB 无→移除),防漂移。 +// loader 由调用方注入(main.go 包 store 查询),本包不依赖 store 避免循环依赖。 +// 退出条件:ctx 取消。 +func (p *WaitPayPool) ReconcileLoop(ctx context.Context, interval time.Duration, loader func() ([]*model.Order, error)) { + if interval <= 0 { + interval = 30 * time.Second + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + if loader == nil { + continue + } + orders, err := loader() + if err != nil { + logger.Warn("pool", "对账加载 DB 待支付订单失败(本周期跳过)", "err", err) + continue + } + if added, removed, rerr := p.Reconcile(ctx, orders); rerr != nil { + logger.Warn("pool", "对账执行失败(本周期跳过)", "err", rerr) + } else if added > 0 || removed > 0 { + logger.Info("pool", "对账同步完成", "added", added, "removed", removed) + } + } +} + +// --- 内部索引维护(必须持写锁) --- + +// lookupLocked 全池按 outTradeNo 定位条目(防御性扫描全部设备/账户桶)。 +func (p *WaitPayPool) lookupLocked(outTradeNo string) (*Entry, bool) { + for _, dp := range p.devices { + for _, m := range dp.buckets { + if e, ok := m[outTradeNo]; ok { + return e, true + } + } + } + return nil, false +} + +// subpoolLocked 获取账户所属设备子池(不存在则创建,更新 acctDev)。 +func (p *WaitPayPool) subpoolLocked(deviceID string) *devicePool { + dp, ok := p.devices[deviceID] + if !ok { + dp = newDevicePool() + p.devices[deviceID] = dp + } + return dp +} + +// indexLocked 将 entry 写入设备子池三张索引表 + 账户→设备反查。 +func (p *WaitPayPool) indexLocked(e *Entry) { + devID := e.DeviceID + if devID == "" { + devID = p.deviceOfEntry(e) + e.DeviceID = devID + } + dp := p.subpoolLocked(devID) + + bm, ok := dp.buckets[e.AccountID] + if !ok { + bm = make(map[string]*Entry) + dp.buckets[e.AccountID] = bm + } + bm[e.OutTradeNo] = e + p.acctDev[e.AccountID] = devID + + if e.NoteCode != "" { + nm, ok := dp.byNote[e.AccountID] + if !ok { + nm = make(map[string]string) + dp.byNote[e.AccountID] = nm + } + nm[e.NoteCode] = e.OutTradeNo + } + + am, ok := dp.byAmount[e.AccountID] + if !ok { + am = make(map[int64]map[string]struct{}) + dp.byAmount[e.AccountID] = am + } + set, ok := am[e.AmountMinor] + if !ok { + set = make(map[string]struct{}) + am[e.AmountMinor] = set + } + set[e.OutTradeNo] = struct{}{} +} + +// unindexLocked 从设备子池三张索引表移除 entry 的所有引用。 +// 注意:buckets 自身的 map[string]*Entry 由调用方负责 delete 与清理空 accountID 桶。 +func (p *WaitPayPool) unindexLocked(e *Entry) { + dp := p.devices[e.DeviceID] + if dp == nil { + return + } + if nm, ok := dp.byNote[e.AccountID]; ok { + if cur, ok := nm[e.NoteCode]; ok && cur == e.OutTradeNo { + delete(nm, e.NoteCode) + if len(nm) == 0 { + delete(dp.byNote, e.AccountID) + } + } + } + if am, ok := dp.byAmount[e.AccountID]; ok { + if set, ok := am[e.AmountMinor]; ok { + delete(set, e.OutTradeNo) + if len(set) == 0 { + delete(am, e.AmountMinor) + if len(am) == 0 { + delete(dp.byAmount, e.AccountID) + } + } + } + } +} + +// publishGlobalLocked 在写锁内重建并发布:全局快照 + 各设备子池 DeviceStat。 +// 调用者必须持有写锁(Lock)。 +func (p *WaitPayPool) publishGlobalLocked() { + now := time.Now() + all := make([]AccountStat, 0, len(p.acctDev)) + grand := 0 + + devIDs := make([]string, 0, len(p.devices)) + for id := range p.devices { + devIDs = append(devIDs, id) + } + sort.Strings(devIDs) + + for _, devID := range devIDs { + dp := p.devices[devID] + stats, total := accountStatsOf(dp, now) + if len(stats) == 0 && total == 0 { + dp.stat.Store(&DeviceStat{DeviceID: devID, UpdatedAt: now}) + continue + } + // 动态窗口(当前设备负载下新增订单的窗口;用于分配器/调度器观测) + win := p.fallback + if p.winCalc != nil { + active, t := 0, 0 + for _, st := range stats { + active++ + t += st.Count + } + if w := p.winCalc(active, t); w > 0 { + win = w + } + } + dp.stat.Store(&DeviceStat{ + DeviceID: devID, + Total: total, + Accounts: stats, + ActiveAccts: len(stats), + CloseWindow: win, + UpdatedAt: now, + }) + all = append(all, stats...) + grand += total + } + + p.snap.Store(&Snapshot{ + Accounts: all, + Total: grand, + UpdatedAt: now, + }) +} + +// accountStatsOf 计算单个设备子池的账户聚合统计(按 accountID 升序,快照稳定)。 +func accountStatsOf(dp *devicePool, now time.Time) ([]AccountStat, int) { + total := 0 + stats := make([]AccountStat, 0, len(dp.buckets)) + accIDs := make([]int64, 0, len(dp.buckets)) + for id := range dp.buckets { + accIDs = append(accIDs, id) + } + sort.Slice(accIDs, func(i, j int) bool { return accIDs[i] < accIDs[j] }) + for _, accID := range accIDs { + m := dp.buckets[accID] + if len(m) == 0 { + continue + } + var ( + oldestCreated time.Time + minClose time.Time + first = true + ) + for _, e := range m { + if first { + oldestCreated = e.CreatedAt + minClose = e.CloseAt + first = false + } else { + if e.CreatedAt.Before(oldestCreated) { + oldestCreated = e.CreatedAt + } + if e.CloseAt.Before(minClose) { + minClose = e.CloseAt + } + } + } + dupAmount := false + if am := dp.byAmount[accID]; len(am) > 0 { + for _, set := range am { + if len(set) >= 2 { // 同金额 ≥2 笔 → 通知栏匹配歧义,需账本页拆单 + dupAmount = true + break + } + } + } + age := now.Sub(oldestCreated) + if age < 0 { + age = 0 + } + ttl := minClose.Sub(now) // 可为负(已超时) + stats = append(stats, AccountStat{ + AccountID: accID, + Count: len(m), + DupAmount: dupAmount, + OldestAge: age, + MinTTL: ttl, + }) + total += len(m) + } + return stats, total +} diff --git a/internal/pool/waitpay_pool_test.go b/internal/pool/waitpay_pool_test.go new file mode 100644 index 0000000..121ad9c --- /dev/null +++ b/internal/pool/waitpay_pool_test.go @@ -0,0 +1,671 @@ +// Package pool 单元测试。 +// +// 设计目标: +// - 每个测试一个独立子测试(t.Run),失败信息含子测试名; +// - 并发测试必须可重复(确定性输出)且 -race 干净; +// - 时间相关测试使用固定基准时间,避免 sleep/flake。 +package pool + +import ( + "context" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "paygateway/internal/model" +) + +// baseTime 测试基准时间(统一基准便于断言 CloseAt/OldestAge) +var baseTime = time.Date(2026, 8, 29, 10, 0, 0, 0, time.UTC) + +// newOrder 构造一个测试订单。Channel 用 "personal.alipay"。 +func newOrder(outTradeNo string, accountID, amountMinor int64, noteCode string, createdAt time.Time) *model.Order { + return &model.Order{ + OutTradeNo: outTradeNo, + AccountID: accountID, + Channel: "personal.alipay", + AmountMinor: amountMinor, + NoteCode: noteCode, + CreatedAt: createdAt, + } +} + +// findAccountStat 工具:按 accountID 取快照元素 +func findAccountStat(snap *Snapshot, accountID int64) *AccountStat { + for i := range snap.Accounts { + if snap.Accounts[i].AccountID == accountID { + return &snap.Accounts[i] + } + } + return nil +} + +// ---------- 1. Add → Snapshot ---------- +func TestWaitPayPool_Add_ShowsInSnapshot(t *testing.T) { + p := NewWaitPayPool(15) + o := newOrder("O1", 100, 1000, "N1", baseTime) + p.Add(o) + + snap := p.Snapshot() + if snap.Total != 1 { + t.Fatalf("Total: want 1, got %d", snap.Total) + } + st := findAccountStat(snap, 100) + if st == nil { + t.Fatalf("账户 100 缺统计") + } + if st.Count != 1 { + t.Fatalf("Count: want 1, got %d", st.Count) + } + // CloseAt 应为 CreatedAt + 15min + if !snap.UpdatedAt.Equal(snap.UpdatedAt) { + t.Fatalf("UpdatedAt 应被设置") + } +} + +// ---------- 2. Add 同 outTradeNo 两次 → 仍 count=1 ---------- +func TestWaitPayPool_Add_IdempotentOverwrite(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Add(newOrder("O1", 100, 2000, "N2", baseTime.Add(time.Minute))) // 覆盖 + + snap := p.Snapshot() + if snap.Total != 1 { + t.Fatalf("Total: want 1, got %d", snap.Total) + } + if p.Count(100) != 1 { + t.Fatalf("Count(100): want 1, got %d", p.Count(100)) + } + // 最新 amountMinor 应为 2000 + entry := p.MatchByAmount(100, 2000, baseTime) + if entry == nil || len(entry) != 1 || entry[0].AmountMinor != 2000 { + t.Fatalf("覆盖后 amountMinor=2000 应可命中:got %v", entry) + } + // 旧 amountMinor=1000 已脱离索引 + if got := p.MatchByAmount(100, 1000, baseTime); got != nil { + t.Fatalf("旧 amountMinor=1000 应无候选:got %v", got) + } +} + +// ---------- 3. Remove 不存在 → 不 panic,count 不变 ---------- +func TestWaitPayPool_Remove_NonExistent(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Remove("DOES_NOT_EXIST") + if p.Count(100) != 1 { + t.Fatalf("Count 应仍为 1:got %d", p.Count(100)) + } + if p.Snapshot().Total != 1 { + t.Fatalf("Total 应仍为 1") + } +} + +// ---------- 4. Remove 已存在 → count=0 ---------- +func TestWaitPayPool_Remove_Existing(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Remove("O1") + if p.Count(100) != 0 { + t.Fatalf("Count 应为 0:got %d", p.Count(100)) + } + if p.Snapshot().Total != 0 { + t.Fatalf("Total 应为 0") + } + // 索引也清理 + if got := p.MatchByNote(100, "N1", baseTime); got != nil { + t.Fatalf("MatchByNote 应 nil:got %v", got) + } + if got := p.MatchByAmount(100, 1000, baseTime); got != nil { + t.Fatalf("MatchByAmount 应 nil:got %v", got) + } +} + +// ---------- 5. Upsert 更新 NoteCode → 新码命中、旧码 miss ---------- +func TestWaitPayPool_Upsert_NoteCodeChange(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Upsert(newOrder("O1", 100, 1000, "N2", baseTime)) + + if got := p.MatchByNote(100, "N2", baseTime); got == nil || got.OutTradeNo != "O1" { + t.Fatalf("MatchByNote 新码应命中 O1:got %v", got) + } + if got := p.MatchByNote(100, "N1", baseTime); got != nil { + t.Fatalf("MatchByNote 旧码应 miss:got %v", got) + } +} + +// ---------- 6. MatchByNote 尊重 since ---------- +func TestWaitPayPool_MatchByNote_SinceFilter(t *testing.T) { + p := NewWaitPayPool(15) + old := baseTime.Add(-2 * time.Hour) + p.Add(newOrder("O1", 100, 1000, "N1", old)) + + // since = baseTime → 旧单 CreatedAt < since → nil + if got := p.MatchByNote(100, "N1", baseTime); got != nil { + t.Fatalf("since 应过滤掉旧单:got %v", got) + } + // since = old → 旧单 CreatedAt >= since → 命中 + if got := p.MatchByNote(100, "N1", old); got == nil || got.OutTradeNo != "O1" { + t.Fatalf("since=old 应命中:got %v", got) + } +} + +// ---------- 7. MatchByNote 备注码复用:返回 CreatedAt 最新者 ---------- +func TestWaitPayPool_MatchByNote_ReuseReturnsLatest(t *testing.T) { + p := NewWaitPayPool(15) + // 同一 noteCode TTL 复用:先一笔旧单被 Remove(关单),再补一笔新单 + p.Add(newOrder("O_OLD", 100, 1000, "N1", baseTime.Add(-3*time.Minute))) + p.Add(newOrder("O_NEW", 100, 1000, "N1", baseTime)) + + got := p.MatchByNote(100, "N1", baseTime.Add(-5*time.Minute)) + if got == nil { + t.Fatalf("MatchByNote 应命中") + } + if got.OutTradeNo != "O_NEW" { + t.Fatalf("应返回最新 CreatedAt 的 O_NEW:got %s", got.OutTradeNo) + } +} + +// ---------- 8. MatchByAmount 多候选返回;无候选 nil ---------- +func TestWaitPayPool_MatchByAmount(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Add(newOrder("O2", 100, 1000, "N2", baseTime.Add(time.Second))) + p.Add(newOrder("O3", 100, 2000, "N3", baseTime)) + + // 同金额 1000 → 两个候选 + got := p.MatchByAmount(100, 1000, baseTime) + if got == nil { + t.Fatalf("MatchByAmount 应有候选") + } + if len(got) != 2 { + t.Fatalf("候选数:want 2, got %d", len(got)) + } + // 排序稳定性:先 CreatedAt 升序 + if !got[0].CreatedAt.Before(got[1].CreatedAt) { + t.Fatalf("候选排序应按 CreatedAt 升序") + } + + // 2000 → 一个候选 + got2 := p.MatchByAmount(100, 2000, baseTime) + if got2 == nil || len(got2) != 1 || got2[0].OutTradeNo != "O3" { + t.Fatalf("2000 候选异常:got %v", got2) + } + + // 不存在的金额 → nil + if got3 := p.MatchByAmount(100, 9999, baseTime); got3 != nil { + t.Fatalf("无候选应 nil:got %v", got3) + } + + // 不存在的账户 → nil + if got4 := p.MatchByAmount(999, 1000, baseTime); got4 != nil { + t.Fatalf("无账户应 nil:got %v", got4) + } +} + +// ---------- 9. 并发 Add/Remove/Snapshot storm → -race 干净,final 一致 ---------- +func TestWaitPayPool_ConcurrentStorm(t *testing.T) { + p := NewWaitPayPool(15) + // 注入 winCalc 以验证并发 Add 不会触发 map 迭代竞态(P0 修复: orderToEntry 在锁内)。 + p.SetWindowCalc(func(active, total int) time.Duration { + _ = active + _ = total + return 5 * time.Minute + }) + // 初始化一个设备绑定,使 windowFor 走活跃账户计数路径 + p.SetDeviceOf(func(accountID int64) (string, bool) { + return "dev1", true + }) + + const ( + workers = 16 + opsPer = 200 + accBase = 1000 + outPrefix = "O" + ) + + var wg sync.WaitGroup + var added, removed int64 + + for w := 0; w < workers; w++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + for i := 0; i < opsPer; i++ { + accID := accBase + int64(workerID) + outNo := outPrefix + strconv.Itoa(workerID) + "_" + strconv.Itoa(i) + note := "N" + strconv.Itoa(i) + switch (workerID + i) % 3 { + case 0: + // Add + p.Add(newOrder(outNo, accID, int64(100+i), note, baseTime.Add(time.Duration(i)*time.Millisecond))) + atomic.AddInt64(&added, 1) + case 1: + // Remove 自己刚 Add 的(可能不存在 → 幂等) + p.Remove(outNo) + atomic.AddInt64(&removed, 1) + case 2: + // Snapshot 读 + snap := p.Snapshot() + if snap == nil { + t.Errorf("Snapshot nil") + return + } + _ = snap.Total + } + } + }(w) + } + + wg.Wait() + + // 终态一致性: + // - 快照 Total == Count(每个账户) 之和 == 池内实际条目数(通过公共 API 统计) + var bucketCount int + for _, st := range p.Snapshot().Accounts { + bucketCount += p.Count(st.AccountID) + } + + snap := p.Snapshot() + if snap.Total != bucketCount { + t.Fatalf("快照 Total(%d) 与 buckets 实际(%d) 不一致", snap.Total, bucketCount) + } + for _, st := range snap.Accounts { + if p.Count(st.AccountID) != st.Count { + t.Fatalf("账户 %d 快照 Count(%d) 与 Count()(实际 %d) 不一致", st.AccountID, st.Count, p.Count(st.AccountID)) + } + } + t.Logf("storm done: added=%d removed=%d final=%d", added, removed, snap.Total) +} + +// ---------- 10. Rebuild 清空后批量加入 ---------- +func TestWaitPayPool_Rebuild_ClearsThenAdds(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("OLD1", 100, 1000, "N1", baseTime)) + p.Add(newOrder("OLD2", 200, 2000, "N2", baseTime)) + + orders := []*model.Order{ + newOrder("NEW1", 100, 1000, "N1", baseTime), + newOrder("NEW2", 300, 3000, "N3", baseTime), + newOrder("NEW3", 300, 4000, "N4", baseTime), + } + p.Rebuild(context.Background(), orders) + + if p.Count(200) != 0 { + t.Fatalf("旧账户 200 应清空:got %d", p.Count(200)) + } + if p.Count(100) != 1 { + t.Fatalf("账户 100 应仅剩 NEW1:got %d", p.Count(100)) + } + if p.Count(300) != 2 { + t.Fatalf("账户 300 应有 2 单:got %d", p.Count(300)) + } + if p.Snapshot().Total != 3 { + t.Fatalf("Total 应 3") + } +} + +// ---------- 11. Reconcile:DB-only / pool-only / shared 三类 ---------- +func TestWaitPayPool_Reconcile_Diff(t *testing.T) { + p := NewWaitPayPool(15) + + // 池内:SHARED + POOL_ONLY + p.Add(newOrder("SHARED", 100, 1000, "N1", baseTime)) + p.Add(newOrder("POOL_ONLY", 100, 2000, "N2", baseTime)) + + // DB:SHARED + DB_ONLY + dbOrders := []*model.Order{ + newOrder("SHARED", 100, 1000, "N1", baseTime), + newOrder("DB_ONLY", 200, 3000, "N3", baseTime), + } + + added, removed, err := p.Reconcile(context.Background(), dbOrders) + if err != nil { + t.Fatalf("err: %v", err) + } + if added != 1 || removed != 1 { + t.Fatalf("added/removed: want 1/1, got %d/%d", added, removed) + } + + // SHARED 仍存在(POOL_ONLY 已被移除 → 账户 100 仅剩 SHARED) + if p.Count(100) != 1 { + t.Fatalf("账户 100 应有 1 单(POOL_ONLY 已移除):got %d", p.Count(100)) + } + if got := p.MatchByAmount(100, 1000, baseTime); got == nil || len(got) != 1 || got[0].OutTradeNo != "SHARED" { + t.Fatalf("SHARED 应命中:got %v", got) + } + // POOL_ONLY 已移除 + if got := p.MatchByAmount(100, 2000, baseTime); got != nil { + t.Fatalf("POOL_ONLY 应被移除:got %v", got) + } + // DB_ONLY 已补入 + if got := p.MatchByAmount(200, 3000, baseTime); got == nil || len(got) != 1 || got[0].OutTradeNo != "DB_ONLY" { + t.Fatalf("DB_ONLY 应补入:got %v", got) + } + if p.Snapshot().Total != 2 { + t.Fatalf("Total 应 2(SHARED+DB_ONLY)") + } +} + +// ---------- 12. closeWindowMinutes=0 → CloseAt 默认 CreatedAt+30min ---------- +func TestWaitPayPool_CloseAt_DefaultWhenZero(t *testing.T) { + p := NewWaitPayPool(0) // 显式 0 → 走 30min 兜底 + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + + e := p.MatchByNote(100, "N1", baseTime) + if e == nil { + t.Fatalf("MatchByNote 应命中") + } + wantClose := baseTime.Add(30 * time.Minute) + if !e.CloseAt.Equal(wantClose) { + t.Fatalf("CloseAt: want %v, got %v", wantClose, e.CloseAt) + } +} + +// ---------- closeWindowMinutes=-1(负数)→ 同样走默认 30min ---------- +func TestWaitPayPool_CloseAt_DefaultWhenNegative(t *testing.T) { + p := NewWaitPayPool(-5) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + + e := p.MatchByNote(100, "N1", baseTime) + if e == nil { + t.Fatalf("MatchByNote 应命中") + } + if !e.CloseAt.Equal(baseTime.Add(30 * time.Minute)) { + t.Fatalf("CloseAt 默认 30min:got %v", e.CloseAt) + } +} + +// ---------- Upsert 改变 Amount → byAmount 索引正确迁移 ---------- +func TestWaitPayPool_Upsert_AmountChange(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Upsert(newOrder("O1", 100, 2000, "N1", baseTime)) + + if got := p.MatchByAmount(100, 1000, baseTime); got != nil { + t.Fatalf("旧金额 1000 应无候选:got %v", got) + } + got := p.MatchByAmount(100, 2000, baseTime) + if got == nil || len(got) != 1 || got[0].OutTradeNo != "O1" { + t.Fatalf("新金额 2000 应命中 O1:got %v", got) + } +} + +// ---------- Remove 后 Reconcile 不应再次"添加"已终态订单 ---------- +func TestWaitPayPool_Reconcile_NoGhostResurrection(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + p.Remove("O1") + + // DB 已不包含 O1 → Reconcile 不应补回 + added, removed, err := p.Reconcile(context.Background(), nil) + if err != nil || added != 0 || removed != 0 { + t.Fatalf("空 DB:want 0/0/nil, got %d/%d/%v", added, removed, err) + } + if p.Count(100) != 0 { + t.Fatalf("Count 应 0:got %d", p.Count(100)) + } +} + +// ---------- Count 无账户 → 0;不 panic ---------- +func TestWaitPayPool_Count_UnknownAccount(t *testing.T) { + p := NewWaitPayPool(15) + if p.Count(999) != 0 { + t.Fatalf("未知账户应 0") + } +} + +// ---------- Snapshot.Accounts 按 accountID 升序(稳定) ---------- +func TestWaitPayPool_Snapshot_AccountsOrdered(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 300, 100, "", baseTime)) + p.Add(newOrder("O2", 100, 100, "", baseTime)) + p.Add(newOrder("O3", 200, 100, "", baseTime)) + + snap := p.Snapshot() + want := []int64{100, 200, 300} + for i, st := range snap.Accounts { + if st.AccountID != want[i] { + t.Fatalf("Accounts[%d]: want %d, got %d", i, want[i], st.AccountID) + } + } +} + +// ---------- 同一 outTradeNo 不同账户:Remove 全局命中 ---------- +func TestWaitPayPool_Remove_GloballyUnique(t *testing.T) { + // 说明:业务上 outTradeNo 全局唯一(接入方订单号),但池内部按 accountID 桶索引; + // 若误把同一 outTradeNo 加到两个账户(人为错误),Remove 应清理所有桶中的同名条目。 + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "", baseTime)) + p.Add(newOrder("O1", 200, 1000, "", baseTime)) // 同 outTradeNo,不同账户(业务不应发生;测试索引清理) + + p.Remove("O1") + if p.Count(100) != 0 || p.Count(200) != 0 { + t.Fatalf("Remove 应清理所有桶中同名:got 100=%d 200=%d", p.Count(100), p.Count(200)) + } +} + +// ---------- MinTTL 负值(已超时)---------- +func TestWaitPayPool_Snapshot_MinTTL_Overdue(t *testing.T) { + p := NewWaitPayPool(0) // 30min closeWindow + // 单子 40 分钟前创建 → 一定已超时 + old := time.Now().Add(-40 * time.Minute) + p.Add(newOrder("O1", 100, 1000, "", old)) + + snap := p.Snapshot() + st := findAccountStat(snap, 100) + if st == nil { + t.Fatalf("缺账户统计") + } + if st.MinTTL >= 0 { + t.Fatalf("MinTTL 应 <0(已超时):got %v", st.MinTTL) + } +} + +// ---------- 13. 设备维度子池:DeviceSnapshot 按设备隔离 ---------- +func TestWaitPayPool_DeviceSnapshot_Isolation(t *testing.T) { + p := NewWaitPayPool(15) + devOf := func(accountID int64) (string, bool) { + switch accountID { + case 100, 101: + return "devA", true + case 200: + return "devB", true + default: + return "", false + } + } + p.SetDeviceOf(devOf) + p.Add(newOrder("A1", 100, 1000, "N1", baseTime)) + p.Add(newOrder("A2", 101, 2000, "N2", baseTime)) + p.Add(newOrder("B1", 200, 3000, "N3", baseTime)) + + da := p.DeviceSnapshot("devA") + if da == nil || da.Total != 2 || da.ActiveAccts != 2 { + t.Fatalf("devA: want Total=2 ActiveAccts=2, got %+v", da) + } + db := p.DeviceSnapshot("devB") + if db == nil || db.Total != 1 || db.ActiveAccts != 1 { + t.Fatalf("devB: want Total=1 ActiveAccts=1, got %+v", db) + } + // 全局快照不受设备拆分影响 + if p.Snapshot().Total != 3 { + t.Fatalf("全局 Total: want 3, got %d", p.Snapshot().Total) + } + // 匹配/计数按账户命中(跨设备不混淆) + if p.Count(100) != 1 || p.Count(200) != 1 { + t.Fatalf("Count 异常: 100=%d 200=%d", p.Count(100), p.Count(200)) + } + if got := p.MatchByNote(100, "N1", baseTime); got == nil || got.OutTradeNo != "A1" { + t.Fatalf("MatchByNote devA 应命中 A1: %v", got) + } + if got := p.MatchByNote(200, "N3", baseTime); got == nil || got.OutTradeNo != "B1" { + t.Fatalf("MatchByNote devB 应命中 B1: %v", got) + } +} + +// ---------- 14. 动态关单窗口:winCalc 注入后 CloseAt 随负载变化且 clamp ---------- +func TestWaitPayPool_DynamicCloseAt_WindowCalc(t *testing.T) { + p := NewWaitPayPool(0) // fallback 30min,但 winCalc 优先 + // winCalc: 负载越高窗口越长,clamp [5min, 10min] + p.SetWindowCalc(func(active, total int) time.Duration { + w := 5*time.Minute + time.Duration(total)*time.Minute + if w > 10*time.Minute { + w = 10 * time.Minute + } + return w + }) + // 第 1 单:total=0 → 5min + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + e1 := p.MatchByNote(100, "N1", baseTime) + if e1 == nil { + t.Fatalf("MatchByNote 应命中") + } + want1 := baseTime.Add(5 * time.Minute) + if !e1.CloseAt.Equal(want1) { + t.Fatalf("第 1 单 CloseAt: want %v, got %v", want1, e1.CloseAt) + } + // 第 2 单:total=1 → 6min + p.Add(newOrder("O2", 100, 2000, "N2", baseTime)) + e2 := p.MatchByNote(100, "N2", baseTime) + if e2 == nil { + t.Fatalf("第 2 单应命中") + } + want2 := baseTime.Add(6 * time.Minute) + if !e2.CloseAt.Equal(want2) { + t.Fatalf("第 2 单 CloseAt: want %v, got %v", want2, e2.CloseAt) + } + // 高负载下窗口应受上限 clamp(模拟 8+ 单 → 10min) + p.Remove("O1") + p.Remove("O2") + for i := 0; i < 12; i++ { + p.Add(newOrder("HI"+string(rune('A'+i)), int64(300+i), 1000, "", baseTime)) + } + hi := p.MatchByAmount(311, 1000, baseTime) // 最后一单 + if hi == nil || len(hi) != 1 { + t.Fatalf("高负载单应命中: %v", hi) + } + // 该设备累计 12 单 → winCalc → 10min 上限 + if !hi[0].CloseAt.Equal(baseTime.Add(10 * time.Minute)) { + t.Fatalf("高负载 CloseAt 应 clamp 到 10min: got %v", hi[0].CloseAt) + } +} + +// ---------- 15. CloseAt 单调只增:Upsert 覆盖时不倒退 ---------- +func TestWaitPayPool_CloseAt_Monotonic(t *testing.T) { + p := NewWaitPayPool(15) + // 初始窗口 15min + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + first := p.MatchByNote(100, "N1", baseTime) + if first == nil { + t.Fatalf("应命中") + } + // 切换到固定 5min 窗口后 Upsert:CloseAt 不得早于旧值 + p.SetWindowCalc(func(active, total int) time.Duration { return 5 * time.Minute }) + p.Upsert(newOrder("O1", 100, 1000, "N2", baseTime.Add(time.Minute))) + got := p.MatchByNote(100, "N2", baseTime) + if got == nil { + t.Fatalf("Upsert 后应命中") + } + if got.CloseAt.Before(first.CloseAt) { + t.Fatalf("CloseAt 单调只增被违反: first=%v got=%v", first.CloseAt, got.CloseAt) + } + // 新窗口 5min 在 CreatedAt+1min 后 → CreatedAt+6min < first(15min) → 保持 15min + want := baseTime.Add(15 * time.Minute) + if !got.CloseAt.Equal(want) { + t.Fatalf("CloseAt 应保持旧值 15min: want %v, got %v", want, got.CloseAt) + } +} + +// ---------- 16. CloseAtOf 查询(扫描器动态关单判断)---------- +func TestWaitPayPool_CloseAtOf(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) + ct, ok := p.CloseAtOf("O1") + if !ok { + t.Fatalf("CloseAtOf 应命中") + } + if !ct.Equal(baseTime.Add(15 * time.Minute)) { + t.Fatalf("CloseAtOf: want 15min, got %v", ct) + } + if _, ok := p.CloseAtOf("NO_SUCH"); ok { + t.Fatalf("CloseAtOf 未知订单应 not found") + } + p.Remove("O1") + if _, ok := p.CloseAtOf("O1"); ok { + t.Fatalf("CloseAtOf 移除后应 not found") + } +} + +// ---------- 17. SetDeviceOf 重分桶:绑定变化后条目迁移到新设备 ---------- +func TestWaitPayPool_SetDeviceOf_Repartition(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("O1", 100, 1000, "N1", baseTime)) // 无 deviceOf,归 "" + if st := p.DeviceSnapshot(""); st == nil || st.Total != 1 { + t.Fatalf("注入前应归兜底桶: %+v", st) + } + // 注入映射:100 → devX + p.SetDeviceOf(func(accountID int64) (string, bool) { + if accountID == 100 { + return "devX", true + } + return "", false + }) + if st := p.DeviceSnapshot("devX"); st == nil || st.Total != 1 { + t.Fatalf("注入后应迁移到 devX: %+v", st) + } + if st := p.DeviceSnapshot(""); st == nil || st.Total != 0 { + t.Fatalf("兜底桶应清空: %+v", st) + } + // 匹配链路仍按账户命中 + if got := p.MatchByNote(100, "N1", baseTime); got == nil || got.OutTradeNo != "O1" { + t.Fatalf("迁移后匹配应命中: %v", got) + } +} + +// ---------- 18. DupAmount:同账户同金额 ≥2 笔 → true;金额互异 → false ---------- +func TestWaitPayPool_DupAmount(t *testing.T) { + p := NewWaitPayPool(15) + + // 账户100: 两笔不同金额 → 无同金额 + p.Add(newOrder("A1", 100, 1000, "", baseTime)) + p.Add(newOrder("A2", 100, 2000, "", baseTime.Add(time.Minute))) + // 账户200: 两笔同为 3000 → 有同金额 + p.Add(newOrder("B1", 200, 3000, "", baseTime)) + p.Add(newOrder("B2", 200, 3000, "", baseTime.Add(time.Minute))) + + snap := p.Snapshot() + if st := findAccountStat(snap, 100); st == nil || st.DupAmount { + t.Fatalf("账户100 金额互异应 DupAmount=false, got %+v", st) + } + if st := findAccountStat(snap, 200); st == nil || !st.DupAmount { + t.Fatalf("账户200 同金额 2 笔应 DupAmount=true, got %+v", st) + } + + if p.HasDupAmount(100) { + t.Fatalf("HasDupAmount(100) 应 false") + } + if !p.HasDupAmount(200) { + t.Fatalf("HasDupAmount(200) 应 true") + } + if p.HasDupAmount(999) { + t.Fatalf("HasDupAmount 未知账户应 false") + } +} + +// ---------- 19. HasDupAmount:同一账户同金额消除(移除一笔/改金额)→ 转 false ---------- +func TestWaitPayPool_HasDupAmount_Resolved(t *testing.T) { + p := NewWaitPayPool(15) + p.Add(newOrder("B1", 200, 3000, "", baseTime)) + p.Add(newOrder("B2", 200, 3000, "", baseTime.Add(time.Minute))) + if !p.HasDupAmount(200) { + t.Fatalf("初始应 true") + } + // 移除一笔 → 只剩一笔 3000 → 歧义解除 + p.Remove("B2") + if p.HasDupAmount(200) { + t.Fatalf("移除一笔后同金额消除应 false") + } +} diff --git a/internal/ratelimit/ratelimit.go b/internal/ratelimit/ratelimit.go new file mode 100644 index 0000000..771c007 --- /dev/null +++ b/internal/ratelimit/ratelimit.go @@ -0,0 +1,108 @@ +// Package ratelimit 提供基于 IP 的滑动窗口速率限制中间件。 +// +// 用途:保护公网支付接口(/qr/real、/qr/refresh、/submit.php 等), +// 防止单 IP 刷接口 / 暴力枚举 token。多实例部署需改用 Redis 后端,本实现仅覆盖单进程。 +package ratelimit + +import ( + "net/http" + "strconv" + "sync" + "time" + + "github.com/gin-gonic/gin" +) + +// limiter 公网 IP 速率限制器实例。 +type limiter struct { + mu sync.Mutex + buckets map[string]*bucket + max int // 窗口内最大请求数 + window time.Duration // 滑动窗口长度 +} + +type bucket struct { + mu sync.Mutex + stamp []time.Time // 窗口内请求时间戳 +} + +func newLimiter(max int, window time.Duration) *limiter { + return &limiter{buckets: make(map[string]*bucket), max: max, window: window} +} + +// allow 判定是否放行;不放行则返回剩余重试等待秒。 +func (l *limiter) allow(key string, now time.Time) (bool, int) { + l.mu.Lock() + b, ok := l.buckets[key] + if !ok { + b = &bucket{} + l.buckets[key] = b + } + l.mu.Unlock() + + b.mu.Lock() + defer b.mu.Unlock() + cutoff := now.Add(-l.window) + keep := b.stamp[:0] + for _, t := range b.stamp { + if t.After(cutoff) { + keep = append(keep, t) + } + } + if len(keep) >= l.max { + b.stamp = keep + // 计算最早一条何时滑出窗口,作为 retry-after + retry := int(keep[0].Add(l.window).Sub(now).Seconds()) + if retry < 1 { + retry = 1 + } + return false, retry + } + b.stamp = append(keep, now) + return true, 0 +} + +// prune 清理无活动桶,避免内存泄漏。 +func (l *limiter) prune(now time.Time) { + cutoff := now.Add(-l.window) + l.mu.Lock() + defer l.mu.Unlock() + for key, b := range l.buckets { + b.mu.Lock() + stale := len(b.stamp) == 0 || (len(b.stamp) > 0 && b.stamp[len(b.stamp)-1].Before(cutoff)) + b.mu.Unlock() + if stale { + delete(l.buckets, key) + } + } +} + +// IPRateLimit 返回一个 gin 中间件:按 c.ClientIP() 限流,每 window 最多 max req。 +// 触发上限时返回 429 + Retry-After 头。 +// 通过 max=0 可禁用(直接 next),便于配置可关闭。 +func IPRateLimit(max int, window time.Duration) gin.HandlerFunc { + if max <= 0 || window <= 0 { + return func(c *gin.Context) { c.Next() } + } + l := newLimiter(max, window) + var pruneCounter int + return func(c *gin.Context) { + ip := c.ClientIP() + now := time.Now() + // 每 64 请求清理一次过期桶,平摊成本 + pruneCounter++ + if pruneCounter%64 == 0 { + l.prune(now) + } + ok, retry := l.allow(ip, now) + if !ok { + c.Header("Retry-After", strconv.Itoa(retry)) + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ + "error": "请求过于频繁,请稍后再试", + "retry_after_sec": retry, + }) + return + } + c.Next() + } +} diff --git a/internal/reconciler/dailyreport.go b/internal/reconciler/dailyreport.go new file mode 100755 index 0000000..0a9ac75 --- /dev/null +++ b/internal/reconciler/dailyreport.go @@ -0,0 +1,166 @@ +// Package reconciler 日终对账报告(RESILIENCE.md P2-1) +// +// 对账维度(网关 vs 通道 vs 上游): +// - 网关订单:按通道拆分(merchant.* / personal.*),统计订单数/成功数/金额 +// - 资金核对:成功订单金额 vs 订单金额(差异 = 异常池 / 金额不匹配单) +// - 回调可靠性:notify_tasks 各状态数量(pending / success / dead) +// - 异常池:待处理数量(自动化兜底失败的最终兜底) +// - 结论:每通道差异明细,供人工核对 +// +// 每日 00:00 由 main 调用(与账本快照/日限额重置同周期),输出结构化日志; +// 管理后台「对账中心」可实时查询(GET /api/reconciler/daily-report?date=YYYY-MM-DD)。 +package reconciler + +import ( + "context" + "fmt" + "strings" + "time" + + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/store" +) + +// ChannelReport 单通道对账 +type ChannelReport struct { + Channel string `json:"channel"` + TotalOrders int64 `json:"total_orders"` // 当日订单 + SuccessOrders int64 `json:"success_orders"` // 当日成功 + ClosedOrders int64 `json:"closed_orders"` // 当日关闭 + AmountMinor int64 `json:"amount_minor"` // 当日成功金额(分) + AmountClosed int64 `json:"amount_closed"` // 当日关闭金额(分) +} + +// DailyReport 日终对账报告 +type DailyReport struct { + Date string `json:"date"` // 2026-08-10 + + OrdersTotal int64 `json:"orders_total"` + OrdersSuccess int64 `json:"orders_success"` + OrdersClosed int64 `json:"orders_closed"` + SuccessAmount int64 `json:"success_amount"` // 成功金额(分) + ClosedAmount int64 `json:"closed_amount"` // 关闭金额(分) + + NotifyPending int64 `json:"notify_pending"` // 待投递回调 + NotifySuccess int64 `json:"notify_success"` + NotifyDead int64 `json:"notify_dead"` // 死信(需人工重发) + + ExceptionsPending int64 `json:"exceptions_pending"` // 异常池待处理 + + ByChannel []*ChannelReport `json:"by_channel"` // 通道拆分 + + Conclusion string `json:"conclusion"` // 对账结论 +} + +// BuildDailyReport 生成指定日期的对账报告(date 格式 2006-01-02,空=今天) +func BuildDailyReport(ctx context.Context, st store.Store, date string) (*DailyReport, error) { + if date == "" { + date = time.Now().Format("2006-01-02") + } + r := &DailyReport{Date: date, ByChannel: []*ChannelReport{}} + + // 1. 网关侧总量 + total, err := st.CountOrders(ctx, "") + if err != nil { + return nil, err + } + success, err := st.CountOrders(ctx, model.StatusSuccess) + if err != nil { + return nil, err + } + closed, err := st.CountOrders(ctx, model.StatusClosed) + if err != nil { + return nil, err + } + r.OrdersTotal, r.OrdersSuccess, r.OrdersClosed = total, success, closed + + if successAmount, err := st.SumAmount(ctx, model.StatusSuccess); err == nil { + r.SuccessAmount = successAmount + } + if closedAmount, err := st.SumAmount(ctx, model.StatusClosed); err == nil { + r.ClosedAmount = closedAmount + } + + // 2. 按通道拆分(当日口径:订单/成功/关闭 + 金额) + for _, name := range channel.AllNames() { + total, _, err := st.DailyStatsByChannel(ctx, name, "", date) + if err != nil { + continue + } + succ, succAmount, err := st.DailyStatsByChannel(ctx, name, model.StatusSuccess, date) + if err != nil { + continue + } + closed, closedAmount, err := st.DailyStatsByChannel(ctx, name, model.StatusClosed, date) + if err != nil { + continue + } + r.ByChannel = append(r.ByChannel, &ChannelReport{ + Channel: name, TotalOrders: total, SuccessOrders: succ, + ClosedOrders: closed, AmountMinor: succAmount, AmountClosed: closedAmount, + }) + } + + // 3. 回调可靠性 + if n, err := st.CountNotifyTasks(ctx, "pending"); err == nil { + r.NotifyPending = n + } + if n, err := st.CountNotifyTasks(ctx, "success"); err == nil { + r.NotifySuccess = n + } + if n, err := st.CountNotifyTasks(ctx, "dead"); err == nil { + r.NotifyDead = n + } + + // 4. 异常池 + if n, err := st.CountExceptions(ctx, model.ExPhasePending); err == nil { + r.ExceptionsPending = n + } + + // 5. 结论 + r.Conclusion = buildConclusion(r) + return r, nil +} + +// buildConclusion 对账结论:差异分析 +func buildConclusion(r *DailyReport) string { + var issues []string + // 金额对账:成功金额 vs 订单金额差异来自异常池(金额不匹配单不计入成功) + // 注意:当日成功金额 CountOrdersByChannelToday 是全天累计;全局成功金额是历史累计, + // 因此这里不做全局金额等式校验,改为提示性结论: + if r.NotifyDead > 0 { + issues = append(issues, fmt.Sprintf("有 %d 笔回调 dead(需人工重发)", r.NotifyDead)) + } + if r.ExceptionsPending > 0 { + issues = append(issues, fmt.Sprintf("异常池有 %d 笔待处理(资金安全关键,请及时人工核实)", r.ExceptionsPending)) + } + for _, c := range r.ByChannel { + if c.SuccessOrders > 0 && c.ClosedOrders > 0 { + // 提示关闭占比过高的通道 + if float64(c.ClosedOrders)/float64(c.SuccessOrders+c.ClosedOrders) > 0.3 { + issues = append(issues, fmt.Sprintf("通道 %s 关闭率偏高(成功 %d / 关闭 %d),注意观察", c.Channel, c.SuccessOrders, c.ClosedOrders)) + } + } + } + if len(issues) == 0 { + return "✅ 对账正常:无 dead 回调、无待处理异常" + } + return "⚠️ 需要关注:" + strings.Join(issues, ";") +} + +// Log 输出结构化日志(每日 00:00 调用) +func (r *DailyReport) Log() { + logger.Info("reconciler", "=== 日终对账报告 "+r.Date+" ===") + logger.Info("reconciler", "订单", "total", r.OrdersTotal, "success", r.OrdersSuccess, + "closed", r.OrdersClosed, "success_amount", r.SuccessAmount) + for _, c := range r.ByChannel { + logger.Info("reconciler", "通道", "name", c.Channel, "total", c.TotalOrders, + "success", c.SuccessOrders, "closed", c.ClosedOrders, + "amount", c.AmountMinor, "closed_amount", c.AmountClosed) + } + logger.Info("reconciler", "回调", "pending", r.NotifyPending, "success", r.NotifySuccess, "dead", r.NotifyDead) + logger.Info("reconciler", "异常池待处理", "count", r.ExceptionsPending) + logger.Info("reconciler", "结论", "text", r.Conclusion) +} diff --git a/internal/reconciler/devicewatcher.go b/internal/reconciler/devicewatcher.go new file mode 100755 index 0000000..8b8f12a --- /dev/null +++ b/internal/reconciler/devicewatcher.go @@ -0,0 +1,363 @@ +// Package reconciler 设备离线检测任务(个人免签通道) +// +// 设计(docs/MIANQIAN.md §9 掉线兜底与异常处理): +// - 设备离线判定以 adb 可达性为准(监听 App 已弃用,2026-08-29 重构) +// - 设备 OFFLINE → 其绑定的收款账户冻结(不可接新单) +// - 该账户下已下单未支付的订单 → 进人工异常池(listen_offline), +// 人工从收款记录核对实际收款情况(用户可能已付款但未监听到) +// +// 与 ConfirmScheduler 的 passive 检查互补: +// - ConfirmScheduler.confirmPersonal 在关单时被动检查监听端是否离线 +// - 本任务主动扫描:设备一掉线立即冻结账户 + 挂起订单,不等关单时间 +package reconciler + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "paygateway/internal/account" + "paygateway/internal/adbmgr" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/order" + "paygateway/internal/personal" + "paygateway/internal/store" +) + +// AdbProber 检测 adb 是否可达的接口(adbmgr.Manager 已实现此语义;抽象为接口便于 mock 与可选依赖) +type AdbProber interface { + Online(ctx context.Context, serial string) bool + Connect(ctx context.Context, serial string) error +} + +// DeviceWatcherConfig 设备离线检测参数 +type DeviceWatcherConfig struct { + HeartbeatTimeout time.Duration // 心跳超时(默认 120s) + ScanInterval time.Duration // 扫描间隔(默认 30s) + // MissThreshold 连续扫描未收到心跳的次数阈值,达到才判离线(默认 2)。 + // 配合 HeartbeatTimeout 形成「超时 + 连续确认」双保险:单次扫描抖动不误判。 + MissThreshold int + // ControlPlaneInterval 控制面状态机扫描间隔(默认 30s)。 + // 独立于 ScanInterval:心跳超时与 adb 重连关注点不同,避免耦合。 + ControlPlaneInterval time.Duration +} + +// DefaultDeviceWatcherConfig 默认参数 +func DefaultDeviceWatcherConfig() *DeviceWatcherConfig { + return &DeviceWatcherConfig{ + HeartbeatTimeout: 120 * time.Second, + ScanInterval: 30 * time.Second, + MissThreshold: 2, + ControlPlaneInterval: 30 * time.Second, + } +} + +// Recoverer 设备离线恢复器(由 admin 包实现并注入),解耦 reconcile → admin 反向依赖。 +// 实现侧负责限频,避免短时间对同一设备反复重启。 +type Recoverer interface { + AttemptRestart(ctx context.Context, id int64) error +} + +// DeviceWatcher 监听端设备离线检测器 +type DeviceWatcher struct { + pdb store.PersonalStore + pool *account.AccountPool + st store.Store + svc *order.Service + cfg *DeviceWatcherConfig + recoverer Recoverer // nil = 关闭自动重启 + adb AdbProber // nil = 关闭控制面状态机 + + // missCount 记录每设备连续超时扫描次数(按设备 ID),达到 MissThreshold 才判离线 + missCount map[int64]int + // controlPlaneLast 记录每设备上次控制面扫描时间(按 device_id),限频 + controlPlaneLast map[string]time.Time +} + +// NewDeviceWatcher 创建设备离线检测器 +// recoverer 可为 nil(关闭自动重启能力,行为退化为原始的「只冻结+挂单」语义)。 +// adb 可为 nil(关闭控制面状态机,行为退化为「仅心跳冻结+挂单」语义)。 +func NewDeviceWatcher(pdb store.PersonalStore, pool *account.AccountPool, st store.Store, svc *order.Service, cfg *DeviceWatcherConfig, recoverer Recoverer, adb AdbProber) *DeviceWatcher { + if cfg == nil { + cfg = DefaultDeviceWatcherConfig() + } + if cfg.HeartbeatTimeout <= 0 { + cfg.HeartbeatTimeout = 120 * time.Second + } + if cfg.ScanInterval <= 0 { + cfg.ScanInterval = 30 * time.Second + } + if cfg.MissThreshold <= 0 { + cfg.MissThreshold = 2 + } + if cfg.ControlPlaneInterval <= 0 { + cfg.ControlPlaneInterval = 30 * time.Second + } + return &DeviceWatcher{ + pdb: pdb, pool: pool, st: st, svc: svc, cfg: cfg, recoverer: recoverer, + adb: adb, + missCount: make(map[int64]int), + controlPlaneLast: make(map[string]time.Time), + } +} + +// AsAdbProber 把 *adbmgr.Manager 适配为 AdbProber 接口(避免在 reconciler 包直接 import adbmgr 类型)。 +func AsAdbProber(m *adbmgr.Manager) AdbProber { + if m == nil { + return nil + } + return adbProberAdapter{m} +} + +type adbProberAdapter struct{ m *adbmgr.Manager } + +func (a adbProberAdapter) Online(ctx context.Context, serial string) bool { + return a.m.Online(ctx, serial) +} +func (a adbProberAdapter) Connect(ctx context.Context, serial string) error { + return a.m.Connect(ctx, serial) +} + +// Start 启动(阻塞,放 goroutine) +func (w *DeviceWatcher) Start(ctx context.Context) { + logger.Info("reconciler", "监听端离线检测已启动", + "heartbeat_timeout", w.cfg.HeartbeatTimeout.String(), + "scan_interval", w.cfg.ScanInterval.String()) + ticker := time.NewTicker(w.cfg.ScanInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + logger.Info("reconciler", "监听端离线检测停止") + return + case <-ticker.C: + w.scan(ctx) + // 控制面状态机:adb prober 配置时才跑 + if w.adb != nil { + w.scanControlPlane(ctx) + } + } + } +} + +// scan 单次扫描:以 adb 可达性为准维护设备在线状态(2026-08-29 重构:脱离监听 App 依赖): +// - adb 可达(USB/WiFi/LAN,含自动 adb connect 重连)→ 视为在线; +// 若此前离线(offline)则恢复 online + 解冻离线联动冻结的账户 +// - adb 不可达 → 回退心跳超时判定,超时判离线并冻结账户 +func (w *DeviceWatcher) scan(ctx context.Context) { + devs, err := w.pdb.ListDevices(ctx) + if err != nil { + logger.Error("reconciler", "设备列表查询失败", "err", err) + return + } + for _, d := range devs { + // 手动冻结/风控冻结的设备:不自动处理(仅人工解冻) + if d.Status == string(personal.DeviceFrozen) { + delete(w.missCount, d.ID) + continue + } + // adb 可达优先:设备 serial 可连(自动 adb connect)→ 视为在线 + if w.adb != nil { + if serial := adbSerialOf(d); serial != "" && w.adb.Online(ctx, serial) { + if d.Status != string(personal.DeviceOnline) { + w.recoverOnline(ctx, d) // 离线→在线:置 online + 解冻账户 + } + delete(w.missCount, d.ID) + continue + } + } + // adb 不可达(且 status=online):连续 miss 达阈值判离线 + 冻结联动。 + // 2026-08-30 重构:监听端 APP 弃用,LastHeartbeat 不再更新——此分支语义从 + // "心跳超时"收敛为"adb 不可达确认"(纯 adb 控制下 adb 是唯一存活信号), + // 保留 missCount 防抖避免瞬时 adb 抖动误判离线。 + if d.Status != string(personal.DeviceOnline) { + delete(w.missCount, d.ID) + continue + } + w.countMiss(d.ID) + if w.missCount[d.ID] >= w.cfg.MissThreshold { + w.handleOffline(ctx, d, "adb 不可达(连续 "+strconv.Itoa(w.cfg.MissThreshold)+" 次扫描确认)") + delete(w.missCount, d.ID) + } + } +} + +// recoverOnline 设备 adb 恢复可达 → 置 online + 解冻离线联动冻结的绑定账户。 +// 仅解冻 FrozenReason=offline 的账户;风控/手动冻结的账户保持 frozen(资金安全)。 +func (w *DeviceWatcher) recoverOnline(ctx context.Context, d *personal.DeviceEncrypted) { + logger.Info("reconciler", "设备 adb 恢复,置 online + 解冻离线联动账户", "device_id", d.DeviceID, "name", d.Name) + if err := w.pdb.SetDeviceStatus(ctx, d.ID, string(personal.DeviceOnline)); err != nil { + logger.Error("reconciler", "设备置在线失败", "err", err, "device_id", d.DeviceID) + } + for _, acctID := range store.DeviceAccountIDs(ctx, w.pdb, d.ID, d.AccountID) { + if acct := w.pool.GetByID(acctID); acct != nil && acct.Status == account.StatusFrozen { + w.pool.UnfreezeByOffline(acctID) + if updated := w.pool.GetByID(acctID); updated != nil && updated.Status == account.StatusNormal { + logger.Info("reconciler", "账户已自动解冻(设备 adb 恢复)", "account_id", acctID, "name", acct.Name) + } + } + } +} + +// adbSerialOf 解析设备 adb serial(TCP host:port 或 USB 纯 serial;与 cmd/gateway deviceADBSerial 同源)。 +// 不可达/未配置返回空串(回退心跳判定)。 +func adbSerialOf(d *personal.DeviceEncrypted) string { + if d.AdbHost != "" { + if d.AdbPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbHost, d.AdbPort) + } + return d.AdbHost + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return fmt.Sprintf("%s:%d", d.AdbWifiHost, d.AdbWifiPort) + } + return "" +} + +// countMiss 累加设备连续超时计数(防止 map 误写) +func (w *DeviceWatcher) countMiss(id int64) { + w.missCount[id]++ +} + +// handleOffline 设备离线联动:置 OFFLINE → 冻结账户 → WAIT_PAY 订单进异常池 +func (w *DeviceWatcher) handleOffline(ctx context.Context, d *personal.DeviceEncrypted, reason string) { + logger.Warn("reconciler", "监听端离线", "device_id", d.DeviceID, "name", d.Name, + "account_id", d.AccountID, "provider", d.Provider, "reason", reason) + + // 1. 设备置 OFFLINE + if err := w.pdb.SetDeviceStatus(ctx, d.ID, string(personal.DeviceOffline)); err != nil { + logger.Error("reconciler", "设备置离线失败", "err", err, "device_id", d.DeviceID) + } + + // 1.5 触发后台自动重启尝试(不阻塞主流程;限频由 recoverer 实现侧负责) + if w.recoverer != nil { + go func(dev *personal.DeviceEncrypted) { + rctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + if err := w.recoverer.AttemptRestart(rctx, dev.ID); err != nil { + logger.Warn("reconciler", "设备自动重启尝试失败", "device_id", dev.DeviceID, "err", err) + } + }(d) + } + + // 2. 冻结全部绑定账户(主账户 + 附加账户,不可接新单;设备 adb 恢复后自动解冻) + for _, acctID := range store.DeviceAccountIDs(ctx, w.pdb, d.ID, d.AccountID) { + acct := w.pool.GetByID(acctID) + if acct != nil && acct.Status == account.StatusNormal { + w.pool.FreezeByOffline(acctID) + logger.Warn("reconciler", "账户已冻结(监听端离线联动)", + "account_id", acctID, "name", acct.Name) + } + // 3. 该账户下 WAIT_PAY 订单进人工异常池(用户可能已付款但未监听到) + w.enqueueWaitPayOrders(ctx, d, acctID) + } +} + +// enqueueWaitPayOrders 将指定账户下待支付订单挂起进异常池(幂等) +func (w *DeviceWatcher) enqueueWaitPayOrders(ctx context.Context, d *personal.DeviceEncrypted, accountID int64) { + orders, err := w.st.ListWaitPayByAccount(ctx, accountID) + if err != nil { + logger.Error("reconciler", "查询待支付订单失败", "err", err, "account_id", accountID) + return + } + for _, o := range orders { + // 已进池的跳过(幂等) + if existing, _ := w.st.GetExceptionByOutTradeNo(ctx, o.OutTradeNo); existing != nil { + continue + } + ev := w.evidence(d, o) + w.svc.EnqueueException(ctx, o, model.ExReasonListenOffline, "LISTEN_OFFLINE", "", + ev, "监听端离线("+d.DeviceID+"),无法确认用户是否已付款,需人工核实") + } +} + +// evidence 生成离线证据时间线片段 +func (w *DeviceWatcher) evidence(d *personal.DeviceEncrypted, o *model.Order) string { + m := map[string]any{ + "action": "listen_offline", + "time": time.Now().Format(time.RFC3339), + "device_id": d.DeviceID, + "device_name": d.Name, + "provider": d.Provider, + "order": o.OutTradeNo, + "amount": o.AmountMinor, + "channel": o.Channel, + } + b, _ := json.Marshal(m) + return string(b) +} + +// scanControlPlane 控制面状态机扫描(docs/P2P-ADB-RECOVERY.md §6.3): +// USB / 局域网 adb 可达 → 'usb';都不可用 → 'tunnel_only'(adbd 监听依赖系统无线调试)。 +// +// 触发时机:每 ControlPlaneInterval 跑一次(与心跳超时解耦;Phase 1 跟随 ScanInterval 共跑)。 +func (w *DeviceWatcher) scanControlPlane(ctx context.Context) { + devs, err := w.pdb.ListDevices(ctx) + if err != nil { + logger.Error("reconciler", "控制面扫描设备列表失败", "err", err) + return + } + now := time.Now() + for _, d := range devs { + // 单设备限频:避免每轮都连同一条 adb 通道 + if last, ok := w.controlPlaneLast[d.DeviceID]; ok && now.Sub(last) < w.cfg.ControlPlaneInterval { + continue + } + // 手动冻结的设备不处理控制面(evaluateControlPlane 内部仍会按 adb 可达性评估状态) + if d.Status == string(personal.DeviceFrozen) { + continue + } + w.controlPlaneLast[d.DeviceID] = now + w.evaluateControlPlane(ctx, d) + } +} + +// evaluateControlPlane 单设备控制面状态计算(docs/P2P-ADB-RECOVERY.md §6.3): +// 1. USB / 局域网 adb serial 在线 → 'usb'(最优先,本机通道延迟最低) +// 2. 不可达 → 'tunnel_only'(adbd 监听依赖系统无线调试) +// +// 监听 App 已弃用(2026-08-29):relay 反向隧道分支移除,adb 可达性是唯一判定依据。 +func (w *DeviceWatcher) evaluateControlPlane(ctx context.Context, d *personal.DeviceEncrypted) { + // 1) USB / 局域网 adb 可达 → usb + if w.adb != nil { + if usbSerial := w.usbSerial(d); usbSerial != "" && w.adb.Online(ctx, usbSerial) { + w.writeControlState(ctx, d, personal.ControlStateUSB) + return + } + } + // 2) 都不可用 → tunnel_only(adbd 监听依赖 Android 11+ 系统无线调试,App 侧无需也不应再下发复活指令) + w.writeControlState(ctx, d, personal.ControlStateTunnelOnly) +} + +// usbSerial 取设备首选 USB/WiFi 通道 serial(与 admin.personalHandler.deviceSerial 同语义) +func (w *DeviceWatcher) usbSerial(d *personal.DeviceEncrypted) string { + if d.AdbHost != "" { + if d.AdbPort > 0 { + return d.AdbHost + ":" + strconv.Itoa(d.AdbPort) + } + return d.AdbHost + } + if d.AdbWifiHost != "" && d.AdbWifiPort > 0 { + return d.AdbWifiHost + ":" + strconv.Itoa(d.AdbWifiPort) + } + return "" +} + +// writeControlState 持久化控制面状态(状态未变不写库,避免噪声) +func (w *DeviceWatcher) writeControlState(ctx context.Context, d *personal.DeviceEncrypted, state string) { + if d.ControlState == state { + return // 状态未变 → 不写库,避免噪声 + } + adbListen := d.AdbListen != 0 + // ts_ip 字段保留 DB 列(Phase 1 迁移产物,监听端暂时兼容可能携带该字段), + // 但心跳路径不再写它 —— 用设备最新 DB 值兜底即可。 + if err := w.pdb.SetDeviceControlPlane(ctx, d.DeviceID, d.TsIP, adbListen, state); err != nil { + logger.Warn("reconciler", "控制面状态写库失败", "err", err, "device_id", d.DeviceID, "state", state) + return + } + d.ControlState = state + logger.Info("reconciler", "控制面状态切换", "device_id", d.DeviceID, "name", d.Name, "state", state) +} diff --git a/internal/reconciler/devicewatcher_test.go b/internal/reconciler/devicewatcher_test.go new file mode 100755 index 0000000..cb5ae4e --- /dev/null +++ b/internal/reconciler/devicewatcher_test.go @@ -0,0 +1,265 @@ +package reconciler + +import ( + "context" + "path/filepath" + "testing" + "time" + + "paygateway/internal/account" + "paygateway/internal/ledger" + "paygateway/internal/model" + "paygateway/internal/order" + "paygateway/internal/personal" + "paygateway/internal/store" + "paygateway/internal/upstream" +) + +// newTestEnv 构建测试环境:临时业务库 + personal.db + 账户池 + 订单服务 +func newTestEnv(t *testing.T) (st store.Store, pdb *store.PersonalDB, pool *account.AccountPool, svc *order.Service, dir string) { + t.Helper() + dir = t.TempDir() + + st, err := store.NewSQLite(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("创建业务库失败: %v", err) + } + t.Cleanup(func() { st.Close() }) + + ldb, err := store.NewLedgerDB(filepath.Join(dir, "ledger.db")) + if err != nil { + t.Fatalf("创建账本库失败: %v", err) + } + t.Cleanup(func() { ldb.Close() }) + + pdb, err = store.NewPersonalDB(filepath.Join(dir, "personal.db")) + if err != nil { + t.Fatalf("创建个人库失败: %v", err) + } + t.Cleanup(func() { pdb.Close() }) + + pool = account.NewPool() + lg, err := ledger.New(ldb, "") + if err != nil { + t.Fatalf("创建账本失败: %v", err) + } + svc = order.New(st, lg, pool, upstream.NewPool()) + + // 账户池加一个个人免签账户 + acct := &account.Account{ + ID: 1, Channel: "personal.alipay", Name: "测试账户", Status: account.StatusNormal, + QRContent: "https://qr.alipay.com/test123", DailyLimit: 100000, + } + pool.Add(acct) + return st, pdb, pool, svc, dir +} + +// TestDeviceWatcherOffline 核心场景:adb 不可达 → 设备 OFFLINE → 账户冻结 → 订单进异常池 +func TestDeviceWatcherOffline(t *testing.T) { + ctx := context.Background() + st, pdb, pool, svc, _ := newTestEnv(t) + + // 1. 创建设备(绑定账户1,status=online,但无 adb serial → adb 不可达) + devID, err := pdb.CreateDevice(ctx, &personal.DeviceEncrypted{ + DeviceID: "dev-offline-1", Name: "离线测试机", AccountID: 1, + Provider: "alipay", SecretEnc: "xx", Status: string(personal.DeviceOnline), + }) + if err != nil { + t.Fatalf("创建设备失败: %v", err) + } + + // 2. 创建 WAIT_PAY 订单(account_id=1) + o := &model.Order{ + OutTradeNo: "T-ORDER-001", GatewayNo: "G-001", Channel: "personal.alipay", + AccountID: 1, AmountMinor: 100, Status: model.StatusWaitPay, + CreatedAt: time.Now(), + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + + // 3. 运行离线扫描(adb 全程不可达:连续两次扫描确认判离线) + w := NewDeviceWatcher(pdb, pool, st, svc, &DeviceWatcherConfig{ + ScanInterval: time.Second, + }, nil, mockAdbProber{online: map[string]bool{}}) + w.scan(ctx) + + // 3.5 单次不可达不应立即判离线(双保险防抖动) + dev0, _ := pdb.GetDevice(ctx, "dev-offline-1") + if dev0 == nil || dev0.Status != string(personal.DeviceOnline) { + t.Fatalf("单次不可达不应置 OFFLINE(需连续确认),实际 status=%v", dev0.Status) + } + if acct0 := pool.GetByID(1); acct0.Status != account.StatusNormal { + t.Fatalf("单次不可达不应冻结账户,实际 status=%v", acct0.Status) + } + + // 3.6 连续第二次扫描 → 判离线 + w.scan(ctx) + + // 4. 断言:设备 OFFLINE + dev, _ := pdb.GetDevice(ctx, "dev-offline-1") + if dev == nil || dev.Status != string(personal.DeviceOffline) { + t.Fatalf("设备应置 OFFLINE,实际 status=%v", dev.Status) + } + + // 5. 断言:账户冻结 + acct := pool.GetByID(1) + if acct.Status != account.StatusFrozen { + t.Fatalf("账户应冻结,实际 status=%v", acct.Status) + } + + // 6. 断言:订单进异常池(reason=listen_offline) + exc, err := st.GetExceptionByOutTradeNo(ctx, "T-ORDER-001") + if err != nil || exc == nil { + t.Fatalf("订单应进异常池,err=%v exc=%v", err, exc) + } + if exc.Reason != model.ExReasonListenOffline { + t.Fatalf("异常原因应为 listen_offline,实际 %s", exc.Reason) + } + + // 7. 幂等:再次扫描不重复进池 + w.scan(ctx) + exc2, _ := st.GetExceptionByOutTradeNo(ctx, "T-ORDER-001") + if exc2.ID != exc.ID { + t.Fatal("重复扫描不应创建重复异常单") + } + + _ = devID +} + +// TestDeviceWatcherOnline adb 可达 → 不触发任何联动 +func TestDeviceWatcherOnline(t *testing.T) { + ctx := context.Background() + st, pdb, pool, svc, _ := newTestEnv(t) + + // 设备配了 WiFi adb 且可达 → 在线 + if _, err := pdb.CreateDevice(ctx, &personal.DeviceEncrypted{ + DeviceID: "dev-online-1", Name: "在线测试机", AccountID: 1, + Provider: "alipay", SecretEnc: "xx", Status: string(personal.DeviceOnline), + AdbWifiHost: "192.168.1.50", AdbWifiPort: 5555, + }); err != nil { + t.Fatalf("创建设备失败: %v", err) + } + o := &model.Order{ + OutTradeNo: "T-ORDER-002", GatewayNo: "G-002", Channel: "personal.alipay", + AccountID: 1, AmountMinor: 200, Status: model.StatusWaitPay, + CreatedAt: time.Now(), + } + if err := st.CreateOrder(ctx, o); err != nil { + t.Fatalf("创建订单失败: %v", err) + } + + w := NewDeviceWatcher(pdb, pool, st, svc, nil, nil, mockAdbProber{ + online: map[string]bool{"192.168.1.50:5555": true}, + }) + w.scan(ctx) + + // 设备仍在线、账户仍 normal、订单未进池 + dev, _ := pdb.GetDevice(ctx, "dev-online-1") + if dev.Status != string(personal.DeviceOnline) { + t.Fatalf("设备应保持在线,实际 %s", dev.Status) + } + if acct := pool.GetByID(1); acct.Status != account.StatusNormal { + t.Fatalf("账户应保持 normal,实际 %s", acct.Status) + } + if exc, _ := st.GetExceptionByOutTradeNo(ctx, "T-ORDER-002"); exc != nil { + t.Fatal("在线设备订单不应进异常池") + } +} + +// TestDeviceWatcherFrozen 手动冻结设备不被自动置离线 +func TestDeviceWatcherFrozen(t *testing.T) { + ctx := context.Background() + st, pdb, pool, svc, _ := newTestEnv(t) + + // 冻结设备(即使 adb 不可达也不重复处理) + if _, err := pdb.CreateDevice(ctx, &personal.DeviceEncrypted{ + DeviceID: "dev-frozen-1", Name: "冻结机", AccountID: 1, + Provider: "alipay", SecretEnc: "xx", Status: string(personal.DeviceFrozen), + }); err != nil { + t.Fatalf("创建设备失败: %v", err) + } + + w := NewDeviceWatcher(pdb, pool, st, svc, nil, nil, mockAdbProber{online: map[string]bool{}}) + w.scan(ctx) + + dev, _ := pdb.GetDevice(ctx, "dev-frozen-1") + if dev.Status != string(personal.DeviceFrozen) { + t.Fatalf("冻结设备不应被改变,实际 %s", dev.Status) + } + if acct := pool.GetByID(1); acct.Status != account.StatusNormal { + t.Fatal("冻结设备不应触发账户冻结(账户冻结由设备离线联动触发,冻结设备不在线)") + } +} + +// mockAdbProber 测试用 AdbProber:serial → 可达性 +type mockAdbProber struct { + online map[string]bool +} + +func (m mockAdbProber) Online(_ context.Context, serial string) bool { return m.online[serial] } +func (mockAdbProber) Connect(context.Context, string) error { return nil } + +// TestDeviceWatcherAdbRecover 设备 offline + adb 恢复 → 自动置 online + 解冻离线联动账户 +func TestDeviceWatcherAdbRecover(t *testing.T) { + ctx := context.Background() + st, pdb, pool, svc, _ := newTestEnv(t) + + // 设备 offline(此前离线联动冻结了账户),配了 WiFi adb + if _, err := pdb.CreateDevice(ctx, &personal.DeviceEncrypted{ + DeviceID: "dev-recover-1", Name: "恢复测试机", AccountID: 1, + Provider: "alipay", SecretEnc: "xx", Status: string(personal.DeviceOffline), + AdbWifiHost: "192.168.1.50", AdbWifiPort: 5555, + }); err != nil { + t.Fatalf("创建设备失败: %v", err) + } + // 账户处于离线联动冻结态(模拟 handleOffline 后的状态) + pool.FreezeByOffline(1) + + w := NewDeviceWatcher(pdb, pool, st, svc, nil, nil, mockAdbProber{ + online: map[string]bool{"192.168.1.50:5555": true}, + }) + w.scan(ctx) + + dev, _ := pdb.GetDevice(ctx, "dev-recover-1") + if dev.Status != string(personal.DeviceOnline) { + t.Fatalf("adb 恢复后设备应置 online,实际 %s", dev.Status) + } + if acct := pool.GetByID(1); acct.Status != account.StatusNormal { + t.Fatalf("离线联动冻结的账户应在设备恢复后自动解冻,实际 %s", acct.Status) + } +} + +// TestDeviceWatcherManualFrozenNotAutoUnfreeze 手动冻结的账户绝不被设备 adb 恢复自动解冻(资金安全底线) +func TestDeviceWatcherManualFrozenNotAutoUnfreeze(t *testing.T) { + ctx := context.Background() + st, pdb, pool, svc, _ := newTestEnv(t) + + // 设备 offline + adb 可达(会触发 recoverOnline) + if _, err := pdb.CreateDevice(ctx, &personal.DeviceEncrypted{ + DeviceID: "dev-manual-1", Name: "手动冻结测试机", AccountID: 1, + Provider: "alipay", SecretEnc: "xx", Status: string(personal.DeviceOffline), + AdbHost: "4f2bdc30", + }); err != nil { + t.Fatalf("创建设备失败: %v", err) + } + // 账户是「手动冻结」(不是离线联动)——模拟管理员 handleFreezeAccount 后的状态 + pool.FreezeManual(1) + + w := NewDeviceWatcher(pdb, pool, st, svc, nil, nil, mockAdbProber{ + online: map[string]bool{"4f2bdc30": true}, + }) + w.scan(ctx) + + // 设备可恢复 online,但手动冻结的账户必须保持 frozen + dev, _ := pdb.GetDevice(ctx, "dev-manual-1") + if dev.Status != string(personal.DeviceOnline) { + t.Fatalf("设备应恢复 online,实际 %s", dev.Status) + } + if acct := pool.GetByID(1); acct.Status != account.StatusFrozen { + t.Fatalf("手动冻结的账户绝不能被自动解冻,实际 %s", acct.Status) + } + if pool.IsOfflineFrozen(1) { + t.Fatal("手动冻结账户不应标记为 offlineFrozen") + } +} diff --git a/internal/reconciler/scheduler.go b/internal/reconciler/scheduler.go new file mode 100755 index 0000000..0db7c50 --- /dev/null +++ b/internal/reconciler/scheduler.go @@ -0,0 +1,441 @@ +// Package reconciler 容灾对账中心: +// ConfirmScheduler(订单确认调度器,主动查单兜底)+ NotifyReaper(dead 通知管理) +// +// 设计要点: +// - 事件驱动 + timer heap(min-heap),不轮询全表;只有 TTL 到期的订单才被处理 +// - 回调是快路径(立即入账),查单是慢路径(超时未回调 → 主动查单补账) +// - 关单前必查单:查单失败宁可推迟关单,绝不误关可能已支付的订单 +// - 自动兜底失败(查单连续失败/金额不一致/状态矛盾)→ 进人工异常池,等人工审查 +package reconciler + +import ( + "container/heap" + "context" + "encoding/json" + "fmt" + "strings" + "sync" + "time" + + "paygateway/internal/channel" + "paygateway/internal/logger" + "paygateway/internal/model" + "paygateway/internal/order" + "paygateway/internal/store" +) + +// Config 确认调度器参数(可配置) +type Config struct { + ConfirmWait time.Duration // 下单后正常回调等待窗口(默认 60s) + QueryIntervalMin time.Duration // 查单最小间隔(默认 30s) + QueryIntervalMax time.Duration // 查单最大间隔(默认 5m) + QueryBackoff time.Duration // 查单失败退避基础(默认 1m) + CloseAfter time.Duration // 关单时间(默认 30m) + QueryFailAlert int // 查单连续失败进异常池阈值(默认 5) + TickInterval time.Duration // 调度 tick(默认 1s) + BatchSize int // 每 tick 最多处理订单数(默认 50) + Concurrency int // 查单并发上限(默认 10) +} + +// DefaultConfig 默认参数 +func DefaultConfig() *Config { + return &Config{ + ConfirmWait: 60 * time.Second, + QueryIntervalMin: 30 * time.Second, + QueryIntervalMax: 5 * time.Minute, + QueryBackoff: time.Minute, + CloseAfter: 30 * time.Minute, + QueryFailAlert: 5, + TickInterval: time.Second, + BatchSize: 50, + Concurrency: 10, + } +} + +// QueryFunc 通道查单(main 注入:遍历通道注册表) +type QueryFunc func(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) + +// PersonalCheckFunc 个人通道订单专用检查(main 注入,nil = 不检查) +// 返回 (offline, reason):offline=true 表示监听端离线/异常,订单进异常池人工核实 +// 用于个人通道关单前的安全确认:监听端掉线时不能关单(用户可能已付款但未监听到) +type PersonalCheckFunc func(ctx context.Context, o *model.Order) (offline bool, reason string) + +// entry 堆元素 +type entry struct { + outTradeNo string + nextCheckAt time.Time + index int +} + +type confirmHeap []*entry + +func (h confirmHeap) Len() int { return len(h) } +func (h confirmHeap) Less(i, j int) bool { return h[i].nextCheckAt.Before(h[j].nextCheckAt) } +func (h confirmHeap) Swap(i, j int) { + h[i], h[j] = h[j], h[i] + h[i].index = i + h[j].index = j +} +func (h *confirmHeap) Push(x any) { + e := x.(*entry) + e.index = len(*h) + *h = append(*h, e) +} +func (h *confirmHeap) Pop() any { + old := *h + n := len(old) + e := old[n-1] + old[n-1] = nil + e.index = -1 + *h = old[:n-1] + return e +} + +// Scheduler 订单确认调度器 +type Scheduler struct { + store store.Store + svc *order.Service + query QueryFunc + cfg *Config + + personalCheck PersonalCheckFunc // 个人通道监听端检查(nil = 不检查,关单前直接关) + + mu sync.Mutex + heap confirmHeap + idx map[string]*entry + checkCount map[string]int // 每单确认次数(退避/进池判定) +} + +// NewScheduler 创建确认调度器 +func NewScheduler(st store.Store, svc *order.Service, q QueryFunc, cfg *Config) *Scheduler { + if cfg == nil { + cfg = DefaultConfig() + } + return &Scheduler{ + store: st, + svc: svc, + query: q, + cfg: cfg, + heap: confirmHeap{}, + idx: make(map[string]*entry), + checkCount: make(map[string]int), + } +} + +// Query 通道查单(管理后台手动查单/人工复核用) +func (s *Scheduler) Query(ctx context.Context, outTradeNo string) (*channel.OrderStatus, error) { + if s.query == nil { + return nil, fmt.Errorf("查单函数未注入") + } + return s.query(ctx, outTradeNo) +} + +// SetPersonalCheck 注入个人通道监听端检查(ConfirmByListen/ConfirmByManual 通道关单前调用) +func (s *Scheduler) SetPersonalCheck(fn PersonalCheckFunc) { + s.personalCheck = fn +} + +// abilityFor 按订单通道名获取确认能力(未知通道默认按可查单处理) +func abilityFor(chName string) channel.ConfirmAbility { + ch, ok := channel.Lookup(chName) + if !ok { + return channel.ConfirmByQuery + } + return ch.ConfirmAbility() +} + +// OnOrderCreated 订单创建事件:进入待确认(回调窗口后检查) +func (s *Scheduler) OnOrderCreated(o *model.Order) { + s.push(o.OutTradeNo, time.Now().Add(s.cfg.ConfirmWait)) +} + +// OnSettled 订单已入账:移出待确认 +func (s *Scheduler) OnSettled(outTradeNo string) { s.remove(outTradeNo) } + +// OnClosed 订单已关闭:移出待确认 +func (s *Scheduler) OnClosed(outTradeNo string) { s.remove(outTradeNo) } + +// Start 启动(阻塞,放 goroutine):恢复堆 + 周期 tick +func (s *Scheduler) Start(ctx context.Context) { + s.restore(ctx) + logger.Info("reconciler", "确认调度器启动", "interval", s.cfg.TickInterval.String(), + "confirm_wait", s.cfg.ConfirmWait.String(), "close_after", s.cfg.CloseAfter.String()) + ticker := time.NewTicker(s.cfg.TickInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + logger.Info("reconciler", "确认调度器停止") + return + case <-ticker.C: + s.tick(ctx) + } + } +} + +// restore 启动恢复:加载所有未完成订单重建堆(动作幂等,重复执行无副作用) +func (s *Scheduler) restore(ctx context.Context) { + orders, err := s.store.ListExpiredOrders(ctx, time.Now()) + if err != nil { + logger.Error("reconciler", "恢复待确认订单失败", "err", err) + return + } + now := time.Now() + for _, o := range orders { + next := o.CreatedAt.Add(s.cfg.ConfirmWait) + if next.Before(now) { + next = now // 已过窗口,立即检查 + } + s.push(o.OutTradeNo, next) + } + logger.Info("reconciler", "确认调度器恢复完成", "pending", len(orders)) +} + +// tick 处理一批到期订单 +func (s *Scheduler) tick(ctx context.Context) { + due := s.popDue(time.Now(), s.cfg.BatchSize) + if len(due) == 0 { + return + } + sem := make(chan struct{}, s.cfg.Concurrency) + var wg sync.WaitGroup + for _, no := range due { + wg.Add(1) + sem <- struct{}{} + go func(outTradeNo string) { + defer wg.Done() + defer func() { <-sem }() + s.confirm(ctx, outTradeNo) + }(no) + } + wg.Wait() +} + +// confirm 单个订单的确认流程(查单补账 / 关单 / 进异常池) +func (s *Scheduler) confirm(ctx context.Context, outTradeNo string) { + o, err := s.store.GetOrder(ctx, outTradeNo) + if err != nil || o == nil { + s.remove(outTradeNo) + return + } + // 终态直接出堆(回调可能恰好先到) + if o.Status == model.StatusSuccess || o.Status == model.StatusClosed { + s.remove(outTradeNo) + return + } + if o.Status != model.StatusWaitPay { + s.remove(outTradeNo) + return + } + + n := s.incCheck(outTradeNo) + + // 按通道确认能力分叉:个人通道(无法主动查单)走监听确认路径 + if abilityFor(o.Channel) != channel.ConfirmByQuery { + s.confirmPersonal(ctx, o, n) + return + } + + // 已到关单时间:关单前最后查一次(绝不误关已支付订单) + if time.Since(o.CreatedAt) >= s.cfg.CloseAfter { + s.confirmBeforeClose(ctx, o, n) + return + } + + // 未到关单时间:主动查单 + st, qerr := s.query(ctx, outTradeNo) + if qerr != nil || st == nil { + if n >= s.cfg.QueryFailAlert { + s.svc.EnqueueException(ctx, o, model.ExReasonQueryFailed, "QUERY_FAILED", "", + s.evidence("query_failed", qerr), "查单连续失败,需人工核对实际支付情况") + s.remove(outTradeNo) + return + } + s.repush(outTradeNo, s.backoff(n)) + return + } + + switch st.Status { + case model.StatusSuccess: + s.settle(ctx, o, st) + case model.StatusClosed: + if ok, _ := s.svc.CloseOrder(ctx, outTradeNo, "query_closed"); ok { + s.remove(outTradeNo) + } else { + s.repush(outTradeNo, 30*time.Second) + } + default: // 未支付:自适应间隔复查 + s.repush(outTradeNo, s.queryInterval(n)) + } +} + +// confirmBeforeClose 关单前查单(查单失败 → 进异常池,绝不关单) +func (s *Scheduler) confirmBeforeClose(ctx context.Context, o *model.Order, n int) { + st, qerr := s.query(ctx, o.OutTradeNo) + if qerr != nil || st == nil { + s.svc.EnqueueException(ctx, o, model.ExReasonCloseUncertain, "QUERY_FAILED", "", + s.evidence("close_query_failed", qerr), "关单前查单失败,需人工确认是否已支付") + s.remove(o.OutTradeNo) + return + } + switch st.Status { + case model.StatusSuccess: + s.settle(ctx, o, st) + case model.StatusClosed: + if ok, _ := s.svc.CloseOrder(ctx, o.OutTradeNo, "query_before_close"); ok { + s.remove(o.OutTradeNo) + } + default: + if ok, _ := s.svc.CloseOrder(ctx, o.OutTradeNo, "query_before_close_not_paid"); ok { + s.remove(o.OutTradeNo) + } + } +} + +// confirmPersonal 个人通道订单确认(无法主动查单,靠监听端推送入账) +// +// 分叉逻辑: +// - 未到关单时间:不查单(个人码无查询 API),自适应间隔复查,等监听端推送 +// - 已到关单时间: +// - 若注入 personalCheck 且监听端离线 → 不能关单(用户可能已付款但未监听到)→ 进异常池人工核实 +// - 监听端在线但无推送 → 用户大概率未付款 → 关单 +// - 复查次数超阈值仍无结果 → 进异常池(人工确认实际收款情况) +func (s *Scheduler) confirmPersonal(ctx context.Context, o *model.Order, n int) { + // 未到关单时间:等监听端推送(不查单),自适应间隔复查 + if time.Since(o.CreatedAt) < s.cfg.CloseAfter { + s.repush(o.OutTradeNo, s.queryInterval(n)) + return + } + + // 已到关单时间 + // 监听端离线检查:离线时订单进异常池(避免误关已付款订单),绝不直接关单 + if s.personalCheck != nil { + offline, reason := s.personalCheck(ctx, o) + if offline { + s.svc.EnqueueException(ctx, o, model.ExReasonListenOffline, "LISTEN_OFFLINE", "", + s.evidence("listen_offline", nil), "监听端掉线,无法确认用户是否已付款,需人工核实: "+reason) + s.remove(o.OutTradeNo) + return + } + } + + // 复查次数过多(监听端在线但长时间无推送):存疑,进异常池人工确认 + if n >= s.cfg.QueryFailAlert { + s.svc.EnqueueException(ctx, o, model.ExReasonListenTimeout, "LISTEN_TIMEOUT", "", + s.evidence("listen_timeout", nil), "监听端在线但长时间未收到收款推送,需人工确认实际收款情况") + s.remove(o.OutTradeNo) + return + } + + // 监听端在线 + 无推送 + 未超复查阈值:关单(用户大概率未付款) + if ok, _ := s.svc.CloseOrder(ctx, o.OutTradeNo, "personal_timeout_close"); ok { + s.remove(o.OutTradeNo) + } else { + s.repush(o.OutTradeNo, 30*time.Second) + } +} + +// settle 查单补账(金额不一致自动进异常池) +func (s *Scheduler) settle(ctx context.Context, o *model.Order, st *channel.OrderStatus) { + ok, err := s.svc.SettleByQuery(ctx, o.OutTradeNo, st.ChannelTxID, st.AmountMinor, + s.evidence("query_success", nil)) + if err != nil { + // 金额不一致已自动进池;其他错误重试 + if !strings.Contains(err.Error(), "金额") { + s.repush(o.OutTradeNo, s.cfg.QueryBackoff) + } else { + s.remove(o.OutTradeNo) + } + return + } + if ok { + logger.Info("reconciler", "查单补账成功", "out_trade_no", o.OutTradeNo, "tx_id", st.ChannelTxID) + s.remove(o.OutTradeNo) + } else { + s.repush(o.OutTradeNo, 30*time.Second) + } +} + +// ========== heap 操作 ========== + +func (s *Scheduler) push(outTradeNo string, next time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + if e, ok := s.idx[outTradeNo]; ok { + e.nextCheckAt = next + heap.Fix(&s.heap, e.index) + return + } + e := &entry{outTradeNo: outTradeNo, nextCheckAt: next} + s.idx[outTradeNo] = e + heap.Push(&s.heap, e) +} + +func (s *Scheduler) remove(outTradeNo string) { + s.mu.Lock() + defer s.mu.Unlock() + if e, ok := s.idx[outTradeNo]; ok { + heap.Remove(&s.heap, e.index) + delete(s.idx, outTradeNo) + } + delete(s.checkCount, outTradeNo) +} + +func (s *Scheduler) repush(outTradeNo string, delay time.Duration) { + s.push(outTradeNo, time.Now().Add(delay)) +} + +// popDue 取出到期且数量受限的订单 +func (s *Scheduler) popDue(now time.Time, max int) []string { + s.mu.Lock() + defer s.mu.Unlock() + var out []string + for len(s.heap) > 0 && len(out) < max { + top := s.heap[0] + if top.nextCheckAt.After(now) { + break + } + heap.Pop(&s.heap) + delete(s.idx, top.outTradeNo) + out = append(out, top.outTradeNo) + } + return out +} + +func (s *Scheduler) incCheck(outTradeNo string) int { + s.mu.Lock() + defer s.mu.Unlock() + s.checkCount[outTradeNo]++ + return s.checkCount[outTradeNo] +} + +// backoff 查单失败退避:1m/5m/15m/1h → 上限 1h +func (s *Scheduler) backoff(n int) time.Duration { + steps := []time.Duration{time.Minute, 5 * time.Minute, 15 * time.Minute, time.Hour} + if n-1 < len(steps) { + return steps[n-1] + } + return time.Hour +} + +// queryInterval 未支付复查间隔:30s → 1m → 2m → 5m(自适应拉长) +func (s *Scheduler) queryInterval(n int) time.Duration { + iv := s.cfg.QueryIntervalMin + for i := 1; i < n && iv < s.cfg.QueryIntervalMax; i++ { + iv *= 2 + } + if iv > s.cfg.QueryIntervalMax { + iv = s.cfg.QueryIntervalMax + } + return iv +} + +// evidence 生成证据时间线片段(JSON,自动收集只读) +func (s *Scheduler) evidence(action string, err error) string { + m := map[string]any{"action": action, "time": time.Now().Format(time.RFC3339)} + if err != nil { + m["error"] = err.Error() + } + b, _ := json.Marshal(m) + return string(b) +} diff --git a/internal/scrcpy/bridge.go b/internal/scrcpy/bridge.go new file mode 100644 index 0000000..cffd599 --- /dev/null +++ b/internal/scrcpy/bridge.go @@ -0,0 +1,361 @@ +// WebSocket 桥接:把 scrcpy 实时流转发给浏览器,接收浏览器控制指令 +// +// 消息协议: +// +// 服务端 → 客户端(二进制):H.264 Annex-B 帧数据(SPS/PPS/IDR/P,逐帧转发) +// 服务端 → 客户端(文本 JSON):连接建立后先发一条 meta +// 客户端 → 服务端(文本 JSON): +// {"type":"touch","action":0|1|2,"x":N,"y":N} +// {"type":"key","action":0|1,"keycode":N} +// {"type":"text","text":"..."} +// {"type":"reset"} +// +// 多查看者:每个设备一个 scrcpy 会话 + 广播 hub,视频循环单读多播。 +// 会话生命周期独立于单个 WS 连接(用 context.Background()), +// 避免某个浏览器断开时误杀共享会话。 +package scrcpy + +import ( + "context" + "encoding/json" + "net/http" + "runtime/debug" + "sync" + "time" + + "github.com/gorilla/websocket" + + "paygateway/internal/logger" +) + +// upgrader WebSocket 升级器 +var upgrader = websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 128 * 1024, + CheckOrigin: func(r *http.Request) bool { return true }, +} + +// StreamServer 实时流服务(会话池 + WebSocket 入口) +type StreamServer struct { + mu sync.Mutex + sessions map[string]*deviceSession // key: adb serial + nextPort int +} + +// deviceSession 单个设备的会话:scrcpy 连接 + 视频广播 +type deviceSession struct { + owner *StreamServer + serial string + conn *Connection + // 会话创建时间(首帧延迟 = videoLoop 启动到第一帧) + startAt time.Time + // 视频广播 + subMu sync.Mutex + subs map[chan []byte]struct{} + stopSub chan struct{} + stopOnce sync.Once + // 最近一个 config 帧(SPS/PPS,新订阅者连接时先发它才能解码 IDR) + lastConfig []byte + // 分辨率(videoLoop 解析 session 头后更新) + width uint16 + height uint16 +} + +// NewStreamServer 创建流服务 +func NewStreamServer() *StreamServer { + return &StreamServer{ + sessions: make(map[string]*deviceSession), + nextPort: 27200, + } +} + +func (s *StreamServer) allocPort() int { + s.nextPort++ + return s.nextPort +} + +// getOrCreate 获取或创建设备会话(后台 ctx,独立于 WS 连接) +func (s *StreamServer) getOrCreate(serial string) (*deviceSession, error) { + s.mu.Lock() + defer s.mu.Unlock() + if ds, ok := s.sessions[serial]; ok { + return ds, nil + } + sess, err := StartSession(context.Background(), serial, s.allocPort()) + if err != nil { + return nil, err + } + ds := &deviceSession{ + owner: s, + serial: serial, + conn: sess.Connection(), + subs: make(map[chan []byte]struct{}), + stopSub: make(chan struct{}), + startAt: time.Now(), + } + s.sessions[serial] = ds + go ds.videoLoop() + return ds, nil +} + +// removeSession 在 videoLoop 退出时被同一会话主动调用来剔除自己 +func (s *StreamServer) removeSession(serial string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.sessions, serial) +} + +func (s *StreamServer) HasSession(serial string) bool { + s.mu.Lock() + defer s.mu.Unlock() + _, ok := s.sessions[serial] + return ok +} + +// videoLoop 单读 scrcpy 视频流,广播给所有订阅者 +// 设备断开/读错误时 break:主动从 sessions map 删除自己 + 通知订阅者 + 关会话 +// 不删除会导致 getOrCreate 拿到死会话(USB 拔了再插复现) +func (ds *deviceSession) videoLoop() { + defer ds.handleDead() + firstFrameAt := time.Time{} + for { + frame, err := ds.conn.ReadVideo() + if err != nil { + break + } + if firstFrameAt.IsZero() { + firstFrameAt = time.Now() + logger.Info("scrcpy", "首帧到达(会话启动耗时)", "serial", ds.serial, "delay_ms", time.Since(ds.startAt).Milliseconds()) + } + // 缓存 config 帧(SPS/PPS),供新订阅者连接时补发 + if frame.Config { + ds.subMu.Lock() + ds.lastConfig = frame.Data + ds.subMu.Unlock() + } + // 分辨率:ReadVideo 已解析 session 头并更新 conn.Width/Height + ds.subMu.Lock() + if ds.conn.Width > 0 { + ds.width = ds.conn.Width + ds.height = ds.conn.Height + } + // 封装:1字节类型前缀 + 帧数据 + // 0x01=config(SPS/PPS) 0x02=IDR关键帧 0x03=P帧 + // 前端据此标记 EncodedVideoChunk 的 key/delta,是 WebCodecs 解码的前提 + var tag byte + switch { + case frame.Config: + tag = 0x01 + case frame.KeyFrame: + tag = 0x02 + default: + tag = 0x03 + } + // 关键帧拼接 SPS/PPS:scrcpy 编码器关闭了重复参数集, + // IDR 帧不含参数集,WebCodecs annexb 模式要求关键帧自带, + // 否则浏览器无法解码(黑屏)。拼接后 = SPS/PPS + IDR 的合法 Annex-B 流。 + data := frame.Data + if tag == 0x02 && ds.lastConfig != nil { + merged := make([]byte, 0, len(ds.lastConfig)+len(data)) + merged = append(merged, ds.lastConfig...) + merged = append(merged, data...) + data = merged + } + payload := make([]byte, 1+len(data)) + payload[0] = tag + copy(payload[1:], data) + for ch := range ds.subs { + select { + case ch <- payload: + default: // 慢消费者丢弃(保持实时性) + } + } + ds.subMu.Unlock() + } +} + +// handleDead videoLoop 退出时清理 +func (ds *deviceSession) handleDead() { + ds.owner.removeSession(ds.serial) + if ds.conn != nil { + ds.conn.Close() + } + ds.closeAll() +} + +// subscribe 订阅视频流,返回接收 channel 和取消函数 +func (ds *deviceSession) subscribe() (chan []byte, func()) { + ch := make(chan []byte, 16) + ds.subMu.Lock() + ds.subs[ch] = struct{}{} + ds.subMu.Unlock() + cancel := func() { + ds.subMu.Lock() + delete(ds.subs, ch) + ds.subMu.Unlock() + } + return ch, cancel +} + +func (ds *deviceSession) closeAll() { + ds.stopOnce.Do(func() { + close(ds.stopSub) + }) +} + +// CloseSession 外部主动关会话(设备将被删除/冻结时调用) +func (s *StreamServer) CloseSession(serial string) { + s.mu.Lock() + ds, ok := s.sessions[serial] + if ok { + delete(s.sessions, serial) + } + s.mu.Unlock() + if !ok { + return + } + if ds.conn != nil { + ds.conn.Close() + } + ds.closeAll() +} + +// HandleStream 处理 WebSocket 连接(video 广播订阅 + control 注入) +// GET /stream?serial=127.0.0.1:5556 +func (s *StreamServer) HandleStream(w http.ResponseWriter, r *http.Request) { + serial := r.URL.Query().Get("serial") + if serial == "" { + http.Error(w, "缺少 serial 参数", http.StatusBadRequest) + return + } + + ds, err := s.getOrCreate(serial) + if err != nil { + http.Error(w, "启动设备会话失败: "+err.Error(), http.StatusBadGateway) + return + } + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer ws.Close() + + // 先订阅:帧开始累积,等待 meta 就绪期间不丢流 + frames, cancelSub := ds.subscribe() + defer cancelSub() + + // 等 meta 就绪(新会话需解析 session 头;已有会话秒就绪) + // 等待期间先发 meta 再补 config,然后正常推流 + deadline := time.Now().Add(3 * time.Second) + sentMeta := false + for time.Now().Before(deadline) { + ds.subMu.Lock() + ready := ds.width > 0 + cfg := ds.lastConfig + w := ds.width + h := ds.height + ds.subMu.Unlock() + if ready && !sentMeta { + sentMeta = true + meta, _ := json.Marshal(metaMsg{ + Type: "meta", + Width: int(w), + Height: int(h), + Device: ds.conn.Device, + }) + if err := ws.WriteMessage(websocket.TextMessage, meta); err != nil { + return + } + // 补发缓存的 config 帧(SPS/PPS,带类型前缀),否则浏览器无法解码后续 IDR + if cfg != nil { + payload := make([]byte, 1+len(cfg)) + payload[0] = 0x01 // config + copy(payload[1:], cfg) + if err := ws.WriteMessage(websocket.BinaryMessage, payload); err != nil { + return + } + } + // 请求关键帧:通知 scrcpy-server 立即输出 IDR + _ = ds.conn.ResetVideo() + // 唤醒屏幕:设备可能已熄屏(MediaCodec 无新帧)。BACK_OR_SCREEN_ON=4 唤醒 + 关锁屏, + // 保证熄屏后再次打开控制台也能立即恢复画面(stay_awake 只能保会话期间的唤醒)。 + _ = ds.conn.WakeScreen() + } + if ready { + break + } + // 避免空转:等待期间也从帧 channel 排空,保持连接活跃 + select { + case <-ds.stopSub: + return + case <-time.After(50 * time.Millisecond): + } + } + + // 视频转发协程(帧已在等待期间开始累积,此处直接转发) + wsDone := make(chan struct{}) + go func() { + defer close(wsDone) + defer func() { + if r := recover(); r != nil { + logger.Error("scrcpy", "视频转发协程 panic(已恢复)", "serial", ds.serial, + "panic", r, "stack", string(debug.Stack())) + } + }() + for { + select { + case <-ds.stopSub: + return + case data, ok := <-frames: + if !ok { + return + } + if err := ws.WriteMessage(websocket.BinaryMessage, data); err != nil { + return + } + } + } + }() + + // 控制接收:读 WS JSON → 注入设备 + for { + _, data, err := ws.ReadMessage() + if err != nil { + break + } + var msg controlMsg + if err := json.Unmarshal(data, &msg); err != nil { + continue + } + switch msg.Type { + case "touch": + _ = ds.conn.InjectTouch(byte(msg.Action), int32(msg.X), int32(msg.Y)) + case "key": + _ = ds.conn.InjectKeycode(byte(msg.Action), int32(msg.Keycode)) + case "text": + _ = ds.conn.InjectText(msg.Text) + case "reset": + _ = ds.conn.ResetVideo() + } + } + <-wsDone +} + +// metaMsg 连接建立后发给浏览器的设备信息 +type metaMsg struct { + Type string `json:"type"` + Width int `json:"width"` + Height int `json:"height"` + Device string `json:"device"` +} + +// controlMsg 浏览器发来的控制指令 +type controlMsg struct { + Type string `json:"type"` + Action int `json:"action"` + X int `json:"x"` + Y int `json:"y"` + Keycode int `json:"keycode"` + Text string `json:"text"` +} diff --git a/internal/scrcpy/scrcpy-server.jar b/internal/scrcpy/scrcpy-server.jar new file mode 100644 index 0000000..5bc5a06 Binary files /dev/null and b/internal/scrcpy/scrcpy-server.jar differ diff --git a/internal/scrcpy/scrcpy.go b/internal/scrcpy/scrcpy.go new file mode 100644 index 0000000..08bb458 --- /dev/null +++ b/internal/scrcpy/scrcpy.go @@ -0,0 +1,286 @@ +// Package scrcpy 纯 Go 实现的 scrcpy 客户端(协议 v4.1) +// +// 用途:为管理后台提供"云手机式"实时安卓设备操作—— +// 通过 adb 启动设备上的 scrcpy-server,接收实时 H.264 视频流, +// 并向前端 WebSocket 转发(视频帧 + 设备 meta),同时把前端触摸/按键消息 +// 注入到设备(INJECT_TOUCH_EVENT / INJECT_KEYCODE / INJECT_TEXT)。 +// +// 监听端载体为 USB 真机 / adb over WiFi(详见 docs/MIANQIAN.md 决策更新 v0.2)。 +// scrcpy 协议本身基于 adb forward,对 USB 直连真机 / adb over WiFi 均适用。 +// +// 协议参考(scrcpy v4.1 doc/develop.md + 源码): +// - 视频 socket: dummy(1) → deviceName(64) → codecId(u32) → [session(12) | frame(12+data)]* +// - 控制 socket: dummy(1) → 客户端发控制消息(服务端不返回数据) +// - 帧头 12B: u64(pts|flags) + u32(size);flags: SESSION=1<<63, CONFIG=1<<62, KEYFRAME=1<<61 +// - session 头: u32(0x80000000|resizeFlag) + u32(width) + u32(height) +package scrcpy + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "time" +) + +// 帧头 flags(高位) +const ( + flagSession uint64 = 1 << 63 + flagConfig uint64 = 1 << 62 + flagKeyFrame uint64 = 1 << 61 +) + +// 控制消息类型(scrcpy v4.1 ControlMessage.java) +const ( + msgInjectKeycode = 0 + msgInjectText = 1 + msgInjectTouch = 2 + msgInjectScroll = 3 + msgBackOrScreenOn = 4 + msgGetClipboard = 8 + msgSetClipboard = 9 + msgSetDisplayPower = 10 + msgRotateDevice = 11 + msgStartApp = 16 + msgResetVideo = 17 +) + +// VideoFrame 一帧视频数据(带 meta) +type VideoFrame struct { + PTS int64 // 微秒 + Config bool // CODEC_CONFIG 包(SPS/PPS) + KeyFrame bool // IDR 关键帧 + Data []byte // Annex-B H.264 数据(可能含多个 NAL) +} + +// SessionMeta 分辨率/会话信息(来自 session 头) +type SessionMeta struct { + Width uint16 + Height uint16 +} + +// Connection 设备连接(video + control 双 socket) +type Connection struct { + video net.Conn + control net.Conn + Device string + Width uint16 + Height uint16 +} + +// Dial 连接 scrcpy-server(通过已建立的 adb forward tcp 端口) +// +// 协议时序(v4.1): +// +// video 连接 → 读 dummy(1) → control 连接(dummy 只在第一个 socket 写一次) +// → 服务端所有 socket 连齐后:video 上发 name(64) + codecId(u32) + [session|帧]* +func Dial(addr string) (*Connection, error) { + video, err := net.Dial("tcp", addr) + if err != nil { + return nil, fmt.Errorf("video socket: %w", err) + } + c := &Connection{video: video} + + // 握手读取全部带超时(server 中途崩溃时避免永久阻塞) + hs := func(n int) ([]byte, error) { + b := make([]byte, n) + video.SetReadDeadline(time.Now().Add(5 * time.Second)) + if _, err := io.ReadFull(video, b); err != nil { + return nil, err + } + video.SetReadDeadline(time.Time{}) + return b, nil + } + + // dummy byte(forward 模式探测,仅在 video socket 上发送一次) + if _, err := hs(1); err != nil { + video.Close() + return nil, fmt.Errorf("dummy byte: %w", err) + } + + // control socket(服务端在 video+control 全部连齐后才发 name/codec) + control, err := net.Dial("tcp", addr) + if err != nil { + video.Close() + return nil, fmt.Errorf("control socket: %w", err) + } + c.control = control + + // device name (64) —— 现在服务端已开始发送 + name, err := hs(64) + if err != nil { + video.Close() + control.Close() + return nil, fmt.Errorf("device name: %w", err) + } + c.Device = trimNull(name) + + // codec id (u32) + codec, err := hs(4) + if err != nil { + video.Close() + control.Close() + return nil, fmt.Errorf("codec id: %w", err) + } + if got := binary.BigEndian.Uint32(codec); got != 0x68323634 { // "h264" + video.Close() + control.Close() + return nil, fmt.Errorf("非 H.264 codec: 0x%08x", got) + } + + return c, nil +} + +// ReadVideo 读取下一帧(解析 12 字节帧头 + 数据) +// 返回 nil 表示连接正常结束。 +func (c *Connection) ReadVideo() (*VideoFrame, error) { + hdr := make([]byte, 12) + if _, err := io.ReadFull(c.video, hdr); err != nil { + return nil, err + } + ptsFlags := binary.BigEndian.Uint64(hdr[0:8]) + size := binary.BigEndian.Uint32(hdr[8:12]) + + if ptsFlags&flagSession != 0 { + // session 头(12 字节):flags(u32) + width(u32) + height(u32) + // 这里 hdr 已含前 12 字节,其中 u32 部分在 hdr[0:4]... 需要重读 + // 实际 session 头是: u32(flags) + u32(w) + u32(h),与帧头格式不同, + // 但都在同一个 12 字节缓冲里。hdr[4:8]=width, hdr[8:12]=height + // 注意:session 的 size 字段无意义,用 flags 判断。 + c.Width = uint16(binary.BigEndian.Uint32(hdr[4:8])) + c.Height = uint16(binary.BigEndian.Uint32(hdr[8:12])) + return c.ReadVideo() // session 头不是媒体帧,继续读 + } + + if size > 64*1024*1024 { + return nil, fmt.Errorf("帧大小异常: %d", size) + } + data := make([]byte, size) + if _, err := io.ReadFull(c.video, data); err != nil { + return nil, err + } + return &VideoFrame{ + PTS: int64(ptsFlags &^ (flagConfig | flagKeyFrame)), + Config: ptsFlags&flagConfig != 0, + KeyFrame: ptsFlags&flagKeyFrame != 0, + Data: data, + }, nil +} + +// ReadSessionMeta 读取首个 session 头(分辨率),返回 meta +// 带 5s 超时:video socket 需要后续持续读帧,避免永久阻塞 +func (c *Connection) ReadSessionMeta() (*SessionMeta, error) { + c.video.SetReadDeadline(time.Now().Add(5 * time.Second)) + defer c.video.SetReadDeadline(time.Time{}) + // 读帧头,若是 session 返回 + for { + hdr := make([]byte, 12) + if _, err := io.ReadFull(c.video, hdr); err != nil { + return nil, err + } + ptsFlags := binary.BigEndian.Uint64(hdr[0:8]) + if ptsFlags&flagSession != 0 { + w := uint16(binary.BigEndian.Uint32(hdr[4:8])) + h := uint16(binary.BigEndian.Uint32(hdr[8:12])) + c.Width, c.Height = w, h + return &SessionMeta{Width: w, Height: h}, nil + } + // 非 session(config 帧),跳过数据 + size := binary.BigEndian.Uint32(hdr[8:12]) + if _, err := io.CopyN(io.Discard, c.video, int64(size)); err != nil { + return nil, err + } + } +} + +// SendControl 发送一条控制消息 +func (c *Connection) SendControl(msg []byte) error { + _, err := c.control.Write(msg) + return err +} + +// InjectTouch 注入触摸事件 +// action: 0=DOWN 1=UP 2=MOVE;x/y 为设备坐标(相对 Width/Height) +func (c *Connection) InjectTouch(action byte, x, y int32) error { + msg := make([]byte, 0, 31) + msg = append(msg, msgInjectTouch, action) + msg = appendU64(msg, 1) // pointerId + msg = appendI32(msg, x) + msg = appendI32(msg, y) + msg = appendU16(msg, c.Width) + msg = appendU16(msg, c.Height) + msg = appendU16(msg, 0xFFFF) // pressure + msg = appendI32(msg, 1) // actionButton + if action == 1 { // UP: buttons=0 + msg = appendI32(msg, 0) + } else { + msg = appendI32(msg, 1) + } + return c.SendControl(msg) +} + +// InjectKeycode 注入按键 +// action: 0=DOWN 1=UP 2=REPEAT;keycode: Android KeyEvent 码 +func (c *Connection) InjectKeycode(action byte, keycode int32) error { + msg := []byte{msgInjectKeycode, action} + msg = appendI32(msg, keycode) + msg = appendI32(msg, 0) // repeat + msg = appendI32(msg, 0) // metaState + return c.SendControl(msg) +} + +// InjectText 注入文本(UTF-8,u32 长度前缀) +func (c *Connection) InjectText(text string) error { + data := []byte(text) + msg := []byte{msgInjectText} + msg = appendU32(msg, uint32(len(data))) + msg = append(msg, data...) + return c.SendControl(msg) +} + +// ResetVideo 请求关键帧(新客户端加入时) +func (c *Connection) ResetVideo() error { + return c.SendControl([]byte{msgResetVideo}) +} + +// WakeScreen 唤醒屏幕(BACK_OR_SCREEN_ON:屏幕熄灭时点亮,锁屏时模拟返回键解锁)。 +// 设备熄屏后 MediaCodec 停止产帧,唤醒是恢复视频流的前提。 +func (c *Connection) WakeScreen() error { + // msgBackOrScreenOn=4:action(1B)=0(SCREEN_ON) + return c.SendControl([]byte{msgBackOrScreenOn, 0}) +} + +// Close 关闭连接 +func (c *Connection) Close() { + if c.control != nil { + c.control.Close() + } + if c.video != nil { + c.video.Close() + } +} + +// ---------- 字节序辅助 ---------- + +func appendU16(b []byte, v uint16) []byte { + return append(b, byte(v>>8), byte(v)) +} +func appendU32(b []byte, v uint32) []byte { + return append(b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) +} +func appendI32(b []byte, v int32) []byte { + return appendU32(b, uint32(v)) +} +func appendU64(b []byte, v uint64) []byte { + return append(b, byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), + byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) +} + +func trimNull(b []byte) string { + for i, c := range b { + if c == 0 { + return string(b[:i]) + } + } + return string(b) +} diff --git a/internal/scrcpy/scrcpy_test.go b/internal/scrcpy/scrcpy_test.go new file mode 100644 index 0000000..c20524e --- /dev/null +++ b/internal/scrcpy/scrcpy_test.go @@ -0,0 +1,62 @@ +package scrcpy + +import ( + "context" + "fmt" + "os" + "testing" + "time" +) + +// TestLiveSession 集成测试:需要一台 adb 已连接的真机/设备 +// 运行条件:环境变量 PG_TEST_SERIAL 指向一个 adb serial(USB 直连如 "4f2bdc30" 或 adb over WiFi 如 "127.0.0.1:5556"),设备在线 +// 手动运行:PG_TEST_SERIAL=4f2bdc30 go test ./internal/scrcpy/ -run TestLiveSession -v +func TestLiveSession(t *testing.T) { + serial := os.Getenv("PG_TEST_SERIAL") + if serial == "" { + t.Skip("未设置 PG_TEST_SERIAL(如 PG_TEST_SERIAL=127.0.0.1:5556)") + } + ctx := context.Background() + sess, err := StartSession(ctx, serial, 27184) + if err != nil { + t.Fatalf("StartSession: %v", err) + } + defer sess.Close() + + conn := sess.Connection() + t.Logf("设备: %s, 分辨率: %dx%d", conn.Device, conn.Width, conn.Height) + if conn.Width == 0 { + // 等待 session meta + meta, err := conn.ReadSessionMeta() + if err != nil { + t.Fatalf("ReadSessionMeta: %v", err) + } + t.Logf("session meta: %dx%d", meta.Width, meta.Height) + } + + // 读几帧验证视频流 + conn.video.SetReadDeadline(time.Now().Add(5 * time.Second)) + frames := 0 + for i := 0; i < 5; i++ { + f, err := conn.ReadVideo() + if err != nil { + t.Fatalf("ReadVideo(%d): %v", i, err) + } + frames++ + t.Logf("帧%d: %d 字节, config=%v keyframe=%v", i+1, len(f.Data), f.Config, f.KeyFrame) + if i == 0 && !f.Config { + t.Log("⚠️ 首帧不是 config(SPS/PPS),浏览器可能无法解码") + } + } + + // 注入触摸 + if err := conn.InjectTouch(0, 100, 100); err != nil { // DOWN + t.Fatalf("InjectTouch DOWN: %v", err) + } + time.Sleep(100 * time.Millisecond) + if err := conn.InjectTouch(1, 100, 100); err != nil { // UP + t.Fatalf("InjectTouch UP: %v", err) + } + t.Logf("✅ 触摸注入成功,共收到 %d 帧", frames) + _ = fmt.Sprint() +} diff --git a/internal/scrcpy/session.go b/internal/scrcpy/session.go new file mode 100644 index 0000000..9287695 --- /dev/null +++ b/internal/scrcpy/session.go @@ -0,0 +1,218 @@ +// Session 会话管理:通过 adb 启动 scrcpy-server 并建立连接 +// +// 流程(对应 doc/develop.md): +// 1. push scrcpy-server.jar 到设备 /data/local/tmp/ +// 2. adb forward tcp: localabstract:scrcpy +// 3. app_process 启动 Server(scid=-1 → socket 名 "scrcpy") +// 4. Dial 连接(video → control → metadata) +package scrcpy + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "os" + "os/exec" + "strings" + "sync" + "time" + + "paygateway/internal/logger" +) + +// ServerJar 嵌入的 scrcpy-server.jar(编译时由脚本下载,go:generate) +// +//go:generate bash ../../scripts/download-scrcpy-server.sh +//go:embed scrcpy-server.jar +var ServerJar []byte + +// Session 单个设备的 scrcpy 会话 +type Session struct { + mu sync.Mutex + serial string // adb serial(host:port) + port int // 本机 forward 端口 + conn *Connection + cancel context.CancelFunc + done chan struct{} +} + +// StartSession 启动并连接设备。 +// 关键设计点: +// - 总超时 30s、socketReady 8s(USB 真机起 server 一般 <5s) +// - socketReady 用 /proc/net/unix 字节扫描,不依赖 shell grep(精简 ROM 无该命令) +// - 不重试 Dial:每次 Dial 消耗 server 一次 accept,重试会破坏协议状态机 +// - 任何失败必先 cleanupAdb:清 forward + pkill(避免端口耗光与旧 server 抢 accept) +func StartSession(parent context.Context, serial string, port int) (*Session, error) { + s := &Session{serial: serial, port: port, done: make(chan struct{})} + s.cancel = func() {} + + ctx, cancel := context.WithTimeout(parent, 30*time.Second) + defer cancel() + + t0 := time.Now() + if err := s.ensureServer(ctx); err != nil { + s.cleanupAdb() + return nil, err + } + tEnsure := time.Since(t0) + if !s.waitSocketReady(ctx, 8*time.Second) { + s.cleanupAdb() + return nil, fmt.Errorf("scrcpy server 未在 8s 监听 socket(设备可能锁屏/无 root 权限/app_process 不可用)") + } + tSocket := time.Since(t0) + time.Sleep(100 * time.Millisecond) // dial 前 dwell:让 server 进入 accept 队列 + conn, err := Dial(fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + s.cleanupAdb() + return nil, fmt.Errorf("连接 scrcpy: %w", err) + } + logger.Info("scrcpy", "会话建立", "serial", s.serial, "port", s.port, + "ensure_ms", tEnsure.Milliseconds(), "socket_ms", tSocket.Milliseconds(), "dial_ms", time.Since(t0).Milliseconds()) + s.conn = conn + return s, nil +} + +// socketReady 通过 /proc/net/unix 检查 scrcpy abstract socket 是否监听 +// 不走 `grep scrcpy` 是因为精简 ROM 可能没有 grep;改为整文件读 + Go 字节扫描 +func (s *Session) socketReady() bool { + out, err := s.adb("shell", "cat /proc/net/unix") + if err != nil { + return false + } + return strings.Contains(out, "scrcpy") +} + +func (s *Session) waitSocketReady(ctx context.Context, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + select { + case <-ctx.Done(): + return false + default: + } + if s.socketReady() { + return true + } + time.Sleep(100 * time.Millisecond) + } + return false +} + +func (s *Session) cleanupAdb() { + _, _ = s.adb("forward", "--remove", fmt.Sprintf("tcp:%d", s.port)) + _ = s.adbShell("pkill -f 'com.genymobile.scrcpy.Server' || true") +} + +// ensureServer 确保 jar 已推送 + server 进程运行 + forward 建立 +// ctx 用于约束每条 adb 子命令的超时:USB 拔了 adb 默认会卡死,必须超时 +func (s *Session) ensureServer(ctx context.Context) error { + out, _ := s.adbCtx(ctx, "shell", "test -f /data/local/tmp/scrcpy-server.jar && echo OK || echo MISSING") + if strings.Contains(out, "MISSING") { + if err := s.adbPushJar(ctx); err != nil { + return fmt.Errorf("推送 scrcpy-server: %w", err) + } + } + + _, _ = s.adbCtx(ctx, "forward", "--remove", fmt.Sprintf("tcp:%d", s.port)) + _, _ = s.adbCtx(ctx, "shell", "pkill -f 'com.genymobile.scrcpy.Server' || true") + + if _, err := s.adbCtx(ctx, "forward", fmt.Sprintf("tcp:%d", s.port), "localabstract:scrcpy"); err != nil { + return fmt.Errorf("adb forward: %w", err) + } + + // stay_awake=true:scrcpy 持有 WakeLock 保持设备屏幕唤醒。 + // 不加的话设备熄屏 → Android 不再向 MediaCodec 提供 Surface 帧 → 视频流静默停止, + // 而 control socket 仍通(鼠标注入"看似有效"但画面黑)——即控制台再次打开黑屏 bug。 + startCmd := "nohup sh -c 'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 4.1 " + + "scid=-1 video=true audio=false control=true tunnel_forward=true cleanup=false stay_awake=true max_size=1080' > /dev/null 2>&1 &" + if _, err := s.adbCtx(ctx, "shell", startCmd); err != nil { + return fmt.Errorf("启动 scrcpy-server: %w", err) + } + return nil +} + +// adbPushJar 推送 jar 到设备 +// 每次 5s 超时;首次失败重试一次(USB 真机偶发 adb busy,重试一次基本必过) +func (s *Session) adbPushJar(ctx context.Context) error { + if len(ServerJar) == 0 { + return fmt.Errorf("scrcpy-server.jar 未嵌入(请先运行 go generate)") + } + tmp, err := os.CreateTemp("", "scrcpy-server-*.jar") + if err != nil { + return fmt.Errorf("创建临时文件: %w", err) + } + tmpPath := tmp.Name() + defer os.Remove(tmpPath) + if _, err := tmp.Write(ServerJar); err != nil { + tmp.Close() + return err + } + tmp.Close() + + doPush := func() error { + cctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + cmd := exec.CommandContext(cctx, "adb", "-s", s.serial, "push", tmpPath, "/data/local/tmp/scrcpy-server.jar") + var errb bytes.Buffer + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + return fmt.Errorf("adb push: %w (%s)", err, strings.TrimSpace(errb.String())) + } + return nil + } + if err := doPush(); err != nil { + time.Sleep(200 * time.Millisecond) + return doPush() + } + return nil +} + +// adb 带固定 8s 超时执行;USB 拔了 adb 默认无超时会卡死整个 session +func (s *Session) adb(args ...string) (string, error) { + return s.adbCtx(context.Background(), args...) +} + +func (s *Session) adbCtx(ctx context.Context, args ...string) (string, error) { + cctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + full := append([]string{"-s", s.serial}, args...) + cmd := exec.CommandContext(cctx, "adb", full...) + var out, errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("adb %s: %w (%s)", strings.Join(args, " "), err, strings.TrimSpace(errb.String())) + } + return out.String(), nil +} + +// Connection 返回活动连接 +func (s *Session) Connection() *Connection { return s.conn } + +func (s *Session) adbShell(cmd string) error { + _, err := s.adb("shell", cmd) + return err +} + +// Close 关闭会话(断开连接 + 停止 server) +// 幂等:deviceSession.videoLoop 退出时也可能间接调用,避免 close(done) 双触发 panic +func (s *Session) Close() { + s.mu.Lock() + defer s.mu.Unlock() + if s.conn != nil { + s.conn.Close() + s.conn = nil + } + if s.cancel != nil { + s.cancel() + s.cancel = nil + } + s.adb("forward", "--remove", fmt.Sprintf("tcp:%d", s.port)) + s.adbShell("pkill -f 'com.genymobile.scrcpy.Server' || true") + select { + case <-s.done: + default: + close(s.done) + } +} diff --git a/internal/store/accountdb.go b/internal/store/accountdb.go new file mode 100755 index 0000000..96ea2ed --- /dev/null +++ b/internal/store/accountdb.go @@ -0,0 +1,303 @@ +// Package store 账户数据库:独立 accounts.db,仅管理后台 API 可读写 +// +// 安全设计: +// - 独立 SQLite 文件,权限 0600 +// - 敏感字段(private_key / public_key / api_key)以 AES-256-GCM 密文存储 +// - AccountStore 接口不暴露给 order/channel 等模块,仅在 admin 包内部使用 +// - 其他模块通过 AccountPool(内存缓存)获取解密后的账户数据 +package store + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + _ "modernc.org/sqlite" + + "paygateway/internal/account" +) + +// AccountStore 账户存储接口(仅限 Admin API 使用) +type AccountStore interface { + ListAccounts(ctx context.Context, channel string) ([]*account.AccountEncrypted, error) + GetAccount(ctx context.Context, id int64) (*account.AccountEncrypted, error) + CreateAccount(ctx context.Context, a *account.AccountEncrypted) (int64, error) + UpdateAccount(ctx context.Context, a *account.AccountEncrypted) error + DeleteAccount(ctx context.Context, id int64) error + SetAccountStatus(ctx context.Context, id int64, status string) error + ResetDailyUsed(ctx context.Context) error + InsertAccountLog(ctx context.Context, log *account.AccountLog) error + ListAccountLogs(ctx context.Context, accountID int64, limit int) ([]*account.AccountLog, error) + Close() error +} + +// AccountDB 独立账户数据库:仅存 pay_accounts + account_logs +// 文件权限 0600,WAL 模式,仅 Admin API 访问 +type AccountDB struct { + db *sql.DB +} + +// NewAccountDB 打开/创建独立账户数据库 +func NewAccountDB(path string) (*AccountDB, error) { + // 先创建空文件以确保可以设置权限 + fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, fmt.Errorf("创建账户数据库文件失败: %w", err) + } + fd.Close() + + // 强制设置权限为 0600(只有文件所有者可读写) + if err := os.Chmod(path, 0600); err != nil { + return nil, fmt.Errorf("设置账户数据库权限失败: %w", err) + } + + // 打开连接:WAL 模式防崩溃 + db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=10000&_synchronous=FULL") + if err != nil { + return nil, fmt.Errorf("打开账户数据库失败: %w", err) + } + + // 连接池:单连接更安全 + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + a := &AccountDB{db: db} + if err := a.migrate(); err != nil { + db.Close() + return nil, err + } + return a, nil +} + +// migrate 创建账户表 + 操作审计日志表 +func (a *AccountDB) migrate() error { + // 迁移旧库:daily_used / qr_content_enc / frozen_reason 列(幂等,已存在则报错忽略) + _, _ = a.db.Exec(`ALTER TABLE pay_accounts ADD COLUMN daily_used INTEGER NOT NULL DEFAULT 0`) + _, _ = a.db.Exec(`ALTER TABLE pay_accounts ADD COLUMN qr_content_enc TEXT NOT NULL DEFAULT ''`) + _, _ = a.db.Exec(`ALTER TABLE pay_accounts ADD COLUMN frozen_reason TEXT NOT NULL DEFAULT ''`) + stmts := []string{ + `CREATE TABLE IF NOT EXISTS pay_accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel TEXT NOT NULL, + name TEXT NOT NULL, + app_id TEXT NOT NULL DEFAULT '', + mch_id TEXT NOT NULL DEFAULT '', + private_key_enc TEXT NOT NULL DEFAULT '', + public_key_enc TEXT NOT NULL DEFAULT '', + api_key_enc TEXT NOT NULL DEFAULT '', + qr_content_enc TEXT NOT NULL DEFAULT '', + weight INTEGER NOT NULL DEFAULT 50, + priority INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'normal', + frozen_reason TEXT NOT NULL DEFAULT '', + auto_ban INTEGER NOT NULL DEFAULT 1, + daily_limit INTEGER NOT NULL DEFAULT 0, + daily_used INTEGER NOT NULL DEFAULT 0, + group_name TEXT NOT NULL DEFAULT 'default', + is_prod INTEGER NOT NULL DEFAULT 0, + remark TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_accounts_channel ON pay_accounts(channel)`, + `CREATE INDEX IF NOT EXISTS idx_accounts_status ON pay_accounts(status)`, + + // 账户操作审计日志 + `CREATE TABLE IF NOT EXISTS account_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + action TEXT NOT NULL, + detail TEXT NOT NULL DEFAULT '', + operator TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_account_logs_account ON account_logs(account_id)`, + + // WAL 模式 + `PRAGMA journal_mode=WAL`, + `PRAGMA synchronous=FULL`, + } + for _, stmt := range stmts { + if _, err := a.db.Exec(stmt); err != nil { + return fmt.Errorf("账户数据库迁移失败: %w", err) + } + } + return nil +} + +// ---------- 账户 CRUD ---------- + +// ListAccounts 列出账户(按通道筛选,空=全部) +func (a *AccountDB) ListAccounts(ctx context.Context, channel string) ([]*account.AccountEncrypted, error) { + var rows *sql.Rows + var err error + if channel == "" { + rows, err = a.db.QueryContext(ctx, + `SELECT id, channel, name, app_id, mch_id, private_key_enc, public_key_enc, api_key_enc, qr_content_enc, + weight, priority, status, frozen_reason, auto_ban, daily_limit, daily_used, group_name, is_prod, remark, created_at, updated_at + FROM pay_accounts ORDER BY channel, priority DESC, weight DESC`) + } else { + rows, err = a.db.QueryContext(ctx, + `SELECT id, channel, name, app_id, mch_id, private_key_enc, public_key_enc, api_key_enc, qr_content_enc, + weight, priority, status, frozen_reason, auto_ban, daily_limit, daily_used, group_name, is_prod, remark, created_at, updated_at + FROM pay_accounts WHERE channel=? ORDER BY priority DESC, weight DESC`, channel) + } + if err != nil { + return nil, err + } + defer rows.Close() + + var out []*account.AccountEncrypted + for rows.Next() { + e, err := scanAccountEncrypted(rows) + if err != nil { + return nil, err + } + out = append(out, e) + } + return out, rows.Err() +} + +// GetAccount 按 ID 获取账户 +func (a *AccountDB) GetAccount(ctx context.Context, id int64) (*account.AccountEncrypted, error) { + row := a.db.QueryRowContext(ctx, + `SELECT id, channel, name, app_id, mch_id, private_key_enc, public_key_enc, api_key_enc, qr_content_enc, + weight, priority, status, frozen_reason, auto_ban, daily_limit, daily_used, group_name, is_prod, remark, created_at, updated_at + FROM pay_accounts WHERE id=?`, id) + return scanAccountEncrypted(row) +} + +// CreateAccount 创建账户,返回新 ID +func (a *AccountDB) CreateAccount(ctx context.Context, acct *account.AccountEncrypted) (int64, error) { + now := time.Now() + res, err := a.db.ExecContext(ctx, + `INSERT INTO pay_accounts (channel, name, app_id, mch_id, private_key_enc, public_key_enc, api_key_enc, qr_content_enc, + weight, priority, status, frozen_reason, auto_ban, daily_limit, daily_used, group_name, is_prod, remark, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + acct.Channel, acct.Name, acct.AppID, acct.MchID, + acct.PrivateKeyEnc, acct.PublicKeyEnc, acct.ApiKeyEnc, acct.QRContentEnc, + acct.Weight, acct.Priority, acct.Status, acct.FrozenReason, boolToInt(acct.AutoBan), + acct.DailyLimit, acct.DailyUsed, acct.Group, boolToInt(acct.IsProd), acct.Remark, + now.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano)) + if err != nil { + return 0, fmt.Errorf("创建账户失败: %w", err) + } + return res.LastInsertId() +} + +// UpdateAccount 更新账户(全部字段覆盖) +func (a *AccountDB) UpdateAccount(ctx context.Context, acct *account.AccountEncrypted) error { + _, err := a.db.ExecContext(ctx, + `UPDATE pay_accounts SET channel=?, name=?, app_id=?, mch_id=?, + private_key_enc=?, public_key_enc=?, api_key_enc=?, qr_content_enc=?, + weight=?, priority=?, status=?, frozen_reason=?, auto_ban=?, daily_limit=?, daily_used=?, group_name=?, is_prod=?, remark=?, updated_at=? + WHERE id=?`, + acct.Channel, acct.Name, acct.AppID, acct.MchID, + acct.PrivateKeyEnc, acct.PublicKeyEnc, acct.ApiKeyEnc, acct.QRContentEnc, + acct.Weight, acct.Priority, acct.Status, acct.FrozenReason, boolToInt(acct.AutoBan), + acct.DailyLimit, acct.DailyUsed, acct.Group, boolToInt(acct.IsProd), acct.Remark, + time.Now().Format(time.RFC3339Nano), acct.ID) + return err +} + +// DeleteAccount 删除账户 +func (a *AccountDB) DeleteAccount(ctx context.Context, id int64) error { + _, err := a.db.ExecContext(ctx, `DELETE FROM pay_accounts WHERE id=?`, id) + return err +} + +// SetAccountStatus 设置账户状态(启停/冻结/解冻;解冻/启停时清空 frozen_reason) +func (a *AccountDB) SetAccountStatus(ctx context.Context, id int64, status string) error { + _, err := a.db.ExecContext(ctx, + `UPDATE pay_accounts SET status=?, frozen_reason=CASE WHEN ?='frozen' THEN frozen_reason ELSE '' END, updated_at=? WHERE id=?`, + status, status, time.Now().Format(time.RFC3339Nano), id) + return err +} + +// SetAccountFrozen 冻结账户并记录原因(仅人工冻结路径使用;解冻统一走 SetAccountStatus) +func (a *AccountDB) SetAccountFrozen(ctx context.Context, id int64, reason string) error { + _, err := a.db.ExecContext(ctx, + `UPDATE pay_accounts SET status='frozen', frozen_reason=?, updated_at=? WHERE id=?`, + reason, time.Now().Format(time.RFC3339Nano), id) + return err +} + +// ResetDailyUsed 重置所有账户的日已用额度(每日 00:00 调用) +func (a *AccountDB) ResetDailyUsed(ctx context.Context) error { + _, err := a.db.ExecContext(ctx, + `UPDATE pay_accounts SET daily_used=0, updated_at=?`, + time.Now().Format(time.RFC3339Nano)) + return err +} + +// ---------- 账户操作审计日志 ---------- + +// InsertAccountLog 写入操作审计日志 +func (a *AccountDB) InsertAccountLog(ctx context.Context, log *account.AccountLog) error { + _, err := a.db.ExecContext(ctx, + `INSERT INTO account_logs (account_id, action, detail, operator, created_at) + VALUES (?, ?, ?, ?, ?)`, + log.AccountID, log.Action, log.Detail, log.Operator, time.Now().Format(time.RFC3339Nano)) + return err +} + +// ListAccountLogs 列出账户操作日志 +func (a *AccountDB) ListAccountLogs(ctx context.Context, accountID int64, limit int) ([]*account.AccountLog, error) { + rows, err := a.db.QueryContext(ctx, + `SELECT id, account_id, action, detail, operator, created_at + FROM account_logs WHERE account_id=? ORDER BY id DESC LIMIT ?`, accountID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]*account.AccountLog, 0) + for rows.Next() { + var l account.AccountLog + var created string + if err := rows.Scan(&l.ID, &l.AccountID, &l.Action, &l.Detail, &l.Operator, &created); err != nil { + return nil, err + } + l.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &l) + } + return out, rows.Err() +} + +// Close 关闭账户数据库 +func (a *AccountDB) Close() error { + return a.db.Close() +} + +// ---------- 辅助函数 ---------- + +// scanAccountEncrypted 扫描一行到 AccountEncrypted +func scanAccountEncrypted(s rowScanner) (*account.AccountEncrypted, error) { + var e account.AccountEncrypted + var autoBan, isProd int + var createdAt, updatedAt string + err := s.Scan(&e.ID, &e.Channel, &e.Name, &e.AppID, &e.MchID, + &e.PrivateKeyEnc, &e.PublicKeyEnc, &e.ApiKeyEnc, &e.QRContentEnc, + &e.Weight, &e.Priority, &e.Status, &e.FrozenReason, &autoBan, + &e.DailyLimit, &e.DailyUsed, &e.Group, &isProd, &e.Remark, &createdAt, &updatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + e.AutoBan = autoBan != 0 + e.IsProd = isProd != 0 + e.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAt) + e.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAt) + return &e, nil +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/internal/store/ledgerdb.go b/internal/store/ledgerdb.go new file mode 100755 index 0000000..fa7c10b --- /dev/null +++ b/internal/store/ledgerdb.go @@ -0,0 +1,237 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + _ "modernc.org/sqlite" +) + +// LedgerStore 账本存储接口(只增不改,独立数据库) +type LedgerStore interface { + AppendLedger(ctx context.Context, prevHash, payloadHash, entryHash, signature string, eventID int64) error + GetLastLedgerEntry(ctx context.Context) (*LedgerEntry, error) + CountLedger(ctx context.Context) (int64, error) + GetLedgerEntry(ctx context.Context, id int64) (*LedgerEntry, error) + ListLedger(ctx context.Context, limit int) ([]*LedgerEntry, error) + // HasLedgerEvent 按 eventID(订单ID)查询账本是否已有记录(崩溃恢复去重用) + HasLedgerEvent(ctx context.Context, eventID int64) (bool, error) + CreateSnapshot(ctx context.Context, date string, lastHash, signature string) error + GetLatestSnapshot(ctx context.Context) (*Snapshot, error) + ListSnapshots(ctx context.Context, limit int) ([]*Snapshot, error) + Close() error +} + +// LedgerDB 独立账本数据库:只存 audit_ledger + daily_snapshots +// 文件权限 0600,WAL 模式,触发器防 UPDATE/DELETE +type LedgerDB struct { + db *sql.DB +} + +// NewLedgerDB 打开/创建独立账本数据库 +func NewLedgerDB(path string) (*LedgerDB, error) { + // 先创建空文件以确保我们可以设置权限 + fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, fmt.Errorf("创建账本数据库文件失败: %w", err) + } + fd.Close() + + // 强制设置权限为 0600(只有文件所有者可读写) + if err := os.Chmod(path, 0600); err != nil { + return nil, fmt.Errorf("设置账本数据库权限失败: %w", err) + } + + // 打开连接:WAL 模式防崩溃 + 严格外键 + db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=10000&_synchronous=FULL") + if err != nil { + return nil, fmt.Errorf("打开账本数据库失败: %w", err) + } + + // 连接池:账本写入频率低,单连接更安全 + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + l := &LedgerDB{db: db} + if err := l.migrate(); err != nil { + db.Close() + return nil, err + } + return l, nil +} + +// migrate 创建账本表 + 触发器(只增不改) +func (l *LedgerDB) migrate() error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS audit_ledger ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prev_hash TEXT NOT NULL, + payload_hash TEXT NOT NULL, + entry_hash TEXT NOT NULL, + signature TEXT NOT NULL, + event_id INTEGER, + created_at TEXT NOT NULL + )`, + // 禁止 UPDATE — 任何修改账本条目的尝试都被拒绝 + `CREATE TRIGGER IF NOT EXISTS trg_ledger_no_update BEFORE UPDATE ON audit_ledger + BEGIN SELECT RAISE(ABORT, 'audit_ledger is append-only: UPDATE denied'); END`, + // 禁止 DELETE — 任何删除账本条目的尝试都被拒绝 + `CREATE TRIGGER IF NOT EXISTS trg_ledger_no_delete BEFORE DELETE ON audit_ledger + BEGIN SELECT RAISE(ABORT, 'audit_ledger is append-only: DELETE denied'); END`, + `CREATE TABLE IF NOT EXISTS daily_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_date TEXT NOT NULL UNIQUE, + last_hash TEXT NOT NULL, + signature TEXT NOT NULL, + created_at TEXT NOT NULL + )`, + // 快照同样防篡改 + `CREATE TRIGGER IF NOT EXISTS trg_snapshot_no_update BEFORE UPDATE ON daily_snapshots + BEGIN SELECT RAISE(ABORT, 'daily_snapshots is append-only: UPDATE denied'); END`, + `CREATE TRIGGER IF NOT EXISTS trg_snapshot_no_delete BEFORE DELETE ON daily_snapshots + BEGIN SELECT RAISE(ABORT, 'daily_snapshots is append-only: DELETE denied'); END`, + // 启用 WAL 模式(持久化) + `PRAGMA journal_mode=WAL`, + // 同步模式 FULL — 每次写入都等待数据落盘 + `PRAGMA synchronous=FULL`, + } + for _, stmt := range stmts { + if _, err := l.db.Exec(stmt); err != nil { + return fmt.Errorf("账本数据库迁移失败: %w", err) + } + } + return nil +} + +// ---------- 账本操作(只增不改) ---------- + +func (l *LedgerDB) AppendLedger(ctx context.Context, prevHash, payloadHash, entryHash, signature string, eventID int64) error { + _, err := l.db.ExecContext(ctx, + `INSERT INTO audit_ledger (prev_hash, payload_hash, entry_hash, signature, event_id, created_at) + VALUES (?, ?, ?, ?, ?, ?)`, + prevHash, payloadHash, entryHash, signature, eventID, nowStr()) + return err +} + +func (l *LedgerDB) GetLastLedgerEntry(ctx context.Context) (*LedgerEntry, error) { + row := l.db.QueryRowContext(ctx, + `SELECT id, prev_hash, payload_hash, entry_hash, signature, event_id, created_at + FROM audit_ledger ORDER BY id DESC LIMIT 1`) + var e LedgerEntry + var created string + if err := row.Scan(&e.ID, &e.PrevHash, &e.PayloadHash, &e.EntryHash, &e.Signature, &e.EventID, &created); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + return &e, nil +} + +func (l *LedgerDB) CountLedger(ctx context.Context) (int64, error) { + var n int64 + if err := l.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM audit_ledger`).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func (l *LedgerDB) GetLedgerEntry(ctx context.Context, id int64) (*LedgerEntry, error) { + row := l.db.QueryRowContext(ctx, + `SELECT id, prev_hash, payload_hash, entry_hash, signature, event_id, created_at + FROM audit_ledger WHERE id=?`, id) + var e LedgerEntry + var created string + if err := row.Scan(&e.ID, &e.PrevHash, &e.PayloadHash, &e.EntryHash, &e.Signature, &e.EventID, &created); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + return &e, nil +} + +// HasLedgerEvent 按 eventID(订单ID)查询账本是否已有记录(崩溃恢复去重用) +func (l *LedgerDB) HasLedgerEvent(ctx context.Context, eventID int64) (bool, error) { + var n int64 + if err := l.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM audit_ledger WHERE event_id=?`, eventID).Scan(&n); err != nil { + return false, err + } + return n > 0, nil +} + +func (l *LedgerDB) ListLedger(ctx context.Context, limit int) ([]*LedgerEntry, error) { + rows, err := l.db.QueryContext(ctx, + `SELECT id, prev_hash, payload_hash, entry_hash, signature, event_id, created_at + FROM audit_ledger ORDER BY id DESC LIMIT ?`, limit) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*LedgerEntry{} + for rows.Next() { + var e LedgerEntry + var created string + if err := rows.Scan(&e.ID, &e.PrevHash, &e.PayloadHash, &e.EntryHash, &e.Signature, &e.EventID, &created); err != nil { + return nil, err + } + e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &e) + } + return out, rows.Err() +} + +// ---------- 快照操作 ---------- + +func (l *LedgerDB) ListSnapshots(ctx context.Context, limit int) ([]*Snapshot, error) { + rows, err := l.db.QueryContext(ctx, + `SELECT id, snapshot_date, last_hash, signature, created_at FROM daily_snapshots ORDER BY id DESC LIMIT ?`, limit) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*Snapshot{} + for rows.Next() { + var sn Snapshot + var created string + if err := rows.Scan(&sn.ID, &sn.SnapshotDate, &sn.LastHash, &sn.Signature, &created); err != nil { + return nil, err + } + sn.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &sn) + } + return out, rows.Err() +} + +func (l *LedgerDB) CreateSnapshot(ctx context.Context, date string, lastHash, signature string) error { + _, err := l.db.ExecContext(ctx, + `INSERT INTO daily_snapshots (snapshot_date, last_hash, signature, created_at) VALUES (?, ?, ?, ?)`, + date, lastHash, signature, nowStr()) + return err +} + +func (l *LedgerDB) GetLatestSnapshot(ctx context.Context) (*Snapshot, error) { + row := l.db.QueryRowContext(ctx, + `SELECT id, snapshot_date, last_hash, signature, created_at FROM daily_snapshots ORDER BY id DESC LIMIT 1`) + var sn Snapshot + var created string + if err := row.Scan(&sn.ID, &sn.SnapshotDate, &sn.LastHash, &sn.Signature, &created); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + sn.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + return &sn, nil +} + +// Close 关闭账本数据库 +func (l *LedgerDB) Close() error { + return l.db.Close() +} diff --git a/internal/store/logdb.go b/internal/store/logdb.go new file mode 100644 index 0000000..f622527 --- /dev/null +++ b/internal/store/logdb.go @@ -0,0 +1,133 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + _ "modernc.org/sqlite" +) + +// LogDB 独立日志数据库:只存 app_logs,与订单主库分离,支持长期保存。 +// 文件权限 0600,WAL 模式。日志只增不删(由外部策略清理), +// 避免日志膨胀拖累订单主库的读写性能,且便于独立备份/迁移/清理。 +type LogDB struct { + db *sql.DB +} + +// NewLogDB 打开/创建独立日志数据库 +func NewLogDB(path string) (*LogDB, error) { + fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, fmt.Errorf("创建日志数据库文件失败: %w", err) + } + fd.Close() + if err := os.Chmod(path, 0600); err != nil { + return nil, fmt.Errorf("设置日志数据库权限失败: %w", err) + } + db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=10000&_synchronous=NORMAL") + if err != nil { + return nil, fmt.Errorf("打开日志数据库失败: %w", err) + } + // 日志写入并发低,单连接足够且避免锁竞争 + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + l := &LogDB{db: db} + if err := l.migrate(); err != nil { + db.Close() + return nil, err + } + return l, nil +} + +// migrate 创建日志表 + 索引(与主库 app_logs 同构) +func (l *LogDB) migrate() error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS app_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL, + scope TEXT, + message TEXT NOT NULL, + trace_id TEXT, + fields TEXT, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_logs_level_time ON app_logs(level, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_logs_scope_time ON app_logs(scope, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_logs_trace ON app_logs(trace_id)`, + `PRAGMA journal_mode=WAL`, + } + for _, stmt := range stmts { + if _, err := l.db.Exec(stmt); err != nil { + return fmt.Errorf("日志数据库迁移失败: %w", err) + } + } + return nil +} + +// InsertLog 写入一条日志(实现 logger.DBWriter) +func (l *LogDB) InsertLog(ctx context.Context, level, scope, message, traceID, fields string) error { + _, err := l.db.ExecContext(ctx, + `INSERT INTO app_logs (level, scope, message, trace_id, fields, created_at) VALUES (?, ?, ?, ?, ?, ?)`, + level, scope, message, traceID, fields, nowStr()) + return err +} + +// QueryLogs 分页查询日志 +func (l *LogDB) QueryLogs(ctx context.Context, level, scope string, limit, offset int) ([]*LogRecord, error) { + q := `SELECT id, level, scope, message, trace_id, fields, created_at FROM app_logs WHERE 1=1` + args := []any{} + if level != "" { + q += " AND LOWER(level)=LOWER(?)" + args = append(args, level) + } + if scope != "" { + q += " AND scope=?" + args = append(args, scope) + } + q += " ORDER BY id DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + rows, err := l.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*LogRecord{} + for rows.Next() { + var r LogRecord + var created string + if err := rows.Scan(&r.ID, &r.Level, &r.Scope, &r.Message, &r.TraceID, &r.Fields, &created); err != nil { + return nil, err + } + r.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &r) + } + return out, rows.Err() +} + +// CountLogs 按条件统计日志数 +func (l *LogDB) CountLogs(ctx context.Context, level, scope string) (int64, error) { + q := `SELECT COUNT(*) FROM app_logs WHERE 1=1` + args := []any{} + if level != "" { + q += " AND LOWER(level)=LOWER(?)" + args = append(args, level) + } + if scope != "" { + q += " AND scope=?" + args = append(args, scope) + } + var n int64 + if err := l.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +// Close 关闭日志数据库 +func (l *LogDB) Close() error { + return l.db.Close() +} diff --git a/internal/store/notify_tasks_test.go b/internal/store/notify_tasks_test.go new file mode 100644 index 0000000..367ad72 --- /dev/null +++ b/internal/store/notify_tasks_test.go @@ -0,0 +1,69 @@ +package store + +import ( + "context" + "path/filepath" + "testing" + "time" +) + +func newNotifyTestStore(t *testing.T) *SQLiteStore { + t.Helper() + st, err := NewSQLite(filepath.Join(t.TempDir(), "notify.db")) + if err != nil { + t.Fatalf("创建 SQLite 失败: %v", err) + } + return st +} + +// TestNotifyTask_CRUDPlusPayload 覆盖死信管理新增方法:GetNotifyTask / UpdateNotifyTaskPayload / DeleteNotifyTask。 +func TestNotifyTask_CRUDPlusPayload(t *testing.T) { + st := newNotifyTestStore(t) + ctx := context.Background() + + // CreateNotifyTask 需要一个真实 order_id(外键/展示用),这里先用 0 也不影响 notify_tasks 表。 + id, err := st.CreateNotifyTask(ctx, 123, `{"notify_url":"http://old.example/cb","out_trade_no":"T1"}`) + if err != nil { + t.Fatalf("CreateNotifyTask err: %v", err) + } + + task, err := st.GetNotifyTask(ctx, id) + if err != nil || task == nil { + t.Fatalf("GetNotifyTask = %v, %v; 期望非 nil", task, err) + } + if task.OrderID != 123 || task.Status != "pending" { + t.Fatalf("GetNotifyTask 字段不符: order_id=%d status=%s", task.OrderID, task.Status) + } + + // UpdateNotifyTaskPayload:换成最新回调地址 + newPayload := `{"notify_url":"http://new.example/cb","out_trade_no":"T1"}` + if err := st.UpdateNotifyTaskPayload(ctx, id, newPayload); err != nil { + t.Fatalf("UpdateNotifyTaskPayload err: %v", err) + } + got, _ := st.GetNotifyTask(ctx, id) + if got.Payload != newPayload { + t.Fatalf("payload 未更新: %s", got.Payload) + } + + // 重置为 pending(死信重发路径) + if err := st.UpdateNotifyTask(ctx, id, "pending", 0, time.Now()); err != nil { + t.Fatalf("UpdateNotifyTask err: %v", err) + } + got, _ = st.GetNotifyTask(ctx, id) + if got.Status != "pending" || got.RetryCount != 0 { + t.Fatalf("重置 pending 失败: status=%s retry=%d", got.Status, got.RetryCount) + } + + // DeleteNotifyTask:删除后 GetNotifyTask 返回 nil + if err := st.DeleteNotifyTask(ctx, id); err != nil { + t.Fatalf("DeleteNotifyTask err: %v", err) + } + if gone, _ := st.GetNotifyTask(ctx, id); gone != nil { + t.Fatalf("删除后仍能查到任务: %+v", gone) + } + + // GetNotifyTask 不存在的 id 返回 nil(非错误) + if miss, _ := st.GetNotifyTask(ctx, 999999); miss != nil { + t.Fatalf("不存在的 id 应返回 nil: %+v", miss) + } +} diff --git a/internal/store/personalstore.go b/internal/store/personalstore.go new file mode 100755 index 0000000..2289000 --- /dev/null +++ b/internal/store/personalstore.go @@ -0,0 +1,231 @@ +// Package store 个人免签通道存储:独立 personal.db +// +// 表: +// - personal_devices 监听端设备(含加密通信密钥) +// - personal_pushes 收款事件流水(审计用) +// - unmatched_payments 无订单收款(人工处理) +// +// 安全设计:独立 SQLite 文件 0600,WAL 模式,设备密钥 AES-256-GCM 加密存储。 +package store + +import ( + "context" + "database/sql" + "fmt" + "os" + + _ "modernc.org/sqlite" + + "paygateway/internal/personal" +) + +// PersonalStore 个人免签通道存储接口(仅 admin + personal 通道使用) +type PersonalStore interface { + // 设备 + ListDevices(ctx context.Context) ([]*personal.DeviceEncrypted, error) + GetDevice(ctx context.Context, deviceID string) (*personal.DeviceEncrypted, error) + GetDeviceByID(ctx context.Context, id int64) (*personal.DeviceEncrypted, error) + CreateDevice(ctx context.Context, d *personal.DeviceEncrypted) (int64, error) + UpdateDevice(ctx context.Context, d *personal.DeviceEncrypted) error + DeleteDevice(ctx context.Context, id int64) error + SetDeviceStatus(ctx context.Context, id int64, status string) error + SetDevicePushTime(ctx context.Context, deviceID string) error + SetDeviceAdb(ctx context.Context, id int64, host string, port int, wifiHost string, wifiPort int) error + SetDeviceSetupProgress(ctx context.Context, id int64, progress string) error + SetDeviceControlPlane(ctx context.Context, deviceID, tsIP string, adbListen bool, controlState string) error + + // 设备 ↔ 账户 多对多绑定(应用双开多账户:一台设备服务多个收款账户) + // 设备主账户仍存于 personal_devices.account_id(兼容旧逻辑),附加账户存 device_accounts 表 + AddDeviceAccount(ctx context.Context, deviceID int64, accountID int64) error + RemoveDeviceAccount(ctx context.Context, deviceID int64, accountID int64) error + AccountBound(ctx context.Context, accountID int64) (bool, error) + ListDeviceAccounts(ctx context.Context, deviceID int64) ([]int64, error) + + // 收款推送流水 + InsertPush(ctx context.Context, p *personal.Push) error + ListPushes(ctx context.Context, limit int) ([]*personal.Push, error) + CountPushes(ctx context.Context) (int64, error) + PushExists(ctx context.Context, deviceID, rawHash string) (bool, error) + + // 无订单收款 + CreateUnmatched(ctx context.Context, u *personal.UnmatchedPayment) (int64, error) + ListUnmatched(ctx context.Context, status string, limit int) ([]*personal.UnmatchedPayment, error) + CountUnmatched(ctx context.Context, status string) (int64, error) + ResolveUnmatched(ctx context.Context, id int64, status, handler string) error + + // 动态收款码 + EnsureQrcodeSchema(ctx context.Context) error + UpsertQrcode(ctx context.Context, q *personal.DeviceQrcode) (int64, error) + GetLatestQrcode(ctx context.Context, deviceID string) (*personal.DeviceQrcode, error) + GetLatestQrcodeByPlatform(ctx context.Context, deviceID, platform string) (*personal.DeviceQrcode, error) + GetLatestQrcodeByPlatformUser(ctx context.Context, deviceID, platform string, userID int) (*personal.DeviceQrcode, error) + ListQrcodes(ctx context.Context, limit int) ([]*personal.DeviceQrcode, error) + DeleteExpiredQrcodes(ctx context.Context) (int64, error) + DeleteQrcodeByDevice(ctx context.Context, deviceID string) error + DeleteQrcode(ctx context.Context, deviceID, platform string, userID int) error + + // 监听会话(事件驱动收款监听,docs/PLAN-EVENT-MONITOR.md) + + Close() error +} + +// PersonalDB 独立个人通道数据库 +type PersonalDB struct { + db *sql.DB +} + +// NewPersonalDB 打开/创建独立个人通道数据库(0600 + WAL) +func NewPersonalDB(path string) (*PersonalDB, error) { + fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, fmt.Errorf("创建个人通道数据库文件失败: %w", err) + } + fd.Close() + if err := os.Chmod(path, 0600); err != nil { + return nil, fmt.Errorf("设置个人通道数据库权限失败: %w", err) + } + db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=10000&_synchronous=FULL") + if err != nil { + return nil, fmt.Errorf("打开个人通道数据库失败: %w", err) + } + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + p := &PersonalDB{db: db} + if err := p.migrate(); err != nil { + db.Close() + return nil, err + } + return p, nil +} + +// migrate 建表(幂等) +func (p *PersonalDB) migrate() error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS personal_devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL UNIQUE, + name TEXT NOT NULL DEFAULT '', + account_id INTEGER NOT NULL DEFAULT 0, + provider TEXT NOT NULL DEFAULT '', + secret_enc TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'offline', + last_heartbeat TEXT NOT NULL DEFAULT '', + last_push TEXT NOT NULL DEFAULT '', + ip TEXT NOT NULL DEFAULT '', + adb_host TEXT NOT NULL DEFAULT '', + adb_port INTEGER NOT NULL DEFAULT 0, + adb_wifi_host TEXT NOT NULL DEFAULT '', + adb_wifi_port INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_personal_devices_status ON personal_devices(status)`, + `CREATE INDEX IF NOT EXISTS idx_personal_devices_account ON personal_devices(account_id)`, + `CREATE TABLE IF NOT EXISTS personal_pushes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL, + provider TEXT NOT NULL, + amount_minor INTEGER NOT NULL DEFAULT 0, + note TEXT NOT NULL DEFAULT '', + raw_hash TEXT NOT NULL DEFAULT '', + matched_order TEXT NOT NULL DEFAULT '', + match_result TEXT NOT NULL DEFAULT '', + risk_text TEXT NOT NULL DEFAULT '', + raw TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_personal_pushes_device ON personal_pushes(device_id, created_at)`, + `CREATE UNIQUE INDEX IF NOT EXISTS idx_personal_pushes_dedup ON personal_pushes(device_id, raw_hash) WHERE raw_hash != ''`, + `CREATE TABLE IF NOT EXISTS unmatched_payments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider TEXT NOT NULL, + amount_minor INTEGER NOT NULL DEFAULT 0, + note TEXT NOT NULL DEFAULT '', + raw TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + handler TEXT NOT NULL DEFAULT '', + payer TEXT NOT NULL DEFAULT '', + device_id TEXT NOT NULL DEFAULT '', + risk_tags TEXT NOT NULL DEFAULT '', + risk_level INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_unmatched_status ON unmatched_payments(status, created_at)`, + `CREATE TABLE IF NOT EXISTS device_accounts ( + device_id INTEGER NOT NULL, + account_id INTEGER NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (device_id, account_id) + )`, + `CREATE INDEX IF NOT EXISTS idx_device_accounts_account ON device_accounts(account_id)`, + `CREATE TABLE IF NOT EXISTS device_qrcodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL, + platform TEXT NOT NULL DEFAULT '', + qr_content TEXT NOT NULL DEFAULT '', + account TEXT NOT NULL DEFAULT '', + expire_in INTEGER NOT NULL DEFAULT 0, + expire_at TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'active', + pending_fetch INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_device_qrcodes_device ON device_qrcodes(device_id)`, + `CREATE TABLE IF NOT EXISTS monitor_sessions ( + account_id INTEGER PRIMARY KEY, + device_id TEXT NOT NULL DEFAULT '', + provider TEXT NOT NULL DEFAULT '', + state TEXT NOT NULL DEFAULT 'idle', + active INTEGER NOT NULL DEFAULT 0, + retry INTEGER NOT NULL DEFAULT 0, + last_error TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT '' + )`, + `CREATE TABLE IF NOT EXISTS ledger_checksums ( + device_id TEXT NOT NULL, + account_id INTEGER NOT NULL DEFAULT 0, + fingerprint TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT '', + PRIMARY KEY (device_id, account_id, fingerprint) + )`, + `CREATE INDEX IF NOT EXISTS idx_ledger_checksums_dev ON ledger_checksums(device_id, account_id)`, + `PRAGMA journal_mode=WAL`, + `PRAGMA synchronous=FULL`, + } + for _, stmt := range stmts { + if _, err := p.db.Exec(stmt); err != nil { + return fmt.Errorf("个人通道数据库迁移失败: %w", err) + } + } + // 兼容迁移:老库补 risk_text 列(已存在则忽略错误) + _, _ = p.db.Exec(`ALTER TABLE personal_pushes ADD COLUMN risk_text TEXT NOT NULL DEFAULT ''`) + // 兼容迁移:unmatched_payments 补风控留痕列(payer/device/risk_tags/risk_level) + _, _ = p.db.Exec(`ALTER TABLE unmatched_payments ADD COLUMN payer TEXT NOT NULL DEFAULT ''`) + _, _ = p.db.Exec(`ALTER TABLE unmatched_payments ADD COLUMN device_id TEXT NOT NULL DEFAULT ''`) + _, _ = p.db.Exec(`ALTER TABLE unmatched_payments ADD COLUMN risk_tags TEXT NOT NULL DEFAULT ''`) + _, _ = p.db.Exec(`ALTER TABLE unmatched_payments ADD COLUMN risk_level INTEGER NOT NULL DEFAULT 0`) + // 索引在 ALTER 之后建(老库 payer 列由上面 ALTER 补齐;放 stmts 会让老库启动直接失败)——生产事故 2026-09-14 + _, _ = p.db.Exec(`CREATE INDEX IF NOT EXISTS idx_unmatched_payer ON unmatched_payments(payer, created_at)`) + // 兼容迁移:老库补 space 列(通知来源空间:0=主空间,999=双开(XSpace)) + _, _ = p.db.Exec(`ALTER TABLE personal_pushes ADD COLUMN space INTEGER NOT NULL DEFAULT 0`) + // 兼容迁移:老库补 adb 连接列(web 可视化操作设备用) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN adb_host TEXT NOT NULL DEFAULT ''`) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN adb_port INTEGER NOT NULL DEFAULT 0`) + // 兼容迁移:老库补 WiFi 备用通道列(USB 断线自动切换用) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN adb_wifi_host TEXT NOT NULL DEFAULT ''`) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN adb_wifi_port INTEGER NOT NULL DEFAULT 0`) + // 兼容迁移:老库补 setup_progress 列(引导向导完成度,JSON 数组字符串;缺省 '[]' = 未配置) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN setup_progress TEXT NOT NULL DEFAULT '[]'`) + // 兼容迁移:老库补 device_qrcodes.account 列(收款账户标识,昵称/微信号) + _, _ = p.db.Exec(`ALTER TABLE device_qrcodes ADD COLUMN account TEXT NOT NULL DEFAULT ''`) + // 兼容迁移:P2P 组网控制面(docs/P2P-ADB-RECOVERY.md Phase 1) + // ts_ip : 设备组网 IP(Tailscale 100.x.x.x,未上线为空) + // adb_listen : adbd TCP 监听是否就绪(心跳上报,0/1) + // control_state : 控制面状态:usb / tunnel_only + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN ts_ip TEXT NOT NULL DEFAULT ''`) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN adb_listen INTEGER NOT NULL DEFAULT 0`) + _, _ = p.db.Exec(`ALTER TABLE personal_devices ADD COLUMN control_state TEXT NOT NULL DEFAULT 'tunnel_only'`) + return nil +} + +// Close 关闭数据库 +func (p *PersonalDB) Close() error { return p.db.Close() } diff --git a/internal/store/personalstore_device_accounts.go b/internal/store/personalstore_device_accounts.go new file mode 100644 index 0000000..1e07980 --- /dev/null +++ b/internal/store/personalstore_device_accounts.go @@ -0,0 +1,102 @@ +package store + +import "context" + +// ---------- 设备 ↔ 账户 多对多绑定(应用双开多账户)---------- +// +// 一台物理设备(双开微信/支付宝)可服务多个收款账户: +// - 主账户存 personal_devices.account_id(兼容旧逻辑 + 金额兜底主校验) +// - 附加账户存 device_accounts 表(每账户一行) +// 匹配时校验订单账户 ∈ {主账户} ∪ {附加账户集合}。 + +// AddDeviceAccount 绑定附加账户到设备(幂等:已绑返回 nil) +func (p *PersonalDB) AddDeviceAccount(ctx context.Context, deviceID int64, accountID int64) error { + _, err := p.db.ExecContext(ctx, + `INSERT OR IGNORE INTO device_accounts (device_id, account_id, created_at) VALUES (?, ?, ?)`, + deviceID, accountID, nowStr()) + return err +} + +// RemoveDeviceAccount 解绑附加账户(幂等) +func (p *PersonalDB) RemoveDeviceAccount(ctx context.Context, deviceID int64, accountID int64) error { + _, err := p.db.ExecContext(ctx, + `DELETE FROM device_accounts WHERE device_id=? AND account_id=?`, deviceID, accountID) + return err +} + +// AccountBound 查询账户是否仍被任何设备绑定 +func (p *PersonalDB) AccountBound(ctx context.Context, accountID int64) (bool, error) { + var n int + err := p.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM device_accounts WHERE account_id=?`, accountID).Scan(&n) + return n > 0, err +} + +// ListDeviceAccounts 列出设备全部附加账户 ID(不含主账户 account_id) +func (p *PersonalDB) ListDeviceAccounts(ctx context.Context, deviceID int64) ([]int64, error) { + rows, err := p.db.QueryContext(ctx, + `SELECT account_id FROM device_accounts WHERE device_id=? ORDER BY account_id`, deviceID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + out = append(out, id) + } + return out, rows.Err() +} + +// DeviceIDsOfAccount 反向查询:绑定了该账户(附加绑定或主账户)的所有设备 ID(int64 主键)。 +// 一台设备承载多账户多通道(device_accounts 多对多),账户级判定必须走账户→设备反查, +// 不能按 personal_devices.provider 单值过滤(跨通道订单会误判设备离线)。 +func (p *PersonalDB) DeviceIDsOfAccount(ctx context.Context, accountID int64) ([]int64, error) { + rows, err := p.db.QueryContext(ctx, + `SELECT device_id FROM device_accounts WHERE account_id=? + UNION + SELECT id FROM personal_devices WHERE account_id=? + ORDER BY 1`, accountID, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []int64 + seen := make(map[int64]bool) + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + if !seen[id] { + seen[id] = true + out = append(out, id) + } + } + return out, rows.Err() +} + +// DeviceAccountIDs 返回设备全部绑定账户 ID(主账户 + 附加账户,去重)。 +// 供匹配引擎/守卫查询设备可服务的账户集合。参数 d 可传 nil(仅附加账户)。 +func DeviceAccountIDs(ctx context.Context, pdb PersonalStore, deviceID int64, primaryAccountID int64) []int64 { + extra, err := pdb.ListDeviceAccounts(ctx, deviceID) + if err != nil { + extra = nil + } + seen := make(map[int64]bool) + var out []int64 + add := func(id int64) { + if id > 0 && !seen[id] { + seen[id] = true + out = append(out, id) + } + } + add(primaryAccountID) + for _, id := range extra { + add(id) + } + return out +} diff --git a/internal/store/personalstore_devices.go b/internal/store/personalstore_devices.go new file mode 100755 index 0000000..e1921fc --- /dev/null +++ b/internal/store/personalstore_devices.go @@ -0,0 +1,147 @@ +package store + +import ( + "context" + "database/sql" + + "paygateway/internal/personal" +) + +// ---------- 设备 CRUD ---------- + +// ListDevices 列出全部设备 +func (p *PersonalDB) ListDevices(ctx context.Context) ([]*personal.DeviceEncrypted, error) { + rows, err := p.db.QueryContext(ctx, + `SELECT id, device_id, name, account_id, provider, secret_enc, status, last_heartbeat, last_push, ip, adb_host, adb_port, adb_wifi_host, adb_wifi_port, setup_progress, ts_ip, adb_listen, control_state, created_at + FROM personal_devices ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []*personal.DeviceEncrypted + for rows.Next() { + e, err := scanDeviceEncrypted(rows) + if err != nil { + return nil, err + } + out = append(out, e) + } + return out, rows.Err() +} + +// GetDevice 按 device_id 获取设备 +func (p *PersonalDB) GetDevice(ctx context.Context, deviceID string) (*personal.DeviceEncrypted, error) { + row := p.db.QueryRowContext(ctx, + `SELECT id, device_id, name, account_id, provider, secret_enc, status, last_heartbeat, last_push, ip, adb_host, adb_port, adb_wifi_host, adb_wifi_port, setup_progress, ts_ip, adb_listen, control_state, created_at + FROM personal_devices WHERE device_id=?`, deviceID) + return scanDeviceEncrypted(row) +} + +// GetDeviceByID 按 ID 获取设备(配置二维码用) +func (p *PersonalDB) GetDeviceByID(ctx context.Context, id int64) (*personal.DeviceEncrypted, error) { + row := p.db.QueryRowContext(ctx, + `SELECT id, device_id, name, account_id, provider, secret_enc, status, last_heartbeat, last_push, ip, adb_host, adb_port, adb_wifi_host, adb_wifi_port, setup_progress, ts_ip, adb_listen, control_state, created_at + FROM personal_devices WHERE id=?`, id) + return scanDeviceEncrypted(row) +} + +// CreateDevice 创建设备,返回新 ID +func (p *PersonalDB) CreateDevice(ctx context.Context, d *personal.DeviceEncrypted) (int64, error) { + progress := d.SetupProgress + if progress == "" { + progress = "[]" + } + res, err := p.db.ExecContext(ctx, + `INSERT INTO personal_devices (device_id, name, account_id, provider, secret_enc, status, last_heartbeat, last_push, ip, adb_host, adb_port, adb_wifi_host, adb_wifi_port, setup_progress, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + d.DeviceID, d.Name, d.AccountID, d.Provider, d.SecretEnc, + d.Status, "", d.LastPush, d.IP, d.AdbHost, d.AdbPort, d.AdbWifiHost, d.AdbWifiPort, progress, nowStr()) + if err != nil { + return 0, err + } + return res.LastInsertId() +} + +// UpdateDevice 更新设备 +func (p *PersonalDB) UpdateDevice(ctx context.Context, d *personal.DeviceEncrypted) error { + progress := d.SetupProgress + if progress == "" { + progress = "[]" + } + _, err := p.db.ExecContext(ctx, + `UPDATE personal_devices SET name=?, account_id=?, provider=?, secret_enc=?, status=?, last_heartbeat=?, last_push=?, ip=?, adb_host=?, adb_port=?, adb_wifi_host=?, adb_wifi_port=?, setup_progress=? + WHERE id=?`, + d.Name, d.AccountID, d.Provider, d.SecretEnc, d.Status, "", d.LastPush, d.IP, d.AdbHost, d.AdbPort, d.AdbWifiHost, d.AdbWifiPort, progress, d.ID) + return err +} + +// SetDeviceAdb 更新设备的 adb 连接信息(USB serial / host:port / WiFi 备用通道) +func (p *PersonalDB) SetDeviceAdb(ctx context.Context, id int64, host string, port int, wifiHost string, wifiPort int) error { + _, err := p.db.ExecContext(ctx, + `UPDATE personal_devices SET adb_host=?, adb_port=?, adb_wifi_host=?, adb_wifi_port=? WHERE id=?`, host, port, wifiHost, wifiPort, id) + return err +} + +// DeleteDevice 删除设备 +func (p *PersonalDB) DeleteDevice(ctx context.Context, id int64) error { + _, err := p.db.ExecContext(ctx, `DELETE FROM personal_devices WHERE id=?`, id) + return err +} + +// SetDeviceStatus 设置设备状态 +func (p *PersonalDB) SetDeviceStatus(ctx context.Context, id int64, status string) error { + _, err := p.db.ExecContext(ctx, + `UPDATE personal_devices SET status=? WHERE id=?`, status, id) + return err +} + +// SetDevicePushTime 更新最后推送时间 +func (p *PersonalDB) SetDevicePushTime(ctx context.Context, deviceID string) error { + _, err := p.db.ExecContext(ctx, + `UPDATE personal_devices SET last_push=? WHERE device_id=?`, + nowStr(), deviceID) + return err +} + +// SetDeviceSetupProgress 更新引导向导完成度(JSON 数组字符串) +func (p *PersonalDB) SetDeviceSetupProgress(ctx context.Context, id int64, progress string) error { + if progress == "" { + progress = "[]" + } + _, err := p.db.ExecContext(ctx, + `UPDATE personal_devices SET setup_progress=? WHERE id=?`, progress, id) + return err +} + +// SetDeviceControlPlane 更新设备的 P2P 控制面状态(心跳上报 ts_ip + adb_listen;reconciler 维护 control_state) +func (p *PersonalDB) SetDeviceControlPlane(ctx context.Context, deviceID, tsIP string, adbListen bool, controlState string) error { + if controlState == "" { + controlState = personal.ControlStateTunnelOnly + } + _, err := p.db.ExecContext(ctx, + `UPDATE personal_devices SET ts_ip=?, adb_listen=?, control_state=? WHERE device_id=?`, + tsIP, boolToInt(adbListen), controlState, deviceID) + return err +} + +// scanDeviceEncrypted 扫描一行到 DeviceEncrypted +func scanDeviceEncrypted(s rowScanner) (*personal.DeviceEncrypted, error) { + var e personal.DeviceEncrypted + var adbListen int + var lastHeartbeat string // 遗留列(listener 弃用后不再维护,仅占位避免改 schema) + err := s.Scan(&e.ID, &e.DeviceID, &e.Name, &e.AccountID, &e.Provider, + &e.SecretEnc, &e.Status, &lastHeartbeat, &e.LastPush, &e.IP, &e.AdbHost, &e.AdbPort, + &e.AdbWifiHost, &e.AdbWifiPort, &e.SetupProgress, &e.TsIP, &adbListen, &e.ControlState, &e.CreatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + e.AdbListen = adbListen + if e.ControlState == "" { + e.ControlState = personal.ControlStateTunnelOnly + } + return &e, nil +} diff --git a/internal/store/personalstore_ledger.go b/internal/store/personalstore_ledger.go new file mode 100644 index 0000000..e07d05a --- /dev/null +++ b/internal/store/personalstore_ledger.go @@ -0,0 +1,70 @@ +package store + +import ( + "context" + "strings" +) + +// ledgerSeenTable ledger_checksums:账本页增量检测基线持久化(docs/PLAN-ADB-LEDGER-IMPL.md §6.2)。 +// 每 (device_id, account_id) 保存已见指纹集合(时间|金额),服务端重启后恢复, +// 保证重启期间的新收款不因基线重建而漏检。集合小(可见行 ≤ 数十),覆盖写即可。 +const ledgerSeenTable = `CREATE TABLE IF NOT EXISTS ledger_checksums ( + device_id TEXT NOT NULL, + account_id INTEGER NOT NULL DEFAULT 0, + fingerprint TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT '', + PRIMARY KEY (device_id, account_id, fingerprint) +)` + +// LoadSeen 加载某 (设备, 账户) 已见指纹集合。无记录返回空切片(nil, 非错误)。 +// 实现 ledgermon.SeenStore 接口(ledgermon 通过最小接口注入,避免与 PersonalStore 全接口耦合)。 +func (p *PersonalDB) LoadSeen(ctx context.Context, deviceID string, accountID int64) ([]string, error) { + rows, err := p.db.QueryContext(ctx, + `SELECT fingerprint FROM ledger_checksums WHERE device_id = ? AND account_id = ?`, + deviceID, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []string + for rows.Next() { + var fp string + if err := rows.Scan(&fp); err != nil { + return nil, err + } + out = append(out, fp) + } + return out, rows.Err() +} + +// SaveSeen 覆盖写某 (设备, 账户) 的已见指纹集合(先删后插,单连接串行安全)。 +func (p *PersonalDB) SaveSeen(ctx context.Context, deviceID string, accountID int64, fingerprints []string) error { + tx, err := p.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.ExecContext(ctx, + `DELETE FROM ledger_checksums WHERE device_id = ? AND account_id = ?`, deviceID, accountID); err != nil { + return err + } + now := nowStr() + for _, fp := range fingerprints { + if strings.TrimSpace(fp) == "" { + continue + } + if _, err := tx.ExecContext(ctx, + `INSERT OR IGNORE INTO ledger_checksums (device_id, account_id, fingerprint, created_at) VALUES (?,?,?,?)`, + deviceID, accountID, fp, now); err != nil { + return err + } + } + return tx.Commit() +} + +// ClearSeen 清理某 (设备, 账户) 基线(设备解绑/账户变更时用) +func (p *PersonalDB) ClearSeen(ctx context.Context, deviceID string, accountID int64) error { + _, err := p.db.ExecContext(ctx, + `DELETE FROM ledger_checksums WHERE device_id = ? AND account_id = ?`, deviceID, accountID) + return err +} diff --git a/internal/store/personalstore_migration_test.go b/internal/store/personalstore_migration_test.go new file mode 100644 index 0000000..6a698ec --- /dev/null +++ b/internal/store/personalstore_migration_test.go @@ -0,0 +1,53 @@ +// 老库迁移回归测试:旧 schema(无 payer 列)→ NewPersonalDB 必须成功补列+建索引。 +// 生产事故 2026-09-14:idx_unmatched_payer 放主迁移列表,老库启动直接崩。 +package store + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + + "paygateway/internal/personal" +) + +func TestPersonalDBMigratesOldUnmatchedSchema(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "personal.db") + + // 先手工建旧 schema(无 payer/device_id/risk_tags/risk_level 列) + raw, err := sql.Open("sqlite", dbPath) + if err != nil { + t.Fatalf("打开旧库失败: %v", err) + } + if _, err := raw.Exec(`CREATE TABLE unmatched_payments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, provider TEXT NOT NULL, + amount_minor INTEGER NOT NULL DEFAULT 0, note TEXT NOT NULL DEFAULT '', + raw TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'pending', + handler TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL)`); err != nil { + t.Fatalf("建旧表失败: %v", err) + } + if _, err := raw.Exec(`INSERT INTO unmatched_payments (provider, amount_minor, created_at) VALUES ('alipay', 10, '2026-09-14T10:00:00+08:00')`); err != nil { + t.Fatalf("插旧数据失败: %v", err) + } + if err := raw.Close(); err != nil { + t.Fatal(err) + } + + pdb, err := NewPersonalDB(dbPath) + if err != nil { + t.Fatalf("旧库迁移应成功: %v", err) + } + defer pdb.Close() + + list, err := pdb.ListUnmatched(context.Background(), "", 10) + if err != nil { + t.Fatalf("ListUnmatched 应读到旧数据: %v", err) + } + if len(list) != 1 || list[0].AmountMinor != 10 { + t.Fatalf("旧行应保留: %+v", list) + } + if _, err := pdb.CreateUnmatched(context.Background(), &personal.UnmatchedPayment{Provider: "wechat", AmountMinor: 5}); err != nil { + t.Fatalf("新库写入(含风控列)应成功: %v", err) + } +} diff --git a/internal/store/personalstore_pushes.go b/internal/store/personalstore_pushes.go new file mode 100755 index 0000000..c37d2d0 --- /dev/null +++ b/internal/store/personalstore_pushes.go @@ -0,0 +1,146 @@ +package store + +import ( + "context" + "database/sql" + "time" + + "paygateway/internal/personal" +) + +// ---------- 收款推送流水 ---------- + +// InsertPush 写入收款事件流水(审计) +func (p *PersonalDB) InsertPush(ctx context.Context, pu *personal.Push) error { + _, err := p.db.ExecContext(ctx, + `INSERT INTO personal_pushes (device_id, provider, amount_minor, note, raw_hash, matched_order, match_result, risk_text, space, raw, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + pu.DeviceID, pu.Provider, pu.AmountMinor, pu.Note, pu.RawHash, + pu.MatchedOrder, pu.MatchResult, pu.RiskText, pu.Space, pu.Raw, nowStr()) + return err +} + +// ListPushes 列出收款事件流水 +func (p *PersonalDB) ListPushes(ctx context.Context, limit int) ([]*personal.Push, error) { + if limit <= 0 || limit > 500 { + limit = 50 + } + rows, err := p.db.QueryContext(ctx, + `SELECT id, device_id, provider, amount_minor, note, raw_hash, matched_order, match_result, risk_text, space, raw, created_at + FROM personal_pushes ORDER BY id DESC LIMIT ?`, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []*personal.Push + for rows.Next() { + var pu personal.Push + var created string + if err := rows.Scan(&pu.ID, &pu.DeviceID, &pu.Provider, &pu.AmountMinor, &pu.Note, + &pu.RawHash, &pu.MatchedOrder, &pu.MatchResult, &pu.RiskText, &pu.Space, &pu.Raw, &created); err != nil { + return nil, err + } + pu.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &pu) + } + if out == nil { + out = []*personal.Push{} + } + return out, rows.Err() +} + +// CountPushes 统计收款事件数 +func (p *PersonalDB) CountPushes(ctx context.Context) (int64, error) { + var n int64 + err := p.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM personal_pushes`).Scan(&n) + return n, err +} + +// PushExists 幂等去重:同设备同通知原文哈希是否已处理 +func (p *PersonalDB) PushExists(ctx context.Context, deviceID, rawHash string) (bool, error) { + if rawHash == "" { + return false, nil + } + var n int64 + err := p.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM personal_pushes WHERE device_id=? AND raw_hash=?`, + deviceID, rawHash).Scan(&n) + if err != nil { + return false, err + } + return n > 0, nil +} + +// ---------- 无订单收款 ---------- + +// CreateUnmatched 创建无订单收款记录 +func (p *PersonalDB) CreateUnmatched(ctx context.Context, u *personal.UnmatchedPayment) (int64, error) { + res, err := p.db.ExecContext(ctx, + `INSERT INTO unmatched_payments (provider, amount_minor, note, raw, status, handler, payer, device_id, risk_tags, risk_level, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + u.Provider, u.AmountMinor, u.Note, u.Raw, u.Status, u.Handler, u.Payer, u.DeviceID, u.RiskTags, u.RiskLevel, nowStr()) + if err != nil { + return 0, err + } + return res.LastInsertId() +} + +// unmatchedCols 无订单收款查询列(与 ListUnmatched 扫描顺序一致) +var unmatchedCols = `id, provider, amount_minor, note, raw, status, handler, payer, device_id, risk_tags, risk_level, created_at` + +// ListUnmatched 列出无订单收款 +func (p *PersonalDB) ListUnmatched(ctx context.Context, status string, limit int) ([]*personal.UnmatchedPayment, error) { + if limit <= 0 || limit > 500 { + limit = 50 + } + var rows *sql.Rows + var err error + if status == "" { + rows, err = p.db.QueryContext(ctx, + `SELECT `+unmatchedCols+` FROM unmatched_payments ORDER BY id DESC LIMIT ?`, limit) + } else { + rows, err = p.db.QueryContext(ctx, + `SELECT `+unmatchedCols+` FROM unmatched_payments WHERE status=? ORDER BY id DESC LIMIT ?`, status, limit) + } + if err != nil { + return nil, err + } + defer rows.Close() + + var out []*personal.UnmatchedPayment + for rows.Next() { + var u personal.UnmatchedPayment + var created string + if err := rows.Scan(&u.ID, &u.Provider, &u.AmountMinor, &u.Note, &u.Raw, + &u.Status, &u.Handler, &u.Payer, &u.DeviceID, &u.RiskTags, &u.RiskLevel, &created); err != nil { + return nil, err + } + u.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &u) + } + if out == nil { + out = []*personal.UnmatchedPayment{} + } + return out, rows.Err() +} + +// CountUnmatched 统计无订单收款数 +func (p *PersonalDB) CountUnmatched(ctx context.Context, status string) (int64, error) { + var n int64 + var err error + if status == "" { + err = p.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM unmatched_payments`).Scan(&n) + } else { + err = p.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM unmatched_payments WHERE status=?`, status).Scan(&n) + } + return n, err +} + +// ResolveUnmatched 处理无订单收款 +func (p *PersonalDB) ResolveUnmatched(ctx context.Context, id int64, status, handler string) error { + _, err := p.db.ExecContext(ctx, + `UPDATE unmatched_payments SET status=?, handler=? WHERE id=?`, + status, handler, id) + return err +} diff --git a/internal/store/personalstore_qrcodes.go b/internal/store/personalstore_qrcodes.go new file mode 100644 index 0000000..69f7663 --- /dev/null +++ b/internal/store/personalstore_qrcodes.go @@ -0,0 +1,194 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "time" + + "paygateway/internal/personal" +) + +// ---------- 设备动态收款码 CRUD ---------- + +// EnsureQrcodeSchema 迁移 device_qrcodes 表,确保含 user_id 列。 +// 首次部署或旧表升级时自动执行(幂等:列已存在则跳过)。 +func (p *PersonalDB) EnsureQrcodeSchema(ctx context.Context) error { + // 1. 检查 user_id 列是否已存在 + var count int + err := p.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM pragma_table_info('device_qrcodes') WHERE name='user_id'`).Scan(&count) + if err != nil { + return fmt.Errorf("检查 device_qrcodes 结构: %w", err) + } + if count > 0 { + return nil // 已迁移 + } + // 2. 迁移:旧表重命名 → 建新表 → 复制数据 → 删旧表 + // 旧唯一约束 (device_id, platform) → 新唯一约束 (device_id, platform, user_id) + tx, err := p.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("开始迁移事务: %w", err) + } + defer tx.Rollback() + + _, err = tx.ExecContext(ctx, `ALTER TABLE device_qrcodes RENAME TO device_qrcodes_old`) + if err != nil { + return fmt.Errorf("重命名旧表: %w", err) + } + _, err = tx.ExecContext(ctx, ` + CREATE TABLE device_qrcodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT NOT NULL, + platform TEXT NOT NULL DEFAULT '', + user_id INTEGER NOT NULL DEFAULT 0, + qr_content TEXT NOT NULL DEFAULT '', + account TEXT NOT NULL DEFAULT '', + expire_in INTEGER NOT NULL DEFAULT 0, + expire_at TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'active', + pending_fetch INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + UNIQUE(device_id, platform, user_id) + )`) + if err != nil { + return fmt.Errorf("建新表: %w", err) + } + _, err = tx.ExecContext(ctx, ` + INSERT INTO device_qrcodes (id, device_id, platform, user_id, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at) + SELECT id, device_id, platform, 0, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at + FROM device_qrcodes_old`) + if err != nil { + return fmt.Errorf("迁移数据: %w", err) + } + _, err = tx.ExecContext(ctx, `DROP TABLE device_qrcodes_old`) + if err != nil { + return fmt.Errorf("删除旧表: %w", err) + } + return tx.Commit() +} + +// UpsertQrcode 写入/更新设备指定平台+用户空间的收款码(每设备每平台每用户空间幂等覆盖)。 +// 同一设备可同时持有 wechat(主空间) + wechat(双开) + alipay(主空间) + alipay(双开) 四条互不覆盖。 +func (p *PersonalDB) UpsertQrcode(ctx context.Context, q *personal.DeviceQrcode) (int64, error) { + var id int64 + err := p.db.QueryRowContext(ctx, + `SELECT id FROM device_qrcodes WHERE device_id=? AND platform=? AND user_id=? ORDER BY id DESC LIMIT 1`, + q.DeviceID, q.Platform, q.UserID).Scan(&id) + if err == nil { + _, uerr := p.db.ExecContext(ctx, + `UPDATE device_qrcodes SET qr_content=?, account=?, expire_in=?, expire_at=?, status=?, created_at=? WHERE id=?`, + q.QRContent, q.Account, q.ExpireIn, q.ExpireAt, q.Status, q.CreatedAt, id) + return id, uerr + } + if err != sql.ErrNoRows { + return 0, err + } + res, ierr := p.db.ExecContext(ctx, + `INSERT INTO device_qrcodes (device_id, platform, user_id, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`, + q.DeviceID, q.Platform, q.UserID, q.QRContent, q.Account, q.ExpireIn, q.ExpireAt, q.Status, q.CreatedAt) + if ierr != nil { + return 0, ierr + } + return res.LastInsertId() +} + +// GetLatestQrcode 获取设备最新收款码(任意平台、任意用户空间) +func (p *PersonalDB) GetLatestQrcode(ctx context.Context, deviceID string) (*personal.DeviceQrcode, error) { + row := p.db.QueryRowContext(ctx, + `SELECT id, device_id, platform, user_id, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at + FROM device_qrcodes WHERE device_id=? ORDER BY id DESC LIMIT 1`, deviceID) + return scanQrcode(row) +} + +// GetLatestQrcodeByPlatform 获取设备指定平台的最新收款码(跨用户空间,取最新一条;用于旧版去重/兼容) +func (p *PersonalDB) GetLatestQrcodeByPlatform(ctx context.Context, deviceID, platform string) (*personal.DeviceQrcode, error) { + row := p.db.QueryRowContext(ctx, + `SELECT id, device_id, platform, user_id, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at + FROM device_qrcodes WHERE device_id=? AND platform=? ORDER BY id DESC LIMIT 1`, deviceID, platform) + return scanQrcode(row) +} + +// GetLatestQrcodeByPlatformUser 获取设备指定平台+用户空间的最新收款码(续期调度用) +func (p *PersonalDB) GetLatestQrcodeByPlatformUser(ctx context.Context, deviceID, platform string, userID int) (*personal.DeviceQrcode, error) { + row := p.db.QueryRowContext(ctx, + `SELECT id, device_id, platform, user_id, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at + FROM device_qrcodes WHERE device_id=? AND platform=? AND user_id=? ORDER BY id DESC LIMIT 1`, + deviceID, platform, userID) + return scanQrcode(row) +} + +// ListQrcodes 列出最新收款码(按 id 倒序) +func (p *PersonalDB) ListQrcodes(ctx context.Context, limit int) ([]*personal.DeviceQrcode, error) { + if limit <= 0 { + limit = 100 + } + rows, err := p.db.QueryContext(ctx, + `SELECT id, device_id, platform, user_id, qr_content, account, expire_in, expire_at, status, pending_fetch, created_at + FROM device_qrcodes ORDER BY id DESC LIMIT ?`, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var out []*personal.DeviceQrcode + for rows.Next() { + q, err := scanQrcode(rows) + if err != nil { + return nil, err + } + out = append(out, q) + } + return out, rows.Err() +} + +func scanQrcode(row interface{ Scan(dest ...any) error }) (*personal.DeviceQrcode, error) { + q := &personal.DeviceQrcode{} + err := row.Scan(&q.ID, &q.DeviceID, &q.Platform, &q.UserID, &q.QRContent, &q.Account, &q.ExpireIn, &q.ExpireAt, &q.Status, &q.PendingFetch, &q.CreatedAt) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + return q, nil +} + +// DeleteExpiredQrcodes 清理过期收款码:删除已过期或 status=expired 的记录, +// 但每设备每平台每用户空间保留最新一条(防清空后无码可用)。 +// 返回删除的行数。 +func (p *PersonalDB) DeleteExpiredQrcodes(ctx context.Context) (int64, error) { + // 每设备每平台每用户空间最新一条的 id(无论状态都保留) + keep := `SELECT MAX(id) FROM device_qrcodes GROUP BY device_id, platform, user_id` + res, err := p.db.ExecContext(ctx, ` + DELETE FROM device_qrcodes + WHERE id NOT IN (`+keep+`) + AND (status = 'expired' OR expire_at <= ?)`, + time.Now().Format(time.RFC3339)) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + +// DeleteQrcodeByDevice 删除指定设备的所有收款码记录(设备解绑/删除时用) +func (p *PersonalDB) DeleteQrcodeByDevice(ctx context.Context, deviceID string) error { + _, err := p.db.ExecContext(ctx, `DELETE FROM device_qrcodes WHERE device_id=?`, deviceID) + return err +} + +// DeleteQrcode 删除单条收款码记录(deviceID+platform+userID 唯一定位) +func (p *PersonalDB) DeleteQrcode(ctx context.Context, deviceID, platform string, userID int) error { + _, err := p.db.ExecContext(ctx, + `DELETE FROM device_qrcodes WHERE device_id=? AND platform=? AND user_id=?`, + deviceID, platform, userID) + return err +} + +// ExpireAtStr 计算过期时间字符串(RFC3339,now + expireIn 秒;expireIn<=0 默认 90s) +func ExpireAtStr(expireIn int64) string { + if expireIn <= 0 { + expireIn = 90 + } + return time.Now().Add(time.Duration(expireIn) * time.Second).Format(time.RFC3339) +} diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go new file mode 100755 index 0000000..d894e86 --- /dev/null +++ b/internal/store/sqlite.go @@ -0,0 +1,387 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "time" + + "paygateway/internal/model" + + _ "modernc.org/sqlite" +) + +// SQLiteStore SQLite 存储实现 +type SQLiteStore struct { + db *sql.DB +} + +// NewSQLite 打开 SQLite 数据库并执行迁移 +func NewSQLite(path string) (*SQLiteStore, error) { + db, err := sql.Open("sqlite", path) + if err != nil { + return nil, fmt.Errorf("打开数据库失败: %w", err) + } + // 关键配置:WAL 模式 + busy timeout(并发安全) + db.SetMaxOpenConns(1) // SQLite 单写者,串行化保证一致性 + if _, err := db.Exec("PRAGMA journal_mode=WAL;"); err != nil { + return nil, fmt.Errorf("设置 WAL 失败: %w", err) + } + if _, err := db.Exec("PRAGMA busy_timeout=5000;"); err != nil { + return nil, fmt.Errorf("设置 busy_timeout 失败: %w", err) + } + if _, err := db.Exec("PRAGMA foreign_keys=ON;"); err != nil { + return nil, fmt.Errorf("设置外键失败: %w", err) + } + s := &SQLiteStore{db: db} + if err := s.migrate(); err != nil { + db.Close() + return nil, err + } + return s, nil +} + +// migrate 建表(幂等) +func (s *SQLiteStore) migrate() error { + // 迁移旧数据库:添加 account_id / pid / upstream_name 列(如果不存在) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN account_id INTEGER NOT NULL DEFAULT 0`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN pid TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN upstream_name TEXT NOT NULL DEFAULT ''`) + // 迁移旧数据库:个人免签通道字段(列已存在时 ALTER 报错被忽略,新库直接走 CREATE TABLE) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN note_code TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN qr_token TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN device_id TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN buyer_key TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_orders_buyer_key ON orders(buyer_key) WHERE buyer_key != ''`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN fp_key TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`ALTER TABLE orders ADD COLUMN session_expires_at TEXT NOT NULL DEFAULT ''`) + _, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_orders_fp_key ON orders(fp_key) WHERE fp_key != ''`) + stmts := []string{ + `CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + out_trade_no TEXT NOT NULL UNIQUE, + gateway_no TEXT NOT NULL UNIQUE, + channel TEXT NOT NULL, + channel_tx_id TEXT DEFAULT NULL, + account_id INTEGER NOT NULL DEFAULT 0, + pid TEXT NOT NULL DEFAULT '', + upstream_name TEXT NOT NULL DEFAULT '', + attach TEXT NOT NULL DEFAULT '', + amount_minor INTEGER NOT NULL, + currency TEXT NOT NULL DEFAULT 'CNY', + subject TEXT NOT NULL, + status TEXT NOT NULL, + notify_url TEXT, + return_url TEXT, + qr_code TEXT, + pay_url TEXT, + payer TEXT, + notified INTEGER NOT NULL DEFAULT 0, + notify_count INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + session_expires_at TEXT NOT NULL DEFAULT '', + paid_at TEXT, + notified_at TEXT, + note_code TEXT NOT NULL DEFAULT '', + qr_token TEXT NOT NULL DEFAULT '', + device_id TEXT NOT NULL DEFAULT '', + buyer_key TEXT NOT NULL DEFAULT '', + fp_key TEXT NOT NULL DEFAULT '' + )`, + `CREATE UNIQUE INDEX IF NOT EXISTS idx_orders_channel_tx ON orders(channel, channel_tx_id) WHERE channel_tx_id IS NOT NULL`, + `CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status)`, + `CREATE INDEX IF NOT EXISTS idx_orders_created ON orders(created_at)`, + `CREATE INDEX IF NOT EXISTS idx_orders_channel ON orders(channel)`, + `CREATE INDEX IF NOT EXISTS idx_orders_pid ON orders(pid)`, + `CREATE INDEX IF NOT EXISTS idx_orders_note_code ON orders(note_code) WHERE note_code != ''`, + `CREATE INDEX IF NOT EXISTS idx_orders_qr_token ON orders(qr_token) WHERE qr_token != ''`, + // 调度器/池按账户查待支付订单(ListWaitPayByAccount / ListWaitPayAll 高频路径) + `CREATE INDEX IF NOT EXISTS idx_orders_account_status ON orders(account_id, status)`, + `CREATE TABLE IF NOT EXISTS payment_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + event_type TEXT NOT NULL, + channel_tx_id TEXT, + amount_minor INTEGER NOT NULL, + raw_payload TEXT, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_events_order ON payment_events(order_id)`, + `CREATE TABLE IF NOT EXISTS notify_tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL, + payload TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + retry_count INTEGER NOT NULL DEFAULT 0, + next_retry_at TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_notify_status ON notify_tasks(status, next_retry_at)`, + `CREATE TABLE IF NOT EXISTS callback_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER NOT NULL, + http_status INTEGER, + response TEXT, + latency_ms INTEGER, + created_at TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS exception_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + out_trade_no TEXT NOT NULL UNIQUE, + pid TEXT NOT NULL DEFAULT '', + upstream_name TEXT NOT NULL DEFAULT '', + channel TEXT NOT NULL DEFAULT '', + amount_minor INTEGER NOT NULL DEFAULT 0, + order_status TEXT NOT NULL DEFAULT '', + channel_status TEXT NOT NULL DEFAULT 'UNKNOWN', + channel_tx_id TEXT NOT NULL DEFAULT '', + paid_amount INTEGER NOT NULL DEFAULT 0, + payer TEXT NOT NULL DEFAULT '', + reason TEXT NOT NULL DEFAULT '', + phase TEXT NOT NULL DEFAULT 'pending', + resolved_action TEXT NOT NULL DEFAULT '', + evidence TEXT NOT NULL DEFAULT '', + suggestion TEXT NOT NULL DEFAULT '', + handler TEXT NOT NULL DEFAULT '', + handler_note TEXT NOT NULL DEFAULT '', + resolved_at TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_exception_phase ON exception_orders(phase, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_exception_reason ON exception_orders(reason)`, + `CREATE TABLE IF NOT EXISTS exception_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + exception_id INTEGER NOT NULL, + action TEXT NOT NULL, + operator TEXT NOT NULL DEFAULT '', + detail TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_exception_logs ON exception_logs(exception_id, created_at)`, + `CREATE TABLE IF NOT EXISTS app_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL, + scope TEXT, + message TEXT NOT NULL, + trace_id TEXT, + fields TEXT, + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_logs_level_time ON app_logs(level, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_logs_scope_time ON app_logs(scope, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_logs_trace ON app_logs(trace_id)`, + `CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL + )`, + } + for _, stmt := range stmts { + if _, err := s.db.Exec(stmt); err != nil { + return fmt.Errorf("迁移失败: %w", err) + } + } + return nil +} + +func nowStr() string { return time.Now().Format(time.RFC3339Nano) } + +// ---------- 订单 ---------- + +func (s *SQLiteStore) CreateOrder(ctx context.Context, o *model.Order) error { + _, err := s.db.ExecContext(ctx, ` + INSERT INTO orders (out_trade_no, gateway_no, channel, channel_tx_id, account_id, pid, upstream_name, attach, + amount_minor, currency, subject, status, notify_url, return_url, qr_code, pay_url, payer, notified, notify_count, created_at, session_expires_at, + note_code, qr_token, device_id, buyer_key, fp_key) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + o.OutTradeNo, o.GatewayNo, o.Channel, nullIfEmpty(o.ChannelTxID), o.AccountID, o.Pid, o.UpstreamName, o.Attach, + o.AmountMinor, o.Currency, o.Subject, o.Status, o.NotifyURL, o.ReturnURL, o.QrCode, o.PayURL, o.Payer, + o.Notified, o.NotifyCount, o.CreatedAt.Format(time.RFC3339Nano), nullIfTimeValue(o.SessionExpiresAt), + o.NoteCode, o.QrToken, o.DeviceID, o.BuyerKey, o.FPKey) + return err +} + +func nullIfTimeValue(t time.Time) any { + if t.IsZero() { + return "" + } + return t.Format(time.RFC3339Nano) +} + +func nullIfEmpty(s string) any { + if s == "" { + return nil + } + return s +} + +func nullIfTime(t *time.Time) any { + if t == nil { + return nil + } + return t.Format(time.RFC3339Nano) +} + +// ReplaceOrder 原子完成买家替换:同一事务内把 WAIT_PAY 订单改为 CLOSED 并写 replaced 事件。 +// replaced 事件与关单同事务持久化 ⇒ 观察者看到 CLOSED 时必能看到 replaced 事件, +// 从而可靠区分「并发替换者关闭」(有 replaced)与「清扫器超时关闭」(无 replaced)。 +// 返回 changed=true 表示本次调用赢得替换;false 表示订单已非 WAIT_PAY(被并发结算或替换者处理)。 +func (s *SQLiteStore) ReplaceOrder(ctx context.Context, outTradeNo string, orderID int64, amountMinor int64, rawJSON string) (bool, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return false, err + } + defer tx.Rollback() + + res, err := tx.ExecContext(ctx, + `UPDATE orders SET status=?, paid_at=COALESCE(paid_at, ?) WHERE out_trade_no=? AND status=?`, + model.StatusClosed, nowStr(), outTradeNo, model.StatusWaitPay) + if err != nil { + return false, err + } + n, err := res.RowsAffected() + if err != nil { + return false, err + } + if n == 0 { + // 非 WAIT_PAY(已结算 / 已被并发替换关闭)→ 未赢得替换 + return false, nil + } + if _, err := tx.ExecContext(ctx, + `INSERT INTO payment_events (order_id, event_type, channel_tx_id, amount_minor, raw_payload, created_at) + VALUES (?, ?, ?, ?, ?, ?)`, + orderID, "replaced", "", amountMinor, rawJSON, nowStr()); err != nil { + return false, err + } + return true, tx.Commit() +} + +// UpdateOrderAmount 更新订单金额(复用订单时以最新下单金额覆盖)。 +func (s *SQLiteStore) UpdateOrderAmount(ctx context.Context, outTradeNo string, amountMinor int64) error { + _, err := s.db.ExecContext(ctx, + `UPDATE orders SET amount_minor=? WHERE out_trade_no=? AND amount_minor<>?`, + amountMinor, outTradeNo, amountMinor) + return err +} + +func (s *SQLiteStore) GetOrder(ctx context.Context, outTradeNo string) (*model.Order, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, out_trade_no, gateway_no, channel, channel_tx_id, account_id, pid, upstream_name, attach, amount_minor, + currency, subject, status, notify_url, return_url, qr_code, pay_url, payer, notified, notify_count, created_at, session_expires_at, paid_at, notified_at, + note_code, qr_token, device_id, buyer_key, fp_key + FROM orders WHERE out_trade_no = ?`, outTradeNo) + return scanOrder(row) +} + +func (s *SQLiteStore) DeleteOrder(ctx context.Context, outTradeNo string) (bool, error) { + res, err := s.db.ExecContext(ctx, + `DELETE FROM orders WHERE out_trade_no=? AND status=?`, + outTradeNo, model.StatusWaitPay) + if err != nil { + return false, err + } + n, err := res.RowsAffected() + return n > 0, err +} + +func (s *SQLiteStore) GetOrderByID(ctx context.Context, id int64) (*model.Order, error) { + row := s.db.QueryRowContext(ctx, `SELECT id, out_trade_no, gateway_no, channel, channel_tx_id, account_id, pid, upstream_name, attach, amount_minor, + currency, subject, status, notify_url, return_url, qr_code, pay_url, payer, notified, notify_count, created_at, session_expires_at, paid_at, notified_at, + note_code, qr_token, device_id, buyer_key, fp_key + FROM orders WHERE id = ?`, id) + return scanOrder(row) +} + +type rowScanner interface { + Scan(dest ...any) error +} + +func scanOrder(row rowScanner) (*model.Order, error) { + var ( + o model.Order + notified int + createdAt, sessionExpiresAt string + paidAt, nb, txID sql.NullString + ) + if err := row.Scan(&o.ID, &o.OutTradeNo, &o.GatewayNo, &o.Channel, &txID, &o.AccountID, &o.Pid, &o.UpstreamName, &o.Attach, &o.AmountMinor, + &o.Currency, &o.Subject, &o.Status, &o.NotifyURL, &o.ReturnURL, &o.QrCode, &o.PayURL, &o.Payer, + ¬ified, &o.NotifyCount, &createdAt, &sessionExpiresAt, &paidAt, &nb, + &o.NoteCode, &o.QrToken, &o.DeviceID, &o.BuyerKey, &o.FPKey); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + o.Notified = notified != 0 + o.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAt) + o.SessionExpiresAt, _ = time.Parse(time.RFC3339Nano, sessionExpiresAt) + if paidAt.Valid { + t, _ := time.Parse(time.RFC3339Nano, paidAt.String) + o.PaidAt = &t + } + if nb.Valid { + t, _ := time.Parse(time.RFC3339Nano, nb.String) + o.NotifiedAt = &t + } + if txID.Valid { + o.ChannelTxID = txID.String + } + return &o, nil +} + +// UpdateOrderStatus 条件更新:from -> to;影响行数=0 表示状态非法(幂等/防覆盖) +// set 回调可修改订单字段,修改后自动写回数据库 +func (s *SQLiteStore) UpdateOrderStatus(ctx context.Context, outTradeNo, from, to string, set func(o *model.Order)) (bool, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return false, err + } + defer tx.Rollback() + + var updateSQL string + var updateArgs []any + if from == to { + // 状态不变(如保存支付凭据),不修改 paid_at + updateSQL = `UPDATE orders SET status=? WHERE out_trade_no=? AND status=?` + updateArgs = []any{to, outTradeNo, from} + } else { + updateSQL = `UPDATE orders SET status=?, paid_at=COALESCE(paid_at, ?) WHERE out_trade_no=? AND status=?` + updateArgs = []any{to, nowStr(), outTradeNo, from} + } + res, err := tx.ExecContext(ctx, updateSQL, updateArgs...) + if err != nil { + return false, err + } + n, err := res.RowsAffected() + if err != nil { + return false, err + } + if n == 0 { + return false, nil + } + if set != nil { + // 读回最新(在事务内用单独的查询) + row := tx.QueryRowContext(ctx, `SELECT id, out_trade_no, gateway_no, channel, channel_tx_id, account_id, pid, upstream_name, attach, amount_minor, + currency, subject, status, notify_url, return_url, qr_code, pay_url, payer, notified, notify_count, created_at, session_expires_at, paid_at, notified_at, + note_code, qr_token, device_id, buyer_key, fp_key + FROM orders WHERE out_trade_no = ?`, outTradeNo) + upd, err := scanOrder(row) + if err != nil { + return false, err + } + if upd != nil { + set(upd) + // 写回修改的字段(含 channel/created_at/paid_at:订单换通道迁移/重置倒计时/复活清除支付时间用) + _, err := tx.ExecContext(ctx, + `UPDATE orders SET pay_url=?, qr_code=?, channel_tx_id=?, payer=?, account_id=?, notified=?, notify_count=?, notified_at=COALESCE(notified_at, ?), + note_code=?, qr_token=?, device_id=?, buyer_key=?, fp_key=?, channel=?, created_at=?, session_expires_at=?, paid_at=? WHERE out_trade_no=?`, + upd.PayURL, upd.QrCode, nullIfEmpty(upd.ChannelTxID), upd.Payer, upd.AccountID, upd.Notified, upd.NotifyCount, nowStr(), + upd.NoteCode, upd.QrToken, upd.DeviceID, upd.BuyerKey, upd.FPKey, upd.Channel, upd.CreatedAt.Format(time.RFC3339Nano), nullIfTimeValue(upd.SessionExpiresAt), nullIfTime(upd.PaidAt), outTradeNo) + if err != nil { + return false, err + } + } + } + return true, tx.Commit() +} diff --git a/internal/store/sqlite_exception.go b/internal/store/sqlite_exception.go new file mode 100755 index 0000000..7173b7e --- /dev/null +++ b/internal/store/sqlite_exception.go @@ -0,0 +1,183 @@ +package store + +import ( + "context" + "database/sql" + "fmt" + "time" + + "paygateway/internal/model" +) + +// ========== 人工异常处理池 ========== + +const exceptionCols = `id, out_trade_no, pid, upstream_name, channel, amount_minor, order_status, + channel_status, channel_tx_id, paid_amount, payer, reason, phase, resolved_action, + evidence, suggestion, handler, handler_note, resolved_at, updated_at, created_at` + +func scanException(row *sql.Row) (*model.ExceptionOrder, error) { + var e model.ExceptionOrder + var updatedAt, createdAt string + err := row.Scan(&e.ID, &e.OutTradeNo, &e.Pid, &e.UpstreamName, &e.Channel, &e.AmountMinor, + &e.OrderStatus, &e.ChannelStatus, &e.ChannelTxID, &e.PaidAmount, &e.Payer, &e.Reason, + &e.Phase, &e.ResolvedAction, &e.Evidence, &e.Suggestion, &e.Handler, &e.HandlerNote, + &e.ResolvedAt, &updatedAt, &createdAt) + if err != nil { + return nil, err + } + e.UpdatedAt = updatedAt + e.CreatedAt = createdAt + return &e, nil +} + +// CreateException 创建异常单(幂等:同 out_trade_no 已存在则返回已有) +func (s *SQLiteStore) CreateException(ctx context.Context, e *model.ExceptionOrder) error { + now := time.Now().Format(time.RFC3339) + if e.PaidAmount == 0 { + e.PaidAmount = e.AmountMinor + } + if e.Phase == "" { + e.Phase = model.ExPhasePending + } + if e.ChannelStatus == "" { + e.ChannelStatus = "UNKNOWN" + } + _, err := s.db.ExecContext(ctx, `INSERT INTO exception_orders + (out_trade_no, pid, upstream_name, channel, amount_minor, order_status, channel_status, + channel_tx_id, paid_amount, payer, reason, phase, resolved_action, evidence, suggestion, + handler, handler_note, resolved_at, updated_at, created_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(out_trade_no) DO NOTHING`, + e.OutTradeNo, e.Pid, e.UpstreamName, e.Channel, e.AmountMinor, e.OrderStatus, e.ChannelStatus, + e.ChannelTxID, e.PaidAmount, e.Payer, e.Reason, e.Phase, e.ResolvedAction, e.Evidence, e.Suggestion, + e.Handler, e.HandlerNote, e.ResolvedAt, now, now) + if err != nil { + return fmt.Errorf("创建异常单失败: %w", err) + } + return nil +} + +func (s *SQLiteStore) GetException(ctx context.Context, id int64) (*model.ExceptionOrder, error) { + row := s.db.QueryRowContext(ctx, `SELECT `+exceptionCols+` FROM exception_orders WHERE id = ?`, id) + return scanException(row) +} + +func (s *SQLiteStore) GetExceptionByOutTradeNo(ctx context.Context, outTradeNo string) (*model.ExceptionOrder, error) { + row := s.db.QueryRowContext(ctx, `SELECT `+exceptionCols+` FROM exception_orders WHERE out_trade_no = ?`, outTradeNo) + return scanException(row) +} + +func (s *SQLiteStore) ListExceptions(ctx context.Context, phase, reason string, limit, offset int) ([]*model.ExceptionOrder, error) { + if limit <= 0 { + limit = 50 + } + q := `SELECT ` + exceptionCols + ` FROM exception_orders WHERE 1=1` + args := []any{} + if phase != "" { + q += ` AND phase = ?` + args = append(args, phase) + } + if reason != "" { + q += ` AND reason = ?` + args = append(args, reason) + } + q += ` ORDER BY id DESC LIMIT ? OFFSET ?` + args = append(args, limit, offset) + + rows, err := s.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + var out []*model.ExceptionOrder + for rows.Next() { + var e model.ExceptionOrder + var updatedAt, createdAt string + if err := rows.Scan(&e.ID, &e.OutTradeNo, &e.Pid, &e.UpstreamName, &e.Channel, &e.AmountMinor, + &e.OrderStatus, &e.ChannelStatus, &e.ChannelTxID, &e.PaidAmount, &e.Payer, &e.Reason, + &e.Phase, &e.ResolvedAction, &e.Evidence, &e.Suggestion, &e.Handler, &e.HandlerNote, + &e.ResolvedAt, &updatedAt, &createdAt); err != nil { + return nil, err + } + e.UpdatedAt = updatedAt + e.CreatedAt = createdAt + out = append(out, &e) + } + return out, rows.Err() +} + +func (s *SQLiteStore) CountExceptions(ctx context.Context, phase string) (int64, error) { + q := `SELECT COUNT(*) FROM exception_orders` + args := []any{} + if phase != "" { + q += ` WHERE phase = ?` + args = append(args, phase) + } + var n int64 + err := s.db.QueryRowContext(ctx, q, args...).Scan(&n) + return n, err +} + +// UpdateExceptionPhase 条件更新阶段(防并发处理:pending -> processing 锁) +func (s *SQLiteStore) UpdateExceptionPhase(ctx context.Context, id int64, from, to string) (bool, error) { + res, err := s.db.ExecContext(ctx, `UPDATE exception_orders SET phase = ?, updated_at = ? WHERE id = ? AND phase = ?`, + to, time.Now().Format(time.RFC3339), id, from) + if err != nil { + return false, err + } + n, _ := res.RowsAffected() + return n > 0, nil +} + +// UpdateException 更新异常单(set 修改字段,updated_at 自动刷新) +func (s *SQLiteStore) UpdateException(ctx context.Context, id int64, set func(e *model.ExceptionOrder)) error { + e, err := s.GetException(ctx, id) + if err != nil { + return err + } + set(e) + _, err = s.db.ExecContext(ctx, `UPDATE exception_orders SET + channel_status=?, channel_tx_id=?, paid_amount=?, payer=?, reason=?, phase=?, + resolved_action=?, evidence=?, suggestion=?, handler=?, handler_note=?, resolved_at=?, updated_at=? + WHERE id=?`, + e.ChannelStatus, e.ChannelTxID, e.PaidAmount, e.Payer, e.Reason, e.Phase, + e.ResolvedAction, e.Evidence, e.Suggestion, e.Handler, e.HandlerNote, e.ResolvedAt, + time.Now().Format(time.RFC3339), id) + return err +} + +func (s *SQLiteStore) InsertExceptionLog(ctx context.Context, exceptionID int64, action, operator, detail string) error { + _, err := s.db.ExecContext(ctx, `INSERT INTO exception_logs (exception_id, action, operator, detail, created_at) + VALUES (?,?,?,?,?)`, exceptionID, action, operator, detail, time.Now().Format(time.RFC3339)) + return err +} + +func (s *SQLiteStore) ListExceptionLogs(ctx context.Context, exceptionID int64, limit int) ([]*model.ExceptionLog, error) { + if limit <= 0 { + limit = 100 + } + rows, err := s.db.QueryContext(ctx, `SELECT id, exception_id, action, operator, detail, created_at + FROM exception_logs WHERE exception_id = ? ORDER BY id DESC LIMIT ?`, exceptionID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var out []*model.ExceptionLog + for rows.Next() { + var l model.ExceptionLog + if err := rows.Scan(&l.ID, &l.ExceptionID, &l.Action, &l.Operator, &l.Detail, &l.CreatedAt); err != nil { + return nil, err + } + out = append(out, &l) + } + return out, rows.Err() +} + +// UpdateOrderHandled 订单人工处理标记(人工池处理后永久留痕) +func (s *SQLiteStore) UpdateOrderHandled(ctx context.Context, outTradeNo string, exceptionID int64, action, handler, note string) error { + _, err := s.db.ExecContext(ctx, `UPDATE orders SET + exception_id = ?, handled_by = ?, handled_action = ?, handled_note = ?, handled_at = ? + WHERE out_trade_no = ?`, + exceptionID, handler, action, note, time.Now().Format(time.RFC3339), outTradeNo) + return err +} diff --git a/internal/store/sqlite_rest.go b/internal/store/sqlite_rest.go new file mode 100755 index 0000000..4b7ec01 --- /dev/null +++ b/internal/store/sqlite_rest.go @@ -0,0 +1,697 @@ +package store + +import ( + "context" + "database/sql" + "strings" + "time" + + "paygateway/internal/model" +) + +// ---------- 订单查询 ---------- + +func (s *SQLiteStore) ListOrders(ctx context.Context, status, channel string, limit, offset int) ([]*model.Order, error) { + q := `SELECT ` + orderColumns + ` FROM orders WHERE 1=1` + args := []any{} + if status != "" { + q += " AND status=?" + args = append(args, status) + } + if channel != "" { + q += " AND channel=?" + args = append(args, channel) + } + q += " ORDER BY id DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + rows, err := s.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +func (s *SQLiteStore) ListExpiredOrders(ctx context.Context, before time.Time) ([]*model.Order, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT `+orderColumns+` FROM orders WHERE status=? AND created_at <= ? ORDER BY id`, + model.StatusWaitPay, before.Format(time.RFC3339)) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +func (s *SQLiteStore) CountOrders(ctx context.Context, status string) (int64, error) { + q := `SELECT COUNT(*) FROM orders` + args := []any{} + if status != "" { + q += " WHERE status=?" + args = append(args, status) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func (s *SQLiteStore) SumAmount(ctx context.Context, status string) (int64, error) { + q := `SELECT COALESCE(SUM(amount_minor),0) FROM orders` + args := []any{} + if status != "" { + q += " WHERE status=?" + args = append(args, status) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func (s *SQLiteStore) ListOrdersFiltered(ctx context.Context, status, channel, keyword, from, to string, limit, offset int) ([]*model.Order, error) { + q := `SELECT ` + orderColumns + ` FROM orders WHERE 1=1` + args := []any{} + if status != "" { + q += " AND status=?" + args = append(args, status) + } + if channel != "" { + q += " AND channel=?" + args = append(args, channel) + } + if keyword != "" { + q += " AND (out_trade_no LIKE ? OR gateway_no LIKE ? OR channel_tx_id LIKE ? OR subject LIKE ?)" + kw := "%" + keyword + "%" + args = append(args, kw, kw, kw, kw) + } + if from != "" { + q += " AND substr(created_at,1,10) >= ?" + args = append(args, from) + } + if to != "" { + q += " AND substr(created_at,1,10) <= ?" + args = append(args, to) + } + q += " ORDER BY id DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + rows, err := s.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +func (s *SQLiteStore) CountOrdersFiltered(ctx context.Context, status, channel, keyword, from, to string) (int64, error) { + q := `SELECT COUNT(*) FROM orders WHERE 1=1` + args := []any{} + if status != "" { + q += " AND status=?" + args = append(args, status) + } + if channel != "" { + q += " AND channel=?" + args = append(args, channel) + } + if keyword != "" { + q += " AND (out_trade_no LIKE ? OR gateway_no LIKE ? OR channel_tx_id LIKE ? OR subject LIKE ?)" + kw := "%" + keyword + "%" + args = append(args, kw, kw, kw, kw) + } + if from != "" { + q += " AND substr(created_at,1,10) >= ?" + args = append(args, from) + } + if to != "" { + q += " AND substr(created_at,1,10) <= ?" + args = append(args, to) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func (s *SQLiteStore) CountOrdersByChannel(ctx context.Context, status, channel string) (int64, error) { + q := `SELECT COUNT(*) FROM orders WHERE channel=?` + args := []any{channel} + if status != "" { + q += " AND status=?" + args = append(args, status) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func (s *SQLiteStore) SumAmountByChannel(ctx context.Context, status, channel string) (int64, error) { + q := `SELECT COALESCE(SUM(amount_minor),0) FROM orders WHERE channel=?` + args := []any{channel} + if status != "" { + q += " AND status=?" + args = append(args, status) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func (s *SQLiteStore) CountOrdersByChannelToday(ctx context.Context, channel, date string) (int64, error) { + var n int64 + err := s.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM orders WHERE channel=? AND substr(created_at,1,10)=?`, + channel, date).Scan(&n) + return n, err +} + +func (s *SQLiteStore) SumAmountByChannelToday(ctx context.Context, channel, date string) (int64, error) { + var n int64 + err := s.db.QueryRowContext(ctx, + `SELECT COALESCE(SUM(amount_minor),0) FROM orders WHERE channel=? AND status='TRADE_SUCCESS' AND substr(created_at,1,10)=?`, + channel, date).Scan(&n) + return n, err +} + +// DailyStatsByChannel 日终对账:按通道+状态+日期统计订单数与金额 +// status 传 "" 表示全部状态;date 格式 2006-01-02(与 created_at 前 10 位比对) +func (s *SQLiteStore) DailyStatsByChannel(ctx context.Context, channel, status, date string) (int64, int64, error) { + q := `SELECT COUNT(*), COALESCE(SUM(amount_minor),0) FROM orders + WHERE channel=? AND substr(created_at,1,10)=?` + args := []any{channel, date} + if status != "" { + q += " AND status=?" + args = append(args, status) + } + var count, amount int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&count, &amount); err != nil { + return 0, 0, err + } + return count, amount, nil +} + +// OrdersTrend 按日期范围统计订单/金额趋势。start/end 为 "YYYY-MM-DD";空则回退到近 90 天。 +func (s *SQLiteStore) OrdersTrend(ctx context.Context, start, end string) ([]DailyStat, error) { + // 解析范围:缺省回退近 90 天(订单表长期保留,趋势数据随时可查,仅图表默认范围受限) + from, err := time.Parse("2006-01-02", start) + if err != nil { + from = time.Now().AddDate(0, 0, -89) + } + to, err := time.Parse("2006-01-02", end) + if err != nil { + to = time.Now() + } + if from.After(to) { + from, to = to, from + } + // 上限防护:单次最多拉 370 天,避免超大范围拖垮图表 + if to.Sub(from) > 370*24*time.Hour { + from = to.AddDate(0, 0, -369) + } + fromStr, toStr := from.Format("2006-01-02"), to.Format("2006-01-02") + + rows, err := s.db.QueryContext(ctx, ` + SELECT substr(created_at, 1, 10) AS d, + COUNT(*) AS total, + SUM(CASE WHEN status='TRADE_SUCCESS' THEN 1 ELSE 0 END) AS succ, + COALESCE(SUM(CASE WHEN status='TRADE_SUCCESS' THEN amount_minor ELSE 0 END), 0) AS amt + FROM orders + WHERE substr(created_at, 1, 10) >= ? AND substr(created_at, 1, 10) <= ? + GROUP BY d ORDER BY d ASC`, fromStr, toStr) + if err != nil { + return nil, err + } + defer rows.Close() + + // 用 map 填充缺失日期 + byDate := make(map[string]DailyStat) + for rows.Next() { + var d DailyStat + if err := rows.Scan(&d.Date, &d.TotalOrders, &d.SuccessOrders, &d.AmountMinor); err != nil { + return nil, err + } + byDate[d.Date] = d + } + if err := rows.Err(); err != nil { + return nil, err + } + + // 填充完整日期序列(缺失的天补 0) + out := make([]DailyStat, 0, int(to.Sub(from)/24/time.Hour)+1) + for d := from; !d.After(to); d = d.AddDate(0, 0, 1) { + date := d.Format("2006-01-02") + if v, ok := byDate[date]; ok { + out = append(out, v) + } else { + out = append(out, DailyStat{Date: date}) + } + } + return out, nil +} + +func (s *SQLiteStore) MarkNotified(ctx context.Context, outTradeNo string, notifyCount int) error { + _, err := s.db.ExecContext(ctx, + `UPDATE orders SET notified=1, notify_count=?, notified_at=COALESCE(notified_at, ?) WHERE out_trade_no=?`, + notifyCount, nowStr(), outTradeNo) + return err +} + +// ---------- 资金流水 ---------- + +func (s *SQLiteStore) InsertEvent(ctx context.Context, orderID int64, eventType, channelTxID string, amountMinor int64, rawPayload string) error { + _, err := s.db.ExecContext(ctx, + `INSERT INTO payment_events (order_id, event_type, channel_tx_id, amount_minor, raw_payload, created_at) + VALUES (?, ?, ?, ?, ?, ?)`, + orderID, eventType, channelTxID, amountMinor, rawPayload, nowStr()) + return err +} + +func (s *SQLiteStore) ListEvents(ctx context.Context, orderID int64, limit int) ([]*PaymentEvent, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT id, order_id, event_type, channel_tx_id, amount_minor, raw_payload, created_at + FROM payment_events WHERE order_id=? ORDER BY id DESC LIMIT ?`, orderID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*PaymentEvent{} // 空数组而非 nil,避免 JSON 序列化为 null + for rows.Next() { + var e PaymentEvent + var created string + if err := rows.Scan(&e.ID, &e.OrderID, &e.EventType, &e.ChannelTxID, &e.AmountMinor, &e.RawPayload, &created); err != nil { + return nil, err + } + e.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &e) + } + return out, rows.Err() +} + +// ---------- 回调任务(outbox) ---------- + +func (s *SQLiteStore) CreateNotifyTask(ctx context.Context, orderID int64, payload string) (int64, error) { + res, err := s.db.ExecContext(ctx, + `INSERT INTO notify_tasks (order_id, payload, status, next_retry_at, created_at, updated_at) + VALUES (?, ?, 'pending', ?, ?, ?)`, + orderID, payload, nowStr(), nowStr(), nowStr()) + if err != nil { + return 0, err + } + return res.LastInsertId() +} + +func (s *SQLiteStore) GetPendingNotifyTasks(ctx context.Context, limit int) ([]*NotifyTask, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT id, order_id, payload, status, retry_count, next_retry_at, created_at, updated_at + FROM notify_tasks WHERE status='pending' AND next_retry_at <= ? + ORDER BY id ASC LIMIT ?`, nowStr(), limit) + if err != nil { + return nil, err + } + defer rows.Close() + return scanTasks(rows) +} + +// GetNotifyTask 按 id 查询单个回调任务(死信重发定位用) +func (s *SQLiteStore) GetNotifyTask(ctx context.Context, id int64) (*NotifyTask, error) { + row := s.db.QueryRowContext(ctx, + `SELECT id, order_id, payload, status, retry_count, next_retry_at, created_at, updated_at + FROM notify_tasks WHERE id=?`, id) + var t NotifyTask + var nextRetry, created, updated string + if err := row.Scan(&t.ID, &t.OrderID, &t.Payload, &t.Status, &t.RetryCount, &nextRetry, &created, &updated); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + t.NextRetryAt, _ = time.Parse(time.RFC3339Nano, nextRetry) + t.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + t.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updated) + return &t, nil +} + +// UpdateNotifyTaskPayload 更新回调任务 payload(死信重发时按订单最新 notify_url 重建) +func (s *SQLiteStore) UpdateNotifyTaskPayload(ctx context.Context, id int64, payload string) error { + _, err := s.db.ExecContext(ctx, + `UPDATE notify_tasks SET payload=?, updated_at=? WHERE id=?`, payload, nowStr(), id) + return err +} + +// DeleteNotifyTask 删除回调任务(彻底移除死信,确认放弃该笔通知) +func (s *SQLiteStore) DeleteNotifyTask(ctx context.Context, id int64) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM notify_tasks WHERE id=?`, id) + return err +} + +func (s *SQLiteStore) ListNotifyTasks(ctx context.Context, status string, limit int) ([]*NotifyTask, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT id, order_id, payload, status, retry_count, next_retry_at, created_at, updated_at + FROM notify_tasks WHERE (?='' OR status=?) ORDER BY id DESC LIMIT ?`, + status, status, limit) + if err != nil { + return nil, err + } + defer rows.Close() + return scanTasks(rows) +} + +func (s *SQLiteStore) CountNotifyTasks(ctx context.Context, status string) (int64, error) { + q := `SELECT COUNT(*) FROM notify_tasks` + args := []any{} + if status != "" { + q += " WHERE status=?" + args = append(args, status) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +func scanTasks(rows *sql.Rows) ([]*NotifyTask, error) { + out := []*NotifyTask{} + for rows.Next() { + var t NotifyTask + var nextRetry, created, updated string + if err := rows.Scan(&t.ID, &t.OrderID, &t.Payload, &t.Status, &t.RetryCount, &nextRetry, &created, &updated); err != nil { + return nil, err + } + t.NextRetryAt, _ = time.Parse(time.RFC3339Nano, nextRetry) + t.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + t.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updated) + out = append(out, &t) + } + return out, rows.Err() +} + +func (s *SQLiteStore) UpdateNotifyTask(ctx context.Context, id int64, status string, retryCount int, nextRetryAt time.Time) error { + _, err := s.db.ExecContext(ctx, + `UPDATE notify_tasks SET status=?, retry_count=?, next_retry_at=?, updated_at=? WHERE id=?`, + status, retryCount, nextRetryAt.Format(time.RFC3339Nano), nowStr(), id) + return err +} + +// ---------- 日志 ---------- + +func (s *SQLiteStore) InsertLog(ctx context.Context, level, scope, message, traceID, fields string) error { + _, err := s.db.ExecContext(ctx, + `INSERT INTO app_logs (level, scope, message, trace_id, fields, created_at) VALUES (?, ?, ?, ?, ?, ?)`, + level, scope, message, traceID, fields, nowStr()) + return err +} + +func (s *SQLiteStore) QueryLogs(ctx context.Context, level, scope string, limit, offset int) ([]*LogRecord, error) { + q := `SELECT id, level, scope, message, trace_id, fields, created_at FROM app_logs WHERE 1=1` + args := []any{} + if level != "" { + q += " AND LOWER(level)=LOWER(?)" + args = append(args, level) + } + if scope != "" { + q += " AND scope=?" + args = append(args, scope) + } + q += " ORDER BY id DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + rows, err := s.db.QueryContext(ctx, q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*LogRecord{} + for rows.Next() { + var l LogRecord + var created string + if err := rows.Scan(&l.ID, &l.Level, &l.Scope, &l.Message, &l.TraceID, &l.Fields, &created); err != nil { + return nil, err + } + l.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &l) + } + return out, rows.Err() +} + +func (s *SQLiteStore) CountLogs(ctx context.Context, level, scope string) (int64, error) { + q := `SELECT COUNT(*) FROM app_logs WHERE 1=1` + args := []any{} + if level != "" { + q += " AND LOWER(level)=LOWER(?)" + args = append(args, level) + } + if scope != "" { + q += " AND scope=?" + args = append(args, scope) + } + var n int64 + if err := s.db.QueryRowContext(ctx, q, args...).Scan(&n); err != nil { + return 0, err + } + return n, nil +} + +// ---------- 系统设置 ---------- + +func (s *SQLiteStore) GetSetting(ctx context.Context, key string) (string, error) { + var v string + err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key=?`, key).Scan(&v) + if err == sql.ErrNoRows { + return "", nil + } + return v, err +} + +func (s *SQLiteStore) SetSetting(ctx context.Context, key, value string) error { + _, err := s.db.ExecContext(ctx, + `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, + key, value, nowStr()) + return err +} + +func (s *SQLiteStore) DeleteSetting(ctx context.Context, key string) error { + _, err := s.db.ExecContext(ctx, `DELETE FROM settings WHERE key = ?`, key) + return err +} + +func (s *SQLiteStore) GetAllSettings(ctx context.Context) (map[string]string, error) { + rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM settings`) + if err != nil { + return nil, err + } + defer rows.Close() + out := make(map[string]string) + for rows.Next() { + var k, v string + if err := rows.Scan(&k, &v); err != nil { + return nil, err + } + out[k] = v + } + return out, rows.Err() +} + +// ---------- 个人免签匹配查询 ---------- + +// orderColumns 常用 SELECT 列(与 scanOrder 对应) +const orderColumns = `id, out_trade_no, gateway_no, channel, channel_tx_id, account_id, pid, upstream_name, attach, amount_minor, + currency, subject, status, notify_url, return_url, qr_code, pay_url, payer, notified, notify_count, created_at, session_expires_at, paid_at, notified_at, + note_code, qr_token, device_id, buyer_key, fp_key` + +// GetOrderByNoteCode 按备注码查询未支付订单(精确匹配,个人免签主匹配) +// since: 只查该时间点之后创建的订单(5 分钟窗口) +func (s *SQLiteStore) GetOrderByNoteCode(ctx context.Context, noteCode string, since time.Time) (*model.Order, error) { + row := s.db.QueryRowContext(ctx, + `SELECT `+orderColumns+` FROM orders + WHERE note_code=? AND status=? AND created_at >= ? ORDER BY id LIMIT 1`, + noteCode, model.StatusWaitPay, since.Format(time.RFC3339)) + return scanOrder(row) +} + +// GetOrdersByAmount 查询某时间窗口内同金额的未支付订单(金额兜底匹配) +func (s *SQLiteStore) GetOrdersByAmount(ctx context.Context, amountMinor int64, since time.Time) ([]*model.Order, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT `+orderColumns+` FROM orders + WHERE amount_minor=? AND status=? AND created_at >= ? ORDER BY id`, + amountMinor, model.StatusWaitPay, since.Format(time.RFC3339)) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +// GetRecentSettledByAmount 查询某时间窗口内同金额的已支付订单(重复捕获判定用) +func (s *SQLiteStore) GetRecentSettledByAmount(ctx context.Context, amountMinor int64, since time.Time, accountIDs []int64) ([]*model.Order, error) { + query := `SELECT ` + orderColumns + ` FROM orders + WHERE amount_minor=? AND status=? AND created_at >= ?` + args := []interface{}{amountMinor, model.StatusSuccess, since.Format(time.RFC3339)} + // 按当前设备可服务账户集合限缩:同金额但属其他设备/账户的已支付订单不纳入, + // 避免「通知栏已入账的那笔」被账单页误判为无订单收款。 + if len(accountIDs) > 0 { + placeholders := make([]string, 0, len(accountIDs)) + for _, id := range accountIDs { + placeholders = append(placeholders, "?") + args = append(args, id) + } + query += ` AND account_id IN (` + strings.Join(placeholders, ",") + `)` + } + query += ` ORDER BY id DESC LIMIT 2` + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +// GetOrderByQrToken 按套码 token 查询订单(/mq/{token} 跳转校验) +func (s *SQLiteStore) GetOrderByQrToken(ctx context.Context, qrToken string) (*model.Order, error) { + row := s.db.QueryRowContext(ctx, + `SELECT `+orderColumns+` FROM orders WHERE qr_token=? ORDER BY id LIMIT 1`, qrToken) + return scanOrder(row) +} + +// ListWaitPayByAccount 查询指定收款账户下所有待支付订单 +// 个人免签:监听端离线联动用(设备离线 → 其绑定账户的 WAIT_PAY 订单进异常池人工核实) +func (s *SQLiteStore) ListWaitPayByAccount(ctx context.Context, accountID int64) ([]*model.Order, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT `+orderColumns+` FROM orders + WHERE account_id=? AND status=? ORDER BY id`, + accountID, model.StatusWaitPay) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +// ListWaitPayAll 查询全部待支付订单(WaitPayPool 启动重建 + 周期对账的数据源) +func (s *SQLiteStore) ListWaitPayAll(ctx context.Context) ([]*model.Order, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT `+orderColumns+` FROM orders + WHERE status=? ORDER BY id`, + model.StatusWaitPay) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +// ListWaitPayByBuyer 查询指定买家环境(buyer_key)下所有待支付订单 +// 买家级串行:一个买家环境同时只允许一笔待支付订单(防海量刷单/错配攻击) +func (s *SQLiteStore) ListWaitPayByBuyer(ctx context.Context, buyerKey string) ([]*model.Order, error) { + if buyerKey == "" { + return nil, nil + } + rows, err := s.db.QueryContext(ctx, + `SELECT `+orderColumns+` FROM orders + WHERE buyer_key=? AND status=? ORDER BY id`, + buyerKey, model.StatusWaitPay) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +// ListWaitPayByBuyerOrFP 按买家环境(buyer_key)或浏览器指纹(fp_key)查询待支付订单 +// 支付页 JS 上报指纹后订单持有 fp_key;同环境换 IP 再下单时 buyer_key 不同, +// 但 fp_key 相同 → 串行检查需同时匹配两维(防指纹绑定后串行被绕过) +func (s *SQLiteStore) ListWaitPayByBuyerOrFP(ctx context.Context, buyerKey, fpKey string) ([]*model.Order, error) { + if buyerKey == "" && fpKey == "" { + return nil, nil + } + rows, err := s.db.QueryContext(ctx, + `SELECT `+orderColumns+` FROM orders + WHERE status=? AND (buyer_key=? OR (fp_key != '' AND fp_key=?)) ORDER BY id`, + model.StatusWaitPay, buyerKey, fpKey) + if err != nil { + return nil, err + } + defer rows.Close() + out := []*model.Order{} + for rows.Next() { + o, err := scanOrder(rows) + if err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +func (s *SQLiteStore) Close() error { return s.db.Close() } diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100755 index 0000000..b2a3c32 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,164 @@ +// Package store 订单存储抽象:接口 + SQLite 实现 +package store + +import ( + "context" + "time" + + "paygateway/internal/model" +) + +// Store 存储接口:资金数据必须走这里(SQLite/PG 可切换) +type Store interface { + // 订单 + CreateOrder(ctx context.Context, o *model.Order) error + // DeleteOrder 删除订单;仅当状态为 WAIT_BUYER_PAY 时生效并返回 true + DeleteOrder(ctx context.Context, outTradeNo string) (bool, error) + GetOrder(ctx context.Context, outTradeNo string) (*model.Order, error) + GetOrderByID(ctx context.Context, id int64) (*model.Order, error) + // 条件更新:from -> to,影响行数=0 表示状态非法(幂等/防覆盖) + UpdateOrderStatus(ctx context.Context, outTradeNo, from, to string, set func(o *model.Order)) (bool, error) + // ReplaceOrder 买家替换:原子关闭 WAIT_PAY -> CLOSED 并在同一事务写 replaced 事件(见实现,关单与事件同事务持久化)。 + ReplaceOrder(ctx context.Context, outTradeNo string, orderID int64, amountMinor int64, rawJSON string) (bool, error) + // UpdateOrderAmount 更新订单金额(复用订单时以最新下单金额覆盖,防实付与订单金额不一致导致回调失败) + UpdateOrderAmount(ctx context.Context, outTradeNo string, amountMinor int64) error + // ListExpiredOrders 查询超过 before 时间仍未支付的订单(自动关闭用) + ListExpiredOrders(ctx context.Context, before time.Time) ([]*model.Order, error) + // 个人免签:按备注码查询未支付订单(匹配引擎用) + GetOrderByNoteCode(ctx context.Context, noteCode string, since time.Time) (*model.Order, error) + // 个人免签:查询某时间窗口内同金额的未支付订单(金额兜底匹配) + GetOrdersByAmount(ctx context.Context, amountMinor int64, since time.Time) ([]*model.Order, error) + // 个人免签:查询某时间窗口内同金额的已支付订单(重复捕获判定:通知栏/账本双链 5s 内同时捕获同一笔收款,败者零 WAIT_PAY 候选) + // accountIDs 限定待匹配的账户集合(当前设备可服务的账户);同金额但属其他设备/账户的已支付订单不纳入, + // 避免「通知栏已入账的那笔」被账单页误判为无订单收款。空集合 = 不过滤全查。 + GetRecentSettledByAmount(ctx context.Context, amountMinor int64, since time.Time, accountIDs []int64) ([]*model.Order, error) + // 个人免签:按套码 token 查询订单 + GetOrderByQrToken(ctx context.Context, qrToken string) (*model.Order, error) + // 个人免签:查询指定收款账户下所有待支付订单(监听端离线联动:进异常池人工核实) + ListWaitPayByAccount(ctx context.Context, accountID int64) ([]*model.Order, error) + // 个人免签:查询全部待支付订单(WaitPayPool 启动重建/对账用) + ListWaitPayAll(ctx context.Context) ([]*model.Order, error) + // 个人免签:查询指定买家环境(buyer_key)下所有待支付订单(买家级串行:一个环境同时只允许一笔待支付) + ListWaitPayByBuyer(ctx context.Context, buyerKey string) ([]*model.Order, error) + // 个人免签:按买家环境(buyer_key)或浏览器指纹(fp_key)查询待支付订单(指纹绑定后串行检查兜底) + ListWaitPayByBuyerOrFP(ctx context.Context, buyerKey, fpKey string) ([]*model.Order, error) + + // ---------- 系统设置 ---------- + GetSetting(ctx context.Context, key string) (string, error) + SetSetting(ctx context.Context, key, value string) error + DeleteSetting(ctx context.Context, key string) error + GetAllSettings(ctx context.Context) (map[string]string, error) + MarkNotified(ctx context.Context, outTradeNo string, notifyCount int) error + ListOrders(ctx context.Context, status, channel string, limit, offset int) ([]*model.Order, error) + CountOrders(ctx context.Context, status string) (int64, error) + SumAmount(ctx context.Context, status string) (int64, error) + // 增强查询:keyword 搜索订单号,from/to 时间范围(yyyy-mm-dd) + ListOrdersFiltered(ctx context.Context, status, channel, keyword, from, to string, limit, offset int) ([]*model.Order, error) + CountOrdersFiltered(ctx context.Context, status, channel, keyword, from, to string) (int64, error) + CountOrdersByChannel(ctx context.Context, status, channel string) (int64, error) + SumAmountByChannel(ctx context.Context, status, channel string) (int64, error) + CountOrdersByChannelToday(ctx context.Context, channel, date string) (int64, error) + SumAmountByChannelToday(ctx context.Context, channel, date string) (int64, error) + // 日终对账:按通道+状态+日期统计(RESILIENCE P2-1) + DailyStatsByChannel(ctx context.Context, channel, status, date string) (count, amount int64, err error) + // 按天趋势:返回最近 days 天(含今天)每日订单统计 + OrdersTrend(ctx context.Context, start, end string) ([]DailyStat, error) + + // 资金流水(只增不改,审计基础) + InsertEvent(ctx context.Context, orderID int64, eventType, channelTxID string, amountMinor int64, rawPayload string) error + ListEvents(ctx context.Context, orderID int64, limit int) ([]*PaymentEvent, error) + + // 回调任务(outbox) + CreateNotifyTask(ctx context.Context, orderID int64, payload string) (int64, error) + GetPendingNotifyTasks(ctx context.Context, limit int) ([]*NotifyTask, error) + GetNotifyTask(ctx context.Context, id int64) (*NotifyTask, error) + UpdateNotifyTask(ctx context.Context, id int64, status string, retryCount int, nextRetryAt time.Time) error + // UpdateNotifyTaskPayload 更新回调任务 payload(死信重发时按订单最新 notify_url 重建) + UpdateNotifyTaskPayload(ctx context.Context, id int64, payload string) error + // DeleteNotifyTask 删除回调任务(彻底移除死信,确认放弃该笔通知) + DeleteNotifyTask(ctx context.Context, id int64) error + ListNotifyTasks(ctx context.Context, status string, limit int) ([]*NotifyTask, error) + CountNotifyTasks(ctx context.Context, status string) (int64, error) + + // 日志落库(管理界面查询) + InsertLog(ctx context.Context, level, scope, message, traceID, fields string) error + QueryLogs(ctx context.Context, level, scope string, limit, offset int) ([]*LogRecord, error) + CountLogs(ctx context.Context, level, scope string) (int64, error) + + // ---------- 人工异常处理池 ---------- + CreateException(ctx context.Context, e *model.ExceptionOrder) error + GetException(ctx context.Context, id int64) (*model.ExceptionOrder, error) + GetExceptionByOutTradeNo(ctx context.Context, outTradeNo string) (*model.ExceptionOrder, error) + ListExceptions(ctx context.Context, phase, reason string, limit, offset int) ([]*model.ExceptionOrder, error) + CountExceptions(ctx context.Context, phase string) (int64, error) + UpdateException(ctx context.Context, id int64, set func(e *model.ExceptionOrder)) error + UpdateExceptionPhase(ctx context.Context, id int64, from, to string) (bool, error) + InsertExceptionLog(ctx context.Context, exceptionID int64, action, operator, detail string) error + ListExceptionLogs(ctx context.Context, exceptionID int64, limit int) ([]*model.ExceptionLog, error) + // UpdateOrderHandled 订单人工处理标记(人工池处理后永久留痕) + UpdateOrderHandled(ctx context.Context, outTradeNo string, exceptionID int64, action, handler, note string) error + + Close() error +} + +// DailyStat 按天订单统计(趋势图用) +type DailyStat struct { + Date string `json:"date"` // 2006-01-02 + TotalOrders int64 `json:"total_orders"` // 当日全部订单 + SuccessOrders int64 `json:"success_orders"` // 当日成功订单 + AmountMinor int64 `json:"amount_minor"` // 当日成功金额(分) +} + +// PaymentEvent 资金流水 +type PaymentEvent struct { + ID int64 `json:"id"` + OrderID int64 `json:"order_id"` + EventType string `json:"event_type"` + ChannelTxID string `json:"channel_tx_id"` + AmountMinor int64 `json:"amount_minor"` + RawPayload string `json:"raw_payload"` + CreatedAt time.Time `json:"created_at"` +} + +// NotifyTask 回调任务(outbox) +type NotifyTask struct { + ID int64 `json:"id"` + OrderID int64 `json:"order_id"` + Payload string `json:"payload"` + Status string `json:"status"` // pending / success / dead + RetryCount int `json:"retry_count"` + NextRetryAt time.Time `json:"next_retry_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// LogRecord 日志记录 +type LogRecord struct { + ID int64 `json:"id"` + Level string `json:"level"` + Scope string `json:"scope"` + Message string `json:"message"` + TraceID string `json:"trace_id"` + Fields string `json:"fields"` + CreatedAt time.Time `json:"created_at"` +} + +// LedgerEntry 账本条目 +type LedgerEntry struct { + ID int64 `json:"id"` + PrevHash string `json:"prev_hash"` + PayloadHash string `json:"payload_hash"` + EntryHash string `json:"entry_hash"` + Signature string `json:"signature"` + EventID int64 `json:"event_id"` + CreatedAt time.Time `json:"created_at"` +} + +// Snapshot 每日快照 +type Snapshot struct { + ID int64 + SnapshotDate string + LastHash string + Signature string + CreatedAt time.Time +} diff --git a/internal/store/upstreamdb.go b/internal/store/upstreamdb.go new file mode 100755 index 0000000..f23a78f --- /dev/null +++ b/internal/store/upstreamdb.go @@ -0,0 +1,246 @@ +// Package store 上游数据库:独立 upstreams.db,仅管理后台 API 可读写 +// +// 安全设计: +// - 独立 SQLite 文件,权限 0600 +// - 敏感字段(易支付密钥 key)以 AES-256-GCM 密文存储 +// - UpstreamStore 接口不暴露给 order/epay 等模块,仅在 admin 包内部使用 +// - 其他模块通过 UpstreamPool(内存缓存)获取解密后的上游数据 +package store + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + _ "modernc.org/sqlite" + + "paygateway/internal/upstream" +) + +// UpstreamStore 上游存储接口(仅限 Admin API 使用) +type UpstreamStore interface { + ListUpstreams(ctx context.Context) ([]*upstream.UpstreamEncrypted, error) + GetUpstream(ctx context.Context, id int64) (*upstream.UpstreamEncrypted, error) + CreateUpstream(ctx context.Context, u *upstream.UpstreamEncrypted) (int64, error) + UpdateUpstream(ctx context.Context, u *upstream.UpstreamEncrypted) error + DeleteUpstream(ctx context.Context, id int64) error + SetUpstreamStatus(ctx context.Context, id int64, status string) error + InsertUpstreamLog(ctx context.Context, log *upstream.UpstreamLog) error + ListUpstreamLogs(ctx context.Context, upstreamID int64, limit int) ([]*upstream.UpstreamLog, error) + Close() error +} + +// UpstreamDB 独立上游数据库:仅存 upstreams + upstream_logs +// 文件权限 0600,WAL 模式,仅 Admin API 访问 +type UpstreamDB struct { + db *sql.DB +} + +// NewUpstreamDB 打开/创建独立上游数据库 +func NewUpstreamDB(path string) (*UpstreamDB, error) { + // 先创建空文件以确保可以设置权限 + fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return nil, fmt.Errorf("创建上游数据库文件失败: %w", err) + } + fd.Close() + + // 强制设置权限为 0600(只有文件所有者可读写) + if err := os.Chmod(path, 0600); err != nil { + return nil, fmt.Errorf("设置上游数据库权限失败: %w", err) + } + + // 打开连接:WAL 模式防崩溃 + db, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_busy_timeout=10000&_synchronous=FULL") + if err != nil { + return nil, fmt.Errorf("打开上游数据库失败: %w", err) + } + + // 连接池:单连接更安全 + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + u := &UpstreamDB{db: db} + if err := u.migrate(); err != nil { + db.Close() + return nil, err + } + return u, nil +} + +// migrate 创建上游表 + 操作审计日志表 +func (u *UpstreamDB) migrate() error { + stmts := []string{ + `CREATE TABLE IF NOT EXISTS upstreams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + pid TEXT NOT NULL UNIQUE, + key_enc TEXT NOT NULL DEFAULT '', + base_url TEXT NOT NULL DEFAULT '', + notify_domains TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'enabled', + remark TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_upstreams_status ON upstreams(status)`, + + // 上游操作审计日志 + `CREATE TABLE IF NOT EXISTS upstream_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + upstream_id INTEGER NOT NULL, + action TEXT NOT NULL, + detail TEXT NOT NULL DEFAULT '', + operator TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_upstream_logs_upstream ON upstream_logs(upstream_id)`, + + // WAL 模式 + `PRAGMA journal_mode=WAL`, + `PRAGMA synchronous=FULL`, + } + for _, stmt := range stmts { + if _, err := u.db.Exec(stmt); err != nil { + return fmt.Errorf("上游数据库迁移失败: %w", err) + } + } + return nil +} + +// ---------- 上游 CRUD ---------- + +// ListUpstreams 列出全部上游 +func (u *UpstreamDB) ListUpstreams(ctx context.Context) ([]*upstream.UpstreamEncrypted, error) { + rows, err := u.db.QueryContext(ctx, + `SELECT id, name, pid, key_enc, base_url, notify_domains, status, remark, created_at, updated_at + FROM upstreams ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []*upstream.UpstreamEncrypted + for rows.Next() { + e, err := scanUpstreamEncrypted(rows) + if err != nil { + return nil, err + } + out = append(out, e) + } + return out, rows.Err() +} + +// GetUpstream 按 ID 获取上游 +func (u *UpstreamDB) GetUpstream(ctx context.Context, id int64) (*upstream.UpstreamEncrypted, error) { + row := u.db.QueryRowContext(ctx, + `SELECT id, name, pid, key_enc, base_url, notify_domains, status, remark, created_at, updated_at + FROM upstreams WHERE id=?`, id) + return scanUpstreamEncrypted(row) +} + +// GetUpstreamByPid 按 pid 获取上游(启动迁移 default 用) +func (u *UpstreamDB) GetUpstreamByPid(ctx context.Context, pid string) (*upstream.UpstreamEncrypted, error) { + row := u.db.QueryRowContext(ctx, + `SELECT id, name, pid, key_enc, base_url, notify_domains, status, remark, created_at, updated_at + FROM upstreams WHERE pid=?`, pid) + return scanUpstreamEncrypted(row) +} + +// CreateUpstream 创建上游,返回新 ID +func (u *UpstreamDB) CreateUpstream(ctx context.Context, up *upstream.UpstreamEncrypted) (int64, error) { + now := time.Now() + res, err := u.db.ExecContext(ctx, + `INSERT INTO upstreams (name, pid, key_enc, base_url, notify_domains, status, remark, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + up.Name, up.Pid, up.KeyEnc, up.BaseURL, up.NotifyDomains, up.Status, up.Remark, + now.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano)) + if err != nil { + return 0, fmt.Errorf("创建上游失败: %w", err) + } + return res.LastInsertId() +} + +// UpdateUpstream 更新上游(全部字段覆盖) +func (u *UpstreamDB) UpdateUpstream(ctx context.Context, up *upstream.UpstreamEncrypted) error { + _, err := u.db.ExecContext(ctx, + `UPDATE upstreams SET name=?, pid=?, key_enc=?, base_url=?, notify_domains=?, status=?, remark=?, updated_at=? + WHERE id=?`, + up.Name, up.Pid, up.KeyEnc, up.BaseURL, up.NotifyDomains, up.Status, up.Remark, + time.Now().Format(time.RFC3339Nano), up.ID) + return err +} + +// DeleteUpstream 删除上游 +func (u *UpstreamDB) DeleteUpstream(ctx context.Context, id int64) error { + _, err := u.db.ExecContext(ctx, `DELETE FROM upstreams WHERE id=?`, id) + return err +} + +// SetUpstreamStatus 设置上游状态(启用/禁用) +func (u *UpstreamDB) SetUpstreamStatus(ctx context.Context, id int64, status string) error { + _, err := u.db.ExecContext(ctx, + `UPDATE upstreams SET status=?, updated_at=? WHERE id=?`, + status, time.Now().Format(time.RFC3339Nano), id) + return err +} + +// ---------- 上游操作审计日志 ---------- + +// InsertUpstreamLog 写入操作审计日志 +func (u *UpstreamDB) InsertUpstreamLog(ctx context.Context, log *upstream.UpstreamLog) error { + _, err := u.db.ExecContext(ctx, + `INSERT INTO upstream_logs (upstream_id, action, detail, operator, created_at) + VALUES (?, ?, ?, ?, ?)`, + log.UpstreamID, log.Action, log.Detail, log.Operator, time.Now().Format(time.RFC3339Nano)) + return err +} + +// ListUpstreamLogs 列出上游操作日志 +func (u *UpstreamDB) ListUpstreamLogs(ctx context.Context, upstreamID int64, limit int) ([]*upstream.UpstreamLog, error) { + rows, err := u.db.QueryContext(ctx, + `SELECT id, upstream_id, action, detail, operator, created_at + FROM upstream_logs WHERE upstream_id=? ORDER BY id DESC LIMIT ?`, upstreamID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]*upstream.UpstreamLog, 0) + for rows.Next() { + var l upstream.UpstreamLog + var created string + if err := rows.Scan(&l.ID, &l.UpstreamID, &l.Action, &l.Detail, &l.Operator, &created); err != nil { + return nil, err + } + l.CreatedAt, _ = time.Parse(time.RFC3339Nano, created) + out = append(out, &l) + } + return out, rows.Err() +} + +// Close 关闭上游数据库 +func (u *UpstreamDB) Close() error { + return u.db.Close() +} + +// ---------- 辅助函数 ---------- + +// scanUpstreamEncrypted 扫描一行到 UpstreamEncrypted +func scanUpstreamEncrypted(s rowScanner) (*upstream.UpstreamEncrypted, error) { + var e upstream.UpstreamEncrypted + var createdAt, updatedAt string + err := s.Scan(&e.ID, &e.Name, &e.Pid, &e.KeyEnc, &e.BaseURL, + &e.NotifyDomains, &e.Status, &e.Remark, &createdAt, &updatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + e.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAt) + e.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAt) + return &e, nil +} diff --git a/internal/tunnel/cloudflared.go b/internal/tunnel/cloudflared.go new file mode 100644 index 0000000..af559f4 --- /dev/null +++ b/internal/tunnel/cloudflared.go @@ -0,0 +1,22 @@ +package tunnel + +import ( + "fmt" + "os" + "os/exec" + "runtime" +) + +func cloudflaredPath() (string, error) { + if path := os.Getenv("CLOUDFLARED_PATH"); path != "" { + if _, err := os.Stat(path); err == nil { + return path, nil + } + return "", fmt.Errorf("CLOUDFLARED_PATH 不存在: %s", path) + } + path, err := exec.LookPath("cloudflared") + if err != nil { + return "", fmt.Errorf("未找到 cloudflared,请安装后设置 CLOUDFLARED_PATH(平台: %s/%s): %w", runtime.GOOS, runtime.GOARCH, err) + } + return path, nil +} diff --git a/internal/tunnel/config.go b/internal/tunnel/config.go new file mode 100755 index 0000000..c761c45 --- /dev/null +++ b/internal/tunnel/config.go @@ -0,0 +1,114 @@ +package tunnel + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// Config 隧道配置 +type Config struct { + LocalPort int // 本地服务端口(默认 8080) + CallbackPath string // 回调路径(如 /alipay/notify) + StartTimeout time.Duration // 等待隧道就绪超时(默认 30s) + Connections int // cloudflared 连接数(默认 4,高并发可调大) + OnURLChange func(url string) // URL 变化回调(隧道重启换域名时自动调用) + // Hostname 固定域名(如 paygateway.de5.net)。 + // 非空 = 命名隧道模式(cloudflared tunnel run,需 ~/.cloudflared/paygateway.yml + paygateway.json 凭据); + // 空 = 默认快速隧道(trycloudflare.com 随机域名)。 + Hostname string + // ConfigPath 命名隧道的 cloudflared 配置文件路径(默认 ~/.cloudflared/paygateway.yml)�� + // 仅 Hostname 非空时生效。 + ConfigPath string +} + +// DefaultConfig 默认配置 +func DefaultConfig() *Config { + return &Config{ + LocalPort: 8080, + CallbackPath: "/alipay/notify", + StartTimeout: 30 * time.Second, + Connections: 4, + } +} + +// SetDefaultConfigPath 返回默认的 cloudflared 配置文件路径(~/.cloudflared/paygateway.yml)。 +// 外部(main.go)可调用本函数设置 Config.ConfigPath;留空则 tunnel.New 使用此默认路径。 +func SetDefaultConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "paygateway.yml" + } + return filepath.Join(home, ".cloudflared", "paygateway.yml") +} + +// EnsureCredentials 从环境变量生成 cloudflared 凭据和配置文件。 +// +// 环境变量: +// - CF_TUNNEL_HOSTNAME — 固定域名(如 paygateway.de5.net),空 = 跳过 +// - CF_TUNNEL_CREDENTIALS — tunnel 凭据 JSON 的 base64(由 setup-domain.sh 或首次启动写入 .env) +// +// 写入文件: +// +// ~/.cloudflared/paygateway.json (凭据) +// ~/.cloudflared/paygateway.yml (隧道配置) +// +// 幂等:文件已存在则跳过写入,不影响运行中隧道。换环境时只需复制 .env 即可。 +func EnsureCredentials() error { + hostname := os.Getenv("CF_TUNNEL_HOSTNAME") + b64 := os.Getenv("CF_TUNNEL_CREDENTIALS") + if hostname == "" || b64 == "" { + return nil // 快速隧道模式或未配置,跳过 + } + raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64)) + if err != nil { + return fmt.Errorf("CF_TUNNEL_CREDENTIALS base64 解码失败: %w", err) + } + // 解析凭据提取 TunnelID + var cred struct { + TunnelID string `json:"TunnelID"` + } + if err := json.Unmarshal(raw, &cred); err != nil { + return fmt.Errorf("CF_TUNNEL_CREDENTIALS JSON 解析失败: %w", err) + } + if cred.TunnelID == "" { + return fmt.Errorf("CF_TUNNEL_CREDENTIALS 缺少 TunnelID") + } + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("获取 HOME 目录失败: %w", err) + } + cfgDir := filepath.Join(home, ".cloudflared") + credFile := filepath.Join(cfgDir, "paygateway.json") + ymlFile := filepath.Join(cfgDir, "paygateway.yml") + + // 创建目录 + if err := os.MkdirAll(cfgDir, 0700); err != nil { + return fmt.Errorf("创建 %s 失败: %w", cfgDir, err) + } + // 写凭据文件(幂等) + if _, err := os.Stat(credFile); os.IsNotExist(err) { + if err := os.WriteFile(credFile, raw, 0600); err != nil { + return fmt.Errorf("写入 %s 失败: %w", credFile, err) + } + } + // 写配置文件(幂等) + if _, err := os.Stat(ymlFile); os.IsNotExist(err) { + yml := fmt.Sprintf(`tunnel: %s +credentials-file: %s + +ingress: + - hostname: %s + service: http://localhost:8080 + - service: http_status:404 +`, cred.TunnelID, credFile, hostname) + if err := os.WriteFile(ymlFile, []byte(yml), 0600); err != nil { + return fmt.Errorf("写入 %s 失败: %w", ymlFile, err) + } + } + return nil +} diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go new file mode 100755 index 0000000..48acaf2 --- /dev/null +++ b/internal/tunnel/tunnel.go @@ -0,0 +1,348 @@ +// Package tunnel Cloudflare Tunnel 管理:启动隧道、获取回调 URL、健康检查 +// +// 使用 trycloudflare.com 快速隧道(无需认证),每次启动获取随机域名。 +// 启动后通过 GetCallbackURL() 获取当前可用回调地址。 +package tunnel + +import ( + "bufio" + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "paygateway/internal/logger" +) + +// Tunnel 单个隧道实例 +type Tunnel struct { + url string // https://xxx.trycloudflare.com 或 https:// + hostname string // 命名隧道的原始 hostname(paygateway.de5.net);快速隧道为空 + localPort int + connections int // ha-connections 数(重启时保留,避免传 0 给 cloudflared) + cmd *exec.Cmd + cancel context.CancelFunc + healthy bool + done chan struct{} + mu sync.RWMutex + onURLChange func(url string) // URL 变化回调 +} + +// New 创建并启动隧道 +// 阻塞直到隧道就绪或超时 +func New(ctx context.Context, cfg *Config) (*Tunnel, error) { + if cfg == nil { + cfg = DefaultConfig() + } + + tunCtx, cancel := context.WithCancel(ctx) + + // 使用编译时嵌入的 cloudflared 二进制 + cfPath, err := cloudflaredPath() + if err != nil { + cancel() + return nil, fmt.Errorf("获取 cloudflared 路径失败: %w", err) + } + + var cmd *exec.Cmd + if cfg.Hostname != "" { + // 命名隧道模式:cloudflared tunnel run --config + configPath := cfg.ConfigPath + if configPath == "" { + configPath = defaultConfigPath() + } + if _, err := os.Stat(configPath); err != nil { + cancel() + return nil, fmt.Errorf("命名隧道配置文件不存在: %s(请先创建 %s,含 ingress 路由)", configPath, configPath) + } + cmd = exec.CommandContext(tunCtx, cfPath, + "tunnel", "--no-autoupdate", + "--config", configPath, + "run", + ) + } else { + // 快速隧道模式:trycloudflare 随机域名 + cmd = exec.CommandContext(tunCtx, cfPath, + "tunnel", "--no-autoupdate", + "--url", fmt.Sprintf("http://localhost:%d", cfg.LocalPort), + "--ha-connections", fmt.Sprintf("%d", cfg.Connections), + ) + } + + stderr, err := cmd.StderrPipe() + if err != nil { + cancel() + return nil, fmt.Errorf("创建 stderr pipe 失败: %w", err) + } + + if err := cmd.Start(); err != nil { + cancel() + return nil, fmt.Errorf("启动 cloudflared 失败: %w", err) + } + + t := &Tunnel{ + localPort: cfg.LocalPort, + connections: cfg.Connections, + hostname: cfg.Hostname, + cmd: cmd, + cancel: cancel, + done: make(chan struct{}), + onURLChange: cfg.OnURLChange, + } + + if cfg.Hostname != "" { + // 命名隧道:URL 固定为 https://,就绪判定 = cloudflared 注册成功 + t.url = "https://" + cfg.Hostname + t.healthy = true + // 异步等待连接注册成功(读到 Registered tunnel connection 即视为就绪) + go t.waitNamedReady(stderr) + logger.Info("tunnel", "命名隧道已启动(等待连接注册)", "url", t.url, "config", cfg.ConfigPath) + if cfg.OnURLChange != nil { + cfg.OnURLChange(t.url) + } + return t, nil + } + + // 异步读取 stderr 获取域名 + domainCh := make(chan string, 1) + go t.readDomain(stderr, domainCh) + + // 等待域名就绪 + select { + case url := <-domainCh: + t.mu.Lock() + t.url = url + t.healthy = true + t.mu.Unlock() + logger.Info("tunnel", "隧道就绪", "url", url) + // 通知 URL 变化 + if cfg.OnURLChange != nil { + cfg.OnURLChange(url) + } + return t, nil + + case <-time.After(cfg.StartTimeout): + cancel() + cmd.Wait() + return nil, fmt.Errorf("隧道启动超时(%v)", cfg.StartTimeout) + + case <-ctx.Done(): + cancel() + cmd.Wait() + return nil, ctx.Err() + } +} + +// waitNamedReady 等待命名隧道连接注册成功;注册失败/进程退出时标记不健康。 +// 注册成功后继续排空 stderr:cloudflared 持续写日志,若停止读取 pipe 缓冲区写满会阻塞 +// cloudflared 主循环,导致隧道假死(健康检查失败 → 无限重启)。 +func (t *Tunnel) waitNamedReady(stderr io.ReadCloser) { + defer close(t.done) + scanner := bufio.NewScanner(stderr) + scanner.Buffer(make([]byte, 1024*64), 1024*64) + registered := false + for scanner.Scan() { + line := scanner.Text() + if !registered { + if strings.Contains(line, "Registered tunnel connection") { + logger.Info("tunnel", "命名隧道连接已注册", "url", t.URL()) + registered = true + } + if strings.Contains(line, "no more connections active and exiting") || + strings.Contains(line, "Fatal") { + t.mu.Lock() + t.healthy = false + t.mu.Unlock() + logger.Warn("tunnel", "命名隧道连接异常退出", "line", line) + return + } + } + } +} + +// readDomain 从 cloudflared stderr 中解析 trycloudflare 隧道域名 +// 注意:cloudflared 早期日志会输出 api.trycloudflare.com(官方 API 地址), +// 它不是隧道域名,必须跳过,否则 notify_url 会变成 https://api.trycloudflare.com/... 导致回调 404 +func (t *Tunnel) readDomain(stderr io.ReadCloser, domainCh chan<- string) { + defer close(t.done) + re := regexp.MustCompile(`https://([a-z0-9-]+\.trycloudflare\.com)`) + scanner := bufio.NewScanner(stderr) + scanner.Buffer(make([]byte, 1024*64), 1024*64) + for scanner.Scan() { + line := scanner.Text() + if matches := re.FindStringSubmatch(line); len(matches) >= 2 { + host := matches[1] + // 排除 Cloudflare 官方控制面域名(非隧道地址) + if host == "api.trycloudflare.com" || host == "dash.trycloudflare.com" { + continue + } + select { + case domainCh <- "https://" + host: + default: + } + return + } + } +} + +// URL 返回隧道公网地址(如 https://xxx.trycloudflare.com) +func (t *Tunnel) URL() string { + t.mu.RLock() + defer t.mu.RUnlock() + return t.url +} + +// CallbackURL 返回完整的回调地址 +func (t *Tunnel) CallbackURL(callbackPath string) string { + t.mu.RLock() + defer t.mu.RUnlock() + if t.url == "" { + return "" + } + return t.url + callbackPath +} + +// Healthy 返回隧道是否健康 +func (t *Tunnel) Healthy() bool { + t.mu.RLock() + defer t.mu.RUnlock() + return t.healthy +} + +// Stop 停止隧道 +func (t *Tunnel) Stop() { + if t.cancel != nil { + t.cancel() + } + _ = t.cmd.Wait() +} + +// HealthLoop 定期健康检查,自动重启 +func (t *Tunnel) HealthLoop(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.checkHealth(ctx) + } + } +} + +// checkHealth 检查隧道健康。 +// 注意:不走公网 URL 自环(网络封锁/ISP 劫持会使公网 /health 永远不可达 → 误判不健康 → 无限重启风暴)。 +// 改为:本地 8080 可达 + cloudflared 进程存活 = 健康(进程死了才重启,URL 变才触发回调)。 +func (t *Tunnel) checkHealth(ctx context.Context) { + t.mu.Lock() + wasHealthy := t.healthy + url := t.url + t.mu.Unlock() + if url == "" { + return + } + + // 1. 本地源站可达(不经隧道,绕开公网链路) + localOK := t.checkLocal(ctx) + // 2. cloudflared 进程存活 + procOK := t.cmd != nil && t.cmd.Process != nil && t.cmd.ProcessState == nil + healthy := localOK && procOK + + t.mu.Lock() + t.healthy = healthy + t.mu.Unlock() + + if wasHealthy && !healthy { + logger.Warn("tunnel", "隧道不健康,准备重启", "url", url, "local_ok", localOK, "proc_ok", procOK) + go t.restart(ctx) + } else if !wasHealthy && healthy { + logger.Info("tunnel", "隧道恢复健康", "url", url) + } +} + +// checkLocal 检测本地源站是否可达(http://localhost:/health) +func (t *Tunnel) checkLocal(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "GET", + fmt.Sprintf("http://localhost:%d/health", t.localPort), nil) + if err != nil { + return false + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK +} + +func (t *Tunnel) restart(ctx context.Context) { + // 已不再持有旧进程,立即标记为不健康 + t.mu.Lock() + t.healthy = false + t.mu.Unlock() + // 停止旧进程(调用 cancel 后 cmd 会退出,Wait 清理僵尸) + if t.cancel != nil { + t.cancel() + _ = t.cmd.Wait() + } + time.Sleep(2 * time.Second) + + // 退避重试:失败后递增等待(30s/60s/120s/240s… 最高 300s),直到成功或主 ctx 取消 + backoff := 30 * time.Second + maxBackoff := 300 * time.Second + for { + newT, err := New(ctx, &Config{ + LocalPort: t.localPort, + StartTimeout: 30 * time.Second, + Connections: t.connections, + OnURLChange: t.onURLChange, + Hostname: t.hostname, + }) + if err == nil { + // 替换自身 + t.mu.Lock() + oldURL := t.url + t.url = newT.url + t.healthy = newT.healthy + t.cmd = newT.cmd + t.cancel = newT.cancel + t.done = newT.done + t.mu.Unlock() + logger.Info("tunnel", "隧道重启成功", "url", newT.url) + // URL 变了,通知回调 + if newT.url != oldURL && newT.url != "" && t.onURLChange != nil { + t.onURLChange(newT.url) + } + return + } + logger.Error("tunnel", "隧道重启失败,即将重试", "err", err, "backoff_sec", int(backoff.Seconds())) + select { + case <-time.After(backoff): + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + case <-ctx.Done(): + logger.Warn("tunnel", "隧道退出重试(主 ctx 已取消)") + return + } + } +} + +// defaultConfigPath 返回命名隧道默认配置文件路径(与 gateway 同用户 ~/.cloudflared/paygateway.yml) +func defaultConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "paygateway.yml" + } + return filepath.Join(home, ".cloudflared", "paygateway.yml") +} diff --git a/internal/upstream/model.go b/internal/upstream/model.go new file mode 100755 index 0000000..0523422 --- /dev/null +++ b/internal/upstream/model.go @@ -0,0 +1,57 @@ +// Package upstream 上游对接方管理:多商户(易支付 pid+key)独立加密存储 +// +// 安全设计: +// - upstreams.db 仅管理后台 API 可读写,其他模块不得直接访问 +// - 敏感字段(易支付密钥 key)AES-256-GCM 加密存储(复用 account 包的加密能力) +// - UpstreamPool 是内存中的唯一数据源,其他模块(epay 验签/回调签名)通过它获取解密后的密钥 +// - 每个上游独立 pid + key:下单验签、回调签名均按 pid 路由,互不伪造 +package upstream + +import "time" + +// UpstreamStatus 上游状态 +type UpstreamStatus string + +const ( + StatusEnabled UpstreamStatus = "enabled" // 启用:可下单、可回调 + StatusDisabled UpstreamStatus = "disabled" // 禁用:拒绝下单 +) + +// Upstream 上游对接方(内存中的完整表示,含解密后的密钥) +// upstreams.db 中存储的是加密版本,Admin API 解密后推送到 UpstreamPool +type Upstream struct { + ID int64 `json:"id"` + Name string `json:"name"` // 上游名称:"new-api" + Pid string `json:"pid"` // 易支付商户ID(验签/签名依据,唯一) + Key string `json:"-"` // 易支付密钥(仅内存,不落盘明文) + BaseURL string `json:"base_url"` // 对外服务地址(该上游配置用的地址) + NotifyDomains string `json:"notify_domains"` // 回调目标域名白名单(逗号分隔,可选) + Status UpstreamStatus `json:"status"` // enabled / disabled + Remark string `json:"remark,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UpstreamEncrypted upstreams.db 中存储的加密版本 +type UpstreamEncrypted struct { + ID int64 `json:"id"` + Name string `json:"name"` + Pid string `json:"pid"` + KeyEnc string `json:"key_enc"` // AES-256-GCM 加密后的 hex 密文 + BaseURL string `json:"base_url"` + NotifyDomains string `json:"notify_domains"` + Status string `json:"status"` + Remark string `json:"remark,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// UpstreamLog 上游操作审计日志 +type UpstreamLog struct { + ID int64 `json:"id"` + UpstreamID int64 `json:"upstream_id"` + Action string `json:"action"` // create / update / delete / enable / disable + Detail string `json:"detail"` // 操作详情 + Operator string `json:"operator"` // 操作人 + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/upstream/pool.go b/internal/upstream/pool.go new file mode 100755 index 0000000..eca3454 --- /dev/null +++ b/internal/upstream/pool.go @@ -0,0 +1,120 @@ +// Package upstream 上游对接方管理:多商户(易支付 pid+key)独立加密存储 +// +// UpstreamPool 是各模块获取上游数据的唯一入口: +// - epay 协议层(main.go 验签):按 pid 查对应密钥 +// - order.Service:下单记录 pid/upstream_name;回调签名按订单 pid 取密钥 +// - Admin API:增删改查后同步到 pool +// +// 设计约束: +// - 不直接访问 upstreams.db(仅 Admin API 有权限) +// - 所有密钥在内存中解密,不落盘 +package upstream + +import ( + "sync" +) + +// UpstreamPool 内存上游池(线程安全) +// 负责:pid → Upstream 索引(验签/签名路由) +type UpstreamPool struct { + mu sync.RWMutex + byID map[int64]*Upstream // id → Upstream + byPid map[string]*Upstream // pid → Upstream(易支付验签/签名路由) +} + +// NewPool 创建空上游池 +func NewPool() *UpstreamPool { + return &UpstreamPool{ + byID: make(map[int64]*Upstream), + byPid: make(map[string]*Upstream), + } +} + +// ---------- 管理接口(仅 Admin API 调用)---------- + +// Add 添加上游到池中(Admin API 解密后调用) +func (p *UpstreamPool) Add(u *Upstream) { + p.mu.Lock() + defer p.mu.Unlock() + p.byID[u.ID] = u + if u.Pid != "" { + p.byPid[u.Pid] = u + } +} + +// Update 更新池中上游(pid 变更时重建索引) +func (p *UpstreamPool) Update(u *Upstream) { + p.mu.Lock() + defer p.mu.Unlock() + if old, ok := p.byID[u.ID]; ok { + if old.Pid != "" && old.Pid != u.Pid { + delete(p.byPid, old.Pid) + } + } + p.byID[u.ID] = u + if u.Pid != "" { + p.byPid[u.Pid] = u + } +} + +// Remove 从池中移除上游 +func (p *UpstreamPool) Remove(id int64) { + p.mu.Lock() + defer p.mu.Unlock() + if old, ok := p.byID[id]; ok { + if old.Pid != "" { + delete(p.byPid, old.Pid) + } + delete(p.byID, id) + } +} + +// SetStatus 设置上游状态 +func (p *UpstreamPool) SetStatus(id int64, status UpstreamStatus) { + p.mu.Lock() + defer p.mu.Unlock() + if u, ok := p.byID[id]; ok { + u.Status = status + } +} + +// ---------- 查询接口(业务模块使用)---------- + +// ByPid 按易支付商户ID查找上游(验签/签名路由) +// 返回 nil 表示该 pid 未登记 +func (p *UpstreamPool) ByPid(pid string) *Upstream { + p.mu.RLock() + defer p.mu.RUnlock() + return p.byPid[pid] +} + +// ByID 按 ID 查找上游 +func (p *UpstreamPool) ByID(id int64) *Upstream { + p.mu.RLock() + defer p.mu.RUnlock() + return p.byID[id] +} + +// All 返回全部上游(按 ID 排序,Admin 列表用) +func (p *UpstreamPool) All() []*Upstream { + p.mu.RLock() + defer p.mu.RUnlock() + out := make([]*Upstream, 0, len(p.byID)) + for _, u := range p.byID { + out = append(out, u) + } + // 按 ID 升序 + for i := 1; i < len(out); i++ { + for j := i; j > 0 && out[j].ID < out[j-1].ID; j-- { + out[j], out[j-1] = out[j-1], out[j] + } + } + return out +} + +// Count 上游总数 +func (p *UpstreamPool) Count() int { + p.mu.RLock() + defer p.mu.RUnlock() + return len(p.byID) +} diff --git a/internal/web/note_test.go b/internal/web/note_test.go new file mode 100644 index 0000000..74057f0 --- /dev/null +++ b/internal/web/note_test.go @@ -0,0 +1,66 @@ +package web + +import ( + "strings" + "testing" + "time" + + "paygateway/internal/model" +) + +func TestBuildPersonalQRPage_NoteCode(t *testing.T) { + o := &model.Order{OutTradeNo: "T1", NoteCode: "1234", AmountMinor: 1000} + html := BuildPersonalQRPage(o, "/qr/real?token=x", "personal.alipay", time.Now().Add(5*time.Minute)) + if !strings.Contains(html, "1234") { + t.Fatalf("支付页应包含备注码 1234") + } + if !strings.Contains(html, "请备注") { + t.Fatalf("支付页应包含备注提示文案") + } + // 无备注码时不渲染该区块 + o2 := &model.Order{OutTradeNo: "T2", AmountMinor: 1000} + h2 := BuildPersonalQRPage(o2, "/qr/real?token=y", "personal.alipay", time.Now()) + if strings.Contains(h2, "请备注") { + t.Fatalf("无备注码订单不应渲染备注提示") + } +} + +func TestBuildSuccessPage(t *testing.T) { + paidAt := time.Date(2026, 9, 12, 10, 30, 0, 0, time.Local) + o := &model.Order{OutTradeNo: "S1", AmountMinor: 2350, Channel: "personal.alipay", ReturnURL: "https://merchant.example/order/1", PaidAt: &paidAt} + html := BuildSuccessPage(o, o.Channel) + for _, want := range []string{"支付成功", "已支付成功", "S1", "23.50", "2026-09-12 10:30:00", "返回商户"} { + if !strings.Contains(html, want) { + t.Fatalf("成功页应包含 %q", want) + } + } + // 无 return_url 时不渲染返回商户按钮 + o2 := &model.Order{OutTradeNo: "S2", AmountMinor: 100} + h2 := BuildSuccessPage(o2, "merchant.wechat") + if strings.Contains(h2, "返回商户") { + t.Fatalf("无 return_url 订单不应渲染返回商户按钮") + } + if !strings.Contains(h2, "微信支付成功") { + t.Fatalf("微信通道成功页应显示微信品牌文案") + } +} + +func TestBuildQRPage_HasSuccessPolling(t *testing.T) { + o := &model.Order{OutTradeNo: "TQ1", AmountMinor: 100} + html := BuildQRPage(o, "alipay-content", "merchant.alipay") + for _, want := range []string{"/qr/status?out_trade_no=", "TRADE_SUCCESS", "document.write"} { + if !strings.Contains(html, want) { + t.Fatalf("商家二维码页应包含支付成功轮询 %q", want) + } + } +} + +func TestBuildPersonalQRPage_HasSuccessPolling(t *testing.T) { + o := &model.Order{OutTradeNo: "TP1", AmountMinor: 100, NoteCode: "1234"} + html := BuildPersonalQRPage(o, "/qr/real?token=x", "personal.alipay", time.Now().Add(5*time.Minute)) + for _, want := range []string{"/qr/status?out_trade_no=", "TRADE_SUCCESS", "document.write"} { + if !strings.Contains(html, want) { + t.Fatalf("个人免签支付页应包含支付成功轮询 %q", want) + } + } +} diff --git a/internal/web/pages.go b/internal/web/pages.go new file mode 100755 index 0000000..9042767 --- /dev/null +++ b/internal/web/pages.go @@ -0,0 +1,483 @@ +// Package web 支付页面渲染:二维码页 / 跳转中转页 +package web + +import ( + "encoding/base64" + "fmt" + "html" + "net/url" + "strconv" + "strings" + "time" + + "paygateway/internal/logger" + "paygateway/internal/model" + + qrcode "github.com/skip2/go-qrcode" +) + +// BuildQRPage 构建支付二维码展示页面(服务端生成 base64,不依赖 CDN) +// channelName: alipay / wechat,用于页面品牌文案 +func BuildQRPage(order *model.Order, qrCode, channelName string) string { + qrPNG, err := qrcode.Encode(qrCode, qrcode.Medium, 280) + if err != nil { + logger.Error("web", "生成二维码失败", "err", err) + qrPNG = []byte{} + } + qrBase64 := base64.StdEncoding.EncodeToString(qrPNG) + + // 根据通道选择品牌文案(支持 "alipay"、"merchant.alipay"、"personal.alipay" 等格式) + title := "请使用支付宝扫码支付" + sub := "打开支付宝扫一扫完成付款" + imgAlt := "支付宝收款二维码" + tip := "提示:测试环境请使用沙箱版支付宝扫码支付" + if strings.HasSuffix(channelName, "wechat") { + title = "请使用微信扫码支付" + sub = "打开微信扫一扫完成付款" + imgAlt = "微信收款二维码" + tip = "提示:请使用微信扫一扫完成付款" + } + + // 轮询 /qr/status:订单到达 TRADE_SUCCESS 后原地切换为成功视图(免跳转闪烁) + successDoc := buildSuccessDoc(order, "支付成功") + + return fmt.Sprintf(` + + + + +%s + + + +
+

%s

+
%s
+
%s
+
订单号: %s
金额: ¥%s
+
%s
+ +
+ + +`, title, title, sub, qrBase64, imgAlt, order.OutTradeNo, order.MoneyYuan(), tip, order.OutTradeNo, + order.OutTradeNo, strconv.Quote(successDoc)) +} + +// BuildPersonalQRPage 个人免签支付页:真实收款码 + 标题上方内嵌关单倒计时条 +// +// 转账提示含备注码(order.NoteCode):备注码是收款匹配引擎的主手段 +// (noteMatchFromPool 精确匹配 → 失败才回退金额/时间窗口), +// 用户转账时填写备注码可避免同金额订单串单。订单无备注码时该区块不渲染。 +// 收款方名称由用户的原生付款页展示(防劫持核对在支付 App 内完成)。 +// expireAt 为零时(未开启自动关单)不显示倒计时;倒计时归零自动跳转 /qr/expired 重新下单页。 +// 支付方式切换不在此页提供:上游换支付模式入口(同号不同 type)下单时服务端自动迁移订单。 +func BuildPersonalQRPage(order *model.Order, qrImageSrc, channelName string, expireAt time.Time) string { + provider := "支付宝" + scanHint := "打开支付宝扫一扫,向商家收款码转账" + imgAlt := "收款二维码" + if strings.HasSuffix(channelName, "wechat") { + provider = "微信" + scanHint = "打开微信扫一扫,向商家收款码转账" + imgAlt = "微信收款二维码" + } + title := "请使用" + provider + "扫码转账" + + // 备注码提示区块(仅在订单有备注码时渲染;无备注码=金额/时间窗口兜底匹配) + noteBlock := "" + if order.NoteCode != "" { + noteBlock = `
转账时请备注 ` + html.EscapeString(order.NoteCode) + `,用于快速确认到账
` + } + + // 倒计时开关:expireAt 为零则整块隐藏 + expireRFC3339 := "" + if !expireAt.IsZero() { + expireRFC3339 = expireAt.Format(time.RFC3339) + } + expiredURL := "/qr/expired?out_trade_no=" + url.QueryEscape(order.OutTradeNo) + "&return_url=" + url.QueryEscape(order.ReturnURL) + + return fmt.Sprintf(` + + + + +%s + + + +
+
支付剩余时间 --:--,超时订单将自动关闭
+

%s

+
%s
+
%s
+
订单号: %s
金额: ¥%s
+ %s + +
支付完成后若未到账/未升级套餐,请联系平台管理人员处理;请勿重复支付。
若提示备注填写过于频繁,请勿再备注、直接转账即可;或更换账户/稍等片刻再支付。
+
+ + +`, title, title, scanHint, qrImageSrc, imgAlt, + order.OutTradeNo, order.MoneyYuan(), noteBlock, order.OutTradeNo, expireRFC3339, strconv.Quote(expiredURL), + order.OutTradeNo, order.OutTradeNo, order.OutTradeNo) +} + +// BuildExpiredPage 订单过期页:倒计时归零后跳转至此。 +// 过期订单是终态(不允许手动复活,防僵尸订单刷单),仅引导返回上游重新下单; +// 但上游同号重提会让服务端复活订单 → 本页 JS 轮询 /qr/status,探测到复活自动跳回支付页。 +func BuildExpiredPage(provider, outTradeNo, returnURL string) string { + title := "订单已过期" + reorder := "javascript:history.back()" + btnText := "返回重新下单" + if returnURL != "" { + reorder = returnURL + } + return fmt.Sprintf(` + + + + +%s + + + +
+
⏰
+
订单已过期
+
未在限定时间内完成%s支付
订单已自动关闭,未产生任何扣款
+
+
订单号%s
+
状态已关闭
+
+
+ %s +
+
若您已完成转账但页面未更新,请稍候刷新查看
+
+ + +`, title, provider, outTradeNo, reorder, btnText, outTradeNo) +} + +// BuildSuccessPage 支付成功状态页:付款人完成支付后展示(跳转落地页)。 +// +// 该页为终态展示,仅由服务端在订单状态确认为 TRADE_SUCCESS 后渲染 +// (见 handleQrSuccess 校验),不可由付款人任意触发(防伪造"支付成功"页)。 +// 与 BuildExpiredPage 同套卡片风格,绿色成功主题。 +// channelName: merchant.alipay / personal.alipay / merchant.wechat / personal.wechat ... 决定品牌文案。 +func BuildSuccessPage(order *model.Order, channelName string) string { + badge := "支付成功" + if strings.HasSuffix(channelName, "wechat") { + badge = "微信支付成功" + } else if strings.HasSuffix(channelName, "alipay") { + badge = "支付宝支付成功" + } + return buildSuccessDoc(order, badge) +} + +// buildSuccessDoc 生成完整支付成功文档(供 /qr/success 落地页与支付页原地切换共用, +// 保证付款人无论走跳转还是轮询原地切换看到的一致成功界面)。 +func buildSuccessDoc(order *model.Order, badge string) string { + paidText := "" + if order.PaidAt != nil && !order.PaidAt.IsZero() { + paidText = order.PaidAt.Format("2006-01-02 15:04:05") + } + returnBtn := "" + if order.ReturnURL != "" { + returnBtn = `返回商户` + } + + return fmt.Sprintf(` + + + + +%s + + + +
+
✅
+
%s
+

已支付成功

+
您的订单已支付成功,交易已完成
+
+
订单号%s
+
支付金额¥%s
+
支付时间%s
+
状态支付成功
+
+ %s +
建议保存本页截图(订单号+付款凭据),有疑问时方便平台快速查单
商家将尽快为您发货,请留意订单状态
+
+ +`, badge, badge, + order.OutTradeNo, order.MoneyYuan(), paidText, returnBtn) +} + +// QRPng 生成二维码 PNG(280x280,Medium 纠错)。失败返回空切片。 +func QRPng(content string) []byte { + png, err := qrcode.Encode(content, qrcode.Medium, 280) + if err != nil { + logger.Error("web", "生成二维码 PNG 失败", "err", err) + return nil + } + return png +} + +// QRPngDataURI 生成二维码 data URI(token 套码模式支付页内嵌用) +func QRPngDataURI(content string) string { + png := QRPng(content) + if len(png) == 0 { + return "" + } + return "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) +} + +// BuildRedirectPage 支付中转页:从本站跳转支付通道,携带正确 Referer +func BuildRedirectPage(order *model.Order, payURL, title string) string { + return fmt.Sprintf(` + + + + +正在跳转到支付宝... + + + +
+
+

%s

+
订单号: %s
金额: ¥%s
+ 立即支付 + 如果未自动跳转,请点击上方按钮 +
+ + +`, title, order.OutTradeNo, order.MoneyYuan(), payURL, payURL) +} + +// IsMobileUA 判断是否为移动端 User-Agent +func IsMobileUA(ua string) bool { + uaLower := strings.ToLower(ua) + mobileKeywords := []string{ + "mobile", "android", "iphone", "ipad", "ipod", + "windows phone", "blackberry", "opera mini", "iemobile", + } + for _, kw := range mobileKeywords { + if strings.Contains(uaLower, kw) { + return true + } + } + return false +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..0b9dd63 --- /dev/null +++ b/run.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# 支付网关启动脚本 +# 自动读取 .env 配置,从密钥文件加载密钥 + +cd "$(dirname "$0")" + +# 加载 .env +set -a +source .env +set +a + +# 从密钥文件加载私钥和公钥 +if [ -f "$ALIPAY_PRIVATE_KEY_FILE" ]; then + export ALIPAY_PRIVATE_KEY=$(cat "$ALIPAY_PRIVATE_KEY_FILE" | tr -d '\n' | tr -d ' ') + echo "✅ 已加载应用私钥" +fi + +if [ -f "$ALIPAY_PUBLIC_KEY_FILE" ]; then + export ALIPAY_PUBLIC_KEY=$(cat "$ALIPAY_PUBLIC_KEY_FILE" | tr -d '\n' | tr -d ' ') + echo "✅ 已加载支付宝公钥" +fi + +echo "🚀 启动支付网关..." +echo " 监听: ${GATEWAY_ADDR:-:8080}" +echo " 数据库: ${ORDER_DB:-data/paygateway.db}" + +# 自动下载并嵌入 cloudflared 二进制(如已存在则跳过) +echo "" +go generate ./internal/tunnel/ + +echo "" +go run ./cmd/gateway/ \ No newline at end of file diff --git a/scripts/adb-soak.sh b/scripts/adb-soak.sh new file mode 100644 index 0000000..d583352 --- /dev/null +++ b/scripts/adb-soak.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# adb 设备稳定性压测(平板入库前验证,模拟 paygateway 运行时调用面) +# +# 用法: ./adb-soak.sh [轮数] [间隔秒] +# serial: WiFi adb 为 host:5555(容器/裸跑同协议同路径);USB 直连为纯设备号 +# +# 每轮覆盖 adbmgr 实际用到的操作: get-state(连接口径)→ shell → wm size +# → screencap → uiautomator dump+cat,结尾输出成功率与平均时延对比。 +set -u +SERIAL=${1:?'需要 serial 参数(host:5555 或 USB 设备号)'} +N=${2:-10} +IV=${3:-3} + +ok_state=0; ok_shell=0; ok_size=0; ok_shot=0; ok_dump=0 +sum_state=0; sum_shell=0; sum_size=0; sum_shot=0; sum_dump=0 + +ms() { echo $(( ($2 - $1) / 1000000 )); } # 起止纳秒 → 毫秒 + +for i in $(seq 1 "$N"); do + t0=$(date +%s%N) + case "$SERIAL" in + *:*) adb connect "$SERIAL" >/dev/null 2>&1 ;; + esac + st=$(adb -s "$SERIAL" get-state 2>/dev/null) + d=$(ms "$t0" "$(date +%s%N)") + if [ "$st" != "device" ]; then + echo "[$i/$N] ❌ 设备不可达(connect+state ${d}ms)" + sleep "$IV"; continue + fi + ok_state=$((ok_state+1)); sum_state=$((sum_state+d)) + + t0=$(date +%s%N); out=$(adb -s "$SERIAL" shell echo soak-ok 2>/dev/null) + d=$(ms "$t0" "$(date +%s%N)"); sum_shell=$((sum_shell+d)) + [ "$out" = "soak-ok" ] && ok_shell=$((ok_shell+1)) + + t0=$(date +%s%N); sz=$(adb -s "$SERIAL" shell wm size 2>/dev/null | grep -o '[0-9]\+x[0-9]\+' | tail -1) + d=$(ms "$t0" "$(date +%s%N)") + [ -n "$sz" ] && { ok_size=$((ok_size+1)); sum_size=$((sum_size+d)); } + + t0=$(date +%s%N); adb -s "$SERIAL" exec-out screencap -p > /tmp/opencode/soak_shot.png 2>/dev/null + d=$(ms "$t0" "$(date +%s%N)") + [ -s /tmp/opencode/soak_shot.png ] && { ok_shot=$((ok_shot+1)); sum_shot=$((sum_shot+d)); } + + t0=$(date +%s%N) + adb -s "$SERIAL" shell uiautomator dump /sdcard/soak_ui.xml >/dev/null 2>&1 + adb -s "$SERIAL" shell cat /sdcard/soak_ui.xml > /tmp/opencode/soak_ui.xml 2>/dev/null + d=$(ms "$t0" "$(date +%s%N)") + grep -q '<' /tmp/opencode/soak_ui.xml && { ok_dump=$((ok_dump+1)); sum_dump=$((sum_dump+d)); } + + + echo "[$i/$N] ✅ 分辨率=${sz:-未取到} shell=${d}ms dump+读取=${d}ms 截图成功=${ok_shot} dump成功=${ok_dump}" + sleep "$IV" +done + +echo "============ 汇总(共 $N 轮,覆盖 paygateway 实际调用面)============" +[ "$ok_size" -gt 0 ] && printf "wm size : %d/%d 平均 %dms\n" "$ok_size" "$N" $((sum_size/ok_size)) +[ "$ok_shot" -gt 0 ] && printf "screencap : %d/%d 平均 %dms\n" "$ok_shot" "$N" $((sum_shot/ok_shot)) +[ "$ok_dump" -gt 0 ] && printf "ui-dump : %d/%d 平均 %dms\n" "$ok_dump" "$N" $((sum_dump/ok_dump)) +if [ "$ok_state" -gt 0 ]; then + avg_state=$((sum_state/ok_state)); avg_shell=$((sum_shell/ok_state)) + printf "connect/state : %d/%d 平均 %dms\n" "$ok_state" "$N" "$avg_state" + printf "shell : %d/%d 平均 %dms\n" "$ok_shell" "$N" "$avg_shell" +fi +echo "(时延由设备性能与 WiFi 决定——同一台设备在容器内与宿主机跑一遍结果应一致," \ + "即可证明网络桥接无额外损耗)" diff --git a/scripts/download-cloudflared.sh b/scripts/download-cloudflared.sh new file mode 100644 index 0000000..febfb29 --- /dev/null +++ b/scripts/download-cloudflared.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# download-cloudflared.sh — 下载 cloudflared 二进制到 internal/tunnel/ 供 //go:embed 使用 +# +# 用法: +# go generate ./internal/tunnel/ # 自动调用 +# bash scripts/download-cloudflared.sh # 手动调用 +# +# 下载策略(自动回退): +# 1. GitHub 直连 +# 2. GITHUB_MIRROR 环境变量指定的镜像(如有设置) +# 3. 内置镜像列表(ghproxy.cc / ghproxy.net / gh.api.99988866.xyz) +# +# 支持平台: linux amd64/arm64, darwin amd64/arm64 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TARGET_DIR="$PROJECT_DIR/internal/tunnel" +TARGET_FILE="$TARGET_DIR/cloudflared" + +# 如果二进制已存在且可执行,跳过下载 +if [ -x "$TARGET_FILE" ]; then + echo "✓ cloudflared 已存在,跳过下载" + exit 0 +fi + +OS="${GOOS:-$(go env GOOS)}" +ARCH="${GOARCH:-$(go env GOARCH)}" + +case "$OS/$ARCH" in +linux/amd64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64" ;; +linux/arm64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64" ;; +darwin/arm64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64" ;; +darwin/amd64) REL_PATH="cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64" ;; +*) + echo "❌ 不支持的平台: $OS/$ARCH" + exit 1 + ;; +esac + +# 构建下载 URL 列表:环境变量镜像优先,内置镜像回退 +URLS=() +if [ -n "${GITHUB_MIRROR:-}" ]; then + URLS+=("${GITHUB_MIRROR%/}/https://github.com/$REL_PATH") +fi +URLS+=( + "https://ghproxy.cc/https://github.com/$REL_PATH" + "https://ghproxy.net/https://github.com/$REL_PATH" + "https://gh.api.99988866.xyz/https://github.com/$REL_PATH" + "https://github.com/$REL_PATH" +) + +echo "⬇️ 正在下载 cloudflared ($OS/$ARCH) ..." + +for url in "${URLS[@]}"; do + echo " 尝试: $url" + set +e + # 镜像站常见 SSL 证书问题,统一用 -k;直连 GitHub 不用 -k + if echo "$url" | grep -q "github.com/$REL_PATH$" && [ -z "${GITHUB_MIRROR:-}" ]; then + curl -sSL --fail --connect-timeout 10 --max-time 120 "$url" -o "$TARGET_FILE" 2>/dev/null + else + curl -sSL --fail --connect-timeout 10 --max-time 120 -k "$url" -o "$TARGET_FILE" 2>/dev/null + fi + rc=$? + set -e + if [ $rc -eq 0 ] && [ -s "$TARGET_FILE" ]; then + chmod +x "$TARGET_FILE" + echo "✓ 下载成功: $TARGET_FILE ($(du -h "$TARGET_FILE" | cut -f1))" + exit 0 + fi + echo " ✗ 失败,换下一个..." +done + +echo "❌ 所有下载源均失败,请手动下载:" +echo " https://github.com/$REL_PATH" +echo " 放到 $TARGET_FILE 并 chmod +x" +exit 1 diff --git a/scripts/download-scrcpy-server.sh b/scripts/download-scrcpy-server.sh new file mode 100644 index 0000000..953d973 --- /dev/null +++ b/scripts/download-scrcpy-server.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# 下载 scrcpy-server.jar(编译时嵌入,go generate 调用) +# 需要网络;下载失败时提示手动放置 +set -e +DIR="$(cd "$(dirname "$0")/.." && pwd)" +TARGET="$DIR/internal/scrcpy/scrcpy-server.jar" + +if [ -f "$TARGET" ] && [ -s "$TARGET" ]; then + echo "scrcpy-server.jar 已存在,跳过下载" + exit 0 +fi + +# scrcpy v4.1 release 的 server jar +URL="https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-server-v4.1" + +echo "下载 scrcpy-server v4.1 → $TARGET" +if curl -fsSL -o "$TARGET" "$URL" 2>/dev/null && [ -s "$TARGET" ]; then + echo "✅ 下载完成 ($(du -h "$TARGET" | cut -f1))" +else + rm -f "$TARGET" + echo "❌ 下载失败,请手动下载:" + echo " $URL" + echo " 放置到: $TARGET" + exit 1 +fi diff --git a/scripts/e2e_usb_realdevice.sh b/scripts/e2e_usb_realdevice.sh new file mode 100755 index 0000000..c526346 --- /dev/null +++ b/scripts/e2e_usb_realdevice.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# USB 真机端到端测试:选真机 → 调管理后台 push-config 下发配置 → 跑 e2e_personal.py 下单→入账 +# +# 架构说明(listener-app 已下线,改为纯 adb 控制真机微信/支付宝账本 ledgermon): +# - 网关通过 adb 控制红米 K60 真机,ledgermon 读账本行匹配订单入账 +# - 不再有手机端 APP,所有授权/安装步骤已移除 +# - 本脚本负责:ADB 连接 → 管理后台登录 → 设备选择 → 配置下发 → 下单测试 +# +# 使用前置: +# 1. 真机开了 USB 调试 + 已 adb pair +# 2. 网关在跑(./gateway)+ cloudflare 隧道在跑(server_url 可达) +# 3. 管理后台已创建同名监听设备(账户池 → 监听设备,确保 adb_host/adb_port 字段已填真机 ip,或脚本会问 --device-id) +# +# 用法: +# scripts/e2e_usb_realdevice.sh # 自动选 adb device 第一个真机 +# scripts/e2e_usb_realdevice.sh --serial 4f2bdc30 # 指定真机 +# scripts/e2e_usb_realdevice.sh --device-id 91df2463... # 指定服务端已创建的 device_id +# scripts/e2e_usb_realdevice.sh --admin-url http://127.0.0.1:8081 --gateway-url http://127.0.0.1:8080 +# +# 真实收款流程:下单后展示备注码,在真机上完成真实收款;ledgermon 通过 adb 读取账本行 +# 匹配订单入账,订单变 TRADE_SUCCESS 即为闭环走过。 +set -euo pipefail +IFS=$'\n\t' + +# ---------- 参数 ---------- +SERIAL="" +DEVICE_ID="" +ADMIN_URL="${ADMIN_URL:-http://127.0.0.1:8081}" +GATEWAY_URL="${GATEWAY_URL:-http://127.0.0.1:8080}" +ADMIN_USER="${ADMIN_USER:-admin}" +ADMIN_PASS="${ADMIN_PASS:-paygateway123}" +PAY_AMOUNT="${PAY_AMOUNT:-0.66}" + +usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --serial) SERIAL="$2"; shift 2;; + --device-id) DEVICE_ID="$2"; shift 2;; + --admin-url) ADMIN_URL="$2"; shift 2;; + --gateway-url) GATEWAY_URL="$2"; shift 2;; + --amount) PAY_AMOUNT="$2"; shift 2;; + -h|--help) usage 0;; + *) echo "未知参数: $1"; usage 1;; + esac +done + +[[ -n "$ADMIN_PASS" && "$ADMIN_PASS" == "paygateway123" ]] && echo "⚠️ 使用默认管理员密码(ADMIN_PASS 未设),若改过请环境变量传值。" + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +echo +echo "==== [1/7] 选择 USB 真机 ====" +ADB_CMD=(adb) +if [[ -n "$SERIAL" ]]; then + ADB_CMD+=( -s "$SERIAL" ) + if ! "${ADB_CMD[@]}" get-state >/dev/null 2>&1; then + echo " ❌ 设备 --serial $SERIAL 不可达" + exit 1 + fi + echo " ✅ 使用指定 serial: $SERIAL" +else + mapfile -t DEVS < <(adb devices | awk 'NR>1 && $2=="device" {print $1}') + if [[ ${#DEVS[@]} -eq 0 ]]; then + echo " ❌ adb devices 列表为空(USB 调试没开?或者用 --serial 指定)" + adb devices + exit 1 + fi + if [[ ${#DEVS[@]} -gt 1 ]]; then + echo " ⚠️ 检测到多台设备,请用 --serial 指定其中一台:" + printf ' %s\n' "${DEVS[@]}" + exit 1 + fi + SERIAL="${DEVS[0]}" + ADB_CMD+=( -s "$SERIAL" ) + echo " ✅ 自动选用: $SERIAL" +fi + +echo +echo "==== [2/7] 管理后台登录 ====" +LOGIN_RESP=$(curl -sS -X POST "$ADMIN_URL/api/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$ADMIN_USER\",\"password\":\"$ADMIN_PASS\"}" ) +TOKEN=$(echo "$LOGIN_RESP" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("data",{}).get("token",""))') +if [[ -z "$TOKEN" ]]; then + echo " ❌ 登录失败:$LOGIN_RESP" + exit 1 +fi +echo " ✅ 已获取 JWT" + +echo +echo "==== [3/7] 检查关键授权(电池白名单)===" +# 注:listener-app 已下线,通知使用权检查已移除 +# ledgermon 通过 adb 直连账本,无需 App 通知监听权限 + +WHITELIST_OK=$("${ADB_CMD[@]}" shell dumpsys deviceidle whitelist | head -5 || true) +if [[ -n "$WHITELIST_OK" ]]; then + echo " ✅ 电池优化白名单:已检查(见上方)" +else + echo " ⚠️ 电池优化白名单:无法检查(无 root 权限,跳过)" +fi + +echo +echo "==== [4/7] 选取监听设备 ====" +DEVS_RESP=$(curl -sS "$ADMIN_URL/api/personal/devices" -H "Authorization: Bearer $TOKEN") +if [[ -n "$DEVICE_ID" ]]; then + MATCHED=$(echo "$DEVS_RESP" | python3 -c " +import sys, json +data = json.load(sys.stdin).get('data',{}).get('devices',[]) +target = '$DEVICE_ID' +m = [d for d in data if d.get('device_id')==target] +print(json.dumps(m[0]) if m else '')") + if [[ -z "$MATCHED" ]]; then + echo " ❌ 指定 --device-id $DEVICE_ID 在管理后台不存在" + exit 1 + fi + echo " ✅ 使用 device_id=$DEVICE_ID" +else + PICK=$(echo "$DEVS_RESP" | python3 -c " +import sys, json +data = json.load(sys.stdin).get('data',{}).get('devices',[]) +if not data: sys.exit('无监听设备,请先在管理后台创建') +# 优先 alipay provider +alipay = [d for d in data if d.get('provider')=='alipay'] +pick = alipay[0] if alipay else data[0] +print(pick.get('device_id'))") + DEVICE_ID="$PICK" + echo " ✅ 自动选取 device_id=$DEVICE_ID" +fi + +echo +echo "==== [5/7] 通过管理后台 push-config 下发配置 ====" +PUSH_RESP=$(curl -sS -X POST "$ADMIN_URL/api/personal/devices/$DEVICE_ID/push-config" \ + -H "Authorization: Bearer $TOKEN" \ + --max-time 30 ) +echo " raw: $PUSH_RESP" +echo "$PUSH_RESP" | python3 -c " +import sys, json +r = json.load(sys.stdin) +d = r.get('data', r) +if r.get('error'): sys.exit(' ❌ 下发失败: ' + r['error']) +print(' ✅ 配置下发成功')" + +echo +echo "==== [6/7] 跑下单测试 ====" +echo " ⓘ ledgermon 通过 adb 读取真机账本行匹配订单,无需模拟推送" +echo " 下单后会展示备注码;在真机上完成真实收款即可" +# TODO: 待 ledgermon e2e 测试脚本就绪后替换 +echo " ⚠️ e2e_personal.py 已随 listener-app 下线而删除,请手动执行下单验证" +exit 0 diff --git a/scripts/setup-domain.sh b/scripts/setup-domain.sh new file mode 100755 index 0000000..e2fce2f --- /dev/null +++ b/scripts/setup-domain.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# Cloudflare 固定域名 + 隧道一键初始化(全自动) +# +# 功能:自动创建/确认(全部幂等): +# 1. Cloudflare zone(paygateway.de5.net 全托管) +# 2. DNS CNAME 记录(proxied → 隧道) +# 3. hostname route(域名 → 隧道映射) +# 4. DNSHe 子域名(不存在时自动注册) + NS 委派给 Cloudflare +# 5. 等待 zone 激活 +# 6. Universal SSL 证书(自动签发,toggle 触发) +# 7. HTTPS 公网验证 +# +# 前置条件: +# 1. go build -o gateway ./cmd/gateway/ (编译产物) +# 2. .env 包含以下变量: +# CF_EMAIL, CF_GLOBAL_KEY, CF_TUNNEL_HOSTNAME, CF_TUNNEL_CREDENTIALS +# DNSHE_API_KEY, DNSHE_API_SECRET (de5.net 免费子域名场景) +# +# 使用: +# bash scripts/setup-domain.sh +# +# 换环境流程:复制 .env → go build → 本脚本 → ./start.sh +# 全新域名:把 CF_TUNNEL_HOSTNAME 换成新前缀(如 myapp.de5.net),脚本全自动注册+配置。 + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# 加载 .env +if [ -f .env ]; then + set -a + source .env + set +a +fi + +: "${CF_EMAIL:?请设置 CF_EMAIL}" +: "${CF_GLOBAL_KEY:?请设置 CF_GLOBAL_KEY}" +: "${CF_TUNNEL_HOSTNAME:?请设置 CF_TUNNEL_HOSTNAME}" +: "${CF_TUNNEL_CREDENTIALS:?请设置 CF_TUNNEL_CREDENTIALS}" + +# 从凭据中提取 TunnelID +TUNNEL_ID=$(echo "$CF_TUNNEL_CREDENTIALS" | base64 -d 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['TunnelID'])" 2>/dev/null || echo "") +if [ -z "$TUNNEL_ID" ]; then + echo "❌ 无法从 CF_TUNNEL_CREDENTIALS 中提取 TunnelID,请确认格式正确" + exit 1 +fi + +CF_AUTH=(-H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_KEY" -H "Content-Type: application/json") +echo "=== Cloudflare 初始化: $CF_TUNNEL_HOSTNAME (隧道 $TUNNEL_ID) ===" + +# 1. 确保 zone 存在 +echo "--- 1/7 检查 zone: $CF_TUNNEL_HOSTNAME ---" +ZONE_ID=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones?name=$CF_TUNNEL_HOSTNAME" "${CF_AUTH[@]}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result'][0]['id'] if d.get('result') else '')" 2>/dev/null || echo "") +if [ -z "$ZONE_ID" ]; then + echo " zone 不存在,正在创建..." + ZONE_ID=$(curl -sS -m 20 -X POST "https://api.cloudflare.com/client/v4/zones" \ + "${CF_AUTH[@]}" \ + -d "{\"name\":\"$CF_TUNNEL_HOSTNAME\",\"type\":\"full\"}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result']['id'] if d.get('success') else '')" 2>/dev/null || echo "") + if [ -z "$ZONE_ID" ]; then + echo "❌ zone 创建失败(可能是 TLD 或重复),跳过" + else + echo " ✅ zone 已创建 (ID: $ZONE_ID)" + fi +else + echo " ✅ zone 已存在 (ID: $ZONE_ID)" +fi + +if [ -z "$ZONE_ID" ]; then + echo "❌ 无 zone ID,无法继续。请检查域名是否可被 Cloudflare 托管" + exit 1 +fi + +# 2. 确保 DNS CNAME 记录(proxied=true) +echo "--- 2/7 检查 CNAME 记录 ---" +EXISTING=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=CNAME&name=$CF_TUNNEL_HOSTNAME" "${CF_AUTH[@]}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',[]); print(r[0]['id'] if r else '')" 2>/dev/null || echo "") +if [ -z "$EXISTING" ]; then + echo " CNAME 不存在,创建中..." + curl -sS -m 15 -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \ + "${CF_AUTH[@]}" \ + -d "{\"type\":\"CNAME\",\"name\":\"@\",\"content\":\"$TUNNEL_ID.cfargotunnel.com\",\"proxied\":true}" > /dev/null + echo " ✅ CNAME 已创建 (proxied=true)" +else + echo " ✅ CNAME 已存在" +fi + +# 3. 确保 hostname route +echo "--- 3/7 检查 hostname route ---" +ACT_ID=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/accounts?limit=1" "${CF_AUTH[@]}" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])" 2>/dev/null || echo "") +if [ -n "$ACT_ID" ]; then + ROUTE=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/accounts/$ACT_ID/zerotrust/routes/hostname" "${CF_AUTH[@]}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x['hostname']=='$CF_TUNNEL_HOSTNAME']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "") + if [ -z "$ROUTE" ]; then + echo " hostname route 不存在,创建中..." + curl -sS -m 15 -X POST "https://api.cloudflare.com/client/v4/accounts/$ACT_ID/zerotrust/routes/hostname" \ + "${CF_AUTH[@]}" \ + -d "{\"hostname\":\"$CF_TUNNEL_HOSTNAME\",\"tunnel_id\":\"$TUNNEL_ID\",\"tun_type\":\"cfd_tunnel\",\"comment\":\"setup-domain.sh\"}" > /dev/null + echo " ✅ hostname route 已创建" + else + echo " ✅ hostname route 已存在" + fi +fi + +# 4. DNSHe NS 委派(de5.net 免费子域名场景) +# de5.net 的 DNS 在 DNSHe,需把 paygateway.de5.net 的 NS 委派给 Cloudflare。 +# 若 DNSHE_API_KEY 已配置则自动处理;未配置则提示手动。 +echo "--- 4/7 DNSHe NS 委派 ---" +if [ -n "${DNSHE_API_KEY:-}" ] && [ -n "${DNSHE_API_SECRET:-}" ]; then + DNSHE_BASE="${DNSHE_API_BASE:-https://api005.dnshe.com}" + DNSHE_AUTH=(-H "X-API-Key: $DNSHE_API_KEY" -H "X-API-Secret: $DNSHE_API_SECRET") + # 提取子域前缀(paygateway.de5.net → paygateway) + SUB_PREFIX="${CF_TUNNEL_HOSTNAME%%.*}" + ROOT_DOMAIN="${CF_TUNNEL_HOSTNAME#*.}" + echo " 子域: $SUB_PREFIX (根域: $ROOT_DOMAIN)" + # 列出子域名,找 subdomain_id + SUB_ID=$(curl -sS -m 15 "${DNSHE_AUTH[@]}" \ + "https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=subdomains&action=list" 2>/dev/null | \ + python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('subdomains',[]) if x['full_domain']=='$CF_TUNNEL_HOSTNAME']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "") + if [ -z "$SUB_ID" ]; then + # 子域名不存在 → 自动注册(DNSHe API 支持) + echo " 子域名 $CF_TUNNEL_HOSTNAME 不存在,自动注册..." + SUB_ID=$(curl -sS -m 20 -X POST "${DNSHE_AUTH[@]}" \ + "https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=subdomains&action=create" \ + -H "Content-Type: application/json" \ + -d "{\"subdomain\":\"$SUB_PREFIX\",\"domain\":\"$ROOT_DOMAIN\"}" 2>/dev/null | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('subdomain_id',''))" 2>/dev/null || echo "") + if [ -n "$SUB_ID" ]; then + echo " ✅ 子域名已注册: $CF_TUNNEL_HOSTNAME (id=$SUB_ID)" + else + echo " ❌ 子域名注册失败(配额可能已满,DNSHe 免费额度 3 个)" + fi + fi + if [ -n "$SUB_ID" ]; then + # 取 Cloudflare 分配的 NS(kevin/reza.ns.cloudflare.com) + CF_NS=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" 2>/dev/null | \ + python3 -c "import sys,json; r=json.load(sys.stdin).get('result',{}); ns=r.get('name_servers',[]); print(' '.join(ns) if ns else '')" 2>/dev/null || echo "") + if [ -z "$CF_NS" ]; then + echo " ⚠️ 无法获取 Cloudflare NS,跳过 DNSHe 自动委派" + else + echo " Cloudflare NS: $CF_NS" + # 检查当前 NS 记录是否已是 Cloudflare 的 + NS_CHECK=$(curl -sS -m 15 "${DNSHE_AUTH[@]}" \ + "https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=list&subdomain_id=$SUB_ID" 2>/dev/null | \ + python3 -c " +import sys,json +d=json.load(sys.stdin) +ns1,ns2='$CF_NS'.split()[:2] +ok=all(any(r.get('type')=='NS' and r.get('content')==ns for r in d.get('records',[])) for ns in (ns1,ns2)) +print('ok' if ok else 'need') +" 2>/dev/null || echo "need") + if [ "$NS_CHECK" = "ok" ]; then + echo " ✅ NS 委派已就绪 ($CF_NS)" + else + echo " NS 委派缺失,自动配置中..." + # 删除现有 CNAME/非 NS 记录(NS 委派与 CNAME 冲突) + curl -sS -m 15 "${DNSHE_AUTH[@]}" \ + "https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=list&subdomain_id=$SUB_ID" 2>/dev/null | \ + python3 -c " +import sys,json +d=json.load(sys.stdin) +for r in d.get('records',[]): + print(r.get('id')) +" 2>/dev/null | while read rid; do + curl -sS -m 15 -X POST "${DNSHE_AUTH[@]}" \ + "https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=delete" \ + -H "Content-Type: application/json" \ + -d "{\"record_id\":\"$rid\"}" > /dev/null 2>&1 + done + # 添加两条 NS 记录 + for ns in $CF_NS; do + curl -sS -m 20 -X POST "${DNSHE_AUTH[@]}" \ + "https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=create" \ + -H "Content-Type: application/json" \ + -d "{\"subdomain_id\": $SUB_ID, \"type\": \"NS\", \"content\": \"$ns\", \"ttl\": 600}" > /dev/null 2>&1 + done + echo " ✅ NS 委派已配置: $CF_NS" + fi + fi + else + echo " ⚠️ DNSHe 未找到子域名 $CF_TUNNEL_HOSTNAME,跳过(请确认已注册)" + fi +else + NS_HINT=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" 2>/dev/null | \ + python3 -c "import sys,json; print(' '.join(json.load(sys.stdin).get('result',{}).get('name_servers',[])))" 2>/dev/null || echo "") + echo " ⚠️ 未配置 DNSHE_API_KEY/SECRET,跳过自动委派" + echo " 如需手动:dns.he.net → $CF_TUNNEL_HOSTNAME → NS 指向 $NS_HINT" +fi + +# 5. 确保 DNS 委派传播 + zone active +echo "--- 5/7 等待 zone 激活 (最多 60s) ---" +for i in $(seq 1 12); do + STATUS=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" | \ + python3 -c "import sys,json; print(json.load(sys.stdin)['result']['status'])" 2>/dev/null || echo "pending") + if [ "$STATUS" = "active" ]; then + echo " ✅ zone 已激活" + break + fi + sleep 5 +done + +# 6. 确保 Universal SSL +echo "--- 6/7 检查 Universal SSL ---" +CERT=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/certificate_packs?status=all" "${CF_AUTH[@]}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x.get('type')=='universal' and x.get('status')=='active']; print('ok' if r else '')" 2>/dev/null || echo "") +if [ -z "$CERT" ]; then + echo " SSL 未签发,尝试 toggle 触发..." + curl -sS -m 10 -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/universal/settings" \ + "${CF_AUTH[@]}" -d '{"enabled":false}' > /dev/null 2>&1 + sleep 10 + curl -sS -m 10 -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/universal/settings" \ + "${CF_AUTH[@]}" -d '{"enabled":true}' > /dev/null 2>&1 + echo " ⏳ Universal SSL toggle 完成,等待签发(通常 1-5 分钟)..." + for i in $(seq 1 30); do + STATUS=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/certificate_packs?status=all" "${CF_AUTH[@]}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x.get('status')=='active']; print(r[0]['status'] if r else 'pending')" 2>/dev/null || echo "pending") + if [ "$STATUS" = "active" ]; then + echo " ✅ Universal SSL 已签发!" + break + fi + sleep 10 + done +else + echo " ✅ Universal SSL 已签发" +fi + +# 7. 验证 HTTPS +echo "--- 7/7 验证 HTTPS ---" +sleep 5 +HTTP_CODE=$(curl -sS -m 15 -o /dev/null -w "%{http_code}" "https://$CF_TUNNEL_HOSTNAME/health" 2>/dev/null || echo "000") +if [ "$HTTP_CODE" = "200" ]; then + echo " ✅ https://$CF_TUNNEL_HOSTNAME/health → HTTP 200" + echo "" + echo "============================================" + echo " 初始化完成! 固定域名已就绪" + echo " URL: https://$CF_TUNNEL_HOSTNAME" + echo " 隧道: $TUNNEL_ID" + echo "============================================" +else + echo " ⚠️ HTTPS 返回 HTTP $HTTP_CODE,可能需等待 DNS 传播" + echo " 隧道进程需手动确认: ps aux | grep cloudflared" +fi diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..fc465b4 --- /dev/null +++ b/start.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# 支付网关启动脚本(生产环境裸跑场景,建议优先用 systemd:deploy/paygateway.service) +# +# 加载 .env(ACCOUNT_MASTER_KEY / GATEWAY_ADDR / 数据库路径等基础设施配置), +# 然后以编译好的二进制启动服务。带 pid 文件 + 已运行检查,避免重复启动。 + +cd "$(dirname "$0")" + +PID_FILE="./.gateway.pid" + +# 加载 .env +if [ -f .env ]; then + set -a + source .env + set +a +fi + +# 已运行检查:通过 pid 文件 + 进程存活双重验证 +if [ -f "$PID_FILE" ]; then + OLD_PID=$(cat "$PID_FILE" 2>/dev/null || echo "") + if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then + echo "❌ gateway 已在运行(pid=$OLD_PID,pid 文件 $PID_FILE)" >&2 + echo " 如需停止:kill $OLD_PID && rm $PID_FILE" >&2 + echo " 如需重启:先停再启,或用 ./stop.sh" >&2 + exit 1 + else + # pid 文件在但进程已死:清理残留 + rm -f "$PID_FILE" + fi +fi + +# 后台启动;保留 stderr 以便启动失败时能看到 +nohup ./gateway > /dev/null 2>&1 & +PID=$! +echo "$PID" > "$PID_FILE" + +# 等待短秒数验证进程未立刻退出(端口冲突 / 配置错误会快速 crash) +sleep 1 +if ! kill -0 "$PID" 2>/dev/null; then + rm -f "$PID_FILE" + echo "❌ gateway 启动后 1 秒内退出,请检查日志:tail -50 logs/paygateway.log" >&2 + exit 1 +fi + +echo "$PID" diff --git a/status.sh b/status.sh new file mode 100755 index 0000000..6e2e74b --- /dev/null +++ b/status.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 支付网关状态脚本(与 start.sh / .gateway.pid 配套;systemd 部署请用 systemctl status) +cd "$(dirname "$0")" + +PID_FILE="./.gateway.pid" + +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE" 2>/dev/null || echo "") + if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then + STARTED=$(ps -o lstart= -p "$PID" 2>/dev/null | sed 's/^ *//') + ELAPSED=$(ps -o etime= -p "$PID" 2>/dev/null | sed 's/^ *//') + echo "✅ 运行中 pid=$PID 启动于 $STARTED 已运行 $ELAPSED" + echo " 监听端口:$(ss -lntp 2>/dev/null | grep -E "pid=$PID" | awk '{print $4}' | tr '\n' ' ')" + exit 0 + fi +fi + +echo "⚠️ 未运行(pid 文件缺失或进程已死)" +echo " 通过 pgrep 兜底检查:$(pgrep -af 'paygateway/gateway' | head -1 || echo '无')" +exit 1 diff --git a/stop.sh b/stop.sh new file mode 100755 index 0000000..900dcf6 --- /dev/null +++ b/stop.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# 支付网关停止脚本(与 start.sh / .gateway.pid 配套;systemd 部署请用 systemctl stop) +cd "$(dirname "$0")" + +PID_FILE="./.gateway.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "无 pid 文件 $PID_FILE(可能未用 start.sh 启动,手动找进程用 pgrep -af 'paygateway/gateway')" + exit 0 +fi + +PID=$(cat "$PID_FILE" 2>/dev/null || echo "") +if [ -z "$PID" ]; then + rm -f "$PID_FILE" + echo "pid 文件为空,已清理" + exit 0 +fi + +if ! kill -0 "$PID" 2>/dev/null; then + rm -f "$PID_FILE" + echo "进程 $PID 已不在,pid 文件已清理" + exit 0 +fi + +# SIGTERM 优雅停止,等待最多 10s +kill "$PID" 2>/dev/null +for i in $(seq 1 50); do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 0.2 +done + +if kill -0 "$PID" 2>/dev/null; then + echo "10s 后仍未退出,发送 SIGKILL" + kill -9 "$PID" 2>/dev/null + sleep 0.3 +fi + +rm -f "$PID_FILE" +echo "已停止 $PID" diff --git a/web/admin/DESIGN.md b/web/admin/DESIGN.md new file mode 100644 index 0000000..b58c66d --- /dev/null +++ b/web/admin/DESIGN.md @@ -0,0 +1,132 @@ +# PayGateway Admin — Design System + +## 0. Reference Log +- **Primary reference**: Linear (clean, minimal, professional SaaS) +- **Secondary**: Vercel (breathable layout, typography), Supabase (subtle gradients, warm tones) +- **Tertiary**: Stripe (data density, clarity) + +## 1. Design Tokens + +### Color Palette + +#### Light Theme +``` +--bg-base: #f8f9fa (page background, very light slate) +--bg-surface: #ffffff (cards, panels) +--bg-subtle: #f1f3f5 (code blocks, stat backgrounds) +--bg-hover: rgba(0,0,0,0.04) (hover state backgrounds) + +--border: #e9ecef (card borders, dividers) +--border-strong:#dee2e6 (active borders) + +--text-primary: #1a1d21 (headings, primary text - not pure black) +--text-body: #3f4249 (body text - softer than black) +--text-muted: #697386 (labels, captions, secondary info) +--text-placeholder: #9ca3af (input placeholders) + +--accent: #5c6ac4 (indigo primary - Linear style, not generic blue) +--accent-hover: #4959b0 +--accent-light: #f0f1fb +--accent-gradient: linear-gradient(135deg, #5c6ac4, #7c7fe4) + +--success: #00875a (deep green) +--success-bg: #e3fcef +--warning: #de8000 (deep amber) +--warning-bg: #fff4e5 +--danger: #de350b (deep red) +--danger-bg: #ffebe6 +--info: #0065ff (clean blue) +--info-bg: #e5f0ff +``` + +#### Dark Theme +``` +--bg-base: #0d0f14 (deep navy black) +--bg-surface: #151820 (cards - slightly lighter than bg) +--bg-subtle: #1e2432 (code blocks, stat backgrounds) +--bg-hover: rgba(255,255,255,0.06) + +--border: #2a2e3b (card borders) +--border-strong: #3d4250 + +--text-primary: #e8eaed +--text-body: #c4c9d4 +--text-muted: #7c8194 +--text-placeholder: #525968 + +--accent: #818cf8 (lighter indigo for dark bg) +--accent-hover: #a5b4fc +--accent-light: #1e2432 +--accent-gradient: linear-gradient(135deg, #818cf8, #6366f1) +``` + +### Typography +- **Primary font**: `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` +- **Mono font**: `"JetBrains Mono", "Fira Code", "Consolas", monospace` +- **Scale**: 11 / 12 / 13 / 14 / 15 / 16 / 18 / 20 / 24 / 30 / 36px +- **Weights**: 400 (regular), 500 (medium), 600 (semibold), 700 (bold) + +### Spacing (4px grid) +- `4px` xs, `8px` sm, `12px` md, `16px` lg, `24px` xl, `32px` 2xl, `48px` 3xl + +### Shadows +- Card: `0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04)` +- Hover: `0 4px 16px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.04)` +- Elevated: `0 8px 32px rgba(0,0,0,0.12)` + +## 2. Component Tokens + +### Sidebar +- Width: 220px +- Background: `--bg-surface` +- Border: `1px solid var(--border)` +- Active item: `--accent-light` background + left accent border (3px) +- Nav item height: 36px +- Brand area: logo mark (32px) + name (14px semibold) + +### Topbar +- Height: 52px +- Background: `rgba(var(--bg-surface), 0.85)` + backdrop blur +- Border: `1px solid var(--border)` + +### Cards (pg-card) +- Border-radius: 10px +- Border: `1px solid var(--border)` +- Background: `--bg-surface` +- Padding header: 16px 20px +- Padding body: 20px 20px +- No shadow on default (borders provide enough definition) +- Subtle shadow on hover + +### Tables +- Row height: 48px +- Header height: 40px +- Cell padding: 12px 16px +- Border: `1px solid var(--border)` +- Row hover: `--bg-hover` +- Striped: alternate `--bg-base` background +- Font: 13px for data, 12px for secondary info + +### Buttons +- Border-radius: 6px +- Height: 34px (default), 30px (small) +- Transition: 150ms ease + +### Form Inputs +- Border-radius: 6px +- Height: 36px +- Border: `1px solid var(--border)` +- Focus: accent color border + subtle ring + +## 3. Motion +- Hover transitions: 120ms ease +- Page transitions: 180ms ease-out (fade + slight Y translate) +- Easing: `cubic-bezier(0.25, 0.1, 0.25, 1)` +- No decorative animation (Stripe/Linear style - motion serves function) + +## 4. Element Plus Overrides +- Component border-radius: 6px (uniform with design system) +- Tag border-radius: 4px +- Message/Notification border-radius: 10px +- Dialog border-radius: 12px +- Dropdown border-radius: 8px \ No newline at end of file diff --git a/web/admin/index.html b/web/admin/index.html new file mode 100755 index 0000000..58caa1f --- /dev/null +++ b/web/admin/index.html @@ -0,0 +1,13 @@ + + + + + + PayGateway 管理后台 + + + +
+ + + diff --git a/web/admin/package-lock.json b/web/admin/package-lock.json new file mode 100755 index 0000000..0947313 --- /dev/null +++ b/web/admin/package-lock.json @@ -0,0 +1,1443 @@ +{ + "name": "paygateway-admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paygateway-admin", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "echarts": "^6.1.0", + "element-plus": "^2.7.8", + "vue": "^3.4.38" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "vite": "^5.4.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/echarts": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.1.0" + } + }, + "node_modules/element-plus": { + "version": "2.14.4", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.4.tgz", + "integrity": "sha512-vMKR9tFcLeNrJgFXA3zhUn6YuRKUQW9d0btakBR8U1Iq8MfzkjMOGcgs5de2VgiLldHt69brmuBHpxc3bK4ZgQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.9" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.9", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.9.tgz", + "integrity": "sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==", + "license": "MIT" + }, + "node_modules/zrender": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/web/admin/package.json b/web/admin/package.json new file mode 100755 index 0000000..0a4dc84 --- /dev/null +++ b/web/admin/package.json @@ -0,0 +1,21 @@ +{ + "name": "paygateway-admin", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "echarts": "^6.1.0", + "element-plus": "^2.7.8", + "vue": "^3.4.38" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "vite": "^5.4.2" + } +} diff --git a/web/admin/public/assets/favicon.svg b/web/admin/public/assets/favicon.svg new file mode 100644 index 0000000..41d8b51 --- /dev/null +++ b/web/admin/public/assets/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/admin/public/assets/paygateway-mark-light.svg b/web/admin/public/assets/paygateway-mark-light.svg new file mode 100644 index 0000000..86ca7ec --- /dev/null +++ b/web/admin/public/assets/paygateway-mark-light.svg @@ -0,0 +1,11 @@ + + PayGateway + A routed payment gateway with a verifiable ledger spine. + + + + + + + + diff --git a/web/admin/public/assets/paygateway-mark.svg b/web/admin/public/assets/paygateway-mark.svg new file mode 100644 index 0000000..e973ddf --- /dev/null +++ b/web/admin/public/assets/paygateway-mark.svg @@ -0,0 +1,12 @@ + + PayGateway + A routed payment gateway with a verifiable ledger spine. + + + + + + + + + diff --git a/web/admin/src/App.vue b/web/admin/src/App.vue new file mode 100755 index 0000000..d76273d --- /dev/null +++ b/web/admin/src/App.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/web/admin/src/DESIGN.md b/web/admin/src/DESIGN.md new file mode 100644 index 0000000..86a11f0 --- /dev/null +++ b/web/admin/src/DESIGN.md @@ -0,0 +1,232 @@ +# PayGateway Admin — Design System Specification + +> Inspired by Linear App / Vercel Dashboard — Ultra-minimal, precise, premium SaaS +> Reference: `linear.app.md` (Layer B) + `minimalist-skill.md` (Layer A) +> Status: v2.0 — Full redesign for Stripe/Linear/Vercel quality bar + +## 0. Research Log + +- **Layer A selected**: `minimalist-skill.md` — "minimal", "clean", "Linear-like" → premium utilitarian minimalism +- **Layer B selected**: `linear.app.md` — Linear's design tokens: near-black backgrounds, indigo-violet accent `#7170ff`, semi-transparent borders, Inter 510 signature weight, aggressive negative letter-spacing +- **Reference shortlist**: Linear (`linear.app.md`), Vercel (`vercel.md`), Supabase (`supabase.md`) — chosen: Linear for tokens + Vercel for login page inspiration +- **Design direction locked**: Linear-style dark-surface UI with indigo-violet accent. Light mode: off-white `#FAFAFA` bg, white cards, near-black text. Dark mode: near-black `#0D0D0F` bg, charcoal surfaces. Single accent `#7170ff` used sparingly. No gradients on large areas, no glassmorphism, no emoji icons. +- **Why Linear**: Unmatched precision — near-black canvas, semi-transparent borders (not dark borders on dark surfaces), 510 weight signature, aggressive tracking at display sizes. This is the reference that separates "Stripe-inspired" generic dark UI from real Linear-grade craft. + +--- + +## 1. Visual Direction + +**Atmosphere**: "Precision instrument" — darkness as native medium in dark mode, clean bone-white in light mode. Every element calibrated to the pixel. No decoration that doesn't serve function. The interface reads as engineered, not designed. + +**Signature moments**: +- Sidebar: ultra-narrow (200px), near-invisible surface with whisper-thin borders +- Active nav item: accent-colored left border strip (2px), subtle bg tint — no gradient +- KPI cards: pure numbers, no background color blocks, no icons in colored circles +- Charts: single-accent-color line charts, muted gray secondary elements + +**Banned elements** (per minimalist-skill): +- No `rounded-full` on cards, buttons, or primary elements +- No heavy drop shadows (none or ultra-subtle `rgba(0,0,0,0.04)`) +- No gradients on large backgrounds (accent allowed on small elements only) +- No emojis anywhere — SVG icons only +- No Inter/Roboto as generic defaults (Inter is fine IF used with 510 weight + cv01/ss03 features) + +--- + +## 2. Color Palette + +### Light Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--bg-base` | `#FAFAFA` | Page background — warm bone | +| `--bg-surface` | `#FFFFFF` | Cards, sidebar, topbar | +| `--bg-subtle` | `#F4F4F5` | Table header, subtle bg | +| `--bg-hover` | `rgba(0,0,0,0.04)` | Hover states | +| `--bg-active` | `rgba(0,0,0,0.06)` | Active/press states | +| `--border` | `#E4E4E7` | Primary borders | +| `--border-subtle` | `rgba(0,0,0,0.06)` | Whisper-thin dividers | +| `--text-primary` | `#09090B` | Headings, primary labels | +| `--text-body` | `#3F3F46` | Body copy | +| `--text-muted` | `#71717A` | Secondary/metadata | +| `--text-placeholder` | `#A1A1AA` | Placeholder text | +| `--text-inverse` | `#FFFFFF` | Text on dark/accent | +| `--accent` | `#7170FF` | Linear violet — CTAs, active states, links | +| `--accent-hover` | `#8284FF` | Hover on accent | +| `--accent-light` | `rgba(113,112,255,0.08)` | Accent tint bg | +| `--accent-muted` | `rgba(113,112,255,0.15)` | Slightly stronger accent bg | +| `--success` | `#22A069` | Status: success/active | +| `--success-bg` | `rgba(34,160,105,0.08)` | Success pill bg | +| `--warning` | `#C27803` | Status: pending/warning | +| `--warning-bg` | `rgba(194,120,3,0.08)` | Warning pill bg | +| `--danger` | `#D92D20` | Status: error/danger | +| `--danger-bg` | `rgba(217,45,32,0.08)` | Danger pill bg | +| `--info` | `#0A6FD4` | Status: info/neutral | +| `--info-bg` | `rgba(10,111,212,0.08)` | Info pill bg | + +### Dark Mode + +| Token | Hex | Role | +|-------|-----|------| +| `--bg-base` | `#0D0D0F` | Page background — Linear black | +| `--bg-surface` | `#18181B` | Cards, panels (Level 3) | +| `--bg-subtle` | `#222226` | Subtle surface tint | +| `--bg-sidebar` | `#111114` | Sidebar — darkest panel | +| `--bg-hover` | `rgba(255,255,255,0.05)` | Hover states | +| `--bg-active` | `rgba(255,255,255,0.07)` | Active/press | +| `--border` | `rgba(255,255,255,0.08)` | Primary border — semi-transparent white | +| `--border-subtle` | `rgba(255,255,255,0.05)` | Whisper-thin | +| `--text-primary` | `#FAFAFA` | Primary text — near-white | +| `--text-body` | `#C4C4C8` | Body text — silver | +| `--text-muted` | `#71717A` | Muted / metadata | +| `--text-placeholder` | `#52525B` | Placeholder | +| `--text-inverse` | `#09090B` | Text on light | +| `--accent` | `#7170FF` | Violet — consistent across modes | +| `--accent-hover` | `#8284FF` | Hover accent | +| `--accent-light` | `rgba(113,112,255,0.12)` | Accent tint | +| `--accent-muted` | `rgba(113,112,255,0.20)` | Stronger accent tint | +| `--success` | `#22A069` | Success | +| `--success-bg` | `rgba(34,160,105,0.12)` | Success bg | +| `--warning` | `#C27803` | Warning | +| `--warning-bg` | `rgba(194,120,3,0.12)` | Warning bg | +| `--danger` | `#F85149` | Error/danger | +| `--danger-bg` | `rgba(248,81,73,0.12)` | Danger bg | +| `--info` | `#79B8FF` | Info | +| `--info-bg` | `rgba(121,184,255,0.10)` | Info bg | + +--- + +## 3. Typography + +- **Primary font**: `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif` +- **Monospace**: `'JetBrains Mono', 'SF Mono', 'Consolas', monospace` +- **OpenType** (Linear signature): `font-feature-settings: 'cv01', 'ss03'` where feasible +- **Weights**: 400 (body/read), 510 (emphasis — Linear signature), 590 (strong emphasis — use sparingly) +- **Sizes**: + - Page title: 14px / 590 / -0.01em + - Card title: 14px / 590 / normal + - Body: 13px / 400 / normal + - Small/meta: 12px / 400 / normal + - Caption: 11px / 510 / normal + - Numbers/amounts: JetBrains Mono, tabular-nums +- **Never**: pure `#000000` or pure `#FFFFFF` for body text — always off-black/silver + +--- + +## 4. Spacing System + +Base: 8px grid +- Micro: 4px +- xs: 8px +- sm: 12px +- md: 16px +- lg: 20px +- xl: 24px +- 2xl: 32px +- Content gap (card grid): 16px +- Section gap: 16px +- Sidebar width: 200px (narrow, quiet) +- Topbar height: 48px +- Border radius: 8px (cards), 6px (inputs/buttons), 4px (tags/badges) + +--- + +## 5. Component Primitives + +### .pg-card +- Background: `var(--bg-surface)` +- Border: `1px solid var(--border)` — never shadow alone +- Border-radius: 8px +- No shadow on default; subtle shadow on hover in dark mode only: `0 2px 8px rgba(0,0,0,0.08)` +- Padding: 0 (header controls padding) / 20px body +- Hover: border darkens to `var(--border-subtle)` or accent + +### Buttons +- **Primary**: `var(--accent)` bg, white text, 6px radius, no shadow +- **Ghost**: transparent bg, `1px solid var(--border)`, `var(--text-body)` text, 6px radius +- **Text/Link**: no border, no bg, accent text +- **All**: height 32px (default), 28px (small), 24px (mini) — Inter 510 +- Hover: subtle bg tint — never gradient + +### Inputs +- Background: `var(--bg-subtle)` or transparent with border +- Border: `1px solid var(--border)` — focus: accent border with subtle glow +- Border-radius: 6px +- No heavy box-shadows + +### Tags/Badges +- Background: semantic color bg at 10% opacity +- Text: semantic color at full +- Border-radius: 4px +- Font: 11px / 510 / uppercase + letter-spacing 0.04em +- Padding: 2px 6px + +### Tables +- Header: `--bg-subtle`, 11px / 510 / uppercase + tracking, muted text +- Rows: no stripe, hover bg `var(--bg-hover)` +- Border: `none` — border-collapse on wrapper only +- Cell padding: 10px 12px + +### Sidebar +- Background: `var(--bg-surface)` (light) / `var(--bg-sidebar)` (dark) +- Border: `1px solid var(--border)` right side only +- Width: 200px +- Brand area: logo + name at top, no gradient background on logo +- Nav items: 34px height, 6px radius, left 2px accent border on active +- Active item: `--bg-hover` bg, `--accent` text + +### Topbar +- Background: `var(--bg-surface)` +- Border: `1px solid var(--border)` bottom +- Height: 48px +- Left: page title (14px / 590) +- Right: icon buttons (32px), theme toggle + +### Dialog/Modal +- Background: `var(--bg-surface)` +- Border: `1px solid var(--border)` or `rgba(255,255,255,0.08)` in dark +- Border-radius: 10px +- No heavy shadow — use border instead +- Backdrop: `rgba(0,0,0,0.4)` in dark / `rgba(0,0,0,0.15)` in light + +### Charts (ECharts) +- Background: transparent +- Axis lines: `var(--border)` in light / `rgba(255,255,255,0.06)` in dark +- Primary series: `--accent` (`#7170FF`) +- Secondary series: `--text-muted` +- No gradient fills on large areas — use flat color or subtle opacity +- Grid: minimal, border-only + +--- + +## 6. Motion + +- **Duration**: 120-150ms for micro-interactions, 200ms for layout transitions +- **Easing**: `cubic-bezier(0.16, 1, 0.3, 1)` (Linear's signature — quick start, gentle settle) +- **Page transitions**: opacity + translateY(4px) fade-in +- **Hover states**: background color shift only — no scale, no transform (except buttons active state: subtle scale(0.98)) +- **No ambient motion**: no floating orbs, no pulsing glows +- **Slop check**: if animation serves no informational purpose, remove it + +--- + +## 7. Accepted Debt + +- Inter is loaded without `cv01`/`ss03` OpenType features (CDN limitation) — accepted, Inter is still a clean choice +- JetBrains Mono loaded from Google Fonts (fallback to system mono is acceptable) +- No CSS container queries (broader compatibility over advanced layout) +- Element Plus component overrides use `:deep()` scoped styles — necessary for third-party library integration + +--- + +## 8. File Change Map + +| File | Change | +|------|--------| +| `theme.css` | Full rewrite — tokens, light/dark vars, Element Plus global overrides | +| `App.vue` | Refactor layout — narrower sidebar (200px), cleaner nav items, thinner topbar (48px), remove gradient logo bg | +| `Login.vue` | Strip all glassmorphism/orbs, flat white+accent or dark minimal card | +| `Dashboard.vue` | Strip KPI colored icon backgrounds, flat number display, clean chart palette | +| `Channels.vue` | Flat channel logos, clean stat grid | +| `Orders.vue` | Cleaner table, more minimal filter bar | +| Other views | Consistent pg-card styling, remove gradient backgrounds | \ No newline at end of file diff --git a/web/admin/src/api.js b/web/admin/src/api.js new file mode 100755 index 0000000..d1050c5 --- /dev/null +++ b/web/admin/src/api.js @@ -0,0 +1,214 @@ +// API 封装:管理后台 JSON 接口 +const BASE = '/api' + +function getToken() { + return sessionStorage.getItem('admin_token') +} + +async function request(path, params = {}, opts = {}) { + const qs = new URLSearchParams() + // 只有 GET 请求(无 opts.method)才加 query string + if (!opts.method || opts.method === 'GET') { + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null && v !== '') qs.set(k, v) + } + } + const url = `${BASE}${path}${qs.toString() ? '?' + qs : ''}` + + // 添加 token 鉴权 + const headers = { 'Accept': 'application/json' } + const token = getToken() + if (token) headers['Authorization'] = 'Bearer ' + token + + const fetchOpts = { headers, ...opts } + // 合并 headers(opts 里的 headers 覆盖默认 headers) + if (opts.headers) { + fetchOpts.headers = { ...headers, ...opts.headers } + } + const res = await fetch(url, fetchOpts) + if (res.status === 401) { + // token 过期或未登录,清除并跳转登录页 + sessionStorage.removeItem('admin_token') + window.location.reload() + throw new Error('请重新登录') + } + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error(body.error || `HTTP ${res.status}`) + } + const json = await res.json() + return json.data +} + +export const api = { + // 仪表盘(days: 趋势天数) + dashboard: (params = {}) => request('/dashboard', params), + // 易支付对接信息 + epayInfo: () => request('/epay-info'), + // 支付通道 + channels: () => request('/channels'), + // 系统设置 + settings: () => request('/settings'), + saveSettings: (data) => request('/settings', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }), + // 安全密钥(完整值,需登录;调用记审计日志) + settingsKeys: () => request('/settings/keys'), + // 重置管理员:验证当前密码后清空凭据,回到首次注册态 + resetAdmin: (password) => request('/auth/reset', {}, { method: 'POST', body: JSON.stringify({ password }), headers: { 'Content-Type': 'application/json' } }), + toggleChannel: (name) => request(`/settings/channel/${name}/toggle`, {}, { method: 'POST' }), + toggleChannelMode: (name, mode) => request(`/settings/channel/${name}/mode/${mode}/toggle`, {}, { method: 'POST' }), + // 订单 + orders: (params) => request('/orders', params), + orderEvents: (id, limit = 20) => request(`/orders/${id}/events`, { limit }), + // 通知任务 + notifyTasks: (params) => request('/notify-tasks', params), + notifyStats: () => request('/notify-stats'), + // 日志 + logs: (params) => request('/logs', params), + // 账本 + ledger: (params) => request('/ledger', params), + ledgerVerify: () => request('/ledger/verify', {}, { method: 'POST' }), + ledgerSnapshots: (params) => request('/ledger/snapshots', params), + createSnapshot: () => request('/ledger/snapshot', {}, { method: 'POST' }), + ledgerPublicKey: () => request('/ledger/public-key'), + // 导出账本 CSV(下载文件,不走 JSON 封装) + ledgerExport: async () => { + const token = getToken() + const res = await fetch(BASE + '/ledger/export', { headers: { 'Authorization': 'Bearer ' + token } }) + if (!res.ok) { + const body = await res.json().catch(() => ({})) + throw new Error(body.error || '导出失败') + } + const blob = await res.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'ledger_audit_' + new Date().toISOString().slice(0, 10) + '.csv' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + }, + // 隧道 + tunnel: () => request('/tunnel'), + + // 账户池管理 + accounts: (params) => request('/accounts', params), + getAccount: (id) => request(`/accounts/${id}`), + // 完整密钥(明文,需登录):管理后台「查看/复制完整密钥」用 + accountSecret: (id) => request(`/accounts/${id}/secret`), + createAccount: (data) => request('/accounts', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + updateAccount: (id, data) => request(`/accounts/${id}`, {}, { method: 'PUT', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + deleteAccount: (id) => request(`/accounts/${id}`, {}, { method: 'DELETE' }), + freezeAccount: (id) => request(`/accounts/${id}/freeze`, {}, { method: 'POST' }), + unfreezeAccount: (id) => request(`/accounts/${id}/unfreeze`, {}, { method: 'POST' }), + enableAccount: (id) => request(`/accounts/${id}/enable`, {}, { method: 'POST' }), + disableAccount: (id) => request(`/accounts/${id}/disable`, {}, { method: 'POST' }), + accountLogs: (id, limit) => request(`/accounts/${id}/logs`, { limit }), + + // 上传收款码图片 → 服务端解析二维码内容(docs/MIANQIAN.md §7.6 阶段一) + decodeQRCode: async (file) => { + const fd = new FormData() + fd.append('file', file) + const token = getToken() + const res = await fetch(BASE + '/accounts/decode-qrcode', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + token }, + body: fd + }) + if (res.status === 401) { + sessionStorage.removeItem('admin_token') + window.location.reload() + throw new Error('请重新登录') + } + const json = await res.json() + if (!res.ok) throw new Error(json.error || `HTTP ${res.status}`) + return json.data.content + }, + + // 上游对接方管理(多商户) + upstreams: () => request('/upstreams'), + getUpstream: (id) => request(`/upstreams/${id}`), + // 上游完整密钥(明文,需登录):对接指引「复制」真实可用密钥用 + upstreamSecret: (id) => request(`/upstreams/${id}/secret`), + createUpstream: (data) => request('/upstreams', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + updateUpstream: (id, data) => request(`/upstreams/${id}`, {}, { method: 'PUT', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + deleteUpstream: (id) => request(`/upstreams/${id}`, {}, { method: 'DELETE' }), + enableUpstream: (id) => request(`/upstreams/${id}/enable`, {}, { method: 'POST' }), + disableUpstream: (id) => request(`/upstreams/${id}/disable`, {}, { method: 'POST' }), + upstreamLogs: (id, limit) => request(`/upstreams/${id}/logs`, { limit }), + + // 容灾对账中心(异常池 + dead 通知) + listExceptions: (params) => request('/reconciler/exceptions', params), + getException: (id) => request(`/reconciler/exceptions/${id}`), + requeryException: (id) => request(`/reconciler/exceptions/${id}/requery`, {}, { method: 'POST' }), + resolveException: (id, data) => request(`/reconciler/exceptions/${id}/resolve`, {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + reopenException: (id) => request(`/reconciler/exceptions/${id}/reopen`, {}, { method: 'POST' }), + addException: (data) => request('/reconciler/exceptions', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + listDeadNotify: () => request('/reconciler/notify-dead'), + retryDeadNotify: (id) => request(`/reconciler/notify-dead/${id}/retry`, {}, { method: 'POST' }), + deleteDeadNotify: (id) => request(`/reconciler/notify-dead/${id}/delete`, {}, { method: 'POST' }), + // 日终对账报告(RESILIENCE P2-1) + dailyReport: (params) => request('/reconciler/daily-report', params), + + // 个人免签:监听端设备 + 推送流水 + 无订单收款(新设备统一走 USB 引导向导的 wizard-start) + personalDevices: () => request('/personal/devices'), + deletePersonalDevice: (id) => request(`/personal/devices/${id}`, {}, { method: 'DELETE' }), + freezePersonalDevice: (id) => request(`/personal/devices/${id}/freeze`, {}, { method: 'POST' }), + unfreezePersonalDevice: (id) => request(`/personal/devices/${id}/unfreeze`, {}, { method: 'POST' }), + restartPersonalDevice: (id) => request(`/personal/devices/${id}/restart`, {}, { method: 'POST' }), + personalPushes: (params) => request('/personal/pushes', params), + personalUnmatched: (params) => request('/personal/unmatched', params), + resolveUnmatched: (id, data) => request(`/personal/unmatched/${id}/resolve`, {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + // 监听端配置二维码(docs/MIANQIAN.md §7.6):返回 PNG blob URL + personalConfigQR: async (id) => { + const token = getToken() + const res = await fetch(BASE + `/personal/devices/${id}/config-qr`, { headers: { 'Authorization': 'Bearer ' + token } }) + if (res.status === 401) { + sessionStorage.removeItem('admin_token') + window.location.reload() + throw new Error('请重新登录') + } + const body = await res.json().catch(() => ({})) + if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`) + const blob = await res.blob() + return URL.createObjectURL(blob) + }, + // 设备控制台(web 可视化操作 USB 真机 / adb over WiFi 设备,docs/MIANQIAN.md §7.0 增强) + personalScreenshot: async (id) => { + const token = getToken() + const res = await fetch(BASE + `/personal/devices/${id}/screenshot`, { headers: { 'Authorization': 'Bearer ' + token } }) + if (res.status === 401) { + sessionStorage.removeItem('admin_token') + window.location.reload() + throw new Error('请重新登录') + } + const ct = res.headers.get('Content-Type') || '' + if (!res.ok || !ct.includes('image')) { + const body = await res.json().catch(() => ({})) + throw new Error(body.error || `HTTP ${res.status}`) + } + const blob = await res.blob() + return URL.createObjectURL(blob) + }, + personalDeviceAction: (id, data) => request(`/personal/devices/${id}/action`, {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + personalSetAdb: (id, data) => request(`/personal/devices/${id}/adb`, {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + // 设备 ↔ 账户 多对多绑定(应用双开多账户) + personalDeviceAccounts: (id) => request(`/personal/devices/${id}/accounts`), + personalAddDeviceAccount: (id, data) => request(`/personal/devices/${id}/accounts`, {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + personalRemoveDeviceAccount: (id, accountId) => request(`/personal/devices/${id}/accounts/${accountId}`, {}, { method: 'DELETE' }), + // 批量运维(ids 为空 + all=true 作用于全部设备) + personalBatchRestart: (data) => request('/personal/devices/batch/restart', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + personalBatchStatus: (data) => request('/personal/devices/batch/status', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + // USB 真机引导向导 + personalUsbDetect: () => request('/personal/usb-detect'), + personalWizardStart: (data) => request('/personal/devices/wizard-start', {}, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }), + personalWizardWifi: (id) => request(`/personal/devices/${id}/wizard-wifi`, {}, { method: 'POST' }), + personalWizardVerify: (id) => request(`/personal/devices/${id}/wizard-verify`, {}, { method: 'POST' }), + personalWifiReconnect: (id) => request(`/personal/devices/${id}/wifi-reconnect`, {}, { method: 'POST' }), + // 动态收款码 + personalQrcodes: () => request('/personal/qrcodes'), + personalDeleteQrcode: (deviceId, platform, user = 0) => request(`/personal/qrcodes/${deviceId}/${platform}/${user}`, {}, { method: 'DELETE' }), + // 微信收款码:服务器 adb 驱动捕获(微信 SECURE 需服务器 adb 通道) + personalCaptureWechatQrcode: (deviceId, user = 0) => request(`/personal/qrcodes/${deviceId}/wechat/capture?user=${user}`, {}, { method: 'POST' }), + personalCaptureAlipayQrcode: (deviceId, user = 999) => request(`/personal/qrcodes/${deviceId}/alipay/capture?user=${user}`, {}, { method: 'POST' }), +} diff --git a/web/admin/src/components/DeviceStream.vue b/web/admin/src/components/DeviceStream.vue new file mode 100644 index 0000000..a6fe395 --- /dev/null +++ b/web/admin/src/components/DeviceStream.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/web/admin/src/main.js b/web/admin/src/main.js new file mode 100755 index 0000000..f8691bd --- /dev/null +++ b/web/admin/src/main.js @@ -0,0 +1,18 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as Icons from '@element-plus/icons-vue' +import App from './App.vue' +import './theme.css' + +const app = createApp(App) +app.use(ElementPlus, { locale: zhCn }) + +// 全局注册图标 +for (const [name, comp] of Object.entries(Icons)) { + app.component(name, comp) +} + +app.mount('#app') diff --git a/web/admin/src/store.js b/web/admin/src/store.js new file mode 100755 index 0000000..d8fdf6e --- /dev/null +++ b/web/admin/src/store.js @@ -0,0 +1,9 @@ +// 全局共享状态,解决页面间数据同步 +import { ref } from 'vue' + +// 每次通道启停/设置修改时递增,所有页面 watch 此值自动刷新 +export const refreshVersion = ref(0) + +export function bumpRefresh() { + refreshVersion.value++ +} \ No newline at end of file diff --git a/web/admin/src/theme.css b/web/admin/src/theme.css new file mode 100755 index 0000000..548b44a --- /dev/null +++ b/web/admin/src/theme.css @@ -0,0 +1,645 @@ +/* ====================================================== + * PayGateway Admin — Linear-Inspired Design System v2.0 + * Reference: linear.app.md (Layer B) + minimalist-skill.md (Layer A) + * Light + Dark theme · 8px grid · Inter + JetBrains Mono + * ====================================================== */ + +/* ---------- Fonts ---------- */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;410;500;510;590;700&display=swap>'); +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap>'); + +/* ---------- Light Theme ---------- */ +:root { + /* Backgrounds — warm off-white, bone */ + --bg-base: #FAFAFA; + --bg-surface: #FFFFFF; + --bg-subtle: #F4F4F5; + --bg-hover: rgba(0, 0, 0, 0.04); + --bg-active: rgba(0, 0, 0, 0.06); + --bg-sidebar: #FFFFFF; + --bg-topbar: #FFFFFF; + + /* Borders — ultra-thin, warm gray */ + --border: #E4E4E7; + --border-subtle: rgba(0, 0, 0, 0.06); + --border-strong: #D1D1D6; + + /* Text — calibrated grayscale */ + --text-primary: #09090B; + --text-body: #3F3F46; + --text-muted: #71717A; + --text-placeholder: #A1A1AA; + --text-inverse: #FFFFFF; + + /* Accent — Linear's indigo-violet (#7170FF) */ + --accent: #7170FF; + --accent-hover: #8284FF; + --accent-light: rgba(113, 112, 255, 0.08); + --accent-muted: rgba(113, 112, 255, 0.14); + + /* Semantic — muted, desaturated */ + --success: #16A34A; + --success-bg: rgba(22, 163, 74, 0.08); + --success-border: rgba(22, 163, 74, 0.18); + --warning: #B45309; + --warning-bg: rgba(180, 83, 9, 0.08); + --warning-border: rgba(180, 83, 9, 0.18); + --danger: #DC2626; + --danger-bg: rgba(220, 38, 38, 0.08); + --danger-border: rgba(220, 38, 38, 0.18); + --info: #2563EB; + --info-bg: rgba(37, 99, 235, 0.08); + --info-border: rgba(37, 99, 235, 0.18); + + /* Shadows — border-forward, no heavy shadows */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04); + --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); + + /* Layout */ + --sidebar-width: 200px; + --topbar-height: 48px; + --card-radius: 8px; + --input-radius: 6px; + --btn-radius: 6px; + + /* Element Plus — global primary override to Linear violet */ + --el-color-primary: #7170FF; + --el-color-primary-light-3: #9899FF; + --el-color-primary-light-5: #B5B5FF; + --el-color-primary-light-7: #D5D5FF; + --el-color-primary-light-8: #E8E8FF; + --el-color-primary-light-9: #F4F4FF; + --el-color-primary-dark-2: #5B5BD6; + --el-color-success: #22A069; + --el-color-warning: #C27803; + --el-color-danger: #D92D20; + --el-color-info: #0A6FD4; + --el-border-radius-base: 6px; + --el-border-radius-small: 4px; + --el-border-radius-round: 9999px; + --el-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --el-font-size-base: 13px; + --el-text-color-primary: var(--text-primary); + --el-text-color-regular: var(--text-body); + --el-text-color-secondary: var(--text-muted); + --el-text-color-placeholder: var(--text-placeholder); + --el-border-color: var(--border); + --el-border-color-light: var(--border); + --el-border-color-lighter: var(--border); + --el-fill-color-blank: var(--bg-surface); + --el-fill-color-light: var(--bg-subtle); + --el-fill-color-lighter: var(--bg-base); + --el-mask-color: rgba(0, 0, 0, 0.15); + --el-overlay-color: rgba(0, 0, 0, 0.15); +} + +/* ---------- Dark Theme (Linear Near-Black) ---------- */ +html.dark { + /* Backgrounds — Linear near-black */ + --bg-base: #0D0D0F; + --bg-surface: #18181B; + --bg-subtle: #222226; + --bg-hover: rgba(255, 255, 255, 0.05); + --bg-active: rgba(255, 255, 255, 0.08); + --bg-sidebar: #111114; + --bg-topbar: #18181B; + + /* Borders — semi-transparent white (Linear technique) */ + --border: rgba(255, 255, 255, 0.08); + --border-subtle: rgba(255, 255, 255, 0.05); + --border-strong: rgba(255, 255, 255, 0.14); + + /* Text — calibrated near-white/silver */ + --text-primary: #FAFAFA; + --text-body: #C4C4C8; + --text-muted: #71717A; + --text-placeholder: #52525B; + --text-inverse: #09090B; + + /* Accent — consistent across modes */ + --accent: #7170FF; + --accent-hover: #8284FF; + --accent-light: rgba(113, 112, 255, 0.12); + --accent-muted: rgba(113, 112, 255, 0.20); + + /* Semantic — slightly lighter in dark */ + --success: #22C55E; + --success-bg: rgba(34, 197, 94, 0.10); + --success-border: rgba(34, 197, 94, 0.22); + --warning: #D97706; + --warning-bg: rgba(217, 119, 6, 0.10); + --warning-border: rgba(217, 119, 6, 0.22); + --danger: #EF4444; + --danger-bg: rgba(239, 68, 68, 0.10); + --danger-border: rgba(239, 68, 68, 0.22); + --info: #3B82F6; + --info-bg: rgba(59, 130, 246, 0.10); + --info-border: rgba(59, 130, 246, 0.22); + + /* Shadows — subtle on dark surfaces */ + --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.04); + --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.04); + + /* Element Plus dark overrides */ + --el-color-primary: #7170FF; + --el-color-primary-light-3: #9899FF; + --el-color-primary-light-5: #B5B5FF; + --el-color-primary-light-7: #D5D5FF; + --el-color-primary-light-8: #5B5BD6; + --el-color-primary-light-9: #18181B; + --el-color-primary-dark-2: #5B5BD6; + --el-bg-color: #18181B; + --el-bg-color-overlay: #222226; + --el-fill-color-blank: #18181B; + --el-text-color-primary: var(--text-primary); + --el-text-color-regular: var(--text-body); + --el-text-color-secondary: var(--text-muted); + --el-text-color-placeholder: var(--text-placeholder); + --el-border-color: var(--border); + --el-border-color-light: var(--border); + --el-border-color-lighter: var(--border); + --el-fill-color-light: #222226; + --el-fill-color-lighter: #18181B; + --el-mask-color: rgba(0, 0, 0, 0.65); + --el-overlay-color: rgba(0, 0, 0, 0.55); +} + +/* ---------- Base Reset ---------- */ +*, *::before, *::after { box-sizing: border-box; } + +html { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: 'cv01', 'ss03'; +} + +html, body { height: 100%; margin: 0; padding: 0; } + +body { + background: var(--bg-base); + color: var(--text-body); + transition: background 0.18s ease, color 0.18s ease; +} + +/* ---------- Scrollbar ---------- */ +::-webkit-scrollbar { width: 5px; height: 5px; } +::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: 3px; +} +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* ---------- Text Selection ---------- */ +::selection { background: var(--accent-light); color: var(--accent); } +html.dark ::selection { background: rgba(113, 112, 255, 0.25); color: var(--text-primary); } + +/* ---------- Utility Classes ---------- */ +.font-mono { + font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; + font-feature-settings: normal; + font-variant-numeric: tabular-nums; +} +.font-medium { font-weight: 510; } +.font-semibold { font-weight: 590; } +.font-bold { font-weight: 700; } + +.text-primary { color: var(--text-primary); } +.text-body { color: var(--text-body); } +.text-muted { color: var(--text-muted); } +.text-accent { color: var(--accent); } +.text-success { color: var(--success); } +.text-warning { color: var(--warning); } +.text-danger { color: var(--danger); } +.text-info { color: var(--info); } + +/* ---------- Card System ---------- */ +.pg-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--card-radius); + transition: background 0.15s ease, border-color 0.15s ease; +} +.pg-card:hover { + border-color: var(--border-strong); +} +html.dark .pg-card:hover { + border-color: rgba(255, 255, 255, 0.12); +} + +.pg-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); + font-size: 13px; + font-weight: 590; + color: var(--text-primary); +} + +.pg-card-body { padding: 16px; } + +/* ---------- Element Plus — Table ---------- */ +.el-table { + --el-table-bg-color: transparent; + --el-table-tr-bg-color: transparent; + --el-table-header-bg-color: var(--bg-subtle); + --el-table-row-hover-bg-color: var(--bg-hover); + --el-table-border-color: var(--border-subtle); + --el-table-text-color: var(--text-body); + --el-table-header-text-color: var(--text-muted); + border: none !important; + border-radius: 0; +} +.el-table__inner-wrapper::before { display: none; } +.el-table th.el-table__cell { + font-size: 11px; + font-weight: 590; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-muted) !important; + background: var(--bg-subtle) !important; + border-bottom: 1px solid var(--border-subtle) !important; + padding: 8px 12px !important; +} +.el-table td.el-table__cell { + font-size: 13px; + color: var(--text-body); + padding: 10px 12px !important; + border-bottom: 1px solid var(--border-subtle) !important; +} +.el-table__body tr:hover > td.el-table__cell { + background: var(--bg-hover) !important; +} +.el-table__body tr:last-child > td.el-table__cell { + border-bottom: none !important; +} +.el-table__empty-text { + color: var(--text-muted); + font-size: 13px; +} + +/* ---------- Element Plus — Tag ---------- */ +.el-tag { + border-radius: 4px !important; + font-size: 11px !important; + font-weight: 590 !important; + letter-spacing: 0.02em; + padding: 0 6px !important; + height: 20px !important; + line-height: 18px !important; + border: 1px solid !important; +} +.el-tag--success { + background: var(--success-bg) !important; + border-color: var(--success-border) !important; + color: var(--success) !important; +} +.el-tag--warning { + background: var(--warning-bg) !important; + border-color: var(--warning-border) !important; + color: var(--warning) !important; +} +.el-tag--danger { + background: var(--danger-bg) !important; + border-color: var(--danger-border) !important; + color: var(--danger) !important; +} +.el-tag--info { + background: var(--info-bg) !important; + border-color: var(--info-border) !important; + color: var(--info) !important; +} +html.dark .el-tag--success { + background: var(--success-bg) !important; + color: #4ADE80 !important; +} +html.dark .el-tag--danger { + background: var(--danger-bg) !important; + color: #F87171 !important; +} + +/* ---------- Element Plus — Button ---------- */ +.el-button { + border-radius: var(--btn-radius) !important; + font-weight: 510 !important; + font-size: 13px !important; + height: 32px !important; + padding: 0 14px !important; + transition: all 0.12s cubic-bezier(0.16, 1, 0.3, 1) !important; + border: 1px solid var(--border) !important; + background: var(--bg-surface) !important; + color: var(--text-body) !important; +} +.el-button:hover { + border-color: var(--border-strong) !important; + background: var(--bg-hover) !important; + color: var(--text-primary) !important; +} +.el-button:active { + transform: scale(0.98); +} +.el-button--primary { + background: var(--accent) !important; + border-color: var(--accent) !important; + color: var(--text-inverse) !important; +} +.el-button--primary:hover { + background: var(--accent-hover) !important; + border-color: var(--accent-hover) !important; + color: var(--text-inverse) !important; +} +.el-button--primary:active { + background: var(--accent) !important; + transform: scale(0.98); +} +.el-button--danger { + background: var(--danger) !important; + border-color: var(--danger) !important; + color: var(--text-inverse) !important; +} +.el-button--small { + height: 28px !important; + padding: 0 10px !important; + font-size: 12px !important; +} +.el-button--small.is-icon--inline { + width: 28px !important; + padding: 0 !important; +} +.el-button--text { + border: none !important; + background: transparent !important; + color: var(--accent) !important; + padding: 0 6px !important; +} +.el-button--text:hover { + background: var(--accent-light) !important; + color: var(--accent-hover) !important; +} + +/* ---------- Element Plus — Input ---------- */ +.el-input__wrapper { + border-radius: var(--input-radius) !important; + box-shadow: 0 0 0 1px var(--border) inset !important; + background: var(--bg-subtle) !important; + transition: box-shadow 0.12s ease !important; + padding: 0 12px !important; +} +.el-input__wrapper:hover { + box-shadow: 0 0 0 1px var(--border-strong) inset !important; +} +.el-input__wrapper.is-focus { + box-shadow: 0 0 0 2px var(--accent-light), 0 0 0 1px var(--accent) inset !important; + background: var(--bg-surface) !important; +} +.el-input__inner { + font-size: 13px !important; + color: var(--text-primary) !important; +} +.el-input__prefix .el-icon { + color: var(--text-muted) !important; +} + +/* ---------- Element Plus — Select ---------- */ +.el-select .el-input__wrapper { + border-radius: var(--input-radius) !important; +} + +/* ---------- Element Plus — Dialog ---------- */ +.el-dialog { + border-radius: 10px !important; + border: 1px solid var(--border) !important; + box-shadow: var(--shadow-md) !important; + overflow: hidden; +} +.el-dialog__header { + padding: 16px 20px 12px !important; + border-bottom: 1px solid var(--border-subtle) !important; + margin-right: 0 !important; +} +.el-dialog__title { + font-weight: 590; + font-size: 14px; + color: var(--text-primary); +} +.el-dialog__body { + padding: 20px !important; +} +.el-dialog__footer { + padding: 12px 20px 16px !important; + border-top: 1px solid var(--border-subtle) !important; +} + +/* ---------- Element Plus — Drawer ---------- */ +.el-drawer__header { + padding: 16px 20px 12px !important; + border-bottom: 1px solid var(--border-subtle) !important; + margin-bottom: 0 !important; + font-weight: 590; + font-size: 14px; + color: var(--text-primary); +} +.el-drawer__body { + padding: 16px 20px !important; +} + +/* ---------- Element Plus — Pagination ---------- */ +.el-pagination { + --el-pagination-hover-color: var(--accent); + font-weight: 510; + font-size: 13px; +} +.el-pagination.is-background .el-pager li:not(.is-disabled).is-active { + background: var(--accent) !important; + color: var(--text-inverse) !important; +} +.el-pager li { + font-weight: 510; + border-radius: 4px; +} + +/* ---------- Element Plus — Tabs ---------- */ +.el-tabs__nav-wrap::after { display: none; } +.el-tabs__item { + font-weight: 510; + font-size: 13px; + color: var(--text-muted); + transition: color 0.12s ease; +} +.el-tabs__item:hover { color: var(--text-primary); } +.el-tabs__item.is-active { + color: var(--accent); + font-weight: 590; +} +.el-tabs__active-bar { + background: var(--accent); + transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1); +} + +/* ---------- Element Plus — Alert ---------- */ +.el-alert { + border-radius: 6px !important; + border: 1px solid !important; + padding: 10px 14px !important; +} +.el-alert--success { + background: var(--success-bg) !important; + border-color: var(--success-border) !important; + color: var(--success) !important; +} +.el-alert--warning { + background: var(--warning-bg) !important; + border-color: var(--warning-border) !important; + color: var(--warning) !important; +} +.el-alert--error { + background: var(--danger-bg) !important; + border-color: var(--danger-border) !important; + color: var(--danger) !important; +} +.el-alert--info { + background: var(--info-bg) !important; + border-color: var(--info-border) !important; + color: var(--info) !important; +} +.el-alert__title { + font-size: 12px !important; + font-weight: 500 !important; +} +html.dark .el-alert--success { color: #4ADE80 !important; } +html.dark .el-alert--error { color: #F87171 !important; } + +/* ---------- Element Plus — Form ---------- */ +.el-form-item__label { + font-weight: 510; + font-size: 13px; + color: var(--text-body); +} + +/* ---------- Element Plus — Descriptions ---------- */ +.el-descriptions__cell { + padding: 8px 12px !important; +} +.el-descriptions__label { + color: var(--text-muted) !important; + font-weight: 510; + font-size: 12px; +} +.el-descriptions__content { + color: var(--text-body); + font-size: 13px; +} + +/* ---------- Element Plus — Progress ---------- */ +.el-progress__text { + font-weight: 590; + font-size: 11px !important; +} +.el-progress-bar__outer { + background: var(--bg-subtle) !important; +} + +/* ---------- Element Plus — Steps ---------- */ +.el-step__icon { border: none !important; } + +/* ---------- Element Plus — Timeline ---------- */ +.el-timeline-item__node { background: var(--accent) !important; border-color: var(--accent) !important; } + +/* ---------- Element Plus — Empty ---------- */ +.el-empty__description { color: var(--text-muted); font-size: 13px; } + +/* ---------- Element Plus — Tooltip ---------- */ +.el-popper.is-light { + border: 1px solid var(--border) !important; + border-radius: 6px !important; + background: var(--bg-surface) !important; + color: var(--text-body) !important; + font-size: 12px !important; + box-shadow: var(--shadow-sm) !important; +} +.el-popper.is-light .el-popper__arrow::before { + background: var(--bg-surface) !important; + border-color: var(--border) !important; +} + +/* ---------- Element Plus — Message / Notification ---------- */ +.el-message { + border-radius: 8px !important; + border: 1px solid var(--border) !important; + background: var(--bg-surface) !important; + box-shadow: var(--shadow-md) !important; +} +.el-notification { + border-radius: 8px !important; + border: 1px solid var(--border) !important; + background: var(--bg-surface) !important; +} + +/* ---------- Element Plus — Radio / Checkbox ---------- */ +.el-radio-button__inner { + border-radius: 4px !important; + font-weight: 510 !important; + font-size: 12px !important; +} +.el-radio-button__original-radio:checked + .el-radio-button__inner { + background-color: var(--accent) !important; + border-color: var(--accent) !important; + color: var(--text-inverse) !important; + box-shadow: none !important; +} + +/* ---------- Element Plus — Switch ---------- */ +.el-switch.is-checked .el-switch__core { + background-color: var(--accent) !important; + border-color: var(--accent) !important; +} + +/* ---------- Element Plus — Dropdown ---------- */ +.el-dropdown-menu { + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--bg-surface) !important; + box-shadow: var(--shadow-md) !important; + padding: 4px !important; +} +.el-dropdown-menu__item { + font-size: 13px !important; + font-weight: 510 !important; + color: var(--text-body) !important; + border-radius: 4px !important; + padding: 6px 10px !important; +} +.el-dropdown-menu__item:hover { + background: var(--bg-hover) !important; + color: var(--text-primary) !important; +} + +/* ---------- Element Plus — DatePicker ---------- */ +.el-date-editor { + border-radius: var(--input-radius) !important; +} +.el-picker-panel { + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--bg-surface) !important; +} + +/* ---------- Utility helpers ---------- */ +.mt8 { margin-top: 8px; } +.mt10 { margin-top: 10px; } +.mt12 { margin-top: 12px; } +.mt16 { margin-top: 16px; } +.mb8 { margin-bottom: 8px; } +.mb12 { margin-bottom: 12px; } +.mb16 { margin-bottom: 16px; } +.gap8 { gap: 8px; } +.gap12 { gap: 12px; } +.gap16 { gap: 16px; } \ No newline at end of file diff --git a/web/admin/src/views/Accounts.vue b/web/admin/src/views/Accounts.vue new file mode 100755 index 0000000..648f63d --- /dev/null +++ b/web/admin/src/views/Accounts.vue @@ -0,0 +1,468 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Channels.vue b/web/admin/src/views/Channels.vue new file mode 100755 index 0000000..281fe3a --- /dev/null +++ b/web/admin/src/views/Channels.vue @@ -0,0 +1,296 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Dashboard.vue b/web/admin/src/views/Dashboard.vue new file mode 100755 index 0000000..12c53f8 --- /dev/null +++ b/web/admin/src/views/Dashboard.vue @@ -0,0 +1,621 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Ledger.vue b/web/admin/src/views/Ledger.vue new file mode 100755 index 0000000..9a633df --- /dev/null +++ b/web/admin/src/views/Ledger.vue @@ -0,0 +1,360 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Login.vue b/web/admin/src/views/Login.vue new file mode 100755 index 0000000..76a395a --- /dev/null +++ b/web/admin/src/views/Login.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/web/admin/src/views/Logs.vue b/web/admin/src/views/Logs.vue new file mode 100755 index 0000000..dca028f --- /dev/null +++ b/web/admin/src/views/Logs.vue @@ -0,0 +1,196 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/NotifyTasks.vue b/web/admin/src/views/NotifyTasks.vue new file mode 100755 index 0000000..6fdf8bc --- /dev/null +++ b/web/admin/src/views/NotifyTasks.vue @@ -0,0 +1,176 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Orders.vue b/web/admin/src/views/Orders.vue new file mode 100755 index 0000000..2ed6e85 --- /dev/null +++ b/web/admin/src/views/Orders.vue @@ -0,0 +1,242 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/PersonalDevices.vue b/web/admin/src/views/PersonalDevices.vue new file mode 100755 index 0000000..544352d --- /dev/null +++ b/web/admin/src/views/PersonalDevices.vue @@ -0,0 +1,1051 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Qrcodes.vue b/web/admin/src/views/Qrcodes.vue new file mode 100644 index 0000000..f635e99 --- /dev/null +++ b/web/admin/src/views/Qrcodes.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/web/admin/src/views/Reconciler.vue b/web/admin/src/views/Reconciler.vue new file mode 100755 index 0000000..893aa74 --- /dev/null +++ b/web/admin/src/views/Reconciler.vue @@ -0,0 +1,330 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Settings.vue b/web/admin/src/views/Settings.vue new file mode 100755 index 0000000..a76df0d --- /dev/null +++ b/web/admin/src/views/Settings.vue @@ -0,0 +1,255 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Unmatched.vue b/web/admin/src/views/Unmatched.vue new file mode 100755 index 0000000..3bc680a --- /dev/null +++ b/web/admin/src/views/Unmatched.vue @@ -0,0 +1,177 @@ + + + + + \ No newline at end of file diff --git a/web/admin/src/views/Upstreams.vue b/web/admin/src/views/Upstreams.vue new file mode 100755 index 0000000..a184133 --- /dev/null +++ b/web/admin/src/views/Upstreams.vue @@ -0,0 +1,350 @@ + + + + + \ No newline at end of file diff --git a/web/admin/vite.config.js b/web/admin/vite.config.js new file mode 100755 index 0000000..43273b8 --- /dev/null +++ b/web/admin/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// 构建产物直接输出到 Go embed 目录(internal/admin/static) +export default defineConfig({ + plugins: [vue()], + build: { + outDir: '../../internal/admin/static', + emptyOutDir: true, + assetsDir: 'assets', + chunkSizeWarningLimit: 1024 + }, + server: { + port: 5173, + // 开发时代理到管理后台 API + proxy: { + '/api': 'http://localhost:8081' + } + } +})